35 Commits
Author SHA1 Message Date
cgalo5758 31ef519889 Skip files under testdata directories when stamping license headers
spdx-headers.sh stamped every tracked HTML file under internal/, the
golden HTML files under internal/server/testdata among them, so a bare
`make spdx-headers` would prepend a template comment to each and fail
the tests that compare them byte for byte. It now skips every file
under a testdata directory, since a test reads those as written. The Go
fixtures under testdata that carry a header keep it, because the script
only adds headers.

Shown on two git archive exports made outside the working tree, each
made a repository of its own so the script finds only the export. On
the export of the parent, `make spdx-headers` stamped 98 of 1035
eligible files and left 98 changed paths under internal/server/testdata.
On the export of this commit it stamped 0 of 934 eligible files and
left no changed path. No Go file changes and no Go test runs the script,
so no package tests were run; build, vet, gofmt, lint and the
notebook-citations check pass, and the lint output is unchanged.

Not probed: the script was not run in the working tree, because it
resolves its root through git and would edit tracked files there.
2026-10-11 21:47:51 -05:00
cgalo5758 abb438bc71 Check a form declaration through named rule groups
CheckInvariants ran every rule of a form declaration in one function
that gocognit scored 139. It now keeps its signature and its problem
recorder and calls checkKindAndFamily, checkFamilyScope (which calls
checkRowsScope), checkRoutes and checkWayOut, then the no-fields rule,
then per field the no-name rule, checkFieldOrder, checkFieldStructure
(which calls checkFieldPlacement and checkFieldWidth) and
checkFieldCopy (which calls checkEscapeHatch and checkFieldDashes), and
finally checkFormDashes. Each takes the recorder as addFunc. Every
message and condition is unchanged and the rules run in the order they
did; the two checkbox conditions became one, and the Select-or-Radio
test is one local. The exclusion rule for CheckInvariants is deleted.
Measured scores: CheckInvariants 5, checkFieldStructure 17,
checkFamilyScope 13, checkFieldCopy 13, checkWayOut 8, checkRowsScope 7,
checkRoutes 7, checkFieldDashes 6, checkKindAndFamily 5, checkFieldWidth
5, checkEscapeHatch 4, checkFieldOrder 3, checkFieldPlacement 3,
checkFormDashes 3.

Three things differ from the parent, none visible in a registered form:
- The two dash checks range over ordered slices. Their maps gave a
  field's problems in map order, so the slices' order is one of the
  orders the maps could give, and the registry test's failures now come
  out in a fixed order. Each test is still strings.Contains(value, "—").
- The escape hatch's keys are checked in sorted order. A field with two
  refused keys gave its two problems in map order; they now come out in
  key order. No registered form carries a refused key.
- ownedAttrs and its message "the field %q escape hatch carries %q,
  which the part owns" are deleted. A key reached that branch only after
  the data-/hx- prefix test, and no key of the map has either prefix, so
  no input reached it; the prefix rule already refuses all of them. The
  escape hatch's comment says why the hatch is narrow.

The "steps wider than a half" branch of the width rule stays; no control
reaches it today.

scripts/deadcode-testonly.txt gains 13 entries for the new helpers,
which only CheckInvariants calls and only tests call CheckInvariants.
TestInvariantsEscapeHatchProblemsComeInKeyOrder is new: it checks, 20
times, that a field whose escape hatch carries style and class gets two
problems with the class one first.

No output change. Shown so:
- go test over ./internal/forms, ./internal/server and
  ./internal/integrations/... gives 2144 test names and results at the
  parent, 2145 here; the only difference is the new test. None fail,
  one is skipped on each side. No golden file changed, so the message
  golden, the HTML goldens and the parse goldens read the same text.
- The new test fails at the parent in 3 of 3 runs, because a two-key map
  ranges from the key written first most of the time, and passes here in
  20 runs of 20 iterations each.
- invariants.go's string literals are equal apart from the 21 attribute
  names and the message of the deleted map, and the import paths "maps"
  and "slices".
- 20 break records (16 on the new side, 15 on the old; four dash and
  way-out records are written once per side because their text moved,
  and the sorted-keys record is new side only) are all caught by the
  existing message golden, and the sorted-keys one by the new test.
- go run . lint prints the same line (91 routes, 53 URL refs), the lint
  snapshot gains and loses nothing, and scripts/deadcode.sh is clean.

Not probed: the 46 messages' words beyond what the equal literal set
shows; the deleted branch, which no input reached.
2026-10-11 17:01:12 -05:00
cgalo5758 d46a79db17 Delete LatestGenerations and its query
LatestGenerations and the LatestLifecycleGenerations query had no
production caller: handlers read NextGenerations. Deleted with them: the
query's generated method and row type (sqlc regenerated), the error text
"latest lifecycle generations: %w", the struct tag
`json:"latest_generation"` of the deleted row type (nothing read it),
the deadcode list line for LatestGenerations, and the two assertions
that used them, in the older-tab test and in the finish subtest of the
lifecycle request query test. The NextGenerations check stays.
sqlc-unused now counts 449 queries, one fewer, with none unused.

Two assertions are NARROWER because their subject is deleted, and no
fact is left unprobed beyond them: "LatestGenerations reports the
highest generation and omits an instance with no request" and
"LatestLifecycleGenerations names the later request's generation".

No output changed. The deleted function was called by no code outside
tests and took no lock, so no statement of any caller changes. The
tests of ./internal/integration and ./internal/integrations/fedwiki/...
give the same 302 names and results at the parent and here, each side
run after a reset of the test databases. The kept break (the next
generation no longer rises above the latest) fails the older-tab test
at the parent and here. The lint snapshot differs by no key, and
golangci-lint reports 0 issues.
2026-10-11 13:16:10 -05:00
cgalo5758 9e1954bb92 Keep in the lifecycle answers test what the answer golden does not hold
TestRecordLifecycleRequestAnswers_DB drops the assertions that
TestLifecycleAnswers_DB now holds with whole answers and whole rows, on
a fresh instance with its priors: the new request, the repeat while open
with reordered keys, the three conflicts and the stored domain, another
workspace's address, the busy answer, and the repeat after the finish.
It keeps the instance identifier being a UUID, the two lookups, the
finish matrix as a table, and the check that no desired-state outbox row
is written. Its cognitive complexity goes from 31 to 10, and its
allowance leaves the lint configuration.

