Skip to content

Commit 95ab1df

Browse files
committed
refactor: retire platforms source seam
1 parent 9abcd7f commit 95ab1df

15 files changed

Lines changed: 93 additions & 151 deletions

docs/adr/0019-request-bound-platform-runtime.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ R65. The per-command cutover table was retired after completion as required by s
1919
enforces the permanent facts-only admission and runtime-proof invariants without naming historical
2020
routes or handler functions.
2121

22+
The #2082 extraction completes the physical ownership boundary: all six platform-family
23+
implementations and their family-owned tests live behind private package exports, `src/platforms`
24+
is retired, and the former R3 folder seam is gone. R13 now owns concrete platform-package import
25+
direction and implementation laziness; the `retired-platforms-zone` gate rejects any attempt to
26+
recreate the old root path.
27+
2228
## Rules at a glance
2329

2430
- Daemon device-execution code depends on platform-neutral contracts. Concrete device mechanics
@@ -167,8 +173,8 @@ selection, R11/R13 package enumeration, and the composite typecheck project list
167173
>
168174
> Enforcement: each substrate package's exported subpaths are pinned in
169175
> `package-boundaries.test.ts` (widening fails the gate), the contracts mechanics gate stays
170-
> planted red, and the `platforms-root-shape` rule rejects any new shared file or directory
171-
> appearing directly under `src/platforms`.
176+
> planted red, and the `retired-platforms-zone` rule rejects every production, test, or fixture
177+
> file under the former `src/platforms` path.
172178
173179
The Apple XCUITest runner client is a durable platform-owned implementation facet colocated
174180
inside `packages/platform-apple` as the `src/runner/` subtree (#2040) — Apple mechanics belong to
@@ -770,11 +776,12 @@ belongs to its domain: test-IME restoration is durable device state with marker-
770776
helper stops follow the owning platform module's lifecycle policy, and close-time cleanup consumes
771777
neutral owner services.
772778

773-
R65 is the end-state enforcement: its planted-red AST tests reject every dependency edge — static,
779+
R65 is the daemon-side end-state enforcement: its planted-red AST tests reject every dependency edge — static,
774780
dynamic, re-export, and type-only — from production `src/daemon/**` modules (test files excluded,
775781
matching the layering scanner's scope) to `src/platforms/**` and concrete
776-
`@agent-device/platform-*` packages. The daemon has been removed from the R3 seam, so platform
777-
freedom is structurally enforced rather than periodically measured.
782+
`@agent-device/platform-*` packages. R13 governs concrete package imports across the whole tree,
783+
while `retired-platforms-zone` prevents the old root seam from being recreated; platform freedom is
784+
therefore structurally enforced rather than periodically measured.
778785

779786
## Relationship to prior decisions
780787

scripts/check-affected/device-lanes.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ test('shared runtime surface owns every device lane', () => {
7575
'packages/kernel/src/errors.ts',
7676
'src/daemon/android-system-dialog.ts', // naming convention in a shared dir, not a boundary
7777
'test/integration/smoke-daemon-clean.test.ts',
78-
'src/platforms/install-source.ts',
78+
'packages/provision-kit/src/install-source.ts',
7979
]) {
8080
assert.equal(deviceLaneLeaf(file), 'shared', file);
8181
assert.deepEqual(lanes(file), [

scripts/check-affected/model.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -259,27 +259,10 @@ const staticTsGates: OwnershipRule = ({ file, isTs, underSrc, underTest }) =>
259259

260260
const srcProdGate: OwnershipRule = ({ file, isSrcProd }) => {
261261
if (!isSrcProd) return [];
262-
const selections = [
262+
return [
263263
reason('layering', file, 'gate:layering', 'layering guard reads production src/ modules'),
264264
reason('build', file, 'src-prod', 'production source is compiled by the build'),
265265
];
266-
if (file.startsWith('src/platforms/')) {
267-
selections.push(
268-
reason(
269-
'provider-integration',
270-
file,
271-
'platform-src',
272-
'platform source shapes device/provider wire behavior',
273-
),
274-
reason(
275-
'coverage',
276-
file,
277-
'platform-src',
278-
'Testing Matrix requires coverage for platform/device-response changes',
279-
),
280-
);
281-
}
282-
return selections;
283266
};
284267

285268
function isNodeIntegrationPath(file: string): boolean {

scripts/layering/check.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
//
44
// Ranked target spine, as rank groups lowest to highest. `A ◄ B` means B may not
55
// be outranked by A (the back-edge order the gate rejects), NOT that every displayed import exists:
6-
// { contracts, request, selectors, platforms } ◄ core ◄ { commands, cli-schema }
6+
// { contracts, request, selectors } ◄ core ◄ { commands, cli-schema }
77
// ◄ { client, daemon-server } ◄ daemon-client ◄ cli
88
// (authoritative ranks: `TARGET_DAG_RANK` in model.ts. The former rank-0 kernel
99
// zone lives in packages/kernel since #1490 W0; R11 owns its boundary.)
1010
//
1111
// This gate enforces five things, across four scopes:
12-
// - GLOBALLY, across every production source file: the R2-R3 move rules and
12+
// - GLOBALLY, across every production source file: the remaining R2 move rule and
1313
// rejection of all production static value-import cycles (R4). R1 kernel-sink
1414
// retired with the kernel's move to packages/kernel (#1490 W0); R8
1515
// zero-dep-job-closure retired with the last `install-deps: false` job
@@ -20,8 +20,8 @@
2020
// same inversion measured over TYPE-ONLY edges (R6).
2121
// - Over the DAEMON only: SessionState field ownership (R7), because the session
2222
// 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.
23+
// concrete-platform boundary (R65), which rejects every import form into the retired
24+
// src/platforms path or a platform package.
2525
// - Over the TYPE GRAPH: the largest type-level import cycle is pinned by
2626
// equality (R9). R4 keeps the value graph acyclic, so these cycles are free at
2727
// runtime but bound what can be read in isolation; growth fails, and so does a
@@ -82,7 +82,7 @@ import {
8282
} from './package-boundaries.ts';
8383
import {
8484
checkPlatformPackagePolicy,
85-
checkPlatformsRootShape,
85+
checkRetiredPlatformsZone,
8686
platformPackagePolicySummary,
8787
} from './platform-package-policy.ts';
8888
import {
@@ -96,7 +96,11 @@ import { selectorPipelineOwnershipViolations } from './selector-pipeline-ownersh
9696
import { recordRuntimeRegistryJoinViolations } from './record-runtime-registry-policy.ts';
9797
import { recordRuntimeDaemonMechanicsViolations } from './record-runtime-mechanics-policy.ts';
9898
import { checkDaemonPlatformBoundary } from './daemon-platform-boundary.ts';
99-
import { listTrackedProductionSources, listTrackedTypeScriptFiles } from './tracked-sources.ts';
99+
import {
100+
listTrackedPlatformZoneFiles,
101+
listTrackedProductionSources,
102+
listTrackedTypeScriptFiles,
103+
} from './tracked-sources.ts';
100104
import { runtimeExecutionIntegrityViolations } from './runtime-execution-policy.ts';
101105
import { sourceExecutionCompatibilityViolations } from './source-execution-policy.ts';
102106
import { sessionResourceOwnershipViolations } from './session-resource-ownership.ts';
@@ -469,7 +473,7 @@ function report(
469473
): number {
470474
if (violations.length === 0) {
471475
process.stdout.write(
472-
`Layering guard: OK — ${files.length} source files satisfy R2-R3 and contain no ` +
476+
`Layering guard: OK — ${files.length} source files satisfy R2 and contain no ` +
473477
`value-import cycles (both checked globally); the ranked target spine contains no ` +
474478
`back-edges (only the composition root is unranked among src zones), and its type-only ` +
475479
`inversions match the R6 ratchet (${Object.values(TYPE_INVERSION_BASELINE).reduce((sum, count) => sum + count, 0)} remaining); ` +
@@ -546,7 +550,7 @@ export const LAYERING_RULE_IDS = [
546550
'bin-alias-fast-path',
547551
'package-boundaries',
548552
'platform-package-policy',
549-
'platforms-root-shape',
553+
'retired-platforms-zone',
550554
] as const;
551555

552556
export type LayeringRuleId = (typeof LAYERING_RULE_IDS)[number];
@@ -584,8 +588,7 @@ export const LAYERING_RULES: Readonly<Record<LayeringRuleId, LayeringRule>> = {
584588
readTrackedPlatformPackageDeclarations(repoRoot),
585589
{ untrackedProductionFiles: listUntrackedProductionTypeScriptFiles(repoRoot) },
586590
),
587-
'platforms-root-shape': (context) =>
588-
checkPlatformsRootShape([...context.allTypeScriptSources.keys()]),
591+
'retired-platforms-zone': () => checkRetiredPlatformsZone(listTrackedPlatformZoneFiles(repoRoot)),
589592
};
590593

591594
export function main(): number {

scripts/layering/model.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ test('largestTypeCycleSize counts type-only cycles and ignores dynamic ones', ()
416416
]);
417417

418418
// A loop closed through a DYNAMIC import is excluded on purpose: a lazy seam is not a
419-
// comprehension barrier, and R3 relies on dynamic imports existing. With no non-dynamic edge at
419+
// comprehension barrier. With no non-dynamic edge at
420420
// all no file enters the walk, so the floor here is 0 rather than 1 — specified, not incidental.
421421
const dynamicCycle = resolveImportEdges(
422422
new Map(

scripts/layering/platform-package-policy.test.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { test } from 'node:test';
33
import {
44
CANONICAL_PLATFORM_FAMILIES,
55
checkPlatformPackagePolicy,
6-
checkPlatformsRootShape,
6+
checkRetiredPlatformsZone,
77
type PlatformPackageDeclaration,
88
} from './platform-package-policy.ts';
99
import { classifyZone } from './model.ts';
@@ -102,7 +102,7 @@ test('the inventory substrate has six private lazy packages and one exact compos
102102
});
103103

104104
test('retired platform family implementations are rejected from src/platforms', () => {
105-
const violations = checkPlatformsRootShape([
105+
const violations = checkRetiredPlatformsZone([
106106
'src/platforms/apple/core/apps.ts',
107107
'src/platforms/harmonyos/app-lifecycle.ts',
108108
'src/platforms/linux/snapshot.ts',
@@ -113,11 +113,11 @@ test('retired platform family implementations are rejected from src/platforms',
113113
assert.deepEqual(
114114
violations.map(({ file, rule }) => ({ file, rule })),
115115
[
116-
{ file: 'src/platforms/apple/core/apps.ts', rule: 'platforms-root-shape' },
117-
{ file: 'src/platforms/harmonyos/app-lifecycle.ts', rule: 'platforms-root-shape' },
118-
{ file: 'src/platforms/linux/snapshot.ts', rule: 'platforms-root-shape' },
119-
{ file: 'src/platforms/vega/interactor.ts', rule: 'platforms-root-shape' },
120-
{ file: 'src/platforms/web/provider.ts', rule: 'platforms-root-shape' },
116+
{ file: 'src/platforms/apple/core/apps.ts', rule: 'retired-platforms-zone' },
117+
{ file: 'src/platforms/harmonyos/app-lifecycle.ts', rule: 'retired-platforms-zone' },
118+
{ file: 'src/platforms/linux/snapshot.ts', rule: 'retired-platforms-zone' },
119+
{ file: 'src/platforms/vega/interactor.ts', rule: 'retired-platforms-zone' },
120+
{ file: 'src/platforms/web/provider.ts', rule: 'retired-platforms-zone' },
121121
],
122122
);
123123
});
@@ -602,23 +602,31 @@ test('Node resolves only each platform package root facade', () => {
602602
}
603603
});
604604

605-
test('the src/platforms root holds only the shared __tests__ directory', () => {
606-
const clean = ['src/platforms/__tests__/install-source.test.ts'];
607-
assert.deepEqual(checkPlatformsRootShape(clean), []);
605+
test('the retired src/platforms zone rejects every production, test, and fixture file', () => {
606+
const planted = [
607+
'src/platforms/__tests__/install-source.test.ts',
608+
'src/platforms/__fixtures__/snapshot.json',
609+
'src/platforms/helper.mjs',
610+
];
611+
const found = checkRetiredPlatformsZone(planted);
612+
assert.deepEqual(
613+
found.map(({ file, rule }) => ({ file, rule })),
614+
planted.map((file) => ({ file, rule: 'retired-platforms-zone' })),
615+
);
608616
});
609617

610618
test('a moved Android family cannot leave production or test files under the old root', () => {
611619
const planted = [
612620
'src/platforms/android/adb.ts',
613621
'src/platforms/android/__tests__/snapshot.test.ts',
614622
];
615-
const found = checkPlatformsRootShape(planted);
623+
const found = checkRetiredPlatformsZone(planted);
616624
assert.deepEqual(
617625
found.map(({ file, message }) => ({ file, message })),
618626
planted.map((file) => ({
619627
file,
620628
message:
621-
'the Android family has moved to packages/platform-android; remove the superseded src/platforms/android path',
629+
'src/platforms is retired; family code belongs in its platform package, shared mechanics in an owning substrate package, and cross-family tests in their root or package test owner',
622630
})),
623631
);
624632
});
@@ -629,14 +637,14 @@ test('a new direct production file or sibling directory under src/platforms fail
629637
'src/platforms/common/util.ts',
630638
'src/platforms/perf-utils.ts',
631639
];
632-
const found = checkPlatformsRootShape(planted);
640+
const found = checkRetiredPlatformsZone(planted);
633641
assert.deepEqual(
634642
found.map(({ file }) => file),
635643
planted,
636644
);
637645
for (const violation of found) {
638-
assert.equal(violation.rule, 'platforms-root-shape');
639-
assert.match(violation.message, /substrate package/);
646+
assert.equal(violation.rule, 'retired-platforms-zone');
647+
assert.match(violation.message, /src\/platforms is retired/);
640648
}
641649
});
642650

scripts/layering/platform-package-policy.ts

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,6 @@ export const CANONICAL_PLATFORM_FAMILIES = [
1111
'linux',
1212
'web',
1313
] as const;
14-
const RETIRED_PLATFORM_FAMILIES = [
15-
'apple',
16-
'android',
17-
'harmonyos',
18-
'linux',
19-
'vega',
20-
'web',
21-
] as const;
2214
type PlatformFamily = (typeof CANONICAL_PLATFORM_FAMILIES)[number];
2315
export type PlatformPackageDeclaration = {
2416
dir: string;
@@ -56,21 +48,15 @@ const PLATFORM_RUNTIME_HOST_FILES = new Set([
5648
// temporary exception).
5749
export const APPLE_RUNNER_SUBTREE = 'packages/platform-apple/src/runner/';
5850

59-
export function checkPlatformsRootShape(files: readonly string[]): LayeringViolation[] {
60-
const allowedChild = new RegExp(`^src/platforms/__tests__/`);
61-
const retiredFamily = new RegExp(`^src/platforms/(?:${RETIRED_PLATFORM_FAMILIES.join('|')})/`);
51+
export function checkRetiredPlatformsZone(files: readonly string[]): LayeringViolation[] {
6252
return files
63-
.filter(
64-
(file) =>
65-
file.startsWith('src/platforms/') && (retiredFamily.test(file) || !allowedChild.test(file)),
66-
)
53+
.filter((file) => file.startsWith('src/platforms/'))
6754
.map((file) => ({
68-
rule: 'platforms-root-shape',
55+
rule: 'retired-platforms-zone',
6956
file,
7057
line: 1,
71-
message: file.startsWith('src/platforms/android/')
72-
? 'the Android family has moved to packages/platform-android; remove the superseded src/platforms/android path'
73-
: 'src/platforms may hold only the shared __tests__ directory; retired family code belongs in its platform package and shared code belongs in a substrate package',
58+
message:
59+
'src/platforms is retired; family code belongs in its platform package, shared mechanics in an owning substrate package, and cross-family tests in their root or package test owner',
7460
}));
7561
}
7662
const APPLE_RUNNER_FACADE = '@agent-device/platform-apple/runner';

scripts/layering/tracked-sources.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ export function listTrackedTypeScriptFiles(repoRoot: string): string[] {
5353
return out.split('\n').filter(Boolean);
5454
}
5555

56+
/** Every tracked file under the retired platform root, regardless of extension. */
57+
export function listTrackedPlatformZoneFiles(repoRoot: string): string[] {
58+
const out = execFileSync('git', ['ls-files', '--', 'src/platforms'], {
59+
cwd: repoRoot,
60+
encoding: 'utf8',
61+
});
62+
return out.split('\n').filter(Boolean);
63+
}
64+
5665
/** Production sources only: test files and `__tests__/` trees are not layering subjects. */
5766
export function isProductionSourceFile(file: string): boolean {
5867
return file.endsWith('.ts') && !/(?:^|\/)__tests__\//.test(file) && !/\.test\.ts$/.test(file);

scripts/layering/zone-policy.test.ts

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ import assert from 'node:assert/strict';
22
import { test } from 'node:test';
33
import { policyLead, policyViolation, ZONE_POLICIES } from './zone-policy.ts';
44

5-
// R2-R3 (and the retired R1) were predicate functions with no unit test for eight months: the only thing
6-
// exercising them was the real tree, which is clean, so a rule that had silently stopped matching
7-
// would have looked exactly like a rule that was being obeyed. These tests assert each boundary
8-
// fires AND each documented exemption holds, so the table cannot quietly become decoration.
5+
// The folder policies originally had no unit test: the only thing exercising them was a clean real
6+
// tree, so a rule that silently stopped matching looked like one being obeyed. These tests keep the
7+
// remaining R2 policy from becoming decoration.
98

109
type Kind = { typeOnly?: boolean; dynamic?: boolean };
1110

@@ -28,8 +27,8 @@ function firing(e: ReturnType<typeof edge>): string[] {
2827
return ZONE_POLICIES.filter((policy) => policyViolation(policy, e) !== null).map((p) => p.rule);
2928
}
3029

31-
test('R2 commands-floor closes the four zones below the command surface, whatever the kind', () => {
32-
for (const zone of ['platforms', 'core', 'daemon']) {
30+
test('R2 commands-floor closes the remaining zones below the command surface, whatever the kind', () => {
31+
for (const zone of ['core', 'daemon']) {
3332
for (const kind of [{}, { typeOnly: true }, { dynamic: true }]) {
3433
assert.ok(
3534
firing(edge(`src/${zone}/thing.ts`, zone, 'commands', kind)).includes('R2 commands-floor'),
@@ -43,27 +42,11 @@ test('R2 commands-floor closes the four zones below the command surface, whateve
4342
assert.deepEqual(firing(edge('src/mcp/tools.ts', 'mcp', 'commands')), []);
4443
});
4544

46-
test('R3 platforms-seam closes static value imports and opens the two declared seam owners', () => {
47-
// Closed from an ordinary zone.
48-
assert.deepEqual(firing(edge('src/cli/run.ts', 'cli', 'platforms')), ['R3 platforms-seam']);
49-
50-
// Open to the seam owners.
51-
for (const file of ['src/core/interactors/android.ts', 'src/sdk/android-adb.ts']) {
52-
assert.deepEqual(firing(edge(file, 'core', 'platforms')), [], `${file} is a seam owner`);
53-
}
54-
55-
// The daemon has completed ADR 0019 and is no longer a platform seam. Neither its server nor
56-
// its process-boundary client may reach platform code directly.
57-
assert.deepEqual(firing(edge('src/daemon/handlers/perf.ts', 'daemon', 'platforms')), [
58-
'R3 platforms-seam',
59-
]);
60-
assert.deepEqual(firing(edge('src/daemon/client/daemon-client.ts', 'daemon', 'platforms')), [
61-
'R3 platforms-seam',
62-
]);
63-
64-
// Both tolerated kinds are the documented escape hatch that preserves CLI cold-start.
65-
assert.deepEqual(firing(edge('src/cli/run.ts', 'cli', 'platforms', { dynamic: true })), []);
66-
assert.deepEqual(firing(edge('src/cli/run.ts', 'cli', 'platforms', { typeOnly: true })), []);
45+
test('the retired R3 platforms seam has no zone-policy declaration', () => {
46+
assert.equal(
47+
ZONE_POLICIES.some(({ rule }) => rule === 'R3 platforms-seam'),
48+
false,
49+
);
6750
});
6851

6952
test('every policy carries a hint, and the lead names the kind it rejected', () => {
@@ -72,17 +55,14 @@ test('every policy carries a hint, and the lead names the kind it rejected', ()
7255
assert.ok(policy.hint.length > 20, `${policy.rule} needs an actionable hint`);
7356
}
7457

75-
assert.match(
76-
policyLead(edge('a', 'platforms', 'commands')),
77-
/platforms\/ must not import commands\//,
78-
);
58+
assert.match(policyLead(edge('a', 'core', 'commands')), /core\/ must not import commands\//);
7959
assert.match(
8060
policyLead(edge('a', 'core', 'commands', { typeOnly: true })),
8161
/must not type-only import commands\//,
8262
);
8363
assert.match(
84-
policyLead(edge('a', 'cli', 'platforms', { dynamic: true })),
85-
/must not dynamic import platforms\//,
64+
policyLead(edge('a', 'core', 'commands', { dynamic: true })),
65+
/must not dynamic import commands\//,
8666
);
8767
});
8868

0 commit comments

Comments
 (0)