@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
|
@@ -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 = [];
|