Skip to content

Commit 455229e

Browse files
fix(pm): dispatch-gates derives the CLI command-id, ADR anchor and error-code provenance gates from the diffs that break them (#31254)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XFw5wfsCwD3s72yDS2BVkmT)_ Fixes #30763 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`dispatch-gates --commands` 新增两种「按改动内容判定」的门禁派生,并给已有的错误码派生补上一道门禁:改到含 CLI 命令字面量的源码或 CLI 命令文件时派生 `check:cli-command-ids`;改到被 ADR 按符号锚定的文件(或移走/删掉任何被锚定的文件)时派生 `check:adr-symbol-anchors`;改到含错误码字面量的文件时,除原有的词表门禁外再派生 `check:error-code-provenance`。 - 为什么改:这三道门禁在 CI 里每个 PR 都跑,但本地派生清单从不列出它们,开发者报「全绿」后 CI 才红,已各自付过一轮 CI(PR #30734、#30749、#31214)。 - 风险与回滚:只动 `scripts/pm/` 下的派生工具与其自测,不改任何门禁检查什么,也不动 workflow;最坏情况是多派生一道门禁(多跑几秒)或漏派生(即现状)。回滚即还原本 PR。 - 先看:`scripts/pm/dispatch-gates.mjs` 的 `carriesACliCommandLiteral` 与 `breaksAnAdrAnchor`;`scripts/pm/dispatch-gates.data.mjs` 新增的三条 `CHANGE_KIND_ROWS` 门禁行。 - 两分钟验证:`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack packages/cli/src/commands/migrate/apply.ts packages/services/service-push/src/push-plugin.ts`,预期输出里有 `pnpm check:cli-command-ids` 与 `pnpm check:adr-symbol-anchors`(main 上两者都没有)。 - 需要你做的:无,席位落地。 ## What changes Three lint steps run on every PR (`lint.yml`, no `paths:` filter) over a population no path trigger in `dispatch-gates` reached, so `--commands` never handed them to a card. Each now reaches a card through a change-kind row (`CHANGE_KIND_ROWS`), the same channel the vocabulary, status and durability gates already use: | Gate | Why it was never derived | Now derived when the diff… | |---|---|---| | `check:cli-command-ids` | Its literal population is a `src` path segment in any package (no subtree spelling covers it); its only declared root is `scripts/**`. Its command-module duty spells the commands dir package-relative. | …holds a quoted binary-name literal in a non-test JS/TS file outside the CLI (content), or touches a CLI command module or the CLI manifest (path, so a deleted or renamed command counts). | | `check:adr-symbol-anchors` | Declared population is `docs/adr/**` only; an anchor also reds from the anchored file's end. | …edits a file some ADR anchors by `path#Symbol`, or moves/deletes a file any ADR anchors. The anchored files are read with the gate's own corpus registration and the shared extractor (`scripts/symbol-anchors.mjs`), cross-repo anchors left out. | | `check:error-code-provenance` | Its only declared literals are the ledger file, so it sat in the artifact-roster block, outside `--commands`. | …carries an error-code-shaped value: it joins `check:dispatcher-error-vocabulary` on the existing `error-code-literal` row (same stamp sites, population-disjoint verdicts). | **`check:cli-command-ids`: content, not every diff.** The step is unconditional in CI, but at about 22 s locally it is not cheap (ADR anchors ~7 s, provenance ~6 s), so a for-every-diff family was rejected per the order's precision rule. The literal limb is a net around the gate's reader, not a copy of it: the gate's module loads `typescript` at import time and `dispatch-gates` runs in checkouts with no install, so it is not imported. The self-test holds the net to the gate's own `binNamesOf` and `literalsOn` in a child process. ## Tests New self-test cases in `scripts/pm/dispatch-gates.self-test.mjs`: - **CLI kind:** the PR #30734 shape (a pt-BR fragment opening with the binary word) and the other limbs through an injected reader; negatives: a comment-opening line, jsdoc, unquoted prose, `osx`, test files, `__tests__`, non-JS, CLI source outside its commands dir, no declared CLI. Live: CLI discovered from its manifest, a command module reached, discrimination (424 of 7002). Gate-reader parity in a child process: same binary names, and every file the gate's reader finds a literal in is inside the net (missed: none). Rendering, whole table, runnable union, STALE. - **ADR kind:** symbol-anchored edit/delete hit; file-level-only edit not a hit, its move/delete a hit; unanchored never. Reader against a scratch corpus: symbol plus continuation, file-level, cross-repo and fenced sample left out, empty corpus throws. Live: 150 records, the PR #30749 file reached, discrimination. Rendering, whole table, runnable union, STALE. - **Provenance:** rides the `error-code-literal` row; the #31214 shape (a scenarios-package constant bound to `VALIDATION_FAILED`) derives it through the whole table; the section now carries both code gates. - Census guard: all three are live families. - One existing pin moved: the inherited-population census row 738 → 744 (the import lines above it). **Ablation** (one fast-tier run, both guards removed through `scripts/ablation-replace.mjs`, the first held and the second in run mode, restore proven: blob == HEAD `098f38af9ac9`, `git diff HEAD` empty): - CLI net loses its comment-line skip ⇒ red: "a line that opens as a comment is not a hit", "…nor a jsdoc line". - ADR predicate loses its on-disk check ⇒ red: "an edit to a file anchored only at file level is not a hit". - Also red, in the "more diagnostics" direction: three whole-table section-count pins on live i18n / metadata-form / gate-script paths, because the widened net then fires on comment-only mentions in those files. The three new pins above are the ones that name the guard; every other case stayed green (6 of 2021 red). ## Acceptance notes - **Third reading (#31214), measured at head `813399e1`:** `--commands` on that PR's 17 paths printed 54 commands and not `check:error-code-provenance`; the human rendering listed it under "Artifact rosters" (outside the runnable total). The `error-code-literal` kind already fired on 5 of the paths (`scenario-operation-grade.ts` bound `VALIDATION_FAILED_CODE: ErrorCode = 'VALIDATION_FAILED'`). Same defect class, so fixed here. With this branch's tool on that tree: 56 commands, `+ check:error-code-provenance`, `+ check:cli-command-ids`. Recall of the predicate against the gate's own `--report` at fe97d85: 162 of 162 site files; the gate's any-name constant limb adds 0 files the predicate does not reach. - **Census of the class (not fixed here; for one closing card).** Unconditional `lint.yml` steps over a whole-root population that `--commands` does not derive for a diff outside their home package (probe: `packages/qa/scenarios/src/score/scenario-run-grade.ts`): - Declared WIDE (11), rendered in their own block, never in `--commands`: `check:init-service-contract`, `check:live-db-isolation`, `check:meta-type-normalized`, `check:optional-error-sink`, `check:resume-authority-declared`, `check:route-envelope`, `check:runner-env-posture`, `check:settings-bind-window`, `check:startup-registry-verdict`, `check:verify-stand-in`, `check:wildcard-fallthrough`. - Artifact-roster families whose gate source walks a whole root and that no change kind reaches (7): `check:stack-collection-maps`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`, `check:tenant-chokepoint`, `check:object-def-param-keys`, `check:published-readme-exports`. Read from a walk/`ls-files` signature in each gate's source; population width not measured per family. - Roster families a change kind already reaches: `check:engine-double-contract`, `check:i18n-stale-fill`, `check:error-status-conformance`, and now `check:error-code-provenance`. - Type-check lanes are conditional (affected-selected), so outside this census. - **Not done here:** making `--commands` emit the roster or wide blocks. That is a choice for the seat, put in the report as an open question. - Bounded by the order: no `.github/**` edit, and no change to what any gate checks. ## Verification Gate union derived by `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on this diff (30 families), run on head `2653b6b1` (after merging `origin/main`), reconciled with `--ran`: "30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED (a DERIVED zero …)". <details><summary>30 gates, exit codes captured before any pipe</summary> - `node scripts/check-closing-keyword-parity.mjs` (and `--self-test`), `check-comment-mask-corpus.mjs`, `check-declaration-mirrors.mjs` (and `--self-test`), `check-self-test-wired.mjs` (and `--self-test`), `check-self-test-workflow-commands.mjs` (and `--self-test`), `check-whole-set-label-write.mjs` (and `--self-test`), `ci/plan-test-core-shards.mjs --self-test`: exit 0 each - `pnpm check:agent-test-spelling`, `check:bash32-floor`, `check:ci-filter-parity`, `check:cli-command-ids` (19 s), `check:cross-package-test-inputs`, `check:declared-population-live`, `check:driver-memory-census`, `check:entry-guard`, `check:gitlink-declared`, `check:nul-bytes`, `check:parse-guard`, `check:pnpm-filter-targets`, `check:pnpm-flag-spellings`, `check:ratchet-remedy-authority`, `check:refd-timer-probe`, `check:scripts-symbol-anchors`, `check:watch-hint-literal`: exit 0 each - `pnpm check:pm-dispatch-gates`: exit 0, "✓ dispatch-gates self-test (fast tier): 2021 cases pass; 7 slow section(s) deferred." </details> - Full battery (`node scripts/pm/dispatch-gates.mjs --self-test`, the slow tier CI runs on `main` only): on head `2653b6b1`, "✓ dispatch-gates self-test: 2310 cases pass." (fast 2021 cases in 428 s, slow 289 cases in 1363 s). - `pnpm exec eslint --no-inline-config` on the three files: exit 0. No package touched, so no package `lint`/`test`/`typecheck` is owed. - CI families are read from this PR's check-runs, not re-run here. `skip-changeset`: `scripts/pm/**` ships in no package. --- _Generated by [Claude Code](https://claude.ai/code)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent c09ffc1 commit 455229e

3 files changed

Lines changed: 422 additions & 2 deletions

File tree

‎scripts/pm/dispatch-gates.data.mjs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,30 @@ export const CHANGE_KIND_ROWS = [
10621062
name: 'check:dispatcher-error-vocabulary',
10631063
why: 'it sweeps the non-test TypeScript sources under the package root for every site that stamps an error code, and reports each value the registered vocabulary (StandardErrorCode joined with ERROR_CODE_LEDGER) does not contain — so a code arriving through a quoted literal, a SCREAMING_SNAKE constant, a typeof reference to one, or a template moves it. This is the gate no path derivation can name: it computes its own population from a bare top-level root, which the bare-root ledger records as REFUSE-WIDE at 39% of the tracked tree, so it scores the same quiet silence for every card and #12843 paid a CI round trip for that silence. It needs NO build — a source scan, one pass, whole tree, and it names the file and line. Repair by REGISTERING the code where the vocabulary is declared, never by widening a consumer to tolerate it; reconciliation runs BOTH ways, so a table row whose site is gone fails too, and a pending-registration row whose code has since been registered fails as the discharge it is. ⚠ This lead is deliberately WIDE — it fires on a file that merely carries a code-shaped value, not only one that adds a new one — because the wasted run is one cheap gate and the miss is a CI round trip',
10641064
},
1065+
{
1066+
name: 'check:error-code-provenance',
1067+
why: "the vocabulary gate's population-disjoint sibling: it reads the SAME stamp sites in every package's non-test src, keeps only the codes the vocabulary DOES register, and fails when the stamping package's own owner key in the ADR-0112 ledger does not list the code and no PROVENANCE_WAIVERS entry names the owner that holds it instead. So a registered code stamped in a package that never stamped it before moves it, and so does a constant BOUND to one (a constant is read at its definition, whatever its name, even one that is only ever compared against). No path derivation names it: its only declared literals are the ledger file, so the derivation files it under the artifact rosters and scores it silent for every card, and PR #31214 paid a CI round trip for that silence on a scenarios-package constant. It needs NO build, being one source scan over the whole tree, and it names the package, the code and the site. Repair by listing the code under the stamping package's owner key, or by not stamping it (compare against the catalogue's spelling instead of holding it in a constant); a waiver is a recorded ruling, never a quick fix. ⚠ The lead is this entry's predicate, wide for the vocabulary gate's reason: at fe97d85b it reached all 162 files holding one of the gate's 513 registered-code sites",
1068+
},
1069+
],
1070+
},
1071+
{
1072+
kind: 'adds or edits a file carrying a CLI command-id literal outside the CLI package, or a CLI command module or manifest (judged from CONTENT — no path derivation can name this gate)',
1073+
matches: 'cli-command-literal',
1074+
gates: [
1075+
{
1076+
name: 'check:cli-command-ids',
1077+
why: "every quoted run that opens with a CLI binary name and a space (a command id such as the migrate-apply one, and just as much the Portuguese word for administrators after that binary name in a translated message) in non-test source outside the CLI package must resolve to a real oclif command id or topic; and every module the CLI emits under its commands directory must default-export a command class. So a new or edited literal anywhere in package source moves it, and so does renaming, adding or deleting a command module. No path derivation names it: the literal population is a `src/` path SEGMENT in any package, a shape no subtree spelling covers, and its only declared root is `scripts/**`, so it scored silent for package source and for the commands directory alike, and PR #30734 paid a CI round trip for that silence on a pt-BR string fragment. It needs NO build, about 22 seconds, and it names the file, the line and the literal. Repair by spelling the real command id (or, for prose that merely starts with the binary's word, rephrasing so the binary name is not the first thing in the quotes); the exemption ledger is for a checker's own fixtures and filed real defects, never a quick fix. Derived by CONTENT rather than for every diff: the gate is unconditional in CI but not cheap enough to name on a card that cannot move it",
1078+
},
1079+
],
1080+
},
1081+
{
1082+
kind: 'edits a file an ADR anchors by symbol, or moves or deletes a file an ADR anchors at all (read from the ADR corpus — no path derivation can name this gate)',
1083+
matches: 'adr-anchored-file',
1084+
gates: [
1085+
{
1086+
name: 'check:adr-symbol-anchors',
1087+
why: "every `path#Symbol` anchor in docs/adr/** must still resolve to a declaration (or a literal) of that name in that file, and every file-level anchor to a tracked file. So removing or renaming an anchored declaration reds it, and so does moving or deleting any anchored file, from a diff that never touches an ADR. No path derivation names it for those diffs: its declared population is docs/adr/** alone, and PR #30749 paid a CI round trip and a landing tier for that silence when it retired a constant ADR-0012 anchored. It needs NO build, about 7 seconds, and it names the record, the line and the anchor. Repair by re-anchoring the ADR where the mechanism now lives, or by keeping the symbol; ⚠ if the ADR SENTENCE has become untrue, that is its own card, and ⛔ an `anchor-exempt` marker is MAINTAINER-ONLY. ⚠ The symbol limb fires on any edit of a symbol-anchored file, not only one that removes the declaration, because the wasted run is one cheap gate and the miss is a CI round trip",
1088+
},
10651089
],
10661090
},
10671091
{

‎scripts/pm/dispatch-gates.mjs‎

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,12 @@ import { GOVERNED_REPOS, HUMAN_MERGE_LINE_THRESHOLD, SELF_REPO_ID, parseNumstat,
456456
// published source and which are its tests — never respelled here (#19936). See
457457
// SUSPECT_TIER_GLOBS for why this predicate and not one of the repo's others.
458458
import { isTestPath } from '../check-undeclared-dep-imports.mjs';
459+
// The ADR anchor gate's corpus registration and the shared extractor it sweeps
460+
// with: the change kind that reads them (#30763) reads exactly the anchors that
461+
// gate judges, never a respelling of them. Both modules are dependency-free, so
462+
// this tool still runs in a checkout with no install.
463+
import { CORPUS as ADR_ANCHOR_CORPUS } from '../check-adr-symbol-anchors.mjs';
464+
import { extractAnchors } from '../symbol-anchors.mjs';
459465
// The hand-written tables — ledgers, marker grammars, the change-kind roster, the tier globs — are
460466
// DATA, and live beside this file; this engine loads them, matches paths against them and derives.
461467
// Every table is re-exported under the name it always had, so no consumer moved.
@@ -10801,6 +10807,200 @@ export function guardsADurabilityCriticalCall(path, readSource = readTrackedSour
1080110807
return new RegExp(`\\b(?:${names.map((n) => n.replace(/\$/g, '\\$')).join('|')})\\b`).test(masked);
1080210808
}
1080310809

10810+
/**
10811+
* Does this path carry a CLI command-id literal outside the CLI package, or sit
10812+
* on a CLI's own command surface? This is the content trigger for
10813+
* `check:cli-command-ids` (#30763), the fourth CONTENT predicate.
10814+
*
10815+
* ## Why no path derivation names this gate
10816+
*
10817+
* The gate judges two populations. Its literal duty reads every non-test JS/TS
10818+
* source outside each oclif package whose path has a `src` SEGMENT anywhere,
10819+
* plus the whole `scripts` root. Only the second is spelled as a root it can
10820+
* declare, so a card touching package source scored silent. Its command duty
10821+
* reads each CLI's commands directory, which it spells as a package-relative
10822+
* constant no hint can anchor, so a card renaming a command scored silent too.
10823+
* PR #30734 paid a CI round trip for the first silence: a pt-BR string fragment
10824+
* opening with the binary's word, in plugin-auth source.
10825+
*
10826+
* ## Why by content, and not for every diff
10827+
*
10828+
* The step is unconditional in lint.yml, so "always" would be true. It is not
10829+
* cheap: about 22 seconds at fe97d85b, against about 7 for the ADR anchor gate
10830+
* and 6 for the provenance gate. So this predicate reads what the gate reads:
10831+
*
10832+
* - LITERAL: a non-test JS/TS file outside every CLI package holding a quote
10833+
* followed by a declared binary name, a space and a lowercase word or digit.
10834+
* That is a net around the gate's own literal reader, never a copy of it:
10835+
* it skips no comment line and resolves no id, so a shape the gate adds
10836+
* later is already inside it.
10837+
* - SURFACE: any path under a CLI package's commands source directory, and
10838+
* the CLI manifest itself. Judged from the PATH, so a deleted or renamed
10839+
* command module still counts.
10840+
*
10841+
* ⛔ The gate's own reader is NOT imported here: its module loads `typescript`
10842+
* at import time, and this tool runs in checkouts with no install. The
10843+
* self-test holds the net against that reader instead, in a child process: the
10844+
* binary names must agree, and every file in which the gate's reader finds a
10845+
* literal must be inside this net. A narrowing that loses one fails there.
10846+
*
10847+
* Measured at fe97d85b over 7002 non-test JS/TS files outside the CLI package:
10848+
* the gate's population holds a literal in 390 of them, and this net reaches
10849+
* all 390 and 34 more (19 under the `scripts` root the path derivation already
10850+
* names, 15 test helpers and lint scripts outside the gate's population).
10851+
*
10852+
* ## What it cannot see
10853+
*
10854+
* A file with nothing on disk answers false on the literal limb, as in the
10855+
* three predicates above. Deleting a literal can only stale an exemption, and
10856+
* every exempted file sits under the `scripts` root the path derivation
10857+
* already matches.
10858+
*/
10859+
const OCLIF_COMMANDS_SOURCE = ['src', 'commands'].join('/');
10860+
const CLI_POPULATION_EXTENSION = /\.(?:ts|tsx|js|mjs|cjs)$/;
10861+
const CLI_POPULATION_TEST = /\.(?:test|spec)\.[tj]sx?$/;
10862+
const COMMENT_LINE_START = /^\s*(?:\/\/|\*|\/\*)/;
10863+
10864+
/**
10865+
* The binary names a CLI manifest declares for itself: `oclif.bin` plus every
10866+
* `bin` key. The gate's own rule as its header states it, read wide (no name
10867+
* shape filter); the self-test holds it equal to the gate's reader.
10868+
*/
10869+
export function cliBinaryNames(pkg) {
10870+
const bin = pkg?.oclif?.bin;
10871+
if (typeof bin !== 'string' || bin === '') return [];
10872+
const keys = pkg.bin && typeof pkg.bin === 'object' && !Array.isArray(pkg.bin) ? Object.keys(pkg.bin) : [];
10873+
return [...new Set([bin, ...keys])];
10874+
}
10875+
10876+
let cliSurfaces = null;
10877+
/**
10878+
* Every CLI package in the tree as `{ dir, bins }`, read from the tracked
10879+
* manifests that declare `oclif.bin` (the gate's rule for what a CLI is),
10880+
* memoised. A manifest that does not parse is skipped, as the gate skips one.
10881+
*/
10882+
export function cliCommandSurfaces(files = null) {
10883+
if (!files && cliSurfaces) return cliSurfaces;
10884+
const surfaces = [];
10885+
for (const file of files ?? trackedFiles()) {
10886+
if (!/(?:^|\/)package\.json$/.test(file)) continue;
10887+
let pkg;
10888+
try {
10889+
pkg = JSON.parse(readFileSync(nodePath.join(ROOT, file), 'utf8'));
10890+
} catch {
10891+
continue;
10892+
}
10893+
const bins = cliBinaryNames(pkg);
10894+
if (bins.length > 0) surfaces.push({ dir: nodePath.posix.dirname(file), bins });
10895+
}
10896+
if (!files) cliSurfaces = surfaces;
10897+
return surfaces;
10898+
}
10899+
10900+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10901+
10902+
export function carriesACliCommandLiteral(path, readSource = readTrackedSource, surfaces = cliCommandSurfaces) {
10903+
const rel = path.replace(/^\.\//, '');
10904+
const clis = surfaces();
10905+
const own = clis.find((c) => c.dir !== '.' && rel.startsWith(`${c.dir}/`));
10906+
if (own) return rel === `${own.dir}/package.json` || rel.startsWith(`${own.dir}/${OCLIF_COMMANDS_SOURCE}/`);
10907+
if (!CLI_POPULATION_EXTENSION.test(rel)) return false;
10908+
if (CLI_POPULATION_TEST.test(rel) || rel.includes('/__tests__/')) return false;
10909+
const bins = [...new Set(clis.flatMap((c) => c.bins))];
10910+
// An empty alternation would match a quote and a space anywhere, so no
10911+
// declared binary must answer false rather than reach every file.
10912+
if (bins.length === 0) return false;
10913+
const source = readSource(rel);
10914+
if (source === null || source === undefined) return false;
10915+
const literal = new RegExp(`['"\`](?:${bins.map(escapeRegExp).join('|')}) [a-z0-9]`);
10916+
if (!literal.test(source)) return false;
10917+
// A line that OPENS as a comment is prose the gate does not read, and jsdoc
10918+
// that cites a command in backticks is most of what the net would catch
10919+
// otherwise. A trailing comment on a code line stays in, as it does there.
10920+
return source.split('\n').some((line) => !COMMENT_LINE_START.test(line) && literal.test(line));
10921+
}
10922+
10923+
/**
10924+
* Would a diff on this path break an ADR anchor? This is the trigger for
10925+
* `check:adr-symbol-anchors` (#30763), read from the ADR corpus rather than
10926+
* from the path's own content.
10927+
*
10928+
* ## Why no path derivation names this gate
10929+
*
10930+
* The gate's declared population is the ADR directory, and that is all a card
10931+
* touching an ADR needs. But an anchor reds from the OTHER end too: a
10932+
* `path#Symbol` anchor reds when its file stops declaring the symbol, and any
10933+
* anchor reds when its file moves or goes. PR #30749 retired a constant an ADR
10934+
* anchored and learned of it in CI, because its diff touched no ADR.
10935+
*
10936+
* ## What it matches
10937+
*
10938+
* The anchors are extracted by the shared extractor, with the gate's own corpus
10939+
* registration (its roots, document pattern, projection and grammar options),
10940+
* so this reads exactly the anchors the gate sweeps. Cross-repo anchors are
10941+
* left out, as the gate leaves them out of its red verdicts.
10942+
*
10943+
* - SYMBOL: a path some `path#Symbol` anchor names. Any edit counts, not only
10944+
* one that removes the declaration: deciding that needs the old and new
10945+
* text, and the wasted run is one seven-second gate.
10946+
* - FILE: a path any anchor names that has nothing on disk. That is a move or
10947+
* a delete, the only way a file-level anchor can red.
10948+
*
10949+
* Measured at fe97d85b: 3204 anchors across 150 records, naming 292 files by
10950+
* symbol and 1281 more at file level only.
10951+
*/
10952+
let adrTargets = null;
10953+
export function adrAnchorTargets(corpus = ADR_ANCHOR_CORPUS, root = ROOT) {
10954+
if (corpus === ADR_ANCHOR_CORPUS && root === ROOT && adrTargets) return adrTargets;
10955+
const symbolFiles = new Set();
10956+
const anchoredFiles = new Set();
10957+
const docs = [];
10958+
const walkDocs = (rel) => {
10959+
let entries;
10960+
try {
10961+
entries = readdirSync(nodePath.join(root, rel), { withFileTypes: true });
10962+
} catch {
10963+
return;
10964+
}
10965+
for (const e of entries) {
10966+
const child = `${rel}/${e.name}`;
10967+
if (e.isDirectory()) {
10968+
if (!(corpus.excludeDirs ?? []).includes(e.name)) walkDocs(child);
10969+
} else if (corpus.docPattern.test(e.name)) docs.push(child);
10970+
}
10971+
};
10972+
for (const docRoot of corpus.docRoots) walkDocs(docRoot);
10973+
if (docs.length === 0) {
10974+
throw new Error(
10975+
`dispatch-gates: the ADR anchor corpus (${corpus.docRoots.join(', ')}) yielded no document, so the anchored files ` +
10976+
'cannot be read. The corpus moved: re-point this reader with it',
10977+
);
10978+
}
10979+
for (const doc of docs) {
10980+
const raw = readFileSync(nodePath.join(root, doc), 'utf8');
10981+
const { anchors } = extractAnchors(corpus.docProjection ? corpus.docProjection(raw) : raw, {
10982+
unspannedAnchors: corpus.unspannedAnchors ?? false,
10983+
pathlessLineCitations: corpus.pathlessLineCitations ?? false,
10984+
tildeRangeCitations: corpus.tildeRangeCitations ?? false,
10985+
});
10986+
for (const a of anchors) {
10987+
if (a.repo || !a.path) continue;
10988+
anchoredFiles.add(a.path);
10989+
if (a.symbol) symbolFiles.add(a.path);
10990+
}
10991+
}
10992+
const targets = { symbolFiles, anchoredFiles, docs: docs.length };
10993+
if (corpus === ADR_ANCHOR_CORPUS && root === ROOT) adrTargets = targets;
10994+
return targets;
10995+
}
10996+
10997+
export function breaksAnAdrAnchor(path, targets = adrAnchorTargets, onDisk = (p) => existsSync(nodePath.join(ROOT, p))) {
10998+
const rel = path.replace(/^\.\//, '');
10999+
const { symbolFiles, anchoredFiles } = targets();
11000+
if (symbolFiles.has(rel)) return true;
11001+
return anchoredFiles.has(rel) && !onDisk(rel);
11002+
}
11003+
1080411004
/**
1080511005
* Is this path inside the ROOT package's tsc program — the population the
1080611006
* root's `typecheck:root` script checks (until the root graduated, the
@@ -11117,6 +11317,8 @@ const CHANGE_KIND_PREDICATES = Object.freeze({
1111711317
'error-code-literal': stampsAnErrorCodeLiteral,
1111811318
'http-status-emit': emitsAnHttpStatus,
1111911319
'durability-seam': guardsADurabilityCriticalCall,
11320+
'cli-command-literal': (path) => carriesACliCommandLiteral(path),
11321+
'adr-anchored-file': (path) => breaksAnAdrAnchor(path),
1112011322
'ui-scripts-suite-input': (path) => readByUiScriptsSuite(path),
1112111323
});
1112211324

0 commit comments

Comments
 (0)