Files
Michał Pierzchała fea7ca8a43 chore(layering): runner modules reach host-kit only through the runner host port (#2470)
R77 apple-runner-host-port bans a direct @agent-device/host-kit/* value
import from packages/platform-apple/src/runner/**; the port at runner/host.ts,
bound in core/runner-host.ts, is the only door. runner/** sits in the eager
closure of seven Apple facade entries eager-closure-budgets.ts holds at a
fixed size, so a direct import grows all seven at once (#2423 measured one
candidate import adding 5 modules to runner/index.ts's closure, 13 -> 18,
after two review rounds spent rediscovering this).
2026-09-10 18:44:36 +02:00

553 lines
26 KiB
TypeScript

// Import-direction lint — enforces the folder DAG established by the Phase-5
// folder moves. The policies and their tests below are the source of truth.
//
// Ranked target spine, as rank groups lowest to highest. `A ◄ B` means B may not
// be outranked by A (the back-edge order the gate rejects), NOT that every displayed import exists:
// { contracts, request, selectors } ◄ core ◄ { commands, cli-schema }
// ◄ { client, daemon-server } ◄ daemon-client ◄ cli
// (authoritative ranks: `TARGET_DAG_RANK` in model.ts. The former rank-0 kernel
// zone lives in packages/kernel since #1490 W0; R11 owns its boundary.)
//
// This gate enforces five things, across four scopes:
// - GLOBALLY, across every production source file: the remaining R2 move rule and
// rejection of all production static value-import cycles (R4). R1 kernel-sink
// retired with the kernel's move to packages/kernel (#1490 W0); R8
// zero-dep-job-closure retired with the last `install-deps: false` job
// (#1781 A6) — its invariant has no subjects. R14 is reserved for the terminal
// src/utils retirement rule (#2149); other new rules take the next free id.
// - Over the RANKED SPINE only: rejection of every spine back-edge (R5), i.e.
// an import whose source zone outranks its target zone, plus a ratchet on the
// same inversion measured over TYPE-ONLY edges (R6).
// - Over the DAEMON only: SessionState field ownership (R7), because the session
// record is store-owned mutable state that any daemon module can write; and the terminal
// concrete-platform boundary (R65), which rejects every import form into the retired
// src/platforms path or a platform package.
// - Over the TYPE GRAPH: the largest type-level import cycle may not grow past the
// merge-base (R9). R4 keeps the value graph acyclic, so these cycles are free at
// runtime but bound what can be read in isolation.
// - Across the DAEMON MODULARITY MIGRATION: R7 ownership pressure and external
// daemon request/session-state importers only shrink, R9 zone membership cannot grow or absorb
// engine files, and planned logical modules start with zero forbidden/internal imports (R10).
// - Over the WORKSPACE PACKAGES: no root back-imports, no relative tunnelling past
// an exports map, and every workspace specifier declared + exports-named (R11).
// - Over PLATFORM PACKAGE COMPOSITION: six private metadata façades meet at the exact root
// composition file; premature implementation loading and forbidden cross-boundary edges fail (R13).
// - Over THE APPLE RUNNER SUBTREE: `runner/**` may not value-import `@agent-device/host-kit/*`
// directly (R77) — the subtree sits in the eager closure of seven Apple façade entries the
// eager-closure-budgets gate holds at a fixed size, so a direct host-kit edge grows all seven;
// host-kit reaches the runner only through `runner/host.ts`, bound in `core/runner-host.ts`.
// - Over REQUEST-BOUND RUNTIME EXECUTION: facts remain the only admission authority and daemon
// code cannot manufacture or repair a narrowed runtime proof (R66).
// - Over CONTRACTS PRODUCTION SOURCE: contracts owns vocabulary only — host, process, and timer
// mechanics belong in capture-kit or an adapter (R18).
// R6, R9, and the R10 R7 counts are ratchets with no written-down reference: each is the same
// measurement taken over the merge-base with origin/main (`ratchet-reference.ts`), so growth
// fails, a shrink needs no edit, and no change can bank headroom.
// `(root)` holds entrypoints and composition roots. The retired `src/utils` zone is deliberately
// outside the spine and is rejected separately by R14; extracted workspace package zones are
// classified separately and held behind R11 instead of the src folder spine.
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import {
fieldClassificationDrift,
findSessionStateWrites,
sessionStateDeclarationFile,
sessionStateFields,
sessionStateFieldCount,
SESSION_STATE_FIELD_OWNERS,
STORE_OWNED_SESSION_STATE_FIELDS,
} from './session-state.ts';
import {
backEdgePair,
findValueImportCycles,
memoizedImportParser,
resolveImportEdges,
topFolder,
type LayeringViolation,
type ResolvedImportEdge,
} from './model.ts';
import { checkTypeInversions } from './type-inversion-ratchet.ts';
import {
measureRatchets,
mergeBaseRatchets,
type LayeringRatchets,
type MergeBaseRatchets,
} from './ratchet-reference.ts';
import {
checkDaemonModularityRatchets,
checkRetiredInteractionPaths,
checkRetiredSessionLifecyclePaths,
checkRetiredSessionObservabilityPaths,
checkRetiredSnapshotExecutionPaths,
daemonModularitySummary,
} from './daemon-modularity.ts';
import {
checkPackageBoundaries,
packageBoundariesSummary,
workspaceSpecifierTargets,
} from './package-boundaries.ts';
import {
checkPlatformPackagePolicy,
checkRetiredPlatformsZone,
platformPackagePolicySummary,
} from './platform-package-policy.ts';
import { appleRunnerHostPortViolations } from './apple-runner-host-port-policy.ts';
import {
listUntrackedProductionTypeScriptFiles,
readTrackedPlatformPackageDeclarations,
} from './platform-package-repository.ts';
import { policyLead, policyViolation, ZONE_POLICIES } from './zone-policy.ts';
import { contractsImplementationAuthorityViolations } from './contracts-implementation-policy.ts';
import { substrateDomainShapeViolations } from './substrate-domain-shape.ts';
import { selectorPipelineOwnershipViolations } from './selector-pipeline-ownership.ts';
import { recordRuntimeRegistryJoinViolations } from './record-runtime-registry-policy.ts';
import { recordRuntimeDaemonMechanicsViolations } from './record-runtime-mechanics-policy.ts';
import { checkDaemonPlatformBoundary } from './daemon-platform-boundary.ts';
import {
checkDaemonPlatformRuntimeInventory,
DAEMON_PLATFORM_RUNTIME_EDGES,
} from './daemon-platform-runtime-inventory.ts';
import { checkSessionAuthorityOverlay, handlerOwnedOverlay } from './session-authority-overlay.ts';
import {
listTrackedPlatformZoneFiles,
listTrackedProductionSources,
listTrackedSrcUtilsFiles,
listTrackedTypeScriptFiles,
} from './tracked-sources.ts';
import { runtimeExecutionIntegrityViolations } from './runtime-execution-policy.ts';
import { sourceExecutionCompatibilityViolations } from './source-execution-policy.ts';
import { sessionResourceOwnershipViolations } from './session-resource-ownership.ts';
import { applicationLifecycleOwnershipViolations } from './application-lifecycle-policy.ts';
import { iosSnapshotEngineOwnershipViolations } from './ios-snapshot-engine-policy.ts';
import { providerSnapshotPresentationViolations } from './provider-snapshot-presentation-policy.ts';
import { snapshotAssemblyPresentationViolations } from './snapshot-assembly-presentation-policy.ts';
import { RETIRED_PATH_RULES, retiredPathRuleViolations } from './retired-paths-policy.ts';
const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
encoding: 'utf8',
}).trim();
export function listTypeScriptFiles(): string[] {
return listTrackedTypeScriptFiles(repoRoot);
}
export function listSourceFiles(): string[] {
return listTrackedProductionSources(repoRoot);
}
function readSources(files: readonly string[]): Map<string, string> {
return new Map(files.map((file) => [file, fs.readFileSync(path.join(repoRoot, file), 'utf8')]));
}
// R1-R3 are declared as a policy table in zone-policy.ts. This walks it; the boundaries
// themselves are data, so adding one is a table entry rather than a fourth predicate.
function checkLayeringRules(edges: readonly ResolvedImportEdge[]): LayeringViolation[] {
const violations: LayeringViolation[] = [];
for (const edge of edges) {
const fromZone = topFolder(edge.file);
const toZone = topFolder(edge.target);
if (fromZone === toZone) continue;
const ctx = { file: edge.file, fromZone, toZone, imp: edge };
for (const policy of ZONE_POLICIES) {
const hint = policyViolation(policy, ctx);
if (hint === null) continue;
violations.push({
rule: policy.rule,
file: edge.file,
line: edge.line,
message: `${policyLead(ctx)} ${hint}`,
});
}
}
return violations;
}
/**
* Catches: a production import cycle — A imports B imports A at the value level — that a
* file-by-file review cannot see because each edge looks locally fine; only walking the
* whole graph exposes the loop. No other gate looks at cycles at all.
* Evidence: 3d70943550 (#984) introduced the import-direction DAG gate this cycle check
* anchors; f19864e486 (#1410) added the dependency-graph report built on the same model.
* Cost: not attributed (folded into check.ts's whole-graph pass; no standalone module or
* test file to size separately).
* Kill criterion: none enforced today; retire only by maintainer decision that an acyclic
* value-import graph no longer matters. tsc rejects a cyclic `references` edge between
* projects, but no tsconfig declares references today and the A4 spike found they are a
* build-cache mechanism, not a boundary: value imports inside one project are never
* cycle-checked, and a cross-package edge resolves through root node_modules with no error.
*/
function checkCycles(edges: readonly ResolvedImportEdge[]): LayeringViolation[] {
return findValueImportCycles(edges).map((cycle) => ({
rule: 'R4 value-import-cycle',
file: cycle[0]!,
line: 1,
message: `production value-import cycle: ${cycle.join(' -> ')}`,
}));
}
function checkContractsImplementationAuthority(
sources: ReadonlyMap<string, string>,
): LayeringViolation[] {
return contractsImplementationAuthorityViolations(
[...sources].map(([path, source]) => ({ path, source })),
);
}
/** Record's descriptor join and mechanics boundary are one permanent ownership rule. */
function checkRecordRuntimeOwnership(sources: ReadonlyMap<string, string>): LayeringViolation[] {
const production = [...sources].map(([file, source]) => ({ path: file, source }));
return [
...recordRuntimeRegistryJoinViolations(production),
...recordRuntimeDaemonMechanicsViolations(production),
].map((violation) => {
const separator = violation.indexOf(': ');
return {
rule: 'R16 record-runtime-ownership',
file: separator < 0 ? '(record runtime)' : violation.slice(0, separator),
line: 1,
message: separator < 0 ? violation : violation.slice(separator + 2),
};
});
}
/**
* Catches: a value import that runs against the ranked target spine's declared order (a lower
* zone importing a higher one) — the runtime-consequential half of what R6 also checks for
* type-only edges; neither zone-policy.ts's table nor the cycle check names direction.
* Evidence: 3d70943550 (#984) introduced the ranked spine and its back-edge check; docs/
* dependency-graph-findings.md tracks the count this rule ratchets.
* Cost: not attributed (folded into check.ts's whole-graph pass; no standalone module or
* test file to size separately).
* Kill criterion: none enforced today; retire only by maintainer decision that the ranked spine's
* import direction no longer matters. Splitting zones into packages would not replace it: the
* A4 spike found an undeclared workspace package still resolves through root node_modules and
* a relative tunnel into another package's src still compiles.
*/
function checkBackEdges(edges: readonly ResolvedImportEdge[]): LayeringViolation[] {
const seen = new Set<string>();
return edges.flatMap((edge) => {
const pair = backEdgePair(edge);
const identity = `${edge.file} -> ${edge.target}`;
if (!pair || seen.has(identity)) return [];
seen.add(identity);
return [
{
rule: 'R5 zero-back-edges',
file: edge.file,
line: edge.line,
message: `${pair} back-edge: ${identity}. Move the shared contract below both owners.`,
},
];
});
}
function checkSessionStateOwnership(sources: ReadonlyMap<string, string>): LayeringViolation[] {
const declarationFile = sessionStateDeclarationFile(sources);
if (!declarationFile) {
return [
{
rule: 'R7 session-state-ownership',
file: 'src/daemon/session-state.ts',
line: 1,
message: 'no daemon module declares SessionState, so its ownership cannot be checked.',
},
];
}
const fields = sessionStateFields(sources.get(declarationFile)!);
const writes = findSessionStateWrites(sources, fields);
const violations: LayeringViolation[] = [];
const seenOwners = new Map<string, Set<string>>();
// Parity first: the rule is only exhaustive if every declared field is classified. A field
// that is in neither table would otherwise pass by being invisible to the scan, and R7 would
// quietly stop covering part of the type it claims to cover.
const DRIFT_MESSAGE: Readonly<Record<string, string>> = {
unclassified:
'is declared by SessionState but classified nowhere. Name its owning module in ' +
'SESSION_STATE_FIELD_OWNERS, or — if the store establishes it at construction and nothing ' +
'mutates it later — add it to STORE_OWNED_SESSION_STATE_FIELDS.',
both:
'is in both SESSION_STATE_FIELD_OWNERS and STORE_OWNED_SESSION_STATE_FIELDS. A field is ' +
'either store-established or owned by a writer, not both.',
'not-a-field':
'is classified but is no longer declared by SessionState — remove it from the table it ' +
'still appears in.',
};
for (const { field, problem } of fieldClassificationDrift(fields)) {
violations.push({
rule: 'R7 session-state-ownership',
file: 'scripts/layering/session-state.ts',
line: 1,
message: `session.${field} ${DRIFT_MESSAGE[problem]}`,
});
}
for (const write of writes) {
const owners = SESSION_STATE_FIELD_OWNERS[write.field];
const seen = seenOwners.get(write.field) ?? new Set<string>();
seen.add(write.file);
seenOwners.set(write.field, seen);
if (write.field === '[computed]') {
violations.push({
rule: 'R7 session-state-ownership',
file: write.file,
line: write.line,
message:
'computed write to a session field (`session[key] = …`). The field cannot be ' +
'attributed to an owner, so write the field by name, or move the write into the ' +
'module that owns the fields it can reach.',
});
continue;
}
if (owners === undefined) {
const storeOwned = STORE_OWNED_SESSION_STATE_FIELDS.has(write.field);
violations.push({
rule: 'R7 session-state-ownership',
file: write.file,
line: write.line,
message: storeOwned
? `session.${write.field} is classified store-established ` +
`(STORE_OWNED_SESSION_STATE_FIELDS), meaning nothing mutates it after construction — ` +
`but this is a direct write. Route it through the store, or move the field into ` +
`SESSION_STATE_FIELD_OWNERS with this module as its owner.`
: `session.${write.field} has no declared owner. SessionStore hands out the live ` +
`record, so this write is durable: name the owning module in ` +
`SESSION_STATE_FIELD_OWNERS (scripts/layering/session-state.ts).`,
});
continue;
}
if (!owners.includes(write.file)) {
violations.push({
rule: 'R7 session-state-ownership',
file: write.file,
line: write.line,
message:
`session.${write.field} is owned by ${owners.join(', ')}. Call the owner instead of ` +
`writing the field here, so whatever invariant it carries stays in one place.`,
});
}
}
// An owner that no longer writes its field is stale documentation; drop it so the table
// keeps describing the tree rather than a past version of it.
for (const [field, owners] of Object.entries(SESSION_STATE_FIELD_OWNERS)) {
const actual = seenOwners.get(field) ?? new Set<string>();
const stale = owners.filter((owner) => !actual.has(owner)).sort();
if (stale.length === 0) continue;
violations.push({
rule: 'R7 session-state-ownership',
file: 'scripts/layering/session-state.ts',
line: 1,
message:
`session.${field} is no longer written by ${stale.join(', ')} — remove ` +
`${stale.length === owners.length ? 'the entry' : 'those owners'} from ` +
`SESSION_STATE_FIELD_OWNERS.`,
});
}
return violations;
}
function report(
files: readonly string[],
violations: readonly LayeringViolation[],
ratchets: LayeringRatchets,
reference: MergeBaseRatchets,
): number {
if (violations.length === 0) {
const inversions = Object.values(ratchets.typeInversions).reduce(
(sum, count) => sum + count,
0,
);
const measuredOverlay = handlerOwnedOverlay(ratchets.sessionAuthority);
const handlerOwnedShapeFiles = measuredOverlay.shapeFiles.length;
const handlerOwnedAuthorityFiles = measuredOverlay.authorityFiles.length;
process.stdout.write(
`Layering guard: OK — ${files.length} source files satisfy R2 and contain no ` +
`value-import cycles (both checked globally); the ranked target spine contains no ` +
`back-edges; the ranked spine's type-only inversions hold at or under the merge-base ` +
`${reference.ref.slice(0, 10)} per zone pair (R6, ${inversions} remaining); ` +
`${RETIRED_PATH_RULES.R14.rule} permits no tracked paths under retired src/utils; ` +
`all ${sessionStateFieldCount()} SessionState fields are classified and every write is ` +
`inside its declared owner (R7); the largest type-level cycle is ` +
`${ratchets.largestTypeCycle.length} files (R9); ${daemonModularitySummary(reference)}; ` +
`${packageBoundariesSummary(repoRoot)}; ${platformPackagePolicySummary()}; ` +
`runtime facts remain the only device-command admission authority and daemon code cannot ` +
`manufacture narrowed runtime proof (R66); R65 keeps production src/daemon free of ` +
`concrete platform imports in every executable and type-only form; ` +
`${DAEMON_PLATFORM_RUNTIME_EDGES.length} daemon-to-root platform-runtime edges hold ` +
`their #2278 classification (R76); and the handler-owned SessionState/SessionStore ` +
`authority overlay holds at or under the merge-base (R75, ` +
`${handlerOwnedShapeFiles} shape / ${handlerOwnedAuthorityFiles} authority files).\n`,
);
return 0;
}
const byRule = new Map<string, LayeringViolation[]>();
for (const violation of violations) {
const group = byRule.get(violation.rule) ?? [];
group.push(violation);
byRule.set(violation.rule, group);
}
process.stderr.write(`Layering guard: ${violations.length} violation(s)\n\n`);
for (const [rule, group] of byRule) {
process.stderr.write(` [${rule}] ${group.length} violation(s):\n`);
for (const violation of group) {
process.stderr.write(` ${violation.file}:${violation.line}${violation.message}\n`);
process.stderr.write(
`::error file=${violation.file},line=${violation.line},title=Layering drift (${violation.rule})::${violation.message}\n`,
);
}
process.stderr.write('\n');
}
return 1;
}
/** Everything the guard reads once per run, so a rule takes one argument whatever it needs. */
export type LayeringContext = Readonly<{
sourceFiles: readonly string[];
sources: ReadonlyMap<string, string>;
allTypeScriptSources: ReadonlyMap<string, string>;
trackedSrcUtilsFiles: readonly string[];
edges: readonly ResolvedImportEdge[];
/** The ratcheted measurements of this tree. */
ratchets: LayeringRatchets;
/** The same measurements at the merge-base with origin/main. */
reference: LayeringRatchets;
}>;
export type LayeringRule = (context: LayeringContext) => LayeringViolation[];
/**
* The rules this guard runs. Registering one is writing a key here, which is why the list is data
* rather than a hand-written array of spreads: an object cannot hold the same key twice, so a rule
* cannot be run — and reported, and ::error-annotated — twice by a copy-paste. `LayeringRuleId`
* then makes a missing key a type error rather than a silently retired rule.
*
* Order is the reporting order: report() groups by rule in first-seen order.
*/
export const LAYERING_RULE_IDS = [
'zone-policies',
'value-import-cycles',
'runtime-execution-integrity',
'source-execution-compatibility',
'record-runtime-ownership',
'session-resource-ownership',
'application-lifecycle-ownership',
'contracts-implementation-authority',
'substrate-domain-shape',
'selector-pipeline-ownership',
'back-edges',
'type-spine-inversions',
'session-state-ownership',
'daemon-modularity-ratchets',
'daemon-platform-boundary',
'package-boundaries',
'platform-package-policy',
'apple-runner-host-port',
'retired-platforms-zone',
'src-utils-retirement',
'replay-ownership',
'ios-snapshot-engine-ownership',
'provider-snapshot-presentation-ownership',
'snapshot-assembly-presentation-neutrality',
'daemon-platform-runtime-inventory',
'session-authority-overlay',
] as const;
export type LayeringRuleId = (typeof LAYERING_RULE_IDS)[number];
export const LAYERING_RULES: Readonly<Record<LayeringRuleId, LayeringRule>> = {
'zone-policies': (context) => checkLayeringRules(context.edges),
'value-import-cycles': (context) => checkCycles(context.edges),
'runtime-execution-integrity': (context) => runtimeExecutionIntegrityViolations(context.sources),
'source-execution-compatibility': (context) =>
sourceExecutionCompatibilityViolations(context.sources),
'record-runtime-ownership': (context) => checkRecordRuntimeOwnership(context.sources),
'session-resource-ownership': (context) => sessionResourceOwnershipViolations(context.sources),
'application-lifecycle-ownership': (context) =>
applicationLifecycleOwnershipViolations(context.sources),
'contracts-implementation-authority': (context) =>
checkContractsImplementationAuthority(context.sources),
'substrate-domain-shape': (context) =>
substrateDomainShapeViolations(
[...context.allTypeScriptSources].map(([path, source]) => ({ path, source })),
),
'selector-pipeline-ownership': (context) =>
selectorPipelineOwnershipViolations(context.edges, workspaceSpecifierTargets(repoRoot)),
'back-edges': (context) => checkBackEdges(context.edges),
'type-spine-inversions': (context) =>
checkTypeInversions(context.edges, context.reference.typeInversions),
'session-state-ownership': (context) => checkSessionStateOwnership(context.sources),
'daemon-modularity-ratchets': (context) => [
...checkDaemonModularityRatchets(context.edges, context.ratchets, context.reference),
...checkRetiredSessionLifecyclePaths(context.sourceFiles),
...checkRetiredSessionObservabilityPaths(context.sourceFiles),
...checkRetiredSnapshotExecutionPaths(context.sourceFiles),
...checkRetiredInteractionPaths(context.sourceFiles),
],
'daemon-platform-boundary': (context) =>
checkDaemonPlatformBoundary([...context.sources].map(([path, source]) => ({ path, source }))),
'package-boundaries': () => checkPackageBoundaries(repoRoot),
'platform-package-policy': (context) =>
checkPlatformPackagePolicy(
context.allTypeScriptSources,
readTrackedPlatformPackageDeclarations(repoRoot),
{ untrackedProductionFiles: listUntrackedProductionTypeScriptFiles(repoRoot) },
),
'apple-runner-host-port': (context) =>
appleRunnerHostPortViolations(context.allTypeScriptSources),
'retired-platforms-zone': () => checkRetiredPlatformsZone(listTrackedPlatformZoneFiles(repoRoot)),
'src-utils-retirement': (context) =>
retiredPathRuleViolations('R14', context.trackedSrcUtilsFiles),
'replay-ownership': (context) => retiredPathRuleViolations('R71', context.sourceFiles),
'ios-snapshot-engine-ownership': (context) =>
iosSnapshotEngineOwnershipViolations(
[...context.sources].map(([path, source]) => ({ path, source })),
),
'provider-snapshot-presentation-ownership': (context) =>
providerSnapshotPresentationViolations(context.sources, context.edges),
'snapshot-assembly-presentation-neutrality': (context) =>
snapshotAssemblyPresentationViolations(context.sources, context.edges),
'daemon-platform-runtime-inventory': (context) =>
checkDaemonPlatformRuntimeInventory(context.edges),
'session-authority-overlay': (context) =>
checkSessionAuthorityOverlay(
context.ratchets.sessionAuthority,
context.reference.sessionAuthority,
),
};
export function main(): number {
const sourceFiles = listSourceFiles();
const sources = readSources(sourceFiles);
const allTypeScriptSources = readSources(listTypeScriptFiles());
const trackedSrcUtilsFiles = listTrackedSrcUtilsFiles(repoRoot);
// One memoizing parser for both trees: every file the merge-base shares with the working tree
// is parsed once, whichever scan reaches it first.
const parse = memoizedImportParser();
const edges = resolveImportEdges(sources, workspaceSpecifierTargets(repoRoot), parse);
// Measured once and threaded: the rules and the success line must report the same numbers.
const ratchets = measureRatchets(sources, edges);
const reference = mergeBaseRatchets(repoRoot, parse);
const context: LayeringContext = {
sourceFiles,
sources,
allTypeScriptSources,
trackedSrcUtilsFiles,
edges,
ratchets,
reference,
};
const violations = Object.values(LAYERING_RULES).flatMap((rule) => rule(context));
return report(sourceFiles, violations, ratchets, reference);
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
process.exit(main());
}