@nextcommerce/campaigns-os 1.43.2 → 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 (72) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +648 -5103
  3. package/README.md +32 -11
  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/archive/CHANGELOG.2026-09-30.md +5111 -0
  14. package/contracts/archive/release-ledger.2026-09-30.json +5068 -0
  15. package/contracts/effects.v1.json +1176 -113
  16. package/contracts/orientation-reason-codes.v1.json +7 -0
  17. package/contracts/release-ledger.json +2345 -6087
  18. package/contracts/supported-surface.json +7 -4
  19. package/contracts/template-slot-manifest.shared-content-core.v0.json +403 -0
  20. package/docs/brand-theme-bridge.md +81 -0
  21. package/docs/build-packet.md +158 -21
  22. package/docs/campaigns-os-build-flow.md +3 -3
  23. package/docs/design-source-package.md +73 -0
  24. package/docs/effects.md +50 -8
  25. package/docs/gateway-login.md +3 -0
  26. package/docs/local-setup.md +1 -1
  27. package/docs/orientation-contract-reference.md +42 -2
  28. package/docs/polish-evidence.md +74 -0
  29. package/docs/qa-and-test-orders.md +99 -13
  30. package/docs/release-ledger-authoring-guide.md +64 -4
  31. package/docs/runtime-readiness.md +1 -1
  32. package/docs/sdk-storage-compatibility.md +1 -1
  33. package/docs/skills-revision.md +10 -10
  34. package/docs/supported-surface.md +2 -2
  35. package/docs/versioning.md +4 -1
  36. package/package.json +1 -1
  37. package/schemas/campaigns-os-release-ledger.v1.schema.json +32 -2
  38. package/schemas/campaigns-os-tooling-orientation.v1.schema.json +1 -0
  39. package/skills/campaign-lifecycle-orientation/SKILL.md +16 -5
  40. package/skills/campaign-readback-classification/SKILL.md +3 -3
  41. package/skills/campaign-run-evidence/SKILL.md +7 -6
  42. package/skills/contribution-intake/SKILL.md +3 -3
  43. package/skills/next-campaigns-build/SKILL.md +7 -6
  44. package/skills/next-campaigns-os/SKILL.md +7 -7
  45. package/skills/next-campaigns-os/references/session-intake.md +9 -3
  46. package/skills/next-campaigns-os-setup/SKILL.md +5 -5
  47. package/skills/next-campaigns-polish/SKILL.md +28 -9
  48. package/skills/next-campaigns-qa/SKILL.md +7 -4
  49. package/skills.json +10 -10
  50. package/src/brand-theme.mjs +320 -20
  51. package/src/built-site-scope.mjs +16 -4
  52. package/src/cli.mjs +280 -46
  53. package/src/commercial-parity.mjs +48 -2
  54. package/src/deviation.mjs +13 -1
  55. package/src/diagnostic.mjs +5 -2
  56. package/src/doctor/checks.mjs +320 -81
  57. package/src/doctor/inspect.mjs +55 -13
  58. package/src/doctor/source-provenance.mjs +184 -0
  59. package/src/invocation.mjs +4 -0
  60. package/src/live-campaign-refs.mjs +466 -0
  61. package/src/login.mjs +2 -2
  62. package/src/page-kit-store-profile.mjs +69 -12
  63. package/src/page-kit-sync.mjs +31 -12
  64. package/src/progress-node.mjs +3 -1
  65. package/src/qa-browser.mjs +538 -28
  66. package/src/qa-commercial-parity.mjs +48 -5
  67. package/src/qa-node.mjs +122 -7
  68. package/src/qa-test-order-topology.mjs +148 -0
  69. package/src/sdk-markup.mjs +72 -8
  70. package/src/source-html-intake.mjs +116 -0
  71. package/src/stage-record.mjs +551 -0
  72. package/src/upsell-selector-scope.mjs +112 -2
@@ -14,6 +14,7 @@
14
14
  // every other file are left as they are. This module is pure: the CLI does
15
15
  // the reading, the writing, and the printing.
