mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
ddb415a2c7
* refactor: sink package-closed src modules into existing packages Move closed modules into contracts, kernel, capture-kit, and ad-script, and declare DaemonCommandDescriptor in core so R6/R9 can pin the remaining provider-webdriver type cycle. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: keep contracts and capture-kit off generic sinks Move interaction-outcome, snapshot warning rendering, and inventory ALS behind focused owners, and plant R18/R70 domain-shape gates so they cannot return as package export-map growth. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: drop moved implementation comments from owner modules Names, types, and tests already carry those invariants; the relocated files should not keep review-history or control-flow narration. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: drop the empty snapshot-quality layering zone W1 moved the verdict into capture-kit and this PR moved warning rendering into snapshot-presentation, so the ranked zone no longer has production files. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
111 lines
3.9 KiB
TypeScript
111 lines
3.9 KiB
TypeScript
import { parseSync } from 'oxc-parser';
|
|
import type { LayeringViolation } from './model.ts';
|
|
|
|
export const SUBSTRATE_DOMAIN_SHAPE_RULE = 'R70 substrate-domain-shape';
|
|
|
|
export type SubstrateDomainSource = Readonly<{ path: string; source: string }>;
|
|
|
|
const ASYNC_HOOKS = /^(?:node:)?async_hooks(?:\/|$)/;
|
|
|
|
/** Capture-kit is durable capture; request ALS and retired src/contracts/ stay out of substrate packages. */
|
|
export function substrateDomainShapeViolations(
|
|
sources: readonly SubstrateDomainSource[],
|
|
): LayeringViolation[] {
|
|
const violations: LayeringViolation[] = [];
|
|
for (const file of sources) {
|
|
if (isRetiredContractsRoot(file.path)) {
|
|
violations.push(
|
|
violation(
|
|
file.path,
|
|
1,
|
|
'src/contracts/ is retired; vocabulary lives in packages/contracts, and executable policy belongs to its owning domain',
|
|
),
|
|
);
|
|
continue;
|
|
}
|
|
if (!isCaptureKitProduction(file.path)) continue;
|
|
const parsed = parseSync(file.path, file.source);
|
|
for (const site of moduleSpecifiers(parsed.module, file.source)) {
|
|
if (!ASYNC_HOOKS.test(site.spec)) continue;
|
|
violations.push(
|
|
violation(
|
|
file.path,
|
|
site.line,
|
|
`capture-kit imports '${site.spec}'; request-scoped AsyncLocalStorage dispatch belongs in src/request`,
|
|
),
|
|
);
|
|
}
|
|
visit(parsed.program, (node) => {
|
|
if (!isAsyncLocalStorageConstruction(node)) return;
|
|
violations.push(
|
|
violation(
|
|
file.path,
|
|
lineAt(file.source, Number(node.start ?? 0)),
|
|
'capture-kit constructs AsyncLocalStorage; request-scoped dispatch belongs in src/request',
|
|
),
|
|
);
|
|
});
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
function isRetiredContractsRoot(file: string): boolean {
|
|
return file.startsWith('src/contracts/') && file.endsWith('.ts');
|
|
}
|
|
|
|
function isCaptureKitProduction(file: string): boolean {
|
|
return (
|
|
file.startsWith('packages/capture-kit/src/') &&
|
|
!file.endsWith('.test.ts') &&
|
|
!file.includes('/__tests__/')
|
|
);
|
|
}
|
|
|
|
function isAsyncLocalStorageConstruction(node: Record<string, unknown>): boolean {
|
|
if (node.type !== 'NewExpression') return false;
|
|
const callee = node.callee;
|
|
if (callee === null || typeof callee !== 'object') return false;
|
|
const expression = callee as Record<string, unknown>;
|
|
return expression.type === 'Identifier' && expression.name === 'AsyncLocalStorage';
|
|
}
|
|
|
|
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 violation(file: string, line: number, message: string): LayeringViolation {
|
|
return { rule: SUBSTRATE_DOMAIN_SHAPE_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);
|
|
}
|