Files
callstack__agent-device/scripts/maestro-conformance/build-manifest.mjs
Michał Pierzchała eb3fc5b28d chore: scan packages/** with fallow instead of ignoring it (#1591)
`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>
2026-08-04 18:26:03 +02:00

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)`);
}