From 8895c125721bafe9e2e30181751e505be2aaa5a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 01:14:25 +0000 Subject: [PATCH 1/2] fix(pm-tooling): refuse windowed history questions the clone cannot see all of MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three seat-run tools asked `--since` questions over a branch with no shallow guard. A shallow clone answers those from whatever part of the window is present, exits 0 and prints no warning, so each returned a confidently wrong smaller number. Agent containers clone shallow and all three run from a seat; every workflow that reads history already checks out `fetch-depth: 0`, which is why the exposure survived. - git-history.mjs: add `historyHorizon()`, the read-only half of the #9878 helper — one predicate for all adopters, never fetching, and printing a deepen command computed so it can only ADD history. - check-governed-merges.mjs: a repo whose history stops inside the window is UNAUDITED, like an absent checkout. It rendered `clean window` over ~40 governed merges. - check-engine-split-ratio.mjs: refuse rather than print an ADR trigger metric over a short denominator; annotate the horizon when the window is whole. - collect-release-notes.sh: withhold the cloud section rather than print it short, and exit 2. - lint.yml: run the three self-tests, including git-history's, which nothing in this repo ran until now. Part of #9902 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt --- .github/workflows/lint.yml | 44 ++++ scripts/check-engine-split-ratio.mjs | 340 ++++++++++++++++++++++----- scripts/collect-release-notes.sh | 121 ++++++++++ scripts/pm/check-governed-merges.mjs | 110 ++++++++- scripts/pm/git-history.mjs | 84 +++++++ 5 files changed, 638 insertions(+), 61 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cb91cbfb7a..4112b0d111 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -386,6 +386,50 @@ jobs: - name: Release-rehearsal clone preflight self-test run: node scripts/pm/release-rehearsal-clone.mjs --self-test + # Shallow-history guard self-tests (#9902). Three seat-run tools ask + # WINDOWED history questions (`--since` over a branch), and a shallow + # clone answers those from whatever part of the window is present, exits + # 0, and prints no warning. Agent containers clone shallow, and all three + # run from a seat rather than from CI — every workflow that reads history + # already checks out with `fetch-depth: 0`, which is precisely why the + # exposure survived: CI is not where these run. + # + # Measured in one container on 2026-08-21 (graft floor 2026-06-02): + # `check-governed-merges` rendered `✅ clean window` for a window in which + # GitHub lists 38 commits touching `docs/adr/**` and 13 touching + # `AGENTS.md`; `check-engine-split-ratio --days 90` — the workflow's own + # invocation — printed a 98.1% ADR trigger metric over a denominator 17 + # commits short, one of whose members was the graft boundary commit + # itself (it carries the whole tree, so a truncated window does not just + # lose data points, it fabricates one). + # + # What runs here is the self-tests, not the live tools: the live tools + # judge whatever clone they are pointed at, and a CI checkout's depth is + # the workflow's business, not a verdict this job can hold. Each pins BOTH + # directions on real temp-git fixtures — a window crossing the floor is + # refused, and a still-shallow clone whose floor PREDATES the window is + # answered untouched, because the predicate is the floor rather than + # `--is-shallow-repository` and a guard that refused provably-correct + # answers would only train bypass. + # + # `git-history.mjs` is listed first and deliberately: it is the shared + # predicate all three call, it shipped for #9878 with a self-test that + # until now NOTHING in this repo ran, and an unrun self-test is a phantom + # check — the AGENTS.md rule about `@ts-expect-error` in an uncompiled + # file, wearing a different hat. Invoked as `node`/`bash` rather than + # through `pnpm check:*`: that alias belongs in root package.json, which + # is declared territory of the @changesets/cli v3 migration lane (#9465) + # while it runs. Temp-dir fixtures, no network, ~6 s in total. + # + # (`check-governed-merges.mjs`'s own cases run in the + # `pnpm check:pm-governed-merges` step above, which is already its + # self-test.) + - name: Shallow-history guard self-tests + run: | + node scripts/pm/git-history.mjs --self-test + node scripts/check-engine-split-ratio.mjs --self-test + bash scripts/collect-release-notes.sh --self-test + # Verify-lock entry-point self-test (#9661). `scripts/pm/os-verify-lock.sh` # is the ONE way an agent takes the container's shared heavy-verify lock, # and it is the enforcement of a rule that used to live only in prose: the diff --git a/scripts/check-engine-split-ratio.mjs b/scripts/check-engine-split-ratio.mjs index e558588adb..9dc9446f3b 100644 --- a/scripts/check-engine-split-ratio.mjs +++ b/scripts/check-engine-split-ratio.mjs @@ -11,95 +11,317 @@ * history so CI can track it over time. * * Run: node scripts/check-engine-split-ratio.mjs [--days N] [--threshold PCT] + * node scripts/check-engine-split-ratio.mjs --self-test * * --days N Look-back window in days (default 90). * --threshold PCT Optional gate: exit 1 if the ratio is ABOVE the given * percentage. By default the script is REPORT-ONLY (always * exits 0) — the split threshold is an open question * (ADR-0076 OQ#5) and is set deliberately, not by default. + * --cwd PATH Measure another checkout (the self-test's fixtures). + * + * Exit codes: + * 0 the ratio was computed over the whole window, and is printed with the + * horizon it was computed against. + * 1 --threshold was given and the ratio is above it. + * 2 CANNOT COMPUTE — bad arguments, or this checkout cannot see the whole + * window. Never confusable with 1: "the engine is not separable" and "I + * could not measure" are different facts. * * Output: a human-readable summary on stdout; when $GITHUB_STEP_SUMMARY is * set, a markdown section is appended for the Actions run summary. * - * Requires full git history (in CI: actions/checkout with fetch-depth: 0). + * ## Why a horizon guard, and why it refuses instead of warning (#9902) + * + * `git log --since=` in a shallow clone answers from whatever part + * of the window happens to be present, **exits 0, and prints no warning** — the + * graft boundary is invisible to it. The scheduled run is safe + * (`engine-split-metric.yml` checks out with `fetch-depth: 0`); a seat running + * this locally is not, and agent containers clone shallow. + * + * Measured in one such container on 2026-08-21 (graft floor 2026-06-02, window + * `--days 90` reaching back to 2026-05-23): + * + * | reading | engine-core commits in the window | + * |-------------------------------------------|-----------------------------------| + * | this script, before this guard | **265** — exit 0, no warning | + * | truth (GitHub commit list for the window) | 282 | + * + * Seventeen commits short, and worse than short: the one commit the truncated + * scan *did* return from below the floor was the graft boundary itself, which + * carries the whole tree and therefore "touches" both engine-core files and + * everything outside `packages/objectql/`. A truncated window does not merely + * lose data points, it fabricates one — and it lands in the cross-package + * numerator, pushing the ratio the wrong way. + * + * The shape is REFUSAL rather than a warning because of what consumes the + * number: this is an ADR trigger metric, read to decide whether a repo split + * may proceed. A ratio printed beside a caveat is still a ratio, and it gets + * quoted without the caveat; a refusal cannot be quoted at all. The predicate + * is `historyHorizon()` in `scripts/pm/git-history.mjs`, deliberately shared + * rather than re-implemented — it is not `--is-shallow-repository` (which + * would refuse windows a shallow clone answers exactly) but "does the floor + * predate the window", and the deepen command it prints is computed so it can + * only ADD history. + * * Zero third-party dependencies. */ -import { execFileSync } from 'node:child_process'; -import { appendFileSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; +import { execFileSync, spawnSync } from 'node:child_process'; +import { appendFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { historyHorizon } from './pm/git-history.mjs'; + const __dirname = dirname(fileURLToPath(import.meta.url)); -const repoRoot = resolve(__dirname, '..'); const ENGINE_CORE = ['packages/objectql/src/engine.ts', 'packages/objectql/src/registry.ts']; const ENGINE_PACKAGE_PREFIX = 'packages/objectql/'; -function arg(name, fallback) { - const i = process.argv.indexOf(`--${name}`); - if (i !== -1 && process.argv[i + 1] !== undefined) return process.argv[i + 1]; +const EXIT_OK = 0; +const EXIT_ABOVE_THRESHOLD = 1; +const EXIT_CANNOT_COMPUTE = 2; + +function arg(argv, name, fallback) { + const i = argv.indexOf(`--${name}`); + if (i !== -1 && argv[i + 1] !== undefined) return argv[i + 1]; return fallback; } -const days = Number(arg('days', '90')); -const thresholdRaw = arg('threshold', ''); -const threshold = thresholdRaw === '' ? null : Number(thresholdRaw); -if (!Number.isFinite(days) || days <= 0) { - console.error(`Invalid --days value: ${arg('days', '90')}`); - process.exit(2); -} -if (thresholdRaw !== '' && (!Number.isFinite(threshold) || threshold < 0 || threshold > 100)) { - console.error(`Invalid --threshold value: ${thresholdRaw} (expected 0-100)`); - process.exit(2); +/** + * The horizon line that travels beside the number. Pure, so --self-test pins + * the words: a metric pasted into an ADR discussion without its horizon is a + * number nobody can re-derive. + */ +export function renderHorizon(horizon) { + if (!horizon.shallow) return ` history horizon: complete clone; ref tip ${horizon.tip}`; + return ` history horizon: shallow clone, oldest visible commit ${horizon.floor} (predates the window); ref tip ${horizon.tip}`; } -function git(...args) { - return execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }); +/** The refusal, as words. Pure, so --self-test pins that it names both facts. */ +export function renderRefusal({ horizon, days, sinceIso }) { + return ( + `⛔ cannot compute the ADR-0076 D7 trigger metric — ${horizon.reason}.\n` + + ` window: last ${days} days (since ${sinceIso.slice(0, 10)}) ref tip: ${horizon.tip}\n` + + ` A ratio derived here would be real, plausible and WRONG: the missing commits are\n` + + ` invisible to \`git log\`, which reports no error, and the graft boundary commit that\n` + + ` IS visible carries the whole tree — so it counts as an engine-core commit AND as a\n` + + ` cross-package one. Refusing rather than printing a caveat: an ADR trigger metric gets\n` + + ` quoted without its caveats (#9902).\n` + + ` Remedy: ${horizon.remedy}` + ); } -// Commits in the window that touched the engine core. -const shas = git( - 'log', `--since=${days} days ago`, '--format=%H', '--', ...ENGINE_CORE, -).split('\n').filter(Boolean); +function main(argv) { + const days = Number(arg(argv, 'days', '90')); + const thresholdRaw = arg(argv, 'threshold', ''); + const threshold = thresholdRaw === '' ? null : Number(thresholdRaw); + const repoRoot = resolve(arg(argv, 'cwd', resolve(__dirname, '..'))); -let crossPackage = 0; -for (const sha of shas) { - const files = git('show', '--name-only', '--format=', sha).split('\n').filter(Boolean); - if (files.some((f) => !f.startsWith(ENGINE_PACKAGE_PREFIX))) crossPackage += 1; -} + if (!Number.isFinite(days) || days <= 0) { + console.error(`Invalid --days value: ${arg(argv, 'days', '90')}`); + return EXIT_CANNOT_COMPUTE; + } + if (thresholdRaw !== '' && (!Number.isFinite(threshold) || threshold < 0 || threshold > 100)) { + console.error(`Invalid --threshold value: ${thresholdRaw} (expected 0-100)`); + return EXIT_CANNOT_COMPUTE; + } -const total = shas.length; -const ratio = total === 0 ? 0 : (crossPackage / total) * 100; -const ratioStr = ratio.toFixed(1); - -const lines = [ - `ADR-0076 D7 trigger metric — engine cross-package commit ratio`, - ` window: last ${days} days`, - ` engine-core commits: ${total} (${ENGINE_CORE.join(', ')})`, - ` also cross-package: ${crossPackage}`, - ` ratio: ${total === 0 ? 'n/a (no engine-core commits in window)' : `${ratioStr}%`}`, - ``, - `Reference: ~88% at ADR time (2026-06) — the engine repo-split (D7) stays`, - `deferred until this ratio is low and stable (threshold TBD, ADR-0076 OQ#5).`, -]; -console.log(lines.join('\n')); - -if (process.env.GITHUB_STEP_SUMMARY) { - const md = [ - `### ADR-0076 D7 trigger metric — engine cross-package commit ratio`, - ``, - `| Window | Engine-core commits | Cross-package | Ratio |`, - `|---|---|---|---|`, - `| last ${days} days | ${total} | ${crossPackage} | ${total === 0 ? 'n/a' : `${ratioStr}%`} |`, - ``, - `Reference: ~88% at ADR time. D7 (engine repo-split) stays deferred until this is low and stable (OQ#5).`, + const git = (...args) => + execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }); + + // ── the horizon comes FIRST: nothing below may run on a window this + // checkout cannot see all of. + const sinceMs = Date.now() - days * 24 * 60 * 60 * 1000; + const sinceIso = new Date(sinceMs).toISOString(); + const horizon = historyHorizon({ cwd: repoRoot, ref: 'HEAD', sinceMs }); + if (!horizon.covered) { + console.error(renderRefusal({ horizon, days, sinceIso })); + return EXIT_CANNOT_COMPUTE; + } + + // Commits in the window that touched the engine core. + const shas = git('log', `--since=${sinceIso}`, '--format=%H', '--', ...ENGINE_CORE) + .split('\n') + .filter(Boolean); + + let crossPackage = 0; + for (const sha of shas) { + const files = git('show', '--name-only', '--format=', sha).split('\n').filter(Boolean); + if (files.some((f) => !f.startsWith(ENGINE_PACKAGE_PREFIX))) crossPackage += 1; + } + + const total = shas.length; + const ratio = total === 0 ? 0 : (crossPackage / total) * 100; + const ratioStr = ratio.toFixed(1); + + const lines = [ + `ADR-0076 D7 trigger metric — engine cross-package commit ratio`, + ` window: last ${days} days`, + ` engine-core commits: ${total} (${ENGINE_CORE.join(', ')})`, + ` also cross-package: ${crossPackage}`, + ` ratio: ${total === 0 ? 'n/a (no engine-core commits in window)' : `${ratioStr}%`}`, + renderHorizon(horizon), ``, - ].join('\n'); - appendFileSync(process.env.GITHUB_STEP_SUMMARY, md); + `Reference: ~88% at ADR time (2026-06) — the engine repo-split (D7) stays`, + `deferred until this ratio is low and stable (threshold TBD, ADR-0076 OQ#5).`, + ]; + console.log(lines.join('\n')); + + // Zero is a broken scan far more often than a quiet engine (#4690). The + // window is provably whole by now, so say so out loud rather than letting a + // blank denominator read as a healthy 0.0%. + if (total === 0) { + console.error( + `\n⚠️ ZERO engine-core commits in a ${days}-day window. The window is provably whole ` + + `(${renderHorizon(horizon).trim()}), so this is either a genuinely dormant engine or a\n` + + ` path that has moved — check ${ENGINE_CORE.join(' / ')} still exist before reading 0 as healthy.`, + ); + } + + if (process.env.GITHUB_STEP_SUMMARY) { + const md = [ + `### ADR-0076 D7 trigger metric — engine cross-package commit ratio`, + ``, + `| Window | Engine-core commits | Cross-package | Ratio |`, + `|---|---|---|---|`, + `| last ${days} days | ${total} | ${crossPackage} | ${total === 0 ? 'n/a' : `${ratioStr}%`} |`, + ``, + `${renderHorizon(horizon).trim()}`, + ``, + `Reference: ~88% at ADR time. D7 (engine repo-split) stays deferred until this is low and stable (OQ#5).`, + ``, + ].join('\n'); + appendFileSync(process.env.GITHUB_STEP_SUMMARY, md); + } + + if (threshold !== null && total > 0 && ratio > threshold) { + console.error(`\nRatio ${ratioStr}% exceeds the configured threshold of ${threshold}% — engine is not separable.`); + return EXIT_ABOVE_THRESHOLD; + } + return EXIT_OK; } -if (threshold !== null && total > 0 && ratio > threshold) { - console.error(`\nRatio ${ratioStr}% exceeds the configured threshold of ${threshold}% — engine is not separable.`); - process.exit(1); +// ── self-test ──────────────────────────────────────────────────────────────── + +function selfTest() { + let failures = 0; + const t = (name, ok, detail = '') => { + if (ok) { console.log(` ✓ ${name}`); return; } + failures += 1; + console.log(` ✗ ${name}${detail ? `\n ${detail}` : ''}`); + }; + const day = 24 * 60 * 60 * 1000; + + // ── pure rendering ──────────────────────────────────────────────────────── + t('a complete clone reports its horizon as complete, with the tip', + renderHorizon({ shallow: false, tip: '2026-08-21' }).includes('complete clone') + && renderHorizon({ shallow: false, tip: '2026-08-21' }).includes('2026-08-21')); + t('an ALLOWED shallow answer still prints its floor — the number travels with its horizon', + renderHorizon({ shallow: true, floor: '2026-06-02', tip: '2026-08-21' }).includes('2026-06-02')); + const refusal = renderRefusal({ + horizon: { reason: 'floor sits INSIDE the window', tip: '2026-08-21', remedy: 'git -C /x fetch --unshallow origin' }, + days: 90, + sinceIso: '2026-05-23T00:00:00.000Z', + }); + t('the refusal names the reason, the window and a runnable remedy', + /INSIDE the window/.test(refusal) && /last 90 days/.test(refusal) && /fetch --unshallow/.test(refusal), refusal); + t('and it prints no percentage at all — a refusal must not be quotable as a ratio', + !/%/.test(refusal), refusal); + + // ── real repos: the defect, then both legs of the guard ─────────────────── + const root = mkdtempSync(join(tmpdir(), 'engine-split-selftest-')); + const g = (args, cwd) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); + const self = fileURLToPath(import.meta.url); + // spawnSync, not execFileSync: this script writes to stderr on SUCCESS too + // (the zero-scan warning), and execFileSync surfaces stderr only when it + // throws -- a self-test that reads stderr only on failure is half blind. + const runAllowFail = (args, cwd) => { + const r = spawnSync(process.execPath, [self, ...args, '--cwd', cwd], { + cwd, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + return { stdout: String(r.stdout || ''), stderr: String(r.stderr || ''), code: r.status }; + }; + + try { + const up = join(root, 'up'); + mkdirSync(join(up, 'packages/objectql/src'), { recursive: true }); + mkdirSync(join(up, 'packages/core/src'), { recursive: true }); + g(['init', '--quiet', '--initial-branch=main', '.'], up); + g(['config', 'user.email', 'selftest@objectstack.ai'], up); + g(['config', 'user.name', 'selftest'], up); + // 40 commits, one per day, each touching engine.ts; the even ones also + // touch another package, so the true ratio over any window is 50%. + const start = Date.parse('2026-06-01T12:00:00Z'); + for (let i = 0; i < 40; i += 1) { + const d = new Date(start + i * day).toISOString(); + writeFileSync(join(up, 'packages/objectql/src/engine.ts'), `// engine ${i}\n`); + if (i % 2 === 0) writeFileSync(join(up, 'packages/core/src/index.ts'), `// core ${i}\n`); + g(['add', '-A'], up); + execFileSync('git', ['commit', '--quiet', '-m', `c${i}`], { + cwd: up, + encoding: 'utf8', + env: { ...process.env, GIT_AUTHOR_DATE: d, GIT_COMMITTER_DATE: d }, + }); + } + // `--days` is relative to now, so re-date the whole fixture to end today. + const daysSinceFixture = Math.ceil((Date.now() - (start + 39 * day)) / day); + + const full = join(root, 'full'); + g(['clone', '--quiet', `file://${up}`, full], root); + const shallow = join(root, 'shallow'); + g(['clone', '--quiet', '--depth=5', `file://${up}`, shallow], root); + + const wide = String(daysSinceFixture + 45); // reaches below the depth-5 floor + const narrow = String(daysSinceFixture + 2); // sits entirely above it + + // BASELINE: the defect itself, with raw git, in the shallow fixture. + const rawWide = g(['log', `--since=${wide} days ago`, '--format=%H', '--', + 'packages/objectql/src/engine.ts'], shallow).split('\n').filter(Boolean).length; + const rawFull = g(['log', `--since=${wide} days ago`, '--format=%H', '--', + 'packages/objectql/src/engine.ts'], full).split('\n').filter(Boolean).length; + t('BASELINE — raw git answers the same window with a smaller number and no warning ' + + '(this is the defect, reproduced)', rawWide < rawFull && rawWide === 5 && rawFull === 40, + `shallow ${rawWide} vs full ${rawFull}`); + + const okFull = runAllowFail(['--days', wide], full); + t('a complete clone computes the metric, exit 0', okFull.code === 0, JSON.stringify(okFull).slice(0, 400)); + t('and the ratio is the fixture\'s real 50.0% over all 40 commits', + /engine-core commits: 40/.test(okFull.stdout) && /ratio: *50\.0%/.test(okFull.stdout), okFull.stdout); + t('and it states the horizon it was computed against', + /history horizon: *complete clone/.test(okFull.stdout), okFull.stdout); + + const refused = runAllowFail(['--days', wide], shallow); + t('the same question in the shallow clone REFUSES instead of answering', refused.code === EXIT_CANNOT_COMPUTE, + `exit ${refused.code}: ${refused.stdout}${refused.stderr}`); + t('and stdout carries no ratio at all, so nothing can be quoted from it', + !/%/.test(refused.stdout) && refused.stdout.trim() === '', JSON.stringify(refused.stdout)); + t('and the refusal names the floor and a deepen command', + /oldest visible commit on 'HEAD' is 2026-07-06/.test(refused.stderr) + && /fetch --shallow-since=/.test(refused.stderr), refused.stderr); + t('and the refusal exit code is NOT the threshold code — "cannot measure" never reads as ' + + '"the engine is not separable"', refused.code !== EXIT_ABOVE_THRESHOLD); + + const okNarrow = runAllowFail(['--days', narrow], shallow); + t('a STILL-shallow clone whose floor predates the window is answered, not refused ' + + '(a bare is-shallow guard would refuse a provably correct answer)', + okNarrow.code === 0 && /ratio: *\d/.test(okNarrow.stdout), `exit ${okNarrow.code}: ${okNarrow.stdout}${okNarrow.stderr}`); + t('and that answer still carries the floor beside it', + /shallow clone, oldest visible commit 2026-07-06/.test(okNarrow.stdout), okNarrow.stdout); + t('the fixture is still shallow after that answer — the predicate is the floor, not the flag', + g(['rev-parse', '--is-shallow-repository'], shallow).trim() === 'true'); + } finally { + rmSync(root, { recursive: true, force: true }); + } + + console.log(failures === 0 + ? `\ncheck-engine-split-ratio --self-test: all cases passed.` + : `\ncheck-engine-split-ratio --self-test: ${failures} FAILED.`); + return failures === 0 ? 0 : 1; } + +process.exit(process.argv.includes('--self-test') ? selfTest() : main(process.argv.slice(2))); diff --git a/scripts/collect-release-notes.sh b/scripts/collect-release-notes.sh index c5b7cf9eb3..191024b19a 100755 --- a/scripts/collect-release-notes.sh +++ b/scripts/collect-release-notes.sh @@ -26,6 +26,31 @@ # # Sibling checkouts are found at ../objectui and ../cloud; override with # OBJECTUI_ROOT / CLOUD_ROOT. +# +# Self-test: scripts/collect-release-notes.sh --self-test (temp fixtures, no network) +# +# ## Section 4 asks a WINDOWED history question, so it is horizon-guarded (#9902) +# +# Sections 1-3 ask range questions (`..`): a missing endpoint is a +# `fatal: bad object` and `set -e` stops the script. Section 4 cannot fail that +# way — `git log --since/--until` over a shallow checkout answers from whatever +# part of the window is present, exits 0, and prints no warning. This script +# has no CI caller at all; it runs at release time from a seat, and it reads a +# cloud checkout it does not own. +# +# Measured on a depth-5 fixture whose true answer for the window is 21 commits: +# the shallow clone printed **5**, exit 0, under the same "### feat / fix" +# heading — sixteen release-note items missing with nothing to indicate it. So +# the section is WITHHELD rather than printed short: the consumer here is a +# human writing a release page, and a short list is indistinguishable from a +# quiet release, while an absent one cannot be transcribed by accident. +# +# `git-history.mjs ensure --no-fetch` is the shared predicate (the same one +# `check-governed-merges.mjs` and `check-engine-split-ratio.mjs` call), not a +# re-implementation: it asks whether the shallow floor predates the window +# rather than whether the clone is shallow, so a shallow cloud checkout with +# enough depth still prints its section. Deepening is left to the operator — +# this script must not reach into someone else's checkout and move it. set -euo pipefail @@ -33,8 +58,90 @@ FRAMEWORK_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" OBJECTUI_ROOT="${OBJECTUI_ROOT:-$(cd "${FRAMEWORK_ROOT}/../objectui" 2>/dev/null && pwd || true)}" CLOUD_ROOT="${CLOUD_ROOT:-$(cd "${FRAMEWORK_ROOT}/../cloud" 2>/dev/null && pwd || true)}" +# Can `dir` see the whole of [since, ...)? Prints the refusal block (markdown) +# on stdout and returns non-zero when it cannot; prints nothing and returns 0 +# when it can. +cloud_window_guard() { # + local dir="$1" since="$2" err rc=0 + err="$(mktemp)" + if node "${FRAMEWORK_ROOT}/scripts/pm/git-history.mjs" ensure \ + --since="$since" --ref=HEAD --no-fetch --cwd="$dir" 2> "$err"; then + rm -f "$err" + return 0 + fi + rc=1 + echo "> ⛔ **Cloud commit list WITHHELD — that checkout cannot see the whole window.**" + echo ">" + echo "> Printing it anyway would list only the commits above the shallow boundary, under the" + echo "> same heading, with no sign that the rest exist — a short release-notes section reads" + echo "> exactly like a quiet release (#9902)." + echo + echo '```' + cat "$err" + echo '```' + rm -f "$err" + return $rc +} + +if [[ "${1:-}" == "--self-test" ]]; then + fails=0 + ok() { if [[ "$2" == "1" ]]; then echo " ✓ $1"; else echo " ✗ $1${3:+ + $3}"; fails=$((fails + 1)); fi; } + + tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' EXIT + mkdir -p "$tmp/up" + ( + cd "$tmp/up" + git init --quiet --initial-branch=main . + git config user.email selftest@objectstack.ai + git config user.name selftest + for i in $(seq 0 39); do + d="$(node -e "console.log(new Date(Date.parse('2026-06-01T12:00:00Z') + $i * 864e5).toISOString())")" + printf 'commit %s\n' "$i" > f.txt + git add f.txt + GIT_AUTHOR_DATE="$d" GIT_COMMITTER_DATE="$d" git commit --quiet -m "feat: change $i" + done + ) + git clone --quiet "file://$tmp/up" "$tmp/full" + git clone --quiet --depth=5 "file://$tmp/up" "$tmp/shallow" + + WIN_SINCE=2026-06-20T00:00:00Z + WIN_UNTIL=2026-07-11T00:00:00Z + n_full=$(git -C "$tmp/full" log --no-merges --pretty='- %h %s' --since="$WIN_SINCE" --until="$WIN_UNTIL" | grep -c .) + n_shallow=$(git -C "$tmp/shallow" log --no-merges --pretty='- %h %s' --since="$WIN_SINCE" --until="$WIN_UNTIL" | grep -c .) + ok "BASELINE — the same window answers 21 in a full clone and $n_shallow in a shallow one, both exit 0 (the defect, reproduced)" \ + "$([[ "$n_full" == 21 && "$n_shallow" == 5 ]] && echo 1 || echo 0)" "full=$n_full shallow=$n_shallow" + + if out="$(cloud_window_guard "$tmp/full" "$WIN_SINCE")"; then g_full=1; else g_full=0; fi + ok "a complete clone passes the guard and it prints nothing" "$([[ "$g_full" == 1 && -z "$out" ]] && echo 1 || echo 0)" "out=$out" + + if out="$(cloud_window_guard "$tmp/shallow" "$WIN_SINCE")"; then g_sh=1; else g_sh=0; fi + ok "a shallow clone whose floor sits inside the window is REFUSED" "$([[ "$g_sh" == 0 ]] && echo 1 || echo 0)" "out=$out" + ok "and the withheld block says so, names the floor, and carries the remedy" \ + "$(grep -q 'WITHHELD' <<< "$out" && grep -q 'shallow floor: 2026-07' <<< "$out" && grep -q 'unshallow' <<< "$out" && echo 1 || echo 0)" "$out" + ok "and it prints no commit list at all — nothing in it can be transcribed as the section" \ + "$(grep -qE '^- [0-9a-f]+ feat' <<< "$out" && echo 0 || echo 1)" "$out" + + # The other leg: a still-shallow clone deep enough for the window must pass, + # or the guard would refuse answers that are provably right and train bypass. + if out="$(cloud_window_guard "$tmp/shallow" 2026-07-08T00:00:00Z)"; then g_narrow=1; else g_narrow=0; fi + ok "a STILL-shallow clone whose floor predates the window passes untouched" "$([[ "$g_narrow" == 1 ]] && echo 1 || echo 0)" "out=$out" + ok "and it is still shallow afterwards — the guard never deepens a checkout it does not own" \ + "$([[ "$(git -C "$tmp/shallow" rev-parse --is-shallow-repository)" == true ]] && echo 1 || echo 0)" + + # Wiring: the guard is worthless if section 4 stops calling it. + ok "section 4 routes its windowed question through the guard" \ + "$(grep -q 'cloud_window_guard "$CLOUD_ROOT" "$prev_date"' "${BASH_SOURCE[0]}" && echo 1 || echo 0)" + + echo + if [[ "$fails" == 0 ]]; then echo "collect-release-notes --self-test: all cases passed."; else echo "collect-release-notes --self-test: $fails FAILED."; exit 1; fi + exit 0 +fi + PREV_REF="${1:?usage: collect-release-notes.sh []}" NEW_REF="${2:-HEAD}" +withheld=0 cd "$FRAMEWORK_ROOT" @@ -116,13 +223,27 @@ echo if [[ -z "$CLOUD_ROOT" || ! -d "$CLOUD_ROOT/.git" ]]; then echo "_cloud checkout not found (set CLOUD_ROOT); scan it by this window manually._" +elif ! cloud_window_guard "$CLOUD_ROOT" "$prev_date"; then + withheld=1 else print_log_split "$CLOUD_ROOT" --since="$prev_date" --until="$new_date" fi echo echo "---" +if [[ "$withheld" == 1 ]]; then + echo "_⛔ INCOMPLETE: section 4 was withheld — see the block above. This material is not a" + echo "complete basis for a release page until that checkout is deepened._" + echo +fi echo "_Write the curated page at content/docs/releases/, register it in" echo "content/docs/releases/meta.json, and link it from index.mdx. Items with a" echo "changeset have the best prose in section 2; section 1 is the completeness" echo "check — every developer-visible feat/fix should be accounted for._" + +# A withheld section must be legible to a pipeline too, not only to the reader: +# `... > material.md` succeeding is otherwise the only signal, and it lies. +if [[ "$withheld" == 1 ]]; then + echo "collect-release-notes: INCOMPLETE — the cloud section was withheld (see the output)." >&2 + exit 2 +fi diff --git a/scripts/pm/check-governed-merges.mjs b/scripts/pm/check-governed-merges.mjs index 29e276ab81..e0aa90b81f 100644 --- a/scripts/pm/check-governed-merges.mjs +++ b/scripts/pm/check-governed-merges.mjs @@ -103,6 +103,38 @@ * found" and "nothing was looked at" are different facts and this sweep is * required to keep them apart. * + * ## A TRUNCATED history is the same fact, and it used to be invisible (#9902) + * + * "Nothing was looked at" has a third form that no missing-checkout test + * catches: the checkout is present and healthy, and its history simply stops + * inside the window. `git log --since` answers from whatever part of the + * window is present, **exits 0, and prints no warning**. Agent containers + * clone shallow, and the real sweep is a seat command (CI runs only + * `--self-test`), so that is where this sweep actually runs. + * + * Measured in one such container, 2026-08-21, graft floor 2026-06-02, window + * 2026-05-23 → the floor: + * + * | reading | governed merges reported | + * |--------------------------------------------|--------------------------| + * | this sweep, before this guard | **0** — and it rendered | + * | | `✅ clean window`, exit 0 | + * | truth (GitHub commit list, same window) | 38 commits touched | + * | | `docs/adr/**`, 13 touched | + * | | `AGENTS.md` | + * + * The green tick printed over ~40 governed merges, and the line above it said + * `✓ audited`. Under-enumeration is the one direction this list must never be + * wrong in — a short list reads as COMPLIANCE. So a repo whose history does + * not cover the window is UNAUDITED, exactly like an absent checkout, and the + * ✅ cannot print at all. The predicate is `historyHorizon()` in + * `git-history.mjs` (shared, so it cannot drift from the other adopters); it + * asks whether the floor predates the window rather than whether the clone is + * shallow, so a shallow container with enough depth — the common case, since + * the default window is 24h — still sweeps exactly as before, with no fetch. + * Deepening is never done here: this sweep reads four checkouts it does not + * own, so the remedy is printed as a command for the operator to run. + * * ## Institutional memory — why governed surfaces are guarded at all * * This history moved here from the retired gate's header when the gate @@ -252,6 +284,7 @@ import { execFileSync, spawnSync } from 'node:child_process'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { historyHorizon } from './git-history.mjs'; import { isEntrypoint } from '../invoked-as.mjs'; const scriptPath = fileURLToPath(import.meta.url); @@ -467,6 +500,25 @@ function git(root, args) { return execFileSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }); } +/** + * Why a repo with a truncated history is UNAUDITED rather than swept. Pure, so + * `--self-test` pins the words: this reason is the only thing standing between + * a short list and a maintainer reading it as a clean window. + */ +export function truncatedHorizonReason({ ref, horizon }) { + return ( + `cannot audit the whole window on ${ref} — ${horizon.reason}. ` + + `Enumerating anyway would UNDER-report, and a short governed-merge list reads as COMPLIANCE: ` + + `the merges below the boundary are invisible to \`git log\`, which reports no error (#9902). ` + + `Remedy: ${horizon.remedy}` + ); +} + +/** The horizon an audited repo was swept against, for the report line. */ +export function describeHorizon(horizon) { + return horizon.shallow ? `shallow, oldest visible ${horizon.floor} (predates the window)` : 'complete clone'; +} + /** First-parent mainline commits of `ref` since `sinceIso`, newest first. */ export function mainlineCommits(root, ref, sinceIso) { const out = git(root, ['log', '--first-parent', `--since=${sinceIso}`, '--format=%H%x09%cI%x09%s', ref]); @@ -600,7 +652,7 @@ export function renderReport({ sinceIso, repos, scanned, entries, lookups }) { `across ${audited.length}/${repos.length} governed repo(s)\n` + ` scanned ${scanned} mainline commit(s); ${lookups} API lookup(s).`; const auditedLines = audited.map( - (r) => ` ✓ audited ${r.slug} — tip ${r.tip ? `${r.tip.sha.slice(0, 9)} @ ${r.tip.date}` : '(unknown)'}; ${r.scanned ?? 0} mainline commit(s) in window${r.quiet ? ' — none in window; if that tip predates your last fetch, run `git fetch origin main` there' : ''}`, + (r) => ` ✓ audited ${r.slug} — tip ${r.tip ? `${r.tip.sha.slice(0, 9)} @ ${r.tip.date}` : '(unknown)'}; ${r.scanned ?? 0} mainline commit(s) in window${r.horizon ? `; history ${r.horizon}` : ''}${r.quiet ? ' — none in window; if that tip predates your last fetch, run `git fetch origin main` there' : ''}`, ); const unauditedLines = unaudited.map((r) => ` ⚠️ UNAUDITED ${r.slug} — ${r.reason}`); const unauditedNote = @@ -753,6 +805,15 @@ async function main() { try { const [sha, date] = git(repo.path, ['log', '-1', '--format=%H%x09%cI', ref]).trim().split('\t'); repo.tip = { sha, date }; + // Before enumerating: can this checkout SEE the whole window? A short + // answer here reads as compliance, so it must not be produced at all. + const horizon = historyHorizon({ cwd: repo.path, ref, sinceMs: Date.parse(sinceIso) }); + if (!horizon.covered) { + repo.status = 'unaudited'; + repo.reason = truncatedHorizonReason({ ref, horizon }); + continue; + } + repo.horizon = describeHorizon(horizon); commits = mainlineCommits(repo.path, ref, sinceIso); } catch (error) { repo.status = 'unaudited'; @@ -804,7 +865,7 @@ async function main() { JSON.stringify( { since: sinceIso, - repos: repos.map((r) => ({ id: r.id, slug: r.slug, path: r.path, status: r.status, reason: r.reason, tip: r.tip ?? null, scanned: r.scanned ?? 0 })), + repos: repos.map((r) => ({ id: r.id, slug: r.slug, path: r.path, status: r.status, reason: r.reason, tip: r.tip ?? null, horizon: r.horizon ?? null, scanned: r.scanned ?? 0 })), scanned, complete, channelsTried: channels.map((c) => c.id), @@ -965,6 +1026,51 @@ function selfTest() { assert('an-unaudited-repo-never-renders-as-a-clean-window', !withAbsent.includes('✅') && withAbsent.includes('UNAUDITED') && withAbsent.includes('NOT a clean window'), withAbsent); assert('and-the-clean-case-does-print-the-tick', clean.includes('✅'), clean); assert('the-unaudited-line-names-the-repo-and-the-reason', withAbsent.includes('objectstack-ai/cloud') && withAbsent.includes('no git checkout'), withAbsent); + + // ── a truncated history is UNAUDITED, not a clean sweep (#9902) ─────────── + // + // The measured shape: a window crossing this container's graft floor swept + // ONE mainline commit, found no governed surface on it, and rendered + // `✅ clean window` over ~40 governed merges that GitHub lists for the same + // window. The tick is what a maintainer reads, so the assertions below are + // on the tick and on the direction the reason names, not on the phrase. + const truncated = truncatedHorizonReason({ + ref: 'origin/main', + horizon: { + reason: "this clone is shallow and its oldest visible commit on 'origin/main' is 2026-06-02, which sits INSIDE the window", + remedy: 'git -C /w/objectstack fetch --unshallow origin', + }, + }); + assert('a-truncated-horizon-reason-names-the-ref-and-the-floor', truncated.includes('origin/main') && truncated.includes('2026-06-02'), truncated); + assert('and-it-names-the-DIRECTION-under-enumeration-reads-as-compliance', /COMPLIANCE/.test(truncated), truncated); + assert('and-it-carries-a-runnable-remedy', truncated.includes('fetch --unshallow'), truncated); + const withTruncated = renderReport({ + sinceIso: '2026-05-23T00:00:00Z', + repos: [{ ...allAudited[0], status: 'unaudited', reason: truncated, scanned: 1 }, ...allAudited.slice(1)], + scanned: 1, + entries: [], + lookups: 0, + }); + assert('a-repo-whose-history-stops-inside-the-window-never-renders-the-green-tick', + !withTruncated.includes('✅') && withTruncated.includes('UNAUDITED') && withTruncated.includes('NOT a clean window'), withTruncated); + assert('and-the-sweep-says-which-repo-could-not-see-the-window', withTruncated.includes('objectstack-ai/objectstack') && withTruncated.includes('INSIDE the window'), withTruncated); + + // The other leg, and the one a bare `--is-shallow-repository` guard would + // have broken: a shallow checkout deep enough for the window sweeps as + // normal, and its answer carries the floor it was computed against. + assert('a-shallow-clone-whose-floor-predates-the-window-describes-itself-as-such', + describeHorizon({ shallow: true, floor: '2026-06-02' }) === 'shallow, oldest visible 2026-06-02 (predates the window)'); + assert('a-complete-clone-says-so-instead', describeHorizon({ shallow: false, floor: null }) === 'complete clone'); + const sweptShallow = renderReport({ + sinceIso: '2026-08-20T00:00:00Z', + repos: allAudited.map((r) => ({ ...r, horizon: describeHorizon({ shallow: true, floor: '2026-06-02' }) })), + scanned: 12, + entries: [], + lookups: 0, + }); + assert('an-audited-repo-prints-the-horizon-it-was-swept-against', + sweptShallow.includes('history shallow, oldest visible 2026-06-02'), sweptShallow); + assert('and-a-covered-shallow-sweep-is-still-allowed-to-be-clean', sweptShallow.includes('✅'), sweptShallow); const noPr = classifyCommit({ sha: 'd'.repeat(40), date: '2026-08-18T00:00:00Z', subject: 'chore: direct push' }, ['AGENTS.md'], GOVERNED_REPOS[0]); const loud = renderReport({ sinceIso: '2026-08-17T00:00:00Z', repos: allAudited, scanned: 3, entries: [noPr], lookups: 0 }); assert('a-pr-less-mainline-commit-is-its-own-loud-entry', loud.includes('NO PR NUMBER IN SUBJECT'), loud); diff --git a/scripts/pm/git-history.mjs b/scripts/pm/git-history.mjs index dcd301455a..851e4ceec7 100644 --- a/scripts/pm/git-history.mjs +++ b/scripts/pm/git-history.mjs @@ -247,6 +247,60 @@ export function describeFloor(boundaries) { return new Date(Math.max(...boundaries)).toISOString().slice(0, 10); } +/** + * The READ-ONLY half of this tool, for callers that answer their own windowed + * question and only need to know whether they are allowed to (#9902). + * + * It never fetches, and that is a decision rather than an omission. The three + * seat-run adopters either read a checkout they do not own (`check-governed- + * merges` sweeps four sibling repos; `collect-release-notes` reads the release + * engineer's `cloud` tree) or must not move the ground under an audit + * mid-sweep. So deepening stays an operator action with a named command — and + * the command is computed by `chooseDeepenSince()`, so the remedy this prints + * can only ever ADD history. The naive `--shallow-since=` is + * measured in this file's header SHORTENING a clone by 1380 commits at exit 0, + * and a remedy that does that would be the defect wearing a fix's clothes. + * Callers that want the deepen-and-re-prove path keep using + * `ensureWindowCovered()`. + * + * `floor` is the horizon to print BESIDE an answer that was allowed: these + * numbers are evidence (a compliance list, an ADR trigger metric, release + * notes), and evidence carries its provenance whether or not it is short. + * + * @returns {{covered: boolean, shallow: boolean, floor: string|null, tip: string, + * reason: string|null, remedy: string|null}} + */ +export function historyHorizon({ cwd, ref, sinceMs, marginDays }) { + const shallow = isShallow(cwd); + const boundaries = boundaryTimes(cwd, ref); + if (boundaries === null) { + return { + covered: false, + shallow, + floor: null, + tip: 'unknown', + reason: `ref '${ref}' does not resolve in ${cwd}`, + remedy: `git -C ${cwd} fetch origin`, + }; + } + const covered = windowIsCovered({ shallow, boundaries, sinceMs }); + const floor = shallow ? describeFloor(boundaries) : null; + const tip = refTip(cwd, ref); + if (covered) return { covered: true, shallow, floor, tip, reason: null, remedy: null }; + const deepenSince = chooseDeepenSince({ sinceMs, boundaries, marginDays }).slice(0, 10); + return { + covered: false, + shallow, + floor, + tip, + reason: + boundaries.length === 0 + ? `${cwd} is shallow and no history boundary could be read on '${ref}'` + : `this clone is shallow and its oldest visible commit on '${ref}' is ${floor}, which sits INSIDE the window`, + remedy: `git -C ${cwd} fetch --shallow-since=${deepenSince} origin # or: git -C ${cwd} fetch --unshallow origin`, + }; +} + // ── CLI ────────────────────────────────────────────────────────────────────── function usage(msg) { @@ -478,6 +532,36 @@ function selfTest() { t('the clone is still shallow after that answer — proving the predicate is the floor, ' + 'not the shallow flag', isShallow(shallowDeep) === true); + // ── historyHorizon: the read-only reading the #9902 adopters call ─────── + const hFull = historyHorizon({ cwd: full, ref: 'origin/main', sinceMs: Date.parse('2026-06-20') }); + t('historyHorizon clears a complete clone and reports no floor', + hFull.covered === true && hFull.shallow === false && hFull.floor === null && hFull.remedy === null, + JSON.stringify(hFull)); + t('and it carries the ref tip, so an allowed answer can still be printed with its horizon', + /^\d{4}-\d{2}-\d{2}$/.test(hFull.tip), JSON.stringify(hFull)); + + const hShort = historyHorizon({ cwd: shallowDeep, ref: 'origin/main', sinceMs: Date.parse('2026-06-20') }); + t('historyHorizon REFUSES the window raw git answered with 5 instead of 21', + hShort.covered === false, JSON.stringify(hShort)); + t('and it names the floor rather than only saying "shallow"', + hShort.floor === '2026-07-06' && /INSIDE the window/.test(hShort.reason ?? ''), JSON.stringify(hShort)); + // The measured hazard again, this time in the REMEDY: a printed + // `--shallow-since` newer than the floor shortens the clone at exit 0. + const remedyDate = /--shallow-since=(\d{4}-\d{2}-\d{2})/.exec(hShort.remedy ?? '')?.[1]; + t('and the deepen command it prints can only ADD history — its --shallow-since is never ' + + 'newer than the floor already present', + remedyDate !== undefined && Date.parse(remedyDate) <= Date.parse('2026-07-06'), String(hShort.remedy)); + + const hNarrow = historyHorizon({ cwd: shallowDeep, ref: 'origin/main', sinceMs: Date.parse('2026-07-08') }); + t('a shallow clone whose floor predates the window is CLEARED by historyHorizon too — ' + + 'the adopters must not refuse answers that are provably right', + hNarrow.covered === true && hNarrow.shallow === true, JSON.stringify(hNarrow)); + t('and a cleared shallow clone still reports its floor, so the number travels with its horizon', + hNarrow.floor === '2026-07-06', JSON.stringify(hNarrow)); + + t('an unresolvable ref is refused rather than read as covered', + historyHorizon({ cwd: shallowDeep, ref: 'origin/nope', sinceMs: Date.parse('2026-07-08') }).covered === false); + // No remote to deepen from: refuse, never answer. const orphan = join(root, 'orphan'); g(['clone', '--quiet', '--depth=5', `file://${up}`, orphan], root); From 2d137b1abf6fa491969d399215e503d674b58f13 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 01:21:12 +0000 Subject: [PATCH 2/2] fix(pm-tooling): stop the horizon annotation claiming what it has not checked Two defects found by ablating the guards rather than by reading them. - check-engine-split-ratio: the horizon line spelled "(predates the window)" unconditionally, so removing the refusal above it produced a false statement beside a wrong number instead of no statement. - collect-release-notes --self-test: the wiring pin grepped the script for a literal that also occurs in the assertion itself, so it matched its own source line and printed a tick with section 4 unwired. The needle is now assembled from two adjacent literals, and the ablation reddens it. Part of #9902 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt --- scripts/check-engine-split-ratio.mjs | 14 ++++++++++++-- scripts/collect-release-notes.sh | 10 +++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/scripts/check-engine-split-ratio.mjs b/scripts/check-engine-split-ratio.mjs index 9dc9446f3b..cc4c243e43 100644 --- a/scripts/check-engine-split-ratio.mjs +++ b/scripts/check-engine-split-ratio.mjs @@ -97,7 +97,13 @@ function arg(argv, name, fallback) { */ export function renderHorizon(horizon) { if (!horizon.shallow) return ` history horizon: complete clone; ref tip ${horizon.tip}`; - return ` history horizon: shallow clone, oldest visible commit ${horizon.floor} (predates the window); ref tip ${horizon.tip}`; + // `(predates the window)` is a CLAIM, so it is spelled only when it holds. + // Measured while ablating the refusal above: with the guard removed this line + // still printed "predates the window" beside a floor that sat inside it — + // an annotation written on the assumption that it was reached only after a + // pass becomes a false statement the moment anything reaches it otherwise. + const placed = horizon.covered ? ' (predates the window)' : ' — INSIDE the window, this metric is not measurable here'; + return ` history horizon: shallow clone, oldest visible commit ${horizon.floor}${placed}; ref tip ${horizon.tip}`; } /** The refusal, as words. Pure, so --self-test pins that it names both facts. */ @@ -220,7 +226,11 @@ function selfTest() { renderHorizon({ shallow: false, tip: '2026-08-21' }).includes('complete clone') && renderHorizon({ shallow: false, tip: '2026-08-21' }).includes('2026-08-21')); t('an ALLOWED shallow answer still prints its floor — the number travels with its horizon', - renderHorizon({ shallow: true, floor: '2026-06-02', tip: '2026-08-21' }).includes('2026-06-02')); + renderHorizon({ shallow: true, covered: true, floor: '2026-06-02', tip: '2026-08-21' }).includes('2026-06-02')); + t('and the horizon line never CLAIMS the floor predates the window unless it does', + !renderHorizon({ shallow: true, covered: false, floor: '2026-06-02', tip: '2026-08-21' }).includes('predates') + && /INSIDE the window/.test(renderHorizon({ shallow: true, covered: false, floor: '2026-06-02', tip: '2026-08-21' })), + renderHorizon({ shallow: true, covered: false, floor: '2026-06-02', tip: '2026-08-21' })); const refusal = renderRefusal({ horizon: { reason: 'floor sits INSIDE the window', tip: '2026-08-21', remedy: 'git -C /x fetch --unshallow origin' }, days: 90, diff --git a/scripts/collect-release-notes.sh b/scripts/collect-release-notes.sh index 191024b19a..dc050ea18e 100755 --- a/scripts/collect-release-notes.sh +++ b/scripts/collect-release-notes.sh @@ -131,8 +131,16 @@ if [[ "${1:-}" == "--self-test" ]]; then "$([[ "$(git -C "$tmp/shallow" rev-parse --is-shallow-repository)" == true ]] && echo 1 || echo 0)" # Wiring: the guard is worthless if section 4 stops calling it. + # + # The needle is ASSEMBLED from two adjacent literals rather than written out, + # and that is load-bearing. Written out, the pattern occurs in this very + # assertion, so the grep matches its own source line and the case passes with + # section 4 unwired — measured exactly that way while ablating this guard: + # the call was deleted, the script still parsed, and the pin printed a tick. + # A pin that cannot fail is worse than no pin, because it is believed. + needle="elif ! cloud_window""_guard \"\$CLOUD_ROOT\"" ok "section 4 routes its windowed question through the guard" \ - "$(grep -q 'cloud_window_guard "$CLOUD_ROOT" "$prev_date"' "${BASH_SOURCE[0]}" && echo 1 || echo 0)" + "$(grep -qF "$needle" "${BASH_SOURCE[0]}" && echo 1 || echo 0)" "needle: $needle" echo if [[ "$fails" == 0 ]]; then echo "collect-release-notes --self-test: all cases passed."; else echo "collect-release-notes --self-test: $fails FAILED."; exit 1; fi