generate: a Norwegian run on a local model, and what it took to read it

Every prompt and reply is now kept under <out>/llm/. An empty reply is
a retry, not a fatal error; a spec that never parses stops the run
before the structure phase; --visitor-llm lets a different model play
the visitor. The file-block parser finds markers anywhere in a line,
accepts a short END and lets a new FILE close an open one, after a
reply glued two files together with `<<<END>></think>`. The reference
gains a language section: copy in the intake's language, identifiers
in ASCII.

runs/rehab-entreprenor-no-2026-09-21: nemotron-3.5-lightning:30b-a3b
did not produce a loadable site. Written up with the reason and the
way forward: structure from needs.yaml deterministically, words from
the model.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-21 20:21:46 +02:00
co-authored by Claude Fable 5.1
parent b9f2acb7b8
commit b53e7e6aca
17 changed files with 2123 additions and 12 deletions
+11
View File
@@ -158,6 +158,17 @@ title: <business name>
lang: en lang: en
``` ```
## Language
Everything a person reads is written in the intake's `language`: page
names, headings, descriptions, field labels, button labels, the
`who`/`wants` of each need, and the personas. Write it the way people
in that trade and country actually speak, not as a translation.
Everything a machine reads stays plain ASCII snake_case or kebab-case:
need ids, persona ids, bucket names, state names, field `name`s, file
names and paths. Letters such as æ, ø and å belong in the copy and
never in an identifier. Set `lang` in site.yaml to the intake's code.
## Copy rules ## Copy rules
- Headings say what the visitor gets, in the visitor's words. - Headings say what the visitor gets, in the visitor's words.
+48
View File
@@ -0,0 +1,48 @@
# En fiktiv norsk rehabiliteringsentreprenør, inspirert av hva
# Klingenberg Bygg driver med: tilstandsrapporter, prosjektering,
# byggesøknader og utførelse på eldre murgårder. Skrevet slik eieren
# ville sagt det, uten portal-ord.
business: Åsrud Bygg og Rehabilitering AS
contact: { name: Marte Åsrud, email: marte@asrudbygg.example }
language: "no"
what_we_do: >
Vi er fjorten ansatte og jobber mest med eldre murgårder og
trehus. Vi lager tilstandsrapporter, prosjekterer rehabilitering,
sender byggesøknad med ansvarsrett, og utfører arbeidet med egne
tømrere og murere. Elektriker, rørlegger, stillas og blikkenslager
leier vi inn som underentreprenører.
who_comes_to_us: >
Styreledere i borettslag og sameier, og private huseiere, som har
et problem med bygget og vil at noen skal se på det. Eiere som
allerede vet hva de vil bygge eller endre, og trenger tegninger,
søknad til kommunen og en pris. Håndverksfirmaer som vil stå på
lista vår over underentreprenører. Tømrere, murere og lærlinger som
lurer på om vi trenger folk.
what_we_keep_track_of: >
Henvendelser: de kommer inn, vi avtaler og drar på befaring, vi
sender tilbud, og så vinner vi jobben eller taper den. Noen ganger
er det rett og slett ikke noe for oss, og da sier vi det med en gang.
Underentreprenører: de melder seg, vi sjekker organisasjonsnummer,
sentral godkjenning, forsikring og at folka har HMS-kort. Ofte
mangler det papirer og vi må etterlyse dem. Så blir de godkjent
eller avslått.
Jobbsøknader: vi leser dem, kaller inn noen til intervju, og
ansetter eller takker nei.
Avvik og uønskede hendelser på byggeplass: løse rekkverk,
nestenulykker, feil materialer levert. Noen melder fra, HMS-ansvarlig
bestemmer tiltak, og lukker saken når det er utbedret.
who_is_involved: >
To prosjektledere tar seg av henvendelser, underentreprenører og
ansettelser. En HMS-ansvarlig tar alt som meldes fra byggeplass.
Mannskapet, både våre egne og underentreprenørenes folk, er de som
står på plassen og melder fra. De sitter med telefonen, har hansker
på, og fyller ikke ut lange skjemaer.
what_slows_us_down: >
Henvendelser kommer på telefon, e-post og SMS og blir borte. Vi vet
aldri sikkert hvilke underentreprenører som har papirene i orden.
Avvik blir nevnt i lunsjen og glemt.
@@ -0,0 +1,30 @@
# Norwegian contractor, generated by a local model: failed
Intake: `intakes/rehab-entreprenor-no.yaml`. Generator:
`nemotron-3.5-lightning:30b-a3b` on ergo's Ollama, six rounds per
phase, about 16 minutes. It never produced a site that loads.
- The spec parsed on the fourth try, after nesting personas inside
each need once and returning no files at all twice.
- Structure went 11 failures, 1, then a YAML syntax error it could not
get past in four rounds: an end marker came out as
`<<<END>></think>` with the next file glued to it, so two files were
written as one. The parser now tolerates that; the model did not
notice it when shown the file back.
- It copied the reference's English example into its desk page
(`bucket: leads`, "Quote sent") instead of writing its own, never
wrote `aggregates.yaml`, and one reply was 40 KB of empty code
fences (truncated here).
- The Norwegian is mixed: "Hva trenger du hjelp med?" is fine,
"kvalifisert mandskap" is Danish spelling, and it kept `å` in a state
name against the reference's rule.
A Sonnet-class model did the English contractor in one round per
phase. What this run says is that a 3B-active model cannot hold the
whole content reference and write a dozen interlocking files at once,
in any language. The way to make small local models useful is to give
them less to do: generate `aggregates.yaml`, desks and followup pages
deterministically from `needs.yaml` (`stages`, `done_when`,
`handled_by` already say everything those files contain), and ask the
model only for words - the page question, headings, descriptions and
field labels, one page at a time.
@@ -0,0 +1,13 @@
needs round 1: FAIL: parsing needs.yaml: needs[0]: unknown field `personas`, expected one of `id`, `who`, `wants`, `enters`, `enters_as`, `via`, `lands_in`, `handled_by`, `done_when`, `max_steps`, `max_fields`, `planned`, `success`, `stages` at line 13 column 5
needs round 2: the reply had no file blocks, asking again
needs round 3: the reply had no file blocks, asking again
needs round 4: needs.yaml parses, frozen
ignored "needs.yaml": outside what the generator may write
structure round 1: 11 FAIL line(s)
structure round 2: 1 FAIL line(s)
structure round 3: 0 FAIL line(s)
structure round 4: 0 FAIL line(s)
structure round 5: 0 FAIL line(s)
structure round 6: 0 FAIL line(s)
Error: structure never went green in 6 round(s); last verifier output is in run.log
GENERATOR_EXIT=1 in 944s
@@ -0,0 +1,82 @@
needs:
- id: leads
who: styreledere i borettslag og sameier, private huseiere
wants: at noen skal se på et problem med bygget
enters: /
lands_in: leads
handled_by: prosjektledere
done_when: [won, lost]
max_steps: 1
max_fields: 4
success: [won]
stages: [open, quoted, won, lost]
- id: subcontractors
who: underentreprenører (tømrere, murere, lærlinger)
wants: å bli godkjent som fast leverandør
enters: /
lands_in: subcontractors
handled_by: prosjektledere
done_when: [approved, rejected]
max_steps: 1
max_fields: 3
success: [approved]
stages: [open, approved, rejected]
- id: job_applications
who: personer som søker jobb
wants: å bli ansatt som tømrer, murer eller lærling
enters: /
lands_in: job_applications
handled_by: prosjektledere
done_when: [hired, rejected]
max_steps: 2
max_fields: 6
success: [hired]
stages: [open, interviewed, hired, rejected]
- id: incident_reports
who: HMS-ansvarlig, mandskap på byggeplass
wants: å rapportere avvik og uønskede hendelser
enters: /
lands_in: incident_reports
handled_by: HMS-ansvarlig
done_when: [closed]
max_steps: 1
max_fields: 3
success: [closed]
stages: [open, closed]
- id: internal_referral
who: prosjektledere
wants: anbefalinger av kvalifisert mandskap
enters: /
lands_in: internal_referral
handled_by: prosjektledere
done_when: [referred]
max_steps: 1
max_fields: 2
success: [referred]
stages: [open, referred]
personas:
- id: hus_eier_med_problem
need: leads
text: >
Har et problem med huset mitt, enten det leaker, sitter losse eller trenger renovering. Trenger noen som ser på det og gir et tilbud.
- id: borettslag_styre
need: leads
text: >
Ansvarlig for fellesareal og byggestandard i borettslaget. Vil at en fagmann skal kartlegge tilstand og anbefale tiltak.
- id: underentreprenor
need: subcontractors
text: >
Har fagbrev og HKH-kort, men mangler alltid tid for å sende papirene inn. Søker om å bli fast leverandør på prosjekter.
- id: jobb_soker
need: job_applications
text: >
Lurer på om det finnes jobb som tømrer eller murer. Har fagbrev og lyst til å jobbe praktisk.
- id: hms_ansvarlig
need: incident_reports
text: >
Har sett noe på byggeplassen som ikke er greit: feil materialer, nestenulykke eller avvik fra planen. Må rapporteres rett opp og ned.
- id: prosjektleder_anbefaling
need: internal_referral
text: >
Kjenner på noen som skulle passa godt på laget vårt, enten det gjelder fagfolk eller underentreprenører.
@@ -0,0 +1,150 @@
<<<FILE needs.yaml>>>
needs:
- id: henvendelse-til-bygger
who: styreleder i borettslag eller sameier, eller privat huseier
wants: at noen skal se på et problem med bygget
enters: /
lands_in: henvendelser
handled_by: prosjektledere
done_when: [vunnet, tapt]
max_steps: 1
max_fields: 4
success: [vunnet]
stages: [open, avtale, tilbud, vunnet, tapt]
personas:
- id: styreleder-huseier
need: henvendelse-til-bygger
text: >
Vi har et problem med huset vårt og trenger at dere kommer og ser på det. Kan dere hjelpe oss med å finne ut av hva som er galt og hva det koster?
- id: huseier-ved-vedtak
need: henvendelse-til-bygger
text: >
Jeg vet nøyaktig hva jeg vil bygge eller endre. Treng tegninger, kommunesøknad og et fast prisoverslag.
- id: huseier-ukjent
need: henvendelse-til-bygger
text: >
Jeg har et problem med huset men vet ikke helt hva som er galt eller hva jeg trenger.
- id: underentreprenor-anmeldelse
who: tømrer, murer eller lærling som lurer på om firmaet vårt trenger folk
wants: å bli kjent av oss og eventuelt få jobb
enters: /
lands_in: underentreprenører
handled_by: prosjektledere
done_when: [godkjent, avslått]
max_steps: 1
max_fields: 3
success: [godkjent]
stages: [open, sjekk, godkjent, avslått]
personas:
- id: tømrer-lurer
need: underentreprenor-anmeldelse
text: >
Lurer på om vi trenger flere hender på jobb, eller om dere har plass til meg.
- id: murer-sjekker-lista
need: underentreprenor-anmeldelse
text: >
Sjekker om dere tar inn flere underentreprenører i år. Har organisasjonsnummer og referanser klar.
- id: lærling-uklar
need: underentreprenor-anmeldelse
text: >
Er ikke sikker på om fagbransjen passer meg ennå, men vil gjerne bli kjent.
- id: jobbansøker
who: person som søker jobb som tømrer, murer eller lærling
wants: å få jobb som håndverker
enters: /
lands_in: jobbansøkere
handled_by: prosjektledere
done_when: [ansatt, takk-nej]
max_steps: 2
max_fields: 5
success: [ansatt]
stages: [open, intervju, ansatt, takk-nej]
personas:
- id: jobbsoeker-klart
need: jobbansøker
text: >
Har klarte eksamen og har jobbsøkt en stund. Klar for intervju og påbør.
- id: jobbsoeker-uklar
need: jobbansøker
text: >
Er nysgjerrig på håndverkerfaget men har ikke fått bevis på ferdigheter ennå.
- id: jobbsoeker-etter-praktikk
need: jobbansøker
text: >
Har jobbet som praktikant og vil gjerne få fast stilling.
- id: avvik-og-hendelse
who: HMS-ansvarlig eller ansatt på byggeplassen
wants: å rapportere avvik eller uønsket hendelse og få det utbedret
enters: /
lands_in: avvik
handled_by: hms-ansvarlig
done_when: [lukket]
max_steps: 1
max_fields: 3
success: [lukket]
stages: [meldt, utbedret, lukket]
personas:
- id: hms-ansvarlig
need: avvik-og-hendelse
text: >
Ansvarlig for helse, miljø og sikkerhet. Må sikre at alt rapporteres og settes rett.
- id:arbeider-raporterer
need: avvik-og-hendelse
text: >
Sitter på stedet, har sett noe skje eller fått i oppdrag av kollega. Har telefonen og hanskene på.
- id: koordinator-etteropp
need: avvik-og-hendelse
text: >
Mottar meldingen og sikrer at tiltak blir tatt. Lukker saken når det er utbedret.
- id: underentreprenor-dokumentasjon
who: underentreprenør som har mottatt godkjennelse
wants: å få på plass papir og forsikring for å kunne jobbe trygt
enters: /
lands_in: underentreprenør-dok
handled_by: prosjektledere
done_when: [godkjent]
max_steps: 2
max_fields: 4
success: [godkjent]
stages: [innsendt, godkjent]
personas:
- id: underentreprenør-papirer
need: underentreprenør-dokumentasjon
text: >
Har fått jobb, men må få organisasjonsnummer, HMS-kort og forsikring på plass før første arbeidsdag.
- id: underentreprenør-forsikring
need: underentreprenør-dokumentasjon
text: >
Trenger å sjekke at forsikringspapirene er gyldige og at alt dokumentasjon er i orden.
- id: underentreprenør-koordinator
need: underentreprenør-dokumentasjon
text: >
Mottar ansøkningen og sjekker opp på at papirene samsvarer kravene.
- id: jobbansøk-behandling
who: prosjektleder som tar intervju og avgjør ansettelse
wants: å finne rette person til teamet
enters: /desk
lands_in: jobbansøkere
handled_by: prosjektledere
done_when: [ansatt, takk-nej]
max_steps: 2
max_fields: 4
success: [ansatt]
stages: [open, intervju, ansatt, takk-nej]
enters_as: prosjektledere
personas:
- id: prosjektleder-intervjuer
need: jobbansøk-behandling
text: >
Tar intervjuer og avgjør hvem som passer teamet best.
- id: prosjektleder-avgjør
need: jobbansøk-behandling
text: >
Tar beslutningen om ansettelse videre til teamet.
<<<END>>>
@@ -0,0 +1,148 @@
<<<FILE needs.yaml>>>
needs:
- id: henvendelse-til-bygger
who: styreleder i borettslag eller sameier, eller privat huseier
wants: at noen skal se på et problem med bygget
enters: /
lands_in: henvendelser
handled_by: prosjektledere
done_when: [vunnet, tapt]
max_steps: 1
max_fields: 4
success: [vunnet]
stages: [open, avtale, tilbud, vunnet, tapt]
personas:
- id: styreleder-huseier
need: henvendelse-til-bygger
text: >
Vi har et problem med huset vårt og trenger at dere kommer og ser på det. Kan dere hjelpe oss med å finne ut av hva som er galt og hva det koster?
- id: huseier-ved-vedtak
need: henvendelse-til-bygger
text: >
Jeg vet nøyaktig hva jeg vil bygge eller endre. Treng tegninger, kommunesøknad og et fast prisoverslag.
- id: huseier-ukjent
need: henvendelse-til-bygger
text: >
Jeg har et problem med huset men vet ikke helt hva som er galt eller hva jeg trenger.
- id: underentreprenor-anmeldelse
who: tømrer, murer eller lærling som lurer på om firmaet vårt trenger folk
wants: å bli kjent av oss og eventuelt få jobb
enters: /
lands_in: underentreprenører
handled_by: prosjektledere
done_when: [godkjent, avslått]
max_steps: 1
max_fields: 3
success: [godkjent]
stages: [open, sjekk, godkjent, avslått]
personas:
- id: tømrer-lurer
need: underentreprenor-anmeldelse
text: >
Lurer på om vi trenger flere hender på jobb, eller om dere har plass til meg.
- id: murer-sjekker-lista
need: underentreprenor-anmeldelse
text: >
Sjekker om dere tar inn flere underentreprenører i år. Har organisasjonsnummer og referanser klar.
- id: lærling-uklar
need: underentreprenor-anmeldelse
text: >
Er ikke sikker på om fagbransjen passer meg ennå, men vil gjerne bli kjent.
- id: jobbansøker
who: person som søker jobb som tømrer, murer eller lærling
wants: å få jobb som håndverker
enters: /
lands_in: jobbansøkere
handled_by: prosjektledere
done_when: [ansatt, takk-nej]
max_steps: 2
max_fields: 5
success: [ansatt]
stages: [open, intervju, ansatt, takk-nej]
personas:
- id: jobbsoeker-klart
need: jobbansøker
text: >
Har klarte eksamen og har jobbsøkt en stund. Klar for intervju og påbør.
- id: jobbsoeker-uklar
need: jobbansøker
text: >
Er nysgjerrig på håndverkerfaget men har ikke fått bevis på ferdigheter ennå.
- id: jobbsoeker-etter-praktikk
need: jobbansøker
text: >
Har jobbet som praktikant og vil gjerne få fast stilling.
- id: avvik-og-hendelse
who: HMS-ansvarlig eller ansatt på byggeplassen
wants: å rapportere avvik eller uønsket hendelse og få det utbedret
enters: /
lands_in: avvik
handled_by: hms-ansvarlig
done_when: [lukket]
max_steps: 1
max_fields: 3
success: [lukket]
stages: [meldt, utbedret, lukket]
personas:
- id: hms-ansvarlig
need: avvik-og-hendelse
text: >
Ansvarlig for helse, miljø og sikkerhet. Må sikre at alt rapporteres og settes rett.
- id: arbeider-raporterer
need: avvik-og-hendelse
text: >
Sitter på stedet, har sett noe skje eller fått i oppdrag av kollega. Har telefonen og hanskene på.
- id: koordinator-etteropp
need: avvik-og-hendelse
text: >
Mottar meldingen og sikrer at tiltak blir tatt. Lukker saken når det er utbedret.
- id: underentreprenør-dokumentasjon
who: underentreprenør som har mottatt godkjennelse
wants: å få på plass papir og forsikring for å kunne jobbe trygt
enters: /
lands_in: underentreprenør-dok
handled_by: prosjektledere
done_when: [godkjent]
max_steps: 2
max_fields: 4
success: [godkjent]
stages: [innsendt, godkjent]
personas:
- id: underentreprenør-papirer
need: underentreprenør-dokumentasjon
text: >
Har fått jobb, men må få organisasjonsnummer, HMS-kort og forsikring på plass før første arbeidsdag.
- id: underentreprenør-forsikring
need: underentreprenør-dokumentasjon
text: >
Trenger å sjekke at forsikringspapirene er gyldige og at alt dokumentasjon er i orden.
- id: underentreprenør-koordinator
need: underentreprenør-dokumentasjon
text: >
Mottar ansøkningen og sjekker opp på at papirene samsvarer kravene.
- id: jobbansøk-behandling
who: prosjektleder som tar intervju og avgjør ansettelse
wants: å finne rette person til teamet
enters: /desk
lands_in: jobbansøkere
handled_by: prosjektledere
done_when: [ansatt, takk-nej]
max_steps: 2
max_fields: 4
success: [ansatt]
stages: [open, intervju, ansatt, takk-nej]
enters_as: prosjektledere
personas:
- id: prosjektleder-intervjuer
need: jobbansøk-behandling
text: >
Tar intervjuer og avgjør hvem som passer teamet best.
- id: prosjektleder-avgjør
need: jobbansøk-behandling
text: >
Tar beslutningen om ansettelse videre til teamet.
@@ -0,0 +1,331 @@
<<<FILE needs.yaml>>>
needs:
- id: henvendelse-til-byggr
who: styreleder i borettslag eller sameie, eller privat huseier
wants: at noen skal se på et problem med bygget
enters: /
lands_in: henvendelser
handled_by: prosjektledere
done_when:
- vunnet
- tapt
max_steps: 1
max_fields: 4
success: [vunnet]
stages: [open, avtale, tilbud, vunnet, tapt]
personas:
- id: styreleder-hus
need: henvendelse-til-byggr
text: >
Vi har et problem med huset vårt og trenger at dere kommer og ser på det.
- id: huseier-vedtatt
need: henvendelse-til-byggr
text: >
Jeg vet nøyaktig hva jeg vil bygge eller endre, og trenger tegninger, søknad til kommunen og en pris.
- id: håndverksfirma
need: henvendelse-til-byggr
text: >
Vi lurer på om Dere trenger folk på arbeidsplassen deres.
- id: underentreprenor-anmeldelse
who: underentreprenør (tømrer, murer, lærling)
wants: å melde seg på listen over underentreprenører
enters: /
lands_in: underentreprenører
handled_by: prosjektledere
done_when: [godkjent, avslått]
max_steps: 1
max_fields: 3
success: [godkjent]
stages: [open, sjekk, godkjent, avslått]
personas:
- id: tømrer-anmeldelse
need: underentreprenor-anmeldelse
text: >
Jeg er tømrer og vil jobbe med Dere.
- id: murer-anmeldelse
need: underentreprenor-anmeldelse
text: >
Jeg er murer og har papirene i orden.
- id: lærling-uklar
need: underentreprenor-anmeldelse
text: >
Jeg er lærling og har ikke fått sertifisert alt enda.
- id: jobbansøker
who: person som søker jobb som tømrer, murer eller lærling
wants: å bli ansatt som håndverker
enters: /
lands_in: jobbansøkninger
handled_by: prosjektledere
done_when: [ansatt, takk-nei]
max_steps: 2
max_fields: 5
success: [ansatt]
stages: [open, samtale, ansatt, takk-nei]
personas:
- id: jobb-søker-energisk
need: jobbansøker
text: >
Jeg er en energisk håndverker som lurer på om Dere har plasser ledige.
- id: jobb-søker-rolig
need: jobbansøker
text: >
Jeg jobber allerede men vil skifte arbeidsplass til en bedre atmosfære.
- id: avvik-og-ulykke
who: HMS-ansvarlig på byggeplassen, eller ansatt/underentreprenør som melder fra
wants: å rapportere avvik eller uønsket hendelse
enters: /
lands_in: avvik
handled_by: hms-ansvarlig
done_when: [lukket]
max_steps: 1
max_fields: 3
success: [lukket]
stages: [open, utbedret, lukket]
personas:
- id: hms-ansvarlig-rapport
need: avvik-og-ulykke
text: >
Det skjedde en hendelse på byggeplassen som krever oppmærksomhet.
- id: tømrer-ukjent
need: avvik-og-ulykke
text: >
Noe gikk galt i jobbingen i dag, men det er løst nå.
- id: maskinoperatør-rolig
need: avvik-og-ulykke
text: >
Vi klarte å løse problemet selv uten at noe brøt.
aggregates:
- bucket: henvendelser
initial: open
states:
open: { event: received }
avtale: { event: avtale }
tilbud: { event: tilbud }
vunnet: { event: vunnet }
tapt: { event: tapt }
transitions:
open: [avtale, tapt]
avtale: [tilbud, tapt]
tilbud: [vunnet, tapt]
- bucket: underentreprenører
initial: open
states:
open: { event: meldt }
sjekk: { event: sjekket }
godkjent: { event: godkjent }
avslått: { event: avslått }
transitions:
open: [sjekk, avslått]
sjekk: [godkjent, avslått]
- bucket: jobbansøkninger
initial: open
states:
open: { event: mottatt }
samtale: { event: samtale }
ansatt: { event: ansett }
takk-nei: { event: takk_nei }
transitions:
open: [samtale, takk-nei]
samtale: [ansatt, takk-nei]
- bucket: avvik
initial: open
states:
open: { event: meldt }
utbedret: { event: utbedret }
lukket: { event: lukket }
transitions:
open: [utbedret, lukket]
utbedret: [lukket]
desk:
- group: prosjektledere
bucket: henvendelser
transitions:
open: [avtale, tapt]
avtale: [tilbud, tapt]
tilbud: [vunnet, tapt]
- group: prosjektledere
bucket: underentreprenører
transitions:
open: [sjekk, avslått]
sjekk: [godkjent, avslått]
- group: prosjektledere
bucket: jobbansøkninger
transitions:
open: [samtale, takk-nei]
samtale: [ansatt, takk-nei]
- group: hms-ansvarlig
bucket: avvik
transitions:
open: [utbedret, lukket]
utbedret: [lukket]
<<<END>></think><<<FILE needs.yaml>>>
needs:
- id: henvendelse-til-byggr
who: styreleder i borettslag eller sameie, eller privat huseier
wants: at noen skal se på et problem med bygget
enters: /
lands_in: henvendelser
handled_by: prosjektledere
done_when:
- vunnet
- tapt
max_steps: 1
max_fields: 4
success: [vunnet]
stages: [open, avtale, tilbud, vunnet, tapt]
personas:
- id: styreleder-hus
need: henvendelse-til-byggr
text: >
Vi har et problem med huset vårt og trenger at dere kommer og ser på det.
- id: huseier-vedtatt
need: henvendelse-til-byggr
text: >
Jeg vet nøyaktig hva jeg vil bygge eller endre, og trenger tegninger, søknad til kommunen og en pris.
- id: håndverksfirma
need: henvendelse-til-byggr
text: >
Vi lurer på om Dere trenger folk på arbeidsplassen deres.
- id: underentreprenor-anmeldelse
who: underentreprenør (tømrer, murer, lærling)
wants: å melde seg på listen over underentreprenører
enters: /
lands_in: underentreprenører
handled_by: prosjektledere
done_when: [godkjent, avslått]
max_steps: 1
max_fields: 3
success: [godkjent]
stages: [open, sjekk, godkjent, avslått]
personas:
- id: tømrer-anmeldelse
need: underentreprenor-anmeldelse
text: >
Jeg er tømrer og vil jobbe med Dere.
- id: murer-anmeldelse
need: underentreprenor-anmeldelse
text: >
Jeg er murer og har papirene i orden.
- id: lærling-uklar
need: underentreprenor-anmeldelse
text: >
Jeg er lærling og har ikke fått sertifisert alt enda.
- id: jobbansøker
who: person som søker jobb som tømrer, murer eller lærling
wants: å bli ansatt som håndverker
enters: /
lands_in: jobbansøkninger
handled_by: prosjektledere
done_when: [ansatt, takk-nei]
max_steps: 2
max_fields: 5
success: [ansatt]
stages: [open, samtale, ansatt, takk-nei]
personas:
- id: jobb-søker-energisk
need: jobbansøker
text: >
Jeg er en energisk håndverker som lurer på om Dere har plasser ledige.
- id: jobb-søker-rolig
need: jobbansøker
text: >
Jeg jobber allerede men vil skifte arbeidsplass til en bedre atmosfære.
- id: avvik-og-ulykke
who: HMS-ansvarlig på byggeplassen, eller ansatt/underentreprenør som melder fra
wants: å rapportere avvik eller uønsket hendelse
enters: /
lands_in: avvik
handled_by: hms-ansvarlig
done_when: [lukket]
max_steps: 1
max_fields: 3
success: [lukket]
stages: [open, utbedret, lukket]
personas:
- id: hms-ansvarlig-rapport
need: avvik-og-ulykke
text: >
Det skjedde en hendelse på byggeplassen som krever oppmærksomhet.
- id: tømrer-ukjent
need: avvik-og-ulykke
text: >
Noe gikk galt i jobbingen i dag, men det er løst nå.
- id: maskinoperatør-rolig
need: avvik-og-ulykke
text: >
Vi klarte å løse problemet selv uten at noe brøt.
aggregates:
- bucket: henvendelser
initial: open
states:
open: { event: received }
avtale: { event: avtale }
tilbud: { event: tilbud }
vunnet: { event: vunnet }
tapt: { event: tapt }
transitions:
open: [avtale, tapt]
avtale: [tilbud, tapt]
tilbud: [vunnet, tapt]
- bucket: underentreprenører
initial: open
states:
open: { event: meldt }
sjekk: { event: sjekket }
godkjent: { event: godkjent }
avslått: { event: avslått }
transitions:
open: [sjekk, avslått]
sjekk: [godkjent, avslått]
- bucket: jobbansøkninger
initial: open
states:
open: { event: mottatt }
samtale: { event: samtale }
ansatt: { event: ansett }
takk-nei: { event: takk_nei }
transitions:
open: [samtale, takk-nei]
samtale: [ansatt, takk-nei]
- bucket: avvik
initial: open
states:
open: { event: meldt }
utbedret: { event: utbedret }
lukket: { event: lukket }
transitions:
open: [utbedret, lukket]
utbedret: [lukket]
desk:
- group: prosjektledere
bucket: henvendelser
transitions:
open: [avtale, tapt]
avtale: [tilbud, tapt]
tilbud: [vunnet, tapt]
- group: prosjektledere
bucket: underentreprenører
transitions:
open: [sjekk, avslått]
sjekk: [godkjent, avslått]
- group: prosjektledere
bucket: jobbansøkninger
transitions:
open: [samtale, takk-nei]
samtale: [ansatt, takk-nei]
- group: hms-ansvarlig
bucket: avvik
transitions:
open: [utbedret, lukket]
utbedret: [lukket]
<<<END>>
@@ -0,0 +1,84 @@
<<<FILE needs.yaml>>>
needs:
- id: leads
who: styreledere i borettslag og sameier, private huseiere
wants: at noen skal se på et problem med bygget
enters: /
lands_in: leads
handled_by: prosjektledere
done_when: [won, lost]
max_steps: 1
max_fields: 4
success: [won]
stages: [open, quoted, won, lost]
- id: subcontractors
who: underentreprenører (tømrere, murere, lærlinger)
wants: å bli godkjent som fast leverandør
enters: /
lands_in: subcontractors
handled_by: prosjektledere
done_when: [approved, rejected]
max_steps: 1
max_fields: 3
success: [approved]
stages: [open, approved, rejected]
- id: job_applications
who: personer som søker jobb
wants: å bli ansatt som tømrer, murer eller lærling
enters: /
lands_in: job_applications
handled_by: prosjektledere
done_when: [hired, rejected]
max_steps: 2
max_fields: 6
success: [hired]
stages: [open, interviewed, hired, rejected]
- id: incident_reports
who: HMS-ansvarlig, mandskap på byggeplass
wants: å rapportere avvik og uønskede hendelser
enters: /
lands_in: incident_reports
handled_by: HMS-ansvarlig
done_when: [closed]
max_steps: 1
max_fields: 3
success: [closed]
stages: [open, closed]
- id: internal_referral
who: prosjektledere
wants: anbefalinger av kvalifisert mandskap
enters: /
lands_in: internal_referral
handled_by: prosjektledere
done_when: [referred]
max_steps: 1
max_fields: 2
success: [referred]
stages: [open, referred]
personas:
- id: hus_eier_med_problem
need: leads
text: >
Har et problem med huset mitt, enten det leaker, sitter losse eller trenger renovering. Trenger noen som ser på det og gir et tilbud.
- id: borettslag_styre
need: leads
text: >
Ansvarlig for fellesareal og byggestandard i borettslaget. Vil at en fagmann skal kartlegge tilstand og anbefale tiltak.
- id: underentreprenor
need: subcontractors
text: >
Har fagbrev og HKH-kort, men mangler alltid tid for å sende papirene inn. Søker om å bli fast leverandør på prosjekter.
- id: jobb_soker
need: job_applications
text: >
Lurer på om det finnes jobb som tømrer eller murer. Har fagbrev og lyst til å jobbe praktisk.
- id: hms_ansvarlig
need: incident_reports
text: >
Har sett noe på byggeplassen som ikke er greit: feil materialer, nestenulykke eller avvik fra planen. Må rapporteres rett opp og ned.
- id: prosjektleder_anbefaling
need: internal_referral
text: >
Kjenner på noen som skulle passa godt på laget vårt, enten det gjelder fagfolk eller underentreprenører.
<<<END>>>
@@ -0,0 +1,462 @@
<<<FILE site.yaml>>>
title: Åsrud Bygg og Rehabilitering AS
lang: no
<<<END>>>
<<<FILE needs.yaml>>>
needs:
- id: leads
who: styreledere i borettslag og sameier, private huseiere
wants: at noen skal se på et problem med bygget
enters: /
lands_in: leads
handled_by: prosjektledere
done_when: [won, lost]
max_steps: 1
max_fields: 4
success: [won]
stages: [open, quoted, won, lost]
- id: subcontractors
who: underentreprenører (tømrere, murere, lærlinger)
wants: å bli godkjent som fast leverandør
enters: /
lands_in: subcontractors
handled_by: prosjektledere
done_when: [approved, rejected]
max_steps: 1
max_fields: 3
success: [approved]
stages: [open, approved, rejected]
- id: job_applications
who: personer som søker jobb
wants: å bli ansatt som tømrer, murer eller lærling
enters: /
lands_in: job_applications
handled_by: prosjektledere
done_when: [hired, rejected]
max_steps: 2
max_fields: 6
success: [hired]
stages: [open, interviewed, hired, rejected]
- id: incident_reports
who: HMS-ansvarlig, mandskap på byggeplass
wants: å rapportere avvik og uønskede hendelser
enters: /
lands_in: incident_reports
handled_by: HMS-ansvarlig
done_when: [closed]
max_steps: 1
max_fields: 3
success: [closed]
stages: [open, closed]
- id: internal_referral
who: prosjektledere
wants: anbefalinger av kvalifisert mandskap
enters: /
lands_in: internal_referral
handled_by: prosjektledere
done_when: [referred]
max_steps: 1
max_fields: 2
success: [referred]
stages: [open, referred]
<<<END>>>
<<<FILE aggregates.yaml>>>
aggregates:
- bucket: leads
initial: open
states:
open: { event: received }
quoted: { event: quoted }
won: { event: won }
lost: { event: lost }
transitions:
open: [quoted, lost]
quoted: [won, lost]
- bucket: subcontractors
initial: open
states:
open: { event: received }
approved: { event: approved }
rejected: { event: rejected }
transitions:
open: [approved, rejected]
approved: []
rejected: []
- bucket: job_applications
initial: open
states:
open: { event: received }
interviewed: { event: interviewed }
hired: { event: hired }
rejected: { event: rejected }
transitions:
open: [interviewed]
interviewed: [hired, rejected]
hired: []
rejected: []
- bucket: incident_reports
initial: open
states:
open: { event: reported }
closed: { event: closed }
transitions:
open: [closed]
closed: []
- bucket: internal_referral
initial: open
states:
open: { event: referred }
referred: { event: referred }
transitions:
open: [referred]
referred: []
<<<END>
<<<FILE questions/index.yaml>>>
name: Hva hjelper du på?
description: >
Vi hjelper med tilstandsrapporter, prosjektering, byggesøknad og jobb.
responsible: { name: Marte Åsrud, email: marte@asrudbygg.example }
alternatives:
- name: Melding om problem med bygget
description: >
action: /leads
consequence: [Send]
record_as: leads
- name: Melding om underentreprenør
description: >
action: /subcontractors
consequence: [Send]
record_as: subcontractors
- name: Søk jobb
description: >
action: /job_applications
consequence: [Send]
record_as: job_applications
- name: Melding om avvik på byggeplass
description: >
action: /incident_reports
consequence: [Send]
record_as: incident_reports
- name: Anbefaling av kompetanse
description: >
action: /internal_referral
consequence: [Send]
record_as: internal_referral
<<<END>
<<<FILE questions/leads.yaml>>>
name: Hva problemet gjelder
description: >
Fortell kort hva som trenger å gjøres med bygget ditt.
responsible: { name: Marte Åsrud, email: marte@asrudbygg.example }
alternatives:
- name: Send tilbud
description: >
action: /leads/quoted
consequence: [Send]
record_as: leads
features:
- name: E-post
description: >
requirements:
- { name: email, label: E-post, type: email }
- { name: problem, label: Hva er problemet, type: textarea }
- { name: urgency, label: Hvor snart, type: select, options: [I denne uken, I denne måneden, Ikke akutt] }
- { name: address, label: Adresse, optional: true }
- name: Avslå
description: >
action: /leads/lost
consequence: [Send]
record_as: leads
<<<END>
<<<FILE questions/leads/quoted.yaml>>>
name: Tilbud sendt
description: >
Vi sender et tilbud på arbeidet.
responsible: { name: Marte Åsrud, email: marte@asrudbygg.example }
alternatives:
- name: Aksepter tilbudet
description: >
action: /leads/won
consequence: [Godkjenn]
record_as: leads
- name: Trekke tilbudet
description: >
action: /leads/lost
consequence: [Avslå]
record_as: leads
<<<END>
<<<FILE questions/subcontractors.yaml>>>
name: Melding om underentreprenør
description: >
Fortell kort hvem du er og hva du kan.
responsible: { name: Marte Åsrud, email: marte@asrudbygg.example }
alternatives:
- name: Godkjenn
description: >
action: /subcontractors/approved
consequence: [Godkjenn]
record_as: subcontractors
features:
- name: Organisasjonsnummer
description: >
requirements:
- { name: org_number, label: Organisasjonsnummer, type: text }
- { name: hms_kort, label: HMS-kort, type: select, options: [Har, Ikke har] }
- { name: insurance, label: Forsikring, type: select, options: [OK, Mangler] }
- { name: availability, label: Tilgjengelighet, type: select, options: [Normalt, Begrenset] }
- name: Avslå
description: >
action: /subcontractors/rejected
consequence: [Avslå]
record_as: subcontractors
<<<END>
<<<FILE questions/subcontractors/approved.yaml>>>
name: Godkjent som leverandør
description: >
Underentreprenøren er godkjent.
responsible: { name: Marte Åsrud, email: marte@asrudbygg.example }
alternatives:
- name: Gå tilbake til hovedmeny
description: >
action: /
consequence: [Tilbake]
record_as: subcontractors
<<<END>
<<<FILE questions/subcontractors/rejected.yaml>>>
name: Ikke kvalifisert
description: >
Underentreprenøren blir ikke godkjent nå.
responsible: { name: Marte Åsrud, email: marte@asrudbygg.example }
alternatives:
- name: Gå tilbake til hovedmeny
description: >
action: /
consequence: [Tilbake]
record_as: subcontractors
<<<END>
<<<FILE questions/job_applications.yaml>>>
name: Stillingsansøkning
description: >
Fortell kort hvem du er og hva du kan.
responsible: { name: Marte Åsrud, email: marte@asrudbygg.example }
alternatives:
- name: Send påmelding
description: >
action: /job_applications/interviewed
consequence: [Send]
record_as: job_applications
features:
- name: Navn
description: >
requirements:
- { name: name, label: Navn, type: text }
- { name: phone, label: Telefon, type: tel }
- { name: email, label: E-post, type: email }
- { name: experience, label: Erfaring, type: textarea }
- name: Avslå påmelding
description: >
action: /job_applications/rejected
consequence: [Avslå]
record_as: job_applications
<<<END>
<<<FILE questions/job_applications/interviewed.yaml>>>
name: Intervju
description: >
Vi kaller deg til intervju.
responsible: { name: Marte Åsrud, email: marte@asrudbygg.example }
alternatives:
- name: Ansett
description: >
action: /job_applications/hired
consequence: [Ansett]
record_as: job_applications
- name: Takk nei
description: >
action: /job_applications/rejected
consequence: [Avslå]
record_as: job_applications
<<<END>
<<<FILE questions/job_applications/hired.yaml>>>
name: Ansett
description: >
Du blir ansatt som tømrer, murer eller lærling.
responsible: { name: Marte Åsrud, email: marte@asrudbygg.example }
alternatives:
- name: Gå tilbake til hovedmeny
description: >
action: /
consequence: [Tilbake]
record_as: job_applications
<<<END>
<<<FILE questions/job_applications/rejected.yaml>>>
name: Takk nei
description: >
Vi takker nei til din påmelding.
responsible: { name: Marte Åsrud, email: marte@asrudbygg.example }
alternatives:
- name: Gå tilbake til hovedmeny
description: >
action: /
consequence: [Tilbake]
record_as: job_applications
<<<END>
<<<FILE questions/incident_reports.yaml>>>
name: Rapporter avvik
description: >
Fortell kort hva som har skjedd.
responsible: { name: Marte Åsrud, email: marte@asrudbygg.example }
alternatives:
- name: Lukk saken
description: >
action: /incident_reports/closed
consequence: [Lukk]
record_as: incident_reports
features:
- name: beskrivelse
description: >
requirements:
- { name: description, label: Hva skjedde, type: textarea }
- { name: avvik_type, label: Typen avvik, type: select, options: [Løse rekkverk, Nestenulykke, Feil materialer] }
- { name: rapportert_av, label: Hvem rapporterte, type: text }
- name: Avslå å rapportere
description: >
action: /incident_reports/closed
consequence: [Avslå]
record_as: incident_reports
<<<END>
<<<FILE questions/incident_reports/closed.yaml>>>
name: Saken er lukket
description: >
Avviket er løst og saken lukkes.
responsible: { name: Marte Åsrud, email: marte@asrudbygg.example }
alternatives:
- name: Gå tilbake til hovedmeny
description: >
action: /
consequence: [Tilbake]
record_as: incident_reports
<<<END>
<<<FILE questions/internal_referral.yaml>>>
name: Anbefaling av kompetanse
description: >
Fortell kort hvem du anbefaler og hvorfor.
responsible: { name: Marte Åsrud, email: marte@asrudbygg.example }
alternatives:
- name: Send anbefaling
description: >
action: /internal_referral/referred
consequence: [Send]
record_as: internal_referral
features:
- name: Navn
description: >
requirements:
- { name: name, label: Navn, type: text }
- { name: role, label: Rolle, type: text }
- { name: contact, label: Kontakt, type: email }
- name: Avslå anbefaling
description: >
action: /internal_referral/referred
consequence: [Avslå]
record_as: internal_referral
<<<END>
<<<FILE questions/internal_referral/referred.yaml>>>
name: Anbefaling sendt
description: >
Anbefalingen er sendt videre.
responsible: { name: Marte Åsrud, email: marte@asrudbygg.example }
alternatives:
- name: Gå tilbake til hovedmeny
description: >
action: /
consequence: [Tilbake]
record_as: internal_referral
<<<END>
<<<FILE questions/desk/_section.yaml>>>
qualifies: prosjektledere
<<<END>
<<<FILE questions/desk/index.yaml>>>
name: Hva kom inn?
description: >
Henvendelser og saker som trenger handling.
responsible: { name: Marte Åsrud, email: marte@asrudbygg.example }
alternatives:
- name: Leads
description: >
action: /desk
consequence: [Confirm]
record_as: leads
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: leads }
requires_group: prosjektledere
transitions:
- { to: quoted, label: Quote sent }
- { to: lost, label: Not for us }
- { from: quoted, to: won, label: Won }
- { from: quoted, to: lost, label: Lost }
- name: Subcontractors
description: >
action: /desk
consequence: [Confirm]
record_as: subcontractors
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: subcontractors }
requires_group: prosjektledere
- name: Job applications
description: >
action: /desk
consequence: [Confirm]
record_as: job_applications
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: job_applications }
requires_group: prosjektledere
- name: Incident reports
description: >
action: /desk
consequence: [Confirm]
record_as: incident_reports
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: incident_reports }
requires_group: HMS-ansvarlig
- name: Internal referrals
description: >
action: /desk
consequence: [Confirm]
record_as: internal_referral
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: internal_referral }
requires_group: prosjektledere
<<<END>
@@ -0,0 +1,310 @@
<<<FILE questions/index.yaml>>>
name: Hva trenger du hjelp med?
description: >
Velg hva du trenger hjelp med.
responsible: { name: Åsrud Bygg og Rehabilitering AS, contact: post@asrud.no }
alternatives:
- name: Meld inn et problem
description: >
Fortell oss om det er noe galt med bygget ditt.
action: /leads
consequence: [Send]
record_as: leads
features:
- name: Om bygget
description: >
Hva slags problem opplever du?
requirements:
- { name: problem, label: Hva er problemet, type: textarea }
- { name: urgency, label: Hvor akutt, type: select, options: [Dette uken, Dette måneden, Ikke akutt] }
- { name: contact, label: Kontaktinformasjon, type: email }
- name: Bliv godkjent som leverandør
description: >
Søk om å bli fast leverandør til oss.
action: /subcontractors
consequence: [Send]
record_as: subcontractors
features:
- name: Førstehåndsinformasjon
description: >
Hvem er du og hva kan du?
requirements:
- { name: company, label: Bedriftsnavn, type: text }
- { name: contact, label: Kontaktinformasjon, type: email }
- { name: experience, label: Erfaring, type: textarea }
- name: Søk jobb
description: >
Søk om jobb som tømrer, murer eller lærling.
action: /job_applications
consequence: [Send]
record_as: job_applications
features:
- name: Faglig profil
description: >
Hvilken stilling søker du?
requirements:
- { name: role, label: Stillingsnavn, type: select, options: [Tømrer, Murer, Lærling] }
- { name: contact, label: Kontaktinformasjon, type: email }
- { name: experience, label: Erfaring, type: textarea }
- name: Rapporter avvik
description: >
Rapporter uønskede hendelser eller avvik på byggeplassen.
action: /incident_reports
consequence: [Send]
record_as: incident_reports
features:
- name: Avviksinformasjon
description: >
Hva skjedde?
requirements:
- { name: incident, label: Hva skjedde, type: textarea }
- { name: severity, label: Alvorlighetsgrad, type: select, options: [Høyt, Medium, Lavt] }
- { name: reporter, label: Rapportert av, type: text }
- name: Anbefal kvalifisert mandskap
description: >
Anbefal noen vi bør snakke med.
action: /internal_referral
consequence: [Send]
record_as: internal_referral
features:
- name: Anbefaling
description: >
Hvem anbefaler du og hvorfor?
requirements:
- { name: person, label: Navn, type: text }
- { name: reason, label: Grunn, type: textarea }
- { name: contact, label: Kontaktinformasjon, type: email }
<<<END>>>
<<<FILE questions/leads/index.yaml>>>
name: Meld inn et problem
description: >
Fortell oss om det er noe galt med bygget ditt.
responsible: { name: prosjektledere, contact: post@asrud.no }
alternatives:
- name: Send
description: >
Send inn meldingen.
action: /received
consequence: [Send]
record_as: leads
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: leads }
requires_group: prosjektledere
transitions:
- { to: quoted, label: Quote sent }
- { to: lost, label: Not for us }
- { from: quoted, to: won, label: Won }
- { from: quoted, to: lost, label: Lost }
<<<END>
<<<FILE questions/subcontractors/index.yaml>>>
name: Bliv godkjent som leverandør
description: >
Søk om å bli fast leverandør til oss.
responsible: { name: prosjektledere, contact: post@asrud.no }
alternatives:
- name: Send
description: >
Send inn søknaden.
action: /received
consequence: [Send]
record_as: subcontractors
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: subcontractors }
requires_group: prosjektledere
transitions:
- { to: approved, label: Godkjent }
- { to: rejected, label: Ikke godkjent }
<<<END>
<<<FILE questions/job_applications/index.yaml>
name: Søk jobb
description: >
Søk om jobb som tømrer, murer eller lærling.
responsible: { name: prosjektledere, contact: post@asrud.no }
alternatives:
- name: Send
description: >
Send inn søknaden.
action: /received
consequence: [Send]
record_as: job_applications
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: job_applications }
requires_group: prosjektledere
transitions:
- { to: interviewed, label: Interview requested }
- { to: rejected, label: Ikke relevant }
- { from: interviewed, to: hired, label: Hired }
- { from: interviewed, to: rejected, label: Rejected }
<<<END>
<<<FILE questions/incident_reports/index.yaml>
name: Rapporter avvik
description: >
Rapporter uønskede hendelser eller avvik på byggeplassen.
responsible: { name: HMS-ansvarlig, contact: post@asrud.no }
alternatives:
- name: Send
description: >
Send inn rapporten.
action: /received
consequence: [Send]
record_as: incident_reports
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: incident_reports }
requires_group: HMS-ansvarlig
transitions:
- { to: closed, label: Closed }
<<<END>
<<<FILE questions/internal_referral/index.yaml>
name: Anbefal kvalifisert mandskap
description: >
Anbefal noen vi bør snakke med.
responsible: { name: prosjektledere, contact: post@asrud.no }
alternatives:
- name: Send
description: >
Send inn anbefalingen.
action: /received
consequence: [Send]
record_as: internal_referral
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: internal_referral }
requires_group: prosjektledere
transitions:
- { to: referred, label: Referred }
<<<END>
<<<FILE questions/desk/_section.yaml>>>
qualifies: prosjektledere
<<<END>
<<<FILE questions/desk/index.yaml>>>
name: Hva kom inn?
responsible: { name: prosjektledere, contact: post@asrud.no }
alternatives:
- name: Leads
description: >
Behandl innkommende meldinger.
action: /desk
consequence: [Confirm]
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: leads }
requires_group: prosjektledere
transitions:
- { to: quoted, label: Quote sent }
- { to: lost, label: Not for us }
- { from: quoted, to: won, label: Won }
- { from: quoted, to: lost, label: Lost }
- name: Subcontractors
description: >
Behandl leverandør-søknader.
action: /desk
consequence: [Confirm]
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: subcontractors }
requires_group: prosjektledere
transitions:
- { to: approved, label: Godkjent }
- { to: rejected, label: Ikke godkjent }
- name: Job applications
description: >
Behandl jobb-søknader.
action: /desk
consequence: [Confirm]
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: job_applications }
requires_group: prosjektledere
transitions:
- { to: interviewed, label: Interview requested }
- { to: rejected, label: Ikke relevant }
- { from: interviewed, to: hired, label: Hired }
- { from: interviewed, to: rejected, label: Rejected }
- name: Incident reports
description: >
Behandl avvik rapporter.
action: /desk
consequence: [Confirm]
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: incident_reports }
requires_group: HMS-ansvarlig
transitions:
- { to: closed, label: Closed }
- name: Internal referrals
description: >
Behandl anbefalinger.
action: /desk
consequence: [Confirm]
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: internal_referral }
requires_group: prosjektledere
transitions:
- { to: referred, label: Referred }
<<<END>
<<<FILE questions/received.yaml>>>
name: Melding mottatt
description: >
Meldingen er mottatt. Hvem skal lese den og hva skjer nå?
responsible: { name: prosjektledere, contact: post@asrud.no }
alternatives:
- name: Gå tilbake
description: >
Gå tilbake til hovedmenyen.
action: /
consequence: [Gå tilbake]
followup: true
<<<END>
<<<FILE questions/leads/_section.yaml>>>
qualifies: prosjektledere
<<<END>
<<<FILE questions/subcontractors/_section.yaml>>>
qualifies: prosjektledere
<<<END>
<<<FILE questions/job_applications/_section.yaml>>>
qualifies: prosjektledere
<<<END>
<<<FILE questions/incident_reports/_section.yaml>>>
qualifies: HMS-ansvarlig
<<<END>
<<<FILE questions/internal_referral/_section.yaml>>>
qualifies: prosjektledere
<<<END>
@@ -0,0 +1,181 @@
<<<FILE questions/index.yaml>>>
name: Hva trenger du hjelp med?
description: >
Velg hva du trenger hjelp med.
responsible: { name: Åsrud Bygg og Rehabilitering AS, contact: post@asrud.no }
alternatives:
- name: Meld inn et problem
description: >
Fortell oss om det er noe galt med bygget ditt.
action: /questions/leads
consequence: [Send]
record_as: leads
features:
- name: Om bygget
description: >
Hva slags problem opplever du?
requirements:
- { name: problem, label: Hva er problemet, type: textarea }
- { name: urgency, label: Hvor akutt, type: select, options: [Dette uken, Dette måneden, Ikke akutt] }
- { name: contact, label: Kontaktinformasjon, type: email }
- name: Bliv godkjent som leverandør
description: >
Søk om å bli fast leverandør til oss.
action: /questions/subcontractors
consequence: [Send]
record_as: subcontractors
features:
- name: Førstehåndsinformasjon
description: >
Hvem er du og hva kan du?
requirements:
- { name: company, label: Bedriftsnavn, type: text }
- { name: contact, label: Kontaktinformasjon, type: email }
- { name: experience, label: Erfaring, type: textarea }
- name: Søk jobb
description: >
Søk om jobb som tømrer, murer eller lærling.
action: /questions/job_applications
consequence: [Send]
record_as: job_applications
features:
- name: Faglig profil
description: >
Hvilken stilling søker du?
requirements:
- { name: role, label: Stillingsnavn, type: select, options: [Tømrer, Murer, Lærling] }
- { name: contact, label: Kontaktinformasjon, type: email }
- { name: experience, label: Erfaring, type: textarea }
- name: Rapporter avvik
description: >
Rapporter uønskede hendelser eller avvik på byggeplassen.
action: /questions/incident_reports
consequence: [Send]
record_as: incident_reports
features:
- name: Avviksinformasjon
description: >
Hva skjedde?
requirements:
- { name: incident, label: Hva skjedde, type: textarea }
- { name: severity, label: Alvorlighetsgrad, type: select, options: [Høyt, Medium, Lavt] }
- { name: reporter, label: Rapportert av, type: text }
- name: Anbefal kvalifisert mandskap
description: >
Anbefal noen vi bør snakke med.
action: /questions/internal_referral
consequence: [Send]
record_as: internal_referral
features:
- name: Anbefaling
description: >
Hvem anbefaler du og hvorfor?
requirements:
- { name: person, label: Navn, type: text }
- { name: reason, label: Grunn, type: textarea }
- { name: contact, label: Kontaktinformasjon, type: email }
<<<END>>>
<<<FILE questions/leads.yaml>>>
name: Meld inn et problem
description: >
Fortell oss om det er noe galt med bygget ditt.
responsible: { name: prosjektledere, contact: post@asrud.no }
alternatives:
- name: Send
consequence: [Send]
record_as: leads
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: leads }
requires_group: prosjektledere
transitions:
- { to: quoted, label: Quote sent }
- { to: lost, label: Not for us }
- { from: quoted, to: won, label: Won }
- { from: quoted, to: lost, label: Lost }
```
<<<FILE questions/subcontractors.yaml>>>
name: Bliv godkjent som leverandør
description: >
Søk om å bli fast leverandør til oss.
responsible: { name: prosjektledere, contact: post@asrud.no }
alternatives:
- name: Send
consequence: [Send]
record_as: subcontractors
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: subcontractors }
requires_group: prosjektledere
transitions:
- { to: approved, label: Godkjent }
- { to: rejected, label: Ikke godkjent }
```
<<<FILE questions/job_applications.yaml>>>
name: Søk jobb
description: >
Søk om jobb som tømrer, murer eller lærling.
responsible: { name: prosjektledere, contact: post@asrud.no }
alternatives:
- name: Send
consequence: [Send]
record_as: job_applications
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: job_applications }
requires_group: prosjektledere
transitions:
- { to: interviewed, label: Interview }
- { to: rejected, label: Ikke den rette }
- { from: interviewed, to: hired, label: Hired }
- { from: interviewed, to: rejected, label: Ikke den rette }
```
<<<FILE questions/incident_reports.yaml>>
name: Rapporter avvik
description: >
Rapporter uønskede hendelser eller avvik på byggeplassen.
responsible: { name: HMS-ansvarlig, contact: post@asrud.no }
alternatives:
- name: Send
consequence: [Send]
record_as: incident_reports
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: incident_reports }
requires_group: HMS-ansvarlig
transitions:
- { to: closed, label: Lukket }
```
<<<FILE questions/internal_referral.yaml>>>
name: Anbefal kvalifisert mandskap
description: >
Anbefal noen vi bør snakke med.
responsible: { name: prosjektledere, contact: post@asrud.no }
alternatives:
- name: Send
consequence: [Send]
record_as: internal_referral
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: internal_referral }
requires_group: prosjektledere
transitions:
- { to: referred, label: Anbefalt }
```
<<<END>>>
@@ -0,0 +1,93 @@
<<<FILE questions/internal_referral.yaml>>>
name: Anbefal kvalifisert mandskap
description: >
Anbefal noen vi bør snakke med.
responsible: { name: prosjektledere, contact: post@asrud.no }
alternatives:
- name: Send
consequence: [Send]
record_as: internal_referral
features:
- name: Anbefaling
description: >
Hvem anbefaler du og hvorfor?
requirements:
- { name: person, label: Navn, type: text }
- { name: reason, label: Grunn, type: textarea }
- { name: contact, label: Kontaktinformasjon, type: email }
<<<END>>>
<<<FILE questions/desk/_section.yaml>>>
qualifies: prosjektledere
<<<END>>
<<<FILE questions/desk/index.yaml>>>
name: What came in?
responsible: { name: ..., contact: ... }
alternatives:
- name: Leads
description: One line on what to do with them.
action: /desk
consequence: [Confirm]
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: leads }
requires_group: project_managers
transitions:
- { to: quoted, label: Quote sent }
- { to: lost, label: Not for us }
- { from: quoted, to: won, label: Won }
- { from: quoted, to: lost, label: Lost }
```
<<<END>>>
Wait, I need to check the verifier output again. The error is about `questions/internal_referral.yaml` having a parsing error at line 17. Let me look at the file content that was provided.
The file `questions/internal_referral.yaml` ends with:
```
transitions:
- { to: referred, label: Anbefalt }
```
It seems like the file is truncated or has a syntax error. The YAML structure seems incomplete. There's no closing brackets for the `features` list or the `alternatives` list or the resource block.
Looking at the original file that was rejected:
```
name: Anbefal kvalifisert mandskap
description: >
Anbefal noen vi bør snakke med.
responsible: { name: prosjektledere, contact: post@asrud.no }
alternatives:
- name: Send
consequence: [Send]
record_as: internal_referral
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: internal_referral }
requires_group: prosjektledere
transitions:
- { to: referred, label: Anbefalt }
```
The file seems to be missing closing brackets. The `features` list has one item, but the list isn't closed. The `alternatives` list has one item, but isn't closed. The YAML document itself might not have the proper closing.
Actually, looking more carefully, the error message says: "found character that cannot start any token at line 17 column 1, while scanning for the next token". This typically means there's an issue with indentation or unexpected characters.
But wait, the user says: "The verifier rejected the repo below. Fix it." and provides the verifier output. The current files include `questions/internal_referral.yaml` which appears to be truncated or malformed.
Let me also check the other files. There's `questions/desk/_section.yaml` and `questions/desk/index.yaml` but they seem to have issues too. The `questions/desk/index.yaml` has `responsible: { name: ..., contact: ... }` with ellipsis which might not be valid, and it references `project_managers` but the need says `handled_by: prosjektledere`.
Actually, re-reading the task: "Return only the files you change, each complete. To remove a file, return it with the single line DELETE. needs.yaml is frozen; do not return it."
So I need to fix the parsing error in `questions/internal_referral.yaml`. The error is at line 17 column 1. Looking at the file content, it seems like the file was cut off. I need to provide a complete valid YAML file.
Let me construct the complete `questions/internal_referral.yaml` file based on the pattern from other files and the needs specification.
From `needs.yaml`, the `internal_referral` need has:
- id: internal_referral
- who:
@@ -0,0 +1,64 @@
<<<FILE questions/internal_referral.yaml>>>
name: Anbefal kvalifisert mandskap
description: >
Anbefal noen vi bør snakke med.
responsible: { name: prosjektledere, contact: post@asrud.no }
alternatives:
- name: Send
consequence: [Send]
record_as: internal_referral
features:
- name: Anbefaling
description: >
Hvem anbefaler du og hvorfor?
requirements:
- { name: person, label: Navn, type: text }
- { name: reason, label: Grunn, type: textarea }
- { name: contact, label: Kontaktinformasjon, type: email }
<<<END>>>
<<<FILE site.yaml>>>
title: Åsrud Bygg og Rehabilitering AS
lang: no
<<<END>>>
<<<FILE questions/desk/index.yaml>>>
name: What came in?
responsible: { name: ..., contact: ... }
alternatives:
- name: Leads
description: One line on what to do with them.
action: /desk
consequence: [Confirm]
features:
- name: ""
description: ""
resource:
source: { kind: kv, bucket: leads }
requires_group: project_managers
transitions:
- { to: quoted, label: Quote sent }
- { to: lost, label: Not for us }
- { from: quoted, to: won, label: Won }
- { from: quoted, to: lost, label: Lost }
<<<END>></think><<<FILE questions/index.yaml>>>
name: Hva trenger du hjelp med?
description: >
Velg hva du trenger hjelp med.
responsible: { name: Åsrud Bygg og Rehabilitering AS, contact: post@asrud.no }
alternatives:
- name: Meld inn et problem
description: >
Fortell oss om det er noe galt med bygget ditt.
action: /questions/leads
consequence: [Send]
record_as: leads
features:
- name: Om bygget
description: >
Hva slags problem opplever du?
requirements:
- { name: problem, label: Hva er problemet, type: textarea }
- { name: urgency, label: Hvor akutt, type: select, options: [Dette uken, Dette måneden, Ikke akutt] }
- { name: contact, label: Kontaktinformasjon, type: email }
<<<END>>>
@@ -0,0 +1,24 @@
<<<FILE questions/internal_referral.yaml>>>
name: Anbefal kvalifisert mandskap
description: >
Anbefal noen vi bør snakke med.
responsible: { name: prosjektledere, contact: post@asrud.no }
alternatives:
- name: Send
consequence: [Send]
record_as: internal_referral
features:
- name: Anbefaling
description: >
Hvem anbefaler du og hvorfor?
requirements:
- { name: person, label: Navn, type: text }
- { name: reason, label: Grunn, type: textarea }
- { name: contact, label: Kontaktinformasjon, type: email }
<<<END>>>
<<<FILE site.yaml>>>
title: Åsrud Bygg og Rehabilitering AS
lang: no
<<<END>>>
```
@@ -0,0 +1,9 @@
needs: parsed in round 4
structure round 1: ok=false fails=11
structure round 2: ok=false fails=1
structure round 3: ok=false fails=0
structure round 4: ok=false fails=0
structure round 5: ok=false fails=0
structure round 6: ok=false fails=0
Error: parsing desk/index.yaml: could not find expected ':' at line 20 column 1, while scanning a simple key at line 19 column 1
+83 -12
View File
@@ -34,6 +34,9 @@ struct Opts {
llm: String, llm: String,
rounds: usize, rounds: usize,
wording_rounds: usize, wording_rounds: usize,
/// The model that plays the visitor. A generator grading its own
/// wording agrees with itself, so this should be a different one.
visitor: Option<String>,
} }
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
@@ -64,6 +67,8 @@ fn main() -> anyhow::Result<()> {
let intake = std::fs::read_to_string(&opts.intake) let intake = std::fs::read_to_string(&opts.intake)
.with_context(|| format!("reading {}", opts.intake.display()))?; .with_context(|| format!("reading {}", opts.intake.display()))?;
std::fs::create_dir_all(opts.out.join("questions"))?; std::fs::create_dir_all(opts.out.join("questions"))?;
std::fs::create_dir_all(opts.out.join("llm"))?;
LLM_LOG.set(opts.out.join("llm")).ok();
let mut log = Vec::new(); let mut log = Vec::new();
// Phase 1: the spec. // Phase 1: the spec.
@@ -72,7 +77,11 @@ fn main() -> anyhow::Result<()> {
let prompt = format!( let prompt = format!(
"{REFERENCE}\n\n# Task\n\nA business filled in this intake. Write ONLY `needs.yaml` for it: every need the intake implies, for the public and for each role inside the business, with states taken from how the business says things progress, and personas.\n\n## Intake\n\n{intake}\n{feedback}\n{FORMAT}" "{REFERENCE}\n\n# Task\n\nA business filled in this intake. Write ONLY `needs.yaml` for it: every need the intake implies, for the public and for each role inside the business, with states taken from how the business says things progress, and personas.\n\n## Intake\n\n{intake}\n{feedback}\n{FORMAT}"
); );
let files = parse_files(&llm(&opts.llm, &prompt)?)?; let Ok(files) = parse_files(&llm(&opts.llm, &prompt)?) else {
eprintln!("needs round {round}: the reply had no file blocks, asking again");
feedback = "\n## Your last attempt\n\nIt contained no <<<FILE ...>>> blocks at all. Return needs.yaml in exactly that format.\n".to_string();
continue;
};
let Some(needs) = files.get("needs.yaml") else { let Some(needs) = files.get("needs.yaml") else {
feedback = "\n## Your last attempt\n\nIt contained no needs.yaml.\n".to_string(); feedback = "\n## Your last attempt\n\nIt contained no needs.yaml.\n".to_string();
continue; continue;
@@ -89,8 +98,12 @@ fn main() -> anyhow::Result<()> {
} }
eprintln!("needs round {round}: needs.yaml parses, frozen"); eprintln!("needs round {round}: needs.yaml parses, frozen");
log.push(format!("needs: parsed in round {round}")); log.push(format!("needs: parsed in round {round}"));
feedback.clear();
break; break;
} }
if !feedback.is_empty() {
bail!("needs.yaml never parsed in {} round(s); the replies are in {}/llm/", opts.rounds, opts.out.display());
}
let needs = std::fs::read_to_string(opts.out.join("needs.yaml")).context("no needs.yaml was produced")?; let needs = std::fs::read_to_string(opts.out.join("needs.yaml")).context("no needs.yaml was produced")?;
// Phase 2: structure. The page-writing model sees the needs and // Phase 2: structure. The page-writing model sees the needs and
@@ -116,7 +129,10 @@ fn main() -> anyhow::Result<()> {
dump(&opts.out)? dump(&opts.out)?
) )
}; };
apply(&opts.out, parse_files(&llm(&opts.llm, &prompt)?)?)?; match parse_files(&llm(&opts.llm, &prompt)?) {
Ok(files) => apply(&opts.out, files)?,
Err(_) => eprintln!("structure round {round}: the reply had no file blocks"),
}
let (ok, r) = lint(&opts, &[])?; let (ok, r) = lint(&opts, &[])?;
report = r; report = r;
let fails = report.lines().filter(|l| l.starts_with("FAIL")).count(); let fails = report.lines().filter(|l| l.starts_with("FAIL")).count();
@@ -143,7 +159,7 @@ fn main() -> anyhow::Result<()> {
args.push("--skim".to_string()); args.push("--skim".to_string());
} }
lint(&opts, &args)?; lint(&opts, &args)?;
let answers = answer_blind(&opts.llm, &std::fs::read_to_string(&tasks_path)?)?; let answers = answer_blind(opts.visitor.as_deref().unwrap_or(&opts.llm), &std::fs::read_to_string(&tasks_path)?)?;
let answers_path = opts.out.join(if skim { "answers-skim.jsonl" } else { "answers.jsonl" }); let answers_path = opts.out.join(if skim { "answers-skim.jsonl" } else { "answers.jsonl" });
std::fs::write(&answers_path, answers)?; std::fs::write(&answers_path, answers)?;
let mut args = vec!["--needs-score".to_string(), answers_path.display().to_string()]; let mut args = vec!["--needs-score".to_string(), answers_path.display().to_string()];
@@ -198,6 +214,7 @@ fn parse_args() -> anyhow::Result<Opts> {
llm: "claude -p".to_string(), llm: "claude -p".to_string(),
rounds: 4, rounds: 4,
wording_rounds: 2, wording_rounds: 2,
visitor: None,
}; };
while let Some(arg) = args.next() { while let Some(arg) = args.next() {
let mut value = || args.next().ok_or_else(|| anyhow!("{arg} needs a value")); let mut value = || args.next().ok_or_else(|| anyhow!("{arg} needs a value"));
@@ -208,6 +225,7 @@ fn parse_args() -> anyhow::Result<Opts> {
"--llm" => opts.llm = value()?, "--llm" => opts.llm = value()?,
"--rounds" => opts.rounds = value()?.parse()?, "--rounds" => opts.rounds = value()?.parse()?,
"--wording-rounds" => opts.wording_rounds = value()?.parse()?, "--wording-rounds" => opts.wording_rounds = value()?.parse()?,
"--visitor-llm" => opts.visitor = Some(value()?),
other => bail!("unknown argument {other}"), other => bail!("unknown argument {other}"),
} }
} }
@@ -218,8 +236,23 @@ fn parse_args() -> anyhow::Result<Opts> {
} }
/// Prompt on stdin, text on stdout. /// Prompt on stdin, text on stdout.
/// Where `generate` keeps every prompt and reply (`<out>/llm/`). A
/// model that returns nothing usable has to be read, not guessed at.
static LLM_LOG: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
static LLM_CALLS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
fn llm(cmd: &str, prompt: &str) -> anyhow::Result<String> { fn llm(cmd: &str, prompt: &str) -> anyhow::Result<String> {
llm_with(cmd, prompt, &[]) let reply = llm_with(cmd, prompt, &[]);
if let Some(dir) = LLM_LOG.get() {
let n = LLM_CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
std::fs::write(dir.join(format!("{n:02}-prompt.md")), prompt).ok();
let text = match &reply {
Ok(text) => text.clone(),
Err(e) => format!("ERROR: {e}"),
};
std::fs::write(dir.join(format!("{n:02}-reply.md")), text).ok();
}
reply
} }
/// As `llm`, with hints for the backend in its environment (`SEED`, /// As `llm`, with hints for the backend in its environment (`SEED`,
@@ -262,20 +295,45 @@ fn lint(opts: &Opts, extra: &[String]) -> anyhow::Result<(bool, String)> {
/// `<<<FILE path>>> ... <<<END>>>` blocks. Anything outside a block is /// `<<<FILE path>>> ... <<<END>>>` blocks. Anything outside a block is
/// ignored, so a model that adds a sentence does no harm. /// ignored, so a model that adds a sentence does no harm.
fn parse_files(text: &str) -> anyhow::Result<BTreeMap<String, String>> { fn parse_files(text: &str) -> anyhow::Result<BTreeMap<String, String>> {
// Small models mangle the markers: `<<<END>>` with a bracket short,
// a stray `</think>`, the next `<<<FILE` glued onto the same line.
// So markers are found anywhere in a line, an END needs only its
// opening, and a new FILE closes whatever was open.
let mut files = BTreeMap::new(); let mut files = BTreeMap::new();
let mut current: Option<(String, String)> = None; let mut current: Option<(String, String)> = None;
let mut close = |current: &mut Option<(String, String)>, files: &mut BTreeMap<String, String>| {
if let Some((path, body)) = current.take() {
files.insert(path, body);
}
};
for line in text.lines() { for line in text.lines() {
if let Some(path) = line.trim().strip_prefix("<<<FILE ").and_then(|r| r.strip_suffix(">>>")) { let mut rest = line;
current = Some((path.trim().to_string(), String::new())); loop {
} else if line.trim() == "<<<END>>>" { let (file_at, end_at) = (rest.find("<<<FILE "), rest.find("<<<END"));
if let Some((path, body)) = current.take() { match (file_at, end_at) {
files.insert(path, body); (Some(f), e) if e.is_none_or(|e| f < e) => {
close(&mut current, &mut files);
let after = &rest[f + "<<<FILE ".len()..];
let stop = after.find(">>>").unwrap_or(after.len());
current = Some((after[..stop].trim().to_string(), String::new()));
rest = after.get(stop + 3..).unwrap_or("");
}
(_, Some(e)) => {
close(&mut current, &mut files);
rest = &rest[e + "<<<END".len()..];
}
_ => break,
}
}
// A line that carried a marker carries no content.
if std::ptr::eq(rest, line) {
if let Some((_, body)) = current.as_mut() {
body.push_str(line);
body.push('\n');
} }
} else if let Some((_, body)) = current.as_mut() {
body.push_str(line);
body.push('\n');
} }
} }
close(&mut current, &mut files);
if files.is_empty() { if files.is_empty() {
bail!("the model returned no <<<FILE ...>>> blocks"); bail!("the model returned no <<<FILE ...>>> blocks");
} }
@@ -403,6 +461,19 @@ mod tests {
assert!(parse_files("no blocks here").is_err()); assert!(parse_files("no blocks here").is_err());
} }
#[test]
fn mangled_markers_from_a_small_model_still_separate_the_files() {
// Verbatim shape of a nemotron reply: a short END, a stray
// think tag, the next FILE glued on, and no END at the very end.
let files = parse_files(
"<<<FILE questions/desk/index.yaml>>>\nname: Desk?\n<<<END>></think><<<FILE questions/index.yaml>>>\nname: Hva trenger du?\n<<<FILE site.yaml>>>\ntitle: A\n",
)
.unwrap();
assert_eq!(files["questions/desk/index.yaml"], "name: Desk?\n");
assert_eq!(files["questions/index.yaml"], "name: Hva trenger du?\n");
assert_eq!(files["site.yaml"], "title: A\n");
}
#[test] #[test]
fn only_content_paths_may_be_written() { fn only_content_paths_may_be_written() {
assert!(allowed("site.yaml")); assert!(allowed("site.yaml"));