Skip to content

refactor(test): give the architecture and tenant guards a loader seam - #1067

Merged
mforce merged 5 commits into
mainfrom
refactor/859-s1-loader-seam
Oct 4, 2026
Merged

mforce merged 5 commits into
mainfrom
refactor/859-s1-loader-seam

Conversation

@mforce

@mforce mforce commented Oct 4, 2026

Copy link
Copy Markdown
Owner

TL;DR

Step 1, slice S1 of #859: the loader seam. The architecture and tenant-bypass guards now take rules as objects, not file paths, and nothing moves or is deleted. The real rules still load from module-ledger.json, tenant-bypass-allowlist.json and filter-free-set-sites.tsv, through exactly three accessors. S2 and S3 can then swap each accessor's source for C# rows without touching a scanner or a fixture.

No rule, #632 key or hash, reason text or real-tree diagnostic changes. The proof is below.

Refs #859

Change map

File What Why
Architecture/ModuleLedger.cs Load splits into Parse(path) (JSON shape only) and Validate(ModuleLedger) (every rule about the values, same messages and labels) Fixtures and, in S2, C# rows go through the same Validate the real ledger uses
Architecture/RealModuleLedger.cs (new) Value = Validate(Parse(module-ledger.json)) The only reader of the real ledger
ModuleLedgerScanner, AdapterReachScanner (Scan and ScanPeers), AdapterTierScanner, CompatibilityExceptionScanner Entry point takes a ModuleLedger instead of a path The walks are unchanged
9 real-tree tests (*RealTreeTests, TableOwnerRealModelTests, ModuleContractRealAssemblyTests, AccessOwnershipClaimTests) Read RealModuleLedger.Value One accessor
ModuleLedgerTests, AdapterReachTests, AdapterTierTests, CompatibilityExceptionTests, PeerContractTests, CouplingMatrixTests Fixture helpers build records and call Validate instead of writing JSON S2 can delete Parse without touching them
TableOwnerTests JSON-shape cases call Validate(Parse(path)) and stay until S2. Two new facts check that Validate rejects a blank table name and a blank FK name Plan §6: the semantic counterparts of the [null] and "name": 1 JSON cases
TenantBypass/GuardScanner.cs Scan(srcRoot, IReadOnlyList<AllowListEntry>) Same as the ledger
TenantBypass/BypassAllowList.cs, FilterFreeSetSites.cs (new) Entries (the same AllowList.Load, with its blank-field filter) and All (TSV rows as FilterFreeSetSite records) The only readers of the two tenant files
TenantBypassRealTreeTests Reads both accessors. Reason.Trim() and the blank-reason check stay at runtime The #632 key is string.Join("\t", Symbol, Set, Signature), as before
TenantBypassWalkTests An explicit, commented empty list Its old path omitted TenantBypass/, so it always ran with no entries (plan §2)
TenantBypassAllowListTests Builds AllowListEntry objects
docs/decisions/859-typed-rule-registries.md, README.md The new decision record and its index row Plan §7 places it in S1

Review order. Read ModuleLedger.cs first (the split), then any one fixture file. The rest is mechanical.

Proof that behaviour is unchanged

Counts. The Architecture and TenantBypass scope was measured at base e63ad81d: 429 passed. At head: 431 passed, which is 429 plus the 2 new Validate facts. A name diff shows only those 2 additions, and 22 theory cases whose arguments now show plain values instead of JSON fragments. Documentation tests (20) and the tracked-file guards (PostgresImagePin, RedisImagePin, CommittedSchemaDocs_CarryNoEnvironmentSpecificContent) pass with the branch staged.

Loader equivalence (local, uncommitted harness). A verbatim copy of the old Load and the new Validate(Parse(path)) read 115 inputs: every ledger file the 429-test suite writes (93), the real ledger, a missing path, and 20 hand-written malformed files.

  • Records were equal for 115 of 115.
  • Error sets were equal for all 95 real and fixture inputs.
  • The 5 differences are malformed shapes no test or real row uses (listed below).

Fixture equivalence. Every ledger the suite builds was serialised, records plus sorted errors, at base (in Load) and at head (in Validate). All 92 distinct base ledgers occur at head. The only 2 extra head ledgers are the two new facts. So the record-built fixtures reproduce exactly the inputs the JSON fixtures produced.

Mutation matrix (plan §5). Each mutation was applied to the bin/ data or to src/ and then restored. Base and head match on every row: same red test, same failure count, same message.

