@nextcommerce/campaigns-os 1.43.1 → 1.46.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 (92) hide show
  1. package/AGENTS.md +9 -2
  2. package/CHANGELOG.md +1099 -5103
  3. package/README.md +34 -13
  4. package/agents/claude/CLAUDE.md +1 -1
  5. package/agents/codex/AGENTS.md +1 -1
  6. package/agents/copilot/copilot-instructions.md +1 -1
  7. package/agents/cursor/campaigns-os.mdc +1 -1
  8. package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +2 -2
  9. package/campaign-spec/dist/rules/analytics-contract-shape.js +2 -2
  10. package/campaign-spec/dist/rules/store-profile-shape.d.ts +5 -1
  11. package/campaign-spec/dist/rules/store-profile-shape.js +8 -10
  12. package/campaign-spec/dist/types.d.ts +2 -2
  13. package/contracts/agent-relevant-change-policy.v1.json +5 -0
  14. package/contracts/archive/CHANGELOG.2026-09-30.md +5111 -0
  15. package/contracts/archive/release-ledger.2026-09-30.json +5068 -0
  16. package/contracts/effects.v1.json +1184 -121
  17. package/contracts/orientation-reason-codes.v1.json +7 -0
  18. package/contracts/release-ledger.json +2190 -5260
  19. package/contracts/supported-surface.json +7 -4
  20. package/contracts/template-slot-manifest.shared-content-core.v0.json +403 -0
  21. package/docs/brand-theme-bridge.md +81 -0
  22. package/docs/build-packet.md +222 -23
  23. package/docs/campaigns-os-build-flow.md +4 -3
  24. package/docs/design-source-package.md +162 -15
  25. package/docs/effects.md +66 -12
  26. package/docs/gateway-login.md +3 -0
  27. package/docs/local-setup.md +1 -1
  28. package/docs/orientation-contract-reference.md +42 -2
  29. package/docs/polish-evidence.md +74 -0
  30. package/docs/progress-snapshots.md +10 -6
  31. package/docs/qa-and-test-orders.md +230 -20
  32. package/docs/release-ledger-authoring-guide.md +70 -8
  33. package/docs/runtime-readiness.md +1 -1
  34. package/docs/sdk-storage-compatibility.md +1 -1
  35. package/docs/skills-revision.md +10 -10
  36. package/docs/supported-surface.md +2 -2
  37. package/docs/versioning.md +4 -1
  38. package/package.json +1 -1
  39. package/schemas/campaigns-os-release-ledger.v1.schema.json +32 -2
  40. package/schemas/campaigns-os-tooling-orientation.v1.schema.json +1 -0
  41. package/skills/campaign-lifecycle-orientation/SKILL.md +16 -5
  42. package/skills/campaign-readback-classification/SKILL.md +3 -3
  43. package/skills/campaign-run-evidence/SKILL.md +7 -6
  44. package/skills/contribution-intake/SKILL.md +3 -3
  45. package/skills/next-campaigns-build/SKILL.md +7 -6
  46. package/skills/next-campaigns-os/SKILL.md +7 -7
  47. package/skills/next-campaigns-os/references/session-intake.md +9 -3
  48. package/skills/next-campaigns-os-setup/SKILL.md +5 -5
  49. package/skills/next-campaigns-polish/SKILL.md +28 -9
  50. package/skills/next-campaigns-qa/SKILL.md +7 -4
  51. package/skills.json +10 -10
  52. package/src/brand-theme.mjs +320 -20
  53. package/src/build-brief.mjs +6 -4
  54. package/src/built-script-syntax.mjs +480 -0
  55. package/src/built-site-scope.mjs +16 -4
  56. package/src/campaigns-api-key.mjs +99 -0
  57. package/src/cli-helpers.mjs +118 -0
  58. package/src/cli.mjs +1530 -7580
  59. package/src/commercial-parity.mjs +48 -2
  60. package/src/design-source-package.mjs +1 -1
  61. package/src/design-source-publication.mjs +898 -0
  62. package/src/deviation.mjs +13 -1
  63. package/src/diagnostic.mjs +6 -2
  64. package/src/directory-lock.mjs +270 -0
  65. package/src/doctor/checks.mjs +4654 -0
  66. package/src/doctor/inspect.mjs +678 -0
  67. package/src/doctor/next-step.mjs +731 -0
  68. package/src/doctor/source-provenance.mjs +184 -0
  69. package/src/install-invocation.mjs +29 -0
  70. package/src/invocation.mjs +183 -0
  71. package/src/live-campaign-refs.mjs +466 -0
  72. package/src/login.mjs +2 -2
  73. package/src/page-kit-store-profile.mjs +69 -12
  74. package/src/page-kit-sync.mjs +31 -12
  75. package/src/private-template-source.mjs +1 -1
  76. package/src/progress-node.mjs +9 -36
  77. package/src/proof-policy.mjs +1 -1
  78. package/src/qa-analytics-correctness.mjs +3 -0
  79. package/src/qa-binding-evidence.mjs +76 -11
  80. package/src/qa-browser.mjs +1316 -105
  81. package/src/qa-build-scope.mjs +47 -0
  82. package/src/qa-commercial-parity.mjs +48 -5
  83. package/src/qa-node.mjs +339 -19
  84. package/src/qa-test-order-topology.mjs +148 -0
  85. package/src/sdk-markup.mjs +72 -8
  86. package/src/source-html-intake.mjs +117 -1
  87. package/src/source-html-manifest.mjs +9 -2
  88. package/src/stage-ledger.mjs +28 -0
  89. package/src/stage-record.mjs +551 -0
  90. package/src/target-lock.mjs +54 -0
  91. package/src/template-brand-contract.mjs +17 -1
  92. package/src/upsell-selector-scope.mjs +112 -2
