@nextcommerce/campaigns-os 1.33.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 (310) hide show
  1. package/AGENTS.md +204 -0
  2. package/CHANGELOG.md +5002 -0
  3. package/CONTEXT.md +685 -0
  4. package/LICENSE +202 -0
  5. package/NOTICE +4 -0
  6. package/README.md +368 -0
  7. package/agents/claude/CLAUDE.md +32 -0
  8. package/agents/codex/AGENTS.md +27 -0
  9. package/agents/copilot/copilot-instructions.md +14 -0
  10. package/agents/cursor/campaigns-os.mdc +13 -0
  11. package/bin/campaigns-os.mjs +38 -0
  12. package/campaign-spec/README.md +138 -0
  13. package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
  14. package/campaign-spec/dist/analytics-vocabulary.js +74 -0
  15. package/campaign-spec/dist/index.d.ts +40 -0
  16. package/campaign-spec/dist/index.js +77 -0
  17. package/campaign-spec/dist/normalize.d.ts +22 -0
  18. package/campaign-spec/dist/normalize.js +41 -0
  19. package/campaign-spec/dist/routing.d.ts +190 -0
  20. package/campaign-spec/dist/routing.js +263 -0
  21. package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
  22. package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
  23. package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
  24. package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
  25. package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
  26. package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
  27. package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
  28. package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
  29. package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
  30. package/campaign-spec/dist/rules/cycle-detection.js +141 -0
  31. package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
  32. package/campaign-spec/dist/rules/design-source-shape.js +142 -0
  33. package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
  34. package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
  35. package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
  36. package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
  37. package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
  38. package/campaign-spec/dist/rules/funnel-count.js +37 -0
  39. package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
  40. package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
  41. package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
  42. package/campaign-spec/dist/rules/funnel-identity.js +57 -0
  43. package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
  44. package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
  45. package/campaign-spec/dist/rules/index.d.ts +69 -0
  46. package/campaign-spec/dist/rules/index.js +131 -0
  47. package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
  48. package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
  49. package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
  50. package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
  51. package/campaign-spec/dist/rules/page-count.d.ts +11 -0
  52. package/campaign-spec/dist/rules/page-count.js +31 -0
  53. package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
  54. package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
  55. package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
  56. package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
  57. package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
  58. package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
  59. package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
  60. package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
  61. package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
  62. package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
  63. package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
  64. package/campaign-spec/dist/rules/schema-version.js +53 -0
  65. package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
  66. package/campaign-spec/dist/rules/sdk-version.js +96 -0
  67. package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
  68. package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
  69. package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
  70. package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
  71. package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
  72. package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
  73. package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
  74. package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
  75. package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
  76. package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
  77. package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
  78. package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
  79. package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
  80. package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
  81. package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
  82. package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
  83. package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
  84. package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
  85. package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
  86. package/campaign-spec/dist/sdk-version-parse.js +62 -0
  87. package/campaign-spec/dist/types.d.ts +674 -0
  88. package/campaign-spec/dist/types.js +40 -0
  89. package/campaign-spec/package.json +12 -0
  90. package/compatibility.json +25 -0
  91. package/contracts/agent-relevant-change-policy.v1.json +111 -0
  92. package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
  93. package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
  94. package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
  95. package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
  96. package/contracts/commerce-surface-catalog.json +2452 -0
  97. package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
  98. package/contracts/fixtures/orientation/envelope/current.json +95 -0
  99. package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
  100. package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
  101. package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
  102. package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
  103. package/contracts/fixtures/orientation/envelope/refused.json +100 -0
  104. package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
  105. package/contracts/fixtures/orientation/envelope/updated.json +135 -0
  106. package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
  107. package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
  108. package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
  109. package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
  110. package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
  111. package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
  112. package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
  113. package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
  114. package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
  115. package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
  116. package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
  117. package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
  118. package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
  119. package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
  120. package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
  121. package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
  122. package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
  123. package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
  124. package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
  125. package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
  126. package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
  127. package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
  128. package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
  129. package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
  130. package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
  131. package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
  132. package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
  133. package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
  134. package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
  135. package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
  136. package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
  137. package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
  138. package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
  139. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
  140. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
  141. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
  142. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
  143. package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
  144. package/contracts/migration-sidecar-bundle.v0.json +149 -0
  145. package/contracts/orientation-limits.v1.json +41 -0
  146. package/contracts/orientation-reason-codes.v1.json +196 -0
  147. package/contracts/private-template-sources.json +8 -0
  148. package/contracts/release-ledger.json +4598 -0
  149. package/contracts/reserved-skill-names.json +13 -0
  150. package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
  151. package/contracts/supported-surface.json +180 -0
  152. package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
  153. package/contracts/template-brand-contract.apollo.v0.json +27 -0
  154. package/contracts/template-brand-contract.demeter.v0.json +27 -0
  155. package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
  156. package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
  157. package/contracts/template-brand-contract.olympus.v0.json +27 -0
  158. package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
  159. package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
  160. package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
  161. package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
  162. package/contracts/template-slot-manifest.apollo.v0.json +12 -0
  163. package/contracts/template-slot-manifest.demeter.v0.json +30 -0
  164. package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
  165. package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
  166. package/contracts/template-slot-manifest.olympus.v0.json +12 -0
  167. package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
  168. package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
  169. package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
  170. package/docs/brand-theme-bridge.md +159 -0
  171. package/docs/build-packet.md +1300 -0
  172. package/docs/campaign-build-brief.md +145 -0
  173. package/docs/campaign-standardization-report.md +329 -0
  174. package/docs/campaigns-os-build-flow.md +117 -0
  175. package/docs/design-source-package.md +784 -0
  176. package/docs/legacy-migration.md +58 -0
  177. package/docs/migration-sidecar-bundle.md +139 -0
  178. package/docs/orientation-contract-reference.md +1220 -0
  179. package/docs/polish-evidence.md +502 -0
  180. package/docs/qa-and-test-orders.md +1691 -0
  181. package/docs/release-ledger-authoring-guide.md +274 -0
  182. package/docs/runtime-readiness.md +211 -0
  183. package/docs/supported-surface.md +83 -0
  184. package/docs/versioning.md +55 -0
  185. package/docs/workflow-findings-sidecar.md +588 -0
  186. package/package.json +135 -0
  187. package/prompts/first-build.md +27 -0
  188. package/prompts/friction-log.md +30 -0
  189. package/schemas/campaign-build-brief.v1.schema.json +149 -0
  190. package/schemas/campaign-design-source-package.v0.schema.json +697 -0
  191. package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
  192. package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
  193. package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
  194. package/schemas/campaign-spec.v4.schema.json +582 -0
  195. package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
  196. package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
  197. package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
  198. package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
  199. package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
  200. package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
  201. package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
  202. package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
  203. package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
  204. package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
  205. package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
  206. package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
  207. package/schemas/source-html-manifest.v0.schema.json +252 -0
  208. package/skills/next-campaigns-build/SKILL.md +73 -0
  209. package/skills/next-campaigns-os/SKILL.md +101 -0
  210. package/skills/next-campaigns-os/references/session-intake.md +160 -0
  211. package/skills/next-campaigns-os-setup/SKILL.md +22 -0
  212. package/skills/next-campaigns-polish/SKILL.md +145 -0
  213. package/skills/next-campaigns-qa/SKILL.md +92 -0
  214. package/skills.json +56 -0
  215. package/skills.sh +64 -0
  216. package/src/adapter-decision-contract.mjs +333 -0
  217. package/src/brand-theme.mjs +1151 -0
  218. package/src/browser-launch.mjs +79 -0
  219. package/src/build-brief.mjs +781 -0
  220. package/src/built-site-scope.mjs +312 -0
  221. package/src/campaign-ecosystem.mjs +734 -0
  222. package/src/campaign-identity.mjs +405 -0
  223. package/src/campaign-workspace.mjs +127 -0
  224. package/src/checkpoint-waiver.mjs +302 -0
  225. package/src/cli.mjs +13442 -0
  226. package/src/commercial-journey.mjs +1119 -0
  227. package/src/commercial-parity.mjs +965 -0
  228. package/src/consent.mjs +347 -0
  229. package/src/content-residue.mjs +322 -0
  230. package/src/deadline.mjs +81 -0
  231. package/src/design-source-package.mjs +2604 -0
  232. package/src/deviation.mjs +107 -0
  233. package/src/doctor-check-registry.mjs +49 -0
  234. package/src/doctor-sidecar.mjs +106 -0
  235. package/src/finding-cause.mjs +557 -0
  236. package/src/findings.mjs +326 -0
  237. package/src/fs-identity.mjs +68 -0
  238. package/src/gate-actions.mjs +105 -0
  239. package/src/html-scan.mjs +53 -0
  240. package/src/install-mode.mjs +272 -0
  241. package/src/legacy-migration.d.ts +128 -0
  242. package/src/legacy-migration.mjs +510 -0
  243. package/src/lifecycle.mjs +338 -0
  244. package/src/local-proof.mjs +401 -0
  245. package/src/map-pin-writeback.mjs +210 -0
  246. package/src/orchestration-stage-contract.mjs +81 -0
  247. package/src/package-install-fixture.mjs +33 -0
  248. package/src/page-kit-build-summary.mjs +175 -0
  249. package/src/page-kit-campaign-config.mjs +57 -0
  250. package/src/page-kit-sdk-version.mjs +392 -0
  251. package/src/page-kit-store-profile.mjs +369 -0
  252. package/src/page-kit-sync.mjs +162 -0
  253. package/src/polish-browser.mjs +867 -0
  254. package/src/polish-capture.mjs +1094 -0
  255. package/src/polish-deadline.mjs +51 -0
  256. package/src/polish-gate.mjs +739 -0
  257. package/src/polish-node.mjs +639 -0
  258. package/src/polish-page-load.mjs +1100 -0
  259. package/src/private-template-source.mjs +237 -0
  260. package/src/proof-policy.mjs +82 -0
  261. package/src/qa-analytics-correctness.mjs +307 -0
  262. package/src/qa-analytics-errors.mjs +38 -0
  263. package/src/qa-analytics-parity.mjs +699 -0
  264. package/src/qa-binding-evidence.mjs +140 -0
  265. package/src/qa-browser.mjs +6608 -0
  266. package/src/qa-cart-entry.mjs +406 -0
  267. package/src/qa-commercial-parity.mjs +641 -0
  268. package/src/qa-node.mjs +3620 -0
  269. package/src/qa-order-bump.mjs +381 -0
  270. package/src/qa-parity-capture.mjs +428 -0
  271. package/src/qa-parity-fixture.mjs +359 -0
  272. package/src/qa-publish.mjs +362 -0
  273. package/src/qa-purchase-data-layer.mjs +263 -0
  274. package/src/qa-route-probe.mjs +272 -0
  275. package/src/qa-sidecar.mjs +188 -0
  276. package/src/qa-test-order-topology.mjs +207 -0
  277. package/src/qa-url-privacy.mjs +13 -0
  278. package/src/qa-verdict-discovery.mjs +192 -0
  279. package/src/qa-verdict-publish.mjs +105 -0
  280. package/src/qa-verdict.mjs +287 -0
  281. package/src/remit.mjs +388 -0
  282. package/src/repo-scan.mjs +83 -0
  283. package/src/route-identity.mjs +133 -0
  284. package/src/run-record-closeout.mjs +229 -0
  285. package/src/run-record.mjs +839 -0
  286. package/src/run-session.mjs +226 -0
  287. package/src/runtime-state-ignore.mjs +113 -0
  288. package/src/sdk-attribute-index.mjs +212 -0
  289. package/src/sdk-markup.mjs +358 -0
  290. package/src/sdk-meta-tags.mjs +52 -0
  291. package/src/shell-token.mjs +7 -0
  292. package/src/sidecar-bundle.mjs +399 -0
  293. package/src/source-asset-crawl.mjs +469 -0
  294. package/src/source-html-intake.mjs +627 -0
  295. package/src/source-html-manifest.mjs +276 -0
  296. package/src/source-prep.mjs +284 -0
  297. package/src/spec-derive-store.mjs +431 -0
  298. package/src/spec-derive.mjs +514 -0
  299. package/src/spec-fetch.mjs +66 -0
  300. package/src/spec-hash.mjs +48 -0
  301. package/src/spec-identity.mjs +27 -0
  302. package/src/stage-ledger.mjs +509 -0
  303. package/src/standardization-report.mjs +1297 -0
  304. package/src/template-brand-contract.mjs +473 -0
  305. package/src/template-freshness.mjs +196 -0
  306. package/src/template-reference.mjs +81 -0
  307. package/src/template-slot-manifest.mjs +150 -0
  308. package/src/text-safety.mjs +62 -0
  309. package/src/theme-gate.mjs +185 -0
  310. package/src/upsell-selector-scope.mjs +299 -0
