Skip to content

Commit 31192d8

Browse files
committed
chore: enforce the terminal daemon platform boundary
1 parent 7a959fb commit 31192d8

5 files changed

Lines changed: 456 additions & 13 deletions

File tree

scripts/layering/check.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
// an import whose source zone outranks its target zone, plus a ratchet on the
2020
// same inversion measured over TYPE-ONLY edges (R6).
2121
// - Over the DAEMON only: SessionState field ownership (R7), because the session
22-
// record is store-owned mutable state that any daemon module can write.
22+
// record is store-owned mutable state that any daemon module can write; and the terminal
23+
// concrete-platform boundary (R65), which rejects every import form into src/platforms or a
24+
// platform package.
2325
// - Over the TYPE GRAPH: the largest type-level import cycle is pinned by
2426
// equality (R9). R4 keeps the value graph acyclic, so these cycles are free at
2527
// runtime but bound what can be read in isolation; growth fails, and so does a
@@ -98,6 +100,7 @@ import { policyLead, policyViolation, ZONE_POLICIES } from './zone-policy.ts';
98100
import { contractsImplementationAuthorityViolations } from './contracts-implementation-policy.ts';
99101
import { selectorPipelineOwnershipViolations } from './selector-pipeline-ownership.ts';
100102
import { recordRuntimeRegistryJoinViolations } from './record-runtime-registry-policy.ts';
103+
import { checkDaemonPlatformBoundary } from './daemon-platform-boundary.ts';
101104
import { listTrackedProductionSources, listTrackedTypeScriptFiles } from './tracked-sources.ts';
102105

