mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
ef6ec2995b
* chore(layering): document R12/R18/R19, retire R8, make R9 shrink mandatory (#1781 A6) The A6 review kept `check:layering` in full (15/15 planted violations fired, no other enforcer exists) and left four follow-throughs. R12 bin-alias-fast-path, R18 contracts-implementation-authority and R19 selector-pipeline-ownership were live rules with no ADR or CONTEXT anchor — they now carry one each, in the same list as R7/R9/R10/R13. R8 zero-dep-job-closure is retired: no CI job sets `install-deps: false` and ci.yml records why each keeps it enabled, so the invariant has no subjects. R11's relative-into-packages exception existed only because a zero-dep closure cannot coexist with specifier loads, so it retires with R8; the route is now closed to every caller. R1 was retired the same way at #1490. R9 was growth-only and merely suggested lowering the ceiling, which is headroom the next change spends without a number moving. It is now an equality pin like R6 and the R10 R7 counts, and the committed baseline drops 47 -> 46 (daemon-server ceiling 17 -> 16) to match the measurement. ADR 0019 §6 now says each runtime-command-cutover row is deleted when that command's migration is declared closed. * chore(layering): rename R9 to type-cycle-size now that it fails both ways (#1781 A6)
108 lines
4.6 KiB
TypeScript
108 lines
4.6 KiB
TypeScript
import type { ImportEdge } from './model.ts';
|
|
|
|
/**
|
|
* R1-R3 as data: which zone may import which, and on what terms.
|
|
*
|
|
* 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 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),
|
|
* R7 (field ownership) and R9 (cycle size) are whole-graph or non-import properties, and each
|
|
* 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;
|
|
/** Source zones this policy governs. Omit for "every zone". */
|
|
from?: readonly string[];
|
|
/** Target zones this policy forbids importing. Omit and use `exceptTo` for "everything but". */
|
|
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;
|
|
};
|
|
|
|
/**
|
|
* The policy table. Order is presentation only — every policy is evaluated against every edge.
|
|
*
|
|
* R1 kernel-sink retired 2026-07-30 (#1490 W0): the kernel moved to
|
|
* packages/kernel, where package resolution and R11 package-boundaries enforce
|
|
* the sink property physically — a package cannot import root src at all.
|
|
*/
|
|
export const ZONE_POLICIES: readonly ZonePolicy[] = [
|
|
{
|
|
rule: 'R2 commands-floor',
|
|
from: ['platforms', '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/daemon/', 'src/sdk/'],
|
|
seamExcept: ['src/daemon/client/'],
|
|
hint:
|
|
'Only src/core/interactors/, the daemon server 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' {
|
|
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.
|
|
*/
|
|
export function policyViolation(
|
|
policy: ZonePolicy,
|
|
edge: { file: string; fromZone: string; toZone: string; imp: ImportEdge },
|
|
): string | null {
|
|
if (policy.from && !policy.from.includes(edge.fromZone)) return null;
|
|
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;
|
|
}
|
|
|
|
/** The one-line lead every zone-policy violation shares, before its rule-specific hint. */
|
|
export function policyLead(edge: { fromZone: string; toZone: string; imp: ImportEdge }): string {
|
|
const kind = kindOf(edge.imp);
|
|
const qualifier = kind === 'value' ? '' : `${kind} `;
|
|
return `${edge.fromZone}/ must not ${qualifier}import ${edge.toZone}/ (imports '${edge.imp.spec}').`;
|
|
}
|