@@ -0,0 +1,2604 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export const DESIGN_SOURCE_PACKAGE_SCHEMA = "campaign-design-source-package/v0";
4
+ export const DESIGN_SOURCE_PACKAGE_REL_PATH = ".campaign-runtime/input/design-source-package.json";
5
+ export const DESIGN_SOURCE_PACKAGE_FINGERPRINT_PATTERN = /^sha256:[0-9a-f]{64}$/;
6
+
7
+ export const COVERAGE_ROLES = Object.freeze([
8
+ "primary_design",
9
+ "partial_design",
10
+ "brand_tokens",
11
+ "asset_source",
12
+ "copy_source",
13
+ "template_baseline",
14
+ "reference_only",
15
+ "fallback_legacy",
16
+ ]);
17
+
18
+ export const MAPPING_CONFIDENCE_LEVELS = Object.freeze(["high", "medium", "low", "unknown"]);
19
+ export const CHECKPOINT_STATUSES = Object.freeze([
20
+ "pending",
21
+ "blocked",
22
+ "ready",
23
+ "ready_with_gaps",
24
+ "ready_with_waivers",
25
+ "completed",
26
+ "completed_with_warnings",
27
+ "skipped",
28
+ ]);
29
+ export const SOURCE_READINESS_STATUSES = Object.freeze([
30
+ "pending",
31
+ "blocked",
32
+ "ready",
33
+ "ready_with_gaps",
34
+ "ready_with_waivers",
35
+ ]);
36
+
37
+ const CONTRIBUTION_KINDS = new Set([
38
+ "html_funnel",
39
+ "figma_frames",
40
+ "figma_sections",
41
+ "page_kit",
42
+ "static_source",
43
+ "template_stock",
44
+ "agency_source",
45
+ "other",
46
+ ]);
47
+ const CONTRIBUTION_TRUST = new Set(["native", "structured", "rendered", "opaque"]);
48
+ const SURFACE_KINDS = new Set(["campaign", "page", "section", "runtime_surface"]);
49
+ const VISUAL_KINDS = new Set([
50
+ "source_screenshot",
51
+ "template_reference_screenshot",
52
+ "render_reference",
53
+ "export_reference",
54
+ "unavailable_render",
55
+ ]);
56
+ const VIEWPORTS = new Set(["mobile", "desktop", "tablet"]);
57
+ const SOURCE_REF_KINDS = new Set(["html_file", "manifest", "asset", "url", "export", "document", "other"]);
58
+ const REQUIRED_SOURCE_VIEWPORTS = Object.freeze(["desktop", "mobile"]);
59
+ const MATERIAL_FINGERPRINT_PREFIX = "sha256:";
60
+ const SHA256_PATTERN = /^(?:sha256:)?[0-9a-f]{64}$/;
61
+ const SAFE_SURFACE_ID = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;
62
+ const PRIMARY_COVERAGE_SCOPES = new Set([
63
+ "primary_design",
64
+ "primary_design_coverage",
65
+ "source_coverage",
66
+ "page_design",
67
+ "design_source",
68
+ ]);
69
+ const SOURCE_SCREENSHOT_SCOPES = new Set([
70
+ "source_screenshot",
71
+ "source_screenshots",
72
+ "source_render",
73
+ "visual_reference",
74
+ "source_reference",
75
+ ]);
76
+ const TEMPLATE_REFERENCE_SCOPES = new Set(["template_reference", "template_baseline"]);
77
+ const EXTERNAL_WAIVER_TARGETS = new Set([
78
+ "stages.assembly.source_package_material_fingerprint",
79
+ "design_source_package.material_fingerprint",
80
+ "polish.assembly_source_package_fingerprint_missing",
81
+ "polish.assembly_source_package_stale",
82
+ ]);
83
+ const SOURCE_SCREENSHOT_KINDS = new Set(["source_screenshot", "unavailable_render"]);
84
+ const COMPARISON_REFERENCE_KINDS = new Set([
85
+ "render_reference",
86
+ "export_reference",
87
+ "template_reference_screenshot",
88
+ "unavailable_render",
89
+ ]);
90
+ const TEMPLATE_SCREENSHOT_KINDS = new Set(["template_reference_screenshot", "unavailable_render"]);
91
+
92
+ const TOP_LEVEL_FIELDS = new Set([
93
+ "schema_version",
94
+ "package_id",
95
+ "source_kind",
96
+ "generated_at",
97
+ "material_fingerprint",
98
+ "surface_identity",
99
+ "contributions",
100
+ "source_gaps",
101
+ "source_todos",
102
+ "waivers",
103
+ "divergences",
104
+ "proposed_exceptions",
105
+ "notes",
106
+ "readiness",
107
+ "readback",
108
+ ]);
109
+ const SURFACE_FIELDS = new Set(["id", "kind", "label", "aliases", "mappings"]);
110
+ const SURFACE_MAPPING_FIELDS = new Set([
111
+ "campaign_map_id",
112
+ "campaign_slug",
113
+ "campaign_spec_page_id",
114
+ "map_builder_label",
115
+ "map_builder_custom_name",
116
+ "public_route",
117
+ "producer_page_type",
118
+ "source_page_id",
119
+ "source_path",
120
+ "page_kit",
121
+ "parent_surface_id",
122
+ "dom_selector",
123
+ "runtime_surface",
124
+ ]);
125
+ const PAGE_KIT_FIELDS = new Set([
126
+ "target_path",
127
+ "output_path",
128
+ "public_route",
129
+ "spec_route",
130
+ "page_type",
131
+ "permalink_required",
132
+ ]);
133
+ const CONTRIBUTION_FIELDS = new Set([
134
+ "id",
135
+ "kind",
136
+ "trust",
137
+ "renderable",
138
+ "provenance",
139
+ "presentation_intent",
140
+ "source_refs",
141
+ "screenshot_refs",
142
+ "reference_refs",
143
+ "template_reference",
144
+ "mappings",
145
+ "notes",
146
+ ]);
147
+ const PROVENANCE_FIELDS = new Set([
148
+ "source_type",
149
+ "adapter",
150
+ "producer",
151
+ "generator",
152
+ "generator_version",
153
+ "source_root",
154
+ "manifest_schema_version",
155
+ "manifest_path",
156
+ "manifest_sha256",
157
+ "producer_material_fingerprint",
158
+ "asset_crawl_schema_version",
159
+ ]);
160
+ const PRESENTATION_INTENT_FIELDS = new Set([
161
+ "summary",
162
+ "composition",
163
+ "content_hierarchy",
164
+ "imagery",
165
+ "copy",
166
+ "brand",
167
+ "responsive_behavior",
168
+ ]);
169
+ const SOURCE_REF_FIELDS = new Set(["id", "kind", "path", "url", "sha256", "role", "media_type", "notes"]);
170
+ const VISUAL_REF_FIELDS = new Set([
171
+ "id",
172
+ "kind",
173
+ "viewport",
174
+ "availability",
175
+ "url",
176
+ "path",
177
+ "sha256",
178
+ "width",
179
+ "height",
180
+ "device_profile",
181
+ "scale_factor",
182
+ "browser",
183
+ "captured_at",
184
+ "source_ref_id",
185
+ "unavailable_reason",
186
+ "notes",
187
+ ]);
188
+ const TEMPLATE_REFERENCE_FIELDS = new Set([
189
+ "id",
190
+ "family",
191
+ "version",
192
+ "contract_path",
193
+ "artifact_path",
194
+ "sha256",
195
+ "standard_viewport_refs",
196
+ ]);
197
+ const COVERAGE_MAPPING_FIELDS = new Set([
198
+ "id",
199
+ "surface_id",
200
+ "coverage_role",
201
+ "confidence",
202
+ "source_refs",
203
+ "screenshot_refs",
204
+ "reference_refs",
205
+ "template_reference_id",
206
+ "notes",
207
+ ]);
208
+ const GAP_FIELDS = new Set([
209
+ "id",
210
+ "kind",
211
+ "scope",
212
+ "applies_to",
213
+ "reason",
214
+ "status",
215
+ "attributed_by",
216
+ "attributed_at",
217
+ "evidence_refs",
218
+ ]);
219
+ const TODO_FIELDS = new Set([
220
+ "id",
221
+ "kind",
222
+ "scope",
223
+ "applies_to",
224
+ "description",
225
+ "status",
226
+ "owner",
227
+ "required_viewports",
228
+ "source_ref_ids",
229
+ ]);
230
+ const WAIVER_FIELDS = new Set([
231
+ "id",
232
+ "scope",
233
+ "applies_to",
234
+ "reason",
235
+ "status",
236
+ "waived_by",
237
+ "waived_at",
238
+ "expires_at",
239
+ "review_condition",
240
+ "evidence_refs",
241
+ ]);
242
+ const DIVERGENCE_FIELDS = new Set([
243
+ "id",
244
+ "scope",
245
+ "applies_to",
246
+ "summary",
247
+ "status",
248
+ "recorded_stage",
249
+ "readiness_affecting",
250
+ "attributed_by",
251
+ "evidence_refs",
252
+ ]);
253
+ const EXCEPTION_FIELDS = new Set([
254
+ "id",
255
+ "scope",
256
+ "applies_to",
257
+ "reason",
258
+ "status",
259
+ "readiness_affecting",
260
+ "proposed_by",
261
+ "evidence_refs",
262
+ ]);
263
+ const READINESS_FIELDS = new Set([
264
+ "status",
265
+ "blocking_reasons",
266
+ "gap_count",
267
+ "todo_count",
268
+ "waiver_count",
269
+ "generated_at",
270
+ ]);
271
+ const READBACK_FIELDS = new Set([
272
+ "summary",
273
+ "included_sources",
274
+ "handled",
275
+ "blockers",
276
+ "gaps",
277
+ "todos",
278
+ "waivers",
279
+ "next_actions",
280
+ ]);
281
+
282
+ /**
283
+ * Synthesize the normalized v0 package for the source-html adapter. This is a
284
+ * pure adapter: every input is supplied by the caller and it performs no file
285
+ * reads, captures, or network access. In particular, asset-crawl images and
286
+ * section exports are never promoted to screenshot proof unless the input
287
+ * record explicitly declares a shared viewport key.
288
+ */
289
+ export function synthesizeHtmlFunnelDesignSourcePackage({
290
+ activePages = [],
291
+ mappings = [],
292
+ pageMappings = null,
293
+ manifest = null,
294
+ manifestPath = null,
295
+ assetCrawl = null,
296
+ sourceAssetCrawl = null,
297
+ templateFamily = null,
298
+ templateStockPageIds = [],
299
+ sourceScreenshots = [],
300
+ packageId = "design-source-package",
301
+ campaignMapId = null,
302
+ campaignSlug = null,
303
+ sourceRoot = null,
304
+ generatedAt = new Date().toISOString(),
305
+ sourceGaps = [],
306
+ sourceTodos = [],
307
+ waivers = [],
308
+ divergences = [],
309
+ proposedExceptions = [],
310
+ notes = [],
311
+ presentationIntent = null,
312
+ } = {}) {
313
+ const resolvedMappings = pageMappings == null ? mappings : pageMappings;
314
+ const resolvedMappingsName = pageMappings == null ? "mappings" : "pageMappings";
315
+ assertInputArray(activePages, "activePages");
316
+ assertInputArray(resolvedMappings, resolvedMappingsName);
317
+ assertInputArray(sourceScreenshots, "sourceScreenshots");
318
+ assertInputArray(templateStockPageIds, "templateStockPageIds");
319
+ for (const [name, value] of [
320
+ ["sourceGaps", sourceGaps],
321
+ ["sourceTodos", sourceTodos],
322
+ ["waivers", waivers],
323
+ ["divergences", divergences],
324
+ ["proposedExceptions", proposedExceptions],
325
+ ["notes", notes],
326
+ ]) assertInputArray(value, name);
327
+
328
+ activePages.forEach((page, index) => {
329
+ if (!isObject(page) || !isNonEmptyString(page.id)) {
330
+ throw new TypeError(`activePages[${index}] must be an object with a non-empty id.`);
331
+ }
332
+ });
333
+ resolvedMappings.forEach((mapping, index) => {
334
+ if (!isObject(mapping) || !isNonEmptyString(mapping.page_id)) {
335
+ throw new TypeError(`${resolvedMappingsName}[${index}] must be an object with a non-empty page_id.`);
336
+ }
337
+ });
338
+
339
+ const manifestInput = normalizeManifestInput(manifest, manifestPath);
340
+ const crawl = isObject(sourceAssetCrawl) ? sourceAssetCrawl : isObject(assetCrawl) ? assetCrawl : null;
341
+ const crawlInputName = isObject(sourceAssetCrawl) ? "sourceAssetCrawl" : "assetCrawl";
342
+ assertOptionalInputArray(manifestInput.value?.files, "manifest.files");
343
+ assertOptionalInputArray(manifestInput.value?.pages, "manifest.pages");
344
+ assertOptionalInputArray(crawl?.scanned_files, `${crawlInputName}.scanned_files`);
345
+ assertOptionalInputArray(crawl?.references, `${crawlInputName}.references`);
346
+ const pages = collectPageInputs(activePages, resolvedMappings);
347
+ const { surfaces, surfaceByPageId } = createSurfaceCatalog(pages, {
348
+ campaignMapId,
349
+ campaignSlug,
350
+ });
351
+ const manifestPagesById = indexManifestPagesById(manifestInput.value?.pages);
352
+
353
+ const sourceRefRegistry = createSourceReferenceRegistry();
354
+ if (manifestInput.path) {
355
+ sourceRefRegistry.add({
356
+ id: "source-html-manifest",
357
+ kind: "manifest",
358
+ path: manifestInput.path,
359
+ sha256: validSha(manifestInput.value?.sha256),
360
+ role: "producer_manifest",
361
+ });
362
+ }
363
+
364
+ const manifestFiles = sortByCanonicalLocation(
365
+ arrayOrEmpty(manifestInput.value?.files),
366
+ (file) => optionalString(file?.path),
367
+ );
368
+ for (const file of manifestFiles) {
369
+ if (!isObject(file) || !isNonEmptyString(file.path)) continue;
370
+ sourceRefRegistry.add({
371
+ id: `manifest-${slugify(file.path) || "file"}`,
372
+ kind: sourceReferenceKindForManifestFile(file),
373
+ path: file.path,
374
+ sha256: validSha(file.sha256),
375
+ role: optionalString(file.role),
376
+ });
377
+ }
378
+
379
+ const crawlHashes = new Map();
380
+ for (const file of sortByCanonicalLocation(
381
+ arrayOrEmpty(crawl?.scanned_files),
382
+ (entry) => optionalString(entry?.path),
383
+ )) {
384
+ if (isObject(file) && isNonEmptyString(file.path)) {
385
+ crawlHashes.set(file.path, validSha(file.sha256));
386
+ }
387
+ }
388
+ const crawlReferenceIdsByPageId = new Map();
389
+ const crawlReferences = sortByCanonicalLocation(
390
+ arrayOrEmpty(crawl?.references),
391
+ canonicalCrawlReferenceLocation,
392
+ );
393
+ for (const ref of crawlReferences) {
394
+ if (!isObject(ref)) continue;
395
+ const url = /^https?:\/\//i.test(String(ref.raw || "")) ? ref.raw : null;
396
+ const path = optionalString(ref.source_path) || (!url ? optionalString(ref.normalized) : null);
397
+ if (!path && !url) continue;
398
+ const registered = sourceRefRegistry.add({
399
+ id: `asset-${slugify(path || url) || "reference"}`,
400
+ kind: "asset",
401
+ ...(path ? { path } : {}),
402
+ ...(url ? { url } : {}),
403
+ ...(crawlHashes.get(path) ? { sha256: crawlHashes.get(path) } : {}),
404
+ role: optionalString(ref.asset_kind) || "asset",
405
+ });
406
+ if (!registered) continue;
407
+ for (const source of arrayOrEmpty(ref.referenced_by)) {
408
+ for (const pageId of arrayOrEmpty(source?.page_ids)) {
409
+ if (isNonEmptyString(pageId)) appendMapArray(crawlReferenceIdsByPageId, pageId, registered.id);
410
+ }
411
+ }
412
+ }
413
+
414
+ const globalVisualRegistry = createVisualReferenceRegistry("source_screenshot");
415
+ const explicitSourceScreenshotsById = new Map(
416
+ sourceScreenshots
417
+ .filter((ref) => isObject(ref) && isNonEmptyString(ref.id))
418
+ .map((ref) => [ref.id, ref]),
419
+ );
420
+ const sectionVisualsByPageId = indexSectionVisualCandidates(manifestInput.value);
421
+ const sourceScreenshotsByPageId = indexVisualCandidatesByPage(
422
+ sourceScreenshots,
423
+ surfaceByPageId,
424
+ );
425
+
426
+ const htmlCoverage = [];
427
+ for (const page of pages) {
428
+ const mapping = page.mapping;
429
+ const surfaceId = surfaceByPageId.get(page.pageId);
430
+ const explicitRole = COVERAGE_ROLES.includes(mapping?.coverage_role) ? mapping.coverage_role : null;
431
+ const hasSourcePath = isNonEmptyString(mapping?.path);
432
+ if (!hasSourcePath && !explicitRole) continue;
433
+ if (explicitRole === "template_baseline") continue;
434
+
435
+ const manifestPage = manifestPagesById.get(page.pageId);
436
+ const sourceRefIds = [];
437
+ if (hasSourcePath) {
438
+ const sourceHash = validSha(mapping.source_hash)
439
+ || validSha(manifestPage?.source_hash)
440
+ || crawlHashes.get(mapping.path)
441
+ || null;
442
+ const sourceRef = sourceRefRegistry.add({
443
+ id: `source-${surfaceId}-html`,
444
+ kind: "html_file",
445
+ path: mapping.path,
446
+ ...(sourceHash ? { sha256: sourceHash } : {}),
447
+ role: "page",
448
+ });
449
+ sourceRefIds.push(sourceRef.id);
450
+ }
451
+ sourceRefIds.push(...arrayOrEmpty(crawlReferenceIdsByPageId.get(page.pageId)));
452
+
453
+ const screenshotIds = [];
454
+ const visualCandidates = [
455
+ ...visualCandidatesFrom(mapping, ["screenshot_refs", "source_screenshot_refs", "screenshots"]),
456
+ ...visualCandidatesFrom(manifestPage, ["screenshot_refs", "source_screenshot_refs", "screenshots"]),
457
+ ...arrayOrEmpty(sectionVisualsByPageId.get(page.pageId)),
458
+ ...arrayOrEmpty(sourceScreenshotsByPageId.get(page.pageId)),
459
+ ];
460
+ for (const raw of visualCandidates) {
461
+ if (typeof raw === "string") {
462
+ const declared = explicitSourceScreenshotsById.get(raw);
463
+ const registered = declared
464
+ ? globalVisualRegistry.add(declared, `source-${surfaceId}`, sourceRefIds[0] || null)
465
+ : globalVisualRegistry.byId(raw);
466
+ if (registered) screenshotIds.push(registered.id);
467
+ continue;
468
+ }
469
+ const registered = globalVisualRegistry.add(raw, `source-${surfaceId}`, sourceRefIds[0] || null);
470
+ if (registered) screenshotIds.push(registered.id);
471
+ }
472
+
473
+ const referenceRegistry = createVisualReferenceRegistry("render_reference");
474
+ const referenceIds = [];
475
+ for (const raw of visualCandidatesFrom(mapping, ["reference_refs", "render_references"])) {
476
+ const registered = referenceRegistry.add(raw, `reference-${surfaceId}`, sourceRefIds[0] || null);
477
+ if (registered) referenceIds.push(registered.id);
478
+ }
479
+
480
+ htmlCoverage.push({
481
+ id: `html-${surfaceId}-coverage`,
482
+ surface_id: surfaceId,
483
+ coverage_role: explicitRole || "primary_design",
484
+ confidence: normalizeConfidence(mapping?.confidence)
485
+ || confidenceForHtmlMapping(mapping, manifestPage),
486
+ source_refs: sortStrings(uniqueStrings(sourceRefIds)),
487
+ screenshot_refs: sortStrings(uniqueStrings(screenshotIds)),
488
+ reference_refs: sortStrings(uniqueStrings(referenceIds)),
489
+ ...(isNonEmptyString(mapping?.notes) ? { notes: mapping.notes } : {}),
490
+ _reference_records: referenceRegistry.values(),
491
+ });
492
+ }
493
+
494
+ const htmlReferenceRefs = [];
495
+ for (const mapping of htmlCoverage) {
496
+ htmlReferenceRefs.push(...mapping._reference_records);
497
+ delete mapping._reference_records;
498
+ }
499
+ const htmlContribution = {
500
+ id: "html-funnel",
501
+ kind: "html_funnel",
502
+ trust: manifestInput.value ? "structured" : "rendered",
503
+ renderable: true,
504
+ provenance: compactDefined({
505
+ source_type: "html_funnel",
506
+ adapter: "source-html-intake",
507
+ producer: optionalString(manifestInput.value?.producer_provenance?.source_type),
508
+ generator: optionalString(manifestInput.value?.generator),
509
+ generator_version: optionalString(manifestInput.value?.producer_provenance?.generator_version),
510
+ source_root: optionalString(sourceRoot),
511
+ manifest_schema_version: optionalString(manifestInput.value?.schema_version),
512
+ manifest_path: optionalString(manifestInput.path),
513
+ manifest_sha256: validSha(manifestInput.value?.sha256),
514
+ producer_material_fingerprint: optionalString(manifestInput.value?.producer_provenance?.material_fingerprint),
515
+ asset_crawl_schema_version: optionalString(crawl?.schema_version),
516
+ }, { preserveNull: true }),
517
+ presentation_intent: normalizePresentationIntent(
518
+ presentationIntent,
519
+ "Preserve the provided source HTML composition and presentation intent while CampaignSpec and Page Kit retain commerce/runtime authority.",
520
+ ),
521
+ source_refs: sourceRefRegistry.values(),
522
+ screenshot_refs: globalVisualRegistry.values(),
523
+ reference_refs: dedupeRecordsById(htmlReferenceRefs),
524
+ mappings: htmlCoverage,
525
+ };
526
+
527
+ const contributions = [htmlContribution];
528
+ const template = normalizeTemplateFamily(templateFamily);
529
+ const qualifyingHtmlSurfaces = new Set(
530
+ htmlCoverage
531
+ .filter((mapping) => mapping.coverage_role === "primary_design" && ["high", "medium"].includes(mapping.confidence))
532
+ .map((mapping) => mapping.surface_id),
533
+ );
534
+ const explicitTemplateSurfaces = new Set(
535
+ pages
536
+ .filter((page) => page.mapping?.coverage_role === "template_baseline")
537
+ .map((page) => surfaceByPageId.get(page.pageId)),
538
+ );
539
+ const templateCandidateSurfaces = surfaces
540
+ .filter((surface) => surface.kind === "page")
541
+ .map((surface) => surface.id)
542
+ .filter((surfaceId) => explicitTemplateSurfaces.has(surfaceId) || !qualifyingHtmlSurfaces.has(surfaceId));
543
+ const templateProof = normalizeTemplateReference(template);
544
+ const templateProofAssessment = assessTemplateReference(templateProof);
545
+
546
+ if (template.family) {
547
+ const referenceRefs = templateProof?.standard_viewport_refs || [];
548
+ const templateContribution = {
549
+ id: "template-baseline",
550
+ kind: "template_stock",
551
+ trust: "native",
552
+ renderable: true,
553
+ provenance: {
554
+ source_type: "template_stock",
555
+ adapter: "template-reference",
556
+ producer: "Campaign Page Kit",
557
+ generator: null,
558
+ generator_version: template.version || null,
559
+ source_root: null,
560
+ manifest_schema_version: null,
561
+ manifest_path: null,
562
+ manifest_sha256: null,
563
+ producer_material_fingerprint: null,
564
+ asset_crawl_schema_version: null,
565
+ },
566
+ presentation_intent: normalizePresentationIntent(
567
+ template.presentation_intent,
568
+ `Use the ${template.family} Template Reference only as the safe runtime baseline for explicitly template-backed pages.`,
569
+ ),
570
+ source_refs: templateSourceRefs(templateProof),
571
+ screenshot_refs: [],
572
+ reference_refs: referenceRefs,
573
+ ...(templateProof ? { template_reference: templateProof } : {}),
574
+ mappings: templateProofAssessment.complete
575
+ ? templateCandidateSurfaces.map((surfaceId) => ({
576
+ id: `template-${surfaceId}-coverage`,
577
+ surface_id: surfaceId,
578
+ coverage_role: "template_baseline",
579
+ confidence: "high",
580
+ source_refs: templateSourceRefs(templateProof).map((ref) => ref.id),
581
+ screenshot_refs: [],
582
+ reference_refs: referenceRefs.map((ref) => ref.id),
583
+ template_reference_id: templateProof.id,
584
+ }))
585
+ : [],
586
+ };
587
+ contributions.push(templateContribution);
588
+ }
589
+
590
+ // Template-stock pages. A page intake declared out of source scope
591
+ // (templateStockPageIds) has no design of the merchant's: the selected
592
+ // family's stock page IS the design, and the build stage materialises it.
593
+ // Where the family publishes complete Template Reference proof, the
594
+ // template_baseline mapping above already covers the surface. Where it does
595
+ // not, the package used to demand a Template Reference link the operator
596
+ // has no channel to author; it now records an accepted coverage_absence
597
+ // Source Gap attributed by prepare-build instead, so the surface is
598
+ // excepted from primary coverage, no link/capture TODO fires for it, and
599
+ // readiness lands at ready_with_gaps rather than blocked.
600
+ const clonedGaps = cloneJsonArray(sourceGaps);
601
+ const existingGapIds = new Set(clonedGaps.map((gap) => gap?.id).filter(isNonEmptyString));
602
+ if (template.family && !templateProofAssessment.complete) {
603
+ const templateStockSurfaceIds = new Set(
604
+ templateStockPageIds
605
+ .filter(isNonEmptyString)
606
+ .map((pageId) => surfaceByPageId.get(pageId))
607
+ .filter(Boolean),
608
+ );
609
+ for (const surface of surfaces.filter((entry) => entry.kind === "page" && templateStockSurfaceIds.has(entry.id))) {
610
+ const gap = {
611
+ id: uniqueRecordId(`template-stock-${surface.id}`, existingGapIds),
612
+ kind: "coverage_absence",
613
+ scope: "primary_design_coverage",
614
+ applies_to: [surface.id],
615
+ reason: `Page surface "${surface.id}" is template stock from the ${template.family} family: it has no design source of its own, and the build stage materialises it from that family's stock page.`,
616
+ status: "accepted",
617
+ attributed_by: "prepare-build",
618
+ attributed_at: null,
619
+ };
620
+ clonedGaps.push(gap);
621
+ }
622
+ }
623
+ const clonedWaivers = cloneJsonArray(waivers);
624
+ const generatedTodos = cloneJsonArray(sourceTodos);
625
+ const now = Date.parse(generatedAt);
626
+ const readinessNow = Number.isNaN(now) ? Date.now() : now;
627
+ const existingTodoIds = new Set(generatedTodos.map((todo) => todo?.id).filter(isNonEmptyString));
628
+ const addTodo = (todo) => {
629
+ todo.id = uniqueRecordId(todo.id, existingTodoIds);
630
+ generatedTodos.push(todo);
631
+ };
632
+ const coverageClaimsBySurface = indexCoverageClaimsBySurface(contributions);
633
+ const readinessExceptions = indexReadinessExceptions(clonedGaps, clonedWaivers, readinessNow);
634
+ const templateCandidateSurfaceIds = new Set(templateCandidateSurfaces);
635
+
636
+ for (const surface of surfaces.filter((entry) => entry.kind === "page")) {
637
+ const claims = coverageClaimsBySurface.get(surface.id) || [];
638
+ const primaryClaims = claims
639
+ .filter(({ mapping }) => mapping.coverage_role === "primary_design");
640
+ const qualifyingPrimaryClaims = sortCoverageClaims(primaryClaims
641
+ .filter(({ mapping }) => ["high", "medium"].includes(mapping.confidence)));
642
+ const coverageExcepted = readinessExceptions.hasCoverage(surface.id, "primary_coverage");
643
+ if (qualifyingPrimaryClaims.length) {
644
+ if (qualifyingPrimaryClaims.some(primaryClaimHasRequiredProof)) continue;
645
+ if (readinessExceptions.hasCoverage(surface.id, "source_screenshot")) continue;
646
+ const primary = bestPrimaryClaimForTodo(qualifyingPrimaryClaims);
647
+ const missingViewports = missingRequiredViewportsForPrimaryClaim(primary);
648
+ for (const viewport of missingViewports) {
649
+ addTodo({
650
+ id: `capture-${surface.id}-${viewport}`,
651
+ kind: "missing_source_screenshot",
652
+ scope: "source_screenshot",
653
+ applies_to: [surface.id],
654
+ description: `Capture an explicit ${viewport} source screenshot for renderable primary design surface "${surface.id}".`,
655
+ status: "blocked",
656
+ owner: "source_preparation",
657
+ required_viewports: [viewport],
658
+ source_ref_ids: primary.mapping.source_refs,
659
+ });
660
+ }
661
+ continue;
662
+ }
663
+ if (coverageExcepted) continue;
664
+
665
+ if (primaryClaims.some(({ mapping }) => ["low", "unknown"].includes(mapping.confidence))) {
666
+ addTodo({
667
+ id: `confirm-${surface.id}-primary-design`,
668
+ kind: "low_confidence_primary_design",
669
+ scope: "primary_design_coverage",
670
+ applies_to: [surface.id],
671
+ description: `Confirm the primary design mapping for page surface "${surface.id}" at medium or high confidence.`,
672
+ status: "blocked",
673
+ owner: "source_preparation",
674
+ });
675
+ continue;
676
+ }
677
+
678
+ if (template.family && templateCandidateSurfaceIds.has(surface.id) && !templateProofAssessment.complete) {
679
+ if (!templateProofAssessment.linked) {
680
+ addTodo({
681
+ id: `link-${surface.id}-template-reference`,
682
+ kind: "missing_template_reference",
683
+ scope: "template_reference",
684
+ applies_to: [surface.id],
685
+ description: `Link ${template.family} family/version to a Template Reference artifact or contract before claiming template_baseline coverage.`,
686
+ status: "blocked",
687
+ owner: "source_preparation",
688
+ });
689
+ } else {
690
+ for (const viewport of templateProofAssessment.missing_viewports) {
691
+ addTodo({
692
+ id: `capture-${surface.id}-template-${viewport}`,
693
+ kind: "missing_template_viewport",
694
+ scope: "template_reference",
695
+ applies_to: [surface.id],
696
+ description: `Provide the ${viewport} standard viewport reference for ${template.family} before claiming template_baseline coverage for "${surface.id}".`,
697
+ status: "blocked",
698
+ owner: "source_preparation",
699
+ required_viewports: [viewport],
700
+ });
701
+ }
702
+ }
703
+ continue;
704
+ }
705
+
706
+ if (!claims.some(({ mapping }) => mapping.coverage_role === "template_baseline")) {
707
+ addTodo({
708
+ id: `provide-${surface.id}-primary-design`,
709
+ kind: "missing_primary_design",
710
+ scope: "primary_design_coverage",
711
+ applies_to: [surface.id],
712
+ description: `Provide primary design coverage, a proven Template Reference baseline, an attributed Source Gap, or an approved waiver for page surface "${surface.id}".`,
713
+ status: "blocked",
714
+ owner: "source_preparation",
715
+ });
716
+ }
717
+ }
718
+
719
+ const artifact = {
720
+ schema_version: DESIGN_SOURCE_PACKAGE_SCHEMA,
721
+ package_id: String(packageId || "design-source-package"),
722
+ source_kind: "html_funnel",
723
+ generated_at: String(generatedAt),
724
+ material_fingerprint: `${MATERIAL_FINGERPRINT_PREFIX}${"0".repeat(64)}`,
725
+ surface_identity: surfaces,
726
+ contributions,
727
+ source_gaps: clonedGaps,
728
+ source_todos: generatedTodos,
729
+ waivers: clonedWaivers,
730
+ divergences: cloneJsonArray(divergences),
731
+ proposed_exceptions: cloneJsonArray(proposedExceptions),
732
+ notes: notes.map((note) => String(note)),
733
+ readiness: null,
734
+ readback: null,
735
+ };
736
+ artifact.readiness = evaluateDesignSourcePackageReadiness(artifact, {
737
+ generatedAt: String(generatedAt),
738
+ now: readinessNow,
739
+ });
740
+ artifact.readback = generateDesignSourcePackageReadback(artifact);
741
+ artifact.material_fingerprint = computeDesignSourcePackageMaterialFingerprint(artifact);
742
+ return artifact;
743
+ }
744
+
745
+ export function evaluateDesignSourcePackageReadiness(value, {
746
+ generatedAt = value?.generated_at || new Date().toISOString(),
747
+ now = Date.now(),
748
+ } = {}) {
749
+ const surfaces = arrayOrEmpty(value?.surface_identity);
750
+ const pageSurfaces = surfaces.filter((surface) => surface?.kind === "page");
751
+ const contributions = arrayOrEmpty(value?.contributions);
752
+ const gaps = arrayOrEmpty(value?.source_gaps);
753
+ const todos = arrayOrEmpty(value?.source_todos);
754
+ const waivers = arrayOrEmpty(value?.waivers);
755
+ const blockingReasons = [];
756
+ const coverageClaimsBySurface = indexCoverageClaimsBySurface(contributions);
757
+ const readinessExceptions = indexReadinessExceptions(gaps, waivers, now);
758
+
759
+ if (pageSurfaces.length === 0) {
760
+ blockingReasons.push("No page-level Surface Identity is available for source-readiness evaluation.");
761
+ }
762
+
763
+ for (const surface of pageSurfaces) {
764
+ const claims = coverageClaimsBySurface.get(surface.id) || [];
765
+ const primaryClaims = sortCoverageClaims(claims.filter(({ mapping }) =>
766
+ mapping.coverage_role === "primary_design" && ["high", "medium"].includes(mapping.confidence)));
767
+ const validPrimary = primaryClaims.some(primaryClaimHasRequiredProof);
768
+ const validBaseline = claims.some((claim) =>
769
+ claim.mapping.coverage_role === "template_baseline"
770
+ && claim.template_reference_complete === true);
771
+ const coverageExcepted = readinessExceptions.hasCoverage(surface.id, "primary_coverage");
772
+
773
+ if (!primaryClaims.length && !validBaseline && !coverageExcepted) {
774
+ const lowClaim = claims.find(({ mapping }) =>
775
+ mapping.coverage_role === "primary_design" && ["low", "unknown"].includes(mapping.confidence));
776
+ blockingReasons.push(lowClaim
777
+ ? `Page surface "${surface.id}" has only low or unknown confidence primary_design coverage.`
778
+ : `Page surface "${surface.id}" lacks non-low primary_design, proven template_baseline, an accepted Source Gap, or an approved waiver.`);
779
+ }
780
+
781
+ if (primaryClaims.length && !validPrimary
782
+ && !readinessExceptions.hasCoverage(surface.id, "source_screenshot")) {
783
+ blockingReasons.push(`Renderable primary_design surface "${surface.id}" has no qualifying claim with linked desktop and mobile source_screenshot proof.`);
784
+ }
785
+ }
786
+
787
+ for (const todo of todos) {
788
+ if (todo?.status === "completed") continue;
789
+ if (readinessExceptions.hasWaiverForRecord(todo)) continue;
790
+ blockingReasons.push(`Source TODO "${todo?.id || "unknown"}" is ${todo?.status}.`);
791
+ }
792
+ for (const gap of gaps) {
793
+ if (gap?.status === "proposed") {
794
+ blockingReasons.push(`Source Gap "${gap.id || "unknown"}" remains proposed and unattributed as accepted coverage.`);
795
+ }
796
+ }
797
+ for (const divergence of arrayOrEmpty(value?.divergences)) {
798
+ if (divergence?.readiness_affecting === true && divergence?.status === "proposed") {
799
+ blockingReasons.push(`Readiness-affecting Source Divergence "${divergence.id || "unknown"}" remains proposed.`);
800
+ }
801
+ }
802
+ for (const exception of arrayOrEmpty(value?.proposed_exceptions)) {
803
+ if (exception?.readiness_affecting === true && exception?.status === "proposed") {
804
+ blockingReasons.push(`Readiness-affecting proposed exception "${exception.id || "unknown"}" is not accepted.`);
805
+ }
806
+ }
807
+
808
+ const uniqueBlockingReasons = uniqueStrings(blockingReasons);
809
+ const activeWaivers = readinessExceptions.activeWaivers;
810
+ const acceptedGaps = gaps.filter((gap) => gap?.status === "accepted");
811
+ const status = pageSurfaces.length === 0
812
+ ? "pending"
813
+ : uniqueBlockingReasons.length
814
+ ? "blocked"
815
+ : activeWaivers.length
816
+ ? "ready_with_waivers"
817
+ : acceptedGaps.length
818
+ ? "ready_with_gaps"
819
+ : "ready";
820
+
821
+ return {
822
+ status,
823
+ blocking_reasons: uniqueBlockingReasons,
824
+ gap_count: gaps.length,
825
+ todo_count: todos.length,
826
+ waiver_count: waivers.length,
827
+ generated_at: String(generatedAt),
828
+ };
829
+ }
830
+
831
+ export function generateDesignSourcePackageReadback(value) {
832
+ const readiness = value?.readiness || evaluateDesignSourcePackageReadiness(value);
833
+ const contributions = arrayOrEmpty(value?.contributions);
834
+ const gaps = arrayOrEmpty(value?.source_gaps);
835
+ const todos = arrayOrEmpty(value?.source_todos);
836
+ const waivers = arrayOrEmpty(value?.waivers);
837
+ const handled = [];
838
+ for (const contribution of contributions) {
839
+ for (const mapping of arrayOrEmpty(contribution?.mappings)) {
840
+ handled.push(`${mapping.surface_id}: ${mapping.coverage_role} (${mapping.confidence}) via ${contribution.id}`);
841
+ }
842
+ }
843
+ const nextActions = arrayOrEmpty(readiness.blocking_reasons).length
844
+ ? arrayOrEmpty(readiness.blocking_reasons).map((reason) => `Resolve: ${reason}`)
845
+ : ["Proceed to Build with this Design Source Package material fingerprint."];
846
+ return {
847
+ summary: `Design Source Package is ${readiness.status}; ${contributions.length} contribution(s), ${handled.length} coverage mapping(s), ${gaps.length} gap(s), ${todos.length} TODO(s), and ${waivers.length} waiver(s).`,
848
+ included_sources: sortStrings(contributions.map((contribution) => `${contribution.id} (${contribution.kind})`)),
849
+ handled: sortStrings(handled),
850
+ blockers: sortStrings(arrayOrEmpty(readiness.blocking_reasons)),
851
+ gaps: sortStrings(gaps.map((gap) => `${gap.id}: ${gap.reason}`)),
852
+ todos: sortStrings(todos.map((todo) => `${todo.id}: ${todo.description}`)),
853
+ waivers: sortStrings(waivers.map((waiver) => `${waiver.id}: ${waiver.reason}`)),
854
+ next_actions: sortStrings(nextActions),
855
+ };
856
+ }
857
+
858
+ /**
859
+ * Explicit v0 material projection. It intentionally does not spread whole
860
+ * records: generated_at, readiness/readback prose, notes, key order, and visual
861
+ * captured_at are administrative. Every ADR-0001 material class is listed
862
+ * below. A fingerprint is always `sha256:` followed by 64 lowercase hex chars.
863
+ */
864
+ export function designSourcePackageMaterialProjection(value) {
865
+ if (!isObject(value)) throw new TypeError("Design Source Package must be an object.");
866
+ return {
867
+ schema_version: value.schema_version,
868
+ source_kind: value.source_kind,
869
+ surface_identity: arrayOrEmpty(value.surface_identity)
870
+ .map(projectSurfaceIdentity)
871
+ .sort(compareById),
872
+ contributions: arrayOrEmpty(value.contributions)
873
+ .map(projectContribution)
874
+ .sort(compareById),
875
+ source_gaps: arrayOrEmpty(value.source_gaps)
876
+ .map(projectGap)
877
+ .sort(compareById),
878
+ source_todos: arrayOrEmpty(value.source_todos)
879
+ .map(projectTodo)
880
+ .sort(compareById),
881
+ accepted_waivers: arrayOrEmpty(value.waivers)
882
+ .filter((waiver) => waiver?.status === "approved")
883
+ .map(projectWaiver)
884
+ .sort(compareById),
885
+ readiness_affecting_divergences: arrayOrEmpty(value.divergences)
886
+ .filter((divergence) => divergence?.readiness_affecting === true)
887
+ .map(projectDivergence)
888
+ .sort(compareById),
889
+ readiness_affecting_exceptions: arrayOrEmpty(value.proposed_exceptions)
890
+ .filter((exception) => exception?.readiness_affecting === true)
891
+ .map(projectException)
892
+ .sort(compareById),
893
+ };
894
+ }
895
+
896
+ export function computeDesignSourcePackageMaterialFingerprint(value) {
897
+ const canonical = canonicalJson(designSourcePackageMaterialProjection(value));
898
+ return `${MATERIAL_FINGERPRINT_PREFIX}${createHash("sha256").update(canonical).digest("hex")}`;
899
+ }
900
+
901
+ export function serializeDesignSourcePackage(value) {
902
+ return `${JSON.stringify(canonicalize(value), null, 2)}\n`;
903
+ }
904
+
905
+ export function hashDesignSourcePackage(value) {
906
+ return hashSerializedDesignSourcePackage(serializeDesignSourcePackage(value));
907
+ }
908
+
909
+ export function hashSerializedDesignSourcePackage(serialized) {
910
+ if (!(typeof serialized === "string" || serialized instanceof Uint8Array || serialized instanceof ArrayBuffer)) {
911
+ throw new TypeError("Serialized Design Source Package must be a string, Uint8Array, or ArrayBuffer of exact artifact bytes.");
912
+ }
913
+ const bytes = serialized instanceof ArrayBuffer ? new Uint8Array(serialized) : serialized;
914
+ return `${MATERIAL_FINGERPRINT_PREFIX}${createHash("sha256").update(bytes).digest("hex")}`;
915
+ }
916
+
917
+ export function createDesignSourcePackageArtifactReference(value, {
918
+ path = DESIGN_SOURCE_PACKAGE_REL_PATH,
919
+ serialized = null,
920
+ } = {}) {
921
+ return {
922
+ path,
923
+ schema_version: DESIGN_SOURCE_PACKAGE_SCHEMA,
924
+ sha256: serialized == null ? hashDesignSourcePackage(value) : hashSerializedDesignSourcePackage(serialized),
925
+ material_fingerprint: computeDesignSourcePackageMaterialFingerprint(value),
926
+ };
927
+ }
928
+
929
+ export function validateDesignSourcePackage(value, {
930
+ now = Date.now(),
931
+ verifyFingerprint = true,
932
+ currentPageScope = null,
933
+ currentHtmlFunnelScope = null,
934
+ } = {}) {
935
+ const errors = [];
936
+ const warnings = [];
937
+ const add = (code, path, message) => errors.push({ code, path, message });
938
+ if (!checkStrictObject(value, "$", TOP_LEVEL_FIELDS, [
939
+ "schema_version",
940
+ "package_id",
941
+ "source_kind",
942
+ "generated_at",
943
+ "material_fingerprint",
944
+ "surface_identity",
945
+ "contributions",
946
+ "source_gaps",
947
+ "source_todos",
948
+ "waivers",
949
+ "divergences",
950
+ "proposed_exceptions",
951
+ "notes",
952
+ "readiness",
953
+ "readback",
954
+ ], add)) return { ok: false, errors, warnings };
955
+
956
+ if (value.schema_version !== DESIGN_SOURCE_PACKAGE_SCHEMA) {
957
+ add("design_source_package.schema_version", "$.schema_version", `schema_version must be ${DESIGN_SOURCE_PACKAGE_SCHEMA}.`);
958
+ }
959
+ checkNonEmptyString(value.package_id, "$.package_id", add);
960
+ checkNonEmptyString(value.source_kind, "$.source_kind", add);
961
+ checkNonEmptyString(value.generated_at, "$.generated_at", add);
962
+ if (!DESIGN_SOURCE_PACKAGE_FINGERPRINT_PATTERN.test(value.material_fingerprint || "")) {
963
+ add("design_source_package.material_fingerprint", "$.material_fingerprint", "material_fingerprint must use sha256:<64 lowercase hex>." );
964
+ }
965
+
966
+ validateStringArray(value.notes, "$.notes", add);
967
+ const surfaces = validateArray(value.surface_identity, "$.surface_identity", add);
968
+ const surfaceIds = new Set();
969
+ let campaignCount = 0;
970
+ surfaces.forEach((surface, index) => {
971
+ const path = `$.surface_identity[${index}]`;
972
+ if (!validateSurface(surface, path, add)) return;
973
+ if (surfaceIds.has(surface.id)) add("design_source_package.surface_id_duplicate", `${path}.id`, `Duplicate Surface Identity id "${surface.id}".`);
974
+ surfaceIds.add(surface.id);
975
+ if (surface.id === "campaign" && surface.kind === "campaign") campaignCount += 1;
976
+ if (surface.id === "campaign" && surface.kind !== "campaign") {
977
+ add("design_source_package.campaign_surface_kind", `${path}.kind`, "Reserved Surface Identity campaign must use kind campaign.");
978
+ }
979
+ if (surface.kind === "campaign" && surface.id !== "campaign") {
980
+ add("design_source_package.campaign_surface_id", `${path}.id`, "kind campaign is reserved for Surface Identity id campaign.");
981
+ }
982
+ });
983
+ if (campaignCount !== 1) {
984
+ add("design_source_package.campaign_surface", "$.surface_identity", "Exactly one reserved {id: campaign, kind: campaign} Surface Identity is required.");
985
+ }
986
+ if (currentPageScope != null) {
987
+ validateCurrentPageScope(value, currentPageScope, add);
988
+ }
989
+ if (currentHtmlFunnelScope != null) {
990
+ validateCurrentHtmlFunnelScope(value, currentHtmlFunnelScope, add);
991
+ }
992
+
993
+ const contributions = validateArray(value.contributions, "$.contributions", add);
994
+ const contributionIds = new Set();
995
+ contributions.forEach((contribution, index) => {
996
+ const path = `$.contributions[${index}]`;
997
+ if (!validateContribution(contribution, path, surfaceIds, add)) return;
998
+ if (contributionIds.has(contribution.id)) add("design_source_package.contribution_id_duplicate", `${path}.id`, `Duplicate contribution id "${contribution.id}".`);
999
+ contributionIds.add(contribution.id);
1000
+ });
1001
+
1002
+ validateRecordArray(value.source_gaps, "$.source_gaps", GAP_FIELDS,
1003
+ ["id", "kind", "scope", "applies_to", "reason", "status", "attributed_by"], add,
1004
+ (record, path) => validateGap(record, path, surfaceIds, add));
1005
+ validateRecordArray(value.source_todos, "$.source_todos", TODO_FIELDS,
1006
+ ["id", "kind", "scope", "applies_to", "description", "status", "owner"], add,
1007
+ (record, path) => validateTodo(record, path, surfaceIds, add));
1008
+ validateRecordArray(value.waivers, "$.waivers", WAIVER_FIELDS,
1009
+ ["id", "scope", "applies_to", "reason", "status", "waived_by", "waived_at"], add,
1010
+ (record, path) => validateWaiver(record, path, add));
1011
+ validateRecordArray(value.divergences, "$.divergences", DIVERGENCE_FIELDS,
1012
+ ["id", "scope", "applies_to", "summary", "status", "recorded_stage", "readiness_affecting", "attributed_by"], add,
1013
+ (record, path) => validateDivergence(record, path, surfaceIds, add));
1014
+ validateRecordArray(value.proposed_exceptions, "$.proposed_exceptions", EXCEPTION_FIELDS,
1015
+ ["id", "scope", "applies_to", "reason", "status", "readiness_affecting", "proposed_by"], add,
1016
+ (record, path) => validateException(record, path, surfaceIds, add));
1017
+ validateUniqueRecordIds(value.source_gaps, "$.source_gaps", add);
1018
+ validateUniqueRecordIds(value.source_todos, "$.source_todos", add);
1019
+ validateUniqueRecordIds(value.waivers, "$.waivers", add);
1020
+ validateUniqueRecordIds(value.divergences, "$.divergences", add);
1021
+ validateUniqueRecordIds(value.proposed_exceptions, "$.proposed_exceptions", add);
1022
+ const knownWaiverTargets = collectPackageReferenceIds(value);
1023
+ arrayOrEmpty(value.waivers).forEach((waiver, waiverIndex) => {
1024
+ arrayOrEmpty(waiver?.applies_to).forEach((target, targetIndex) => {
1025
+ if (!knownWaiverTargets.has(target) && !EXTERNAL_WAIVER_TARGETS.has(target)) {
1026
+ add(
1027
+ "design_source_package.waiver_target_missing",
1028
+ `$.waivers[${waiverIndex}].applies_to[${targetIndex}]`,
1029
+ `Waiver target "${target}" does not reference a package record, Surface Identity, or supported source-freshness target.`,
1030
+ );
1031
+ }
1032
+ });
1033
+ });
1034
+ const validationReadinessExceptions = indexReadinessExceptions(
1035
+ value.source_gaps,
1036
+ value.waivers,
1037
+ now,
1038
+ );
1039
+ arrayOrEmpty(value.source_todos).forEach((todo, todoIndex) => {
1040
+ if (todo?.status === "skipped" && !validationReadinessExceptions.hasWaiverForRecord(todo)) {
1041
+ add(
1042
+ "design_source_package.todo_skipped_without_waiver",
1043
+ `$.source_todos[${todoIndex}].status`,
1044
+ "A skipped Source TODO remains blocking unless an approved, scoped waiver targets it or its affected surface.",
1045
+ );
1046
+ }
1047
+ });
1048
+ validateReadiness(value.readiness, "$.readiness", add);
1049
+ validateReadback(value.readback, "$.readback", add);
1050
+
1051
+ let authoritativeReadiness = null;
1052
+ if (isObject(value.readiness)) {
1053
+ authoritativeReadiness = evaluateDesignSourcePackageReadiness(value, {
1054
+ generatedAt: value.readiness.generated_at,
1055
+ now,
1056
+ });
1057
+ for (const field of ["status", "gap_count", "todo_count", "waiver_count"]) {
1058
+ if (value.readiness[field] !== authoritativeReadiness[field]) {
1059
+ add("design_source_package.readiness_contradiction", `$.readiness.${field}`, `Generated readiness ${field} must be ${JSON.stringify(authoritativeReadiness[field])}, not ${JSON.stringify(value.readiness[field])}.`);
1060
+ }
1061
+ }
1062
+ if (!sameStringSet(value.readiness.blocking_reasons, authoritativeReadiness.blocking_reasons)) {
1063
+ add("design_source_package.readiness_blockers", "$.readiness.blocking_reasons", "blocking_reasons do not match the authoritative package records and coverage semantics.");
1064
+ }
1065
+ }
1066
+ if (isObject(value.readback) && authoritativeReadiness) {
1067
+ const expectedReadback = generateDesignSourcePackageReadback({
1068
+ ...value,
1069
+ readiness: authoritativeReadiness,
1070
+ });
1071
+ if (canonicalJson(value.readback) !== canonicalJson(expectedReadback)) {
1072
+ add("design_source_package.readback_contradiction", "$.readback", "Readback must be generated from the current authoritative package records and readiness summary.");
1073
+ }
1074
+ }
1075
+
1076
+ if (verifyFingerprint && DESIGN_SOURCE_PACKAGE_FINGERPRINT_PATTERN.test(value.material_fingerprint || "")) {
1077
+ const expectedFingerprint = computeDesignSourcePackageMaterialFingerprint(value);
1078
+ if (value.material_fingerprint !== expectedFingerprint) {
1079
+ add("design_source_package.material_fingerprint_stale", "$.material_fingerprint", `material_fingerprint must be ${expectedFingerprint} for the current material projection.`);
1080
+ }
1081
+ }
1082
+
1083
+ return { ok: errors.length === 0, errors, warnings };
1084
+ }
1085
+
1086
+ function validateCurrentPageScope(value, scope, add) {
1087
+ if (!isObject(scope)) {
1088
+ add("design_source_package.current_page_scope", "$", "currentPageScope must be an object when supplied.");
1089
+ return;
1090
+ }
1091
+ const activePages = scope.activePages;
1092
+ const mappings = scope.mappings;
1093
+ if (!Array.isArray(activePages) || !Array.isArray(mappings)) {
1094
+ add(
1095
+ "design_source_package.current_page_scope",
1096
+ "$",
1097
+ "currentPageScope requires activePages and mappings arrays.",
1098
+ );
1099
+ return;
1100
+ }
1101
+
1102
+ let malformedItem = false;
1103
+ activePages.forEach((page, index) => {
1104
+ if (!isObject(page) || !isNonEmptyString(page.id)) {
1105
+ malformedItem = true;
1106
+ add(
1107
+ "design_source_package.current_page_scope",
1108
+ `$.currentPageScope.activePages[${index}]`,
1109
+ `currentPageScope.activePages[${index}] must be an object with a non-empty id.`,
1110
+ );
1111
+ }
1112
+ });
1113
+ mappings.forEach((mapping, index) => {
1114
+ if (!isObject(mapping) || !isNonEmptyString(mapping.page_id)) {
1115
+ malformedItem = true;
1116
+ add(
1117
+ "design_source_package.current_page_scope",
1118
+ `$.currentPageScope.mappings[${index}]`,
1119
+ `currentPageScope.mappings[${index}] must be an object with a non-empty page_id.`,
1120
+ );
1121
+ }
1122
+ });
1123
+ if (malformedItem) return;
1124
+
1125
+ const expected = createSurfaceCatalog(collectPageInputs(activePages, mappings), {
1126
+ campaignMapId: scope.campaignMapId,
1127
+ campaignSlug: scope.campaignSlug,
1128
+ }).surfaces;
1129
+ const expectedPages = expected.filter((surface) => surface.kind === "page");
1130
+ const actualPages = arrayOrEmpty(value?.surface_identity).filter((surface) => surface?.kind === "page");
1131
+ const expectedSpecIds = new Set(activePages.map((page) => page.id));
1132
+ const expectedSourceIds = new Set(mappings.map((mapping) => mapping.page_id));
1133
+ const actualPagesBySpecId = indexRecordsByStringKey(
1134
+ actualPages,
1135
+ (surface) => surface?.mappings?.campaign_spec_page_id,
1136
+ );
1137
+ const actualPagesBySourceId = indexRecordsByStringKey(
1138
+ actualPages,
1139
+ (surface) => surface?.mappings?.source_page_id,
1140
+ );
1141
+
1142
+ const campaignSurface = arrayOrEmpty(value?.surface_identity)
1143
+ .find((surface) => surface?.id === "campaign" && surface?.kind === "campaign");
1144
+ for (const [field, expectedValue] of [
1145
+ ["campaign_map_id", optionalString(scope.campaignMapId)],
1146
+ ["campaign_slug", optionalString(scope.campaignSlug)],
1147
+ ]) {
1148
+ if (expectedValue && campaignSurface?.mappings?.[field] !== expectedValue) {
1149
+ add(
1150
+ "design_source_package.current_campaign_identity",
1151
+ `$.surface_identity[campaign].mappings.${field}`,
1152
+ `Current campaign identity requires ${field} ${JSON.stringify(expectedValue)}.`,
1153
+ );
1154
+ }
1155
+ }
1156
+
1157
+ for (const expectedSurface of expectedPages) {
1158
+ const specId = expectedSurface.mappings.campaign_spec_page_id;
1159
+ const sourceId = expectedSurface.mappings.source_page_id;
1160
+ const matches = [...new Set([
1161
+ ...(specId ? actualPagesBySpecId.get(specId) || [] : []),
1162
+ ...(sourceId ? actualPagesBySourceId.get(sourceId) || [] : []),
1163
+ ])];
1164
+ if (matches.length === 0) {
1165
+ add(
1166
+ "design_source_package.current_page_missing",
1167
+ "$.surface_identity",
1168
+ `Current active/mapped page ${JSON.stringify(specId || sourceId)} has no page-level Surface Identity.`,
1169
+ );
1170
+ continue;
1171
+ }
1172
+ if (matches.length > 1) {
1173
+ add(
1174
+ "design_source_package.current_page_duplicate",
1175
+ "$.surface_identity",
1176
+ `Current active/mapped page ${JSON.stringify(specId || sourceId)} resolves to more than one page-level Surface Identity.`,
1177
+ );
1178
+ continue;
1179
+ }
1180
+ const actual = matches[0];
1181
+ for (const field of [
1182
+ "campaign_spec_page_id",
1183
+ "map_builder_label",
1184
+ "map_builder_custom_name",
1185
+ "public_route",
1186
+ "producer_page_type",
1187
+ "source_page_id",
1188
+ "source_path",
1189
+ "page_kit",
1190
+ ]) {
1191
+ if (canonicalJson(actual?.mappings?.[field] ?? null) !== canonicalJson(expectedSurface.mappings[field] ?? null)) {
1192
+ add(
1193
+ "design_source_package.current_page_mapping_stale",
1194
+ `$.surface_identity[${actual?.id || "unknown"}].mappings.${field}`,
1195
+ `Current page ${JSON.stringify(specId || sourceId)} requires ${field} ${canonicalJson(expectedSurface.mappings[field] ?? null)}.`,
1196
+ );
1197
+ }
1198
+ }
1199
+ }
1200
+
1201
+ for (const surface of actualPages) {
1202
+ const specId = optionalString(surface?.mappings?.campaign_spec_page_id);
1203
+ const sourceId = optionalString(surface?.mappings?.source_page_id);
1204
+ if (specId && !expectedSpecIds.has(specId)) {
1205
+ add(
1206
+ "design_source_package.current_active_page_stale",
1207
+ `$.surface_identity[${surface.id}].mappings.campaign_spec_page_id`,
1208
+ `Page Surface Identity references CampaignSpec page ${JSON.stringify(specId)}, which is not active in the current build scope.`,
1209
+ );
1210
+ }
1211
+ if (sourceId && !expectedSourceIds.has(sourceId)) {
1212
+ add(
1213
+ "design_source_package.current_mapped_page_stale",
1214
+ `$.surface_identity[${surface.id}].mappings.source_page_id`,
1215
+ `Page Surface Identity references source page ${JSON.stringify(sourceId)}, which is not mapped by the current source intake.`,
1216
+ );
1217
+ }
1218
+ }
1219
+ }
1220
+
1221
+ function validateCurrentHtmlFunnelScope(value, scope, add) {
1222
+ if (!isObject(scope)) {
1223
+ add(
1224
+ "design_source_package.current_html_funnel_scope",
1225
+ "$",
1226
+ "currentHtmlFunnelScope must be an object when supplied.",
1227
+ );
1228
+ return;
1229
+ }
1230
+
1231
+ let expected;
1232
+ try {
1233
+ expected = synthesizeHtmlFunnelDesignSourcePackage({
1234
+ ...scope,
1235
+ generatedAt: value?.generated_at || new Date(0).toISOString(),
1236
+ });
1237
+ } catch (error) {
1238
+ add(
1239
+ "design_source_package.current_html_funnel_scope",
1240
+ "$",
1241
+ `Could not derive the current html_funnel material scope: ${error.message}`,
1242
+ );
1243
+ return;
1244
+ }
1245
+
1246
+ const expectedHtml = expected.contributions.find((contribution) => contribution.kind === "html_funnel");
1247
+ const actualHtmlCandidates = arrayOrEmpty(value?.contributions)
1248
+ .filter((contribution) => contribution?.kind === "html_funnel");
1249
+ if (actualHtmlCandidates.length !== 1) {
1250
+ add(
1251
+ "design_source_package.current_html_funnel_material_stale",
1252
+ "$.contributions",
1253
+ `Current html_funnel intake requires exactly one html_funnel contribution; found ${actualHtmlCandidates.length}.`,
1254
+ );
1255
+ return;
1256
+ }
1257
+ const actualHtml = actualHtmlCandidates[0];
1258
+
1259
+ if (canonicalJson(projectProvenance(actualHtml.provenance)) !== canonicalJson(projectProvenance(expectedHtml.provenance))) {
1260
+ add(
1261
+ "design_source_package.current_html_funnel_material_stale",
1262
+ `$.contributions[${actualHtml.id}].provenance`,
1263
+ "HTML source root, manifest/provenance, or asset-crawl provenance does not match the current prepare-build inputs.",
1264
+ );
1265
+ }
1266
+
1267
+ const actualRefs = arrayOrEmpty(actualHtml.source_refs);
1268
+ const actualRefMaterialKeys = new Set(actualRefs.map(currentSourceRefMaterialKey));
1269
+ for (const expectedRef of arrayOrEmpty(expectedHtml.source_refs)) {
1270
+ if (!actualRefMaterialKeys.has(currentSourceRefMaterialKey(expectedRef))) {
1271
+ add(
1272
+ "design_source_package.current_source_material_stale",
1273
+ `$.contributions[${actualHtml.id}].source_refs`,
1274
+ `Current source material ${JSON.stringify(expectedRef.path || expectedRef.url)} is missing or has stale kind, role, or byte hash.`,
1275
+ );
1276
+ }
1277
+ }
1278
+
1279
+ const expectedRefsById = new Map(arrayOrEmpty(expectedHtml.source_refs).map((ref) => [ref.id, ref]));
1280
+ const actualRefsById = new Map(actualRefs.map((ref) => [ref?.id, ref]));
1281
+ const actualMappingsByCoverage = new Map();
1282
+ for (const mapping of arrayOrEmpty(actualHtml.mappings)) {
1283
+ const linkedRefKeys = new Set(arrayOrEmpty(mapping?.source_refs)
1284
+ .map((id) => actualRefsById.get(id))
1285
+ .filter(Boolean)
1286
+ .map((ref) => canonicalJson(projectCurrentSourceRef(ref))));
1287
+ appendMapArray(actualMappingsByCoverage, coverageLookupKey(mapping), linkedRefKeys);
1288
+ }
1289
+ for (const expectedMapping of arrayOrEmpty(expectedHtml.mappings)) {
1290
+ const expectedLinkedRefKeys = arrayOrEmpty(expectedMapping.source_refs)
1291
+ .map((id) => expectedRefsById.get(id))
1292
+ .filter(Boolean)
1293
+ .map((ref) => canonicalJson(projectCurrentSourceRef(ref)));
1294
+ const candidates = actualMappingsByCoverage.get(coverageLookupKey(expectedMapping)) || [];
1295
+ const currentMappingExists = candidates.some((actualLinkedRefKeys) =>
1296
+ expectedLinkedRefKeys.every((expectedRefKey) => actualLinkedRefKeys.has(expectedRefKey)));
1297
+ if (!currentMappingExists) {
1298
+ add(
1299
+ "design_source_package.current_html_funnel_material_stale",
1300
+ `$.contributions[${actualHtml.id}].mappings`,
1301
+ `HTML coverage for current page surface ${JSON.stringify(expectedMapping.surface_id)} is missing or stale.`,
1302
+ );
1303
+ }
1304
+ }
1305
+
1306
+ validateCurrentTemplateMaterial(value, expected, scope.templateFamily, add);
1307
+ }
1308
+
1309
+ function projectCurrentSourceRef(ref) {
1310
+ return compactDefined({
1311
+ ...pickFields(ref, ["kind", "path", "url", "role", "media_type"]),
1312
+ sha256: canonicalHashSpelling(ref?.sha256),
1313
+ });
1314
+ }
1315
+
1316
+ function currentSourceRefMaterialKey(ref) {
1317
+ return `${optionalString(ref?.path) || ""}\u0000${optionalString(ref?.url) || ""}\u0000${canonicalJson(projectCurrentSourceRef(ref))}`;
1318
+ }
1319
+
1320
+ function coverageLookupKey(mapping) {
1321
+ return `${mapping?.surface_id || ""}\u0000${mapping?.coverage_role || ""}\u0000${mapping?.confidence || ""}`;
1322
+ }
1323
+
1324
+ function validateCurrentTemplateMaterial(value, expected, templateFamily, add) {
1325
+ const expectedTemplate = expected.contributions.find((contribution) => contribution.kind === "template_stock");
1326
+ const actualTemplates = arrayOrEmpty(value?.contributions)
1327
+ .filter((contribution) => contribution?.kind === "template_stock");
1328
+ if (!expectedTemplate) {
1329
+ if (actualTemplates.length) {
1330
+ add(
1331
+ "design_source_package.current_template_material_stale",
1332
+ "$.contributions",
1333
+ "The Design Source Package selects template-stock material, but current prepare-build inputs do not select a template family.",
1334
+ );
1335
+ }
1336
+ return;
1337
+ }
1338
+
1339
+ const normalized = normalizeTemplateFamily(templateFamily);
1340
+ const actualTemplate = actualTemplates.find((contribution) => contribution?.id === expectedTemplate.id)
1341
+ || (actualTemplates.length === 1 ? actualTemplates[0] : null);
1342
+ const declaredFamily = optionalString(actualTemplate?.template_reference?.family);
1343
+ const familyMatches = declaredFamily
1344
+ ? declaredFamily === normalized.family
1345
+ : canonicalJson(actualTemplate?.presentation_intent?.summary)
1346
+ === canonicalJson(expectedTemplate.presentation_intent?.summary);
1347
+ if (!actualTemplate || !familyMatches) {
1348
+ add(
1349
+ "design_source_package.current_template_material_stale",
1350
+ "$.contributions",
1351
+ `Template material does not match current family ${JSON.stringify(normalized.family)}.`,
1352
+ );
1353
+ return;
1354
+ }
1355
+
1356
+ if (expectedTemplate.template_reference) {
1357
+ const expectedReference = canonicalJson(projectTemplateReference(expectedTemplate.template_reference));
1358
+ if (canonicalJson(projectTemplateReference(actualTemplate.template_reference)) !== expectedReference) {
1359
+ add(
1360
+ "design_source_package.current_template_material_stale",
1361
+ "$.contributions",
1362
+ "Template Reference material does not match the current family/version/reference inputs.",
1363
+ );
1364
+ }
1365
+ }
1366
+ }
1367
+
1368
+ function projectSurfaceIdentity(surface) {
1369
+ return compactDefined({
1370
+ id: surface?.id,
1371
+ kind: surface?.kind,
1372
+ label: surface?.label,
1373
+ aliases: sortStrings(uniqueStrings(arrayOrEmpty(surface?.aliases))),
1374
+ mappings: pickFields(surface?.mappings, [...SURFACE_MAPPING_FIELDS]),
1375
+ });
1376
+ }
1377
+
1378
+ function projectContribution(contribution) {
1379
+ return compactDefined({
1380
+ id: contribution?.id,
1381
+ kind: contribution?.kind,
1382
+ trust: contribution?.trust,
1383
+ renderable: contribution?.renderable,
1384
+ provenance: projectProvenance(contribution?.provenance),
1385
+ presentation_intent: pickFields(contribution?.presentation_intent, [...PRESENTATION_INTENT_FIELDS]),
1386
+ source_refs: arrayOrEmpty(contribution?.source_refs).map(projectSourceRef).sort(compareById),
1387
+ screenshot_refs: arrayOrEmpty(contribution?.screenshot_refs).map(projectVisualRef).sort(compareById),
1388
+ reference_refs: arrayOrEmpty(contribution?.reference_refs).map(projectVisualRef).sort(compareById),
1389
+ template_reference: contribution?.template_reference ? projectTemplateReference(contribution.template_reference) : undefined,
1390
+ mappings: arrayOrEmpty(contribution?.mappings).map(projectCoverageMapping).sort(compareById),
1391
+ });
1392
+ }
1393
+
1394
+ function projectSourceRef(ref) {
1395
+ return compactDefined({
1396
+ ...pickFields(ref, ["id", "kind", "path", "url", "role", "media_type"]),
1397
+ sha256: canonicalHashSpelling(ref?.sha256),
1398
+ });
1399
+ }
1400
+
1401
+ function projectVisualRef(ref) {
1402
+ return compactDefined({
1403
+ ...pickFields(ref, [
1404
+ "id",
1405
+ "kind",
1406
+ "viewport",
1407
+ "availability",
1408
+ "url",
1409
+ "path",
1410
+ "width",
1411
+ "height",
1412
+ "device_profile",
1413
+ "scale_factor",
1414
+ "browser",
1415
+ "source_ref_id",
1416
+ "unavailable_reason",
1417
+ ]),
1418
+ sha256: canonicalHashSpelling(ref?.sha256),
1419
+ });
1420
+ }
1421
+
1422
+ function projectTemplateReference(reference) {
1423
+ return compactDefined({
1424
+ ...pickFields(reference, ["id", "family", "version", "contract_path", "artifact_path"]),
1425
+ sha256: canonicalHashSpelling(reference?.sha256),
1426
+ standard_viewport_refs: arrayOrEmpty(reference?.standard_viewport_refs)
1427
+ .map(projectVisualRef)
1428
+ .sort(compareById),
1429
+ });
1430
+ }
1431
+
1432
+ function projectProvenance(provenance) {
1433
+ return compactDefined({
1434
+ ...pickFields(provenance, [
1435
+ "source_type",
1436
+ "adapter",
1437
+ "producer",
1438
+ "generator",
1439
+ "generator_version",
1440
+ "source_root",
1441
+ "manifest_schema_version",
1442
+ "manifest_path",
1443
+ "asset_crawl_schema_version",
1444
+ ]),
1445
+ manifest_sha256: canonicalHashSpelling(provenance?.manifest_sha256),
1446
+ producer_material_fingerprint: canonicalProducerMaterialFingerprint(
1447
+ provenance?.producer_material_fingerprint,
1448
+ ),
1449
+ });
1450
+ }
1451
+
1452
+ function projectCoverageMapping(mapping) {
1453
+ return compactDefined({
1454
+ id: mapping?.id,
1455
+ surface_id: mapping?.surface_id,
1456
+ coverage_role: mapping?.coverage_role,
1457
+ confidence: mapping?.confidence,
1458
+ source_refs: sortStrings(uniqueStrings(arrayOrEmpty(mapping?.source_refs))),
1459
+ screenshot_refs: sortStrings(uniqueStrings(arrayOrEmpty(mapping?.screenshot_refs))),
1460
+ reference_refs: sortStrings(uniqueStrings(arrayOrEmpty(mapping?.reference_refs))),
1461
+ template_reference_id: mapping?.template_reference_id,
1462
+ });
1463
+ }
1464
+
1465
+ function projectGap(gap) {
1466
+ return compactDefined({
1467
+ ...pickFields(gap, ["id", "kind", "scope", "reason", "status", "attributed_by", "attributed_at"]),
1468
+ applies_to: sortStrings(uniqueStrings(arrayOrEmpty(gap?.applies_to))),
1469
+ evidence_refs: sortStrings(uniqueStrings(arrayOrEmpty(gap?.evidence_refs))),
1470
+ });
1471
+ }
1472
+
1473
+ function projectTodo(todo) {
1474
+ return compactDefined({
1475
+ ...pickFields(todo, ["id", "kind", "scope", "description", "status", "owner"]),
1476
+ applies_to: sortStrings(uniqueStrings(arrayOrEmpty(todo?.applies_to))),
1477
+ required_viewports: sortStrings(uniqueStrings(arrayOrEmpty(todo?.required_viewports))),
1478
+ source_ref_ids: sortStrings(uniqueStrings(arrayOrEmpty(todo?.source_ref_ids))),
1479
+ });
1480
+ }
1481
+
1482
+ function projectWaiver(waiver) {
1483
+ return compactDefined({
1484
+ ...pickFields(waiver, ["id", "scope", "reason", "status", "waived_by", "waived_at", "expires_at", "review_condition"]),
1485
+ applies_to: sortStrings(uniqueStrings(arrayOrEmpty(waiver?.applies_to))),
1486
+ evidence_refs: sortStrings(uniqueStrings(arrayOrEmpty(waiver?.evidence_refs))),
1487
+ });
1488
+ }
1489
+
1490
+ function projectDivergence(divergence) {
1491
+ return compactDefined({
1492
+ ...pickFields(divergence, ["id", "scope", "summary", "status", "recorded_stage", "readiness_affecting", "attributed_by"]),
1493
+ applies_to: sortStrings(uniqueStrings(arrayOrEmpty(divergence?.applies_to))),
1494
+ evidence_refs: sortStrings(uniqueStrings(arrayOrEmpty(divergence?.evidence_refs))),
1495
+ });
1496
+ }
1497
+
1498
+ function projectException(exception) {
1499
+ return compactDefined({
1500
+ ...pickFields(exception, ["id", "scope", "reason", "status", "readiness_affecting", "proposed_by"]),
1501
+ applies_to: sortStrings(uniqueStrings(arrayOrEmpty(exception?.applies_to))),
1502
+ evidence_refs: sortStrings(uniqueStrings(arrayOrEmpty(exception?.evidence_refs))),
1503
+ });
1504
+ }
1505
+
1506
+ function collectPageInputs(activePages, mappings) {
1507
+ const byId = new Map();
1508
+ for (const page of activePages) byId.set(page.id, { pageId: page.id, page, mapping: null });
1509
+ for (const mapping of mappings) {
1510
+ const current = byId.get(mapping.page_id) || { pageId: mapping.page_id, page: null, mapping: null };
1511
+ if (!current.mapping) current.mapping = mapping;
1512
+ byId.set(mapping.page_id, current);
1513
+ }
1514
+ return [...byId.values()];
1515
+ }
1516
+
1517
+ function createSurfaceCatalog(pages, { campaignMapId, campaignSlug }) {
1518
+ const surfaces = [{
1519
+ id: "campaign",
1520
+ kind: "campaign",
1521
+ label: "Campaign",
1522
+ aliases: uniqueStrings([campaignMapId, campaignSlug].filter(Boolean)),
1523
+ mappings: compactDefined({
1524
+ campaign_map_id: optionalString(campaignMapId),
1525
+ campaign_slug: optionalString(campaignSlug),
1526
+ }),
1527
+ }];
1528
+ const used = new Set(["campaign"]);
1529
+ const roleCounts = new Map();
1530
+ const surfaceByPageId = new Map();
1531
+ pages.forEach(({ pageId, page, mapping }, index) => {
1532
+ const role = normalizedPageRole(page?.type || mapping?.page_type);
1533
+ roleCounts.set(role, (roleCounts.get(role) || 0) + 1);
1534
+ let id = SAFE_SURFACE_ID.test(pageId) && pageId !== "campaign"
1535
+ ? pageId
1536
+ : roleSurfaceId(role, roleCounts.get(role), index + 1);
1537
+ id = uniqueSurfaceId(id, used);
1538
+ surfaceByPageId.set(pageId, id);
1539
+ const pageKit = normalizePageKitProjection(mapping?.page_kit);
1540
+ const route = optionalString(pageKit?.public_route)
1541
+ || optionalString(page?.page_url)
1542
+ || optionalString(page?.url);
1543
+ const label = optionalString(page?.label)
1544
+ || optionalString(page?.custom_name)
1545
+ || optionalString(page?.name)
1546
+ || pageId;
1547
+ const aliases = uniqueStrings([
1548
+ pageId,
1549
+ page?.label,
1550
+ page?.custom_name,
1551
+ page?.name,
1552
+ page?.type,
1553
+ mapping?.page_type,
1554
+ mapping?.path,
1555
+ route,
1556
+ pageKit?.page_type,
1557
+ pageKit?.target_path,
1558
+ pageKit?.output_path,
1559
+ ].filter(isNonEmptyString));
1560
+ surfaces.push({
1561
+ id,
1562
+ kind: "page",
1563
+ label,
1564
+ aliases,
1565
+ mappings: {
1566
+ campaign_spec_page_id: page ? pageId : null,
1567
+ map_builder_label: optionalString(page?.label),
1568
+ map_builder_custom_name: optionalString(page?.custom_name) || optionalString(page?.name),
1569
+ public_route: route,
1570
+ producer_page_type: optionalString(mapping?.page_type),
1571
+ source_page_id: mapping ? pageId : null,
1572
+ source_path: optionalString(mapping?.path),
1573
+ page_kit: pageKit,
1574
+ },
1575
+ });
1576
+ });
1577
+ return { surfaces, surfaceByPageId };
1578
+ }
1579
+
1580
+ function normalizePageKitProjection(value) {
1581
+ if (!isObject(value)) return null;
1582
+ if (![value.target_path, value.output_path, value.public_route, value.page_type].every(isNonEmptyString)) return null;
1583
+ return compactDefined({
1584
+ target_path: value.target_path,
1585
+ output_path: value.output_path,
1586
+ public_route: value.public_route,
1587
+ spec_route: value.spec_route == null ? null : String(value.spec_route),
1588
+ page_type: value.page_type,
1589
+ permalink_required: typeof value.permalink_required === "boolean" ? value.permalink_required : null,
1590
+ }, { preserveNull: true });
1591
+ }
1592
+
1593
+ function normalizeManifestInput(input, explicitPath) {
1594
+ if (isObject(input?.manifest)) {
1595
+ return { value: input.manifest, path: optionalString(explicitPath) || optionalString(input.path) };
1596
+ }
1597
+ return { value: isObject(input) ? input : null, path: optionalString(explicitPath) };
1598
+ }
1599
+
1600
+ function indexManifestPagesById(pages) {
1601
+ const byId = new Map();
1602
+ for (const entry of arrayOrEmpty(pages)) {
1603
+ if (!isObject(entry) || !isNonEmptyString(entry.page_id)) continue;
1604
+ if (byId.has(entry.page_id)) {
1605
+ throw new TypeError(`manifest.pages contains duplicate page_id ${JSON.stringify(entry.page_id)}.`);
1606
+ }
1607
+ byId.set(entry.page_id, entry);
1608
+ }
1609
+ return byId;
1610
+ }
1611
+
1612
+ function normalizePresentationIntent(value, fallbackSummary) {
1613
+ const input = isObject(value) ? value : {};
1614
+ const out = { summary: optionalString(input.summary) || fallbackSummary };
1615
+ for (const field of ["composition", "content_hierarchy", "imagery", "copy", "brand", "responsive_behavior"]) {
1616
+ if (Array.isArray(input[field])) out[field] = input[field].filter(isNonEmptyString).map((entry) => entry.trim());
1617
+ }
1618
+ return out;
1619
+ }
1620
+
1621
+ function normalizeTemplateFamily(value) {
1622
+ if (isNonEmptyString(value)) return { family: value.trim(), version: null, reference: null, presentation_intent: null };
1623
+ if (!isObject(value)) return { family: null, version: null, reference: null, presentation_intent: null };
1624
+ const reference = isObject(value.template_reference)
1625
+ ? value.template_reference
1626
+ : isObject(value.templateReference)
1627
+ ? value.templateReference
1628
+ : isObject(value.reference)
1629
+ ? value.reference
1630
+ : value;
1631
+ return {
1632
+ family: optionalString(value.family) || optionalString(value.value) || optionalString(value.name) || optionalString(reference.family),
1633
+ version: optionalString(value.version) || optionalString(value.commerce_catalog_version) || optionalString(reference.version),
1634
+ reference,
1635
+ presentation_intent: isObject(value.presentation_intent) ? value.presentation_intent : null,
1636
+ };
1637
+ }
1638
+
1639
+ function normalizeTemplateReference(template) {
1640
+ if (!template.family || !isObject(template.reference)) return null;
1641
+ const raw = template.reference;
1642
+ const family = optionalString(raw.family) || template.family;
1643
+ const version = optionalString(raw.version) || template.version;
1644
+ const contractPath = optionalString(raw.contract_path) || optionalString(raw.contractPath);
1645
+ const artifactPath = optionalString(raw.artifact_path) || optionalString(raw.artifactPath);
1646
+ if (!version || (!contractPath && !artifactPath)) return null;
1647
+ const referenceId = optionalString(raw.id) || `template-reference-${slugify(family)}`;
1648
+ const sourceRefId = artifactPath ? `${referenceId}-artifact` : `${referenceId}-contract`;
1649
+ const registry = createVisualReferenceRegistry("template_reference_screenshot");
1650
+ for (const ref of arrayOrEmpty(raw.standard_viewport_refs || raw.standardViewportRefs)) {
1651
+ registry.add(ref, `template-${slugify(family) || "reference"}`, sourceRefId);
1652
+ }
1653
+ return compactDefined({
1654
+ id: referenceId,
1655
+ family,
1656
+ version,
1657
+ contract_path: contractPath,
1658
+ artifact_path: artifactPath,
1659
+ sha256: validSha(raw.sha256 || raw.hash),
1660
+ standard_viewport_refs: registry.values(),
1661
+ });
1662
+ }
1663
+
1664
+ function assessTemplateReference(reference, sourceRefs = templateSourceRefs(reference)) {
1665
+ if (!reference) {
1666
+ return { linked: false, complete: false, missing_viewports: [...REQUIRED_SOURCE_VIEWPORTS] };
1667
+ }
1668
+ const sourceRefIds = new Set(arrayOrEmpty(sourceRefs).map((ref) => typeof ref === "string" ? ref : ref?.id));
1669
+ const available = new Set(arrayOrEmpty(reference.standard_viewport_refs)
1670
+ .filter((ref) => ref.kind === "template_reference_screenshot"
1671
+ && ref.availability === "available"
1672
+ && sourceRefIds.has(ref.source_ref_id))
1673
+ .map((ref) => ref.viewport));
1674
+ const missing = REQUIRED_SOURCE_VIEWPORTS.filter((viewport) => !available.has(viewport));
1675
+ return { linked: true, complete: missing.length === 0, missing_viewports: missing };
1676
+ }
1677
+
1678
+ function templateSourceRefs(reference) {
1679
+ if (!reference) return [];
1680
+ const refs = [];
1681
+ if (reference.contract_path) refs.push({
1682
+ id: `${reference.id}-contract`,
1683
+ kind: "document",
1684
+ path: reference.contract_path,
1685
+ ...(reference.sha256 ? { sha256: reference.sha256 } : {}),
1686
+ role: "template_reference_contract",
1687
+ });
1688
+ if (reference.artifact_path) refs.push({
1689
+ id: `${reference.id}-artifact`,
1690
+ kind: "export",
1691
+ path: reference.artifact_path,
1692
+ ...(reference.sha256 ? { sha256: reference.sha256 } : {}),
1693
+ role: "template_reference_artifact",
1694
+ });
1695
+ return refs;
1696
+ }
1697
+
1698
+ function indexCoverageClaimsBySurface(contributions) {
1699
+ const claimsBySurface = new Map();
1700
+ for (const contribution of arrayOrEmpty(contributions)) {
1701
+ const contributionSourceIds = new Set(
1702
+ arrayOrEmpty(contribution?.source_refs).map((ref) => ref?.id),
1703
+ );
1704
+ const screenshotRefsById = new Map(
1705
+ arrayOrEmpty(contribution?.screenshot_refs).map((ref) => [ref?.id, ref]),
1706
+ );
1707
+ const templateReferenceComplete = contribution?.template_reference
1708
+ ? assessTemplateReference(contribution.template_reference, contribution.source_refs).complete
1709
+ : false;
1710
+ for (const mapping of arrayOrEmpty(contribution?.mappings)) {
1711
+ const mappingSourceIds = new Set(arrayOrEmpty(mapping?.source_refs));
1712
+ const availableViewports = new Set();
1713
+ for (const screenshotId of arrayOrEmpty(mapping?.screenshot_refs)) {
1714
+ const ref = screenshotRefsById.get(screenshotId);
1715
+ if (ref?.kind === "source_screenshot"
1716
+ && ref.availability === "available"
1717
+ && mappingSourceIds.has(ref.source_ref_id)
1718
+ && contributionSourceIds.has(ref.source_ref_id)
1719
+ && VIEWPORTS.has(ref.viewport)) {
1720
+ availableViewports.add(ref.viewport);
1721
+ }
1722
+ }
1723
+ appendMapArray(claimsBySurface, mapping?.surface_id, {
1724
+ contribution,
1725
+ mapping,
1726
+ available_viewports: availableViewports,
1727
+ template_reference_complete: templateReferenceComplete
1728
+ && mapping?.template_reference_id === contribution?.template_reference?.id,
1729
+ });
1730
+ }
1731
+ }
1732
+ return claimsBySurface;
1733
+ }
1734
+
1735
+ function primaryClaimHasRequiredProof(claim) {
1736
+ if (claim?.contribution?.renderable !== true) return true;
1737
+ const viewports = claim.available_viewports || new Set();
1738
+ return REQUIRED_SOURCE_VIEWPORTS.every((viewport) => viewports.has(viewport));
1739
+ }
1740
+
1741
+ function missingRequiredViewportsForPrimaryClaim(claim) {
1742
+ if (claim?.contribution?.renderable !== true) return [];
1743
+ const viewports = claim.available_viewports || new Set();
1744
+ return REQUIRED_SOURCE_VIEWPORTS.filter((viewport) => !viewports.has(viewport));
1745
+ }
1746
+
1747
+ function sortCoverageClaims(claims) {
1748
+ return [...claims].sort((left, right) => compareStrings(
1749
+ `${left?.contribution?.id || ""}\u0000${left?.mapping?.id || ""}`,
1750
+ `${right?.contribution?.id || ""}\u0000${right?.mapping?.id || ""}`,
1751
+ ));
1752
+ }
1753
+
1754
+ function bestPrimaryClaimForTodo(claims) {
1755
+ return [...claims].sort((left, right) => {
1756
+ const missingDelta = missingRequiredViewportsForPrimaryClaim(left).length
1757
+ - missingRequiredViewportsForPrimaryClaim(right).length;
1758
+ if (missingDelta !== 0) return missingDelta;
1759
+ return compareStrings(
1760
+ `${left?.contribution?.id || ""}\u0000${left?.mapping?.id || ""}`,
1761
+ `${right?.contribution?.id || ""}\u0000${right?.mapping?.id || ""}`,
1762
+ );
1763
+ })[0];
1764
+ }
1765
+
1766
+ function indexReadinessExceptions(gaps, waivers, now) {
1767
+ const coverageKeys = new Set();
1768
+ const activeWaivers = arrayOrEmpty(waivers)
1769
+ .filter((waiver) => isActiveApprovedWaiver(waiver, now));
1770
+ const waiversByTarget = new Map();
1771
+ const purposes = ["primary_coverage", "source_screenshot", "template_reference"];
1772
+
1773
+ const indexCoverageRecord = (record, matchesPurpose) => {
1774
+ for (const purpose of purposes) {
1775
+ if (!matchesPurpose(record, purpose)) continue;
1776
+ for (const target of arrayOrEmpty(record?.applies_to)) {
1777
+ coverageKeys.add(`${target}\u0000${purpose}`);
1778
+ }
1779
+ }
1780
+ };
1781
+
1782
+ for (const gap of arrayOrEmpty(gaps)) {
1783
+ if (gap?.status === "accepted") indexCoverageRecord(gap, gapMatchesPurpose);
1784
+ }
1785
+ for (const waiver of activeWaivers) {
1786
+ indexCoverageRecord(waiver, waiverMatchesPurpose);
1787
+ for (const target of arrayOrEmpty(waiver?.applies_to)) {
1788
+ appendMapArray(waiversByTarget, target, waiver);
1789
+ }
1790
+ }
1791
+
1792
+ return {
1793
+ activeWaivers,
1794
+ hasCoverage(surfaceId, purpose) {
1795
+ return coverageKeys.has(`${surfaceId}\u0000${purpose}`)
1796
+ || coverageKeys.has(`campaign\u0000${purpose}`);
1797
+ },
1798
+ hasWaiverForRecord(record) {
1799
+ const candidates = new Set([
1800
+ ...arrayOrEmpty(waiversByTarget.get(record?.id)),
1801
+ ...arrayOrEmpty(waiversByTarget.get("campaign")),
1802
+ ]);
1803
+ for (const surfaceId of arrayOrEmpty(record?.applies_to)) {
1804
+ for (const waiver of arrayOrEmpty(waiversByTarget.get(surfaceId))) candidates.add(waiver);
1805
+ }
1806
+ const purpose = purposeForTodo(record);
1807
+ return [...candidates].some((waiver) =>
1808
+ waiverMatchesPurpose(waiver, purpose, record?.scope));
1809
+ },
1810
+ };
1811
+ }
1812
+
1813
+ function gapMatchesPurpose(gap, purpose) {
1814
+ if (purpose === "primary_coverage") {
1815
+ return ["coverage_absence", "source_limitation"].includes(gap?.kind)
1816
+ && PRIMARY_COVERAGE_SCOPES.has(gap?.scope);
1817
+ }
1818
+ if (purpose === "source_screenshot") {
1819
+ return ["screenshot_absence", "source_limitation"].includes(gap?.kind)
1820
+ && SOURCE_SCREENSHOT_SCOPES.has(gap?.scope);
1821
+ }
1822
+ if (purpose === "template_reference") {
1823
+ return ["reference_absence", "source_limitation"].includes(gap?.kind)
1824
+ && TEMPLATE_REFERENCE_SCOPES.has(gap?.scope);
1825
+ }
1826
+ return false;
1827
+ }
1828
+
1829
+ function waiverMatchesPurpose(waiver, purpose, recordScope = null) {
1830
+ if (purpose === "primary_coverage") return PRIMARY_COVERAGE_SCOPES.has(waiver?.scope);
1831
+ if (purpose === "source_screenshot") return SOURCE_SCREENSHOT_SCOPES.has(waiver?.scope);
1832
+ if (purpose === "template_reference") return TEMPLATE_REFERENCE_SCOPES.has(waiver?.scope);
1833
+ return isNonEmptyString(recordScope) && waiver?.scope === recordScope;
1834
+ }
1835
+
1836
+ function purposeForTodo(todo) {
1837
+ if (todo?.kind === "missing_source_screenshot") return "source_screenshot";
1838
+ if (["missing_template_reference", "missing_template_viewport"].includes(todo?.kind)) return "template_reference";
1839
+ if (["missing_primary_design", "low_confidence_primary_design"].includes(todo?.kind)) return "primary_coverage";
1840
+ return "record_scope";
1841
+ }
1842
+
1843
+ function isActiveApprovedWaiver(waiver, now) {
1844
+ if (!isObject(waiver) || waiver.status !== "approved") return false;
1845
+ if (!isNonEmptyString(waiver.waived_by) || !isNonEmptyString(waiver.waived_at)) return false;
1846
+ if (isNonEmptyString(waiver.expires_at)) {
1847
+ const expires = Date.parse(waiver.expires_at);
1848
+ return !Number.isNaN(expires) && expires > now;
1849
+ }
1850
+ return isNonEmptyString(waiver.review_condition);
1851
+ }
1852
+
1853
+ function createSourceReferenceRegistry() {
1854
+ const records = [];
1855
+ const byKey = new Map();
1856
+ const usedIds = new Set();
1857
+ return {
1858
+ add(raw) {
1859
+ if (!isObject(raw)) return null;
1860
+ const path = optionalString(raw.path);
1861
+ const url = optionalString(raw.url);
1862
+ if (!path && !url) return null;
1863
+ const key = `${path || ""}\u0000${url || ""}`;
1864
+ const existing = byKey.get(key);
1865
+ if (existing) {
1866
+ if (!existing.sha256 && validSha(raw.sha256)) existing.sha256 = validSha(raw.sha256);
1867
+ if (!existing.role && isNonEmptyString(raw.role)) existing.role = raw.role;
1868
+ return existing;
1869
+ }
1870
+ const id = uniqueRecordId(optionalString(raw.id) || "source-reference", usedIds);
1871
+ const record = compactDefined({
1872
+ id,
1873
+ kind: SOURCE_REF_KINDS.has(raw.kind) ? raw.kind : "other",
1874
+ path,
1875
+ url,
1876
+ sha256: validSha(raw.sha256),
1877
+ role: optionalString(raw.role),
1878
+ media_type: optionalString(raw.media_type),
1879
+ notes: optionalString(raw.notes),
1880
+ });
1881
+ records.push(record);
1882
+ byKey.set(key, record);
1883
+ return record;
1884
+ },
1885
+ values() {
1886
+ return records.sort(compareById);
1887
+ },
1888
+ };
1889
+ }
1890
+
1891
+ function createVisualReferenceRegistry(defaultKind) {
1892
+ const records = [];
1893
+ const byId = new Map();
1894
+ const byIdentity = new Map();
1895
+ const usedIds = new Set();
1896
+ return {
1897
+ add(raw, idPrefix, defaultSourceRefId = null) {
1898
+ const contextualKinds = defaultKind === "source_screenshot"
1899
+ ? SOURCE_SCREENSHOT_KINDS
1900
+ : defaultKind === "template_reference_screenshot"
1901
+ ? TEMPLATE_SCREENSHOT_KINDS
1902
+ : COMPARISON_REFERENCE_KINDS;
1903
+ if (visualReferenceRejection(raw, contextualKinds)) return null;
1904
+ const normalized = normalizeVisualReference(raw, defaultKind, idPrefix, defaultSourceRefId);
1905
+ if (!normalized) return null;
1906
+ const explicitId = optionalString(raw?.id);
1907
+ const materialIdentity = canonicalJson(projectVisualRef({ ...normalized, id: undefined }));
1908
+ const identity = `${explicitId || ""}\u0000${materialIdentity}`;
1909
+ if (byIdentity.has(identity)) return byIdentity.get(identity);
1910
+ if (explicitId && byId.has(explicitId)) {
1911
+ throw new TypeError(`Visual reference id "${explicitId}" is reused for different evidence.`);
1912
+ }
1913
+ if (!explicitId) {
1914
+ const digest = createHash("sha256").update(materialIdentity).digest("hex").slice(0, 16);
1915
+ normalized.id = `${idPrefix || "visual"}-${normalized.viewport}-${digest}`;
1916
+ }
1917
+ normalized.id = uniqueRecordId(normalized.id, usedIds);
1918
+ records.push(normalized);
1919
+ byId.set(normalized.id, normalized);
1920
+ byIdentity.set(identity, normalized);
1921
+ return normalized;
1922
+ },
1923
+ byId(id) {
1924
+ return byId.get(id) || null;
1925
+ },
1926
+ values() {
1927
+ return records.sort(compareById);
1928
+ },
1929
+ };
1930
+ }
1931
+
1932
+ // The field tests that decide whether a visual-reference record carries
1933
+ // evidence at all, in one place, so the acceptance path (below) and the
1934
+ // operator diagnostic (diagnoseSourceScreenshotRecord) can never disagree
1935
+ // about why a record was dropped. Returns null for an acceptable record, or
1936
+ // { field, detail } naming the first field that fails.
1937
+ function visualReferenceRejection(raw, contextualKinds = null) {
1938
+ if (!isObject(raw)) {
1939
+ return { field: "record", detail: "the record is not a JSON object", fix: "Replace the record with an object" };
1940
+ }
1941
+ if (contextualKinds && isNonEmptyString(raw.kind) && !contextualKinds.has(raw.kind)) {
1942
+ return {
1943
+ field: "kind",
1944
+ detail: `kind ${JSON.stringify(raw.kind)} is not one of ${[...contextualKinds].sort().join(", ")}`,
1945
+ fix: "Fix kind on that record",
1946
+ };
1947
+ }
1948
+ const viewport = optionalString(raw.viewport || raw.viewport_key)?.toLowerCase();
1949
+ if (!VIEWPORTS.has(viewport)) {
1950
+ return {
1951
+ field: "viewport",
1952
+ detail: viewport
1953
+ ? `viewport ${JSON.stringify(viewport)} is not one of ${[...VIEWPORTS].sort().join(", ")}`
1954
+ : `viewport is missing; it must be one of ${[...VIEWPORTS].sort().join(", ")}`,
1955
+ fix: "Fix viewport on that record",
1956
+ };
1957
+ }
1958
+ const availability = visualReferenceAvailability(raw);
1959
+ if (!availability) {
1960
+ return {
1961
+ // "evidence" is a synthetic field: the fix names the three real keys.
1962
+ field: "evidence",
1963
+ detail: "the record declares no path, no url, and no unavailable_reason, so it points at no evidence",
1964
+ fix: "Set path, url, or unavailable_reason on that record",
1965
+ };
1966
+ }
1967
+ if (availability === "unavailable" && !visualReferenceUnavailableReason(raw)) {
1968
+ return {
1969
+ field: "unavailable_reason",
1970
+ detail: 'availability is "unavailable" without an unavailable_reason explaining what is missing',
1971
+ fix: "Add unavailable_reason to that record",
1972
+ };
1973
+ }
1974
+ return null;
1975
+ }
1976
+
1977
+ // A per-record diagnostic for source screenshot proof: null when the record is
1978
+ // accepted, otherwise the field that makes it unusable. Exported so manifest
1979
+ // validation can warn about a record the package build would otherwise drop in
1980
+ // silence.
1981
+ export function diagnoseSourceScreenshotRecord(raw) {
1982
+ return visualReferenceRejection(raw, SOURCE_SCREENSHOT_KINDS);
1983
+ }
1984
+
1985
+ function visualReferencePath(raw) {
1986
+ return optionalString(raw.path) || optionalString(raw.artifact_path) || optionalString(raw.file_path);
1987
+ }
1988
+
1989
+ function visualReferenceUnavailableReason(raw) {
1990
+ return optionalString(raw.unavailable_reason) || optionalString(raw.reason);
1991
+ }
1992
+
1993
+ function visualReferenceAvailability(raw) {
1994
+ const path = visualReferencePath(raw);
1995
+ const url = optionalString(raw.url) || optionalString(raw.canonical_url);
1996
+ if (raw.availability === "unavailable" || (!path && !url && visualReferenceUnavailableReason(raw))) {
1997
+ return "unavailable";
1998
+ }
1999
+ return path || url ? "available" : null;
2000
+ }
2001
+
2002
+ function normalizeVisualReference(raw, defaultKind, idPrefix, defaultSourceRefId = null) {
2003
+ if (visualReferenceRejection(raw)) return null;
2004
+ const viewport = optionalString(raw.viewport || raw.viewport_key)?.toLowerCase();
2005
+ const path = visualReferencePath(raw);
2006
+ const url = optionalString(raw.url) || optionalString(raw.canonical_url);
2007
+ const unavailableReason = visualReferenceUnavailableReason(raw);
2008
+ const availability = visualReferenceAvailability(raw);
2009
+ const dimensions = isObject(raw.dimensions) ? raw.dimensions : {};
2010
+ const kind = VISUAL_KINDS.has(raw.kind) ? raw.kind : defaultKind;
2011
+ const id = optionalString(raw.id)
2012
+ || `${idPrefix || "visual"}-${viewport}${availability === "unavailable" ? "-unavailable" : ""}`;
2013
+ return compactDefined({
2014
+ id,
2015
+ kind: availability === "unavailable" && !VISUAL_KINDS.has(raw.kind) ? "unavailable_render" : kind,
2016
+ viewport,
2017
+ availability,
2018
+ url,
2019
+ path,
2020
+ sha256: validSha(raw.sha256 || raw.artifact_hash || raw.hash),
2021
+ width: positiveInteger(raw.width ?? dimensions.width),
2022
+ height: positiveInteger(raw.height ?? dimensions.height),
2023
+ device_profile: optionalString(raw.device_profile),
2024
+ scale_factor: positiveNumber(raw.scale_factor),
2025
+ browser: optionalString(raw.browser),
2026
+ captured_at: optionalString(raw.captured_at || raw.capture_timestamp),
2027
+ source_ref_id: optionalString(raw.source_ref_id) || optionalString(defaultSourceRefId),
2028
+ unavailable_reason: unavailableReason,
2029
+ notes: optionalString(raw.notes),
2030
+ });
2031
+ }
2032
+
2033
+ function visualCandidatesFrom(value, keys) {
2034
+ if (!isObject(value)) return [];
2035
+ return keys.flatMap((key) => arrayOrEmpty(value[key]));
2036
+ }
2037
+
2038
+ function indexSectionVisualCandidates(manifest) {
2039
+ const byPageId = new Map();
2040
+ for (const section of arrayOrEmpty(manifest?.producer_provenance?.section_exports)) {
2041
+ if (!isObject(section)) continue;
2042
+ const sectionPageId = optionalString(section.page_id) || optionalString(section.campaign_spec_page_id);
2043
+ if (!sectionPageId) continue;
2044
+ for (const image of arrayOrEmpty(section.images)) {
2045
+ // A string export or an object without `viewport`/`viewport_key` is an
2046
+ // asset, not source screenshot proof.
2047
+ if (isObject(image) && isNonEmptyString(image.viewport || image.viewport_key)) {
2048
+ appendMapArray(byPageId, sectionPageId, image);
2049
+ }
2050
+ }
2051
+ }
2052
+ return byPageId;
2053
+ }
2054
+
2055
+ function indexVisualCandidatesByPage(records, surfaceByPageId) {
2056
+ const byPageId = new Map();
2057
+ const pageIdBySurfaceId = new Map(
2058
+ [...surfaceByPageId].map(([pageId, surfaceId]) => [surfaceId, pageId]),
2059
+ );
2060
+ for (const ref of arrayOrEmpty(records)) {
2061
+ if (!isObject(ref)) continue;
2062
+ const pageIds = new Set();
2063
+ for (const pageId of [ref.page_id, ref.campaign_spec_page_id]) {
2064
+ if (isNonEmptyString(pageId) && surfaceByPageId.has(pageId)) pageIds.add(pageId);
2065
+ }
2066
+ const surfacePageId = pageIdBySurfaceId.get(ref.surface_id);
2067
+ if (surfacePageId) pageIds.add(surfacePageId);
2068
+ for (const pageId of pageIds) appendMapArray(byPageId, pageId, ref);
2069
+ }
2070
+ return byPageId;
2071
+ }
2072
+
2073
+ function confidenceForHtmlMapping(mapping, manifestPage) {
2074
+ if (manifestPage && manifestPage.path === mapping?.path) return "high";
2075
+ return "medium";
2076
+ }
2077
+
2078
+ function sourceReferenceKindForManifestFile(file) {
2079
+ if (file.role === "page" || /\.html?$/i.test(file.path || "")) return "html_file";
2080
+ if (file.role === "asset") return "asset";
2081
+ if (file.role === "export_log") return "export";
2082
+ return "other";
2083
+ }
2084
+
2085
+ function normalizedPageRole(type) {
2086
+ const normalized = slugify(type || "page");
2087
+ if (["landing", "product", "presell", "advertorial"].includes(normalized)) return "landing";
2088
+ if (["checkout", "select", "cart"].includes(normalized)) return "checkout";
2089
+ if (["upsell", "oto", "one-time-offer"].includes(normalized)) return "upsell";
2090
+ if (["downsell", "downsell-offer"].includes(normalized)) return "downsell";
2091
+ if (["receipt", "thank-you", "thankyou", "confirmation"].includes(normalized)) return "receipt";
2092
+ return normalized || "page";
2093
+ }
2094
+
2095
+ function roleSurfaceId(role, ordinal, fallbackOrdinal) {
2096
+ if (["landing", "checkout", "receipt"].includes(role) && ordinal === 1) return role;
2097
+ return `${role || "page"}-${ordinal || fallbackOrdinal}`;
2098
+ }
2099
+
2100
+ function uniqueSurfaceId(value, used) {
2101
+ const base = SAFE_SURFACE_ID.test(value || "") && value !== "campaign" ? value : "page";
2102
+ let candidate = base;
2103
+ let ordinal = 2;
2104
+ while (used.has(candidate)) candidate = `${base}-${ordinal++}`;
2105
+ used.add(candidate);
2106
+ return candidate;
2107
+ }
2108
+
2109
+ function uniqueRecordId(value, used) {
2110
+ const base = optionalString(value) || "record";
2111
+ let candidate = base;
2112
+ let ordinal = 2;
2113
+ while (used.has(candidate)) candidate = `${base}-${ordinal++}`;
2114
+ used.add(candidate);
2115
+ return candidate;
2116
+ }
2117
+
2118
+ function normalizeConfidence(value) {
2119
+ return MAPPING_CONFIDENCE_LEVELS.includes(value) ? value : null;
2120
+ }
2121
+
2122
+ function validateSurface(surface, path, add) {
2123
+ if (!checkStrictObject(surface, path, SURFACE_FIELDS, ["id", "kind", "label", "aliases", "mappings"], add)) return false;
2124
+ checkNonEmptyString(surface.id, `${path}.id`, add);
2125
+ if (isNonEmptyString(surface.id) && !SAFE_SURFACE_ID.test(surface.id)) add("design_source_package.surface_id", `${path}.id`, "Surface Identity id must be a stable lowercase human-semantic id.");
2126
+ checkEnum(surface.kind, SURFACE_KINDS, `${path}.kind`, add);
2127
+ checkNonEmptyString(surface.label, `${path}.label`, add);
2128
+ validateStringArray(surface.aliases, `${path}.aliases`, add, { unique: true, nonEmpty: true });
2129
+ if (checkStrictObject(surface.mappings, `${path}.mappings`, SURFACE_MAPPING_FIELDS, [], add)) {
2130
+ for (const field of SURFACE_MAPPING_FIELDS) {
2131
+ if (field === "page_kit") continue;
2132
+ if (surface.mappings[field] != null && !isNonEmptyString(surface.mappings[field])) {
2133
+ add("design_source_package.surface_mapping", `${path}.mappings.${field}`, `${field} must be a non-empty string or null when present.`);
2134
+ }
2135
+ }
2136
+ if (surface.kind === "page") {
2137
+ for (const field of [
2138
+ "campaign_spec_page_id",
2139
+ "map_builder_label",
2140
+ "map_builder_custom_name",
2141
+ "public_route",
2142
+ "producer_page_type",
2143
+ "source_page_id",
2144
+ "source_path",
2145
+ "page_kit",
2146
+ ]) {
2147
+ if (!Object.hasOwn(surface.mappings, field)) add("design_source_package.page_surface_mapping", `${path}.mappings.${field}`, `Page Surface Identity mappings must preserve the distinct ${field} slot (use null when unavailable).`);
2148
+ }
2149
+ }
2150
+ if (surface.mappings.page_kit != null) validatePageKit(surface.mappings.page_kit, `${path}.mappings.page_kit`, add);
2151
+ }
2152
+ return true;
2153
+ }
2154
+
2155
+ function validatePageKit(value, path, add) {
2156
+ if (!checkStrictObject(value, path, PAGE_KIT_FIELDS, ["target_path", "output_path", "public_route", "page_type"], add)) return;
2157
+ for (const field of ["target_path", "output_path", "public_route", "page_type"]) checkNonEmptyString(value[field], `${path}.${field}`, add);
2158
+ if (value.spec_route != null && typeof value.spec_route !== "string") add("design_source_package.page_kit", `${path}.spec_route`, "spec_route must be a string or null.");
2159
+ if (value.permalink_required != null && typeof value.permalink_required !== "boolean") add("design_source_package.page_kit", `${path}.permalink_required`, "permalink_required must be a boolean or null.");
2160
+ }
2161
+
2162
+ function validateContribution(contribution, path, surfaceIds, add) {
2163
+ if (!checkStrictObject(contribution, path, CONTRIBUTION_FIELDS, [
2164
+ "id", "kind", "trust", "renderable", "provenance", "presentation_intent",
2165
+ "source_refs", "screenshot_refs", "reference_refs", "mappings",
2166
+ ], add)) return false;
2167
+ checkNonEmptyString(contribution.id, `${path}.id`, add);
2168
+ checkEnum(contribution.kind, CONTRIBUTION_KINDS, `${path}.kind`, add);
2169
+ checkEnum(contribution.trust, CONTRIBUTION_TRUST, `${path}.trust`, add);
2170
+ if (typeof contribution.renderable !== "boolean") add("design_source_package.contribution_renderable", `${path}.renderable`, "renderable must be a boolean.");
2171
+ validateProvenance(contribution.provenance, `${path}.provenance`, add);
2172
+ validatePresentationIntent(contribution.presentation_intent, `${path}.presentation_intent`, add);
2173
+ const sourceRefs = validateArray(contribution.source_refs, `${path}.source_refs`, add);
2174
+ const screenshotRefs = validateArray(contribution.screenshot_refs, `${path}.screenshot_refs`, add);
2175
+ const referenceRefs = validateArray(contribution.reference_refs, `${path}.reference_refs`, add);
2176
+ const sourceIds = validateReferenceArray(sourceRefs, `${path}.source_refs`, "source", add);
2177
+ const screenshotIds = validateReferenceArray(screenshotRefs, `${path}.screenshot_refs`, "source_visual", add, sourceIds);
2178
+ const referenceIds = validateReferenceArray(referenceRefs, `${path}.reference_refs`, "comparison_visual", add, sourceIds);
2179
+ if (contribution.template_reference != null) validateTemplateReference(contribution.template_reference, `${path}.template_reference`, add, sourceIds);
2180
+ if (contribution.notes != null && typeof contribution.notes !== "string") add("design_source_package.notes", `${path}.notes`, "notes must be a string or null.");
2181
+ const mappings = validateArray(contribution.mappings, `${path}.mappings`, add);
2182
+ const mappingIds = new Set();
2183
+ mappings.forEach((mapping, index) => {
2184
+ const mappingPath = `${path}.mappings[${index}]`;
2185
+ if (!validateCoverageMapping(mapping, mappingPath, add)) return;
2186
+ if (mappingIds.has(mapping.id)) add("design_source_package.mapping_id_duplicate", `${mappingPath}.id`, `Duplicate mapping id "${mapping.id}" within contribution.`);
2187
+ mappingIds.add(mapping.id);
2188
+ if (!surfaceIds.has(mapping.surface_id)) add("design_source_package.mapping_surface_ref", `${mappingPath}.surface_id`, `Unknown Surface Identity "${mapping.surface_id}".`);
2189
+ validateIdReferences(mapping.source_refs, sourceIds, `${mappingPath}.source_refs`, "source reference", add);
2190
+ validateIdReferences(mapping.screenshot_refs, screenshotIds, `${mappingPath}.screenshot_refs`, "screenshot reference", add);
2191
+ validateIdReferences(mapping.reference_refs, referenceIds, `${mappingPath}.reference_refs`, "reference", add);
2192
+ if (mapping.coverage_role === "template_baseline") {
2193
+ if (!contribution.template_reference) {
2194
+ add("design_source_package.template_baseline_reference", mappingPath, "template_baseline coverage requires contribution.template_reference proof.");
2195
+ } else if (mapping.template_reference_id !== contribution.template_reference.id) {
2196
+ add("design_source_package.template_baseline_link", `${mappingPath}.template_reference_id`, "template_reference_id must match the contribution Template Reference id.");
2197
+ } else if (!assessTemplateReference(contribution.template_reference, contribution.source_refs).complete) {
2198
+ add("design_source_package.template_baseline_viewports", mappingPath, "template_baseline proof requires available desktop and mobile standard viewport refs.");
2199
+ }
2200
+ }
2201
+ });
2202
+ return true;
2203
+ }
2204
+
2205
+ function validateProvenance(value, path, add) {
2206
+ if (!checkStrictObject(value, path, PROVENANCE_FIELDS, ["source_type", "adapter"], add)) return;
2207
+ checkNonEmptyString(value.source_type, `${path}.source_type`, add);
2208
+ checkNonEmptyString(value.adapter, `${path}.adapter`, add);
2209
+ for (const field of PROVENANCE_FIELDS) {
2210
+ if (["source_type", "adapter", "manifest_sha256"].includes(field)) continue;
2211
+ if (value[field] != null && !isNonEmptyString(value[field])) add("design_source_package.provenance", `${path}.${field}`, `${field} must be a non-empty string or null.`);
2212
+ }
2213
+ if (value.manifest_sha256 != null && !SHA256_PATTERN.test(value.manifest_sha256)) add("design_source_package.sha256", `${path}.manifest_sha256`, "manifest_sha256 must be a sha256 digest or null.");
2214
+ }
2215
+
2216
+ function validatePresentationIntent(value, path, add) {
2217
+ if (!checkStrictObject(value, path, PRESENTATION_INTENT_FIELDS, ["summary"], add)) return;
2218
+ checkNonEmptyString(value.summary, `${path}.summary`, add);
2219
+ for (const field of PRESENTATION_INTENT_FIELDS) {
2220
+ if (field !== "summary" && value[field] != null) validateStringArray(value[field], `${path}.${field}`, add, { nonEmpty: true });
2221
+ }
2222
+ }
2223
+
2224
+ function validateReferenceArray(records, path, kind, add, sourceIds = new Set()) {
2225
+ const ids = new Set();
2226
+ records.forEach((record, index) => {
2227
+ const recordPath = `${path}[${index}]`;
2228
+ if (kind === "source") validateSourceReference(record, recordPath, add);
2229
+ else {
2230
+ validateVisualReference(record, recordPath, add);
2231
+ const allowedKinds = kind === "source_visual"
2232
+ ? SOURCE_SCREENSHOT_KINDS
2233
+ : kind === "template_visual"
2234
+ ? TEMPLATE_SCREENSHOT_KINDS
2235
+ : COMPARISON_REFERENCE_KINDS;
2236
+ if (isObject(record) && !allowedKinds.has(record.kind)) {
2237
+ add("design_source_package.visual_kind_context", `${recordPath}.kind`, `${record.kind} is not valid in ${kind} evidence.`);
2238
+ }
2239
+ if (isNonEmptyString(record?.source_ref_id) && !sourceIds.has(record.source_ref_id)) {
2240
+ add("design_source_package.visual_source_ref", `${recordPath}.source_ref_id`, `Unknown source reference id "${record.source_ref_id}".`);
2241
+ }
2242
+ }
2243
+ if (isNonEmptyString(record?.id)) {
2244
+ if (ids.has(record.id)) add("design_source_package.reference_id_duplicate", `${recordPath}.id`, `Duplicate reference id "${record.id}".`);
2245
+ ids.add(record.id);
2246
+ }
2247
+ });
2248
+ return ids;
2249
+ }
2250
+
2251
+ function validateSourceReference(value, path, add) {
2252
+ if (!checkStrictObject(value, path, SOURCE_REF_FIELDS, ["id", "kind"], add)) return;
2253
+ checkNonEmptyString(value.id, `${path}.id`, add);
2254
+ checkEnum(value.kind, SOURCE_REF_KINDS, `${path}.kind`, add);
2255
+ if (!isNonEmptyString(value.path) && !isNonEmptyString(value.url)) add("design_source_package.source_ref_location", path, "Source reference requires path or url.");
2256
+ for (const field of ["path", "url", "role", "media_type"]) if (value[field] != null) checkNonEmptyString(value[field], `${path}.${field}`, add);
2257
+ if (value.sha256 != null && !SHA256_PATTERN.test(value.sha256)) add("design_source_package.sha256", `${path}.sha256`, "sha256 must be 64 lowercase hex, with optional sha256: prefix.");
2258
+ if (value.notes != null && typeof value.notes !== "string") add("design_source_package.notes", `${path}.notes`, "notes must be a string or null.");
2259
+ }
2260
+
2261
+ function validateVisualReference(value, path, add) {
2262
+ if (!checkStrictObject(value, path, VISUAL_REF_FIELDS, ["id", "kind", "viewport", "availability"], add)) return;
2263
+ checkNonEmptyString(value.id, `${path}.id`, add);
2264
+ checkEnum(value.kind, VISUAL_KINDS, `${path}.kind`, add);
2265
+ checkEnum(value.viewport, VIEWPORTS, `${path}.viewport`, add);
2266
+ checkEnum(value.availability, new Set(["available", "unavailable"]), `${path}.availability`, add);
2267
+ if (value.availability === "available" && !isNonEmptyString(value.path) && !isNonEmptyString(value.url)) add("design_source_package.visual_location", path, "Available visual reference requires path or url.");
2268
+ if (value.availability === "unavailable" && !isNonEmptyString(value.unavailable_reason)) add("design_source_package.visual_unavailable", `${path}.unavailable_reason`, "Unavailable visual reference requires unavailable_reason.");
2269
+ for (const field of ["path", "url", "device_profile", "browser", "captured_at", "source_ref_id", "unavailable_reason"]) if (value[field] != null) checkNonEmptyString(value[field], `${path}.${field}`, add);
2270
+ for (const field of ["width", "height"]) if (value[field] != null && (!Number.isInteger(value[field]) || value[field] < 1)) add("design_source_package.visual_dimensions", `${path}.${field}`, `${field} must be a positive integer.`);
2271
+ if (value.scale_factor != null && (typeof value.scale_factor !== "number" || !(value.scale_factor > 0))) add("design_source_package.visual_scale", `${path}.scale_factor`, "scale_factor must be positive.");
2272
+ if (value.sha256 != null && !SHA256_PATTERN.test(value.sha256)) add("design_source_package.sha256", `${path}.sha256`, "sha256 must be 64 lowercase hex, with optional sha256: prefix.");
2273
+ if (["source_screenshot", "template_reference_screenshot"].includes(value.kind)
2274
+ && value.availability === "available"
2275
+ && !isNonEmptyString(value.source_ref_id)) {
2276
+ add("design_source_package.visual_source_ref", `${path}.source_ref_id`, "Available source/template screenshot proof requires source_ref_id linkage.");
2277
+ }
2278
+ if (value.notes != null && typeof value.notes !== "string") add("design_source_package.notes", `${path}.notes`, "notes must be a string or null.");
2279
+ }
2280
+
2281
+ function validateTemplateReference(value, path, add, sourceIds) {
2282
+ if (!checkStrictObject(value, path, TEMPLATE_REFERENCE_FIELDS, ["id", "family", "version", "standard_viewport_refs"], add)) return;
2283
+ for (const field of ["id", "family", "version"]) checkNonEmptyString(value[field], `${path}.${field}`, add);
2284
+ if (!isNonEmptyString(value.contract_path) && !isNonEmptyString(value.artifact_path)) add("design_source_package.template_reference_location", path, "Template Reference requires contract_path or artifact_path.");
2285
+ for (const field of ["contract_path", "artifact_path"]) if (value[field] != null) checkNonEmptyString(value[field], `${path}.${field}`, add);
2286
+ if (value.sha256 != null && !SHA256_PATTERN.test(value.sha256)) add("design_source_package.sha256", `${path}.sha256`, "sha256 must be 64 lowercase hex, with optional sha256: prefix.");
2287
+ const refs = validateArray(value.standard_viewport_refs, `${path}.standard_viewport_refs`, add);
2288
+ validateReferenceArray(refs, `${path}.standard_viewport_refs`, "template_visual", add, sourceIds);
2289
+ }
2290
+
2291
+ function validateCoverageMapping(value, path, add) {
2292
+ if (!checkStrictObject(value, path, COVERAGE_MAPPING_FIELDS, [
2293
+ "id", "surface_id", "coverage_role", "confidence", "source_refs", "screenshot_refs", "reference_refs",
2294
+ ], add)) return false;
2295
+ checkNonEmptyString(value.id, `${path}.id`, add);
2296
+ checkNonEmptyString(value.surface_id, `${path}.surface_id`, add);
2297
+ checkEnum(value.coverage_role, new Set(COVERAGE_ROLES), `${path}.coverage_role`, add);
2298
+ checkEnum(value.confidence, new Set(MAPPING_CONFIDENCE_LEVELS), `${path}.confidence`, add);
2299
+ validateStringArray(value.source_refs, `${path}.source_refs`, add, { nonEmpty: true });
2300
+ validateStringArray(value.screenshot_refs, `${path}.screenshot_refs`, add, { nonEmpty: true });
2301
+ validateStringArray(value.reference_refs, `${path}.reference_refs`, add, { nonEmpty: true });
2302
+ if (value.coverage_role === "template_baseline" && !isNonEmptyString(value.template_reference_id)) add("design_source_package.template_baseline_link", `${path}.template_reference_id`, "template_baseline requires template_reference_id.");
2303
+ if (value.notes != null && typeof value.notes !== "string") add("design_source_package.notes", `${path}.notes`, "notes must be a string or null.");
2304
+ return true;
2305
+ }
2306
+
2307
+ function validateGap(value, path, surfaceIds, add) {
2308
+ checkEnum(value.kind, new Set(["coverage_absence", "screenshot_absence", "reference_absence", "source_limitation", "other"]), `${path}.kind`, add);
2309
+ validateScopedRecord(value, path, ["reason", "attributed_by"], surfaceIds, add);
2310
+ checkEnum(value.status, new Set(["proposed", "accepted", "resolved"]), `${path}.status`, add);
2311
+ if (value.attributed_at != null && typeof value.attributed_at !== "string") add("design_source_package.gap_attribution", `${path}.attributed_at`, "attributed_at must be a string or null.");
2312
+ if (value.evidence_refs != null) validateStringArray(value.evidence_refs, `${path}.evidence_refs`, add, { nonEmpty: true });
2313
+ }
2314
+
2315
+ function validateTodo(value, path, surfaceIds, add) {
2316
+ checkEnum(value.kind, new Set([
2317
+ "missing_source_screenshot", "missing_template_reference", "missing_template_viewport",
2318
+ "missing_primary_design", "low_confidence_primary_design", "unreadable_reference", "other",
2319
+ ]), `${path}.kind`, add);
2320
+ validateScopedRecord(value, path, ["description", "owner"], surfaceIds, add);
2321
+ checkEnum(value.status, new Set(["pending", "blocked", "completed", "skipped"]), `${path}.status`, add);
2322
+ if (value.required_viewports != null) {
2323
+ const viewports = validateStringArray(value.required_viewports, `${path}.required_viewports`, add, { unique: true, nonEmpty: true });
2324
+ viewports.forEach((viewport, index) => checkEnum(viewport, VIEWPORTS, `${path}.required_viewports[${index}]`, add));
2325
+ }
2326
+ if (value.source_ref_ids != null) validateStringArray(value.source_ref_ids, `${path}.source_ref_ids`, add, { nonEmpty: true });
2327
+ }
2328
+
2329
+ function validateWaiver(value, path, add) {
2330
+ validateScopedRecord(value, path, ["reason", "waived_by", "waived_at"], null, add);
2331
+ checkEnum(value.status, new Set(["proposed", "approved", "revoked", "expired"]), `${path}.status`, add);
2332
+ if (!isNonEmptyString(value.expires_at) && !isNonEmptyString(value.review_condition)) add("design_source_package.waiver_time_bound", path, "Waiver requires expires_at or review_condition.");
2333
+ for (const field of ["expires_at", "review_condition"]) if (value[field] != null) checkNonEmptyString(value[field], `${path}.${field}`, add);
2334
+ if (value.evidence_refs != null) validateStringArray(value.evidence_refs, `${path}.evidence_refs`, add, { nonEmpty: true });
2335
+ }
2336
+
2337
+ function validateDivergence(value, path, surfaceIds, add) {
2338
+ validateScopedRecord(value, path, ["summary", "attributed_by"], surfaceIds, add);
2339
+ checkEnum(value.status, new Set(["proposed", "accepted", "rejected", "resolved"]), `${path}.status`, add);
2340
+ checkEnum(value.recorded_stage, new Set(["prepare_build", "build", "polish", "qa", "operator"]), `${path}.recorded_stage`, add);
2341
+ if (typeof value.readiness_affecting !== "boolean") add("design_source_package.readiness_affecting", `${path}.readiness_affecting`, "readiness_affecting must be a boolean.");
2342
+ if (value.evidence_refs != null) validateStringArray(value.evidence_refs, `${path}.evidence_refs`, add, { nonEmpty: true });
2343
+ }
2344
+
2345
+ function validateException(value, path, surfaceIds, add) {
2346
+ validateScopedRecord(value, path, ["reason", "proposed_by"], surfaceIds, add);
2347
+ checkEnum(value.status, new Set(["proposed", "accepted", "rejected"]), `${path}.status`, add);
2348
+ if (typeof value.readiness_affecting !== "boolean") add("design_source_package.readiness_affecting", `${path}.readiness_affecting`, "readiness_affecting must be a boolean.");
2349
+ if (value.evidence_refs != null) validateStringArray(value.evidence_refs, `${path}.evidence_refs`, add, { nonEmpty: true });
2350
+ }
2351
+
2352
+ function validateScopedRecord(value, path, requiredStrings, surfaceIds, add) {
2353
+ checkNonEmptyString(value.id, `${path}.id`, add);
2354
+ checkNonEmptyString(value.scope, `${path}.scope`, add);
2355
+ for (const field of requiredStrings) checkNonEmptyString(value[field], `${path}.${field}`, add);
2356
+ const appliesTo = validateStringArray(value.applies_to, `${path}.applies_to`, add, { unique: true, nonEmpty: true, minItems: 1 });
2357
+ if (surfaceIds) appliesTo.forEach((id, index) => {
2358
+ if (!surfaceIds.has(id)) add("design_source_package.applies_to_surface", `${path}.applies_to[${index}]`, `Unknown Surface Identity "${id}".`);
2359
+ });
2360
+ }
2361
+
2362
+ function validateReadiness(value, path, add) {
2363
+ if (!checkStrictObject(value, path, READINESS_FIELDS, ["status", "blocking_reasons", "gap_count", "todo_count", "waiver_count", "generated_at"], add)) return;
2364
+ checkEnum(value.status, new Set(SOURCE_READINESS_STATUSES), `${path}.status`, add);
2365
+ validateStringArray(value.blocking_reasons, `${path}.blocking_reasons`, add, { nonEmpty: true });
2366
+ for (const field of ["gap_count", "todo_count", "waiver_count"]) if (!Number.isInteger(value[field]) || value[field] < 0) add("design_source_package.readiness_count", `${path}.${field}`, `${field} must be a non-negative integer.`);
2367
+ checkNonEmptyString(value.generated_at, `${path}.generated_at`, add);
2368
+ }
2369
+
2370
+ function validateReadback(value, path, add) {
2371
+ if (!checkStrictObject(value, path, READBACK_FIELDS, [...READBACK_FIELDS], add)) return;
2372
+ checkNonEmptyString(value.summary, `${path}.summary`, add);
2373
+ for (const field of READBACK_FIELDS) if (field !== "summary") validateStringArray(value[field], `${path}.${field}`, add, { nonEmpty: true });
2374
+ }
2375
+
2376
+ function validateRecordArray(value, path, allowed, required, add, validateRecord) {
2377
+ const records = validateArray(value, path, add);
2378
+ records.forEach((record, index) => {
2379
+ const recordPath = `${path}[${index}]`;
2380
+ if (checkStrictObject(record, recordPath, allowed, required, add)) validateRecord(record, recordPath);
2381
+ });
2382
+ }
2383
+
2384
+ function validateUniqueRecordIds(value, path, add) {
2385
+ const ids = new Set();
2386
+ arrayOrEmpty(value).forEach((record, index) => {
2387
+ if (!isNonEmptyString(record?.id)) return;
2388
+ if (ids.has(record.id)) add("design_source_package.record_id_duplicate", `${path}[${index}].id`, `Duplicate record id "${record.id}".`);
2389
+ ids.add(record.id);
2390
+ });
2391
+ }
2392
+
2393
+ function validateIdReferences(values, known, path, label, add) {
2394
+ arrayOrEmpty(values).forEach((id, index) => {
2395
+ if (!known.has(id)) add("design_source_package.reference_missing", `${path}[${index}]`, `Unknown ${label} id "${id}".`);
2396
+ });
2397
+ }
2398
+
2399
+ function collectPackageReferenceIds(value) {
2400
+ const ids = new Set(arrayOrEmpty(value?.surface_identity).map((record) => record?.id).filter(isNonEmptyString));
2401
+ const addRecords = (records) => {
2402
+ for (const record of arrayOrEmpty(records)) if (isNonEmptyString(record?.id)) ids.add(record.id);
2403
+ };
2404
+ addRecords(value?.source_gaps);
2405
+ addRecords(value?.source_todos);
2406
+ addRecords(value?.divergences);
2407
+ addRecords(value?.proposed_exceptions);
2408
+ for (const contribution of arrayOrEmpty(value?.contributions)) {
2409
+ if (isNonEmptyString(contribution?.id)) ids.add(contribution.id);
2410
+ addRecords(contribution?.source_refs);
2411
+ addRecords(contribution?.screenshot_refs);
2412
+ addRecords(contribution?.reference_refs);
2413
+ addRecords(contribution?.mappings);
2414
+ if (isNonEmptyString(contribution?.template_reference?.id)) ids.add(contribution.template_reference.id);
2415
+ addRecords(contribution?.template_reference?.standard_viewport_refs);
2416
+ }
2417
+ return ids;
2418
+ }
2419
+
2420
+ function checkStrictObject(value, path, allowed, required, add) {
2421
+ if (!isObject(value)) {
2422
+ add("design_source_package.type", path, `${path} must be an object.`);
2423
+ return false;
2424
+ }
2425
+ for (const field of required) if (!Object.hasOwn(value, field)) add("design_source_package.required", `${path}.${field}`, `${field} is required.`);
2426
+ for (const field of Object.keys(value)) if (!allowed.has(field)) add("design_source_package.additional_property", `${path}.${field}`, `Unknown property "${field}" is not allowed.`);
2427
+ return true;
2428
+ }
2429
+
2430
+ function checkNonEmptyString(value, path, add) {
2431
+ if (!isNonEmptyString(value)) add("design_source_package.string", path, `${path} must be a non-empty string.`);
2432
+ }
2433
+
2434
+ function checkEnum(value, allowed, path, add) {
2435
+ if (!allowed.has(value)) add("design_source_package.enum", path, `${path} must be one of ${[...allowed].join(", ")}.`);
2436
+ }
2437
+
2438
+ function validateArray(value, path, add) {
2439
+ if (!Array.isArray(value)) {
2440
+ add("design_source_package.array", path, `${path} must be an array.`);
2441
+ return [];
2442
+ }
2443
+ return value;
2444
+ }
2445
+
2446
+ function validateStringArray(value, path, add, { unique = false, nonEmpty = false, minItems = 0 } = {}) {
2447
+ const array = validateArray(value, path, add);
2448
+ if (array.length < minItems) add("design_source_package.array_min_items", path, `${path} must contain at least ${minItems} item(s).`);
2449
+ const seen = new Set();
2450
+ array.forEach((entry, index) => {
2451
+ if (typeof entry !== "string" || (nonEmpty && !isNonEmptyString(entry))) add("design_source_package.string_array", `${path}[${index}]`, `${path}[${index}] must be ${nonEmpty ? "a non-empty " : "a "}string.`);
2452
+ if (unique && seen.has(entry)) add("design_source_package.array_unique", `${path}[${index}]`, `${path} must not contain duplicate values.`);
2453
+ seen.add(entry);
2454
+ });
2455
+ return array;
2456
+ }
2457
+
2458
+ function canonicalJson(value) {
2459
+ return JSON.stringify(canonicalize(value));
2460
+ }
2461
+
2462
+ function canonicalize(value) {
2463
+ if (Array.isArray(value)) return value.map(canonicalize);
2464
+ if (!isObject(value)) return value;
2465
+ const output = {};
2466
+ for (const key of Object.keys(value).sort(compareStrings)) {
2467
+ if (value[key] !== undefined) output[key] = canonicalize(value[key]);
2468
+ }
2469
+ return output;
2470
+ }
2471
+
2472
+ function pickFields(value, fields) {
2473
+ if (!isObject(value)) return {};
2474
+ const output = {};
2475
+ for (const field of fields) if (value[field] !== undefined) output[field] = cloneJsonValue(value[field]);
2476
+ return output;
2477
+ }
2478
+
2479
+ function compactDefined(value, { preserveNull = false } = {}) {
2480
+ const output = {};
2481
+ for (const [key, entry] of Object.entries(value || {})) {
2482
+ if (entry === undefined || (!preserveNull && entry === null)) continue;
2483
+ output[key] = entry;
2484
+ }
2485
+ return output;
2486
+ }
2487
+
2488
+ function cloneJsonArray(value) {
2489
+ return arrayOrEmpty(value).map(cloneJsonValue);
2490
+ }
2491
+
2492
+ function cloneJsonValue(value) {
2493
+ if (value === undefined) return undefined;
2494
+ return JSON.parse(JSON.stringify(value));
2495
+ }
2496
+
2497
+ function dedupeRecordsById(records) {
2498
+ const byId = new Map();
2499
+ for (const record of records) if (isObject(record) && isNonEmptyString(record.id) && !byId.has(record.id)) byId.set(record.id, record);
2500
+ return [...byId.values()].sort(compareById);
2501
+ }
2502
+
2503
+ function appendMapArray(index, key, value) {
2504
+ const records = index.get(key);
2505
+ if (records) records.push(value);
2506
+ else index.set(key, [value]);
2507
+ }
2508
+
2509
+ function indexRecordsByStringKey(records, getKey) {
2510
+ const index = new Map();
2511
+ for (const record of arrayOrEmpty(records)) {
2512
+ const key = getKey(record);
2513
+ if (isNonEmptyString(key)) appendMapArray(index, key, record);
2514
+ }
2515
+ return index;
2516
+ }
2517
+
2518
+ function sortByCanonicalLocation(records, getLocation) {
2519
+ return [...arrayOrEmpty(records)].sort((left, right) => {
2520
+ const locationDelta = compareStrings(getLocation(left) || "", getLocation(right) || "");
2521
+ return locationDelta || compareStrings(canonicalJson(left), canonicalJson(right));
2522
+ });
2523
+ }
2524
+
2525
+ function canonicalCrawlReferenceLocation(ref) {
2526
+ if (!isObject(ref)) return "";
2527
+ const url = /^https?:\/\//i.test(String(ref.raw || "")) ? optionalString(ref.raw) : null;
2528
+ const path = optionalString(ref.source_path) || (!url ? optionalString(ref.normalized) : null);
2529
+ return `${path || ""}\u0000${url || ""}`;
2530
+ }
2531
+
2532
+ function compareById(a, b) {
2533
+ return compareStrings(String(a?.id || ""), String(b?.id || ""));
2534
+ }
2535
+
2536
+ function compareStrings(left, right) {
2537
+ return left < right ? -1 : left > right ? 1 : 0;
2538
+ }
2539
+
2540
+ function sortStrings(values) {
2541
+ return [...values].sort(compareStrings);
2542
+ }
2543
+
2544
+ function sameStringSet(a, b) {
2545
+ return JSON.stringify(sortStrings(uniqueStrings(arrayOrEmpty(a)))) === JSON.stringify(sortStrings(uniqueStrings(arrayOrEmpty(b))));
2546
+ }
2547
+
2548
+ function uniqueStrings(values) {
2549
+ return [...new Set(values.filter(isNonEmptyString).map((value) => value.trim()))];
2550
+ }
2551
+
2552
+ function validSha(value) {
2553
+ return canonicalHashSpelling(value);
2554
+ }
2555
+
2556
+ function canonicalHashSpelling(value) {
2557
+ if (!isNonEmptyString(value) || !SHA256_PATTERN.test(value)) return null;
2558
+ return `${MATERIAL_FINGERPRINT_PREFIX}${value.replace(/^sha256:/, "")}`;
2559
+ }
2560
+
2561
+ function canonicalProducerMaterialFingerprint(value) {
2562
+ return canonicalHashSpelling(value) ?? optionalString(value);
2563
+ }
2564
+
2565
+ function optionalString(value) {
2566
+ return isNonEmptyString(value) ? value.trim() : null;
2567
+ }
2568
+
2569
+ function positiveInteger(value) {
2570
+ return Number.isInteger(value) && value > 0 ? value : null;
2571
+ }
2572
+
2573
+ function positiveNumber(value) {
2574
+ return typeof value === "number" && value > 0 ? value : null;
2575
+ }
2576
+
2577
+ function slugify(value) {
2578
+ return String(value || "")
2579
+ .trim()
2580
+ .toLowerCase()
2581
+ .replace(/[^a-z0-9]+/g, "-")
2582
+ .replace(/^-+|-+$/g, "")
2583
+ .slice(0, 80);
2584
+ }
2585
+
2586
+ function assertInputArray(value, name) {
2587
+ if (!Array.isArray(value)) throw new TypeError(`${name} must be an array.`);
2588
+ }
2589
+
2590
+ function assertOptionalInputArray(value, name) {
2591
+ if (value != null) assertInputArray(value, name);
2592
+ }
2593
+
2594
+ function arrayOrEmpty(value) {
2595
+ return Array.isArray(value) ? value : [];
2596
+ }
2597
+
2598
+ function isObject(value) {
2599
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2600
+ }
2601
+
2602
+ function isNonEmptyString(value) {
2603
+ return typeof value === "string" && value.trim().length > 0;
2604
+ }