@nextcommerce/campaigns-os 1.48.0 → 1.52.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 (80) hide show
  1. package/CHANGELOG.md +539 -0
  2. package/agents/claude/CLAUDE.md +6 -5
  3. package/agents/codex/AGENTS.md +6 -5
  4. package/agents/copilot/copilot-instructions.md +3 -3
  5. package/agents/cursor/campaigns-os.mdc +3 -3
  6. package/campaign-spec/dist/rules/campaign-metadata.d.ts +5 -1
  7. package/campaign-spec/dist/rules/campaign-metadata.js +9 -2
  8. package/campaign-spec/dist/rules/design-source-shape.js +13 -3
  9. package/campaign-spec/dist/rules/sdk-version.js +2 -1
  10. package/compatibility.json +1 -1
  11. package/contracts/commerce-surface-catalog.json +26 -46
  12. package/contracts/effects.v1.json +254 -2
  13. package/contracts/release-ledger.json +1239 -0
  14. package/contracts/supported-surface.json +4 -4
  15. package/contracts/template-brand-contract.shared-commerce.v0.json +2 -2
  16. package/contracts/template-slot-manifest.shared-content-core.v0.json +24 -0
  17. package/docs/brand-theme-bridge.md +12 -6
  18. package/docs/build-packet.md +101 -12
  19. package/docs/campaign-build-brief.md +25 -1
  20. package/docs/effects.md +6 -0
  21. package/docs/local-setup.md +1 -1
  22. package/docs/orientation-contract-reference.md +1 -1
  23. package/docs/polish-evidence.md +10 -0
  24. package/docs/qa-and-test-orders.md +66 -11
  25. package/docs/runtime-readiness.md +1 -1
  26. package/docs/sdk-storage-compatibility.md +1 -1
  27. package/docs/skills-revision.md +10 -10
  28. package/package.json +1 -1
  29. package/schemas/campaign-runtime-build-packet.v0.schema.json +4 -0
  30. package/schemas/campaigns-os-qa-verdict.v0.schema.json +8 -3
  31. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  32. package/skills/campaign-readback-classification/SKILL.md +3 -3
  33. package/skills/campaign-run-evidence/SKILL.md +3 -3
  34. package/skills/contribution-intake/SKILL.md +3 -3
  35. package/skills/next-campaigns-build/SKILL.md +4 -4
  36. package/skills/next-campaigns-os/SKILL.md +4 -4
  37. package/skills/next-campaigns-os/references/session-intake.md +7 -3
  38. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  39. package/skills/next-campaigns-polish/SKILL.md +5 -4
  40. package/skills/next-campaigns-qa/SKILL.md +6 -5
  41. package/skills.json +10 -10
  42. package/src/adapter-decision-contract.mjs +1 -1
  43. package/src/brand-theme.mjs +25 -2
  44. package/src/build-brief.mjs +68 -21
  45. package/src/built-site-scope.mjs +39 -6
  46. package/src/built-smoke-qc.mjs +1117 -0
  47. package/src/campaign-identity.mjs +36 -2
  48. package/src/cart-placeholders.mjs +730 -0
  49. package/src/cli.mjs +320 -42
  50. package/src/commercial-journey.mjs +65 -4
  51. package/src/commercial-parity.mjs +6 -1
  52. package/src/doctor/checks.mjs +291 -24
  53. package/src/doctor/inspect.mjs +53 -2
  54. package/src/doctor/next-step.mjs +1 -1
  55. package/src/install-mode.mjs +0 -8
  56. package/src/invocation.mjs +5 -2
  57. package/src/local-preview-policy.mjs +1 -1
  58. package/src/local-proof.mjs +4 -1
  59. package/src/polish-browser.mjs +218 -1
  60. package/src/polish-capture.mjs +1 -1
  61. package/src/polish-media-weight.mjs +492 -0
  62. package/src/polish-node.mjs +96 -4
  63. package/src/progress-node.mjs +5 -1
  64. package/src/qa-binding-evidence.mjs +21 -0
  65. package/src/qa-browser.mjs +338 -97
  66. package/src/qa-content-params.mjs +889 -0
  67. package/src/qa-node.mjs +114 -14
  68. package/src/qa-order-bump.mjs +22 -1
  69. package/src/qa-policy-links.mjs +1019 -0
  70. package/src/qa-tracking-params.mjs +1389 -0
  71. package/src/qa-url-privacy.mjs +168 -0
  72. package/src/qc-accept.mjs +446 -0
  73. package/src/qc-check-registry.mjs +83 -0
  74. package/src/qc-results.mjs +1049 -0
  75. package/src/sdk-attribute-index.mjs +71 -0
  76. package/src/sdk-markup.mjs +2 -2
  77. package/src/sdk-storage-compatibility.mjs +63 -3
  78. package/src/source-prep.mjs +37 -7
  79. package/src/stage-record.mjs +356 -36
  80. package/src/theme-gate.mjs +3 -3
