7 Commits
Author SHA1 Message Date
cgalo5758 e5ab7da9d0 Drop the filler phrases from five member-facing sentences
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.
2026-10-11 21:47:51 -05:00
cgalo5758 e44488d8aa Read a form's fields through one step per kind of control
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.
2026-10-11 17:01:12 -05:00
cgalo5758 113c672e3b Resume a marked grant when the plan that replaced it ends, and archive the change
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.
2026-10-01 18:16:40 -05:00
cgalo5758 f8a3478f2a Rebuild the entitlement set Rules surface as a staged batch
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.
2026-09-19 19:46:09 -05:00
cgalo5758 782ca8f326 Derive Stripe mode from API key and refine disabled controls
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.
2026-09-13 16:59:12 -05:00
cgalo5758 88db730fcc Add dual licensing and SPDX headers
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.
2026-09-06 02:29:42 -05:00
cgalo5758 9ff516ba95 Introduce the forms library and migrate all forms
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.
2026-09-05 03:16:45 -05:00