Commit Graph

10 Commits

Author SHA1 Message Date
Michał Pierzchała 006f2d9f60 chore(gates): layering baselines ratchet against merge-base (#2299)
* refactor(layering): ratchet R6, R9 and R10 against the merge-base tree

R6 type-spine inversions, R9's largest type cycle and R10's R7 ownership
pressure now compare the working tree with the same measurement taken over
the merge-base with origin/main, read through the shared committed-tree
reader (one git ls-tree, one git cat-file --batch, no second checkout).
Growth still fails with the same message shape, a shrink needs no edit, and
no change can bank headroom by leaving a number above the tree.

R9's per-zone check gains membership from the reference, so the overflow
message names the file that joined instead of listing the whole zone.

* chore(gates): delete the R6, R9 and R10 pins the merge-base now supplies

TYPE_INVERSION_BASELINE, LARGEST_TYPE_CYCLE_ZONE_CEILINGS, TYPE_CYCLE_BASELINE
and DAEMON_MODULARITY_BASELINE.sessionState were the hand-edited references
these three ratchets compared against. The merge-base measurement replaces
them, so there is no number left to leave above the tree and no entry to raise.
externalDaemonTypesImporters stays: it names files, not a count.
2026-09-05 20:48:46 +02:00
Michał Pierzchała a5a7f6dfa1 docs(layering): kill criteria on every rule module (#2244)
Adds a four-line Catches/Evidence/Cost/Kill-criterion header to every
layering rule module for R2, R4-R7, R9-R14, R16, R18, R19, R65-R73,
and the rule-id uniqueness gate, so each structural check states what
it catches, why no other gate sees it, its LOC cost, and the concrete
condition under which it gets deleted. No behavior change.
2026-09-03 11:13:15 +02:00
Michał Pierzchała b15121ffc8 test(ios): establish snapshot convergence baselines and permanent evidence (#2204)
* test(ios): add snapshot convergence evidence harness

* fix(ios): satisfy benchmark CI guards

* fix(ios): constrain benchmark proxy routes

* fix(ios-benchmark): enforce cell admission evidence

* fix(ios-benchmark): protect benchmark state ownership

* fix(ios-benchmark): use proxy port flag

* fix(ios-benchmark): let proxy choose an ephemeral port

* test(ios-benchmark): keep CLI process seam local

* fix(ios-benchmark): parse proxy startup envelope

* fix(ios-benchmark): bind proxy lease to simulator

* fix(ios-benchmark): keep fresh proxy CLI sessions isolated

* fix(ios-benchmark): preserve async timeout evidence

* docs(ios-benchmark): retain exact-head evidence

* test(ios): reveal offscreen alert fixture controls

* test(ios): reset alert between relaunch samples

* test(ios): admit native alert snapshots

* docs(ios): publish snapshot convergence corpus

* chore(ios): format benchmark evidence

* fix(ios-benchmark): admit proxy fixture anchors

* docs(ios): republish exact-head benchmark corpus

* fix(size): make publish asset evidence hermetic

* style(size): format package evidence test

* test(size): update publish preparation contracts

* fix: retire stale utils layering zone

* test: pin shared publish asset owner

* test: verify preserved size reporter closure

* fix: move mutation ownership to snapshot module

* test(ios): add snapshot convergence evidence harness

* fix(ios): satisfy benchmark CI guards

* fix(ios): constrain benchmark proxy routes

* fix(ios-benchmark): enforce cell admission evidence

* fix(ios-benchmark): protect benchmark state ownership

* fix(ios-benchmark): use proxy port flag

* fix(ios-benchmark): let proxy choose an ephemeral port

* test(ios-benchmark): keep CLI process seam local

* fix(ios-benchmark): parse proxy startup envelope

* fix(ios-benchmark): bind proxy lease to simulator

* fix(ios-benchmark): keep fresh proxy CLI sessions isolated

* fix(ios-benchmark): preserve async timeout evidence

* docs(ios-benchmark): retain exact-head evidence

* test(ios): reveal offscreen alert fixture controls

* test(ios): reset alert between relaunch samples

* test(ios): admit native alert snapshots

* docs(ios): publish snapshot convergence corpus

* chore(ios): format benchmark evidence

* fix(ios-benchmark): admit proxy fixture anchors

* docs(ios): republish exact-head benchmark corpus

* fix(size): make publish asset evidence hermetic

* test(size): update publish preparation contracts

* fix: keep git-state gates out of mutation sandboxes
2026-09-01 21:39:05 +02:00
Michał Pierzchała cc521decb8 test(structure): per-package eager-closure budgets — the ADR-0019 loading-shape probe (#1739) (#1965)
* test(structure): per-package eager-closure budgets (#1960)

ADR-0019 requires platform-package façades to stay implementation-lazy
and is explicit that a startup threshold alone is not a substitute for
preserving the loading shape. #1950 built the AST-level walker
(eager-import-closure.fixtures.ts) and proved the planted-red
procedure on one file (session-teardown.ts's android-helper
denylist); this generalizes it into a data-driven budget table so any
workspace-package façade -- or a designated hub module -- can get an
eager-closure ceiling without a bespoke test.

Seeds a budget for every packages/*/src/facades/*.ts file (discovered
the same way package-boundaries.test.ts discovers façades, not
hand-listed) from its measured current closure size, plus a
platform-implementation denylist for façades whose contract is
implementation-neutral vocabulary. Also demonstrates the mechanism on
two designated hub modules (cli.ts, session-teardown.ts) alongside
their existing, more specific ad hoc pins.

Closes #1960

* test(structure): derive facade roots from manifests, add edge chains, reseed tight

Review findings on #1965:

1. Discovery scanned only `packages/*/src/facades/*.ts`, which omits every
   package that publishes its entry surface straight from the manifest —
   including all six `packages/platform-*/src/index.ts` façades, the exact
   subject of ADR-0019's implementation-laziness rule. Discovery now derives
   from `readWorkspacePackages(...).exportTargets` and then adds `/src/facades/`
   files, reusing the R11 helper rather than reimplementing it so the two gates
   cannot disagree about what an entry surface is. The table grows from 14
   façades + 2 hubs to 95 entry surfaces + 8 hubs.

2. Budgets carried a few files of slack each. They are now exact ratchets with
   no headroom, matching how the repo pins R9/R10 and test-file size: growth is
   allowed, it just has to be a visible number change in the diff of the PR that
   causes it. Every budget is reseeded from post-#1969 measurement.

3. Violations printed a flat sorted set, which named the offender but not the
   route. `eagerClosureGraphOf` records each file's discoverer, so failures now
   print the transitive chain entry -> ... -> offender. `eagerClosureOf` keeps
   its contract and is expressed in terms of the new walk; per-file edges are
   memoized, which also cuts the existing pins' runtime (cli closure test
   2336ms -> ~550ms).

The platform-package façades evaluate exactly one module each — themselves —
so their budget of 1 is the tightest statement of "metadata-eager,
implementation-lazy" the walker can make.

* test(structure): make the eager-closure pins exact, bounded, and single-owner

Second review pass on #1965 found four holes, two of which were places the
PR text claimed a property the code did not have.

1. Rows were documented as exact ratchets but asserted with `<=`, so a shrink
   silently became headroom a later regression could grow back into. The
   comparison is now equality, in a pure `classifyBudget` with a separate
   message for each direction ("lower its pin to N in this PR so the ratchet
   keeps the gain"), matching test-file-size-ratchet.ts and the R9/R10 pins.

2. An over-pin failure printed a chain per evaluated module — 361 of them for
   src/cli.ts. It now prints a bounded attribution: the entry's heaviest direct
   edges (capped at 4) with a couple of representative deep routes each, ranked
   so a newly added import sorts first. The comment states plainly that this
   attributes by shortest import route and does NOT diff against a recorded
   baseline; naming a true delta would mean checking in ~1,500 module paths and
   rewriting them on every contracts refactor.

3. Discovery reimplemented a one-level `src/facades` scan while canonical R11
   discovery is recursive, so a nested façade file could be covered by R11 and
   silently missing here. `facadeEntryFiles` is now a single exported owner in
   package-boundaries.ts that both R11's façade gate and this table consume.

4. Rows were converted to Sets before any uniqueness check, so a duplicate was
   unobservable. The table is now two `Record<string, number>` literals keyed by
   path, making an in-record duplicate a TypeScript error (ts1117); the only
   remaining case — one path in both records — is asserted on the array.

Each of the four holes gets a test that fails when the rule is broken, since a
tree that happens to satisfy its pins cannot distinguish a correct rule from a
vacuous one. Writing those found a real bug in the duplicate check itself
(`Set.add` returns the Set, so the filter never matched).

Pins reseeded on 04e4c23b9.

* test(structure): restore tracked-only layering discovery and bound platform diagnostics

Two findings from the third #1965 review.

1. R11 REGRESSION, introduced by the previous commit in this PR. Extracting one
   recursive facade-discovery owner was right, but it was implemented as a raw
   filesystem walk, which silently changed R11 from tracked-source discovery
   (listSourceFiles) to "whatever is on disk". A layering gate describes
   committed state, so an uncommitted scratch facade under a scanned path must
   be invisible; instead it could fail both R11 and the budget gate on a
   contributor's working directory. Worse, the test asserted the new behaviour,
   pinning the regression in place.

   The tracked-source enumerator now lives in a leaf module,
   scripts/layering/tracked-sources.ts, that both check.ts and
   package-boundaries.ts consume (check.ts imports package-boundaries.ts, so the
   dependency cannot run the other way). facadeEntryFiles reads
   listTrackedProductionSources, keeping the single recursive owner while
   restoring tracked-only scope.

   The temp-dir test is replaced by a real git fixture following
   platform-package-repository.test.ts: a tracked nested facade that must be
   discovered, plus untracked siblings — one beside it and one beside the nested
   file — that must not be. Reverting to the filesystem walk turns it red.

2. Only the numeric-pressure diagnostic was bounded; the platform-implementation
   assertion still emitted every offender with a full chain, so one eager
   platform subtree buried the single import that caused it under hundreds of
   lines. Both diagnostics now share one renderer that groups targets by the
   entry's own import they arrived through, caps edges and routes, and states
   the omitted counts. A wide planted-red fixture (300 offenders under one
   owning edge plus extra edges) proves the cap engages rather than assuming it.

src/cli.ts reseeded 361 -> 362 for drift from the rebase onto 7aaa559e2.

* test(structure): close the manifest half of tracked-only discovery

The previous commit intersected the `src/facades/` walk with tracked sources
but left manifest-derived entry surfaces bypassing that intersection entirely
— the same bug class, on the path it did not close. Two distinct leaks:

1. `readWorkspacePackages` enumerated `packages/` with `readdirSync`, so an
   uncommitted scratch package contributed a name, export targets, and
   dependency edges to every R11 rule built on it, not just to facade
   discovery. Closed at the source: it now reads tracked manifests via
   `listTrackedPackageManifests`, mirroring how R13's
   `readTrackedPlatformPackageDeclarations` has always enumerated its own.

2. A TRACKED manifest can still name an uncommitted target in its working-tree
   content, which the trailing `existsSync` admitted. `facadeEntryFiles` now
   intersects manifest-derived targets with the tracked production set as well,
   so every path it returns is committed whatever its origin.

Three fixtures, each verified failing before the fix and passing after:

- `readWorkspacePackages reads tracked manifests only` (in R11's own suite,
  since leak 1 affects every rule that reads packages, not only this gate) —
  red when manifest enumeration reverts to `readdirSync`.
- `an untracked PACKAGE contributes no entry surface` — red when both fixes
  are reverted.
- `a dirty manifest naming an UNTRACKED target contributes no entry surface` —
  red when the target intersection is dropped.

The shared git fixture builder is factored out of the existing discovery test
so all four use one committed baseline.

Pins reseeded on 8d280a208: six new contracts entry surfaces from #1993, which
the exhaustiveness gate caught on its own, plus dispatch 100 -> 94 and
platform-runtime 31 -> 36.
2026-08-24 12:40:02 +02:00
Michał Pierzchała ef6ec2995b chore(layering): document R12/R18/R19, retire R8, make R9 shrink mandatory (#1781 A6) (#1825)
* chore(layering): document R12/R18/R19, retire R8, make R9 shrink mandatory (#1781 A6)

The A6 review kept `check:layering` in full (15/15 planted violations fired,
no other enforcer exists) and left four follow-throughs.

R12 bin-alias-fast-path, R18 contracts-implementation-authority and R19
selector-pipeline-ownership were live rules with no ADR or CONTEXT anchor —
they now carry one each, in the same list as R7/R9/R10/R13.

R8 zero-dep-job-closure is retired: no CI job sets `install-deps: false` and
ci.yml records why each keeps it enabled, so the invariant has no subjects.
R11's relative-into-packages exception existed only because a zero-dep closure
cannot coexist with specifier loads, so it retires with R8; the route is now
closed to every caller. R1 was retired the same way at #1490.

R9 was growth-only and merely suggested lowering the ceiling, which is
headroom the next change spends without a number moving. It is now an equality
pin like R6 and the R10 R7 counts, and the committed baseline drops 47 -> 46
(daemon-server ceiling 17 -> 16) to match the measurement.

ADR 0019 §6 now says each runtime-command-cutover row is deleted when that
command's migration is declared closed.

* chore(layering): rename R9 to type-cycle-size now that it fails both ways (#1781 A6)
2026-08-18 15:35:46 +02:00
Michał Pierzchała 83322a3f2f test(layering): pin exact façade symbols for all workspace packages (#1574)
* test(layering): pin exact façade symbols for all workspace packages

#1555 added the repo's first exact exported-symbol gate, pinning
@agent-device/ad-replay's named export list. Every other workspace
package was still covered only by the exports-subpath locks, which
prove which files a package exposes but say nothing about what those
files name — so any façade could grow a symbol silently.

Pin all 29 exported subpaths across the remaining 8 packages:
ad-script, contracts (14), kernel (8), maestro, provider-limrun,
provider-webdriver, replay-test, and xml. The lists are the honest
current surface, untrimmed — contracts/interaction alone names 140
symbols, and pinning the real number is what makes the next widening
visible. The table is checked in both directions, so a new package or
subpath that nobody pinned fails rather than being silently skipped.

Pinning contracts needed the export-discovery helper widened: 13 of
its 14 façades are bare `export * from '../x.ts'` barrels, and
readNamedExports throws on those by design, because given only a
source string the contributed set is genuinely unknowable. Given the
FILE it is not, so readFacadeExports resolves the relative re-export
chain and enumerates it. Resolution stays narrow — a package-specifier
star still throws (that would mean re-entering another package's
exports map, the unbounded widening the gate exists to refuse), cycles
are visit-guarded, and a default export still throws through a barrel.

Helper unit tests cover the shapes the merged AST scan handles but
left unpinned: `export { default as x }` (the form between the two
rejection rules — named, so reported, never `default`), a local
`export { … }` list with no `from`, and multi-declarator
`export const a = 1, b = 2`.

Plant-verified per package rather than asserted: a stray export on
ad-script, one two files deep behind contracts' `export *` chain, and
an unpinned new subpath on xml each failed with a named diff; each
reverted to green.

Gates: check:layering (63 tests, up from 53) / typecheck / lint /
format:check — green.

* fix(layering): model real `export *` semantics; split the pinned table out

Addresses both P1 findings on #1574.

P1 — `readFacadeExports` did not model `export *` façade semantics. It
unioned every child name and threw on every child default. Both are
wrong:

- Per GetExportedNames, a star export excludes the child's `default`,
  so a private `export default` in a leaf is not reachable through the
  barrel and does not widen the façade. It is now passed over rather
  than rejected; the previous test codified that false positive and is
  replaced. A default on the ENTRY file is still a real default export
  of the façade and still throws.
- Per ResolveExport, a name two star sources resolve differently is
  `ambiguous` — importing it is a SyntaxError, so it is not part of
  the surface at all. Unioning would pin a symbol no consumer can
  import; ambiguity now throws and names both origins.

Origins are tracked by declaring module rather than by path taken, so
a diamond (two barrels reaching one declaration) resolves normally,
and an explicit export shadows a star-provided name of the same name
as the spec's own precedence does. Both counterfactuals are tested
alongside the two rejection cases.

P1 — module size. The 885-line generated FACADE_SYMBOLS table moves to
a focused sibling, scripts/layering/facade-symbols.ts, leaving the
behavioral tests at 642 lines (from 1,455) so the test file stays one
bounded read per AGENTS.md.

Gates: check:layering (66 tests, up from 63) / typecheck / lint /
format:check — green. Contracts plant re-verified under the corrected
semantics: a stray two files deep behind the `export *` chain still
fails with a named diff, and reverts to green.

* fix(layering): resolve re-export identity transitively; extract facade-exports

Addresses both P1 findings on the second review round.

P1 — named re-export identity stopped at the immediate source. Given
`a` re-exporting `x` from `b`, `c` re-exporting `x` from `a`, and a
façade starring both, ESM resolves ONE binding (b's `x`), but the
walker identified the two paths as `b#x` and `a#x` and falsely
rejected the façade as ambiguous. Reproduced before fixing.

Origins now resolve through the chain to the binding a name ultimately
names, by asking the child's own already-resolved map instead of
synthesizing an identity from the specifier. A package specifier keeps
a stable synthetic identity (it is not a file this gate reads), and a
cycle in progress falls back to the immediate source.

Two tests, counterfactual-verified against each other: the chain
diamond now resolves to one name (confirmed failing with the old
immediate-source identity, passing with the fix), and a same-depth
chain whose branches bottom out in two genuinely distinct declarations
still throws — so the fix cannot be satisfied by simply collapsing
every duplicate.

P1 — context-safety extraction was incomplete. Façade export
enumeration moves to scripts/layering/facade-exports.ts (219 lines)
with its own facade-exports.test.ts (245), registered in
check:layering. package-boundaries.ts drops to 338 from 528 and its
test file to 450 from 642: the boundary rules answer "may this file
import that one?", this module answers "what does this façade name?".
Every layering file is now under the 500-line tripwire except the
generated symbol table, which the rule exempts.

Gates: check:layering (68 tests, up from 66) / typecheck / lint /
format:check — green. Contracts plant re-verified after the split.

* fix(layering): filter `default` at the star, not at its source

The reported P1 does not reproduce: intermediate `export { default }
from './x.ts'` links are reported by oxc as kind `Name` with the name
`default`, not kind `Default`, so they already resolve transitively;
and for a terminal `export default <decl>`, the fallback identity
`${child}#default` is exactly the canonical binding, so both paths
agree. The exact five-module scenario from the review returns ['x'].
That behavior is now pinned by a test so it cannot silently regress.

Investigating it did surface a real spec violation in the opposite
direction. Because a re-exported `default` is a named entry, it landed
in the module's map and was then copied wholesale by star enumeration,
so `export * from './mid.ts'` reported `default` as part of the
surface — a name `GetExportedNames` explicitly skips, and which oxc
itself labels `AllButDefault` on the star's own import.

`default` is now filtered at the star rather than at the source. That
placement is the point: the name has to stay in the module's map so a
later `export { default as x }` can resolve its binding, while never
being reachable through a star. Filtering at the source would have
broken identity resolution — the very thing the review round before
this one fixed.

A façade entry re-exporting a default under the name `default` is now
rejected too. It carries a default export exactly as `export default …`
does; only the parse shape differs, and only the declared form was
being caught.

Three tests: the star filter (counterfactual-verified — removing the
filter fails it — with a sibling name proving the module is still
read), entry-level rejection, and the two-paths-to-one-default-binding
case from the review.

Gates: check:layering (71 tests, up from 68) / typecheck / lint /
format:check — green. Contracts plant re-verified.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 10:34:57 +02:00
Michał Pierzchała 761317deb7 refactor(daemon): extract native .ad replay to packages/ad-replay (#1478 P5) (#1555)
* refactor(replay): move the dependency-free engine leaves into packages/ad-replay

Stage A of the #1478 P5 extraction: vars, plan-digest (+canonical-json,
sole consumer), the target-identity classification core, report-action,
and suggestion-ranking move verbatim; imports updated. The package facade
temporarily re-exports the moved symbols so root consumers keep compiling;
a later stage narrows it to inspectAdReplay/runAdReplay only.

* chore(layering): register packages/ad-replay in the workspace and DAG

* refactor(replay): define the three-operation replay selector port with dual adapters (#1478 P5)

* refactor(daemon): route replay handlers through the selector port (#1478 P5)

* refactor(replay): split target verification into engine policy and daemon authority (#1478 P5)

* refactor(replay): move the .ad step loop behind inspectAdReplay/runAdReplay (#1478 P5)

* refactor(replay): lock the ad-replay façade to its real consumers (#1478 P5)

* test(replay): prove shared-id demotion on both selector-port adapters (#1555 review)

* fix(replay): restore invalid replayBackend rejection on the native path (#1555 review)

* refactor(replay): move shared .ad vocabulary to its owner, packages/ad-script (#1555 review)

* refactor(replay): neutral step/run outcomes and digest/resume behind inspectAdReplay (#1555 review)

P1 "do not smuggle daemon wire failures through a generic": drop the
TResponse generic from AdReplayStepRuntime/runAdReplay. executeStep and
handleActionFailure now return neutral tagged AdReplayStepOutcome/
AdReplayStepFailure values (kind/message/artifactPaths only); runAdReplay
returns a neutral completed/failed AdReplayRunOutcome. The engine never
holds or returns a DaemonResponse. The daemon adapter
(createAdReplayStepRuntime, session-replay-runtime.ts) keeps its real wire
response in a local side-map as it builds each neutral outcome, and
runReplayScriptFile reads it back once runAdReplay reports which step
failed, so the final response is byte-identical to before this split.

P1 "parsing/planning/digest/resume must also occur behind runAdReplay":
relocate computeReplayPlanDigest's call site and the --from/--plan-digest
resume-point math (resolveReplayEntryIndex) behind inspectAdReplay's
manifest as planDigest and a resolveEntryIndex closure. Neither is a new
top-level export -- inspectAdReplay/runAdReplay stay the only two. Timing
is preserved exactly (still called eagerly in prepareReplayPlan, before
prepareReplaySession's coordinator-mutating side effects) since moving
resume validation to run inside runAdReplay itself would let a rejected
--from request mutate coordinator/session state first -- a real ordering
hazard, not just a cosmetic one.

computeReplayPlanDigest/ReplayPlanDigestMetadata/resolveReplayEntryIndex
leave the ad-replay façade; request-router-repair-expired.test.ts and
prepareReplayPlan read the digest/resume result off the manifest instead.

* refactor(replay): relocate classifyTargetBindingMatch and pin the ad-replay façade (#1555 review)

P1 "complete the binding façade instead of documenting deviations":
classifyTargetBindingMatch never had a real consumer reachable through
inspectAdReplay/runAdReplay -- both its callers (the daemon's record-time
self-check in session-target-evidence.ts and its replay-time
classification wrapper in session-replay-target-classification.ts) are
daemon files that imported it directly. It interprets TargetAnnotationV1
evidence semantics shared beyond the engine, so it moves to
packages/ad-script alongside target-annotation-identity.ts (new
target-annotation-classification.ts + its test), and both daemon call
sites now import it from there instead of @agent-device/ad-replay.

One deviation remains and is reported rather than papered over per the
review's own instruction: the four target-verification policy functions
(planPreDispatchTargetVerification, planPostResolutionTargetVerification,
deriveReplayTargetGuardMismatchEvidence,
deriveWaitLandmarkMismatchEvidence) and the ReplaySelectorPort type
family stay exported. Their sole caller,
session-replay-target-verification.ts, interleaves these pure decisions
with daemon-only async work (capture, SessionStore, coordinator/resume
stamping, wire shaping) that must stay outside the engine by design;
moving their call sites to live only behind runAdReplay would require
restructuring that whole orchestration into new fine-grained
AdReplayStepRuntime capabilities, which is out of scope for this pass.
See packages/ad-replay/src/index.ts's header comment for the full
reasoning.

P1 "add the reviewer-required exact exported-symbol gate": adds
readNamedExports (scripts/layering/package-boundaries.ts), a small
parser over a façade's `export { .. } from`, `export type { .. } from`,
and direct-declaration forms, and pins @agent-device/ad-replay's exact
21-symbol export list in package-boundaries.test.ts. Plant-verified: a
stray `export const` addition failed the assertion; removed it and the
gate went green again.

* refactor(replay): drive target verification from the engine step loop (#1555 review)

Moves the verify-then-dispatch decision flow into packages/ad-replay's
step loop so the four target-verification policy functions
(plan{PostResolution,PreDispatch}TargetVerification,
derive{ReplayTargetGuardMismatch,WaitLandmark}MismatchEvidence) become
engine-private and leave the ad-replay façade. The daemon
(session-replay-target-verification.ts) shrinks to the narrow
AdReplayStepRuntime capabilities the engine drives: routing
(beginTargetVerification), capture (captureObservation), classification
(classifyTarget), dispatch (dispatchStep), and wire-building
(buildRecordedUnverifiableFailure, buildTargetBindingFailure,
buildPostDispatchTargetBindingFailure). Wire output and replay-compat
stay byte-identical; the exact-symbol façade gate is updated to the
shrunken export list.

* refactor(daemon): decompose the replay adapter's two over-threshold functions (#1555)

* refactor(replay): fold #1554's keep-session terminal-lifecycle policy into the ad-replay engine

Rebasing p5/extract-ad-replay onto main pulled in #1554's --keep-session
feature, which had grown its own daemon-side terminal-close-suppression
predicate (session-replay-terminal-lifecycle.ts's
resolveSuppressedTerminalCloseIndex/countExecutedReplayActions) independently
of this branch's own engine-side one (step-loop.ts's
isRepairArmedTerminalCloseAction). Both are the same decision family — replay
--keep-session and an active --save-script repair now share ONE structural
resolution (resolveSuppressedTerminalCloseIndex, generalized to "terminal
among EXECUTABLE actions" rather than the old physical-last-index check) and
one suppression check inside runAdReplay, gated on keepSession OR
runtime.isRepairArmed(). AdReplayRunRequest grew a keepSession field; the
neutral 'replayed' count in AdReplayRunOutcome is now computed inline in the
loop instead of the daemon's old actions.length - entryIndex approximation.

requireLiveSessionForKeepSession (the --keep-session live-session
postcondition) stays daemon-side, inlined into session-replay-runtime.ts,
since it inspects SessionStore state the engine never sees. The daemon-only
session-replay-terminal-lifecycle.ts this arrived with is deleted entirely —
its isExecutableReplayAction was a duplicate of the engine's own.

runReplayScriptFile's Maestro-format routing (including the new --keep-session
Maestro rejection) was extracted into routeMaestroReplay to keep the function
under fallow's complexity threshold after re-threading keepSession through it.

Added packages/ad-replay/src/internal/__tests__/step-loop.test.ts covering the
unified suppression decision (both keepSession and repair-armed) directly
against runAdReplay, including the terminal-among-executable-actions case with
a trailing nested replay marker. The daemon-level integration tests (6 tests
in session-replay-terminal-lifecycle.test.ts, exercising the same behavior
through runReplayScriptFile) and the SDK provider-scenario test
(active-session-script-publication.test.ts) needed no changes and pass
unmodified.

* refactor(daemon): decompose session-replay-runtime.ts into three modules (#1555)

Splits the ~1096-line replay runtime into cohesive pieces, keeping
session-replay-runtime.ts as thin orchestration (~240 LOC):

- session-replay-runtime-engine-adapter.ts: the AdReplayStepRuntime
  adapter (createAdReplayStepRuntime, the build*Failure capability
  implementations, and the lastResponse/lastObservation side-map
  mechanics), extracted verbatim.
- session-replay-runtime-plan.ts: extended with the plan-side helpers
  (validateReplayBackendFlag, inspectReplayPlanManifest,
  resolveReplayPlanEntryIndex, prepareReplayPlan, routeMaestroReplay)
  alongside the buildReplayMetadataFlags helper already there —
  buildReplayMetadataFlags is now module-private since its one caller
  moved into the same file. Also introduces ReplayScriptFileParams,
  named here (instead of derived via Parameters<typeof
  runReplayScriptFile>) so routeMaestroReplay can reference the shape
  without importing back from session-replay-runtime.ts.
- session-replay-runtime-session.ts (new): session preparation
  (prepareReplaySession and its coordinator arming/repair-preflight
  helpers), extracted verbatim.

Coordinator ownership is unchanged: createReplayCoordinator is still
constructed only in session-replay-runtime.ts, matching
replay-coordinator-ownership.test.ts's allowlist as-is — every
extracted module receives the already-constructed ReplayCoordinator as
a parameter. Pure move; no behavior change.

* test(replay): cover pre-step artifact ordering and resume-before-mutation (#1555)

Two invariants found during the P5 decomposition pass now have direct
counterfactual-verified coverage:

- packages/ad-replay/src/internal/__tests__/step-loop.test.ts: a
  post-dispatch target-binding mismatch (dispatchWithGuard) must report
  the accumulated PRE-step artifact snapshot it was called with, never
  the artifacts the failed dispatch itself produced. Verified red by
  swapping the buildPostDispatchTargetBindingFailure call to
  outcome.artifactPaths.

- src/daemon/handlers/__tests__/session-replay-runtime-plan.test.ts: a
  rejected --from/--plan-digest resume must never reach
  prepareReplaySession's coordinator-mutating writes (the R2 ordering
  invariant) — a pre-armed repair transaction and corrective-resume
  watermark are asserted byte-for-byte unchanged after rejection.
  Verified red by calling prepareReplaySession before honoring the
  plan-validation rejection.

* fix(ad-replay): enforce the exact two-entrypoint facade (#1555 review P1)

packages/ad-replay/src/index.ts now exports exactly two value symbols,
inspectAdReplay and runAdReplay, and zero types — formatReplaySuccessMessage
(presentation) moves beside its one caller in session-replay-runtime.ts, and
every type a root daemon file needs is derived structurally off the two
entrypoints in the one new src/daemon/ad-replay-facade-types.ts module
instead of being named off the façade.

scripts/layering/package-boundaries.ts's readNamedExports is rewritten on
oxc-parser's own static-export table instead of a regex, so it can no longer
silently miss a widening export form: a bare `export *` re-export or an
`export default` now throws (an un-enumerable, and therefore un-pinnable,
export), while `export * as ns` and every other enumerable form is still
counted. The pinned exact-symbol assertion in package-boundaries.test.ts is
narrowed to ['inspectAdReplay', 'runAdReplay'].

* fix(ad-replay): translate wire failures before the engine boundary (#1555 review P1)

AdReplayDispatchOutcome's guard-mismatch/landmark-mismatch variants carried
a generic `details: Record<string, unknown> | undefined` bag straight off
the wire response — a daemon wire projection crossing into the engine even
though the outcome itself was already a neutral type. The daemon adapter
(session-replay-runtime-engine-adapter.ts) now narrows that bag into the
typed AdReplayGuardMismatchEvidence/AdReplayLandmarkMismatchEvidence shapes
(observed identity, expected/observed structural denotation, ancestry
entries, match count) before returning the outcome; the unknown-parsing
readers move there with the wire-reading responsibility they always were.
target-verification.ts's deriveReplayTargetGuardMismatchEvidence/
deriveWaitLandmarkMismatchEvidence now consume only the typed values — no
`unknown`-valued record type remains on any engine-crossing signature.

* fix(ad-replay): move variable semantics/planning behind runAdReplay (#1555 review P1)

The daemon assembled the `${VAR}` scope (buildPreparedReplayScope) and
interpolated actions at two independent call sites: dispatch's own
(invokeReplayAction) and target verification's separate one
(resolveTargetVerificationEntry) — duplicated orchestration the P5 design
assigns to the engine.

runAdReplay's request now carries the raw scope INPUTS (varSources: plain
builtins/file/shell/cli-env data, plus actionLines/actionSourcePaths/
resolvedPath for interpolation-error location) instead of a built scope; the
engine builds the scope and resolves each action exactly once per step,
handing the RESOLVED action to dispatchStep/beginTargetVerification while
every other capability still receives the ORIGINAL recorded action (a
target-binding divergence reports the recorded selector, never an expanded
${VAR}). This is the one resolution site now — session-replay-action-runtime.ts's
invokeReplayAction and session-replay-target-verification.ts's
resolveTargetVerificationEntry no longer hold a scope or call
resolveReplayAction themselves.

Scrub-value collection (collectReplayScrubbableVarValues, for divergence-report
redaction) is kept single-sourced in the engine too: it's computed from the
engine's own live scope and threaded to each build-failure/handleActionFailure
capability as an explicit scrubVars argument, rather than the daemon
recomputing it from a second scope object (which would have gone stale,
since expandedBuiltinNames tracking now only happens engine-side).

The Maestro replay path's own daemon-side vars usage is unrelated (a
different engine) and is out of scope here.

* fix(ad-script): make ${VAR} interpolation a linear scanner

CodeQL flagged the interpolation regex's fallback group as js/polynomial-redos
once vars.ts moved into packages/ (library-input classification): every
${NAME:- prefix of an unclosed input rescanned to end-of-string, quadratic
overall — 1,857 ms measured on 20k repetitions of '${A:-['. Replaced with a
single-pass scanner; failed fallback scans emit their span verbatim and resume
after it (escape-pair alignment is identical from every candidate start inside
the span, so no later candidate can terminate where the failed scan could not).
Equivalence: 200k-trial differential fuzz against the retired regex over the
adversarial alphabet, zero mismatches; both adversarial shapes now resolve in
1-2 ms.

* refactor(ad-replay): typed façade replaces the zero-type rule (#1555 structural-quality review)

Reverses the exact-two-value zero-type export shape #1555's second review
pass established: it forced every root type derivation through one shim
(src/daemon/ad-replay-facade-types.ts) and left four daemon-side twin types
(TargetVerificationEntry, TargetClassificationOutcome,
TargetBindingFailureEvidence, ReplayVerifiedTargetGuard) plus a
toDaemonEvidence copy translator shadowing the engine's own shapes.

packages/ad-replay/src/index.ts now exports inspectAdReplay/runAdReplay
(unchanged, still the only two values) plus the neutral vocabulary their
signatures are built from, by name — following packages/maestro's façade
precedent. The exact-symbol gate in scripts/layering/package-boundaries.test.ts
is widened to pin the full sorted list (values + types).

The four daemon twins are deleted; session-replay-target-verification.ts and
session-replay-runtime-engine-adapter.ts now use the engine's own
AdReplayVerificationEntry/AdReplayTargetClassification/
AdReplayTargetBindingEvidence/AdReplayVerifiedTargetGuard directly.
TargetBindingDivergenceBuilt's array fields are now readonly-compatible, so
toDaemonEvidence's copy is gone — evidence flows through unchanged.

* fix(ad-replay): honor the selector port's own contract in the parse gate

target-verification.ts's planPreDispatchTargetVerification used
resolveRecordedTarget (operation 2, resolve) over an empty node tree purely
to read its parse-invalid reason — a resolve call standing in for a parse
call, even though readSelectorExpression (operation 1, parse) exists to
answer exactly that question and was already unused inside the engine.

Replaced with port.readSelectorExpression('ordinary', [token]). The mapping
is not 'invalid' -> skip: production's 'ordinary'/'wait' grammars only ever
record a boundary once it has already parsed, so a single malformed token
can only come back 'not-applicable' there ('invalid' is unreachable from
this call site on the production adapter). Both non-'expression' outcomes
map to skip, matching the historical behavior (a single parse-invalid reason
covered both cases). platform dropped from the function's params — it was
only ever threaded to the resolve call this replaces.

Added a contract-suite cell pinning the exact (diverging) discriminant each
adapter reports for a selector-shaped-but-malformed bare token, and why the
divergence is harmless for the one real consumer.

* refactor(ad-replay): split step-loop.ts and shrink the daemon adapter (#1555 structural-quality review)

step-loop.ts (810 LOC) splits three ways, following packages/maestro's own
precedent:
- internal/runtime-port-types.ts: the AdReplayStepRuntime boundary
  vocabulary (all the neutral types the engine/daemon exchange).
- internal/verify-dispatch.ts: verifyAndDispatchStep + its dispatchNoGuard/
  dispatchWithGuard helpers.
- internal/step-loop.ts: runAdReplay itself plus the terminal-close/
  executable-action structural logic (isExecutableReplayAction,
  resolveSuppressedTerminalCloseIndex).

packages/ad-replay/src/index.ts's type exports now source from
runtime-port-types.ts. step-loop.test.ts's AdReplayStepRuntime import moves
to the new path (no assertion changes).

src/daemon/handlers/session-replay-runtime-engine-adapter.ts (553 LOC after
item 1's twin removal) shrinks to 294 via two further extractions:
- session-replay-dispatch-narrowing.ts: the wire `details` bag -> typed
  evidence narrowing and dispatch-failure classification.
- session-replay-runtime-step-support.ts: ReplayStepContext (moved here to
  avoid a cycle with the adapter, which re-exports it by name) plus the
  failure-wrapping/diagnostics-support helpers.

Final LOC: adapter 294, dispatch-narrowing 148, step-support 153,
step-loop 225, verify-dispatch 246, runtime-port-types 374.

* test(ad-replay): package-local tests for resume.ts/target-verification.ts + terminal-lifecycle test rename

resume.test.ts covers resolveReplayEntryIndex directly (previously only
exercised transitively through the daemon's session-replay-runtime-plan
tests): no --from/--plan-digest, the paired-flags requirement, in-range
--from, out-of-range rejection, stale-digest rejection, the authorized
empty-tail boundary (actionCount + 1) gated on a matching watermark, and the
unperformed-record-and-heal growth check. Counterfactual run and restored:
widening describeOutOfRangeResumeFrom's bound turns the out-of-range/
empty-tail-without-watermark assertions red (2 failures observed).

target-verification.test.ts covers all four engine policy functions
directly: the two plan* pre-capture gates and the two derive* post-dispatch
evidence builders, including item 2's own new decision surface (a fake
ReplaySelectorPort proving both non-'expression' readSelectorExpression
outcomes map to skip). Counterfactual run and restored: narrowing the check
to the literal `'invalid' -> skip` reading turns the 'not-applicable' case
red (reports recorded-unverifiable instead of skip).

session-replay-terminal-lifecycle.test.ts renamed to
session-replay-runtime-keep-session.test.ts: its production module
(session-replay-terminal-lifecycle.ts) was already deleted by the #1554
fold-in, and its six cases drive the full runReplayScriptFile round trip
against a real SessionStore (including daemon-only postconditions the
engine's step loop never reaches) rather than testing engine policy through
the façade in isolation — the engine's own terminal-close-suppression
decision already has direct, cheaper coverage in step-loop.test.ts. No
assertion changes; both files' header comments cross-reference the split.

* refactor(ad-replay): compute scrub values once per step, one name end to end

collectReplayScrubbableVarValues(scope) was called fresh at 5 separate
return points inside one verifyAndDispatchStep invocation plus once more in
handleActionFailure — always the same result, since nothing between them
mutates scope. step-loop.ts's runAdReplay now computes scrubVars ONCE per
step, right after resolveReplayAction (the one call that can grow the
scope's expanded-builtins set), and threads it as a plain
readonly AdReplayScrubValue[] value; verify-dispatch.ts no longer imports
ReplayVarScope or collectReplayScrubbableVarValues at all.

"One name" end to end: the daemon's TargetBindingDivergenceContext.scrubVars
and withReplayFailureDiagnostics's scrubVars param used a separately-derived
ReturnType<typeof collectReplayScrubbableVarValues> (mutable array) instead
of the engine's own AdReplayScrubValue, requiring a [...scrubVars] copy at
every daemon call site to satisfy the mutable-array type. Both now use
readonly AdReplayScrubValue[]/readonly ReplayVarScrubEntry[] (structurally
identical, already readonly-safe downstream — scrubReplayVarValues and
createReplayDivergenceSanitizer already accepted readonly arrays), so the
four [...scrubVars] copies in session-replay-runtime-engine-adapter.ts are
gone.

* fix(daemon): make lastObservation genuinely per-step, not per-run

createAdReplayStepRuntime's lastObservation closure lives for the whole
replay run (one factory call covers every step), but was never reset
between steps. Every current buildTargetBindingFailure call site happens to
be preceded by this same step's own captureObservation, so the
`lastObservation ?? { reason: 'observation-missing' }` fallback could never
actually fire — but if it ever did (a future call path reaching
buildTargetBindingFailure without capturing first), it would silently
attach the PREVIOUS step's screen instead of reporting the missing-capture
condition the fallback message claims.

armStep runs exactly once per step, before any of that step's other
capabilities (verified against step-loop.ts's runAdReplay loop order) — the
natural per-step boundary. It now clears lastObservation first. No behavior
change on any reachable path today (full daemon + ad-replay suite: 1766/1766
green); an unrelated device-claim-prune contention flake was observed once
and did not reproduce on isolated or full-suite reruns.

* docs(ad-replay): fix decayed review-changelog comments naming defunct symbols

Four comments named symbols/paths that no longer exist, left behind by
earlier review passes describing PR history rather than the current
constraint:
- session-replay-runtime-step-support.ts / session-replay-runtime.ts (2
  sites): referenced a function called executeStep, which was never
  reintroduced under that name after the P5 split — the actual mechanism is
  the runtime's dispatch/build-failure capabilities recording into the
  lastResponse side-map.
- session-replay-runtime.ts: referenced an engine collectArtifactPaths
  capability that does not exist — artifactPaths is a daemon-side Set the
  adapter mutates via collectReplayActionArtifactPaths.
- packages/ad-replay/src/internal/selector-port.ts: pointed at
  ./testing/in-memory-selector-port.ts, the in-memory adapter's pre-stage-D
  location — it has lived at
  src/__tests__/test-utils/in-memory-replay-selector-port.ts since.
- session-replay-repair-hint.ts / session-replay-runtime-step-support.ts (2
  sites): named target-identity.ts, which does not exist (the real file is
  target-identity-node.ts); the second site additionally mislabeled
  classifyReplayTarget as engine-side when it is daemon-side
  (session-replay-target-classification.ts).

Comment-only; no behavior change.

* refactor(ad-script): move declaredScriptPlatform to its natural shared owner

packages/ad-replay/src/internal/inspect.ts's declaredScriptPlatform and
src/daemon/replay-device-selection.ts's readScriptReplaySelection each kept
their own copy of the same "platform declared before the first open" scan
over runtime/open actions — .ad script semantics, not engine or daemon
policy, needed independently by ad-replay's plan-digest precedence and the
daemon's device-selection platform resolution.

Verified this was a genuine duplicate (not the single-sourced state I
initially reported): readScriptReplaySelection's platform-tracking loop
computes the identical result via a differently-shaped traversal fused with
its own app-target scan.

resolveDeclaredScriptPlatform now lives in packages/ad-script (its natural
owner: the one package both ad-replay and the daemon already depend on,
avoiding the R11 issue that justified the original duplication). The
daemon's app-target scan stays its own separate pass; fusing it back into
the shared function would smuggle a daemon-only concern into ad-script for
no measurable cost (the actions array is small, and the shared function
already stops at the same point the app-target scan needs to look).

* docs(ad-replay): fix package.json description to match the current façade

Described "target-identity, variable substitution, plan-digest, and report
primitives" — the wide pre-#1555-review façade shape. Vars/identity/report
vocabulary moved to ad-script/daemon across the P5 and #1555 review passes;
the package now exports exactly inspectAdReplay/runAdReplay plus the
neutral AdReplayStepRuntime vocabulary. Description updated to match.

* refactor(daemon): fold the step-support fragment back into the engine adapter

A simplicity audit judged session-replay-runtime-step-support.ts a
size-target fragment, not a concern boundary: four unrelated concerns,
one consumer, and a header comment admitting it existed to satisfy the
<300 LOC metric. Folded back; the previously-exported helpers are
module-private again; the adapter's honest size is renegotiated from the
plan metric (dispatch-narrowing stays extracted — it has one nameable
job).
2026-08-03 17:25:13 +02:00
Michał Pierzchała da93191201 refactor: move Limrun provider behind package facade (#1518)
* refactor: move Limrun provider behind package facade

* fix: preserve Limrun public provider types

* fix: tighten Limrun provider facade boundaries

* test: harden Limrun compatibility coverage

* fix: narrow Limrun public type exports

* fix: narrow Limrun provider exports
2026-07-31 15:35:49 +02:00
Michał Pierzchała b125435989 refactor: extract WebDriver provider package (#1504)
* refactor: extract webdriver provider package

* refactor: consolidate shared XML codec
2026-07-31 09:10:04 +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