@reddoorla/maintenance 0.82.0 → 0.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/dist/airtable-5ZPU3MT4.js +9 -0
  2. package/dist/{announce-7QHQGJPV.js → announce-4H64L2Z6.js} +4 -4
  3. package/dist/{blux-EATHOHKK.js → blux-ZGDSHI3U.js} +5 -5
  4. package/dist/{bump-deps-ZREAMK7M.js → bump-deps-2OL6PGTM.js} +5 -5
  5. package/dist/{chunk-OIDDJLS7.js → chunk-2WN4SNBQ.js} +2 -2
  6. package/dist/{chunk-HVOOCK6L.js → chunk-4DHQCN5H.js} +2 -1
  7. package/dist/{chunk-5E4L7WKE.js → chunk-5P2HVNLZ.js} +3 -3
  8. package/dist/{chunk-ZXRRKKMR.js → chunk-6V7565O2.js} +8 -8
  9. package/dist/{chunk-YRM6EED7.js → chunk-CLWNAMUX.js} +28 -2
  10. package/dist/chunk-CLWNAMUX.js.map +1 -0
  11. package/dist/{chunk-VXXF4Y25.js → chunk-FSRJAF2Q.js} +60 -3
  12. package/dist/chunk-FSRJAF2Q.js.map +1 -0
  13. package/dist/{chunk-B5ZUQZFL.js → chunk-ICXV25N7.js} +2 -2
  14. package/dist/{chunk-UH5YPYS4.js → chunk-JVWJJ36K.js} +5 -4
  15. package/dist/chunk-JVWJJ36K.js.map +1 -0
  16. package/dist/{chunk-4PW6ISX7.js → chunk-KV7DIRRO.js} +3 -3
  17. package/dist/{chunk-ODC4PXZL.js → chunk-MQUOUVQP.js} +2 -2
  18. package/dist/{chunk-LMCO7CYQ.js → chunk-OQFOMKBP.js} +2 -2
  19. package/dist/{chunk-FFI7P74K.js → chunk-PBCGXXG6.js} +2 -2
  20. package/dist/{chunk-T4FQ6TZT.js → chunk-QRDB4NLB.js} +6 -6
  21. package/dist/{chunk-K5JZ5KLO.js → chunk-R4AB6GCT.js} +3 -3
  22. package/dist/chunk-R53VL3RI.js +32 -0
  23. package/dist/chunk-R53VL3RI.js.map +1 -0
  24. package/dist/{chunk-FX7RAMGC.js → chunk-R6FPFMOS.js} +3 -3
  25. package/dist/{chunk-MGJ72BA4.js → chunk-RJB3QUSP.js} +2 -2
  26. package/dist/{chunk-ATJ6ZA2V.js → chunk-SDUJIANJ.js} +2 -2
  27. package/dist/{chunk-7ADQL5Y3.js → chunk-THEFTZ7F.js} +2 -2
  28. package/dist/{chunk-NT4WIP7I.js → chunk-UYCAF77E.js} +2 -2
  29. package/dist/chunk-VNB5XBQ4.js +699 -0
  30. package/dist/chunk-VNB5XBQ4.js.map +1 -0
  31. package/dist/{chunk-YAMRVXOD.js → chunk-VRWUL4TR.js} +2 -2
  32. package/dist/{chunk-2BHSGLWR.js → chunk-XDCRVYFL.js} +3 -3
  33. package/dist/chunk-XDCRVYFL.js.map +1 -0
  34. package/dist/{chunk-4NAOR44V.js → chunk-ZMIPG6TR.js} +2 -2
  35. package/dist/cli/bin.js +51 -23
  36. package/dist/cli/bin.js.map +1 -1
  37. package/dist/cli/commands/audit.d.ts +1 -1
  38. package/dist/cli/commands/audit.js +10 -10
  39. package/dist/{convert-to-pnpm-UT2R6EOW.js → convert-to-pnpm-KCXOYOHW.js} +5 -5
  40. package/dist/{digest-CLFUWW62.js → digest-WGEPAWMP.js} +12 -7
  41. package/dist/digest-WGEPAWMP.js.map +1 -0
  42. package/dist/{ensure-site-LLPOXLWP.js → ensure-site-HYAIVZY2.js} +2 -2
  43. package/dist/{forms-notify-target-PN6P3WPW.js → forms-notify-target-HSL4ZMT5.js} +2 -2
  44. package/dist/{github-signals-HTH7474O.js → github-signals-I7XACMCE.js} +6 -6
  45. package/dist/{header-image-WMHZFU7B.js → header-image-ADYPTGVY.js} +2 -2
  46. package/dist/{health-endpoint-4E53ZMLE.js → health-endpoint-JNCUILHO.js} +6 -6
  47. package/dist/index.d.ts +65 -3
  48. package/dist/index.js +32 -31
  49. package/dist/index.js.map +1 -1
  50. package/dist/{init-3ZJ7Z5NG.js → init-FLGLJCMJ.js} +13 -13
  51. package/dist/{launch-FRCDOB56.js → launch-AVKH2JVF.js} +10 -10
  52. package/dist/{lighthouse-airtable-I46QCEIS.js → lighthouse-airtable-CHANWVO6.js} +3 -3
  53. package/dist/{onboard-45NPDMMW.js → onboard-HF4FTJDE.js} +5 -5
  54. package/dist/{orchestrate-34JYNIXT.js → orchestrate-5FMNYJEA.js} +3 -3
  55. package/dist/{preflight-WVD32COX.js → preflight-HTWIX3MN.js} +4 -4
  56. package/dist/prismic-ci-BH225Q4L.js +363 -0
  57. package/dist/prismic-ci-BH225Q4L.js.map +1 -0
  58. package/dist/prismic-models-HXCAM4ZL.js +1020 -0
  59. package/dist/prismic-models-HXCAM4ZL.js.map +1 -0
  60. package/dist/{protection-audit-2UR45SOC.js → protection-audit-64PLLBLI.js} +3 -3
  61. package/dist/recipes/sync-configs.d.ts +1 -1
  62. package/dist/{renovate-dispatch-LOOCNZTW.js → renovate-dispatch-HBZG2UEU.js} +4 -4
  63. package/dist/{report-CLF2753O.js → report-LV7NY5W3.js} +20 -13
  64. package/dist/report-LV7NY5W3.js.map +1 -0
  65. package/dist/{self-updating-5TJ2NB26.js → self-updating-4Q65OVXY.js} +6 -6
  66. package/dist/{selftest-BTCZLCTL.js → selftest-K5XZKVTE.js} +7 -7
  67. package/dist/{smoke-suite-GHKXMKIR.js → smoke-suite-J3LL3JZJ.js} +6 -6
  68. package/dist/{submissions-KUXLKCSQ.js → submissions-GMBUP4LV.js} +2 -2
  69. package/dist/{svelte-codemods-YTCTLK7G.js → svelte-codemods-JTMX4BSX.js} +4 -4
  70. package/dist/{sync-configs-KSSQZU5L.js → sync-configs-5DXSL64Q.js} +4 -4
  71. package/dist/{types-DnEL3NRn.d.ts → types-OIQZ5hWN.d.ts} +1 -1
  72. package/dist/{upgrade-UDAPY3SN.js → upgrade-HCR25P25.js} +5 -5
  73. package/dist/{websites-JXG3WWAU.js → websites-VITQ3WXN.js} +6 -2
  74. package/dist/{write-audits-to-airtable-7D3LV4KP.js → write-audits-to-airtable-3SOXLKJK.js} +4 -4
  75. package/package.json +1 -1
  76. package/dist/airtable-ZDHLKK5K.js +0 -9
  77. package/dist/chunk-2BHSGLWR.js.map +0 -1
  78. package/dist/chunk-KRDLHAKK.js +0 -17
  79. package/dist/chunk-KRDLHAKK.js.map +0 -1
  80. package/dist/chunk-UH5YPYS4.js.map +0 -1
  81. package/dist/chunk-VXXF4Y25.js.map +0 -1
  82. package/dist/chunk-YRM6EED7.js.map +0 -1
  83. package/dist/digest-CLFUWW62.js.map +0 -1
  84. package/dist/report-CLF2753O.js.map +0 -1
  85. /package/dist/{airtable-ZDHLKK5K.js.map → airtable-5ZPU3MT4.js.map} +0 -0
  86. /package/dist/{announce-7QHQGJPV.js.map → announce-4H64L2Z6.js.map} +0 -0
  87. /package/dist/{blux-EATHOHKK.js.map → blux-ZGDSHI3U.js.map} +0 -0
  88. /package/dist/{bump-deps-ZREAMK7M.js.map → bump-deps-2OL6PGTM.js.map} +0 -0
  89. /package/dist/{chunk-OIDDJLS7.js.map → chunk-2WN4SNBQ.js.map} +0 -0
  90. /package/dist/{chunk-HVOOCK6L.js.map → chunk-4DHQCN5H.js.map} +0 -0
  91. /package/dist/{chunk-5E4L7WKE.js.map → chunk-5P2HVNLZ.js.map} +0 -0
  92. /package/dist/{chunk-ZXRRKKMR.js.map → chunk-6V7565O2.js.map} +0 -0
  93. /package/dist/{chunk-B5ZUQZFL.js.map → chunk-ICXV25N7.js.map} +0 -0
  94. /package/dist/{chunk-4PW6ISX7.js.map → chunk-KV7DIRRO.js.map} +0 -0
  95. /package/dist/{chunk-ODC4PXZL.js.map → chunk-MQUOUVQP.js.map} +0 -0
  96. /package/dist/{chunk-LMCO7CYQ.js.map → chunk-OQFOMKBP.js.map} +0 -0
  97. /package/dist/{chunk-FFI7P74K.js.map → chunk-PBCGXXG6.js.map} +0 -0
  98. /package/dist/{chunk-T4FQ6TZT.js.map → chunk-QRDB4NLB.js.map} +0 -0
  99. /package/dist/{chunk-K5JZ5KLO.js.map → chunk-R4AB6GCT.js.map} +0 -0
  100. /package/dist/{chunk-FX7RAMGC.js.map → chunk-R6FPFMOS.js.map} +0 -0
  101. /package/dist/{chunk-MGJ72BA4.js.map → chunk-RJB3QUSP.js.map} +0 -0
  102. /package/dist/{chunk-ATJ6ZA2V.js.map → chunk-SDUJIANJ.js.map} +0 -0
  103. /package/dist/{chunk-7ADQL5Y3.js.map → chunk-THEFTZ7F.js.map} +0 -0
  104. /package/dist/{chunk-NT4WIP7I.js.map → chunk-UYCAF77E.js.map} +0 -0
  105. /package/dist/{chunk-YAMRVXOD.js.map → chunk-VRWUL4TR.js.map} +0 -0
  106. /package/dist/{chunk-4NAOR44V.js.map → chunk-ZMIPG6TR.js.map} +0 -0
  107. /package/dist/{convert-to-pnpm-UT2R6EOW.js.map → convert-to-pnpm-KCXOYOHW.js.map} +0 -0
  108. /package/dist/{ensure-site-LLPOXLWP.js.map → ensure-site-HYAIVZY2.js.map} +0 -0
  109. /package/dist/{forms-notify-target-PN6P3WPW.js.map → forms-notify-target-HSL4ZMT5.js.map} +0 -0
  110. /package/dist/{github-signals-HTH7474O.js.map → github-signals-I7XACMCE.js.map} +0 -0
  111. /package/dist/{header-image-WMHZFU7B.js.map → header-image-ADYPTGVY.js.map} +0 -0
  112. /package/dist/{health-endpoint-4E53ZMLE.js.map → health-endpoint-JNCUILHO.js.map} +0 -0
  113. /package/dist/{init-3ZJ7Z5NG.js.map → init-FLGLJCMJ.js.map} +0 -0
  114. /package/dist/{launch-FRCDOB56.js.map → launch-AVKH2JVF.js.map} +0 -0
  115. /package/dist/{lighthouse-airtable-I46QCEIS.js.map → lighthouse-airtable-CHANWVO6.js.map} +0 -0
  116. /package/dist/{onboard-45NPDMMW.js.map → onboard-HF4FTJDE.js.map} +0 -0
  117. /package/dist/{orchestrate-34JYNIXT.js.map → orchestrate-5FMNYJEA.js.map} +0 -0
  118. /package/dist/{preflight-WVD32COX.js.map → preflight-HTWIX3MN.js.map} +0 -0
  119. /package/dist/{protection-audit-2UR45SOC.js.map → protection-audit-64PLLBLI.js.map} +0 -0
  120. /package/dist/{renovate-dispatch-LOOCNZTW.js.map → renovate-dispatch-HBZG2UEU.js.map} +0 -0
  121. /package/dist/{self-updating-5TJ2NB26.js.map → self-updating-4Q65OVXY.js.map} +0 -0
  122. /package/dist/{selftest-BTCZLCTL.js.map → selftest-K5XZKVTE.js.map} +0 -0
  123. /package/dist/{smoke-suite-GHKXMKIR.js.map → smoke-suite-J3LL3JZJ.js.map} +0 -0
  124. /package/dist/{submissions-KUXLKCSQ.js.map → submissions-GMBUP4LV.js.map} +0 -0
  125. /package/dist/{svelte-codemods-YTCTLK7G.js.map → svelte-codemods-JTMX4BSX.js.map} +0 -0
  126. /package/dist/{sync-configs-KSSQZU5L.js.map → sync-configs-5DXSL64Q.js.map} +0 -0
  127. /package/dist/{upgrade-UDAPY3SN.js.map → upgrade-HCR25P25.js.map} +0 -0
  128. /package/dist/{websites-JXG3WWAU.js.map → websites-VITQ3WXN.js.map} +0 -0
  129. /package/dist/{write-audits-to-airtable-7D3LV4KP.js.map → write-audits-to-airtable-3SOXLKJK.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/prismic/models/canon.ts","../src/prismic/models/diff.ts","../src/prismic/models/config.ts","../src/prismic/models/token.ts","../src/prismic/models/local.ts","../src/prismic/models/remote.ts","../src/prismic/models/push.ts","../src/prismic/models/write.ts","../src/util/working-tree.ts"],"sourcesContent":["// Canonical form for comparing a model on disk against the copy REGISTERED in\n// Prismic. Prismic hands its copy back through its own serializer, so it differs\n// from the file on disk in ways that mean NOTHING: key order, an explicit\n// `\"select\": null` added to Link fields, `imageUrl` (the slice preview\n// screenshot — Prismic owns it, so its value is meaningless for comparison; on\n// disk it is usually `\"\"` but sometimes a stale URL: nine fleet repos'\n// RichText/model.json carry a real `https://…` value — caltex-landing,\n// data-dynamiq, erp-industrial, espada, gallerysonder, hedloc,\n// medical-solutions-of-texas, reddoor-website, vineyard-custom-homes), and —\n// the one beachfront's original did not handle — EMPTY STRINGS.\n//\n// The empty-string rule is load-bearing, not cosmetic — but a thumbnail\n// height:\"\" is the RARE trigger, not the common one. Fleet-wide (grep\n// '\"height\": *\"\"' across every *.json, not just model.json — see the warning\n// below) it occurs exactly ONCE: hedloc/customtypes/home/index.json, a\n// thumbnail {\"name\":\"desktop\",\"width\":1200,\"height\":\"\"}. The other four real\n// thumbnail entries on disk (caltex-landing/src/lib/slices/Hero/model.json ×2,\n// hedloc/src/lib/slices/Hero/model.json ×2) already carry \"height\":null, not\n// \"\". What makes the rule load-bearing is its reach far beyond thumbnails:\n// plain \"\" shows up in ordinary field configs constantly — \"placeholder\":\"\"\n// alone appears 801 times fleet-wide. Any one of those is the same trap in\n// miniature if Prismic ever normalises it away on ingest. The proof for the\n// specific hedloc case is a round-trip through the Types API on the-pinnacle:\n// sent height:\"\", read back height:null — confirming Prismic really does\n// coerce \"\" to null (and drop the key) rather than store it verbatim, so\n// keeping \"\" while dropping null would have made hedloc's copies unequal\n// FOREVER: a push sends \"\", Prismic stores null, the next scan diffs again.\n//\n// WARNING for the next person grepping to check a claim like this one: half\n// of what canon() compares are custom types, not slice models, and they live\n// at a different path — customtypes/<id>/index.json, not\n// src/lib/slices/<Name>/model.json. A model.json-only search misses them\n// entirely, which is exactly how the hedloc occurrence above stays invisible\n// to a scan that only looks at slice models.\n//\n// Safe because the only comparison it collapses is `\"\" vs absent`, which IS the\n// non-difference. `\"\" vs \"something\"` still differs (one side keeps the key),\n// and `0`/`false` are untouched — they are not `\"\"`.\n//\n// `imageUrl` is dropped by key name alone, at ANY depth — not just on\n// variations. That's a chosen tradeoff, not an oversight: narrowing it to\n// \"variation-shaped objects\" would trade this hole for a shape heuristic that\n// could itself be wrong. The risk it accepts: a Prismic field whose literal API\n// ID is `imageUrl` would go invisible to comparison on both sides — the exact\n// silent-drop failure class this module exists to prevent. Not currently live:\n// fleet field API IDs are snake_case (`background_image`, `meta_image`, …),\n// and all 180 current occurrences of the key sit at `variations[].imageUrl`\n// (measured 2026-08-12 by walking every model this pipeline loads, at every\n// depth, across the 15 in-scope fleet repos on their DEFAULT branches — the\n// starters carry a sentinel repositoryName and are never read, so their models\n// are not counted; zero occurrences were found anywhere but on a variation).\n\n/** Recursively normalise a model for comparison. Pure. */\nexport function canon(v: unknown): unknown {\n // Elements are NOT filtered here — only object keys are, below. Dropping a\n // null/\"\"/undefined element would shift array indices, so [1, null] would\n // wrongly canon-equal [1]. An empty array stays `[]`, not dropped: e.g.\n // `thumbnails: []` round-trips through Prismic unchanged, so \"empty\" has to\n // stay distinguishable from \"absent\".\n if (Array.isArray(v)) return v.map(canon);\n if (v !== null && typeof v === \"object\") {\n return Object.fromEntries(\n Object.entries(v as Record<string, unknown>)\n // Only the VALUE of a noise key is filtered out — an object left with\n // zero keys collapses to `{}`, which is exactly what `constraint: {}`\n // already is on disk. Nothing here treats \"now empty\" as \"absent\".\n .filter(([k, x]) => x !== null && x !== undefined && x !== \"\" && k !== \"imageUrl\")\n .sort(([a], [b]) => (a < b ? -1 : 1))\n .map(([k, x]) => [k, canon(x)]),\n );\n }\n return v;\n}\n\n/** True when two models are the same MODEL, ignoring serializer noise.\n * Precondition: callers pass models (plain objects/arrays read from JSON),\n * never `undefined` — this performs no defensive check for that case. */\nexport function sameModel(a: unknown, b: unknown): boolean {\n return JSON.stringify(canon(a)) === JSON.stringify(canon(b));\n}\n","import { sameModel } from \"./canon.js\";\nimport type { LocalEntry, ModelDiff, ModelKind, PrismicModel, RemoteEntry } from \"./types.js\";\n\n/** Identity of a model ACROSS collections. A custom type and a slice can share\n * an id (they are different Types API resources), so `kind` is part of the key —\n * keying on id alone pairs them and reports a phantom update whose push would\n * go to the wrong endpoint. */\nconst key = (e: { kind: ModelKind; id: string }): string => `${e.kind}:${e.id}`;\n\n/** Sort local + remote models into the four buckets. Pure — no IO, no ordering\n * assumptions. `remoteOnly` is reported only; nothing downstream may delete it. */\nexport function diffModels(local: LocalEntry[], remote: RemoteEntry[]): ModelDiff {\n const remoteByKey = new Map(remote.map((e) => [key(e), e]));\n const localKeys = new Set(local.map(key));\n const diff: ModelDiff = { toCreate: [], toUpdate: [], unchanged: [], remoteOnly: [] };\n for (const l of local) {\n const r = remoteByKey.get(key(l));\n if (!r) diff.toCreate.push(l);\n else if (sameModel(l.model, r.model)) diff.unchanged.push(l);\n else diff.toUpdate.push({ local: l, remote: r });\n }\n for (const r of remote) if (!localKeys.has(key(r))) diff.remoteOnly.push(r);\n return diff;\n}\n\ntype Zone = Record<string, unknown>;\nconst asZone = (v: unknown): Zone => (v !== null && typeof v === \"object\" ? (v as Zone) : {});\n\ntype Verb = \"added\" | \"removed\" | \"changed\";\n\n/** Real fleet JSON nests at most 13 levels deep, and this only recurses at\n * Group/Slices boundaries (Prismic does not nest those more than one level\n * in practice) — so this bound sits comfortably above anything real data\n * will hit. It exists purely to stop a malformed/pathological model from\n * recursing without limit, not to clip genuine fleet depth. */\nconst MAX_FIELD_DEPTH = 20;\n\n/** CAVEAT that applies to every add/remove pass in this file (`compareMeta`\n * here AND `compareZone` below): they test raw key PRESENCE via\n * `Object.hasOwn`, not `sameModel`. Only the CHANGED pass runs values\n * through `sameModel`/`canon()`. So a key whose VALUE `canon` drops as\n * noise (`null`/`\"\"`) but that exists on only ONE side could, in\n * principle, render an add/remove line for what is really serializer\n * noise, not a real difference.\n *\n * Bounded and currently harmless: 0 of the 1,167 real `\"\"`/`null` keys in\n * the fleet sit at a raw-presence level here — all are inside field\n * configs, one level down, where the CHANGED pass's `sameModel` call does\n * guard them — and a pair whose ONLY difference is canon-noise is\n * classified `unchanged` by `diffModels` and never reaches `describeDiff`\n * at all.\n *\n * BUT that second bound is a property of the CALLER, not of this\n * function. If any future caller ever runs `describeDiff` on a pair\n * `diffModels` sorted into `unchanged` — a verbose mode, a debug flag —\n * that bound disappears and this becomes a false-alarm source: crying\n * \"changed\" on pure serializer noise is the INVERTED failure from the one\n * this whole file exists to prevent, and the nightly drift sweep must\n * never produce it. */\n\n/** Flat key-diff for a METADATA object — the model's own top-level keys, or\n * a variation's own top-level keys. Deliberately NOT recursive: metadata\n * values (`label`, `name`, `repeatable`, …) are leaves, never Group/Slices\n * containers, so there is nothing here for `compareZone` to descend into.\n * `format` renders each line so metadata reads distinctly from the\n * dot-notation field-path lines `compareZone` emits — `~ (model) label`\n * must never be mistaken for a field path. */\nconst compareMeta = (\n a: Zone,\n b: Zone,\n excluded: readonly string[],\n format: (k: string, verb: Verb) => string,\n): string[] => {\n const skip = new Set<string>(excluded);\n const out: string[] = [];\n for (const k of Object.keys(a)) {\n if (skip.has(k)) continue;\n if (!Object.hasOwn(b, k)) out.push(format(k, \"added\"));\n }\n for (const k of Object.keys(b)) {\n if (skip.has(k)) continue;\n if (!Object.hasOwn(a, k)) out.push(format(k, \"removed\"));\n }\n for (const k of Object.keys(a)) {\n if (skip.has(k) || !Object.hasOwn(b, k)) continue;\n if (!sameModel(a[k], b[k])) out.push(format(k, \"changed\"));\n }\n return out;\n};\n\nconst modelMetaLine = (k: string, verb: Verb): string =>\n verb === \"added\"\n ? `+ (model) ${k}`\n : verb === \"removed\"\n ? `- (model) ${k} (only in Prismic — pushing DELETES it)`\n : `~ (model) ${k}`;\n\nconst variationMetaLine = (id: string, k: string, verb: Verb): string =>\n verb === \"added\"\n ? `+ variation ${id}: ${k}`\n : verb === \"removed\"\n ? `- variation ${id}: ${k} (only in Prismic — pushing DELETES it)`\n : `~ variation ${id}: ${k}`;\n\n/** Top-level keys that hold FIELD structure, not metadata — excluded from\n * the model-level metadata diff because each has its own dedicated pass. */\nconst MODEL_FIELD_KEYS = [\"variations\", \"json\"] as const;\n\n/** A variation's own field zones. */\nconst VARIATION_FIELD_ZONES = [\"primary\", \"items\"] as const;\n\n/** `imageUrl` is the slice-preview screenshot URL — Prismic-owned, and\n * `canon()` (see canon.ts) already treats it as meaningless noise at ANY\n * depth, dropping the key outright before comparing. A per-key metadata\n * diff bypasses that whole-object canonicalization, so without this\n * exclusion a stale on-disk `imageUrl` (nine real fleet repos carry one,\n * per canon.ts) would surface as a spurious\n * `~ variation X: imageUrl (changed)` line riding alongside — or instead\n * of — a real change. */\nconst VARIATION_META_EXCLUDE = [...VARIATION_FIELD_ZONES, \"imageUrl\"] as const;\n\n/** A field's nested comparison zone, if it has one. `Group` fields keep\n * their member fields at `config.fields`; `Slices` fields keep their\n * allowed slice choices at `config.choices`. Anything else has no children\n * to recurse into. Returns `undefined` (not `{}`) for \"not a container\", so\n * the caller can tell \"no container\" apart from \"container with zero\n * fields\". */\nconst containerChildren = (field: unknown): Zone | undefined => {\n const f = asZone(field);\n if (f.type === \"Group\") return asZone(asZone(f.config).fields);\n if (f.type === \"Slices\") return asZone(asZone(f.config).choices);\n return undefined;\n};\n\n/** A container field's own definition with its CHILDREN key excluded —\n * `fields` for Group, `choices` for Slices. Isolates \"did the container's\n * own config change\" (a Group's `repeat` flag, a Slices field's `fieldset`\n * label) from \"did a child change\", so `compareZone` can report the two\n * independently instead of one hiding the other. */\nconst withoutChildren = (field: unknown, childrenKey: \"fields\" | \"choices\"): unknown => {\n const f = asZone(field);\n const cfg = asZone(f.config);\n const { [childrenKey]: _children, ...restCfg } = cfg;\n return { ...f, config: restCfg };\n};\n\n/** Field-zone diff — used for `primary`/`items`, custom-type tabs, and\n * (recursively) the inside of Group/Slices containers. A field present on\n * both sides but different is, by default, one `~ label.field (changed)`\n * line. But when BOTH sides agree the field is the SAME container type\n * (Group or Slices) and recursing into its children finds the actual\n * difference, that specific child is reported instead of the opaque parent\n * line — e.g. `+ Main.slices.lead_text` rather than `~ Main.slices\n * (changed)`, which used to be the only signal a reviewer got for \"someone\n * removed a slice-zone choice\" (137 real Group/Slices containers in the\n * fleet, some hiding up to 28 children).\n *\n * The container's OWN config (everything except its children) is ALSO\n * compared whenever a child difference is found, not only when it isn't:\n * reporting the child line alone was itself a metadata blind spot one\n * level down from the one `describeDiff` fixes at the model/variation\n * level — mutating a Group's `repeat` flag alongside a field addition\n * rendered as a lone `+ Main.sections.new_field`, with the destructive\n * half of the change invisible (155/155 real fleet containers, mutating\n * both at once). If recursion finds nothing at all — the container's own\n * config changed with byte-identical children — the generic changed line\n * is still emitted, so the change never goes silent either way. */\nconst compareZone = (label: string, lz: unknown, rz: unknown, depth: number): string[] => {\n const a = asZone(lz);\n const b = asZone(rz);\n const out: string[] = [];\n for (const k of Object.keys(a)) if (!Object.hasOwn(b, k)) out.push(`+ ${label}.${k}`);\n for (const k of Object.keys(b))\n if (!Object.hasOwn(a, k)) out.push(`- ${label}.${k} (only in Prismic — pushing DELETES it)`);\n for (const k of Object.keys(a)) {\n if (!Object.hasOwn(b, k) || sameModel(a[k], b[k])) continue;\n const childPath = `${label}.${k}`;\n if (depth < MAX_FIELD_DEPTH) {\n const av = asZone(a[k]);\n const bv = asZone(b[k]);\n if (av.type === bv.type && (av.type === \"Group\" || av.type === \"Slices\")) {\n const childrenKey = av.type === \"Group\" ? \"fields\" : \"choices\";\n const nested = compareZone(\n childPath,\n containerChildren(a[k]),\n containerChildren(b[k]),\n depth + 1,\n );\n if (nested.length > 0) {\n if (!sameModel(withoutChildren(a[k], childrenKey), withoutChildren(b[k], childrenKey)))\n out.push(`~ ${childPath} (changed)`);\n out.push(...nested);\n continue;\n }\n }\n }\n out.push(`~ ${childPath} (changed)`);\n }\n return out;\n};\n\n/** Field-level lines for ONE model pair, used in PR comments, CLI output,\n * and the nightly drift sweep. `remote` is optional so a first-ever push\n * (nothing registered in Prismic yet) can still be described — everything\n * local renders as new instead of throwing.\n *\n * Three passes, not one: `primary`/`items`/`json`-tab field diffs are\n * necessary but not sufficient. A model-level rename (`label`, `format`,\n * `status`, `repeatable`) or a variation-level rename (`name`,\n * `description`) touches NEITHER zone, so without the other two passes\n * those changes rendered as zero lines while `diffModels` still sorted the\n * pair into `toUpdate` — a PR comment reading \"CHANGED customtype page\"\n * with nothing underneath it, and a reviewer approving a change they never\n * actually saw. Verified against the real fleet: mutating slice `name` or\n * `description` alone (174/174 real variations) and customtype `label`,\n * `status`, `format`, or `repeatable` alone (77/77 real custom types) both\n * rendered blank before this fix.\n *\n * Those two counts are an EXPERIMENT RECORD, not a live inventory, and they\n * deliberately do not match the 132 slice models / 68 custom types cited\n * elsewhere in this directory. The run (2026-08-12) covered every model file\n * ON DISK, including the two starter templates whose sentinel\n * `repositoryName` keeps them out of this pipeline — a strict SUPERSET of\n * what `readPrismicConfig` admits. That makes the evidence stronger than the\n * in-scope figure would suggest: the blind spot reproduced across all 174 and\n * all 77, the in-scope models among them. Do not \"reconcile\" these numbers\n * down to the in-scope count — that would claim an experiment that was never\n * run. `repeatable` flipping is destructive, not\n * cosmetic — and this is also precisely what the nightly drift sweep exists\n * to catch, since an out-of-band dashboard edit (someone renaming a type,\n * say) IS a metadata-only edit by definition. */\nexport function describeDiff(local: PrismicModel, remote: PrismicModel | undefined): string[] {\n const l = asZone(local);\n const r = asZone(remote);\n const lines: string[] = [];\n\n // On an insert (no remote yet) EVERY key is new, so itemizing each one is\n // pure noise once the structural `+ tab X (new)` / `+ variation X (new)`\n // lines already say \"this is all new\" — verified against the real fleet:\n // the metadata pass alone produced 1,111 `+ (model) k` lines against 368\n // structural ones on inserts (75% noise), a custom type opening with five\n // `+ (model) …` lines before the first `+ tab Main (new)`.\n if (remote !== undefined) lines.push(...compareMeta(l, r, MODEL_FIELD_KEYS, modelMetaLine));\n\n // Slice shape.\n if (Array.isArray(l.variations) || Array.isArray(r.variations)) {\n const variationEntries = (arr: unknown): Array<[string, Zone]> =>\n Array.isArray(arr)\n ? arr.map((v, i) => {\n const z = asZone(v);\n // A cast (`z.id as string`) would LIE when `id` is absent: it\n // would compile-time-assert a string that isn't there at\n // runtime, and every id-less variation on a side would key to\n // the literal string \"undefined\" and collide with every OTHER\n // id-less variation on that SAME side — silently dropping all\n // but the last one's diff. 0 of the 180 in-scope fleet\n // variations lack an id (measured 2026-08-12 on each repo's\n // DEFAULT branch across the 15 repos this pipeline loads; the\n // starters carry a sentinel repositoryName and are never read),\n // so this is defensive, but the index fallback is unique per\n // side and its label says exactly what it is.\n const id = typeof z.id === \"string\" ? z.id : `variations[${i}]`;\n return [id, z] as [string, Zone];\n })\n : [];\n const lv = new Map(variationEntries(l.variations));\n const rv = new Map(variationEntries(r.variations));\n for (const [id, v] of lv) {\n const rr = rv.get(id);\n if (!rr) {\n lines.push(`+ variation ${id} (new)`);\n continue;\n }\n for (const zone of VARIATION_FIELD_ZONES)\n lines.push(...compareZone(`${id}.${zone}`, v[zone], rr[zone], 0));\n lines.push(\n ...compareMeta(v, rr, VARIATION_META_EXCLUDE, (k, verb) => variationMetaLine(id, k, verb)),\n );\n }\n for (const id of rv.keys())\n if (!lv.has(id)) lines.push(`- variation ${id} (only in Prismic — pushing DELETES it)`);\n }\n\n // Custom-type shape.\n if (l.json !== undefined || r.json !== undefined) {\n const lt = asZone(l.json);\n const rt = asZone(r.json);\n for (const tab of Object.keys(lt)) {\n if (!Object.hasOwn(rt, tab)) {\n lines.push(`+ tab ${tab} (new)`);\n continue;\n }\n lines.push(...compareZone(tab, lt[tab], rt[tab], 0));\n }\n for (const tab of Object.keys(rt))\n if (!Object.hasOwn(lt, tab))\n lines.push(`- tab ${tab} (only in Prismic — pushing DELETES it)`);\n }\n\n // A blank \"CHANGED <model>\" is the failure this function exists to prevent, and\n // three separate blind spots have now produced one (metadata, containers, zone\n // presence). This is the backstop: anything `sameModel` calls different MUST\n // render at least one line. It closes the known zone-presence class and\n // degrades any FUTURE blind spot to an honest \"changed, no detail\" instead of a\n // silent blank. It also turns the property test below from a sample into a\n // guarantee.\n if (lines.length === 0 && remote !== undefined && !sameModel(local, remote))\n lines.push(\"~ (model) changed (no field-level detail)\");\n\n return lines;\n}\n\n/** A push REPLACES the whole model, and Prismic — not the repo — owns the\n * slice-preview screenshot at `variations[].imageUrl`: it is `\"\"` on disk\n * for most models and a stale URL on nine real fleet RichText models (see\n * canon.ts). Sending the local value would blank or rot every slice\n * preview in the editor as a side effect of pushing one unrelated field\n * change. This is the deliberate asymmetry with `canon()`: comparison\n * DROPS `imageUrl` entirely (it must never cause a diff), but a push still\n * has to put a real value on the wire, and the only trustworthy one is\n * whatever is currently live on the remote.\n *\n * PRESENCE, not truthiness — the remote's value wins whenever the MATCHED\n * remote variation HAS an `imageUrl` KEY, including when its value is\n * `\"\"`. A truthy check (`shot ? … : v`) looks equivalent and is tempting\n * to \"simplify\" back to, but it is not: it conflates \"no remote data for\n * this id\" (nothing is known, so keeping local is correct) with \"remote\n * explicitly reports `\"\"`\" (Prismic currently has NO screenshot for this\n * variation — that IS the live truth). Falling through to local on a\n * remote `\"\"` doesn't preserve a preview, it WRITES a stale URL onto a\n * variation that currently has none — the exact damage this function\n * exists to prevent, on exactly the nine RichText models that carry one.\n * The check has to be `Object.hasOwn(remoteVariation, \"imageUrl\")`, not\n * `shots.has(id)`: an id MATCH alone is not proof the matched remote\n * variation carries the key at all — without this, a matched remote\n * variation that genuinely has no `imageUrl` property would overwrite a\n * real local URL with `undefined`.\n *\n * A variation with no string `id` is left untouched rather than matched by\n * array index. `describeDiff` can fall back to an index label\n * (`variations[i]`) because a wrong pairing there only costs a mislabeled\n * comment line; here a wrong pairing WRITES the wrong screenshot onto a\n * live variation on push. Guessing identity is the wrong trade when being\n * wrong is destructive instead of cosmetic, so an id-less local variation\n * just passes through with whatever `imageUrl` it already has.\n *\n * On an INSERT (`remote === undefined`) there is no remote to defer to at\n * all, but the local value is still not safe to send verbatim — nine real\n * fleet repos carry a stale on-disk URL, and sending it on a fresh insert\n * is the exact damage this function exists to prevent, just on the one\n * path with no remote value to fall back on. Every variation's `imageUrl`\n * is normalised to `\"\"` instead (unconditionally — there is no matching\n * happening here, so the id-less-variation caveat above does not apply):\n * `\"\"` is what Slice Machine writes normally and what 171 of the 180\n * in-scope fleet variations already carry (measured 2026-08-12 on each\n * repo's DEFAULT branch across the 15 repos this pipeline loads; the other\n * 9 hold a real URL and NONE omit the key), so it is proven acceptable to\n * the Types API. The KEY stays present — never deleted —\n * because an absent `imageUrl` is unproven against the API, and a live\n * push is not the place to find out. */\nexport function withRemoteScreenshots(\n local: PrismicModel,\n remote: PrismicModel | undefined,\n): PrismicModel {\n if (!Array.isArray(local.variations)) return local;\n\n if (!remote) {\n return {\n ...local,\n variations: local.variations.map((v) => ({ ...asZone(v), imageUrl: \"\" })),\n };\n }\n\n const shots = new Map(\n (Array.isArray(remote.variations) ? remote.variations : [])\n .map((v) => asZone(v))\n .filter((o): o is Zone & { id: string } => typeof o.id === \"string\")\n .map((o) => [o.id, o] as const),\n );\n return {\n ...local,\n variations: local.variations.map((v) => {\n const o = asZone(v);\n if (typeof o.id !== \"string\") return v;\n const remoteVariation = shots.get(o.id);\n if (!remoteVariation || !Object.hasOwn(remoteVariation, \"imageUrl\")) return v;\n return { ...o, imageUrl: remoteVariation.imageUrl };\n }),\n };\n}\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\n/** What this pipeline needs from a site's Prismic config. */\nexport type PrismicConfig = {\n /** The Prismic repository name — the `repository` header on every Types API call. */\n repositoryName: string;\n /** Slice library directories, repo-relative. Fleet-uniform at [\"./src/lib/slices\"],\n * but read rather than assumed: alamo-anatomy points at a directory that does\n * not exist, and assuming the path would make its models invisible instead of\n * visibly empty. An ABSENT key defaults; a present-but-malformed one throws\n * (see {@link readPrismicConfig}). */\n libraries: string[];\n};\n\n/** Slice Machine's file, and the name the Prismic CLI renames it to, in\n * preference order. Both are read so an adopting repo does not go dark;\n * slicemachine.config.json is tried first because that is what every Prismic\n * repo in the fleet ships today — measured 2026-08-12 across the 30 local\n * checkouts: 17 carry a Prismic config and all 17 use the Slice Machine name;\n * none ships prismic.config.json yet. That makes prismic.config.json the\n * FORWARD path, which is exactly why a stale first file must not be allowed to\n * mask it — see the sentinel handling in {@link readPrismicConfig}. */\nconst CONFIG_FILES = [\"slicemachine.config.json\", \"prismic.config.json\"] as const;\n\n/**\n * Repository names that mean \"this repo is not pointed at a content model of\n * its own\" — a placeholder, never a client's Prismic repository.\n *\n * `your-prismic-repo-name` is the starter's placeholder, carried unreplaced by\n * three fleet repos: reddoor-starter, canvas-starter, and\n * composition-hospitality. Only the first two are visible from a local sweep\n * (verified 2026-08-12; the third has no local checkout and was confirmed\n * through the GitHub API), so a measurement taken across `~/Documents/GitHub`\n * finds two and is not evidence that the third is gone.\n *\n * `reddoor-wireframer` is Reddoor's shared wireframe repository (operator\n * ruling 2026-08-14). Only data-dynamiq names it. It belongs here for a reason\n * the starter sentinel never had: IT RESOLVES. The repository genuinely exists\n * — HTTP 200, two starter documents last published 2024-03-12 — so unlike a\n * 404-ing placeholder, nothing about querying it will ever reveal that it is\n * one. Without this entry the site is a permanent `unknown` verdict and a\n * nightly `prismic-unknown:` cockpit warning that no credential can clear,\n * because the correct number of tokens to mint for it is zero.\n *\n * ADDING TO THIS LIST IS A DECISION ONLY AN OPERATOR CAN MAKE. A name here is\n * dropped from every sweep, so a real client repository added by mistake stops\n * being checked and reports as \"no Prismic here\" rather than as an error.\n *\n * A plain readonly array rather than a Set: this module's capability guard\n * rejects free ambient identifiers, and `ReadonlySet` is one. Two entries do\n * not need a hash lookup, and the guard is worth more than the micro-optimum.\n */\nconst PLACEHOLDER_REPOSITORY_NAMES: readonly string[] = [\n \"your-prismic-repo-name\",\n \"reddoor-wireframer\",\n];\n\n/**\n * Read a repo's Prismic config, or null when it is not a Prismic site.\n *\n * null means \"no Prismic here, skip this repo\" — no config file at all, or\n * every candidate file still carrying the starter sentinel. A PRESENT but\n * malformed or unreadable config THROWS: a repo that has Prismic and a broken\n * config must surface as an error, never as a silent skip. That distinction is\n * the whole point of the return type, and it is a bigger lever here than\n * anywhere else in this module — one unreadable model file loses one model, but\n * one unreadable config drops a LIVE SITE out of the fleet sweep entirely, with\n * the sweep still reporting success.\n *\n * A sentinel `repositoryName` means \"not configured YET\", so it moves to the\n * next candidate filename rather than ending the search. That matters because\n * the Prismic CLI's migration RENAMES slicemachine.config.json to\n * prismic.config.json, so a half-migrated repo can hold a stale sentinel in the\n * first file and its real configuration in the second. Returning null on the\n * first sight of a sentinel would drop that live site from the sweep on the\n * strength of a file nobody uses any more. Only when every candidate is\n * exhausted does a sentinel yield null.\n */\nexport async function readPrismicConfig(repoRoot: string): Promise<PrismicConfig | null> {\n for (const name of CONFIG_FILES) {\n let raw: string;\n try {\n raw = await readFile(join(repoRoot, name), \"utf-8\");\n } catch (e) {\n // ENOENT is the ONLY error that means \"this repo does not have this\n // file\". EACCES, EISDIR (a directory named slicemachine.config.json),\n // ELOOP and I/O errors all mean the file is THERE and we cannot read it —\n // and falling through on those would walk to the next candidate name and\n // then to `return null`, i.e. \"not a Prismic site\", silently removing a\n // live site from every sweep that calls this.\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") continue;\n throw new Error(`${name}: present but unreadable (${(e as Error).message})`, { cause: e });\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n throw new Error(`${name}: invalid JSON (${(e as Error).message})`, { cause: e });\n }\n // A config file containing literal `null` (or `[]`, or a bare string) is\n // valid JSON, so it arrives here parsed. Reading `.repositoryName` off it\n // would throw a bare `Cannot read properties of null` naming neither the\n // file nor the repo — the same nameless-throw defect just fixed one layer\n // down in local.ts, and worse here because this runs per REPO in a fleet\n // sweep, where \"which one?\" is the operator's first question.\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n const got = parsed === null ? \"null\" : Array.isArray(parsed) ? \"an array\" : typeof parsed;\n throw new Error(`${name}: not a JSON object (got ${got})`);\n }\n const cfg = parsed as { repositoryName?: unknown; libraries?: unknown };\n if (typeof cfg.repositoryName !== \"string\" || cfg.repositoryName.trim() === \"\") {\n throw new Error(`${name}: repositoryName is missing or not a string`);\n }\n // Trimmed on the way OUT, not just for validation. Untrimmed, \" espada \"\n // would go verbatim into the Types API `repository` header and 404 — which\n // presents to an operator as \"the Prismic repository does not exist\" rather\n // than \"your config has a stray space\", sending them to the wrong problem.\n const repositoryName = cfg.repositoryName.trim();\n\n // Placeholder = \"no content model of its own\" → try the next candidate\n // filename rather than declaring the repo non-Prismic. Checked before\n // `libraries` because a repo this pipeline never processes has no\n // configuration worth validating.\n //\n // `continue`, not `return null`, and that applies to EVERY placeholder: a\n // half-migrated repo can hold a stale placeholder in slicemachine.config.json\n // and its real configuration in prismic.config.json, so short-circuiting\n // here would drop a live site from the sweep on the strength of a file\n // nobody reads any more.\n if (PLACEHOLDER_REPOSITORY_NAMES.includes(repositoryName)) continue;\n\n // ABSENT defaults; PRESENT-but-unusable throws. Replacing a malformed value\n // with the fleet default is the same collapse this module refuses\n // everywhere else: if the site's real library is somewhere other than the\n // default, every slice it owns reads as missing, and the sweep reports\n // \"this site has no slices\" as fact rather than \"we could not tell\".\n //\n // `libraries: []` is deliberately LEGAL and is not the same thing — an\n // explicitly empty array is a statement (\"this site has no slice\n // libraries\"), not a malformation. Do not \"fix\" it into the default.\n if (\n cfg.libraries !== undefined &&\n !(Array.isArray(cfg.libraries) && cfg.libraries.every((l) => typeof l === \"string\"))\n ) {\n const got = Array.isArray(cfg.libraries)\n ? \"an array with a non-string entry\"\n : cfg.libraries === null\n ? \"null\"\n : typeof cfg.libraries;\n throw new Error(`${name}: libraries is present but is not an array of strings (got ${got})`);\n }\n const libraries =\n cfg.libraries === undefined ? [\"./src/lib/slices\"] : (cfg.libraries as string[]);\n return { repositoryName, libraries };\n }\n return null;\n}\n","/** Where a resolved token came from — printed by the tokens doctor so an\n * operator can see WHICH env var answered, never the value. */\nexport type ResolvedToken = { token: string; source: string };\n\n/**\n * The canonical env var holding a Prismic repository's write token:\n * `PRISMIC_TOKEN_<REPOSITORY_NAME>` upper-snaked.\n *\n * One rule, derivable from the repository name alone — no hand-maintained map\n * from site slug to env var. That matters because the two genuinely differ:\n * medical-solutions-of-texas's Prismic repository is `msot`, reddoor-website's\n * is `reddoor-la`, and beachfront-dentistry's is the hash `48bb12d1`. A draft\n * of the nightly workflow hardcoded PRISMIC_TOKEN_MEDICAL_SOLUTIONS_OF_TEXAS;\n * that secret would never have resolved, and the failure would have looked like\n * a credentials problem rather than a naming one.\n *\n * Verified across all 15 Prismic-configured fleet repos: every derived name is\n * a legal identifier and no two repositories collide onto one name.\n *\n * THROWS on a name with no alphanumeric characters. Such a name would collapse\n * to the bare prefix, so every malformed config would share ONE env var — two\n * sites silently reading one credential, which is precisely the cross-wiring\n * `allowGeneric: false` exists to prevent, reintroduced through the naming\n * rule. `readPrismicConfig` already rejects an empty repositoryName, so this is\n * unreachable through the normal path and is here to keep it that way.\n */\nexport function prismicTokenEnvName(repositoryName: string): string {\n const slug = repositoryName\n .toUpperCase()\n .replace(/[^A-Z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\");\n if (slug === \"\") {\n throw new Error(\n `cannot derive a token env var from repositoryName ${JSON.stringify(repositoryName)}: ` +\n `it has no alphanumeric characters`,\n );\n }\n return `PRISMIC_TOKEN_${slug}`;\n}\n\n/**\n * Resolve the write token for one Prismic repository.\n *\n * `allowGeneric` is the mode switch and it matters. IN-REPO (CI) runs set it\n * true: the site's own Actions secret is `PRISMIC_WRITE_TOKEN`, the name every\n * site's code already reads, and there is exactly one Prismic repository in\n * scope. FLEET runs set it FALSE: a single generic token in the environment\n * while iterating 18 repositories would silently attach the wrong credential to\n * every site after the first.\n */\nexport function resolvePrismicToken(\n repositoryName: string,\n env: Record<string, string | undefined>,\n opts: { allowGeneric: boolean },\n): ResolvedToken | null {\n const canonical = prismicTokenEnvName(repositoryName);\n const names = opts.allowGeneric ? [canonical, \"PRISMIC_WRITE_TOKEN\"] : [canonical];\n for (const name of names) {\n const value = env[name]?.trim();\n if (value) return { token: value, source: name };\n }\n return null;\n}\n","import { lstat, readdir, readFile, stat } from \"node:fs/promises\";\nimport { join, posix, relative, sep } from \"node:path\";\nimport type { LocalEntry, ModelKind, PrismicModel } from \"./types.js\";\n\n/** Repo-relative, forward-slashed — the form used in PR comments and passed to\n * the target repo's prettier, both of which are POSIX-shaped even on Windows. */\nconst relPath = (repoRoot: string, full: string): string =>\n relative(repoRoot, full).split(sep).join(posix.sep);\n\n/**\n * Whether `full` is PROVEN not to exist — the ONLY condition under which this\n * reader is allowed to move on quietly.\n *\n * The polarity is the whole point: absence must be proved, never assumed. A\n * clean ENOENT is proof; every other outcome — a successful stat, EACCES on the\n * parent directory, ENOTDIR, an I/O error — answers \"not proven absent\", and\n * the caller then goes on to read the path and fails loudly with a real errno.\n * A helper that returned \"absent\" on ANY error (the obvious `try { … } catch {\n * return false }` shape) reintroduces exactly the failure this module exists to\n * prevent: an unreadable model becomes indistinguishable from a model that was\n * never there, CI reports \"in sync\", and the field is quietly missing in\n * Prismic — the same silence that has already cost live sites five fields.\n *\n * `lstat`, not `stat`, deliberately. `stat` follows symlinks, so a DANGLING\n * symlink answers ENOENT and would read as absent even though the repo plainly\n * contains an entry at that path. `lstat` sees the link itself, so a broken\n * link reads as present and the read that follows throws.\n */\nasync function isProvenAbsent(full: string): Promise<boolean> {\n try {\n await lstat(full);\n return false;\n } catch (e) {\n return (e as NodeJS.ErrnoException).code === \"ENOENT\";\n }\n}\n\n/**\n * Parse one model file that we have already established is NOT proven absent.\n *\n * Throws — never skips — on unreadable content, malformed JSON, or a missing\n * id: a model that cannot be read is INDISTINGUISHABLE from a model that does\n * not exist, and a silent skip would let CI report \"in sync\" while a field was\n * actually missing in Prismic — the five-field drop this pipeline exists to\n * prevent.\n *\n * That covers the \"present but not a regular file\" case by construction: a\n * DIRECTORY named `model.json` fails the read with EISDIR and throws here. That\n * is the intended outcome — it is a broken repo, not an absent model, and the\n * operator needs to see it rather than have the site's model silently vanish\n * from the comparison.\n */\nasync function readModel(repoRoot: string, full: string, kind: ModelKind): Promise<LocalEntry> {\n const rel = relPath(repoRoot, full);\n let raw: string;\n try {\n raw = await readFile(full, \"utf-8\");\n } catch (e) {\n throw new Error(`${rel}: cannot read model file (${(e as Error).message})`, { cause: e });\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n throw new Error(`${rel}: invalid JSON (${(e as Error).message})`, { cause: e });\n }\n // `null`, `[]` and `\"a string\"` are all valid JSON, so they reach here having\n // parsed cleanly. Without this guard, a file containing literal `null` throws\n // a raw TypeError from the `.id` read below — the ONE throw in this file that\n // would name neither the file nor a cause, leaving an operator with\n // \"Cannot read properties of null\" and 200-odd model files to search.\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n const got = parsed === null ? \"null\" : Array.isArray(parsed) ? \"an array\" : typeof parsed;\n throw new Error(`${rel}: model is not a JSON object (got ${got})`);\n }\n const model = parsed as Partial<PrismicModel>;\n if (typeof model.id !== \"string\" || model.id === \"\") {\n throw new Error(`${rel}: model has no string \"id\"`);\n }\n return { kind, id: model.id, model: model as PrismicModel, path: rel };\n}\n\n/**\n * The names of `dir`'s entries that are model DIRECTORIES, or [] when `dir`\n * itself is PROVEN absent.\n *\n * The two halves of that sentence are both load-bearing. A configured library\n * pointing nowhere (alamo-anatomy) must read as EMPTY — that is a config wart,\n * not a broken model, and failing the whole run over it would take a live site\n * out of the pipeline for a typo. But a directory that EXISTS and cannot be\n * listed must throw: reading it as empty would report every one of that site's\n * slices as absent, which downstream looks exactly like \"this site deleted all\n * its slices\". ENOTDIR is the concrete case worth surfacing — a library path\n * that points at a FILE is a config error, and answering [] would hide it.\n *\n * A SYMLINK to a directory counts as a directory. `readdir` reports it as\n * `isSymbolicLink()` and NOT `isDirectory()`, so the obvious\n * `.filter((d) => d.isDirectory())` drops a symlinked model directory with no\n * entry and no error — a present, perfectly readable model vanishing in\n * silence, which is the exact class this module exists to prevent. It would\n * also be internally inconsistent: at the FILE level this reader follows\n * symlinks happily (a symlinked `model.json` is read like any other file), so\n * refusing them one level up is an accident, not a policy.\n *\n * The polarity here is deliberately NOT the polarity of {@link isProvenAbsent},\n * and the difference is the difference between the two questions being asked:\n *\n * - At the FILE level we already know a model is supposed to be at that exact\n * path, so anything other than a clean ENOENT is an error.\n * - Here we are ENUMERATING candidates, and a non-directory entry is\n * legitimately not a candidate — a stray `README.md` in the slices folder\n * is not a broken model, and a symlink to a file is the same thing wearing\n * a hat. Both are skipped in silence.\n *\n * What we are never entitled to do is call an entry \"not a candidate\" when we\n * could not resolve it at all. A DANGLING symlink therefore throws rather than\n * skipping: it may well have been a model directory, and we cannot tell.\n *\n * Names are sorted so that read order — and therefore the file named \"first\" in\n * a duplicate-id error, and the order entries reach the PR comment — does not\n * depend on the filesystem's arbitrary directory ordering.\n */\nasync function subdirs(repoRoot: string, dir: string): Promise<string[]> {\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch (e) {\n if (await isProvenAbsent(dir)) return [];\n throw new Error(`${relPath(repoRoot, dir)}: cannot list directory (${(e as Error).message})`, {\n cause: e,\n });\n }\n\n const names: string[] = [];\n for (const d of entries) {\n if (d.isDirectory()) {\n names.push(d.name);\n continue;\n }\n // A plain file, socket, fifo — not a candidate, and not a problem.\n if (!d.isSymbolicLink()) continue;\n\n const full = join(dir, d.name);\n let target;\n try {\n // `stat`, which FOLLOWS the link — the one place in this file where\n // following is what we want, because the question is \"what is on the\n // other end\", not \"is something here\".\n target = await stat(full);\n } catch (e) {\n throw new Error(\n `${relPath(repoRoot, full)}: symlink cannot be resolved (${(e as Error).message}). ` +\n `A link we cannot follow may be a model directory — refusing to guess.`,\n { cause: e },\n );\n }\n if (target.isDirectory()) names.push(d.name);\n }\n return names.sort();\n}\n\n/**\n * Every model on disk: custom types from `customtypes/<id>/index.json`, slices\n * from `<library>/<Dir>/model.json` for each configured library.\n *\n * Slices are keyed by the model's own `id`, NOT the directory name — the two\n * differ across the fleet (gallerysonder's `ContentWidthMedia` holds\n * `video_block`), and keying on the directory would send Prismic ids it has\n * never seen.\n *\n * A subdirectory with no model file is skipped (a leftover component folder is\n * normal); anything present-but-unreadable throws. See {@link isProvenAbsent}\n * for why that line, and not \"any error means absent\", is where the skip stops.\n */\nexport async function localModels(repoRoot: string, libraries: string[]): Promise<LocalEntry[]> {\n const out: LocalEntry[] = [];\n\n const ctRoot = join(repoRoot, \"customtypes\");\n for (const d of await subdirs(repoRoot, ctRoot)) {\n const full = join(ctRoot, d, \"index.json\");\n if (await isProvenAbsent(full)) continue;\n out.push(await readModel(repoRoot, full, \"customtype\"));\n }\n\n // The same library listed twice — or listed as both \"./src/lib/slices\" and\n // \"src/lib/slices\" — is a config typo, not two libraries. Deduplicating on\n // the RESOLVED path keeps it a harmless wart (the same call as alamo-anatomy's\n // missing library) instead of a hard duplicate-id failure whose message would\n // nonsensically name one file as the collision partner of itself.\n for (const libRoot of new Set(libraries.map((lib) => join(repoRoot, lib)))) {\n for (const d of await subdirs(repoRoot, libRoot)) {\n const full = join(libRoot, d, \"model.json\");\n if (await isProvenAbsent(full)) continue;\n out.push(await readModel(repoRoot, full, \"slice\"));\n }\n }\n\n assertNoDuplicateIds(out);\n assertVariationsHaveIds(out);\n return out;\n}\n\n/**\n * Refuse a repo where two files of the SAME kind declare the same model id.\n *\n * This is possible on disk because a slice's directory name and its model `id`\n * are independent — gallerysonder's `ContentWidthMedia/` holds `video_block` —\n * so two directories can quietly claim one id. Undetected, it is a silent and\n * ARBITRARY push: `diffModels` processes both entries independently, so one can\n * land in `unchanged` while the other lands in `toUpdate` against the same\n * remote model, and the push sends whichever of the two directories sorts last.\n * (Sorting the listing made that stable, not correct — a stable coin flip is\n * still a coin flip, and nothing would tell the operator the other file was\n * ignored.) That is the silent-divergence class this module exists to prevent, so\n * it fails loudly here instead of being papered over in `diffModels` — the\n * collision is a real problem in the repo and only a human can say which file\n * is the intended one.\n *\n * Same id across DIFFERENT kinds is legal (custom types and slices are separate\n * Types API collections, and `diffModels` keys on kind+id), so the check is\n * scoped by kind.\n */\nfunction assertNoDuplicateIds(entries: LocalEntry[]): void {\n const seen = new Map<string, string>();\n for (const e of entries) {\n const k = `${e.kind}:${e.id}`;\n const first = seen.get(k);\n if (first !== undefined) {\n throw new Error(\n `duplicate ${e.kind} id \"${e.id}\": declared by both ${first} and ${e.path}. ` +\n `Two files cannot own one model — delete or rename one.`,\n );\n }\n seen.set(k, e.path);\n }\n}\n\n/**\n * Refuse a slice model whose variation lacks a string `id`.\n *\n * The Types API cannot accept such a model, so this is a load-time rejection of\n * something that would fail on push anyway — but WHERE it fails is the whole\n * value: here it names the file and the index, while on push Prismic answers\n * with an opaque 422 that names neither, against a repo the operator may not\n * have knowingly touched.\n *\n * It does NOT exist to protect `describeDiff`, which is safe on its own:\n * `diff.ts` keys an id-less variation by a positional `variations[i]` fallback\n * that is unique per side, precisely so two of them cannot collide there.\n *\n * Measured 2026-08-12: all 180 variations in the fleet's 132 slice models carry\n * an `id`, so this is defensive, not a live repair.\n *\n * That count — and every fleet count in this directory — is taken from each\n * repo's DEFAULT BRANCH (`origin/HEAD`) via git plumbing, never from the local\n * working tree. Six of the fifteen checkouts sit on feature branches at any\n * given moment, and one of them gained a commit mid-measurement, so a\n * working-tree count is not reproducible and drifted by 3 variations while\n * these very comments were being written. \"In scope\" excludes the starter\n * repos, whose sentinel `repositoryName` makes `readPrismicConfig` return null:\n * this pipeline never loads their models, so counting them would describe files\n * nothing reads.\n *\n * A model with no `variations` array is untouched — custom types have none at\n * all, so anything stricter here would reject every custom type in the fleet.\n */\nfunction assertVariationsHaveIds(entries: LocalEntry[]): void {\n for (const e of entries) {\n const variations = e.model.variations;\n if (!Array.isArray(variations)) continue;\n variations.forEach((v, i) => {\n const id = (v as { id?: unknown } | null)?.id;\n if (typeof id !== \"string\" || id === \"\") {\n throw new Error(\n `${e.path}: variation at index ${i} has no string \"id\". ` +\n `Prismic rejects such a model, and the diff cannot describe it.`,\n );\n }\n });\n }\n}\n","// The Prismic Custom Types API — the capability this whole design rests on, and\n// the one that was believed impossible. Proven 2026-08-12: GET /customtypes and\n// GET /slices returned 200 across five fleet repositories, POST\n// /customtypes/insert returned 201 on the-pinnacle, and the historical 403 that\n// produced the standing \"types can only push via Slice Machine\" rule reproduced\n// on exactly ONE stale token (gallerysonder / reddoor-la) — a property of that\n// credential, not of the API.\n//\n// There is deliberately NO delete function in this module. The API supports\n// DELETE /customtypes/{id} (verified 204, zero residue), but a stale local\n// checkout must never be able to remove a live content model, and the surest\n// way to guarantee that is for the capability not to exist in the code at all.\n// Deleting a model is an operator action taken in the Prismic dashboard.\n//\n// THE RULE FOR EVERY EDIT TO THIS FILE: no failure may become an empty array.\n// `remoteModels` returning [] means \"this Prismic repository has no models\",\n// which sends every local model to `toCreate`, empties `remoteOnly` so the\n// never-delete invariant guards nothing, and in --apply pushes the entire model\n// set at a repository that already holds good models. An empty result is a real\n// state (a brand-new repository) and must be reachable ONLY through a\n// successful call that genuinely returned nothing.\n//\n// Every `catch` below rethrows, with ONE deliberate exception: `errorBody`\n// returns a placeholder string when the response body cannot be read. It is\n// named here rather than left for a reader to discover, because this paragraph\n// is the rule future editors will apply and an unnamed exception reads as a\n// violation. It is safe for a reason that does not generalise: it runs only on\n// paths that are ALREADY throwing, so the worst it can do is degrade a message.\n// A catch that can influence what this module RETURNS has no such excuse.\nimport type { LocalEntry, ModelKind, PrismicModel, RemoteEntry } from \"./types.js\";\n\nexport const CUSTOM_TYPES_API = \"https://customtypes.prismic.io\";\n\n/** Types API collection path for each model kind. */\nconst COLLECTION: Record<ModelKind, string> = { customtype: \"customtypes\", slice: \"slices\" };\n\n/**\n * Per-request deadline. 15 s matches the two other JSON API clients here —\n * `src/audits/netlify-deploy.ts`'s `NETLIFY_FETCH_TIMEOUT_MS` and\n * `src/github/gh-rest.ts`'s `DEPENDABOT_FETCH_TIMEOUT_MS`. It is NOT a\n * fleet-wide constant: of the nine other `AbortSignal.timeout` call sites across\n * five files, seven use 10 s (`src/audits/browser.ts` ×5,\n * `src/audits/function-health.ts`, `src/audits/form-e2e.ts`) and only those two\n * named above use 15 s. Each is set for its own workload. 15 s is chosen here\n * because a model collection is small JSON, not because 15 is the house number.\n *\n * It is here because a hung connection is the ONLY failure mode with no error\n * to propagate, so it is the only one that would sit in a fleet sweep instead of\n * failing it. `fetch` takes no deadline of its own — `RequestInit` carries a\n * `signal` and nothing else — so the only bound on an unsignalled call is\n * undici's dispatcher-level `headersTimeout`, which defaults to 300 s. That is\n * a bound, but at fleet scale it is a sweep-killer rather than a safety net.\n *\n * A timeout on a WRITE is deliberately not set tight: it leaves an ambiguous\n * state, because Prismic may have applied the change before the deadline fired.\n * That ambiguity is recoverable — the next run re-diffs and sends an `update` —\n * but it is worth not provoking.\n */\nconst PRISMIC_TIMEOUT_MS = 15_000;\n\nconst authHeaders = (repo: string, token: string): Record<string, string> => ({\n repository: repo,\n Authorization: `Bearer ${token}`,\n});\n\n/** The name JSON gives the value, for an error message. `typeof null` is\n * \"object\", which would report a null body as an object and send an operator\n * looking for a key that was never there. Same shape as `local.ts` /\n * `config.ts` use for the identical question. */\nconst jsonTypeOf = (v: unknown): string =>\n v === null ? \"null\" : Array.isArray(v) ? \"an array\" : typeof v;\n\n/**\n * The response body as text, for an ERROR message only.\n *\n * This is the one `catch` in the file that returns a value instead of throwing,\n * and it is safe for a reason that must survive future edits: it runs only on\n * paths that are already throwing, so the worst it can do is degrade a message.\n * It exists because `await res.text()` inline in the `new Error(...)` argument\n * would, if the body stream itself failed, reject out of the throw expression —\n * discarding the status we went to the trouble of capturing and replacing a\n * \"403 on repository X\" with an opaque \"terminated\".\n */\nconst errorBody = async (res: Response, limit: number): Promise<string> =>\n res.text().then(\n (t) => t.slice(0, limit),\n (e: unknown) => `<response body unreadable: ${String(e)}>`,\n );\n\n/**\n * One fetch with a deadline attached, rethrowing with context.\n *\n * RETHROWS, never defaults — see the file header. The wrap exists because the\n * two errors that reach it name neither the repository nor the URL: undici's\n * network failure is the bare string \"fetch failed\", and an aborted request is\n * \"The operation was aborted due to timeout\". In a fleet sweep the operator's\n * first question is \"which site?\", and neither message answers it.\n */\nasync function request(\n url: string,\n init: RequestInit,\n repo: string,\n fetchImpl: typeof fetch,\n timeoutMs: number,\n): Promise<Response> {\n try {\n return await fetchImpl(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });\n } catch (e) {\n const what = `${init.method ?? \"GET\"} ${url} [repository: ${repo}]`;\n // `?.` and `instanceof`, not a bare `(e as Error).message`: a rejection is\n // not guaranteed to be an Error, and reading `.name` off a thrown `null`\n // would throw a second time FROM INSIDE this catch — replacing the real\n // failure with a bare TypeError naming nothing, which is the one outcome\n // this wrapper exists to prevent.\n const detail =\n (e as Error | null | undefined)?.name === \"TimeoutError\"\n ? `timed out after ${timeoutMs}ms`\n : `failed: ${e instanceof Error ? e.message : String(e)}`;\n throw new Error(`${what} ${detail}`, { cause: e });\n }\n}\n\nasync function getCollection(\n repo: string,\n token: string,\n kind: ModelKind,\n fetchImpl: typeof fetch,\n timeoutMs: number,\n): Promise<RemoteEntry[]> {\n const url = `${CUSTOM_TYPES_API}/${COLLECTION[kind]}`;\n const res = await request(url, { headers: authHeaders(repo, token) }, repo, fetchImpl, timeoutMs);\n if (!res.ok) {\n // EVERY non-2xx, 404 included. The plan's discrimination rule — \"404 is the\n // only status that means this model does not exist\" — is about the\n // SINGLE-model endpoint, which this module never calls. On a COLLECTION GET\n // a 404 means the repository or the route is not there, which is the\n // opposite of \"the repository has no models\": a typo'd repositoryName would\n // read as an empty Prismic and push every local model. Do not add a 404\n // branch here.\n //\n // Name the status AND the repository: the one failure mode seen in the wild\n // is a stale/wrong token, and \"403\" alone is what got generalised last time\n // into \"the API does not allow this\". The status is also attached to the\n // Error object itself (not just interpolated into the message) so a caller\n // can tell a dead token (401/403 — fix the secret) apart from a rejected\n // model (422 — fix the model) without parsing text. Same idiom used\n // elsewhere in this codebase: `Object.assign(new Error(...), { exitCode })`.\n throw Object.assign(\n new Error(`GET ${url} [repository: ${repo}] -> ${res.status} ${await errorBody(res, 200)}`),\n { status: res.status },\n );\n }\n // Read the text, then parse it — rather than `res.json()`, which conflates two\n // different failures under one message. A body that fails MID-STREAM is a\n // transport fault, not malformed JSON, and calling it a parse error sends the\n // operator to look at Prismic's response instead of the connection. Reading\n // once also means the body is never consumed twice: `res.text()` after a\n // failed `res.json()` throws on the second read and would replace the real\n // diagnosis with \"Body is unusable\". Same read-then-parse shape as\n // `local.ts`'s `readModel`.\n // EVERY throw from here down names the repository. The URL cannot identify\n // the site — it is the same string for all of them, because the repository\n // travels in a HEADER (see `authHeaders`) — so without this a fleet sweep\n // reports \"expected an array, got null\" with no way to tell which of the\n // fleet's sites produced it.\n const where = `GET ${url} [repository: ${repo}]`;\n let raw: string;\n try {\n raw = await res.text();\n } catch (e) {\n throw new Error(`${where}: ${res.status} but the body could not be read`, { cause: e });\n }\n // A 200 whose body is not JSON (an HTML error page from a proxy, a truncated\n // response) must not surface as an opaque SyntaxError with no context. Wrapped\n // with the first of the body, never caught-and-defaulted — see the\n // absent-vs-unreadable rule at the top of the plan.\n let body: unknown;\n try {\n body = JSON.parse(raw);\n } catch (e) {\n // `res.status`, not a hardcoded 200: this branch is reached on ANY 2xx, and\n // a message naming a status that was not received sends an operator looking\n // for a response that never existed.\n throw new Error(\n `${where}: ${res.status} but the body did not parse as JSON: ${raw.slice(0, 200)}`,\n { cause: e },\n );\n }\n if (!Array.isArray(body)) {\n throw new Error(`${where}: expected an array, got ${jsonTypeOf(body)}`);\n }\n return body.map((m, i) => {\n // THROW, never drop. Dropping an id-less remote entry is the absent-vs-\n // unreadable collapse pointed at the remote set, and it fails in the\n // destructive direction: the local counterpart then looks like `toCreate`\n // and --apply pushes over a model Prismic already holds, while an entry with\n // no local counterpart never reaches `remoteOnly` at all — invisible to the\n // one bucket that exists to report Prismic-only models.\n //\n // The trade is deliberate: a malformed entry fails this site's whole check\n // rather than silently corrupting it. If Prismic ever returns such an\n // entry, that is a real event an operator needs to see, not one to route\n // around.\n const id = (m as { id?: unknown } | null)?.id;\n if (typeof id !== \"string\" || id === \"\") {\n throw new Error(`${where}: entry at index ${i} has no string \"id\"`);\n }\n return { kind, id, model: m as PrismicModel };\n });\n}\n\n/** Every model registered in one Prismic repository, both collections.\n *\n * The two GETs are independent and run SEQUENTIALLY on purpose. `Promise.all`\n * would halve the per-repository wall clock, but at two small JSON requests per\n * site that is noise inside a nightly sweep that also clones and audits — and\n * the cost is not noise: when a failure hits BOTH collections (a dead token,\n * the case actually seen in the wild) the reported error becomes whichever\n * request lost the race, so the same broken credential reports a different\n * message run to run. Deterministic attribution is worth more here than the\n * seconds. Do not \"optimise\" this without a measurement showing the sweep is\n * actually bounded by it. */\nexport async function remoteModels(\n repo: string,\n token: string,\n fetchImpl: typeof fetch = fetch,\n timeoutMs: number = PRISMIC_TIMEOUT_MS,\n): Promise<RemoteEntry[]> {\n const customtypes = await getCollection(repo, token, \"customtype\", fetchImpl, timeoutMs);\n const slices = await getCollection(repo, token, \"slice\", fetchImpl, timeoutMs);\n return [...customtypes, ...slices];\n}\n\n/** Create or REPLACE one model in Prismic. `update` replaces the whole model —\n * which is why callers must send `withRemoteScreenshots(local, remote)` rather\n * than the raw file on disk.\n *\n * This function has no idea whether the model differs from what Prismic holds;\n * it sends whatever it is given. \"Never send an unchanged model\" is the\n * caller's invariant (`pushModels`), enforced by diffing first. */\nexport async function sendModel(\n repo: string,\n token: string,\n entry: Pick<LocalEntry, \"kind\" | \"id\" | \"model\">,\n action: \"insert\" | \"update\",\n fetchImpl: typeof fetch = fetch,\n timeoutMs: number = PRISMIC_TIMEOUT_MS,\n): Promise<void> {\n // Prismic keys the write on the id inside the BODY; `entry.id` only ever\n // reaches the error message and the push report. If the two disagreed, an\n // `update` would replace a different live model than every log line names.\n // `types.ts` says they cannot disagree — both constructors derive `id` from\n // the parsed model — but this is the point of no return, and the parameter\n // type is structural, so any caller can hand over a pair that was never built\n // by either constructor. Check it here rather than trust it.\n // `=== \"\"` is checked separately from the mismatch, and it is not redundant:\n // an entry with `id: \"\"` AND `model.id: \"\"` AGREES, so the mismatch test\n // alone waves it through and Prismic is sent a model with no id. `local.ts`\n // rejects an empty `model.id` on the way in for the same reason — but the\n // argument three lines up is that this parameter is structural and a caller\n // can hand-build a pair that never passed through `local.ts` at all, which\n // applies to the empty string exactly as much as to the mismatch.\n const bodyId = (entry.model as { id?: unknown }).id;\n if (typeof bodyId !== \"string\" || bodyId === \"\" || bodyId !== entry.id) {\n throw new Error(\n `refusing to ${action} ${entry.kind} \"${entry.id}\": the model body's id is ` +\n `${JSON.stringify(bodyId)}. Prismic writes the body's id, not the reported ` +\n `one, so this would modify the wrong model or none at all.`,\n );\n }\n const url = `${CUSTOM_TYPES_API}/${COLLECTION[entry.kind]}/${action}`;\n const res = await request(\n url,\n {\n method: \"POST\",\n headers: { ...authHeaders(repo, token), \"Content-Type\": \"application/json\" },\n body: JSON.stringify(entry.model),\n },\n repo,\n fetchImpl,\n timeoutMs,\n );\n if (!res.ok) {\n // Same reasoning as getCollection above: attach `status` to the Error so\n // pushModels can distinguish a dead write token from a bad model.\n throw Object.assign(\n new Error(\n `POST ${url} [${entry.kind} ${entry.id}] -> ${res.status} ${await errorBody(res, 300)}`,\n ),\n { status: res.status },\n );\n }\n}\n","// The one place a diff turns into writes, and therefore the one place the\n// design's safety properties have to be true rather than merely intended.\n//\n// TWO INVARIANTS, both proven by mutation in tests/prismic/models/push.test.ts\n// rather than asserted here:\n//\n// 1. NOTHING is ever sent for a `remoteOnly` model. A model Prismic holds and\n// the repo does not is REPORTED — onto `report.remoteOnlyReported`, by\n// identity only — and never acted on, because a stale local checkout must\n// never be able to remove a live content model. This module cannot delete\n// even if it wanted to — `remote.ts` has no delete function at all — but\n// \"no delete verb downstream\" is not the same claim as \"this module never\n// touches a remote-only model\", so it gets its own guard. Copying the ids\n// into the report is a READ of that bucket and does not weaken the\n// invariant, which is about what goes on the WIRE; the guard is stated\n// behaviourally (\"`send` was never called\") for exactly that reason.\n// 2. NOTHING is ever sent for an `unchanged` model. That is not an\n// optimisation. `withRemoteScreenshots` rewrites `variations[].imageUrl`\n// on models `canon()` calls identical, so routing `unchanged` through the\n// send path would rewrite every slice in the fleet on every run — a\n// permanent stream of no-op writes against live repositories.\n//\n// Both hold because `work` below is built from exactly two of the diff's four\n// buckets. The other two are absent by construction, not filtered out later.\nimport { withRemoteScreenshots } from \"./diff.js\";\nimport type { LocalEntry, ModelDiff, PrismicModel, PushReport } from \"./types.js\";\n\n/**\n * Sends one model. Injected so `pushModels` stays testable and so the fetch\n * layer, not this one, owns credentials — nothing here knows a repository name\n * or a token, which is also what keeps this module's only channel to Prismic\n * visible in its signature.\n *\n * `remote` is the model Prismic currently holds, `undefined` on an insert. It\n * arrives ALONGSIDE the already-merged entry rather than instead of it: the\n * entry's model has been through `withRemoteScreenshots` and is what goes on\n * the wire, while `remote` is the before-picture a sender can hand to\n * `describeDiff` for a log line. A sender must never put `remote` on the wire.\n */\nexport type SendFn = (\n entry: LocalEntry,\n remote: PrismicModel | undefined,\n action: \"insert\" | \"update\",\n) => Promise<void>;\n\nexport type PushOptions = {\n /** false = dry run: compute and report, send nothing. */\n apply: boolean;\n send: SendFn;\n /**\n * There is no `true`. The type is `false | undefined` on purpose: a stale\n * local checkout must never be able to remove a live content model, and the\n * compiler is a better guarantee of that than a comment. `remoteOnly` models\n * are reported for a human; deleting one is a dashboard action.\n */\n allowDelete?: false;\n};\n\n/**\n * The HTTP status carried by a rejection, or undefined when there is none.\n *\n * The property READ is inside the try, not merely the cast around it, because a\n * getter can throw: `Object.assign(new Error(), { get status() { throw … } })`\n * is a perfectly ordinary object to build. `sendModel` never produces one — it\n * throws an Error with a numeric `status` — but `send` is INJECTED, and the\n * population this module can promise nothing about (a retry wrapper, a batching\n * decorator, a test double) is precisely the population that hand-builds error\n * objects. Not hypothetical: before this existed, a throwing `status` getter\n * escaped `pushModels` entirely — pinned now by \"survives a thrown value whose\n * accessors throw\" in tests/prismic/models/push.test.ts.\n *\n * This is one of the two `catch`es in this file that SWALLOW rather than\n * rethrow, and they are named here rather than left to be discovered, on the\n * rule remote.ts states for `errorBody`: both run ONLY on a path that is already\n * reporting a failure, so the worst either can do is degrade a message. Throwing\n * from here would instead abandon the entire report — including the `sent` list\n * of models Prismic has ALREADY accepted — which is strictly worse than losing\n * one status field. A catch that can influence what this module returns for a\n * SUCCESSFUL model would have no such excuse; there is none.\n */\nconst statusOf = (e: unknown): number | undefined => {\n try {\n const s = (e as { status?: unknown } | null | undefined)?.status;\n return typeof s === \"number\" ? s : undefined;\n } catch {\n // A throwing getter tells us nothing about the status, which is exactly\n // what `undefined` means here.\n return undefined;\n }\n};\n\n/**\n * A rejection rendered for the report's `error` field, for ANY thrown value.\n *\n * Every obvious step can throw for a value that is neither null nor undefined,\n * and each shape below was run against the old one-line expression to confirm it\n * really escaped: `Object.create(null)` has no `toString` at all, so `String(e)`\n * throws \"Cannot convert object to primitive value\"; an object can define a\n * `toString` that throws; an Error can define a `message` getter that throws —\n * and `String(e)` on THAT one throws too, because `Error.prototype.toString`\n * reads `message`. Hence the placeholder at the end: it is reachable, not\n * decorative, and all four are pinned by the `it.each` in push.test.ts.\n *\n * See `statusOf` above for why swallowing is the right call in this one place.\n */\nconst messageOf = (e: unknown): string => {\n try {\n if (e instanceof Error) {\n const m = e.message;\n if (typeof m === \"string\") return m;\n }\n } catch {\n // A throwing `message` getter. Fall through — `String(e)` will very likely\n // throw as well, and the placeholder below is the honest answer.\n }\n try {\n return String(e);\n } catch {\n // No `toString`, or one that throws. Say so, rather than losing the report.\n return `<a thrown ${typeof e} that cannot be converted to a string>`;\n }\n};\n\n/**\n * Apply a diff to Prismic.\n *\n * ORDER MATTERS: every slice goes before every custom type. A custom type's\n * `Slices` field references slice ids by name, and Prismic rejects a type\n * referencing a slice it does not yet hold — the-tower-burbank's reconciliation\n * pushed 11 slices before 6 types for exactly this reason. It is not a\n * theoretical dependency: 23 of the fleet's 68 in-scope custom types declare a\n * non-empty slice zone, and every one of the 115 distinct slice ids they name\n * (counted per repo) resolves to a slice model in that SAME repo — so on a first\n * push, all 23 depend on slices landing first. Measured 2026-08-12 from each\n * repo's default branch via `git ls-tree`, never a working tree: eight of the\n * fifteen checkouts were on feature branches at the time, and they move.\n *\n * NO ORDER IS ATTEMPTED AMONG CUSTOM TYPES, and that is deliberate rather than\n * an oversight: 7 of the 68 carry a Link field constrained to a custom type, and\n * TWO of those point at THEMSELVES — reddoor-website's `project` and\n * vineyard-custom-homes' `project`, each through two separate fields. A\n * self-reference has no valid creation order, so no total order over custom\n * types exists in general — if Prismic ever does reject such an insert, no sort\n * could have saved it, and it surfaces as a `failed` entry naming the type\n * rather than as silence.\n *\n * The count is the less useful half of that. Both self-references are the\n * `project` model, which sites copy from one another, so the open question is\n * not \"will a second site ever do this\" — a second site already has — but what\n * this pipeline does the first time Prismic rejects one. (Re-measured 2026-08-12\n * under the same rule as the paragraph above, every repo's default branch\n * resolving to `origin/main`. An earlier revision of this comment said \"one\",\n * having counted reddoor-website and stopped.)\n *\n * FAILURE IS PER-MODEL: one rejected model is recorded and the run continues,\n * so a single bad model cannot silently strand the rest of a merge's changes\n * half-applied with no report of which half. A 401/403 is deliberately NOT\n * treated as fatal-for-the-run even though it certainly is one in practice: an\n * early exit would need a third bucket for \"not attempted\", and a `sent`/`failed`\n * pair that silently omits models is precisely the kind of report this pipeline\n * exists to stop producing. The cost is N identical 401 lines on a dead token —\n * noisy, honest, and `status` (below) says which of the two it is at a glance.\n */\nexport async function pushModels(diff: ModelDiff, opts: PushOptions): Promise<PushReport> {\n // Two buckets, never four. See invariants 1 and 2 in the file header — this\n // list IS the enforcement. The type system helps with one of them: a\n // `remoteOnly` entry is a `RemoteEntry` and has no `path`, so it cannot be\n // spread in here without someone fabricating one, which makes the mistake\n // conspicuous rather than impossible (the mutation that proves the guard fires\n // wrote `path: \"\"` and compiled). `unchanged` gets no help at all — it is a\n // `LocalEntry[]` and would slot straight in. Both are pinned by tests.\n const work: Array<{\n entry: LocalEntry;\n remote: PrismicModel | undefined;\n action: \"insert\" | \"update\";\n }> = [\n ...diff.toCreate.map((entry) => ({ entry, remote: undefined, action: \"insert\" as const })),\n ...diff.toUpdate.map(({ local, remote }) => ({\n entry: local,\n remote: remote.model,\n action: \"update\" as const,\n })),\n ];\n // Slices first — see the ORDER MATTERS note above. Returning 0 for a same-kind\n // pair leans on `sort` being stable (required since ES2019), which is what\n // keeps inserts ahead of updates within a kind and keeps the report's order\n // reproducible run to run.\n work.sort((a, b) => (a.entry.kind === b.entry.kind ? 0 : a.entry.kind === \"slice\" ? -1 : 1));\n\n // `mode` is the MODE, not the verdict. On a dry run `sent` is populated with\n // what WOULD go on the wire and nothing is sent, so no reader may treat `sent`\n // as \"Prismic accepted these\" without checking `mode` first.\n const report: PushReport = {\n mode: opts.apply ? \"apply\" : \"dry\",\n sent: [],\n failed: [],\n // Filled BEFORE the loop and identically in both modes, because it is a fact\n // about the COMPARISON rather than about what this run did — and because it\n // is the only thing that keeps an empty report from being ambiguous between\n // \"in sync\" and \"this checkout can no longer see its own slices\". See\n // `PushReport.remoteOnlyReported` for the failure it exists to name.\n remoteOnlyReported: diff.remoteOnly.map((e) => ({ kind: e.kind, id: e.id })),\n };\n\n for (const { entry, remote, action } of work) {\n if (!opts.apply) {\n report.sent.push({ kind: entry.kind, id: entry.id, action });\n continue;\n }\n try {\n // The merge is INSIDE the send path, not a caller's job: a push REPLACES\n // the whole model, and 9 of the fleet's 180 in-scope variations carry a\n // stale screenshot URL on disk (one in each of nine repos, measured\n // 2026-08-12). Sending the file verbatim would rot those previews in the\n // editor, and `canon()` drops `imageUrl` entirely — so the damage could\n // never show up as a diff on the next run either. Silent and permanent is\n // the combination this module is built to avoid.\n await opts.send(\n { ...entry, model: withRemoteScreenshots(entry.model, remote) },\n remote,\n action,\n );\n report.sent.push({ kind: entry.kind, id: entry.id, action });\n } catch (e) {\n // Carry the HTTP status (when there is one) so a caller can tell a dead\n // write token (401/403 — fix the secret) apart from a rejected model\n // (422 — fix the model). The nightly sweep must not conflate the two, and\n // Prismic's token expiry is undocumented, so 401 is not a rare branch.\n // `sendModel` attaches it to the Error object precisely so nobody has to\n // parse it back out of the message text.\n //\n // Read through `statusOf`/`messageOf`, never as `(e as Error).message`.\n // `send` is INJECTED, so this module can promise nothing about what it\n // throws — and EVERY step of the obvious version can itself throw, which\n // would escape `pushModels` and strand every model after this one with no\n // report at all, discarding even the `sent` list of models Prismic has\n // already accepted. That is far worse than the failure being reported. The\n // two helpers above enumerate the shapes and cite the verification.\n const status = statusOf(e);\n report.failed.push({\n kind: entry.kind,\n id: entry.id,\n error: messageOf(e),\n // Spread, so the key is ABSENT rather than present-and-undefined when\n // there was no status (a network failure has none). `PushReport` is\n // serialised into PR comments and Airtable; a blank `status` key reads\n // as \"there was one and it was empty\".\n ...(status === undefined ? {} : { status }),\n });\n }\n }\n return report;\n}\n","// The pull-down path: a model that exists ONLY in Prismic, brought into the repo\n// as a file. It is the safe answer to a `remoteOnly` model — the alternative\n// being deletion, which this design forbids outright — and it is the only module\n// here that writes into a LIVE CLIENT REPO. Human-invoked; never runs in CI.\n//\n// Which makes the failure to avoid the mirror image of the one the rest of this\n// module guards against. Everywhere else the question is \"did I really read\n// nothing?\"; here it is \"is this path really free?\" — and the two errors have\n// the same shape, because a write onto an occupied path destroys a file exactly\n// as silently as a failed read drops a model. Both answers must be PROVEN, never\n// assumed, and the proof is what `occupantId` below is.\nimport { mkdir, readFile, realpath, rename, writeFile } from \"node:fs/promises\";\nimport { dirname, join, posix, relative, sep } from \"node:path\";\nimport type { RemoteEntry } from \"./types.js\";\n\n/**\n * The ONLY capability this module takes for formatting — and it is deliberately\n * NOT a process spawner.\n *\n * `writeModelFile` used to take `SpawnFn`, which is an arbitrary-argv primitive:\n * with one in hand this module could run `rm -rf` over a live model, `git rm`\n * it, or `curl -X DELETE` at the Types API — none of which names a module, a\n * binding or an HTTP verb that a source guard can see. The guard's answer was a\n * sentinel counting direct calls to a callee spelled `spawn`, and a deny-list on\n * a SPELLING dies to `const run = spawn`, to a helper parameter, or to a\n * property; this file used the helper idiom three lines away.\n *\n * So the module holds the narrower thing instead: \"format these repo-relative\n * paths, in this repo, with this binary\". The argv is built by the caller's\n * binding — one line at the injection site — and this module never holds a\n * primitive that can run anything else. That removes the capability class rather\n * than narrowing the hole, which is why it was worth changing a signature for.\n *\n * Bind it with the recipe helper the fleet already uses:\n *\n * const format: FormatModelFile = (root, paths, opts) =>\n * formatWithPrettier(spawn, root, paths, opts);\n *\n * BEST-EFFORT BY CONTRACT, exactly like `formatWithPrettier`: resolve `false`,\n * never throw. `writeModelFile` defends against a binding that breaks that\n * anyway — the model is already on disk before this runs, and losing a pulled\n * model to a formatting failure is the worst outcome on offer.\n */\nexport type FormatModelFile = (\n repoRoot: string,\n relPaths: readonly string[],\n opts: { bin: string; timeoutMs: number },\n) => Promise<boolean>;\n\n/**\n * The characters a model id may contain, as an ALLOW-LIST.\n *\n * The id is the one value in this module that is not ours: it arrives from\n * Prismic and it is spliced straight into a filesystem path under a client\n * repo's root. A deny-list of the shapes that scare us (\"..\", \"/\") is the guard\n * that has failed three times in this project, defeated each time by a shape\n * nobody listed — so this enumerates what is permitted and refuses everything\n * else.\n *\n * Measured 2026-08-13 from every in-scope repo's `origin/main` via `git ls-tree`\n * (never a working tree — eight of the fifteen checkouts sit on feature branches\n * that move): all 200 model ids in the fleet — 68 custom types, 132 slices — use\n * only `[a-z_]`, with a single camelCase outlier (`contractorTestimonials`).\n * `-` and digits are permitted because Prismic accepts them and neither can\n * change what a path means; nothing else is.\n *\n * If Prismic ever accepts an id outside this set, this THROWS and the operator\n * creates the file by hand. That is a stop, not a silent misplacement, and a\n * stop is the correct outcome for \"I do not know where this belongs\".\n */\nconst ID_ALLOWED = /^[A-Za-z0-9_-]+$/;\n\n/**\n * A path segment we are willing to create inside a client repo, again as an\n * allow-list: it must START with a letter, a digit or an underscore.\n *\n * That one anchor is doing three separate jobs. It rejects `\"\"`, `.` and `..`\n * (so the returned path cannot climb out of the repo or address itself); it\n * rejects a leading `-`, which the target repo's prettier would read as a FLAG\n * rather than a filename; and it rejects a leading `.`, so a pull-down cannot\n * quietly populate a dot-directory nobody looks at in review.\n */\nconst SEGMENT_ALLOWED = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;\n\n/** `video_block` -> `VideoBlock`. Slice Machine's on-disk directory convention.\n * A model that exists only in Prismic has no local directory to reuse, so the\n * name has to be derived — and this is LOSSY (it drops every separator), which\n * is why {@link ID_ALLOWED} is applied to the raw id BEFORE this runs. Checking\n * only the output would wave `../../x` through as the innocuous `X`. */\nconst pascal = (id: string): string =>\n id\n .split(/[^A-Za-z0-9]+/)\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(\"\");\n\n/**\n * Repo-relative path a pulled-down model should live at.\n *\n * Custom types are keyed by id on disk — all 68 in the fleet sit in a directory\n * named exactly `model.id` — so that half is a fact. The slice half is a GUESS:\n * the directory name and the model id are independent, and 6 of the fleet's 132\n * slice directories differ from `pascal(id)` today (caltex-landing and hedloc\n * hold `content_width_media` in `ContentWidth/`; gallerysonder holds\n * `video_block` in `ContentWidthMedia/`). The guess is unavoidable — a\n * remote-only model has no local directory — but it must never be allowed to\n * land on another model's file, which is `writeModelFile`'s job, not this one's.\n *\n * The returned string has TWO readers with different rules: `join(repoRoot, …)`\n * resolves it against the repo, and the target repo's prettier resolves it\n * against its own cwd as an argv. They agree only for a plain relative path —\n * an absolute or climbing path makes the write and the format touch DIFFERENT\n * files, with both reporting success. Hence the per-segment check at the end.\n *\n * That check is a STRING-SHAPE check and nothing more. It proves the path spells\n * a plain relative location; it cannot prove where that location resolves to,\n * because a symlinked component (`customtypes`, or the slice library) redirects\n * the whole subtree without changing a single character of this string. The\n * containment guarantee for the write itself is {@link assertResolvesInsideRepo}\n * in `writeModelFile`, which resolves it.\n */\nexport function modelFilePath(entry: Pick<RemoteEntry, \"kind\" | \"id\">, library: string): string {\n if (!ID_ALLOWED.test(entry.id)) {\n throw new Error(\n `refusing to derive a file path for ${entry.kind} id ${JSON.stringify(entry.id)}: ` +\n `a model id may contain only letters, digits, \"_\" and \"-\". ` +\n `Create the file by hand rather than letting this guess where it goes.`,\n );\n }\n\n let rel: string;\n if (entry.kind === \"customtype\") {\n rel = posix.join(\"customtypes\", entry.id, \"index.json\");\n } else {\n const lib = library.replace(/^\\.\\//, \"\").split(sep).join(posix.sep).trim();\n if (lib === \"\") {\n throw new Error(\n `refusing to place slice \"${entry.id}\": no slice library was given. ` +\n `An empty library is not the fleet default — config.ts treats \"libraries: []\" as ` +\n `\"this site has no slice library\" — so there is nowhere to put this model.`,\n );\n }\n const dir = pascal(entry.id);\n if (dir === \"\") {\n throw new Error(\n `refusing to place slice ${JSON.stringify(entry.id)}: it has no letters or digits, ` +\n `so it yields no directory name.`,\n );\n }\n rel = posix.join(lib, dir, \"model.json\");\n }\n\n const bad = rel.split(posix.sep).filter((s) => !SEGMENT_ALLOWED.test(s));\n if (bad.length > 0) {\n throw new Error(\n `refusing to write ${entry.kind} \"${entry.id}\" to ${JSON.stringify(rel)}: ` +\n `the path segment(s) ${JSON.stringify(bad)} are not a plain relative location inside ` +\n `the repo. Check the \"libraries\" entry in this site's Prismic config.`,\n );\n }\n return rel;\n}\n\nexport type WriteResult = { path: string; formatted: boolean };\n\n/**\n * The model id declared by whatever already occupies `full`, or `null` when that\n * path is PROVEN free.\n *\n * The polarity is the same one local.ts states and for the same reason, pointed\n * the other way: only a clean ENOENT proves a path is free. EACCES, ENOTDIR\n * (a file where a directory should be), EISDIR, ELOOP and I/O errors all mean\n * something is there, or that we cannot tell — and \"cannot tell\" must not be\n * spelled the same way as \"nothing there\" when the next statement overwrites it.\n *\n * A file that is there but does not parse, or parses without a string `id`,\n * throws rather than being treated as free: an unidentifiable file is precisely\n * the one we must not clobber. There is no reading of \"I could not understand\n * this file\" that justifies replacing it.\n */\nasync function occupantId(rel: string, full: string): Promise<string | null> {\n let raw: string;\n try {\n raw = await readFile(full, \"utf-8\");\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw new Error(\n `${rel}: cannot be read, so this pipeline cannot tell whether a model is already ` +\n `there (${(e as Error).message}). Refusing to write over what it cannot see.`,\n { cause: e },\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n throw new Error(\n `${rel}: already exists and is not valid JSON (${(e as Error).message}). ` +\n `Refusing to overwrite a file this pipeline cannot identify.`,\n { cause: e },\n );\n }\n const id = (parsed as { id?: unknown } | null)?.id;\n if (typeof id !== \"string\" || id === \"\") {\n throw new Error(\n `${rel}: already exists but declares no string \"id\", so there is no way to tell ` +\n `whether it is the model being pulled down. Refusing to overwrite it.`,\n );\n }\n return id;\n}\n\n/**\n * `realpath` of the deepest ancestor of `p` that exists.\n *\n * Only ENOENT walks up — the same polarity `occupantId` uses, for the same\n * reason. EACCES, ELOOP (a symlink cycle) and ENOTDIR are \"I cannot tell where\n * this resolves\", and the answer to that must never be spelled the same way as\n * \"it resolves here\".\n */\nasync function realpathOfNearestExisting(p: string): Promise<string> {\n let cur = p;\n for (;;) {\n try {\n return await realpath(cur);\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== \"ENOENT\") {\n throw new Error(`${cur}: cannot be resolved (${(e as Error).message})`, { cause: e });\n }\n const parent = dirname(cur);\n /* c8 ignore next 2 -- unreachable: repoRoot is an ancestor and realpaths. */\n if (parent === cur) throw new Error(`${p}: no ancestor of this path exists`, { cause: e });\n cur = parent;\n }\n }\n}\n\n/**\n * Refuse unless `dir` really resolves inside `repoRoot`.\n *\n * `modelFilePath`'s per-segment check proves the path is spelled as a plain\n * relative location. It cannot prove where it LANDS: make `customtypes` — or a\n * site's slice library — a symlink to a sibling directory and every segment\n * still reads as innocuous, `readFile` answers ENOENT at the leaf so the path\n * reads as PROVEN free, `mkdir -p` follows the link, and the exclusive create\n * succeeds. The model lands in the sibling, which in this fleet is another LIVE\n * CLIENT REPO, and the returned repo-relative path is a lie.\n *\n * This is deliberately ASYMMETRIC with local.ts, which reads models through\n * symlinks by design and must keep doing so. Reading through a symlink returns\n * a file the operator put there; writing through one puts a file somewhere the\n * operator never named. The two directions do not deserve the same rule, and\n * this rule is only ever applied to the write path.\n *\n * Nothing under `customtypes/` or a slice library is a symlink in any of the 15\n * in-scope repos (measured 2026-08-13 from each repo's `origin/main` via\n * `git ls-tree`: zero entries of mode 120000), so this is a latent hole rather\n * than a live one — which is exactly when it is cheap to close.\n */\nasync function assertResolvesInsideRepo(repoRoot: string, rel: string, dir: string): Promise<void> {\n const realRoot = await realpathOfNearestExisting(repoRoot);\n const real = await realpathOfNearestExisting(dir);\n if (real !== realRoot && !real.startsWith(realRoot + sep)) {\n throw new Error(\n `refusing to write ${rel}: it resolves to ${real}, which is outside this repo ` +\n `(${realRoot}). A symlinked path component redirects the write while every segment ` +\n `of the repo-relative path still looks ordinary — and every sibling of a repo in ` +\n `this fleet is another live client repo.`,\n );\n }\n}\n\n/**\n * Absolute path to the TARGET repo's OWN prettier, or `null` when it has none.\n *\n * This exists because the alternative — spawn and let resolution sort it out —\n * cannot tell the two answers apart, and the whole justification for formatting\n * through the target's binary instead of `--stdin-filepath` is that it is the\n * TARGET's. Measured 2026-08-13, `pnpm exec prettier` in a fixture repo with no\n * `node_modules`:\n *\n * - it first runs a dependency check and executes a full `pnpm install` in the\n * target — an unrequested mutation of a live client repo, and the thing that\n * would hang a pull-down on a slow or unreachable registry;\n * - in a repo that does not depend on prettier, it then falls through to the\n * CALLING repo's `node_modules/.bin` (which `pnpm reddoor-maint …` puts on\n * PATH), formats the client's file with a binary the client does not have,\n * and exits 0 — so `formatted: true` reports a format the target never did.\n *\n * `node_modules/.bin/prettier` under the repo root is the right probe rather\n * than a resolve-from-`repoRoot`: Node's algorithm walks UP, which would find\n * the maintenance repo's own prettier for any clone checked out beneath it —\n * the precise confusion this is here to end. All 15 in-scope repos declare\n * prettier as a root devDependency and pnpm materialises the shim there\n * (measured 2026-08-13).\n *\n * Every failure collapses to `null`, including EACCES: unlike a read on the\n * model path, \"I cannot tell\" here costs only `formatted: false`, which is the\n * conservative flag and the documented degraded path.\n */\nasync function targetPrettierBin(repoRoot: string): Promise<string | null> {\n try {\n // realpath doubles as the existence probe, so this module does not need a\n // `stat` capability on top of the one it already has — and it resolves the\n // shim, so a DANGLING `.bin/prettier` reads as absent rather than present.\n return await realpath(join(repoRoot, \"node_modules\", \".bin\", \"prettier\"));\n } catch {\n return null;\n }\n}\n\n/**\n * How long the target's prettier gets before the pull-down gives up on it.\n *\n * A stalled format wedges a human-invoked CLI with nothing printed and no\n * `formatted: false` — the one outcome worse than not formatting. 60s is this\n * repo's established budget for a CLI spawn that may touch the network\n * (`gh.ts` ×7, `deps-outdated`); it is deliberately far above remote.ts's 15s\n * HTTP budget, because this is process startup rather than one request, and far\n * below the 5-minute install budgets, because it formats ONE small JSON file.\n *\n * A timeout also makes the fleet's default spawn implementation detach the\n * child (`src/audits/util/spawn.ts` only sets `detached` when `timeoutMs` is\n * present), so the kill reaches prettier and not just a wrapper. This module\n * does not hold that spawn — see {@link FormatModelFile} — but the number it\n * passes is what decides that behaviour at the injection site.\n */\nconst PRETTIER_TIMEOUT_MS = 60_000;\n\n/**\n * Write one model into a repo and format it with THAT repo's own prettier.\n *\n * The formatting step is not cosmetic. The first pull-down PR of the 2026-08-12\n * reconciliation failed CI on `prettier --check` for\n * `customtypes/frozen_page/index.json` while `catalog_page/index.json` —\n * generated by the identical code path in the same run — passed. Prettier's\n * output for JSON is CONTENT-dependent, so there is no canonical\n * `JSON.stringify` shape that is safe fleet-wide; only the target repo's own\n * prettier knows the answer, and a pull-down PR that imposes OUR house style\n * instead buries the real change in a diff full of reformatting noise.\n *\n * Which is why the target's prettier is resolved POSITIVELY, by\n * {@link targetPrettierBin}, and run by absolute path. Spawning `pnpm exec\n * prettier` and reading the exit code cannot distinguish \"the target's prettier\n * formatted this\" from \"the target has none and the calling repo's prettier\n * guessed\" — both are 0 — and the second answer is the one this whole deviation\n * from `--stdin-filepath` exists to avoid.\n *\n * Formatting is best-effort by contract ({@link FormatModelFile} never throws,\n * and this refuses to take its word for it — see the call), so\n * the file is always written; `formatted: false` tells the caller to flag the PR\n * for a manual format check rather than losing the model. It is one bit and it\n * deliberately merges three causes — the target has no prettier, its prettier\n * exited non-zero, or it timed out — because the operator's next action is\n * identical for all three and the CLI prints the same `PRETTIER_FLAG_NOTE`. The\n * consequence is bounded and visible downstream: an unformatted file reds the\n * target repo's own `prettier --check` in CI, which is how the trap above was\n * found in the first place.\n *\n * One residual is worth naming rather than papering over: prettier's own\n * `--write` is not atomic, so a format killed by the timeout can in principle\n * leave the model half-rewritten. The window is prettier's, not ours, and it\n * opens only after the complete model is already on disk — the stall this\n * guards against (process startup) is before prettier writes anything.\n *\n * NOTHING else here is best-effort. Every way this can fail to write the RIGHT\n * file at the RIGHT path throws, because a pull-down that silently writes the\n * wrong file is indistinguishable from one that worked.\n */\nexport async function writeModelFile(\n format: FormatModelFile,\n repoRoot: string,\n entry: RemoteEntry,\n library: string,\n): Promise<WriteResult> {\n // `entry.id` picks the PATH; `entry.model.id` is what every later reader keys\n // on (local.ts reads the body, never the directory name). remote.ts derives\n // both from the same parsed model so they cannot disagree — and `sendModel`\n // re-checks it anyway at the other end of the pipeline, because the parameter\n // is structural and any caller can hand over a pair that no constructor built.\n // The same argument holds here, with a worse consequence: a mismatch files the\n // model under a name nothing reads, so the next run sees it as remote-only\n // again and pulls a SECOND copy — a loop that never converges, in silence.\n const bodyId = (entry.model as { id?: unknown }).id;\n if (typeof bodyId !== \"string\" || bodyId === \"\" || bodyId !== entry.id) {\n throw new Error(\n `refusing to write ${entry.kind} \"${entry.id}\": the model body's id is ` +\n `${JSON.stringify(bodyId)}. The path is derived from one and every reader of the ` +\n `file keys on the other, so this model would never be found again.`,\n );\n }\n\n const rel = modelFilePath(entry, library);\n const full = join(repoRoot, rel);\n const dir = dirname(full);\n\n // FIRST, before anything reads or creates: prove this path resolves inside the\n // repo. Ordering it here means a symlinked component is reported as what it is\n // rather than as whatever the sibling repo happens to hold — and it means the\n // pull-down never so much as reads a file outside the repo it was pointed at.\n await assertResolvesInsideRepo(repoRoot, rel, dir);\n\n // Proven free, or occupied by this exact model. Anything else — another\n // model's file, a file we cannot parse, a path we cannot read — throws above.\n const occupant = await occupantId(rel, full);\n if (occupant !== null && occupant !== entry.id) {\n throw new Error(\n `refusing to pull ${entry.kind} \"${entry.id}\" into ${rel}: that file already holds ` +\n `\"${occupant}\". A slice's directory name and its model id are independent — 6 of the ` +\n `fleet's 132 slice directories differ (measured 2026-08-13) — so the directory this ` +\n `derives can belong to a different model. Move or rename the existing model, or ` +\n `create this one by hand.`,\n );\n }\n\n // If the write below fails, this directory stays behind empty — and so, on the\n // refresh path, may one partial temp file. Both are deliberate and neither is\n // worth \"fixing\": undoing them needs a delete verb, and this module imports\n // none. The pull-down is the safe answer to a model CI may never delete, so it\n // does not get the ability to remove things in order to tidy up after itself.\n // Both leftovers are visible in `git status`, named in the error that produced\n // them, read by nothing, and BOUNDED — one empty directory and one `.tmp` per\n // model path, however many times it fails, with the next attempt overwriting\n // the temp and a successful one consuming it. A delete verb in this file would\n // be none of those things.\n try {\n await mkdir(dir, { recursive: true });\n } catch (e) {\n throw new Error(\n `${posix.dirname(rel)}: cannot create the directory for this model ` +\n `(${(e as Error).message})`,\n { cause: e },\n );\n }\n\n // Again, now that the directory exists. The check above resolved the deepest\n // ancestor that existed AT THE TIME — `mkdir -p` follows symlinks, so that\n // check is what stops a link from putting a directory inside a sibling repo;\n // this one resolves the leaf that will actually be written into. `mkdir` only\n // ever creates real directories, so the two agree unless something changed\n // underneath us, and this is the answer that counts because it is the last one\n // before the write.\n await assertResolvesInsideRepo(repoRoot, rel, dir);\n\n // Two spaces + a trailing newline is a BASELINE, not an opinion: prettier\n // below is the authority, and this only decides what a file looks like when\n // prettier could not run. Measured 2026-08-13 from each of the 15 in-scope\n // repos' `origin/main` via `git ls-tree`: 180 of the 200 committed model\n // files are 2-space indented and only 20 are tab-indented (erp-industrial and\n // gallerysonder, the only two repos that set `useTabs: true`; the other 13\n // either set no `useTabs` or ship no prettier config at all, and prettier's\n // default is spaces). A tab baseline reds `prettier --check` in 13 of 15\n // repos on exactly the degraded path this baseline is for.\n const body = JSON.stringify(entry.model, null, 2) + \"\\n\";\n\n if (occupant === null) {\n try {\n // `wx` — an exclusive create, so the kernel rechecks atomically what\n // `occupantId` could only observe a moment ago. That closes two holes at\n // once, and the second is not hypothetical: a DANGLING symlink answers\n // ENOENT on read (local.ts documents the same lie from the reading side),\n // so without `wx` this would happily write the model through the link to\n // wherever it points — outside the repo, if the link says so.\n //\n // This path deliberately does NOT go through the temp+rename below.\n // `rename` overwrites unconditionally; there is no exclusive variant of\n // it, so routing the free path through a rename would trade a proven\n // guarantee (nothing is destroyed, ever) for a lesser one (a new file is\n // never partial). `link()` would give both — it fails EEXIST if the name\n // is taken — but only at the cost of an `unlink` to clear the temp, and a\n // delete verb in this module is precisely what the module-wide capability\n // guard in index.test.ts exists to forbid. The free path has nothing to destroy,\n // so it does not need to buy the protection twice.\n await writeFile(full, body, { encoding: \"utf-8\", flag: \"wx\" });\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"EEXIST\") {\n throw new Error(\n `${rel}: nothing readable was at this path a moment ago and something is there now — ` +\n `a dangling symlink, or a concurrent write. Refusing to overwrite it.`,\n { cause: e },\n );\n }\n throw e;\n }\n } else {\n // The sanctioned same-id refresh — the ONE path in this module that may\n // legitimately destroy bytes in a live client repo, which is why it is the\n // one path that must never destroy them by accident.\n //\n // A plain `w` opens with O_TRUNC: any failure between the truncate and the\n // last byte — ENOSPC, EDQUOT, EIO, or the operator interrupting this\n // human-invoked CLI — leaves the live model as a fragment that is not valid\n // JSON. That wreckage is self-latching, because the fragment is exactly\n // what `occupantId` reads on the retry, so the retry refuses too; and it is\n // indistinguishable from this module's other failures, every one of which\n // provably leaves the repo untouched.\n //\n // Writing a complete temp file first and renaming it over the target makes\n // the replacement a single atomic step: the model path holds the old model\n // or the new one, never a fragment. Same directory, so it is the same\n // filesystem and `rename` cannot fail with EXDEV.\n // ONE deterministic temp path per model, opened with `w`. Both halves of\n // that are load-bearing, because this module has no delete verb and so can\n // never tidy up after itself.\n //\n // Deterministic, because a per-run unique name (`…<pid>-<n>.tmp`) means every\n // interrupted write leaves ANOTHER untracked file in a live client repo,\n // forever — ten interruptions, ten files for a human to find. One name per\n // model path bounds the mess at one file no matter how often it fails.\n //\n // `w` and NOT `wx`, and this must not be \"hardened\" later: with a\n // deterministic name, `wx` would let a single leftover temp block this model\n // from ever being pulled down again until a human intervened, converting an\n // accumulation nuisance into a permanent hard stop. `w` overwrites the stale\n // temp instead, so the next attempt heals the mess — and a SUCCEEDING attempt\n // removes it outright, because `rename` consumes the source name. That is\n // cleanup without a delete verb, which is the whole trick.\n //\n // The trade being made explicit: `wx` would also stop a concurrent run from\n // clobbering an in-flight temp. That is not a real scenario here — this is a\n // human-invoked CLI against a single checkout — and `rename` is atomic\n // regardless, so the worst a concurrency loss could do is publish one of two\n // complete models rather than a fragment.\n const tmp = `${full}.tmp`;\n const tmpRel = relative(repoRoot, tmp);\n try {\n await writeFile(tmp, body, { encoding: \"utf-8\", flag: \"w\" });\n } catch (e) {\n throw new Error(\n `${rel}: could not stage the replacement model (${(e as Error).message}). ` +\n `The model already in the repo is UNTOUCHED. A partial ${tmpRel} may be left ` +\n `behind — the next pull-down of this model overwrites it, and nothing reads it.`,\n { cause: e },\n );\n }\n try {\n // `rename` replaces the NAME, so if the model path is a symlink (to\n // somewhere inside this repo — anywhere else was refused above) it is\n // replaced by a regular file rather than written through. No entry under\n // `customtypes/` or a slice library is a symlink in any in-scope repo\n // (measured 2026-08-13), and were one to appear the swap shows up in\n // `git diff` as a 120000→100644 mode change rather than silently.\n await rename(tmp, full);\n } catch (e) {\n throw new Error(\n `${rel}: could not replace the model with the staged copy ` +\n `(${(e as Error).message}). The model already in the repo is UNTOUCHED and the ` +\n `staged copy is at ${tmpRel} — the next pull-down of this model overwrites it, ` +\n `and nothing reads it.`,\n { cause: e },\n );\n }\n }\n\n // Resolved, never resolved-by-PATH: see targetPrettierBin. No prettier of its\n // own means the formatter is never invoked at all — this module refuses to\n // hand a path to something it did not find under the TARGET's root.\n const bin = await targetPrettierBin(repoRoot);\n if (bin === null) return { path: rel, formatted: false };\n try {\n return {\n path: rel,\n formatted: await format(repoRoot, [rel], { bin, timeoutMs: PRETTIER_TIMEOUT_MS }),\n };\n } catch {\n // {@link FormatModelFile} is best-effort BY CONTRACT, and this does not take\n // the injection site's word for it. The model is already on disk; rejecting\n // here would report a failed pull-down for a file that is sitting in the\n // repo — the silent-wrong-report failure this module is written against,\n // pointed at its own caller.\n return { path: rel, formatted: false };\n }\n}\n","/** Directory entries that are NOT evidence of a checked-out working tree. A\n * directory containing only these is git metadata (or Finder litter) with no\n * repository checked out into it — see {@link noWorkingTreeFailure}. */\nconst NOT_A_WORKING_TREE: ReadonlySet<string> = new Set([\".git\", \".DS_Store\"]);\n\n/**\n * POSITIVE EVIDENCE OF A CHECKOUT, or the named reason there is none.\n *\n * A `readdir` that succeeds proves the DIRECTORY is readable. It does not prove\n * anything was checked out into it, and every verdict a Prismic command draws is\n * drawn from what is NOT in that directory — so without this, \"I could not\n * establish a checkout\" comes out as \"this repo has no Prismic in it\".\n *\n * The gap that makes it severe is durability. `cloneIfNeeded` accepts ANY\n * non-empty directory at the site's path as that site's checkout\n * (clone-if-needed.ts:138), and `git clone` creates `.git` FIRST and fills the\n * working tree afterwards — so a clone killed in between (its own 5-minute\n * timeout, a cancelled job, a reboot, a full disk) leaves `<workdir>/<slug>/`\n * holding `.git` alone, with `origin` already pointing at the right repo. That\n * directory is non-empty, so no clone is ever attempted again; every config read\n * inside it is ENOENT, so `readPrismicConfig` returns null; and the site then\n * reports \"not a Prismic site — skipped\", exit 0, ON EVERY RUN AFTER THE FIRST.\n * One interrupted clone silently retires that site's drift alarm, and a fleet of\n * them is a green sweep. In the rollout command it is the same directory, one\n * decision earlier: the site is quietly never offered the delivery workflow.\n *\n * THE REAL CAUSE IS ONE LAYER DOWN and is deliberately not fixed here:\n * `cloneIfNeeded`'s \"non-empty directory ⇒ this is the checkout\" rule, and its\n * identity guard, which RETURNS SILENTLY when it cannot verify\n * (clone-if-needed.ts:100 — a directory with no git origin is treated as\n * verified). Both are shared by nine fleet commands, so changing them from one\n * command would be an unreviewed fleet-wide behaviour change; that deserves its\n * own task. This guard makes the Prismic commands honest meanwhile — the answer\n * it refuses to give is the DANGEROUS one (\"no Prismic here\"), never the safe\n * one.\n *\n * It lives in `util/` rather than in either command because two copies of a\n * guard are two chances to fix only one of them: `prismic-models` (which reads)\n * and `prismic-ci` (which opens a PR against a repo) reach the identical\n * directory by the identical route.\n *\n * `.DS_Store` sits alongside `.git` because a fleet workdir on darwin acquires\n * one from Finder, and it is no more a working tree than `.git` is.\n */\nexport function noWorkingTreeFailure(\n repoRoot: string,\n entries: readonly string[],\n noEffect: string,\n): string | null {\n if (!entries.every((e) => NOT_A_WORKING_TREE.has(e))) return null;\n return (\n `this checkout has no working tree: ${repoRoot} holds` +\n ` ${entries.length === 0 ? \"nothing at all\" : `only ${entries.join(\", \")}`}.` +\n ` ${noEffect} — this is NOT \"no Prismic in this repo\"; nothing at all was established` +\n ` about this repo. A git clone killed part-way leaves exactly this (a .git and no files),` +\n ` and a fleet workdir REUSES any non-empty directory as that site's checkout, so it will` +\n ` never re-clone by itself. Delete ${repoRoot} and re-run.`\n );\n}\n"],"mappings":"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","names":["readFile","join","readFile","join","posix","relative","sep"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  defaultSpawn
3
- } from "./chunk-HVOOCK6L.js";
3
+ } from "./chunk-4DHQCN5H.js";
4
4
 