@@ -0,0 +1,47 @@
1
+ import { resolveBuiltSiteScope } from "./built-site-scope.mjs";
2
+ import { runtimeRelativeRouteForSpecValue } from "./route-identity.mjs";
3
+
4
+ // Use the recorded declaration, not a doctor's cached derived scope: failed
5
+ // intake also writes skip_reason mappings, which must never hide missing pages.
6
+ export function applyQaBuildScope(topologies, { packet, report, targetRepo, publicRouteSlug } = {}) {
7
+ const declared = report?.stages?.prepare_build?.declared_out_of_scope;
8
+ if (!Array.isArray(declared) || !packet) return { topologies, excludedPages: [] };
9
+ const declaredIds = new Set(declared.map(page => page?.page_id));
10
+ const skippedIds = new Set((packet.source_html?.pages || [])
11
+ .filter(page => declaredIds.has(page.page_id) && !page.path && typeof page.skip_reason === "string" && page.skip_reason.trim())
12
+ .map(page => page.page_id));
13
+ if (!skippedIds.size) return { topologies, excludedPages: [] };
14
+ const built = targetRepo ? resolveBuiltSiteScope(targetRepo, { slug: publicRouteSlug }) : null;
15
+ const normalizeRoute = route => runtimeRelativeRouteForSpecValue(route, publicRouteSlug).replace(/^\/+|\/+$/g, "").replace(/(?:^|\/)index\.html$/, "").replace(/\/$/, "");
16
+ const builtRoutes = new Set((built?.pages || []).map(page => normalizeRoute(page.route)));
17
+ const excludedPages = [];
18
+ const scoped = topologies.map(topology => ({
19
+ ...topology,
20
+ partial_build_scope: topology.pages.some(page => skippedIds.has(page.page_id)),
21
+ pages: topology.pages.filter(page => {
22
+ if (!skippedIds.has(page.page_id)) return true;
23
+ // Without a resolved URL we cannot prove which output file represents
24
+ // this page. Keep it in QA so unresolved/materialized pages are not
25
+ // silently hidden as unbuilt declarations.
26
+ if (!page.url) return true;
27
+ // An explicitly materialized stock page rejoins QA. A declaration alone
28
+ // is not an instruction to build it, nor proof that it exists.
29
+ let route;
30
+ try { route = new URL(page.url).pathname; } catch { return true; }
31
+ if (builtRoutes.has(normalizeRoute(route))) return true;
32
+ excludedPages.push(page);
33
+ return false;
34
+ }),
35
+ }));
36
+ return { topologies: scoped, excludedPages };
37
+ }
38
+
39
+ export function specForQaScope(spec, excludedPages = []) {
40
+ const excluded = new Set(excludedPages.map(page => page.page_id));
41
+ if (Array.isArray(spec.funnel_pages) && !spec.funnels?.length) {
42
+ return { ...spec, funnel_pages: spec.funnel_pages.filter(page => !excluded.has(page.id)) };
43
+ }
44
+ return { ...spec, funnels: (spec.funnels || []).map(funnel => ({
45
+ ...funnel, pages: (funnel.pages || []).filter(page => !excluded.has(page.id)),
46
+ })) };
47
+ }
@@ -11,7 +11,10 @@ import {
11
11
  CommercialParityLimitError,
12
12
  createCommercialParityReport,
13
13
  extractCommercialClaims,
14
+ recurringClaimAbsences,
15
+ recurringClaimAbsentAssertions,
14
16
  } from "./commercial-parity.mjs";
17
+ import { extractRenderedPackageRefs } from "./live-campaign-refs.mjs";
15
18
 
