Skip to content

test(arch): adapter tiers, dated privilege exemptions declared before their surfaces exist (#843) - #880

Merged
mforce merged 12 commits into
mainfrom
chore/843-adapter-tier
Sep 15, 2026
Merged

mforce merged 12 commits into
mainfrom
chore/843-adapter-tier

Conversation

@mforce

@mforce mforce commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Closes #843

Track A slice 2 of #514, stacked on #879 (retarget to main after it merges). Enforcement only, no src/ change.

What it adds

A tier is a dated exemption: a namespace, a privilege a module may not otherwise have, the surface that maps it, a reason and a reviewBy issue. The ledger gains one adapterTiers row today, Cluckwork.Api.Mcp → DirectRepository, surfaced by MapMcp, reviewed by #806. It is committed dormant: green now, with no /mcp and no tool class in the tree, so the ledger is not wrong the day #806 lands.

AdapterTierScanner (syntax-only, same walk as the sibling guards) fails on:

  • a [McpServerToolType] type outside every tier namespace,
  • a MapMcp call with no tier row (prints the JSON row to add),
  • a tier row missing a field, with a privilege outside the closed set, or with a reviewBy that is not #NNN (an exemption with no end date).

A tier also unions into the reach ratchet's adapter roots and its persistence-forbidden set, so a tool class's repository parameters still need an adapters row and an AppDbContext parameter still fails.

Mutations run against the real tree (each red before it was claimed, then reverted)

Mutation Result
[McpServerToolType] class Probe under Cluckwork.Api.Endpoints red, tool type outside every tier
app.MapMcp("/mcp") in Program.cs with the tier row removed red, mapped surface with no row; output prints the row
reviewBy removed from the row red, registry error
WaterTools(IWaterUsageRepository) under Cluckwork.Api.Mcp red, undeclared reach WaterTools.ctor → GeneralInventory
same class taking AppDbContext red, forbidden persistence type

Why syntax, not EndpointDataSource

The issue suggested keying the tier on EndpointDataSource. The ledger guards live in the Application test project with no host, and the reach ratchet already finds top-level routes by scanning Map* calls in Program.cs; a MapMcp scan is the same mechanism. The decision record covers this and the other rejected shapes.

Scope against the issue

The issue's second bullet, owner rows for the four OpenIddict tables, is delivered by #877, not here: the table-owner guard fails #795's PR the moment it maps a table with no owner row, so the rows are written there by that author. The "add a mapped table with no owner row → red" criterion is therefore met on main already. The remaining criteria are the four mutations above.

For #806's author

Put tool classes under Cluckwork.Api.Mcp, add their adapters rows when the ratchet prints them, and delete AdapterTierRealTreeTests.McpTierRow_IsDormantToday, which goes stale on purpose.

Tests: Architecture and Documentation filter 246/246 (was 244). Full solution builds warnings-as-errors clean.

@coderabbitai

coderabbitai Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: c1f5f45c-ad0e-4487-8f07-ab45cbeb29d6


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mforce

mforce commented Sep 15, 2026

Copy link
Copy Markdown
Owner Author

Review round 1 (Codex gpt-5.6-sol, reviewer of record), head d61836a: changes requested, 3 findings, all accepted.

  1. P2, surface is not validated. A row with surface: "MapMcpTypo" passes the registry, stays Dormant, and still grants its namespace the exemption. Fix in flight: the known surfaces become one shared closed set the registry validates against, with a test that an unknown surface is a registry error.
  2. P2, a using alias hides the attribute. using ToolMarker = ModelContextProtocol.Server.McpServerToolTypeAttribute; plus [ToolMarker] outside the tier stays green; confirmed by the reviewer against the real tree. Fix in flight: file, namespace-block and global using aliases are resolved syntactically before the match, with fixture tests for each form and a real-tree mutation.
  3. P3, inactive #if branches are not walked. The fixed parse-symbol list is shared with the three sibling ledger scanners since test(arch): module ledger and cross-owner edge ratchet (#842) #872. Rather than guess symbols, the fix adds a trust check: any <DefineConstants> in a src project that the parse options do not know is red, telling the author which symbol to add. No project defines one today, so it is green now. Recorded as a limit in the decision record.

Fix commits and the mutation evidence follow on this PR; round 2 goes back to the same reviewer.

Base automatically changed from chore/848-coupling-matrix to main September 15, 2026 04:11
Declares the MCP tier (Cluckwork.Api.Mcp -> DirectRepository, reviewBy
#806) dormant, before #806's /mcp surface exists. AdapterTierScanner
walks tool-attributed types and MapMcp call sites, reporting a tool
type outside every declared tier namespace, a mapped surface with no
tier row, and a dormant row (green, informational) when neither exists
yet.
A tier is an adapter root with a privilege: adapterTiers[].namespace
now feeds AdapterReachScanner's namespace roots (so a tool type's
repository parameters are walked for module reach) and its
persistence-forbidden set (DirectRepository is not direct DbContext).
…osed set (#843)

codex-sol review of #880, finding 1. AdapterTierScanner.SurfaceCalls was a
private set the ledger never checked a row's `surface` against, so an
unknown surface (e.g. a typo'd MapMcpTypo) passed registry validation while
granting the DirectRepository exemption over a namespace the scanner never
walks for that surface. AdapterTier.KnownSurfaces is now the one shared map
from surface to the privilege it grants; both the scanner's invocation walk
and ModuleLedger's row validation read it, so a surface outside the set is a
registry error naming the surface and the closed set.
…843)

codex-sol review of #880, finding 2. AdapterTierScanner.HasToolAttribute
matched only an attribute's bare last identifier, so `using ToolMarker =
ModelContextProtocol.Server.McpServerToolTypeAttribute;` plus `[ToolMarker]`
went undetected outside Cluckwork.Api.Mcp — confirmed against the real tree
with a locally-declared attribute and alias, all three
AdapterTierRealTreeTests stayed green.

The scanner now resolves file-local and namespace-block using aliases in the
declaring file, plus `global using` aliases declared anywhere in the same
project (grouped by ModuleLedgerScanner.ProjectRootNamespace), before
comparing the attribute's identifier against the known tool-attribute names.
Resolution is syntactic, matching the existing bare/qualified/Attribute-suffix
handling. Verified with a real-tree mutation: a class outside every tier
namespace carrying a locally-declared McpServerToolTypeAttribute, referenced
only through a local alias, went RED (ToolTypeOutsideTier); reverted with
`rm` and confirmed `git status` clean. Evidence in
/tmp/514/mutations-843/6-alias-outside-tier.txt.
…essor symbol (#843)

codex-sol review of #880, finding 3. AdapterTierScanner and ModuleLedgerScanner
both parse every file with one fixed preprocessor symbol list
(ModuleLedgerScanner.ParseOptions), so a project-defined constant guarding
real code in an inactive #if branch (e.g. `#if MCP` around a future MCP
surface) is invisible to the walk while the guard stays green — contrary to
the decision record's claim that it walks every invocation and attributed
type.

ModuleLedgerScanner.UndeclaredDefineConstants enumerates every *.csproj under
src/ plus the repo-root Directory.Build.props, collects each
<DefineConstants> value (skipping the $(DefineConstants) inheritance
placeholder), and reports any symbol not in ParseOptions.PreprocessorSymbolNames.
It does not add a symbol to the parse or read an inactive branch; it only
fails the walk-trust check the moment a project defines one the fixed list
does not carry, which is the signal to add it to ParseOptions before the
guarded code can hide behind it. Wired into AdapterTierScanner.Evaluate only
in this PR, as a sibling scanners can adopt later.

Verified with a real-tree mutation: added <DefineConstants>MCP</DefineConstants>
to src/Cluckwork.Api/Cluckwork.Api.csproj, confirmed RED with the exact
"add it to ModuleLedgerScanner.ParseOptions" message, reverted with
`git checkout --` and confirmed `git status` clean. Evidence in
/tmp/514/mutations-843/7-csproj-defines-mcp.txt.
@mforce
mforce force-pushed the chore/843-adapter-tier branch from d61836a to 7cae308 Compare September 15, 2026 04:27
@mforce

mforce commented Sep 15, 2026

Copy link
Copy Markdown
Owner Author

Round 1 fixes pushed, and the branch is replayed onto main after #879 merged (d61836a → 7cae308).

Finding Fix Commit Verification
1 surface unvalidated AdapterTier.KnownSurfaces is one map from surface to the privilege it grants; the registry validates a row's surface against it and the scanner's call walk reads the same map f46f1dd UnknownSurface_IsRegistryError
2 using alias hides the attribute file, namespace-block and global using aliases are resolved syntactically (global ones per project) before the attribute match c503762 three fixture tests; real-tree mutation with a local alias to a locally declared McpServerToolTypeAttribute went red ToolTypeOutsideTier
3 inactive #if branches ModuleLedgerScanner.UndeclaredDefineConstants reads <DefineConstants> from every src project and the root props file; any symbol the parse options do not carry fails the walk with the symbol to add. No project defines one today bd6a2d0 two fixture tests; real-tree mutation adding <DefineConstants>MCP</DefineConstants> to the Api project went red naming it

Also 7cae308 drops narration comments. Architecture and Documentation guards 252/252 (was 246). Round 2 goes to the same reviewer on this head.

@mforce

mforce commented Sep 15, 2026

Copy link
Copy Markdown
Owner Author

Review round 2 (Codex gpt-5.6-sol), head 7cae308: changes requested, 3 findings, all accepted. Round-1 fixes 1 and 2 verified complete by the reviewer's own re-run of its mutations; fix 3 verified but incomplete.

  1. P2, the define-constant check misses src/Directory.Build.props and imported .props files. MSBuild imports them automatically, so #if MCP code could compile unseen while the trust check stays green. Fix in flight: no import resolution; the check over-approximates instead and reads every .csproj, .props and .targets in the repo, red on any unknown constant wherever and under whatever condition it sits. Mutations for both file forms.
  2. P2, alias chains. using A = ...McpServerToolTypeAttribute; using B = A; and [B] outside the tier still hid the type. Fix in flight: alias targets resolve to a fixed point within scope, bounded so a cycle terminates, with chain and cycle tests and a real-tree mutation.
  3. P3, the surface-to-privilege map's values were never read. Fix in flight: a row's privilege must equal the privilege its surface maps to, and the allowed privilege set derives from the map.

Product-defect tally for the stop rule: round 1 found two real guard holes, round 2 found two more (props import, alias chain). The loop continues to round 3.

…ving imports (#843)

codex-sol round 2 finding 1: UndeclaredDefineConstants only read src/**/*.csproj
plus the repo-root Directory.Build.props, so a nested Directory.Build.props or an
imported .props file could define an undeclared symbol invisibly. Walk every
*.csproj/*.props/*.targets under the repo root instead (skipping bin/, obj/,
node_modules/, .git/, web/), matching wherever the element sits, Condition or not.
…ol attribute (#843)

codex-sol round 2 finding 2: an alias whose target was itself an alias (e.g.
`using ToolMarker = ActualMarker;`) matched against ToolAttributes only one hop
deep and never matched, so a tool type reached through such a chain was invisible
to the outside-tier check. Resolve local aliases then global aliases at each hop
to a fixed point, bounded by the alias count so a cycle terminates.
…843)

codex-sol round 2 finding 3: KnownSurfaces mapped a surface to the privilege
it grants, but nothing read the value — the loader checked privilege against
the separate DirectRepositoryPrivilege constant, and the closed set it
validated against was hardcoded rather than derived from the map. Changing
MapMcp's mapped privilege would have left every check and test green. Derive
the allowed-privilege set from KnownSurfaces.Values, and add a registry error
when a row's privilege does not equal KnownSurfaces[row.Surface], naming both.
RenderRow's suggested-row template now reads the same map instead of the
constant, so the two cannot drift from each other either.
@mforce

mforce commented Sep 15, 2026

Copy link
Copy Markdown
Owner Author

Round 2 fixes pushed (7cae308 → e255139).

Finding Fix Commit Verification
1 props and imported files unseen no import resolution; the check reads every .csproj, .props and .targets in the repo (skipping bin, obj, node_modules, .git, web) and is red on any unknown constant, Condition or not 75670ac three fixture tests; real-tree mutations src/Directory.Build.props and tools/mcp.props defining MCP both went red
2 alias chains alias targets resolve to a fixed point, local then global per hop, bounded by the alias count so a cycle terminates e0fba9e chain-of-two, global-then-local and cycle tests; real-tree two-alias chain outside the tier went red ToolTypeOutsideTier
3 map values unread a row's privilege must equal KnownSurfaces[surface]; the allowed privilege set derives from the map's values; the suggested row reads the map d085e52 PrivilegeDoesNotMatchSurfaceMapping_IsRegistryError; mutating the map value went red with two registry errors

e255139 drops narration comments. Guards 259/259 (was 252). Round 3 goes to the same reviewer on this head.

@mforce

mforce commented Sep 15, 2026

Copy link
Copy Markdown
Owner Author

Review round 3 (Codex gpt-5.6-sol), head e255139: approve, 0 findings.

The reviewer re-ran its own round-2 mutations (a src/Directory.Build.props defining MCP; a two-alias chain outside the tier) and both went red as claimed, and it names the remaining gaps as exotic for this repo: an imported build file under a skipped generated or frontend directory, a directory-symlink cycle, or a generic or array type used as an attribute alias. Guards 259/259.

Review yield over three rounds: 9 findings, 9 fixed with tests and real-tree mutations, 0 dismissed. Rounds 1 and 2 each found real holes in the guard (alias forms, unvalidated surface, MSBuild constants); round 3 found none. Loop stopped at the approval per the review rules. Ready to merge once CI finishes.

@mforce
mforce merged commit 5b85c3a into main Sep 15, 2026
16 checks passed
@mforce
mforce deleted the chore/843-adapter-tier branch September 15, 2026 05:46
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.

[A] #514 slice 2: adapter-tier rows for the MCP and OpenIddict surfaces

1 participant