mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
eb3fc5b28d
`ignorePatterns: ["packages/**"]` landed in #1494 W0 with the recorded reason "its resolver cannot follow workspace specifiers". That was either wrong at the time or never re-checked: the fallow version has not moved (^2.95.0 then and now) and it resolves @agent-device/* through each package's exports map today. packages/kernel alone exposes 8 subpaths and ~110 exports reachable only via workspace specifiers, and scanning it reports zero findings — a resolver that could not follow the specifier would report all of them. The cost of the ignore is that every package extraction silently removes its code from dead-code analysis. #1589 moved the selector engine into packages/selectors/ and shipped a façade with 15 zero-consumer exports, including `selectorUsesKey`, written in that PR and never called. A follow-up commit removed them by hand; nothing would have caught them. Removing the pattern surfaced 43 findings, driven to zero by deleting the dead code rather than by baselining or excluding it (fallow-baselines/*.json are empty on purpose — the posture is fix-or-document-the-exemption, so a first baseline entry would be a policy change): - 38 are deleted. 24 façade type re-exports whose only claim was that a consumer might one day want to name them — typecheck is green without every one, so the claim was theoretical; 5 façade value re-exports; 9 `export` keywords on symbols used only inside their own file. Every deleted façade symbol comes off scripts/layering/facade-symbols.ts (and ad-replay's inline pin in package-boundaries.test.ts) in the same change, so R11 is narrowed with the façade, never weakened around it. - 4 stale suppressions in src/provider-limrun-runtime.ts existed only because packages/ was invisible. - 5 have consumers analysis genuinely cannot see, and get an `ignoreExports` entry naming the consumer per the existing `comment` convention: four test-tree importers that --production does not walk, and `LimrunIosCommandExecution`, which src/sdk/limrun.ts republishes as agent-device/limrun — its only importer compiles in a temp checkout, so no static edge reaches it. test/integration/limrun-public-types.test.ts is the standing proof that one is real API. Three doc comments named types their façade no longer exports and are corrected rather than left asserting something false — including #1555's claim in session-replay-target-verification.ts that the daemon imports `AdReplayVerifiedTargetGuard` directly. It does not; it reaches that shape through `AdReplayTargetClassification`/`AdReplayDispatchGuard`, which is why the name read as dead. `scripts/maestro-conformance/**` was ignored wholesale to cover its corpus data. Narrowed to `corpus/**`, which un-hides the tooling beside it and turned up one more file-local export (`buildManifest`); regenerate.mjs's importer of `fixtureContentHash` becomes visible, so that needs no exemption at all. scripts/check-affected/model.ts deliberately did not select the `fallow` check for packages/*/src/**, carrying the same stale rationale as a comment. Without that selection the new scope would never run in the affected-driven lane, so the ignore removal would have bought nothing. model.test.ts now pins the selection. Verified: check:fallow and check:production-exports green with packages in scope; full-repo `fallow dead-code` back to its one pre-existing finding; typecheck, layering (R11), lint, format, build, check:package, and the limrun published-types integration test all pass. Probed by adding a fresh zero-consumer export to the xml façade — check:production-exports reports it, so the #1589 case now fails the gate. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
121 lines
5.7 KiB
JavaScript
121 lines
5.7 KiB
JavaScript
#!/usr/bin/env node
|
|
// Rebuilds corpus/manifest.json from the flows on disk. `regenerate.mjs` runs
|
|
// this first, so adding a corpus flow is just: drop the .yaml in, add a note
|
|
// below if it needs one, and regenerate.
|
|
//
|
|
// The manifest is the corpus's provenance record: vendored upstream flows carry
|
|
// their upstream repo path and sha256 so a silently-edited flow is detectable;
|
|
// authored flows say why they exist.
|
|
import { createHash } from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
const CORPUS_DIR = path.join(HERE, 'corpus');
|
|
const UPSTREAM_REPO_DIR = 'maestro-test/src/test/resources';
|
|
|
|
const NOTES = {
|
|
'bug-classes/percent-decimal-swipe': 'Bug class 1: decimal percentage rejection at parse.',
|
|
'bug-classes/target-swipe-missing-direction':
|
|
'Bug class 2: target swipe requires an explicit direction.',
|
|
'bug-classes/retry-over-cap':
|
|
'Bug class 3: retry maxRetries parses verbatim; the clamp is a layer-2 vector.',
|
|
'bug-classes/settle-after-tap':
|
|
'Bug class 4: plain tap; settle ordering is a layer-3 differential.',
|
|
'authored/runscript': 'Coverage: runScript file command (no self-contained upstream flow).',
|
|
'authored/runflow-main': 'Coverage: runFlow file include + provenance.',
|
|
'authored/runflow-child': 'Include target for runflow-main (not parsed as a top-level flow).',
|
|
'authored/doubletap':
|
|
'Coverage: doubleTapOn (upstream 101 uses the unsupported retryTapIfNoChange option).',
|
|
'authored/scroll-until-visible':
|
|
'Coverage: scrollUntilVisible (upstream 079 uses unsupported speed/visibilityPercentage).',
|
|
'authored/extended-wait':
|
|
'Coverage: extendedWaitUntil (upstream 042 interpolates ${TIMEOUT} from a flow env block).',
|
|
'authored/repeat':
|
|
'Coverage: repeat.times (upstream 053 also uses the unsupported evalScript command).',
|
|
'authored/presskey':
|
|
'Coverage: pressKey supported keys (upstream 034 exercises many unsupported keycodes).',
|
|
'authored/numeric-variable-tap':
|
|
'Coverage: tapOn repeat/delay/index numeric option fields accept ${VAR} tokens (upstream rejects repeat/delay as integer-typed; agent-device is lenient).',
|
|
'authored/numeric-variable-doubletap':
|
|
'Coverage: doubleTapOn delay accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).',
|
|
'authored/numeric-variable-swipe':
|
|
'Coverage: swipe duration accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).',
|
|
'authored/numeric-variable-erase':
|
|
'Coverage: eraseText charactersToErase accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).',
|
|
'authored/numeric-variable-tap-index':
|
|
'Coverage: tapOn index accepts a ${VAR} token and projects identically through the canonical model.',
|
|
'authored/numeric-variable-wait':
|
|
'Coverage: waitForAnimationToEnd timeout accepts a ${VAR} token and projects identically through the canonical model.',
|
|
'invalid/bad-swipe-direction': 'Lenient-guard: unknown SwipeDirection enum value.',
|
|
'invalid/unknown-command': 'Lenient-guard: unknown command name (tapOn typo).',
|
|
'invalid/malformed-selector': 'Lenient-guard: selector given as a sequence.',
|
|
'invalid/duplicate-keys': 'Lenient-guard: duplicate YAML mapping keys.',
|
|
'invalid/unknown-selector-field': 'Lenient-guard: unknown field inside a selector map.',
|
|
'invalid/commands-not-a-list': 'Lenient-guard: command document is not a sequence.',
|
|
};
|
|
|
|
const sha256 = (file) => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
|
|
const yamlIn = (dir) =>
|
|
fs.existsSync(path.join(CORPUS_DIR, dir))
|
|
? fs
|
|
.readdirSync(path.join(CORPUS_DIR, dir))
|
|
.filter((n) => n.endsWith('.yaml'))
|
|
.sort()
|
|
: [];
|
|
|
|
function entriesFor(dir, kind) {
|
|
return yamlIn(dir).map((name) => {
|
|
const stem = name.slice(0, -'.yaml'.length);
|
|
const id = `${dir}/${stem}`;
|
|
const entry = { id, file: `${dir}/${name}` };
|
|
if (kind === 'upstream') {
|
|
entry.origin = {
|
|
kind: 'upstream',
|
|
repoPath: `${UPSTREAM_REPO_DIR}/${name}`,
|
|
sha256: sha256(path.join(CORPUS_DIR, dir, name)),
|
|
};
|
|
} else {
|
|
entry.origin = { kind: 'authored', note: NOTES[id] ?? '' };
|
|
}
|
|
if (dir === 'bug-classes') entry.bugClass = stem;
|
|
if (id === 'authored/runflow-child') entry.includeTargetOnly = true;
|
|
return entry;
|
|
});
|
|
}
|
|
|
|
function buildManifest(pin) {
|
|
return {
|
|
description:
|
|
'Corpus of Maestro flows driven through the upstream parser to generate layer-1 fixtures. Upstream flows are vendored verbatim from the pinned commit; sha256 records provenance. Authored flows fill coverage gaps, encode the four #1217 bug classes, and (invalid/) give the never-accept-what-upstream-rejects guard teeth. Generated by build-manifest.mjs — do not hand-edit.',
|
|
upstreamCommit: pin.commit,
|
|
flows: [
|
|
...entriesFor('upstream', 'upstream'),
|
|
...entriesFor('bug-classes', 'authored'),
|
|
...entriesFor('invalid', 'authored'),
|
|
...entriesFor('authored', 'authored'),
|
|
],
|
|
};
|
|
}
|
|
|
|
export function writeManifest(pin) {
|
|
const manifest = buildManifest(pin);
|
|
fs.writeFileSync(
|
|
path.join(CORPUS_DIR, 'manifest.json'),
|
|
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
);
|
|
const missing = manifest.flows.filter((f) => f.origin.kind === 'authored' && !f.origin.note);
|
|
for (const flow of missing) {
|
|
console.warn(`warning: ${flow.id} has no note — add one to NOTES in build-manifest.mjs`);
|
|
}
|
|
return manifest;
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
const pin = JSON.parse(fs.readFileSync(path.join(HERE, 'pinned-upstream.json'), 'utf8'));
|
|
const manifest = writeManifest(pin);
|
|
console.log(`wrote corpus/manifest.json (${manifest.flows.length} flows)`);
|
|
}
|