@nextcommerce/campaigns-os 1.59.0 → 1.61.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 (96) hide show
  1. package/CHANGELOG.md +242 -0
  2. package/README.md +11 -10
  3. package/agents/claude/CLAUDE.md +15 -0
  4. package/agents/codex/AGENTS.md +15 -0
  5. package/agents/copilot/copilot-instructions.md +15 -0
  6. package/agents/cursor/campaigns-os.mdc +15 -0
  7. package/compatibility.json +1 -1
  8. package/contracts/brand-theme-target-tokens.next-core.v0.json +95 -25
  9. package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
  10. package/contracts/commerce-surface-catalog.json +27 -26
  11. package/contracts/release-ledger.json +1311 -0
  12. package/contracts/supported-surface.json +4 -4
  13. package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
  14. package/docs/access-model.md +20 -0
  15. package/docs/brand-theme-bridge.md +62 -3
  16. package/docs/build-packet.md +42 -13
  17. package/docs/campaign-build-brief.md +17 -1
  18. package/docs/campaigns-os-build-flow.md +62 -3
  19. package/docs/campaignspec-authoring-examples.md +116 -0
  20. package/docs/design-source-package.md +46 -2
  21. package/docs/entry-points.md +188 -0
  22. package/docs/local-setup.md +1 -1
  23. package/docs/orientation-contract-reference.md +1 -1
  24. package/docs/polish-evidence.md +41 -3
  25. package/docs/qa-and-test-orders.md +50 -2
  26. package/docs/quickstart.md +438 -0
  27. package/docs/readback.md +119 -6
  28. package/docs/runtime-readiness.md +1 -1
  29. package/docs/sdk-storage-compatibility.md +3 -1
  30. package/docs/skills-revision.md +10 -10
  31. package/docs/source-adapters.md +213 -0
  32. package/package.json +10 -3
  33. package/schemas/campaign-design-source-package.v0.schema.json +8 -0
  34. package/schemas/campaigns-os-readback.v2.schema.json +177 -1
  35. package/schemas/source-html-manifest.v0.schema.json +3 -0
  36. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  37. package/skills/campaign-readback-classification/SKILL.md +3 -3
  38. package/skills/campaign-run-evidence/SKILL.md +3 -3
  39. package/skills/contribution-intake/SKILL.md +3 -3
  40. package/skills/next-campaigns-build/SKILL.md +5 -5
  41. package/skills/next-campaigns-os/SKILL.md +4 -3
  42. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  43. package/skills/next-campaigns-polish/SKILL.md +12 -16
  44. package/skills/next-campaigns-qa/SKILL.md +3 -3
  45. package/skills/next-campaigns-sdk-bump/SKILL.md +286 -0
  46. package/skills.json +18 -10
  47. package/src/brand-theme.mjs +413 -8
  48. package/src/brief-summary.mjs +6 -2
  49. package/src/browser-launch.mjs +2 -1
  50. package/src/build-brief.mjs +9 -2
  51. package/src/built-site-scope.mjs +7 -14
  52. package/src/campaign-identity.mjs +6 -4
  53. package/src/cli.mjs +89 -73
  54. package/src/design-file-reference.mjs +93 -0
  55. package/src/design-source-package.mjs +55 -1
  56. package/src/doctor/checks.mjs +14 -77
  57. package/src/doctor/ci.mjs +112 -0
  58. package/src/doctor/inspect.mjs +20 -8
  59. package/src/doctor/next-step.mjs +6 -3
  60. package/src/doctor/source-provenance.mjs +74 -0
  61. package/src/evidence-debt.mjs +119 -0
  62. package/src/orchestration-stage-contract.mjs +6 -3
  63. package/src/page-kit-build-summary.mjs +21 -1
  64. package/src/page-kit-campaign-config.mjs +19 -0
  65. package/src/parity/compare.mjs +316 -0
  66. package/src/parity/design-access.mjs +54 -0
  67. package/src/parity/element-parity-record.mjs +569 -0
  68. package/src/parity/probe.mjs +205 -0
  69. package/src/parity/snapshot.mjs +110 -0
  70. package/src/png-header.mjs +87 -0
  71. package/src/polish-browser.mjs +186 -9
  72. package/src/polish-captures-record.mjs +52 -0
  73. package/src/polish-deadline.mjs +14 -0
  74. package/src/polish-gate.mjs +26 -11
  75. package/src/polish-node.mjs +81 -7
  76. package/src/polish-visual-fixtures.mjs +74 -0
  77. package/src/polish-visual.mjs +606 -0
  78. package/src/progress-node.mjs +2 -1
  79. package/src/qa-analytics-correctness.mjs +14 -2
  80. package/src/qa-analytics-parity.mjs +94 -5
  81. package/src/qa-node.mjs +39 -6
  82. package/src/qc-accept.mjs +148 -6
  83. package/src/qc-check-registry.mjs +37 -1
  84. package/src/qc-results.mjs +404 -16
  85. package/src/readback.mjs +57 -13
  86. package/src/sdk-attribute-index.mjs +11 -11
  87. package/src/sdk-markup.mjs +77 -2
  88. package/src/sdk-storage-compatibility.mjs +94 -4
  89. package/src/source-html-manifest.mjs +7 -0
  90. package/src/source-html-server.mjs +129 -0
  91. package/src/spec-derive.mjs +24 -9
  92. package/src/stage-ledger.mjs +27 -1
  93. package/src/stage-record.mjs +63 -9
  94. package/src/theme-gate.mjs +2 -2
  95. package/src/visual-diff.mjs +296 -0
  96. package/src/visual-reference.mjs +126 -0
