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:
co-authored by
Claude Fable 5.1
commit
7caaa27ba7
@@ -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.
|
||||||
@@ -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}
|
||||||
@@ -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}
|
||||||
@@ -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"]}
|
||||||
@@ -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.
|
||||||
@@ -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
@@ -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.
|
||||||
Reference in New Issue
Block a user