Skip to content

Commit c739de7

Browse files
objectstack-fleet[bot]os-warrenclaude
authored
tooling(dispatch-gates): the STALE TREE reading keeps only the moved paths the derivation actually opened (#26585)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ87iti5Co7zHGEHxwKnxJVDN)_ Fixes #26334 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`scripts/pm/dispatch-gates.mjs` 的 STALE TREE 读数只再数派生真正打开过的文件(`derivationInputs`,从 discovery 实测:全部 workflow、跟过的 composite action、根 `package.json`、每个门禁族的脚本/import/spawn/按路径读取/包清单,加引擎自身闭包);`scripts/` 下没人读的生成记录(sdui 清单记录、ADR 锚点)移动时不再误报,安静行点名被排除的路径。 - 为什么改:ui/重构落地每次都动 `scripts/` 下的生成记录,pm-only 树因此每次都读到假 STALE TREE(卡上实测派生集 52 不变),警告被忽视就失去意义。 - 风险与回滚:只改读数的过滤,不改派生族、不改任何门禁命令;一个不该排除的输入会漏报——自检用实测集钉住闭包(清单、全部 workflow、每个门禁文件、引擎与其数据模块);回滚即 revert 本 PR 的合并提交。 - 先看:`scripts/pm/dispatch-gates.mjs` 的 `derivationInputs` / `liveDerivationInputs`(排除规则与降级方向)、`baseDrift` 的 `narrowed` hunk、`driftLines` 安静分支;然后 `dispatch-gates.self-test.mjs` 的 base drift 段。 - 两分钟验证:在落后 `origin/main` 且只差一次 ui 落地的 pm 检出跑 `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`,预期 stderr 的 banner 是 "none of the commit(s) this tree can SEE touched … 1 path(s) under the surface moved too, none of them an input of this answer: scripts/sdui-manifest.record.json",不再出现 STALE TREE(`--tier` 不做派生,仍列原始清单并注明未收窄)。 - 需要你做的:无,席位落地。 ## What changes `DERIVATION_SURFACE` stays the pathspec roots the staleness diff is scoped to (`.github/workflows`, `.github/actions`, `package.json`, `scripts`). What is new is that the moved paths under those roots are **narrowed to the files the derivation actually opened**, measured rather than listed — the "narrowed surface" option of the two the order named, chosen because its self-test can prove the negative from the discovery itself instead of from a hand list that would rot the next time a generated record lands under `scripts/`. - `derivationInputs()` (new, exported): the measured set — every workflow file, every composite action the follow opened, the root `package.json`, and for every discovered family its gate files, first-party imports, spawned programs, program-text reads and package manifests, plus the engine's own module closure (this file, its first-party imports — the data module two levels down from every family — and the battery it loads by `import()`). Memoised per discovery, which is memoised per tree, so a derivation that already discovered pays nothing more. - `narrowToDerivationInputs(moved, inputs)` (new, exported): the pure split into `changed` (inputs) and `ignored` (under the roots, not inputs). `inputs` null keeps everything — the superset reading. - `baseDrift({ cwd, inputs })`: reads the same three-dot diff, then narrows it by the `inputs` its CALLER names — a set, or a function asked only once something moved under the roots. The reading gains an `ignored` field beside `changed` (`null` = not narrowed), and only `changed` decides loud vs quiet. The CLI's deriving modes (`--commands`, `--ran`, the human list) hand in `liveDerivationInputs`, which costs nothing extra because the derivation discovers the same tree through the same memo; `--tier` reads no workflow by contract, so it hands in nothing and its loud reading says it was not narrowed. A first draft let `baseDrift` discover on its own: a battery of `--tier` children on a stale tree then paid a ~14 s discovery each and the full self-test ran 24 minutes — so the choice moved to the caller. A discovery that throws degrades to no narrowing (over-report, never under-report). - `driftLines`: the quiet branch names what it set aside (`N path(s) under the surface moved too, none of them an input of this answer: …`, first three named), so the quiet reads as a measurement and not as the silence a level tree gets. A reading that set nothing aside renders byte-identically to before. - Derived families and gate commands: untouched — no discovery code changed; `--commands` derives the same 30 families on a51cd08 and on 4b203a9. The card's named specimen, `scripts/sdui-manifest.sources/**`, had already been retired on `main` by e931621 (2026-10-27T01:15Z, #26367) before the seat's premise check — see Acceptance notes — so the live specimens are `scripts/sdui-manifest.record.json` (moves on every ui landing that regenerates the manifest) and `scripts/adr-anchors/*.json` (moves on refactor landings); the fix closes the class rather than the one path. ## Tests `node scripts/pm/dispatch-gates.mjs --self-test` (both tiers) on head 4b203a9: `✓ dispatch-gates self-test: 2224 cases pass.` (`fast 1935 case(s) in 353.9s · slow 289 case(s) in 974.3s`). ⚠️ The base-drift section is in the SLOW tier, and CI runs only the fast tier on `pull_request` / `merge_group` (`scripts/pm/check-dispatch-gates.mjs`, #22991) — so this PR's check-runs do not exercise the new temporary-repository and LIVE cases; this local two-tier run is their evidence, and the hourly `schedule` run is the first CI run that will. The full run also caught two slow-tier-only ordering bugs in the first draft of these cases (a case reading a fixture above its declaration); fixed in 9a12ebe and 4b203a9, and every identifier the new cases read was then checked declared-before-read. New cases in the base-drift section, one per direction the order asked for and the live pins the measured set needs: - FRESH — temporary repository: upstream regenerates `scripts/sdui-manifest.sources/core.json` under the roots; the raw reading reports it (the departure pin: what used to go loud); narrowed by the inputs the same tree reads `changed: []`, `ignored: [that record]`, and renders the quiet two-line shape naming the set-aside record, no `STALE TREE`; a foreign checkout handed no inputs is not narrowed. - STALE — same repository: a changed `scripts/check-thing.mjs` still goes loud and is named alone (`Stale here: scripts/check-thing.mjs`) with the record still set aside; a workflow (`.github/workflows/lint.yml`) and the root `package.json` moving upstream go loud, both named. - LIVE pins on this tree's measured set: every input is a tracked file; it holds the root manifest and every workflow file; this engine, its data module and its battery; every gate file of every discovered family; it is a strict subset of the tracked files under `scripts/` (265 of 442 on 4b203a9), so the narrowing is not vacuous; `liveDerivationInputs()` answers that same set; and `scripts/sdui-manifest.record.json` is tracked and NOT an input. - Mode split: a level tree never asks for the inputs (a throwing function proves it is not called); inputs handed in as a function read identically to the set; an un-narrowed loud reading says `Not narrowed to what the answer is made of … A --commands or --ran run measures the inputs and lists only those.`, a narrowed one does not. - Renderings: the quiet line with set-aside paths (counted past the third), byte-identity when nothing was set aside, the pure split's order. ## Acceptance notes - **Premise, measured** (os-dev rule 6): the card's trigger directory `scripts/sdui-manifest.sources/` does not exist on `main` — retired by e931621 (2026-10-27T01:15Z, "retire the per-package SDUI manifest records", #26367), after the card was filed (2026-10-26T21:5xZ) and before the seat's read-only premise check at b53322d (`git ls-tree b53322d scripts/` lists `scripts/sdui-manifest.record.json`, `check-sdui-manifest.mjs`, `gen-sdui-manifest-node.mjs` and no `sdui-manifest.sources`). The mechanism the card names holds unchanged: the whole `scripts/` root was counted, and generated records under it still move on landings in other lanes — `scripts/sdui-manifest.record.json` at 1bbb7bf (ui), `scripts/adr-anchors/*.json` at b7f8a2e, 2e7127b (refactor) — and the derivation opens none of them (measured: 265 of 442 tracked `scripts/**` files are inputs on 4b203a9). The card's "Wanted" is met for the class; `premise_still_valid: true` with that correction. - **Inputs outside the roots** (observation, not fixed here): the measured set holds 43 files outside `DERIVATION_SURFACE` — gate scripts resolved through package manifests (`packages/spec/scripts/check-*.ts`, `packages/lint/scripts/*.mjs`), `packages/spec/package.json`, and program-text reads such as `.claude/skills/pm-dispatch/SKILL.md`. The diff stays scoped to the roots, as the ruling's "nothing wider" asks, so an upstream change to one of those still reads as off-surface, exactly as before this PR. Widening the diff to the measured set would be the honest next step; it is a separate decision. - The `fix:` remedy in the third unmeasured door still prints the raw `git diff --name-only … -- ROOTS` command: it is the superset a reader can run by hand; the narrowing is applied on top of it and is not something a shell one-liner can reproduce. - A changed `dispatch-gates.self-test.mjs` upstream counts as an input (the engine closure includes the battery) — a conservative choice: it does not move the answer, but `check:pm-dispatch-gates` runs it, and over-reporting is this file's degraded direction. ## Verification Three real landings replayed on throwaway checkouts (a `--shared` clone of this worktree with `refs/remotes/origin/main` pinned at the landing and HEAD at its parent; "before" = that tree's own engine, `--tier`; "after" = this PR's engine module closure committed on top, run in a deriving mode, `--commands`): <details><summary>1bbb7bf4 — a ui landing whose only movement under the roots is <code>scripts/sdui-manifest.record.json</code> (the card's class)</summary> Before: ``` ⚠️ STALE TREE — this answer is derived from a tree at least 1 commit(s) behind origin/main, and 1 file(s) it derives from CHANGED across that range. Stale here: scripts/sdui-manifest.record.json ``` After: ``` At least 1 commit(s) behind origin/main, and none of the commit(s) this tree can SEE touched what this answer derives from — HEAD 2026-10-26T12:40:29+00:00 vs origin/main 1bbb7bf 2026-10-26T12:45:21+00:00. 1 path(s) under the surface moved too, none of them an input of this answer: scripts/sdui-manifest.record.json. ``` </details> <details><summary>b7f8a2eb — a refactor landing whose only movement under the roots is an ADR anchor record</summary> Before: `STALE TREE … 1 file(s) it derives from CHANGED` / `Stale here: scripts/adr-anchors/packages__objectql__src__engine-execute-transactions.ts.json`. After: `none of the commit(s) this tree can SEE touched what this answer derives from … 1 path(s) under the surface moved too, none of them an input of this answer: scripts/adr-anchors/packages__objectql__src__engine-execute-transactions.ts.json.` </details> <details><summary>e931621a — the sentinel retirement itself: two workflows and three gate scripts moved beside 27 records (the control that must stay STALE)</summary> Before: `STALE TREE … 32 file(s) it derives from CHANGED` / `Stale here: .github/workflows/ci.yml .github/workflows/lint.yml scripts/check-changeset-no-major.mjs scripts/check-sdui-manifest.mjs scripts/gen-sdui-manifest-node.mjs scripts/sdui-manifest.record.json … +26 more`. After: `STALE TREE … 5 file(s) it derives from CHANGED` / `Stale here: .github/workflows/ci.yml .github/workflows/lint.yml scripts/check-changeset-no-major.mjs scripts/check-sdui-manifest.mjs scripts/gen-sdui-manifest-node.mjs` — the verdict for a real derivation-input change is unchanged; only the records left the list. </details> The `--ran` reconciliation on this branch, head 4b203a9 (no drift line: the tree was level with the fetched `origin/main` when it ran): <details><summary>Derived gates — 30 families, run locally on 4b203a9, reconciled with <code>--ran</code></summary> ``` node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs --self-test :: exit 0 node scripts/check-self-test-wired.mjs :: exit 0 node scripts/check-self-test-wired.mjs --self-test :: exit 0 node scripts/check-self-test-workflow-commands.mjs :: exit 0 node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0 node scripts/check-whole-set-label-write.mjs :: exit 0 node scripts/check-whole-set-label-write.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 pnpm check:agent-test-spelling :: exit 0 pnpm check:bash32-floor :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:cli-command-ids :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:declared-population-live :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:entry-guard :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:parse-guard :: exit 0 pnpm check:pnpm-filter-targets :: exit 0 pnpm check:pnpm-flag-spellings :: exit 0 pnpm check:ratchet-remedy-authority :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:scripts-symbol-anchors :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:pm-dispatch-gates :: exit 0 Run reconciliation — 30 derived, 30 run, 0 NOT-MEASURED, 0 UNRUN. ✓ dispatch-gates --ran: 30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED (a DERIVED zero — all 30 recorded an exit code and none of them is 3). ``` </details> `check:dual-build-cjs-loads` is not in this card's derived list. `--tier` on each throwaway tree now prints the raw list plus the not-narrowed line (e.g. 32 files on e931621), by design. No package is touched (no ① closure, no ② package suite); `pnpm lint` as a repo-wide sweep is CI's run — `eslint scripts/pm/dispatch-gates.mjs scripts/pm/dispatch-gates.self-test.mjs` exits 0 locally. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (n/a — no schema touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (eslint on the two files, exit 0; the repo sweep is CI's) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (`scripts/pm/**` publishes nothing — `skip-changeset`) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (n/a) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JQwxbZ5vD35MzQ8UqhhTJB --------- Co-authored-by: os-warren <warren@objectstack.ai> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0b5ab8b commit c739de7

2 files changed

Lines changed: 277 additions & 15 deletions

File tree

‎scripts/pm/dispatch-gates.mjs‎

Lines changed: 168 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15451,15 +15451,117 @@ export function repoIdentity({ cwd = ROOT } = {}) {
1545115451
}
1545215452

1545315453
/**
15454-
* The files a derivation's ANSWER is made of — the ones whose staleness can
15455-
* change it. The gate inventory is read from the workflow files, `check:*` is
15456-
* resolved through the manifest, and the checks themselves live under
15457-
* `scripts/`. Everything else in the tree can be arbitrarily old without moving
15458-
* a single family, which is what makes this list the right filter and raw
15459-
* commit distance the wrong one.
15454+
* Where the files a derivation's ANSWER is made of LIVE — the pathspec roots
15455+
* the staleness diff is scoped to. The gate inventory is read from the
15456+
* workflow files, `check:*` is resolved through the manifest, and the checks
15457+
* themselves live under `scripts/`. Everything outside these roots can be
15458+
* arbitrarily old without moving a single family, which is what makes the
15459+
* roots the right filter and raw commit distance the wrong one.
15460+
*
15461+
* Roots, not the surface itself (#26334). `scripts/` also holds generated
15462+
* records nothing derives from — the SDUI manifest record, the ADR anchor
15463+
* files — and those move on landings in other lanes, so a diff scoped to the
15464+
* roots ALONE named them as "files it derives from" on every such landing: a
15465+
* STALE TREE over a pm-only tree whose derived set had not moved, which is the
15466+
* warning-on-every-honest-run shape the paragraph under `baseDrift` prices.
15467+
* Which files under the roots the answer is really made of is MEASURED, never
15468+
* listed beside it: `derivationInputs` below, and `baseDrift` keeps only the
15469+
* moved paths that are in it.
1546015470
*/
1546115471
export const DERIVATION_SURFACE = ['.github/workflows', '.github/actions', 'package.json', 'scripts'];
1546215472

15473+
/**
15474+
* This engine's own module closure — the file itself, the first-party modules
15475+
* it imports, and the battery it loads by `import()` — spelled from this
15476+
* file's own URL, so a rename moves it. The discovery follows a gate's imports
15477+
* ONE level, and this file is reached as an import or a spawn target of the
15478+
* gates that use it, never as a gate of its own, so its data module sits two
15479+
* levels down from every family: a tree whose `CHANGE_KIND_GATES` row moved
15480+
* upstream would otherwise read FRESH.
15481+
*/
15482+
function engineClosure() {
15483+
const self = nodePath.relative(ROOT, new URL(import.meta.url).pathname);
15484+
const battery = nodePath.relative(ROOT, new URL('./dispatch-gates.self-test.mjs', import.meta.url).pathname);
15485+
return [self, battery, ...firstPartyImportTargets(self, readFileSync(nodePath.join(ROOT, self), 'utf8'))];
15486+
}
15487+
15488+
const derivationInputsMemo = new WeakMap();
15489+
15490+
/**
15491+
* The files this checkout's derivation ACTUALLY opens — the honest derivation
15492+
* surface, measured from the discovery rather than declared beside it.
15493+
*
15494+
* Every workflow file (the discovery reads all of them), every composite
15495+
* action the follow opened, the root manifest, and for every family its gate
15496+
* files, the modules it imports, the programs it spawns, the files it opens by
15497+
* path and the package manifests it resolves through — plus this engine's own
15498+
* closure. A path under `DERIVATION_SURFACE` that is in none of those is a
15499+
* path the answer was not made of, and a change to it upstream cannot have
15500+
* moved a family: a change that WOULD (a new gate, a new import, a new read)
15501+
* has to arrive through a file that IS in the set — the manifest, a workflow,
15502+
* or the gate that gained the edge — so the set is closed under the answer it
15503+
* describes. Measured on this tree: 266 of 443 tracked files under `scripts/`
15504+
* are inputs; the rest can move without moving a family.
15505+
*
15506+
* Memoised per discovery, which is itself memoised per tree, so a derivation
15507+
* that already discovered pays nothing more for it — and that is the ONLY
15508+
* price this may have. A discovery costs seconds (measured: ~14 s cold on
15509+
* this tree), so the modes that derive hand it to `baseDrift` and `--tier`,
15510+
* which reads no workflow by contract, does not: a battery of `--tier`
15511+
* children on a tree a few commits behind was measured to quadruple its wall
15512+
* clock when the banner asked for a discovery the mode never needed.
15513+
*/
15514+
export function derivationInputs({ tree = repoCorpus().tree } = {}) {
15515+
const discovered = discoverFamilies({ tree });
15516+
const hit = derivationInputsMemo.get(discovered);
15517+
if (hit !== undefined) return hit;
15518+
const inputs = new Set(['package.json', ...engineClosure()]);
15519+
for (const wf of discovered.workflows) inputs.add(`.github/workflows/${wf}`);
15520+
for (const action of discovered.compositeActions) inputs.add(action);
15521+
for (const entry of discovered.byCheck.values()) {
15522+
for (const list of [entry.files, entry.imports, entry.runs, entry.reads, entry.manifests]) {
15523+
for (const rel of list ?? []) inputs.add(rel);
15524+
}
15525+
}
15526+
derivationInputsMemo.set(discovered, inputs);
15527+
return inputs;
15528+
}
15529+
15530+
/**
15531+
* Split the paths a diff reported under the roots into the ones the answer is
15532+
* made of and the ones it is not. `inputs` null = NOT narrowed: every moved
15533+
* path is kept and `ignored` is `null` (no reading was taken, which is not an
15534+
* empty one — the same distinction `changed` draws), the only reading a
15535+
* checkout with no discovery of its own (a temporary repository in the
15536+
* battery) or a mode that reads no workflow (`--tier`) can honestly take: a
15537+
* superset, so it over-reports and never under-reports.
15538+
*/
15539+
export function narrowToDerivationInputs(moved, inputs) {
15540+
if (inputs === null || inputs === undefined) return { changed: [...moved], ignored: null };
15541+
const changed = [];
15542+
const ignored = [];
15543+
for (const rel of moved) (inputs.has(rel) ? changed : ignored).push(rel);
15544+
return { changed, ignored };
15545+
}
15546+
15547+
/**
15548+
* The inputs a DERIVING mode hands `baseDrift`: this checkout's own measured
15549+
* set, behind a function so it is asked for only once something moved under
15550+
* the roots — a level tree never pays for it, and a deriving mode pays for it
15551+
* once, since the derivation that follows the banner discovers the same tree
15552+
* through the same memo. A discovery that refuses degrades to no narrowing
15553+
* (the raw roots, a superset): the reading over-reports, which is this file's
15554+
* direction for every degraded read, and the derivation then refuses loudly
15555+
* on its own.
15556+
*/
15557+
export function liveDerivationInputs() {
15558+
try {
15559+
return derivationInputs();
15560+
} catch {
15561+
return null;
15562+
}
15563+
}
15564+
1546315565
/**
1546415566
* How far behind `DEFAULT_BASE_REF` this checkout is — and whether that matters.
1546515567
*
@@ -15491,6 +15593,24 @@ export const DERIVATION_SURFACE = ['.github/workflows', '.github/actions', 'pack
1549115593
* make the true number bigger, never smaller — which is what lets this stay
1549215594
* honest without the derivation reaching for the network.
1549315595
*
15596+
* The CHANGED SET is NARROWED to what the answer is made of (#26334). The
15597+
* diff is scoped to `DERIVATION_SURFACE`, and `scripts/` is a root that also
15598+
* holds generated records nothing derives from, regenerated by landings in
15599+
* other lanes; reported raw, every such landing read as a STALE TREE over a
15600+
* tree whose families had not moved — the warning-nobody-reads shape the
15601+
* paragraph above was written to avoid, arriving by another route. So the
15602+
* moved paths are split against `derivationInputs` — the files the discovery
15603+
* really opened, measured, never listed — into `changed` (inputs) and
15604+
* `ignored` (under the roots, not inputs), and only `changed` decides between
15605+
* loud and quiet. The CALLER names `inputs` — a set, or a function asked for
15606+
* one only once something moved — because measuring it is a discovery, and
15607+
* which modes may pay for one is a fact about the mode: the deriving modes
15608+
* hand in `liveDerivationInputs` and pay nothing extra, `--tier` reads no
15609+
* workflow by contract and hands in nothing, and a battery driving a temporary
15610+
* repository (no discovery of its own) hands in the set it means. `null` keeps
15611+
* every moved path with `ignored: null`, the superset reading, and the loud
15612+
* rendering says that reading was not narrowed.
15613+
*
1549415614
* The CHANGED SET inherits that lower-bound property, and for a while nothing
1549515615
* said so (#13392). `changed` is diffed against the same possibly-stale
1549615616
* snapshot the count is, so files-changed-vs-upstream is a SUPERSET of what
@@ -15529,7 +15649,7 @@ export const DERIVATION_SURFACE = ['.github/workflows', '.github/actions', 'pack
1552915649
* not read, and only an ARRAY when it was. `unmeasuredDrift` below is the
1553015650
* single predicate all three degraded fields are read through.
1553115651
*/
15532-
export function baseDrift({ cwd = ROOT } = {}) {
15652+
export function baseDrift({ cwd = ROOT, inputs = null } = {}) {
1553315653
const read = (args) => {
1553415654
try {
1553515655
const r = runGit(args, cwd);
@@ -15539,19 +15659,32 @@ export function baseDrift({ cwd = ROOT } = {}) {
1553915659
}
1554015660
};
1554115661
const base = read(['rev-parse', '--short', DEFAULT_BASE_REF]);
15542-
if (base === null) return { base: null, behind: null, changed: null, headDate: null, baseDate: null };
15662+
if (base === null) return { base: null, behind: null, changed: null, ignored: null, headDate: null, baseDate: null };
1554315663
const counted = read(['rev-list', '--count', `HEAD..${DEFAULT_BASE_REF}`]);
1554415664
const behind = /^\d+$/.test(counted ?? '') ? Number(counted) : null;
1554515665
// At `behind: 0` the empty set is exact BY CONSTRUCTION — an empty commit
1554615666
// range moves no files — so it is a reading without running the diff. With
1554715667
// no distance in hand there is no range to read, so `changed` is unread too.
1554815668
const names = behind ? read(['diff', '--name-only', `HEAD...${DEFAULT_BASE_REF}`, '--', ...DERIVATION_SURFACE]) : behind === 0 ? '' : null;
15669+
// `null` = the diff was NOT read (a failed read is not an empty one);
15670+
// an array — even empty — = the diff ran and this is what it said.
15671+
const moved = names === null ? null : names.split('\n').filter(Boolean);
15672+
// Narrowed to the inputs ONLY once the diff is in hand and non-empty: an
15673+
// unread diff has nothing to narrow, an empty one is not worth a discovery,
15674+
// and `ignored` degrades with `changed` so the two never describe different
15675+
// readings.
15676+
const narrowed = moved === null
15677+
? null
15678+
: narrowToDerivationInputs(moved, typeof inputs === 'function' ? (moved.length > 0 ? inputs() : null) : inputs);
1554915679
return {
1555015680
base,
1555115681
behind,
15552-
// `null` = the diff was NOT read (a failed read is not an empty one);
15553-
// an array — even empty — = the diff ran and this is what it said.
15554-
changed: names === null ? null : names.split('\n').filter(Boolean),
15682+
changed: narrowed === null ? null : narrowed.changed,
15683+
// The moved paths under the roots that are NOT inputs — set aside, never
15684+
// dropped: the quiet rendering names them so a reader can see the quiet
15685+
// was measured rather than assumed. `null` = not narrowed (no inputs were
15686+
// named, or the diff was not read), never an empty array.
15687+
ignored: narrowed === null ? null : narrowed.ignored,
1555515688
headDate: read(['log', '-1', '--format=%cI', 'HEAD']),
1555615689
baseDate: read(['log', '-1', '--format=%cI', DEFAULT_BASE_REF]),
1555715690
};
@@ -15688,11 +15821,19 @@ export function driftLines(drift) {
1568815821
];
1568915822
}
1569015823
if (!drift.behind) return [];
15691-
const { behind, base, changed, headDate, baseDate } = drift;
15824+
const { behind, base, changed, ignored, headDate, baseDate } = drift;
1569215825
const span = `HEAD${headDate ? ` ${headDate}` : ''} vs ${DEFAULT_BASE_REF} ${base}${baseDate ? ` ${baseDate}` : ''}`;
1569315826
if (changed.length === 0) {
15827+
// What moved under the roots and was set aside as not an input (#26334),
15828+
// named, so the quiet line reads as a measurement and not as the silence
15829+
// a level tree gets. Absent or empty prints nothing: a reading that set
15830+
// nothing aside renders byte-identically to one taken before the field
15831+
// existed.
15832+
const setAside = Array.isArray(ignored) && ignored.length > 0
15833+
? ` ${ignored.length} path(s) under the surface moved too, none of them an input of this answer: ${ignored.slice(0, 3).join(' ')}${ignored.length > 3 ? ` … +${ignored.length - 3} more` : ''}.`
15834+
: '';
1569415835
return [
15695-
` At least ${behind} commit(s) behind ${DEFAULT_BASE_REF}, and none of the commit(s) this tree can SEE touched what this answer derives from — ${span}.`,
15836+
` At least ${behind} commit(s) behind ${DEFAULT_BASE_REF}, and none of the commit(s) this tree can SEE touched what this answer derives from — ${span}.${setAside}`,
1569615837
` Whether UNSEEN upstream work did, this run cannot tell: the range above ends at ${DEFAULT_BASE_REF}, a LOCAL snapshot only a fetch moves — not at upstream. Run 'git fetch ${DEFAULT_BASE_REMOTE} ${DEFAULT_BASE_BRANCH}' and derive again for the strongest reading a checkout can take.`,
1569715838
];
1569815839
}
@@ -15702,6 +15843,14 @@ export function driftLines(drift) {
1570215843
` Stale here: ${changed.slice(0, 6).join(' ')}${changed.length > 6 ? ` … +${changed.length - 6} more` : ''}`,
1570315844
` Those files ARE the families printed below, so this run read their old copies and still exited 0 — a well-formed answer about a tree nobody is on.`,
1570415845
` "At least": ${DEFAULT_BASE_REF} is a LOCAL ref only a fetch moves. Run 'git fetch ${DEFAULT_BASE_REMOTE} ${DEFAULT_BASE_BRANCH}' and derive again from a tree at ${DEFAULT_BASE_REF}.`,
15846+
// A list nobody narrowed may name paths the answer is not made of — a
15847+
// generated record under `scripts/` — and a reader cannot tell which from
15848+
// the list alone, so the reading says so and names the run that measures
15849+
// it (#26334). Absent from a narrowed reading: there the list IS the
15850+
// inputs.
15851+
...(Array.isArray(ignored) ? [] : [
15852+
` Not narrowed to what the answer is made of: this run measured no derivation, so every moved path under the surface is listed, generated records included. A --commands or --ran run measures the inputs and lists only those.`,
15853+
]),
1570515854
];
1570615855
}
1570715856

@@ -16225,7 +16374,12 @@ if (invokedDirectly) {
1622516374
for (const line of sisterTier.stdout) console.log(line);
1622616375
process.exit(0);
1622716376
}
16228-
for (const line of bannerLines({ identity, paths: declaredPaths, drift: baseDrift() })) console.error(line);
16377+
// `--tier` reads no workflow and no check script by contract (its branch
16378+
// below), so it hands the drift no inputs to narrow by and its loud reading
16379+
// says so; every deriving mode hands in the measured set, which it is about
16380+
// to discover anyway.
16381+
const drift = baseDrift({ inputs: process.argv.includes('--tier') ? null : liveDerivationInputs });
16382+
for (const line of bannerLines({ identity, paths: declaredPaths, drift })) console.error(line);
1622916383
if (argv.assertion !== null) {
1623016384
// An assertion the tree contradicts is the measured failure, caught. It
1623116385
// ends the run: a caller that named the repo it needs has stated a

0 commit comments

Comments
 (0)