16
19
  export const COMMERCIAL_QA_LIMITS = Object.freeze({
17
20
  max_html_bytes: 2 * 1024 * 1024,
@@ -171,10 +174,15 @@ function captureFailure(page, code, error) {
171
174
 
172
175
  export function captureCommercialClaims(page, html) {
173
176
  try {
174
- return extractCommercialClaims(html, {
175
- pageId: present(page?.page_id) ? String(page.page_id) : null,
176
- url: page?.url,
177
- });
177
+ return {
178
+ ...extractCommercialClaims(html, {
179
+ pageId: present(page?.page_id) ? String(page.page_id) : null,
180
+ url: page?.url,
181
+ }),
182
+ // Every package the page renders, by the refs doctor reads, so a
183
+ // subscription whose rebill copy yielded no claim is still seen (#533).
184
+ rendered_package_refs: [...extractRenderedPackageRefs(html)],
185
+ };
178
186
  } catch (error) {
179
187
  const code = error instanceof CommercialParityLimitError
180
188
  ? error.code
@@ -398,6 +406,29 @@ export function planCommercialParity(spec, { maxScenarios = COMMERCIAL_QA_LIMITS
398
406
  };
399
407
  }
400
408
 
409
+ // A package recurs when the spec says so, or carries a recurring price and a
410
+ // cadence. Page rows override the campaign catalog per ref, as the journey does.
411
+ function isSubscriptionPackage(pkg) {
412
+ return pkg?.is_recurring === true
413
+ || (present(pkg?.price_recurring) && (present(pkg?.interval_count) || present(pkg?.interval)));
414
+ }
415
+
416
+ export function subscriptionPackageRefsByPage(spec, pages) {
417
+ const packageRef = (pkg) => (present(pkg?.ref_id) ? String(pkg.ref_id) : present(pkg?.package_id) ? String(pkg.package_id) : null);
418
+ const catalog = new Map(array(spec?.packages).map((pkg) => [packageRef(pkg), pkg]).filter(([ref]) => ref));
419
+ const byPage = new Map();
420
+ array(pages).forEach((page) => {
421
+ if (!present(page?.id)) return;
422
+ const refs = new Set();
423
+ array(page.packages).forEach((pkg) => {
424
+ const ref = packageRef(pkg);
425
+ if (ref && isSubscriptionPackage({ ...(catalog.get(ref) || {}), ...pkg })) refs.add(ref);
426
+ });
427
+ if (refs.size) byPage.set(String(page.id), refs);
428
+ });
429
+ return byPage;
430
+ }
431
+
401
432
  function countBy(values, keyFor) {
402
433
  const counts = {};
403
434
  values.forEach((value) => {
@@ -473,6 +504,7 @@ function compactReport(report, journey, plan, executed, issues, observedClaims,
473
504
  unresolved_voucher_claims: report.unresolved_voucher_claims,
474
505
  serialized_assertion_count: report.serialized_assertion_count,
475
506
  omitted_assertion_count: report.omitted_assertion_count,
507
+ recurring_claim_absences: array(report.recurring_claim_absences),
476
508
  observed_claims: observedClaims,
477
509
  claim_limit: claimLimit,
478
510
  finding_count: report.findings.length,
@@ -539,6 +571,7 @@ export function unavailableCommercialReport(code, { status = "not_run" } = {}) {
539
571
  unresolved_voucher_claims: 0,
540
572
  serialized_assertion_count: 0,
541
573
  omitted_assertion_count: 0,
574
+ recurring_claim_absences: [],
542
575
  observed_claims: 0,
543
576
  claim_limit: COMMERCIAL_QA_LIMITS.max_aggregate_claims,
544
577
  finding_count: 0,
@@ -625,8 +658,18 @@ export async function runCommercialParity({
625
658
  maxAssertions,
626
659
  countsOnly: aggregateClaimOverflow,
627
660
  });
661
+ // Subscription packages a page renders with no recurring claim read for
662
+ // them: incomplete evidence (an issue), and a warning naming the package.
663
+ const absences = aggregateClaimOverflow
664
+ ? []
665
+ : recurringClaimAbsences(captures, subscriptionPackageRefsByPage(planningSpec, commercialPlanning.pages));
666
+ absences.forEach(() => issues.push({ code: "recurring_claim_absent" }));
667
+ const absentAssertions = recurringClaimAbsentAssertions(absences, {
668
+ maxAssertions: Math.max(0, maxAssertions - parity.assertions.length),
669
+ });
670
+ parity.recurring_claim_absences = absences;
628
671
  return {
629
- assertions: parity.assertions,
672
+ assertions: [...parity.assertions, ...absentAssertions],
630
673
  commercial: compactReport(
631
674
  parity,
632
675
  parityJourney,