LifecycleAnswer.Accepted is deleted: its only callers were five of the
dropped checks, each on a line whose kind comparison already decides the
answer. The five checks are narrower than the kind comparison they sat
on: the new request, the repeat while open, the conflict check (one
line, run over other parameters, another operation and another person),
the busy answer, and the repeat after the finish.
LifecycleAnswerKind.String moves, body unchanged, to the package's test
exports, where the answer golden and the failure messages print kinds
through it. Neither is listed as test-only code any more. Tests that
print an answer with %+v in failure messages only (the FedWiki package's
tests in settle_db_test.go, reconcile_domains_db_test.go and
usage_repair_db_test.go, and its web and workflows tests) print the
kind's number there now, because String is absent from their test
binaries. No result changes.

Shown to change no output: the production change deletes two methods and
moves one into test code; the answer golden is byte-equal; the test and
subtest names and results of the integration package and of the FedWiki
packages equal the parent's (299 results each side); all 12 listed
breaks of lifecycle.go and its queries fail the old test at the parent
and the new one at this commit; the lock key, the transaction wrappers
and the content comparison are untouched. The lint snapshot adds and
removes nothing. Unprobed: the five narrower Accepted checks above,
because their method is deleted; the instance being a UUID (the column
is uuid, so no one-line edit returns anything else), no outbox row
written (no one-line edit makes recording write one), and accepted not
being an outcome (two layers refuse it).
2026-10-11 12:38:35 -05:00
cgalo5758 208e10dd9d Replay recorded claim verification and push histories through one replay kit
Add internal/testkit/replay: Run and File replay a package's recorded
Temporal histories against the workflows it registers, and Record writes
them with stand-in activities. TEMPORAL_REPLAY_RECORD names a temporal CLI
binary (the recorder starts a private in-memory dev server from it) or a
running server's host:port; TEMPORAL_REPLAY_ONLY, a file-name prefix,
records the files it names again. FedWiki's replay test moves onto the kit
with its fourteen histories and every subtest name unchanged. Its two
variables FEDWIKI_REPLAY_RECORD and FEDWIKI_REPLAY_ONLY are replaced by
the two above, and its recorder skip message now reads
"TEMPORAL_REPLAY_RECORD not set".

The recorder now writes only the histories that do not exist yet, where
FedWiki's used to rewrite every file; a recorded history stands for runs a
release started, so replacing one takes TEMPORAL_REPLAY_ONLY.

Claim verification gets nine recorded histories (activates, polls,
check-now, expires, probe-error, mark-refused, record-fails, canceled,
rollover) and the push workflow seven (applied, rose, divergent,
conflict-then-applied, permanent, unlinked, gone), each replayed by
TestWorkflowHistoriesReplay in internal/workflows/domains and
internal/workflows/desiredstate. The kit has its own test over a two-step
workflow and a recorded history of it. The 17 histories were recorded at
the parent's code, from a private dev server started from the temporal CLI
(1.5.1); the commands they hold match an earlier independent recording
of the same scenarios, apart from the two clamped timers' durations. No
file names the recording machine or its user.

New tests: the kit's TestTarget, TestRecordsNeverReplaceAHistoryUnasked,
TestRunReplaysARecordedHistory (and its two-steps.json subtest),
TestFileRefusesAChangedWorkflow, TestEndingNamesWhatTheRunDid and
TestRecordTwoSteps (skips); the import guard's subtest for
internal/testkit/replay; TestWorkflowHistoriesReplay with its 9 and 7
subtests and TestRecordReplayHistories (skips) in the two workflow
packages. The kit is listed in docs/testing.md and in the test-only
deadcode list.

