mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
d8a7d03faf
* refactor: route application lifecycle through runtime facts Moves the canonical `open`, `prepare`, `close` and internal `runtime` descriptors behind package-owned lifecycle bindings admitted from device runtime facts, while daemon request/session policy and public response construction stay put. Based on main, which already carries the boot unit, the parametrized cutover gate and the apps unit. Readiness is package-owned there, so the Apple and Android bindings call ensureAppleReady/ensureAndroidReady rather than a root readiness bag; ensureAppleReady gained an onColdBootStart hook so open keeps warming the runner cache in parallel with a cold boot, and a narrow markBooted port publishes readiness' fresh observation so a flow still makes one simctl listing. Cutover rows take R24-R27, clear of the accepted catalog and the sibling install stack, and cutoverTableDefects rejects a duplicate rule id. Two defects this unit introduced are fixed here rather than shipped: `open <app> <url>` dropped the URL on a first open, and test-IME activation was first fatal on an unobtainable helper and then over-caught. Helper unavailability is a typed non-activation outcome now; fence, lock and post-record failures propagate. The duplication the unit had accumulated is gone: one runtime-admission module instead of five per-command copies, one direct-lifecycle binding factory instead of six hand-rolled packages, one transport-hint predicate, one session finalization path, and no identity-wrapper module. * fix: allocate lifecycle cutover rows after deployment * chore: preserve lifecycle union reconstruction * fix: reconcile lifecycle runtime stack * refactor: tighten lifecycle runtime topology * refactor: remove superseded runtime adapters * fix: preserve stacked runtime cutovers * test: preserve migrated runtime ownership * test: move Android deployment retry ownership * test: extract runtime hint fixtures * fix: preserve lifecycle stack invariants * fix: complete lifecycle runtime cutover * fix: remove lifecycle cutover residue
107 lines
4.1 KiB
TypeScript
107 lines
4.1 KiB
TypeScript
import { readdirSync, readFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
/**
|
|
* Rule-ID uniqueness.
|
|
*
|
|
* Layering rules are addressed by number in review, in CI annotations, and in
|
|
* the code comments that point at them, so a number must name exactly one rule.
|
|
* Two branches allocating the same free number do not conflict in git — the
|
|
* files differ — and land as two rules answering to one id, after which every
|
|
* reference to it is ambiguous. #1656 and #1750 both reached for R17.
|
|
*
|
|
* The claim being checked is about source text (which id a rule declares), so
|
|
* reading the declarations IS the check rather than a proxy for one. It is a
|
|
* naming registry, not a behavioural claim.
|
|
*/
|
|
|
|
export type RuleDeclaration = {
|
|
id: string;
|
|
name: string;
|
|
file: string;
|
|
};
|
|
|
|
/**
|
|
* A rule id is declared as a WHOLE string literal — `rule: 'R7 …'` at an
|
|
* emitter, `const RULE = 'R17 …'` at the top of a policy module. Matching the
|
|
* complete literal is what separates a declaration from the prose that also
|
|
* names rules ("R17 holds the devices command to…"): a sentence keeps going
|
|
* where a declaration closes its quote.
|
|
*/
|
|
const RULE_LITERAL = /'(R\d+) ([a-z0-9-]+)'/g;
|
|
|
|
export function collectRuleDeclarations(files: readonly { path: string; source: string }[]) {
|
|
const declarations: RuleDeclaration[] = [];
|
|
for (const file of files) {
|
|
for (const match of file.source.matchAll(RULE_LITERAL)) {
|
|
declarations.push({ id: match[1]!, name: match[2]!, file: file.path });
|
|
}
|
|
}
|
|
return declarations;
|
|
}
|
|
|
|
export function duplicateRuleIds(declarations: readonly RuleDeclaration[]): string[] {
|
|
const byId = new Map<string, Set<string>>();
|
|
for (const declaration of declarations) {
|
|
const names = byId.get(declaration.id) ?? new Set<string>();
|
|
names.add(declaration.name);
|
|
byId.set(declaration.id, names);
|
|
}
|
|
return [...byId]
|
|
.filter(([, names]) => names.size > 1)
|
|
.map(([id, names]) => `${id} names ${[...names].sort().join(' and ')}`)
|
|
.sort();
|
|
}
|
|
|
|
/**
|
|
* Collisions that predate this gate. Transitional, and each entry expires on
|
|
* contact — see `ruleIdCollisionFailures`. Empty is the end state, and the gate
|
|
* gets there on its own.
|
|
*
|
|
* The R11/R13 collisions were retired by the accepted R18 contracts and R17 devices
|
|
* allocations. New collisions fail closed; there is no transitional allowance left.
|
|
*/
|
|
export const KNOWN_RULE_ID_COLLISIONS: readonly string[] = [];
|
|
|
|
/**
|
|
* Both halves of any transition, because an allowance that outlives the thing
|
|
* it allows fails OPEN: a list still naming a retired collision would wave it
|
|
* straight back through if anyone reintroduced it.
|
|
*
|
|
* So an allowance is only valid while its collision is actually present. A
|
|
* collision nobody allowed fails, and an allowance whose collision is gone
|
|
* fails too — which forces the entry to be deleted in the same change that
|
|
* removes the collision, and leaves an empty list that admits nothing.
|
|
*/
|
|
export function ruleIdCollisionFailures(params: {
|
|
declarations: readonly RuleDeclaration[];
|
|
allowed: readonly string[];
|
|
}): string[] {
|
|
const present = new Set(duplicateRuleIds(params.declarations));
|
|
const allowed = new Set(params.allowed);
|
|
return [
|
|
...[...present]
|
|
.filter((collision) => !allowed.has(collision))
|
|
.map(
|
|
(collision) => `two rules answer to one id: ${collision}. Allocate the next free number.`,
|
|
),
|
|
...[...allowed]
|
|
.filter((collision) => !present.has(collision))
|
|
.map(
|
|
(collision) =>
|
|
`stale allowance: "${collision}" no longer occurs, so the entry admits a collision ` +
|
|
'nobody is fixing. Delete it from KNOWN_RULE_ID_COLLISIONS.',
|
|
),
|
|
].sort();
|
|
}
|
|
|
|
/** The layering policy modules, which is where rule ids are declared. */
|
|
export function readLayeringSources(directory: string): { path: string; source: string }[] {
|
|
return readdirSync(directory)
|
|
.filter((entry) => entry.endsWith('.ts') && !entry.endsWith('.test.ts'))
|
|
.map((entry) => ({
|
|
path: `scripts/layering/${entry}`,
|
|
source: readFileSync(path.join(directory, entry), 'utf8'),
|
|
}));
|
|
}
|