feat(scripts): dependency-graph report over the layering gate's model - #1410
Conversation
Reports what the layering gate deliberately does not enforce, as JSON plus a short
summary. No renderer: the productive artifact is the JSON.
pnpm depgraph # -> .tmp/depgraph/graph.json + summary
pnpm depgraph:test
Dependency graph: 898 files, 4627 edges, 25 zones
value-import cycles (R4): 0
type-only/dynamic cycles (not gate-rejected): 8
spine back-edges (R5): 0
type-only spine inversions (R6): 42
transitively redundant value edges: 1338
The two numbers worth having are the ones CI cannot give you. Transitively
redundant value edges — where the target is still reachable at distance >= 2, so
the direct import changes nothing about what the module can see — need a real
reachability pass, not a grep. And cycle detection over type-only and dynamic
edges covers the loops R4 excludes by design. Both are candidate lists, never work
lists; at ~1300 the redundancy set is a place to look.
It reuses scripts/layering/model.ts, the same module check.ts uses in CI, so the
file set, zone partition, edge kinds and cycle definition are the enforced ones. A
second extractor would describe a graph nobody gates. Consequence worth having:
its R6 count reproduces TYPE_INVERSION_BASELINE, so a mismatch means one of the two
is stale.
This is the analysis half of a viewer that was built and dropped. The render cost
~2200 lines and needed a Fallow exemption for a 920-line canvas file, and nobody
read it. Everything here clears the repo's bar with NO exemption — scripts/depgraph
is deliberately absent from ignorePatterns, unlike scripts/layering, scripts/perf
and scripts/maestro-conformance.
Getting there meant fixing rather than suppressing: extracted `valueSuccessors`
(the value-edge adjacency was built identically in two places — a real clone),
split `buildGraph` into four named aggregation steps, split
`reachableBeyondDirectEdge` out of `markRedundantEdges`, extracted
`compareZoneEdges`/`crossedZonePair`, extracted `edgeKindCode`/`edgeFlags` from a
nested ternary scoring CRAP 42, and deleted `fileGroup` plus the `group` node field
once the cluster layout went.
Two additive exports on scripts/layering/model.ts: `zoneRank` and `targetDagZone`
(previously module-private). The gate's behaviour is unchanged.
`pnpm check` green, 4488 unit tests, 5 model tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
|
Not ready to merge. The renderer/Fallow-exemption removal is a good scope reduction, and #1409's HTML/JSON overwrite bug is structurally gone, but two P1s remain:
Two P2 validation/docs issues also need correction:
Keep |
The report reads the same model as the gate, so its inversion count must equal TYPE_INVERSION_BASELINE. That agreement was previously a nice property nobody checked; the Layering Guard job now runs scripts/depgraph/model.test.ts, so the two cannot be green independently. Verified by bumping a baseline entry by one and confirming the job fails with a message naming the fix. The count feeding the check is computed by `typeInversionsByPair`, which applies the gate's rule — once per FILE pair, over the raw resolved edges — rather than reading the collapsed edge list. That matters: `collapseEdges` keeps one edge per pair with the strongest kind winning, and `dynamic` outranks `type`, so a module imported both lazily and for its types would collapse to `dynamic` and drop out of the count. No such pair exists today (measured: 0 of 42 inverting pairs), but a number wired into a CI equality check must not be able to drift for a reason unrelated to layering. Stated honestly in the README and the test: this is a cross-check of the report's extraction and the baseline against the real tree, not two independent algorithms. The gate remains the authority — if they disagree, the baseline or the tree is wrong, never the test. TYPE_INVERSION_BASELINE is now exported for this purpose. Not done here, deliberately: the ~1338 transitively redundant value edges are a candidate for a loose growth-only ratchet later. They are a candidate list, not a work list, and a hard count would be noise. `pnpm check` green, 4488 unit tests, 6 depgraph model tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur
|
Re-reviewed exact head
Keep |
…s proposed P1: the section referenced scripts/depgraph/ as present and #1410 as kept — that PR is open and this tree has no depgraph directory. Both the acyclicity and component-metrics bullets now name it as proposed (#1410), not landed. P2: enforcement claims scoped to the concrete guards: publicPlatformString gates one serialized-output DTO class, runnerProvider is one adopted seam, R7 covers SessionState only, and boundaries-are-earned plus stable-interface testing are labeled norms (gated only at the test-only-DI-seam slice). The intro now says explicitly that bullets distinguish gate, ledger, and norm.
…vability
Four review findings, all of them real.
P1 - the implementation was binary. scripts/depgraph/model.ts contained two raw NUL
bytes used as map-key delimiters, so Git classified a ~346-line file as binary and
hid its entire diff behind `- -`. Replaced with a unicode escape: identical at
runtime, textual on disk. I had seen the symptom repeatedly - every grep on that
file printed "binary file matches" - and worked around it with python instead of
asking why, which is how it survived to review.
Guarded repo-wide rather than for this one file: a new test asserts no tracked .ts
under src/ or scripts/ contains a raw NUL, verified by reintroducing one and
watching it fail. Nothing else would catch a recurrence, and the failure mode is
silent - the code works, the review does not.
P1 - "transitively redundant" claimed removability it cannot support. Module
reachability does not carry bindings: if `a` imports `{ c }` while `b` only
re-exports it as `{ c as b }`, the path a -> b -> c exists and deleting a -> c still
breaks `a`. The fixture in model.test.ts is exactly that shape and its comment said
"removable". Reachability also says nothing about when a module's side effects run.
Renamed throughout to what it measures - `transitivelyReachable`,
`markTransitivelyReachableEdges`, and a summary line reading "value edges whose
target is also reachable at distance >= 2 (reachability only - not a removability
claim)". The caveats and the counterexample are now stated in the marker function,
the fixture comment and the README, and symbol-level analysis is named as what
deciding any individual edge would actually require.
P2 - build.ts had no coverage. Every test exercised model.ts, so the CLI could break
its output path, wire shape or summary silently. Added three subprocess tests:
default path plus summary-agrees-with-payload, `--out` honoured and valid JSON
written, and a trailing `--out` falling back rather than crashing (pinned so it is a
decision, not an accident). `pnpm depgraph:test` now runs inside `check:tooling`, so
`pnpm check` covers it.
P2 - README was wrong three ways: it queried `.tmp/depgraph/index.json` after the
output moved to `graph.json` (the documented command failed as written), it derived
inversions from collapsed `zoneEdges`, which can undercount, and it claimed both
that the report runs in CI and that nothing here runs in CI. The query now reads
`typeInversions` and was run verbatim; the CI sentence names exactly which single
test runs and states that nothing else gates a merge.
pnpm check green, 4488 unit tests, 10 depgraph tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur
|
All four addressed in P1 — binary source. Two raw NUL bytes used as map-key delimiters, now a unicode escape: identical at runtime, textual on disk. Worth admitting how this survived: I saw the symptom repeatedly — every P1 — removability overclaim. You're right, and my own fixture proved it: Renamed to what it measures — The three caveats (bindings aren't carried, side-effect timing isn't preserved, a direct import is often deliberate) are stated at the marker function, on the fixture, and in the README, with your P2 — no P2 — README. Three errors, all real: it queried One thing I did not change, flagged in case you disagree: Generated by Claude Code |
|
Re-reviewed exact head |
…ers (#1428) * docs: map architecture principles to their enforcing gates and ledgers in CONTEXT.md Writes down the rule → gate → ledger mapping (dependency rule → layering R1-R3/spine + R6 ratchet; acyclicity → R4 + depgraph tolerated-cycle report; policy×detail → the two registries + leak guard + transport seam; information hiding → R7; boundaries-are-earned with the #1409/platform-descriptor evidence; component metrics as observatory data per #1423) so contributors and agents get the why behind the guards they hit, in one place. * review: scope principle claims to actual enforcement; mark depgraph as proposed P1: the section referenced scripts/depgraph/ as present and #1410 as kept — that PR is open and this tree has no depgraph directory. Both the acyclicity and component-metrics bullets now name it as proposed (#1410), not landed. P2: enforcement claims scoped to the concrete guards: publicPlatformString gates one serialized-output DTO class, runnerProvider is one adopted seam, R7 covers SessionState only, and boundaries-are-earned plus stable-interface testing are labeled norms (gated only at the test-only-DI-seam slice). The intro now says explicitly that bullets distinguish gate, ledger, and norm.
Summary
The analysis half of the graph tooling, without the renderer. Emits the production import graph as
JSON plus a short summary of what the layering gate deliberately does not enforce.
392 lines,
pnpm checkgreen, and — the part that matters — it needs no Fallow exemption.Why keep this when the viewer was dropped
#1409 rendered the same data and was closed: ~2200 lines plus an exemption for a 920-line canvas
file, and nobody drew a conclusion from the picture. This is the half that computes things you
cannot get another way:
≥ 2, so the direct import changes nothing about what the module can see. That needs a real
reachability pass, not a grep. A candidate list, not a defect list: a direct import is often
clearer than a re-export chain, and at ~1300 it is a place to look, never a work list.
is free at runtime; a dynamic one is a deliberate cold-start seam). 8 of them.
lvl(longest path to a sink), per-zone size — blast radius and"which boundary is big", in one query.
It reuses the gate's model
Extraction is
scripts/layering/model.ts, the same modulecheck.tsuses in CI, so the file set,zone partition, edge kinds and cycle definition are the ones actually enforced. A separate extractor
would describe a graph nobody gates.
That gives a free consistency check: the R6 count reproduces
TYPE_INVERSION_BASELINE. If theydisagree, one of the two is stale — and the gate is the one that wins.
No exemption, and what that took
scripts/depgraph/**is deliberately absent from Fallow'signorePatterns, unlikescripts/layering/**,scripts/perf/**andscripts/maestro-conformance/**. Getting there meantfixing findings rather than suppressing them:
valueSuccessors— the value-edge adjacency was built identically inmarkRedundantEdgesandcomputeLevelsbuildGraph81 lines / 20 cyclomaticindexCyclesByFile,buildNodes,aggregateZones,aggregateZoneEdgesmarkRedundantEdges20 cognitivereachableBeyondDirectEdgeaggregateZoneEdgesCRAP 31.6compareZoneEdgesandcrossedZonePairedgeKindCode/edgeFlagsbuildPayload/main; deletedfileGroupand thegroupnode field, both dead once the layout wentTwo additive exports
scripts/layering/model.tsgainszoneRank(so a consumer can tell an inversion from an ordinaryedge without re-deriving the spine) and exports
targetDagZone, previously module-private. Thegate's own behaviour is unchanged.
Validation
pnpm checkgreen — lint, typecheck, R1–R8, production exports, MCP metadata, build, bundle ownerfiles, Fallow with no exemption, 4488 unit tests, plus 5 tests on the graph model.
Note
docs/dependency-graph-findings.mdonmaindocuments an inline throwaway probe for the samequeries, added when the viewer was dropped. That stays useful for one-off questions; this is for the
two computations a probe cannot cheaply reproduce. If you would rather keep only the probe, this PR
is the thing to drop — the doc does not depend on it.
Generated by Claude Code