Commit Graph

4 Commits

Author SHA1 Message Date
Michał Pierzchała a79f0d2e81 feat: add symbol-aware depgraph authority overlay (#2157)
* feat: add depgraph authority overlay

* fix: centralize depgraph authority declarations

* refactor: make authority overlay declaration-driven
2026-08-31 11:54:16 +02:00
Michał Pierzchała 76453add71 refactor: pnpm workspace + @agent-device/kernel pilot (#1490 W0) (#1494)
* refactor: pnpm workspace + @agent-device/kernel pilot (#1490 W0)

Extend the workspace with packages/* and move the kernel behind an
enforced public API: packages/kernel with nine consumer-earned subpath
exports (errors, device, snapshot, contracts, collections, rect,
redaction, daemon-error, bounds — the last absorbed from utils as Rect
vocabulary). Every kernel import repo-wide becomes the
@agent-device/kernel/<sub> specifier; kernel tests move to
src/__tests__/kernel/ and exercise the package surface. The root
declares the package in devDependencies (workspace:*), tsdown bundles
it (noExternal) so the published artifact and its runtime dependency
manifest are unchanged.

Gate rewiring in the same change, per the W0 brief:
- R1 kernel-sink retires (physically subsumed); new R11
  package-boundaries guards no-root-back-imports, relative tunnelling
  past exports maps, undeclared workspace deps, and non-exported
  subpaths, with runtime resolution pins via import.meta.resolve.
- resolveImportEdges and mutation ownership follow workspace
  specifiers through exports maps, keeping R4 cycle checks, depgraph,
  and derived test ownership connected across the seam (kernel-errors
  still owns 495 tests). listSourceFiles includes packages/*/src.
- kernel becomes an unranked zone; mutation registry, stryker mutate
  globs, and the mutation-affected workflow path filter move to
  packages/kernel/src/errors.ts.
- check:affected gains packages/ ownership (manifests fail open);
  vitest and coverage include packages/*/src; fallow ignores
  packages/** (its resolver cannot follow workspace specifiers).
- The affected-selector CI job installs dependencies: its closure now
  crosses workspace specifiers, and the R8 relative exception is
  unsafe for production src files (Node ESM does not realpath, so dual
  specifier/relative loads would instantiate modules twice). The R8
  zero-dep set is pinned empty with that rationale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* fix: address W0 review — mutation sandbox, exports-map resolution, tsc -b

Review findings on #1494, all five:

1. contracts-schema-public.test.ts reads the kernel source at its
   packages/ path (fs access invisible to the codemod and typecheck).
2. Mutation lane: Stryker sandboxes the tree but pnpm's node_modules
   symlink resolves @agent-device/* back to the real repo, so mutants
   in the sandbox never load and vitest.related finds no tests.
   vitest.mutation.config.ts now aliases each EXPORTED specifier to
   its source (derived from exports maps, never a wildcard), keeping
   resolution inside the mutated tree. Validated: kernel-errors module
   runs end to end (dry run 3,984 tests, mutants killed, exit 0).
3. Layering/depgraph resolve workspace specifiers through the
   exports-derived map (workspaceSpecifierTargets) instead of
   reconstructing paths, so '.'-facade packages resolve; the
   positional fallback remains only for map-less fixtures (P0 pin).
4. Per-package project references implemented: packages/kernel is
   composite (emitDeclarationOnly -> dist-types, gitignored), the root
   references it, and typecheck becomes tsc -b — probed to catch type
   errors on both sides under TypeScript 7 native.
5. R11's relative-route exception now requires membership in an actual
   R8 zero-dep job closure (zeroDepClosureFiles walks entries), not
   mere scripts/ placement — closing the dual-instantiation bypass.

Also from review discussion: daemon-error moves out of the kernel
package to src/client/ — its consumers (cli, client facade) rehydrate
wire DaemonErrors client-side; the daemon only produces them. Kernel
drops to 8 exported subpaths before any of them ship.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* refactor: one exports-map reader for mutation alias and ownership

Fallow flagged workspaceExportAliases (cognitive 15, CRAP 90). The
manifest-reading logic already exists as workspaceSpecifierTargets in
scripts/layering/package-boundaries.ts, so both the Stryker sandbox
alias table and the mutation ownership walker now consume it instead
of carrying near-clones. Behavior unchanged; mutation suite 45/45 and
changed-code fallow green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* fix: composite kernel without a root references edge

FreeRange runs plain `tsc -p tsconfig.json`, and a root `references`
entry makes non-build-mode TypeScript demand the referenced project's
built declarations (TS6305) — a standing "build first" tax on every
plain -p consumer (fr, editors). Keep the per-package composite
project and build it in typecheck (`tsc -b packages/kernel` before the
root and examples/sdk passes), but drop the root references edge: root
consumption resolves through exports to source, identical to runtime
and to the bundler. Probed: plain -p green with no prebuilt output;
kernel-side type errors still caught by its own build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* fix: R11 uses the layering parser; mutation config is a fallow entry

Review blockers on #1494:

- R11's private single-quote regex could miss a double-quoted or
  re-export route into packages/*/src. specifierSites now delegates to
  the layering model's parseImports (both quote styles, side-effect
  imports, re-exports, dynamic imports), with direct regressions for
  each formerly-invisible form.
- vitest.mutation.config.ts becomes a declared fallow entry instead of
  a tolerated unused-file finding: the full-repo audit now reports it
  reachable (unused files 2 -> 1; the remainder predates this PR).

FreeRange clean-checkout evidence: with packages/kernel/dist-types and
every *.tsbuildinfo deleted, `pnpm check:freerange` reports 0 findings
on this head — the TS6305 topology died with the root references edge
in the previous commit; check:freerange has no build precondition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-30 12:12:46 +02:00
devin-ai-integration[bot] edca35d122 chore(deps): Renovate config, packageManager-derived pnpm in CI, repo-wide format (#1444)
* chore(deps): add Renovate config and enforce packageManager pnpm version in CI

Refs #1422

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore: bump pnpm to 11.17.0 and format the whole repo with oxfmt

format/format:check drop their hand-maintained path list: oxfmt already skips
node_modules and honors .gitignore, so the only exclusion list is
.oxfmtrc.json ignorePatterns.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(mutation): accept either quote style in the affected-lane path filter

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore(deps): keep fixture-app runtime deps as individual Renovate PRs

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-28 13:50:39 +02:00
Michał Pierzchała f19864e486 feat(scripts): dependency-graph report over the layering gate's model (#1410)
* feat(scripts): dependency-graph report over the layering gate's model

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

* ci(layering): assert the depgraph report reproduces the gate's baseline

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

* fix(depgraph): make the source reviewable, and stop overclaiming removability

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 13:59:53 +02:00