@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
@@ -138,6 +138,122 @@ function pageRouteForPageKit(value) {
138
138
  }
139
139
  }
140
140
 
141
+ // The SDK meta tags whose content is a route. Doctor checks the same three.
142
+ export const SDK_ROUTING_META_TAGS = [
143
+ "next-success-url",
144
+ "next-upsell-accept-url",
145
+ "next-upsell-decline-url",
146
+ ];
147
+
148
+ export const HOST_STRIPPED_CODE = "routing_meta.host_stripped";
149
+
150
+ // A route value that carries a host in front of its path (#531): an older
151
+ // saved Map stored `shop.example.com/route/upsell/` where the route is
152
+ // `/route/upsell/`, and every stage that roots a route then nested the host
153
+ // inside the campaign path. Returns `{ host, from, to }` — `to` is the rooted
154
+ // path with any query and fragment kept — or null when the value is not
155
+ // host-prefixed.
156
+ //
157
+ // Read as host-prefixed:
158
+ // - `http://` or `https://` URLs (any host);
159
+ // - protocol-relative `//<host>/...`;
160
+ // - bare `<host>/...`, where the first segment is followed by "/" and is
161
+ // `localhost`, a valid IPv4 address (each octet 0-255), any name with a
162
+ // `:port`, or a dotted name whose last label is 2-63 letters and not a
163
+ // page or script extension (ROUTE_FILE_EXTENSION: `html`, `htm`,
164
+ // `shtml`, `php`, `asp`, `aspx`, `jsp`, `cgi`; not `pl`, which is also
165
+ // Poland's country-code domain), such as
166
+ // `shop.example.com`. `//<host>/...` takes the same hosts.
167
+ // Everything else is a route and is left to the existing route checks: a
168
+ // rooted `/...` value, a first segment with no dot (`route/x/`), a dotted
169
+ // segment whose last label is not all letters (`v1.2/offer/`), a dotted
170
+ // quad with an octet over 255 (`300.1.2.3/offer/`), a page or script
171
+ // filename (`checkout.html`, `index.php/checkout/`), and a bare host with no
172
+ // path after it.
173
+ // `keepAbsolute: true` leaves an absolute http(s) URL alone: it is a valid
174
+ // SDK routing meta target, and projection already converts an absolute
175
+ // page_url to its path, so doctor accepts both; only the bare and
176
+ // protocol-relative forms count.
177
+ export function parseHostPrefixedRoute(value, { keepAbsolute = false } = {}) {
178
+ if (typeof value !== "string") return null;
179
+ const raw = value.trim();
180
+ if (!raw || (raw.startsWith("/") && !raw.startsWith("//"))) return null;
181
+ const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(raw);
182
+ if (scheme) {
183
+ if (keepAbsolute || !/^https?$/i.test(scheme[1])) return null;
184
+ return splitHostPrefix(value, raw.slice(scheme[0].length), { requireHostShape: false });
185
+ }
186
+ if (raw.startsWith("//")) return splitHostPrefix(value, raw.slice(2), { requireHostShape: true });
187
+ return splitHostPrefix(value, raw, { requireHostShape: true, requirePath: true });
188
+ }
189
+
190
+ function splitHostPrefix(from, rest, { requireHostShape, requirePath = false }) {
191
+ const end = rest.search(/[/?#]/);
192
+ const host = end === -1 ? rest : rest.slice(0, end);
193
+ const tail = end === -1 ? "" : rest.slice(end);
194
+ if (!host || /\s/.test(host)) return null;
195
+ if (requirePath && !tail.startsWith("/")) return null;
196
+ if (requireHostShape && !looksLikeHost(host)) return null;
197
+ return { host, from, to: tail.startsWith("/") ? tail : `/${tail}` };
198
+ }
199
+
200
+ // A dotted first segment ending in one of these is a page or script filename
201
+ // (`index.php/checkout/`), not a host.
202
+ const ROUTE_FILE_EXTENSION = /^(?:html?|shtml|php|aspx?|jsp|cgi)$/i;
203
+
204
+ function looksLikeHost(segment) {
205
+ if (/^localhost(?::\d+)?$/i.test(segment)) return true;
206
+ if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(segment) && segment.split(".").every((octet) => Number(octet) <= 255)) return true;
207
+ if (/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*:\d+$/.test(segment)) return true;
208
+ const labels = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)+$/.test(segment)
209
+ ? segment.split(".")
210
+ : null;
211
+ if (!labels) return false;
212
+ const last = labels[labels.length - 1];
213
+ return /^[A-Za-z]{2,63}$/.test(last) && !ROUTE_FILE_EXTENSION.test(last);
214
+ }
215
+
216
+ // Intake normalisation (#531): every host-prefixed page_url, and every
217
+ // host-prefixed SDK routing meta tag value, in funnels[].pages[] and the
218
+ // funnel_pages[] mirror, reduced to its rooted path before anything reads the
219
+ // spec. Returns the spec unchanged (the same object) with no evidence when
220
+ // nothing carries a host; otherwise a copy and one evidence record per changed
221
+ // value, `{ code, page_id, field, from, to }`, where `from` is the value
222
+ // exactly as the Map or file held it.
223
+ export function stripHostPrefixedRoutes(spec) {
224
+ const evidence = [];
225
+ if (!spec || typeof spec !== "object") return { spec, evidence };
226
+ const copy = JSON.parse(JSON.stringify(spec));
227
+ const lists = [];
228
+ if (Array.isArray(copy.funnels)) {
229
+ copy.funnels.forEach((funnel, funnelIndex) => {
230
+ if (Array.isArray(funnel?.pages)) lists.push([`funnels[${funnelIndex}].pages`, funnel.pages]);
231
+ });
232
+ }
233
+ if (Array.isArray(copy.funnel_pages)) lists.push(["funnel_pages", copy.funnel_pages]);
234
+ for (const [label, pages] of lists) {
235
+ pages.forEach((page, pageIndex) => {
236
+ if (!page || typeof page !== "object") return;
237
+ const at = `${label}[${pageIndex}]`;
238
+ const pageId = typeof page.id === "string" ? page.id : null;
239
+ const stripped = parseHostPrefixedRoute(page.page_url);
240
+ if (stripped) {
241
+ page.page_url = stripped.to;
242
+ evidence.push({ code: HOST_STRIPPED_CODE, page_id: pageId, field: `${at}.page_url`, from: stripped.from, to: stripped.to });
243
+ }
244
+ const metaTags = page.sdk_hints?.meta_tags;
245
+ if (!metaTags || typeof metaTags !== "object" || Array.isArray(metaTags)) return;
246
+ for (const tag of SDK_ROUTING_META_TAGS) {
247
+ const meta = parseHostPrefixedRoute(metaTags[tag], { keepAbsolute: true });
248
+ if (!meta) continue;
249
+ metaTags[tag] = meta.to;
250
+ evidence.push({ code: HOST_STRIPPED_CODE, page_id: pageId, field: `${at}.sdk_hints.meta_tags.${tag}`, from: meta.from, to: meta.to });
251
+ }
252
+ });
253
+ }
254
+ return evidence.length ? { spec: copy, evidence } : { spec, evidence };
255
+ }
256
+
141
257
  function applyManifestToPages(specPages, manifest, manifestPath, { buildScope = null, templateFamily = null } = {}) {
142
258
  const mappings = [];
143
259
  const prompts = [];