Files
portal/skill/SKILL.md
T

146 lines
6.3 KiB
Markdown
Raw Normal View History

---
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
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/<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.