@@ -259,8 +259,15 @@ function gateBase(subject) {
259
259
  /**
260
260
  * Evaluate cross-page campaign identity over built pages.
261
261
  *
262
+ * A page marked `spec_page: false` is built output that no CampaignSpec page
263
+ * builds to. It is still scanned, because it is still served, but a finding
264
+ * that names it says so: such a file is usually left in `_site/` by an earlier
265
+ * build, or built from an HTML file copied into the source (a design export's
266
+ * `index.html` under `assets/`), and the repair is to remove it, not to retag
267
+ * it.
268
+ *
262
269
  * @param {{ subject: object, pages: Array<{ page_id: string, route?: string|null,
263
- * file?: string|null, content: string,
270
+ * file?: string|null, content: string, spec_page?: boolean,
264
271
  * scripts?: Array<{ src: string, file?: string|null, content: string }> }> }} input
265
272
  */
266
273
  export function evaluateCampaignIdentity({ subject, pages = [] } = {}) {
@@ -300,6 +307,7 @@ export function evaluateCampaignIdentity({ subject, pages = [] } = {}) {
300
307
  code: CAMPAIGN_IDENTITY_KINDS.api_key_drift,
301
308
  a: { page_id: firstKey.page_id, file: firstKey.where, source: firstKey.source, value: firstKey.value },
302
309
  b: { page_id: observation.page_id, file: observation.where, source: observation.source, value: observation.value },
310
+ evidence: `API key differs across pages: ${firstKey.where} has ${quote(firstKey.value)} (${firstKey.source}) but ${observation.where} has ${quote(observation.value)} (${observation.source}).`,
303
311
  message: `API key differs across pages: ${firstKey.where} has ${quote(firstKey.value)} (${firstKey.source}) but ${observation.where} has ${quote(observation.value)} (${observation.source}). One of these pages was borrowed from another campaign; make both name the same key.`,
304
312
  });
305
313
  break; // one finding per kind names the first pair; the rest follow from the same edit
@@ -316,6 +324,7 @@ export function evaluateCampaignIdentity({ subject, pages = [] } = {}) {
316
324
  code: CAMPAIGN_IDENTITY_KINDS.funnel_drift,
317
325
  a: { page_id: firstFunnel.page_id, file: firstFunnel.file || firstFunnel.page_id, value: firstFunnel.funnel },
318
326
  b: { page_id: identity.page_id, file: identity.file || identity.page_id, value: identity.funnel },
327
+ evidence: `next-funnel differs across pages: ${firstFunnel.file || firstFunnel.page_id} has ${quote(firstFunnel.funnel)} but ${identity.file || identity.page_id} has ${quote(identity.funnel)}.`,
319
328
  message: `next-funnel differs across pages: ${firstFunnel.file || firstFunnel.page_id} has ${quote(firstFunnel.funnel)} but ${identity.file || identity.page_id} has ${quote(identity.funnel)}. Orders from the second page attribute to the other funnel; set the same <meta name="next-funnel"> on both.`,
320
329
  });
321
330
  break;
