Skip to content

Commit ab20d5c

Browse files
authored
refactor: retire platforms source seam (#2119)
1 parent d967730 commit ab20d5c

17 files changed

Lines changed: 134 additions & 170 deletions

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

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,19 @@ 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

24-
- Daemon device-execution code depends on platform-neutral contracts. Concrete device mechanics
25-
live in private `@agent-device/platform-*` packages and are value-imported only by the root
26-
composition module.
30+
- Daemon device-execution code depends on platform-neutral contracts. Concrete device mechanics for
31+
the six canonical families live in private `@agent-device/platform-*` packages; each family owns
32+
its implementation and family-specific tests, while shared install-source tests are root-owned
33+
under `src/__tests__/`. The root composition module and R13-governed named consumer facades are
34+
the only production static value-import sites.
2735
- The platform registry is **metadata-eager and implementation-lazy**. Cheap family identity,
2836
inventory entrypoints, and static fact declarations may load at composition time; platform
2937
mechanics and process-lived helper managers load only when discovery or the first binding for that
@@ -105,9 +113,11 @@ provider resolver table and wrapper ordering; only the canonical root may load i
105113
lazy until a request enters a provider scope. Daemon device-execution modules import the canonical
106114
root interface or runtime contracts only.
107115
Shared runtime interfaces and neutral data types live in `@agent-device/contracts`. In production,
108-
only that composition module or its one R13-governed private implementation submodule may import a
109-
concrete platform package; reusable types do not leak through type-only platform imports. Platform
110-
packages may import contracts, kernel/domain packages,
116+
only that composition module, its one R13-governed private implementation submodule, or the
117+
R13-governed consumer seams under `src/core/interactors/` may statically import concrete platform
118+
package roots; approved runtime hosts use deferred or type-only root imports, and named Apple
119+
facades expose only their governed domain seam. Reusable types do not leak through type-only
120+
platform imports. Platform packages may import contracts, kernel/domain packages,
111121
and explicitly injected host capabilities; they may not import daemon requests or responses, mutable
112122
session state, command catalogs/grammar, root implementation files, sibling platform packages, or raw
113123
process primitives outside the shared host-command port. R13 applies these rules to static, type-only,
@@ -167,22 +177,27 @@ selection, R11/R13 package enumeration, and the composite typecheck project list
167177
>
168178
> Enforcement: each substrate package's exported subpaths are pinned in
169179
> `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`.
180+
> planted red, and the `retired-platforms-zone` rule rejects every production, test, or fixture
181+
> file under the former `src/platforms` path.
172182
173183
The Apple XCUITest runner client is a durable platform-owned implementation facet colocated
174184
inside `packages/platform-apple` as the `src/runner/` subtree (#2040) — Apple mechanics belong to
175-
the Apple package. R13 models the facet by enumeration rather than by exception sprawl: the family
176-
exports its root façade plus exactly the `./runner`, `./runner/client`, and `./runner/test-host`
177-
subpaths; the `./runner` façade subpath is the seam through which daemon and root consumers reach
178-
runner mechanics directly today; the host-bound `./runner/client` factory has one composition root
179-
and `./runner/test-host` one vitest installer; the facet owns its cache files and usbmux sockets
180-
(the ambient-host rule exempts exactly that subtree), while raw process primitives stay banned —
181-
host authority still enters through one focused injected port (`AppleRunnerHost`: process
182-
execution, diagnostics, retry, probes, locks, foreground Apple tooling, physical-device control)
183-
constructed by exactly one composition root. No current issue owns migrating the runner's direct
184-
consumers behind the composition gateway; if such a migration retires them, the `./runner` seam
185-
narrows with it, but the facet itself is the intended ownership model, not a temporary exception.
185+
the Apple package. R13 models the package by enumeration rather than by exception sprawl: the family
186+
exports its root façade plus fourteen named domain/mechanics facades — `./app-lifecycle`,
187+
`./app-resolution`, `./debug-symbols`, `./doctor`, `./interactions`, `./install-artifact`, `./macos`,
188+
`./perf`, `./physical-device`, `./runner-owner`, `./runner/operations`, `./simctl`, `./simulator`, and
189+
`./tool-provider` — as well as exactly the `./runner`, `./runner/client`, and `./runner/test-host`
190+
subpaths. The named facades replace root-only access for synchronous domain consumers without a
191+
broad compatibility barrel; R13 pins the exact export set and allowed consumer seams. The
192+
`./runner` façade subpath is the seam through which daemon and root consumers reach runner mechanics
193+
directly today; the host-bound `./runner/client` factory has one composition root and
194+
`./runner/test-host` one vitest installer; the facet owns its cache files and usbmux sockets (the
195+
ambient-host rule exempts exactly that subtree), while raw process primitives stay banned — host
196+
authority still enters through one focused injected port (`AppleRunnerHost`: process execution,
197+
diagnostics, retry, probes, locks, foreground Apple tooling, physical-device control) constructed by
198+
exactly one composition root. No current issue owns migrating the runner's direct consumers behind
199+
the composition gateway; if such a migration retires them, the `./runner` seam narrows with it, but
200+
the facet itself is the intended ownership model, not a temporary exception.
186201
Mechanics-facet declarations are explicit per family: the Apple runner and Android mechanics/host
187202
facets are enumerated above, and a new family adds its own named facet only with an owning consumer
188203
and evidence.
@@ -770,11 +785,12 @@ belongs to its domain: test-IME restoration is durable device state with marker-
770785
helper stops follow the owning platform module's lifecycle policy, and close-time cleanup consumes
771786
neutral owner services.
772787

773-
R65 is the end-state enforcement: its planted-red AST tests reject every dependency edge — static,
788+
R65 is the daemon-side end-state enforcement: its planted-red AST tests reject every dependency edge — static,
774789
dynamic, re-export, and type-only — from production `src/daemon/**` modules (test files excluded,
775790
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.
791+
`@agent-device/platform-*` packages. R13 governs concrete package imports across the whole tree,
792+
while `retired-platforms-zone` prevents the old root seam from being recreated; platform freedom is
793+
therefore structurally enforced rather than periodically measured.
778794

779795
## Relationship to prior decisions
780796

‎docs/dependency-graph-findings.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,16 @@ declarative syntax gains, and `ZONE_POLICIES` gets that syntax anyway:
443443
Worth re-evaluating if the monorepo migration happens — per-package ESLint configs change the
444444
calculus — or once `jsPlugins` is stable and the ratchet gap is addressable.
445445

446+
## Terminal current-state note
447+
448+
The measurements and R3 experiment above are historical audit evidence, not the current layering
449+
contract. After #2082, `src/platforms/` is retired: family implementations and family-owned tests
450+
live in their workspace packages, while the shared install-source tests live under
451+
`src/__tests__/`. Package-level R13 owns platform exports and consumer seams, R65 owns the daemon's
452+
complete concrete-platform ban, and `retired-platforms-zone` rejects every tracked file under the
453+
old path. Legacy `src/platforms` spellings remain only in deliberate negative fixtures and
454+
implementation-pattern checks so reintroduction fails closed.
455+
446456
## Suggested order from here
447457

448458
1. ~~**Move the 10 outward-facing `daemon/types.ts` types into `contracts/`** (§2).~~ Mostly

‎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.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ test('production source selects static/build gates and delegates tests to Vitest
3939
}
4040
});
4141

42-
test('platform source additionally selects provider-integration', () => {
42+
test('platform package source additionally selects provider-integration', () => {
4343
const result = ids(['packages/platform-apple/src/core/app-resolution.ts']);
4444
assert.ok(result.includes('provider-integration'));
4545
assert.ok(result.includes('coverage'));

‎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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ test('classifyZone separates the ranked spine from intentionally-unranked zones'
182182
assert.equal(classifyZone('daemon-server'), 'ranked');
183183
assert.equal(classifyZone('(root)'), 'unranked');
184184
assert.equal(classifyZone('platform-runtime'), 'unranked');
185+
assert.equal(classifyZone('platforms'), 'unclassified');
185186
assert.equal(classifyZone('utils'), 'ranked');
186187
// Every satellite zone joined the spine; only the composition root stays out, because R2
187188
// forbids daemon/ from importing commands/ so the files that wire them cannot be ranked.
@@ -416,7 +417,7 @@ test('largestTypeCycleSize counts type-only cycles and ignores dynamic ones', ()
416417
]);
417418

418419
// 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
420+
// comprehension barrier. With no non-dynamic edge at
420421
// all no file enters the walk, so the floor here is 0 rather than 1 — specified, not incidental.
421422
const dynamicCycle = resolveImportEdges(
422423
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

0 commit comments

Comments
 (0)