Id Section Mutation Red test Base Head
M-a src/ a Finance type references Domain.Sales.DiscountCeiling ModuleLedgerRealTreeTests: "undeclared cross-owner edge Finance -> Commerce" red red
M-b adapters drop ListAccountsCliCommand.RunAsync AdapterReachRealTreeTests: "undeclared adapter reach Cluckwork.Api.Cli.ListAccounts…" red red
M-c src/ an unlisted IgnoreQueryFilters in Infrastructure …AllBypassesAreAllowListed: "unexcused bypass" red red
M-d src/ reorder AccountScopedUserValidator's classified db.Users predicate …FilterFreeSetSitesAreStableAndClassified: "unclassified filter-free-set candidates" red red
M-e1 allow-list drop row 1 …AllBypassesAreAllowListed: "unexcused bypass" red red
M-e2 filter-free TSV drop the validator's row …FilterFreeSetSitesAreStableAndClassified: "unclassified…" red red
M-f1 foreignKeys drop row 1 TableOwnerRealModelTests: "undeclared cross-owner foreign key FK_AspNetUsers_Accounts_AccountId" red red
M-f2 compatibilityExceptions drop the row CompatibilityExceptionRealTreeTests: "undeclared compatibility exception" red red
M-f3 adapterTiers drop the MCP row AdapterTierRealTreeTests: 2 of 3 red, including McpTierRow_IsDeclared red red
M-g tables drop Finance ExpenseCategories TableOwnerRealModelTests: "table 'ExpenseCategories' … has no owner" red red
M-h tableOwnerOverrides drop AspNetRoleClaims TableOwnerRealModelTests: "… disagrees with CLR namespace owner " red red
M-i owners drop IFinanceModule from Finance's contract AdapterReachRealTreeTests: "IFinanceModule at src/Cluckwork.Api/Endpoints/Expenses/ExpenseEndpoints.cs" red red
M-j edges drop the first symbol of edge cell 1 ModuleLedgerRealTreeTests: "undeclared cross-owner edge Access -> Commerce" red red
M-k adapterRoots empty persistenceForbiddenNamespaces AdapterReachRealTreeTests: "adapter registry error: adapterRoots declares no persistenceForbiddenNamespaces" red red

The data was restored after each mutation. The full scope then ran green at head (431).

What does differ (all JSON-only, all gone in S2)

The harness above found these. None is reachable from the real files or from any test.

  • Missing kind (owner or edge) now reads kind '', where it read kind '<missing>'. A record cannot tell missing from empty.
  • A non-object owner, edge or compatibility row now also reports the checks its blank placeholder fails. For example, a non-object edge adds "missing 'from' or 'to'", a kind error and "blank reason".
  • Error order. Shape errors are listed before value errors. Two fixture inputs with 2 errors each have the pair in the other order. Every assertion uses Contains, and the failure text shows up to 10 errors.
  • Unknown tables owner per JSON key. This difference is closed in Parse, so the error multiset is equal. The harness found it on 4 fixture inputs: JSON reports once per key, while records report once per owner.
  • TSV malformed-row message. It prints the parsed row (Key\tReason), not the raw line. They differ only by trailing tabs on a short row.
  • A corrupt real data file now fails in the accessor's static initializer. Before, it failed inside Scan. The same test goes red either way.

Not in this PR

  • No data moves and no file is deleted. S2 handles the ledger and S3 handles the tenant files, each with the two-push parity rule.
  • Three TableOwnerTests.MalformedRows cases are semantic and stay JSON here: unknown table owner, blank FK reason and missing override reason. S2 converts them to records when it deletes Parse.
  • AllowListEntry keeps its required JSON shape until S3 (plan §1).
  • The compatibility exception with deleteWhen: "#859" is untouched. Removing it is still [C] #514 slice 17: assembly split — conditional on Track A/B evidence #859's job.

mforce added 4 commits October 4, 2026 20:56
Scanners take a ModuleLedger instead of a path. RealModuleLedger.Value is the
only reader of the real ledger. Fixtures build records and pass them through
ModuleLedger.Validate instead of writing JSON. Parse keeps the JSON-shape
errors; Validate holds every rule about the values.

Refs #859
GuardScanner.Scan takes the allow-list entries instead of a path.
BypassAllowList.Entries and FilterFreeSetSites.All are the only readers of the
two tenant files. The #632 key and the runtime reason trim are unchanged.

Refs #859
@mforce

mforce commented Oct 4, 2026

Copy link
Copy Markdown
Owner Author

