The id hashes scope, need, code and subject, nothing of the wording, so the same gap keeps its id across runs. Every message that knows its fix says it after "fix:". Codes listed in docs/lint.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95 lines
4.6 KiB
Markdown
95 lines
4.6 KiB
Markdown
# The lint
|
|
|
|
The iris over a site. A content push is an incoming wormhole; nothing
|
|
comes through until it checks out.
|
|
|
|
```
|
|
portal iris --path questions # a local checkout
|
|
portal lint --repo https://project.uhhm.no/uhhm/questions
|
|
portal lint replay --path questions --nats nats://127.0.0.1:14222
|
|
```
|
|
|
|
`portal iris` is the lint's name, after the iris that keeps bad content
|
|
out of the portal; `portal lint` runs the same thing, so workflows that
|
|
already call it keep working.
|
|
|
|
It was its own binary, iris, until portal 0.6: the same code, released
|
|
as portal's twin. Now it is the `portal` binary's `lint` command, so a
|
|
content repo's CI downloads the release and runs it:
|
|
|
|
```yaml
|
|
- run: |
|
|
curl -sfL "https://project.uhhm.no/uhhm/portal/releases/download/$PORTAL_RELEASE/portal-x86_64-linux" -o portal
|
|
chmod +x portal && ./portal lint --path questions
|
|
```
|
|
|
|
`check` loads a content repo exactly the way the running site does,
|
|
validates it (every action lands on a page, every transition is one
|
|
the state machine allows, every recorded bucket is read back
|
|
somewhere), and then holds it to the business needs the repo declares
|
|
in `needs.yaml`, when it has one:
|
|
|
|
- a path of submissions gets each kind of visitor's answer into the
|
|
bucket it names, within a submission and required-field budget;
|
|
- the handling group holds a desk over that bucket, and every
|
|
finished state is reachable through the buttons it (and any
|
|
`also_moved_by` group) offers; no record strands on the way;
|
|
- every stage the business named is a state of the machine, and each
|
|
working stage can follow the one before it;
|
|
- across each bucket, whichever need brought a record in: every state
|
|
is somewhere a record can get to, no state a record gets to holds it
|
|
with no way on (before done or after), every declared move is a
|
|
button, a visitor's own link or a clock (a move nobody offers warns),
|
|
no desk is gated to one group on the page and another on its list,
|
|
and every mail goes out from a state records reach and names only
|
|
fields the forms collect.
|
|
|
|
Every finding says what to change and where: the missing
|
|
`{ from, to, label }` and the desk page it goes on, the group that
|
|
already has the button, or the edge aggregates.yaml lacks. A schema
|
|
error no longer hides the needs findings - both are printed, so one
|
|
round can fix them together.
|
|
|
|
`--findings <out.jsonl>` writes the same findings one JSON object per
|
|
line: `id`, `level` (`fail`/`warn`), `code`, `scope` (`need`,
|
|
`bucket`, or `repo` for a schema error), `need` (the need's id, or the
|
|
bucket's name), `subject` (the state, the move as `from>to`, or the
|
|
page), `message` and `fix`. The file is written empty when the content
|
|
is clean. `id` hashes scope, need, code and subject and nothing of the
|
|
wording, so the same gap keeps its id from one run to the next: a
|
|
trainer can tell what a round closed, what is still open and what it
|
|
broke. The codes:
|
|
|
|
| code | |
|
|
|---|---|
|
|
| `schema` | content the site would refuse to load |
|
|
| `need-duplicate`, `persona-unknown-need` | needs.yaml itself |
|
|
| `entry-missing`, `entry-gated`, `entry-no-responsible` | the page a visitor arrives on |
|
|
| `path-missing`, `path-closed`, `path-over-budget`, `path-no-action` | getting the answer recorded |
|
|
| `machine-missing`, `desk-missing`, `desk-gated-twice` | who works the bucket |
|
|
| `done-unknown`, `done-unreachable`, `success-not-done` | finishing a case |
|
|
| `stage-unknown`, `stage-unreachable`, `stage-order` | the business's own stages |
|
|
| `state-unreached`, `strand`, `edge-unoffered` | every state and move of the bucket |
|
|
| `mail-unreachable`, `mail-unknown-field` | what a state mails |
|
|
|
|
A need marked `planned` warns instead of failing. Exit status is the
|
|
verdict, so a content repo's CI is one line.
|
|
|
|
Beyond the verdict, `check` exports what a trainer needs to grade the
|
|
wording: `--needs-tasks` writes the personas as typed choice tasks
|
|
(`--skim` for headings only), `--needs-score` grades an engine's
|
|
answers, and `--needs-sim` writes a resolved model of the whole site.
|
|
`portal lint replay` runs simulated cases through the site's real aggregate
|
|
engine on a throwaway JetStream and reports every bucket by state;
|
|
`--report-only` takes the same scorecard from a live site's buckets.
|
|
See [Training a site](training.md) for what sits around those.
|
|
|
|
## Matching the site
|
|
|
|
The lint is the site: `portal lint` and `portal serve` are one binary
|
|
built from one source, so a content shape the site reads is a shape
|
|
the lint knows, at the same version. A content repo pins
|
|
`PORTAL_RELEASE` to what its site runs and lints with that release's
|
|
`portal`. The `IRIS_RELEASE` pins from before portal 0.6 keep working
|
|
against the iris releases that exist; nothing new is published there.
|