@nextcommerce/campaigns-os 1.50.0 → 1.53.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 (87) hide show
  1. package/CHANGELOG.md +474 -0
  2. package/agents/claude/CLAUDE.md +4 -2
  3. package/agents/codex/AGENTS.md +3 -1
  4. package/agents/copilot/copilot-instructions.md +3 -1
  5. package/agents/cursor/campaigns-os.mdc +3 -1
  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 +282 -14
  13. package/contracts/release-ledger.json +1233 -0
  14. package/contracts/supported-surface.json +2 -2
  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 +2 -0
  18. package/docs/build-packet.md +148 -23
  19. package/docs/campaign-build-brief.md +57 -2
  20. package/docs/effects.md +14 -0
  21. package/docs/local-setup.md +1 -1
  22. package/docs/orientation-contract-reference.md +1 -1
  23. package/docs/polish-evidence.md +12 -0
  24. package/docs/progress-snapshots.md +2 -4
  25. package/docs/qa-and-test-orders.md +47 -4
  26. package/docs/readback.md +19 -2
  27. package/docs/runtime-readiness.md +1 -1
  28. package/docs/sdk-storage-compatibility.md +1 -1
  29. package/docs/skills-revision.md +10 -10
  30. package/package.json +1 -1
  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 +5 -3
  36. package/skills/next-campaigns-os/SKILL.md +11 -20
  37. package/skills/next-campaigns-os/references/session-intake.md +17 -9
  38. package/skills/next-campaigns-os-setup/SKILL.md +4 -3
  39. package/skills/next-campaigns-polish/SKILL.md +7 -5
  40. package/skills/next-campaigns-qa/SKILL.md +8 -5
  41. package/skills.json +10 -10
  42. package/src/adapter-decision-contract.mjs +1 -1
  43. package/src/brand-theme.mjs +33 -20
  44. package/src/brief-summary.mjs +275 -0
  45. package/src/build-brief.mjs +229 -30
  46. package/src/built-site-scope.mjs +39 -6
  47. package/src/built-smoke-qc.mjs +1117 -0
  48. package/src/campaign-identity.mjs +36 -2
  49. package/src/cart-placeholders.mjs +730 -0
  50. package/src/cli.mjs +467 -143
  51. package/src/commercial-journey.mjs +65 -4
  52. package/src/commercial-parity.mjs +6 -1
  53. package/src/contrast.mjs +649 -0
  54. package/src/design-source-publication.mjs +110 -3
  55. package/src/doctor/checks.mjs +415 -33
  56. package/src/doctor/inspect.mjs +53 -2
  57. package/src/doctor/next-step.mjs +16 -7
  58. package/src/input-currency.mjs +394 -0
  59. package/src/input-refresh.mjs +708 -0
  60. package/src/invocation.mjs +5 -2
  61. package/src/local-preview-policy.mjs +1 -1
  62. package/src/local-proof.mjs +4 -1
  63. package/src/polish-browser.mjs +660 -141
  64. package/src/polish-capture.mjs +1 -1
  65. package/src/polish-deadline.mjs +7 -0
  66. package/src/polish-media-weight.mjs +492 -0
  67. package/src/polish-node.mjs +400 -28
  68. package/src/polish-readability.mjs +828 -0
  69. package/src/progress-node.mjs +19 -3
  70. package/src/qa-browser.mjs +439 -180
  71. package/src/qa-content-params.mjs +889 -0
  72. package/src/qa-node.mjs +116 -13
  73. package/src/qa-order-bump.mjs +22 -1
  74. package/src/qa-policy-links.mjs +1019 -0
  75. package/src/qa-tracking-params.mjs +1389 -0
  76. package/src/qa-url-privacy.mjs +168 -0
  77. package/src/qc-accept.mjs +482 -0
  78. package/src/qc-check-registry.mjs +91 -0
  79. package/src/qc-results.mjs +1341 -0
  80. package/src/readback.mjs +58 -10
  81. package/src/sdk-attribute-index.mjs +71 -0
  82. package/src/sdk-markup.mjs +2 -2
  83. package/src/sdk-storage-compatibility.mjs +63 -3
  84. package/src/source-prep.mjs +37 -7
  85. package/src/stage-ledger.mjs +235 -44
  86. package/src/stage-record.mjs +165 -33
  87. package/src/tooling-setup.mjs +1 -1
@@ -7,7 +7,11 @@ import { writeThemeArtifacts } from "./brand-theme.mjs";
7
7
  import { cloneJson, filesystemPathsMatch, isObject, isNonEmptyString, optionalString, readJson, resolveFromFile, sha256File } from "./cli-helpers.mjs";
8
8
  import { DESIGN_SOURCE_PACKAGE_REL_PATH, createDesignSourcePackageArtifactReference, hashSerializedDesignSourcePackage, serializeDesignSourcePackage, synthesizeHtmlFunnelDesignSourcePackage, validateDesignSourcePackage } from "./design-source-package.mjs";
