@nextcommerce/campaigns-os 1.43.2 → 1.47.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.
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +798 -5103
- package/README.md +33 -12
- package/agents/claude/CLAUDE.md +1 -1
- package/agents/codex/AGENTS.md +1 -1
- package/agents/copilot/copilot-instructions.md +1 -1
- package/agents/cursor/campaigns-os.mdc +1 -1
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +2 -2
- package/campaign-spec/dist/rules/analytics-contract-shape.js +2 -2
- package/campaign-spec/dist/rules/store-profile-shape.d.ts +5 -1
- package/campaign-spec/dist/rules/store-profile-shape.js +8 -10
- package/campaign-spec/dist/types.d.ts +2 -2
- package/compatibility.json +1 -1
- package/contracts/archive/CHANGELOG.2026-09-30.md +5111 -0
- package/contracts/archive/release-ledger.2026-09-30.json +5068 -0
- package/contracts/commerce-surface-catalog.json +1204 -129
- package/contracts/effects.v1.json +1179 -116
- package/contracts/orientation-reason-codes.v1.json +7 -0
- package/contracts/release-ledger.json +2515 -5919
- package/contracts/supported-surface.json +7 -4
- package/contracts/template-brand-contract.shared-commerce.v0.json +3 -3
- package/contracts/template-slot-manifest.shared-content-core.v0.json +403 -0
- package/docs/brand-theme-bridge.md +81 -0
- package/docs/build-packet.md +180 -23
- package/docs/campaigns-os-build-flow.md +3 -3
- package/docs/design-source-package.md +73 -0
- package/docs/effects.md +50 -8
- package/docs/gateway-login.md +3 -0
- package/docs/local-setup.md +7 -4
- package/docs/orientation-contract-reference.md +42 -2
- package/docs/polish-evidence.md +74 -0
- package/docs/qa-and-test-orders.md +118 -14
- package/docs/release-ledger-authoring-guide.md +64 -4
- package/docs/runtime-readiness.md +1 -1
- package/docs/sdk-storage-compatibility.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/docs/supported-surface.md +2 -2
- package/docs/versioning.md +4 -1
- package/package.json +1 -1
- package/schemas/campaigns-os-release-ledger.v1.schema.json +32 -2
- package/schemas/campaigns-os-tooling-orientation.v1.schema.json +1 -0
- package/skills/campaign-lifecycle-orientation/SKILL.md +16 -5
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +7 -6
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +7 -6
- package/skills/next-campaigns-os/SKILL.md +7 -7
- package/skills/next-campaigns-os/references/session-intake.md +9 -3
- package/skills/next-campaigns-os-setup/SKILL.md +5 -5
- package/skills/next-campaigns-polish/SKILL.md +28 -9
- package/skills/next-campaigns-qa/SKILL.md +7 -4
- package/skills.json +10 -10
- package/src/brand-theme.mjs +320 -20
- package/src/built-script-syntax.mjs +116 -15
- package/src/built-site-scope.mjs +16 -4
- package/src/cli.mjs +280 -46
- package/src/commercial-parity.mjs +48 -2
- package/src/deviation.mjs +13 -1
- package/src/diagnostic.mjs +6 -2
- package/src/doctor/checks.mjs +319 -81
- package/src/doctor/inspect.mjs +55 -13
- package/src/doctor/source-provenance.mjs +184 -0
- package/src/invocation.mjs +4 -0
- package/src/live-campaign-refs.mjs +466 -0
- package/src/login.mjs +2 -2
- package/src/page-kit-store-profile.mjs +69 -12
- package/src/page-kit-sync.mjs +31 -12
- package/src/progress-node.mjs +3 -1
- package/src/qa-analytics-parity.mjs +37 -2
- package/src/qa-binding-evidence.mjs +4 -2
- package/src/qa-browser.mjs +612 -40
- package/src/qa-commercial-parity.mjs +48 -5
- package/src/qa-node.mjs +122 -7
- package/src/qa-test-order-topology.mjs +148 -0
- package/src/sdk-markup.mjs +32 -7
- package/src/sdk-storage-compatibility.mjs +3 -2
- package/src/source-html-intake.mjs +116 -0
- package/src/stage-record.mjs +551 -0
- package/src/tooling-setup.mjs +9 -0
- package/src/upsell-selector-scope.mjs +112 -2
package/src/page-kit-sync.mjs
CHANGED
|
@@ -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
|
|
29
|
-
// not carry
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// shape (or the demo value itself),
|
|
33
|
-
//
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
package/src/progress-node.mjs
CHANGED
|
@@ -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,
|
|
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;
|
|
@@ -547,6 +547,10 @@ function valuesEqual(a, b) {
|
|
|
547
547
|
// flag carried-over-tag regressions for human review.
|
|
548
548
|
// `options.url` is the candidate URL that was captured (the resolved capture
|
|
549
549
|
// target) — stamped on every emitted assertion, pass and fail alike.
|
|
550
|
+
// `options.candidatePage` describes the page the candidate was captured on when
|
|
551
|
+
// the leg picked it automatically (#512): `{ receipt, source, page_type }`.
|
|
552
|
+
// An explicit --analytics-candidate passes none and is treated as the receipt
|
|
553
|
+
// the operator named.
|
|
550
554
|
export function diffAnalyticsParity(baseline, candidate, options = {}) {
|
|
551
555
|
const assertions = [];
|
|
552
556
|
const auditedUrl = (typeof options.url === "string" && options.url.trim()) ? options.url.trim() : null;
|
|
@@ -561,13 +565,44 @@ export function diffAnalyticsParity(baseline, candidate, options = {}) {
|
|
|
561
565
|
const cEff = effectivePurchase(c);
|
|
562
566
|
|
|
563
567
|
// 1. Purchase present on candidate — the highest-value blocking check.
|
|
564
|
-
|
|
568
|
+
// #512: Purchase fires only on a receipt. When the candidate is known not to
|
|
569
|
+
// be one (the automatic capture page is the campaign root or a built entry),
|
|
570
|
+
// a missing Purchase is a page mismatch, not a regression, so it goes to
|
|
571
|
+
// manual review naming the mismatch. A receipt candidate, or one the
|
|
572
|
+
// operator named, still blocks.
|
|
573
|
+
const candidatePage = options.candidatePage && typeof options.candidatePage === "object" ? options.candidatePage : null;
|
|
574
|
+
if (!cEff.fired && candidatePage?.receipt === false) {
|
|
575
|
+
const baselineFired = effectivePurchase(b).fired;
|
|
576
|
+
assertions.push(emit({
|
|
577
|
+
id: "analytics-parity:purchase-present",
|
|
578
|
+
status: STATUS.MANUAL_REVIEW,
|
|
579
|
+
severity: SEVERITY.WARN,
|
|
580
|
+
expected: "a receipt candidate to check Purchase on; the automatic candidate is not a receipt, so pair receipts with --analytics-candidate",
|
|
581
|
+
actual: baselineFired
|
|
582
|
+
? "baseline fired a Purchase but the automatic candidate is not a receipt page; pass --analytics-candidate <candidate receipt url> to compare receipts"
|
|
583
|
+
: "neither page fired a Purchase and the automatic candidate is not a receipt page; pass receipt URLs to --analytics-baseline and --analytics-candidate to compare Purchase",
|
|
584
|
+
evidence: {
|
|
585
|
+
via: cEff.via,
|
|
586
|
+
candidate_events: c.eventNames || [],
|
|
587
|
+
baseline_purchase: bp,
|
|
588
|
+
page_mismatch: {
|
|
589
|
+
reason: baselineFired ? "receipt_baseline_non_receipt_candidate" : "candidate_not_receipt",
|
|
590
|
+
baseline_fired_purchase: baselineFired,
|
|
591
|
+
candidate_receipt: candidatePage.receipt,
|
|
592
|
+
candidate_source: candidatePage.source ?? null,
|
|
593
|
+
candidate_page_type: candidatePage.page_type ?? null,
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
}));
|
|
597
|
+
} else assertions.push(emit({
|
|
565
598
|
id: "analytics-parity:purchase-present",
|
|
566
599
|
status: cEff.fired ? STATUS.PASS : STATUS.FAIL,
|
|
567
600
|
severity: SEVERITY.BLOCKER,
|
|
568
601
|
expected: "candidate fires a Purchase (dl_purchase, or Meta/GA4 pixel if the SDK event is blocked)",
|
|
569
602
|
actual: cEff.fired ? `purchase fired via ${cEff.via}` : "no purchase fire captured on candidate (dataLayer, Meta, or GA4)",
|
|
570
|
-
|
|
603
|
+
// An automatic candidate's page is recorded even when it fired, so a pass on
|
|
604
|
+
// a non-receipt page reads as such.
|
|
605
|
+
evidence: { via: cEff.via, candidate_events: c.eventNames || [], candidate_signals: c.purchaseSignals || {}, baseline_purchase: bp, ...(candidatePage ? { candidate_page: candidatePage } : {}) },
|
|
571
606
|
}));
|
|
572
607
|
|
|
573
608
|
if (cEff.fired) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parse as parseHtml } from 'parse5';
|
|
2
2
|
import { parse as parseJs } from 'acorn';
|
|
3
3
|
import { createPageSourceLoader, resolveCommercialApiKey } from './qa-commercial-parity.mjs';
|
|
4
|
-
import { HTML_NAMESPACE, baseInEffect, documentBases, frozenBaseUrl, parseFailureDiagnostic, scriptKind } from './built-script-syntax.mjs';
|
|
4
|
+
import { HTML_NAMESPACE, baseInEffect, documentBases, endsUnclosed, frozenBaseUrl, parseFailureDiagnostic, scriptKind } from './built-script-syntax.mjs';
|
|
5
5
|
|
|
6
6
|
export const BINDING_SCHEMA = 'campaigns-os-page-binding/v0';
|
|
7
7
|
export const BINDING_LIMITS = Object.freeze({ scripts_per_page: 6, scripts_per_run: 24, script_bytes: 262144, timeout_ms: 5000 });
|
|
@@ -124,8 +124,10 @@ export async function observeBinding({ source, page, expected, scriptLoader, par
|
|
|
124
124
|
// Only an HTML-namespace <script> loads `src`. An SVG script runs from
|
|
125
125
|
// href / xlink:href or its inline text, which this static read does not
|
|
126
126
|
// model: it is not fetched and leaves the binding dynamic.
|
|
127
|
+
// A script the file ends inside never reaches its end tag, so the parser
|
|
128
|
+
// never prepares it: the browser neither fetches nor runs it (#515).
|
|
127
129
|
if (node.tagName === 'script' && node.namespaceURI !== HTML_NAMESPACE) dynamic = true;
|
|
128
|
-
else if (node.tagName === 'script') scripts.push({ attrs, base: baseInEffect(bases, node), text: (node.childNodes || []).map(n => n.value || '').join('') });
|
|
130
|
+
else if (node.tagName === 'script' && !endsUnclosed(node)) scripts.push({ attrs, base: baseInEffect(bases, node), text: (node.childNodes || []).map(n => n.value || '').join('') });
|
|
129
131
|
// parse5 keeps template content separate; it is inert, as is noscript at boot.
|
|
130
132
|
if (node.tagName !== 'noscript') for (const child of node.childNodes || []) walk(child);
|
|
131
133
|
};
|