Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/platform-apple/src/runner/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ import type { XmlNode } from '@agent-device/xml';
* runner uses; the composition-root assignment is the conformance check, so a
* root signature drifting incompatibly fails typecheck there rather than at
* runtime.
*
* A host-kit symbol the runner needs is added HERE, on {@link AppleRunnerHost}, and bound to the
* real implementation in `core/runner-host.ts` -- never imported directly from a `runner/*`
* module. The reason: `packages/platform-apple/src/runner/` sits in the eager import closure of
* seven Apple facade entries (`app-lifecycle-facade.ts`, `app-resolution-facade.ts`,
* `doctor-facade.ts`, `perf-facade.ts`, `physical-device-facade.ts`, `runner-operations-facade.ts`,
* `runner/index.ts`) that `scripts/__tests__/eager-closure-budgets.ts` holds at a fixed size (no
* growth against the merge-base); a static `@agent-device/host-kit/*` value import from a runner
* module adds every module on its own import path to all seven closures 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). `scripts/layering/` enforces the port at the import-graph
* level (R77 apple-runner-host-port): a `runner/**` file may hold a type-only
* `@agent-device/host-kit/*` import, which evaluates nothing, but never a value one. A pure
* constant that both the runner and another package need is not a host-kit exception to this -- it
* belongs in a runner module already inside every facade closure (e.g.
* `runner/apple-runner-platform.ts`), imported directly from there.
*/

