Files
msd-core/hooks/lib/git-probe.js
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

85 lines
3.7 KiB
JavaScript

'use strict';
// hooks/lib/git-probe.js — classify a bounded spawnSync(git, ...) result,
// distinguishing a GENUINE negative answer (git ran and said "no") from
// "could not determine" (timeout / spawn failure / signal kill) — #3911.
//
// Proven defect: hooks/msd-worktree-path-guard.js, hooks/msd-workflow-guard.js,
// and hooks/msd-windsurf-pre-write.js each treat spawnSync(git, ..., {timeout})
// returning a non-zero/empty result as "not applicable" and allow silently.
// A macOS CI run showed three deny cases land at 2084ms/2112ms/2177ms — just
// past the 2000ms budget — each returning exit 0 with EMPTY stdout AND EMPTY
// stderr: a timeout is INDISTINGUISHABLE, at the call site, from git cleanly
// answering "not a repo" / "not this worktree" unless the spawnSync() result
// itself is inspected for `error`/`signal`, not just `status`/`stdout`.
//
// This module does not change any hook's decision or exit code — it only
// answers "was this probe's answer real?" so a caller can emit a diagnostic
// on the undetermined branch while keeping the existing fail-open behavior.
//
// Classification mirrors tests/helpers/process-seam.cjs's OUTCOME vocabulary
// (EXITED vs TIMED_OUT/SPAWN_FAILED/KILLED) — same evidence-over-classification
// ordering (a populated `status` outranks an attached `error`, since Node can
// report `error.code === 'ETIMEDOUT'` on a result that also carried a real
// exit that beat the timer) — so a probe here and a subprocess outcome in the
// test suite never disagree on the same raw spawnSync() shape.
/**
* @param {{error?: (Error & {code?: string})|null, status: number|null, signal: string|null}} result
* the raw return value of `spawnSync('git', ...)`.
* @returns {{determined: true} | {determined: false, reason: string}}
*/
function classifyGitProbe(result) {
const error = result && result.error;
const status = result ? result.status : null;
const signal = result ? result.signal : null;
if (!error && signal === null) {
// A real exit — status 0 or non-zero is a genuine, trustworthy answer.
return { determined: true };
}
if (status !== null) {
// `status` is only ever populated by a real exit; it outranks an attached
// `error` even at the exact timeout boundary (process-seam.cjs's own
// "evidence over classification" rule).
return { determined: true };
}
const code = error ? (error.code ?? null) : null;
let reason;
if (code === 'ETIMEDOUT' || signal !== null) {
reason = signal ? `timed out (signal ${signal})` : 'timed out';
} else if (code) {
reason = `spawn failed (${code})`;
} else {
reason = 'could not run (unknown reason)';
}
return { determined: false, reason };
}
/**
* If `result` could not be determined (timeout / spawn failure / signal
* kill), write a stderr diagnostic naming the probe and why — never changes
* any exit code, never throws. A no-op when the probe genuinely ran and
* answered (status 0 or non-zero with no error/signal).
*
* @param {string} hookName - e.g. 'msd-worktree-path-guard'
* @param {string} probeLabel - e.g. "git rev-parse --show-toplevel"
* @param {*} result - the raw spawnSync() return value
*/
function reportIfUndetermined(hookName, probeLabel, result) {
const classification = classifyGitProbe(result);
if (classification.determined) return;
try {
process.stderr.write(
`${hookName}: git probe '${probeLabel}' ${classification.reason} — ` +
`allowing this call because the probe's answer is unknown, not because ` +
`git answered "no" (#3911).\n`
);
} catch {
// A stderr write failing (EPIPE, closed fd) must never itself change the
// hook's fail-open outcome.
}
}
module.exports = { classifyGitProbe, reportIfUndetermined };