@nextcommerce/campaigns-os 1.60.0 → 1.62.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 +2 -1
- package/CHANGELOG.md +244 -0
- package/README.md +86 -438
- package/agents/claude/CLAUDE.md +20 -0
- package/agents/codex/AGENTS.md +19 -0
- package/agents/copilot/copilot-instructions.md +19 -0
- package/agents/cursor/campaigns-os.mdc +21 -0
- package/campaign-spec/README.md +7 -0
- package/campaign-spec/dist/analytics-vendors.d.ts +40 -0
- package/campaign-spec/dist/analytics-vendors.js +44 -0
- package/campaign-spec/dist/checkout-flow.d.ts +18 -0
- package/campaign-spec/dist/checkout-flow.js +28 -0
- package/campaign-spec/dist/index.d.ts +4 -2
- package/campaign-spec/dist/index.js +5 -1
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +8 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.js +39 -0
- package/campaign-spec/dist/rules/campaign-currencies.d.ts +29 -0
- package/campaign-spec/dist/rules/campaign-currencies.js +130 -0
- package/campaign-spec/dist/rules/checkout-path-has-packages.d.ts +31 -0
- package/campaign-spec/dist/rules/checkout-path-has-packages.js +79 -0
- package/campaign-spec/dist/rules/index.d.ts +2 -0
- package/campaign-spec/dist/rules/index.js +6 -0
- package/campaign-spec/dist/types.d.ts +43 -9
- package/compatibility.json +1 -1
- package/contracts/brand-theme-target-tokens.next-core.v0.json +95 -25
- package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
- package/contracts/commerce-surface-catalog.json +68 -61
- package/contracts/effects.v1.json +549 -165
- package/contracts/fixtures/runtime-recipe/accept/current.json +3 -0
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +3 -0
- package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +3 -0
- package/contracts/release-ledger.json +1767 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +3 -0
- package/contracts/supported-surface.json +10 -10
- package/contracts/template-brand-contract.shared-commerce.v0.json +23 -1
- package/docs/access-model.md +20 -0
- package/docs/brand-theme-bridge.md +62 -3
- package/docs/build-packet.md +456 -36
- package/docs/campaign-build-brief.md +16 -0
- package/docs/campaign-standardization-report.md +4 -4
- package/docs/campaigns-os-build-flow.md +112 -2
- package/docs/campaignspec-authoring-examples.md +141 -0
- package/docs/design-source-package.md +64 -8
- package/docs/effects.md +17 -9
- package/docs/entry-points.md +188 -0
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/polish-evidence.md +42 -2
- package/docs/progress-snapshots.md +7 -4
- package/docs/qa-and-test-orders.md +197 -3
- package/docs/quickstart.md +438 -0
- package/docs/readback.md +154 -6
- package/docs/runtime-readiness.md +6 -3
- package/docs/sdk-storage-compatibility.md +4 -2
- package/docs/skills-revision.md +10 -10
- package/docs/source-adapters.md +213 -0
- package/package.json +10 -3
- package/schemas/campaign-design-source-package.v0.schema.json +8 -0
- package/schemas/campaign-runtime-build-context.v0.schema.json +9 -0
- package/schemas/campaign-runtime-build-packet.v0.schema.json +9 -0
- package/schemas/campaign-spec.v4.schema.json +19 -3
- package/schemas/campaign-spec.v5.schema.json +19 -3
- package/schemas/campaigns-os-readback.v2.schema.json +238 -1
- package/schemas/source-html-manifest.v0.schema.json +3 -0
- 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 -4
- package/skills/next-campaigns-os/SKILL.md +7 -4
- package/skills/next-campaigns-os-setup/SKILL.md +5 -3
- package/skills/next-campaigns-polish/SKILL.md +8 -10
- package/skills/next-campaigns-qa/SKILL.md +7 -4
- package/skills/next-campaigns-sdk-bump/SKILL.md +151 -25
- package/skills.json +11 -11
- package/src/brand-theme.mjs +415 -9
- package/src/brief-summary.mjs +6 -2
- package/src/browser-launch.mjs +2 -1
- package/src/build-brief.mjs +13 -3
- package/src/built-site-scope.mjs +2 -2
- package/src/campaign-workspace.mjs +288 -15
- package/src/change-scope.mjs +284 -0
- package/src/cli.mjs +358 -97
- package/src/commercial-journey.mjs +16 -3
- package/src/commercial-parity.mjs +15 -7
- package/src/content-residue.mjs +4 -2
- package/src/design-file-reference.mjs +93 -0
- package/src/design-source-package.mjs +91 -1
- package/src/design-source-publication.mjs +9 -6
- package/src/directory-lock.mjs +73 -19
- package/src/doctor/checks.mjs +87 -81
- package/src/doctor/ci.mjs +177 -0
- package/src/doctor/inspect.mjs +41 -6
- package/src/doctor/next-step.mjs +7 -0
- package/src/doctor/source-provenance.mjs +74 -0
- package/src/doctor-sidecar.mjs +3 -2
- package/src/evidence-debt.mjs +159 -0
- package/src/finding-cause.mjs +12 -11
- package/src/input-refresh.mjs +339 -32
- package/src/invocation.mjs +7 -1
- package/src/live-campaign-refs.mjs +7 -2
- package/src/local-preview-policy.mjs +35 -2
- package/src/page-kit-analytics-vendors.mjs +132 -0
- package/src/parity/compare.mjs +316 -0
- package/src/parity/design-access.mjs +54 -0
- package/src/parity/element-parity-record.mjs +569 -0
- package/src/parity/probe.mjs +205 -0
- package/src/parity/snapshot.mjs +110 -0
- package/src/png-header.mjs +87 -0
- package/src/polish-browser.mjs +189 -10
- package/src/polish-captures-record.mjs +52 -0
- package/src/polish-deadline.mjs +14 -0
- package/src/polish-gate.mjs +25 -6
- package/src/polish-node.mjs +81 -7
- package/src/polish-readability.mjs +221 -9
- package/src/polish-visual-fixtures.mjs +74 -0
- package/src/polish-visual.mjs +613 -0
- package/src/progress-node.mjs +3 -2
- package/src/qa-analytics-correctness.mjs +14 -2
- package/src/qa-analytics-parity.mjs +111 -6
- package/src/qa-browser.mjs +40 -4
- package/src/qa-commercial-parity.mjs +50 -3
- package/src/qa-node.mjs +235 -22
- package/src/qa-publish.mjs +9 -8
- package/src/qa-sidecar.mjs +16 -4
- package/src/qa-vendor-beacons.mjs +232 -0
- package/src/qa-vendor-endpoints.mjs +306 -0
- package/src/qc-accept.mjs +176 -6
- package/src/qc-check-registry.mjs +42 -2
- package/src/qc-results.mjs +477 -42
- package/src/readback.mjs +113 -21
- package/src/route-identity.mjs +10 -0
- package/src/run-record.mjs +18 -10
- package/src/runtime-state-ignore.mjs +6 -2
- package/src/sdk-attribute-index.mjs +11 -11
- package/src/sdk-markup.mjs +77 -2
- package/src/sdk-storage-compatibility.mjs +115 -6
- package/src/source-html-intake.mjs +15 -1
- package/src/source-html-manifest.mjs +7 -0
- package/src/source-html-server.mjs +129 -0
- package/src/spec-derive-campaign.mjs +216 -0
- package/src/spec-derive.mjs +473 -53
- package/src/spec-identity.mjs +14 -0
- package/src/stage-ledger.mjs +40 -1
- package/src/stage-record.mjs +100 -17
- package/src/target-lock.mjs +11 -1
- package/src/theme-gate.mjs +2 -2
- package/src/visual-diff.mjs +296 -0
- package/src/visual-reference.mjs +126 -0
package/src/qa-node.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { campaignSpecIdentity, resolveCampaignIdentity, campaignIdentitiesMatch } from "./spec-source-identity.mjs";
|
|
2
|
-
import { applyLocalPreviewToCheckpoint, applyLocalPreviewToPolishGate, carriedForwardMessage, CARRIED_FORWARD, ensureUrlTrailingSlash, normalizeQaBaseUrl, starterResidueIsExpected } from "./local-preview-policy.mjs";
|
|
2
|
+
import { applyLocalPreviewToCheckpoint, applyLocalPreviewToPolishGate, applyPageOnlyChangeToCheckpoint, applyPageOnlyChangeToPolishGate, carriedForwardMessage, CARRIED_FORWARD, ensureUrlTrailingSlash, isPageOnlyCarry, normalizeQaBaseUrl, starterResidueIsExpected } from "./local-preview-policy.mjs";
|
|
3
|
+
import { assessEvidenceDebt } from "./evidence-debt.mjs";
|
|
4
|
+
import { routePageIdForUrl } from "./change-scope.mjs";
|
|
3
5
|
import { expectedBinding, createBindingScriptLoader, observeBinding, bindingAssertion, scriptParseAssertion } from './qa-binding-evidence.mjs';
|
|
4
6
|
import { shellToken } from "./shell-token.mjs";
|
|
5
|
-
import { currentBriefMaterial } from "./input-currency.mjs";
|
|
7
|
+
import { currentBriefMaterial, deriveInputCurrency } from "./input-currency.mjs";
|
|
6
8
|
import { applyQaBuildScope, specForQaScope } from "./qa-build-scope.mjs";
|
|
7
9
|
import { requiredActionText } from "./gate-actions.mjs";
|
|
8
10
|
import { parseOrderPathDepthFlag } from "./proof-policy.mjs";
|
|
@@ -37,6 +39,7 @@ import { assessReceiptPurchase } from "./qa-analytics-correctness.mjs";
|
|
|
37
39
|
import { trackingQaAssertion, trackingRunScopeRows } from "./qa-tracking-params.mjs";
|
|
38
40
|
import { contentParamNotRequestedRows, contentParamQaAssertion } from "./qa-content-params.mjs";
|
|
39
41
|
import { policyLinkNotRequestedRows, policyLinkQaAssertion } from "./qa-policy-links.mjs";
|
|
42
|
+
import { createVendorBeaconLedger, QA_VENDOR_BEACONS, vendorBeaconAssertion, vendorBeaconMode } from "./qa-vendor-beacons.mjs";
|
|
40
43
|
import { createVerdict, formatTestOrderSummary, isFindingAssertion, QA_ASSERTION_FAMILY_VOCABULARY, SESSION_ENDING_DISPOSITIONS, SEVERITY, STATUS, validateVerdict } from "./qa-verdict.mjs";
|
|
41
44
|
import { normalizeSdkMetaName, lookupSdkIgnoredMetaTag } from "./sdk-meta-tags.mjs";
|
|
42
45
|
import { annotateQaAssertionCauses, formatCauseReportLines, formatCauseTag } from "./finding-cause.mjs";
|
|
@@ -78,12 +81,12 @@ import {
|
|
|
78
81
|
} from "./polish-capture.mjs";
|
|
79
82
|
import { resolveConsent } from "./consent.mjs";
|
|
80
83
|
import { markDoctorSidecarStale } from "./doctor-sidecar.mjs";
|
|
81
|
-
import { commitAssemblyReport } from "./stage-ledger.mjs";
|
|
84
|
+
import { commitAssemblyReport, qaExtendedBuildFingerprint } from "./stage-ledger.mjs";
|
|
82
85
|
// Tags a refusal raised before a qa handler runs, so the CLI's lifecycle
|
|
83
86
|
// persist step suppresses the journal append from one place. lifecycle.mjs
|
|
84
87
|
// imports nothing from this repository, so this cannot be circular.
|
|
85
88
|
import { refused, refusing } from "./lifecycle.mjs";
|
|
86
|
-
import { campaignSidecarPaths, explicitReportPath, resolveCampaignWorkspace, targetRepoFor } from "./campaign-workspace.mjs";
|
|
89
|
+
import { campaignSidecarPaths, explicitReportPath, packetRuntimeRelPath, resolveCampaignWorkspace, targetRepoFor } from "./campaign-workspace.mjs";
|
|
87
90
|
import { loadParityFixture } from "./qa-parity-fixture.mjs";
|
|
88
91
|
import { assessParityCapture, resolveParityScenario, runParityCapture } from "./qa-parity-capture.mjs";
|
|
89
92
|
import { loadPageKitCampaignEntry, PAGE_KIT_CAMPAIGNS_REL_PATH } from "./page-kit-campaign-config.mjs";
|
|
@@ -131,14 +134,14 @@ const HELP = `campaigns-os qa — Node/npm spec-aware QA
|
|
|
131
134
|
Usage:
|
|
132
135
|
campaigns-os qa parity --fixture <parity-fixture.json> --scenario <scenario-id> [--base-url <override>] [--baseline <url>] [--parity-order-json <file>] [--no-post-verdict]
|
|
133
136
|
campaigns-os qa resolve --packet <campaign-runtime.build.json> [--base-url <url>] [--no-probe] [--probe-timeout-ms <ms>] [--json]
|
|
134
|
-
campaigns-os qa run --packet <campaign-runtime.build.json> [--base-url <url>] [--currency <code>] [--output-dir <dir>] [--no-remit] [--no-live-refs] [--json]
|
|
137
|
+
campaigns-os qa run --packet <campaign-runtime.build.json> [--base-url <url>] [--currency <code>] [--page <page_id>]... [--output-dir <dir>] [--no-remit] [--no-live-refs] [--json]
|
|
135
138
|
campaigns-os qa policy set --packet <campaign-runtime.build.json> [--allowed-domains-confirmed true|false] [--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--order-path-depth <off|common|full>] [--json]
|
|
136
139
|
campaigns-os qa waive --packet <campaign-runtime.build.json> --assertion analytics-correctness:purchase-fires --reason "<why>" [--waived-by <who>] [--report <assembly-report.json>] [--json]
|
|
137
140
|
campaigns-os qa promote --packet <campaign-runtime.build.json> --verdict <full-verdict.json> [--json] # project one explicit qa-output verdict to the committed .campaign-runtime/qa-verdict.json sidecar
|
|
138
141
|
campaigns-os qa publish --packet <campaign-runtime.build.json> [--verdict <full-verdict.json>] [--republish] [--proxy-base <url>] [--dry-run] [--json] # post an already-stored verdict to the QA portal; no re-run, no orders
|
|
139
142
|
campaigns-os qa resolve <map-id> --spec <campaign-spec.json> [--base-url <url>]
|
|
140
143
|
campaigns-os qa run <map-id> --spec <campaign-spec.json> --base-url <url>
|
|
141
|
-
campaigns-os qa run --site <page-kit-target-repo> --base-url <url> --family <family> [--slug <slug>] [--browser] # L7: QA a built _site/ with no packet/spec
|
|
144
|
+
campaigns-os qa run --site <page-kit-target-repo> --base-url <url> --family <family> [--slug <slug>] [--currency <code>] [--browser] # L7: QA a built _site/ with no packet/spec
|
|
142
145
|
campaigns-os qa install-browser [--json] # one-time: install the package-owned Playwright Chromium (same as npm run qa:install-browser from a checkout)
|
|
143
146
|
|
|
144
147
|
Options:
|
|
@@ -147,6 +150,9 @@ Options:
|
|
|
147
150
|
--baseline <url> Optional live analytics baseline for qa parity.
|
|
148
151
|
--parity-order-json <path> Assessment-only saved order + analytics capture bundle; skips Playwright.
|
|
149
152
|
--packet <path> Read Map ID, local CampaignSpec, deploy URL, and QA metadata from a Build Packet.
|
|
153
|
+
--page <page_id> Check only this page; repeat for more pages. Pays the QA a page-only change
|
|
154
|
+
owes on those pages (doctor --ci and next name them). Packet QA only; places no
|
|
155
|
+
test orders, and refuses a change that touches commerce or leaves a changed page out.
|
|
150
156
|
--site <path> L7 non-packet QA: resolve scope (pages + funnel types) from a built page-kit _site/.
|
|
151
157
|
Requires --base-url and --family. No Map ID / CampaignSpec needed.
|
|
152
158
|
--spec <path> Local exported CampaignSpec JSON for the non-packet Map ID flow.
|
|
@@ -195,7 +201,10 @@ Options:
|
|
|
195
201
|
--currency <code> qa run: enter each funnel's entry page with ?currency=<CODE> (three letters, e.g. GBP),
|
|
196
202
|
keeping its existing query. The flag takes one code per run (given twice, the
|
|
197
203
|
last one is used); the verdict records it as evidence.currency on the entry
|
|
198
|
-
page's http:<page> row.
|
|
204
|
+
page's http:<page> row. Expected totals are calculated in the code, and a test
|
|
205
|
+
order or calculate response in another currency is a blocker (exit 4) on its
|
|
206
|
+
browser-order-currency:<path> or commercial-currency:<CODE>:<ANSWERED> row.
|
|
207
|
+
Not supported by qa parity or --legacy-api-test-order.
|
|
199
208
|
--auth-cookie <cookie> Cookie header for protected previews.
|
|
200
209
|
--browser Run Playwright-rendered browser checks after static Node checks.
|
|
201
210
|
Requires one-time setup: campaigns-os qa install-browser
|
|
@@ -229,6 +238,15 @@ Options:
|
|
|
229
238
|
--max-order-creations <n> Hard bound on REAL order creations in this run, reserved before each submit
|
|
230
239
|
click. Default: the planned path count. A path whose failure is confirmed to
|
|
231
240
|
have created an order is inspected read-only, never resubmitted.
|
|
241
|
+
--vendor-beacons live qa run --test-order: let the page's analytics vendor beacons reach the vendors.
|
|
242
|
+
By default a test-order run answers requests to the listed vendor beacon
|
|
243
|
+
endpoints (GA4, Meta, TikTok, Snap, Pinterest, Taboola, Triple Whale, AppLovin
|
|
244
|
+
Axon) locally in every browser context, so test purchases stay out of vendor
|
|
245
|
+
data; loader scripts still load. The verdict row browser-test-order:vendor-beacons
|
|
246
|
+
records the requests per vendor as evidence, never as purchase proof, and names
|
|
247
|
+
the vendors not intercepted (Northbeam; GTM, which sends nothing of its own).
|
|
248
|
+
live is the only value; any other is refused, as is the flag without
|
|
249
|
+
--test-order.
|
|
232
250
|
--allowed-domains-confirmed <bool>
|
|
233
251
|
qa policy set: persist non-localhost SDK-origin confirmation.
|
|
234
252
|
Localhost on any port is a global Development domain with analytics suppressed.
|
|
@@ -249,6 +267,19 @@ Options:
|
|
|
249
267
|
--test-exp-year <yyyy> Test card expiration year. Default: 2030.
|
|
250
268
|
--test-email <email> Customer email for browser test orders. Env CAMPAIGNS_OS_QA_TEST_EMAIL is also recognized.
|
|
251
269
|
--test-email-prefix <prefix> Legacy unique email prefix override.
|
|
270
|
+
--test-first-name <name> Test customer first name. Default: QA.
|
|
271
|
+
--test-last-name <name> Test customer last name. Default: Playwright.
|
|
272
|
+
--test-phone <phone> Test customer phone, filled when the checkout has the field. Default: empty.
|
|
273
|
+
--test-country <code> Shipping (and billing) country, as the checkout's country select option value
|
|
274
|
+
or label (e.g. GB). Default: US. Changing it does not change the run's currency:
|
|
275
|
+
pair it with --currency <code>, and pass the four address flags below for that
|
|
276
|
+
country, since their defaults are a US address. A non-USD --currency on an
|
|
277
|
+
ordering run with no --test-country prints a warning; the run still goes ahead.
|
|
278
|
+
--test-address1 <line> Street address. Default: 1600 Amphitheatre Pkwy.
|
|
279
|
+
--test-city <city> City. Default: Mountain View.
|
|
280
|
+
--test-province <code> State, province or region, as the province select's option value or label.
|
|
281
|
+
Default: CA. The checkout must show a province select for the country.
|
|
282
|
+
--test-postal <code> Postal code. Default: 94043.
|
|
252
283
|
--legacy-api-test-order <off|accept|decline|both>
|
|
253
284
|
Diagnostic-only direct Campaigns API order creation; bypasses deployed checkout.
|
|
254
285
|
--api-key <key> Campaigns API key for legacy direct API diagnostics. Env QA_CAMPAIGNS_API_KEY is also recognized.
|
|
@@ -511,12 +542,25 @@ async function resolveQaInputs(args, {
|
|
|
511
542
|
hiddenEagerMediaGate,
|
|
512
543
|
packet,
|
|
513
544
|
baseUrl: stringArg(args["base-url"]),
|
|
545
|
+
evidenceDebt: checkpointPreflight?.evidenceDebt || null,
|
|
514
546
|
});
|
|
515
547
|
const qaWaivers = resolveQaWaivers({ packetPath, report: checkpointPreflight?.runtimeReport });
|
|
516
|
-
const
|
|
548
|
+
const buildScope = applyQaBuildScope(topologies, {
|
|
517
549
|
packet, report: checkpointPreflight?.runtimeReport,
|
|
518
550
|
targetRepo: checkpointPreflight?.targetRepo, publicRouteSlug,
|
|
519
551
|
});
|
|
552
|
+
const requestedPages = qaPageArgs(args);
|
|
553
|
+
const pageScope = applyQaPageScope(buildScope.topologies, {
|
|
554
|
+
pages: requestedPages,
|
|
555
|
+
baseUrl,
|
|
556
|
+
debt: checkpointPreflight?.evidenceDebt || null,
|
|
557
|
+
report: checkpointPreflight?.runtimeReport,
|
|
558
|
+
// Read against the spec this run judges, as doctor reads it.
|
|
559
|
+
qaInputCurrency: requestedPages
|
|
560
|
+
? deriveInputCurrency({ packet, packetPath, report: checkpointPreflight?.runtimeReport || null, spec: isPlainObject(rawSpec) ? rawSpec : null }).stages.qa
|
|
561
|
+
: undefined,
|
|
562
|
+
});
|
|
563
|
+
const qaScope = { ...buildScope, topologies: pageScope.topologies };
|
|
520
564
|
const brandContract = loadBrandContract(templateFamily);
|
|
521
565
|
const localServeAnalytics = resolveLocalServeAnalytics({
|
|
522
566
|
packet,
|
|
@@ -552,6 +596,7 @@ async function resolveQaInputs(args, {
|
|
|
552
596
|
commerceStructureContract,
|
|
553
597
|
topologies: qaScope.topologies,
|
|
554
598
|
excludedPages: qaScope.excludedPages,
|
|
599
|
+
pageScope: pageScope.scope,
|
|
555
600
|
checkpointGates: checkpointPreflight?.checkpointGates || nonPacketCheckpointGates(),
|
|
556
601
|
// The report the checkpoint gates were evaluated on, and the target repo
|
|
557
602
|
// whose default it may or may not be: what the printed remediation names.
|
|
@@ -624,13 +669,18 @@ function resolvePacketCheckpointPreflight(args, {
|
|
|
624
669
|
waivers: report?.waivers,
|
|
625
670
|
required: true,
|
|
626
671
|
}),
|
|
627
|
-
applyLocalPreviewToCheckpoint(
|
|
628
|
-
evaluateRecordedHiddenEagerMediaCheckpoint({ packet, report }),
|
|
629
|
-
{ packet, report, baseUrl: stringArg(args["base-url"]) },
|
|
630
|
-
),
|
|
631
672
|
];
|
|
632
673
|
const runtimeReport = reportMatchesPacketIdentity(report, packet) ? report : null;
|
|
674
|
+
// The evidence debt doctor and readback assess: a page-only change since
|
|
675
|
+
// Polish last reviewed the campaign makes Polish advisory (#720), and a
|
|
676
|
+
// page-scoped run (--page) pays the QA it owes.
|
|
677
|
+
const evidenceDebt = assessEvidenceDebt({ packet, packetPath, report: runtimeReport });
|
|
678
|
+
checkpointGates.push(applyPageOnlyChangeToCheckpoint(applyLocalPreviewToCheckpoint(
|
|
679
|
+
evaluateRecordedHiddenEagerMediaCheckpoint({ packet, report }),
|
|
680
|
+
{ packet, report, baseUrl: stringArg(args["base-url"]) },
|
|
681
|
+
), evidenceDebt));
|
|
633
682
|
return {
|
|
683
|
+
evidenceDebt,
|
|
634
684
|
packetPath,
|
|
635
685
|
packet,
|
|
636
686
|
specPath,
|
|
@@ -765,8 +815,8 @@ function resolvedFromBlockedCheckpointPreflight(preflight, args) {
|
|
|
765
815
|
// yields "not_applicable" rather than blocking, so browser QA still runs the
|
|
766
816
|
// residue/placeholder/demo gates. Test orders are not attempted (no policy).
|
|
767
817
|
export function resolveQaInputsFromSite(args) {
|
|
768
|
-
const
|
|
769
|
-
if (!
|
|
818
|
+
const inputBaseUrl = normalizeBaseUrl(stringArg(args["base-url"]));
|
|
819
|
+
if (!inputBaseUrl) {
|
|
770
820
|
throw refused("Non-packet site QA requires --base-url <served-campaign-root> so built pages have a fetchable URL.");
|
|
771
821
|
}
|
|
772
822
|
const templateFamily = stringArg(args.family);
|
|
@@ -778,6 +828,10 @@ export function resolveQaInputsFromSite(args) {
|
|
|
778
828
|
if (!scope.ok) {
|
|
779
829
|
throw new Error(scope.error || `Could not resolve a built campaign from ${targetRepo}.`);
|
|
780
830
|
}
|
|
831
|
+
// The built pages live under _site/<slug>/, so compose the slug onto a site
|
|
832
|
+
// origin the way packet QA does; a base URL already ending in the slug is
|
|
833
|
+
// left alone (#683).
|
|
834
|
+
const baseUrl = normalizeQaBaseUrl(inputBaseUrl, scope.slug);
|
|
781
835
|
const brandContract = loadBrandContract(templateFamily);
|
|
782
836
|
if (brandContract.status !== "loaded" || !brandContract.contract) {
|
|
783
837
|
throw new Error(`Non-packet site QA requires a loadable template brand contract for --family "${templateFamily}" (got ${brandContract.status}).`);
|
|
@@ -800,8 +854,8 @@ export function resolveQaInputsFromSite(args) {
|
|
|
800
854
|
polishGate,
|
|
801
855
|
// Non-packet site QA has no Assembly Report, so no recorded waivers.
|
|
802
856
|
qaWaivers: {},
|
|
803
|
-
// Non-packet site QA:
|
|
804
|
-
//
|
|
857
|
+
// Non-packet site QA: the served campaign root is --base-url with the
|
|
858
|
+
// built slug composed on (no spec identity exists to compose from).
|
|
805
859
|
analyticsCaptureTarget: buildAnalyticsCaptureTarget({
|
|
806
860
|
url: ensureUrlTrailingSlash(baseUrl),
|
|
807
861
|
publicRouteSlug: scope.slug || null,
|
|
@@ -963,22 +1017,125 @@ function currentBuiltOutputFingerprint(packetPath) {
|
|
|
963
1017
|
}
|
|
964
1018
|
}
|
|
965
1019
|
|
|
1020
|
+
// qa run --page <page_id> (#720): repeat the flag or separate ids with
|
|
1021
|
+
// commas. Null when the run is not page-scoped.
|
|
1022
|
+
function qaPageArgs(args) {
|
|
1023
|
+
if (args.page === undefined) return null;
|
|
1024
|
+
const values = Array.isArray(args.page) ? args.page : [args.page];
|
|
1025
|
+
const ids = values.flatMap((value) => (typeof value === "string" ? value.split(",") : [""])).map((id) => id.trim());
|
|
1026
|
+
if (!ids.length || ids.some((id) => !id)) {
|
|
1027
|
+
throw refused("--page takes a page id: the id `doctor --ci` or `next` names for a changed page, or the CampaignSpec page id. Repeat --page for more pages.");
|
|
1028
|
+
}
|
|
1029
|
+
return [...new Set(ids)];
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// A page-scoped run checks pages, not purchases: it pays the QA a page-only
|
|
1033
|
+
// change owes, and a change that owes test orders owes a full run.
|
|
1034
|
+
function refuseBadPageScope(args) {
|
|
1035
|
+
if (qaPageArgs(args) === null) return;
|
|
1036
|
+
if (!stringArg(args.packet)) throw refused("--page needs --packet: a page-scoped run pays the QA a recorded build owes, which only a Build Packet's Assembly Report records.");
|
|
1037
|
+
for (const flag of ["test-order", "legacy-api-test-order"]) {
|
|
1038
|
+
if (args[flag] !== undefined && String(args[flag]).toLowerCase() !== "off") {
|
|
1039
|
+
throw refused(`--page cannot be combined with --${flag}: a page-scoped run checks the named pages and places no test orders. Run ${cmd("qa")} run without --page for purchase proof.`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
if (args["vendor-beacons"] !== undefined) {
|
|
1043
|
+
throw refused("--page cannot be combined with --vendor-beacons: a page-scoped run places no test orders, so there are no vendor beacons to route. Drop --vendor-beacons.");
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Narrow the QA topology to the pages --page names, after checking that the
|
|
1048
|
+
// run can pay what QA owes: the output on disk is the recorded build, the
|
|
1049
|
+
// change since the build QA reviewed is page-only, and the named pages cover
|
|
1050
|
+
// every changed page. The run extends a completed QA, and QA's inputs (the
|
|
1051
|
+
// brief's qa_policy and the CampaignSpec) are current: `qaInputCurrency` is
|
|
1052
|
+
// derived.input_currency.stages.qa, and a QA an input change owes, or whose
|
|
1053
|
+
// inputs cannot be confirmed, owes a full run. Re-running a page-scoped
|
|
1054
|
+
// record for the same build is allowed too. Each requested id is a topology
|
|
1055
|
+
// page id or the page id of its route (the id the change lists use).
|
|
1056
|
+
export function applyQaPageScope(topologies, { pages, baseUrl, debt, report, qaInputCurrency = undefined } = {}) {
|
|
1057
|
+
if (!pages) return { topologies, scope: null };
|
|
1058
|
+
const all = topologies.flatMap((topology) => topology.pages || []);
|
|
1059
|
+
const idsOf = (page) => [String(page.page_id), page.url && baseUrl ? routePageIdForUrl(page.url, baseUrl) : null].filter(Boolean);
|
|
1060
|
+
const known = new Set(all.flatMap(idsOf));
|
|
1061
|
+
const unknown = pages.filter((id) => !known.has(id));
|
|
1062
|
+
if (unknown.length) {
|
|
1063
|
+
throw new Error(`qa run --page: ${unknown.map((id) => JSON.stringify(id)).join(", ")} is not a page of this campaign's QA topology. Pages: ${[...new Set(all.map((page) => String(page.page_id)))].join(", ") || "none"}.`);
|
|
1064
|
+
}
|
|
1065
|
+
const selected = new Set(pages);
|
|
1066
|
+
const isSelected = (page) => idsOf(page).some((id) => selected.has(id));
|
|
1067
|
+
const selectedRouteIds = new Set(all.filter(isSelected).flatMap(idsOf));
|
|
1068
|
+
const qa = debt?.qa;
|
|
1069
|
+
const changes = debt?.changes;
|
|
1070
|
+
if (!qa || !changes?.current_build_fingerprint) {
|
|
1071
|
+
throw new Error("qa run --page needs the Assembly Report and the built output under _site/<public_route_slug>/ to read what QA owes; run it without --page.");
|
|
1072
|
+
}
|
|
1073
|
+
if (changes.recorded_build_fingerprint !== changes.current_build_fingerprint) {
|
|
1074
|
+
throw new Error(`qa run --page: the built output is not the recorded build; run ${cmd("record")} build --packet <packet> first, so the page-scoped verdict is bound to the output it checked.`);
|
|
1075
|
+
}
|
|
1076
|
+
const extended = qaExtendedBuildFingerprint(report);
|
|
1077
|
+
const previousScope = report?.stages?.qa?.evidence?.page_scope;
|
|
1078
|
+
let base = qa.change?.since_build_fingerprint || qa.recorded_build_fingerprint;
|
|
1079
|
+
if (qa.status === "current") {
|
|
1080
|
+
const previousPages = Array.isArray(previousScope?.owed_pages) ? previousScope.owed_pages : null;
|
|
1081
|
+
if (!previousPages || !previousPages.every((id) => selectedRouteIds.has(id))) {
|
|
1082
|
+
throw new Error(`qa run --page: QA is already recorded for this build. Run ${cmd("qa")} run without --page to re-run it in full.`);
|
|
1083
|
+
}
|
|
1084
|
+
base = previousScope.base_build_fingerprint || base;
|
|
1085
|
+
} else if (qa.status !== "owed" || qa.scope !== "pages") {
|
|
1086
|
+
const kind = qa.change?.kind === "commerce_affecting" ? "commerce-affecting"
|
|
1087
|
+
: qa.status === "owed" && qa.change?.kind === "page_only"
|
|
1088
|
+
? extended ? "page-only, but QA is required again by an input change" : "page-only, but the QA recorded before it did not complete"
|
|
1089
|
+
: "not classified as page-only";
|
|
1090
|
+
throw new Error(`qa run --page: the change since the build QA reviewed is ${kind}${(qa.change?.reasons || []).length ? ` (${[...new Set(qa.change.reasons.map((reason) => reason.code))].join(", ")})` : ""}, so it owes a full QA run. Run ${cmd("qa")} run without --page.`);
|
|
1091
|
+
} else {
|
|
1092
|
+
const uncovered = qa.owed_pages.filter((page) => !selectedRouteIds.has(page.page_id));
|
|
1093
|
+
if (uncovered.length) {
|
|
1094
|
+
throw new Error(`qa run --page: QA is owed on ${qa.owed_pages.map((page) => page.page_id).join(", ")}; add ${uncovered.map((page) => `--page ${page.page_id}`).join(" ")}.`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (qaInputCurrency === "owed" || qaInputCurrency === "unknown") {
|
|
1098
|
+
throw new Error(`qa run --page: ${qaInputCurrency === "owed" ? "an input change (the brief's qa_policy or the CampaignSpec) owes QA again" : "QA's inputs (the brief and the CampaignSpec) cannot be confirmed current"}, so it owes a full QA run. Run ${cmd("qa")} run without --page.`);
|
|
1099
|
+
}
|
|
1100
|
+
if (!extended) {
|
|
1101
|
+
throw new Error(`qa run --page: the QA recorded before this change did not complete${report?.stages?.qa?.status ? ` (status ${JSON.stringify(String(report.stages.qa.status))})` : ""}, so there is no passed QA to extend and it owes a full QA run. Run ${cmd("qa")} run without --page.`);
|
|
1102
|
+
}
|
|
1103
|
+
const excluded = new Map();
|
|
1104
|
+
const scoped = topologies.map((topology) => {
|
|
1105
|
+
const kept = (topology.pages || []).filter(isSelected);
|
|
1106
|
+
for (const page of topology.pages || []) if (!isSelected(page)) excluded.set(String(page.page_id), page);
|
|
1107
|
+
return kept.length === (topology.pages || []).length ? topology : { ...topology, partial_build_scope: true, pages: kept };
|
|
1108
|
+
}).filter((topology) => (topology.pages || []).length);
|
|
1109
|
+
const owedPages = qa.status === "current" ? previousScope.owed_pages : qa.owed_pages.map((page) => page.page_id);
|
|
1110
|
+
return {
|
|
1111
|
+
topologies: scoped,
|
|
1112
|
+
scope: {
|
|
1113
|
+
pages: [...new Set(all.filter(isSelected).map((page) => String(page.page_id)))],
|
|
1114
|
+
owed_pages: owedPages,
|
|
1115
|
+
base_build_fingerprint: base,
|
|
1116
|
+
build_fingerprint: changes.current_build_fingerprint,
|
|
1117
|
+
excluded: [...excluded.values()].filter((page) => !all.some((other) => isSelected(other) && String(other.page_id) === String(page.page_id))),
|
|
1118
|
+
},
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
|
|
966
1122
|
function resolvePolishGate({
|
|
967
1123
|
packetPath,
|
|
968
1124
|
report: reportOverride = undefined,
|
|
969
1125
|
hiddenEagerMediaGate = undefined,
|
|
970
1126
|
packet = null,
|
|
971
1127
|
baseUrl = null,
|
|
1128
|
+
evidenceDebt = null,
|
|
972
1129
|
}) {
|
|
973
1130
|
const report = reportOverride === undefined
|
|
974
1131
|
? loadRuntimeArtifact(packetPath, "assembly-report.json")
|
|
975
1132
|
: reportOverride;
|
|
976
|
-
const gate = applyLocalPreviewToPolishGate(evaluatePolishGate({
|
|
1133
|
+
const gate = applyPageOnlyChangeToPolishGate(applyLocalPreviewToPolishGate(evaluatePolishGate({
|
|
977
1134
|
report,
|
|
978
1135
|
required: true,
|
|
979
1136
|
hiddenEagerMediaGate,
|
|
980
1137
|
currentOutputFingerprint: currentBuiltOutputFingerprint(packetPath),
|
|
981
|
-
}), { packet, checkpointGate: hiddenEagerMediaGate, baseUrl });
|
|
1138
|
+
}), { packet, checkpointGate: hiddenEagerMediaGate, baseUrl }), { debt: evidenceDebt, checkpointGate: hiddenEagerMediaGate });
|
|
982
1139
|
gate.scope_source = report ? "assembly_report" : "missing_assembly_report";
|
|
983
1140
|
return gate;
|
|
984
1141
|
}
|
|
@@ -1084,6 +1241,19 @@ function polishGateAssertion(gate) {
|
|
|
1084
1241
|
},
|
|
1085
1242
|
});
|
|
1086
1243
|
}
|
|
1244
|
+
if (isPageOnlyCarry(gate)) {
|
|
1245
|
+
// Advisory for a page-only change (#720): reported, not a warning that
|
|
1246
|
+
// would hold the verdict to ready_with_exceptions.
|
|
1247
|
+
return assertion({
|
|
1248
|
+
id: gate.code,
|
|
1249
|
+
family: "polish_gate",
|
|
1250
|
+
page,
|
|
1251
|
+
status: STATUS.SKIPPED,
|
|
1252
|
+
expected: "current structured Polish evidence, or a page-only change since Polish last reviewed the campaign",
|
|
1253
|
+
actual: carriedForwardMessage(gate),
|
|
1254
|
+
evidence: { ...evidence, reason: gate.reason, carried_forward: gate.carried_forward },
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1087
1257
|
if (gate.status === CARRIED_FORWARD) {
|
|
1088
1258
|
return assertion({
|
|
1089
1259
|
id: gate.code,
|
|
@@ -1402,6 +1572,9 @@ function hiddenEagerMediaGateAssertion(gate) {
|
|
|
1402
1572
|
if (summary.status === "waived") {
|
|
1403
1573
|
return assertion({ ...common, status: STATUS.WARN, severity: SEVERITY.WARN, waiver: summary.waiver });
|
|
1404
1574
|
}
|
|
1575
|
+
if (isPageOnlyCarry(summary)) {
|
|
1576
|
+
return assertion({ ...common, status: STATUS.SKIPPED, actual: `${summary.code}: ${carriedForwardMessage(summary)}` });
|
|
1577
|
+
}
|
|
1405
1578
|
if (summary.status === CARRIED_FORWARD) {
|
|
1406
1579
|
return assertion({ ...common, status: STATUS.WARN, severity: SEVERITY.WARN, actual: `${summary.code}: ${carriedForwardMessage(summary)}` });
|
|
1407
1580
|
}
|
|
@@ -1607,7 +1780,7 @@ function serializeThrownValue(error) {
|
|
|
1607
1780
|
// `derived.assembly_report_path` handling — so the printed remediations can
|
|
1608
1781
|
// name it and a default-report campaign's output is unchanged.
|
|
1609
1782
|
function reportPathField(resolved) {
|
|
1610
|
-
const reportPath = explicitReportPath(resolved?.reportPath, resolved?.targetRepo);
|
|
1783
|
+
const reportPath = explicitReportPath(resolved?.reportPath, resolved?.targetRepo, packetRuntimeRelPath(resolved?.packet));
|
|
1611
1784
|
return reportPath ? { report_path: reportPath } : {};
|
|
1612
1785
|
}
|
|
1613
1786
|
|
|
@@ -1902,6 +2075,7 @@ function updateQaPolicy(args) {
|
|
|
1902
2075
|
// #171: packet edits change what doctor would conclude; the retained
|
|
1903
2076
|
// doctor sidecar (if any) now predates them.
|
|
1904
2077
|
markDoctorSidecarStale(targetRepoFor(packetPath, packet), {
|
|
2078
|
+
path: campaignSidecarPaths(targetRepoFor(packetPath, packet), packetRuntimeRelPath(packet)).doctorOutPath,
|
|
1905
2079
|
command: "qa policy set",
|
|
1906
2080
|
reason: staleReason,
|
|
1907
2081
|
});
|
|
@@ -2175,6 +2349,21 @@ function qaRunCurrency(args) {
|
|
|
2175
2349
|
return value.toUpperCase();
|
|
2176
2350
|
}
|
|
2177
2351
|
|
|
2352
|
+
// `qa run --currency <code>` pins the market the pages and orders run in, but
|
|
2353
|
+
// the order runner's shipping address comes from the --test-* flags and
|
|
2354
|
+
// defaults to a US one. A non-USD currency with no --test-country is the
|
|
2355
|
+
// likely mismatch, so it is named on stderr. Only the missing flag is judged:
|
|
2356
|
+
// a currency such as EUR has many markets, so an explicit --test-country is
|
|
2357
|
+
// taken as the operator's choice. The run is never refused.
|
|
2358
|
+
function warnCurrencyWithoutTestCountry(args, write = (message) => process.stderr.write(message)) {
|
|
2359
|
+
const currency = qaRunCurrency(args);
|
|
2360
|
+
if (!currency || currency === "USD") return false;
|
|
2361
|
+
if (String(args["test-order"] || "off").toLowerCase() === "off") return false;
|
|
2362
|
+
if (stringArg(args["test-country"])) return false;
|
|
2363
|
+
write(`[qa:test-order] --currency ${currency} with no --test-country: test orders use the default US shipping address. Pass --test-country, --test-address1, --test-city, --test-province and --test-postal for the ${currency} market, or ignore this if a US address is intended.\n`);
|
|
2364
|
+
return true;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2178
2367
|
async function runParityQa(args) {
|
|
2179
2368
|
// Checked here as well as on the budget itself: the budget is built after a
|
|
2180
2369
|
// browser has launched, and a flag the operator typed wrong should cost them
|
|
@@ -2253,7 +2442,8 @@ async function runQa(args, options = {}) {
|
|
|
2253
2442
|
// Fail-fast before anything resolves or launches. The authoritative check
|
|
2254
2443
|
// lives on the creation budget itself, which every browser path builds.
|
|
2255
2444
|
refuseBadOrderCreationLimit(args);
|
|
2256
|
-
|
|
2445
|
+
refuseBadPageScope(args);
|
|
2446
|
+
warnCurrencyWithoutTestCountry(args);
|
|
2257
2447
|
// Match dispatch: an active browser mode takes precedence over legacy API
|
|
2258
2448
|
// diagnostics. Only the selected legacy path requires a cart and API mode.
|
|
2259
2449
|
const browserMode = String(args["test-order"] || "off").toLowerCase();
|
|
@@ -2261,6 +2451,7 @@ async function runQa(args, options = {}) {
|
|
|
2261
2451
|
if (browserMode === "off" && legacyMode !== "off") {
|
|
2262
2452
|
refusing(() => legacyTestOrderInputs({ ...args, "test-order": legacyMode }));
|
|
2263
2453
|
}
|
|
2454
|
+
vendorBeaconMode(args);
|
|
2264
2455
|
const resolved = await resolveQaInputs(args);
|
|
2265
2456
|
return runResolvedQa(args, resolved, options);
|
|
2266
2457
|
}
|
|
@@ -2353,12 +2544,20 @@ async function runResolvedQa(args, resolvedInputs, { runSessionActive = false, l
|
|
|
2353
2544
|
actual: "out_of_build_scope",
|
|
2354
2545
|
evidence: { reason: "out_of_build_scope" },
|
|
2355
2546
|
})),
|
|
2547
|
+
...(resolved.pageScope?.excluded || []).map(page => assertion({
|
|
2548
|
+
id: `page-scope:${page.page_id}`, family: "funnel-flow", page,
|
|
2549
|
+
status: STATUS.SKIPPED,
|
|
2550
|
+
expected: "A page-scoped run (--page) checks only the named pages",
|
|
2551
|
+
actual: "outside_page_scope",
|
|
2552
|
+
evidence: { reason: "outside_page_scope" },
|
|
2553
|
+
})),
|
|
2356
2554
|
...(polishGate?.owned_checkpoint_only ? [] : [polishGateAssertion(polishGate)]),
|
|
2357
2555
|
themeGateAssertion(gate),
|
|
2358
2556
|
];
|
|
2359
2557
|
const contractAssertion = templateBrandContractAssertion(resolved);
|
|
2360
2558
|
if (contractAssertion) assertions.push(contractAssertion);
|
|
2361
|
-
|
|
2559
|
+
// Under --currency the expected totals are calculated in the run currency.
|
|
2560
|
+
const commercialPlanning = planCommercialParity(specForQaScope(resolved.rawSpec || resolved.spec, [...(resolved.excludedPages || []), ...(resolved.pageScope?.excluded || [])]), { currency });
|
|
2362
2561
|
const sourceLoader = createPageSourceLoader({ authCookie: args["auth-cookie"] });
|
|
2363
2562
|
const commercialIds = new Set(commercialPlanning.pages
|
|
2364
2563
|
.filter((page) => page?.id !== undefined && page?.id !== null)
|
|
@@ -2403,6 +2602,12 @@ async function runResolvedQa(args, resolvedInputs, { runSessionActive = false, l
|
|
|
2403
2602
|
proxyBase: resolved.proxyBase,
|
|
2404
2603
|
});
|
|
2405
2604
|
assertions.push(...liveCampaignRefAssertions({ pages: [...livePages.values()], spec: liveSpec, liveCampaign: liveRead }));
|
|
2605
|
+
// A test-order run answers vendor beacons locally in every browser context
|
|
2606
|
+
// it opens, unless --vendor-beacons live; the ledger rides on args to each
|
|
2607
|
+
// browser leg and becomes one evidence row (qa-vendor-beacons.mjs).
|
|
2608
|
+
const vendorBeaconRun = vendorBeaconMode(args);
|
|
2609
|
+
const vendorBeacons = vendorBeaconRun ? createVendorBeaconLedger({ mode: vendorBeaconRun, analytics: resolved.spec?.analytics }) : null;
|
|
2610
|
+
if (vendorBeacons) args = { ...args, [QA_VENDOR_BEACONS]: vendorBeacons };
|
|
2406
2611
|
const qcResults = [];
|
|
2407
2612
|
if (args.browser === true) {
|
|
2408
2613
|
const browserAssertions = await runBrowserChecks(resolved.topologies, args, {
|
|
@@ -2438,6 +2643,7 @@ async function runResolvedQa(args, resolvedInputs, { runSessionActive = false, l
|
|
|
2438
2643
|
}
|
|
2439
2644
|
|
|
2440
2645
|
const testOrders = await runAnalyticsOrderSequence({ args, resolved, runId, assertions, qcResults });
|
|
2646
|
+
if (vendorBeacons) assertions.push(vendorBeaconAssertion(vendorBeacons));
|
|
2441
2647
|
const remainingAssertionBudget = Math.max(0, QA_VERDICT_ASSERTION_LIMIT - assertions.length);
|
|
2442
2648
|
let commercialResult;
|
|
2443
2649
|
try {
|
|
@@ -2446,6 +2652,7 @@ async function runResolvedQa(args, resolvedInputs, { runSessionActive = false, l
|
|
|
2446
2652
|
captures: [...capturesByPageId.values()],
|
|
2447
2653
|
planning: commercialPlanning,
|
|
2448
2654
|
maxAssertions: remainingAssertionBudget,
|
|
2655
|
+
currency,
|
|
2449
2656
|
});
|
|
2450
2657
|
} catch (error) {
|
|
2451
2658
|
commercialResult = {
|
|
@@ -2660,6 +2867,7 @@ async function finalizeQaRun({ args, resolved, runId, startedAt, assertions, tes
|
|
|
2660
2867
|
// `external:qa_verdict` whenever that is not the packet directory.
|
|
2661
2868
|
const causeSummary = annotateQaAssertionCauses(assertions, {
|
|
2662
2869
|
baseDir: resolved.packetPath ? dirname(resolved.packetPath) : null,
|
|
2870
|
+
runtimeRelPath: resolved.packetPath ? packetRuntimeRelPath(resolved.packet) : undefined,
|
|
2663
2871
|
targetRepo: resolved.packetPath ? targetRepoFor(resolved.packetPath, resolved.packet) : null,
|
|
2664
2872
|
mapId: resolved.mapId,
|
|
2665
2873
|
localSpecId: resolved.localSpecId,
|
|
@@ -2775,6 +2983,9 @@ async function finalizeQaRun({ args, resolved, runId, startedAt, assertions, tes
|
|
|
2775
2983
|
// The QC rows the QA stage records; readers re-derive them from the qc.*
|
|
2776
2984
|
// assertions of the verdict written above.
|
|
2777
2985
|
qc_results: qcResults,
|
|
2986
|
+
// qa run --page: the pages this run checked and the change it pays for,
|
|
2987
|
+
// which the QA stage record keeps (cli.mjs recordQaStageOutcome).
|
|
2988
|
+
...(resolved.pageScope ? { page_scope: resolved.pageScope } : {}),
|
|
2778
2989
|
verdict,
|
|
2779
2990
|
};
|
|
2780
2991
|
}
|
|
@@ -2792,7 +3003,8 @@ function analyticsCaptureScope(resolved) {
|
|
|
2792
3003
|
const rootUrl = rootCaptureUrl(resolved?.analyticsCaptureTarget?.url);
|
|
2793
3004
|
const topologies = topologyList(resolved?.topologies);
|
|
2794
3005
|
const partial = topologies.some((topology) => topology?.partial_build_scope)
|
|
2795
|
-
|| (Array.isArray(resolved?.excludedPages) && resolved.excludedPages.length > 0)
|
|
3006
|
+
|| (Array.isArray(resolved?.excludedPages) && resolved.excludedPages.length > 0)
|
|
3007
|
+
|| Boolean(resolved?.pageScope?.excluded?.length);
|
|
2796
3008
|
const rootInScope = !rootUrl || !partial || topologies.some((topology) =>
|
|
2797
3009
|
(Array.isArray(topology?.pages) ? topology.pages : []).some((page) =>
|
|
2798
3010
|
typeof page?.url === "string" && isSameAnalyticsCapturePage(page.url, rootUrl)));
|
|
@@ -4260,6 +4472,7 @@ export const __qaNodeTestHooks = Object.freeze({
|
|
|
4260
4472
|
templateBrandContractAssertion,
|
|
4261
4473
|
deriveEntryUrls,
|
|
4262
4474
|
qaRunCurrency,
|
|
4475
|
+
warnCurrencyWithoutTestCountry,
|
|
4263
4476
|
withEntryCurrency,
|
|
4264
4477
|
derivePageUrls,
|
|
4265
4478
|
deriveTestedUrlsFromAssertions,
|
package/src/qa-publish.mjs
CHANGED
|
@@ -19,9 +19,9 @@ import { createHash } from "node:crypto";
|
|
|
19
19
|
import { existsSync, readFileSync } from "node:fs";
|
|
20
20
|
import { basename, dirname, join, resolve } from "node:path";
|
|
21
21
|
|
|
22
|
-
import { campaignSidecarPaths, targetRepoFor } from "./campaign-workspace.mjs";
|
|
22
|
+
import { campaignSidecarPaths, packetRuntimeRelPath, targetRepoFor } from "./campaign-workspace.mjs";
|
|
23
23
|
import { refused } from "./lifecycle.mjs";
|
|
24
|
-
import {
|
|
24
|
+
import { sidecarPathForPacket } from "./qa-sidecar.mjs";
|
|
25
25
|
import { qaVerdictIdentityMatch } from "./qa-verdict-discovery.mjs";
|
|
26
26
|
import { publishQaVerdict, qaPortalUrl, qaVerdictPublishBlock, QA_VERDICT_PUBLISH_ENDPOINT, QA_VERDICT_PUBLISHERS } from "./qa-verdict-publish.mjs";
|
|
27
27
|
import { validateVerdict } from "./qa-verdict.mjs";
|
|
@@ -108,9 +108,9 @@ export function resolveStoredVerdictSource({ args, packetPath, packet, readJsonF
|
|
|
108
108
|
if (explicit) {
|
|
109
109
|
const path = resolve(explicit);
|
|
110
110
|
if (!exists(path)) return { error: refusal(QA_PUBLISH_REFUSALS.verdict_missing, `No verdict file at ${path}.`) };
|
|
111
|
-
return { path, source_kind: path === sidecarPath(packetPath) ? "sidecar_projection" : "explicit" };
|
|
111
|
+
return { path, source_kind: path === sidecarPath(packetPath, packet) ? "sidecar_projection" : "explicit" };
|
|
112
112
|
}
|
|
113
|
-
const sidecar = sidecarPath(packetPath);
|
|
113
|
+
const sidecar = sidecarPath(packetPath, packet);
|
|
114
114
|
if (!exists(sidecar)) {
|
|
115
115
|
return {
|
|
116
116
|
error: refusal(
|
|
@@ -139,8 +139,8 @@ export function resolveStoredVerdictSource({ args, packetPath, packet, readJsonF
|
|
|
139
139
|
return { path: sidecar, source_kind: "sidecar_projection", sidecar_path: sidecar };
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
function sidecarPath(packetPath) {
|
|
143
|
-
return
|
|
142
|
+
function sidecarPath(packetPath, packet) {
|
|
143
|
+
return sidecarPathForPacket(packetPath, packet);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/**
|
|
@@ -282,7 +282,8 @@ async function attemptPublish(args, operations, dryRun) {
|
|
|
282
282
|
} catch {
|
|
283
283
|
verdictDigest = null;
|
|
284
284
|
}
|
|
285
|
-
const
|
|
285
|
+
const runtimeRelPath = packetRuntimeRelPath(packet);
|
|
286
|
+
const records = ops.readRecords(baseDir, runtimeRelPath);
|
|
286
287
|
const recordEntry = findRunRecordForVerdict({ records, packet, verdictRunId, verdictDigest });
|
|
287
288
|
const priorBlock = recordEntry?.record?.qa_verdict_publish?.verdict_run_id === verdictRunId ? recordEntry.record.qa_verdict_publish : null;
|
|
288
289
|
const republish = args.republish === true;
|
|
@@ -377,7 +378,7 @@ async function attemptPublish(args, operations, dryRun) {
|
|
|
377
378
|
} else {
|
|
378
379
|
try {
|
|
379
380
|
const record = { ...recordEntry.record, qa_verdict_publish: stamped };
|
|
380
|
-
const path = ops.writeRecord(record, { baseDir });
|
|
381
|
+
const path = ops.writeRecord(record, { baseDir, runtimeRelPath });
|
|
381
382
|
runRecord = { ...recordSummary({ path, record }, stamped), written: true, preserved: false };
|
|
382
383
|
} catch (error) {
|
|
383
384
|
runRecord = { ...recordSummary(recordEntry, priorBlock), written: false, preserved: false, error: singleLineFragment(error.message) };
|
package/src/qa-sidecar.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { localSpecIdentityFields } from "./spec-source-identity.mjs";
|
|
2
2
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
|
|
6
|
+
import { packetRuntimeRelPath } from "./campaign-workspace.mjs";
|
|
6
7
|
import { deriveExceptions, validateVerdict } from "./qa-verdict.mjs";
|
|
7
8
|
|
|
8
9
|
// The committed QA verdict sidecar: `.campaign-runtime/qa-verdict.json` beside
|
|
9
|
-
// the Build Packet
|
|
10
|
+
// the Build Packet (under the packet's own runtime directory when it is one
|
|
11
|
+
// of several funnels in its project), the artifact campaigns-agent's readback projects. Full
|
|
10
12
|
// verdicts under qa-output/ are gitignored because they carry live storefront
|
|
11
13
|
// URLs, request evidence, and order references; the sidecar is committed to
|
|
12
14
|
// merchant repos, so it is an allowlist PROJECTION of one verdict, never a
|
|
@@ -100,8 +102,18 @@ export function projectVerdictForSidecar(verdict, { generatedAt }) {
|
|
|
100
102
|
};
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
// `packet` is read from `packetPath` when not given; an unreadable packet
|
|
106
|
+
// names the project-level location, which the writers refuse before using.
|
|
107
|
+
export function sidecarPathForPacket(packetPath, packet = readPacketOrNull(packetPath)) {
|
|
108
|
+
return join(dirname(resolve(packetPath)), packetRuntimeRelPath(packet), basename(SIDECAR_RELATIVE_PATH));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readPacketOrNull(packetPath) {
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(readFileSync(resolve(packetPath), "utf8"));
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
const PACKET_SCHEMA = "campaign-runtime-build-packet/v0";
|