refactor: retire platforms source seam (#2119)

This commit is contained in:
Michał Pierzchała
2026-08-29 13:10:47 +02:00
committed by GitHub
parent d9677301f4
commit ab20d5c2af
17 changed files with 134 additions and 170 deletions
+38 -22
View File
@@ -19,11 +19,19 @@ R65. The per-command cutover table was retired after completion as required by s
enforces the permanent facts-only admission and runtime-proof invariants without naming historical
routes or handler functions.
The #2082 extraction completes the physical ownership boundary: all six platform-family
implementations and their family-owned tests live behind private package exports, `src/platforms`
is retired, and the former R3 folder seam is gone. R13 now owns concrete platform-package import
direction and implementation laziness; the `retired-platforms-zone` gate rejects any attempt to
recreate the old root path.
## Rules at a glance
- Daemon device-execution code depends on platform-neutral contracts. Concrete device mechanics
live in private `@agent-device/platform-*` packages and are value-imported only by the root
composition module.
- Daemon device-execution code depends on platform-neutral contracts. Concrete device mechanics for
the six canonical families live in private `@agent-device/platform-*` packages; each family owns
its implementation and family-specific tests, while shared install-source tests are root-owned
under `src/__tests__/`. The root composition module and R13-governed named consumer facades are
the only production static value-import sites.
- The platform registry is **metadata-eager and implementation-lazy**. Cheap family identity,
inventory entrypoints, and static fact declarations may load at composition time; platform
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
lazy until a request enters a provider scope. Daemon device-execution modules import the canonical
root interface or runtime contracts only.
Shared runtime interfaces and neutral data types live in `@agent-device/contracts`. In production,
only that composition module or its one R13-governed private implementation submodule may import a
concrete platform package; reusable types do not leak through type-only platform imports. Platform
packages may import contracts, kernel/domain packages,
only that composition module, its one R13-governed private implementation submodule, or the
R13-governed consumer seams under `src/core/interactors/` may statically import concrete platform
package roots; approved runtime hosts use deferred or type-only root imports, and named Apple
facades expose only their governed domain seam. Reusable types do not leak through type-only
platform imports. Platform packages may import contracts, kernel/domain packages,
and explicitly injected host capabilities; they may not import daemon requests or responses, mutable
session state, command catalogs/grammar, root implementation files, sibling platform packages, or raw
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
>
> Enforcement: each substrate package's exported subpaths are pinned in
> `package-boundaries.test.ts` (widening fails the gate), the contracts mechanics gate stays
> planted red, and the `platforms-root-shape` rule rejects any new shared file or directory
> appearing directly under `src/platforms`.
> planted red, and the `retired-platforms-zone` rule rejects every production, test, or fixture
> file under the former `src/platforms` path.
The Apple XCUITest runner client is a durable platform-owned implementation facet colocated
inside `packages/platform-apple` as the `src/runner/` subtree (#2040) — Apple mechanics belong to
the Apple package. R13 models the facet by enumeration rather than by exception sprawl: the family
exports its root façade plus exactly the `./runner`, `./runner/client`, and `./runner/test-host`
subpaths; the `./runner` façade subpath is the seam through which daemon and root consumers reach
runner mechanics directly today; the host-bound `./runner/client` factory has one composition root
and `./runner/test-host` one vitest installer; the facet owns its cache files and usbmux sockets
(the ambient-host rule exempts exactly that subtree), while raw process primitives stay banned —
host authority still enters through one focused injected port (`AppleRunnerHost`: process
execution, diagnostics, retry, probes, locks, foreground Apple tooling, physical-device control)
constructed by exactly one composition root. No current issue owns migrating the runner's direct
consumers behind the composition gateway; if such a migration retires them, the `./runner` seam
narrows with it, but the facet itself is the intended ownership model, not a temporary exception.
the Apple package. R13 models the package by enumeration rather than by exception sprawl: the family
exports its root façade plus fourteen named domain/mechanics facades — `./app-lifecycle`,
`./app-resolution`, `./debug-symbols`, `./doctor`, `./interactions`, `./install-artifact`, `./macos`,
`./perf`, `./physical-device`, `./runner-owner`, `./runner/operations`, `./simctl`, `./simulator`, and
`./tool-provider` — as well as exactly the `./runner`, `./runner/client`, and `./runner/test-host`
subpaths. The named facades replace root-only access for synchronous domain consumers without a
broad compatibility barrel; R13 pins the exact export set and allowed consumer seams. The
`./runner` façade subpath is the seam through which daemon and root consumers reach runner mechanics
directly today; the host-bound `./runner/client` factory has one composition root and
`./runner/test-host` one vitest installer; the facet owns its cache files and usbmux sockets (the
ambient-host rule exempts exactly that subtree), while raw process primitives stay banned — host
authority still enters through one focused injected port (`AppleRunnerHost`: process execution,
diagnostics, retry, probes, locks, foreground Apple tooling, physical-device control) constructed by
exactly one composition root. No current issue owns migrating the runner's direct consumers behind
the composition gateway; if such a migration retires them, the `./runner` seam narrows with it, but
the facet itself is the intended ownership model, not a temporary exception.
Mechanics-facet declarations are explicit per family: the Apple runner and Android mechanics/host
facets are enumerated above, and a new family adds its own named facet only with an owning consumer
and evidence.
@@ -770,11 +785,12 @@ belongs to its domain: test-IME restoration is durable device state with marker-
helper stops follow the owning platform module's lifecycle policy, and close-time cleanup consumes
neutral owner services.
R65 is the end-state enforcement: its planted-red AST tests reject every dependency edge — static,
R65 is the daemon-side end-state enforcement: its planted-red AST tests reject every dependency edge — static,
dynamic, re-export, and type-only — from production `src/daemon/**` modules (test files excluded,
matching the layering scanner's scope) to `src/platforms/**` and concrete
`@agent-device/platform-*` packages. The daemon has been removed from the R3 seam, so platform
freedom is structurally enforced rather than periodically measured.
`@agent-device/platform-*` packages. R13 governs concrete package imports across the whole tree,
while `retired-platforms-zone` prevents the old root seam from being recreated; platform freedom is
therefore structurally enforced rather than periodically measured.
## Relationship to prior decisions
+10
View File
@@ -443,6 +443,16 @@ declarative syntax gains, and `ZONE_POLICIES` gets that syntax anyway:
Worth re-evaluating if the monorepo migration happens — per-package ESLint configs change the
calculus — or once `jsPlugins` is stable and the ratchet gap is addressable.
## Terminal current-state note
The measurements and R3 experiment above are historical audit evidence, not the current layering
contract. After #2082, `src/platforms/` is retired: family implementations and family-owned tests
live in their workspace packages, while the shared install-source tests live under
`src/__tests__/`. Package-level R13 owns platform exports and consumer seams, R65 owns the daemon's
complete concrete-platform ban, and `retired-platforms-zone` rejects every tracked file under the
old path. Legacy `src/platforms` spellings remain only in deliberate negative fixtures and
implementation-pattern checks so reintroduction fails closed.
## Suggested order from here
1. ~~**Move the 10 outward-facing `daemon/types.ts` types into `contracts/`** (§2).~~ Mostly
+1 -1
View File
@@ -75,7 +75,7 @@ test('shared runtime surface owns every device lane', () => {
'packages/kernel/src/errors.ts',
'src/daemon/android-system-dialog.ts', // naming convention in a shared dir, not a boundary
'test/integration/smoke-daemon-clean.test.ts',
'src/platforms/install-source.ts',
'packages/provision-kit/src/install-source.ts',
]) {
assert.equal(deviceLaneLeaf(file), 'shared', file);
assert.deepEqual(lanes(file), [
+1 -1
View File
@@ -39,7 +39,7 @@ test('production source selects static/build gates and delegates tests to Vitest
}
});
test('platform source additionally selects provider-integration', () => {
test('platform package source additionally selects provider-integration', () => {
const result = ids(['packages/platform-apple/src/core/app-resolution.ts']);
assert.ok(result.includes('provider-integration'));
assert.ok(result.includes('coverage'));
+1 -18
View File
@@ -259,27 +259,10 @@ const staticTsGates: OwnershipRule = ({ file, isTs, underSrc, underTest }) =>
const srcProdGate: OwnershipRule = ({ file, isSrcProd }) => {
if (!isSrcProd) return [];
const selections = [
return [
reason('layering', file, 'gate:layering', 'layering guard reads production src/ modules'),
reason('build', file, 'src-prod', 'production source is compiled by the build'),
];
if (file.startsWith('src/platforms/')) {
selections.push(
reason(
'provider-integration',
file,
'platform-src',
'platform source shapes device/provider wire behavior',
),
reason(
'coverage',
file,
'platform-src',
'Testing Matrix requires coverage for platform/device-response changes',
),
);
}
return selections;
};
function isNodeIntegrationPath(file: string): boolean {
+13 -10
View File
@@ -3,13 +3,13 @@
//
// Ranked target spine, as rank groups lowest to highest. `A ◄ B` means B may not
// be outranked by A (the back-edge order the gate rejects), NOT that every displayed import exists:
// { contracts, request, selectors, platforms } ◄ core ◄ { commands, cli-schema }
// { contracts, request, selectors } ◄ core ◄ { commands, cli-schema }
// ◄ { client, daemon-server } ◄ daemon-client ◄ cli
// (authoritative ranks: `TARGET_DAG_RANK` in model.ts. The former rank-0 kernel
// zone lives in packages/kernel since #1490 W0; R11 owns its boundary.)
//
// This gate enforces five things, across four scopes:
// - GLOBALLY, across every production source file: the R2-R3 move rules and
// - GLOBALLY, across every production source file: the remaining R2 move rule and
// rejection of all production static value-import cycles (R4). R1 kernel-sink
// retired with the kernel's move to packages/kernel (#1490 W0); R8
// zero-dep-job-closure retired with the last `install-deps: false` job
@@ -20,8 +20,8 @@
// same inversion measured over TYPE-ONLY edges (R6).
// - Over the DAEMON only: SessionState field ownership (R7), because the session
// record is store-owned mutable state that any daemon module can write; and the terminal
// concrete-platform boundary (R65), which rejects every import form into src/platforms or a
// platform package.
// concrete-platform boundary (R65), which rejects every import form into the retired
// src/platforms path or a platform package.
// - Over the TYPE GRAPH: the largest type-level import cycle is pinned by
// equality (R9). R4 keeps the value graph acyclic, so these cycles are free at
// runtime but bound what can be read in isolation; growth fails, and so does a
@@ -82,7 +82,7 @@ import {
} from './package-boundaries.ts';
import {
checkPlatformPackagePolicy,
checkPlatformsRootShape,
checkRetiredPlatformsZone,
platformPackagePolicySummary,
} from './platform-package-policy.ts';
import {
@@ -96,7 +96,11 @@ import { selectorPipelineOwnershipViolations } from './selector-pipeline-ownersh
import { recordRuntimeRegistryJoinViolations } from './record-runtime-registry-policy.ts';
import { recordRuntimeDaemonMechanicsViolations } from './record-runtime-mechanics-policy.ts';
import { checkDaemonPlatformBoundary } from './daemon-platform-boundary.ts';
import { listTrackedProductionSources, listTrackedTypeScriptFiles } from './tracked-sources.ts';
import {
listTrackedPlatformZoneFiles,
listTrackedProductionSources,
listTrackedTypeScriptFiles,
} from './tracked-sources.ts';
import { runtimeExecutionIntegrityViolations } from './runtime-execution-policy.ts';
import { sourceExecutionCompatibilityViolations } from './source-execution-policy.ts';
import { sessionResourceOwnershipViolations } from './session-resource-ownership.ts';
@@ -469,7 +473,7 @@ function report(
): number {
if (violations.length === 0) {
process.stdout.write(
`Layering guard: OK — ${files.length} source files satisfy R2-R3 and contain no ` +
`Layering guard: OK — ${files.length} source files satisfy R2 and contain no ` +
`value-import cycles (both checked globally); the ranked target spine contains no ` +
`back-edges (only the composition root is unranked among src zones), and its type-only ` +
`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 = [
'bin-alias-fast-path',
'package-boundaries',
'platform-package-policy',
'platforms-root-shape',
'retired-platforms-zone',
] as const;
export type LayeringRuleId = (typeof LAYERING_RULE_IDS)[number];
@@ -584,8 +588,7 @@ export const LAYERING_RULES: Readonly<Record<LayeringRuleId, LayeringRule>> = {
readTrackedPlatformPackageDeclarations(repoRoot),
{ untrackedProductionFiles: listUntrackedProductionTypeScriptFiles(repoRoot) },
),
'platforms-root-shape': (context) =>
checkPlatformsRootShape([...context.allTypeScriptSources.keys()]),
'retired-platforms-zone': () => checkRetiredPlatformsZone(listTrackedPlatformZoneFiles(repoRoot)),
};
export function main(): number {
+2 -1
View File
@@ -182,6 +182,7 @@ test('classifyZone separates the ranked spine from intentionally-unranked zones'
assert.equal(classifyZone('daemon-server'), 'ranked');
assert.equal(classifyZone('(root)'), 'unranked');
assert.equal(classifyZone('platform-runtime'), 'unranked');
assert.equal(classifyZone('platforms'), 'unclassified');
assert.equal(classifyZone('utils'), 'ranked');
// Every satellite zone joined the spine; only the composition root stays out, because R2
// 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', ()
]);
// A loop closed through a DYNAMIC import is excluded on purpose: a lazy seam is not a
// comprehension barrier, and R3 relies on dynamic imports existing. With no non-dynamic edge at
// comprehension barrier. With no non-dynamic edge at
// all no file enters the walk, so the floor here is 0 rather than 1 — specified, not incidental.
const dynamicCycle = resolveImportEdges(
new Map(
@@ -3,7 +3,7 @@ import { test } from 'node:test';
import {
CANONICAL_PLATFORM_FAMILIES,
checkPlatformPackagePolicy,
checkPlatformsRootShape,
checkRetiredPlatformsZone,
type PlatformPackageDeclaration,
} from './platform-package-policy.ts';
import { classifyZone } from './model.ts';
@@ -102,7 +102,7 @@ test('the inventory substrate has six private lazy packages and one exact compos
});
test('retired platform family implementations are rejected from src/platforms', () => {
const violations = checkPlatformsRootShape([
const violations = checkRetiredPlatformsZone([
'src/platforms/apple/core/apps.ts',
'src/platforms/harmonyos/app-lifecycle.ts',
'src/platforms/linux/snapshot.ts',
@@ -113,11 +113,11 @@ test('retired platform family implementations are rejected from src/platforms',
assert.deepEqual(
violations.map(({ file, rule }) => ({ file, rule })),
[
{ file: 'src/platforms/apple/core/apps.ts', rule: 'platforms-root-shape' },
{ file: 'src/platforms/harmonyos/app-lifecycle.ts', rule: 'platforms-root-shape' },
{ file: 'src/platforms/linux/snapshot.ts', rule: 'platforms-root-shape' },
{ file: 'src/platforms/vega/interactor.ts', rule: 'platforms-root-shape' },
{ file: 'src/platforms/web/provider.ts', rule: 'platforms-root-shape' },
{ file: 'src/platforms/apple/core/apps.ts', rule: 'retired-platforms-zone' },
{ file: 'src/platforms/harmonyos/app-lifecycle.ts', rule: 'retired-platforms-zone' },
{ file: 'src/platforms/linux/snapshot.ts', rule: 'retired-platforms-zone' },
{ file: 'src/platforms/vega/interactor.ts', rule: 'retired-platforms-zone' },
{ file: 'src/platforms/web/provider.ts', rule: 'retired-platforms-zone' },
],
);
});
@@ -602,9 +602,17 @@ test('Node resolves only each platform package root facade', () => {
}
});
test('the src/platforms root holds only the shared __tests__ directory', () => {
const clean = ['src/platforms/__tests__/install-source.test.ts'];
assert.deepEqual(checkPlatformsRootShape(clean), []);
test('the retired src/platforms zone rejects every production, test, and fixture file', () => {
const planted = [
'src/platforms/__tests__/install-source.test.ts',
'src/platforms/__fixtures__/snapshot.json',
'src/platforms/helper.mjs',
];
const found = checkRetiredPlatformsZone(planted);
assert.deepEqual(
found.map(({ file, rule }) => ({ file, rule })),
planted.map((file) => ({ file, rule: 'retired-platforms-zone' })),
);
});
test('a moved Android family cannot leave production or test files under the old root', () => {
@@ -612,13 +620,13 @@ test('a moved Android family cannot leave production or test files under the old
'src/platforms/android/adb.ts',
'src/platforms/android/__tests__/snapshot.test.ts',
];
const found = checkPlatformsRootShape(planted);
const found = checkRetiredPlatformsZone(planted);
assert.deepEqual(
found.map(({ file, message }) => ({ file, message })),
planted.map((file) => ({
file,
message:
'the Android family has moved to packages/platform-android; remove the superseded src/platforms/android path',
'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',
})),
);
});
@@ -629,14 +637,14 @@ test('a new direct production file or sibling directory under src/platforms fail
'src/platforms/common/util.ts',
'src/platforms/perf-utils.ts',
];
const found = checkPlatformsRootShape(planted);
const found = checkRetiredPlatformsZone(planted);
assert.deepEqual(
found.map(({ file }) => file),
planted,
);
for (const violation of found) {
assert.equal(violation.rule, 'platforms-root-shape');
assert.match(violation.message, /substrate package/);
assert.equal(violation.rule, 'retired-platforms-zone');
assert.match(violation.message, /src\/platforms is retired/);
}
});
+5 -19
View File
@@ -11,14 +11,6 @@ export const CANONICAL_PLATFORM_FAMILIES = [
'linux',
'web',
] as const;
const RETIRED_PLATFORM_FAMILIES = [
'apple',
'android',
'harmonyos',
'linux',
'vega',
'web',
] as const;
type PlatformFamily = (typeof CANONICAL_PLATFORM_FAMILIES)[number];
export type PlatformPackageDeclaration = {
dir: string;
@@ -56,21 +48,15 @@ const PLATFORM_RUNTIME_HOST_FILES = new Set([
// temporary exception).
export const APPLE_RUNNER_SUBTREE = 'packages/platform-apple/src/runner/';
export function checkPlatformsRootShape(files: readonly string[]): LayeringViolation[] {
const allowedChild = new RegExp(`^src/platforms/__tests__/`);
const retiredFamily = new RegExp(`^src/platforms/(?:${RETIRED_PLATFORM_FAMILIES.join('|')})/`);
export function checkRetiredPlatformsZone(files: readonly string[]): LayeringViolation[] {
return files
.filter(
(file) =>
file.startsWith('src/platforms/') && (retiredFamily.test(file) || !allowedChild.test(file)),
)
.filter((file) => file.startsWith('src/platforms/'))
.map((file) => ({
rule: 'platforms-root-shape',
rule: 'retired-platforms-zone',
file,
line: 1,
message: file.startsWith('src/platforms/android/')
? 'the Android family has moved to packages/platform-android; remove the superseded src/platforms/android path'
: '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',
message:
'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',
}));
}
const APPLE_RUNNER_FACADE = '@agent-device/platform-apple/runner';
+12 -1
View File
@@ -1,4 +1,6 @@
// The file list every layering scan reads: TRACKED production TypeScript, nothing else.
// Layering scans read tracked repository paths only. Most rules consume production TypeScript;
// the retired-platform zone also consumes every tracked path under its former root so non-TS
// fixtures cannot bypass the ownership boundary.
//
// A leaf module on purpose. `check.ts` owns the scan and imports `package-boundaries.ts`, so the
// boundary rules cannot import `check.ts` back for its file list; without a shared leaf the two
@@ -53,6 +55,15 @@ export function listTrackedTypeScriptFiles(repoRoot: string): string[] {
return out.split('\n').filter(Boolean);
}
/** Every tracked file under the retired platform root, regardless of extension. */
export function listTrackedPlatformZoneFiles(repoRoot: string): string[] {
const out = execFileSync('git', ['ls-files', '--', 'src/platforms'], {
cwd: repoRoot,
encoding: 'utf8',
});
return out.split('\n').filter(Boolean);
}
/** Production sources only: test files and `__tests__/` trees are not layering subjects. */
export function isProductionSourceFile(file: string): boolean {
return file.endsWith('.ts') && !/(?:^|\/)__tests__\//.test(file) && !/\.test\.ts$/.test(file);
+13 -33
View File
@@ -2,10 +2,9 @@ import assert from 'node:assert/strict';
import { test } from 'node:test';
import { policyLead, policyViolation, ZONE_POLICIES } from './zone-policy.ts';
// R2-R3 (and the retired R1) were predicate functions with no unit test for eight months: the only thing
// exercising them was the real tree, which is clean, so a rule that had silently stopped matching
// would have looked exactly like a rule that was being obeyed. These tests assert each boundary
// fires AND each documented exemption holds, so the table cannot quietly become decoration.
// The folder policies originally had no unit test: the only thing exercising them was a clean real
// tree, so a rule that silently stopped matching looked like one being obeyed. These tests keep the
// remaining R2 policy from becoming decoration.
type Kind = { typeOnly?: boolean; dynamic?: boolean };
@@ -28,8 +27,8 @@ function firing(e: ReturnType<typeof edge>): string[] {
return ZONE_POLICIES.filter((policy) => policyViolation(policy, e) !== null).map((p) => p.rule);
}
test('R2 commands-floor closes the four zones below the command surface, whatever the kind', () => {
for (const zone of ['platforms', 'core', 'daemon']) {
test('R2 commands-floor closes the remaining zones below the command surface, whatever the kind', () => {
for (const zone of ['core', 'daemon']) {
for (const kind of [{}, { typeOnly: true }, { dynamic: true }]) {
assert.ok(
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
assert.deepEqual(firing(edge('src/mcp/tools.ts', 'mcp', 'commands')), []);
});
test('R3 platforms-seam closes static value imports and opens the two declared seam owners', () => {
// Closed from an ordinary zone.
assert.deepEqual(firing(edge('src/cli/run.ts', 'cli', 'platforms')), ['R3 platforms-seam']);
// Open to the seam owners.
for (const file of ['src/core/interactors/android.ts', 'src/sdk/android-adb.ts']) {
assert.deepEqual(firing(edge(file, 'core', 'platforms')), [], `${file} is a seam owner`);
}
// The daemon has completed ADR 0019 and is no longer a platform seam. Neither its server nor
// its process-boundary client may reach platform code directly.
assert.deepEqual(firing(edge('src/daemon/handlers/perf.ts', 'daemon', 'platforms')), [
'R3 platforms-seam',
]);
assert.deepEqual(firing(edge('src/daemon/client/daemon-client.ts', 'daemon', 'platforms')), [
'R3 platforms-seam',
]);
// Both tolerated kinds are the documented escape hatch that preserves CLI cold-start.
assert.deepEqual(firing(edge('src/cli/run.ts', 'cli', 'platforms', { dynamic: true })), []);
assert.deepEqual(firing(edge('src/cli/run.ts', 'cli', 'platforms', { typeOnly: true })), []);
test('the retired R3 platforms seam has no zone-policy declaration', () => {
assert.equal(
ZONE_POLICIES.some(({ rule }) => rule === 'R3 platforms-seam'),
false,
);
});
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', ()
assert.ok(policy.hint.length > 20, `${policy.rule} needs an actionable hint`);
}
assert.match(
policyLead(edge('a', 'platforms', 'commands')),
/platforms\/ must not import commands\//,
);
assert.match(policyLead(edge('a', 'core', 'commands')), /core\/ must not import commands\//);
assert.match(
policyLead(edge('a', 'core', 'commands', { typeOnly: true })),
/must not type-only import commands\//,
);
assert.match(
policyLead(edge('a', 'cli', 'platforms', { dynamic: true })),
/must not dynamic import platforms\//,
policyLead(edge('a', 'core', 'commands', { dynamic: true })),
/must not dynamic import commands\//,
);
});
+5 -39
View File
@@ -1,13 +1,10 @@
import type { ImportEdge } from './model.ts';
/**
* R1-R3 as data: which zone may import which, and on what terms.
* R2 as data: which zone may import which.
*
* These three rules used to be three hand-written predicate functions. They were short, but each
* one buried its boundary in control flow you had to read the early-returns to learn that R3
* tolerates dynamic imports, or that R1 opens exactly one door. Stating them as a table means the
* policy is readable without reading code, and a fourth boundary is a table entry rather than a
* fourth function to keep consistent with the other three.
* The original folder policies were hand-written predicate functions. Stating the remaining rule
* as data keeps its scope readable without following control flow.
*
* The evaluator is deliberately small. Everything a policy can say is in `ZonePolicy`, so a rule
* that needs more than these fields does NOT belong here: R4 (cycles), R5/R6 (spine ranking),
@@ -15,9 +12,6 @@ import type { ImportEdge } from './model.ts';
* keeps its own checker.
*/
/** Edge kinds a boundary can tolerate. Both are erased or deferred, so both can be legitimate. */
export type ToleratedKind = 'type-only' | 'dynamic';
export type ZonePolicy = {
/** Rule id reported on violation, e.g. `R1 kernel-sink`. */
rule: string;
@@ -27,15 +21,6 @@ export type ZonePolicy = {
to?: readonly string[];
/** Target zones this policy does NOT govern — the complement form of `to`. */
exceptTo?: readonly string[];
/**
* Edge kinds that do not violate this boundary. A type-only edge costs nothing at runtime; a
* dynamic edge defers the cost past startup. Omitted means the boundary is closed to every kind.
*/
tolerates?: readonly ToleratedKind[];
/** Path prefixes allowed to cross regardless: the declared seam. */
seam?: readonly string[];
/** Subtrees of `seam` that are NOT part of it (a seam owner with a non-owning child). */
seamExcept?: readonly string[];
/** Why the boundary exists and what to do instead. ADR 0010: every error carries a hint. */
hint: string;
};
@@ -50,35 +35,20 @@ export type ZonePolicy = {
export const ZONE_POLICIES: readonly ZonePolicy[] = [
{
rule: 'R2 commands-floor',
from: ['platforms', 'core', 'daemon'],
from: ['core', 'daemon'],
to: ['commands'],
hint:
'commands/ is the command surface, above these zones. Depend on shared kernel/contracts ' +
'instead; if two zones need the same rule, put the rule below both of them.',
},
{
rule: 'R3 platforms-seam',
to: ['platforms'],
tolerates: ['type-only', 'dynamic'],
seam: ['src/core/interactors/', 'src/sdk/'],
hint:
'Only src/core/interactors/ and the sdk barrel may statically import ' +
'platforms/; elsewhere use a dynamic import() or a type-only import to preserve CLI ' +
'cold-start.',
},
];
function kindOf(imp: ImportEdge): ToleratedKind | 'value' {
function kindOf(imp: ImportEdge): 'type-only' | 'dynamic' | 'value' {
if (imp.dynamic) return 'dynamic';
if (imp.typeOnly) return 'type-only';
return 'value';
}
function crossesSeam(policy: ZonePolicy, file: string): boolean {
if (!policy.seam?.some((prefix) => file.startsWith(prefix))) return false;
return !policy.seamExcept?.some((prefix) => file.startsWith(prefix));
}
/**
* Whether `policy` governs this edge AND the edge violates it. Returns the hint on violation so
* the caller can build the message, or `null` when the policy is silent about this edge.
@@ -91,10 +61,6 @@ export function policyViolation(
if (policy.to && !policy.to.includes(edge.toZone)) return null;
if (policy.exceptTo?.includes(edge.toZone)) return null;
const kind = kindOf(edge.imp);
if (kind !== 'value' && policy.tolerates?.includes(kind)) return null;
if (crossesSeam(policy, edge.file)) return null;
return policy.hint;
}
+1 -1
View File
@@ -28,7 +28,7 @@ import {
*
* - Catches: an entry surface or vocabulary module silently going eager -- the regression class
* #1950 fixed once and #1959/#1969 fixed at five more sites. Nothing else prevents the next
* instance: layering R3/R13 govern import DIRECTION (may this file reach that one at all),
* instance: layering R13 governs import DIRECTION (may this file reach that one at all),
* never evaluation WEIGHT (how much of the repo an importer drags along).
* - Evidence: planted red re-verified against this gate itself, not merely cited from #1950 --
* see the PR description. Every rule the real-tree assertions rest on (the equality ratchet,
+5 -5
View File
@@ -14,7 +14,7 @@
// imports are `import type` (erased), and every implementation loads through a function-scoped
// `await import`. That is precisely ADR-0019's "metadata-eager and implementation-lazy" property,
// and a single static value import would silently destroy it while every other gate stayed green:
// R3/R13 govern import DIRECTION (may this file reach that one at all), never evaluation WEIGHT.
// R13 governs import DIRECTION (may this file reach that one at all), never evaluation WEIGHT.
//
// Entry files are repo-root-relative, and are the KEYS of the two records below. Keying by path
// is what makes a duplicate row unwritable rather than merely discouraged: a repeated key in an
@@ -68,15 +68,15 @@ export type EagerClosureBudget = {
* domain/mechanics facet entries (see the exceptions below), whose closure IS the implementation
* intentionally exposed through that subpath.
* Hub rows set it false -- a hub is a CONSUMER of façades, not neutral vocabulary, and three of
* them legitimately hold the R3-permitted static platform seam that has not migrated yet.
* them legitimately hold R13-permitted static platform-package seams.
*/
denyPlatformImplementations: boolean;
};
/**
* A concrete platform implementation: the legacy daemon-owned `src/platforms/<family>/` tree, or
* a private `@agent-device/platform-<family>` workspace package. ADR-0019 names both as "concrete
* device mechanics" that platform-neutral code reaches only through contracts.
* A concrete platform implementation lives in a private `@agent-device/platform-<family>`
* workspace package. The retired `src/platforms/<family>/` spelling remains matched so a
* reintroduced legacy path cannot launder an eager edge past this guard.
*/
export const PLATFORM_IMPLEMENTATION_PATTERNS: RegExp[] = [
/[/\\]platforms[/\\](apple|android|harmonyos|vega|linux|web)[/\\]/,
@@ -1,7 +1,7 @@
import { PUBLIC_COMMANDS as C } from '../../command-catalog.ts';
import { PUBLIC_COMMANDS as C } from '../command-catalog.ts';
export const ANDROID_INSTALL_SOURCE_CONTRACT_EVIDENCE = {
commands: [C.installFromSource],
owner: 'platforms/install-source',
owner: 'provision-kit/install-source',
testName: 'prepareAndroidInstallArtifact resolves package identity for direct APK URL sources',
} as const;
@@ -24,7 +24,7 @@ import {
withAppleToolProvider,
} from '@agent-device/platform-apple';
import { ANDROID_INSTALL_SOURCE_CONTRACT_EVIDENCE } from './install-source.coverage.ts';
import { mkdtempForTest } from '../../__tests__/test-utils/tmp-dir.ts';
import { mkdtempForTest } from './test-utils/tmp-dir.ts';
import * as networkTransport from '@agent-device/provision-kit/install-source-network-transport';
test('validateDownloadSourceUrl rejects localhost and private literal addresses by default', async () => {
@@ -1,6 +1,6 @@
import { PUBLIC_COMMANDS } from '../../../src/command-catalog.ts';
import { ANDROID_AUDIO_CONTRACT_EVIDENCE } from '../../../src/daemon/handlers/__tests__/session-audio.coverage.ts';
import { ANDROID_INSTALL_SOURCE_CONTRACT_EVIDENCE } from '../../../src/platforms/__tests__/install-source.coverage.ts';
import { ANDROID_INSTALL_SOURCE_CONTRACT_EVIDENCE } from '../../../src/__tests__/install-source.coverage.ts';
import { ANDROID_LIFECYCLE_CONTRACT_EVIDENCE } from '../provider-scenarios/android-lifecycle.coverage.ts';
import { buildCoverageClassificationSummary } from '../support/coverage-classification.ts';
import {