@nextcommerce/campaigns-os 1.52.0 → 1.54.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/CHANGELOG.md +109 -0
- package/README.md +1 -0
- package/agents/claude/CLAUDE.md +2 -0
- package/agents/codex/AGENTS.md +2 -0
- package/agents/copilot/copilot-instructions.md +2 -0
- package/agents/cursor/campaigns-os.mdc +2 -0
- package/compatibility.json +1 -1
- package/contracts/campaign-cart-checkout-field-contract.v0.json +10 -0
- package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
- package/contracts/commerce-surface-catalog.json +27 -27
- package/contracts/effects.v1.json +295 -47
- package/contracts/release-ledger.json +660 -0
- package/contracts/supported-surface.json +3 -3
- package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
- package/docs/brand-theme-bridge.md +2 -0
- package/docs/build-packet.md +67 -19
- package/docs/campaign-build-brief.md +48 -17
- package/docs/campaign-standardization-report.md +32 -0
- package/docs/design-source-package.md +7 -3
- package/docs/effects.md +10 -1
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/polish-evidence.md +2 -0
- package/docs/progress-snapshots.md +5 -5
- package/docs/qa-and-test-orders.md +30 -3
- package/docs/readback.md +19 -2
- package/docs/runtime-readiness.md +1 -1
- package/docs/sdk-storage-compatibility.md +15 -0
- package/docs/skills-revision.md +10 -10
- package/docs/supported-surface.md +1 -0
- package/package.json +1 -1
- package/schemas/campaign-runtime-assembly-report.v0.schema.json +5 -1
- package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +3 -3
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +5 -3
- package/skills/next-campaigns-os/SKILL.md +10 -19
- package/skills/next-campaigns-os/references/session-intake.md +10 -6
- package/skills/next-campaigns-os-setup/SKILL.md +4 -3
- package/skills/next-campaigns-polish/SKILL.md +7 -5
- package/skills/next-campaigns-qa/SKILL.md +5 -3
- package/skills.json +10 -10
- package/src/brand-theme.mjs +21 -20
- package/src/brief-summary.mjs +275 -0
- package/src/build-brief.mjs +167 -15
- package/src/campaign-ecosystem.mjs +153 -31
- package/src/cli.mjs +182 -115
- package/src/contrast.mjs +649 -0
- package/src/design-source-publication.mjs +110 -3
- package/src/directory-lock.mjs +79 -7
- package/src/doctor/checks.mjs +141 -16
- package/src/doctor/inspect.mjs +13 -0
- package/src/doctor/next-step.mjs +15 -6
- package/src/input-currency.mjs +394 -0
- package/src/input-refresh.mjs +708 -0
- package/src/invocation.mjs +6 -2
- package/src/polish-browser.mjs +503 -201
- package/src/polish-deadline.mjs +7 -0
- package/src/polish-node.mjs +326 -46
- package/src/polish-readability.mjs +828 -0
- package/src/progress-node.mjs +18 -5
- package/src/qa-browser.mjs +211 -93
- package/src/qa-node.mjs +47 -7
- package/src/qc-accept.mjs +44 -8
- package/src/qc-check-registry.mjs +15 -7
- package/src/qc-results.mjs +326 -34
- package/src/readback.mjs +58 -10
- package/src/sdk-attribute-index.mjs +111 -23
- package/src/sdk-markup.mjs +49 -10
- package/src/sdk-repin.mjs +282 -0
- package/src/stage-ledger.mjs +235 -44
- package/src/stage-record.mjs +154 -30
- package/src/standardization-report.mjs +26 -1
- package/src/target-lock.mjs +9 -1
- package/src/tooling-setup.mjs +1 -1
|
@@ -2,6 +2,8 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
|
+
import { isReleasedSdkVersion } from "../campaign-spec/dist/index.js";
|
|
6
|
+
import { sdkVersionAtLeast } from "./sdk-attribute-index.mjs";
|
|
5
7
|
import { shellToken } from "./shell-token.mjs";
|
|
6
8
|
import {
|
|
7
9
|
compareVersions,
|
|
@@ -40,6 +42,7 @@ const MAX_SAMPLE_COUNT = 8;
|
|
|
40
42
|
// are still discovered — they just can't be evaluated against version policy.
|
|
41
43
|
const LOADER_URL_PATTERN = /https?:\/\/[^"'\s]*campaign-cart@([^"'\s/]+)[^"'\s]*/g;
|
|
42
44
|
const LOADER_ARTIFACT_PATTERN = /\/loader(?:\.min)?\.js\b|\/dist\//;
|
|
45
|
+
const LOADER_URL_INDEXED_PATTERN = new RegExp(LOADER_URL_PATTERN.source, "gd");
|
|
43
46
|
const CAMPAIGN_ID_META_PATTERN = /<meta\s+[^>]*name=["']next-campaign-id["'][^>]*>/gi;
|
|
44
47
|
const NEXT_CONFIG_PATTERN = /window\.nextConfig\s*=/;
|
|
45
48
|
const DATA_NEXT_PATTERN = /\bdata-next-[a-zA-Z0-9_-]+/g;
|
|
@@ -180,18 +183,19 @@ export function scanCampaignCartAppRoot({
|
|
|
180
183
|
const loader = collectLoaderReferences(root, [...htmlFiles, ...scriptFiles]);
|
|
181
184
|
// Delivery can be a hosted loader URL OR a bundled npm dependency. When the
|
|
182
185
|
// SDK ships as a dependency there is no loader ref to discover, but the pin
|
|
183
|
-
// is still a real version signal — record it and feed
|
|
184
|
-
// version policy like a discovered
|
|
186
|
+
// is still a real version signal — record it and feed its floor (the lowest
|
|
187
|
+
// version the spec lets npm install) into version policy like a discovered
|
|
188
|
+
// version (source-distinguishable below).
|
|
185
189
|
const bundled = detectBundledSdkDependency(root);
|
|
186
190
|
loader.bundled_dependency = bundled
|
|
187
|
-
? { name: bundled.name, version: bundled.version, resolved_version: bundled.resolved_version }
|
|
188
|
-
: null;
|
|
189
|
-
// A prerelease pin sorts strictly before its GA release, so it is never fed
|
|
190
|
-
// into version policy as a clean semver — the numeric triple would wrongly
|
|
191
|
-
// pass the release gate. It is recorded on sdk_loader and flagged instead.
|
|
192
|
-
const bundledPolicyVersion = bundled?.resolved_version && !bundled.prerelease
|
|
193
|
-
? bundled.resolved_version
|
|
191
|
+
? { name: bundled.name, version: bundled.version, resolved_version: bundled.resolved_version, floor_version: bundled.floor_version }
|
|
194
192
|
: null;
|
|
193
|
+
// Policy asks whether every install meets the minimum, so only a floor the
|
|
194
|
+
// spec makes certain is evaluated. A prerelease pin sorts strictly before
|
|
195
|
+
// its GA release and has no released floor; nor does a range like <0.4.39
|
|
196
|
+
// or 0.4.41 || 0.4.18, whose extracted triple is not a version every
|
|
197
|
+
// install meets. Each is recorded on sdk_loader and flagged instead.
|
|
198
|
+
const bundledPolicyVersion = bundled?.floor_version || null;
|
|
195
199
|
const versionEntries = [
|
|
196
200
|
...loader.versions.map((version) => ({ version, source: "loader" })),
|
|
197
201
|
...(bundledPolicyVersion && !loader.versions.includes(bundledPolicyVersion)
|
|
@@ -209,8 +213,20 @@ export function scanCampaignCartAppRoot({
|
|
|
209
213
|
evidence: { name: bundled.name, version: bundled.version, resolved_version: bundled.resolved_version },
|
|
210
214
|
next_action: "Pin the bundled campaign-cart dependency to a released version so version policy can be evaluated.",
|
|
211
215
|
}));
|
|
216
|
+
} else if (bundled && !bundled.floor_version) {
|
|
217
|
+
findings.push(finding({
|
|
218
|
+
severity: "warning",
|
|
219
|
+
category: "standardization_warning",
|
|
220
|
+
code: "version.sdk_dependency_floor_unknown",
|
|
221
|
+
message: `Bundled Campaign Cart dependency ${bundled.name}@${bundled.version} does not fix a lowest version (an exact pin, or one ^, ~ or >= range on a released version); version policy cannot be evaluated against it.`,
|
|
222
|
+
confidence: "static_contract",
|
|
223
|
+
evidence: { name: bundled.name, version: bundled.version, resolved_version: bundled.resolved_version },
|
|
224
|
+
next_action: "Pin the bundled campaign-cart dependency exactly, or to a ^, ~ or >= range on a supported release, so version policy can be evaluated.",
|
|
225
|
+
}));
|
|
212
226
|
}
|
|
213
|
-
const checkoutFields = inspectCheckoutFields(root, htmlFiles, contract, findings
|
|
227
|
+
const checkoutFields = inspectCheckoutFields(root, htmlFiles, contract, findings, {
|
|
228
|
+
sdkVersionForFile: loaderSdkVersionResolver(loader, bundled),
|
|
229
|
+
});
|
|
214
230
|
const payment = inspectPaymentSurfaces(root, htmlFiles, scriptFiles, findings);
|
|
215
231
|
const dataNext = inspectDataNext(root, htmlFiles);
|
|
216
232
|
const runtime = inspectRuntimeArtifacts(root, target, findings);
|
|
@@ -280,6 +296,8 @@ export function scanCampaignCartAppRoot({
|
|
|
280
296
|
// when no such dependency is declared. A prerelease pin sorts strictly BEFORE
|
|
281
297
|
// its GA release in semver, so `prerelease: true` tells the caller the pin
|
|
282
298
|
// must NOT be evaluated against the release policy as if it were the release.
|
|
299
|
+
// `floor_version` is the lowest version the spec allows when that is certain
|
|
300
|
+
// (bundledSdkFloor), else null; it is the version policy evaluates.
|
|
283
301
|
function detectBundledSdkDependency(root) {
|
|
284
302
|
const pkg = readJson(join(root, "package.json"));
|
|
285
303
|
const deps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
|
|
@@ -291,12 +309,23 @@ function detectBundledSdkDependency(root) {
|
|
|
291
309
|
name,
|
|
292
310
|
version: spec,
|
|
293
311
|
resolved_version: semver ? `${semver[1]}${semver[2] || ""}` : null,
|
|
312
|
+
floor_version: bundledSdkFloor(spec),
|
|
294
313
|
prerelease: Boolean(semver && semver[2]),
|
|
295
314
|
};
|
|
296
315
|
}
|
|
297
316
|
return null;
|
|
298
317
|
}
|
|
299
318
|
|
|
319
|
+
// The lowest SDK version a bundled dependency spec allows, when the spec makes
|
|
320
|
+
// it certain: an exact pin (0.4.39, =0.4.39, v0.4.39) or one lower-bounded
|
|
321
|
+
// comparator (^0.4.39, ~0.4.39, >=0.4.39), each on a released version. Any
|
|
322
|
+
// other spec (<0.4.39, a ||, hyphen or x-range, a tag, a prerelease) gives
|
|
323
|
+
// null: the version extracted from it is not a version every install meets.
|
|
324
|
+
function bundledSdkFloor(spec) {
|
|
325
|
+
const match = /^\s*(?:\^|~|>=|=)?\s*v?(\S+)\s*$/.exec(String(spec ?? ""));
|
|
326
|
+
return match && isReleasedSdkVersion(match[1]) ? match[1] : null;
|
|
327
|
+
}
|
|
328
|
+
|
|
300
329
|
export function detectFrameworks(root) {
|
|
301
330
|
const pkg = readJson(join(root, "package.json"));
|
|
302
331
|
const deps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
|
|
@@ -309,16 +338,13 @@ function collectLoaderReferences(root, files) {
|
|
|
309
338
|
for (const file of files) {
|
|
310
339
|
const raw = safeReadText(file);
|
|
311
340
|
if (raw === null) continue;
|
|
312
|
-
const
|
|
313
|
-
for (const match of content.matchAll(withGlobal(LOADER_URL_PATTERN))) {
|
|
314
|
-
if (!LOADER_ARTIFACT_PATTERN.test(match[0])) continue;
|
|
315
|
-
const semver = match[1].match(/^v?(\d+\.\d+\.\d+)$/);
|
|
341
|
+
for (const entry of campaignCartArtifactReferences(raw, { isHtml: HTML_EXTENSIONS.has(extname(file).toLowerCase()) })) {
|
|
316
342
|
references.push({
|
|
317
343
|
path: relPath(root, file),
|
|
318
|
-
line:
|
|
319
|
-
url:
|
|
320
|
-
ref:
|
|
321
|
-
version:
|
|
344
|
+
line: entry.line,
|
|
345
|
+
url: entry.url,
|
|
346
|
+
ref: entry.ref,
|
|
347
|
+
version: entry.version,
|
|
322
348
|
});
|
|
323
349
|
}
|
|
324
350
|
}
|
|
@@ -328,6 +354,73 @@ function collectLoaderReferences(root, files) {
|
|
|
328
354
|
};
|
|
329
355
|
}
|
|
330
356
|
|
|
357
|
+
// Every file the scan walks under `root`: its own skipped directories (.git,
|
|
358
|
+
// .campaign-runtime, _site, node_modules, qa-output, dist, build) left out.
|
|
359
|
+
export const listCampaignCartScanFiles = (root) => listFiles(root);
|
|
360
|
+
|
|
361
|
+
// The HTML and script files among them, the ones the scan reads.
|
|
362
|
+
export function listCampaignCartSourceFiles(root) {
|
|
363
|
+
return listFiles(root).filter((file) => {
|
|
364
|
+
const ext = extname(file).toLowerCase();
|
|
365
|
+
return HTML_EXTENSIONS.has(ext) || SCRIPT_EXTENSIONS.has(ext);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export const isCampaignCartHtmlFile = (file) => HTML_EXTENSIONS.has(extname(file).toLowerCase());
|
|
370
|
+
|
|
371
|
+
// Every Campaign Cart loader/dist reference in one file's text, in order, read
|
|
372
|
+
// exactly as the scan reads it: HTML comments masked in an HTML file, and only
|
|
373
|
+
// URLs that point at a loader or dist artifact. `ref` is the segment after
|
|
374
|
+
// `campaign-cart@` and `version` its x.y.z when the ref is a plain semver pin
|
|
375
|
+
// (with or without a leading v), else null. `url_start` is the URL's offset
|
|
376
|
+
// and `ref_start`/`ref_end` the offsets of that segment in the file text; masking preserves offsets, so they
|
|
377
|
+
// index the unmasked text too. `sdk repin` rewrites only that span.
|
|
378
|
+
export function campaignCartArtifactReferences(text, { isHtml = false } = {}) {
|
|
379
|
+
const content = isHtml ? maskHtmlComments(text) : String(text ?? "");
|
|
380
|
+
const references = [];
|
|
381
|
+
for (const match of content.matchAll(LOADER_URL_INDEXED_PATTERN)) {
|
|
382
|
+
if (!LOADER_ARTIFACT_PATTERN.test(match[0])) continue;
|
|
383
|
+
const semver = match[1].match(/^v?(\d+\.\d+\.\d+)$/);
|
|
384
|
+
references.push({
|
|
385
|
+
line: lineOf(content, match.index || 0),
|
|
386
|
+
url: match[0],
|
|
387
|
+
ref: match[1],
|
|
388
|
+
version: semver ? semver[1] : null,
|
|
389
|
+
url_start: match.index,
|
|
390
|
+
ref_start: match.indices[1][0],
|
|
391
|
+
ref_end: match.indices[1][1],
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return references;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// The SDK version each HTML file's checkout bindings run against, for the
|
|
398
|
+
// version-gated field aliases. A file that loads the SDK itself uses its own
|
|
399
|
+
// loader pin: one exact version, or unknown when its refs are unpinned or
|
|
400
|
+
// disagree. Any other file (a partial, a page whose loader is injected) uses
|
|
401
|
+
// the campaign's version: the lowest of the loader pins and the bundled
|
|
402
|
+
// dependency's floor, or unknown when any loader ref is unpinned, the bundled
|
|
403
|
+
// dependency has no certain floor, or nothing is discovered. Unknown is judged
|
|
404
|
+
// as an earlier SDK (see inspectCheckoutFields).
|
|
405
|
+
function loaderSdkVersionResolver(loader, bundled) {
|
|
406
|
+
const byFile = new Map();
|
|
407
|
+
for (const ref of loader.references) {
|
|
408
|
+
if (!byFile.has(ref.path)) byFile.set(ref.path, []);
|
|
409
|
+
byFile.get(ref.path).push(ref.version);
|
|
410
|
+
}
|
|
411
|
+
const floor = bundled?.floor_version || null;
|
|
412
|
+
const versions = [...loader.versions, ...(floor ? [floor] : [])];
|
|
413
|
+
const campaign = loader.references.some((ref) => !ref.version) || (bundled && !floor) || !versions.length
|
|
414
|
+
? null
|
|
415
|
+
: { version: [...versions].sort(compareVersions)[0], source: "campaign" };
|
|
416
|
+
return (path) => {
|
|
417
|
+
const own = byFile.get(path);
|
|
418
|
+
if (!own) return campaign;
|
|
419
|
+
const versions = unique(own);
|
|
420
|
+
return own.every(Boolean) && versions.length === 1 ? { version: versions[0], source: "loader" } : null;
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
331
424
|
export function evaluateVersionPolicy(versionEntries, policy, findings) {
|
|
332
425
|
const evaluations = versionEntries.map(({ version, source }) => ({
|
|
333
426
|
version,
|
|
@@ -364,7 +457,15 @@ export function evaluateVersionPolicy(versionEntries, policy, findings) {
|
|
|
364
457
|
};
|
|
365
458
|
}
|
|
366
459
|
|
|
367
|
-
|
|
460
|
+
// `sdkVersionForFile(relativePath)` returns the { version, source } a file's
|
|
461
|
+
// bindings run against, or null when it is unknown. A version-gated alias
|
|
462
|
+
// (contract version_gated_aliases) is supported only on an exact SDK release
|
|
463
|
+
// at or after its since-version; on an earlier or unknown version it is
|
|
464
|
+
// classified as the contract classifies it without the gate (a stale alias,
|
|
465
|
+
// or unknown). Unknown is the conservative side: the canonical name works on
|
|
466
|
+
// every version, and accepting the alias there could pass a field that never
|
|
467
|
+
// reaches the order.
|
|
468
|
+
export function inspectCheckoutFields(root, htmlFiles, contract, findings, { sdkVersionForFile = null } = {}) {
|
|
368
469
|
const bindings = [];
|
|
369
470
|
const attributes = contract?.binding_attributes || ["data-next-checkout-field", "os-checkout-field"];
|
|
370
471
|
const attributePattern = new RegExp(`\\b(${attributes.map(escapeRegExp).join("|")})\\s*=\\s*(["'])([^"']*)\\2`, "g");
|
|
@@ -372,17 +473,22 @@ export function inspectCheckoutFields(root, htmlFiles, contract, findings) {
|
|
|
372
473
|
const raw = safeReadText(file);
|
|
373
474
|
if (raw === null) continue;
|
|
374
475
|
const content = maskHtmlComments(raw);
|
|
476
|
+
const path = relPath(root, file);
|
|
477
|
+
const sdk = (typeof sdkVersionForFile === "function" ? sdkVersionForFile(path) : null) || { version: null, source: null };
|
|
375
478
|
for (const match of content.matchAll(attributePattern)) {
|
|
376
479
|
const value = match[3].trim();
|
|
377
|
-
const verdict = classifyFieldBinding(value, contract);
|
|
480
|
+
const verdict = classifyFieldBinding(value, contract, sdk.version);
|
|
378
481
|
bindings.push({
|
|
379
482
|
attribute: match[1],
|
|
380
483
|
value,
|
|
381
|
-
path
|
|
484
|
+
path,
|
|
382
485
|
line: lineOf(content, match.index || 0),
|
|
383
486
|
supported: verdict.classification === "supported",
|
|
384
487
|
classification: verdict.classification,
|
|
385
488
|
canonical: verdict.canonical,
|
|
489
|
+
since: verdict.since,
|
|
490
|
+
sdk_version: sdk.version,
|
|
491
|
+
sdk_version_source: sdk.source,
|
|
386
492
|
});
|
|
387
493
|
}
|
|
388
494
|
}
|
|
@@ -391,16 +497,28 @@ export function inspectCheckoutFields(root, htmlFiles, contract, findings) {
|
|
|
391
497
|
const unknown = bindings.filter((entry) => entry.classification === "unknown");
|
|
392
498
|
|
|
393
499
|
if (unsupported.length) {
|
|
500
|
+
const gated = unsupported.filter((entry) => entry.since);
|
|
501
|
+
const gatedNote = gated.length
|
|
502
|
+
? ` The SDK consumes ${unique(gated.map((entry) => `${entry.value} only from ${entry.since}`)).join(", ")}; these bindings run on SDK ${unique(gated.map((entry) => entry.sdk_version || "of unknown version (judged as an earlier SDK)")).join(", ")}.`
|
|
503
|
+
: "";
|
|
394
504
|
findings.push(finding({
|
|
395
505
|
severity: "blocker",
|
|
396
506
|
category: "standardization_blocker",
|
|
397
507
|
code: "checkout.unsupported_field_binding",
|
|
398
|
-
message: `${unsupported.length} checkout field binding(s) use stale aliases the Campaign Cart SDK does not consume (contract: ${contract?.schema_version || "unknown"})
|
|
508
|
+
message: `${unsupported.length} checkout field binding(s) use stale aliases the Campaign Cart SDK does not consume (contract: ${contract?.schema_version || "unknown"}).${gatedNote}`,
|
|
399
509
|
confidence: "static_contract",
|
|
400
510
|
evidence: dedupeBy(unsupported, (entry) => entry.value)
|
|
401
|
-
.map((entry) => ({
|
|
511
|
+
.map((entry) => ({
|
|
512
|
+
value: entry.value,
|
|
513
|
+
canonical: entry.canonical,
|
|
514
|
+
path: entry.path,
|
|
515
|
+
line: entry.line,
|
|
516
|
+
...(entry.since ? { since: entry.since, sdk_version: entry.sdk_version } : {}),
|
|
517
|
+
}))
|
|
402
518
|
.slice(0, MAX_SAMPLE_COUNT),
|
|
403
|
-
next_action:
|
|
519
|
+
next_action: gated.length
|
|
520
|
+
? "Rewrite each stale alias to its canonical Campaign Cart field name (for an alias with a since-version in the evidence, pinning the SDK at or after that version also works), then prove checkout binding in browser QA."
|
|
521
|
+
: "Rewrite each stale alias to its canonical Campaign Cart field name, then prove checkout binding in browser QA.",
|
|
404
522
|
}));
|
|
405
523
|
}
|
|
406
524
|
if (unknown.length) {
|
|
@@ -411,7 +529,7 @@ export function inspectCheckoutFields(root, htmlFiles, contract, findings) {
|
|
|
411
529
|
message: `${unknown.length} checkout field binding(s) are outside the known Campaign Cart field contract.`,
|
|
412
530
|
confidence: "static_inference",
|
|
413
531
|
evidence: dedupeBy(unknown, (entry) => entry.value)
|
|
414
|
-
.map((entry) => ({ value: entry.value, path: entry.path, line: entry.line }))
|
|
532
|
+
.map((entry) => ({ value: entry.value, path: entry.path, line: entry.line, ...(entry.since ? { since: entry.since, sdk_version: entry.sdk_version } : {}) }))
|
|
415
533
|
.slice(0, MAX_SAMPLE_COUNT),
|
|
416
534
|
next_action: "Confirm the field against the Campaign Cart SDK before treating it as supported or repairing it.",
|
|
417
535
|
}));
|
|
@@ -425,10 +543,11 @@ export function inspectCheckoutFields(root, htmlFiles, contract, findings) {
|
|
|
425
543
|
};
|
|
426
544
|
}
|
|
427
545
|
|
|
428
|
-
function classifyFieldBinding(value, contract) {
|
|
429
|
-
if (!value) return { classification: "unknown", canonical: null };
|
|
546
|
+
function classifyFieldBinding(value, contract, sdkVersion = null) {
|
|
547
|
+
if (!value) return { classification: "unknown", canonical: null, since: null };
|
|
430
548
|
const canonicalFields = new Set(contract?.canonical_fields || []);
|
|
431
549
|
const acceptedAliases = contract?.accepted_aliases || {};
|
|
550
|
+
const gatedAliases = contract?.version_gated_aliases || {};
|
|
432
551
|
const staleAliases = contract?.stale_aliases || {};
|
|
433
552
|
const prefixes = contract?.prefixes || [];
|
|
434
553
|
|
|
@@ -441,12 +560,15 @@ function classifyFieldBinding(value, contract) {
|
|
|
441
560
|
break;
|
|
442
561
|
}
|
|
443
562
|
}
|
|
444
|
-
if (canonicalFields.has(base)) return { classification: "supported", canonical: null };
|
|
445
|
-
if (typeof acceptedAliases[base] === "string") return { classification: "supported", canonical: null };
|
|
563
|
+
if (canonicalFields.has(base)) return { classification: "supported", canonical: null, since: null };
|
|
564
|
+
if (typeof acceptedAliases[base] === "string") return { classification: "supported", canonical: null, since: null };
|
|
565
|
+
const gated = Object.hasOwn(gatedAliases, base) ? gatedAliases[base] : null;
|
|
566
|
+
const since = typeof gated?.since === "string" ? gated.since : null;
|
|
567
|
+
if (since && sdkVersionAtLeast(sdkVersion, since)) return { classification: "supported", canonical: null, since };
|
|
446
568
|
if (typeof staleAliases[base] === "string") {
|
|
447
|
-
return { classification: "stale_alias", canonical: `${prefix}${staleAliases[base]}
|
|
569
|
+
return { classification: "stale_alias", canonical: `${prefix}${staleAliases[base]}`, since };
|
|
448
570
|
}
|
|
449
|
-
return { classification: "unknown", canonical: null };
|
|
571
|
+
return { classification: "unknown", canonical: null, since };
|
|
450
572
|
}
|
|
451
573
|
|
|
452
574
|
function inspectPaymentSurfaces(root, htmlFiles, scriptFiles, findings) {
|