@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.
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +648 -5103
- package/README.md +32 -11
- 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/contracts/archive/CHANGELOG.2026-09-30.md +5111 -0
- package/contracts/archive/release-ledger.2026-09-30.json +5068 -0
- package/contracts/effects.v1.json +1176 -113
- package/contracts/orientation-reason-codes.v1.json +7 -0
- package/contracts/release-ledger.json +2345 -6087
- package/contracts/supported-surface.json +7 -4
- 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 +158 -21
- 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 +1 -1
- package/docs/orientation-contract-reference.md +42 -2
- package/docs/polish-evidence.md +74 -0
- package/docs/qa-and-test-orders.md +99 -13
- 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-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 +5 -2
- package/src/doctor/checks.mjs +320 -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-browser.mjs +538 -28
- 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 +72 -8
- package/src/source-html-intake.mjs +116 -0
- package/src/stage-record.mjs +551 -0
- package/src/upsell-selector-scope.mjs +112 -2
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
// filesystem, so both the packet doctor path and the built-site-only path
|
|
40
40
|
// (`doctor --built`) can drive it with the same evaluator.
|
|
41
41
|
|
|
42
|
+
import { parse } from "parse5";
|
|
43
|
+
|
|
44
|
+
import { ROUTE_TOKENS } from "./built-site-scope.mjs";
|
|
42
45
|
import {
|
|
43
46
|
assessCheckpointWaivers,
|
|
44
47
|
checkpointStateFingerprint,
|
|
@@ -131,15 +134,122 @@ export function isPostPurchasePageType(value) {
|
|
|
131
134
|
return POST_PURCHASE_PAGE_TYPES.has(String(value || "").toLowerCase().trim());
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
const PAGE_TYPE_META = "next-page-type";
|
|
138
|
+
// A `<meta>` tag anywhere in the source text, live or not, and its attributes.
|
|
139
|
+
const META_TAG = /<meta\b((?:"[^"]*"|'[^']*'|[^>"'])*)>/gi;
|
|
140
|
+
const TAG_ATTRIBUTE = /([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
141
|
+
// The looser pattern this reader replaced. Kept so `mentioned` is a superset
|
|
142
|
+
// of what it read: its `\bname=` / `\bcontent=` also match `data-name=` /
|
|
143
|
+
// `data-content=`, and it takes mismatched quotes.
|
|
144
|
+
const LEGACY_PAGE_TYPE_META = /<meta\b(?=[^>]*\bname=["']next-page-type["'])[^>]*\bcontent=["']([^"']*)["'][^>]*>/gi;
|
|
145
|
+
|
|
146
|
+
function liveMetaElements(node, found = []) {
|
|
147
|
+
// childNodes only: a <template>'s children live in `content`, a fragment
|
|
148
|
+
// outside the document, so they are skipped here as the browser skips them.
|
|
149
|
+
for (const child of node.childNodes || []) {
|
|
150
|
+
if (child.tagName === "meta") found.push(child);
|
|
151
|
+
liveMetaElements(child, found);
|
|
152
|
+
}
|
|
153
|
+
return found;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Every `next-page-type` value a built page carries, read two ways. This is
|
|
158
|
+
* the one reader both page-role decisions below share (#529).
|
|
159
|
+
*
|
|
160
|
+
* live the metas the browser puts in the document, in document order,
|
|
161
|
+
* matched as `meta[name="next-page-type"]` matches (the name
|
|
162
|
+
* exactly; attribute names in any case, attributes in any order,
|
|
163
|
+
* any quoting). Parsed with parse5, so a meta in a comment, in
|
|
164
|
+
* <template> content, or in the raw text of <script>, <noscript>
|
|
165
|
+
* (scripting on, which the SDK needs), <style>, <textarea> or
|
|
166
|
+
* <title> is not one. Values trimmed; "" when content is absent.
|
|
167
|
+
* mentioned every `<meta>`-shaped tag in the source text whose name is
|
|
168
|
+
* next-page-type in any case or spacing, live or not, plus every
|
|
169
|
+
* match of the looser pattern this replaced. Values trimmed and
|
|
170
|
+
* deduplicated, first occurrence first. Wider than that pattern,
|
|
171
|
+
* never narrower.
|
|
172
|
+
*/
|
|
173
|
+
export function readBuiltPageTypeMetas(content) {
|
|
174
|
+
const source = String(content || "");
|
|
175
|
+
const live = liveMetaElements(parse(source))
|
|
176
|
+
.map((element) => new Map(element.attrs.map((attr) => [attr.name, attr.value])))
|
|
177
|
+
.filter((attrs) => attrs.get("name") === PAGE_TYPE_META)
|
|
178
|
+
.map((attrs) => (attrs.get("content") ?? "").trim());
|
|
179
|
+
const mentioned = [];
|
|
180
|
+
for (const tag of source.matchAll(META_TAG)) {
|
|
181
|
+
const attrs = new Map();
|
|
182
|
+
for (const attr of tag[1].matchAll(TAG_ATTRIBUTE)) {
|
|
183
|
+
const name = attr[1].toLowerCase();
|
|
184
|
+
if (!attrs.has(name)) attrs.set(name, attr[2] ?? attr[3] ?? attr[4] ?? "");
|
|
185
|
+
}
|
|
186
|
+
if ((attrs.get("name") || "").trim().toLowerCase() === PAGE_TYPE_META) {
|
|
187
|
+
mentioned.push((attrs.get("content") ?? "").trim());
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const tag of source.matchAll(LEGACY_PAGE_TYPE_META)) mentioned.push(tag[1].trim());
|
|
191
|
+
return { live, mentioned: [...new Set(mentioned)] };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// The role the page declares for itself, or null when it declares none or
|
|
195
|
+
// declares it ambiguously. Only live metas count: markup the browser does not
|
|
196
|
+
// put in the document is not a declaration. The first live meta is the one a
|
|
197
|
+
// `querySelector` reads, so a blank first meta declares nothing; and every
|
|
198
|
+
// live meta must agree (case aside), because a page carrying both `checkout`
|
|
199
|
+
// and `receipt` has not said which it is.
|
|
200
|
+
export function builtPageTypeMeta(content) {
|
|
201
|
+
const [first = "", ...rest] = readBuiltPageTypeMetas(content).live;
|
|
202
|
+
if (!first) return null;
|
|
203
|
+
return rest.every((value) => value.toLowerCase() === first.toLowerCase()) ? first : null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Whether the route guess is too weak to stand against the page's own meta:
|
|
207
|
+
// an upsell read from "oto" / "one-time-offer" alone, with no "upsell" word.
|
|
208
|
+
// An explicit "upsell" or "downsell" word is never ambiguous, whatever else
|
|
209
|
+
// the route says: the charge comes from the page's place in the funnel, not
|
|
210
|
+
// its meta. A route that reads only as checkout needs no relief, since its
|
|
211
|
+
// guess is not post-purchase to begin with.
|
|
212
|
+
function routeGuessIsAmbiguous(route) {
|
|
213
|
+
if (route === null || route === undefined) return false;
|
|
214
|
+
const value = String(route).toLowerCase().trim();
|
|
215
|
+
if (ROUTE_TOKENS.upsell.test(value) || ROUTE_TOKENS.downsell.test(value)) return false;
|
|
216
|
+
return ROUTE_TOKENS.one_time_offer.test(value);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// The type to hand in as `page_type` when the only other source is a guess
|
|
220
|
+
// from the built route (resolveBuiltSiteScope's inferPageType). "/checkout-oto/"
|
|
221
|
+
// infers as an upsell, but a checkout page with an embedded one-time offer
|
|
222
|
+
// declares `checkout` in its meta (#529). So a declared `checkout` (any case)
|
|
223
|
+
// replaces the guess, and only when the guess is ambiguous (above). Any other
|
|
224
|
+
// declared role leaves the upsell guess, so a meta copied from the product or
|
|
225
|
+
// thank-you page cannot lift an oto page out of the gate, and an explicit
|
|
226
|
+
// upsell or downsell route keeps its type whatever it declares. The guess also
|
|
227
|
+
// stands for a page that declares nothing, or declares it only in inert or
|
|
228
|
+
// conflicting markup, and when no route is given. A type declared by a spec
|
|
229
|
+
// is not a guess and is passed through as before. Either way the type
|
|
230
|
+
// comes back trimmed and lower-cased (" Checkout " is "checkout"), or null.
|
|
231
|
+
export function builtPageTypeOverRouteGuess({ route = null, route_type = null, content = "" } = {}) {
|
|
232
|
+
const declared = routeGuessIsAmbiguous(route) ? builtPageTypeMeta(content) : null;
|
|
233
|
+
const type = normalizedPageType(declared) === "checkout" ? declared : route_type;
|
|
234
|
+
return normalizedPageType(type);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizedPageType(type) {
|
|
238
|
+
return type == null ? null : String(type).trim().toLowerCase();
|
|
239
|
+
}
|
|
240
|
+
|
|
134
241
|
// A built page's funnel role, from either signal that carries it. The declared
|
|
135
242
|
// spec/scope type and the page's own `next-page-type` meta are both consulted
|
|
136
243
|
// and either one is enough: the meta is what the SDK actually reads, the
|
|
137
244
|
// declared type is what survives when a page ships without the meta, and
|
|
138
245
|
// disagreement between them is a reason to check MORE carefully, not less.
|
|
246
|
+
// Any next-page-type the page carries counts here, live or mentioned: saying
|
|
247
|
+
// "post-purchase" can only add a check, so inert markup that says it errs
|
|
248
|
+
// toward a visible, waivable blocker rather than a silent charge.
|
|
139
249
|
export function builtPageIsPostPurchase({ page_type = null, content = "" } = {}) {
|
|
140
250
|
if (isPostPurchasePageType(page_type)) return true;
|
|
141
|
-
const
|
|
142
|
-
return
|
|
251
|
+
const { live, mentioned } = readBuiltPageTypeMetas(content);
|
|
252
|
+
return [...live, ...mentioned].some(isPostPurchasePageType);
|
|
143
253
|
}
|
|
144
254
|
|
|
145
255
|
function describeSelector(finding) {
|