No output changes: no production file is edited. At the parent and at this
commit, the names and results of the section's packages are equal apart
from the tests above (FedWiki's 16 results pass or skip as before), the
Temporal surface listings (activities, workflows, workflow bodies, payload
types) are byte-equal, go run . lint prints the same line, the lint
snapshot has no added or removed key, and deadcode and golangci-lint are
clean.

Breaks: 9 breaks, each caught by the new tests; the 2 FedWiki breaks
(a request-driven create and a create started before lifecycle requests
that check the quota first) are caught by the old and the new test.

Not probed: Histories and Run failing a test (no test can watch its own
test fail; every production break above shows Run failing a subtest);
connect, runScenario and write against a server, which run only when
recording (the recording at the parent is their check). A replay cannot
see activity inputs, timer durations, the result, or the ending of a
history that ends canceled or failed; later tests pin those.
2026-10-11 11:50:42 -05:00
cgalo5758 11c9911667 Move the one-rule commit and the one-pool drain into the package's test exports
RuleChangeCommit and DrainObligation have no caller outside tests: the
console commits batches through RuleChangeBatchCommit and drains through
DrainChange. Both move, bodies unchanged, from rule_change.go to
export_test.go, where each comment says which tests call it, and their two
lines leave the test-only deadcode list.

One test outside the package called DrainObligation. In
TestSettleObligationOnSettledObligationWritesNothing the redispatch is now
DrainChange over the change; it asserts the pass settles, fails and skips
nothing and that the pool's effect rows are still what they were. This
assertion is NARROWER: the old one checked that a drain of that one pool
returned Skipped, a branch DrainChange's pending-only work list never reaches
from outside the package. That branch stays pinned inside the package by
TestRuleChangeCommitRecordsEffects. A comment in
TestDeferredCommitHoldsNoPoolLock named RuleChangeCommit, which that package
can no longer call; the sentence is dropped (docs correction riding along). The
scenario comment of TestSettleObligationOnSettledObligationWritesNothing said a
redispatched drain skips the pool; it now says the drain lists no settled
obligation and does no work, which is what the rewritten assertion requires.
The comment of DrainObligation names both tests that call it.

No output changes. The names and results of internal/entitlements and
internal/workflows/entitlements are equal at the parent and at this commit
after a database reset on each side: 241 pass, 1 skipped, none failing. No
golden changed. The lint snapshot differs from the parent's by no added
finding and no removed one; the deadcode check passes with the two lines
gone.

Breaks: 3 records, each caught on the side it runs: the Skipped branch of a
drain is caught by the old test outside the package and by the test inside it
after the move (two records, one per side); the pending-only work list is
caught by the new redispatch assertion alone, since the old Skipped assertion
would pass with it dropped.

Unprobed: the narrowing named above, because the Skipped branch is reachable
only from inside the package. What the batch commit left unprobed is still
unprobed, because the one-rule commit is still its one-element wrapper: the
missing rendezvous kept in poolRefusal; the deactivation mismatch, the lock
wait at the act and the lock wait of the pool listing; and the request id's
value, since only its distinctness is tested.
2026-10-10 23:42:22 -05:00
cgalo5758 8cee42f2d3 Compare golden files and name a run's ids through one kit package
The server's golden helpers (the comparer, the -update flag and the
normaliser that turns a fixture's ids into names) move into
internal/testkit/golden, so any package's tests can compare output with a
file under testdata. Ordinals is new: it numbers the UUIDs and UTC times
an act makes by first appearance, for the ids a fixture cannot name.
Normalizer.Add replaces the one place a server test appended to the
normaliser's unexported pairs.

Eleven server test files use the package; the -update flag keeps its
name. docs/testing.md gains the kit row and a bullet, and the kit's import
guard and the dead-code list know the package.

No output changes. No production file changes, and no file under
internal/server/testdata is touched; every server golden test passes
without -update and git status is clean under testdata afterwards. The
names and results of ./internal/testkit/... and ./internal/server are
equal at the parent and at this commit (1452 and 1458 tests and
subtests), apart from the five new tests and the import guard's new
subtest for the package. No assertion changes, so no assertion map.

13 breaks: 12 caught by the new tests, and the one that renames the
normaliser's <today> pair is caught by the golden test at the parent and
at this commit.

Unprobed: that a name outranks a pair in the server's goldens (their ids
are UUIDs and their pairs a date and the suffix, which never start at one
position, so the order is invisible there; the kit test probes it); the
server copy of the comparer at the parent (a break that makes it pass
everything cannot fail the goldens it compares); a migration test that
defines its own update flag (a documented rule, not code).
2026-10-10 14:50:48 -05:00
cgalo5758 eedeb54bb5 Name the organization page's rows through the page's resolver
The organization page named each row with its own read: a product per
delivery, a ladder and a tier per rung, an actor, a ladder and a tier
list per tier change, an actor per entitlement change, and a ladder list
per product in the Issue grant advisory. The page now makes one resolver
after the organization read and every section names its rows through it,
so each kind of name is one statement for the page (#10): the panel reads
every pool's positions first and wants their products, ladders and
tiers; Tier changes fold and page the rows before naming them, so only
the window's rows are named; Entitlement changes want the page's actors;
the advisory reads the tiers of the held ladders in one statement
(productsOnLadders) and flags the products on them. The grants ledger
takes the page's resolver in place of its own. The owner is still one
GetPersonByID read, as before. GetTier is deleted with its last caller,
and the resolver functions that now have a production caller leave the
test-only list.

One current-shape rule now names tiers here: a dash for a row with no
rank, the product at that rank on the ladder as it is now, or "rank N"
when no tier holds the rank. It replaces the page's own closure and its
two caches.

Accepted difference, named: a failed batch read now fails every id of its
kind together (every delivery's product name, every ladder name, every
rank, every actor), where one id could fail while the others resolved.
Only an injected failure reaches it. The failure table's row for a failed
product ladder read is NARROWER for it: the advisory's read is one batch
over the held ladders, so the partial failure has no row.

How it was shown to change no output: every golden under
testdata/org_enrollment is byte-identical, and the tests of
internal/server and internal/entitlements give the same 1588 names and
results at the parent and here (1587 pass, 1 skip). The organization
page's read-count row goes from 8 per row to 0. The failure table keeps
its names and moves the method each row fails: actors to
ListPersonNamesByIDs, products to ListProductNamesByIDs, ladders to
ListPlanLadderNamesByIDs, and rank, tier labels and the product ladder
row to ListTiersWithProductsByLadderIDs; the owner row still fails
GetPersonByID. The operator walkthroughs give 22 equal names, results and
skips at the parent and here. loadOrgEnrollmentData is re-pinned at 63,
the score golangci-lint printed with a placeholder pin.

Breaks: 76 records, 49 names, 51 run per side (26 on both sides, 25
old-only, 25 new-only); all 51 CAUGHT at the parent and all 51 CAUGHT
here.

Unprobed: which ids each section wants (only the page's rows; a wider
want changes the batch's id list, not the statement count or the output);
the sorted order of the batch's ids; the guard that skips the advisory's
read when no ladder is held (an empty batch answers no rows, the same
flags); the removed nil check on the identity querier for entitlement
change actors (every test handler has one).
2026-10-10 03:38:20 -05:00
cgalo5758 a638f1145a Name grants through the request's name resolver
The grants page and the organization page's ledger drew each row with
reads of their own: the organization, the product, the granting person
and the floor product, once per row or per distinct id. They now name
every id on the request's resolver (#10). wantGrantNames wants what the
surface shows, buildGrantViewModels reads nothing per row, and each name keeps
its fallback: no organization name, a blank product, "System" for the
person, the revoke body's generic sentence for the floor. The grants
page wants the organization of each row and the ledger the floor of a
row whose revocation the floor would follow, so neither reads what it
does not show.

The ledger windows the rows of its tab before it names them. With a
search typed it first reads the product names of the whole tab in one
batch, because the search matches product names before the page is cut,
as before. That is the one read beyond the rendered page, and it is one
statement whatever the number of rows. OnActiveTab's switch moved to
onActiveTab, which the ledger applies to rows before they are drawn.

How the commit was shown to change no output:
- internal/server and internal/entitlements give the same 1578 test and
  subtest names and results at the parent and at this commit (1577 pass,
  1 skip), and every file under testdata/grants_listing/ and
  testdata/org_enrollment/ is unchanged.
- Four read-count values changed, as intended: grants 5 to 0 per row,
  grants-ledger 2 to 0, grants-ledger with a search 2 to 0, and the
  organization page 9 to 8 (the ledger's product read left it).
- 13 breaks, each run on both sides and caught on both: the grants
  page's organization, the granting person, the row's product, the
  revoke body's floor, a replacing grant outside the rows, a superseded
  row's replacement, the Active tab's two rules, the ledger search by
  reason and by product, the ledger's paging, and the two per-row read
  counts.
- The gocognit pin of buildGrantViewModels went from 84 to 33, the score
  golangci-lint measured on the committed code. The six resolver
  functions this commit first calls from production left the test-only
  list.

Test change: wantOwnerID looked for one link to the owner on the whole
page. The rich fixture's owner also granted the ledger's grants, and the
failing person read it injects used to blank that Granted by cell. The
ledger now names granters in one batch the injected failure does not
reach, so the cell links the owner as well. The check now looks at the
Owner field's link. NARROWER than before: the page-wide check also held
that a failed person read leaves the ledger's Granted by cells on their
fallback, and that fact is no longer asserted here.

NARROWER than before: a name read that fails now fails for every id of
its kind on the page, where a per-row read failed for its own row alone.
The test doubles fail a method by name, and no double fails the batch
reads for these handlers, so that behaviour is not probed here.

Unprobed:
- The ledger's Granted by cells on their fallback when a person read
  fails: the injected failure fails GetPersonByID alone, the ledger names
  granters through the batch read, and no double fails that batch for
  these handlers.
- A failed batch read on these two pages (above). How operator pages
  show a failed read is #207's.
- The lineage label's branch that names a replacing grant by its own
  loaded row: it equals the row's product on every row the console
  writes, because an extension carries the product of the grant it
  extends. A later section adds a test that reaches it.
- A grant with no organization: the old code skipped the floor read for
  it, and the resolver answers an empty id as unknown without a read; no
  fixture holds such a grant.
- The old per-person and per-product memos: output-equivalent, since the
  resolver reads each id once.
2026-10-10 03:37:53 -05:00
cgalo5758 1e9755a8f0 Name the set history's actors through the request's resolver
The set's History section read each change's actor person with one
statement per row. It now wants every row's actor on the request's name
resolver and builds each row from it, so the section reads the names of
all its actors in one statement however many changes the page lists. An
absent identity querier leaves the actor unnamed as before; the resolver
reads nothing through a nil querier.

Narrower: a name read that failed for one actor alone, while the others
succeeded, can no longer happen; a failed read leaves every actor on the
page unnamed, as one failed read left its own row. The error is still
discarded and no log line is added.

Shown to change no output: 9 breaks, 4 written for the old code and 5 for
the new, each caught by the test it names (the person's name, a failed
read naming nobody, the absent identity querier, and the read-count row,
whose per-row count goes from 1 to 0; the new side adds a break that
drops the wanting, caught by the same row). The names and results of
internal/server (1398 results) and of internal/entitlements (180) are
identical at the parent and at this commit. The walkthroughs give the
same 22 results at the parent and at this commit, and the screens match
the baseline apart from the Stripe integration page, whose size changes
on every run. The set-history test's failed-read case keeps its name and
fails the read of people's names in place of the read of one person.

Unprobed: a name read that fails for one actor alone, because the new
code reads every actor in one statement, which cannot fail for one actor
alone, so no test of this commit can produce it; the resolver's own
behaviour (one read per kind, ids sorted, empty ids skipped), which the
resolver's own tests hold.
2026-10-10 00:41:09 -05:00
cgalo5758 6979bd5647 Name the billing lists' and the lookup's organizations through the request's resolver
The accounts, subscriptions, invoices and payments lists, and the lookup's
invoice candidates, read one organization per row. Each now wants its
rows' organizations from the request's name resolver before the row loop,
which reads them in one query, and the loop looks each row's organization
up in the result. A page with no rows reads nothing. A failed read names
no row, so the lists show the empty name and the lookup shows the
organization's id, as a row whose own read failed did. The per-row count
of the four lists and of the lookup in
TestOperatorPagesReadAFixedNumberOfTimes goes from 1 to 0. The resolver's
newOperatorNames, both names methods, WantOrgs, Org, readOrgs and the
batch's want, lookup and flush now have a production caller, so their
test-only dead-code entries are removed.

Shown to change no output: 36 breaks of the organization reads, 16 run on
the parent and 21 on this commit (one is run on both), each caught by the
test that pins it. The eight golden files of the four lists are unchanged
and no -update ran. The names and results of internal/server and
internal/testkit (1435 results) are identical at the parent and at this
commit. The walkthroughs match the baseline (22 of 22) and the screens match
it apart from the Stripe integration page, whose size changes on every run.
The lint snapshot differs from the previous one by nothing.

Narrower than before: a failure of one row's organization read alone
cannot be shown any more, since one query cannot fail for one row.

Unprobed: the billing lists' ok test, because their fallback is the empty
name, which is also what the resolver answers when not ok; a missing
organization querier, which the resolver answers as unknown and which no
test of these pages sets.
2026-10-09 23:01:19 -05:00
cgalo5758 3c65fa4e6e Read each billing list's Stripe mappings once per page
The accounts, subscriptions, invoices and payments lists read one Stripe
mapping per row. Each loader now reads its page's mappings in one query
before the row loop (ListCustomerMappingsByBillingAccountIDs,
ListSubscriptionMappingsBySubscriptionIDs, ListInvoiceMappingsByInvoiceIDs,
ListPaymentMappingsByPaymentIDs) and the loop looks each row up in the
result. A page with no rows reads nothing, and the payments list still
reads nothing without a Stripe querier. A failed batch keys nothing, so
every row shows not mapped, no Stripe id and no badge, as a row whose own
read failed did. GetPaymentMappingByPaymentID had no other caller and is
deleted. The per-row count of the four lists in
TestOperatorPagesReadAFixedNumberOfTimes goes from 2 to 1 (the
organization name is still read per row). The name resolver's indexBy
now has a production caller, so its test-only dead-code entry is removed.
failingMappingsStripeQ overrides the four batch reads in place of the four
single-row reads; its subtests are unchanged.

Shown to change no output: 39 breaks of the mapping reads, 15 run on the
parent and 24 on this commit, each caught by the test that pins it. The
eight golden files of the four lists are unchanged and no -update ran. The
names and results of cmd, internal/fulfillment,
internal/integrations/stripe, internal/server and internal/testkit (1758
results) are identical at the parent and at this commit. The walkthroughs
match the baseline (22 of 22) and the screens match it apart from the Stripe
integration page, whose size changes on every run. The lint snapshot differs
from the previous one by nothing.

Narrower than before: a failure of one row's mapping read alone cannot be
shown any more, since one query cannot fail for one row.

Unprobed: the payments list's empty page, because its batch is guarded on
the Stripe querier and the test that checks empty pages sets none; the lines
that fill the Stripe id, the sync state and the badge, which this commit
does not edit and the golden pins; the demo seed writes no Stripe mapping,
so the screens show only the unmapped branch; the payments list's guard on a
missing Stripe querier, because no test loads a payments page that has rows
and no querier (the start command sets the querier whether or not a Stripe
key is configured; a page without one would show every row as not mapped).
2026-10-09 23:01:19 -05:00
cgalo5758 e0f9bc9f1e Add the operator pages' request-scoped name resolver and its batch queries
Five sqlc queries read the names one operator page shows in one statement
each: people, organizations (with their type), products, plan ladders, and
the tiers of several ladders with their products. operatorNames collects the
ids a page wants, reads each kind once on the first lookup, and answers
(name, ok) so a caller keeps the fallback text it shows today for an id with
no row or a failed read. tierLabel is the one current-shape rule for a rank
on a ladder.

Nothing calls the resolver from production yet, so no page's output, status,
header, redirect, write or log line changes; the functions only tests reach
are listed in the test-only deadcode file until a page adopts them. Shown by:
the generated Querier rows match the single-row reads they batch
(ListTiersWithProductsByLadderIDsRow equals ListTiersByLadderWithProductsRow),
sqlc-generate run twice leaves no diff, and the packages the commit touches
pass on a reset test database.

New tests: TestOperatorNamesReadEachKindOnce, TestOperatorNamesAnswerNotOK,
TestOperatorNamesTiers, TestOperatorNamesFloor, TestOperatorNamesReadTheDatabase.
25 breaks, each caught by the new tests. Unprobed: the ORDER BY of the tier
batch query (callers index by map), the per-id narrowing (one statement cannot
fail for one id), every caller's fallback text (each adopting page probes it),
and that the resolver logs nothing (it holds no logger).
2026-10-09 15:58:51 -05:00
cgalo5758 619963abff Split member_products.go into the five files the layout names
member_plans.go holds the handler, the plan view models, the plans page and
the cancel prediction; member_plan_moves.go the switch, keep and cancel
handlers; member_entitlements.go the entitlements panel; member_addons.go the
add-ons page; price_text.go the price renderers. Every declaration moves with
its comment, in its old order. The two section banners that divided the old
file are not carried over. Comments that named member_products.go name the
file that now holds what they cite.

No output changes. The multiset of top-level declarations of package server,
each printed with its doc comment, is equal before and after; every line of
the old file other than the two banners appears in the new files, and the new
files add only headers and imports. gofmt, build, vet, the console lint and
the lint snapshot (removed 0, added 0) are clean. The internal/server and
fedwiki packages pass on a reset test database.

The break list is the last new-side record of each break name from the
earlier sections whose text still exists, re-pointed to the destination file:
160 records, all caught on this commit. Five names are dropped because later
sections replaced their text: the rung carries its provision, the flagged
cancellation's date, a failed attachment read refuses the page, a failed
attachment read refuses Checkout, an organization without a pool holds no paid
subscription. Two stay out because they were missed in their own section: an
unmapped price offers no enabled Checkout, and an organization without a
subscription gets no switch confirmation.
2026-10-09 04:50:39 -05:00
cgalo5758 f8f9551ddd Build the FedWiki tests' fixtures through the kit
Adds internal/testkit/world (New, Providers, Person, Org, Workspace,
Product, Rule, Confer, Person.SignIn), internal/testkit/authtest
(NewSessionManager, SignIn) and internal/integrations/fedwiki/fedwikitest
(the environment, tenant, limit and usage, operator root, site rows), and
moves the FedWiki fixtures onto them: newReqEnv, setupQuotaFixture,
newReconcileTestEnv, siblingWorkspace, governFedwikiLimit (Product, Rule,
Confer, no materialization), newSiteEnv, newClaimTestDB (its raw provider
insert goes), seedOperatorSites, newMemberWorkspace, seedUnderstatedQuota,
newTestWorkspace, seedCountedWorkspace and addSiblingWorkspace.
newReconcileDomainsDB and setupReconcileRoot had nothing left to move. The
six sessions the FedWiki web tests build go through authtest. The import
guard covers the three packages, and scripts/deadcode-testonly.txt lists
them.

The six hand-written lock-wait loops become dbtest.AwaitLockWait
(lifecycle_db_test.go, registration_db_test.go, invoice_reconcile_test.go,
stamp_db_test.go, and the two record-swap tests in requests_db_test.go and
swap_record_db_test.go). Backend gains BlockedBy, Query and Abort. The
registration loop is not a verbatim move: its select matches by query text
within the current database and its done check becomes the Abort probe, as
do the two record-swap loops, each keeping its message ("registration
finished (%v) while the first key was held", "the record finished without
writing the activated site's status: %v", "the record finished without
waiting on the parked request: %v"). Kept weaker on purpose: the
never-waited messages are now the helper's, which names the selection
("backend N never waited on a lock" for the pid loops, "no backend waited on
a lock held by backend N" for the record swaps, "no backend running a
statement containing %q waited on a lock" for the registration and invoice
loops), the record-swap and stamp bounds are the helper's ten seconds (were
the tests' thirty), the stamp loop accepts any waiting count (was exactly
one for its pid), and the invoice loop matches the statement text anywhere
in the query (was its prefix). TestDispatchStampsLockRecordsInOneOrder drops
from cognitive complexity 39 to 31; its pin is re-pinned to 31.

Names and results of the three FedWiki packages, internal/integration,
internal/workflows/desiredstate and internal/integrations/stripe/workflows
equal the parent's apart from the new tests (the import guard's forbidden
list with its three subtests, and the lock helper's selection test with its
three subtests and its Abort test) and one change: the three
TestReconcileDomains* tests in internal/integrations/fedwiki now pass on a
freshly reset database. They failed there at the parent, alone and in the
package run, because nothing had registered FedWiki; the fixture now does
(#33).

Fixture equivalence (each fixture run alone on a reset database, every table
of core, fedwiki and domains dumped before and after). Differences, none
read by an assertion: names and addresses carry the builder's shape
(organization "<name> <suffix>", email "<name>-<suffix>@example.test",
subject "sub-<name>-<suffix>"; the display name is the label for tenant
fixtures); the pool is the organization's default pool (name "Default", key
default, auto-managed) where the fixtures wrote a bare one; a workspace now
sits in the organization's default pool through a pool assignment where
newMemberWorkspace, seedOperatorSites and the claim fixtures wrote neither
(seedOperatorSites gains two pools and two assignments); every FedWiki
fixture registers FedWiki and the System tenant, which adds the provider row
(or fills operator_surface_path on the row a claim fixture inserted), the
operations, connection, state and outbox rows, the System tenant's user,
person, 'system' organization type, organization and workspace, and
refreshes the fedwiki_sites resource key; archived sites carry archived_at,
as the status query sets it; sites are minted under a database-minted id;
governing a limit goes through the rule-change function, which adds a
set-change row. Claim tokens are random in both.

25 breaks: 19 failed the old test at the parent and the new test here (one
code break per FedWiki test file in the function it pins, and one per
converted lock wait with the lock removed; the three loops with an Abort
probe fail through it with their own message), and six on the kit alone
failed the new test. No assertion in a test body changed.
2026-10-08 02:43:55 -05:00
cgalo5758 254bc4ccb1 Count the test kit's query calls as test uses
scripts/sqlc-unused counts a call from a package covered by a ".*" entry of
scripts/deadcode-testonly.txt as a test use, so one list defines the
test-support packages for both checks, and the list's header says so. A query
that only the kit's builders call now reads TEST-ONLY, not used. The check
reads the module in a directory it is given, which lets a test run it over a
fixture module.

New tests: TestScanCountsATestSupportPackageAsTestUse, over a fixture module
whose kit package and app package each call one query, and
TestReadSupportTakesTheWholePackageEntries. The other tests, TestCheck and
TestReadAllow, are unchanged. The output of go run ./scripts/sqlc-unused
equals the baseline taken before step 2 once each line's count of test
references is stripped. 2 breaks, each failed the new tests; they have no old
side.
2026-10-08 00:52:30 -05:00
cgalo5758 1f0c697387 Check the shell through the HTML query helper
internal/testkit/htmltest parses rendered HTML and selects from it with CSS
selectors (Parse and ParseIn, One, All, Count, None, Text, Texts, Attr,
HasClass, HTML, Before, InOrder), on golang.org/x/net/html and
github.com/andybalholm/cascadia, which joins go.mod as a direct requirement
(BSD-2-Clause, pure Go, depending only on x/net). A page, which opens with a
doctype or an html element, parses as a whole document, so the attributes of
its html and body elements are kept; any other body parses as a fragment, a
table row under a tbody, an option under a select and anything else under a
body. Both parse with scripting off, so what a noscript holds is elements
that selectors reach, as a browser without JavaScript renders it. Its own
tests: the fragment context for tr and option, the attributes of html and
body, the content of noscript, One's failure message naming the selector,
None, Text, selection by attribute and role, Before, and InOrder. The import
guard gets a row for it, and scripts/deadcode-testonly.txt an entry.

harness_test.go gains the shell assertions both surfaces share, exported for
server_test: the top bar, the account menu, the Identity and Access handoff,
the drawer and the rail's account block. member_nav_test.go and
operator_sidebar_render_test.go select regions by landmark and ARIA
(nav.navbar[aria-label=Account], aside#app-rail, the account group's
aria-labelledby list) and controls by attribute
([data-form="shell.confirm"], the button posting to /logout) instead of
regionBetween, topBarRegion, railRegion and sidebarRegion, read aria-current
on each member rail entry and on both surface switches, check order through
InOrder, and keep a class assertion only where the class is the fact.
sidebarSections selects the entry list instead of slicing the rail string,
and sidebarLinkActive reads the link node instead of matching a regular
expression. The page subtests of TestShellAccountMenu and
TestShellMemberRail call named functions (checkAccountMenuPage,
checkMemberRail); every test and subtest name is unchanged.

Each ban and each count keeps the region the old one covered, or a wider
one; a check that an element is present looks where the element belongs,
such as the trigger and the Get help order in the bar and the entries in the
rail, which is narrower than the body the old strings searched and stricter.
A bar, a rail or a page the old tests searched as a string is searched as
markup (Node.HTML) where the old string covered the element's own tag, the
html and body start tags, or an attribute: the rail's d-lg-none count,
app-rail-signout, d-none d-lg-flex, the identity tooltip, the operator
rail's nav-link count and its ps-4, group and /logout bans, text-danger,
navbar-collapse, the handoff explanation, mt-2 pt-2 border-top, and the
bar's bans over everything before the rail. Counts that were page-wide stay
page-wide: the account menu (any tag), the Get help item, the Operator panel
switch, the dismiss control, the hx-post="/logout" controls (counted as
markup, so data-hx-post counts), the bans on href="/logout" (markup, so any
attribute ending in href counts) and the account-console links; those inside
the rail are counted over the whole rail before they are placed in the
account group: the group's name and email spans, its list, the Identity and
Access, Sign out and Get help entries. The Dashboard link count and the
Operator link ban also run over the page's markup, the drawer's two lists
are counted by class prefix as well, and nothing before the account group
reads Sign out. Where the old tests pinned an element's class as an exact
string, the new ones compare its class token set exactly (AssertClassSet),
which ignores token order only; the checks that were prefixes or regular
expressions stay HasClass or class selectors, and an active operator entry
must still carry nav-link. aria-current appears on the active rail entry and
nowhere else in the member rail, and on neither surface switch nor the
account group's entries. Sign out, Get help and the account group's name and
email stay plain text, the trigger's one child is its label span, the menu
header's children are the line break and the email span, and the line break
follows the name. The closing </main> check stays a text check, since a
parser closes the element anyway.

One weakening is general: a markup change that alters no asserted fact no
longer fails, such as an attribute added to an element the old tests matched
as a whole markup string (a data attribute on Sign out, say), a wrapper, or
a decorative child the comments do not exclude. No specific fact is kept
weaker.

The four gocognit exclusions of TestShellAccountMenu, TestShellMemberRail,
TestShellRailAccountBlock and TestOperatorShellTopBarAndDrawer are gone; the
lint reports nothing in their place.

124 breaks: 118 failed the old and the new shell tests (the template and
code changes that move, drop or mislabel each asserted element; an untitled
Get help item in the bar, the drawer's header and the rail; a Log out title
on the brand link; an extra class on Sign out, on the email span, on the
switch's li and on the page title; a missing class on the rail or on an
account entry's li; a button among the rail's entries; a surface label, a
Get help link or a danger Dashboard link between the bar and the rail; a
logout button in the drawer's header or a logout form among its entries; the
tooltip on the li or on the account trigger; an Operator panel label for a
member without the role; a noscript holding a second main, a Dashboard link
or a Sign out link to /logout; a data-hx-post Sign out in the content or the
menu, and a data-href to /logout; aria-current on a surface switch or on
Identity and Access; an active Billing entry without nav-link; a second
account-console link or Identity and Access item; an element inside Sign
out, the trigger, the menu header or the group's name; a class on the rail's
own tag, on the html or body tag, or a second element where the old string
counted one: the account menu, Get help, the Operator panel switch, a plain
Sign out, the group's name and email, its list; a Sign out button before the
account group on either surface; a Dashboard or an Operator link with a
decorative child in the content; a third nav list; a bar without its navbar
class), and 6 on the helper's own tests have no old side. The names and
results of internal/server equal the parent's; new tests:
TestParseKeepsRowsAndOptionsThatABodyContextDrops,
TestParseKeepsTheAttributesOfHtmlAndBody, TestParseSelectsInsideNoscript,
TestOneFailureNamesTheSelector,
TestNoneFailureNamesTheSelectorAndQuotesTheMatch,
TestTextCollapsesWhitespaceAndSelectsInDocumentOrder,
TestSelectionByAttributeAndRole, TestBeforeHoldsWhatPrecedesTheMatch and
TestInOrder in htmltest, and the guard's htmltest subtest.
2026-10-08 00:52:30 -05:00
cgalo5758 e8b78d93a7 Render the member, root and part tests through the member constructor, and delete the old set helpers
MemberSet joins OperatorSet in harness_test.go. The member, root and
parts-only sets in the server tests go: rootTemplates, errorPageTemplates,
renderIndex, renderEntitlements, memberTestTemplates, renderMemberDomains,
the inline set in the member invoices test, the parts-only sets of the
form-part and help-icon tests, and renderMember, which now calls Render on
MemberSet. partsSet and renderPart go with them; the part tests render on
OperatorSet, because they assert the operator crumb, and read the error of
TestPageHeaderRejectsCountAndAction from ExecuteTemplate directly. The set
of error_page_test.go that has no error.html stays synthetic.

With the last test callers gone, web.ParseUIPartials, server.helpIcon,
server.OperatorSurfaceRoot, server.MemberSurfaceRoot, server.rootLink and
the server.HelpIcon type are deleted, with their scripts/deadcode-testonly.txt
entries and the comments that described them (the partials' header comments
now name the surface constructors).

The names and results of internal/server and internal/web equal the
parent's. 12 breaks (one per helper replaced, a text change in the template
its first caller renders), each failed the old and the new test.
2026-10-07 20:51:57 -05:00
cgalo5758 d104ad91fe Build the member surface's templates in one constructor
web.MemberTemplates builds the member set from CoreTemplates and each
integration's Mount, and composeUITemplates folds into it; operator.html and
the operator-side integration files leave the root set. Start and the six
member handlers call it. TestComposeUITemplates moves to internal/web as
TestMemberTemplatesMounts, its subtests keeping their names except the last,
now "template name without a key prefix is an error" (the constructor returns
the error and Start panics through template.Must). New tests:
TestMemberTemplatesHoldTheMemberFiles. 1 command break, caught by make
screens on the new side; the constructor is new, so no old run.
2026-10-07 20:16:11 -05:00
cgalo5758 811fbdd26d Build the operator surface's templates in one constructor
web.OperatorTemplates builds the operator set from CoreTemplates and each
integration's Mount, registering the whole function map before it parses;
the four operator handlers call it. New tests, in internal/web:
TestOperatorTemplatesHoldTheOperatorFiles,
TestOperatorTemplatesParseASharedPartOnce,
TestOperatorTemplatesRefuseAMountFileWithoutItsKeyPrefix and
TestParseSourcesRefuseTwoFilesWithOneBaseName. 1 command break, caught by
make screens on the new side; the constructor is new, so no old run.
2026-10-07 20:16:11 -05:00
cgalo5758 d1ac278e6b Open every test database through dbtest
internal/testkit/dbtest opens TEST_DATABASE_URL and E2E_DATABASE_URL, never
migrates, and fails with "the test database is behind; run
test/reset-test-db.sh" when the core ledger lacks the highest version of
db.BaseSources(). The opener helpers except the scratch-database and
container openers (26 of the 29) and the inline openers of the database
tests call it; helpers that did more than open (providers, fake farms, the
FedWiki provider row, cleanup of override rows) keep that setup. The
stripe/workflows and workflows/entitlements openers keep their search_path
through WithSearchPath. The closed-database seam tests use Closed.

TestMigration18SetsExistingFedwikiRulesToForceReduce runs on a scratch
database through Scratch and UpTo instead of rolling the shared core ledger
back through goose's global state, because no opener migrates the shared
database any more.

The in-package forms tests of internal/server reached the integrations'
forms only because an external test file imported internal/migrate; a test
file now links internal/integrations explicitly.

TestRegisterProviders_DB is re-pinned from gocognit 37 to 34.

New tests: TestLedgerCheckRefusesAnEmptyDatabaseAndAcceptsTheTestDatabase
(dbtest), TestOpenRefusesAnEmptyDatabaseAndAcceptsTheTestDatabase (dbtest,
through opened, the path Open and OpenE2E share),
TestKitPackagesReachOnlyTheirAllowedModulePackages (testkit). The skip
messages of the openers are unified to "TEST_DATABASE_URL not set, skipping
integration test". 4 breaks: Open skipping its ledger check and the ledger
check accepting an empty database fail the new tests; a kit row that no
longer allows internal/testkit fails the import guard; leaving the fedwiki
rules as they were in migration 00018 fails the migration 18 test on both
sides.
2026-10-07 16:22:18 -05:00
cgalo5758 2bc0bd1610 Add a tool that applies deliberate breaks one at a time
scripts/mutation-check reads a JSON list of breaks, applies each to the tree
one at a time, restores the file byte for byte, and reports CAUGHT, MISSED,
WRONG-MESSAGE, NOT-FOUND, SKIPPED or CONTROL-FAILED for each.

New tests: TestApplyAndRestoreAreByteExact, TestOldTextMustOccurOnce,
TestSidePicksPatternAndMessage, TestARecordWithASideRunsOnlyThere,
TestOutcomes, TestStatelessListStopsOnAFailingControl,
TestControlRunsOncePerDistinctTest,
TestStatefulListPreparesAndControlsEachBreak,
TestStatefulControlFailureMarksOnlyThatBreak, TestPrepareFailureStopsTheList,
TestCommandRecordsAreClassifiedByExitAndText,
TestCommandRecordReadsTheInspectedFileAndNeverAStaleOne,
TestCommandControlFailsWhenTheTextIsAlreadyThere,
TestSummarizeExitsOneUnlessEveryBreakIsCaught.
2026-10-07 16:21:58 -05:00
cgalo5758 d7815d3603 Draw the Products page and the add-ons list from the decision 2026-10-07 05:26:49 -05:00
cgalo5758 2e1617cf4d Add the offer decision and its table test 2026-10-07 05:08:22 -05:00
cgalo5758 d448afcfdd Pin the exception list and run every check in make lint 2026-10-07 01:01:20 -05:00
cgalo5758 75f0572dfb Add the gate's configuration and tools 2026-10-06 22:48:30 -05:00
cgalo5758 d807446ef4 Update allowlist and default models for Kimi and OpenRouter
- Add kimi.ai and openrouter.ai to allowlist for egress
- Replace kimi-for-coding with kimi-code-plan-global in DEFAULT_MODELS
2026-10-05 21:39:47 -05:00
cgalo5758 56e65b669c Read invoices back from Stripe and archive the change
Stripe delivers webhook events in the endpoint's API version. Under a
version later than the 2025-02-24.acacia stripe-go v81 pins, the
payload-shaped invoice handlers projected every invoice with no
subscription link and no lines, and every payment with no payment
intent or method.

- Invoice events read only the invoice id and run ReconcileInvoice,
  which reads the invoice, its lines and its payment intent's charges
  back at the pinned version and converges the core rows under a
  per-invoice lock, in any event order. A payment row is one attempt,
  keyed by its Stripe charge (Stripe store migration 00005).
- Two clocks: created_at is when the console wrote a row; invoices gain
  issued_at and payments paid_at (core migration 00022, backfilled),
  and every invoice and payment list, the activity feed and the
  composite's latest invoice read those.
- An hourly and at-boot invoice sweep reads back invoices never read,
  open invoices once a day, and invoices of dead-lettered events; the
  provider page states when it last finished. The first run is the
  repair of invoices projected so far.
- The API version skew is logged once per version, and
  payment_method.attached links payments recorded before it.
- A restricted key now needs Invoices read and Charges read.
- scripts/db-reader-role.sql maintains console_reader, a login role
  that reads the console's tables without personal data, documented in
  docs/database-management.md and checked against a golden column list
  by TestConsoleReaderColumns. The demo snapshot keeps owners and
  privileges so the role's checks hold on it.
- The demo seed and test fixtures carry issue and paid times.
  status/design-feedback.md gains four upstream notes, one written when
  #60 was closed.

OpenSpec change stripe-invoice-reconcile is archived and its deltas
synced into openspec/specs/. Follow-ups are #150, #151 and #152.
2026-09-28 18:42:29 -05:00
cgalo5758 b245bdb0a9 Fix security audit findings from 2026-09-21 scan
Remediate six confirmed security issues: deployment-only config keys,
bounded provider responses, short-lived registration sessions, private
init file mode, FedWiki workflow authorization, and switch preview
gates.

- Add DeploymentOnly config key declaration; refuse runtime overrides
  for keys that decide where secrets are sent
- Create httplimit package; bound all provider response reads at 8 MiB
- Set fifteen-minute deadline on /register sessions
- Write mc-config.yaml with 0600 permissions
- Derive FedWiki workflow IDs from site IDs; re-authorize sites before
  mutating activities
- Apply switch authorization gates to the proration preview
2026-09-21 13:57:57 -05:00
cgalo5758 bea0ffc024 Add Codex CLI harness to agent runner
Support Codex CLI as a second agent harness alongside opencode, with
per-harness credential mounting, model routing, and reasoning effort
handling.
2026-09-21 02:42:53 -05:00
cgalo5758 0c78d5f425 Move issue tracking to the forge tracker
Replace status/issues.md and status/milestones.md with the Gitea forge
tracker, add issue templates, and archive pre-forge issues as YAML.
2026-09-21 02:35:27 -05:00
cgalo5758 5829091881 Track the agent runner and its method page
The contained runner that drove the 2026-09 security audit, the README
review and four rounds of design ideation lived only inside the ignored
notebook. It moves to scripts/agent-runner/: the prepare, tools, audit,
ideation and teardown scripts, the compose and container files, the
allowlist proxy and the prompt templates, with the paths that assumed
the notebook fixed and findings written to a caller-named directory. Run
outputs, transcripts and the round-specific sheet scripts stay behind.

docs/agent-runner.md states the method: the disposable git archive
copy and the fail-closed proxy, how a task is shaped, union rather than
intersection of findings across models, separate adjudication of every
finding against the source, and the evidence a finding must carry.
2026-09-19 19:47:15 -05:00
cgalo5758 a5a4b9c591 Gate archives on notebook citations and add the harvest skill
A notebook's lessons survive only if they are written into a tracked
home before the decision lands, so landing a change now has three
pieces. The contract: a "Landing a change" section in MAINTAINING.md and
a fifth item in the UI definition of done. The labor: a harvest skill
that inventories the notebook, names each file's kind and routes
findings to the owning docs page, reasoning to the change's design.md,
problems to issues.md and reusable tools to a tracked home. The gate:
scripts/notebook-citations.sh, a git grep over what git would commit for
the notebook's directory shape, which make lint runs after the program's
own lint, and scripts/archive-gate.sh, which refuses an mv into the
archive or an openspec archive while the check reports hits.

The gate is one script that any harness calls with the command it is
about to run; Claude Code reaches it through a PreToolUse hook and
opencode through a plugin. Both checks live in scripts/ rather than the
lint subcommand because they are repository hygiene, not part of the
shipped program. The generated openspec-archive-change skill is not
edited; the post-archive reminder carries the harvest step instead.
2026-09-19 19:47:15 -05:00
cgalo5758 fe19ee415c Add CLA and SPDX headers, fix docs
- Pin Dockerfile to Go 1.23 to match go.mod
- Record README front-door audit findings
2026-09-07 21:32:14 -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