Skip to content

refactor(typescript): project references + tsc -b typecheck; retire the R11 branches tsc enforces - #3289

Merged
thymikee merged 11 commits into
mainfrom
refactor/ws3-project-references
Oct 8, 2026
Merged

thymikee merged 11 commits into
mainfrom
refactor/ws3-project-references

Conversation

@thymikee

@thymikee thymikee commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Summary

All 26 workspace packages declare references derived from the real workspace dependency graph (each package's manifest workspace:* edges; verified programmatically against every tsconfig). pnpm typecheck becomes tsc -b tsconfig.json examples/sdk/tsconfig.json: the root project references the 26 packages, and examples/sdk is a composite build node referencing them too (it keeps its paths mapping into src/sdk/ and self-lists the src/ closure those paths pull in).

R11 package-boundaries (scripts/layering/package-boundaries.ts + rule-registry prose only) keeps exactly what tsc -b cannot express, with planted proofs:

Rebased onto main after #3118 merged provider-testmu as the 26th package. Its wiring is derived like the rest; the one edge with no expressible reference is its dependency on the root agent-device package (agent-device/plugins imports) — the root tsconfig is a noEmit entry point and TS6310 forbids referencing it, so that import type-resolves through the package's own paths mapping. The guard test encodes exactly that rule: root-named workspace deps drop out of the references equality and must have a paths mapping, or it fails (mutation-proven by deleting the mapping).

  • Kept (tsc accepts the violation): an import of a workspace sibling the manifest never declares — tsc -b exits 0 because pnpm's root link farm resolves the specifier anyway, and the declaration is what the published bundle externalizes against; and the root→packages/*/src relative tunnel — tsc -b typechecks it (exit 0); the failure it prevents is Node's runtime double instantiation, which no compiler check sees. Plus the full sweep for scripts/ and package-harness files, which sit outside the type graph by design (insideCompiledSources() names the set).
  • Deleted (tsc fails them natively): unknown workspace package, non-exported subpath — for compiled sources only.
  • Restored after maintainer review (63fd2976a): the relative-escape branch fires for every package source, compiled or not. Planted: packages/host-kit/src importing '../../kernel/src/errors.ts' → tsc -b packages/host-kit --force exit 0 (the reference redirects kernel to its dist-types, so rootDir never sees a foreign file — TS6059/TS6307 only fire on the root escape, where no project redirects). The damage is runtime module double-instantiation, invisible to tsc. The root-escape TS6059+TS6307 proof below stands; it never covered sibling tunnels.

Planted proofs on this tree (TS 7.0.2, exact errors):

  • packages/xml file importing '../../../src/daemon/session-state.ts': error TS6059: File '.../src/daemon/session-state.ts' is not under 'rootDir' '.../packages/xml/src' + error TS6307: File ... is not listed within the file list of project '.../packages/xml/tsconfig.json' (exit 2).
  • packages/xml importing '@agent-device/kernel/internal-nope' (un-exported subpath) or '@agent-device/nonexistent-pkg/x' (unknown package): error TS2307: Cannot find module ... or its corresponding type declarations. (exit 2).
  • packages/xml importing '@agent-device/kernel/errors' with no declaration and no reference: tsc -b exit 0 (buildinfo shows kernel/src/errors.ts consumed as source) → R11's undeclared-dependency branch stays; same for dropping host-kit's references entirely (exit 0, source fallback). This is the "an import from a package lacking a reference" case: references alone don't fence imports here because the exports maps point at .ts sources; the fences that work are the ones listed above.
  • Root file importing '../packages/xml/src/index.ts': tsc -b exit 0 → R11's tunnel branch stays.

Typecheck wall time (this host, 12 cores): cold 13.2s → 9.1s; warm 14.2s → 0.5s. Incremental cone proven: touching packages/host-kit/src/archive.ts rebuilds exactly 18 projects — host-kit plus its dependents (capture-kit, device-selection, provision-kit, proxy, session-journal, the platform/provider packages that consume them, replay-port, maestro, managed-allocation, root, examples/sdk); kernel, contracts, xml, ad-script, selectors, etc. report up-to-date.

Root program now consumes 698 package .d.ts outputs and zero foreign .ts sources. Scope note: no gate-catalog or .fallowrc edits were needed (the typecheck gate keeps running pnpm typecheck), so there is no chore(gates) commit beyond the freerange one; every file is references wiring, the R11 pair, or the guard tests.

Repo Guards Check numeric ranges OOM (exit 134) — root cause and fix. freerange's loader (@chenglou/freerange/src/typescript/project.ts) walks the resolved tsconfig's projectReferences recursively and keeps every project's ts.Program alive simultaneously. On main the root config had no references, so fr loaded 1 program (~87s, green). With this PR's graph it loads 26 programs / 13,461 file slots ≈ 2.8 GB heap before analysis begins → Node's default 4 GB heap dies (~27s, reproduced locally and bisected: main's tsconfigs + this branch's code = green). Removing only the root references array makes it pass (112s). Upstream 0.0.5 bundles the identical loader, so there is nothing to pin away. Fix is at the owning config, not the CI heap or a skipped gate: scripts/freerange/tsconfig.json extends the root tsconfig — and extends does not inherit references (planted with --showConfig) — so fr gets exactly the single root program it analyzed on main (verified byte-identical: No lint findings, coverage 971/13495 fully analyzed), while tsc -b keeps the full graph. The gate ran before references existed and analyzed this same scope; R11/tsc -b deliver their value with the tool as shipped, unpinned and ungutted. The reference-free + extends-to-root properties are pinned by a guard test whose mutation is the OOM itself.

Closes #3279

Validation

  • What cd scripts/freerange && fr reads relative to cwd (confirmed for review). The ONLY cwd-relative input is the tsconfig, found by upward search from process.cwd(): findTypeScriptConfig(process.cwd()) → ts.findConfigFile(resolve(cwd), ts.sys.fileExists, 'tsconfig.json') (@chenglou/freerange/src/project.ts:426, src/typescript/project.ts:16-17). There is no baseline, ignore-file, or cache concept at all: the shipped dist/fr.js bundle's entire node:fs surface is one existsSync (file-mode argument existence) and it contains zero readFileSync/writeFileSync/mkdirSync/homedir/tmpdir references — coverage numbers are recomputed from the program every run, which is also why the byte-identical coverage claim was verifiable across configs. The remaining process.cwd() uses are the report's baseDirectory (src/lower/program.ts:14, src/ir/program.ts:434), a display-only relative(baseDirectory, file) for pretty paths. So the cd exists solely to anchor the upward tsconfig search at scripts/freerange/tsconfig.json (the reference-free wrapper); it reads no config state from the directory it runs in, and moving the wrapper file itself — not the cwd — is the only way to change what fr loads.
  • Head 77f3a47db (review nits: compact references + uniform tsbuildinfo): AGENT_DEVICE_VITEST_MAX_WORKERS=4 pnpm check:affected --run — all runnable checks passed. Cold tsc -b from wiped dist-types/.tmp also verified exit 0 at this head (the compaction touched all 26 reference lists plus testmu's buildinfo name).
  • Head f105bbf5d (cubic re-review): AGENT_DEVICE_VITEST_MAX_WORKERS=4 pnpm check:affected --run — all runnable checks passed; check:layering 297/297. The include pin now matches exact arrays (["src"], ["src","../command-registry/src/global.d.ts"]) instead of a *.global.d.ts suffix class — mutation: a made-up generated-env.global.d.ts include in xml goes red. The escape-branch rationale states the dual-route duplication condition, not an unconditional double load.
  • Head 63fd2976a (maintainer-review fix): AGENT_DEVICE_VITEST_MAX_WORKERS=4 pnpm check:affected --run — all runnable checks passed; check:layering 297/297 with the restored escape branch mutation-proven (re-adding if (compiled) continue; turns three tests red) and the new include/exclude pins mutation-proven (xml exclude → red, second root scripts/ include → red).
  • Head f75da8ba8 (cubic P2 fix): AGENT_DEVICE_VITEST_MAX_WORKERS=4 pnpm check:affected --run — all runnable checks passed. The facade exhaustiveness gate now matches per source module (readFacadeReExportEdgesByModule in facade-exports.ts, alias-aware, export * as ns whole-surface); the planted single-source narrowing behind a duplicated name fails the new rule and passed the old collapsed-name rule (proof in the thread reply). check:layering 293/293.
  • Head 0f1967e9c: AGENT_DEVICE_VITEST_MAX_WORKERS=4 pnpm check:affected --run — all runnable checks passed (unit lanes cover the test-file-size ratchet). The CI Coverage ratchet failure at 8d5df4fb8 was real: the guard-test round pushed package-boundaries.test.ts from 972 to 1,016 lines, over the 1,000-line tripwire. Fixed by splitting along the mirrored source module, move-only: the two real-tree facade-surface gates (explicit exports, exhaustive re-exports) share the facade question with facade-exports.ts, not the R11 specifier/declaration rules this file mirrors, so they moved verbatim to scripts/layering/facade-surface.test.ts (103 lines). No pin raised: the original file is now 928 lines, below its 972 merge-base length. Discovery proven at the new path — the check:layering glob picks it up and the suite stays 292 tests.
  • Head 8d5df4fb8 (rebased onto main 0c1ebb33a): fresh pnpm install --frozen-lockfile + pnpm build, wiped dist-types/.tmp → pnpm typecheck exit 0 cold across the 26-project graph; pnpm check:layering 292/292; pnpm check:freerange exit 0 (No lint findings, coverage 971/13547 — denominator moved upstream); pnpm format/lint clean; AGENT_DEVICE_VITEST_MAX_WORKERS=4 pnpm check:affected --run — all runnable checks passed (full-set selection from the lockfile delta).
  • Head e60436198: pnpm check:affected --run — all runnable checks passed; pnpm gate freerange (the exact CI invocation) passes in ~90s. Heads 7c00b5636 and deea35c4a passed check:affected --run before the freerange fix landed.
  • Focused: node --test scripts/layering/package-boundaries.test.ts — 16/16, including a mutation-proven test that keeps the compiled/uncompiled boundary honest; pnpm check:layering green (R11 summary line updated).
  • New structural gates planted-verified per testing.md (violation → intended error above → removed).
  • Unresolved risk: none local; CI confirms the full set. Device lanes untouched (pure tooling change).

View guided diff Turn on auto-fix

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-08 07:55 UTC

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.13 MB 5.13 MB -567 B
Package (unpacked) 5.13 MB 5.13 MB -567 B
Package (download) 1.54 MB 1.54 MB -125 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 25.9 ms 26.1 ms +0.2 ms
CLI --help 78.7 ms 79.7 ms +0.9 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 31 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread scripts/layering/package-boundaries.test.ts Outdated
Comment thread tsconfig.json
thymikee added a commit that referenced this pull request Oct 7, 2026
… graph

Review #3289 (cubic): a completeness claim carries its proof.

- project-references.test.ts: the workspace package set, the root
  manifest workspace:* declarations, and the references lists in the
  root, examples/sdk, and every package tsconfig must stay equal.
  tsc -b fails on a missing reference it needs but tolerates extras
  silently (planted: an undeclared ../kernel reference in packages/xml
  builds clean), so the guard test is the only owner of edge freshness.
  Mutation-proven both ways: an extra package dir absent from the root
  list fails root and examples assertions; an extra reference fails the
  per-package equality.
- insideCompiledSources documents its one divergence from the graph --
  the root program also includes scripts/help-conformance-command-
  validator.ts, which the predicate classifies as uncompiled (fail-
  closed: R11 keeps the full branch set there, never narrower) -- and
  the boundary test now pins that file as an assertion instead of
  implying the exception does not exist.
thymikee added a commit that referenced this pull request Oct 7, 2026
…ot program

Repo Guards -> 'Check numeric ranges' OOMed (exit 134, ~4 GB) on both
pushed heads. Cause (#3289 CI diagnosis): freerange's loader walks the
resolved tsconfig's projectReferences recursively and keeps every
project's ts.Program alive simultaneously. On main the root config had
no references, so fr loaded one program (~87s, green). With the #3279
references graph it loads 26 programs / 13,461 file slots and ~2.8 GB
heap BEFORE analysis begins; Node's default 4 GB heap dies. Removing
only the root references array makes the gate pass (112s); upstream
0.0.5 bundles the same loader, so this is not a version to pin away.

Fix at the config, not the heap: scripts/freerange/tsconfig.json extends
the root tsconfig — and 'extends' does not inherit references (planted
with --showConfig) — so 'fr' gets exactly the single root program it
analyzed on main (verified: byte-identical findings and coverage,
971/13495 fully analyzed). tsc -b keeps using the real graph.
check:freerange now runs fr from that directory; the reference-free and
extends-to-root properties are pinned by a guard test with the OOM as
its mutation.
…through tsc -b

All 25 workspace packages now declare references derived from the pnpm
workspace dependency graph (their manifest workspace:* edges), the root
project and examples/sdk reference the packages, and every project points
tsBuildInfoFile at its own output directory so one 'tsc -b' pass builds
the whole graph. 'pnpm typecheck' becomes
'tsc -b tsconfig.json examples/sdk/tsconfig.json'.

Measured on this host (Apple silicon, 12 cores):
- cold (package outputs + .tmp wiped): 13.2s -> 9.1s
- warm (no edits):                    14.2s -> 0.5s
- incremental cone: touching packages/host-kit/src/archive.ts rebuilds
  exactly its 17-project dependent cone; kernel, contracts, xml,
  ad-script, selectors, session-journal-free branches stay up to date.

Root and examples/sdk consume packages through the declaration outputs
the graph emits (699 -> 0 package .ts inputs in the root program), so
NodeNext resolution errors surface for un-exported subpaths and unknown
packages, and composite rootDir fails a package->root relative escape.
A missing reference alone does not fail the build (the pnpm link farm
resolves the exports-map .ts targets and the program reads sources);
that gap stays under R11 and is recorded on #3279.
R11 package-boundaries keeps exactly what the type graph cannot express,
per the planted proofs recorded on #3279:

- the undeclared workspace:* sibling import (compiled or not): planted in
  packages/xml, 'tsc -b' exits 0 because pnpm's root link farm resolves
  the specifier anyway; the manifest declaration is what the published
  bundle externalizes against;
- the root->packages/<name>/src relative tunnel: planted in src/, tsc
  exits 0; the failure it prevents is Node's runtime double instantiation
  of a module loaded both relatively and by specifier;
- the full specifier sweep for scripts/ and package-harness files, which
  the compiler never parses (outsideCompiledSources names the set).

Retired for compiled sources (proven failing tsc itself): the package->
root relative escape (TS6059 + TS6307), unknown workspace packages and
non-exported subpaths (TS2307).
… graph

Review #3289 (cubic): a completeness claim carries its proof.

- project-references.test.ts: the workspace package set, the root
  manifest workspace:* declarations, and the references lists in the
  root, examples/sdk, and every package tsconfig must stay equal.
  tsc -b fails on a missing reference it needs but tolerates extras
  silently (planted: an undeclared ../kernel reference in packages/xml
  builds clean), so the guard test is the only owner of edge freshness.
  Mutation-proven both ways: an extra package dir absent from the root
  list fails root and examples assertions; an extra reference fails the
  per-package equality.
- insideCompiledSources documents its one divergence from the graph --
  the root program also includes scripts/help-conformance-command-
  validator.ts, which the predicate classifies as uncompiled (fail-
  closed: R11 keeps the full branch set there, never narrower) -- and
  the boundary test now pins that file as an assertion instead of
  implying the exception does not exist.
…ot program

Repo Guards -> 'Check numeric ranges' OOMed (exit 134, ~4 GB) on both
pushed heads. Cause (#3289 CI diagnosis): freerange's loader walks the
resolved tsconfig's projectReferences recursively and keeps every
project's ts.Program alive simultaneously. On main the root config had
no references, so fr loaded one program (~87s, green). With the #3279
references graph it loads 26 programs / 13,461 file slots and ~2.8 GB
heap BEFORE analysis begins; Node's default 4 GB heap dies. Removing
only the root references array makes the gate pass (112s); upstream
0.0.5 bundles the same loader, so this is not a version to pin away.

Fix at the config, not the heap: scripts/freerange/tsconfig.json extends
the root tsconfig — and 'extends' does not inherit references (planted
with --showConfig) — so 'fr' gets exactly the single root program it
analyzed on main (verified: byte-identical findings and coverage,
971/13495 fully analyzed). tsc -b keeps using the real graph.
check:freerange now runs fr from that directory; the reference-free and
extends-to-root properties are pinned by a guard test with the OOM as
its mutation.
Rebase overlap with #3118: provider-testmu entered the workspace as the 26th
package after this branch's lists were derived. It gets the same treatment as
every other package - manifest-derived references and a per-project
tsBuildInfoFile - plus entries in the root and examples/sdk reference lists,
which the guard test now proves complete again.

Its dependency on the root agent-device package is the first edge that has no
composite project to point at: the root tsconfig is a noEmit entry point and
TS6310 forbids referencing it, so that import type-resolves through the
package's own paths mapping. The guard test encodes exactly that: root-named
workspace deps drop out of the references equality and must instead have a
paths mapping, or the test fails.
@thymikee
thymikee force-pushed the refactor/ws3-project-references branch from e604361 to 8d5df4f Compare October 7, 2026 17:48
…t family

CI Coverage lane: the round-2 guard tests pushed package-boundaries.test.ts
from 972 to 1016 lines, crossing the 1,000-line test-size tripwire, which only
splits may answer. The split is along the mirrored source module: the two
real-tree facade-surface gates (explicit exports, exhaustive re-exports) answer
the facade-surface question shared with facade-exports.ts, not the R11
specifier/declaration rules this file mirrors, so they move verbatim to
scripts/layering/facade-surface.test.ts. Move-only; the check:layering glob
discovers the new path and the suite stays 292 tests.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread scripts/layering/facade-surface.test.ts Outdated
…lapsed names

Cubic review P2 on #3289: the exhaustive-re-export gate collapsed every
module's re-exports into one export-NAME set, so an export removed from one
source stayed satisfied by the same name re-exported from a sibling module -
a single-source narrowing passed. The comparison now walks oxc's re-export
EDGES grouped by module (new readFacadeReExportEdgesByModule in facade-
exports.ts, the module that already owns export enumeration): each source
module's declared names must be carried by an edge from THAT module, matched
on the source-side name so `export { a as b }` covers `a` and `export * as
ns` exempts its module as whole-surface. The check is factored into a pure
helper so the planted regression - one source narrowed, the same name
published by another module - runs directly: it fails on the collapsed rule
and passes on the per-module rule.
The new readFacadeReExportEdgesByModule lives in facade-exports.ts, so its
own forms (alias pair direction, namespace binding, bare-star rejection) are
pinned there rather than only through the surface gate that consumes it.
@thymikee

thymikee commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

The typecheck wiring looks good, but I found one problem in the R11 change at e604361: if (compiled) continue; in package-boundaries.ts now skips every relative specifier in packages/*/src. Before this PR, R11 rejected any relative path that left its package dir, including a tunnel into a sibling such as packages/host-kit/src/x.ts importing '../../../kernel/src/errors.ts'. The planted proofs show TS6059/TS6307 only for an escape into root src/, which is not a referenced project. For a referenced sibling, tsc -b redirects its source files to the dist-types .d.ts, and the PR body says the root program consumes zero foreign .ts sources. So I expect that tunnel to resolve with no rootDir error and exit 0, the same way the root-to-package tunnel you kept does. Then a package could import a declared sibling's src by path and pass both R11 and tsc -b, and Node would load that module twice, once by path and once by specifier (duplicate AppError, broken instanceof). Issue #3279 says not to drop a guard that tsc cannot express. The rule R11 must satisfy is this: reject any relative specifier that resolves outside its own package dir, compiled or not, unless a planted proof shows tsc fails it. The set to enumerate is every compiled file whose resolved path lands under another packages// dir. Please either record a planted tsc -b run of a host-kit to kernel relative import that shows tsc fails it, or restore the branch for that case, and add a checkPackageInternalSites test with compiled=true and a sibling-relative specifier.

Is the smaller change enough here? That would drop only the TS2307-owned branches (unknown package, un-exported subpath) and leave the relative-escape branch alone, since it costs nothing and tsc covers it only in part. Could examples/sdk avoid typechecking root src a second time through ../../src/**/*? The root project is noEmit, so I found no smaller design today. Would making the root project composite and emitting let examples/sdk reference it instead? That needs a decision on root declaration output.

Not blocking: the doc block on insideCompiledSources and the test at package-boundaries.test.ts:129 both say the help-conformance divergence and the root include list are pinned in project-references.test.ts, but that file has no such assertion, so add it or drop the sentence; and insideCompiledSources hard-codes regexes for packages/*/src, src and test instead of deriving them from each tsconfig include/exclude, so derive them or assert that no package tsconfig declares exclude. Take or leave both.

The Cubic threads on the help-conformance classification and on pinning the references are fixed at this head, so please resolve them.

I read the code only and did not run tsc -b with a sibling-relative import. I did not reproduce the typecheck times, the incremental-cone claim, or the byte-identical freerange output. Smoke Tests was still running at e604361, and the diff touches only tsconfigs, the typecheck and freerange scripts, and scripts/layering, so I see no runtime overlap. Before merge, please record the planted tsc -b run (or restore the R11 branch for that case).

This review covers e604361. The newer heads up to f75da8b rebase onto main and add or move tests, but if (compiled) continue; at package-boundaries.ts:187 is unchanged, so the problem above still applies. I will look at the rest of f75da8b separately.

… or not

Maintainer review on #3289: `if (compiled) continue;` retired more than tsc
replaces. Planted the reviewer's exact case:

  packages/host-kit/src/r11-planted-sibling-tunnel.ts
    import { AppError } from '../../kernel/src/errors.ts';
  pnpm exec tsc -b packages/host-kit --force  ->  exit 0, no diagnostics

The reference redirects kernel's sources to their dist-types .d.ts, so nothing
outside host-kit's rootDir is emitted and neither TS6059 nor TS6307 fires -
the #3279 proof held only for the ROOT escape (TS2307 there, no project
redirects it). The sibling tunnel breaks runtime identity instead: Node loads
kernel once by path and once by specifier, so two AppError classes coexist and
instanceof fails - invisible to any compiler. The escape branch returns for
every package source; only the RESOLUTION branches (unknown package,
un-exported subpath) stay compiler-owned. checkPackageInternalSites gains the
requested compiled=true sibling-tunnel test, mutation-proven: re-adding the
skip turns it, the escape test, and the quote-form test red.

The non-blocking items are taken, both true: project-references.test.ts now
asserts the root include list (pinning the help-conformance divergence the
predicate's doc block promises) and the per-package include:[src]/no-exclude
shape insideCompiledSources' regexes assume - mutation: adding an exclude to
xml or a second scripts entry to root goes red - and check.ts's R11 summary
line states the corrected split.
@thymikee

thymikee commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

Fixed in 63fd2976a. You were right on all counts; the blocking case is restored, not just documented.

Blocking — sibling tunnel. Ran your exact case, and tsc -b accepts it:

# packages/host-kit/src/r11-planted-sibling-tunnel.ts
import { AppError } from '../../kernel/src/errors.ts';

$ pnpm exec tsc -b packages/host-kit --force   # exit 0, no diagnostics

Your mechanism explanation matches: the reference redirects kernel's files to their dist-types .d.ts, so nothing outside host-kit's rootDir is ever emitted — TS6059/TS6307 never fire. The #3279 proofs (TS6059+TS6307) held only for the root escape, where no project redirects. And a mis-remembered detail cuts the other way too: at the reviewer-suggested ../../../kernel/src/errors.ts depth (above host-kit), resolution fails outright with TS2307 — it's the ../../kernel/src/... form inside the graph that exits 0. So the tunnel is exactly a guard-tsc-cannot-express: the damage is runtime identity (two AppError classes, broken instanceof), which no compiler check sees.

Per your rule ("reject any relative specifier that resolves outside its own package dir, compiled or not, unless a planted proof shows tsc fails it"): if (compiled) continue; is gone; the escape branch fires unconditionally in checkPackageInternalSites, with the planted run recorded in the function's doc block. Added the requested checkPackageInternalSites test with compiled = true and a sibling-relative specifier, plus flips of the two compiled = true escape expectations. Mutation-proven both directions: re-adding the skip turns the new test, the escape test, and the quote-form test red (+ [] / - ['R11 package-boundaries']).

"Is the smaller change enough?" Yes — and that is exactly what this head now is: only the two TS2307-owned branches (unknown package, un-exported subpath) are retired for compiled sources; the relative-escape branch is whole again for every package source. I'd previously over-read "tsc covers it" from the root-escape proofs to cover sibling tunnels as well; the planted run shows the coverage is partial, so "costs nothing, keep it" is the right call and is now the code.

examples/sdk / root-composite. examples/sdk does re-typecheck the root src closure (as self-listed sources), and it is the only project that does. Making the root project composite would let sdk reference it instead, but the decision it forces is real: (1) root's include covers test/, so composite emit would produce declarations for the whole test tree (or force an include split just for typechecking); (2) root's published types come from tsdown's dist/*.d.ts via its exports map — a parallel dist-types/ declaration output is a second source of truth to keep in sync, and until that drift is answered, noEmit + explicit self-listing is the honest, smaller design. I'd keep the question for the umbrella (#3276) as a "root declaration output" decision rather than settle it inside this PR. Not silence: recorded here and I'll carry it to #3279.

Non-blocking — both taken.

  1. The doc/test claims about project-references.test.ts were untrue when read literally; they are now true. Added: the exact root include list pin, and a dedicated test asserting the only scripts/ entry in the root program is the help-conformance validator — so the divergence is recorded, not promised. Mutation: adding a second scripts/ entry goes red.
  2. insideCompiledSources's regex shapes are now licensed rather than assumed: a new test asserts every package tsconfig has no exclude and includes exactly src (replay-port's global.d.ts sibling excepted). Mutation: adding an exclude to packages/xml goes red. Deriving the predicate from the configs was the other option; I chose assertion because the predicate is consumed per-file inside a hot walk and deriving it there would re-parse 27 tsconfigs per sweep — the pinned shape is where the drift risk actually lives.

check:layering 297/297 at this head. The two cubic threads (help-conformance classification, references pinning) are already resolved — confirming now. check:affected --run evidence for this head lands as a follow-up once the full-set run completes.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread scripts/layering/project-references.test.ts Outdated
Comment thread scripts/layering/package-boundaries.ts Outdated
Cubic re-review on 63fd297:

- P2: the per-package include pin filtered `*.global.d.ts` by suffix, so an
  unrelated include in that shape class drifted past the compiled-set
  predicate silently. The pin now compares exact include ARRAYS against the
  two shapes that exist today ([src] and replay-port/session-journal's
  command-registry global.d.ts sibling); extending it is a deliberate edit
  that says whether insideCompiledSources must widen too. Mutation: adding a
  made-up src/generated-env.global.d.ts include to xml goes red.
- P3: the escape branch's rationale claimed every tunnel duplicates the
  module; duplication needs the SAME module also loaded through its package
  specifier (and type-only tunnels cost nothing at runtime). Doc block and
  violation message now state the mixing/dual-route condition, matching the
  file header's existing precise phrasing.
@thymikee

thymikee commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

I reviewed f105bbf. The code looks right, and all 22 checks pass at that commit, including the typecheck, layering and freerange routes this PR changes. I did not run tsc -b, the planted-violation proofs, the wall-time numbers, the incremental-cone claim or the freerange output comparison myself, so those rest on the PR's own evidence and CI. I also did not check the TS 7.0.2 default for noUncheckedSideEffectImports; it only matters if a compiled file later adds a side-effect import '@agent-device/...', and none do today. I could not tell whether cd scripts/freerange && fr reads any baseline or config relative to cwd; the PR says the output is byte-identical, and CI is green. Please confirm that last point in the PR body. There are no conflicts.

Not blocking, take or leave: the gross diff is about 1,394 lines, over the 1,000-line budget in docs/agents/pull-requests.md, mostly from expanded reference objects in the package tsconfigs and the moved facade tests; the compact one-line { "path": ... } form that provider-testmu uses would shrink it. The per-module facade exhaustiveness fix in https://github.com/callstack/agent-device/blob/f105bbf/scripts/layering/facade-exports.ts#L139 is a second fix riding on this PR, so it could be split out or named as its own commit. provider-testmu uses ./dist-types/.tsbuildinfo and compact references, while the other 25 packages use ./dist-types/index.tsbuildinfo and expanded objects; matching them would keep the set uniform.

Would the smaller version work: wire the references, switch to tsc -b, and keep R11's unknown-package and exports-map branches, which cost nothing at runtime? That would drop the new compiled parameter, the insideCompiledSources predicate and its include/exclude pin tests. I looked and found no smaller design that still meets requirement 3 of #3279, which asks for the deletion, so the current shape follows the issue. Only an amended requirement 3 would change that.

On the open threads: all five cubic-dev-ai threads are addressed at f105bbf, so the author can resolve them. #3289 (comment) is fixed by the fail-closed note in package-boundaries.ts and its pin test. #3289 (comment) is benign, because project-references.test.ts:53 asserts both lists equal the workspace package set. #3289 (comment) is fixed by the per-module check at facade-surface.test.ts:117. #3289 (comment) is fixed by the exact include arrays. #3289 (comment) is fixed by the updated checkPackageInternalSites doc and message.

…fo name

Review nits on f105bbf (take-or-leave, both cheap): reference entries were
expanded to three lines by the JSON writer that generated the first batch;
provider-testmu's hand-written entry was already the compact shape, and it
read better next to the manifest it mirrors. Compacting all of them also cuts
~265 lines from the gross diff. provider-testmu's buildinfo keeps the 25-package
name (index.tsbuildinfo) so no project deviates from the one convention; the
filename is inside the gitignored dist-types and nothing pins it.
@thymikee

thymikee commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

Item 1 — the cwd/config confirmation, now explicit in the PR body's validation section. Finding, with code refs:

cd scripts/freerange && fr reads exactly ONE thing relative to cwd: the tsconfig, located by upward search from process.cwd() — findTypeScriptConfig(process.cwd()) → ts.findConfigFile(resolve(cwd), ts.sys.fileExists, 'tsconfig.json') (@chenglou/freerange/src/project.ts:426, src/typescript/project.ts:16-17). No baseline/ignore/cache concept exists in the tool: the shipped dist/fr.js bundle's entire node:fs surface is a single existsSync (file-mode arg check), with zero readFileSync/writeFileSync/mkdirSync/homedir/tmpdir references — coverage is recomputed from the program each run. The other process.cwd() uses are the report's display-only baseDirectory (src/lower/program.ts:14, src/ir/program.ts:434 → relative(baseDirectory, file) pretty-paths). So the cd exists only to anchor the upward search at the wrapper config; scripts/freerange/ holds nothing but tsconfig.json, and cwd carries no other meaning for the run. This also strengthens the byte-identity claim you couldn't re-run: identical program + stateless tool ⇒ the byte-identical No lint findings + coverage line between main's fr and the wrapper is a property, not a coincidence (both were also re-verified locally at the current head: 971/13547).

Bonus for the thing you couldn't check: TS 7.0.2's noUncheckedSideEffectImports is default-on — a tsconfig.json with just module/moduleResolution: nodenext + a side-effect import of a missing file fails with TS2882 without the flag set (planted in a scratch dir against this repo's tsc, exit 1). Irrelevant to this PR's tree — no bare side-effect imports exist in the compiled set — but it's the answer.

Item 2, positions:

  • (a) Taken (compaction half), at 77f3a47db. All reference entries are now the one-line shape testmu already had; −265 lines off the gross diff (now ~1,030 insertions / 90 files, well inside budget). The three-line expansion was JSON-writer noise from the generating pass, not a choice worth defending. I did not split facade-exports.ts into its own commit: fe8c19b25 already names the per-module fix as its own commit; f75da8ba8 is the only commit touching facade-exports.ts beyond that and it is pure test additions — a further split would move 0 source lines.
  • (b) Taken, same commit. provider-testmu now uses ./dist-types/index.tsbuildinfo like all 25 others; nothing pinned the old name and the file is inside gitignored dist-types/. Cold tsc -b from wiped buildinfos verified green at the head.

check:affected --run evidence for 77f3a47db lands in the body when the run completes.

@thymikee

thymikee commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

I reviewed the changes since f105bbf at 77f3a47, and they look good. The delta only compacts the reference entries and makes the tsbuildinfo name uniform. Every reference set is unchanged, and no script outside the tsconfigs names the old tsbuildinfo path. The PR body now answers the freerange cwd question from my last review. All checks pass, there are no conflicts, and nothing else blocks this PR.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 8, 2026
@thymikee
thymikee merged commit 131479b into main Oct 8, 2026
22 checks passed
@thymikee
thymikee deleted the refactor/ws3-project-references branch October 8, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Architecture WS3: TypeScript project references; typecheck via tsc -b

1 participant