@@ -0,0 +1,126 @@
1
+ import { openSync, readSync, closeSync, realpathSync, existsSync } from "node:fs";
2
+ import { resolve, relative, isAbsolute } from "node:path";
3
+ import { readPngStructure, boundedInflate, sha256Hex, VISUAL_MAX_DECODE_PIXELS, VISUAL_MAX_REFERENCE_BYTES } from "./png-header.mjs";
4
+
5
+ const inside = (root, path) => {
6
+ if (!root || !path) return false;
7
+ const rel = relative(root, path);
8
+ return rel === "" || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith("../"));
9
+ };
10
+ const real = (path) => { try { return realpathSync(path); } catch { return resolve(path); } };
11
+ const hash = (bytes) => `sha256:${sha256Hex(bytes)}`;
12
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
13
+ const own = (value, key) => object(value) && Object.hasOwn(value, key) ? value[key] : undefined;
14
+
15
+ export function referenceViewport({ ihdr, scale_factor, width, height } = {}) {
16
+ if (!Number.isFinite(scale_factor) || scale_factor <= 0) return { css_width: null, dpr: null, reason: "dpr_unknown" };
17
+ const w = ihdr?.width_px ?? width;
18
+ return { css_width: Number.isFinite(w) && w > 0 ? w / scale_factor : null,
19
+ dpr: scale_factor, reason: w > 0 ? null : "width_unknown" };
20
+ }
21
+
22
+ // linkedSource is the independently read html_file identity + provenance.
23
+ export function classifyProvenance({ path, sourceRoot, targetRepo, kind = "source_screenshot", sha256,
24
+ buildCaptureHashes = [], builtHtmlHashes = [], pageKitTargets = [], linkedSource = null,
25
+ recorded = {}, source_ref_id = own(recorded, "source_ref_id"), captured_at = own(recorded, "captured_at") } = {}) {
26
+ recorded = object(recorded) ? recorded : {};
27
+ const reasons = [];
28
+ const html = kind === "html_file" || kind === "source_html";
29
+ const resolved = path ? real(path) : null;
30
+ const paths = html ? [resolved, sourceRoot ? real(sourceRoot) : null] : [resolved];
31
+ if (targetRepo) {
32
+ if (paths.some((p) => inside(real(resolve(targetRepo, "_site")), p))) reasons.push("path_in_build_output");
33
+ if (paths.some((p) => inside(real(resolve(targetRepo, ".campaign-runtime")), p))) reasons.push("path_in_runtime_dir");
34
+ }
35
+ if (html) {
36
+ if (sha256 && new Set(builtHtmlHashes).has(sha256)) reasons.push("hash_equals_built_html");
37
+ if (resolved && pageKitTargets.some((p) => real(resolve(targetRepo || ".", p)) === resolved)) reasons.push("source_is_campaign_page");
38
+ } else {
39
+ const hex = String(sha256 || "").replace(/^sha256:/, "");
40
+ if (sha256 && (new Set(buildCaptureHashes).has(sha256) || (targetRepo && /^[a-f0-9]{64}$/.test(hex)
41
+ && ["captures", "readability", "diff"].some((dir) => existsSync(resolve(targetRepo, `.campaign-runtime/polish/${dir}/${hex}.png`)))))) reasons.push("hash_equals_package_capture");
42
+ if (linkedSource?.provenance?.class === "self_referential") reasons.push("linked_source_self_referential");
43
+ }
44
+ if (reasons.length) return { class: "self_referential", reasons };
45
+ if (!html && !captured_at) reasons.push("capture_time_unrecorded");
46
+ if (!html && (!source_ref_id || !linkedSource || !["html_file", "source_html"].includes(linkedSource.kind))) reasons.push("source_link_missing");
47
+ else {
48
+ const identity = html ? { sha256, sha256_recorded: own(recorded, "sha256") } : own(linkedSource, "identity");
49
+ // The source HTML applies the hash rules to its own html_file ref (§5.2):
50
+ // a ref that records no sha256 is unrecorded whether or not the file was
51
+ // read; a recorded ref whose file was not read is missing.
52
+ if (html && !identity.sha256_recorded) reasons.push("source_hash_unrecorded");
53
+ else if (!identity?.sha256) reasons.push("linked_source_missing");
54
+ else if (!identity.sha256_recorded) reasons.push("source_hash_unrecorded");
55
+ else if (identity.sha256_recorded !== identity.sha256) reasons.push("source_hash_mismatch");
56
+ }
57
+ return { class: reasons.length ? "unknown" : "declared_source", reasons };
58
+ }
59
+
60
+ function readBounded(path, maxBytes) {
61
+ const fd = openSync(path, "r");
62
+ try {
63
+ const chunks = [];
64
+ let length = 0;
65
+ while (length <= maxBytes) {
66
+ const chunk = Buffer.alloc(Math.min(65536, maxBytes + 1 - length));
67
+ const n = readSync(fd, chunk, 0, chunk.length, null);
68
+ if (!n) break;
69
+ chunks.push(chunk.subarray(0, n)); length += n;
70
+ }
71
+ if (length > maxBytes) return null;
72
+ return Buffer.concat(chunks, length);
73
+ } finally { closeSync(fd); }
74
+ }
75
+
76
+ export async function verifyReference({ path, url, recorded = {}, sourceRoot, targetRepo, decoder,
77
+ budget = null, kind = "source_screenshot", ...provenanceInputs } = {}) {
78
+ recorded = object(recorded) ? Object.fromEntries(Object.entries(recorded)) : {};
79
+ let resolved = null;
80
+ const identity = { sha256: null, sha256_recorded: recorded.sha256 ?? null, bytes: null,
81
+ ...Object.fromEntries(["captured_at", "browser", "device_profile", "scale_factor", "width", "height", "source_ref_id"].map((key) => [key, own(recorded, key) ?? null])) };
82
+ const result = { kind, path_rel: path ? relative(targetRepo || sourceRoot || ".", resolve(sourceRoot || ".", path)) : null, url_present: Boolean(url), identity,
83
+ verification: { level: "unverifiable", reason: null, png: null }, css_width: null, dpr: null };
84
+ const finish = (reason, level = "unverifiable") => {
85
+ result.verification.reason = reason; result.verification.level = level;
86
+ result.provenance = classifyProvenance({ ...provenanceInputs, path: resolved, sourceRoot, targetRepo, kind,
87
+ sha256: identity.sha256, recorded });
88
+ return result;
89
+ };
90
+ if (!path) return finish(url ? "remote_not_fetched" : "file_missing");
91
+ try { resolved = realpathSync(resolve(sourceRoot || ".", path)); } catch { return finish("file_missing"); }
92
+ result.path_rel = relative(targetRepo || sourceRoot || ".", resolved);
93
+ if (!inside(real(sourceRoot || "."), resolved)
94
+ && !(targetRepo && ["_site", ".campaign-runtime"].some((dir) => inside(real(resolve(targetRepo, dir)), resolved)))) return finish("path_outside_source_root");
95
+ let bytes;
96
+ try { bytes = readBounded(resolved, VISUAL_MAX_REFERENCE_BYTES); } catch { return finish("file_missing"); }
97
+ if (!bytes) return finish("too_large");
98
+ identity.bytes = bytes.length; identity.sha256 = hash(bytes);
99
+ if (["html_file", "source_html"].includes(kind)) {
100
+ // HTML needs only a successful read/hash for verification. Its declared
101
+ // source hash is a separate provenance member (§5.2), not a decode claim.
102
+ return finish(null, "decoded");
103
+ }
104
+ const structure = readPngStructure(bytes);
105
+ if (!structure.ok) return finish(structure.reason);
106
+ const ihdr = structure.ihdr;
107
+ result.verification.png = ihdr;
108
+ Object.assign(result, referenceViewport({ ihdr, ...recorded }));
109
+ delete result.reason;
110
+ if (ihdr.width_px * ihdr.height_px > VISUAL_MAX_DECODE_PIXELS) return finish("image_too_large");
111
+ const decode = typeof decoder === "function" ? decoder : own(decoder, "decode");
112
+ const available = typeof decode === "function" && (!decoder.status || decoder.status === "available");
113
+ if (available) {
114
+ if (budget && ((typeof budget.remaining === "function" && budget.remaining() <= 0)
115
+ || (Number.isFinite(budget.deadline) && (budget.clock || Date.now)() >= budget.deadline))) return finish("verify_budget_exhausted");
116
+ try {
117
+ boundedInflate(bytes, ihdr);
118
+ const image = await decode(bytes);
119
+ if (image.width !== ihdr.width_px || image.height !== ihdr.height_px) throw new Error("dimensions");
120
+ } catch { return finish("decode_failed"); }
121
+ }
122
+ if (recorded.sha256 && recorded.sha256 !== identity.sha256) return finish("recorded_hash_mismatch");
123
+ if ((recorded.width != null && recorded.width !== ihdr.width_px)
124
+ || (recorded.height != null && recorded.height !== ihdr.height_px)) return finish("recorded_dimensions_mismatch");
125
+ return finish(null, available ? "decoded" : "header_checked");
126
+ }