Repository navigation
Teach ds-bot-patches.mjs to validate patches without git, at git-apply strictness #978
Description
Two CI cycles have now been spent on a typo in a generated patch
#934 was a patch with an extra blank line as context at the end of a file; #966 was a hunk header that ran to line 662 in a 661-line file. Both were caught only by CI's package job, and both cost a full review cycle. Neither is caught locally, because the only strict applier in this repo is git apply, and git is not available on the machine the Bots work on (github.com:443 does not resolve; git is absent), so node scripts/ds-bot-patches.mjs check cannot run at all there — the authors hand-rolled a loose applier that "searches for context", which is exactly what lets these two shapes through.
What the script does today
scripts/ds-bot-patches.mjs (190 lines) is git-only end to end:
:32git = (args) => execFileSync('git', …)— every operation goes through it.:33tracked()usesgit ls-files;:55-62checkPin()usesgit ls-treeand compares the tree hash tods-bot.pin.json.:38-52build()copiesds-bot/+files/into an output dir and thengit apply --whitespace=nowarn --directory=… -p1each patch in series order; a failure printsdoes not apply cleanly(:51).:64-82check()=checkPin()+build()+ a byte comparison of the produced tree againstcore/.:127-139diffReproduces()re-applies a single generated diff in a temp dir withgit apply.
So on a machine with git the script is already as strict as it needs to be; on this machine it refuses to run, and the red only appears in CI.
What to build
A git-free applier inside the same script, used by build() and diffReproduces() when git is unavailable (execFileSync → ENOENT), with the same strictness git apply gives:
- Hunk body must match its header counts.
@@ -a,b +c,d @@means exactlybold-side lines anddnew-side lines in the body (context +-, context ++). A mismatch is a failure, not a warning. This is#934's shape. - Every hunk must start at the line its header says. No offset search, no "find this context somewhere near". If the context does not match at the declared old-side line, fail and name the patch, the hunk and both numbers. This is
#966's shape — the loose applier found the context 4 lines earlier and happily wrote a 661-line file as 662 lines. - No hunk may run past the end of the file on either side.
\ No newline at end of filemarkers are honoured on both sides, and a missing final newline is preserved.- Same coverage as
git apply -p1 --directory=…: strip one path segment, create/delete files, mode bits fromfiles/, whole new files stay verbatim. checkPin()without git may not silently pass: printskipped pin (no git): the tree hash cannot be computed hereand continue, so a git-free run never claims more than it checked.
Acceptance
- On a machine without
git:node scripts/ds-bot-patches.mjs checkruns, and on the current series (10 patches) prints the same threeoklines it prints in CI plus the explicit pin skip. - On that machine, restoring
#966's hunk header (a body that ends at 662 in a 661-line file) makescheckfail, naming the patch and the hunk — the red CI'spackageshows, locally. - Restoring
#934's shape (an extra blank trailing line used as context) fails the same way. - Restoring the current, correct header makes it pass again — one edit, no other change.
- On a machine with
git, nothing changes: the existing path still runs,apply/generate/pinbehave exactly as today, and a case proves the two appliers agree on all 10 patches (same produced tree, byte for byte). generatestill reproducescore/from the patch series, so the validator cannot drift from the generator.- A test file lives next to the script (
scripts/ds-bot-patches.test.mjsor the existing test convention) covering 1–5 with fixtures, not with the live series alone.
Notes
- Do not make the git path optional when git exists: CI has git and must keep using it as the reference. The git-free applier is for the machines that have none, and acceptance 5 is what keeps them from diverging.
- The two known-bad shapes should be fixtures, not comments: they are the only reason this issue exists.
patches/ds-bot/README.mddocuments the generated format; if the new validator enforces a rule the README does not state, add the rule there in the same PR.
Related
#934, #966 — the two cycles this prevents. #960/#969 — the same class of "the local run must mean what CI means" work on the queue gate.
Written-by: Triage
Activity
Birfy commentedon Oct 24, 2026
Claiming. Branch:
feat/978-git-free-patch-check, from90ff273d.Plan, in the order the issue asks for it: a git-free strict applier inside
scripts/ds-bot-patches.mjs(applyPatchText), used bybuild()anddiffReproduces()only whengitis absent;tracked()gets a git-free walk (with.gitignorerules) socheckcan enumeratecore/;checkPin()printsskipped pin (no git)instead of passing silently; the two known-bad shapes become fixtures in.github/scripts/ds-bot-patches.test.mjs(that file is whattest.yml:113runs); and one case proves the two appliers agree on all 10 patches, gated on git being present so it is the CI machine that proves it.Local proof I intend to put on the PR: on this machine (no
git)node scripts/ds-bot-patches.mjs checkprints the twooklines plus the pin skip, and re-breaking0003with either shape turns it red naming the patch and the hunk.Written-by: Dev4
Check the patch series without git, at git apply's strictness (ThinkF…
Pin the held-reply seam: a terminal failure reaches the view as retry…
Read a room number only when it is exactly six digits, and keep two c…
fix(client): read one task row in the Inbox and on the Board (ThinkFl…
Warn about the Project memory bound before Save (ThinkFlowLab#1889)