Skip to content

Commit fea7ca8

Browse files
authored
chore(layering): runner modules reach host-kit only through the runner host port (#2470)
R77 apple-runner-host-port bans a direct @agent-device/host-kit/* value import from packages/platform-apple/src/runner/**; the port at runner/host.ts, bound in core/runner-host.ts, is the only door. runner/** sits in the eager closure of seven Apple facade entries eager-closure-budgets.ts holds at a fixed size, so a direct import grows all seven at once (#2423 measured one candidate import adding 5 modules to runner/index.ts's closure, 13 -> 18, after two review rounds spent rediscovering this).
1 parent 3e89f82 commit fea7ca8

4 files changed

Lines changed: 165 additions & 0 deletions

File tree

packages/platform-apple/src/runner/host.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@ import type { XmlNode } from '@agent-device/xml';
1717
* runner uses; the composition-root assignment is the conformance check, so a
1818
* root signature drifting incompatibly fails typecheck there rather than at
1919
* runtime.
20+
*
21+
* A host-kit symbol the runner needs is added HERE, on {@link AppleRunnerHost}, and bound to the
22+
* real implementation in `core/runner-host.ts` -- never imported directly from a `runner/*`
23+
* module. The reason: `packages/platform-apple/src/runner/` sits in the eager import closure of
24+
* seven Apple facade entries (`app-lifecycle-facade.ts`, `app-resolution-facade.ts`,
25+
* `doctor-facade.ts`, `perf-facade.ts`, `physical-device-facade.ts`, `runner-operations-facade.ts`,
26+
* `runner/index.ts`) that `scripts/__tests__/eager-closure-budgets.ts` holds at a fixed size (no
27+
* growth against the merge-base); a static `@agent-device/host-kit/*` value import from a runner
28+
* module adds every module on its own import path to all seven closures at once (#2423 measured
29+
* one candidate import adding 5 modules to `runner/index.ts`'s closure, 13 -> 18, after two review
30+
* rounds spent rediscovering this). `scripts/layering/` enforces the port at the import-graph
31+
* level (R77 apple-runner-host-port): a `runner/**` file may hold a type-only
32+
* `@agent-device/host-kit/*` import, which evaluates nothing, but never a value one. A pure
33+
* constant that both the runner and another package need is not a host-kit exception to this -- it
34+
* belongs in a runner module already inside every facade closure (e.g.
35+
* `runner/apple-runner-platform.ts`), imported directly from there.
2036
*/
2137

2238
export type ExecResult = {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'node:test';
3+
import { appleRunnerHostPortViolations, RUNNER_SUBTREE } from './apple-runner-host-port-policy.ts';
4+
5+
function sources(entries: Record<string, string>): ReadonlyMap<string, string> {
6+
return new Map(Object.entries(entries));
7+
}
8+
9+
test('a runner module value-importing host-kit directly is refused', () => {
10+
const violations = appleRunnerHostPortViolations(
11+
sources({
12+
[`${RUNNER_SUBTREE}runner-planted.ts`]:
13+
"import { runCmdSync } from '@agent-device/host-kit/command';\n",
14+
}),
15+
);
16+
assert.equal(violations.length, 1);
17+
const [violation] = violations;
18+
assert.equal(violation!.rule, 'R77 apple-runner-host-port');
19+
assert.equal(violation!.file, `${RUNNER_SUBTREE}runner-planted.ts`);
20+
assert.equal(violation!.line, 1);
21+
assert.match(violation!.message, /runner host port/);
22+
assert.match(violation!.message, /runner\/host\.ts, bound in core\/runner-host\.ts/);
23+
assert.match(violation!.message, /eager-closure-budgets/);
24+
});
25+
26+
test('every value-import form that reaches host-kit is refused', () => {
27+
for (const source of [
28+
"import { runCmdSync } from '@agent-device/host-kit/command';",
29+
"import * as command from '@agent-device/host-kit/command';",
30+
"const command = await import('@agent-device/host-kit/command');",
31+
"export { runCmdSync } from '@agent-device/host-kit/command';",
32+
"export * from '@agent-device/host-kit/command';",
33+
"import '@agent-device/host-kit/command';",
34+
]) {
35+
const violations = appleRunnerHostPortViolations(
36+
sources({ [`${RUNNER_SUBTREE}runner-planted.ts`]: source }),
37+
);
38+
assert.equal(violations.length, 1, source);
39+
}
40+
});
41+
42+
test('a type-only host-kit import is exempt, in the runner subtree and on the port itself', () => {
43+
assert.deepEqual(
44+
appleRunnerHostPortViolations(
45+
sources({
46+
[`${RUNNER_SUBTREE}runner-planted.ts`]:
47+
"import type { ExecResult } from '@agent-device/host-kit/command';\n",
48+
[`${RUNNER_SUBTREE}host.ts`]:
49+
"import type { ExecOptions } from '@agent-device/host-kit/command';\n",
50+
}),
51+
),
52+
[],
53+
);
54+
});
55+
56+
test('a host-kit import outside the runner subtree is not this rule’s concern', () => {
57+
assert.deepEqual(
58+
appleRunnerHostPortViolations(
59+
sources({
60+
'packages/platform-apple/src/core/runner-host.ts':
61+
"import { runCmdSync } from '@agent-device/host-kit/command';\n",
62+
}),
63+
),
64+
[],
65+
);
66+
});
67+
68+
test('a runner import of anything other than host-kit is not this rule’s concern', () => {
69+
assert.deepEqual(
70+
appleRunnerHostPortViolations(
71+
sources({
72+
[`${RUNNER_SUBTREE}runner-planted.ts`]: [
73+
"import { PLATFORMS } from '@agent-device/kernel/device';",
74+
"import { runCmdSync } from './host.ts';",
75+
].join('\n'),
76+
}),
77+
),
78+
[],
79+
);
80+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Catches: a runner module reaching `@agent-device/host-kit/*` directly instead of through the
2+
// Apple runner host port (`runner/host.ts`, bound in `core/runner-host.ts`) -- invisible to R13's
3+
// general platform-package import rule, which admits host-kit to every platform-apple file, and
4+
// invisible to typecheck, because the direct import and the port delegator have the same call
5+
// shape. `packages/platform-apple/src/runner/` sits in the eager import closure of seven Apple
6+
// facade entries (`app-lifecycle-facade.ts`, `app-resolution-facade.ts`, `doctor-facade.ts`,
7+
// `perf-facade.ts`, `physical-device-facade.ts`, `runner-operations-facade.ts`, `runner/index.ts`)
8+
// held at a fixed size by `scripts/__tests__/eager-closure-budgets.ts`: a direct host-kit value
9+
// import from a runner module adds every module on its own import path to all seven closures at
10+
// once.
11+
// Evidence: #2423 measured a candidate direct `@agent-device/host-kit/command` import from
12+
// `runner-cache-metadata.ts` adding 5 modules to `runner/index.ts`'s closure (13 -> 18); the
13+
// review spent two rounds rediscovering the port requirement before the symbol was routed back
14+
// through `runner/host.ts`, which is the gap this rule closes.
15+
// Cost: 141 LOC (61 rule + 80 test).
16+
// Kill criterion: none enforced today; retire only by maintainer decision that the eager-closure
17+
// budgets no longer bind the runner subtree, or that the port itself is retired in favor of some
18+
// other seam that keeps the same property.
19+
20+
import { parseImports, type LayeringViolation } from './model.ts';
21+
22+
const RULE = 'R77 apple-runner-host-port';
23+
24+
/** Every file this rule polices, production and test alike -- the port has no test exception. */
25+
export const RUNNER_SUBTREE = 'packages/platform-apple/src/runner/';
26+
27+
const HOST_KIT_PREFIX = '@agent-device/host-kit/';
28+
29+
function violation(file: string, line: number, spec: string): LayeringViolation {
30+
return {
31+
rule: RULE,
32+
file,
33+
line,
34+
message:
35+
`imports '${spec}' directly. Reach host-kit through the runner host port ` +
36+
`(runner/host.ts, bound in core/runner-host.ts); a direct import grows the Apple facade ` +
37+
`eager closures (eager-closure-budgets).`,
38+
};
39+
}
40+
41+
/**
42+
* `runner/**` (every file, `host.ts` included -- it holds none today, and a value import there
43+
* would defeat the port it defines) may not VALUE-import `@agent-device/host-kit/*`. A type-only
44+
* import of the same specifier (an `import type` declaration, or a named `type` specifier) is
45+
* exempt everywhere: it evaluates nothing, so it cannot add a module to a closure the
46+
* eager-closure gate measures at runtime.
47+
*/
48+
export function appleRunnerHostPortViolations(
49+
sources: ReadonlyMap<string, string>,
50+
): LayeringViolation[] {
51+
const violations: LayeringViolation[] = [];
52+
for (const [file, source] of sources) {
53+
if (!file.startsWith(RUNNER_SUBTREE)) continue;
54+
for (const site of parseImports(source)) {
55+
if (site.typeOnly) continue;
56+
if (!site.spec.startsWith(HOST_KIT_PREFIX)) continue;
57+
violations.push(violation(file, site.line, site.spec));
58+
}
59+
}
60+
return violations;
61+
}

scripts/layering/check.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
// an exports map, and every workspace specifier declared + exports-named (R11).
3333
// - Over PLATFORM PACKAGE COMPOSITION: six private metadata façades meet at the exact root
3434
// composition file; premature implementation loading and forbidden cross-boundary edges fail (R13).
35+
// - Over THE APPLE RUNNER SUBTREE: `runner/**` may not value-import `@agent-device/host-kit/*`
36+
// directly (R77) — the subtree sits in the eager closure of seven Apple façade entries the
37+
// eager-closure-budgets gate holds at a fixed size, so a direct host-kit edge grows all seven;
38+
// host-kit reaches the runner only through `runner/host.ts`, bound in `core/runner-host.ts`.
3539
// - Over REQUEST-BOUND RUNTIME EXECUTION: facts remain the only admission authority and daemon
3640
// code cannot manufacture or repair a narrowed runtime proof (R66).
3741
// - Over CONTRACTS PRODUCTION SOURCE: contracts owns vocabulary only — host, process, and timer
@@ -90,6 +94,7 @@ import {
9094
checkRetiredPlatformsZone,
9195
platformPackagePolicySummary,
9296
} from './platform-package-policy.ts';
97+
import { appleRunnerHostPortViolations } from './apple-runner-host-port-policy.ts';
9398
import {
9499
listUntrackedProductionTypeScriptFiles,
95100
readTrackedPlatformPackageDeclarations,
@@ -443,6 +448,7 @@ export const LAYERING_RULE_IDS = [
443448
'daemon-platform-boundary',
444449
'package-boundaries',
445450
'platform-package-policy',
451+
'apple-runner-host-port',
446452
'retired-platforms-zone',
447453
'src-utils-retirement',
448454
'replay-ownership',
@@ -493,6 +499,8 @@ export const LAYERING_RULES: Readonly<Record<LayeringRuleId, LayeringRule>> = {
493499
readTrackedPlatformPackageDeclarations(repoRoot),
494500
{ untrackedProductionFiles: listUntrackedProductionTypeScriptFiles(repoRoot) },
495501
),
502+
'apple-runner-host-port': (context) =>
503+
appleRunnerHostPortViolations(context.allTypeScriptSources),
496504
'retired-platforms-zone': () => checkRetiredPlatformsZone(listTrackedPlatformZoneFiles(repoRoot)),
497505
'src-utils-retirement': (context) =>
498506
retiredPathRuleViolations('R14', context.trackedSrcUtilsFiles),

0 commit comments

Comments
 (0)