Files
callstack__agent-device/scripts/layering/runtime-command-cutover-model.ts
2026-08-12 11:42:19 +02:00

272 lines
11 KiB
TypeScript

import type { LayeringViolation } from './model.ts';
/** A violation before the parametrized gate stamps its row's rule id. */
export type UnruledViolation = Omit<LayeringViolation, 'rule'>;
export type CutoverCheck = (sources: ReadonlyMap<string, string>) => UnruledViolation[];
/** At least one element, so "declared but empty" cannot satisfy a mandatory claim. */
export type NonEmpty<T> = readonly [T, ...T[]];
/**
* What a migrated command must retire. A row declares every form that applies to it;
* `cutoverRowDefects` requires at least one, because a cutover that retires nothing is
* not a cutover.
*/
export type LegacyRetirementClaim = Readonly<{
/** Retired module paths that must not exist in production. */
modulePaths?: readonly string[];
/** Retired module path shapes (matched against the repo-relative path). */
modulePathPatterns?: readonly RegExp[];
/** Import/re-export specifiers of retired modules, static or dynamic. */
importPatterns?: readonly RegExp[];
/** Retired executable names, scanned across all production sources. */
routeNames?: readonly string[];
/** Retired executable names scanned in `src/daemon/` production only. */
daemonOnlyRouteNames?: readonly string[];
/** Legacy provider methods the daemon may not call (`provider.dumpNetwork`). */
daemonOnlyProviderMethods?: readonly string[];
/** `PlatformPlugin` facet keys retired with the legacy adapter. */
pluginFacetKeys?: readonly string[];
}>;
/**
* Command-keyed plugin admission members. Form and file scope differ per row because
* the same syntax means admission for one command and opaque identifier data for
* another — `PUBLIC_COMMANDS.record` is live session-event data.
*/
export type AdmissionMemberClaim = Readonly<{
forms: NonEmpty<'computed-property' | 'public-commands-member'>;
/** Undefined scans every production source. */
files?: readonly string[];
message: string;
}>;
/** Which operations belong to this command: an exact set, or an input-dependent family. */
export type OperationClaim =
| Readonly<{ names: NonEmpty<string>; pattern?: RegExp }>
| Readonly<{ names?: undefined; pattern: RegExp }>;
export type CutoverIdentity = Readonly<{
/** Layering rule id. Per row, so the report keeps each cutover's identity. */
rule: string;
/** Canonical descriptor name: drives admission, descriptor, and static-set checks. */
command: string;
/** Noun used in prose for this command's mechanics ('recording', 'device inventory'). */
subject: string;
/**
* ADR 0019 §8 evidence tier. A `durable-resource` row must carry a lifecycle proof;
* a request-scoped row must not need one.
*/
tier: 'request-scoped' | 'durable-resource';
legacyRetirement: LegacyRetirementClaim;
admissionMember?: AdmissionMemberClaim;
/** Assertions that do not generalize. Stamped with this row's rule id. */
extensions?: readonly CutoverCheck[];
/** Required iff `tier` is `durable-resource`. */
lifecycleProof?: CutoverCheck;
}>;
/**
* An inventory command binds no device runtime, so its singular-execution proof is the
* identity of the one gateway binding its handler must import and call.
*/
export type InventoryCutover = CutoverIdentity &
Readonly<{
execution: 'inventory';
singularExecution: Readonly<{ gatewayProof: CutoverCheck }>;
}>;
type DeviceRuntimeBase = CutoverIdentity &
Readonly<{
execution: 'device-runtime';
runtimeTypeNames: NonEmpty<string>;
/**
* `declared` checks this row's operations. `any-operation` covers every daemon
* runtime operation, which a row whose operation set is input-dependent needs.
*/
nonNullRepairScope?: 'declared' | 'any-operation';
}>;
/**
* A command that names its operations exactly must also enforce each of them exactly
* once. Knowing the whole operation set and checking only the route would leave the
* operations themselves free to be called twice or not at all.
*/
type NamedOperationCutover = DeviceRuntimeBase &
Readonly<{
operations: Readonly<{ names: NonEmpty<string>; pattern?: RegExp }>;
singularExecution: Readonly<{
/** Daemon route functions that must be called exactly once. */
routes: NonEmpty<string>;
/** Narrowed operations that must be called exactly once — the named set. */
operations: NonEmpty<string>;
/**
* Lexical function owner for each narrowed operation call. An unrelated daemon
* caller must neither satisfy a missing command call nor create a false duplicate.
*/
operationOwners: Readonly<Record<string, NonEmpty<string>>>;
}>;
}>;
/**
* A command whose operation set is input-dependent (the app-log plan picks its
* operations from the request) cannot name a fixed set to enforce, so route
* singularity is the whole claim. Route-only singularity is permitted here alone.
*/
type PatternOperationCutover = DeviceRuntimeBase &
Readonly<{
operations: Readonly<{ names?: undefined; pattern: RegExp }>;
singularExecution: Readonly<{ routes: NonEmpty<string>; operations?: undefined }>;
}>;
/**
* A device-runtime command must prove its narrowing (which types re-widen a bound
* runtime, which operations are its own) and that its execution is singular.
*/
export type DeviceRuntimeCutover = NamedOperationCutover | PatternOperationCutover;
/**
* One migrated command's cutover row (ADR 0019 §6/§8). The gate that reads this table
* proves the command has exactly one platform-execution path.
*
* The union is discriminated by `execution` and its claims are mandatory, so a row is
* not a place to opt out of enforcement: adding a command means stating what it
* retired, how its admission is derived, and what makes its execution singular.
* `cutoverRowDefects` rejects the claims the type system cannot require.
*/
export type MigratedCommandCutover = InventoryCutover | DeviceRuntimeCutover;
const RETIREMENT_FORMS = [
'modulePaths',
'modulePathPatterns',
'importPatterns',
'routeNames',
'daemonOnlyRouteNames',
'daemonOnlyProviderMethods',
'pluginFacetKeys',
] as const satisfies readonly (keyof LegacyRetirementClaim)[];
/**
* Cross-field claims the type system cannot express. Returns one message per defect so
* an incomplete row fails the gate instead of passing with nothing to enforce.
*
* Reads defensively: a row that omits whole claim objects is exactly the input this
* rejects, so it must not depend on them being present.
*/
export function cutoverRowDefects(row: MigratedCommandCutover): string[] {
const defects: string[] = [];
if (!hasAnyRetirementForm(row.legacyRetirement)) {
defects.push('declares no legacy retirement form');
}
if (row.tier !== 'request-scoped' && row.tier !== 'durable-resource') {
defects.push('declares no evidence tier');
}
if (row.tier === 'durable-resource' && row.lifecycleProof === undefined) {
defects.push('is durable-resource tier but declares no lifecycle proof');
}
if (row.tier === 'request-scoped' && row.lifecycleProof !== undefined) {
defects.push('is request-scoped tier but declares a durable lifecycle proof');
}
defects.push(...executionDefects(row));
return defects;
}
function executionDefects(row: MigratedCommandCutover): string[] {
if (row.execution === 'inventory') {
return row.singularExecution?.gatewayProof === undefined
? ['declares no singular gateway proof']
: [];
}
if (row.execution !== 'device-runtime') return ['declares no execution kind'];
const defects: string[] = [];
if ((row.runtimeTypeNames?.length ?? 0) === 0) defects.push('declares no runtime type names');
if (!hasOperationClaim(row.operations)) defects.push('declares no operations');
if ((row.singularExecution?.routes?.length ?? 0) === 0) {
defects.push('declares no singular daemon route');
}
defects.push(...namedOperationDefects(row));
return defects;
}
/**
* The named set and the enforced set must be the same set, each listed once. Equality
* has to hold in both directions: an unenforced name leaves an operation free to be
* called twice or never, and an enforced name the row does not claim asserts singularity
* over an operation this command does not own. Route-only singularity stays available to
* pattern-only rows, whose operations are not knowable statically.
*/
function namedOperationDefects(row: DeviceRuntimeCutover): string[] {
const named = row.operations?.names ?? [];
if (named.length === 0) return [];
const enforced = row.singularExecution?.operations ?? [];
if (enforced.length === 0) {
return ['names its operations but enforces none of them exactly once'];
}
const defects: string[] = [];
defects.push(...duplicateDefects('names', named));
defects.push(...duplicateDefects('enforces', enforced));
const enforcedSet = new Set(enforced);
const namedSet = new Set(named);
const unenforced = named.filter((name) => !enforcedSet.has(name));
const unnamed = enforced.filter((name) => !namedSet.has(name));
if (unenforced.length > 0) {
defects.push(
`names operations it does not enforce exactly once: ${sortedUnique(unenforced).join(', ')}`,
);
}
if (unnamed.length > 0) {
defects.push(`enforces operations it does not name: ${sortedUnique(unnamed).join(', ')}`);
}
const owners = row.singularExecution?.operationOwners ?? {};
const owned = Object.keys(owners);
const ownerless = named.filter((name) => (owners[name]?.length ?? 0) === 0);
const unclaimedOwners = owned.filter((name) => !namedSet.has(name));
if (ownerless.length > 0) {
defects.push(`names operations without lexical owners: ${sortedUnique(ownerless).join(', ')}`);
}
if (unclaimedOwners.length > 0) {
defects.push(
`declares lexical owners for unnamed operations: ${sortedUnique(unclaimedOwners).join(', ')}`,
);
}
for (const [operation, lexicalOwners] of Object.entries(owners)) {
const duplicates = duplicateValues(lexicalOwners);
if (duplicates.length > 0) {
defects.push(`declares duplicate lexical owners for ${operation}: ${duplicates.join(', ')}`);
}
}
return defects;
}
function duplicateDefects(verb: 'names' | 'enforces', values: readonly string[]): string[] {
const duplicates = duplicateValues(values);
return duplicates.length === 0 ? [] : [`${verb} duplicate operations: ${duplicates.join(', ')}`];
}
function duplicateValues(values: readonly string[]): string[] {
const seen = new Set<string>();
const duplicates = values.filter((value) => {
if (seen.has(value)) return true;
seen.add(value);
return false;
});
return sortedUnique(duplicates);
}
function sortedUnique(values: readonly string[]): string[] {
return [...new Set(values)].sort();
}
function hasAnyRetirementForm(claim: LegacyRetirementClaim | undefined): boolean {
if (claim === undefined) return false;
return RETIREMENT_FORMS.some((form) => (claim[form]?.length ?? 0) > 0);
}
function hasOperationClaim(claim: OperationClaim | undefined): boolean {
if (claim === undefined) return false;
return (claim.names?.length ?? 0) > 0 || claim.pattern !== undefined;
}