The lift matched cards on a desk by action, which every card there
shares, so a card that left the desk left its words on the next one.
A card with a strong identity - a bucket, a group it invites into, a
page of its own - is matched on that alone, and a desk card's identity
is the bucket its feature lists.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
`about: <need>` on a need puts its form on a page per record of the
other need (/status/<bucket>/[key]), links every row of the public
list to it, carries the record's id in a type: record field, takes a
signed-in person's address without asking, and lets `mails: { open:
owner }` reach whoever sent the record. The vel's plan uses it for
the lending board: notices stand from the moment they are sent, the
board hears, an answer goes straight to the poster, the queue is the
poster's inbox, and the redaktør or the clock takes down what stands.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
Portal 0.5.4 moves an invite whose link ran out to expired and files
what responsible people suggest as cases in portal_edit_suggestions.
Every desk that lists invites gets the move back to open (a fresh link,
mailed), and the desk that invites into the most groups - where portal
seeds the first person - gets the list of suggestions with its moves.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
The page after sending and every public list ended with a Back/Tilbake
alternative. Portal now puts a way down the tree on every page (an
arrow at the left edge and a link at the foot, uhhm/portal PR 1), to
the nearest page below that the visitor may open: the front door after
a public form, the group's desk after an internal one. Sites built from
here need a portal that carries it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
List notes, empty lists and the invite form's guidance are lines no
model is asked about before a page ships; on a Norwegian site they
showed in English until reworded. They now fall back in Norwegian for
no/nb/nn plans, like the mail fallbacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Portal 0.5's content, from the plan: `grants` (an approval that is
membership; refused without a mail sender, since the sign-in link goes
by mail), `deadlines` (portal's `after:`, only along a move the
machine has), and `public_states` with an `empty` line on public lists,
whose words now say which stages they show.
Onboarding is a mail too: a plan that invites or grants gets
`mail.invite` in site.yaml in its own language, and a reword that loses
{link} gets it back. `rate --repo` rates every mail, the invite
included, read as it arrives: LIX, and what happened / what now / who
wrote, each against a gutted twin. The mail words are guided by what
that found on uhhm (name the case by a short required field, sign with
the business, invite a reply rather than promise a reason).
The front door shows public lists instead of pointing at them: each
public bucket is a feature with its newest three records inline and a
link to the whole list. Its card is named for what it shows. The plan
reference gains 'What a portal site can do': the schema's options and
when each pays off, as guidance rather than rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
portal compiles one access rule per state a self_transition names, and
without them assumed the day the case arrived. The builder already
knew the answer - `moves_of(need, SUBMITTER)` carries the from-state
of every move - and now writes it down, so the applicant answering a
request for papers from `incomplete` gets a rule for it, and withdrawal
is offered from wherever the case has got to.
Measured on the building authority: the submitter had two rules, both
out of `open`. It now has ten, across applications and appeals.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A municipal building authority is the first sector that the line a
case walks could not describe. An application is returned for the
drawing it was missing and walks the same stretch again; an appeal
ends the moment the authority changes its own decision, without ever
reaching the board; an applicant may withdraw from anywhere, while a
refusal may only come once the papers are in.
`reached_from: { <stage>: [<stage>, ...] }` says where a stage is
entered from when the line does not. A stage named there is entered
from exactly those stages and nowhere else, so saying where a refusal
may come from also says where it may not - which `turned_down_from`
could not, being one list for every bad ending at once.
Found by replaying fifteen real building cases through portal's own
engine against a throwaway JetStream: it refused three legal paths and
accepted two illegal ones. Portal needed no change - hand-writing the
correct graph into aggregates.yaml, cycle and all, was accepted and
enforced exactly. The rebuilt site now produces that same graph from
the plan, and the access policy follows it.
Also: a public view is of a bucket, not of a need. A permit register
holds the owner's small application and the approved firm's large one
and is one page either way; the rule refused to publish a bucket the
moment any role filed into it. A bucket only a role ever writes to is
still refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`type: location` on a field is portal 0.4.0's map pin, stored as
lat/lon/zoom. The reference tells the model it needs `optional: true`,
since a map needs a browser that can draw one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`requires:` on a need is one sentence naming what the person must
have or be for the alternative to work - a membership, a reference
number, the link from a mail. It renders as the card's first feature,
above the road, who-reads-it and the form, because a condition a
reader meets only after filling the form in has already cost them the
filling in. Whether the section exists is read from the plan, never
from its wording: under a lift every slot holds a token and a token is
never empty, which would conjure a section the live page lacks and
shift every feature after it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five features were emitted empty, as layout: a door's strip, a desk's
list, an invited list, a public list, an invite form. Each now has a
title and a line that says what a row is and what the buttons under it
do, or what the list leaves out - true from the plan before a model
writes a word, and a slot the model can improve. Desk cards, their
links and the desk page itself get real fallbacks too. The lift no
longer copies an empty live string over a fallback, which would have
emptied every new slot on the next rebuild.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A stray placeholder or a lost link was only fixed when a model wrote
the mail, so a words file that already carried one kept it through
every later build. The pass now runs once on the finished
aggregates.yaml words, covering --words, --words-from and the model
alike; the football club's three mails lose the `{chain}` they were
shipped with.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The mail prompt said "short"; it now says how short. The football
club's enrolment mails halved in length and read easier, and the
vel's evened out, with every link kept.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Re-planning the contractor showed three wasted rounds: `moves_by: {
checking: submitter }` written as a bare word, a group without a
label, and the two featured owner roles left with nothing to play,
which the builder only warned about after the fact. The bare word and
the missing label now parse (a label defaults to the id), and a plan
that parses with spectator roles is sent back once with the finding;
on the contractor, round three fixed both.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A need may say which unfinished stages a case can still be turned
down from; the machine no longer offers the bad end from every stage.
uhhm's collaborators are declined before they start and depart after,
as the live site has it. The uhhm plan gets stages_text and three
desk buttons reworded: every uhhm desk scores full marks.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`stages_text` on a need gives each stage plain words in the plan's
language. The fallback mails, the road line and the desk buttons use
them, and needs.yaml carries them as `stage_words` so the desk score
describes an outcome the way the site would. The vel's plan has them;
its desks score 26 of 26 where the id-based outcome scored 24.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A model asked for Norwegian sometimes answers in English. The commonest
function words of each language are counted across a reply, and one
that reads as the other language is refused with a line saying so.
Tomter Vel's six mails, worded by Sonnet in Norwegian, are round 2 of
runs/tomtervel-opener-2026-09-23 and live.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sonnet wrote the ten uhhm mails and replaced the decide link with a
bare {chain}. keep_links puts the fallback's link line back into any
mail that lost it. The mails read easier than the fallbacks (LIX 9 to
33 against 18 to 28) and say something.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The words model can be pointed at a subset of pages (`aggregates.yaml`
for the mails alone) while everything else keeps the words it has.
`rate --repo` rates each mail's prose on its own, LIX and length,
since a person reads a mail with no card around it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The fallback mail phrases move into a `Phrases` table, the one place a
language other than English appears in this code; the `navn` special
case goes (a form names the person's field `name`). Test fixtures
speak English: traffic, board, hazards, report, fixed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`--words-from` matched page by page, so flattening Tomter Vel lost
every heading its door pages carried. Now a second pass pools every
alternative in the repo and hands a rendered card the pooled card
with the same strong identity (bucket, own page, invite group; never
a bare action like `/`). Two things stood in the way and are fixed:
needs.yaml is no longer a source of words (its `via` named the door),
and a live feature's `name` no longer blocks a positional match, so
placeholders and guidance lines come along too. Rebuilding the vel
flat from the live repo now lifts all ten headings and every hint.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`mails: { <stage>: submitter | <address> }` on a need becomes `mail:`
on that state in aggregates.yaml (portal 0.3.43+), and `mail: { from,
reply_to }` on the plan becomes site.yaml's sender. The words map
gains `aggregates.yaml`, one subject and body per mailing stage, so
the model writes them like any other page; the fallback, in the
plan's language, says what happened and where the submitter's own
move is. A submitter mail needs a required email on the form; any
mail needs a sender. The uhhm and Tomter Vel plans tell the submitter
at every stage that matters to them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sequences were walked side by side, so a rebuilt page whose cards sit
in a different order, or with a new card in front, gave every card
its neighbour's words: uhhm.no's front door got "Attention is earned"
on the wrong card that way. Items now match on the bucket they record
into, the page they lead to, a field's name, the group they invite
into or the bucket they read; information features with no identity
still match by position. Lifting live uhhm.no into the current plan
puts each of its three headings on the card with its bucket.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two things the uhhm rounds ran into. Only the first --words was read,
so a round of hand changes had to be merged into the previous words
by hand; now each file layers over the last. And the desk's one
confirm slot was described like any button, so a model wrote "Send
invite" under the cases list; it now says it serves every list.
README: the lift is positional, and where that bit.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`does:` on a plan, at most two features, each `for:` a need: the first
card of the front door says what the business does, one role each,
headed by what that role gets, and asks for nothing. `shows:` is the
second card, portal resource blocks passed through verbatim plus every
public view. Only then the offers, as more alternatives on the same
page; the front-door warning moves to seven and stops recommending
doors. `Plan::spectators()` warns when a featured need gives the
submitter neither a move nor a view. uhhm.no rounds 4 and 5 (flat,
at least as good per persona) and Tomter Vel flat (worse: ten offers
on one page, the close pairs collide) are in runs/.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
`events:` on a need overrides the event a stage is entered by. Portal
replays records by event name, so rebuilding a site that already holds
records has to keep what its aggregates.yaml says, or the desks empty.
`attended_by:` writes portal's annotation for a bucket an automation
moves. plans/uhhm.yaml: uhhm.no from a plan - the five live buckets
with their names, plus trials (the trainer tried on a client's
business, approved by hand) and custody_requests (UHHM as redoal's
custodian, a desk of its own).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`invites_by` on a plan (and `invited_by` on a group) put an invite form
per fillable group on the inviting group's desk, plus a list of the
invites sent - portal's `invite:` does the onboarding. Every desk
group invites into itself by default; an inviter without a desk is
refused.
`--words-from <repo>` lifts the words an existing repo carries back
into the words map by structure: the plan is rendered with a token in
every slot and walked alongside the repo's files. Hand edits and
model-chosen icons survive a rebuild; only new slots get fallbacks,
which `--words <json>` can override. The words as used are written to
words.json. First use: Tomter Vel, additions only.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
Work from another session, committed as found (tests green):
- `plan` reads an intake into a plan.yaml (a model decides needs,
stages, forms and budgets; a person reads the result before build).
- Plans gain `doors` (a first question before the forms, when a front
door would carry more than five needs), `groups` with labels that a
select may draw its options from (`options_from: groups`), `private`
fields kept off public pages, `multiple` selects, and `max_fields`
as what the intake said the person will tolerate.
- `card.rs`: an alternative as the visitor meets it, in text, so the
answering visitor and `rate` read the served card, not the heading.
- `rate` reads followup pages from the repo and reports "what happens
next" as not measured rather than zero when it cannot.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
plan.yaml: `moves_by` says who presses the button into a stage when it
is not `handled_by` - other groups, several groups, or `submitter`,
the person who sent the form, through their own link (the form must
then collect a required email). `watched_by` puts a bucket on a
group's desk with no buttons. Each group's desk carries its own buttons
only; a submitter's moves become a followup page of self_transitions;
needs.yaml gains `also_moved_by` for portal's verifier.
simulate: a route is now a relay across every role that holds a legal
button, the submitter included, and each step is judged as the role
that presses it. `--dry` asks no model anything and walks every simple
route of every aggregate, for needs_replay to put through the real
engine.
plans/contractor-full.yaml: 13 needs, 6 roles, 10 aggregates. Verifier
green by construction; 40 routes, 78 transitions accepted by the real
engine, none refused. What it exposed is in its run README.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
Fallback words read like finished copy, and nemotron handed the front
door straight back. The prompt now shows every string as a slot saying
what belongs there; a slot echoed back counts as no answer and keeps
the fallback.
runs/contractor-build-2026-09-21: nemotron-3.5-lightning:30b-a3b, the
model that produced no loadable site free-form, words a complete
contractor site in 130 s. Verifier green by construction; held-out
personas route 8/8 with descriptions and 8/8 on headings alone
(qwen3:8b as the visitor). "What happens next" is still 0/2 on the
front door: no heading or description says who reads a submission.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
A plan.yaml is a needs spec that also says what each form collects
(and which stages a case may pass over). `portal-trainer build`
derives everything structural from it with no model involved:
needs.yaml, aggregates.yaml with the state machine read off `stages`
(walk the line, be turned down anywhere, succeed only from the end),
the front door, a followup page per need, a gated directory per group
holding its desk and the pages only that group may open, and every
desk button the machine allows.
The model is asked for words only, one page at a time, as a JSON object
whose shape is fixed by built-in fallbacks. Its strings are merged over
the fallbacks key by key, so a missing, malformed or partial reply
costs wording, never a working site, and it can neither add nor drop
structure. `via` in needs.yaml is filled mechanically from the heading
the words gave.
plans/contractor.yaml builds with no model into a site portal's
question_lint accepts: 6 needs met, 12 pages, 5 aggregates.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH