{"_id":"@acsaven/astro-ops","_rev":"5-12ef9e4001d0e12ec5da3c4fc7aefa20","name":"@acsaven/astro-ops","dist-tags":{"latest":"0.2.4"},"versions":{"0.2.0":{"name":"@acsaven/astro-ops","version":"0.2.0","keywords":["astro","build-id","edge-cache","ci","lighthouse","seo"],"author":{"name":"Samson PG"},"license":"MIT","_id":"@acsaven/astro-ops@0.2.0","maintainers":[{"name":"samsonpg","email":"samsonpg077@gmail.com"}],"homepage":"https://acsaven.com/astro-production-starter/","bugs":{"url":"https://github.com/acsavenhq/astro-ops/issues"},"bin":{"astro-ops":"bin/astro-ops.mjs"},"dist":{"shasum":"5513df2258a4b9fb806571e0b9b6c1e371b366cf","tarball":"https://registry.npmjs.org/@acsaven/astro-ops/-/astro-ops-0.2.0.tgz","fileCount":12,"integrity":"sha512-gZwjSufrMHKxSz2aJnd3Ym9bMG2jNFSnuVKmRNZPMK2/3mTJK/8OneFB7ERRu+uaAcpYW+1SC5bwhZ4z/MdUTw==","signatures":[{"sig":"MEUCIQCZzmDLtyiDbyimrkRRSUrQB81zFjzIk0Iv4M0KKyBsigIgKKNvsNKjPhCSOtk6o7fyLRN5MMMd2aJpM2SbdrIVc/E=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":98481},"type":"module","engines":{"node":">=20"},"exports":{".":"./src/index.mjs","./config":"./src/config.mjs","./build-id":"./src/build-id.mjs"},"gitHead":"63db9009a9162888b8cb417c6f3c6ac853967794","scripts":{"test":"node --test","selfcheck":"node bin/astro-ops.mjs --help"},"_npmUser":{"name":"samsonpg","email":"samsonpg077@gmail.com"},"repository":{"url":"git+https://github.com/acsavenhq/astro-ops.git","type":"git"},"_npmVersion":"11.13.0","description":"Production gates for Astro sites — content-hashed build ids, freshness watchdogs, performance budgets, discovery wiring, and a no-third-party-asset tripwire.","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/astro-ops_0.2.0_1787312973528_0.4281871821588037","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@acsaven/astro-ops","version":"0.2.1","keywords":["astro","build-id","edge-cache","ci","lighthouse","seo"],"author":{"name":"Samson PG"},"license":"MIT","_id":"@acsaven/astro-ops@0.2.1","maintainers":[{"name":"samsonpg","email":"samsonpg077@gmail.com"}],"homepage":"https://acsaven.com/astro-production-starter/","bugs":{"url":"https://github.com/acsavenhq/astro-ops/issues"},"bin":{"astro-ops":"bin/astro-ops.mjs"},"dist":{"shasum":"95effc9d15bfe35ea97d2af7254e8be51e485729","tarball":"https://registry.npmjs.org/@acsaven/astro-ops/-/astro-ops-0.2.1.tgz","fileCount":12,"integrity":"sha512-zVFrmrCStSjZVPpDQjbM2BqcrETomPv1LG3bUt/ZRscBxPTd2xkUkgzZlljVjmMKrahKmCw9m5k6n1EleepfyQ==","signatures":[{"sig":"MEYCIQC3vSpOP1DQ65n9mFKm/uwBhlv9/4mF/SyI2yCKRpCstgIhAOnqgF06bzhhuB/HtDOGc/hghytm3HWkr+PMoX8b5j2o","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":99412},"type":"module","engines":{"node":">=20"},"exports":{".":"./src/index.mjs","./config":"./src/config.mjs","./build-id":"./src/build-id.mjs"},"gitHead":"b05c184f68a40b32513d384bc78e611e7275bf5a","scripts":{"test":"node --test","selfcheck":"node bin/astro-ops.mjs --help"},"_npmUser":{"name":"samsonpg","email":"samsonpg077@gmail.com"},"repository":{"url":"git+https://github.com/acsavenhq/astro-ops.git","type":"git"},"_npmVersion":"11.13.0","description":"Production gates for Astro sites — content-hashed build ids, freshness watchdogs, performance budgets, discovery wiring, and a no-third-party-asset tripwire.","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/astro-ops_0.2.1_1787315496684_0.580311084281244","host":"s3://npm-registry-packages-npm-production"}},"0.2.2":{"name":"@acsaven/astro-ops","version":"0.2.2","keywords":["astro","build-id","edge-cache","ci","lighthouse","seo"],"author":{"name":"Samson PG"},"license":"MIT","_id":"@acsaven/astro-ops@0.2.2","maintainers":[{"name":"samsonpg","email":"samsonpg077@gmail.com"}],"homepage":"https://acsaven.com/astro-production-starter/","bugs":{"url":"https://github.com/acsavenhq/astro-ops/issues"},"bin":{"astro-ops":"bin/astro-ops.mjs"},"dist":{"shasum":"5e8caa86d9aeeee904885ee31074d2bb31082720","tarball":"https://registry.npmjs.org/@acsaven/astro-ops/-/astro-ops-0.2.2.tgz","fileCount":12,"integrity":"sha512-lUuic9tmRTSXMGBSfFt7tKgxZIPY4ohmI0ScwqtJknIan01G66dBix8osx3tPQOEpGJ+dmzAg3tzPpuud0QmTA==","signatures":[{"sig":"MEYCIQCjzWiEpjbNMOWmfcepmYgUG3IUGIYj85ZqX4MMYZ2geQIhAIu6apzOTVSXUtI1YlGGamqcwUb9IUrIS2ScknTYLQyZ","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":101175},"type":"module","engines":{"node":">=20"},"exports":{".":"./src/index.mjs","./config":"./src/config.mjs","./build-id":"./src/build-id.mjs"},"gitHead":"d34d4d9ffab515828bd51072df439ddcce74887f","scripts":{"test":"node --test","selfcheck":"node bin/astro-ops.mjs --help"},"_npmUser":{"name":"samsonpg","email":"samsonpg077@gmail.com"},"repository":{"url":"git+https://github.com/acsavenhq/astro-ops.git","type":"git"},"_npmVersion":"11.13.0","description":"Production gates for Astro sites — content-hashed build ids, freshness watchdogs, performance budgets, discovery wiring, and a no-third-party-asset tripwire.","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/astro-ops_0.2.2_1787725016677_0.11854729085068239","host":"s3://npm-registry-packages-npm-production"}},"0.2.3":{"name":"@acsaven/astro-ops","version":"0.2.3","keywords":["astro","build-id","edge-cache","ci","lighthouse","seo"],"author":{"name":"Samson PG"},"license":"MIT","_id":"@acsaven/astro-ops@0.2.3","maintainers":[{"name":"samsonpg","email":"samsonpg077@gmail.com"}],"homepage":"https://acsaven.com/astro-production-starter/","bugs":{"url":"https://github.com/acsavenhq/astro-ops/issues"},"bin":{"astro-ops":"bin/astro-ops.mjs"},"dist":{"shasum":"685118153a506320730f48ece68d1aa7f45cce8d","tarball":"https://registry.npmjs.org/@acsaven/astro-ops/-/astro-ops-0.2.3.tgz","fileCount":12,"integrity":"sha512-nCn8FSXUjEpNZMkU7CDAUIE8qNnPljDpwkgAYJhVgSnZ0ZWgA9xUvQ37HdaabUpa2KF5O1v/sQwoIXCaypXNsg==","signatures":[{"sig":"MEUCIQCQxG/nmQrPupA62O3vT+xKKNAPD/gHtsYTw0rmtm0iXAIgFI/s8adw/S64SHrmL5sK74+2Q4nn9UuzC2kaSTY3+Ec=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":102853},"type":"module","engines":{"node":">=20"},"exports":{".":"./src/index.mjs","./config":"./src/config.mjs","./build-id":"./src/build-id.mjs"},"gitHead":"3fcb0c67164e5b13e94f5d5f2b63f157b426c286","scripts":{"test":"node --test","selfcheck":"node bin/astro-ops.mjs --help"},"_npmUser":{"name":"samsonpg","email":"samsonpg077@gmail.com"},"repository":{"url":"git+https://github.com/acsavenhq/astro-ops.git","type":"git"},"_npmVersion":"11.13.0","description":"Production gates for Astro sites — content-hashed build ids, freshness watchdogs, performance budgets, discovery wiring, and a no-third-party-asset tripwire.","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/astro-ops_0.2.3_1787740800622_0.45403267146461745","host":"s3://npm-registry-packages-npm-production"}},"0.2.4":{"name":"@acsaven/astro-ops","version":"0.2.4","description":"Production gates for Astro sites — content-hashed build ids, freshness watchdogs, performance budgets, discovery wiring, and a no-third-party-asset tripwire.","type":"module","license":"MIT","engines":{"node":">=20"},"bin":{"astro-ops":"bin/astro-ops.mjs"},"exports":{".":"./src/index.mjs","./build-id":"./src/build-id.mjs","./config":"./src/config.mjs"},"scripts":{"test":"node --test","selfcheck":"node bin/astro-ops.mjs --help"},"keywords":["astro","build-id","edge-cache","ci","lighthouse","seo"],"repository":{"type":"git","url":"git+https://github.com/acsavenhq/astro-ops.git"},"publishConfig":{"access":"public"},"author":{"name":"Samson PG"},"homepage":"https://acsaven.com/astro-production-starter/","bugs":{"url":"https://github.com/acsavenhq/astro-ops/issues"},"gitHead":"a89a4f157b38b947420149ddc867198d7a3ad944","_id":"@acsaven/astro-ops@0.2.4","_nodeVersion":"24.16.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-eZsTNhP4yzRQc+8QnPwdmG6tmaUX8j6CXrHv7mplKVdnDZPe2Jjc7/iNIxhqTw/Vc08no6/aposCjdhnnmsW7A==","shasum":"57c6d70c694925eeff4c45de1e676096177995a2","tarball":"https://registry.npmjs.org/@acsaven/astro-ops/-/astro-ops-0.2.4.tgz","fileCount":12,"unpackedSize":105252,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDEt7sC520mF91v+KB9+wpk4tvYM0pwHBp0weEH9sh9JAIgDiCQ7jnkhMsiAgjXMFAi3PnXK49xHx9U67nxJhh0lHU="}]},"_npmUser":{"name":"samsonpg","email":"samsonpg077@gmail.com"},"directories":{},"maintainers":[{"name":"samsonpg","email":"samsonpg077@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/astro-ops_0.2.4_1787746220538_0.8114878275676025"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-21T11:49:33.302Z","modified":"2026-08-26T12:10:20.857Z","0.2.0":"2026-08-21T11:49:33.695Z","0.2.1":"2026-08-21T12:31:36.885Z","0.2.2":"2026-08-26T06:16:56.814Z","0.2.3":"2026-08-26T10:40:00.795Z","0.2.4":"2026-08-26T12:10:20.699Z"},"bugs":{"url":"https://github.com/acsavenhq/astro-ops/issues"},"author":{"name":"Samson PG"},"license":"MIT","homepage":"https://acsaven.com/astro-production-starter/","keywords":["astro","build-id","edge-cache","ci","lighthouse","seo"],"repository":{"type":"git","url":"git+https://github.com/acsavenhq/astro-ops.git"},"description":"Production gates for Astro sites — content-hashed build ids, freshness watchdogs, performance budgets, discovery wiring, and a no-third-party-asset tripwire.","maintainers":[{"name":"samsonpg","email":"samsonpg077@gmail.com"}],"readme":"<p align=\"center\">\r\n  <img src=\"https://raw.githubusercontent.com/acsavenhq/astro-ops/main/docs/astro-ops-banner.png\" alt=\"@acsaven/astro-ops — production gates for Astro sites\" width=\"100%\">\r\n</p>\r\n\r\n<p align=\"center\">\r\n  <a href=\"https://www.npmjs.com/package/@acsaven/astro-ops\"><img alt=\"npm version\" src=\"https://img.shields.io/npm/v/@acsaven/astro-ops?color=34E89A&labelColor=0C1512\"></a>\r\n  <a href=\"https://github.com/acsavenhq/astro-ops/blob/main/LICENSE\"><img alt=\"license\" src=\"https://img.shields.io/npm/l/@acsaven/astro-ops?color=34E89A&labelColor=0C1512\"></a>\r\n  <img alt=\"node\" src=\"https://img.shields.io/node/v/@acsaven/astro-ops?color=34E89A&labelColor=0C1512\">\r\n  <img alt=\"zero dependencies\" src=\"https://img.shields.io/badge/dependencies-0-34E89A?labelColor=0C1512\">\r\n</p>\r\n\r\n# @acsaven/astro-ops\r\n\r\nProduction gates for Astro sites: the operational work that keeps a site *correct* six\r\nmonths later, when nobody is looking at it.\r\n\r\nExtracted from four sites that run these gates in production — not written as a demo. It\r\nexists because the same ~20 build scripts had been copy-pasted into every repo and were\r\ndrifting. Every check here answers something that went wrong once, and the comment above\r\nit says what.\r\n\r\n> **Status: stable.** All seven gates are built, tested, and running in production on three\r\n> live sites. [`template/`](https://raw.githubusercontent.com/acsavenhq/astro-ops/tree/main/template) is a working Astro site with every gate wired into\r\n> its build and CI — green out of the box, and each documented failure verified to fail.\r\n\r\n---\r\n\r\nNew to this codebase? Read **[CODE_GUIDE.md](CODE_GUIDE.md)** — it explains how the\r\ninternals work and why, assuming no prior knowledge.\r\n\r\n## Why this exists\r\n\r\nThe four sites it came from had accumulated the same ~20 build scripts, copy-pasted into\r\neach repo. Measured before extraction:\r\n\r\n| | |\r\n|---|---|\r\n| Core scripts still **byte-identical** across 3 repos | **7 of 12** |\r\n| The other 5 | differed by 1–4 lines — a site name, a path, a threshold |\r\n\r\nThe logic never diverged. Only the config did. But the copies were drifting anyway,\r\nbecause a fix landed in whichever repo hit the bug first and the other two kept the\r\ndefect. One of them had been carrying a known-broken build id for months after another\r\nrepo had already fixed it.\r\n\r\nThat is the whole thesis: **one maintained implementation, one config file per site.**\r\n\r\n## What is in the box\r\n\r\nEach gate answers a failure that a normal test suite cannot see, because in every case\r\nthe origin is fine and only the edge, the crawler, or the calendar is wrong.\r\n\r\n| Gate | Catches | Command |\r\n|---|---|---|\r\n| **Source integrity** | A machine out of disk silently emptying your source files mid-build, and a build about to do it | `check:integrity` |\r\n| **Content-hashed build id** | A deploy silently serving last week's HTML from a cache that was never invalidated | `check:build-id` |\r\n| **Freshness watchdog** | A fact you verified once that the authority changed without telling you | `check:freshness` |\r\n| **External-asset tripwire** | A third-party script, font or embed that appeared without anyone deciding to add it | `check:external` |\r\n| **Discovery wiring** | A sitemap and a set of pages that no longer agree — noindex conflicts, canonicals pointing at 404s | `check:discovery` |\r\n| **Link integrity** | A link to a page you no longer ship, and trailing-slash drift that costs a redirect hop | `check:links` |\r\n| **Performance budgets** | A slow regression nobody would have blocked, because the score was only ever a dashboard | `check:budgets` |\r\n\r\n`astro-ops check` runs all seven and reports **every** failure, not just the first — a CI run\r\nthat surfaces one problem per push turns a five-minute fix into five pushes.\r\n\r\n**Integrity runs first**, because every other gate reads files: if the disk emptied some of\r\nthem, a link checker finds no links and a claims scanner finds no claims, and the run goes\r\ngreen on a repository that was just damaged. A write that runs out of space does not throw —\r\nit leaves the file empty, and nothing downstream notices, because an emptied module simply\r\nexports nothing and a deleted test reports zero failures. Set `integrity.minFreeBytes: 0` to\r\nskip the disk floor on a platform that cannot report free space; the empty-file scan still\r\nruns.\r\n\r\n## Install\r\n\r\nFrom npm:\r\n\r\n```sh\r\nnpm i -D @acsaven/astro-ops\r\n```\r\n\r\nOr as a git dependency, which is how the Try family installs it:\r\n\r\n```jsonc\r\n// package.json\r\n{\r\n  \"devDependencies\": {\r\n    \"@acsaven/astro-ops\": \"github:acsavenhq/astro-ops#v0.2.2\"\r\n  }\r\n}\r\n```\r\n\r\n`npm ci` resolves git dependencies natively, so CI needs no extra setup.\r\n\r\nZero runtime dependencies. It installs into your build pipeline, so every dependency it\r\ncarried would become one you inherit — it carries none.\r\n\r\n## Updating\r\n\r\n**Nothing here updates itself.** npm never pushes a new version at an installed project: a\r\nrange only re-resolves on a fresh install or an explicit `npm update`, and a committed\r\n`package-lock.json` pins the resolved version until something changes it.\r\n\r\nWhat a given consumer gets:\r\n\r\n| declared            | fresh install | `npm update` | left alone |\r\n| ------------------- | ------------- | ------------ | ---------- |\r\n| `^0.2.2` / `~0.2.2` | newest 0.2.x  | newest 0.2.x | unchanged  |\r\n| `0.2.2` exact       | 0.2.2         | 0.2.2        | unchanged  |\r\n| `github:…#v0.2.2`   | v0.2.2        | v0.2.2       | unchanged  |\r\n\r\nThe Try sites pin the git tag deliberately. They used to say `github:acsavenhq/astro-ops`\r\nwith no ref, so every install pulled whatever `main` happened to be — meaning the gates\r\nguarding a deploy could change without anything recording that they had. A pinned tag costs\r\na manual bump and buys the ability to say which version of the checks a given release\r\nactually passed.\r\n\r\nTo move a site to a new release:\r\n\r\n```sh\r\nnpm i -D github:acsavenhq/astro-ops#v0.2.3\r\nnpx astro-ops check          # confirm the gates still pass before deploying\r\n```\r\n\r\nCutting a release, for whoever does it next:\r\n\r\n```sh\r\nnpm test                     # the gates gate themselves\r\nnpm version patch            # or minor / major\r\ngit push && git push --tags\r\nnpm publish --access public  # 2FA prompts in a browser\r\n```\r\n\r\nPublishing to npm and tagging git are separate acts, and both are needed: the registry\r\nserves anyone who installs by name, the tag serves the sites that install by ref. Shipping\r\none without the other leaves the two disagreeing about what `0.2.x` means.\r\n\r\n## Quick start\r\n\r\nAdd the gate to your build and your CI:\r\n\r\n```jsonc\r\n// package.json\r\n{\r\n  \"scripts\": {\r\n    \"postbuild\": \"astro-ops build-id\",\r\n    \"check\": \"astro-ops check\"\r\n  }\r\n}\r\n```\r\n\r\n```yaml\r\n# .github/workflows/ci.yml\r\n- run: npm run build\r\n- run: npm run check          # fails if the committed build id is stale\r\n```\r\n\r\nThen use the id in whatever keys your edge cache:\r\n\r\n```js\r\nimport { BUILD_ID } from './build-id.js';\r\n\r\nconst key = new URL(request.url);\r\nkey.searchParams.set('__build', BUILD_ID);\r\n```\r\n\r\nCommit `build-id.js`. That is the point — see below.\r\n\r\n## Configuration\r\n\r\nOptional. A project with no config file gets the defaults, which are the values that were\r\nactually running in production rather than a neutral guess.\r\n\r\n```js\r\n// astro-ops.config.mjs\r\nexport default {\r\n  buildId: {\r\n    include: ['dist'],          // hashed — what you DEPLOY, not what you wrote\r\n    out: 'build-id.js',\r\n    constName: 'BUILD_ID',\r\n    length: 16,\r\n  },\r\n\r\n  external: {\r\n    // Empty by default, on purpose. Every third party you accept has to be named here,\r\n    // one at a time, so the list is visible and grows where you can see it.\r\n    allowHosts: ['api.producthunt.com'],\r\n  },\r\n\r\n  freshness: {\r\n    // Point it at the file holding your claims. It reads `key: 'value'` literals without\r\n    // importing or parsing the file, so TypeScript is fine.\r\n    scan: { file: 'src/data/specs.ts' },\r\n    recheckMonths: 12,\r\n    warnWindowDays: 45,\r\n    // Optional second tripwire: a feed of { claimId: { detectedAt } } that something you\r\n    // run — typically a weekly cron — updates when a source page's hash changes.\r\n    driftApi: 'https://example.com/api/freshness',\r\n    // Fail when a claim names no source. Off by default; on is stricter and better.\r\n    requireSourceUrl: false,\r\n  },\r\n\r\n  discovery: {\r\n    ignoreRoutes: [],\r\n    rules: { maxTitle: 60, maxDescription: 160, requireJsonLd: false },\r\n  },\r\n\r\n  budgets: {\r\n    // Null skips the gate. Point it at a server you started in CI.\r\n    url: 'http://127.0.0.1:4173/',\r\n    categories: {\r\n      accessibility: { min: 90, blocking: true },\r\n      performance: { min: 80, blocking: false },\r\n    },\r\n  },\r\n};\r\n```\r\n\r\n### Why performance is advisory by default\r\n\r\nLighthouse performance moves several points between runs on identical code, and further\r\nbetween a laptop and a loaded CI runner. Made blocking at a tight threshold it fails\r\nrandomly, people learn to re-run until it passes, and a gate understood as a coin flip\r\nprotects nothing. Accessibility is close to deterministic, so it blocks. Set performance\r\nblocking only at a threshold loose enough that tripping it means something really broke.\r\n\r\nArray options **replace** the defaults rather than merging with them, so you can drop a\r\ndefault you disagree with and your config file always shows the effective value.\r\n\r\nIf you have to edit a script inside this package to make it fit your project, that is a\r\nmissing config option — please report it. Patching locally puts you straight back in the\r\ncopy-drift trap.\r\n\r\n---\r\n\r\n## The build id, and why it is not a timestamp\r\n\r\nThis is the module people are most likely to think they can write in five minutes, so it\r\nis worth the detail.\r\n\r\nA build id keys your edge cache. Change the id, and every cached page becomes unreachable\r\nat once — that is how you invalidate a CDN that has no purge API worth trusting.\r\n\r\n**The trap:** most projects grow a second way to deploy — CI *plus* a manual command, or a\r\nhost's git integration *plus* a CLI. That is good; either can ship when the other is down.\r\nBut it means the same commit can be deployed by two pipelines, and a random or timestamped\r\nid cannot survive that:\r\n\r\n- A git-integration build usually has **no build step**. It uploads the repo as-is, so it\r\n  ships whatever is **committed** in your build-id file.\r\n- A local deploy command rotates that file **on disk**. Unless you commit the result, the\r\n  repo still holds the old value.\r\n\r\nPush a content change while the rotated id sits uncommitted, and you ship new HTML under\r\nthe **previous** id. The cache key never changed, so every colo keeps serving the old page.\r\nThe origin is correct. The deploy \"succeeded\". Nothing in CI can see it.\r\n\r\nThis is not hypothetical. It is the failure this module was written in response to: a page\r\nwhose origin served the new content while the public URL kept returning the pre-change\r\ncopy, from every colo, until someone thought to look at the cache key.\r\n\r\nHashing the deployed content fixes it at the root:\r\n\r\n- Both pipelines compute the **same id for the same commit**, so the committed value is\r\n  always correct.\r\n- Any real change produces a new id, so the cache still busts exactly when it should.\r\n- It is **idempotent** — running it twice is a no-op, so your working tree stops drifting\r\n  and the file stops appearing in unrelated diffs.\r\n\r\nA random id also throws away your entire edge cache on every deploy, which is why a long\r\nTTL never seems to pay off. Content hashing keeps the warm cache across deploys that did\r\nnot change anything.\r\n\r\n**`astro-ops check:build-id` is the half that matters.** Emitting the id is easy to\r\nremember on the day you set it up. The check is what turns \"someone forgot to regenerate\"\r\nfrom a silent stale-cache bug into a failed build, a year later, when you have forgotten\r\nthis file exists.\r\n\r\n### Exit codes\r\n\r\nBecause these run unattended and the output is all a human sees when one fails at 2am:\r\n\r\n| Code | Meaning |\r\n|---|---|\r\n| `0` | Pass |\r\n| `1` | A gate failed — the site has a problem |\r\n| `2` | The tool is misconfigured — a config error must not look like a content failure |\r\n\r\n## Development\r\n\r\n```bash\r\nnpm test        # node:test, no test-framework dependency\r\n```\r\n\r\nThe tests are not incidental. Each one pins a property that the guarantee \"both pipelines\r\ncompute the same id for the same commit\" depends on — stable sort order across platforms,\r\npath-sensitivity so a rename busts the cache, `\\0` delimiters so swapping two files'\r\ncontents cannot collide, and a `fileCount` of zero surfacing as an error instead of a\r\nconstant id forever.\r\n\r\n---\r\n\r\n## The starter\r\n\r\n[`template/`](https://raw.githubusercontent.com/acsavenhq/astro-ops/tree/main/template) is a complete Astro site with all seven gates wired into its build\r\nand CI. Clone it, run `npm install && npm run check`, and every gate reports.\r\n\r\nIt scores 100 / 100 / 96 / 100 on Lighthouse, carries zero npm vulnerabilities, and its\r\nREADME lists five ways to deliberately break it — each verified to produce the failure it\r\nclaims, not asserted.\r\n","readmeFilename":"README.md"}