Work from another session, committed as found (tests green): - `plan` reads an intake into a plan.yaml (a model decides needs, stages, forms and budgets; a person reads the result before build). - Plans gain `doors` (a first question before the forms, when a front door would carry more than five needs), `groups` with labels that a select may draw its options from (`options_from: groups`), `private` fields kept off public pages, `multiple` selects, and `max_fields` as what the intake said the person will tolerate. - `card.rs`: an alternative as the visitor meets it, in text, so the answering visitor and `rate` read the served card, not the heading. - `rate` reads followup pages from the repo and reports "what happens next" as not measured rather than zero when it cannot. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
6.3 KiB
name, description
| name | description |
|---|---|
| portal-trainer | 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: 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
The short path, when a business has filled in an intake: plan
reads it into a plan.yaml (a model decides needs, stages, forms and
budgets; a person reads the result), build turns the plan into every
file of the site with no model deciding anything, and a model writes
only the words. See the repo README. The steps below are the same
procedure done by hand, and what to do when the numbers come back.
- Interview, then write
needs.yamlfirst. 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. That number ismax_fields, and it comes from the person, never from counting the form you would like to write. If the business is in a sector undersectors/, start from that file and cut it down with them; do not start blank. - 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.
- 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. - 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 needplanned: trueonly when the business says "later" - it keeps warning. - 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 seesexpect; 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.
- 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).
# 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 }
# 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]
# 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.