Files
callstack__agent-device/scripts/layering/contracts-implementation-policy.ts
Michał Pierzchała 1f9d940bff refactor(capture-kit): complete ADR 0019 end state — relocate snapshot and recording zones (#2385)
* refactor(capture-kit): relocate snapshot and recording zones into capture-kit

Move the ADR 0019 end-state capture zones into @agent-device/capture-kit:

- src/snapshot/** -> packages/capture-kit/src/snapshot/** (presentation,
  freshness, scroll-edge-state, ios-snapshot-runtime, android occlusion)
- src/recording/** -> packages/capture-kit/src/recording/**
- src/core/snapshot-{chrome,state,tree-ingestion,node-lookup}.ts ->
  packages/capture-kit/src/
- src/snapshot-quality/ test -> capture-kit presentation tree (directory
  retires with its last file)

Pure renames: import re-pointing and gate updates follow in the next commit.
The snapshot-desktop-surface test parks in src/__tests__/ because it pins
the root eager-import-closure walker.

* refactor(capture-kit): re-point capture and recording consumers to the new subpaths

Rewires every consumer of the relocated snapshot/recording modules to the new @agent-device/capture-kit subpath exports, adds the 23 subpath entries to the capture-kit exports map, fixes the moved recording-scripts test's __dirname-relative paths for the deeper location, and records the completed migration in ADR 0019's end state.

* chore(gates): align layering, mutation, fallow and CI gates with the capture-kit relocation

Moves the executable-policy roots, presentation-owner constant, zone ranks, authority fixture, mutation sharding globs, stryker aliases, fallow baselines and the iOS workflow's android-owned paths-ignore entry onto the new packages/capture-kit paths, and extends the planted-red coverage to the new presentation-owner subpath.

* chore: point capture-domain source-of-truth comments at the relocated capture-kit modules

* test: point shutdown recording mock at capture-kit and cover interactor acquisition presentation

* test(capture-kit): update upstream presentation test imports

* chore(gates): follow relocated snapshot assembly in R74

* test(daemon): freeze prewarm deadline assertion clocks
2026-09-08 12:41:39 +02:00

221 lines
8.9 KiB
TypeScript

// Catches: packages/contracts production source calling host, process, or timer mechanics
// directly — contracts owns vocabulary only, and a mechanic call there means an adapter's
// concern leaked into the shared-vocabulary package every zone imports, invisible to
// consumers because the call itself is fully typed and legal Node code.
// Evidence: 8f98d23f14 (#1750) gave R18 its own number after an id collision; 057ab1c82d (#1746)
// fixed double-reporting in this same authority check.
// Cost: 337 LOC (206 rule + 131 test).
// Kill criterion: none enforced today; retire only by maintainer decision that contracts owning
// vocabulary only no longer matters. package.json cannot replace it: node: built-ins are not
// dependencies and timer globals need no import. The A4 spike found `types: []` would break
// contracts' AbortSignal/URL/Buffer/process uses while every lib that supplies them also
// supplies setTimeout, and the file-level bans (interaction-outcome, snapshot-quality-warnings,
// network-traffic and ios-snapshot type-only statements, kernel-only imports) have no tsc form.
import { parseSync } from 'oxc-parser';
import type { LayeringViolation } from './model.ts';
export type ContractsProductionSource = Readonly<{ path: string; source: string }>;
const RULE = 'R18 contracts-implementation-authority';
const FORBIDDEN_HOST_MODULES = /^(?:node:)?(?:child_process|fs|timers)(?:\/|$)/;
const FORBIDDEN_TIMER_CALLS = new Set([
'clearImmediate',
'clearInterval',
'clearTimeout',
'setImmediate',
'setInterval',
'setTimeout',
]);
const IOS_SNAPSHOT_CONTRACT = 'packages/contracts/src/ios-snapshot.ts';
/** Contracts owns vocabulary. Host/process/timer mechanics belong in capture-kit or an adapter. */
export function contractsImplementationAuthorityViolations(
sources: readonly ContractsProductionSource[],
): LayeringViolation[] {
const violations: LayeringViolation[] = [];
for (const file of sources) {
if (!isContractsProduction(file.path)) continue;
const parsed = parseSync(file.path, file.source);
const networkTrafficViolation = networkTrafficImplementationViolation(
file.path,
file.source,
parsed.program.body,
);
if (file.path === 'packages/contracts/src/interaction-outcome.ts') {
violations.push(
violation(
file.path,
1,
'contracts may not own mutable interaction-outcome lifecycle; that WeakMap identity map belongs in src/core',
),
);
}
if (file.path === 'packages/contracts/src/snapshot-quality-warnings.ts') {
violations.push(
violation(
file.path,
1,
'contracts may not own snapshot quality warning rendering; that presentation policy belongs in packages/capture-kit/src/snapshot/snapshot-presentation',
),
);
}
if (networkTrafficViolation) violations.push(networkTrafficViolation);
const iosSnapshotViolation = iosSnapshotContractViolation(file.path, file.source, parsed);
if (iosSnapshotViolation) violations.push(iosSnapshotViolation);
for (const site of moduleSpecifiers(parsed.module, file.source)) {
if (!FORBIDDEN_HOST_MODULES.test(site.spec)) continue;
violations.push(
violation(
file.path,
site.line,
`contracts imports host implementation authority '${site.spec}'; move mechanics to @agent-device/capture-kit`,
),
);
}
visit(parsed.program, (node) => {
if (node.type !== 'CallExpression') return;
const timerName = timerCallName(node.callee);
if (!timerName) return;
violations.push(
violation(
file.path,
lineAt(file.source, Number(node.start ?? 0)),
`contracts calls timer primitive '${timerName}'; move lifecycle mechanics to @agent-device/capture-kit`,
),
);
});
}
return violations;
}
function iosSnapshotContractViolation(
file: string,
source: string,
parsed: ReturnType<typeof parseSync>,
): LayeringViolation | undefined {
if (file !== IOS_SNAPSHOT_CONTRACT) return undefined;
const implementation = parsed.program.body.find((statement) => !isTypeOnlyStatement(statement));
if (implementation && typeof implementation === 'object') {
return violation(
file,
lineAt(source, Number((implementation as Record<string, unknown>).start ?? 0)),
'iOS snapshot contracts own typed vocabulary only; planning algorithms and provider/lifecycle implementations cannot enter contracts',
);
}
const disallowedImport = moduleSpecifiers(parsed.module, source).find(
({ spec }) => !spec.startsWith('@agent-device/kernel/'),
);
if (!disallowedImport) return undefined;
return violation(
file,
disallowedImport.line,
`iOS snapshot contracts may import only kernel vocabulary; '${disallowedImport.spec}' would bring planning algorithms or provider/lifecycle implementation into contracts`,
);
}
function networkTrafficImplementationViolation(
file: string,
source: string,
body: readonly unknown[],
): LayeringViolation | undefined {
if (!file.startsWith('packages/contracts/src/network-traffic')) return undefined;
if (file !== 'packages/contracts/src/network-traffic.ts') {
return violation(
file,
1,
'contracts may own only the neutral network-traffic vocabulary; parser modules belong in @agent-device/capture-kit',
);
}
const implementation = body.find((statement) => !isTypeOnlyStatement(statement));
if (!implementation || typeof implementation !== 'object') return undefined;
return violation(
file,
lineAt(source, Number((implementation as Record<string, unknown>).start ?? 0)),
'contracts network-traffic vocabulary contains runtime implementation; move parser mechanics to @agent-device/capture-kit',
);
}
function isTypeOnlyStatement(value: unknown): boolean {
if (value === null || typeof value !== 'object') return false;
const statement = value as Record<string, unknown>;
if (statement.type === 'ImportDeclaration') return statement.importKind === 'type';
if (statement.type === 'TSTypeAliasDeclaration' || statement.type === 'TSInterfaceDeclaration') {
return true;
}
if (statement.type !== 'ExportNamedDeclaration') return false;
if (statement.exportKind === 'type') return true;
const declaration = statement.declaration as Record<string, unknown> | undefined;
return (
declaration?.type === 'TSTypeAliasDeclaration' || declaration?.type === 'TSInterfaceDeclaration'
);
}
function moduleSpecifiers(
module: ReturnType<typeof parseSync>['module'],
source: string,
): ReadonlyArray<{ spec: string; line: number }> {
const sites: Array<{ spec: string; line: number }> = [];
const add = (request: { value?: string; start?: number } | undefined): void => {
if (request?.value)
sites.push({ spec: request.value, line: lineAt(source, request.start ?? 0) });
};
for (const entry of module.staticImports) add(entry.moduleRequest);
for (const entry of module.staticExports) {
for (const exported of entry.entries) add(exported.moduleRequest);
}
for (const entry of module.dynamicImports) {
const raw = source.slice(entry.moduleRequest.start, entry.moduleRequest.end);
const literal = /^(['"])([^'"]*)\1$/.exec(raw);
if (literal) sites.push({ spec: literal[2]!, line: lineAt(source, entry.moduleRequest.start) });
}
return sites;
}
function timerCallName(value: unknown): string | undefined {
if (value === null || typeof value !== 'object') return undefined;
const callee = value as Record<string, unknown>;
if (callee.type === 'Identifier' && FORBIDDEN_TIMER_CALLS.has(String(callee.name))) {
return String(callee.name);
}
if (callee.type !== 'MemberExpression' || callee.computed === true) return undefined;
const object = callee.object as Record<string, unknown> | undefined;
const property = callee.property as Record<string, unknown> | undefined;
if (
object?.type !== 'Identifier' ||
!['global', 'globalThis', 'window'].includes(String(object.name)) ||
property?.type !== 'Identifier' ||
!FORBIDDEN_TIMER_CALLS.has(String(property.name))
) {
return undefined;
}
return String(property.name);
}
function isContractsProduction(file: string): boolean {
return (
file.startsWith('packages/contracts/src/') &&
!file.endsWith('.test.ts') &&
!file.includes('/__tests__/')
);
}
function violation(file: string, line: number, message: string): LayeringViolation {
return { rule: RULE, file, line, message };
}
function lineAt(source: string, offset: number): number {
return source.slice(0, offset).split('\n').length;
}
function visit(node: unknown, callback: (node: Record<string, unknown>) => void): void {
if (node === null || typeof node !== 'object') return;
if (Array.isArray(node)) {
for (const child of node) visit(child, callback);
return;
}
const record = node as Record<string, unknown>;
callback(record);
for (const value of Object.values(record)) visit(value, callback);
}