Reviewed d2d8ab4 against e63ad81 for #859 S1. No code findings or real-tree behavior regression found. Two P3 documentation corrections remain.

  1. P3: remove live progress state from the decision record. 859-typed-rule-registries.md:7 says step 1 in progress (S1 of three PRs). Ordinary completion of the following slices makes this stale without any guard failing, the issue-state duplication the repository rules reject. Keep Status: accepted; describe the three slices as the plan accepted on 2026-10-04 and leave progress in GitHub.

  2. P3: qualify the claim that no test exercises the diagnostic differences. 859-typed-rule-registries.md:57 includes error ordering among the differences, then says no test produces any of them. Existing TableOwnerTests.MalformedRows_AreRegistryErrors cases for tables: {"Red": [null]} and tables: {"Red": 5} both reverse their two errors. The PR body acknowledges this correctly. Say that the five changed error multisets occur only in synthetic malformed inputs, while two existing fixtures have reordered but otherwise identical errors. This corrects the evidence statement; it does not require changing the implementation.

The decision record belongs in S1, as approved plan §7 explicitly requires. Its storage rationale, slice boundaries and preservation of the deleteWhen: "#859" compatibility obligation are accurate apart from those corrections.

Verification:

Check Result
Locked restore and application-test build Passed, zero warnings/errors
Architecture and TenantBypass, one class at a time 431 passed across 28 classes, with DOTNET_USE_POLLING_FILE_WATCHER=1
Independent old-loader versus Validate(Parse(...)) probe 115/115 records equal; 110/115 error multisets equal; 103/115 error sequences identical
All 95 real/corpus/missing-path inputs Equal records and error multisets
Saved fixture captures, complete serialized outputs 92 base ledgers retained; only the two new semantic cases added
M-c, unlisted IgnoreQueryFilters Expected unexcused bypass; restored green
M-d, reordered classified predicate Expected unclassified filter-free-set candidates; restored green
M-f2, removed compatibility row Expected undeclared compatibility exception; restored green
M-k, empty persistence-forbidden roots Expected adapter registry error; restored green
Remove blank-table / blank-FK validation checks separately Each new fixture fails at its RegistryErrors assertion; restored green
Padded needs-review / blank reason Both rejected; restored green

The five changed error multisets are the worker's synthetic s05, s06, s07, s08 and s17. They remain invalid; missing kinds change their displayed placeholder, and non-object rows gain errors for their empty fields. The reordered errors, reconstructed malformed TSV line and accessor-originated loading exceptions also preserve rejection. These disclosed differences are harmless to the real tree.

The reader audit found only RealModuleLedger.Value, BypassAllowList.Entries and FilterFreeSetSites.All reading real rules. Every old value check remains in Validate or its helpers. Real registry files, reasons, scanner diagnostics and #632 hash functions are unchanged. Identity fields remain exact, and runtime reason trimming and blank-reason rejection remain enforced. TenantBypassWalkTests now explicitly use the intended empty list.

Applied pstack:thermo-nuclear-code-quality-review and ponytail-review, scoped to S1 through P3. No actionable complexity finding. No file crosses 1000 lines; ModuleLedger grows 460→544 for the temporary parser/validator split, while the existing 1427-line scanner shrinks. Ponytail result: Lean already. Ship.

Independent reproduction covered the four requested matrix mutations and four additional checks above. The other matrix rows and the 92-ledger captures were audited from the worker's saved evidence. No full solution or browser run was needed for this test-infrastructure slice.

Detailed reviews and reproduction logs are saved locally under /home/mforce/.cluckwork-slices/reviews/1067-d2d8ab4d-astra-evidence/: 01-loader-and-proof.md, 02-code-quality.md, equivalence.json, tests-summary.tsv and mutations-summary.tsv. All temporary mutations were restored; the review checkout was clean before publication. No implementation changes or pushes.

@mforce

mforce commented Oct 4, 2026

Copy link
Copy Markdown
Owner Author

Both P3 doc fixes are in 4f4509d (docs only, docs/decisions/859-typed-rule-registries.md):

  1. Status: is now just accepted. The slice table is introduced as the plan accepted on 2026-10-04, with no progress state.
  2. The S1 differences paragraph now says the changed error sets occur only in synthetic malformed inputs. It also names the two existing TableOwnerTests.MalformedRows fixtures, "tables": {"Red": [null]} and "tables": {"Red": 5}, that report the same two errors in reverse order. Both assert with Contains.

Verification: the Documentation tests passed 20 of 20. The tracked-file guards (PostgresImagePin, RedisImagePin, CommittedSchemaDocs_CarryNoEnvironmentSpecificContent) passed 3 of 3, run with the change staged. No code changed.

@mforce
mforce merged commit 9477029 into main Oct 4, 2026
16 checks passed
@mforce
mforce deleted the refactor/859-s1-loader-seam branch October 4, 2026 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant