@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
@@ -8,10 +8,13 @@ import { commitAssemblyReport, recordProducerStageOutcome } from "../stage-ledge
8
8
  import { annotateDoctorIssueCauses } from "../finding-cause.mjs";
9
9
  import { DOCTOR_SIDECAR_SCHEMA } from "../doctor-sidecar.mjs";
10
10
  import { resolveCampaignWorkspace } from "../campaign-workspace.mjs";
11
+ import { normalizePublicRouteSlug } from "../route-identity.mjs";
12
+ import { DEFAULT_PROXY_BASE } from "../spec-fetch.mjs";
13
+ import { disabledLiveRead, liveRefsDisabled, readLiveCampaignForPacket } from "../live-campaign-refs.mjs";
11
14
  import { evaluateThemeGate } from "../theme-gate.mjs";
12
15
  import { findForbiddenPriceHides } from "../template-brand-contract.mjs";
13
16
  import { resolveBuiltSiteScope, synthesizeMinimalBuildPacket } from "../built-site-scope.mjs";
14
- import { UPSELL_SELECTOR_SCOPE } from "../upsell-selector-scope.mjs";
17
+ import { UPSELL_SELECTOR_SCOPE, builtPageTypeOverRouteGuess } from "../upsell-selector-scope.mjs";
15
18
  import { CAMPAIGN_IDENTITY } from "../campaign-identity.mjs";
16
19
  import { SDK_MARKUP } from "../sdk-markup.mjs";
17
20
  import { SCRIPT_SYNTAX, collectBuiltScriptSyntaxInputs } from "../built-script-syntax.mjs";
@@ -88,7 +91,38 @@ function relativizeDoctorOutput(result, baseDir) {
88
91
  // Sidecar producer name; see the producer comment above NEXT_PRODUCER in src/cli.mjs.
89
92
  const DOCTOR_PRODUCER = "doctor";
90
93
 
91
- export function doctorCommand(args, { runDoctor = doctorPacket } = {}) {
94
+ // The live campaign read the `doctor` command makes before it inspects (#533):
95
+ // packet mode only, only when the packet's built _site/<route>/ exists (with
96
+ // no built page there is nothing to compare, so nothing is sent), and only
97
+ // under the public Campaigns API key the packet, its local CampaignSpec or its
98
+ // declared campaign-key env var resolves. One GET of {proxy-base}/api/campaign;
99
+ // --proxy-base names the proxy, else the canonical one; --no-live-refs sends
100
+ // nothing and records not_run with reason `disabled`. `undefined` means no
101
+ // read was due, which doctor records as not_run; every other outcome,
102
+ // failures included, is a readLiveCampaign result.
103
+ export async function readDoctorLiveCampaign(args, { fetchImpl = globalThis.fetch, env = process.env, warn = undefined } = {}) {
104
+ if (((args.built || args.site) && !args.packet) || !isNonEmptyString(args.packet)) return undefined;
105
+ if (liveRefsDisabled(args)) return disabledLiveRead();
106
+ let workspace;
107
+ try {
108
+ workspace = resolveCampaignWorkspace(resolve(args.packet), { followContextPointer: false });
109
+ } catch {
110
+ // An unreadable packet is doctor's own blocker; there is nothing to read for.
111
+ return undefined;
112
+ }
113
+ const slug = normalizePublicRouteSlug(workspace.packet?.campaign?.public_route_slug);
114
+ if (!workspace.targetRepo || !slug || !existsSync(join(workspace.targetRepo, "_site", slug))) return undefined;
115
+ return readLiveCampaignForPacket({
116
+ packet: workspace.packet,
117
+ packetPath: workspace.packetPath,
118
+ env,
119
+ fetchImpl,
120
+ proxyBase: optionalString(args["proxy-base"]) || DEFAULT_PROXY_BASE,
121
+ ...(warn ? { warn } : {}),
122
+ });
123
+ }
124
+
125
+ export function doctorCommand(args, { runDoctor = doctorPacket, liveCampaign = undefined } = {}) {
92
126
  // Non-packet mode (learnings L7): doctor a `campaign-build`'d page-kit
93
127
  // campaign that has only a built _site/ and no full Build Packet. Resolves
94
128
  // scope from the built output and runs the built-output residue/text/
@@ -103,6 +137,9 @@ export function doctorCommand(args, { runDoctor = doctorPacket } = {}) {
103
137
  contextPath: args.context ? resolve(args.context) : explicitSidecarArgs ? null : undefined,
104
138
  reportPath: args.report ? resolve(args.report) : explicitSidecarArgs ? null : undefined,
105
139
  outputBaseDir: args["strip-paths"] === true ? dirname(packetPath) : null,
140
+ // A live campaign read (live-campaign-refs.mjs) the caller already made;
141
+ // absent, the live ref check is recorded not_run.
142
+ ...(liveCampaign !== undefined ? { liveCampaign } : {}),
106
143
  };
107
144
  const result = runDoctor(packetPath, doctorOptions);
108
145
  // Inspection and recording are separate operations. A laptop's untracked
@@ -234,20 +271,25 @@ export function doctorBuiltOutput(args) {
234
271
  // branch above: the defect is family-independent, and this mode is reached
235
272
  // without --family more often than with it. Page roles come from the built
236
273
  // route (resolveBuiltSiteScope infers them) and from each page's own
237
- // next-page-type meta, so no packet or spec is needed. No assembly report
238
- // exists on this path, so there are no waivers to assess — a blocker here is
239
- // repaired in the source, or waived through the packet path.
274
+ // next-page-type meta, so no packet or spec is needed; a `checkout` meta the
275
+ // browser reads, declared unambiguously, replaces an ambiguous route guess
276
+ // such as "/checkout-oto-1/", never an explicit upsell or downsell route (#529). No
277
+ // assembly report exists on this path, so there are no waivers to assess — a
278
+ // blocker here is repaired in the source, or waived through the packet path.
240
279
  recordUpsellSelectorScopeGate({
241
280
  subject: {
242
281
  public_route_slug: scope.slug || null,
243
282
  site_root: relFromDir(targetRepo, scope.campaign_dir),
244
283
  },
245
- pages: scope.pages.map((page) => ({
246
- page_id: page.page_id,
247
- page_type: page.page_type,
248
- file: relFromDir(targetRepo, page.built_path),
249
- content: readFileSync(page.built_path, "utf8"),
250
- })),
284
+ pages: scope.pages.map((page) => {
285
+ const content = readFileSync(page.built_path, "utf8");
286
+ return {
287
+ page_id: page.page_id,
288
+ page_type: builtPageTypeOverRouteGuess({ route: page.route, route_type: page.page_type, content }),
289
+ file: relFromDir(targetRepo, page.built_path),
290
+ content,
291
+ };
292
+ }),
251
293
  waivers: null,
252
294
  errors,
253
295
  warnings,
@@ -374,7 +416,7 @@ export function doctorPacket(packetPath, options = {}) {
374
416
  return result;
375
417
  }
376
418
 
377
- function inspectDoctorPacket(packetPath, { contextPath = undefined, reportPath = undefined, outputBaseDir = null } = {}) {
419
+ function inspectDoctorPacket(packetPath, { contextPath = undefined, reportPath = undefined, outputBaseDir = null, liveCampaign = undefined } = {}) {
378
420
  // The Build Context records where prepare-build wrote the report
379
421
  // (--report-out). `next` follows that pointer when no --report is given;
380
422
  // doctor reads the same report so its gates and its next block cannot
@@ -425,7 +467,7 @@ function inspectDoctorPacket(packetPath, { contextPath = undefined, reportPath =
425
467
  },
426
468
  };
427
469
 
428
- validatePacket(packet, packetPath, errors, warnings, ready, derived, { context, report });
470
+ validatePacket(packet, packetPath, errors, warnings, ready, derived, { context, report, liveCampaign });
429
471
  runDoctorChecks(ARTIFACT_DOCTOR_CHECKS, { context, report, errors, warnings, ready, derived });
430
472
 
431
473
  // Doctor and the stage ladder must agree over one packet (#238): when the
@@ -0,0 +1,184 @@
1
+ // Source-provenance checkpoint (#534). A CampaignSpec page whose design_source
2
+ // is Figma makes doctor demand figma-sections-export provenance from the
3
+ // source-html manifest (source_html.producer_provenance*). When the approved
4
+ // source for that page is hand-written HTML and the Figma file is only a
5
+ // render of it, no export exists to supply that provenance. This gate lets a
6
+ // named human record that, per page, with a reason and a bound, through
7
+ // `checkpoint waive --gate source_html.producer_provenance --page <page_id>`.
8
+ //
9
+ // A waiver never suppresses a finding. Doctor reports the provenance findings
10
+ // once per Figma-typed page: as warnings carrying `waived: true` for a waived
11
+ // page, and as errors for an unwaived one. The family is the
12
+ // source_html.producer_provenance* codes plus source_html.files.partial and
13
+ // source_html.files.asset, the export's own file-inventory shape, which only
14
+ // a Figma-typed page is held to. When the manifest itself claims to be a
15
+ // figma-sections-export output, the findings are the manifest's own: they
16
+ // stay manifest-wide errors, the gate is not waivable, and any waiver
17
+ // recorded for the page is inert. Every other source check (manifest
18
+ // validity, wrapper policy, screenshot proof) is evaluated elsewhere and is
19
+ // untouched by this gate.
20
+
21
+ import {
22
+ assessCheckpointWaivers,
23
+ checkpointStateFingerprint,
24
+ projectCheckpointWaiverAssessment,
25
+ } from "../checkpoint-waiver.mjs";
26
+
27
+ export const SOURCE_PROVENANCE_SCOPE = "source_html.producer_provenance";
28
+
29
+ function isPlainObject(value) {
30
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
31
+ }
32
+
33
+ function emptyWaiverAssessment() {
34
+ return { active: null, inert_counts: { stale: 0, foreign: 0, malformed: 0, expired: 0 } };
35
+ }
36
+
37
+ // True when a source-html manifest's generator names figma-sections-export in
38
+ // any form: bare, `@<version>` or another suffix, any case, surrounding
39
+ // whitespace. Such a manifest claims to be a real export, so its provenance
40
+ // findings are its own and no page waiver clears them. Every reader of the
41
+ // generator claim uses this one predicate.
42
+ export function generatorClaimsFigmaExport(generator) {
43
+ if (typeof generator !== "string") return false;
44
+ return /(^|[^a-z0-9_-])figma-sections-export(?![a-z0-9_-])/i.test(generator.trim());
45
+ }
46
+
47
+ export function isSourceProvenanceCode(code) {
48
+ const value = String(code || "");
49
+ return value === SOURCE_PROVENANCE_SCOPE || value.startsWith(`${SOURCE_PROVENANCE_SCOPE}.`);
50
+ }
51
+
52
+ // The Figma-export file inventory: a manifest behind a Figma-typed page must
53
+ // list section partials and exported assets. Hand-written HTML has neither, so
54
+ // these two findings belong to the waivable family.
55
+ export const FIGMA_EXPORT_FILE_CODES = Object.freeze(["source_html.files.partial", "source_html.files.asset"]);
56
+
57
+ // The gate code when the manifest's generator claims figma-sections-export:
58
+ // blocked, not waivable, and any waiver recorded for the page is inert.
59
+ export const SOURCE_PROVENANCE_EXPORTER_CLAIM_CODE = `${SOURCE_PROVENANCE_SCOPE}.exporter_claim`;
60
+
61
+ function waiveCommand(pageId) {
62
+ return `campaigns-os checkpoint waive --packet <packet> --gate ${SOURCE_PROVENANCE_SCOPE} --page ${pageId} --reason "<reason>" --waived-by "<named human>" --expires-at <ISO>`;
63
+ }
64
+
65
+ /**
66
+ * One checkpoint gate per Figma-typed page.
67
+ *
68
+ * @param {{
69
+ * pages: Array<{ page_id: string, design_source: { type: string|null, file_url: string|null } }>,
70
+ * blockingCodes: string[],
71
+ * generatorClaimsExport?: boolean,
72
+ * waivers?: unknown,
73
+ * now?: string,
74
+ * }} input
75
+ */
76
+ export function evaluateSourceProvenanceGates({ pages = [], blockingCodes = [], generatorClaimsExport = false, waivers = null, now = new Date().toISOString() } = {}) {
77
+ const records = (Array.isArray(waivers) ? waivers : [])
78
+ .filter((record) => isPlainObject(record) && record.scope === SOURCE_PROVENANCE_SCOPE);
79
+ const figmaPageIds = new Set(pages.map((page) => page.page_id));
80
+ const findings = [...new Set(blockingCodes)].sort();
81
+ let exporterClaimRecords = 0;
82
+
83
+ const gates = pages.map((page) => {
84
+ const subject = { page_id: page.page_id };
85
+ const state = { design_source: page.design_source, findings };
86
+ const base = {
87
+ id: SOURCE_PROVENANCE_SCOPE,
88
+ scope: SOURCE_PROVENANCE_SCOPE,
89
+ subject,
90
+ state,
91
+ };
92
+ if (findings.length === 0) {
93
+ return {
94
+ ...base,
95
+ status: "pass",
96
+ code: `${SOURCE_PROVENANCE_SCOPE}.pass`,
97
+ reason: `Page "${page.page_id}" has a Figma design source and the source-html manifest carries semantic figma-sections-export provenance.`,
98
+ waivable: false,
99
+ state_fingerprint: null,
100
+ waiver: null,
101
+ waiver_assessment: emptyWaiverAssessment(),
102
+ required_actions: [],
103
+ };
104
+ }
105
+ // Only this page's records are assessed here. Another page's waiver is
106
+ // not "foreign" history for this page; it belongs to that page's gate.
107
+ const pageRecords = records.filter((record) => isPlainObject(record.subject) && record.subject.page_id === page.page_id);
108
+ // An exporter claim takes precedence over every other inert kind: it makes
109
+ // each of this page's records inert whatever else is true of it (stale,
110
+ // expired, malformed), so each is counted once, as exporter_claim, and
111
+ // never assessed.
112
+ if (generatorClaimsExport) {
113
+ exporterClaimRecords += pageRecords.length;
114
+ return {
115
+ ...base,
116
+ status: "blocked",
117
+ code: SOURCE_PROVENANCE_EXPORTER_CLAIM_CODE,
118
+ reason: `Page "${page.page_id}" has a Figma design source and the source-html manifest's generator claims figma-sections-export, but the manifest lacks that export's provenance (${findings.join(", ")}). A real export carries it, so this is not waivable; re-run figma-sections-export, or, when the approved source is hand-written HTML, set the manifest's generator to name the real producer and record a page waiver.`,
119
+ waivable: false,
120
+ state_fingerprint: null,
121
+ waiver: null,
122
+ waiver_assessment: emptyWaiverAssessment(),
123
+ required_actions: [
124
+ {
125
+ id: "repair_target",
126
+ kind: "manual",
127
+ command: null,
128
+ description: "Re-run figma-sections-export so the source-html manifest carries semantic producer_provenance, or correct the manifest's generator when no export produced it, then re-run doctor.",
129
+ },
130
+ ],
131
+ };
132
+ }
133
+ const state_fingerprint = checkpointStateFingerprint({ scope: SOURCE_PROVENANCE_SCOPE, subject, state });
134
+ const checkpoint = { scope: SOURCE_PROVENANCE_SCOPE, subject, state_fingerprint };
135
+ const waiver_assessment = projectCheckpointWaiverAssessment(
136
+ assessCheckpointWaivers(pageRecords, checkpoint, { now }),
137
+ checkpoint,
138
+ );
139
+ const waiver = waiver_assessment.active;
140
+ return {
141
+ ...base,
142
+ status: waiver ? "waived" : "blocked",
143
+ code: waiver ? `${SOURCE_PROVENANCE_SCOPE}.waived` : SOURCE_PROVENANCE_SCOPE,
144
+ reason: waiver
145
+ ? `Page "${page.page_id}" has a Figma design source but no figma-sections-export provenance (${findings.join(", ")}); accepted under a named-human decision that its approved source is hand-written HTML.`
146
+ : `Page "${page.page_id}" has a Figma design source, so doctor requires figma-sections-export provenance in the source-html manifest (${findings.join(", ")}). Re-run figma-sections-export, or, when the approved source is hand-written HTML and the Figma file only renders it, record a named-human waiver for this page.`,
147
+ waivable: true,
148
+ state_fingerprint,
149
+ waiver,
150
+ waiver_assessment,
151
+ required_actions: waiver ? [] : [
152
+ {
153
+ id: "repair_target",
154
+ kind: "manual",
155
+ command: null,
156
+ description: "Re-run figma-sections-export for this page so the source-html manifest carries semantic producer_provenance, then re-run doctor.",
157
+ },
158
+ {
159
+ id: "waive_checkpoint",
160
+ kind: "command",
161
+ command: waiveCommand(page.page_id),
162
+ description: `When page "${page.page_id}"'s approved source is hand-written HTML, record a named-human waiver with a reason and a bound. Manifest, wrapper-policy and screenshot-proof checks still apply.`,
163
+ },
164
+ ],
165
+ };
166
+ });
167
+
168
+ // Records naming a page that no longer has a Figma design source (the
169
+ // page's design_source changed, or the page left the spec) can never
170
+ // satisfy a gate again.
171
+ const noFigmaSource = records.filter((record) => !isPlainObject(record.subject)
172
+ || !figmaPageIds.has(record.subject.page_id));
173
+ const counts = { stale: 0, foreign: 0, malformed: 0, expired: 0 };
174
+ for (const gate of gates) {
175
+ for (const kind of Object.keys(counts)) counts[kind] += gate.waiver_assessment?.inert_counts?.[kind] || 0;
176
+ }
177
+ counts.no_figma_source = noFigmaSource.length;
178
+ counts.exporter_claim = exporterClaimRecords;
179
+ const inertPages = [...new Set(noFigmaSource
180
+ .map((record) => (isPlainObject(record.subject) && typeof record.subject.page_id === "string" ? record.subject.page_id : null))
181
+ .filter(Boolean))].sort();
182
+
183
+ return { gates, inert: { counts, pages: inertPages } };
184
+ }
@@ -65,6 +65,7 @@ const COMMANDS = frozen({
65
65
  theme: { subcommands: ["generate", "inspect", "waive"] },
66
66
  checkpoint: { subcommands: ["waive"] },
67
67
  polish: { subcommands: ["capture"] },
68
+ record: { subcommands: ["build", "polish", "setup"] },
68
69
  "validate-assembly-report": {},
69
70
  "install-agent-context": { dryRun: true },
70
71
  "install-skills": { dryRun: true },
@@ -90,6 +91,9 @@ const SUBCOMMAND_OVERRIDES = frozen({
90
91
  "page-kit sync": { dryRun: true },
91
92
  "spec derive": { dryRun: true },
92
93
  "qa publish": { dryRun: true },
94
+ "record build": { dryRun: true },
95
+ "record polish": { dryRun: true },
96
+ "record setup": { dryRun: true },
93
97
  "qa run": { autoEnd: true },
94
98
  "run start": { sweepRoot: "session" },
95
99
  "run end": { sweepRoot: "session", dryRun: true },