diff --git a/dist/commands/backfill.d.ts b/dist/commands/backfill.d.ts index ff2a6ff5..7d920380 100644 --- a/dist/commands/backfill.d.ts +++ b/dist/commands/backfill.d.ts @@ -36,10 +36,5 @@ export interface BackfillOutcome { exitCode: number; } export declare const toBackfillOptions: (options: BackfillCommandOptions) => BackfillOptions; -/** - * Runs the command and reports what it would print. A failure comes back as an - * outcome rather than an exception: the one thing a user of a cold-start command - * should never see is a stack trace telling them nothing they can act on. - */ export declare const runBackfill: (options: BackfillCommandOptions) => BackfillOutcome; export declare const register: (program: Command) => void; diff --git a/dist/commands/backfill.js b/dist/commands/backfill.js index 05e3f4d8..3bddf3a4 100644 --- a/dist/commands/backfill.js +++ b/dist/commands/backfill.js @@ -17,6 +17,7 @@ * records, stopped at --limit" and "12 records, that was all of them" are * different facts and the user acts differently on each. */ +import { notesAvailability } from '../core/notes.js'; import { DEFAULT_BATCH_SIZE, DEFAULT_LIMIT, backfill, buildEnvelope, } from '../core/backfill.js'; const PREFIX = 'commitlore:'; /** What each stop reason means to somebody deciding whether to run it again. */ @@ -120,32 +121,66 @@ const jsonPayload = (result) => `${JSON.stringify({ prompt: entry.prompt, })), }, null, 2)}\n`; -const present = (result, json) => { +const present = (result, json, incomplete = null) => { + const caveat = incomplete === null + ? '' + : `${PREFIX} the notes mirror is ${incomplete}, so "no record" below means "none this clone can see" — ` + + `counts of note trailers and of targets are both drawn from an index that could not read it. ` + + `fix: commitlore doctor --fix, then git fetch\n`; const detail = [ ...result.report.discarded.map(discardLine), ...result.report.skipped.map(skipLine), ].join(''); if (json) - return { stdout: jsonPayload(result), stderr: detail, exitCode: 0 }; + return { stdout: jsonPayload(result), stderr: caveat + detail, exitCode: 0 }; /* In prompt mode stdout is the payload a session consumes, so the summary goes to stderr and stays out of whatever the user pipes it into. */ if (result.report.mode === 'prompt-only') { return { stdout: promptPayload(result), - stderr: `${detail}${summary(result.report)}`, + stderr: `${caveat}${detail}${summary(result.report)}`, exitCode: 0, }; } - return { stdout: summary(result.report), stderr: detail, exitCode: 0 }; + return { stdout: summary(result.report), stderr: caveat + detail, exitCode: 0 }; }; /** * Runs the command and reports what it would print. A failure comes back as an * outcome rather than an exception: the one thing a user of a cold-start command * should never see is a stack trace telling them nothing they can act on. */ +/** + * #403, and PRD Phase 0: fail closed when the notes mirror was not read. + * + * `backfill` selects its targets from the index — "commits with no record". A + * commit whose record lives only in an unfetched note has, from here, no record, + * so it is eligible for reconstruction. Writing then produces a second record + * for one decision: either a `Record-Id` collision, or worse, no collision and + * two records the lifecycle fold cannot reconcile. + * + * The read-only modes still run — they are how a user diagnoses this — but they + * say the selection was drawn from an index that could not see the mirror. Only + * the writing mode refuses, because only it can create the duplicate. + */ +const NOT_COMPLETE = new Set(['unfetched', 'diverged', 'shallow']); +const incompleteMirror = (options) => { + const state = notesAvailability(options.cwd === undefined ? {} : { cwd: options.cwd }); + return NOT_COMPLETE.has(state) ? state : null; +}; export const runBackfill = (options) => { + const incomplete = incompleteMirror(options); + const writes = options.draft !== undefined && options.dryRun !== true; + if (incomplete !== null && writes) { + return { + stdout: '', + stderr: `${PREFIX} the notes mirror is ${incomplete}, so a record that already exists upstream reads here ` + + `as a commit with none. Reconstructing now can write a second record for one decision.\n` + + `${PREFIX} fix: commitlore doctor --fix, then git fetch, then rerun\n`, + exitCode: 2, + }; + } try { - return present(backfill(toBackfillOptions(options)), options.json === true); + return present(backfill(toBackfillOptions(options)), options.json === true, incomplete); } catch (error) { const detail = error instanceof Error ? error.message : String(error); diff --git a/dist/commands/backfill.js.map b/dist/commands/backfill.js.map index 907ade0b..25cc4806 100644 --- a/dist/commands/backfill.js.map +++ b/dist/commands/backfill.js.map @@ -1 +1 @@ -{"version":3,"file":"backfill.js","sourceRoot":"","sources":["../../src/commands/backfill.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"backfill.js","sourceRoot":"","sources":["../../src/commands/backfill.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/commitlore.mjs b/dist/commitlore.mjs index 295c73b4..062d28f6 100755 --- a/dist/commitlore.mjs +++ b/dist/commitlore.mjs @@ -11090,10 +11090,6 @@ function useColor() { // node_modules/commander/index.js var program = new Command(); -// src/core/backfill.ts -import { spawnSync as spawnSync2 } from "node:child_process"; -import { readFileSync as readFileSync2 } from "node:fs"; - // src/core/git.ts import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; @@ -11145,38 +11141,6 @@ var hasShallowHistory = (cwd) => { return shallow.code === 0 && existsSync(resolve(cwd, shallow.stdout.trim())); }; -// src/core/schema.ts -var import__ = __toESM(require__(), 1); - -// src/core/paths.ts -import { existsSync as existsSync2, readFileSync } from "node:fs"; -import { dirname, join, parse } from "node:path"; -import { fileURLToPath } from "node:url"; -var findPackageRoot = (startDir) => { - const { root } = parse(startDir); - let dir = startDir; - for (; ; ) { - if (existsSync2(join(dir, "package.json"))) return dir; - if (dir === root) { - throw new Error( - `could not find package.json above ${startDir} \u2014 this installation is incomplete` - ); - } - dir = dirname(dir); - } -}; -var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url))); -var installedPath = (...segments) => join(PACKAGE_ROOT, ...segments); -var readInstalledFile = (...segments) => readFileSync(installedPath(...segments), "utf8"); -var cachedVersion = null; -var packageVersion = () => { - if (cachedVersion !== null) return cachedVersion; - const raw = readInstalledFile("package.json"); - const parsed = JSON.parse(raw); - cachedVersion = typeof parsed.version === "string" ? parsed.version : "0.0.0-unknown"; - return cachedVersion; -}; - // src/core/types.ts var KNOWN_KEYS = [ "Limit", @@ -11349,6 +11313,138 @@ var labelRecordBlocks = (message) => { }); }; +// src/core/notes.ts +var NOTES_REF = "refs/notes/commitlore"; +var NOTES_REFSPEC = "+refs/notes/*:refs/notes/*"; +var REF_ARG = `--ref=${NOTES_REF}`; +var NO_NOTE_EXIT = 1; +var SYNTHETIC_SUBJECT = "commitlore notes mirror"; +var gitOptions = (opts, stdin) => ({ + ...opts.cwd === void 0 ? {} : { cwd: opts.cwd }, + ...stdin === void 0 ? {} : { stdin } +}); +var resolveObject = (sha, opts) => execGitOrThrow( + ["rev-parse", "--verify", "--end-of-options", `${sha}^{object}`], + gitOptions(opts) +).trim(); +var writeBody = (sha, body, opts) => { + if (body === "") { + throw new Error( + `refusing to write an empty record to ${NOTES_REF} for ${sha}: an empty note body deletes the note` + ); + } + const object3 = resolveObject(sha, opts); + const args = ["notes", REF_ARG, "add"]; + if (opts.force === true) args.push("--force"); + args.push("--file", "-", "--end-of-options", object3); + const result = execGit(args, gitOptions(opts, body)); + if (result.code !== 0) { + throw Object.assign( + new Error( + `failed to write the record for ${object3} to ${NOTES_REF} (exit ${result.code}): ${result.stderr.trim()}` + ), + { code: result.code, stderr: result.stderr } + ); + } +}; +var writeRecord = (sha, trailers, opts = {}) => writeBody(sha, serializeTrailers(trailers), opts); +var writeRecordBlocks = (sha, blocks, opts = {}) => writeBody(sha, blocks.map(serializeTrailers).join("\n"), opts); +var showNote = (sha, opts) => { + const object3 = resolveObject(sha, opts); + const result = execGit( + ["notes", REF_ARG, "show", "--end-of-options", object3], + gitOptions(opts) + ); + if (result.code === NO_NOTE_EXIT) return null; + if (result.code !== 0) { + throw Object.assign( + new Error( + `failed to read the record for ${object3} from ${NOTES_REF} (exit ${result.code}): ${result.stderr.trim()}` + ), + { code: result.code, stderr: result.stderr } + ); + } + return result.stdout; +}; +var readRecord = (sha, opts = {}) => { + const note = showNote(sha, opts); + return note === null ? [] : parseCommitMessage(`${SYNTHETIC_SUBJECT} + +${note}`); +}; +var readRecordBlocks = (sha, opts = {}) => { + const note = showNote(sha, opts); + return note === null ? [] : parseRecordBlocks(`${SYNTHETIC_SUBJECT} + +${note}`); +}; +var listRecordShas = (opts = {}) => { + const stdout = execGitOrThrow(["notes", REF_ARG, "list"], gitOptions(opts)); + return stdout.split("\n").filter((line) => line.length > 0).map((line) => { + const [, object3 = ""] = line.split(" "); + return object3; + }).filter((object3) => object3.length > 0); +}; +var listRemotes = (opts) => { + const result = execGit(["remote"], gitOptions(opts)); + if (result.code !== 0) return []; + return result.stdout.split("\n").filter((line) => line.length > 0); +}; +var fetchRefspecs = (remote, opts) => { + const result = execGit(["config", "--get-all", `remote.${remote}.fetch`], gitOptions(opts)); + if (result.code !== 0) return []; + return result.stdout.split("\n").filter((line) => line.length > 0); +}; +var coversNotes = (refspec) => { + const [, destination = ""] = refspec.replace(/^\+/, "").split(":"); + if (destination === NOTES_REF) return true; + return destination.endsWith("/*") && NOTES_REF.startsWith(destination.slice(0, -1)); +}; +var notesAvailability = (opts = {}) => { + const ref = execGit(["rev-parse", "--verify", "--quiet", NOTES_REF], gitOptions(opts)); + if (ref.code === 0) return "present"; + const remotes = listRemotes(opts); + if (remotes.length === 0) return "absent"; + const uncovered = remotes.filter((remote) => !fetchRefspecs(remote, opts).some(coversNotes)); + return uncovered.length > 0 ? "unfetched" : "absent"; +}; + +// src/core/backfill.ts +import { spawnSync as spawnSync2 } from "node:child_process"; +import { readFileSync as readFileSync2 } from "node:fs"; + +// src/core/schema.ts +var import__ = __toESM(require__(), 1); + +// src/core/paths.ts +import { existsSync as existsSync2, readFileSync } from "node:fs"; +import { dirname, join, parse } from "node:path"; +import { fileURLToPath } from "node:url"; +var findPackageRoot = (startDir) => { + const { root } = parse(startDir); + let dir = startDir; + for (; ; ) { + if (existsSync2(join(dir, "package.json"))) return dir; + if (dir === root) { + throw new Error( + `could not find package.json above ${startDir} \u2014 this installation is incomplete` + ); + } + dir = dirname(dir); + } +}; +var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url))); +var installedPath = (...segments) => join(PACKAGE_ROOT, ...segments); +var readInstalledFile = (...segments) => readFileSync(installedPath(...segments), "utf8"); +var cachedVersion = null; +var packageVersion = () => { + if (cachedVersion !== null) return cachedVersion; + const raw = readInstalledFile("package.json"); + const parsed = JSON.parse(raw); + cachedVersion = typeof parsed.version === "string" ? parsed.version : "0.0.0-unknown"; + return cachedVersion; +}; + // src/core/schema.ts var import_ajv_formats = __toESM(require_dist(), 1); var addFormats = import_ajv_formats.default.default; @@ -12110,7 +12206,7 @@ var loadDatabaseCtor = () => { } }; var SCHEMA_VERSION = 2; -var NOTES_REF = "refs/notes/commitlore"; +var NOTES_REF2 = "refs/notes/commitlore"; var LOG_BATCH = 1024; var LOG_MAX_BUFFER = 256 * 1024 * 1024; var GIT_NO_SUCH_REF2 = 1; @@ -12322,7 +12418,7 @@ var readCommitRecords = (cwd, shas, excluded) => { return records; }; var readNoteRecords = (cwd, reachable, excluded) => { - const listed = execGitOrThrow(["notes", `--ref=${NOTES_REF}`, "list"], { cwd }); + const listed = execGitOrThrow(["notes", `--ref=${NOTES_REF2}`, "list"], { cwd }); const annotated = listed.split("\n").filter((line) => line !== "").map((line) => line.split(" ")[1] ?? "").filter((sha) => sha !== "" && reachable.has(sha)); if (annotated.length === 0) return []; const typed = execGitOrThrow(["cat-file", "--batch-check"], { @@ -12335,7 +12431,7 @@ var readNoteRecords = (cwd, reachable, excluded) => { const records = []; for (const batch of chunked(commits, LOG_BATCH)) { const result = gitLogByShas(cwd, batch, "%x01%H%x00%ct%x00%cI%x00%N%x00", [ - `--notes=${NOTES_REF}` + `--notes=${NOTES_REF2}` ]); if (result.code !== 0) { throw Object.assign(new Error(`git log --notes failed: ${result.stderr.trim()}`), { @@ -12594,7 +12690,7 @@ var deleteNoteRows = (handle) => { }); }; var indexNotes = (handle, opts = {}, excluded) => { - const refSha = revParseRef(handle.cwd, NOTES_REF); + const refSha = revParseRef(handle.cwd, NOTES_REF2); const indexed = readMeta(handle.db, "notes_ref_sha"); if (!(opts.force ?? false) && refSha === indexed) return 0; const records = refSha === null ? [] : readNoteRecords(handle.cwd, new Set(reachableFromHead(handle.cwd)), excluded); @@ -12633,7 +12729,7 @@ var rebuildIndex = (handle, opts = {}) => { const shas = head === null ? [] : revList(handle.cwd, "HEAD"); const excluded = /* @__PURE__ */ new Map(); const records = readCommitRecords(handle.cwd, shas, excluded); - const notesRef = revParseRef(handle.cwd, NOTES_REF); + const notesRef = revParseRef(handle.cwd, NOTES_REF2); const noteRecords = notesRef === null ? [] : readNoteRecords(handle.cwd, new Set(shas), excluded); const stats = { ...emptyStats(handle, started), @@ -12868,102 +12964,6 @@ var indexInfo = (handle) => ({ paths: handle.db.prepare("SELECT count(*) AS n FROM commit_paths").get()?.n ?? 0 }); -// src/core/notes.ts -var NOTES_REF2 = "refs/notes/commitlore"; -var NOTES_REFSPEC = "+refs/notes/*:refs/notes/*"; -var REF_ARG = `--ref=${NOTES_REF2}`; -var NO_NOTE_EXIT = 1; -var SYNTHETIC_SUBJECT = "commitlore notes mirror"; -var gitOptions = (opts, stdin) => ({ - ...opts.cwd === void 0 ? {} : { cwd: opts.cwd }, - ...stdin === void 0 ? {} : { stdin } -}); -var resolveObject = (sha, opts) => execGitOrThrow( - ["rev-parse", "--verify", "--end-of-options", `${sha}^{object}`], - gitOptions(opts) -).trim(); -var writeBody = (sha, body, opts) => { - if (body === "") { - throw new Error( - `refusing to write an empty record to ${NOTES_REF2} for ${sha}: an empty note body deletes the note` - ); - } - const object3 = resolveObject(sha, opts); - const args = ["notes", REF_ARG, "add"]; - if (opts.force === true) args.push("--force"); - args.push("--file", "-", "--end-of-options", object3); - const result = execGit(args, gitOptions(opts, body)); - if (result.code !== 0) { - throw Object.assign( - new Error( - `failed to write the record for ${object3} to ${NOTES_REF2} (exit ${result.code}): ${result.stderr.trim()}` - ), - { code: result.code, stderr: result.stderr } - ); - } -}; -var writeRecord = (sha, trailers, opts = {}) => writeBody(sha, serializeTrailers(trailers), opts); -var writeRecordBlocks = (sha, blocks, opts = {}) => writeBody(sha, blocks.map(serializeTrailers).join("\n"), opts); -var showNote = (sha, opts) => { - const object3 = resolveObject(sha, opts); - const result = execGit( - ["notes", REF_ARG, "show", "--end-of-options", object3], - gitOptions(opts) - ); - if (result.code === NO_NOTE_EXIT) return null; - if (result.code !== 0) { - throw Object.assign( - new Error( - `failed to read the record for ${object3} from ${NOTES_REF2} (exit ${result.code}): ${result.stderr.trim()}` - ), - { code: result.code, stderr: result.stderr } - ); - } - return result.stdout; -}; -var readRecord = (sha, opts = {}) => { - const note = showNote(sha, opts); - return note === null ? [] : parseCommitMessage(`${SYNTHETIC_SUBJECT} - -${note}`); -}; -var readRecordBlocks = (sha, opts = {}) => { - const note = showNote(sha, opts); - return note === null ? [] : parseRecordBlocks(`${SYNTHETIC_SUBJECT} - -${note}`); -}; -var listRecordShas = (opts = {}) => { - const stdout = execGitOrThrow(["notes", REF_ARG, "list"], gitOptions(opts)); - return stdout.split("\n").filter((line) => line.length > 0).map((line) => { - const [, object3 = ""] = line.split(" "); - return object3; - }).filter((object3) => object3.length > 0); -}; -var listRemotes = (opts) => { - const result = execGit(["remote"], gitOptions(opts)); - if (result.code !== 0) return []; - return result.stdout.split("\n").filter((line) => line.length > 0); -}; -var fetchRefspecs = (remote, opts) => { - const result = execGit(["config", "--get-all", `remote.${remote}.fetch`], gitOptions(opts)); - if (result.code !== 0) return []; - return result.stdout.split("\n").filter((line) => line.length > 0); -}; -var coversNotes = (refspec) => { - const [, destination = ""] = refspec.replace(/^\+/, "").split(":"); - if (destination === NOTES_REF2) return true; - return destination.endsWith("/*") && NOTES_REF2.startsWith(destination.slice(0, -1)); -}; -var notesAvailability = (opts = {}) => { - const ref = execGit(["rev-parse", "--verify", "--quiet", NOTES_REF2], gitOptions(opts)); - if (ref.code === 0) return "present"; - const remotes = listRemotes(opts); - if (remotes.length === 0) return "absent"; - const uncovered = remotes.filter((remote) => !fetchRefspecs(remote, opts).some(coversNotes)); - return uncovered.length > 0 ? "unfetched" : "absent"; -}; - // src/core/backfill.ts var DEFAULT_LIMIT = 50; var DEFAULT_BATCH_SIZE = 10; @@ -13527,24 +13527,42 @@ var jsonPayload = (result) => `${JSON.stringify( 2 )} `; -var present = (result, json) => { +var present = (result, json, incomplete = null) => { + const caveat = incomplete === null ? "" : `${PREFIX} the notes mirror is ${incomplete}, so "no record" below means "none this clone can see" \u2014 counts of note trailers and of targets are both drawn from an index that could not read it. fix: commitlore doctor --fix, then git fetch +`; const detail = [ ...result.report.discarded.map(discardLine), ...result.report.skipped.map(skipLine) ].join(""); - if (json) return { stdout: jsonPayload(result), stderr: detail, exitCode: 0 }; + if (json) return { stdout: jsonPayload(result), stderr: caveat + detail, exitCode: 0 }; if (result.report.mode === "prompt-only") { return { stdout: promptPayload(result), - stderr: `${detail}${summary(result.report)}`, + stderr: `${caveat}${detail}${summary(result.report)}`, exitCode: 0 }; } - return { stdout: summary(result.report), stderr: detail, exitCode: 0 }; + return { stdout: summary(result.report), stderr: caveat + detail, exitCode: 0 }; +}; +var NOT_COMPLETE = /* @__PURE__ */ new Set(["unfetched", "diverged", "shallow"]); +var incompleteMirror = (options) => { + const state = notesAvailability(options.cwd === void 0 ? {} : { cwd: options.cwd }); + return NOT_COMPLETE.has(state) ? state : null; }; var runBackfill = (options) => { + const incomplete = incompleteMirror(options); + const writes = options.draft !== void 0 && options.dryRun !== true; + if (incomplete !== null && writes) { + return { + stdout: "", + stderr: `${PREFIX} the notes mirror is ${incomplete}, so a record that already exists upstream reads here as a commit with none. Reconstructing now can write a second record for one decision. +${PREFIX} fix: commitlore doctor --fix, then git fetch, then rerun +`, + exitCode: 2 + }; + } try { - return present(backfill(toBackfillOptions(options)), options.json === true); + return present(backfill(toBackfillOptions(options)), options.json === true, incomplete); } catch (error2) { const detail = error2 instanceof Error ? error2.message : String(error2); return { stdout: "", stderr: `${PREFIX} ${detail} @@ -14633,7 +14651,7 @@ var runQuery = (opts = {}) => { const notes = notesAvailability({ cwd }); if (notes === "unfetched") { diagnostics.push( - `the notes mirror has not been fetched here, so this answer may be missing records that exist upstream (git fetch does not fetch ${NOTES_REF2} by default). fix: commitlore doctor --fix, then git fetch` + `the notes mirror has not been fetched here, so this answer may be missing records that exist upstream (git fetch does not fetch ${NOTES_REF} by default). fix: commitlore doctor --fix, then git fetch` ); } return { @@ -16833,7 +16851,7 @@ var captureHookStub = () => stubText(UNRESOLVED_CAPTURE); // src/commands/doctor.ts var PROBE_MESSAGE = "commitlore doctor probe\n\nLimit: probe\nBlast: local\n"; -var EXACT_NOTES_REFSPEC = `+${NOTES_REF2}:${NOTES_REF2}`; +var EXACT_NOTES_REFSPEC = `+${NOTES_REF}:${NOTES_REF}`; var EXACT_NOTES_REFSPEC_PATTERN = `^\\${EXACT_NOTES_REFSPEC}$`; var gitOptions3 = (opts) => opts.cwd === void 0 ? {} : { cwd: opts.cwd }; var check = (id, title, status, detail, fix = null, fixed = false, needsAttention = status === "warn" || status === "fail") => ({ id, title, status, needsAttention, detail, fix, fixed }); @@ -16875,7 +16893,7 @@ var checkRefspec = (opts) => { "notes-refspec", title, "warn", - `${missing.join(", ")} does not fetch ${NOTES_REF2}, so records pushed by others stay invisible here`, + `${missing.join(", ")} does not fetch ${NOTES_REF}, so records pushed by others stay invisible here`, missing.map((remote) => `git config --add remote.${remote}.fetch '${NOTES_REFSPEC}'`).join("\n") ); } @@ -16894,7 +16912,7 @@ var checkRefspec = (opts) => { "notes-refspec", title, "ok", - fixed ? `${NOTES_REF2} is now covered for ${remotes.join(", ")} \u2014 nothing has been fetched through it yet` : `git fetch succeeds for ${remotes.join(", ")} and covers ${NOTES_REF2}`, + fixed ? `${NOTES_REF} is now covered for ${remotes.join(", ")} \u2014 nothing has been fetched through it yet` : `git fetch succeeds for ${remotes.join(", ")} and covers ${NOTES_REF}`, fixed ? `git fetch ${remotes[0] ?? "origin"}` : null, fixed ); @@ -16903,8 +16921,8 @@ var checkPush = (opts) => { const title = "notes push"; const remotes = listRemotes(opts); const remote = remotes[0] ?? "origin"; - const command = `git push ${remote} ${NOTES_REF2}`; - const local = execGit(["rev-parse", "--verify", "--quiet", NOTES_REF2], gitOptions3(opts)); + const command = `git push ${remote} ${NOTES_REF}`; + const local = execGit(["rev-parse", "--verify", "--quiet", NOTES_REF], gitOptions3(opts)); if (local.code !== 0) { return check( "notes-push", @@ -16913,7 +16931,7 @@ var checkPush = (opts) => { `no local mirror yet \u2014 nothing to push (${command}, once there is)` ); } - const advertised = execGit(["ls-remote", remote, NOTES_REF2], gitOptions3(opts)); + const advertised = execGit(["ls-remote", remote, NOTES_REF], gitOptions3(opts)); if (advertised.code !== 0) { return check( "notes-push", @@ -16924,13 +16942,13 @@ var checkPush = (opts) => { ); } if (advertised.stdout.split(/\s/)[0] === local.stdout.trim()) { - return check("notes-push", title, "ok", `${remote} has the current ${NOTES_REF2}`); + return check("notes-push", title, "ok", `${remote} has the current ${NOTES_REF}`); } return check( "notes-push", title, "warn", - `this clone has local records in ${NOTES_REF2}; no command pushes them for you`, + `this clone has local records in ${NOTES_REF}; no command pushes them for you`, command ); }; @@ -18649,7 +18667,7 @@ var plural = (count2, unit) => `${count2} ${unit}${count2 === 1 ? "" : "s"}`; var reportUnfetchedNotes = (subject) => { if (notesAvailability() !== "unfetched") return; process.stderr.write( - `commitlore: the notes mirror has not been fetched here, so ${subject} covers the commit messages alone and may be missing records that exist upstream (git fetch does not fetch ${NOTES_REF2} by default). fix: commitlore doctor --fix, then git fetch, then rerun + `commitlore: the notes mirror has not been fetched here, so ${subject} covers the commit messages alone and may be missing records that exist upstream (git fetch does not fetch ${NOTES_REF} by default). fix: commitlore doctor --fix, then git fetch, then rerun ` ); }; diff --git a/src/commands/backfill.ts b/src/commands/backfill.ts index 95c9f024..dfda0bc6 100644 --- a/src/commands/backfill.ts +++ b/src/commands/backfill.ts @@ -20,6 +20,8 @@ import type { Command } from 'commander'; +import { notesAvailability } from '../core/notes.js'; + import { DEFAULT_BATCH_SIZE, DEFAULT_LIMIT, @@ -176,24 +178,34 @@ const jsonPayload = (result: BackfillResult): string => 2, )}\n`; -const present = (result: BackfillResult, json: boolean): BackfillOutcome => { +const present = ( + result: BackfillResult, + json: boolean, + incomplete: string | null = null, +): BackfillOutcome => { + const caveat = + incomplete === null + ? '' + : `${PREFIX} the notes mirror is ${incomplete}, so "no record" below means "none this clone can see" — ` + + `counts of note trailers and of targets are both drawn from an index that could not read it. ` + + `fix: commitlore doctor --fix, then git fetch\n`; const detail = [ ...result.report.discarded.map(discardLine), ...result.report.skipped.map(skipLine), ].join(''); - if (json) return { stdout: jsonPayload(result), stderr: detail, exitCode: 0 }; + if (json) return { stdout: jsonPayload(result), stderr: caveat + detail, exitCode: 0 }; /* In prompt mode stdout is the payload a session consumes, so the summary goes to stderr and stays out of whatever the user pipes it into. */ if (result.report.mode === 'prompt-only') { return { stdout: promptPayload(result), - stderr: `${detail}${summary(result.report)}`, + stderr: `${caveat}${detail}${summary(result.report)}`, exitCode: 0, }; } - return { stdout: summary(result.report), stderr: detail, exitCode: 0 }; + return { stdout: summary(result.report), stderr: caveat + detail, exitCode: 0 }; }; /** @@ -201,9 +213,41 @@ const present = (result: BackfillResult, json: boolean): BackfillOutcome => { * outcome rather than an exception: the one thing a user of a cold-start command * should never see is a stack trace telling them nothing they can act on. */ +/** + * #403, and PRD Phase 0: fail closed when the notes mirror was not read. + * + * `backfill` selects its targets from the index — "commits with no record". A + * commit whose record lives only in an unfetched note has, from here, no record, + * so it is eligible for reconstruction. Writing then produces a second record + * for one decision: either a `Record-Id` collision, or worse, no collision and + * two records the lifecycle fold cannot reconcile. + * + * The read-only modes still run — they are how a user diagnoses this — but they + * say the selection was drawn from an index that could not see the mirror. Only + * the writing mode refuses, because only it can create the duplicate. + */ +const NOT_COMPLETE = new Set(['unfetched', 'diverged', 'shallow']); + +const incompleteMirror = (options: BackfillCommandOptions): string | null => { + const state = notesAvailability(options.cwd === undefined ? {} : { cwd: options.cwd }); + return NOT_COMPLETE.has(state) ? state : null; +}; + export const runBackfill = (options: BackfillCommandOptions): BackfillOutcome => { + const incomplete = incompleteMirror(options); + const writes = options.draft !== undefined && options.dryRun !== true; + if (incomplete !== null && writes) { + return { + stdout: '', + stderr: + `${PREFIX} the notes mirror is ${incomplete}, so a record that already exists upstream reads here ` + + `as a commit with none. Reconstructing now can write a second record for one decision.\n` + + `${PREFIX} fix: commitlore doctor --fix, then git fetch, then rerun\n`, + exitCode: 2, + }; + } try { - return present(backfill(toBackfillOptions(options)), options.json === true); + return present(backfill(toBackfillOptions(options)), options.json === true, incomplete); } catch (error) { const detail = error instanceof Error ? error.message : String(error); // A usage error, never a finding (SPEC §10): a bad flag, an unreadable diff --git a/test/backfill.test.ts b/test/backfill.test.ts index a4a22a43..1e239547 100644 --- a/test/backfill.test.ts +++ b/test/backfill.test.ts @@ -782,3 +782,56 @@ describe('backfill leaves history alone', () => { ); }); }); + +/** + * #403, and the PRD's Phase 0 acceptance: fail closed when the notes mirror was + * not read. + * + * `backfill` picks its targets from the index — "commits with no record". A + * commit whose record lives only in an unfetched note reads here as having + * none, so it is eligible for reconstruction, and writing then produces a second + * record for one decision. The read-only modes still run, because they are how a + * user diagnoses this; only the writing mode refuses, because only it can create + * the duplicate. + */ +describe('#403 backfill fails closed on a mirror it could not read', () => { + const unfetchedClone = (): string => { + const origin = initRepo('b403-origin'); + commit(origin, 'a.txt', 'a\n', 'seed'); + git(origin, [ + 'notes', + '--ref=commitlore', + 'add', + '-m', + 'Limit: only in the note\nRecord-Id: r-note403\nCertainty: firm', + 'HEAD', + ]); + const clone = tempDir('b403-clone'); + git(clone, ['clone', '--quiet', `file://${origin}`, '.']); + return clone; + }; + + it('refuses to write, naming the duplicate it would create', () => { + const cwd = unfetchedClone(); + const draft = join(cwd, 'draft.json'); + writeFileSync(draft, JSON.stringify({ commits: [] })); + const outcome = runBackfill({ cwd, draft }); + expect(outcome.exitCode).toBe(2); + expect(outcome.stderr).toContain('a second record for one decision'); + expect(outcome.stderr).toContain('git fetch'); + }); + + it('still runs the read-only modes — they are how this is diagnosed', () => { + const cwd = unfetchedClone(); + const outcome = runBackfill({ cwd }); + expect(outcome.exitCode).toBe(0); + expect(outcome.stderr).toContain('none this clone can see'); + }); + + it('a dry run is not a write, so it is not refused', () => { + const cwd = unfetchedClone(); + const draft = join(cwd, 'draft.json'); + writeFileSync(draft, JSON.stringify({ commits: [] })); + expect(runBackfill({ cwd, draft, dryRun: true }).exitCode).toBe(0); + }); +});