From f60c51f42c936b32badd7943152dc9a50cfac137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 7 Oct 2026 17:08:03 +0200 Subject: [PATCH 01/11] refactor(typescript): declare project references and drive typecheck 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. --- examples/sdk/tsconfig.json | 39 ++++++++++- package.json | 2 +- packages/ad-replay/tsconfig.json | 19 ++++- packages/ad-script/tsconfig.json | 13 +++- packages/capture-kit/tsconfig.json | 16 ++++- packages/command-registry/tsconfig.json | 16 ++++- packages/contracts/tsconfig.json | 10 ++- packages/device-selection/tsconfig.json | 16 ++++- packages/host-kit/tsconfig.json | 13 +++- packages/kernel/tsconfig.json | 3 +- packages/maestro/tsconfig.json | 25 ++++++- packages/managed-allocation/tsconfig.json | 19 ++++- packages/platform-android/tsconfig.json | 25 ++++++- packages/platform-apple/tsconfig.json | 25 ++++++- packages/platform-harmonyos/tsconfig.json | 22 +++++- packages/platform-linux/tsconfig.json | 19 ++++- packages/platform-vega/tsconfig.json | 19 ++++- packages/platform-web/tsconfig.json | 22 +++++- packages/provider-limrun/tsconfig.json | 19 ++++- packages/provider-webdriver/tsconfig.json | 22 +++++- packages/provision-kit/tsconfig.json | 16 ++++- packages/proxy/tsconfig.json | 16 ++++- packages/replay-port/tsconfig.json | 43 +++++++++++- packages/replay-test/tsconfig.json | 13 +++- packages/selectors/tsconfig.json | 19 ++++- packages/session-journal/tsconfig.json | 11 ++- packages/xml/tsconfig.json | 3 +- tsconfig.json | 84 ++++++++++++++++++++++- 28 files changed, 517 insertions(+), 52 deletions(-) diff --git a/examples/sdk/tsconfig.json b/examples/sdk/tsconfig.json index c38f5fa7ab..7ea1624759 100644 --- a/examples/sdk/tsconfig.json +++ b/examples/sdk/tsconfig.json @@ -1,6 +1,9 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "../../.tmp/examples-sdk.tsbuildinfo", "paths": { "agent-device": ["../../src/sdk/index.ts"], "agent-device/io": ["../../src/sdk/io.ts"], @@ -21,5 +24,39 @@ // prior build, workspace link, or publish step. Running an example for real // still resolves `agent-device` as a self-referencing package (needs // `pnpm build`), matching how a real consumer would import it. - "include": [".", "../../packages/command-registry/src/global.d.ts"] + // + // Composite under `tsc -b`, so every file its program loads through `paths` + // must be listed here — hence the `../../src/**/*` include with the test and + // fixture exclusions. The workspace packages those sources reach are project + // references: their exports-map `.ts` targets resolve to the `dist-types` + // declarations the graph emits instead of becoming unlisted foreign inputs. + "include": [".", "../../src/**/*", "../../packages/command-registry/src/global.d.ts"], + "exclude": ["../../src/**/*.test.ts", "../../src/**/__tests__/**", "../../src/**/*.fixtures.ts"], + "references": [ + { "path": "../../packages/ad-replay" }, + { "path": "../../packages/ad-script" }, + { "path": "../../packages/capture-kit" }, + { "path": "../../packages/command-registry" }, + { "path": "../../packages/contracts" }, + { "path": "../../packages/device-selection" }, + { "path": "../../packages/host-kit" }, + { "path": "../../packages/kernel" }, + { "path": "../../packages/maestro" }, + { "path": "../../packages/managed-allocation" }, + { "path": "../../packages/platform-android" }, + { "path": "../../packages/platform-apple" }, + { "path": "../../packages/platform-harmonyos" }, + { "path": "../../packages/platform-linux" }, + { "path": "../../packages/platform-vega" }, + { "path": "../../packages/platform-web" }, + { "path": "../../packages/provider-limrun" }, + { "path": "../../packages/provider-webdriver" }, + { "path": "../../packages/provision-kit" }, + { "path": "../../packages/proxy" }, + { "path": "../../packages/replay-port" }, + { "path": "../../packages/replay-test" }, + { "path": "../../packages/selectors" }, + { "path": "../../packages/session-journal" }, + { "path": "../../packages/xml" } + ] } diff --git a/package.json b/package.json index 040fd12169..14563a3631 100644 --- a/package.json +++ b/package.json @@ -179,7 +179,7 @@ "check:unit": "pnpm test:unit && pnpm check:tmpdir-leaks && pnpm test:smoke", "check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit", "prepack": "npm_config_dry_run=false pnpm release:prepare", - "typecheck": "tsc -b packages/xml packages/kernel packages/contracts packages/device-selection packages/host-kit packages/capture-kit packages/managed-allocation packages/provision-kit packages/platform-apple packages/platform-android packages/platform-harmonyos packages/platform-vega packages/platform-linux packages/platform-web packages/ad-script packages/selectors packages/command-registry packages/session-journal packages/ad-replay packages/maestro packages/replay-port packages/replay-test packages/provider-webdriver packages/provider-limrun packages/proxy packages/provider-testmu && tsc -p tsconfig.json && tsc -p examples/sdk/tsconfig.json", + "typecheck": "tsc -b tsconfig.json examples/sdk/tsconfig.json", "test-app:install": "pnpm install --dir examples/test-app", "test-app:start": "pnpm --dir examples/test-app start", "test-app:ios": "pnpm --dir examples/test-app ios", diff --git a/packages/ad-replay/tsconfig.json b/packages/ad-replay/tsconfig.json index 935c871a4d..72daf07a79 100644 --- a/packages/ad-replay/tsconfig.json +++ b/packages/ad-replay/tsconfig.json @@ -6,7 +6,22 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../ad-script" + }, + { + "path": "../contracts" + }, + { + "path": "../kernel" + }, + { + "path": "../selectors" + } + ] } diff --git a/packages/ad-script/tsconfig.json b/packages/ad-script/tsconfig.json index 935c871a4d..3404580cdb 100644 --- a/packages/ad-script/tsconfig.json +++ b/packages/ad-script/tsconfig.json @@ -6,7 +6,16 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../contracts" + }, + { + "path": "../kernel" + } + ] } diff --git a/packages/capture-kit/tsconfig.json b/packages/capture-kit/tsconfig.json index 935c871a4d..fb9c5bf1bd 100644 --- a/packages/capture-kit/tsconfig.json +++ b/packages/capture-kit/tsconfig.json @@ -6,7 +6,19 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../contracts" + }, + { + "path": "../host-kit" + }, + { + "path": "../kernel" + } + ] } diff --git a/packages/command-registry/tsconfig.json b/packages/command-registry/tsconfig.json index 935c871a4d..80ad27d7d9 100644 --- a/packages/command-registry/tsconfig.json +++ b/packages/command-registry/tsconfig.json @@ -6,7 +6,19 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../contracts" + }, + { + "path": "../kernel" + }, + { + "path": "../selectors" + } + ] } diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json index 935c871a4d..cf91ccaef6 100644 --- a/packages/contracts/tsconfig.json +++ b/packages/contracts/tsconfig.json @@ -6,7 +6,13 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../kernel" + } + ] } diff --git a/packages/device-selection/tsconfig.json b/packages/device-selection/tsconfig.json index 935c871a4d..fb9c5bf1bd 100644 --- a/packages/device-selection/tsconfig.json +++ b/packages/device-selection/tsconfig.json @@ -6,7 +6,19 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../contracts" + }, + { + "path": "../host-kit" + }, + { + "path": "../kernel" + } + ] } diff --git a/packages/host-kit/tsconfig.json b/packages/host-kit/tsconfig.json index 935c871a4d..3404580cdb 100644 --- a/packages/host-kit/tsconfig.json +++ b/packages/host-kit/tsconfig.json @@ -6,7 +6,16 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../contracts" + }, + { + "path": "../kernel" + } + ] } diff --git a/packages/kernel/tsconfig.json b/packages/kernel/tsconfig.json index 935c871a4d..fd7236ac7e 100644 --- a/packages/kernel/tsconfig.json +++ b/packages/kernel/tsconfig.json @@ -6,7 +6,8 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, "include": ["src"] } diff --git a/packages/maestro/tsconfig.json b/packages/maestro/tsconfig.json index 935c871a4d..3675eedfb0 100644 --- a/packages/maestro/tsconfig.json +++ b/packages/maestro/tsconfig.json @@ -6,7 +6,28 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../capture-kit" + }, + { + "path": "../contracts" + }, + { + "path": "../host-kit" + }, + { + "path": "../kernel" + }, + { + "path": "../provision-kit" + }, + { + "path": "../selectors" + } + ] } diff --git a/packages/managed-allocation/tsconfig.json b/packages/managed-allocation/tsconfig.json index 935c871a4d..71a6557d7f 100644 --- a/packages/managed-allocation/tsconfig.json +++ b/packages/managed-allocation/tsconfig.json @@ -6,7 +6,22 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../capture-kit" + }, + { + "path": "../contracts" + }, + { + "path": "../host-kit" + }, + { + "path": "../kernel" + } + ] } diff --git a/packages/platform-android/tsconfig.json b/packages/platform-android/tsconfig.json index 935c871a4d..3d96620019 100644 --- a/packages/platform-android/tsconfig.json +++ b/packages/platform-android/tsconfig.json @@ -6,7 +6,28 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../capture-kit" + }, + { + "path": "../contracts" + }, + { + "path": "../host-kit" + }, + { + "path": "../kernel" + }, + { + "path": "../provision-kit" + }, + { + "path": "../xml" + } + ] } diff --git a/packages/platform-apple/tsconfig.json b/packages/platform-apple/tsconfig.json index 935c871a4d..3d96620019 100644 --- a/packages/platform-apple/tsconfig.json +++ b/packages/platform-apple/tsconfig.json @@ -6,7 +6,28 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../capture-kit" + }, + { + "path": "../contracts" + }, + { + "path": "../host-kit" + }, + { + "path": "../kernel" + }, + { + "path": "../provision-kit" + }, + { + "path": "../xml" + } + ] } diff --git a/packages/platform-harmonyos/tsconfig.json b/packages/platform-harmonyos/tsconfig.json index 935c871a4d..64db9f0500 100644 --- a/packages/platform-harmonyos/tsconfig.json +++ b/packages/platform-harmonyos/tsconfig.json @@ -6,7 +6,25 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../capture-kit" + }, + { + "path": "../contracts" + }, + { + "path": "../host-kit" + }, + { + "path": "../kernel" + }, + { + "path": "../provision-kit" + } + ] } diff --git a/packages/platform-linux/tsconfig.json b/packages/platform-linux/tsconfig.json index 935c871a4d..71a6557d7f 100644 --- a/packages/platform-linux/tsconfig.json +++ b/packages/platform-linux/tsconfig.json @@ -6,7 +6,22 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../capture-kit" + }, + { + "path": "../contracts" + }, + { + "path": "../host-kit" + }, + { + "path": "../kernel" + } + ] } diff --git a/packages/platform-vega/tsconfig.json b/packages/platform-vega/tsconfig.json index 935c871a4d..80968b9f86 100644 --- a/packages/platform-vega/tsconfig.json +++ b/packages/platform-vega/tsconfig.json @@ -6,7 +6,22 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../contracts" + }, + { + "path": "../host-kit" + }, + { + "path": "../kernel" + }, + { + "path": "../provision-kit" + } + ] } diff --git a/packages/platform-web/tsconfig.json b/packages/platform-web/tsconfig.json index 935c871a4d..1ea0a81460 100644 --- a/packages/platform-web/tsconfig.json +++ b/packages/platform-web/tsconfig.json @@ -6,7 +6,25 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../capture-kit" + }, + { + "path": "../contracts" + }, + { + "path": "../host-kit" + }, + { + "path": "../kernel" + }, + { + "path": "../selectors" + } + ] } diff --git a/packages/provider-limrun/tsconfig.json b/packages/provider-limrun/tsconfig.json index 935c871a4d..5a1ea3f1f7 100644 --- a/packages/provider-limrun/tsconfig.json +++ b/packages/provider-limrun/tsconfig.json @@ -6,7 +6,22 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../capture-kit" + }, + { + "path": "../contracts" + }, + { + "path": "../kernel" + }, + { + "path": "../platform-android" + } + ] } diff --git a/packages/provider-webdriver/tsconfig.json b/packages/provider-webdriver/tsconfig.json index 935c871a4d..375c4d890a 100644 --- a/packages/provider-webdriver/tsconfig.json +++ b/packages/provider-webdriver/tsconfig.json @@ -6,7 +6,25 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../capture-kit" + }, + { + "path": "../contracts" + }, + { + "path": "../host-kit" + }, + { + "path": "../kernel" + }, + { + "path": "../xml" + } + ] } diff --git a/packages/provision-kit/tsconfig.json b/packages/provision-kit/tsconfig.json index 935c871a4d..fb9c5bf1bd 100644 --- a/packages/provision-kit/tsconfig.json +++ b/packages/provision-kit/tsconfig.json @@ -6,7 +6,19 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../contracts" + }, + { + "path": "../host-kit" + }, + { + "path": "../kernel" + } + ] } diff --git a/packages/proxy/tsconfig.json b/packages/proxy/tsconfig.json index 935c871a4d..fb9c5bf1bd 100644 --- a/packages/proxy/tsconfig.json +++ b/packages/proxy/tsconfig.json @@ -6,7 +6,19 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../contracts" + }, + { + "path": "../host-kit" + }, + { + "path": "../kernel" + } + ] } diff --git a/packages/replay-port/tsconfig.json b/packages/replay-port/tsconfig.json index 81bef0203e..ec4c62c373 100644 --- a/packages/replay-port/tsconfig.json +++ b/packages/replay-port/tsconfig.json @@ -6,7 +6,46 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src", "../command-registry/src/global.d.ts"] + "include": ["src", "../command-registry/src/global.d.ts"], + "references": [ + { + "path": "../ad-replay" + }, + { + "path": "../ad-script" + }, + { + "path": "../capture-kit" + }, + { + "path": "../command-registry" + }, + { + "path": "../contracts" + }, + { + "path": "../device-selection" + }, + { + "path": "../host-kit" + }, + { + "path": "../kernel" + }, + { + "path": "../maestro" + }, + { + "path": "../replay-test" + }, + { + "path": "../selectors" + }, + { + "path": "../session-journal" + } + ] } diff --git a/packages/replay-test/tsconfig.json b/packages/replay-test/tsconfig.json index 935c871a4d..3404580cdb 100644 --- a/packages/replay-test/tsconfig.json +++ b/packages/replay-test/tsconfig.json @@ -6,7 +6,16 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../contracts" + }, + { + "path": "../kernel" + } + ] } diff --git a/packages/selectors/tsconfig.json b/packages/selectors/tsconfig.json index 935c871a4d..01473e83b7 100644 --- a/packages/selectors/tsconfig.json +++ b/packages/selectors/tsconfig.json @@ -6,7 +6,22 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, - "include": ["src"] + "include": ["src"], + "references": [ + { + "path": "../ad-script" + }, + { + "path": "../capture-kit" + }, + { + "path": "../contracts" + }, + { + "path": "../kernel" + } + ] } diff --git a/packages/session-journal/tsconfig.json b/packages/session-journal/tsconfig.json index 005119d52e..1f981616ca 100644 --- a/packages/session-journal/tsconfig.json +++ b/packages/session-journal/tsconfig.json @@ -6,10 +6,17 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, // The command-registry sources this package imports read the `__OWNER_FILES__` build-time flag // declared beside them. The root and examples projects already carry that declaration for the // same reason; a package program that reaches registry.ts needs it too. - "include": ["src", "../command-registry/src/global.d.ts"] + "include": ["src", "../command-registry/src/global.d.ts"], + "references": [ + { "path": "../command-registry" }, + { "path": "../contracts" }, + { "path": "../host-kit" }, + { "path": "../kernel" } + ] } diff --git a/packages/xml/tsconfig.json b/packages/xml/tsconfig.json index 935c871a4d..fd7236ac7e 100644 --- a/packages/xml/tsconfig.json +++ b/packages/xml/tsconfig.json @@ -6,7 +6,8 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "rootDir": "./src" + "rootDir": "./src", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, "include": ["src"] } diff --git a/tsconfig.json b/tsconfig.json index d99f51c1a3..3eeb19b0e2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,14 +9,14 @@ "resolveJsonModule": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noEmit": true, "incremental": true, - "tsBuildInfoFile": ".tmp/tsconfig.tsbuildinfo", + "tsBuildInfoFile": "./.tmp/tsconfig.tsbuildinfo", "allowImportingTsExtensions": true, "erasableSyntaxOnly": true, "rewriteRelativeImportExtensions": true, "verbatimModuleSyntax": true, "types": ["node"], + "noEmit": true, "paths": { "agent-device/plugins": ["./src/sdk/plugins.ts"], "agent-device/plugins/webdriver": ["./src/sdk/plugin-webdriver.ts"] @@ -27,5 +27,85 @@ "test", "scripts/help-conformance-command-validator.ts", "packages/command-registry/src/global.d.ts" + ], + "references": [ + { + "path": "packages/ad-replay" + }, + { + "path": "packages/ad-script" + }, + { + "path": "packages/capture-kit" + }, + { + "path": "packages/command-registry" + }, + { + "path": "packages/contracts" + }, + { + "path": "packages/device-selection" + }, + { + "path": "packages/host-kit" + }, + { + "path": "packages/kernel" + }, + { + "path": "packages/maestro" + }, + { + "path": "packages/managed-allocation" + }, + { + "path": "packages/platform-android" + }, + { + "path": "packages/platform-apple" + }, + { + "path": "packages/platform-harmonyos" + }, + { + "path": "packages/platform-linux" + }, + { + "path": "packages/platform-vega" + }, + { + "path": "packages/platform-web" + }, + { + "path": "packages/provider-limrun" + }, + { + "path": "packages/provider-testmu" + }, + { + "path": "packages/provider-webdriver" + }, + { + "path": "packages/provision-kit" + }, + { + "path": "packages/proxy" + }, + { + "path": "packages/replay-port" + }, + { + "path": "packages/replay-test" + }, + { + "path": "packages/selectors" + }, + { + "path": "packages/session-journal" + }, + { + "path": "packages/xml" + } ] } From e0593d6211eecf61646333e4537b986254cd5b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 7 Oct 2026 17:08:03 +0200 Subject: [PATCH 02/11] refactor(layering): retire the R11 branches tsc -b now enforces natively 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//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). --- scripts/layering/check.ts | 6 +- scripts/layering/package-boundaries.test.ts | 85 ++++++++---- scripts/layering/package-boundaries.ts | 139 +++++++++++++------- 3 files changed, 156 insertions(+), 74 deletions(-) diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index dda8227948..35c7406798 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -32,8 +32,10 @@ // - Across the DAEMON MODULARITY MIGRATION: R7 ownership pressure and external // daemon request/session-state importers only shrink, R9 zone membership cannot grow or absorb // engine files, and planned logical modules start with zero forbidden/internal imports (R10). -// - Over the WORKSPACE PACKAGES: no root back-imports, no relative tunnelling past -// an exports map, and every workspace specifier declared + exports-named (R11). +// - Over the WORKSPACE PACKAGES: every workspace specifier declared, no relative tunnelling +// into a package's src, and the full specifier sweep for the scripts/ files the type graph +// omits — resolution itself (unknown package, un-exported subpath) is TS2307 under tsc -b +// for compiled sources (#3279) (R11). // - Over PLATFORM PACKAGE COMPOSITION: six private metadata façades meet at the exact root // composition file; premature implementation loading and forbidden cross-boundary edges fail (R13). // - Over THE APPLE RUNNER SUBTREE: `runner/**` may not value-import `@agent-device/host-kit/*` diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index 91e60414e2..422507b890 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -15,6 +15,7 @@ import { facadeEntryFiles, checkPackageInternalSites, checkRootSites, + insideCompiledSources, readWorkspacePackages, rootExternalDependencyRanges, rootWorkspaceDependencyNames, @@ -109,6 +110,21 @@ test('specifier sites carry 1-based lines for static and dynamic imports', () => ); }); +test('the compiled set names exactly the file set the tsc -b graph parses', () => { + // The compiler-owned/compiler-exempt split above is keyed on this predicate (#3279), so the + // mutation it must fail on is a file joining or leaving `tsc -b`'s programs without the + // predicate following: `scripts/` and the package harness dirs are run by Node's type + // stripper and vitest, never by a tsconfig program, while package `src/`, root `src/` and + // root `test/` all compile. + assert.equal(insideCompiledSources('packages/kernel/src/errors.ts'), true); + assert.equal(insideCompiledSources('packages/maestro/src/index.ts'), true); + assert.equal(insideCompiledSources('src/cli.ts'), true); + assert.equal(insideCompiledSources('test/integration/smoke-web-platform.test.ts'), true); + assert.equal(insideCompiledSources('scripts/layering/check.ts'), false); + assert.equal(insideCompiledSources('packages/maestro/test/conformance/harness.ts'), false); + assert.equal(insideCompiledSources('packages/proxy/tsdown.config.ts'), false); +}); + test('readWorkspacePackages reads tracked manifests only', () => { // R11's own committed-state property, and the source-level half of the #1965 review finding. // `readWorkspacePackages` used to enumerate `packages/` with `readdirSync`, so an uncommitted @@ -279,34 +295,40 @@ test('double-quoted and re-export routes into packages are not invisible to R11' 'src/utils/exec.ts', 'import { AppError } from "../../packages/kernel/src/errors.ts";', ); - assert.equal(checkRootSites(doubleQuoted, ALL, new Set([kernel.name])).length, 1); + assert.equal(checkRootSites(doubleQuoted, ALL, new Set([kernel.name]), true).length, 1); const reExport = specifierSites( 'src/utils/exec.ts', 'export { AppError } from "../../packages/kernel/src/errors.ts";', ); - assert.equal(checkRootSites(reExport, ALL, new Set([kernel.name])).length, 1); + assert.equal(checkRootSites(reExport, ALL, new Set([kernel.name]), true).length, 1); const dynamic = specifierSites( 'src/utils/exec.ts', 'void import("../../packages/kernel/src/errors.ts");', ); - assert.equal(checkRootSites(dynamic, ALL, new Set([kernel.name])).length, 1); + assert.equal(checkRootSites(dynamic, ALL, new Set([kernel.name]), true).length, 1); const packageEscape = specifierSites( 'packages/kernel/src/errors.ts', 'export * from "../../../src/utils/exec.ts";', ); - assert.equal(checkPackageInternalSites(kernel, packageEscape, ALL).length, 1); + // The escape is compiler-owned for compiled sources; scripts/ and package harness files that + // sit outside the type graph keep the R11 branch (#3279). + assert.equal(checkPackageInternalSites(kernel, packageEscape, ALL, true).length, 0); + assert.equal(checkPackageInternalSites(kernel, packageEscape, ALL, false).length, 1); }); // This `src/utils` path is another in-memory arbitrary parser fixture, not a live repository path. -test('a package file importing root src is a violation', () => { +test('a package file importing root src is compiler-owned once tsc -b sees it', () => { + // #3279: composite `rootDir` rejects a package->root relative escape with TS6059 + TS6307 + // (planted proof in the PR). R11 keeps the branch only for package files the graph omits. const sites = specifierSites( 'packages/kernel/src/errors.ts', "import { helper } from '../../../src/utils/exec.ts';", ); - assert.deepEqual(rules(checkPackageInternalSites(kernel, sites, ALL)), [ + assert.deepEqual(checkPackageInternalSites(kernel, sites, ALL, true), []); + assert.deepEqual(rules(checkPackageInternalSites(kernel, sites, ALL, false)), [ 'R11 package-boundaries', ]); }); @@ -316,50 +338,60 @@ test('intra-package relative imports hold', () => { 'packages/kernel/src/daemon-error.ts', "import { AppError } from './errors.ts';", ); - assert.deepEqual(checkPackageInternalSites(kernel, sites, ALL), []); + assert.deepEqual(checkPackageInternalSites(kernel, sites, ALL, true), []); }); -test('an exported package self-reference holds while a deep self-import fails', () => { +test('an exported package self-reference holds while a deep self-import is compiler-owned', () => { const exported = specifierSites( 'packages/kernel/src/errors.test.ts', "import { AppError } from '@agent-device/kernel/errors';", ); - assert.deepEqual(checkPackageInternalSites(kernel, exported, ALL), []); + assert.deepEqual(checkPackageInternalSites(kernel, exported, ALL, true), []); + // #3279: NodeNext resolution rejects the un-exported self subpath with TS2307 (planted proof); + // R11 keeps the branch only where the compiler cannot see the site. const deep = specifierSites( 'packages/kernel/src/errors.test.ts', "import { AppError } from '@agent-device/kernel/src/errors.ts';", ); - assert.equal(checkPackageInternalSites(kernel, deep, ALL).length, 1); + assert.equal(checkPackageInternalSites(kernel, deep, ALL, true).length, 0); + assert.equal(checkPackageInternalSites(kernel, deep, ALL, false).length, 1); }); -test('a cross-package import needs a workspace:* declaration and an exported subpath', () => { +test('a cross-package import needs a workspace:* declaration; the exports map is compiler-owned', () => { const declared = specifierSites( 'packages/contracts/src/gesture.ts', "import { AppError } from '@agent-device/kernel/errors';", ); - assert.deepEqual(checkPackageInternalSites(contracts, declared, ALL), []); + assert.deepEqual(checkPackageInternalSites(contracts, declared, ALL, true), []); + // The undeclared-dep branch stays R11's own for compiled files: pnpm's root link farm makes + // the import resolve and typecheck anyway, and the declaration is what the published bundle + // externalizes against — no compiler check can express it (recorded in #3279). const undeclared = specifierSites( 'packages/kernel/src/errors.ts', "import { g } from '@agent-device/contracts/interaction';", ); - assert.equal(checkPackageInternalSites(kernel, undeclared, ALL).length, 1); + assert.equal(checkPackageInternalSites(kernel, undeclared, ALL, true).length, 1); + // The un-exported subpath is TS2307 for compiled files (planted proof in #3279). const deep = specifierSites( 'packages/contracts/src/gesture.ts', "import { internal } from '@agent-device/kernel/internal/secret';", ); - assert.equal(checkPackageInternalSites(contracts, deep, ALL).length, 1); + assert.equal(checkPackageInternalSites(contracts, deep, ALL, true).length, 0); }); test('a root src file tunnelling into packages/*/src relatively is a violation', () => { // This `src/utils` path is an in-memory arbitrary parser fixture, not a live repository path. + // It stays R11's own for every root file, compiled or not: #3279 planted the tunnel inside the + // compiled tree and tsc accepted it — the failure this rule prevents is the runtime one where + // Node's ESM loader instantiates the module twice. const sites = specifierSites( 'src/utils/exec.ts', "import { AppError } from '../../packages/kernel/src/errors.ts';", ); - const violations = checkRootSites(sites, ALL, new Set([kernel.name])); + const violations = checkRootSites(sites, ALL, new Set([kernel.name]), true); assert.deepEqual(rules(violations), ['R11 package-boundaries']); assert.match(violations[0]!.message, /instantiate the module twice/); }); @@ -373,34 +405,41 @@ test('a scripts/ file gets no relative route into a package either', () => { 'scripts/some-tool/run.ts', "import { AppError } from '../../packages/kernel/src/errors.ts';", ); - assert.equal(checkRootSites(exportsNamed, ALL, new Set([kernel.name])).length, 1); + assert.equal(checkRootSites(exportsNamed, ALL, new Set([kernel.name]), false).length, 1); const nonExported = specifierSites( 'scripts/some-tool/run.ts', "import { hidden } from '../../packages/kernel/src/internal.ts';", ); - assert.equal(checkRootSites(nonExported, ALL, new Set([kernel.name])).length, 1); + assert.equal(checkRootSites(nonExported, ALL, new Set([kernel.name]), false).length, 1); }); -test('root workspace specifiers need a root workspace:* entry and an exported subpath', () => { +test('root workspace specifiers need a root workspace:* entry; resolution is compiler-owned', () => { const fine = specifierSites( 'src/cli.ts', "import { AppError } from '@agent-device/kernel/errors';", ); - assert.deepEqual(checkRootSites(fine, ALL, new Set([kernel.name])), []); + assert.deepEqual(checkRootSites(fine, ALL, new Set([kernel.name]), true), []); - const undeclared = checkRootSites(fine, ALL, new Set()); + // The root-declaration branch stays R11's own (#3279): the root link farm resolves an + // undeclared specifier anyway, and the published bundle externalizes against this entry. + const undeclared = checkRootSites(fine, ALL, new Set(), true); assert.equal(undeclared.length, 1); assert.match(undeclared[0]!.message, /workspace:\*/); + // #3279 planted both below inside the compiled tree and `tsc -b` failed them with TS2307 + // (unknown package and non-exported subpath), so R11 stands down there; uncompiled sites, + // which the compiler never parses, keep both branches. const deep = specifierSites( 'src/cli.ts', "import { x } from '@agent-device/kernel/src/errors.ts';", ); - assert.equal(checkRootSites(deep, ALL, new Set([kernel.name])).length, 1); + assert.equal(checkRootSites(deep, ALL, new Set([kernel.name]), true).length, 0); + assert.equal(checkRootSites(deep, ALL, new Set([kernel.name]), false).length, 1); const unknown = specifierSites('src/cli.ts', "import { x } from '@agent-device/nope/thing';"); - assert.equal(checkRootSites(unknown, ALL, new Set([kernel.name])).length, 1); + assert.equal(checkRootSites(unknown, ALL, new Set([kernel.name]), true).length, 0); + assert.equal(checkRootSites(unknown, ALL, new Set([kernel.name]), false).length, 1); }); test('a published package declares its bundled workspace siblings as devDependencies', () => { @@ -427,7 +466,7 @@ test('a published package declares its bundled workspace siblings as devDependen 'packages/published/src/index.ts', "import { g } from '@agent-device/contracts/interaction';", ); - const [violation] = checkPackageInternalSites(published, undeclared, [...ALL, published]); + const [violation] = checkPackageInternalSites(published, undeclared, [...ALL, published], true); assert.match(violation?.message ?? '', /packages\/published\/package\.json devDependencies\.$/); }); diff --git a/scripts/layering/package-boundaries.ts b/scripts/layering/package-boundaries.ts index 4c8e9cbbdd..ffabddd5ae 100644 --- a/scripts/layering/package-boundaries.ts +++ b/scripts/layering/package-boundaries.ts @@ -1,33 +1,30 @@ -// Catches: a package reaching back into root src/, a root file tunnelling into packages/*/src -// with a relative path, an undeclared workspace import, or a subpath the exports map does not -// name — bypasses Node's own resolution error cannot see, because a relative route resolves -// fine even though it duplicates the module under its specifier form. +// Catches: a package importing a workspace sibling its manifest never declared, and a root +// file tunnelling into a package's src with a relative path — both for every file, compiled +// or not — plus the whole specifier sweep (unknown package, un-exported subpath) for the +// scripts/ files the type graph omits. `tsc -b` owns the rest of the old R11 surface for +// compiled sources: NodeNext resolution rejects a non-exported subpath and an unknown +// package (TS2307), and composite `rootDir` rejects a package→root relative escape +// (TS6059 + TS6307) — planted proofs recorded on #3279. // Evidence: 76453add71 (#1494, #1490 W0) established the workspace split this rule protects; -// 83322a3f2f (#1574) pinned exact facade symbols for every workspace package. -// Cost: 1239 LOC (363 rule + 876 test). -// Kill criterion: none enforced today; retire only by maintainer decision that the workspace -// boundary no longer matters. `pnpm typecheck` already covers two branches for src/ and -// packages/ importers (NodeNext rejects a non-exported subpath with TS2307; composite rootDir -// rejects a package→root relative escape with TS6059), but the A4 spike found no compiler -// mechanism for an undeclared workspace:* dependency, a root→packages/*/src relative tunnel, -// or any scripts/ import: project references are a build-cache mechanism, not a boundary. +// 83322a3f2f (#1574) pinned exact facade symbols for every workspace package; #3279 moved the +// type-check onto `tsc -b` project references and retired the checks it now enforces. +// Cost: 447 LOC (rule) + 978 LOC (test). +// Kill criterion: retire a remaining branch only when something owns its mechanism, not merely +// its symptom. An undeclared `workspace:*` sibling is type-clean and runtime-green INSIDE +// this repo because pnpm links every workspace package under the root `node_modules` — the +// declaration is what the published bundle externalizes against, so no compiler check can +// express it here (planted in #3279: `tsc -b` accepted it). A relative root→package tunnel +// typechecks too; its failure is the runtime double instantiation this rule names. // // R11 package-boundaries: the workspace rules of #1490, as data the gate walks. // -// Package resolution already makes a deep `@agent-device/*` specifier a runtime -// resolution error; these checks close the bypasses resolution alone cannot see: -// a package reaching back into root `src/`, a root file tunnelling into -// `packages/*/src` with a relative path, an import of a workspace package the -// manifest never declared, and a specifier subpath the owning `exports` map -// does not name. -// -// No relative route into a package is tolerated. Node's ESM loader does not -// realpath specifiers, so a module loaded BOTH relatively and via its package -// specifier instantiates twice in one process (duplicate AppError, broken -// instanceof). The one exception this rule used to grant — a file inside an R8 -// zero-dep job closure, where no node_modules means specifier loads cannot -// coexist — retired with R8 itself (#1781 A6), because the repo runs no -// `install-deps: false` job for it to cover. +// What remains is exactly what the type graph cannot see: +// a package import of a sibling the manifest never declared (pnpm's root links make it resolve +// anyway), a root file tunnelling into `packages/*/src` with a relative path (tsc typechecks +// it fine; Node's ESM loader does not realpath specifiers, so a module loaded BOTH relatively +// and via its package specifier instantiates twice in one process — duplicate AppError, broken +// instanceof), and, for `scripts/` files which sit outside the type graph by design, the +// root-side unknown-package and exports-map checks that compiled files get from TS2307. import fs from 'node:fs'; import path from 'node:path'; @@ -149,18 +146,37 @@ function specifierPackageName(specifier: string): string | undefined { } /** - * Rules for files INSIDE a package: no relative escape past the package dir; - * exported package self-references are legal; any sibling-package import must - * be declared `workspace:*`; and every package specifier must name an export. + * The file set the `tsc -b` graph compiles: each package project includes its own `src/`, and + * the root project includes `src/` and `test/`. Everything else the R11 walk visits — `scripts/` + * (whose gate and tooling modules the gate runs through `--experimental-strip-types`, never + * through a `tsconfig`), the `packages/maestro/test/` conformance harness, and package build + * configs — reaches packages through import sites the compiler never parses, so R11 stays the + * only owner of every boundary claim about those routes (#3279). + */ +export function insideCompiledSources(file: string): boolean { + return /^packages\/[^/]+\/src\//.test(file) || /^src\//.test(file) || /^test\//.test(file); +} + +/** + * Rules for files INSIDE a package. An undeclared sibling import is R11's alone: pnpm links + * every workspace package under the root `node_modules`, so the import resolves, compiles, and + * runs in this repo even when the manifest never declares it — and a published bundle + * externalizes against that declaration. The relative escape past the package dir and every + * resolution failure (unknown package, subpath the exports map does not name) are owned by the + * compiler for compiled files: composite `rootDir` fails the escape with TS6059 + TS6307 and + * NodeNext resolution fails the specifier with TS2307. Both are proven in #3279; uncompiled + * files never reach either check, so for them R11 holds every branch. */ export function checkPackageInternalSites( pkg: WorkspacePackage, sites: readonly SpecifierSite[], allPackages: readonly WorkspacePackage[], + compiled: boolean, ): PackageBoundaryViolation[] { const violations: PackageBoundaryViolation[] = []; for (const site of sites) { if (site.specifier.startsWith('.')) { + if (compiled) continue; const resolved = path.posix.normalize( path.posix.join(path.posix.dirname(site.file), site.specifier), ); @@ -180,12 +196,14 @@ export function checkPackageInternalSites( if (!name || !name.startsWith('@agent-device/')) continue; const target = packageByName(allPackages, name); if (!target) { - violations.push({ - rule: 'R11 package-boundaries', - file: site.file, - line: site.line, - message: `'${site.specifier}' names an unknown workspace package.`, - }); + if (!compiled) { + violations.push({ + rule: 'R11 package-boundaries', + file: site.file, + line: site.line, + message: `'${site.specifier}' names an unknown workspace package.`, + }); + } continue; } if (name !== pkg.name && !pkg.workspaceDependencies.has(name)) { @@ -198,7 +216,7 @@ export function checkPackageInternalSites( `in ${pkg.dir}/package.json ${pkg.workspaceDependencyField}.`, }); } - if (!target.exportTargets.has(site.specifier)) { + if (!compiled && !target.exportTargets.has(site.specifier)) { violations.push({ rule: 'R11 package-boundaries', file: site.file, @@ -213,14 +231,20 @@ export function checkPackageInternalSites( } /** - * Rules for files OUTSIDE packages/ (src, test, scripts): workspace specifiers - * must be root-declared and exports-named, and relative paths into - * `packages/·/src` are forbidden outright. + * Rules for files OUTSIDE packages/ (src, test, scripts). Two claims remain R11's own for every + * root file: the relative tunnel into a packages//src directory — the compiler typechecks + * such an import + * fine (#3279), but Node's ESM loader does not realpath specifiers, so dual relative/specifier + * loads instantiate the module twice — and a workspace specifier the root manifest never + * declares, which the root's own link farm resolves anyway. For compiled root files the + * compiler owns unknown packages and un-exported subpaths with TS2307; for scripts/ files, + * which sit outside the type graph by design, R11 keeps every branch. */ export function checkRootSites( sites: readonly SpecifierSite[], packages: readonly WorkspacePackage[], rootWorkspaceDependencies: ReadonlySet, + compiled: boolean, ): PackageBoundaryViolation[] { const violations: PackageBoundaryViolation[] = []; for (const site of sites) { @@ -243,12 +267,14 @@ export function checkRootSites( if (!name || !name.startsWith('@agent-device/')) continue; const target = packageByName(packages, name); if (!target) { - violations.push({ - rule: 'R11 package-boundaries', - file: site.file, - line: site.line, - message: `'${site.specifier}' names an unknown workspace package.`, - }); + if (!compiled) { + violations.push({ + rule: 'R11 package-boundaries', + file: site.file, + line: site.line, + message: `'${site.specifier}' names an unknown workspace package.`, + }); + } continue; } if (!rootWorkspaceDependencies.has(name)) { @@ -261,7 +287,7 @@ export function checkRootSites( `package.json devDependencies.`, }); } - if (!target.exportTargets.has(site.specifier)) { + if (!compiled && !target.exportTargets.has(site.specifier)) { violations.push({ rule: 'R11 package-boundaries', file: site.file, @@ -379,7 +405,14 @@ export function checkPackageBoundaries(repoRoot: string): PackageBoundaryViolati for (const pkg of packages) { for (const file of walkTsFiles(repoRoot, pkg.dir)) { const source = fs.readFileSync(path.join(repoRoot, file), 'utf8'); - violations.push(...checkPackageInternalSites(pkg, specifierSites(file, source), packages)); + violations.push( + ...checkPackageInternalSites( + pkg, + specifierSites(file, source), + packages, + insideCompiledSources(file), + ), + ); } } for (const root of ['src', 'test', 'scripts']) { @@ -389,7 +422,14 @@ export function checkPackageBoundaries(repoRoot: string): PackageBoundaryViolati // src/ and test/ suites stay covered — they import packages for real. if (root === 'scripts' && file.endsWith('.test.ts')) continue; const source = fs.readFileSync(path.join(repoRoot, file), 'utf8'); - violations.push(...checkRootSites(specifierSites(file, source), packages, rootDependencies)); + violations.push( + ...checkRootSites( + specifierSites(file, source), + packages, + rootDependencies, + insideCompiledSources(file), + ), + ); } } return violations; @@ -401,6 +441,7 @@ export function packageBoundariesSummary(repoRoot: string): string { const exported = packages.reduce((sum, pkg) => sum + pkg.exportTargets.size, 0); return ( `R11 holds ${packages.length} workspace package(s) behind ${exported} exported subpath(s) ` + - `with zero root back-imports` + `with zero undeclared or relative-tunnel import routes (the compiler owns specifier ` + + `resolution for compiled sources)` ); } From 48e0f6756de0300d51af25b61b2c258f1418f6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 7 Oct 2026 18:15:41 +0200 Subject: [PATCH 03/11] test(layering): pin the project-reference lists against the workspace 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. --- scripts/layering/package-boundaries.test.ts | 15 ++-- scripts/layering/package-boundaries.ts | 20 +++-- scripts/layering/project-references.test.ts | 90 +++++++++++++++++++++ 3 files changed, 114 insertions(+), 11 deletions(-) create mode 100644 scripts/layering/project-references.test.ts diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index 422507b890..5b973cef92 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -110,12 +110,11 @@ test('specifier sites carry 1-based lines for static and dynamic imports', () => ); }); -test('the compiled set names exactly the file set the tsc -b graph parses', () => { +test('the compiled set classifies every file region the R11 walk visits', () => { // The compiler-owned/compiler-exempt split above is keyed on this predicate (#3279), so the - // mutation it must fail on is a file joining or leaving `tsc -b`'s programs without the - // predicate following: `scripts/` and the package harness dirs are run by Node's type - // stripper and vitest, never by a tsconfig program, while package `src/`, root `src/` and - // root `test/` all compile. + // mutation it must fail on is a file region joining or leaving `tsc -b`'s programs without + // the predicate following: package `src/`, root `src/` and root `test/` compile; `scripts/` + // and the package harness dirs are the fail-closed side. assert.equal(insideCompiledSources('packages/kernel/src/errors.ts'), true); assert.equal(insideCompiledSources('packages/maestro/src/index.ts'), true); assert.equal(insideCompiledSources('src/cli.ts'), true); @@ -123,6 +122,12 @@ test('the compiled set names exactly the file set the tsc -b graph parses', () = assert.equal(insideCompiledSources('scripts/layering/check.ts'), false); assert.equal(insideCompiledSources('packages/maestro/test/conformance/harness.ts'), false); assert.equal(insideCompiledSources('packages/proxy/tsdown.config.ts'), false); + + // The one scripts file the root program DOES compile is classified as uncompiled: R11 + // keeps its full branch set there (redundant with tsc, never narrower). The exact root + // include list and the reference sets are pinned against the graph itself in + // project-references.test.ts, where the divergence is recorded rather than assumed absent. + assert.equal(insideCompiledSources('scripts/help-conformance-command-validator.ts'), false); }); test('readWorkspacePackages reads tracked manifests only', () => { diff --git a/scripts/layering/package-boundaries.ts b/scripts/layering/package-boundaries.ts index ffabddd5ae..afb951a822 100644 --- a/scripts/layering/package-boundaries.ts +++ b/scripts/layering/package-boundaries.ts @@ -146,12 +146,20 @@ function specifierPackageName(specifier: string): string | undefined { } /** - * The file set the `tsc -b` graph compiles: each package project includes its own `src/`, and - * the root project includes `src/` and `test/`. Everything else the R11 walk visits — `scripts/` - * (whose gate and tooling modules the gate runs through `--experimental-strip-types`, never - * through a `tsconfig`), the `packages/maestro/test/` conformance harness, and package build - * configs — reaches packages through import sites the compiler never parses, so R11 stays the - * only owner of every boundary claim about those routes (#3279). + * The file set R11 treats as parsed by the `tsc -b` graph: each package project includes its + * own `src/`, and the root project includes `src/` and `test/`. Everything else the R11 walk + * visits — the rest of `scripts/` (gate and tooling modules run through + * `--experimental-strip-types`, not a tsconfig program), the `packages/maestro/test/` + * conformance harness, and package build configs — reaches packages through import sites the + * compiler never parses, so R11 stays the only owner of every boundary claim about those + * routes (#3279). + * + * One documented divergence: the root program also includes + * `scripts/help-conformance-command-validator.ts`, which this predicate classifies as + * uncompiled. That is fail-closed — R11 keeps its full branch set (unknown package, + * exports-map) for the file on top of what tsc enforces — never fail-open. The divergence is + * pinned in `project-references.test.ts`, so a second scripts entry joining the root program + * must be recorded there rather than silently widening the exception. */ export function insideCompiledSources(file: string): boolean { return /^packages\/[^/]+\/src\//.test(file) || /^src\//.test(file) || /^test\//.test(file); diff --git a/scripts/layering/project-references.test.ts b/scripts/layering/project-references.test.ts new file mode 100644 index 0000000000..c86f11e124 --- /dev/null +++ b/scripts/layering/project-references.test.ts @@ -0,0 +1,90 @@ +// The TypeScript project graph is three hand-written views of one fact: the workspace package +// set, the root manifest's `workspace:*` declarations, and the `references` lists in the root, +// examples/sdk, and per-package tsconfigs. `tsc -b` fails loudly on a MISSING reference it +// needs but silently tolerates EXTRA ones (#3289 review), so a package added to the workspace +// while a references list goes un-updated keeps building a stale graph that still exits 0. +// These tests read the committed declarations against each other so the four views cannot +// drift: an added, renamed, or removed workspace package must edit every list or fail here. + +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { test } from 'node:test'; +import { readWorkspacePackages } from './package-boundaries.ts'; + +const repoRoot = path.resolve(import.meta.dirname, '../..'); + +/** tsconfig JSONC minus line comments; both comment-bearing files use `//` only. */ +function readTsconfig(relativePath: string): { references?: { path: string }[] } { + const source = fs.readFileSync(path.join(repoRoot, relativePath), 'utf8'); + return JSON.parse(source.replaceAll(/^\s*\/\/.*$/gm, '')); +} + +/** Repo-relative package dir for one `references[].path` entry, resolved from its project. */ +function referencedPackageDirs(tsconfigFile: string, projectDir: string): string[] { + const config = readTsconfig(tsconfigFile); + return (config.references ?? []) + .map((reference) => path.posix.normalize(path.posix.join(projectDir, reference.path))) + .sort(); +} + +function workspacePackageDirs(): string[] { + return readWorkspacePackages(repoRoot) + .map((pkg) => pkg.dir) + .sort(); +} + +test('root and examples/sdk reference exactly the workspace package set', () => { + const expected = workspacePackageDirs(); + assert.ok(expected.length >= 25, 'expected the 25+ workspace packages to be enumerable'); + + assert.deepEqual(referencedPackageDirs('tsconfig.json', '.'), expected); + assert.deepEqual(referencedPackageDirs('examples/sdk/tsconfig.json', 'examples/sdk'), expected); +}); + +test('the root manifest declares every workspace package as a workspace dependency', () => { + const manifest = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as { + dependencies?: Record; + devDependencies?: Record; + }; + const declared = Object.entries({ + ...manifest.dependencies, + ...manifest.devDependencies, + }) + .filter(([, range]) => String(range).startsWith('workspace:')) + .map(([name]) => name) + .sort(); + const expected = readWorkspacePackages(repoRoot) + .map((pkg) => pkg.name) + .sort(); + assert.deepEqual( + declared, + expected, + 'a workspace package the root program consumes must be declared workspace:* in the root ' + + 'manifest (its tsconfig references already name it; the root project resolves package ' + + 'specifiers through these declarations)', + ); +}); + +test('every package tsconfig references exactly its manifest workspace dependencies', () => { + // The per-package half of the same single-source-of-truth claim (#3279 requirement 1): + // references are DERIVED from the manifest DAG, so an import a manifest gains must add the + // matching reference (edge freshness, which tsc -b never asks for) and a removed one must + // drop it (an extra reference hides a stale edge that tsc -b tolerates silently). + const packages = readWorkspacePackages(repoRoot); + const nameByDir = new Map(packages.map((pkg) => [pkg.dir, pkg.name])); + for (const pkg of packages) { + const referencedDirs = referencedPackageDirs(`${pkg.dir}/tsconfig.json`, pkg.dir); + for (const dir of referencedDirs) { + assert.ok( + nameByDir.has(dir), + `${pkg.dir}/tsconfig.json references ${dir}, which is not a workspace package`, + ); + } + assert.deepEqual( + referencedDirs.map((dir) => nameByDir.get(dir)), + [...pkg.workspaceDependencies].sort(), + `${pkg.dir}/tsconfig.json references must equal its manifest workspace:* dependencies`, + ); + } +}); From 21556a2c277fbc0bbf9c1ae4e28551a3fbc39f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 7 Oct 2026 18:47:11 +0200 Subject: [PATCH 04/11] chore(gates): give freerange a reference-free tsconfig view of the root program MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- package.json | 2 +- scripts/freerange/tsconfig.json | 12 ++++++++++++ scripts/layering/project-references.test.ts | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 scripts/freerange/tsconfig.json diff --git a/package.json b/package.json index 14563a3631..d0ea93d527 100644 --- a/package.json +++ b/package.json @@ -170,7 +170,7 @@ "check:packaged-runner-swift": "node --experimental-strip-types scripts/check-packaged-runner-swift.ts", "check:tmpdir-leaks": "node --experimental-strip-types scripts/check-tmpdir-leaks.ts", "check:tmpdir-leaks:test": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test --test-concurrency=1 scripts/check-tmpdir-leaks-model.test.ts scripts/vitest-tmpdir-global-setup.test.ts scripts/node-test-tmpdir.test.ts scripts/swift-toolchain-tmpdir.test.ts", - "check:freerange": "fr", + "check:freerange": "cd scripts/freerange && fr", "check:quick": "pnpm lint && pnpm typecheck", "sync:mcp-metadata": "node scripts/sync-mcp-metadata.mjs", "check:mcp-metadata": "node scripts/sync-mcp-metadata.mjs --check", diff --git a/scripts/freerange/tsconfig.json b/scripts/freerange/tsconfig.json new file mode 100644 index 0000000000..d40b133363 --- /dev/null +++ b/scripts/freerange/tsconfig.json @@ -0,0 +1,12 @@ +{ + // Freerange's own view of the root program (#3289 CI OOM). `fr` resolves a tsconfig from the + // process cwd and then loads EVERY `references` project as a full simultaneous ts.Program — + // 26 programs / ~13.5k file slots / >2.8 GB heap before its analysis even starts — so running + // it from the repo root against the reference-bearing tsconfig.json exhausts Node's default + // 4 GB heap (exit 134). `extends` does not inherit `references`, so this file hands fr the + // single root program (includes and options resolve through the base config below, and + // package sources still reach the program transitively through the exports-map `.ts` targets + // exactly as they did before project references existed) while `tsc -b` keeps using the real + // graph. Keep this config reference-free: adding `references` here reintroduces the OOM. + "extends": "../../tsconfig.json" +} diff --git a/scripts/layering/project-references.test.ts b/scripts/layering/project-references.test.ts index c86f11e124..e04a802bc7 100644 --- a/scripts/layering/project-references.test.ts +++ b/scripts/layering/project-references.test.ts @@ -66,6 +66,21 @@ test('the root manifest declares every workspace package as a workspace dependen ); }); +test('the freerange config stays a reference-free view of the root program', () => { + // `fr` loads every `references` project as a full simultaneous ts.Program (~26 programs, + // >2.8 GB heap before analysis) and OOMs Node's default 4 GB on this workspace (#3289 CI). + // The wrapper at scripts/freerange/tsconfig.json exists only because `extends` does not + // inherit references (planted with --showConfig); if someone adds references to it, the + // gate reintroduces the OOM, so the absence is pinned here rather than trusted to the comment. + const config = readTsconfig('scripts/freerange/tsconfig.json'); + assert.equal(config.references, undefined, 'references on the freerange config OOM gate'); + assert.equal( + config.extends, + '../../tsconfig.json', + 'the freerange view must stay the root program itself (no include/exclude drift)', + ); +}); + test('every package tsconfig references exactly its manifest workspace dependencies', () => { // The per-package half of the same single-source-of-truth claim (#3279 requirement 1): // references are DERIVED from the manifest DAG, so an import a manifest gains must add the From 8d5df4fb8d368c9aa5bf1992f9fb2db8ac1c67a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 7 Oct 2026 19:46:03 +0200 Subject: [PATCH 05/11] refactor(typescript): wire the testmu provider into the reference graph 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. --- examples/sdk/tsconfig.json | 1 + packages/provider-testmu/tsconfig.json | 8 +++++- scripts/layering/project-references.test.ts | 30 +++++++++++++++++++-- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/examples/sdk/tsconfig.json b/examples/sdk/tsconfig.json index 7ea1624759..068bb60320 100644 --- a/examples/sdk/tsconfig.json +++ b/examples/sdk/tsconfig.json @@ -50,6 +50,7 @@ { "path": "../../packages/platform-vega" }, { "path": "../../packages/platform-web" }, { "path": "../../packages/provider-limrun" }, + { "path": "../../packages/provider-testmu" }, { "path": "../../packages/provider-webdriver" }, { "path": "../../packages/provision-kit" }, { "path": "../../packages/proxy" }, diff --git a/packages/provider-testmu/tsconfig.json b/packages/provider-testmu/tsconfig.json index aa0881ab1c..9a06efd405 100644 --- a/packages/provider-testmu/tsconfig.json +++ b/packages/provider-testmu/tsconfig.json @@ -6,11 +6,17 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", + "tsBuildInfoFile": "./dist-types/.tsbuildinfo", "rootDir": "./src", "paths": { "agent-device/plugins": ["./node_modules/agent-device/src/sdk/plugins.ts"], "agent-device/plugins/webdriver": ["./node_modules/agent-device/src/sdk/plugin-webdriver.ts"] } }, - "include": ["src"] + "include": ["src"], + "references": [ + { "path": "../contracts" }, + { "path": "../kernel" }, + { "path": "../provider-webdriver" } + ] } diff --git a/scripts/layering/project-references.test.ts b/scripts/layering/project-references.test.ts index e04a802bc7..576f265bf6 100644 --- a/scripts/layering/project-references.test.ts +++ b/scripts/layering/project-references.test.ts @@ -15,11 +15,24 @@ import { readWorkspacePackages } from './package-boundaries.ts'; const repoRoot = path.resolve(import.meta.dirname, '../..'); /** tsconfig JSONC minus line comments; both comment-bearing files use `//` only. */ -function readTsconfig(relativePath: string): { references?: { path: string }[] } { +function readTsconfig(relativePath: string): { + references?: { path: string }[]; + extends?: string; + compilerOptions?: Record; +} { const source = fs.readFileSync(path.join(repoRoot, relativePath), 'utf8'); return JSON.parse(source.replaceAll(/^\s*\/\/.*$/gm, '')); } +/** + * The published root package name. It names no composite project: the root tsconfig is a + * noEmit entry point TS6310 forbids referencing, so a workspace dependency on it is + * type-resolved through tsconfig `paths`, never through a reference edge. + */ +const rootPackageName = ( + JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as { name: string } +).name; + /** Repo-relative package dir for one `references[].path` entry, resolved from its project. */ function referencedPackageDirs(tsconfigFile: string, projectDir: string): string[] { const config = readTsconfig(tsconfigFile); @@ -86,6 +99,10 @@ test('every package tsconfig references exactly its manifest workspace dependenc // references are DERIVED from the manifest DAG, so an import a manifest gains must add the // matching reference (edge freshness, which tsc -b never asks for) and a removed one must // drop it (an extra reference hides a stale edge that tsc -b tolerates silently). + // The one expressible-edge exception: a workspace dependency on the ROOT package + // (`agent-device`) has no composite project to point at (TS6310). Those imports are + // type-resolved through tsconfig `paths`, so the dependency is asserted present in + // paths instead of references. const packages = readWorkspacePackages(repoRoot); const nameByDir = new Map(packages.map((pkg) => [pkg.dir, pkg.name])); for (const pkg of packages) { @@ -98,8 +115,17 @@ test('every package tsconfig references exactly its manifest workspace dependenc } assert.deepEqual( referencedDirs.map((dir) => nameByDir.get(dir)), - [...pkg.workspaceDependencies].sort(), + [...pkg.workspaceDependencies].filter((name) => name !== rootPackageName).sort(), `${pkg.dir}/tsconfig.json references must equal its manifest workspace:* dependencies`, ); + if (pkg.workspaceDependencies.has(rootPackageName)) { + const config = readTsconfig(`${pkg.dir}/tsconfig.json`); + const paths = (config.compilerOptions?.paths ?? {}) as Record; + assert.ok( + Object.keys(paths).some((key) => key.startsWith(`${rootPackageName}/`)), + `${pkg.dir} depends on the root package without a paths mapping; tsc cannot ` + + 'resolve it (the root project cannot be referenced)', + ); + } } }); From 0f1967e9c29259903494b24de1edee24ef773a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 7 Oct 2026 20:00:42 +0200 Subject: [PATCH 06/11] refactor(layering): split the facade-surface gates out of the R11 test 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. --- scripts/layering/facade-surface.test.ts | 103 ++++++++++++++++++++ scripts/layering/package-boundaries.test.ts | 90 +---------------- 2 files changed, 104 insertions(+), 89 deletions(-) create mode 100644 scripts/layering/facade-surface.test.ts diff --git a/scripts/layering/facade-surface.test.ts b/scripts/layering/facade-surface.test.ts new file mode 100644 index 0000000000..f6c5d2ba39 --- /dev/null +++ b/scripts/layering/facade-surface.test.ts @@ -0,0 +1,103 @@ +// The workspace façades' public surface: every entry file names its exports +// explicitly, its lists stay exhaustive over their sources, and the moves here +// stay owned. These real-tree structural gates mirror the façade question, not +// the R11 rule internals (package-boundaries.test.ts) or the export-form +// parser (facade-exports.test.ts); they were split out when the R11 family +// crossed the test-size tripwire, move-only. + +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { test } from 'node:test'; +import { listSourceFiles } from './check.ts'; +import { readDirectNamedExports, readNamedExports } from './facade-exports.ts'; +import { facadeEntryFiles } from './package-boundaries.ts'; + +const repoRoot = path.resolve(import.meta.dirname, '../..'); + +test('every workspace package façade names its exports explicitly (no bare `export *`)', () => { + // #1574 built a hand-maintained pin table (`facade-symbols.ts`, 816 symbols across every + // workspace-package façade) plus a ~200-line star-chain resolver (`readFacadeExports`) whose + // entire job was enumerating what `export *` hides. Once a façade names its exports explicitly, + // the façade file itself IS the pin — a widening shows up in the diff of the file that grew, + // not in a table two files away that only a gate failure would surface. This structural gate is + // what keeps that property true: every façade a package manifest declares (`exportTargets`), + // plus every production file under a `src/facades/` directory, must parse through + // `readNamedExports` without hitting the bare-`export *`/`export default` rejection it already + // implements — reusing that check rather than writing a second, regex-based one that would have + // to independently rediscover every export form to be trustworthy. + // + // The façade set comes from `facadeEntryFiles`, the single owner of "what is an entry surface". + // The ADR-0019 eager-closure budget table consumes the same function, so a file this gate holds + // to an explicit export list is necessarily a file that gate holds to a loading-shape budget. + const facadeFiles = facadeEntryFiles(repoRoot); + assert.ok(facadeFiles.length > 0, 'expected at least one workspace package façade to check'); + for (const file of facadeFiles) { + const source = fs.readFileSync(path.join(repoRoot, file), 'utf8'); + try { + readNamedExports(source); + } catch (error) { + assert.fail( + `${file} must name its exports explicitly instead of a bare \`export *\` (or an ` + + '`export default`) — a façade widened by a star re-export hides the new symbol from ' + + 'its own diff, exactly what the retired symbol-pin table (#1574) used to catch by ' + + `hand. Underlying error: ${(error as Error).message}`, + ); + } + } +}); + +test('every façade re-exports its sources exhaustively (no silent narrowing)', () => { + // The star-rejection above catches a façade WIDENING invisibly. This catches the + // opposite, which is the failure an explicit list makes newly possible: a symbol + // added to a source module simply never reaches the façade, and nothing notices. + // `export *` could not narrow by construction; an explicit list can, so the + // property `export *` gave for free is asserted here instead. + // + // Found by review on #1614: this conversion was generated against the surface at + // fork time, and #1567 landed 13 new exports meanwhile (`DragOptions`, the drag + // gesture vocabulary, `MultiTargetAnnotationV1`). The rebase silently dropped all + // 13 and only a human diff caught it. Exhaustiveness is what makes that mechanical. + // + // Scoped to `packages/*/src/facades/` — the barrels this PR converted, which were + // exhaustive by construction because `export *` cannot narrow. A hand-curated + // package `index.ts` is a different thing: `@agent-device/ad-replay` deliberately + // publishes two values out of a much larger `internal/`, and forcing it exhaustive + // would widen a surface its owner narrowed on purpose (#1555). + const facadeFiles = listSourceFiles().filter((file) => file.includes('/src/facades/')); + assert.ok(facadeFiles.length > 0, 'expected at least one converted façade to check'); + for (const file of [...facadeFiles].sort()) { + const absolute = path.join(repoRoot, file); + const exported = new Set(readNamedExports(fs.readFileSync(absolute, 'utf8'))); + for (const specifier of reExportSources(fs.readFileSync(absolute, 'utf8'))) { + const sourcePath = path.resolve(path.dirname(absolute), specifier); + if (!fs.existsSync(sourcePath)) continue; + // A source may itself carry a bare `export *` (contracts' `gesture-plan.ts` + // stars `gesture-plan-types.ts`). Read its DIRECT exports rather than skipping + // the file: skipping would also drop `buildDragGesturePlan` and friends from + // this check, so removing one from a façade would narrow the surface silently + // (#1614 review P2). The starred names are covered because the façade + // re-exports the starred module directly too, and that path is checked here on + // its own turn. + const sourceNames = readDirectNamedExports(fs.readFileSync(sourcePath, 'utf8')); + const dropped = sourceNames.filter((name) => name !== 'default' && !exported.has(name)); + assert.deepEqual( + dropped, + [], + `${file} re-exports from ${specifier} but omits ${dropped.join(', ')} — an explicit ` + + 'façade list must stay exhaustive over its sources, or a symbol added upstream ' + + 'silently never becomes public. Add the names, or move them out of that module.', + ); + } + } +}); + +/** The relative specifiers a façade re-exports from, in source order. */ +function reExportSources(source: string): string[] { + const found = new Set(); + for (const match of source.matchAll(/\bfrom\s+'(\.[^']*)'/g)) { + const specifier = match[1]; + if (specifier) found.add(specifier); + } + return [...found]; +} diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index 5b973cef92..fc1086a3f0 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -9,10 +9,9 @@ import os from 'node:os'; import path from 'node:path'; import { test } from 'node:test'; import { listSourceFiles } from './check.ts'; -import { readDirectNamedExports, readNamedExports, readReExportSources } from './facade-exports.ts'; +import { readNamedExports, readReExportSources } from './facade-exports.ts'; import { checkPackageBoundaries, - facadeEntryFiles, checkPackageInternalSites, checkRootSites, insideCompiledSources, @@ -204,93 +203,6 @@ test('published ESM plugins declare bundled workspace build dependencies without assert.match(violations[0]?.message ?? '', /without declaring/); }); -test('every workspace package façade names its exports explicitly (no bare `export *`)', () => { - // #1574 built a hand-maintained pin table (`facade-symbols.ts`, 816 symbols across every - // workspace-package façade) plus a ~200-line star-chain resolver (`readFacadeExports`) whose - // entire job was enumerating what `export *` hides. Once a façade names its exports explicitly, - // the façade file itself IS the pin — a widening shows up in the diff of the file that grew, - // not in a table two files away that only a gate failure would surface. This structural gate is - // what keeps that property true: every façade a package manifest declares (`exportTargets`), - // plus every production file under a `src/facades/` directory, must parse through - // `readNamedExports` without hitting the bare-`export *`/`export default` rejection it already - // implements — reusing that check rather than writing a second, regex-based one that would have - // to independently rediscover every export form to be trustworthy. - // - // The façade set comes from `facadeEntryFiles`, the single owner of "what is an entry surface". - // The ADR-0019 eager-closure budget table consumes the same function, so a file this gate holds - // to an explicit export list is necessarily a file that gate holds to a loading-shape budget. - const facadeFiles = facadeEntryFiles(repoRoot); - assert.ok(facadeFiles.length > 0, 'expected at least one workspace package façade to check'); - for (const file of facadeFiles) { - const source = fs.readFileSync(path.join(repoRoot, file), 'utf8'); - try { - readNamedExports(source); - } catch (error) { - assert.fail( - `${file} must name its exports explicitly instead of a bare \`export *\` (or an ` + - '`export default`) — a façade widened by a star re-export hides the new symbol from ' + - 'its own diff, exactly what the retired symbol-pin table (#1574) used to catch by ' + - `hand. Underlying error: ${(error as Error).message}`, - ); - } - } -}); - -test('every façade re-exports its sources exhaustively (no silent narrowing)', () => { - // The star-rejection above catches a façade WIDENING invisibly. This catches the - // opposite, which is the failure an explicit list makes newly possible: a symbol - // added to a source module simply never reaches the façade, and nothing notices. - // `export *` could not narrow by construction; an explicit list can, so the - // property `export *` gave for free is asserted here instead. - // - // Found by review on #1614: this conversion was generated against the surface at - // fork time, and #1567 landed 13 new exports meanwhile (`DragOptions`, the drag - // gesture vocabulary, `MultiTargetAnnotationV1`). The rebase silently dropped all - // 13 and only a human diff caught it. Exhaustiveness is what makes that mechanical. - // - // Scoped to `packages/*/src/facades/` — the barrels this PR converted, which were - // exhaustive by construction because `export *` cannot narrow. A hand-curated - // package `index.ts` is a different thing: `@agent-device/ad-replay` deliberately - // publishes two values out of a much larger `internal/`, and forcing it exhaustive - // would widen a surface its owner narrowed on purpose (#1555). - const facadeFiles = listSourceFiles().filter((file) => file.includes('/src/facades/')); - assert.ok(facadeFiles.length > 0, 'expected at least one converted façade to check'); - for (const file of [...facadeFiles].sort()) { - const absolute = path.join(repoRoot, file); - const exported = new Set(readNamedExports(fs.readFileSync(absolute, 'utf8'))); - for (const specifier of reExportSources(fs.readFileSync(absolute, 'utf8'))) { - const sourcePath = path.resolve(path.dirname(absolute), specifier); - if (!fs.existsSync(sourcePath)) continue; - // A source may itself carry a bare `export *` (contracts' `gesture-plan.ts` - // stars `gesture-plan-types.ts`). Read its DIRECT exports rather than skipping - // the file: skipping would also drop `buildDragGesturePlan` and friends from - // this check, so removing one from a façade would narrow the surface silently - // (#1614 review P2). The starred names are covered because the façade - // re-exports the starred module directly too, and that path is checked here on - // its own turn. - const sourceNames = readDirectNamedExports(fs.readFileSync(sourcePath, 'utf8')); - const dropped = sourceNames.filter((name) => name !== 'default' && !exported.has(name)); - assert.deepEqual( - dropped, - [], - `${file} re-exports from ${specifier} but omits ${dropped.join(', ')} — an explicit ` + - 'façade list must stay exhaustive over its sources, or a symbol added upstream ' + - 'silently never becomes public. Add the names, or move them out of that module.', - ); - } - } -}); - -/** The relative specifiers a façade re-exports from, in source order. */ -function reExportSources(source: string): string[] { - const found = new Set(); - for (const match of source.matchAll(/\bfrom\s+'(\.[^']*)'/g)) { - const specifier = match[1]; - if (specifier) found.add(specifier); - } - return [...found]; -} - // These `src/utils` paths are in-memory arbitrary parser fixtures, not live repository paths. test('double-quoted and re-export routes into packages are not invisible to R11', () => { // The scanner is the layering parser, so quote style and statement form From fe8c19b251aa2b15818814264ae097d3e1dedd48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 7 Oct 2026 20:12:20 +0200 Subject: [PATCH 07/11] fix(layering): match facade exhaustiveness per source module, not collapsed 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. --- scripts/layering/facade-exports.ts | 44 ++++++++++ scripts/layering/facade-surface.test.ts | 104 ++++++++++++++++++------ 2 files changed, 125 insertions(+), 23 deletions(-) diff --git a/scripts/layering/facade-exports.ts b/scripts/layering/facade-exports.ts index 007ac99c1d..12682f7459 100644 --- a/scripts/layering/facade-exports.ts +++ b/scripts/layering/facade-exports.ts @@ -120,3 +120,47 @@ export function readReExportSources(source: string): Map { } return sources; } + +/** + * Re-export edges grouped by the module they come FROM: each `export { … } from + * './m.ts'` entry contributes one `{ imported, exported }` pair to `./m.ts`'s + * list — `imported` is the name in the source module (the alias target), + * `exported` the name the façade publishes — and `export * as ns from './m.ts'` + * contributes the binding `namespace:`, which carries the module's whole + * surface. Bare `export *` throws exactly as in `readNamedExports`. + * + * This is the per-source shape the exhaustiveness gate needs. Keying by export + * NAME — `readReExportSources`, or a flat name set — cannot answer "does the + * façade still carry everything THIS module exports?" when two sources export + * the same name: the survivor's entry hides the dropped one, so narrowing a + * single source passes (#3289 cubic review P2). Edges keep their module, so a + * pair is satisfied only by the module that declares the name. + */ +export function readFacadeReExportEdgesByModule( + source: string, +): Map { + const parsed = parseSync('facade-reexport-edge-scan.ts', source); + const edges = new Map(); + for (const staticExport of parsed.module.staticExports) { + for (const entry of staticExport.entries) { + if (entry.exportName.kind === 'None') { + throw new Error( + "readFacadeReExportEdgesByModule cannot enumerate 'export * from …' — it re-exports " + + 'an unknown set of names. Name the re-exported symbols explicitly instead.', + ); + } + if (entry.exportName.kind !== 'Name' || !entry.exportName.name) continue; + if (!entry.moduleRequest) continue; + const moduleRequest = entry.moduleRequest.value; + const exported = entry.exportName.name; + const pair = + entry.importName.kind === 'All' + ? { imported: `namespace:${exported}`, exported } + : { imported: entry.importName.name ?? exported, exported }; + const list = edges.get(moduleRequest); + if (list) list.push(pair); + else edges.set(moduleRequest, [pair]); + } + } + return edges; +} diff --git a/scripts/layering/facade-surface.test.ts b/scripts/layering/facade-surface.test.ts index f6c5d2ba39..8494f50fe5 100644 --- a/scripts/layering/facade-surface.test.ts +++ b/scripts/layering/facade-surface.test.ts @@ -10,7 +10,11 @@ import fs from 'node:fs'; import path from 'node:path'; import { test } from 'node:test'; import { listSourceFiles } from './check.ts'; -import { readDirectNamedExports, readNamedExports } from './facade-exports.ts'; +import { + readDirectNamedExports, + readFacadeReExportEdgesByModule, + readNamedExports, +} from './facade-exports.ts'; import { facadeEntryFiles } from './package-boundaries.ts'; const repoRoot = path.resolve(import.meta.dirname, '../..'); @@ -59,6 +63,13 @@ test('every façade re-exports its sources exhaustively (no silent narrowing)', // gesture vocabulary, `MultiTargetAnnotationV1`). The rebase silently dropped all // 13 and only a human diff caught it. Exhaustiveness is what makes that mechanical. // + // The comparison is PER SOURCE MODULE, not over one collapsed name set: when two + // re-exported modules export the same name, a flat set lets the survivor's edge + // hide the dropped one, so narrowing a single source passes (#3289 cubic review + // P2). Each edge is matched against the module it comes from, by the name the + // source declares it under — so `export { a as b } from './m.ts'` satisfies `a`, + // not `b` — and a `namespace:` binding carries that module's whole surface. + // // Scoped to `packages/*/src/facades/` — the barrels this PR converted, which were // exhaustive by construction because `export *` cannot narrow. A hand-curated // package `index.ts` is a different thing: `@agent-device/ad-replay` deliberately @@ -68,23 +79,20 @@ test('every façade re-exports its sources exhaustively (no silent narrowing)', assert.ok(facadeFiles.length > 0, 'expected at least one converted façade to check'); for (const file of [...facadeFiles].sort()) { const absolute = path.join(repoRoot, file); - const exported = new Set(readNamedExports(fs.readFileSync(absolute, 'utf8'))); - for (const specifier of reExportSources(fs.readFileSync(absolute, 'utf8'))) { - const sourcePath = path.resolve(path.dirname(absolute), specifier); - if (!fs.existsSync(sourcePath)) continue; - // A source may itself carry a bare `export *` (contracts' `gesture-plan.ts` - // stars `gesture-plan-types.ts`). Read its DIRECT exports rather than skipping - // the file: skipping would also drop `buildDragGesturePlan` and friends from - // this check, so removing one from a façade would narrow the surface silently - // (#1614 review P2). The starred names are covered because the façade - // re-exports the starred module directly too, and that path is checked here on - // its own turn. - const sourceNames = readDirectNamedExports(fs.readFileSync(sourcePath, 'utf8')); - const dropped = sourceNames.filter((name) => name !== 'default' && !exported.has(name)); + const facadeSource = fs.readFileSync(absolute, 'utf8'); + const dropped = droppedFacadeExports( + facadeSource, + (sourcePath) => + fs.existsSync(sourcePath) + ? readDirectNamedExports(fs.readFileSync(sourcePath, 'utf8')) + : undefined, + path.dirname(absolute), + ); + for (const { specifier, names } of dropped) { assert.deepEqual( - dropped, + names, [], - `${file} re-exports from ${specifier} but omits ${dropped.join(', ')} — an explicit ` + + `${file} re-exports from ${specifier} but omits ${names.join(', ')} — an explicit ` + 'façade list must stay exhaustive over its sources, or a symbol added upstream ' + 'silently never becomes public. Add the names, or move them out of that module.', ); @@ -92,12 +100,62 @@ test('every façade re-exports its sources exhaustively (no silent narrowing)', } }); -/** The relative specifiers a façade re-exports from, in source order. */ -function reExportSources(source: string): string[] { - const found = new Set(); - for (const match of source.matchAll(/\bfrom\s+'(\.[^']*)'/g)) { - const specifier = match[1]; - if (specifier) found.add(specifier); +/** + * Per-source exhaustiveness: for each module the façade re-exports from, the + * names that module declares but no edge from THAT module carries. Factored + * from the real-tree loop so the narrowed-source case can be planted against a + * name duplicated in a sibling module (#3289 cubic review P2). `sourceNamesOf` + * reads one resolved source file (undefined = the specifier resolves to nothing + * on disk); a `namespace:` binding exempts its module (whole-surface). + */ +function droppedFacadeExports( + facadeSource: string, + sourceNamesOf: (resolvedSourcePath: string) => string[] | undefined, + facadeDirectory: string, +): { specifier: string; names: string[] }[] { + const findings: { specifier: string; names: string[] }[] = []; + const edgesByModule = readFacadeReExportEdgesByModule(facadeSource); + for (const [specifier, edges] of edgesByModule) { + if (!specifier.startsWith('.')) continue; + if (edges.some((edge) => edge.imported.startsWith('namespace:'))) continue; + const sourceNames = sourceNamesOf(path.resolve(facadeDirectory, specifier)); + if (sourceNames === undefined) continue; + const imported = new Set(edges.map((edge) => edge.imported)); + const names = sourceNames.filter((name) => name !== 'default' && !imported.has(name)); + if (names.length > 0) findings.push({ specifier, names }); } - return [...found]; + return findings; } + +test('a narrowed source fails even when a sibling module exports the same name', () => { + // The planted #3289 cubic review P2. `./widgets.ts` lost `Dropped` from the + // façade while `./other.ts` still publishes the same name. The old check + // collapsed every module's re-exports into one export-NAME set, so + // `Dropped`-from-`other` satisfied the missing `Dropped`-from-`widgets` and + // the narrowing passed. Per-module edges cannot be fooled: the only edges + // whose module is `./widgets.ts` carry Thing and alpha, so its `Dropped` is + // reported — and it is reported for THAT module only. + const facadeSource = [ + "export { Thing, alpha } from './widgets.ts';", + "export { Dropped } from './other.ts';", + ].join('\n'); + const sourceNames = new Map([ + ['/virtual/facades/widgets.ts', ['Thing', 'alpha', 'Dropped']], + ['/virtual/facades/other.ts', ['Dropped']], + ]); + assert.deepEqual( + droppedFacadeExports(facadeSource, (resolved) => sourceNames.get(resolved), '/virtual/facades'), + [{ specifier: './widgets.ts', names: ['Dropped'] }], + ); + // The matching direction for aliases is the SOURCE name: `alpha as aliasAlpha` + // covers widgets' `alpha`. Were the façade-side name compared instead, + // `alpha` would surface as a second drop. + const aliasSource = [ + "export { Thing, alpha as aliasAlpha } from './widgets.ts';", + "export { Dropped } from './other.ts';", + ].join('\n'); + assert.deepEqual( + droppedFacadeExports(aliasSource, (resolved) => sourceNames.get(resolved), '/virtual/facades'), + [{ specifier: './widgets.ts', names: ['Dropped'] }], + ); +}); From f75da8ba85eeaa0996ba0d21012d904ea8dff654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 7 Oct 2026 20:12:37 +0200 Subject: [PATCH 08/11] test(layering): pin the edge-map shapes at the export-enumeration module 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. --- scripts/layering/facade-exports.test.ts | 35 ++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/scripts/layering/facade-exports.test.ts b/scripts/layering/facade-exports.test.ts index 7cf04e7efe..a4d60ec790 100644 --- a/scripts/layering/facade-exports.test.ts +++ b/scripts/layering/facade-exports.test.ts @@ -4,7 +4,7 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; -import { readNamedExports } from './facade-exports.ts'; +import { readFacadeReExportEdgesByModule, readNamedExports } from './facade-exports.ts'; test('readNamedExports collects re-export and direct-declaration forms, resolving aliases', () => { const source = [ @@ -79,3 +79,36 @@ test('readNamedExports collects every declarator of a multi-declarator export', // only exercises a single declarator, so the second name went unpinned. assert.deepEqual(readNamedExports('export const a = 1, b = 2;'), ['a', 'b']); }); + +test('readFacadeReExportEdgesByModule groups edges by module with source-side names', () => { + const edges = readFacadeReExportEdgesByModule( + [ + "export { a } from './x.ts';", + "export { b as c } from './x.ts';", + "export { D } from './y.ts';", + "export * as ns from './z.ts';", + 'export function local() {}', + ].join('\n'), + ); + assert.deepEqual( + [...edges.entries()], + [ + [ + './x.ts', + [ + { imported: 'a', exported: 'a' }, + { imported: 'b', exported: 'c' }, + ], + ], + ['./y.ts', [{ imported: 'D', exported: 'D' }]], + ['./z.ts', [{ imported: 'namespace:ns', exported: 'ns' }]], + ], + ); +}); + +test('readFacadeReExportEdgesByModule refuses bare export * like readNamedExports', () => { + assert.throws( + () => readFacadeReExportEdgesByModule("export * from './x.ts';"), + /cannot enumerate/, + ); +}); From 63fd2976ac68b546044dbcf015fed8cae0518ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 7 Oct 2026 22:18:01 +0200 Subject: [PATCH 09/11] refactor(layering): R11 keeps every relative package escape, compiled 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. --- scripts/layering/check.ts | 5 ++- scripts/layering/package-boundaries.test.ts | 44 ++++++++++++++++----- scripts/layering/package-boundaries.ts | 34 ++++++++++------ scripts/layering/project-references.test.ts | 39 ++++++++++++++++++ 4 files changed, 99 insertions(+), 23 deletions(-) diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index 35c7406798..ba1c452ac9 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -32,8 +32,9 @@ // - Across the DAEMON MODULARITY MIGRATION: R7 ownership pressure and external // daemon request/session-state importers only shrink, R9 zone membership cannot grow or absorb // engine files, and planned logical modules start with zero forbidden/internal imports (R10). -// - Over the WORKSPACE PACKAGES: every workspace specifier declared, no relative tunnelling -// into a package's src, and the full specifier sweep for the scripts/ files the type graph +// - Over the WORKSPACE PACKAGES: every workspace specifier declared, no relative path +// leaving its package dir (compiled or not — a planted sibling tunnel tsc -b accepted, +// #3289 review), and the full specifier sweep for the scripts/ files the type graph // omits — resolution itself (unknown package, un-exported subpath) is TS2307 under tsc -b // for compiled sources (#3279) (R11). // - Over PLATFORM PACKAGE COMPOSITION: six private metadata façades meet at the exact root diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index fc1086a3f0..876993ab6f 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -230,22 +230,48 @@ test('double-quoted and re-export routes into packages are not invisible to R11' 'packages/kernel/src/errors.ts', 'export * from "../../../src/utils/exec.ts";', ); - // The escape is compiler-owned for compiled sources; scripts/ and package harness files that - // sit outside the type graph keep the R11 branch (#3279). - assert.equal(checkPackageInternalSites(kernel, packageEscape, ALL, true).length, 0); + // The escape is R11's own for EVERY package source, compiled or not (#3289 maintainer + // review): the compiled-only skip was retired with a planted proof, below. + assert.equal(checkPackageInternalSites(kernel, packageEscape, ALL, true).length, 1); assert.equal(checkPackageInternalSites(kernel, packageEscape, ALL, false).length, 1); }); // This `src/utils` path is another in-memory arbitrary parser fixture, not a live repository path. -test('a package file importing root src is compiler-owned once tsc -b sees it', () => { - // #3279: composite `rootDir` rejects a package->root relative escape with TS6059 + TS6307 - // (planted proof in the PR). R11 keeps the branch only for package files the graph omits. - const sites = specifierSites( +test('a package relative escape is R11-owned, compiled or not', () => { + // #3289 maintainer review restored this branch after a planted `tsc -b` run showed the + // compiler covers NEITHER package escape for a REFERENCED sibling: + // 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 TS6059/TS6307 never fire; the earlier PR-#3279 proof + // (TS6059+TS6307) only held for the ROOT escape, where no project redirects. What both + // tunnels break instead is runtime identity: Node instantiates the tunnelled module once + // by path and once by specifier, so two AppError classes coexist and instanceof fails — + // invisible to any compiler, so R11 holds it for compiled files too. + const rootEscape = specifierSites( 'packages/kernel/src/errors.ts', "import { helper } from '../../../src/utils/exec.ts';", ); - assert.deepEqual(checkPackageInternalSites(kernel, sites, ALL, true), []); - assert.deepEqual(rules(checkPackageInternalSites(kernel, sites, ALL, false)), [ + assert.deepEqual(rules(checkPackageInternalSites(kernel, rootEscape, ALL, true)), [ + 'R11 package-boundaries', + ]); + assert.match( + checkPackageInternalSites(kernel, rootEscape, ALL, true)[0]!.message, + /escapes packages\/kernel/, + ); + + // The exact reviewer case: compiled=true with a sibling-relative specifier. + const siblingTunnel = specifierSites( + 'packages/kernel/src/errors.ts', + "import { thing } from '../../xml/src/index.ts';", + ); + assert.deepEqual(rules(checkPackageInternalSites(kernel, siblingTunnel, ALL, true)), [ + 'R11 package-boundaries', + ]); + + // Uncompiled package files keep the same branch (they never reach the compiler at all). + assert.deepEqual(rules(checkPackageInternalSites(kernel, rootEscape, ALL, false)), [ 'R11 package-boundaries', ]); }); diff --git a/scripts/layering/package-boundaries.ts b/scripts/layering/package-boundaries.ts index afb951a822..50cb974db2 100644 --- a/scripts/layering/package-boundaries.ts +++ b/scripts/layering/package-boundaries.ts @@ -152,14 +152,18 @@ function specifierPackageName(specifier: string): string | undefined { * `--experimental-strip-types`, not a tsconfig program), the `packages/maestro/test/` * conformance harness, and package build configs — reaches packages through import sites the * compiler never parses, so R11 stays the only owner of every boundary claim about those - * routes (#3279). + * routes (#3279). Only the RESOLUTION branches (unknown package, un-exported subpath) are + * split this way; the relative-escape branch fires regardless — a planted sibling tunnel + * `tsc -b` accepted, see `checkPackageInternalSites`. * * One documented divergence: the root program also includes * `scripts/help-conformance-command-validator.ts`, which this predicate classifies as * uncompiled. That is fail-closed — R11 keeps its full branch set (unknown package, - * exports-map) for the file on top of what tsc enforces — never fail-open. The divergence is - * pinned in `project-references.test.ts`, so a second scripts entry joining the root program - * must be recorded there rather than silently widening the exception. + * exports-map) for the file on top of what tsc enforces — never fail-open. The divergence + * and the exact root include list are pinned in `project-references.test.ts`, and the + * per-package `include: ["src"]` / no-`exclude` shape this predicate's regexes assume is + * pinned there too, so widening any project's file set must be recorded there rather than + * silently diverging from this predicate. */ export function insideCompiledSources(file: string): boolean { return /^packages\/[^/]+\/src\//.test(file) || /^src\//.test(file) || /^test\//.test(file); @@ -169,11 +173,17 @@ export function insideCompiledSources(file: string): boolean { * Rules for files INSIDE a package. An undeclared sibling import is R11's alone: pnpm links * every workspace package under the root `node_modules`, so the import resolves, compiles, and * runs in this repo even when the manifest never declares it — and a published bundle - * externalizes against that declaration. The relative escape past the package dir and every - * resolution failure (unknown package, subpath the exports map does not name) are owned by the - * compiler for compiled files: composite `rootDir` fails the escape with TS6059 + TS6307 and - * NodeNext resolution fails the specifier with TS2307. Both are proven in #3279; uncompiled - * files never reach either check, so for them R11 holds every branch. + * externalizes against that declaration. The relative escape past the package dir is R11's + * too, compiled or not: a planted `pnpm exec tsc -b packages/host-kit --force` with + * `import { AppError } from '../../kernel/src/errors.ts'` in `host-kit/src/` **exits 0** — + * the redirect to kernel's `dist-types` keeps every emitted file under host-kit's `rootDir`, + * so neither TS6059 nor TS6307 fires (the root-escape tunnel DOES fail: TS2307, since `../.. + * /../kernel/...` lands outside every include and no package owns `kernel` at that depth). + * What the sibling tunnel breaks is runtime identity — Node loads kernel once by path and + * once by specifier, so two AppError classes coexist and `instanceof` silently fails. That + * is invisible to any compiler. The remaining resolution failures (unknown package, + * subpath the exports map does not name) are owned by NodeNext for compiled files with + * TS2307; uncompiled files never reach either check, so for them R11 holds every branch. */ export function checkPackageInternalSites( pkg: WorkspacePackage, @@ -184,7 +194,6 @@ export function checkPackageInternalSites( const violations: PackageBoundaryViolation[] = []; for (const site of sites) { if (site.specifier.startsWith('.')) { - if (compiled) continue; const resolved = path.posix.normalize( path.posix.join(path.posix.dirname(site.file), site.specifier), ); @@ -194,8 +203,9 @@ export function checkPackageInternalSites( file: site.file, line: site.line, message: - `'${site.specifier}' escapes ${pkg.dir}/ — a workspace package may not reach root ` + - `code. Depend on another package's specifier, or move the shared code below this package.`, + `'${site.specifier}' escapes ${pkg.dir}/ — a workspace package may not reach ` + + `outside its own directory. Import a sibling through its specifier (Node would ` + + `load the tunnelled module twice), or move the shared code below ${pkg.dir}.`, }); } continue; diff --git a/scripts/layering/project-references.test.ts b/scripts/layering/project-references.test.ts index 576f265bf6..fc3c806ad5 100644 --- a/scripts/layering/project-references.test.ts +++ b/scripts/layering/project-references.test.ts @@ -18,6 +18,8 @@ const repoRoot = path.resolve(import.meta.dirname, '../..'); function readTsconfig(relativePath: string): { references?: { path: string }[]; extends?: string; + include?: string[]; + exclude?: string[]; compilerOptions?: Record; } { const source = fs.readFileSync(path.join(repoRoot, relativePath), 'utf8'); @@ -55,6 +57,43 @@ test('root and examples/sdk reference exactly the workspace package set', () => assert.deepEqual(referencedPackageDirs('examples/sdk/tsconfig.json', 'examples/sdk'), expected); }); +test('the compiled-set predicate is licensed by the tsconfig include/exclude shape it mirrors', () => { + // `insideCompiledSources` (package-boundaries.ts) hard-codes the three include shapes the + // tsc -b graph compiles: `packages/*/src`, root `src`, root `test`. That regex is honest + // only while no project widens its file set behind it, so the shape is pinned here (the + // file that already reads the tsconfigs), and the R11 side keeps its pin in + // package-boundaries.test.ts (the #3279 walk-region test). A package tsconfig that gains + // an `exclude`, or an include beyond `src` (plus replay-port's shared `global.d.ts` + // sibling), or a root file added to the root include beyond src/test, must update the + // predicate or fail here — the same "views cannot drift" rule as the reference lists. + assert.deepEqual(readTsconfig('tsconfig.json').include, [ + 'src', + 'test', + 'scripts/help-conformance-command-validator.ts', + 'packages/command-registry/src/global.d.ts', + ]); + for (const pkg of readWorkspacePackages(repoRoot)) { + const config = readTsconfig(`${pkg.dir}/tsconfig.json`); + assert.equal(config.exclude, undefined, `${pkg.dir}/tsconfig.json must not declare exclude`); + assert.deepEqual( + (config.include ?? ['**/*']).filter((entry) => !entry.endsWith('global.d.ts')), + ['src'], + `${pkg.dir}/tsconfig.json must keep including exactly src/`, + ); + } +}); + +test('the one scripts file in the root program is the divergence insideCompiledSources records', () => { + // The predicate classifies scripts/help-conformance-command-validator.ts as UNCOMPILED + // (fail-closed: R11 keeps its full branch set there) even though the root include names + // it. This is the assertion the predicate's doc block points at: the divergence is + // recorded here, and a second scripts file joining the root program must be recorded + // next to this one rather than silently widening the exception. + const rootInclude = readTsconfig('tsconfig.json').include ?? []; + const scriptsEntries = rootInclude.filter((entry) => entry.startsWith('scripts/')); + assert.deepEqual(scriptsEntries, ['scripts/help-conformance-command-validator.ts']); +}); + test('the root manifest declares every workspace package as a workspace dependency', () => { const manifest = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as { dependencies?: Record; From f105bbf5d9fa34b1e036e5eb16a3f8c5fd29a9bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 7 Oct 2026 22:23:39 +0200 Subject: [PATCH 10/11] fix(layering): exact-shape include pin and honest dual-load wording Cubic re-review on 63fd2976a: - 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. --- scripts/layering/package-boundaries.ts | 19 ++++++++++++------- scripts/layering/project-references.test.ts | 19 +++++++++++++++---- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/scripts/layering/package-boundaries.ts b/scripts/layering/package-boundaries.ts index 50cb974db2..6a5721f9f1 100644 --- a/scripts/layering/package-boundaries.ts +++ b/scripts/layering/package-boundaries.ts @@ -179,11 +179,15 @@ export function insideCompiledSources(file: string): boolean { * the redirect to kernel's `dist-types` keeps every emitted file under host-kit's `rootDir`, * so neither TS6059 nor TS6307 fires (the root-escape tunnel DOES fail: TS2307, since `../.. * /../kernel/...` lands outside every include and no package owns `kernel` at that depth). - * What the sibling tunnel breaks is runtime identity — Node loads kernel once by path and - * once by specifier, so two AppError classes coexist and `instanceof` silently fails. That - * is invisible to any compiler. The remaining resolution failures (unknown package, - * subpath the exports map does not name) are owned by NodeNext for compiled files with - * TS2307; uncompiled files never reach either check, so for them R11 holds every branch. + * What the sibling tunnel risks is runtime identity: Node's ESM loader does not realpath + * specifiers, so when the same module is ALSO loaded through its package specifier — the + * normal route for every other consumer — the two keys instantiate it twice in one process + * (duplicate AppError, broken `instanceof`). Type-only tunnels carry no runtime cost, but + * the branch cannot condition on that from syntax alone and the layering claim fails either + * way. Any duplicate-instance risk invisible to the compiler is R11's to hold; the + * remaining resolution failures (unknown package, subpath the exports map does not name) + * are owned by NodeNext for compiled files with TS2307; uncompiled files never reach either + * check, so for them R11 holds every branch. */ export function checkPackageInternalSites( pkg: WorkspacePackage, @@ -204,8 +208,9 @@ export function checkPackageInternalSites( line: site.line, message: `'${site.specifier}' escapes ${pkg.dir}/ — a workspace package may not reach ` + - `outside its own directory. Import a sibling through its specifier (Node would ` + - `load the tunnelled module twice), or move the shared code below ${pkg.dir}.`, + `outside its own directory. Import a sibling through its specifier (mixing the ` + + `two routes instantiates the module twice), or move the shared code below ` + + `${pkg.dir}.`, }); } continue; diff --git a/scripts/layering/project-references.test.ts b/scripts/layering/project-references.test.ts index fc3c806ad5..7579689073 100644 --- a/scripts/layering/project-references.test.ts +++ b/scripts/layering/project-references.test.ts @@ -72,13 +72,24 @@ test('the compiled-set predicate is licensed by the tsconfig include/exclude sha 'scripts/help-conformance-command-validator.ts', 'packages/command-registry/src/global.d.ts', ]); + // `src` everywhere, plus the one shared ambient-declaration entry replay-port and + // session-journal include. Exact include ARRAYS, not a `global.d.ts` suffix class: a + // new include of any name must extend this allow-list deliberately, or the drift fails + // the test (#3289 cubic review P2 — a suffix filter would silently accept unrelated + // `*.global.d.ts` additions, which is the drift this pin exists to catch). + const knownIncludeShapes = [ + JSON.stringify(['src']), + JSON.stringify(['src', '../command-registry/src/global.d.ts']), + ]; for (const pkg of readWorkspacePackages(repoRoot)) { const config = readTsconfig(`${pkg.dir}/tsconfig.json`); assert.equal(config.exclude, undefined, `${pkg.dir}/tsconfig.json must not declare exclude`); - assert.deepEqual( - (config.include ?? ['**/*']).filter((entry) => !entry.endsWith('global.d.ts')), - ['src'], - `${pkg.dir}/tsconfig.json must keep including exactly src/`, + const include = JSON.stringify(config.include ?? ['**/*']); + assert.ok( + knownIncludeShapes.includes(include), + `${pkg.dir}/tsconfig.json includes ${include}; the compiled-set predicate assumes ` + + `the known shapes ${knownIncludeShapes.join(' / ')} — extend the allow-list here ` + + 'only after widening insideCompiledSources to match', ); } }); From 77f3a47dbf8e4d901d03d6ee7f0a44c6df392f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 8 Oct 2026 05:54:19 +0200 Subject: [PATCH 11/11] style(typescript): one-line reference entries and a uniform tsbuildinfo name Review nits on f105bbf5d (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. --- packages/ad-replay/tsconfig.json | 16 +--- packages/ad-script/tsconfig.json | 9 +- packages/capture-kit/tsconfig.json | 12 +-- packages/command-registry/tsconfig.json | 12 +-- packages/contracts/tsconfig.json | 6 +- packages/device-selection/tsconfig.json | 12 +-- packages/host-kit/tsconfig.json | 9 +- packages/maestro/tsconfig.json | 24 ++--- packages/managed-allocation/tsconfig.json | 16 +--- packages/platform-android/tsconfig.json | 24 ++--- packages/platform-apple/tsconfig.json | 24 ++--- packages/platform-harmonyos/tsconfig.json | 20 ++--- packages/platform-linux/tsconfig.json | 16 +--- packages/platform-vega/tsconfig.json | 16 +--- packages/platform-web/tsconfig.json | 20 ++--- packages/provider-limrun/tsconfig.json | 16 +--- packages/provider-testmu/tsconfig.json | 2 +- packages/provider-webdriver/tsconfig.json | 20 ++--- packages/provision-kit/tsconfig.json | 12 +-- packages/proxy/tsconfig.json | 12 +-- packages/replay-port/tsconfig.json | 48 +++------- packages/replay-test/tsconfig.json | 9 +- packages/selectors/tsconfig.json | 16 +--- tsconfig.json | 104 ++++++---------------- 24 files changed, 105 insertions(+), 370 deletions(-) diff --git a/packages/ad-replay/tsconfig.json b/packages/ad-replay/tsconfig.json index 72daf07a79..b1f2044901 100644 --- a/packages/ad-replay/tsconfig.json +++ b/packages/ad-replay/tsconfig.json @@ -11,17 +11,9 @@ }, "include": ["src"], "references": [ - { - "path": "../ad-script" - }, - { - "path": "../contracts" - }, - { - "path": "../kernel" - }, - { - "path": "../selectors" - } + { "path": "../ad-script" }, + { "path": "../contracts" }, + { "path": "../kernel" }, + { "path": "../selectors" } ] } diff --git a/packages/ad-script/tsconfig.json b/packages/ad-script/tsconfig.json index 3404580cdb..abb0532e6c 100644 --- a/packages/ad-script/tsconfig.json +++ b/packages/ad-script/tsconfig.json @@ -10,12 +10,5 @@ "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, "include": ["src"], - "references": [ - { - "path": "../contracts" - }, - { - "path": "../kernel" - } - ] + "references": [{ "path": "../contracts" }, { "path": "../kernel" }] } diff --git a/packages/capture-kit/tsconfig.json b/packages/capture-kit/tsconfig.json index fb9c5bf1bd..247a915b90 100644 --- a/packages/capture-kit/tsconfig.json +++ b/packages/capture-kit/tsconfig.json @@ -10,15 +10,5 @@ "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, "include": ["src"], - "references": [ - { - "path": "../contracts" - }, - { - "path": "../host-kit" - }, - { - "path": "../kernel" - } - ] + "references": [{ "path": "../contracts" }, { "path": "../host-kit" }, { "path": "../kernel" }] } diff --git a/packages/command-registry/tsconfig.json b/packages/command-registry/tsconfig.json index 80ad27d7d9..d3b1b55769 100644 --- a/packages/command-registry/tsconfig.json +++ b/packages/command-registry/tsconfig.json @@ -10,15 +10,5 @@ "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, "include": ["src"], - "references": [ - { - "path": "../contracts" - }, - { - "path": "../kernel" - }, - { - "path": "../selectors" - } - ] + "references": [{ "path": "../contracts" }, { "path": "../kernel" }, { "path": "../selectors" }] } diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json index cf91ccaef6..475a607655 100644 --- a/packages/contracts/tsconfig.json +++ b/packages/contracts/tsconfig.json @@ -10,9 +10,5 @@ "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, "include": ["src"], - "references": [ - { - "path": "../kernel" - } - ] + "references": [{ "path": "../kernel" }] } diff --git a/packages/device-selection/tsconfig.json b/packages/device-selection/tsconfig.json index fb9c5bf1bd..247a915b90 100644 --- a/packages/device-selection/tsconfig.json +++ b/packages/device-selection/tsconfig.json @@ -10,15 +10,5 @@ "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, "include": ["src"], - "references": [ - { - "path": "../contracts" - }, - { - "path": "../host-kit" - }, - { - "path": "../kernel" - } - ] + "references": [{ "path": "../contracts" }, { "path": "../host-kit" }, { "path": "../kernel" }] } diff --git a/packages/host-kit/tsconfig.json b/packages/host-kit/tsconfig.json index 3404580cdb..abb0532e6c 100644 --- a/packages/host-kit/tsconfig.json +++ b/packages/host-kit/tsconfig.json @@ -10,12 +10,5 @@ "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, "include": ["src"], - "references": [ - { - "path": "../contracts" - }, - { - "path": "../kernel" - } - ] + "references": [{ "path": "../contracts" }, { "path": "../kernel" }] } diff --git a/packages/maestro/tsconfig.json b/packages/maestro/tsconfig.json index 3675eedfb0..eb2efbbb9d 100644 --- a/packages/maestro/tsconfig.json +++ b/packages/maestro/tsconfig.json @@ -11,23 +11,11 @@ }, "include": ["src"], "references": [ - { - "path": "../capture-kit" - }, - { - "path": "../contracts" - }, - { - "path": "../host-kit" - }, - { - "path": "../kernel" - }, - { - "path": "../provision-kit" - }, - { - "path": "../selectors" - } + { "path": "../capture-kit" }, + { "path": "../contracts" }, + { "path": "../host-kit" }, + { "path": "../kernel" }, + { "path": "../provision-kit" }, + { "path": "../selectors" } ] } diff --git a/packages/managed-allocation/tsconfig.json b/packages/managed-allocation/tsconfig.json index 71a6557d7f..3431c632a3 100644 --- a/packages/managed-allocation/tsconfig.json +++ b/packages/managed-allocation/tsconfig.json @@ -11,17 +11,9 @@ }, "include": ["src"], "references": [ - { - "path": "../capture-kit" - }, - { - "path": "../contracts" - }, - { - "path": "../host-kit" - }, - { - "path": "../kernel" - } + { "path": "../capture-kit" }, + { "path": "../contracts" }, + { "path": "../host-kit" }, + { "path": "../kernel" } ] } diff --git a/packages/platform-android/tsconfig.json b/packages/platform-android/tsconfig.json index 3d96620019..90af7078cd 100644 --- a/packages/platform-android/tsconfig.json +++ b/packages/platform-android/tsconfig.json @@ -11,23 +11,11 @@ }, "include": ["src"], "references": [ - { - "path": "../capture-kit" - }, - { - "path": "../contracts" - }, - { - "path": "../host-kit" - }, - { - "path": "../kernel" - }, - { - "path": "../provision-kit" - }, - { - "path": "../xml" - } + { "path": "../capture-kit" }, + { "path": "../contracts" }, + { "path": "../host-kit" }, + { "path": "../kernel" }, + { "path": "../provision-kit" }, + { "path": "../xml" } ] } diff --git a/packages/platform-apple/tsconfig.json b/packages/platform-apple/tsconfig.json index 3d96620019..90af7078cd 100644 --- a/packages/platform-apple/tsconfig.json +++ b/packages/platform-apple/tsconfig.json @@ -11,23 +11,11 @@ }, "include": ["src"], "references": [ - { - "path": "../capture-kit" - }, - { - "path": "../contracts" - }, - { - "path": "../host-kit" - }, - { - "path": "../kernel" - }, - { - "path": "../provision-kit" - }, - { - "path": "../xml" - } + { "path": "../capture-kit" }, + { "path": "../contracts" }, + { "path": "../host-kit" }, + { "path": "../kernel" }, + { "path": "../provision-kit" }, + { "path": "../xml" } ] } diff --git a/packages/platform-harmonyos/tsconfig.json b/packages/platform-harmonyos/tsconfig.json index 64db9f0500..ea5df312b1 100644 --- a/packages/platform-harmonyos/tsconfig.json +++ b/packages/platform-harmonyos/tsconfig.json @@ -11,20 +11,10 @@ }, "include": ["src"], "references": [ - { - "path": "../capture-kit" - }, - { - "path": "../contracts" - }, - { - "path": "../host-kit" - }, - { - "path": "../kernel" - }, - { - "path": "../provision-kit" - } + { "path": "../capture-kit" }, + { "path": "../contracts" }, + { "path": "../host-kit" }, + { "path": "../kernel" }, + { "path": "../provision-kit" } ] } diff --git a/packages/platform-linux/tsconfig.json b/packages/platform-linux/tsconfig.json index 71a6557d7f..3431c632a3 100644 --- a/packages/platform-linux/tsconfig.json +++ b/packages/platform-linux/tsconfig.json @@ -11,17 +11,9 @@ }, "include": ["src"], "references": [ - { - "path": "../capture-kit" - }, - { - "path": "../contracts" - }, - { - "path": "../host-kit" - }, - { - "path": "../kernel" - } + { "path": "../capture-kit" }, + { "path": "../contracts" }, + { "path": "../host-kit" }, + { "path": "../kernel" } ] } diff --git a/packages/platform-vega/tsconfig.json b/packages/platform-vega/tsconfig.json index 80968b9f86..11c54e6f2a 100644 --- a/packages/platform-vega/tsconfig.json +++ b/packages/platform-vega/tsconfig.json @@ -11,17 +11,9 @@ }, "include": ["src"], "references": [ - { - "path": "../contracts" - }, - { - "path": "../host-kit" - }, - { - "path": "../kernel" - }, - { - "path": "../provision-kit" - } + { "path": "../contracts" }, + { "path": "../host-kit" }, + { "path": "../kernel" }, + { "path": "../provision-kit" } ] } diff --git a/packages/platform-web/tsconfig.json b/packages/platform-web/tsconfig.json index 1ea0a81460..7647605163 100644 --- a/packages/platform-web/tsconfig.json +++ b/packages/platform-web/tsconfig.json @@ -11,20 +11,10 @@ }, "include": ["src"], "references": [ - { - "path": "../capture-kit" - }, - { - "path": "../contracts" - }, - { - "path": "../host-kit" - }, - { - "path": "../kernel" - }, - { - "path": "../selectors" - } + { "path": "../capture-kit" }, + { "path": "../contracts" }, + { "path": "../host-kit" }, + { "path": "../kernel" }, + { "path": "../selectors" } ] } diff --git a/packages/provider-limrun/tsconfig.json b/packages/provider-limrun/tsconfig.json index 5a1ea3f1f7..4592bbb24b 100644 --- a/packages/provider-limrun/tsconfig.json +++ b/packages/provider-limrun/tsconfig.json @@ -11,17 +11,9 @@ }, "include": ["src"], "references": [ - { - "path": "../capture-kit" - }, - { - "path": "../contracts" - }, - { - "path": "../kernel" - }, - { - "path": "../platform-android" - } + { "path": "../capture-kit" }, + { "path": "../contracts" }, + { "path": "../kernel" }, + { "path": "../platform-android" } ] } diff --git a/packages/provider-testmu/tsconfig.json b/packages/provider-testmu/tsconfig.json index 9a06efd405..9541eb178c 100644 --- a/packages/provider-testmu/tsconfig.json +++ b/packages/provider-testmu/tsconfig.json @@ -6,7 +6,7 @@ "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist-types", - "tsBuildInfoFile": "./dist-types/.tsbuildinfo", + "tsBuildInfoFile": "./dist-types/index.tsbuildinfo", "rootDir": "./src", "paths": { "agent-device/plugins": ["./node_modules/agent-device/src/sdk/plugins.ts"], diff --git a/packages/provider-webdriver/tsconfig.json b/packages/provider-webdriver/tsconfig.json index 375c4d890a..dddea95d51 100644 --- a/packages/provider-webdriver/tsconfig.json +++ b/packages/provider-webdriver/tsconfig.json @@ -11,20 +11,10 @@ }, "include": ["src"], "references": [ - { - "path": "../capture-kit" - }, - { - "path": "../contracts" - }, - { - "path": "../host-kit" - }, - { - "path": "../kernel" - }, - { - "path": "../xml" - } + { "path": "../capture-kit" }, + { "path": "../contracts" }, + { "path": "../host-kit" }, + { "path": "../kernel" }, + { "path": "../xml" } ] } diff --git a/packages/provision-kit/tsconfig.json b/packages/provision-kit/tsconfig.json index fb9c5bf1bd..247a915b90 100644 --- a/packages/provision-kit/tsconfig.json +++ b/packages/provision-kit/tsconfig.json @@ -10,15 +10,5 @@ "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, "include": ["src"], - "references": [ - { - "path": "../contracts" - }, - { - "path": "../host-kit" - }, - { - "path": "../kernel" - } - ] + "references": [{ "path": "../contracts" }, { "path": "../host-kit" }, { "path": "../kernel" }] } diff --git a/packages/proxy/tsconfig.json b/packages/proxy/tsconfig.json index fb9c5bf1bd..247a915b90 100644 --- a/packages/proxy/tsconfig.json +++ b/packages/proxy/tsconfig.json @@ -10,15 +10,5 @@ "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, "include": ["src"], - "references": [ - { - "path": "../contracts" - }, - { - "path": "../host-kit" - }, - { - "path": "../kernel" - } - ] + "references": [{ "path": "../contracts" }, { "path": "../host-kit" }, { "path": "../kernel" }] } diff --git a/packages/replay-port/tsconfig.json b/packages/replay-port/tsconfig.json index ec4c62c373..1d50b78f96 100644 --- a/packages/replay-port/tsconfig.json +++ b/packages/replay-port/tsconfig.json @@ -11,41 +11,17 @@ }, "include": ["src", "../command-registry/src/global.d.ts"], "references": [ - { - "path": "../ad-replay" - }, - { - "path": "../ad-script" - }, - { - "path": "../capture-kit" - }, - { - "path": "../command-registry" - }, - { - "path": "../contracts" - }, - { - "path": "../device-selection" - }, - { - "path": "../host-kit" - }, - { - "path": "../kernel" - }, - { - "path": "../maestro" - }, - { - "path": "../replay-test" - }, - { - "path": "../selectors" - }, - { - "path": "../session-journal" - } + { "path": "../ad-replay" }, + { "path": "../ad-script" }, + { "path": "../capture-kit" }, + { "path": "../command-registry" }, + { "path": "../contracts" }, + { "path": "../device-selection" }, + { "path": "../host-kit" }, + { "path": "../kernel" }, + { "path": "../maestro" }, + { "path": "../replay-test" }, + { "path": "../selectors" }, + { "path": "../session-journal" } ] } diff --git a/packages/replay-test/tsconfig.json b/packages/replay-test/tsconfig.json index 3404580cdb..abb0532e6c 100644 --- a/packages/replay-test/tsconfig.json +++ b/packages/replay-test/tsconfig.json @@ -10,12 +10,5 @@ "tsBuildInfoFile": "./dist-types/index.tsbuildinfo" }, "include": ["src"], - "references": [ - { - "path": "../contracts" - }, - { - "path": "../kernel" - } - ] + "references": [{ "path": "../contracts" }, { "path": "../kernel" }] } diff --git a/packages/selectors/tsconfig.json b/packages/selectors/tsconfig.json index 01473e83b7..666dbe33e4 100644 --- a/packages/selectors/tsconfig.json +++ b/packages/selectors/tsconfig.json @@ -11,17 +11,9 @@ }, "include": ["src"], "references": [ - { - "path": "../ad-script" - }, - { - "path": "../capture-kit" - }, - { - "path": "../contracts" - }, - { - "path": "../kernel" - } + { "path": "../ad-script" }, + { "path": "../capture-kit" }, + { "path": "../contracts" }, + { "path": "../kernel" } ] } diff --git a/tsconfig.json b/tsconfig.json index 3eeb19b0e2..290b7e9c4f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,83 +29,31 @@ "packages/command-registry/src/global.d.ts" ], "references": [ - { - "path": "packages/ad-replay" - }, - { - "path": "packages/ad-script" - }, - { - "path": "packages/capture-kit" - }, - { - "path": "packages/command-registry" - }, - { - "path": "packages/contracts" - }, - { - "path": "packages/device-selection" - }, - { - "path": "packages/host-kit" - }, - { - "path": "packages/kernel" - }, - { - "path": "packages/maestro" - }, - { - "path": "packages/managed-allocation" - }, - { - "path": "packages/platform-android" - }, - { - "path": "packages/platform-apple" - }, - { - "path": "packages/platform-harmonyos" - }, - { - "path": "packages/platform-linux" - }, - { - "path": "packages/platform-vega" - }, - { - "path": "packages/platform-web" - }, - { - "path": "packages/provider-limrun" - }, - { - "path": "packages/provider-testmu" - }, - { - "path": "packages/provider-webdriver" - }, - { - "path": "packages/provision-kit" - }, - { - "path": "packages/proxy" - }, - { - "path": "packages/replay-port" - }, - { - "path": "packages/replay-test" - }, - { - "path": "packages/selectors" - }, - { - "path": "packages/session-journal" - }, - { - "path": "packages/xml" - } + { "path": "packages/ad-replay" }, + { "path": "packages/ad-script" }, + { "path": "packages/capture-kit" }, + { "path": "packages/command-registry" }, + { "path": "packages/contracts" }, + { "path": "packages/device-selection" }, + { "path": "packages/host-kit" }, + { "path": "packages/kernel" }, + { "path": "packages/maestro" }, + { "path": "packages/managed-allocation" }, + { "path": "packages/platform-android" }, + { "path": "packages/platform-apple" }, + { "path": "packages/platform-harmonyos" }, + { "path": "packages/platform-linux" }, + { "path": "packages/platform-vega" }, + { "path": "packages/platform-web" }, + { "path": "packages/provider-limrun" }, + { "path": "packages/provider-testmu" }, + { "path": "packages/provider-webdriver" }, + { "path": "packages/provision-kit" }, + { "path": "packages/proxy" }, + { "path": "packages/replay-port" }, + { "path": "packages/replay-test" }, + { "path": "packages/selectors" }, + { "path": "packages/session-journal" }, + { "path": "packages/xml" } ] }