103106
const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
@@ -485,7 +488,9 @@ function report(
485488
`inside its declared owner (R7); the largest type-level cycle is ${typeCycle} files ` +
486489
`(R9); ${daemonModularitySummary()}; ` +
487490
`${packageBoundariesSummary(repoRoot)}; ${platformPackagePolicySummary()}; ` +
488-
`${runtimeCommandCutoverSummary()}; and bin.ts imports normalizeCliCommandAlias, ` +
491+
`${runtimeCommandCutoverSummary()}; R65 keeps production src/daemon free of concrete ` +
492+
`platform imports in every executable and type-only form; and bin.ts imports ` +
493+
`normalizeCliCommandAlias, ` +
489494
`actually passes it into buildCommandUsageText, and holds no local alias literals ` +
490495
`(R12).\n`,
491496
);
@@ -543,6 +548,7 @@ export const LAYERING_RULE_IDS = [
543548
'type-spine-inversions',
544549
'session-state-ownership',
545550
'daemon-modularity-ratchets',
551+
'daemon-platform-boundary',
546552
'bin-alias-fast-path',
547553
'package-boundaries',
548554
'platform-package-policy',
@@ -564,6 +570,8 @@ export const LAYERING_RULES: Readonly<Record<LayeringRuleId, LayeringRule>> = {
564570
'session-state-ownership': (context) => checkSessionStateOwnership(context.sources),
565571
'daemon-modularity-ratchets': (context) =>
566572
checkDaemonModularityRatchets(context.edges, context.typeCycleMembers),
573+
'daemon-platform-boundary': (context) =>
574+
checkDaemonPlatformBoundary([...context.sources].map(([path, source]) => ({ path, source }))),
567575
'bin-alias-fast-path': (context) => checkBinAliasFastPath(context.sources),
568576
'package-boundaries': () => checkPackageBoundaries(repoRoot),
569577
'platform-package-policy': (context) =>
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'node:test';
3+
import {
4+
DAEMON_PLATFORM_BOUNDARY_RULE,
5+
daemonPlatformBoundaryViolations,
6+
findDaemonPlatformDependencies,
7+
} from './daemon-platform-boundary.ts';
8+
9+
const daemonFile = 'src/daemon/terminal-boundary-fixture.ts';
10+
11+
function dependencies(source: string, file = daemonFile) {
12+
return findDaemonPlatformDependencies([{ path: file, source }]);
13+
}
14+
15+
function violations(source: string, file = daemonFile) {
16+
return daemonPlatformBoundaryViolations([{ path: file, source }]);
17+
}
18+
19+
test('R65 rejects static, type-only, dynamic, type, and re-export dependencies with source lines', () => {
20+
const source = [
21+
"import { old } from '../platforms/android/old.ts';",
22+
"import type { OldType } from '../platforms/android/types.ts';",
23+
"const lazy = import('../platforms/android/lazy.ts');",
24+
"type ImportedType = import('../platforms/android/type.ts').ImportedType;",
25+
"export { old } from '../platforms/android/re-export.ts';",
26+
"export type { OldType } from '@agent-device/platform-android/types';",
27+
"export * from '@agent-device/platform-apple';",
28+
].join('\n');
29+
30+
assert.deepEqual(
31+
dependencies(source).map(({ kind, spec, line, target }) => ({ kind, spec, line, target })),
32+
[
33+
{
34+
kind: 'static import',
35+
spec: '../platforms/android/old.ts',
36+
line: 1,
37+
target: 'src/platforms/android/old.ts',
38+
},
39+
{
40+
kind: 'type-only import',
41+
spec: '../platforms/android/types.ts',
42+
line: 2,
43+
target: 'src/platforms/android/types.ts',
44+
},
45+
{
46+
kind: 'dynamic import',
47+
spec: '../platforms/android/lazy.ts',
48+
line: 3,
49+
target: 'src/platforms/android/lazy.ts',
50+
},
51+
{
52+
kind: 'type import',
53+
spec: '../platforms/android/type.ts',
54+
line: 4,
55+
target: 'src/platforms/android/type.ts',
56+
},
57+
{
58+
kind: 're-export',
59+
spec: '../platforms/android/re-export.ts',
60+
line: 5,
61+
target: 'src/platforms/android/re-export.ts',
62+
},
63+
{
64+
kind: 'type-only re-export',
65+
spec: '@agent-device/platform-android/types',
66+
line: 6,
67+
target: '@agent-device/platform-android/types',
68+
},
69+
{
70+
kind: 're-export',
71+
spec: '@agent-device/platform-apple',
72+
line: 7,
73+
target: '@agent-device/platform-apple',
74+
},
75+
],
76+
);
77+
78+
assert.deepEqual(
79+
violations(source).map(({ rule, file, line }) => ({ rule, file, line })),
80+
Array.from({ length: 7 }, (_, index) => ({
81+
rule: DAEMON_PLATFORM_BOUNDARY_RULE,
82+
file: daemonFile,
83+
line: index + 1,
84+
})),
85+
);
86+
});
87+
88+
test('R65 recognizes side-effect and aliased imports, including nested daemon paths', () => {
89+
const source = [
90+
"import '../../platforms/web/runtime.ts';",
91+
"import { runtime as platformRuntime } from '@agent-device/platform-web/runtime';",
92+
].join('\n');
93+
94+
assert.deepEqual(
95+
dependencies(source, 'src/daemon/handlers/terminal-boundary-fixture.ts').map((found) => ({
96+
kind: found.kind,
97+
line: found.line,
98+
spec: found.spec,
99+
target: found.target,
100+
})),
101+
[
102+
{
103+
kind: 'static import',
104+
line: 1,
105+
spec: '../../platforms/web/runtime.ts',
106+
target: 'src/platforms/web/runtime.ts',
107+
},
108+
{
109+
kind: 'static import',
110+
line: 2,
111+
spec: '@agent-device/platform-web/runtime',
112+
target: '@agent-device/platform-web/runtime',
113+
},
114+
],
115+
);
116+
});
117+
118+
test('R65 ignores comments, ordinary strings, unresolved dynamic imports, and lookalike names', () => {
119+
const source = [
120+
'const documentation = "import(\'../platforms/android/comment.ts\'); @agent-device/platform-android";',
121+
"// import { ignored } from '../platforms/android/comment.ts';",
122+
"const packageName = '@agent-device/platform-android';",
123+
"const relativeName = '../platforms/android/not-an-import.ts';",
124+
'const computed = import(platformSpecifier);',
125+
"import '../platforms-sibling/not-platform.ts';",
126+
"import '@agent-device/platforms';",
127+
"import '@agent-device/platform';",
128+
].join('\n');
129+
130+
assert.deepEqual(dependencies(source), []);
131+
});
132+
133+
test('R65 rejects require, import-equals, and template-literal type imports', () => {
134+
const source = [
135+
"const android = require('@agent-device/platform-android');",
136+
'const runtime = require(`../platforms/android/runtime.ts`);',
137+
"import web = require('@agent-device/platform-web');",
138+
'type Apple = import(`@agent-device/platform-apple`).Apple;',
139+
'type Android = import(`../platforms/android/types.ts`).Android;',
140+
].join('\n');
141+
142+
assert.deepEqual(
143+
dependencies(source).map(({ kind, spec, line }) => ({ kind, spec, line })),
144+
[
145+
{ kind: 'require', spec: '@agent-device/platform-android', line: 1 },
146+
{ kind: 'require', spec: '../platforms/android/runtime.ts', line: 2 },
147+
{ kind: 'import equals', spec: '@agent-device/platform-web', line: 3 },
148+
{ kind: 'type import', spec: '@agent-device/platform-apple', line: 4 },
149+
{ kind: 'type import', spec: '../platforms/android/types.ts', line: 5 },
150+
],
151+
);
152+
});
153+
154+
test('R65 folds statically constructed dynamic platform specifiers', () => {
155+
const source = [
156+
"const apple = import('@agent-device/' + 'platform-apple');",
157+
'const android = import(`../platforms/${"android"}/runtime.ts`);',
158+
].join('\n');
159+
160+
assert.deepEqual(
161+
dependencies(source).map(({ kind, spec, line }) => ({ kind, spec, line })),
162+
[
163+
{ kind: 'dynamic import', spec: '@agent-device/platform-apple', line: 1 },
164+
{ kind: 'dynamic import', spec: '../platforms/android/runtime.ts', line: 2 },
165+
],
166+
);
167+
});
168+
169+
test('R65 ignores non-daemon and test-shaped records even when their syntax is red', () => {
170+
const source = "import { platform } from '../platforms/android/runtime.ts';";
171+
assert.deepEqual(dependencies(source, 'src/core/terminal-boundary-fixture.ts'), []);
172+
assert.deepEqual(dependencies(source, 'src/daemon/terminal-boundary-fixture.test.ts'), []);
173+
assert.deepEqual(dependencies(source, 'src/daemon/__tests__/terminal-boundary-fixture.ts'), []);
174+
});
175+
176+
test('R65 treats a relative specifier as legacy platform code only after path resolution', () => {
177+
const source = [
178+
"import { platform } from '../../platforms/android/runtime.ts';",
179+
"import { sibling } from '../platforms-sibling/runtime.ts';",
180+
"import { exact } from '../platforms';",
181+
].join('\n');
182+
183+
assert.deepEqual(
184+
dependencies(source).map(({ spec, target, line }) => ({ spec, target, line })),
185+
[
186+
{
187+
spec: '../platforms',
188+
target: 'src/platforms',
189+
line: 3,
190+
},
191+
],
192+
);
193+
});

0 commit comments

Comments
 (0)