From d977c7a5f4b6b11d2e01976e19d2f931e8b99515 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 18:05:17 +0000 Subject: [PATCH] =?UTF-8?q?fix(pm):=20the=20charter=20latest-touch=20readi?= =?UTF-8?q?ng=20is=20taken=20depth-independently=20=E2=80=94=20git-history?= =?UTF-8?q?.mjs=20touch=20proves=20the=20sha=20or=20refuses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a shallow clone `git log -1 -- ` names the graft boundary as the last touch of a path the boundary never changed: the boundary's object still names its parent, the graft hides that parent from traversal, and git diffs the boundary against the empty tree, so every path in its tree reads as touched there — a real sha, exit 0, no warning. Measured at two depths on a constructed repo and on a 50-deep clone of this one; the `git show --stat` leg the card proposed is fooled by the same empty-tree diff. `git-history.mjs touch --path=` answers the last-touch sha only when every parent the commit object names is present locally and the diff against them touches the path; otherwise it deepens with `--deepen=N` (additive, doubling), then `--unshallow`, then refuses with exit 2 and empty stdout. The open-round mutex line in SKILL.md names it, re-keyed in place at 117 B. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr --- .claude/skills/pm-dispatch/SKILL.md | 2 +- scripts/pm/git-history.mjs | 373 +++++++++++++++++++++++++++- 2 files changed, 365 insertions(+), 10 deletions(-) diff --git a/.claude/skills/pm-dispatch/SKILL.md b/.claude/skills/pm-dispatch/SKILL.md index 55ce8ee057..2ce30ce37c 100644 --- a/.claude/skills/pm-dispatch/SKILL.md +++ b/.claude/skills/pm-dispatch/SKILL.md @@ -86,7 +86,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 - 收班简报是前任不再写的显式声明,是释放标记不是锁:简报即最新事件 ⇒ 立即坐席。 - 滞后标题是进场顺手修的半状态,⛔ 不是阻塞;简报点名的留守尾巴作围栏。 - 维护者明示召唤是仲裁:有简报径直坐席;无简报才走保守确认,确认终止即坐席。 -- 互斥清 ⇒ fetch 后读三章程文件(本文、core-rules、本席章程)在 `origin/main` 的最新触碰 sha。 +- 互斥清 ⇒ fetch 后三章程(本文、core-rules、本席章程)最新触碰 sha 走 `git-history.mjs touch`。 - 异于上一开轮标记即先重读;留开轮标记(session ID + fire 时刻 + 该触碰,注明重读)再跑轮。 - 同读 harness 载入面 `.claude/{settings.json,agents/*.md,hooks/*}` 的最新触碰是否已在共享检出 HEAD。 - 否 ⇒ 收班、换新会话再派,⛔ 不推进共享检出;读数走 `scripts/pm/check-harness-current.mjs`。 diff --git a/scripts/pm/git-history.mjs b/scripts/pm/git-history.mjs index 6c1b474231..bfed6a1b58 100644 --- a/scripts/pm/git-history.mjs +++ b/scripts/pm/git-history.mjs @@ -3,18 +3,21 @@ /** * git-history.mjs — answer a "how many commits on in " question, - * or REFUSE, so a churn number can never be quoted from truncated history - * (#9878). + * or a "which commit last touched on " question, or REFUSE, so + * neither a churn number nor a provenance sha can ever be quoted from + * truncated history (#9878). * * node scripts/pm/git-history.mjs count --since=2026-07-19 [--until=2026-08-19] * node scripts/pm/git-history.mjs count --days=30 [--ref=origin/main] [--path=packages/core] * node scripts/pm/git-history.mjs log --since=2026-07-19 [--format=%H%x09%s] * node scripts/pm/git-history.mjs ensure --since=2026-07-19 # deepen + prove, answer nothing + * node scripts/pm/git-history.mjs touch --path= [--ref=origin/main] [--format=%H] + * # the last commit on that touched , PROVEN * node scripts/pm/git-history.mjs --self-test * * Exit codes: **0** answered, and the answer is provable · **2** refused — the - * window is not fully present locally and could not be made so · **1** usage or - * environment. + * window (or the touch) is not provably present locally and could not be made + * so · **1** usage or environment. * * On success the ANSWER alone goes to stdout (so `$(...)` capture works) and a * one-line method receipt goes to stderr, ready to paste beside the number. @@ -62,6 +65,36 @@ * the window sits entirely ABOVE the shallow floor, so that is what is checked: * the newest boundary commit reachable from the ref must predate `--since`. * + * ## The second trap, same root: `git log -1 -- ` on a shallow clone + * + * The windowed guard covers COUNTS. A provenance lookup — "which commit last + * touched " — is broken by the same graft in a worse way, because the + * answer is a single real sha with nothing beside it to look wrong. Measured + * 2026-09-15 on a constructed 40-commit repo whose `charter.md` was last + * touched at c2, cloned at two depths: + * + * | clone | `git log -1 origin/main -- charter.md` | its parent | `git show --stat -- charter.md` | + * |--------------|----------------------------------------|---------------------|---------------------------------------| + * | `--depth=5` | c35 — the graft boundary | named, NOT present | ` charter.md | 1 +` (non-empty) | + * | `--depth=20` | c20 — the graft boundary | named, NOT present | ` charter.md | 1 +` (non-empty) | + * | deepened | c2 — correct | present | ` charter.md | 2 +-` | + * + * Mechanism, established rather than inferred: the boundary commit's OBJECT + * still names its parent, but the graft hides that parent from traversal, so + * git diffs the boundary against the EMPTY tree — every path in its tree reads + * as "added here", the pathspec walk stops, and `-1` prints the boundary as a + * real, plausible sha at exit 0 with no warning. Two depths name two different + * shas for one reason. And the natural verification leg — `git show --stat + * -- ` printing a line — is fooled by the SAME empty-tree diff: the + * whole file reads as an insertion. So `touch` proves an answer two ways before + * printing it: every parent the commit object names is PRESENT locally (a + * boundary fails here; a real root names none and passes), and the diff + * against those parents touches the path. Unprovable ⇒ `fetch --deepen=N` + * (additive by definition — it counts from the current boundary, never from + * the tip — doubling from 64), then `--unshallow`, then REFUSE with exit 2 and + * empty stdout. The predicate is the parent's presence, never the shallow + * flag: a clone that is still shallow answers the moment the parent is here. + * * ## Cost, measured — because a tool nobody runs fixes nothing * * | case | wall | @@ -100,11 +133,12 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'pure decisions': 10, 'real repos': 15, 'historyHorizon: the read-only reading the #9902 adopters call': 12, + 'touch: the provenance reading a shallow clone fabricates': 17, }); // DELETING an entry silences that battery's floor exactly as effectively as // zeroing it, so the roster's own size is pinned too. -const SELF_TEST_BATTERY_FLOOR = 3; +const SELF_TEST_BATTERY_FLOOR = 4; // The key an assertion is filed under when no battery is open. It is not a // declared battery, so it reds by the same set difference rather than silently @@ -332,6 +366,168 @@ export function historyHorizon({ cwd, ref, sinceMs, marginDays }) { }; } +// ── touch: the provenance reading ──────────────────────────────────────────── + +const DEFAULT_DEEPEN_STEP = 64; +/** Deepen steps double from DEFAULT_DEEPEN_STEP up to this before `--unshallow`. */ +const MAX_DEEPEN_STEP = 4096; + +/** + * The parents a commit OBJECT names, each with whether it is present locally. + * Read from the object rather than from `rev-list --parents`, because a + * shallow graft is applied at traversal and never rewrites the object: the + * boundary commit still says `parent `, and `` is simply not here. + * That is what tells a boundary (names a parent this clone lacks) from a real + * root (names none) — the shallow flag cannot, and neither can `--max-parents=0`, + * which lists both. + * + * @returns {Array<{sha: string, present: boolean}>|null} null when `sha` is not readable here. + */ +export function objectParents(cwd, sha) { + const raw = git(['cat-file', '-p', sha], { cwd, allowFail: true }); + if (raw === null) return null; + const parents = []; + for (const line of raw.split('\n')) { + if (line === '') break; // the commit header ends at the first blank line + if (line.startsWith('parent ')) parents.push(line.slice('parent '.length).trim()); + } + return parents.map((p) => ({ + sha: p, + present: git(['cat-file', '-e', `${p}^{commit}`], { cwd, allowFail: true }) !== null, + })); +} + +/** + * Is `sha` a PROVABLE answer to "the last commit on the ref that touched + * `path`"? Two legs, both required, both pure reads: + * + * 1. every parent the commit object names is present locally — a graft + * boundary names one that is not, and git then diffs it against the + * empty tree, so every path in its tree reads as touched there; + * 2. the diff against those parents (against the empty tree for a real + * root) touches `path` — the `git show --stat -- ` leg in its + * machine form, which is only meaningful once leg 1 holds. + * + * @returns {{provable: boolean, boundary: boolean, parents: Array<{sha: string, present: boolean}>|null, + * touched: string[], reason: string|null}} + */ +export function touchIsProvable({ cwd, sha, path }) { + const parents = objectParents(cwd, sha); + if (parents === null) { + return { provable: false, boundary: false, parents, touched: [], reason: `commit ${sha} is not readable in this checkout` }; + } + const missing = parents.filter((p) => !p.present); + if (missing.length > 0) { + return { + provable: false, + boundary: true, + parents, + touched: [], + reason: + `${sha.slice(0, 9)} is a shallow graft boundary — its object names parent ${missing[0].sha.slice(0, 9)}, ` + + 'which this clone does not have, so git diffed it against the EMPTY tree and every path in its tree ' + + `reads as touched there; nothing says whether ${path} was really changed by it`, + }; + } + const rootArgs = parents.length === 0 ? ['--root'] : []; + const out = git(['diff-tree', '-r', '-m', '--no-commit-id', '--name-only', ...rootArgs, sha, '--', path], { cwd, allowFail: true }); + const touched = (out || '').split('\n').filter(Boolean); + if (touched.length === 0) { + return { + provable: false, + boundary: false, + parents, + touched, + reason: `${sha.slice(0, 9)} has its parent(s) locally but its diff does not touch ${path}`, + }; + } + return { provable: true, boundary: false, parents, touched, reason: null }; +} + +/** + * `git log -1 -- `, raw. null when the ref does not resolve; the + * empty string when no commit visible on the ref touches the path — which on + * a shallow clone means nothing yet. + */ +export function lastTouch(cwd, ref, path) { + const out = git(['log', '-1', '--format=%H', ref, '--', path], { cwd, allowFail: true }); + return out === null ? null : out.trim(); +} + +/** + * Take the provenance reading, deepening only as far as proving it needs. + * Deepens with `--deepen=N` (counted from the current boundary, so it can only + * ADD history — the `--shallow-since` hazard in the header does not arise), + * N doubling from `deepenStep`, then `--unshallow`. Fetching also refreshes + * the remote-tracking ref, so a deepened answer is read at the fetched tip. + * + * @returns {{answered: boolean, sha: string|null, verdict: object, steps: string[], reason: string|null}} + */ +export function ensureTouchProvable({ + cwd, ref, path, allowFetch = true, allowUnshallow = true, deepenStep = DEFAULT_DEEPEN_STEP, +}) { + const steps = []; + const attempt = () => { + const sha = lastTouch(cwd, ref, path); + if (sha === null) { + return { sha, verdict: { provable: false, boundary: false, reason: `ref '${ref}' does not resolve in this checkout` } }; + } + if (sha === '') { + const shallow = isShallow(cwd); + // A complete clone with no touch is an answer about the path (there is + // none); a shallow one may simply be hiding it below the floor. + return { + sha, + verdict: { + provable: !shallow, + boundary: shallow, + reason: shallow + ? `no commit visible on '${ref}' touches ${path}, and the clone is shallow — the touch may sit below the floor` + : `no commit on '${ref}' touches ${path}`, + }, + }; + } + return { sha, verdict: touchIsProvable({ cwd, sha, path }) }; + }; + + let r = attempt(); + if (r.verdict.provable) { + steps.push(isShallow(cwd) ? 'proved without fetching (the touch and its parent sit above the shallow floor)' : 'complete clone (no fetch)'); + return { answered: true, sha: r.sha, verdict: r.verdict, steps, reason: null }; + } + if (r.sha === null) return { answered: false, sha: null, verdict: r.verdict, steps, reason: r.verdict.reason }; + if (!allowFetch) { + return { answered: false, sha: r.sha, verdict: r.verdict, steps, reason: `${r.verdict.reason}; --no-fetch was given` }; + } + const remotes = (git(['remote'], { cwd, allowFail: true }) || '').split('\n').filter(Boolean); + const target = splitRemoteRef(ref, remotes); + if (!target) { + return { + answered: false, + sha: r.sha, + verdict: r.verdict, + steps, + reason: + `${r.verdict.reason}; '${ref}' names no remote to deepen from ` + + `(remotes here: ${remotes.length ? remotes.join(', ') : 'none'})`, + }; + } + for (let n = deepenStep; n <= MAX_DEEPEN_STEP && isShallow(cwd); n *= 2) { + const fetched = git(['fetch', `--deepen=${n}`, target.remote, target.branch], { cwd, allowFail: true }); + steps.push(`fetch --deepen=${n} ${target.remote} ${target.branch}${fetched === null ? ' (failed)' : ''}`); + if (fetched === null) break; + r = attempt(); + if (r.verdict.provable) return { answered: true, sha: r.sha, verdict: r.verdict, steps, reason: null }; + } + if (allowUnshallow && isShallow(cwd)) { + const un = git(['fetch', '--unshallow', target.remote], { cwd, allowFail: true }); + steps.push(`fetch --unshallow ${target.remote}${un === null ? ' (failed)' : ''}`); + r = attempt(); + if (r.verdict.provable) return { answered: true, sha: r.sha, verdict: r.verdict, steps, reason: null }; + } + return { answered: false, sha: r.sha, verdict: r.verdict, steps, reason: r.verdict.reason }; +} + // ── CLI ────────────────────────────────────────────────────────────────────── function usage(msg) { @@ -342,8 +538,10 @@ function usage(msg) { ' [--path=

]... [--no-first-parent] [--no-fetch] [--no-unshallow]\n' + ' node scripts/pm/git-history.mjs log --since=|--days= [--format=] [...]\n' + ' node scripts/pm/git-history.mjs ensure --since=|--days= [--ref=]\n' + + ' node scripts/pm/git-history.mjs touch --path= [--ref=] [--format=%H] [--deepen=]\n' + + ' [--no-fetch] [--no-unshallow]\n' + ' node scripts/pm/git-history.mjs --self-test\n\n' + - 'exit 0 answered · 2 refused (window not provably complete) · 1 usage/environment\n', + 'exit 0 answered · 2 refused (window or touch not provably complete) · 1 usage/environment\n', ); process.exit(1); } @@ -361,7 +559,8 @@ function parseArgs(argv) { case '--days': opts.days = Number(v); break; case '--ref': opts.ref = v; break; case '--path': opts.paths.push(v); break; - case '--format': opts.format = v; break; + case '--format': opts.format = v; opts.formatGiven = true; break; + case '--deepen': opts.deepen = Number(v); break; case '--margin-days': opts.marginDays = Number(v); break; case '--no-first-parent': opts.firstParent = false; break; case '--no-fetch': opts.fetch = false; break; @@ -403,7 +602,8 @@ function main(argv) { } const { cmd, opts } = parseArgs(argv); if (cmd === null) usage('a command is required'); - if (!['count', 'log', 'ensure'].includes(cmd)) usage(`unknown command '${cmd}'`); + if (!['count', 'log', 'ensure', 'touch'].includes(cmd)) usage(`unknown command '${cmd}'`); + if (cmd === 'touch') return touchMain(opts); const since = resolveSince(opts); const sinceMs = Date.parse(since); @@ -456,6 +656,65 @@ function main(argv) { return 0; } +/** + * `touch`: print the proven last-touch sha of `--path` on `--ref`, or refuse. + * Same contract as the windowed commands — the answer alone on stdout, a + * one-line method receipt on stderr, exit 2 with EMPTY stdout on refusal. + */ +function touchMain(opts) { + if (opts.since !== undefined || opts.days !== undefined) usage('touch takes no window — drop --since/--days'); + if (opts.paths.length !== 1) usage('touch needs exactly one --path='); + if (opts.deepen !== undefined && (!Number.isInteger(opts.deepen) || opts.deepen <= 0)) usage('--deepen must be a positive integer'); + const path = opts.paths[0]; + const cwd = opts.cwd || process.cwd(); + const format = opts.formatGiven ? opts.format : '%H'; + + const r = ensureTouchProvable({ + cwd, + ref: opts.ref, + path, + allowFetch: opts.fetch, + allowUnshallow: opts.unshallow, + deepenStep: opts.deepen, + }); + + if (r.answered && r.sha === '') { + // Provably nothing: a complete clone in which no commit on the ref touches + // the path. That is about the question, not the history — usage. + process.stderr.write(`git-history touch: ${r.verdict.reason} (ref: ${opts.ref}; ${r.steps.join(' · ')}).\n`); + return 1; + } + if (!r.answered) { + const remote = splitRemoteRef(opts.ref, ['origin']) ? 'origin' : ''; + process.stderr.write( + `⛔ git-history REFUSES to name a last-touch sha — ${r.reason}.\n` + + ` ref: ${opts.ref} path: ${path}` + + `${r.sha ? ` raw git log -1 said: ${r.sha.slice(0, 9)} (NOT a reading of the path)` : ''}\n` + + `${r.steps.length ? ` tried: ${r.steps.join(' · ')}\n` : ''}` + + ' The sha raw git prints here is real, plausible and WRONG — a shallow graft boundary is diffed\n' + + ' against the empty tree, so every path in its tree reads as touched there, at exit 0 (#9878).\n' + + ` Remedy: git -C ${cwd} fetch --deepen=${opts.deepen ?? DEFAULT_DEEPEN_STEP} ${remote} ${opts.ref.replace(/^[^/]+\//, '')}` + + ` # or: git -C ${cwd} fetch --unshallow ${remote}\n`, + ); + return 2; + } + + const answer = git(['log', '-1', `--format=${format}`, r.sha], { cwd }).trim(); + const when = git(['log', '-1', '--format=%cI', r.sha], { cwd }).trim(); + const stat = (git(['show', '--stat', '--format=', r.sha, '--', path], { cwd, allowFail: true }) || '') + .split('\n').map((l) => l.trim()).filter(Boolean)[0] ?? ''; + const parentsNote = r.verdict.parents.length === 0 + ? 'a real root (its object names no parent)' + : `${r.verdict.parents.length} parent(s) present locally`; + const receipt = + `method: git log -1 ${opts.ref} -- ${path} · touch ${r.sha.slice(0, 9)} ${when}` + + ` · proof: ${parentsNote}, diff-tree touches ${r.verdict.touched.join(' ')}${stat ? ` (${stat})` : ''}` + + ` · tip ${refTip(cwd, opts.ref)} · ${r.steps.join(' · ')}`; + process.stdout.write(`${answer}\n`); + process.stderr.write(`${receipt}\n`); + return 0; +} + // ── self-test ──────────────────────────────────────────────────────────────── // Set by `selfTest()` only after its verdict is printed, and read at the @@ -562,11 +821,16 @@ function selfTest() { 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, oldest first: 2026-06-01 .. 2026-07-10. + // 40 commits, one per day, oldest first: 2026-06-01 .. 2026-07-10. `f.txt` + // moves in every commit; `charter.md` only in c0..c2 (its last touch, c2, + // sits below every shallow floor the touch battery cuts); `root.txt` only + // in c0 (a real root, which names no parent and must still be provable). for (let i = 0; i < FIXTURE_COMMITS; i += 1) { const d = new Date(Date.parse(FIXTURE_EPOCH) + i * day).toISOString(); writeFileSync(join(up, 'f.txt'), `commit ${i}\n`); g(['add', 'f.txt'], up); + if (i <= 2) { writeFileSync(join(up, 'charter.md'), `charter ${i}\n`); g(['add', 'charter.md'], up); } + if (i === 0) { writeFileSync(join(up, 'root.txt'), 'root\n'); g(['add', 'root.txt'], up); } execFileSync('git', ['commit', '--quiet', '-m', `c${i}`], { cwd: up, encoding: 'utf8', @@ -682,6 +946,97 @@ function selfTest() { const ens = runCliAllowFail(['ensure', `--since=${NARROW_SINCE}`], shallowDeep); t('ensure proves coverage and prints no number', ens.code === 0 && ens.stdout.trim() === '', JSON.stringify(ens)); + + // ── touch: the provenance reading a shallow clone fabricates ──────────── + // The fixture's `charter.md` was last touched at c2; every shallow clone + // cut below floors above it. Measured on real history the same way: a + // 50-deep container clone named its own boundary as the last touch of a + // lane charter whose true touch sat a day below the floor. + battery('touch: the provenance reading a shallow clone fabricates'); + const trueTouch = g(['log', '-1', '--format=%H', 'origin/main', '--', 'charter.md'], full).trim(); + const trueRoot = g(['log', '-1', '--format=%H', 'origin/main', '--', 'root.txt'], full).trim(); + const tipSha = g(['rev-parse', 'origin/main'], full).trim(); + const boundaryOf = (cwd) => g(['rev-list', '--max-parents=0', 'origin/main'], cwd).trim(); + const rawTouch = (cwd, p) => g(['log', '-1', '--format=%H', 'origin/main', '--', p], cwd).trim(); + const short = (sha) => sha.slice(0, 9); + + const touch5 = join(root, 'touch5'); + g(['clone', '--quiet', '--depth=5', `file://${up}`, touch5], root); + const b5 = boundaryOf(touch5); + const raw5 = rawTouch(touch5, 'charter.md'); + t('BASELINE — on a depth-5 clone raw git names the graft boundary as the last touch of a file ' + + 'the boundary never changed (the defect, reproduced: a real sha, exit 0, no warning)', + raw5 === b5 && raw5 !== trueTouch, `raw ${short(raw5)} boundary ${short(b5)} true ${short(trueTouch)}`); + const boundaryParents = objectParents(touch5, b5); + t('the boundary OBJECT still names one parent and that parent is absent — the graft hides it ' + + 'from traversal, not from the object, which is what tells a boundary from a real root', + boundaryParents !== null && boundaryParents.length === 1 && boundaryParents[0].present === false, + JSON.stringify(boundaryParents)); + t('the naive verification leg is fooled: `git show --stat -- charter.md` prints a line ' + + '(the whole file as an insertion against the empty tree), so a non-empty stat proves nothing', + g(['show', '--stat', '--format=', b5, '--', 'charter.md'], touch5).trim() !== ''); + + const touch20 = join(root, 'touch20'); + g(['clone', '--quiet', '--depth=20', `file://${up}`, touch20], root); + const b20 = boundaryOf(touch20); + const raw20 = rawTouch(touch20, 'charter.md'); + t('a second depth names a second, different sha — its own boundary: one mechanism, seen from two depths', + raw20 === b20 && raw20 !== raw5 && raw20 !== trueTouch, `raw ${short(raw20)} boundary ${short(b20)}`); + + const vBoundary = touchIsProvable({ cwd: touch5, sha: b5, path: 'charter.md' }); + t('touchIsProvable refuses the boundary and says it is one', vBoundary.provable === false && vBoundary.boundary === true, + JSON.stringify(vBoundary)); + const vTrue = touchIsProvable({ cwd: full, sha: trueTouch, path: 'charter.md' }); + t('and accepts the true touch on the complete clone, listing the path its diff touches', + vTrue.provable === true && vTrue.touched.join() === 'charter.md', JSON.stringify(vTrue)); + + const refusedTouch = runCliAllowFail(['touch', '--path=charter.md', '--no-fetch'], touch5); + t('the CLI REFUSES the shallow reading with exit 2', refusedTouch.code === 2, JSON.stringify(refusedTouch)); + t('and stdout stays EMPTY — a captured sha is empty rather than plausible', refusedTouch.stdout.trim() === '', + JSON.stringify(refusedTouch.stdout)); + t('and the refusal names the boundary mechanism, the raw sha it is NOT printing, and a --deepen remedy', + /graft boundary/.test(refusedTouch.stderr) && refusedTouch.stderr.includes(short(b5)) && /--deepen=/.test(refusedTouch.stderr), + refusedTouch.stderr); + + // --deepen=34 from c35 lands the boundary on c1: c2's parent is present, the clone is still shallow. + const answeredTouch = runCliAllowFail(['touch', '--path=charter.md', '--deepen=34'], touch5); + t('with fetching allowed it deepens and answers the TRUE touch, exit 0', + answeredTouch.code === 0 && answeredTouch.stdout.trim() === trueTouch, JSON.stringify(answeredTouch)); + t('while the clone is STILL shallow — the predicate is the parent\'s presence, never the shallow flag', + isShallow(touch5) === true && /fetch --deepen=34/.test(answeredTouch.stderr) && /proof: 1 parent\(s\) present/.test(answeredTouch.stderr), + answeredTouch.stderr); + + // The worst case: --deepen=33 lands the boundary exactly ON c2 — the right sha for the wrong reason. + const touch5c = join(root, 'touch5c'); + g(['clone', '--quiet', '--depth=5', `file://${up}`, touch5c], root); + const onBoundary = runCliAllowFail(['touch', '--path=charter.md', '--deepen=33'], touch5c); + t('a deepen that lands the boundary exactly ON the true touch is not accepted at that step — the tool ' + + 'deepens again and only then answers the same sha, now with its parent present', + onBoundary.code === 0 && onBoundary.stdout.trim() === trueTouch && /--deepen=33 .*--deepen=66/.test(onBoundary.stderr), + onBoundary.stderr); + + const touch5b = join(root, 'touch5b'); + g(['clone', '--quiet', '--depth=5', `file://${up}`, touch5b], root); + const control = runCliAllowFail(['touch', '--path=f.txt', '--no-fetch'], touch5b); + t('FIRING CONTROL — a sha that did touch the path answers on the same shallow clone without any fetch: ' + + 'the check discriminates rather than always refusing', + control.code === 0 && control.stdout.trim() === tipSha && /without fetching/.test(control.stderr), JSON.stringify(control)); + const rootAnswer = runCliAllowFail(['touch', '--path=root.txt'], touch5b); + t('a real root is provable once reached: the deepen dissolves the graft and the answer is the root ' + + 'commit, whose object names no parent', + rootAnswer.code === 0 && rootAnswer.stdout.trim() === trueRoot && isShallow(touch5b) === false && /a real root/.test(rootAnswer.stderr), + JSON.stringify(rootAnswer)); + + const fullTouch = runCliAllowFail(['touch', '--path=charter.md', '--format=%h'], full); + t('a complete clone answers without fetching and honours --format', + fullTouch.code === 0 && fullTouch.stdout.trim() === g(['rev-parse', '--short', trueTouch], full).trim() + && /complete clone \(no fetch\)/.test(fullTouch.stderr), + JSON.stringify(fullTouch)); + const never = runCliAllowFail(['touch', '--path=never.txt'], full); + t('a path no commit on the ref ever touched is usage (exit 1) with empty stdout — neither a refusal nor an answer', + never.code === 1 && never.stdout.trim() === '' && /no commit on/.test(never.stderr), JSON.stringify(never)); + const noPath = runCliAllowFail(['touch'], full); + t('touch without --path is usage', noPath.code === 1 && noPath.stdout.trim() === '', JSON.stringify(noPath)); } finally { rmSync(root, { recursive: true, force: true }); }