9
9
  import { stampDoctorProducer } from "./doctor-sidecar.mjs";
10
+ import { stageWriteInputs } from "./input-currency.mjs";
11
+ import { cmd } from "./install-invocation.mjs";
10
12
  import { ASSEMBLY_REPORT_STAGE_KEYS } from "./orchestration-stage-contract.mjs";
13
+ import { HOST_STRIPPED_CODE } from "./source-html-intake.mjs";
14
+ import { archiveForForceReset, stageHistoryPresent } from "./stage-ledger.mjs";
11
15
  import { targetLockPath, withTargetLock } from "./target-lock.mjs";
12
16
  import { resolveTemplateFamilyDesignSource } from "./template-reference.mjs";
13
17
 
@@ -71,12 +75,93 @@ function guardAssemblyReportOverwrite(reportPath, args, { announced = null } = {
71
75
  const unannounced = announced ? stageKeys.filter((key) => !announced.includes(key)) : stageKeys;
72
76
  if (unannounced.length > 0) {
73
77
  console.warn(
74
- `[campaigns-os prepare-build] --force: overwriting assembly report at ${reportPath}; clearing stage evidence for: ${unannounced.join(", ")}.`,
78
+ `[campaigns-os prepare-build] --force: overwriting assembly report at ${reportPath}; clearing stage evidence for: ${unannounced.join(", ")}. Completed build, Polish and QA records are archived into their stage history, which is kept.`,
75
79
  );
76
80
  }
77
81
  return stageKeys;
78
82
  }
79
83
 
84
+ // Operator state on the report an intake would replace, one line per item:
85
+ // recorded waivers and warning accepts, stage history, an applied theme, and
86
+ // report evidence other than intake's own host-strip lines.
87
+ function assemblyReportOperatorState(existingReport) {
88
+ if (!isObject(existingReport)) return [];
89
+ const items = [];
90
+ const count = (value) => (Array.isArray(value) ? value.length : 0);
91
+ if (count(existingReport.waivers)) items.push(`waivers (${count(existingReport.waivers)} recorded waiver${count(existingReport.waivers) === 1 ? "" : "s"})`);
92
+ if (count(existingReport.qc_accepts)) items.push(`qc_accepts (${count(existingReport.qc_accepts)} recorded warning accept${count(existingReport.qc_accepts) === 1 ? "" : "s"})`);
93
+ if (stageHistoryPresent(existingReport)) items.push("stage history (superseded build, Polish or QA records)");
94
+ if (existingReport.theme?.status === "applied") items.push("theme (status applied)");
95
+ const evidence = Array.isArray(existingReport.evidence) ? existingReport.evidence.filter((entry) => !(isObject(entry) && entry.code === HOST_STRIPPED_CODE)) : [];
96
+ if (evidence.length) items.push(`evidence (${evidence.length} recorded line${evidence.length === 1 ? "" : "s"})`);
97
+ return items;
98
+ }
99
+
100
+ // Packet settings recorded since the last intake that this run would
101
+ // overwrite (every setting `qa policy set` records): each named with the
102
+ // value on disk and the value this run writes.
103
+ const INTAKE_PACKET_SETTINGS = Object.freeze([
104
+ ["deploy.target", (packet) => packet?.deploy?.target],
105
+ ["deploy.preview_url", (packet) => packet?.deploy?.preview_url],
106
+ ["deploy.production_url", (packet) => packet?.deploy?.production_url],
107
+ ["qa.proof_policy.order_path_depth", (packet) => packet?.qa?.proof_policy?.order_path_depth],
108
+ ["campaign.allowed_domains_confirmed", (packet) => packet?.campaign?.allowed_domains_confirmed],
109
+ ]);
110
+
111
+ function packetSettingChanges(packetPath, planned) {
112
+ if (!isObject(planned) || !existsSync(packetPath)) return [];
113
+ let existing;
114
+ try {
115
+ existing = readJson(packetPath);
116
+ } catch {
117
+ return [];
118
+ }
119
+ const items = [];
120
+ for (const [field, read] of INTAKE_PACKET_SETTINGS) {
121
+ const onDisk = read(existing) ?? null;
122
+ const written = read(planned) ?? null;
123
+ if (onDisk !== written) items.push(`${field} (recorded ${JSON.stringify(onDisk)}; this run would write ${JSON.stringify(written)})`);
124
+ }
125
+ return items;
126
+ }
127
+
128
+ // Intake replaces the report and packet whole, so recorded operator state
129
+ // would be lost silently. Without --force it refuses and names each item;
130
+ // with --force it names each item as cleared (once).
131
+ function guardIntakeOperatorState(reportPath, packetPath, { force, planned = null, announced = [] }) {
132
+ let existingReport = null;
133
+ try {
134
+ existingReport = existsSync(reportPath) ? readJson(reportPath) : null;
135
+ } catch {
136
+ existingReport = null;
137
+ }
138
+ const items = [...assemblyReportOperatorState(existingReport), ...packetSettingChanges(packetPath, planned)];
139
+ if (!items.length) return announced;
140
+ if (force !== true) {
141
+ throw new Error(
142
+ `Rerunning prepare-build/start/build would discard recorded operator state: ${items.join("; ")}. `
143
+ + `To save brief answers or a CampaignSpec change without discarding anything, run ${cmd("record")} brief or ${cmd("record")} spec instead. `
144
+ + "To rerun intake anyway, pass the original deploy and order-path flags, or --force to clear the listed state (destructive; completed build, Polish and QA records are archived into stage history). "
145
+ + `After --force, record a cleared packet setting again with ${cmd("qa")} policy set.`,
146
+ );
147
+ }
148
+ const unannounced = items.filter((item) => !announced.includes(item));
149
+ if (unannounced.length) {
150
+ console.warn(`[campaigns-os prepare-build] --force: clearing recorded operator state: ${unannounced.join("; ")}.`);
151
+ }
152
+ return [...announced, ...unannounced];
153
+ }
154
+
155
+ // The report this run replaces, read for the --force archive; null when there
156
+ // is none or it cannot be read.
157
+ function readReplacedReport(reportPath) {
158
+ try {
159
+ return existsSync(reportPath) ? readJson(reportPath) : null;
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+
80
165
  function artifactRelativePath(artifactPath, targetPath) {
81
166
  const rel = relative(dirname(resolve(artifactPath)), resolve(targetPath)).replaceAll("\\", "/");
82
167
  return rel || ".";
@@ -806,12 +891,20 @@ export async function withDesignSourcePublication({ targetRepo, outputs, force =
806
891
  // fetched, and a run waiting here cannot replace it meanwhile.
807
892
  publishSpec?.();
808
893
  let announcedStageEvidence = guardAssemblyReportOverwrite(reportPath, { force });
894
+ // Operator state is checked once the run knows the packet settings it
895
+ // would write, before the package.
896
+ let plannedPacketSettings = null;
897
+ let announcedOperatorState = null;
898
+ const checkOperatorState = () => {
899
+ announcedOperatorState = guardIntakeOperatorState(reportPath, packetPath, { force, planned: plannedPacketSettings, announced: announcedOperatorState || [] });
900
+ };
809
901
  // Stage producers commit the report without this lock, so evidence can land
810
902
  // while the run works. Re-check before each write that is not rolled back:
811
903
  // the theme artifacts, then the JSON outputs. --force names a stage once.
812
904
  const recheckStageEvidence = () => {
813
905
  const found = guardAssemblyReportOverwrite(reportPath, { force }, { announced: announcedStageEvidence });
814
906
  announcedStageEvidence = [...new Set([...announcedStageEvidence, ...found])];
907
+ checkOperatorState();
815
908
  };
816
909
  let completed = 0;
817
910
  const enter = (index, { once = false } = {}) => {
@@ -821,8 +914,13 @@ export async function withDesignSourcePublication({ targetRepo, outputs, force =
821
914
  let designSourcePackage = null;
822
915
  return fn(Object.freeze({
823
916
  paths,
824
- package(inputs) {
917
+ // `plannedPacket`: the part of the packet this run would write that
918
+ // carries the settings INTAKE_PACKET_SETTINGS names, checked against
919
+ // the packet on disk.
920
+ package({ plannedPacket = null, ...inputs }) {
825
921
  enter(0, { once: true });
922
+ plannedPacketSettings = plannedPacket;
923
+ checkOperatorState();
826
924
  // Reject deterministic destination failures before publishing the fixed
827
925
  // DSP. The DSP intentionally precedes the theme and JSON artifacts that
828
926
  // reference it; a later I/O race may therefore leave a valid DSP for a
@@ -858,8 +956,17 @@ export async function withDesignSourcePublication({ targetRepo, outputs, force =
858
956
  completed = Math.max(completed, 2);
859
957
  return written;
860
958
  },
861
- publish({ packet, brief, context, report }) {
959
+ publish({ packet, brief, context, report: reseeded }) {
862
960
  enter(2, { once: true });
961
+ // The records this run replaces stay visible: completed build, Polish
962
+ // and QA records go to their stage history (only --force reaches here
963
+ // with any), and each stage's history and input change carry over.
964
+ const now = new Date().toISOString();
965
+ const bound = { briefMaterial: reseeded?.build_brief?.material ?? null, specMaterial: reseeded?.identity?.spec_material_hash ?? null };
966
+ const report = archiveForForceReset(readReplacedReport(reportPath), reseeded, {
967
+ now,
968
+ detectChange: (key, record) => stageWriteInputs(key, bound).detectChange(record),
969
+ });
863
970
  // Before the report goes out, narrow the pending record to exactly what the
864
971
  // report will say: the package's own hash when it is synthesized, nothing
865
972
  // when it is adopted. A candidate this run or an earlier failed one never