`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>
`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
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