commit 7caaa27ba7e604641f7ca2ab5767d31f2cbc90a4 Author: Bendik Aagaard Lynghaug Date: Mon Sep 21 16:31:37 2026 +0200 portal-trainer: the loop around portal's needs verifier The trainer procedure as an agent skill, the engine contract, two sector specs (software-studio: UHHM's own, green; construction: a target with no pages yet), and the first wording run against uhhm.no. The verifier itself is portal's question_lint (uhhm/portal 188f9bf). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH diff --git a/README.md b/README.md new file mode 100644 index 0000000..f022528 --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# portal-trainer + +Onboarding a business onto [portal](https://project.uhhm.no/uhhm/portal) +is mostly a wording problem: the engine is generic, and whether it +works for a given business depends on whether its pages say, in the +visitor's words, the thing the business needs them to answer. This +repo is where that gets tested and, eventually, learned. + +## The loop + +``` +interview ─► needs.yaml ─► pages + aggregates + desks + │ │ + │ question_lint (structure: pass/fail) + │ │ + └─► personas ─► tasks.jsonl ─► answering engine ─► answers.jsonl + │ + question_lint --needs-score ◄──┘ + │ + misroutes = wording defects ─► revise pages +``` + +- **`needs.yaml`** lives in the business's own content repo. It says + who wants what, which bucket the answer lands in, which group + carries it to a finished state, and what the visitor will tolerate + in submissions and required fields. +- **The verifier is portal's `question_lint`**, not code in this repo. + It reads portal's own content types and ships in every portal + release, so a content repo's CI enforces its needs with no new + dependency. Structure is deterministic and gates CI. Wording is + graded, and gates only if you pass `--min-accuracy`. +- **This repo holds what sits around the verifier**: the trainer + procedure (`skill/SKILL.md`), sector specs to start from + (`sectors/`), and the runs (`runs/`). + +``` +question_lint --path /questions # structure +question_lint --path /questions --needs-tasks tasks.jsonl # full wording +question_lint --path /questions --needs-tasks skim.jsonl --skim # headings only +question_lint --path /questions --needs-score answers.jsonl [--min-accuracy 0.8] +``` + +## The engine contract + +A task is one page on one persona's path, as a typed `choice`: + +```json +{"id":"architecture-firm@/","persona":"architecture-firm","page":"/", + "state":{"body":"We run a small architecture firm and ..."}, + "questions":{"pick":{"type":"choice", + "instructions":"A visitor reads the page question \"What are you here for?\". Which alternative do they choose?", + "criteria":{"":"", "...":"..."}}}, + "expect":[""]} +``` + +Strip `expect` (and `need`) before an engine sees it. An answer is one +line: `{"id": "...", "choice": "", "confidence": 0.0-1.0}`. +`state` and `questions` are exactly the arguments of +[laya](https://github.com/NandhaKishorM/laya)'s +`router.predict(state, questions)`, so a laya adapter is a loop over +the file; an LLM adapter is a prompt around the same two fields. + +## Engines, and what is known about them + +| Engine | Status | What it is good for | +|---|---|---| +| LLM, blind (no `expect`) | used for the first run | Reads like a visitor; slow and costs per task, so use it to find defects, not to gate CI. | +| laya `english` (ModernBERT-large, 421M) | not run yet | 33 ms per question, so it can gate CI and score thousands of generated variants. Its authors measure 0.78 intent accuracy in English. | +| laya `multilingual` | not run yet | Its authors' own sweep puts Norwegian Bokmål at **0.53 accuracy with 0.86 mean confidence**. It is confidently wrong too often to grade Norwegian wording without fine-tuning. Test a Norwegian site in English translation, or with an LLM. | + +An engine is a measuring instrument. Before trusting one on a sector, +score it on tasks whose right answer nobody disputes; an engine that +misses those is measuring itself, not the page. + +## First run: uhhm.no, 2026-09-21 + +Nine personas against the live uhhm.no front door, answered by a small +LLM that never saw `expect`. One run each; eight tasks; treat as a +lead, not a measurement. + +| What the engine could read | Routed right | +|---|---| +| Headings and descriptions | 7 of 8 | +| Headings only (`--skim`) | 5 of 8 | + +- A founder who wants a first version built picked **"Work happens + through dialogue, not tickets"** - the collaborator door - in both + modes. With headings only, so did a firm wanting to pay for storage + and backups. The heading reads as how UHHM works with clients. +- With headings only, a student seeking an internship picked + **"Attention is earned"**. +- Structure: four needs met. The site-improvement flow is advertised + and `disabled`, so it is declared `planned`. + +## Sectors + +A sector file is a needs spec with no pages behind it: a failing test +suite for the trainer to make pass, carrying what the sector demands +(why a lead is a pipeline, why a deviation report gets three fields). + +- `sectors/software-studio/needs.yaml` - UHHM's own, and green against + `uhhm/questions`. +- `sectors/construction/needs.yaml` - a contractor with project + managers, a safety officer, crew and subcontractors: winning survey + and design work, vetting subcontractors, requesting and answering + bids, hiring, deviation reporting. No pages yet. + +## Toward a trained model + +Nothing here trains weights yet, on purpose. The order that makes +training possible: + +1. The verifier has to be trusted first. It is the reward. A generator + trained against a judge that is wrong half the time learns the + judge. +2. Every onboarding leaves a run: needs, the pages at each revision, + the scores. That corpus - what wording routed which persona right, + per sector - is the training data, and it does not exist until the + loop has been run by hand a few dozen times. +3. With a fast engine (laya-class) as the scorer, generating and + scoring many page variants per need is cheap, which is what makes + search, and later fine-tuning a small generator on the winners, + practical. diff --git a/runs/uhhm-2026-09-21/answers-full.jsonl b/runs/uhhm-2026-09-21/answers-full.jsonl new file mode 100644 index 0000000..eb8f7c1 --- /dev/null +++ b/runs/uhhm-2026-09-21/answers-full.jsonl @@ -0,0 +1,8 @@ +{"id":"architecture-firm@/","choice":"Good software begins with a clear argument","confidence":0.9} +{"id":"climbing-gym-founder@/","choice":"Work happens through dialogue, not tickets","confidence":0.75} +{"id":"municipality-buyer@/","choice":"Good software begins with a clear argument","confidence":0.95} +{"id":"exhibition-visitor@/","choice":"Attention is earned","confidence":0.95} +{"id":"tech-journalist@/","choice":"Attention is earned","confidence":0.9} +{"id":"rust-developer@/","choice":"Work happens through dialogue, not tickets","confidence":0.8} +{"id":"interaction-designer@/","choice":"Work happens through dialogue, not tickets","confidence":0.85} +{"id":"student-intern@/","choice":"Work happens through dialogue, not tickets","confidence":0.65} diff --git a/runs/uhhm-2026-09-21/answers-skim.jsonl b/runs/uhhm-2026-09-21/answers-skim.jsonl new file mode 100644 index 0000000..58fa88f --- /dev/null +++ b/runs/uhhm-2026-09-21/answers-skim.jsonl @@ -0,0 +1,8 @@ +{"id":"architecture-firm@/","choice":"Work happens through dialogue, not tickets","confidence":0.70} +{"id":"climbing-gym-founder@/","choice":"Work happens through dialogue, not tickets","confidence":0.85} +{"id":"municipality-buyer@/","choice":"Good software begins with a clear argument","confidence":0.60} +{"id":"exhibition-visitor@/","choice":"Attention is earned","confidence":0.80} +{"id":"tech-journalist@/","choice":"Attention is earned","confidence":0.80} +{"id":"rust-developer@/","choice":"Work happens through dialogue, not tickets","confidence":0.75} +{"id":"interaction-designer@/","choice":"Work happens through dialogue, not tickets","confidence":0.80} +{"id":"student-intern@/","choice":"Attention is earned","confidence":0.55} diff --git a/runs/uhhm-2026-09-21/tasks.jsonl b/runs/uhhm-2026-09-21/tasks.jsonl new file mode 100644 index 0000000..0a0bb8b --- /dev/null +++ b/runs/uhhm-2026-09-21/tasks.jsonl @@ -0,0 +1,8 @@ +{"id":"architecture-firm@/","persona":"architecture-firm","need":"commission","page":"/","state":{"body":"We run a small architecture firm and our project files are scattered across three cloud drives. We want to pay someone to set up proper storage and backups we control ourselves."},"questions":{"pick":{"type":"choice","instructions":"A visitor reads the page question \"What are you here for?\". Which alternative do they choose?","criteria":{"Attention is earned":"Considered notes on what we're building, and why — sent only when there's something worth saying.","Good software begins with a clear argument":"Infrastructure, tools, or a product — bring the problem, and we read the whole brief before answering.","Work happens through dialogue, not tickets":"What you bring shapes what gets built. We onboard collaborators the same deliberate way we build — by hand, through a real conversation."}}},"expect":["Good software begins with a clear argument"]} +{"id":"climbing-gym-founder@/","persona":"climbing-gym-founder","need":"commission","page":"/","state":{"body":"I have an idea for a booking app for climbing gyms and I'm looking for a team that can build the first version with me."},"questions":{"pick":{"type":"choice","instructions":"A visitor reads the page question \"What are you here for?\". Which alternative do they choose?","criteria":{"Attention is earned":"Considered notes on what we're building, and why — sent only when there's something worth saying.","Good software begins with a clear argument":"Infrastructure, tools, or a product — bring the problem, and we read the whole brief before answering.","Work happens through dialogue, not tickets":"What you bring shapes what gets built. We onboard collaborators the same deliberate way we build — by hand, through a real conversation."}}},"expect":["Good software begins with a clear argument"]} +{"id":"municipality-buyer@/","persona":"municipality-buyer","need":"commission","page":"/","state":{"body":"Our municipality needs an internal tool for tracking building permits. We are looking for a vendor who can scope it and deliver it."},"questions":{"pick":{"type":"choice","instructions":"A visitor reads the page question \"What are you here for?\". Which alternative do they choose?","criteria":{"Attention is earned":"Considered notes on what we're building, and why — sent only when there's something worth saying.","Good software begins with a clear argument":"Infrastructure, tools, or a product — bring the problem, and we read the whole brief before answering.","Work happens through dialogue, not tickets":"What you bring shapes what gets built. We onboard collaborators the same deliberate way we build — by hand, through a real conversation."}}},"expect":["Good software begins with a clear argument"]} +{"id":"exhibition-visitor@/","persona":"exhibition-visitor","need":"follow","page":"/","state":{"body":"I saw your drawing network at an exhibition and liked it. I don't want to get involved, I'd just like to know when there is news."},"questions":{"pick":{"type":"choice","instructions":"A visitor reads the page question \"What are you here for?\". Which alternative do they choose?","criteria":{"Attention is earned":"Considered notes on what we're building, and why — sent only when there's something worth saying.","Good software begins with a clear argument":"Infrastructure, tools, or a product — bring the problem, and we read the whole brief before answering.","Work happens through dialogue, not tickets":"What you bring shapes what gets built. We onboard collaborators the same deliberate way we build — by hand, through a real conversation."}}},"expect":["Attention is earned"]} +{"id":"tech-journalist@/","persona":"tech-journalist","need":"follow","page":"/","state":{"body":"I write about Nordic technology. Keep me posted on what you release so I can cover it."},"questions":{"pick":{"type":"choice","instructions":"A visitor reads the page question \"What are you here for?\". Which alternative do they choose?","criteria":{"Attention is earned":"Considered notes on what we're building, and why — sent only when there's something worth saying.","Good software begins with a clear argument":"Infrastructure, tools, or a product — bring the problem, and we read the whole brief before answering.","Work happens through dialogue, not tickets":"What you bring shapes what gets built. We onboard collaborators the same deliberate way we build — by hand, through a real conversation."}}},"expect":["Attention is earned"]} +{"id":"rust-developer@/","persona":"rust-developer","need":"collaborate","page":"/","state":{"body":"I'm a Rust developer between jobs and I like that you build in the open. I would love to contribute, and maybe join you."},"questions":{"pick":{"type":"choice","instructions":"A visitor reads the page question \"What are you here for?\". Which alternative do they choose?","criteria":{"Attention is earned":"Considered notes on what we're building, and why — sent only when there's something worth saying.","Good software begins with a clear argument":"Infrastructure, tools, or a product — bring the problem, and we read the whole brief before answering.","Work happens through dialogue, not tickets":"What you bring shapes what gets built. We onboard collaborators the same deliberate way we build — by hand, through a real conversation."}}},"expect":["Work happens through dialogue, not tickets"]} +{"id":"interaction-designer@/","persona":"interaction-designer","need":"collaborate","page":"/","state":{"body":"I'm an interaction designer. I want to work with you on the drawing interface."},"questions":{"pick":{"type":"choice","instructions":"A visitor reads the page question \"What are you here for?\". Which alternative do they choose?","criteria":{"Attention is earned":"Considered notes on what we're building, and why — sent only when there's something worth saying.","Good software begins with a clear argument":"Infrastructure, tools, or a product — bring the problem, and we read the whole brief before answering.","Work happens through dialogue, not tickets":"What you bring shapes what gets built. We onboard collaborators the same deliberate way we build — by hand, through a real conversation."}}},"expect":["Work happens through dialogue, not tickets"]} +{"id":"student-intern@/","persona":"student-intern","need":"collaborate","page":"/","state":{"body":"I'm a computer science student looking for a summer internship doing infrastructure work."},"questions":{"pick":{"type":"choice","instructions":"A visitor reads the page question \"What are you here for?\". Which alternative do they choose?","criteria":{"Attention is earned":"Considered notes on what we're building, and why — sent only when there's something worth saying.","Good software begins with a clear argument":"Infrastructure, tools, or a product — bring the problem, and we read the whole brief before answering.","Work happens through dialogue, not tickets":"What you bring shapes what gets built. We onboard collaborators the same deliberate way we build — by hand, through a real conversation."}}},"expect":["Work happens through dialogue, not tickets"]} diff --git a/sectors/construction/needs.yaml b/sectors/construction/needs.yaml new file mode 100644 index 0000000..b0754b2 --- /dev/null +++ b/sectors/construction/needs.yaml @@ -0,0 +1,158 @@ +# A building contractor's needs, as a portal needs spec: a small to +# mid-sized firm doing surveys, design and rehabilitation, run by a few +# project managers with its own crew and a ring of subcontractors. +# +# This file is a target, not a description: there are no pages behind +# it yet, so `question_lint` fails every need. The trainer's job is to +# write the pages, aggregates.yaml and desks that turn it green, then +# word them until the personas route right. +# +# Groups (Kanidm): project_managers, hse, crew, subcontractors. +# Everyone else arrives anonymous. +# +# What the sector demands of the flows, and why the budgets are what +# they are: +# - Winning work is a pipeline, not an inbox: a lead is surveyed, then +# quoted, then won or lost. "answered" is not an outcome. +# - A subcontractor must be vetted before they set foot on site +# (company registration, trade approval, insurance, tax standing, +# site ID cards for their people). Documents go missing; the state +# machine has to say so and let them come back. +# - Deviation and near-miss reporting only works when it costs the +# reporter seconds. Three required fields, one submission, from a +# phone, and the safety officer owns closing it. +# - People move between firms constantly; onboarding a worker is the +# same shape as onboarding a subcontractor, one size smaller. +needs: + - id: win-survey-work + who: a building owner or housing board with a problem in their building + wants: someone qualified to look at it and say what it takes + lands_in: leads + handled_by: project_managers + done_when: [won, lost] + max_steps: 2 + max_fields: 5 + + - id: win-design-work + who: an owner who knows what they want built or changed + wants: drawings, a permit application, and a price + lands_in: leads + handled_by: project_managers + done_when: [won, lost] + max_steps: 2 + max_fields: 6 + + - id: vet-subcontractor + who: a trade firm that wants to work for us + wants: to be approved once, and then be asked + lands_in: subcontractors + handled_by: project_managers + done_when: [approved, rejected] + max_steps: 2 + max_fields: 7 + + - id: request-bids + who: a project manager with a job to staff + wants: approved subcontractors to see it and answer + enters: /jobs/post + enters_as: project_managers + lands_in: job_requests + handled_by: project_managers + done_when: [awarded, withdrawn] + max_steps: 1 + max_fields: 5 + + - id: answer-bid + who: an approved subcontractor + wants: to say yes to a job, with a price and a start date + enters: /jobs + enters_as: subcontractors + lands_in: bids + handled_by: project_managers + done_when: [accepted, declined] + max_steps: 2 + max_fields: 3 + + - id: hire-crew + who: a tradesperson looking for work + wants: to be considered, without writing a letter + lands_in: applicants + handled_by: project_managers + done_when: [hired, declined] + max_steps: 2 + max_fields: 4 + + - id: report-deviation + who: anyone on site - crew or a subcontractor's people + wants: to report a hazard, a near miss or a defect before they forget + enters: /site + enters_as: crew + lands_in: deviations + handled_by: hse + done_when: [closed] + max_steps: 1 + max_fields: 3 + +personas: + - id: housing-board-damp + need: win-survey-work + text: > + I chair the board of a housing cooperative from the seventies. + Two ground-floor flats have damp in the walls and we don't know + if it's drainage or the facade. We need someone to tell us. + + - id: buyer-before-purchase + need: win-survey-work + text: > + We are about to buy an old timber house. Before we sign we want + a professional to go through it and tell us what is wrong with it. + + - id: attic-conversion + need: win-design-work + text: > + We want to turn our attic into two bedrooms and a bathroom. We + need drawings and someone who handles the application to the + municipality. + + - id: shop-to-flat + need: win-design-work + text: > + I own a former shop on the ground floor and want to make it a + flat. I have no idea what is allowed. Can you sort that out and + give me a price? + + - id: electrician-firm + need: vet-subcontractor + text: > + We are a four-person electrical firm, fully certified. We would + like to get on your list so you call us when you need + electricians. + + - id: scaffolder + need: vet-subcontractor + text: > + I run a scaffolding company. How do I get to do jobs for you? + + - id: carpenter-jobseeker + need: hire-crew + text: > + I'm a certified carpenter with eight years on rehabilitation + jobs. Are you hiring? + + - id: apprentice + need: hire-crew + text: > + I finish vocational school this spring and need an + apprenticeship as a bricklayer. + + - id: loose-railing + need: report-deviation + text: > + The railing on the third level of the scaffold on the north + side is loose. Nobody got hurt but someone will. + + - id: wrong-membrane + need: report-deviation + text: > + The bathroom membrane delivered today is not the one in the + specification. We stopped work on it. diff --git a/sectors/software-studio/needs.yaml b/sectors/software-studio/needs.yaml new file mode 100644 index 0000000..a8ab2fe --- /dev/null +++ b/sectors/software-studio/needs.yaml @@ -0,0 +1,127 @@ +# What UHHM, as a business, needs this site to do - stated apart from +# how the pages word it. `question_lint` checks every need against the +# pages and aggregates.yaml (portal `src/needs.rs`): a path of +# submissions gets the visitor's answer into `lands_in` within the +# effort budget, `handled_by` has a desk over that bucket, and every +# `done_when` state is reachable through the moves the desk offers. +# +# The personas are visitors in their own words - never the site's. +# `question_lint --needs-tasks` turns each page on their path into a +# typed choice for an answering engine; `--needs-score` grades its +# picks. A misroute means the page said one thing and they heard +# another. The runtime never reads this file. +needs: + - id: commission + who: someone with a problem software could solve, and a budget + wants: UHHM to take the work on + via: Good software begins with a clear argument + lands_in: projects + handled_by: owners + done_when: [accepted, declined] + max_steps: 1 + # name, email, the build, the organization - the page argues for + # each of the four; a fifth would need the same argument. + max_fields: 4 + + - id: follow + who: someone curious who isn't ready to get involved + wants: to hear when something ships + via: Attention is earned + lands_in: subscribers + handled_by: owners + # Being on the list is the finished state; leaving is theirs to do. + done_when: [open, unsubscribed] + max_steps: 1 + max_fields: 2 + + - id: collaborate + who: a developer, designer or researcher + wants: to work with UHHM + via: Work happens through dialogue, not tickets + lands_in: applicants + handled_by: owners + done_when: [active, declined] + max_steps: 1 + max_fields: 3 + + - id: improve-site + who: anyone using this site + wants: the site itself to change + enters: /develop/proposal + # Announced, not open: the automation that turns a proposal into a + # pull request is still being built, so the alternative is disabled. + planned: true + lands_in: development_proposals + handled_by: owners + done_when: [approved, rejected] + max_steps: 1 + max_fields: 3 + + - id: track-prospect + who: an owner who met an organization worth following up + wants: the relationship written down and walked to an outcome + enters: /review + enters_as: owners + via: Record a prospect + lands_in: organizations + handled_by: owners + done_when: [client, past_client] + max_steps: 1 + max_fields: 2 + +personas: + - id: architecture-firm + need: commission + text: > + We run a small architecture firm and our project files are + scattered across three cloud drives. We want to pay someone to + set up proper storage and backups we control ourselves. + + - id: climbing-gym-founder + need: commission + text: > + I have an idea for a booking app for climbing gyms and I'm + looking for a team that can build the first version with me. + + - id: municipality-buyer + need: commission + text: > + Our municipality needs an internal tool for tracking building + permits. We are looking for a vendor who can scope it and + deliver it. + + - id: exhibition-visitor + need: follow + text: > + I saw your drawing network at an exhibition and liked it. I don't + want to get involved, I'd just like to know when there is news. + + - id: tech-journalist + need: follow + text: > + I write about Nordic technology. Keep me posted on what you + release so I can cover it. + + - id: rust-developer + need: collaborate + text: > + I'm a Rust developer between jobs and I like that you build in + the open. I would love to contribute, and maybe join you. + + - id: interaction-designer + need: collaborate + text: > + I'm an interaction designer. I want to work with you on the + drawing interface. + + - id: student-intern + need: collaborate + text: > + I'm a computer science student looking for a summer internship + doing infrastructure work. + + - id: site-critic + need: improve-site + text: > + Your contact form never says how long a reply takes. You should + add that. diff --git a/skill/SKILL.md b/skill/SKILL.md new file mode 100644 index 0000000..927d871 --- /dev/null +++ b/skill/SKILL.md @@ -0,0 +1,136 @@ +--- +name: portal-trainer +description: Onboard a business onto portal - interview them, write their needs.yaml, then write the content repo (pages, aggregates.yaml, desks) until question_lint proves every need is met and an answering engine routes the personas right. Use when someone wants a new portal site, a new flow on an existing one, or asks why visitors pick the wrong alternative. +--- + +# Portal trainer + +You are onboarding a business onto [portal](https://project.uhhm.no/uhhm/portal): +a site that is one question per page, a few alternatives per question, +each alternative optionally a form whose answer is recorded into a +bucket with a state machine, worked from a group-gated desk. + +You have a verifier. Use it instead of your judgment wherever it can +speak: `question_lint --path /questions` exits non-zero until +the structure meets every need in `/needs.yaml`. + +## The order of work + +1. **Interview, then write `needs.yaml` first.** Never pages first. + Ask who comes to the business and what each of them wants, who in + the business handles it, and what "handled" means - the states a + case ends in. Ask what it costs the visitor to ask: how many + submissions, how many required fields they will tolerate. If the + business is in a sector under `sectors/`, start from that file and + cut it down with them; do not start blank. +2. **Write personas in the visitor's words, not the business's.** Two + or three per need, including one who would not use the trade's own + vocabulary. Never reuse a phrase you intend to put on a page - a + persona that quotes the page tests nothing. +3. **Run the lint. It fails. Read every line.** Each failure names + what is missing: no path, a closed (`disabled`) path, a field budget + blown, no desk held by the handling group, a finished state no desk + move reaches, a state records strand in. +4. **Write the least content that turns it green**: `aggregates.yaml` + (one aggregate per bucket, states the business named, only the + transitions they described), the visitor pages, one followup page + per recording alternative saying what happens next, and one desk + page per handling group with exactly the moves the state machine + allows. Mark a need `planned: true` only when the business says + "later" - it keeps warning. +5. **Then word it.** `question_lint ... --needs-tasks tasks.jsonl`, + and again with `--skim` (headings only - most visitors read nothing + else). Give the tasks to an answering engine that never sees + `expect`; grade with `--needs-score answers.jsonl`. + - A **misroute** is a wording defect. Fix the heading before the + description: a heading that needs its description has failed. + - Two alternatives a persona confuses are usually one distinction + the business makes and the visitor does not. Name the visitor's + side of it. + - **Hesitant** (right, under 0.6) means it works and is still a coin + flip. Same treatment. + - Change the page, never the persona, to make a score go up. +6. **Keep the run.** Copy tasks, answers and the score into + `runs/-/` with a line on which engine answered. + Runs are the record of which wording worked, per sector. + +## What a page is + +Full reference: portal's `README.md` and +`docs/design/filesystem-routes.md`; a live example is +`uhhm/questions`. The file tree under `questions/` is the router +(`index.yaml` is `/`, `_section.yaml` applies `qualifies` to a +directory, nested non-index files are followups). + +```yaml +# questions/index.yaml +name: What brings you here? # the question - always a question +responsible: { name: ..., contact: ... } +alternatives: + - name: # graded alone under --skim + description: + action: /received # where the submission lands them + consequence: [Send] # the button + record_as: leads # bucket - must exist in aggregates.yaml + features: + - name:
+ requirements: + - { name: email, label: Email, type: email } + - { name: problem, label: What's wrong, type: textarea } + - { name: address, label: Address, optional: true } +``` + +```yaml +# 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] +``` + +```yaml +# questions/desk/_section.yaml +qualifies: project_managers +# questions/desk/index.yaml +name: What came in? +alternatives: + - name: Leads + action: /desk + consequence: [Confirm] + features: + - name: "" + resource: + source: { kind: kv, bucket: leads } + requires_group: project_managers + transitions: + - { to: quoted, label: Quote sent } # from defaults to open + - { to: lost, label: Not for us } + - { from: quoted, to: won, label: Won } + - { from: quoted, to: lost, label: Lost } +``` + +## House rules for the copy + +- The page name is a question the visitor could ask themselves. +- Headings say what the visitor gets, in the visitor's words. Lead + with what a thing enables; never define it by what it isn't. +- Every required field must be one the business will actually use to + answer. If they can't say what they do with it, it's optional or + gone. +- A followup page says who reads it, when they answer, and what the + visitor can do meanwhile. + +## Engines + +Any engine that reads a task's `state` and `questions` and returns +`{"id","choice","confidence"}` per line works. Compare at least two +before trusting a score, and re-run the same engine before trusting a +change: one run of eight tasks is an anecdote. See the repo README for +what has been measured about each.