5
5
  // src/github/gh.ts
6
6
  import { mkdtemp, rm, writeFile } from "fs/promises";
@@ -442,4 +442,4 @@ export {
442
442
  assertUrlSegment,
443
443
  makeGitHub
444
444
  };
445
- //# sourceMappingURL=chunk-YAMRVXOD.js.map
445
+ //# sourceMappingURL=chunk-VRWUL4TR.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  hasRealScores,
3
3
  lighthouseScoresFromResult
4
- } from "./chunk-LMCO7CYQ.js";
4
+ } from "./chunk-OQFOMKBP.js";
5
5
  import {
6
6
  detectAuditEvents
7
7
  } from "./chunk-5L3FPQIJ.js";
@@ -9,7 +9,7 @@ import {
9
9
  normalizeSecurityAdvisory,
10
10
  siteSlug,
11
11
  updateAuditFields
12
- } from "./chunk-YRM6EED7.js";
12
+ } from "./chunk-CLWNAMUX.js";
13
13
 
14
14
  // src/audits/a11y-airtable.ts
15
15
  function hasA11yCounts(result) {
@@ -315,4 +315,4 @@ export {
315
315
  formatFleetWriteSummary,
316
316
  writeFleetAuditsToAirtable
317
317
  };
318
- //# sourceMappingURL=chunk-2BHSGLWR.js.map
318
+ //# sourceMappingURL=chunk-XDCRVYFL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/audits/a11y-airtable.ts","../src/audits/deps-airtable.ts","../src/audits/security-airtable.ts","../src/audits/domain-airtable.ts","../src/audits/browser-airtable.ts","../src/audits/netlify-deploy-airtable.ts","../src/audits/function-health-airtable.ts","../src/audits/smoke-airtable.ts","../src/audits/form-e2e-airtable.ts","../src/audits/write-audits-to-airtable.ts"],"sourcesContent":["import type { AuditResult } from \"../types.js\";\n\ntype A11yDetails = {\n totalViolations: number;\n byImpact: Partial<Record<\"minor\" | \"moderate\" | \"serious\" | \"critical\", number>>;\n};\n\n/** True when an a11y AuditResult carries real counts worth persisting.\n * Mirrors the `hasRealScores` policy on lighthouse: write whenever real\n * data exists, regardless of status (a \"warn\" or \"fail\" with concrete\n * violation counts is exactly what the dashboard needs to track). */\nexport function hasA11yCounts(result: AuditResult): boolean {\n if (result.audit !== \"a11y\") return false;\n const details = result.details as A11yDetails | undefined;\n return typeof details?.totalViolations === \"number\";\n}\n\nexport function a11yCountsFromResult(result: AuditResult): { violations: number } {\n if (result.audit !== \"a11y\") {\n throw new Error(`Expected an 'a11y' AuditResult, got '${result.audit}'`);\n }\n const details = result.details as A11yDetails | undefined;\n return { violations: details?.totalViolations ?? 0 };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { DepsDetails, DepsDriftEntry } from \"./deps.js\";\n\n/** True when a deps AuditResult carries the structured details (an entries\n * array, per {@link DepsDetails}). */\nexport function hasDepsCounts(result: AuditResult): boolean {\n if (result.audit !== \"deps\") return false;\n const d = result.details as Partial<DepsDetails> | undefined;\n return Array.isArray(d?.entries);\n}\n\nexport function depsCountsFromResult(result: AuditResult): {\n drifted: number;\n majorBehind: number;\n outdated: number | null;\n majorOutdated: number | null;\n} {\n if (result.audit !== \"deps\") {\n throw new Error(`Expected a 'deps' AuditResult, got '${result.audit}'`);\n }\n const details = (result.details ?? {}) as Partial<DepsDetails>;\n const entries = (details.entries ?? []) as DepsDriftEntry[];\n // \"drifted\" = drift !== \"same\", which intentionally includes \"newer\" (ahead of\n // baseline) — parity with the deps audit summary text.\n const drifted = entries.filter((e) => e.drift !== \"same\").length;\n const majorBehind = entries.filter((e) => e.drift === \"major\").length;\n // The real installed-version drift, distinct from declared-range drift. Null\n // when the audit couldn't determine it (no/stale lockfile) — kept null (not 0)\n // so the dashboard shows \"—\" rather than a misleading \"clean\".\n const outdated = details.outdated?.outdated ?? null;\n // How many of those outdated installs are a *major* behind the registry's\n // latest — surfaced separately from the total so the cockpit can show\n // \"N major\" available, distinct from the baseline-drift \"major\" count. Shares\n // the outdated signal's null-when-undetermined semantics.\n const majorOutdated = details.outdated?.major ?? null;\n return { drifted, majorBehind, outdated, majorOutdated };\n}\n","import type { AuditResult } from \"../types.js\";\nimport { normalizeSecurityAdvisory, type SecurityAdvisory } from \"../reports/airtable/websites.js\";\n\ntype SecurityDetails = {\n counts: { low: number; moderate: number; high: number; critical: number };\n advisories?: unknown;\n};\n\n/** True when a security AuditResult carries a counts object. Skipped runs\n * (no pnpm + no npm) have no details and return false. */\nexport function hasSecurityCounts(result: AuditResult): boolean {\n if (result.audit !== \"security\") return false;\n const details = result.details as SecurityDetails | undefined;\n return !!details && typeof details.counts === \"object\";\n}\n\nexport function securityCountsFromResult(result: AuditResult): {\n critical: number;\n high: number;\n moderate: number;\n low: number;\n} {\n if (result.audit !== \"security\") {\n throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);\n }\n const details = result.details as SecurityDetails | undefined;\n const c = details?.counts ?? { low: 0, moderate: 0, high: 0, critical: 0 };\n return { critical: c.critical, high: c.high, moderate: c.moderate, low: c.low };\n}\n\n/** The known advisories behind the counts, normalized for persistence. Empty when the\n * run was clean or carried no advisory detail (npm/pnpm sometimes omit it). Malformed\n * entries are dropped via the shared `normalizeSecurityAdvisory`. */\nexport function advisoriesFromResult(result: AuditResult): SecurityAdvisory[] {\n if (result.audit !== \"security\") {\n throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);\n }\n const details = result.details as SecurityDetails | undefined;\n const raw = details?.advisories;\n if (!Array.isArray(raw)) return [];\n return raw.map(normalizeSecurityAdvisory).filter((a): a is SecurityAdvisory => a !== null);\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { DomainResult } from \"../reports/airtable/websites.js\";\n\ntype DomainDetails = { resolved: boolean; certDaysRemaining: number | null; checkedAt: string };\n\n/** True when an AuditResult is a `domain` audit carrying a usable details payload (i.e. it\n * actually ran — a \"skip\" for a site with no deployed URL has no details). */\nexport function hasDomainResult(result: AuditResult): boolean {\n if (result.audit !== \"domain\") return false;\n const d = result.details as DomainDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable domain result (cert days + checked-at) from a `domain` audit. */\nexport function domainResultFromAudit(result: AuditResult): DomainResult {\n if (result.audit !== \"domain\") {\n throw new Error(`Expected a 'domain' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as DomainDetails | undefined;\n return {\n certDaysRemaining: typeof d?.certDaysRemaining === \"number\" ? d.certDaysRemaining : null,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { BrowserAuditFields } from \"../reports/airtable/websites.js\";\n\ntype BrowserDetails = {\n desktopOk: boolean;\n mobileOk: boolean;\n linksOk: boolean;\n reachableOk: boolean;\n titleMetaOk: boolean;\n brokenLinks: number;\n checkedAt: string;\n};\n\n/** True when an AuditResult is a `browser` audit that actually ran (has a details payload — a\n * \"skip\" for a site with no deployed URL has none). */\nexport function hasBrowserResult(result: AuditResult): boolean {\n if (result.audit !== \"browser\") return false;\n const d = result.details as BrowserDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable browser verdicts from a `browser` audit. */\nexport function browserFieldsFromAudit(result: AuditResult): BrowserAuditFields {\n if (result.audit !== \"browser\") {\n throw new Error(`Expected a 'browser' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as BrowserDetails | undefined;\n return {\n desktopOk: d?.desktopOk === true,\n mobileOk: d?.mobileOk === true,\n linksOk: d?.linksOk === true,\n reachableOk: d?.reachableOk === true,\n titleMetaOk: d?.titleMetaOk === true,\n brokenLinks: typeof d?.brokenLinks === \"number\" ? d.brokenLinks : 0,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { NetlifyDeployResult } from \"../reports/airtable/websites.js\";\n\ntype NetlifyDeployDetails = {\n state: string | null;\n deployedAt: string | null;\n logUrl: string | null;\n checkedAt: string;\n};\n\n/** True when an AuditResult is a `netlify-deploy` audit carrying a usable details\n * payload (i.e. it actually ran — a \"skip\" for a site with no Netlify id / no\n * token has no details). */\nexport function hasNetlifyDeployResult(result: AuditResult): boolean {\n if (result.audit !== \"netlify-deploy\") return false;\n const d = result.details as NetlifyDeployDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable deploy result (state + deployedAt + logUrl +\n * checkedAt) from a `netlify-deploy` audit. */\nexport function netlifyDeployResultFromAudit(result: AuditResult): NetlifyDeployResult {\n if (result.audit !== \"netlify-deploy\") {\n throw new Error(`Expected a 'netlify-deploy' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as NetlifyDeployDetails | undefined;\n return {\n state: typeof d?.state === \"string\" ? d.state : null,\n deployedAt: typeof d?.deployedAt === \"string\" ? d.deployedAt : null,\n logUrl: typeof d?.logUrl === \"string\" ? d.logUrl : null,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { FunctionHealthResult } from \"../reports/airtable/websites.js\";\n\ntype FunctionHealthDetails = {\n ok: boolean;\n prismic: \"ok\" | \"error\" | \"skipped\" | null;\n forms: unknown;\n checkedAt: string;\n};\n\n/** True when an AuditResult is a `function-health` audit carrying a usable details payload (i.e. it\n * actually ran — a self-skip for an unreachable / non-JSON `/health`, or a site with no deployed\n * URL, has no details, so the writer preserves the prior verdict). */\nexport function hasFunctionHealthResult(result: AuditResult): boolean {\n if (result.audit !== \"function-health\") return false;\n const d = result.details as FunctionHealthDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable function-health verdicts. `functionHealth` from `ok`; `cmsReachable`\n * from the same body's `prismic` sub-status (R2.2): `\"ok\"` → `\"pass\"`, `\"error\"` → `\"fail\"`, and\n * anything else (`\"skipped\"` — a placeholder repo, or a raw `null` from a \"deployed but erroring\"\n * body / an unrecognized value) → `null` — the CMS probe never actually ran, so it must not red CMS\n * reachability for a site that simply hasn't wired Prismic yet. Only `functionHealth` keys off\n * `ok`. */\nexport function functionHealthResultFromAudit(result: AuditResult): FunctionHealthResult {\n if (result.audit !== \"function-health\") {\n throw new Error(`Expected a 'function-health' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as FunctionHealthDetails | undefined;\n const cmsReachable: \"pass\" | \"fail\" | null =\n d?.prismic === \"ok\" ? \"pass\" : d?.prismic === \"error\" ? \"fail\" : null;\n // `forms` is untyped on the details payload (unknown) — validate defensively. A boolean\n // `turnstile` inside an object → concrete verdict; anything else (null forms from an older\n // site package or a \"deployed but erroring\" body, a malformed shape) → null: the widget\n // state is simply unknown this run, never a fail. Powers the Require-Turnstile guardrail.\n const formsRaw = d?.forms;\n const turnstileFlag =\n formsRaw && typeof formsRaw === \"object\"\n ? (formsRaw as Record<string, unknown>)[\"turnstile\"]\n : undefined;\n const turnstileWidget: \"pass\" | \"fail\" | null =\n typeof turnstileFlag === \"boolean\" ? (turnstileFlag ? \"pass\" : \"fail\") : null;\n return {\n functionHealth: d?.ok === true ? \"pass\" : \"fail\",\n cmsReachable,\n turnstileWidget,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { SmokeResult } from \"../reports/airtable/websites.js\";\n\ntype SmokeDetails = { ok: \"pass\" | \"fail\"; checkedAt: string };\n\n/** True when an AuditResult is a `smoke` audit that actually ran (has details with\n * a checkedAt — a \"skip\" for a missing `pnpm`/`test:smoke` script has none, so the\n * writer preserves the prior verdict). */\nexport function hasSmokeResult(result: AuditResult): boolean {\n if (result.audit !== \"smoke\") return false;\n const d = result.details as SmokeDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable smoke verdict (pass/fail + checked-at). */\nexport function smokeResultFromAudit(result: AuditResult): SmokeResult {\n if (result.audit !== \"smoke\") {\n throw new Error(`Expected a 'smoke' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as SmokeDetails | undefined;\n return {\n ok: d?.ok === \"fail\" ? \"fail\" : \"pass\",\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { FormE2eResult } from \"../reports/airtable/websites.js\";\n\ntype FormE2eDetails = { ok: \"pass\" | \"fail\" | null; formPresent: boolean; checkedAt: string };\n\n/** True when an AuditResult is a `form-e2e` audit that actually RAN (has details\n * with a checkedAt). Both a real verdict AND the no-form n/a case carry a checkedAt,\n * so both persist; only a no-deployed-URL skip (no details) is preserved-prior. */\nexport function hasFormE2eResult(result: AuditResult): boolean {\n if (result.audit !== \"form-e2e\") return false;\n const d = result.details as FormE2eDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable form-e2e verdict (pass/fail/null + checked-at). */\nexport function formE2eResultFromAudit(result: AuditResult): FormE2eResult {\n if (result.audit !== \"form-e2e\") {\n throw new Error(`Expected a 'form-e2e' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as FormE2eDetails | undefined;\n const ok = d?.ok === \"pass\" ? \"pass\" : d?.ok === \"fail\" ? \"fail\" : null;\n return {\n ok,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport { type WebsiteRow, siteSlug, updateAuditFields } from \"../reports/airtable/websites.js\";\nimport type {\n A11yCounts,\n DepsCounts,\n SecurityCounts,\n SecurityAdvisory,\n DomainResult,\n BrowserAuditFields,\n NetlifyDeployResult,\n FunctionHealthResult,\n SmokeResult,\n FormE2eResult,\n} from \"../reports/airtable/websites.js\";\nimport type { LighthouseScoreWriteback } from \"../reports/types.js\";\nimport { hasRealScores, lighthouseScoresFromResult } from \"./lighthouse-airtable.js\";\nimport { hasA11yCounts, a11yCountsFromResult } from \"./a11y-airtable.js\";\nimport { hasDepsCounts, depsCountsFromResult } from \"./deps-airtable.js\";\nimport {\n hasSecurityCounts,\n securityCountsFromResult,\n advisoriesFromResult,\n} from \"./security-airtable.js\";\nimport { hasDomainResult, domainResultFromAudit } from \"./domain-airtable.js\";\nimport { hasBrowserResult, browserFieldsFromAudit } from \"./browser-airtable.js\";\nimport { hasNetlifyDeployResult, netlifyDeployResultFromAudit } from \"./netlify-deploy-airtable.js\";\nimport {\n hasFunctionHealthResult,\n functionHealthResultFromAudit,\n} from \"./function-health-airtable.js\";\nimport { hasSmokeResult, smokeResultFromAudit } from \"./smoke-airtable.js\";\nimport { hasFormE2eResult, formE2eResultFromAudit } from \"./form-e2e-airtable.js\";\nimport { detectAuditEvents } from \"./fleet-event-detectors.js\";\nimport type { FleetEvent } from \"../db/fleet-events.js\";\n\ntype WriteSummary = {\n siteName: string;\n writes: Array<{\n audit:\n | \"lighthouse\"\n | \"a11y\"\n | \"deps\"\n | \"security\"\n | \"github-signals\"\n | \"domain\"\n | \"browser\"\n | \"netlify-deploy\"\n | \"function-health\"\n | \"smoke\"\n | \"form-e2e\"\n // Not an audit: the nightly Prismic model sweep reuses this result shape so\n // its write-back reports through the same `FLEET_WRITE_SUMMARY` line every\n // other fleet writer emits, rather than inventing a second contract for CI\n // to grep.\n | \"prismic-models\";\n counts: object;\n }>;\n /** Fleet-activity events detected from this site's prior row vs the fresh audits.\n * Optional: only the fleet path records them; the single-site path ignores them. */\n events?: FleetEvent[];\n};\n\n/** Orchestrates the per-audit Airtable writes for `audit --write-airtable`.\n * Extracted from the CLI command so it can be unit-tested with a fake base\n * and so adding new audit types is a one-line addition here rather than\n * growing the CLI handler.\n *\n * Throws (with .exitCode set) on the failure modes the CLI surfaces today:\n * - 2: --only ran without lighthouse, or no Websites row matched the slug\n * - 1: lighthouse ran but produced no real scores (infrastructure failure).\n * The a11y/deps/security writes still complete FIRST — a Lighthouse\n * miss flags the site without discarding its other audit data.\n *\n * Precedence note: the Websites-row lookup (exitCode 2) is checked BEFORE the\n * no-scores gate (exitCode 1), so the rare no-row + no-scores combo surfaces\n * as exitCode 2. */\nexport async function writeAuditsToAirtable(args: {\n base: AirtableBase;\n websites: WebsiteRow[];\n slug: string;\n results: AuditResult[];\n}): Promise<WriteSummary> {\n const { base, websites, slug, results } = args;\n\n // Lighthouse is OPTIONAL. The checkout-free `--only lighthouse,domain,browser` nightly includes\n // it, but a standalone `--only security` (checkout-ful) sweep legitimately has none. We write\n // whatever audits produced values; only the lighthouse-MISS flag below is lighthouse-specific\n // (so a real Lighthouse infra failure still reds the run without discarding other audit data).\n const lhResult = results.find((r) => r.audit === \"lighthouse\");\n const target = websites.find((w) => siteSlug(w.name) === slug);\n if (!target) {\n throw Object.assign(new Error(`No Websites row matched slug \"${slug}\"`), { exitCode: 2 });\n }\n\n const writes: WriteSummary[\"writes\"] = [];\n const audits: {\n scores?: LighthouseScoreWriteback;\n a11y?: A11yCounts;\n deps?: DepsCounts;\n security?: SecurityCounts;\n securityAdvisories?: SecurityAdvisory[];\n domain?: DomainResult;\n browser?: BrowserAuditFields;\n netlifyDeploy?: NetlifyDeployResult;\n functionHealth?: FunctionHealthResult;\n smoke?: SmokeResult;\n formE2e?: FormE2eResult;\n } = {};\n\n // Collect every audit that produced real values into ONE merged input, then do a\n // SINGLE atomic Airtable update (was: up to four sequential updates on the same\n // row — a mid-sequence failure left it half-written yet reported fully failed, at\n // 4× the request volume). Lighthouse is the most timeout-prone audit: a Lighthouse\n // miss must NOT discard the site's valid a11y/deps/security results (morning-brief\n // 2026-06-10 MEDIUM-E). So include Lighthouse scores only when real, include the\n // other audits whenever present, write all of them in one update, then — if\n // Lighthouse missed — throw AFTER that atomic write so the site is still flagged\n // (exitCode 1 / collected in FleetWriteResult.failed) without losing its other data.\n const lhHasScores = lhResult ? hasRealScores(lhResult) : false;\n if (lhResult && lhHasScores) {\n const scores = lighthouseScoresFromResult(lhResult);\n audits.scores = scores;\n writes.push({ audit: \"lighthouse\", counts: scores });\n }\n\n const a11y = results.find((r) => r.audit === \"a11y\");\n if (a11y && hasA11yCounts(a11y)) {\n const counts = a11yCountsFromResult(a11y);\n audits.a11y = counts;\n writes.push({ audit: \"a11y\", counts });\n }\n\n const deps = results.find((r) => r.audit === \"deps\");\n if (deps && hasDepsCounts(deps)) {\n const counts = depsCountsFromResult(deps);\n audits.deps = counts;\n writes.push({ audit: \"deps\", counts });\n }\n\n const sec = results.find((r) => r.audit === \"security\");\n if (sec && hasSecurityCounts(sec)) {\n const counts = securityCountsFromResult(sec);\n audits.security = counts;\n // Persist the advisory list alongside the counts so the dashboard can show which packages\n // are vulnerable. An empty array (clean run) clears any stale list.\n audits.securityAdvisories = advisoriesFromResult(sec);\n writes.push({ audit: \"security\", counts });\n }\n\n const dom = results.find((r) => r.audit === \"domain\");\n if (dom && hasDomainResult(dom)) {\n const result = domainResultFromAudit(dom);\n audits.domain = result;\n writes.push({ audit: \"domain\", counts: result });\n }\n\n const browser = results.find((r) => r.audit === \"browser\");\n if (browser && hasBrowserResult(browser)) {\n const fields = browserFieldsFromAudit(browser);\n audits.browser = fields;\n writes.push({ audit: \"browser\", counts: fields });\n }\n\n const netlifyDeploy = results.find((r) => r.audit === \"netlify-deploy\");\n if (netlifyDeploy && hasNetlifyDeployResult(netlifyDeploy)) {\n const result = netlifyDeployResultFromAudit(netlifyDeploy);\n audits.netlifyDeploy = result;\n writes.push({ audit: \"netlify-deploy\", counts: result });\n }\n\n const functionHealth = results.find((r) => r.audit === \"function-health\");\n if (functionHealth && hasFunctionHealthResult(functionHealth)) {\n const result = functionHealthResultFromAudit(functionHealth);\n audits.functionHealth = result;\n writes.push({ audit: \"function-health\", counts: result });\n }\n\n const smoke = results.find((r) => r.audit === \"smoke\");\n if (smoke && hasSmokeResult(smoke)) {\n const result = smokeResultFromAudit(smoke);\n audits.smoke = result;\n writes.push({ audit: \"smoke\", counts: result });\n }\n\n const formE2e = results.find((r) => r.audit === \"form-e2e\");\n if (formE2e && hasFormE2eResult(formE2e)) {\n const result = formE2eResultFromAudit(formE2e);\n audits.formE2e = result;\n writes.push({ audit: \"form-e2e\", counts: result });\n }\n\n // One atomic write of everything that ran. Skip the call only if there is nothing\n // to write at all (no real scores AND no other audit produced values) — an empty\n // update is a wasted request.\n if (Object.keys(audits).length > 0) {\n await updateAuditFields(base, target.id, audits);\n }\n\n // Detect fleet-activity transitions from the prior row (`target`, loaded before this\n // write) vs the fresh audits. Computed here where both are in hand; recorded by the\n // caller (fleet path) — single-site callers simply ignore `events`.\n const events = detectAuditEvents(\n target,\n {\n ...(audits.security !== undefined ? { security: audits.security } : {}),\n ...(audits.domain !== undefined ? { domain: audits.domain } : {}),\n },\n new Date().toISOString(),\n );\n\n // Lighthouse-miss flag: only when lighthouse WAS requested (in results) but produced no scores —\n // an infra failure worth reding the run, AFTER persisting the other audits. A sweep that never\n // ran lighthouse (e.g. `--only security`) skips this entirely.\n if (lhResult && !lhHasScores) {\n // Enumerate what WAS persisted so the failure (surfaced to the single-site\n // CLI operator via console.error) reads as a partial write, not a total one.\n const persisted = writes.map((w) => w.audit);\n throw Object.assign(\n new Error(\n `Lighthouse audit produced no scores; ${\n persisted.length ? `wrote ${persisted.join(\"/\")} but refused Lighthouse` : \"wrote nothing\"\n }. Summary: ${lhResult.summary}`,\n ),\n { exitCode: 1 },\n );\n }\n\n return { siteName: target.name, writes, events };\n}\n\nexport type FleetWriteResult = {\n written: WriteSummary[];\n failed: Array<{ slug: string; error: string }>;\n};\n\n/** Render the fleet write-back outcome for the CLI/CI. Beyond the human-readable\n * lines, it emits a single deterministic, machine-parseable line —\n * `FLEET_WRITE_SUMMARY wrote=N failed=M total=T` — that the nightly workflow\n * greps to decide pass/fail. Keying CI on this line (not the prose, and not a\n * \"wrote ≥ 1\" heuristic) lets the gate tolerate a single known flake while\n * still reding on a total or mass write-back failure. */\nexport function formatFleetWriteSummary(result: FleetWriteResult): string {\n const wrote = result.written.length;\n const failed = result.failed.length;\n const total = wrote + failed;\n let out = `→ wrote ${wrote} site(s) to Airtable`;\n if (failed > 0) {\n out += `\\n⚠ ${failed} site(s) not written: ${result.failed\n .map((f) => `${f.slug} (${f.error})`)\n .join(\"; \")}`;\n }\n out += `\\nFLEET_WRITE_SUMMARY wrote=${wrote} failed=${failed} total=${total}`;\n return out;\n}\n\n/** Write each site's pooled audit results back to its own Websites row,\n * best-effort. Results are grouped by `result.site` (the slug the fleet\n * inventory stamped as Site.name). A per-site failure (no scores, no matching\n * row) is collected — not thrown — so one bad site never aborts the batch. */\nexport async function writeFleetAuditsToAirtable(args: {\n base: AirtableBase;\n websites: WebsiteRow[];\n results: AuditResult[];\n}): Promise<FleetWriteResult> {\n const { base, websites, results } = args;\n\n const bySlug = new Map<string, AuditResult[]>();\n for (const r of results) {\n const arr = bySlug.get(r.site) ?? [];\n arr.push(r);\n bySlug.set(r.site, arr);\n }\n\n const written: WriteSummary[] = [];\n const failed: FleetWriteResult[\"failed\"] = [];\n // Serial on purpose: even at one (now atomic) update call per site, Airtable's\n // ~5 req/sec limit means a Promise.all fan-out across the fleet would burst and\n // trip 429s (silently filed as failures). Below a few dozen sites, serial trades\n // wall-clock for safety. (morning-brief 2026-06-09 MEDIUM-3.) Add a bounded pool\n // when the fleet grows.\n for (const [slug, siteResults] of bySlug) {\n try {\n written.push(await writeAuditsToAirtable({ base, websites, slug, results: siteResults }));\n } catch (e) {\n failed.push({ slug, error: (e as Error).message });\n }\n }\n return { written, failed };\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  defaultSpawn
3
- } from "./chunk-HVOOCK6L.js";
3
+ } from "./chunk-4DHQCN5H.js";
4
4
  import {
5
5
  sameOwnerRepo
6
6
  } from "./chunk-XTK5VIQB.js";
@@ -150,4 +150,4 @@ export {
150
150
  appendSkipNotice,
151
151
  fleetWorkdir
152
152
  };
153
- //# sourceMappingURL=chunk-4NAOR44V.js.map
153
+ //# sourceMappingURL=chunk-ZMIPG6TR.js.map