@nextcommerce/campaigns-os 1.47.0 → 1.48.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 +194 -0
- package/README.md +30 -3
- package/agents/claude/CLAUDE.md +2 -0
- package/agents/codex/AGENTS.md +1 -0
- package/agents/copilot/copilot-instructions.md +1 -0
- package/agents/cursor/campaigns-os.mdc +1 -1
- package/compatibility.json +1 -1
- package/contracts/commerce-surface-catalog.json +17 -17
- package/contracts/release-ledger.json +465 -0
- package/contracts/supported-surface.json +1 -1
- package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
- package/docs/build-packet.md +1 -1
- package/docs/campaign-build-brief.md +25 -28
- package/docs/local-setup.md +7 -2
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/qa-and-test-orders.md +49 -2
- package/docs/runtime-readiness.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/package.json +1 -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 +9 -4
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +4 -3
- package/skills/next-campaigns-os/SKILL.md +3 -3
- package/skills/next-campaigns-os-setup/SKILL.md +3 -3
- package/skills/next-campaigns-polish/SKILL.md +4 -4
- package/skills/next-campaigns-qa/SKILL.md +3 -3
- package/skills.json +10 -10
- package/src/cli.mjs +6 -3
- package/src/content-residue.mjs +18 -90
- package/src/doctor/checks.mjs +21 -29
- package/src/doctor/inspect.mjs +13 -3
- package/src/doctor/next-step.mjs +4 -0
- package/src/gate-actions.mjs +8 -0
- package/src/local-preview-policy.mjs +92 -0
- package/src/page-kit-sdk-version.mjs +8 -1
- package/src/polish-node.mjs +26 -2
- package/src/progress-node.mjs +5 -1
- package/src/qa-binding-evidence.mjs +1 -1
- package/src/qa-browser.mjs +62 -13
- package/src/qa-node.mjs +33 -4
- package/src/readback.mjs +19 -10
- package/src/source-prep.mjs +1 -1
package/src/doctor/checks.mjs
CHANGED
|
@@ -363,7 +363,7 @@ const SPEC_DOCTOR_CHECKS = createDoctorCheckRegistry([
|
|
|
363
363
|
{
|
|
364
364
|
id: PAGE_KIT_SDK_VERSION_SCOPE,
|
|
365
365
|
phase: "target",
|
|
366
|
-
run: ({ spec, errors, warnings, ready, derived, buildState }) => validateTargetSdkVersion(spec, errors, warnings, ready, derived, buildState),
|
|
366
|
+
run: ({ spec, packet, errors, warnings, ready, derived, buildState }) => validateTargetSdkVersion(spec, errors, warnings, ready, derived, buildState, packet),
|
|
367
367
|
},
|
|
368
368
|
{
|
|
369
369
|
id: PAGE_KIT_STORE_PROFILE_SCOPE,
|
|
@@ -1159,17 +1159,22 @@ function validateLocalProof(packet, report, errors, warnings, ready) {
|
|
|
1159
1159
|
addIssue(errors, LOCAL_PROOF_PARITY_SCOPE, `Production parity FAILED${difference ? ` — first non-gated difference: ${singleLineField(String(difference.kind))} at ${singleLineField(String(difference.route))}${difference.line ? ` line ${difference.line}` : ""}: ${singleLineField(String(difference.detail || ""))}` : `: ${singleLineField(String(parity.summary || ""))}`}. Rebuild in development, re-prove, and run ${parityCommand} again. ${LOCAL_PROOF_NEVER_EDIT_RULE}`, difference ? { first_difference: difference } : undefined);
|
|
1160
1160
|
}
|
|
1161
1161
|
|
|
1162
|
-
function validateTargetSdkVersion(spec, errors, warnings, ready, derived, buildState) {
|
|
1162
|
+
function validateTargetSdkVersion(spec, errors, warnings, ready, derived, buildState, packet = null) {
|
|
1163
1163
|
const report = buildState?.report || null;
|
|
1164
1164
|
const required = stageIsTerminal(report?.stages?.setup?.status)
|
|
1165
1165
|
|| stageIsTerminal(report?.stages?.assembly?.status)
|
|
1166
1166
|
|| derived.scaffold_required !== true;
|
|
1167
|
+
const family = packet?.assembly?.template_family;
|
|
1168
|
+
const verified = isNonEmptyString(family) && isCertifiedTemplateFamily(family)
|
|
1169
|
+
? assessTemplateFreshness({ family, catalog: resolveCommerceCatalog(), sdkSupportPolicy: defaultSdkSupportPolicy() }).verified_sdk_version
|
|
1170
|
+
: null;
|
|
1167
1171
|
const gate = evaluatePageKitSdkVersion({
|
|
1168
1172
|
spec,
|
|
1169
1173
|
specStatus: buildState?.specStatus || "ok",
|
|
1170
1174
|
targetLoad: buildState?.pageKitCampaignConfig,
|
|
1171
1175
|
waivers: report?.waivers,
|
|
1172
1176
|
required,
|
|
1177
|
+
familyVerification: verified ? { family, sdk_version: verified } : null,
|
|
1173
1178
|
});
|
|
1174
1179
|
derived.checkpoint_gates.push(gate);
|
|
1175
1180
|
|
|
@@ -3199,7 +3204,7 @@ function validateSourceCoverage(packet, packetPath, spec, errors, warnings, read
|
|
|
3199
3204
|
addIssue(
|
|
3200
3205
|
warnings,
|
|
3201
3206
|
"source_html.pages.source_hash",
|
|
3202
|
-
`Source page "${page.page_id}" hash mismatch — file at ${page.path} has changed since the
|
|
3207
|
+
`Source page "${page.page_id}" hash mismatch — file at ${page.path} has changed since intake recorded it in the Build Packet (recorded sha256=${expectedHash.slice(0, 12)}…, on-disk sha256=${actualHash.slice(0, 12)}…). This warning does not block. Re-running start or prepare-build with --force records the current file and clears recorded stage evidence; editing the source-html manifest alone does not change the hash the packet holds.`,
|
|
3203
3208
|
);
|
|
3204
3209
|
}
|
|
3205
3210
|
}
|
|
@@ -4018,11 +4023,11 @@ function collectPlaceholderTextResidueMatches(root, terms) {
|
|
|
4018
4023
|
}
|
|
4019
4024
|
|
|
4020
4025
|
// Rendered-output content-residue scan (assembly-surfaces prototype): scans
|
|
4021
|
-
// BUILT pages for the needs-merchant-input marker and
|
|
4022
|
-
//
|
|
4023
|
-
//
|
|
4024
|
-
//
|
|
4025
|
-
// only exists after the build.
|
|
4026
|
+
// BUILT pages for the needs-merchant-input marker and, on a brief-backed
|
|
4027
|
+
// build, starter countdown chrome the brief does not verify (blockers), plus
|
|
4028
|
+
// template demo/placeholder residue (warnings). The merchant's own proof and
|
|
4029
|
+
// urgency copy is not scanned. Scans _site output, not frontmatter —
|
|
4030
|
+
// layout/script-rendered chrome only exists after the build.
|
|
4026
4031
|
export function validateBuiltContentResidue(packet, errors, warnings, ready, derived, buildState = {}) {
|
|
4027
4032
|
if (!isStageComplete(buildState.report, "assembly")) return;
|
|
4028
4033
|
// Scan the RENDERED _site output, not the campaign source dir: the Liquid
|
|
@@ -4035,7 +4040,7 @@ export function validateBuiltContentResidue(packet, errors, warnings, ready, der
|
|
|
4035
4040
|
const urgencyVerified = briefUrgencyVerified(brief?.payload);
|
|
4036
4041
|
const findings = scanBuiltOutputContentResidue(siteRoot, { urgencyVerified });
|
|
4037
4042
|
if (!findings.length) {
|
|
4038
|
-
ready.push("Built output carries no needs-input markers, unverified urgency chrome, or
|
|
4043
|
+
ready.push("Built output carries no needs-input markers, unverified urgency chrome, or template demo residue");
|
|
4039
4044
|
return;
|
|
4040
4045
|
}
|
|
4041
4046
|
// One issue per finding id, carrying the FULL file inventory: the id keeps
|
|
@@ -4068,9 +4073,8 @@ export function validateBuiltContentResidue(packet, errors, warnings, ready, der
|
|
|
4068
4073
|
);
|
|
4069
4074
|
} else if (id === "unverified_urgency_countdown") {
|
|
4070
4075
|
// The scanner emits this only when the brief does not verify urgency
|
|
4071
|
-
// (urgencyVerified=false).
|
|
4072
|
-
//
|
|
4073
|
-
// with no brief payload may carry an intentional countdown — warning.
|
|
4076
|
+
// (urgencyVerified=false). Only a readable brief payload (AI-assembled)
|
|
4077
|
+
// makes it a finding.
|
|
4074
4078
|
if (briefReadable) {
|
|
4075
4079
|
addIssue(
|
|
4076
4080
|
errors,
|
|
@@ -4078,30 +4082,18 @@ export function validateBuiltContentResidue(packet, errors, warnings, ready, der
|
|
|
4078
4082
|
`Built output renders countdown chrome without verified offer urgency (${where}). Set offer.urgency.verified in ${BRIEF_PAYLOAD_REL_PATH} from a real promotion window, or blank the urgency slots.`,
|
|
4079
4083
|
);
|
|
4080
4084
|
} else if (brief?.error) {
|
|
4081
|
-
//
|
|
4082
|
-
// proof_attestation.unreadable
|
|
4083
|
-
//
|
|
4084
|
-
// brief parses — "confirm the urgency is real" copy here would point
|
|
4085
|
-
// the operator at the wrong fix.
|
|
4086
|
-
} else {
|
|
4087
|
-
addIssue(
|
|
4088
|
-
warnings,
|
|
4089
|
-
"content_residue.urgency_unattested",
|
|
4090
|
-
`Built output renders countdown chrome and no brief payload attests the promotion window (${where}). Confirm the urgency is real (a genuine offer window or live inventory) before launch.`,
|
|
4091
|
-
);
|
|
4085
|
+
// Deliberately nothing: an unreadable brief payload already blocks
|
|
4086
|
+
// through proof_attestation.unreadable (validateProofAttestation),
|
|
4087
|
+
// and urgency is re-evaluated once the brief parses.
|
|
4092
4088
|
}
|
|
4089
|
+
// No brief payload at all: the countdown is the merchant's content and
|
|
4090
|
+
// is not ours to question.
|
|
4093
4091
|
} else if (id === "demo_residue_term" || id === "bracket_placeholder_stub") {
|
|
4094
4092
|
addIssue(
|
|
4095
4093
|
warnings,
|
|
4096
4094
|
"content_residue.demo_residue",
|
|
4097
4095
|
`Built output carries template demo/placeholder residue (${where}; e.g. "${finding.excerpt}"). Fill or blank the slot — demo values must never ship.`,
|
|
4098
4096
|
);
|
|
4099
|
-
} else {
|
|
4100
|
-
addIssue(
|
|
4101
|
-
warnings,
|
|
4102
|
-
"content_residue.anti_pattern",
|
|
4103
|
-
`Built output matches content anti-pattern "${id}" (${where}; e.g. "${finding.excerpt}"). ${finding.rule || "Remove it or route it through brief-sourced proof."} This is a review warning and nothing downstream blocks on it: the claim is the operator's and the client's responsibility — remove or evidence it, never make it more plausible.`,
|
|
4104
|
-
);
|
|
4105
4097
|
}
|
|
4106
4098
|
}
|
|
4107
4099
|
}
|
package/src/doctor/inspect.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Doctor entry points: the doctor command, packet inspection and built-output inspection.
|
|
2
2
|
import { resolveCampaignIdentity } from "../spec-source-identity.mjs";
|
|
3
|
+
import { applyLocalPreviewToCheckpoint, applyLocalPreviewToPolishGate, carriedForwardMessage, CARRIED_FORWARD } from "../local-preview-policy.mjs";
|
|
3
4
|
import { withHtmlScanSnapshot } from "../html-scan.mjs";
|
|
4
5
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -506,16 +507,23 @@ function inspectDoctorPacket(packetPath, { contextPath = undefined, reportPath =
|
|
|
506
507
|
}
|
|
507
508
|
runPricingCssHideCheck({ packet, derived, warnings, ready, report });
|
|
508
509
|
|
|
509
|
-
|
|
510
|
+
// The local preview policy (local-preview-policy.mjs) carries missing
|
|
511
|
+
// polish and page-load evidence forward as warnings on a local-serve packet.
|
|
512
|
+
const polishCheckpointGate = applyLocalPreviewToCheckpoint(
|
|
513
|
+
evaluateRecordedHiddenEagerMediaCheckpoint({ packet, report }),
|
|
514
|
+
{ packet, report },
|
|
515
|
+
);
|
|
510
516
|
derived.polish_checkpoint_gate = polishCheckpointGate;
|
|
511
|
-
const polishGate = evaluatePolishGate({
|
|
517
|
+
const polishGate = applyLocalPreviewToPolishGate(evaluatePolishGate({
|
|
512
518
|
report,
|
|
513
519
|
hiddenEagerMediaGate: polishCheckpointGate,
|
|
514
520
|
currentOutputFingerprint: derived.build_output_fingerprint?.value || null,
|
|
515
|
-
});
|
|
521
|
+
}), { packet, checkpointGate: polishCheckpointGate });
|
|
516
522
|
derived.polish_gate = polishGate;
|
|
517
523
|
if (polishGate.status === "blocked" && !polishGate.owned_checkpoint_only) {
|
|
518
524
|
pushGateIssue({ errors, warnings }, gateIssue("polish_gate", polishGate));
|
|
525
|
+
} else if (polishGate.status === CARRIED_FORWARD && !polishGate.owned_checkpoint_only) {
|
|
526
|
+
addIssue(warnings, polishGate.code, carriedForwardMessage(polishGate), { polish_gate: polishGate });
|
|
519
527
|
} else if (polishGate.status === "waived" && !polishGate.owned_checkpoint_only) {
|
|
520
528
|
ready.push(`Polish gate passed under waiver: ${polishGate.waiver?.reason || "(no reason recorded)"}`);
|
|
521
529
|
} else if (polishGate.status === "pass") {
|
|
@@ -524,6 +532,8 @@ function inspectDoctorPacket(packetPath, { contextPath = undefined, reportPath =
|
|
|
524
532
|
|
|
525
533
|
if (polishCheckpointGate.status === "blocked") {
|
|
526
534
|
pushGateIssue({ errors, warnings }, gateIssue("polish_checkpoint_gate", polishCheckpointGate));
|
|
535
|
+
} else if (polishCheckpointGate.status === CARRIED_FORWARD) {
|
|
536
|
+
addIssue(warnings, polishCheckpointGate.code, carriedForwardMessage(polishCheckpointGate), { polish_checkpoint_gate: polishCheckpointGate });
|
|
527
537
|
} else if (polishCheckpointGate.status === "waived") {
|
|
528
538
|
addIssue(
|
|
529
539
|
warnings,
|
package/src/doctor/next-step.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The next step doctor recommends, and the gate issues `next` reads from doctor.
|
|
2
2
|
import { campaignIdentitiesMatch } from "../spec-source-identity.mjs";
|
|
3
|
+
import { CARRIED_FORWARD } from "../local-preview-policy.mjs";
|
|
3
4
|
import { resolve } from "node:path";
|
|
4
5
|
import { orderPathDepthDriftText } from "../proof-policy.mjs";
|
|
5
6
|
import { anyAssemblyReportStageBlocked } from "../stage-ledger.mjs";
|
|
@@ -462,6 +463,9 @@ function pickNextStage(report, { errors = [], derived = null }, prepareBuildGate
|
|
|
462
463
|
}
|
|
463
464
|
|
|
464
465
|
for (const cliStage of NEXT_STAGE_ORDER) {
|
|
466
|
+
// On the local preview, missing polish is carried forward as a warning
|
|
467
|
+
// (local-preview-policy.mjs): the ladder moves on to deploy and QA.
|
|
468
|
+
if (cliStage === "polish" && polishGate.status === CARRIED_FORWARD) continue;
|
|
465
469
|
const reportKey = reportKeyForCliStage(cliStage);
|
|
466
470
|
const stage = report.stages[reportKey];
|
|
467
471
|
if (!stage) {
|
package/src/gate-actions.mjs
CHANGED
|
@@ -62,6 +62,14 @@ export const HIDDEN_EAGER_MEDIA_ACTIONS = Object.freeze({
|
|
|
62
62
|
command: null,
|
|
63
63
|
description: localProofRebuildText(),
|
|
64
64
|
}),
|
|
65
|
+
// Every mapped page is template stock: capture has no design route, so
|
|
66
|
+
// neither a recapture nor an authority repair can produce the evidence.
|
|
67
|
+
map_design_route: Object.freeze({
|
|
68
|
+
id: "polish.hidden_eager_media.map_design_route",
|
|
69
|
+
kind: "manual",
|
|
70
|
+
command: null,
|
|
71
|
+
description: "Every mapped page is template stock, so polish capture has nothing to measure. Map at least one page to its design source HTML (source_html.pages[].path) and rerun intake, or prove the campaign on the local preview (deploy.target local-serve served from localhost), where this missing evidence is carried forward as a warning.",
|
|
72
|
+
}),
|
|
65
73
|
});
|
|
66
74
|
|
|
67
75
|
const PACKET_PLACEHOLDER = "--packet <packet>";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// The local preview policy: one place that decides which missing evidence a
|
|
2
|
+
// localhost preview carries forward as a warning instead of a blocker.
|
|
3
|
+
//
|
|
4
|
+
// The local preview is a packet whose deploy target is `local-serve`, served
|
|
5
|
+
// from a loopback host. There a campaign must still prove its commerce (store
|
|
6
|
+
// and campaign binding, routes, SDK loading, prices, a typed-card order), but
|
|
7
|
+
// evidence that only design work or launch produces may be missing. Missing
|
|
8
|
+
// evidence is reported as missing, never as passed: a carried-forward gate is
|
|
9
|
+
// a warning in doctor, `next` does not stop on it, and QA records it as a
|
|
10
|
+
// `warn` row, so the verdict is at best `ready_with_exceptions`. A hosted
|
|
11
|
+
// preview or production packet never takes this path, and every check not
|
|
12
|
+
// named below keeps its meaning on every path.
|
|
13
|
+
//
|
|
14
|
+
// Doctor and QA apply this to the gates they evaluate; `next` reads doctor's
|
|
15
|
+
// gates. Recording a stage (`record polish`) and the waiver commands keep the
|
|
16
|
+
// strict gates.
|
|
17
|
+
import { isLocalServePacket } from "./local-proof.mjs";
|
|
18
|
+
import { isLoopbackHostname } from "./remit.mjs";
|
|
19
|
+
|
|
20
|
+
export const LOCAL_PREVIEW_POLICY = "local_preview";
|
|
21
|
+
export const CARRIED_FORWARD = "carried_forward";
|
|
22
|
+
|
|
23
|
+
// The checks carried forward, one by one. Each is evidence the campaign does
|
|
24
|
+
// not have yet, not evidence that something is wrong:
|
|
25
|
+
// - polish was never recorded for this build;
|
|
26
|
+
// - no page-load capture exists, either because polish capture never ran or
|
|
27
|
+
// because every mapped page is template stock and there is no design route
|
|
28
|
+
// to capture.
|
|
29
|
+
const CARRIED_POLISH_CODES = Object.freeze(new Set([
|
|
30
|
+
"polish.report_missing",
|
|
31
|
+
"polish.evidence_missing",
|
|
32
|
+
]));
|
|
33
|
+
export const NO_CAPTURABLE_ROUTES_CODE = "polish.hidden_eager_media.no_capturable_routes";
|
|
34
|
+
const MISSING_CAPTURE_CODE = "polish.hidden_eager_media.capture_malformed";
|
|
35
|
+
|
|
36
|
+
// With no generatable brand theme the starter template is the design, so its
|
|
37
|
+
// palette and chrome are expected on the local preview: residue findings are
|
|
38
|
+
// warnings there, as they are under a waived theme gate.
|
|
39
|
+
const STARTER_IS_THE_DESIGN_THEME_CODE = "theme_gate.nothing_generatable";
|
|
40
|
+
|
|
41
|
+
function loopbackOrAbsent(url) {
|
|
42
|
+
if (url === undefined || url === null || url === "") return true;
|
|
43
|
+
try {
|
|
44
|
+
return isLoopbackHostname(new URL(String(url)).hostname);
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isLocalPreview(packet, { baseUrl = null } = {}) {
|
|
51
|
+
return isLocalServePacket(packet)
|
|
52
|
+
&& loopbackOrAbsent(packet?.deploy?.preview_url)
|
|
53
|
+
&& loopbackOrAbsent(baseUrl);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function pageLoadRecorded(report) {
|
|
57
|
+
return report?.stages?.polish?.evidence?.visual_review?.page_load != null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function carried(gate) {
|
|
61
|
+
return {
|
|
62
|
+
...gate,
|
|
63
|
+
status: CARRIED_FORWARD,
|
|
64
|
+
carried_forward: { policy: LOCAL_PREVIEW_POLICY, from_status: gate.status, evidence: "missing" },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The hidden eager-media checkpoint, evaluated on its own.
|
|
69
|
+
export function applyLocalPreviewToCheckpoint(gate, { packet, report, baseUrl = null } = {}) {
|
|
70
|
+
if (gate?.status !== "blocked" || !isLocalPreview(packet, { baseUrl })) return gate;
|
|
71
|
+
const missing = gate.code === NO_CAPTURABLE_ROUTES_CODE
|
|
72
|
+
|| (gate.code === MISSING_CAPTURE_CODE && !pageLoadRecorded(report));
|
|
73
|
+
return missing ? carried(gate) : gate;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The polish gate folds the checkpoint in, so it is carried forward only when
|
|
77
|
+
// whatever it owns from the checkpoint was carried forward too.
|
|
78
|
+
export function applyLocalPreviewToPolishGate(gate, { packet, checkpointGate = null, baseUrl = null } = {}) {
|
|
79
|
+
if (gate?.status !== "blocked" || !isLocalPreview(packet, { baseUrl })) return gate;
|
|
80
|
+
const checkpointCarried = checkpointGate?.status === CARRIED_FORWARD;
|
|
81
|
+
if (gate.owned_checkpoint_status === "blocked" && !checkpointCarried) return gate;
|
|
82
|
+
if (gate.owned_checkpoint_only) return checkpointCarried ? carried(gate) : gate;
|
|
83
|
+
return CARRIED_POLISH_CODES.has(gate.code) ? carried(gate) : gate;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function starterResidueIsExpected(themeGate, { packet, baseUrl = null } = {}) {
|
|
87
|
+
return themeGate?.code === STARTER_IS_THE_DESIGN_THEME_CODE && isLocalPreview(packet, { baseUrl });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function carriedForwardMessage(gate) {
|
|
91
|
+
return `Carried forward on the local preview: ${gate.reason} This evidence is missing, not passed; a hosted preview or production run requires it.`;
|
|
92
|
+
}
|
|
@@ -92,6 +92,10 @@ export function evaluatePageKitSdkVersion({
|
|
|
92
92
|
waivers = [],
|
|
93
93
|
required = true,
|
|
94
94
|
now = new Date().toISOString(),
|
|
95
|
+
// { family, sdk_version } of the selected certified template family, when
|
|
96
|
+
// the caller knows it: the missing-pin reason names that verified SDK so the
|
|
97
|
+
// pin is not chosen by searching docs.
|
|
98
|
+
familyVerification = null,
|
|
95
99
|
} = {}) {
|
|
96
100
|
const pin = resolveSpecSdkPin(spec);
|
|
97
101
|
const { has_canonical: hasCanonical, has_alias: hasAlias } = pin;
|
|
@@ -139,7 +143,10 @@ export function evaluatePageKitSdkVersion({
|
|
|
139
143
|
scope: PAGE_KIT_SDK_VERSION_SCOPE,
|
|
140
144
|
status: "blocked",
|
|
141
145
|
code: "page_kit.sdk_version.spec_missing",
|
|
142
|
-
reason: "CampaignSpec is missing global_config.sdk_version (and the runtime.sdk_version alias); add an explicit released SDK pin before build or QA."
|
|
146
|
+
reason: "CampaignSpec is missing global_config.sdk_version (and the runtime.sdk_version alias); add an explicit released SDK pin before build or QA."
|
|
147
|
+
+ (isReleasedSdkVersion(familyVerification?.sdk_version) && typeof familyVerification?.family === "string"
|
|
148
|
+
? ` The "${familyVerification.family}" template family was last verified against ${familyVerification.sdk_version}.`
|
|
149
|
+
: ""),
|
|
143
150
|
waivable: false,
|
|
144
151
|
subject,
|
|
145
152
|
state: { spec_status: "missing" },
|
package/src/polish-node.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { campaignIdentitiesMatch, localSpecIdentityFields } from "./spec-source-identity.mjs";
|
|
2
|
+
import { NO_CAPTURABLE_ROUTES_CODE } from "./local-preview-policy.mjs";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { HIDDEN_EAGER_MEDIA_ACTIONS } from "./gate-actions.mjs";
|
|
4
5
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -93,6 +94,8 @@ function mappedSpecRoute(value, pageId) {
|
|
|
93
94
|
return route;
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
const NO_CAPTURABLE_ROUTES_ERROR = "no_capturable_routes";
|
|
98
|
+
|
|
96
99
|
export function planPolishCapture({ packet, baseUrl } = {}) {
|
|
97
100
|
if (!isPlainObject(packet) || !Array.isArray(packet?.source_html?.pages) || packet.source_html.pages.length === 0) {
|
|
98
101
|
throw new Error("polish capture requires packet.source_html.pages mappings.");
|
|
@@ -129,7 +132,14 @@ export function planPolishCapture({ packet, baseUrl } = {}) {
|
|
|
129
132
|
});
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
if (routes.length === 0)
|
|
135
|
+
if (routes.length === 0) {
|
|
136
|
+
const error = new Error(
|
|
137
|
+
"polish capture has no mapped non-skipped routes to capture: every mapped page is template stock (skip_reason), so there is no design route to compare."
|
|
138
|
+
+ " On a local-serve preview, missing polish evidence is carried forward as a warning; run `next` for the next stage.",
|
|
139
|
+
);
|
|
140
|
+
error.code = NO_CAPTURABLE_ROUTES_ERROR;
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
133
143
|
routes.sort((a, b) => a.requested_route.localeCompare(b.requested_route) || a.page_id.localeCompare(b.page_id));
|
|
134
144
|
for (let index = 1; index < routes.length; index += 1) {
|
|
135
145
|
if (routes[index - 1].requested_route === routes[index].requested_route) {
|
|
@@ -189,6 +199,19 @@ function recordedAuthorityBlock({ packet, report, plan = null, now } = {}) {
|
|
|
189
199
|
};
|
|
190
200
|
}
|
|
191
201
|
|
|
202
|
+
// Every mapped page is template stock: there is nothing for polish capture to
|
|
203
|
+
// measure, which is missing evidence rather than a malformed packet. It stays
|
|
204
|
+
// a non-waivable block; the local preview policy carries it forward.
|
|
205
|
+
function noCapturableRoutesBlock({ packet, report, now } = {}) {
|
|
206
|
+
const block = recordedAuthorityBlock({ packet, report, now });
|
|
207
|
+
return {
|
|
208
|
+
...block,
|
|
209
|
+
code: NO_CAPTURABLE_ROUTES_CODE,
|
|
210
|
+
reason: "Every mapped page is template stock (skip_reason), so polish capture has no design route to capture and this build has no page-load evidence.",
|
|
211
|
+
required_actions: [HIDDEN_EAGER_MEDIA_ACTIONS.map_design_route],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
192
215
|
function recordedCheckpointNotApplicable() {
|
|
193
216
|
return {
|
|
194
217
|
id: HIDDEN_EAGER_MEDIA_SCOPE,
|
|
@@ -219,7 +242,8 @@ export function evaluateRecordedHiddenEagerMediaCheckpoint({ packet, report, now
|
|
|
219
242
|
let plan;
|
|
220
243
|
try {
|
|
221
244
|
plan = planPolishCapture({ packet, baseUrl: "https://polish-capture.invalid" });
|
|
222
|
-
} catch {
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (error?.code === NO_CAPTURABLE_ROUTES_ERROR) return noCapturableRoutesBlock({ packet, report, now });
|
|
223
247
|
return recordedAuthorityBlock({ packet, report, now });
|
|
224
248
|
}
|
|
225
249
|
|
package/src/progress-node.mjs
CHANGED
|
@@ -58,7 +58,11 @@ export function projectProgressObservation({workspace,context,report,doctor,cont
|
|
|
58
58
|
binding:reportBound&&id(verdict.run_id)&&id(verdict.run_id)===id(report?.stages?.qa?.verdict_run_id)&&build&&qaSource===build&&doctor?.derived?.build_output_fingerprint?.status==='pass'&&hash(verdict.spec_hash)===localHash?'matching':'unconfirmed',
|
|
59
59
|
publish_state:accepted(qaResult?.qa_verdict_publish?.state,['skipped','ok','failed']),
|
|
60
60
|
}:null;
|
|
61
|
-
|
|
61
|
+
// The snapshot schema has no carried-forward state: a gate the local preview
|
|
62
|
+
// policy carried forward does not apply on this path, so it is not_applicable
|
|
63
|
+
// here (never pass).
|
|
64
|
+
const gateState=status=>accepted(status==='carried_forward'?'not_applicable':status,['pass','blocked','waived','not_applicable']);
|
|
65
|
+
const gates=(Array.isArray(continuation?.gates)?continuation.gates:[]).slice(0,PROGRESS_GATE_LIMIT).map(gate=>({id:accepted(gate?.id,PROGRESS_GATE_IDS),state:PROGRESS_GATE_IDS.includes(gate?.id)?gateState(gate?.status):'unknown'}));
|
|
62
66
|
const actions=[...new Set((Array.isArray(continuation?.next_actions)?continuation.next_actions:[]).slice(0,64).map(action=>accepted(action?.id,PROGRESS_ACTION_IDS)))];
|
|
63
67
|
const stage=accepted(continuation?.stage,PROGRESS_CONTINUATIONS);
|
|
64
68
|
const preview=typeof packet?.deploy?.preview_url==='string'&&packet.deploy.preview_url?packet.deploy.preview_url:null;
|
|
@@ -5,7 +5,7 @@ import { HTML_NAMESPACE, baseInEffect, documentBases, endsUnclosed, frozenBaseUr
|
|
|
5
5
|
|
|
6
6
|
export const BINDING_SCHEMA = 'campaigns-os-page-binding/v0';
|
|
7
7
|
export const BINDING_LIMITS = Object.freeze({ scripts_per_page: 6, scripts_per_run: 24, script_bytes: 262144, timeout_ms: 5000 });
|
|
8
|
-
const SDK = /^https:\/\/cdn\.jsdelivr\.net\/gh\/NextCommerceCo\/campaign-cart@[^/]+\/(?:dist\/index\.js|public\/loader\.js)(?:\?[^#]*)?$/;
|
|
8
|
+
const SDK = /^https:\/\/cdn\.jsdelivr\.net\/gh\/NextCommerceCo\/campaign-cart@[^/]+\/(?:dist\/(?:index|loader)\.js|public\/loader\.js)(?:\?[^#]*)?$/;
|
|
9
9
|
const str = value => typeof value === 'string' && value.length ? value : null;
|
|
10
10
|
|
|
11
11
|
// The expected credential uses the existing producer resolver. Disagreement
|
package/src/qa-browser.mjs
CHANGED
|
@@ -2822,7 +2822,22 @@ async function pricingVisibilityAssertions(browserPage, page, options = {}) {
|
|
|
2822
2822
|
selectors.length ? countVisiblePriceRows(browserPage, selectors) : 0,
|
|
2823
2823
|
countVisiblePriceRows(browserPage, totalSelectors),
|
|
2824
2824
|
]);
|
|
2825
|
-
|
|
2825
|
+
// Nothing priced is visible. When the checkout also has no package
|
|
2826
|
+
// selection of its own and the SDK reports an empty cart, the cart is
|
|
2827
|
+
// filled on an earlier page (a landing link carrying forcePackageId), and
|
|
2828
|
+
// opening the checkout directly shows a state no shopper reaches. The
|
|
2829
|
+
// test-order path enters that cart from the landing page instead.
|
|
2830
|
+
const emptyEntry = visibleCount === 0 && totalVisibleCount === 0
|
|
2831
|
+
? await Promise.all([
|
|
2832
|
+
browserPage.evaluate(() => (typeof window.next?.getCartCount === "function" ? window.next.getCartCount() : null)),
|
|
2833
|
+
browserPage.evaluate(checkoutSelectionSurfaceScript()),
|
|
2834
|
+
]).then(
|
|
2835
|
+
([cart_count, selection_surface]) => ({ cart_count, selection_surface }),
|
|
2836
|
+
// A failed probe leaves the row failed, and says why on the row.
|
|
2837
|
+
(error) => ({ probe_error: trim(error?.message || String(error)).slice(0, 200) }),
|
|
2838
|
+
)
|
|
2839
|
+
: null;
|
|
2840
|
+
return [checkoutPriceVisibilityAssertion({ page, selectors, visibleCount, totalSelectors, totalVisibleCount, emptyEntry })];
|
|
2826
2841
|
}
|
|
2827
2842
|
return [];
|
|
2828
2843
|
}
|
|
@@ -2896,25 +2911,37 @@ function upsellPriceVisibilityAssertion({ page, selectors, visibleCount }) {
|
|
|
2896
2911
|
// no `totalSelectors` ran the bundle-row check alone, and the row says so
|
|
2897
2912
|
// rather than reporting an empty selector list that reads like a check that
|
|
2898
2913
|
// ran and found nothing.
|
|
2899
|
-
|
|
2914
|
+
//
|
|
2915
|
+
// `emptyEntry` ({ cart_count, selection_surface }, or { probe_error }) is read
|
|
2916
|
+
// only when nothing priced is visible. An empty SDK cart (cart_count 0, never
|
|
2917
|
+
// null: null means no SDK was read) on a checkout with no package selection of
|
|
2918
|
+
// its own is not a price the shopper fails to see: the row is skipped with
|
|
2919
|
+
// that reason rather than failed, and only that row carries the probe values.
|
|
2920
|
+
function checkoutPriceVisibilityAssertion({ page, selectors, visibleCount, totalSelectors, totalVisibleCount = 0, emptyEntry = null }) {
|
|
2900
2921
|
const totalChecked = Array.isArray(totalSelectors);
|
|
2901
2922
|
const ok = visibleCount >= 1 || (totalChecked && totalVisibleCount >= 1);
|
|
2923
|
+
const enteredUpstream = !ok && emptyEntry?.cart_count === 0 && emptyEntry?.selection_surface?.count === 0;
|
|
2924
|
+
const counts = totalChecked
|
|
2925
|
+
? `${visibleCount} visible price row(s); ${totalVisibleCount} visible cart-summary total(s)`
|
|
2926
|
+
: `${visibleCount} visible price row(s)`;
|
|
2902
2927
|
return assertion({
|
|
2903
2928
|
id: "pricing.checkout_price_visible",
|
|
2904
2929
|
family: "pricing",
|
|
2905
2930
|
page,
|
|
2906
|
-
status: ok ? STATUS.PASS : STATUS.FAIL,
|
|
2907
|
-
severity: ok ? undefined : SEVERITY.WARN,
|
|
2931
|
+
status: ok ? STATUS.PASS : enteredUpstream ? STATUS.SKIPPED : STATUS.FAIL,
|
|
2932
|
+
severity: ok || enteredUpstream ? undefined : SEVERITY.WARN,
|
|
2908
2933
|
expected: totalChecked
|
|
2909
2934
|
? "at least one visible checkout bundle price row or a visible cart-summary total"
|
|
2910
2935
|
: "at least one visible checkout bundle price row",
|
|
2911
|
-
actual:
|
|
2912
|
-
? `${
|
|
2913
|
-
:
|
|
2936
|
+
actual: enteredUpstream
|
|
2937
|
+
? `${counts}: opened directly, the checkout has an empty cart and no package selection of its own; its cart is filled on an earlier page, and the test order enters it from there`
|
|
2938
|
+
: counts,
|
|
2914
2939
|
evidence: {
|
|
2915
2940
|
selectors,
|
|
2916
2941
|
visible_count: visibleCount,
|
|
2917
2942
|
...(totalChecked ? { total_selectors: totalSelectors, total_visible_count: totalVisibleCount } : {}),
|
|
2943
|
+
...(enteredUpstream ? { cart_count: emptyEntry.cart_count, checkout_selection_surface: emptyEntry.selection_surface } : {}),
|
|
2944
|
+
...(emptyEntry?.probe_error ? { empty_cart_probe_error: emptyEntry.probe_error } : {}),
|
|
2918
2945
|
page_url: page.url,
|
|
2919
2946
|
},
|
|
2920
2947
|
});
|
|
@@ -4962,10 +4989,7 @@ async function waitForCheckoutResult(page, events = null) {
|
|
|
4962
4989
|
if (outcomeUrl.test(String(safePageUrl(page) || ""))) break;
|
|
4963
4990
|
if (events) {
|
|
4964
4991
|
const rejected = rejectedOrderCreateResponse(events);
|
|
4965
|
-
if (rejected)
|
|
4966
|
-
const detail = typeof rejected.body?.detail === "string" ? `: ${trim(rejected.body.detail)}` : "";
|
|
4967
|
-
throw new Error(`order create rejected: HTTP ${rejected.status}${detail}`);
|
|
4968
|
-
}
|
|
4992
|
+
if (rejected) throw new Error(orderCreateRejectionMessage(rejected));
|
|
4969
4993
|
const failed = failedOrderCreateRequest(events);
|
|
4970
4994
|
if (failed) throw new Error(`order create request failed: ${failed.failure || "network failure"}`);
|
|
4971
4995
|
}
|
|
@@ -4979,6 +5003,23 @@ async function waitForCheckoutResult(page, events = null) {
|
|
|
4979
5003
|
// The MOST RECENT create response decides the outcome: SDKs/platforms retry
|
|
4980
5004
|
// transient create failures, so [..., 400, 201] means the retry succeeded and
|
|
4981
5005
|
// the earlier rejection is history, not the result.
|
|
5006
|
+
// The platform refuses an order whose customer, items and total match one it
|
|
5007
|
+
// accepted or is still processing in the last 30 minutes, and reports it in
|
|
5008
|
+
// `payment_details`. QA reuses one test customer, so two runs against the same
|
|
5009
|
+
// campaign at once, or a rerun after an attempt that died mid-submit, trip it.
|
|
5010
|
+
const DUPLICATE_ORDER_PATTERN = /duplicate order/i;
|
|
5011
|
+
const DUPLICATE_ORDER_REMEDY =
|
|
5012
|
+
"duplicate_order: the platform refused an order matching a recent one from the same test customer with the same items and total, " +
|
|
5013
|
+
"for up to 30 minutes. Concurrent QA runs that share a test customer collide. Re-run with a different --test-email-prefix " +
|
|
5014
|
+
"(or --test-email), or wait. The shipping address is not part of the match";
|
|
5015
|
+
|
|
5016
|
+
function orderCreateRejectionMessage(rejected) {
|
|
5017
|
+
const body = rejected?.body;
|
|
5018
|
+
const reason = [body?.detail, body?.payment_details].find((value) => typeof value === "string" && value.trim());
|
|
5019
|
+
const base = `order create rejected: HTTP ${rejected?.status}${reason ? `: ${trim(reason)}` : ""}`;
|
|
5020
|
+
return reason && DUPLICATE_ORDER_PATTERN.test(reason) ? `${base} (${DUPLICATE_ORDER_REMEDY})` : base;
|
|
5021
|
+
}
|
|
5022
|
+
|
|
4982
5023
|
function rejectedOrderCreateResponse(events) {
|
|
4983
5024
|
for (let index = events.responses.length - 1; index >= 0; index -= 1) {
|
|
4984
5025
|
const response = events.responses[index];
|
|
@@ -6393,7 +6434,7 @@ function sanitizedEvents(events) {
|
|
|
6393
6434
|
responses: events.responses.slice(-20).map((response) => ({
|
|
6394
6435
|
status: response.status,
|
|
6395
6436
|
url: response.url,
|
|
6396
|
-
body: summarizeResponseBody(response.body),
|
|
6437
|
+
body: summarizeResponseBody(response.body, { status: response.status }),
|
|
6397
6438
|
})),
|
|
6398
6439
|
failed: events.failed.slice(-20),
|
|
6399
6440
|
console: events.console.slice(-20),
|
|
@@ -6420,7 +6461,7 @@ function summarizeRequestPostData(value) {
|
|
|
6420
6461
|
}
|
|
6421
6462
|
}
|
|
6422
6463
|
|
|
6423
|
-
function summarizeResponseBody(body) {
|
|
6464
|
+
function summarizeResponseBody(body, { status = null } = {}) {
|
|
6424
6465
|
if (typeof body === "string") return trim(body).slice(0, 1000);
|
|
6425
6466
|
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
|
6426
6467
|
return {
|
|
@@ -6432,6 +6473,12 @@ function summarizeResponseBody(body) {
|
|
|
6432
6473
|
...(body.checkout_url ? { checkout_url: body.checkout_url } : {}),
|
|
6433
6474
|
...(Array.isArray(body.lines) ? { lines: extractReceiptLines(body) } : {}),
|
|
6434
6475
|
...(body.detail ? { detail: body.detail } : {}),
|
|
6476
|
+
// A rejected request names its reason here (for example the
|
|
6477
|
+
// duplicate-order refusal). Kept only on an error response, so a
|
|
6478
|
+
// successful create never carries payment_details into evidence.
|
|
6479
|
+
...(Number(status) >= 400 && typeof body.payment_details === "string"
|
|
6480
|
+
? { payment_details: trim(body.payment_details).slice(0, 300) }
|
|
6481
|
+
: {}),
|
|
6435
6482
|
};
|
|
6436
6483
|
}
|
|
6437
6484
|
|
|
@@ -7789,6 +7836,8 @@ export const __qaBrowserTestHooks = Object.freeze({
|
|
|
7789
7836
|
linePriceDeltaEvidence,
|
|
7790
7837
|
packageMatchesLine,
|
|
7791
7838
|
rejectedOrderCreateResponse,
|
|
7839
|
+
orderCreateRejectionMessage,
|
|
7840
|
+
summarizeResponseBody,
|
|
7792
7841
|
failedOrderCreateRequest,
|
|
7793
7842
|
extractOrderVouchers,
|
|
7794
7843
|
orderDiscountTotal,
|
package/src/qa-node.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { campaignSpecIdentity, resolveCampaignIdentity, campaignIdentitiesMatch } from "./spec-source-identity.mjs";
|
|
2
|
+
import { applyLocalPreviewToCheckpoint, applyLocalPreviewToPolishGate, carriedForwardMessage, CARRIED_FORWARD, starterResidueIsExpected } from "./local-preview-policy.mjs";
|
|
2
3
|
import { expectedBinding, createBindingScriptLoader, observeBinding, bindingAssertion, scriptParseAssertion } from './qa-binding-evidence.mjs';
|
|
3
4
|
import { shellToken } from "./shell-token.mjs";
|
|
4
5
|
import { applyQaBuildScope, specForQaScope } from "./qa-build-scope.mjs";
|
|
@@ -481,6 +482,8 @@ async function resolveQaInputs(args, {
|
|
|
481
482
|
packetPath,
|
|
482
483
|
report: checkpointPreflight?.runtimeReport,
|
|
483
484
|
hiddenEagerMediaGate,
|
|
485
|
+
packet,
|
|
486
|
+
baseUrl: stringArg(args["base-url"]),
|
|
484
487
|
});
|
|
485
488
|
const qaWaivers = resolveQaWaivers({ packetPath, report: checkpointPreflight?.runtimeReport });
|
|
486
489
|
const qaScope = applyQaBuildScope(topologies, {
|
|
@@ -593,7 +596,10 @@ function resolvePacketCheckpointPreflight(args, {
|
|
|
593
596
|
waivers: report?.waivers,
|
|
594
597
|
required: true,
|
|
595
598
|
}),
|
|
596
|
-
|
|
599
|
+
applyLocalPreviewToCheckpoint(
|
|
600
|
+
evaluateRecordedHiddenEagerMediaCheckpoint({ packet, report }),
|
|
601
|
+
{ packet, report, baseUrl: stringArg(args["base-url"]) },
|
|
602
|
+
),
|
|
597
603
|
];
|
|
598
604
|
const runtimeReport = reportMatchesPacketIdentity(report, packet) ? report : null;
|
|
599
605
|
return {
|
|
@@ -692,6 +698,8 @@ function resolvedFromBlockedCheckpointPreflight(preflight, args) {
|
|
|
692
698
|
packetPath: preflight.packetPath,
|
|
693
699
|
report: preflight.runtimeReport,
|
|
694
700
|
hiddenEagerMediaGate,
|
|
701
|
+
packet: preflight.packet,
|
|
702
|
+
baseUrl: stringArg(args["base-url"]),
|
|
695
703
|
});
|
|
696
704
|
return {
|
|
697
705
|
themeGate,
|
|
@@ -931,16 +939,18 @@ function resolvePolishGate({
|
|
|
931
939
|
packetPath,
|
|
932
940
|
report: reportOverride = undefined,
|
|
933
941
|
hiddenEagerMediaGate = undefined,
|
|
942
|
+
packet = null,
|
|
943
|
+
baseUrl = null,
|
|
934
944
|
}) {
|
|
935
945
|
const report = reportOverride === undefined
|
|
936
946
|
? loadRuntimeArtifact(packetPath, "assembly-report.json")
|
|
937
947
|
: reportOverride;
|
|
938
|
-
const gate = evaluatePolishGate({
|
|
948
|
+
const gate = applyLocalPreviewToPolishGate(evaluatePolishGate({
|
|
939
949
|
report,
|
|
940
950
|
required: true,
|
|
941
951
|
hiddenEagerMediaGate,
|
|
942
952
|
currentOutputFingerprint: currentBuiltOutputFingerprint(packetPath),
|
|
943
|
-
});
|
|
953
|
+
}), { packet, checkpointGate: hiddenEagerMediaGate, baseUrl });
|
|
944
954
|
gate.scope_source = report ? "assembly_report" : "missing_assembly_report";
|
|
945
955
|
return gate;
|
|
946
956
|
}
|
|
@@ -1044,6 +1054,18 @@ function polishGateAssertion(gate) {
|
|
|
1044
1054
|
},
|
|
1045
1055
|
});
|
|
1046
1056
|
}
|
|
1057
|
+
if (gate.status === CARRIED_FORWARD) {
|
|
1058
|
+
return assertion({
|
|
1059
|
+
id: gate.code,
|
|
1060
|
+
family: "polish_gate",
|
|
1061
|
+
page,
|
|
1062
|
+
status: STATUS.WARN,
|
|
1063
|
+
severity: SEVERITY.WARN,
|
|
1064
|
+
expected: "current structured Polish evidence produced by next-campaigns-polish",
|
|
1065
|
+
actual: carriedForwardMessage(gate),
|
|
1066
|
+
evidence: { ...evidence, reason: gate.reason, carried_forward: gate.carried_forward },
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1047
1069
|
if (gate.status === "not_applicable") {
|
|
1048
1070
|
return assertion({
|
|
1049
1071
|
id: gate.code,
|
|
@@ -1350,6 +1372,9 @@ function hiddenEagerMediaGateAssertion(gate) {
|
|
|
1350
1372
|
if (summary.status === "waived") {
|
|
1351
1373
|
return assertion({ ...common, status: STATUS.WARN, severity: SEVERITY.WARN, waiver: summary.waiver });
|
|
1352
1374
|
}
|
|
1375
|
+
if (summary.status === CARRIED_FORWARD) {
|
|
1376
|
+
return assertion({ ...common, status: STATUS.WARN, severity: SEVERITY.WARN, actual: `${summary.code}: ${carriedForwardMessage(summary)}` });
|
|
1377
|
+
}
|
|
1353
1378
|
if (summary.status === "not_applicable") {
|
|
1354
1379
|
return assertion({ ...common, status: STATUS.SKIPPED });
|
|
1355
1380
|
}
|
|
@@ -2306,7 +2331,11 @@ async function runResolvedQa(args, resolved, { runSessionActive = false, liveCam
|
|
|
2306
2331
|
if (args.browser === true) {
|
|
2307
2332
|
assertions.push(...await runBrowserChecks(resolved.topologies, args, {
|
|
2308
2333
|
brandContract: resolved.brandContract,
|
|
2309
|
-
|
|
2334
|
+
// With no generatable brand theme on the local preview, the starter
|
|
2335
|
+
// template is the design: its residue is a warning (local-preview-policy.mjs).
|
|
2336
|
+
residueSeverity: starterResidueIsExpected(gate, { packet: resolved.packet, baseUrl: resolved.baseUrl })
|
|
2337
|
+
? SEVERITY.WARN
|
|
2338
|
+
: residueSeverityForThemeGate(gate.status),
|
|
2310
2339
|
supportedPaymentMethods: supportedPaymentMethodsFromSpec(resolved.spec),
|
|
2311
2340
|
}));
|
|
2312
2341
|
}
|