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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-21 16:31:37 +02:00
co-authored by Claude Fable 5.1
commit 7caaa27ba7
7 changed files with 568 additions and 0 deletions
+123
View File
@@ -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 <repo>/questions # structure
question_lint --path <repo>/questions --needs-tasks tasks.jsonl # full wording
question_lint --path <repo>/questions --needs-tasks skim.jsonl --skim # headings only
question_lint --path <repo>/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":{"<alternative heading>":"<its description>", "...":"..."}}},
"expect":["<every alternative that still meets the need>"]}
```
Strip `expect` (and `need`) before an engine sees it. An answer is one
line: `{"id": "...", "choice": "<heading, verbatim>", "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.
+8
View File
@@ -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}
+8
View File
@@ -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}
+8
View File
@@ -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"]}
+158
View File
@@ -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.
+127
View File
@@ -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.
+136
View File
@@ -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 <repo>/questions` exits non-zero until
the structure meets every need in `<repo>/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/<business>-<date>/` 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: <heading the visitor picks> # graded alone under --skim
description: <one or two sentences>
action: /received # where the submission lands them
consequence: [Send] # the button
record_as: leads # bucket - must exist in aggregates.yaml
features:
- name: <form section>
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.