diff --git a/README.md b/README.md index 902da37..960e263 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ produced is content, and every line of it is held to a verifier. ``` portal-trainer build --plan plans/contractor.yaml --out out/nordvik \ - --lint question_lint --llm "MODEL=nemotron-3.5-lightning:30b-a3b engines/ollama.sh" + --lint iris --llm "MODEL=sonnet engines/claude.sh" ``` A `plan.yaml` is a needs spec that also says what each form collects @@ -30,7 +30,10 @@ result by construction; what it still checks is the budget. The model writes words only, one page at a time, into a JSON object whose shape is fixed. Its strings are merged over built-in fallbacks -key by key, so a bad reply costs wording, never a working site. +key by key, so a bad reply costs wording, never a working site. `--llm-for ` points the model at some pages only +(`aggregates.yaml` for the mails alone); a reply in the wrong language +for the plan is refused, and a mail that lost the submitter's link +gets it back. ### Onboarding from the plan @@ -195,7 +198,7 @@ against the lint. It is kept for the record of what it measured. ▼ phase 1 frozen after this phase: the generator needs.yaml ─────────────────────── may not weaken the spec to pass │ - ▼ phase 2 ◄── question_lint findings, until it exits 0 + ▼ phase 2 ◄── iris findings, until it exits 0 aggregates.yaml + questions/** + desks │ ▼ phase 3 ◄── misroutes from a blind visitor model @@ -208,7 +211,7 @@ against the lint. It is kept for the record of what it measured. handles it, the states that count as finished, and the effort the visitor will tolerate. 2. **Structure.** Pages, state machines and desks are generated, then - revised against portal's `question_lint` until every need is met: + revised against the lint, `iris check`, until every need is met: a path within budget, a desk held by the right group, every finished state reachable, no record stranding. The page-writing model sees the needs and never the personas. @@ -222,7 +225,7 @@ and `questions/**.yaml` inside the output directory. ``` portal-trainer generate --intake intakes/contractor.yaml --out out/nordvik \ - --lint /srv/app//current/question_lint --llm engines/ollama.sh + --lint iris --llm engines/ollama.sh portal-trainer answer --tasks tasks.jsonl --llm engines/ollama.sh > answers.jsonl ``` @@ -232,7 +235,7 @@ sees another visitor's situation, a heading's place on the page cannot carry the answer, and a rerun on the same site is the same experiment. The earlier runs under `runs/` answered all tasks in one prompt. -The verifier is portal's `question_lint`, not code in this repo: it +The verifier is uhhm/iris, not code in this repo: it reads portal's own content types and ships in every portal release, so a generated repo's CI keeps enforcing its needs after handover. diff --git a/skill/SKILL.md b/skill/SKILL.md index 4e9feca..c411029 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -1,6 +1,6 @@ --- 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. +description: Onboard a business onto portal - interview them, write a plan, build the content repo (pages, aggregates.yaml, desks, mails) with the words written by a model, and score it until iris proves every need is met and a blind visitor routes the personas right. Use when someone wants a new portal site, a new flow on an existing one, a rebuild of a live one, or asks why visitors pick the wrong alternative. --- # Portal trainer @@ -8,66 +8,64 @@ description: Onboard a business onto portal - interview them, write their needs. 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. +bucket with a state machine, worked from a group-gated desk, with a +mail to the person when their case moves. 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`. +speak: `iris check --path /questions` (uhhm/iris, the release +matching the portal the site runs) exits non-zero until the structure +meets every need in `/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. - -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. That - number is `max_fields`, and it comes from the person, never from - counting the form you would like to write. 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. +1. **Interview, then write the plan.** Never pages first. `plan` + reads a filled-in intake into `plan.yaml` (a model decides needs, + stages, forms and budgets; a person reads the result before + anything is built). By hand, the same questions: who comes to the + business and what each of them wants, who inside handles it, what + "handled" means (the states a case ends in), what it costs the + visitor to ask (`max_fields`, from the person, never from counting + the form you would like). Start from a `sectors/` file when there + is one. +2. **Say what the plan can say.** `does:` (one or two features, one + role each, the front door's opener that asks for nothing); + `shows:` (things to look at before asking); `doors:` only when the + offers are more than a page holds - measure it, a flat page with + ten offers scored worse than four doors; `mails:` per need (who + hears when a case enters a stage) with `mail: { from }` on the + plan; `moves_by: submitter` so the person has something to play + after sending; `invites_by` for onboarding; `events:` when + rebuilding a live site, so its records keep replaying. +3. **Write personas in the visitor's words, not the business's.** Two + or three per need, one who would not use the trade's vocabulary. + Never reuse a phrase you intend to put on a page. They are held + out from every model that writes words. +4. **Build.** `build --plan ... --lint iris` derives every file with + no model deciding structure; `--llm` lets a model write the words, + `--llm-for ` only some of them (the mails alone); + `--words-from ` keeps a live site's hand-edited words + through a rebuild, following each card across pages; `--words + ` overrides slots. The builder refuses a reply in the wrong + language and puts back a submitter's link a model dropped. +5. **Score.** `iris check --needs-tasks` then `answer`, then + `--needs-score`, with and without descriptions; `rate --repo` for + readability and the four understanding questions, mails included. + A card with no button is context, never a choice. Trust the + descriptions score; the headings-only score swings by a third + between seeds. Before believing a change, `runs/spread.sh` it over + three seeds and require it to beat the spread. A change that does + not is rejected and kept as a run. + - A **misroute** is a wording defect until the same one survives + every rewording; then it is a seam in the plan. + - Change the page or the plan, never the persona. +6. **Keep the run.** `runs/-/` with the words as used + (`words.json`), tasks, answers, scores per seed, and a README with + what moved and why. Runs are the record of what worked. ## 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 +Full reference: portal's `README.md`; a live example is +`uhhm/questions`. The tree under `questions/` is the router (`index.yaml` is `/`, `_section.yaml` applies `qualifies` to a directory, nested non-index files are followups). @@ -76,6 +74,9 @@ directory, nested non-index files are followups). name: What brings you here? # the question - always a question responsible: { name: ..., contact: ... } alternatives: + - name: What we do # the opener: features, no button + features: + - { name: , description: } - name: # graded alone under --skim description: action: /received # where the submission lands them @@ -86,7 +87,6 @@ alternatives: requirements: - { name: email, label: Email, type: email } - { name: problem, label: What's wrong, type: textarea } - - { name: address, label: Address, optional: true } ``` ```yaml @@ -95,15 +95,20 @@ aggregates: - bucket: leads initial: open states: - open: { event: received } + open: { event: received, mail: { to: submitter, subject: "We have it", body: "..." } } quoted: { event: quoted } - won: { event: won } - lost: { event: lost } + won: { event: won, mail: { to: submitter, subject: "...", body: "..." } } + lost: { event: lost } transitions: open: [quoted, lost] quoted: [won, lost] ``` +```yaml +# site.yaml +mail: { from: "Nordvik ", reply_to: ola@nordvik.example } +``` + ```yaml # questions/desk/_section.yaml qualifies: project_managers @@ -120,26 +125,27 @@ alternatives: 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. +- The front door opens modestly: what the business does for one or + two roles, then what there is to look at, then the offers. No + second tier. - 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. +- Every required field must be one the business will use to answer. - A followup page says who reads it, when they answer, and what the - visitor can do meanwhile. + visitor can do meanwhile. A mail says what happened and, where the + person has a move, the link to make it. +- Code in English; the site in the business's language. ## 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. +Any engine that reads a task's `state` (`body` is the persona, +`page` what the page says before its options) and `questions` and +returns `{"id","choice","confidence"}` per line works. The visitor +and the writer must be different models. See the repo README for what +has been measured about each, and for the noise floor.