# Fleet Patch-Script Preflight For anyone about to run a script across more than one site. The failures here are not crashes. They are **success-shaped**: exit code 0, no error, an empty diff, and a summary line that reads exactly like a correct run. Copy the helper module at the bottom and the rest of this becomes assertions rather than care. --- ## Before you write the patch ### 1. Census the shapes "Remove it everywhere" assumes the markup is identical everywhere. In a fleet it is usually two or three shapes, and often only some are broken. ```bash for f in sites/*/index.html; do grep -A5 'data-widget="rates"' "$f" | sha1sum | cut -c1-8 done | sort | uniq -c ``` One hash means one shape and a simple patch. Three hashes means your anchor matches three different things, and a replacement tuned to one of them will silently do nothing to the others. ### 2. Sweep by the failing VALUE, not by the selector list If a checker told you which selectors are broken, do not patch that list. Patch everything carrying the offending value. The list is what the checker could see; the value is the truth. ### 3. Decide what "already done" looks like, and pick a marker that cannot collide An idempotency marker that also occurs elsewhere in the file makes the script report `already patched: 12` while patching none. That output is indistinguishable from a correct second run. Grep for your marker across the fleet BEFORE you use it: ```bash grep -rc 'jw-patch-v7' sites/ | grep -v ':0' || echo "marker is unused, safe" ``` --- ## While it runs ### 4. Every replacement asserts its match count A `String.replace` that matches nothing returns the input and reports nothing. ```js function sub(text, find, replace, expected = 1) { const parts = text.split(find); const found = parts.length - 1; if (found !== expected) { throw new Error( `Expected ${expected} match(es) of ${JSON.stringify(String(find).slice(0, 60))}, found ${found}` ); } return parts.join(replace); } ``` Use the split/join form rather than a regex or a string `replace` when the replacement text may contain `$`. In a string replacement `$&`, `$1` and `$'` are substitution patterns and will corrupt the output. A function replacement escapes nothing, which is the other safe option. ### 5. Never make a node-level edit with a block-level string match If you are editing structured data, JSON, or anything with nesting, parse it, change the node, and serialize. A string match on the surrounding block will eventually delete a sibling that happened to share the container. Delete a block only when you have proven the block contains nothing but the thing you are removing. ### 6. Never swallow a parse error ```js // This exits 0 having skipped the files it could not read. try { patch(file); } catch (e) { skipped++; } ``` A skip is fine to record and never fine to ignore. If the `&&` behind your script runs a deploy, you have just shipped the unpatched estate while reporting ok. --- ## Before you believe the result ### 7. The arithmetic ```js if (changed + alreadyDone !== total) { throw new Error(`Unaccounted: ${total - changed - alreadyDone} file(s)`); } if (skipped) throw new Error(`${skipped} skipped, which is never ok in a patch run`); if (isFirstRun && changed === 0) throw new Error('First run changed nothing. The anchor is wrong.'); ``` ### 8. Print the denominator, with reasons Not `18 passed`. Instead: ``` 18 passed, 4 skipped (no pricing block), 2 unreachable (timeout) ``` A bare total cannot distinguish a clean run from a run that examined almost nothing. ### 9. A result identical on every site is evidence about your harness Uniformity reads as consensus and it is the result you should trust least. A real fleet of fourteen sites has variance in it. **Disprove one by hand** before acting on any finding that hits 100%. If it is real on the one you open, it is probably real everywhere. If it is not, you just avoided fourteen wrong commits. ### 10. Mutation-test any gate you did not write today A gate copied from an earlier project is green and asserts nothing, which is worse than no gate because it occupies the slot. Introduce the exact defect it exists to catch, run it, confirm it fails, then remove the defect. A gate you have never seen fail is a gate you have never tested. ### 11. Your verification code is untested code It is the code nobody writes a test for, because it *is* the test. A stray `continue` once made an audit report 9 of 24 when the answer was 18 of 24. ```js if (assertionsRun !== pages.length * checksPerPage) { throw new Error(`Ran ${assertionsRun}, expected ${pages.length * checksPerPage}`); } ``` ### 12. Prove the generator is a fixed point If one member of the fleet is generated rather than hand-maintained, a directory-walking patch edits its build output and the next build reverts you. ```bash npm run build -- --out /tmp/build-a npm run build -- --out /tmp/build-b diff -r /tmp/build-a /tmp/build-b && echo "fixed point" ``` Patch the source of a generated site, never its output. --- ## Deploy-time traps that look like patch bugs - **The CLI reads config from the current directory, not from `--dir`.** Publishing folder A while standing in folder B can pick up B's redirects and headers. - **A hand-maintained table of per-site IDs has one wrong row**, and the deploy succeeds, into the wrong site. Preflight every ID against the host's own list before the first upload. - **A full-manifest deploy expresses deletion by omission.** A script that walks a flat local mirror will remove anything the mirror lacks, including functions you never touched. - **Cleaning the publish root can delete deployed artifacts** that were never in your source tree. - **Your build tool does not remove output it no longer generates.** Stale directories from an older config sit in the publish root and ship forever. Gate on it: ```js const stale = ['src', 'node_modules', 'dist'].filter((d) => existsSync(`_site/${d}`)); if (stale.length) throw new Error(`Stale output in publish root: ${stale.join(', ')}`); ``` --- ## The one-line summary If your patch script cannot tell you how many things it changed, how many it skipped, and why, it has not told you anything.