@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
@@ -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 meta = /<meta\b(?=[^>]*\bname=["']next-page-type["'])[^>]*\bcontent=["']([^"']*)["'][^>]*>/i.exec(String(content || ""));
142
- return meta ? isPostPurchasePageType(meta[1]) : false;
251
+ const { live, mentioned } = readBuiltPageTypeMetas(content);
252
+ return [...live, ...mentioned].some(isPostPurchasePageType);
143
253
  }
144
254
 
145
255
  function describeSelector(finding) {