diff --git a/PLAN.md b/PLAN.md index fde1ff8..6586752 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1622,6 +1622,17 @@ a mechanism rather than diligence: replaceable by a file copy, and that promise survives exactly as long as nobody types the address into a paragraph. Same argument as `checkTokens.mjs` and colour literals — the check is the mechanism, diligence is not. + + **All three network checks read a file through Gitea's `contents` endpoint, never `raw`** — + `checkFacts.mjs`, `checkQuickstart.mjs`, `checkReference.mjs`. Phase 12b found the reason. + `raw` answers with `Cache-Control: public, max-age=21600`, so the CDN in front of Gitea keeps + a copy for six hours: on cutover day this check read `website`'s `version.js` from a fortnight + earlier and failed the site for saying Module API 1.9.0 when `main` said 1.6.0 — except that + `main` said 1.9.0, and nothing anyone could edit here would have made it pass. `contents` + answers `private, must-revalidate` and is not cached, at the cost of a base64 decode. Same + argument as `checkLinks.mjs` fetching nothing: a check that goes red on someone else's + infrastructure is a check people learn to ignore, and one that goes red on a stale copy is + worse — it is indistinguishable from the failure it exists to report. - **`scripts/checkLinks.mjs`** — every internal link resolves; every outbound link into a `RunicGateway` repo points at a branch path, not a commit permalink. **Built in phase 4** (D23), and it reads `dist/client` rather than `src/`: half the links these pages carry are assembled from diff --git a/scripts/checkFacts.mjs b/scripts/checkFacts.mjs index 8bf8fd2..f6e3a33 100644 --- a/scripts/checkFacts.mjs +++ b/scripts/checkFacts.mjs @@ -60,8 +60,28 @@ async function api(pathname) { return res; } -const raw = async (repo, filePath, ref) => - (await api(`${repo}/raw/${filePath}?ref=${encodeURIComponent(ref)}`)).text(); +/** + * A file's bytes, read through the `contents` endpoint rather than `raw`. + * + * `raw` answers with `Cache-Control: public, max-age=21600`, so the CDN in front of Gitea + * serves a copy for six hours and this check can read a blob most of a working day old. + * That is not theoretical: on the day of the engagement cutover it reported website's + * MODULE_API_VERSION as 1.6.0 -- the value from two weeks earlier -- and failed a site + * whose number was right. A check that goes red on stale data is a check people learn to + * ignore, which is the one failure mode this file exists to avoid. + * + * `contents` answers `private, must-revalidate`, which the CDN does not cache, so it is + * always the ref's current blob. The cost is a JSON parse and a base64 decode. + */ +async function raw(repo, filePath, ref) { + const meta = await json(`${repo}/contents/${filePath}?ref=${encodeURIComponent(ref)}`); + if (meta.encoding !== 'base64' || typeof meta.content !== 'string') { + throw new Error( + `${repo}:${filePath}@${ref} did not come back as a base64 file (encoding ${meta.encoding}).` + ); + } + return Buffer.from(meta.content, 'base64').toString('utf8'); +} const json = async (pathname) => (await api(pathname)).json(); diff --git a/scripts/checkQuickstart.mjs b/scripts/checkQuickstart.mjs index d824222..7b4da4b 100644 --- a/scripts/checkQuickstart.mjs +++ b/scripts/checkQuickstart.mjs @@ -55,12 +55,18 @@ const checked = []; const ok = (what) => checked.push(what); const fail = (what, detail) => failures.push({ what, detail }); -/** Same raw-file accessor checkFacts.mjs uses, and for the same reason. */ +/** Same file accessor checkFacts.mjs uses, and for the same reason -- including the CDN one. */ async function raw(repo, filePath, ref) { - const url = `${BASE}/api/v1/repos/${ORG}/${repo}/raw/${filePath}?ref=${encodeURIComponent(ref)}`; + const url = `${BASE}/api/v1/repos/${ORG}/${repo}/contents/${filePath}?ref=${encodeURIComponent(ref)}`; const res = await fetch(url, { headers: { Authorization: `token ${TOKEN}` } }); if (!res.ok) throw new Error(`${res.status} ${res.statusText} for ${url}`); - return res.text(); + const meta = await res.json(); + if (meta.encoding !== 'base64' || typeof meta.content !== 'string') { + throw new Error( + `${repo}:${filePath}@${ref} did not come back as a base64 file (encoding ${meta.encoding}).` + ); + } + return Buffer.from(meta.content, 'base64').toString('utf8'); } /** diff --git a/scripts/checkReference.mjs b/scripts/checkReference.mjs index e297ce0..93bd51b 100644 --- a/scripts/checkReference.mjs +++ b/scripts/checkReference.mjs @@ -53,12 +53,18 @@ const checked = []; const ok = (what) => checked.push(what); const fail = (what, detail) => failures.push({ what, detail }); -/** Same raw-file accessor checkFacts.mjs and checkQuickstart.mjs use. */ +/** Same file accessor checkFacts.mjs and checkQuickstart.mjs use, CDN caveat included. */ async function raw(repo, filePath, ref = 'main') { - const url = `${BASE}/api/v1/repos/${ORG}/${repo}/raw/${filePath}?ref=${encodeURIComponent(ref)}`; + const url = `${BASE}/api/v1/repos/${ORG}/${repo}/contents/${filePath}?ref=${encodeURIComponent(ref)}`; const res = await fetch(url, { headers: { Authorization: `token ${TOKEN}` } }); if (!res.ok) throw new Error(`${res.status} ${res.statusText} for ${url}`); - return res.text(); + const meta = await res.json(); + if (meta.encoding !== 'base64' || typeof meta.content !== 'string') { + throw new Error( + `${repo}:${filePath}@${ref} did not come back as a base64 file (encoding ${meta.encoding}).` + ); + } + return Buffer.from(meta.content, 'base64').toString('utf8'); } /** diff --git a/src/content/docs/docs/architecture/protocol-versions.mdx b/src/content/docs/docs/architecture/protocol-versions.mdx index c14d451..dbaf1ae 100644 --- a/src/content/docs/docs/architecture/protocol-versions.mdx +++ b/src/content/docs/docs/architecture/protocol-versions.mdx @@ -4,12 +4,13 @@ description: One number, declared in three repositories, that decides whether a --- import { Aside } from '@astrojs/starlight/components'; +import platform from '../../../../data/platform.json'; The loopback wire protocol between the game plugin and the sidecar is a **versioned compatibility contract**, not a build dependency. Nothing compiles the three sides together, so the number is what stops a mismatch from being discovered as corrupted data. -The current protocol is **4**. +The current protocol is **{platform.protocol}**. ## Three declaration sites @@ -17,8 +18,8 @@ The same number is written down in three places, and they must move together. | Where | What declares it | |---|---| -| `link/sidecar/src/main.rs` | `pub const PROTOCOL_VERSION: u32 = 4` — what the sidecar speaks | -| `servuo-plugins/overlay.toml` | `protocol = 4` — what the plugin overlay speaks | +| `link/sidecar/src/main.rs` | `PROTOCOL_VERSION`, currently {platform.protocol} — what the sidecar speaks | +| `servuo-plugins/overlay.toml` | `protocol`, currently {platform.protocol} — what the plugin overlay speaks | | The bundle manifest | Copied from `overlay.toml` by CI, so a released pair carries its own claim |