@@ -336,6 +345,7 @@ export function evaluateCampaignIdentity({ subject, pages = [] } = {}) {
336
345
  code: CAMPAIGN_IDENTITY_KINDS.funnel_missing,
337
346
  a: { page_id: identity.page_id, file: identity.file || identity.page_id, value: null },
338
347
  b: { page_id: firstFunnel.page_id, file: firstFunnel.file || firstFunnel.page_id, value: firstFunnel.funnel },
348
+ evidence: `${identity.file || identity.page_id} declares next-page-type=${quote(identity.page_type)} but no <meta name="next-funnel">, while ${firstFunnel.file || firstFunnel.page_id} carries ${quote(firstFunnel.funnel)}.`,
339
349
  message: `${identity.file || identity.page_id} declares next-page-type=${quote(identity.page_type)} but no <meta name="next-funnel">, while ${firstFunnel.file || firstFunnel.page_id} carries ${quote(firstFunnel.funnel)}. Orders from the untagged page attribute differently from the rest; add the same next-funnel meta.`,
340
350
  });
341
351
  break;
@@ -354,13 +364,37 @@ export function evaluateCampaignIdentity({ subject, pages = [] } = {}) {
354
364
  findings.push({
355
365
  kind: "attribution_drift",
356
366
  code: CAMPAIGN_IDENTITY_KINDS.attribution_drift,
357
- a: { page_id: identity.page_id, file: expectedWhere, value: expected },
367
+ // a is the page the expected tag came from: this page's own tag, or,
368
+ // when it has none, the campaign's first tagged page.
369
+ a: { page_id: identity.funnel ? identity.page_id : firstFunnel.page_id, file: expectedWhere, value: expected },
358
370
  b: { page_id: identity.page_id, file: attribution.where, value: attribution.value },
371
+ evidence: `setAttribution({ funnel: ${quote(attribution.value)} }) in ${attribution.where} disagrees with next-funnel ${quote(expected)} in ${expectedWhere}.`,
359
372
  message: `setAttribution({ funnel: ${quote(attribution.value)} }) in ${attribution.where} disagrees with next-funnel ${quote(expected)} in ${expectedWhere}. The call overrides the tag, so orders attribute to ${quote(attribution.value)}; change the call to ${quote(expected)} or remove it.`,
360
373
  });
361
374
  }
362
375
  }
363
376
 
377
+ const strayFiles = new Map(scanned
378
+ .filter((page) => page.spec_page === false)
379
+ .map((page) => [page.page_id, page.file || page.page_id]));
380
+ for (const finding of findings) {
381
+ const stray = [...new Set([finding.a?.page_id, finding.b?.page_id]
382
+ .filter((pageId) => strayFiles.has(pageId))
383
+ .map((pageId) => strayFiles.get(pageId)))];
384
+ if (!stray.length) continue;
385
+ finding.stray_files = stray;
386
+ // The stray is the repair, whichever side of the finding it is on, so it
387
+ // leads, and the drift it causes follows as evidence only. The finding's
388
+ // own instruction (retag, or add a missing tag to a real page) is dropped:
389
+ // that edit would land on the wrong file.
390
+ const named = stray.length > 2 ? `${stray.slice(0, -1).join(", ")}, and ${stray.at(-1)}` : stray.join(" and ");
391
+ finding.message = `${named} ${stray.length === 1 ? "is" : "are"} not the built page of any CampaignSpec page: built output left by an earlier build, or an HTML file copied into the source (a design export's index.html under assets/ builds as its own page). Remove the source file if there is one, delete the built file, then rebuild and record the build again; do not retag either page. What it causes: ${finding.evidence}`;
392
+ }
393
+
394
+ // evidence is the message's first sentence, kept apart only so a stray
395
+ // repair can quote it without the finding's own instruction.
396
+ for (const finding of findings) delete finding.evidence;
397
+
364
398
  const identity = {
365
399
  api_key: firstKey ? firstKey.value : null,
366
400
  api_key_source: firstKey ? firstKey.source : null,