Five sentences a member can read carried a phrase the copy rule
refuses. They now read:
The form could not be read. Try again. (a form body, and a batch
body, that cannot be read)
That name isn't available. Choose another.
... Another can start there after %s. ... (abandonment budget)
... Another can start after %s. (initiation budget)
They were "Please try again." (twice), "Please choose another." and
"You can start another ..." (twice). The lint does not read
internal/forms or internal/domains, so it never saw them; its reach is
unchanged. The sentences in workflow results and FedWiki's sync errors
are untouched.
The two "malformed body" rows of
internal/forms/testdata/parse-answers.golden carry the new sentence;
no other golden changes. The new TestMemberMessageWording pins the
three domain sentences word for word; no test pinned them before. It
failed at the parent and passes at this commit.
How it was shown to change no other output: the names and results of
./internal/forms, ./internal/domains, ./internal/server and
./internal/integrations/fedwiki/web (on freshly reset test databases)
are equal at the parent and at this commit apart from the added test,
which fails at the parent. Five breaks, each restoring one old
sentence, are caught: three by TestMemberMessageWording, two by
TestParseAnswers.
Not probed: that no screen shows these sentences. The new tests read
the strings and the parse golden; no browser run was taken for this
commit.
parseFields keeps its signature and now reads each submitted value
through readRaw, keeps the checkbox branch, and hands a select or radio
group to parseChoice and every other control to parseText. assignTyped
keeps its signature and calls parseNumber and parseDate, each returning
the value or the refusal. A malformed Min or Max is still no bound.
Each declared pattern is compiled once: patterns is a sync.Map keyed by
the pattern text, and patternMatches stores a pattern that does not
compile as nil, which refuses every value as before. That a pattern is
compiled only once is not probed: no answer changes with the cache, so a
break that compiled on every request again would pass every test, and
the cache is checked by reading. The map's size and the first-use race
(two requests may both compile before LoadOrStore keeps one) were not
probed either: only declarations supply a pattern and no production
declaration uses one, so the map stays empty in production, and both
compiled copies of a pattern are equivalent.
The two gocognit exclusions for parseFields (49) and assignTyped (33)
are deleted. Measured with the pinned golangci-lint, min-complexity 1:
parseFields 9, assignTyped 9, parseText 9, parseDate 6, parseChoice 5,
parseNumber 5, patternMatches 4, readRaw 2.
No output changes. Shown by:
- ./internal/forms ./internal/server ./internal/integrations/fedwiki/web
./internal/integrations/discourse/web, app down, test databases reset
before each side: 1756 test and subtest names with identical results
at the parent and here, 0 failed, 0 skipped. No golden file changed.
- The demo-state walkthroughs, each side prepared right before it from a
rebuilt demo snapshot: 16 pass and 6 skip, names, results and skip
messages equal at the parent and here.
- parse.go's string literals: the distinct set is equal apart from the
import path "sync"; the empty string literal occurs 27 times where the
parent has 23 (the new helpers return "" for no refusal).
- 14 break records (11 per side) on parse.go, each CAUGHT by
TestParseAnswers on the old side at the parent and on the new side
here. A malformed Min, an optional radio group and a pattern that
does not compile are one record per side, because the code moved.
- go vet, gofmt, project lint, golangci-lint (0 issues), deadcode, and
the lint snapshot (nothing added or removed) are clean.
An operator can mark a grant to resume. When a plan replaces a marked
grant and that plan later ends (the subscription ends, or a granted plan
is revoked or expires), the grant delivers again from its own row before
the org-type floor runs (core migration 00023, core.grant_waits).
- Grants: a resumes_after_supersession column, off by default and never
on a default grant; delivery states gain On hold, with a badge hover
that names what the grant waits on.
- Operator: one Manage action per grant opens a card under its row
(Note, Valid until, Resumes; Save, Cancel, Revoke). A changed Valid
until extends the grant; otherwise the note and mark change in place.
An extension now keeps its predecessor's valid_from.
- Member: a held rung reads Granted and is reached by cancelling; the
cancel confirmation names each ladder's outcome; Cancel plan sits
beside Keep plan while a downgrade is scheduled.
- Smaller fixes from the screens reviews: 24-hour times everywhere,
search placeholders that name what they match, a checkbox label above
its box, and the org-types walkthrough restoring the default it
changes.
The Rules section is one record table grouped by kind, Limit then
Boolean, on fixed columns, edited in place: Edit opens a row's controls
in their columns, Add rule opens a dense row above the table, and every
change is staged into a tray that lists the deltas with Undo and applies
them as one rule-change act. The reduction policy is a column of the
rule beside its limit. History shows counts only. Group rows are a quiet
heading rather than a divider, the maintainer's pick from four rounds of
outside-model ideation.
Dense rows align to the top and render each error under its control in
every form family (design D16), replacing the below-row error block; the
forms library gains the batch form (rows plus one tray) and the RowField
dense and label-hidden options. Migration 00019 records the governing
reduction policy on effect rows.
Archive staged-rule-changes with its spec updates (entitlement-set-
management, entitlement-set-history, entitlements, form-library,
form-conventions, ui-quality-gate). Screens accepted 2026-09-19.
Derive Stripe test/live mode from the API key prefix at boot, failing on
unrecognized prefixes, and drop the separate `stripe-mode` config key.
Refine disabled controls to render through the shared `disabledControl`
part with the not-allowed cursor, and add a lint rule refusing
hand-rolled disabled buttons.
Adjust plan cards to offer no purchase control on free rungs, fix bound
checkbox Bool handling, and rename "Public/Private" to "Listed/Unlisted"
with enhanced readiness verdicts.
Introduce a commercial license option alongside AGPL-3.0-only, require a
CLA for contributors, and document the terms in COMMERCIAL.md and
NOTICE. Add a script to stamp SPDX headers on Go files and apply it
across the tree.
Every form on both surfaces is now a declared FormSpec in
internal/forms, rendered through the shared form part and parsed
by its handler through the same declaration. Create and edit
share one field list, refusals answer 422 with values carried
back, and layout, buttons and errors come from one part.
Also adds the form registry with its invariant and route-mapping
tests, the raw-form, filler-copy and Go em-dash lint rules,
declared action triggers, and the capture-coverage cross-check.
The forms-library change is archived under
openspec/changes/archive/2026-09-05-forms-library.