--- name: portal-trainer 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 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, with a mail to the person when their case moves. You have a verifier. Use it instead of your judgment wherever it can 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 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`; 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). ```yaml # questions/index.yaml 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 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 } ``` ```yaml # aggregates.yaml aggregates: - bucket: leads initial: open states: open: { event: received, mail: { to: submitter, subject: "We have it", body: "..." } } quoted: { event: quoted } 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 # 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 - { from: quoted, to: won, label: Won } ``` ## 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 use to answer. - A followup page says who reads it, when they answer, and what the 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` (`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.