export type ExecResult = {
Expand Down
80 changes: 80 additions & 0 deletions scripts/layering/apple-runner-host-port-policy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { appleRunnerHostPortViolations, RUNNER_SUBTREE } from './apple-runner-host-port-policy.ts';

function sources(entries: Record<string, string>): ReadonlyMap<string, string> {
return new Map(Object.entries(entries));
}

test('a runner module value-importing host-kit directly is refused', () => {
const violations = appleRunnerHostPortViolations(
sources({
[`${RUNNER_SUBTREE}runner-planted.ts`]:
"import { runCmdSync } from '@agent-device/host-kit/command';\n",
}),
);
assert.equal(violations.length, 1);
const [violation] = violations;
assert.equal(violation!.rule, 'R77 apple-runner-host-port');
assert.equal(violation!.file, `${RUNNER_SUBTREE}runner-planted.ts`);
assert.equal(violation!.line, 1);
assert.match(violation!.message, /runner host port/);
assert.match(violation!.message, /runner\/host\.ts, bound in core\/runner-host\.ts/);
assert.match(violation!.message, /eager-closure-budgets/);
});

test('every value-import form that reaches host-kit is refused', () => {
for (const source of [
"import { runCmdSync } from '@agent-device/host-kit/command';",
"import * as command from '@agent-device/host-kit/command';",
"const command = await import('@agent-device/host-kit/command');",
"export { runCmdSync } from '@agent-device/host-kit/command';",
"export * from '@agent-device/host-kit/command';",
"import '@agent-device/host-kit/command';",
]) {
const violations = appleRunnerHostPortViolations(
sources({ [`${RUNNER_SUBTREE}runner-planted.ts`]: source }),
);
assert.equal(violations.length, 1, source);
}
});

test('a type-only host-kit import is exempt, in the runner subtree and on the port itself', () => {
assert.deepEqual(
appleRunnerHostPortViolations(
sources({
[`${RUNNER_SUBTREE}runner-planted.ts`]:
"import type { ExecResult } from '@agent-device/host-kit/command';\n",
[`${RUNNER_SUBTREE}host.ts`]:
"import type { ExecOptions } from '@agent-device/host-kit/command';\n",
}),
),
[],
);
});

test('a host-kit import outside the runner subtree is not this rule’s concern', () => {
assert.deepEqual(
appleRunnerHostPortViolations(
sources({
'packages/platform-apple/src/core/runner-host.ts':
"import { runCmdSync } from '@agent-device/host-kit/command';\n",
}),
),
[],
);
});

test('a runner import of anything other than host-kit is not this rule’s concern', () => {
assert.deepEqual(
appleRunnerHostPortViolations(
sources({
[`${RUNNER_SUBTREE}runner-planted.ts`]: [
"import { PLATFORMS } from '@agent-device/kernel/device';",
"import { runCmdSync } from './host.ts';",
].join('\n'),
}),
),
[],
);
});
61 changes: 61 additions & 0 deletions scripts/layering/apple-runner-host-port-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Catches: a runner module reaching `@agent-device/host-kit/*` directly instead of through the
// Apple runner host port (`runner/host.ts`, bound in `core/runner-host.ts`) -- invisible to R13's
// general platform-package import rule, which admits host-kit to every platform-apple file, and
// invisible to typecheck, because the direct import and the port delegator have the same call
// shape. `packages/platform-apple/src/runner/` sits in the eager import closure of seven Apple
// facade entries (`app-lifecycle-facade.ts`, `app-resolution-facade.ts`, `doctor-facade.ts`,
// `perf-facade.ts`, `physical-device-facade.ts`, `runner-operations-facade.ts`, `runner/index.ts`)
// held at a fixed size by `scripts/__tests__/eager-closure-budgets.ts`: a direct host-kit value
// import from a runner module adds every module on its own import path to all seven closures at
// once.
// Evidence: #2423 measured a candidate direct `@agent-device/host-kit/command` import from
// `runner-cache-metadata.ts` adding 5 modules to `runner/index.ts`'s closure (13 -> 18); the
// review spent two rounds rediscovering the port requirement before the symbol was routed back
// through `runner/host.ts`, which is the gap this rule closes.
// Cost: 141 LOC (61 rule + 80 test).
// Kill criterion: none enforced today; retire only by maintainer decision that the eager-closure
// budgets no longer bind the runner subtree, or that the port itself is retired in favor of some
// other seam that keeps the same property.

import { parseImports, type LayeringViolation } from './model.ts';

const RULE = 'R77 apple-runner-host-port';

/** Every file this rule polices, production and test alike -- the port has no test exception. */
export const RUNNER_SUBTREE = 'packages/platform-apple/src/runner/';

const HOST_KIT_PREFIX = '@agent-device/host-kit/';

function violation(file: string, line: number, spec: string): LayeringViolation {
return {
rule: RULE,
file,
line,
message:
`imports '${spec}' directly. Reach host-kit through the runner host port ` +
`(runner/host.ts, bound in core/runner-host.ts); a direct import grows the Apple facade ` +
`eager closures (eager-closure-budgets).`,
};
}

/**
* `runner/**` (every file, `host.ts` included -- it holds none today, and a value import there
* would defeat the port it defines) may not VALUE-import `@agent-device/host-kit/*`. A type-only
* import of the same specifier (an `import type` declaration, or a named `type` specifier) is
* exempt everywhere: it evaluates nothing, so it cannot add a module to a closure the
* eager-closure gate measures at runtime.
*/
export function appleRunnerHostPortViolations(
sources: ReadonlyMap<string, string>,
): LayeringViolation[] {
const violations: LayeringViolation[] = [];
for (const [file, source] of sources) {
if (!file.startsWith(RUNNER_SUBTREE)) continue;
for (const site of parseImports(source)) {
if (site.typeOnly) continue;
if (!site.spec.startsWith(HOST_KIT_PREFIX)) continue;
violations.push(violation(file, site.line, site.spec));
}
}
return violations;
}
8 changes: 8 additions & 0 deletions scripts/layering/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
// an exports map, and every workspace specifier declared + exports-named (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/*`
// directly (R77) — the subtree sits in the eager closure of seven Apple façade entries the
// eager-closure-budgets gate holds at a fixed size, so a direct host-kit edge grows all seven;
// host-kit reaches the runner only through `runner/host.ts`, bound in `core/runner-host.ts`.
// - Over REQUEST-BOUND RUNTIME EXECUTION: facts remain the only admission authority and daemon
// code cannot manufacture or repair a narrowed runtime proof (R66).
// - Over CONTRACTS PRODUCTION SOURCE: contracts owns vocabulary only — host, process, and timer
Expand Down Expand Up @@ -90,6 +94,7 @@ import {
checkRetiredPlatformsZone,
platformPackagePolicySummary,
} from './platform-package-policy.ts';
import { appleRunnerHostPortViolations } from './apple-runner-host-port-policy.ts';
import {
listUntrackedProductionTypeScriptFiles,
readTrackedPlatformPackageDeclarations,
Expand Down Expand Up @@ -443,6 +448,7 @@ export const LAYERING_RULE_IDS = [
'daemon-platform-boundary',
'package-boundaries',
'platform-package-policy',
'apple-runner-host-port',
'retired-platforms-zone',
'src-utils-retirement',
'replay-ownership',
Expand Down Expand Up @@ -493,6 +499,8 @@ export const LAYERING_RULES: Readonly<Record<LayeringRuleId, LayeringRule>> = {
readTrackedPlatformPackageDeclarations(repoRoot),
{ untrackedProductionFiles: listUntrackedProductionTypeScriptFiles(repoRoot) },
),
'apple-runner-host-port': (context) =>
appleRunnerHostPortViolations(context.allTypeScriptSources),
'retired-platforms-zone': () => checkRetiredPlatformsZone(listTrackedPlatformZoneFiles(repoRoot)),
'src-utils-retirement': (context) =>
retiredPathRuleViolations('R14', context.trackedSrcUtilsFiles),
Expand Down
Loading