16
16
  import {
17
+ isAuthoritativeEmptyStoreProfileValue,
17
18
  isDemoResidue,
18
19
  normalizeStoreProfileValue,
19
20
  PAGE_KIT_STORE_PROFILE_FIELDS,
@@ -25,17 +26,23 @@ export const PAGE_KIT_SYNC_FIELDS = Object.freeze([...PAGE_KIT_STORE_PROFILE_FIE
25
26
 
26
27
  // The field-by-field plan: what the entry holds, what the spec says, and
27
28
  // whether a write is owed. `changes` are the fields whose value will move,
28
- // `unchanged` already match, `not_in_spec` are governed fields the spec does
29
- // not carry (left as they are; doctor's `target_only` warning still applies),
30
- // and `not_synced` are fields the target cannot be made authoritative for: an
31
- // invalid or conflicting spec SDK pin, a spec value of the wrong type or
32
- // shape (or the demo value itself), or starter demo residue in a field the
33
- // spec does not carry. Absent, null and blank spec values are "not carried".
29
+ // `unchanged` already match, `not_in_spec` are governed fields left as they
30
+ // are because the spec does not carry them or because its "" was not applied
31
+ // (doctor's `target_only` warning still applies), and `not_synced` are fields
32
+ // the target cannot be made authoritative for: an invalid or conflicting spec
33
+ // SDK pin, a spec value of the wrong type or shape (or the demo value itself),
34
+ // or starter demo residue in a field the spec does not carry. Absent and null
35
+ // spec values are "not carried"; an explicit empty (or whitespace-only) string
36
+ // blanks the starter demo value and otherwise leaves the target as it is.
37
+ // `spec_empty_not_applied` names that last case, the not_in_spec fields the
38
+ // spec sets to "" over a real, non-demo target value, which only a hand edit
39
+ // removes.
34
40
  export function planPageKitSync({ spec, entry, waivedGates = [] } = {}) {
35
41
  const target = entry && typeof entry === "object" && !Array.isArray(entry) ? entry : {};
36
42
  const changes = [];
37
43
  const unchanged = [];
38
44
  const notInSpec = [];
45
+ const specEmptyNotApplied = [];
39
46
  const notSynced = [];
40
47
  // A gate under an ACTIVE named-human waiver recorded a human accepting the
41
48
  // target's current values; sync must not silently reverse that decision.
@@ -61,8 +68,15 @@ export function planPageKitSync({ spec, entry, waivedGates = [] } = {}) {
61
68
  for (const field of PAGE_KIT_STORE_PROFILE_FIELDS) {
62
69
  const raw = spec?.campaign?.[field];
63
70
  const current = Object.hasOwn(target, field) ? target[field] : undefined;
64
- const carried = raw !== undefined && raw !== null && !(typeof raw === "string" && !raw.trim());
65
- if (!carried) {
71
+ // An explicit empty value blanks only the starter demo value, and confirms
72
+ // a target that already reads as empty. Any other target value is left as
73
+ // it is, as for a field the spec does not carry (doctor's target_only
74
+ // warning): Maps saved "" for every cleared store field before "" meant
75
+ // empty, so it never wipes a value someone entered.
76
+ const authoritativeEmpty = isAuthoritativeEmptyStoreProfileValue(field, raw);
77
+ const targetBlankOrDemo = current === undefined || current === null
78
+ || (typeof current === "string" && (!normalizeStoreProfileValue(current) || isDemoResidue(field, normalizeStoreProfileValue(current))));
79
+ if (raw === undefined || raw === null || (authoritativeEmpty && !targetBlankOrDemo)) {
66
80
  // Starter demo residue in a field the spec does not carry is the one
67
81
  // state sync cannot end: doctor blocks on it without a waiver and there
68
82
  // is no spec value to write over it. Say so instead of reporting a
@@ -75,13 +89,16 @@ export function planPageKitSync({ spec, entry, waivedGates = [] } = {}) {
75
89
  });
76
90
  } else {
77
91
  notInSpec.push(field);
92
+ if (authoritativeEmpty) specEmptyNotApplied.push(field);
78
93
  }
79
94
  continue;
80
95
  }
81
96
  // A carried value the target cannot be made authoritative for: the wrong
82
97
  // type (doctor's spec_invalid_type), the demo value itself, or a shape a
83
98
  // template would put into an href unescaped.
84
- const problem = typeof raw !== "string" ? "spec_invalid_type" : storeProfileSpecValueProblem(field, raw);
99
+ // An explicit empty value reaching here is written as "" over the starter
100
+ // demo value.
101
+ const problem = typeof raw !== "string" ? "spec_invalid_type" : authoritativeEmpty ? null : storeProfileSpecValueProblem(field, raw);
85
102
  if (problem) {
86
103
  notSynced.push({ field, reason: problem, detail: NOT_SYNCED_DETAIL[problem](field, raw) });
87
104
  continue;
@@ -91,9 +108,11 @@ export function planPageKitSync({ spec, entry, waivedGates = [] } = {}) {
91
108
  const row = { field, before, after, source: `campaign.${field}` };
92
109
  // The gate compares normalized forms, so a target that differs only in
93
110
  // surrounding whitespace or Unicode normalization already passes; a
94
- // rewrite would report a change doctor never saw.
111
+ // rewrite would report a change doctor never saw. For the same reason an
112
+ // absent or null target already agrees with an explicit empty value.
95
113
  const alreadyMatches = before === after
96
- || (typeof before === "string" && normalizeStoreProfileValue(before) === after);
114
+ || (typeof before === "string" && normalizeStoreProfileValue(before) === after)
115
+ || (authoritativeEmpty && (before === undefined || before === null));
97
116
  if (alreadyMatches) unchanged.push(row);
98
117
  else if (storeProfileWaiver) notSynced.push({ field, reason: "waived", detail: waivedDetail(field, storeProfileWaiver, "page_kit.store_profile") });
99
118
  else changes.push(row);
@@ -130,7 +149,7 @@ export function planPageKitSync({ spec, entry, waivedGates = [] } = {}) {
130
149
  });
131
150
  }
132
151
 
133
- return { changes, unchanged, not_in_spec: notInSpec, not_synced: notSynced };
152
+ return { changes, unchanged, not_in_spec: notInSpec, spec_empty_not_applied: specEmptyNotApplied, not_synced: notSynced };
134
153
  }
135
154
 
136
155
  // Apply a plan to the parsed campaigns.json document. Mutates ONLY the
@@ -11,6 +11,8 @@ import {resolveConsent,CANONICAL_REMIT_SCOPE,normalizeConsentScope,announceDefau
11
11
  import {boundedResponseText,isLoopbackHostname} from './remit.mjs';
12
12
  export const PROGRESS_OBSERVATION = Symbol('canonical progress observation');
13
13
  export const PROGRESS_ENDPOINT = '/api/progress';
14
+ // A projection keeps the first PROGRESS_GATE_LIMIT continuation gates (the snapshot schema's gates maxItems).
15
+ export const PROGRESS_GATE_LIMIT = 16;
14
16
  const accepted = (value,values,fallback='unknown')=>values.includes(value)?value:fallback;
15
17
  const hash = value=>typeof value==='string'&&/^(?:sha256:)?[0-9a-f]{64}$/i.test(value)?`sha256:${value.replace(/^sha256:/i,'').toLowerCase()}`:null;
16
18
  const id = value=>typeof value==='string'&&/^[A-Za-z0-9_-]{1,64}$/.test(value)?value:null;
@@ -56,7 +58,7 @@ export function projectProgressObservation({workspace,context,report,doctor,cont
56
58
  binding:reportBound&&id(verdict.run_id)&&id(verdict.run_id)===id(report?.stages?.qa?.verdict_run_id)&&build&&qaSource===build&&doctor?.derived?.build_output_fingerprint?.status==='pass'&&hash(verdict.spec_hash)===localHash?'matching':'unconfirmed',
57
59
  publish_state:accepted(qaResult?.qa_verdict_publish?.state,['skipped','ok','failed']),
58
60
  }:null;
59
- const gates=(Array.isArray(continuation?.gates)?continuation.gates:[]).slice(0,16).map(gate=>({id:accepted(gate?.id,PROGRESS_GATE_IDS),state:PROGRESS_GATE_IDS.includes(gate?.id)?accepted(gate?.status,['pass','blocked','waived','not_applicable']):'unknown'}));
61
+ const gates=(Array.isArray(continuation?.gates)?continuation.gates:[]).slice(0,PROGRESS_GATE_LIMIT).map(gate=>({id:accepted(gate?.id,PROGRESS_GATE_IDS),state:PROGRESS_GATE_IDS.includes(gate?.id)?accepted(gate?.status,['pass','blocked','waived','not_applicable']):'unknown'}));
60
62
  const actions=[...new Set((Array.isArray(continuation?.next_actions)?continuation.next_actions:[]).slice(0,64).map(action=>accepted(action?.id,PROGRESS_ACTION_IDS)))];
61
63
  const stage=accepted(continuation?.stage,PROGRESS_CONTINUATIONS);
62
64
  const preview=typeof packet?.deploy?.preview_url==='string'&&packet.deploy.preview_url?packet.deploy.preview_url:null;