@nextcommerce/campaigns-os 1.54.0 → 1.57.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 +148 -0
- package/CONTEXT.md +19 -5
- package/README.md +26 -12
- package/campaign-spec/README.md +15 -1
- package/campaign-spec/dist/checkout-flow.d.ts +90 -0
- package/campaign-spec/dist/checkout-flow.js +229 -0
- package/campaign-spec/dist/index.d.ts +1 -0
- package/campaign-spec/dist/index.js +6 -1
- package/campaign-spec/dist/normalize.d.ts +3 -2
- package/campaign-spec/dist/normalize.js +7 -5
- package/campaign-spec/dist/routing.d.ts +6 -3
- package/campaign-spec/dist/routing.js +6 -3
- package/campaign-spec/dist/rules/checkout-forward-target.d.ts +17 -0
- package/campaign-spec/dist/rules/checkout-forward-target.js +48 -0
- package/campaign-spec/dist/rules/checkout-step-reaches-checkout.d.ts +12 -0
- package/campaign-spec/dist/rules/checkout-step-reaches-checkout.js +37 -0
- package/campaign-spec/dist/rules/exit-intent-validation.js +2 -1
- package/campaign-spec/dist/rules/index.d.ts +4 -0
- package/campaign-spec/dist/rules/index.js +12 -0
- package/campaign-spec/dist/rules/one-checkout-per-path.d.ts +25 -0
- package/campaign-spec/dist/rules/one-checkout-per-path.js +84 -0
- package/campaign-spec/dist/rules/pre-payment-forward-target.d.ts +12 -0
- package/campaign-spec/dist/rules/pre-payment-forward-target.js +42 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.js +2 -1
- package/campaign-spec/dist/rules/schema-version.d.ts +9 -4
- package/campaign-spec/dist/rules/schema-version.js +10 -5
- package/campaign-spec/dist/types.d.ts +20 -3
- package/campaign-spec/dist/types.js +3 -2
- package/compatibility.json +2 -2
- package/contracts/fixtures/runtime-recipe/accept/current.json +6 -1
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +6 -1
- package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +1 -1
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +5 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +6 -1
- package/contracts/release-ledger.json +1146 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +6 -1
- package/contracts/supported-surface.json +9 -5
- package/docs/brand-theme-bridge.md +10 -0
- package/docs/build-packet.md +86 -26
- package/docs/campaigns-os-build-flow.md +1 -1
- package/docs/effects.md +2 -5
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/qa-and-test-orders.md +133 -29
- package/docs/runtime-readiness.md +8 -3
- package/docs/skills-revision.md +10 -10
- package/docs/supported-surface.md +1 -1
- package/docs/versioning.md +4 -1
- package/docs/workflow-findings-sidecar.md +57 -2
- package/package.json +4 -1
- package/schemas/campaign-runtime-build-context.v0.schema.json +3 -1
- package/schemas/campaign-runtime-build-packet.v0.schema.json +3 -1
- package/schemas/campaign-spec.v5.schema.json +586 -0
- package/schemas/campaigns-os-run-record.v0.schema.json +20 -15
- package/skills/campaign-lifecycle-orientation/SKILL.md +10 -8
- 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 +6 -4
- 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 +21 -6
- package/skills/next-campaigns-qa/SKILL.md +5 -4
- package/skills.json +10 -10
- package/src/adapter-decision-contract.mjs +27 -13
- package/src/brand-theme.mjs +44 -8
- package/src/build-brief.mjs +14 -5
- package/src/campaign-identity.mjs +33 -4
- package/src/cli-helpers.mjs +129 -0
- package/src/cli.mjs +156 -111
- package/src/commercial-journey.mjs +25 -2
- package/src/consent.mjs +3 -2
- package/src/design-source-package.mjs +1 -1
- package/src/design-source-publication.mjs +8 -8
- package/src/deviation.mjs +2 -1
- package/src/doctor/checks.mjs +212 -19
- package/src/doctor/inspect.mjs +4 -2
- package/src/doctor/next-step.mjs +46 -16
- package/src/doctor-sidecar.mjs +5 -3
- package/src/gate-actions.mjs +21 -1
- package/src/input-refresh.mjs +6 -6
- package/src/invocation.mjs +4 -3
- package/src/lifecycle.mjs +88 -6
- package/src/local-preview-policy.mjs +116 -12
- package/src/login.mjs +2 -1
- package/src/polish-browser.mjs +8 -2
- package/src/polish-node.mjs +9 -4
- package/src/polish-readability.mjs +2 -2
- package/src/qa-browser.mjs +741 -120
- package/src/qa-cart-entry.mjs +107 -20
- package/src/qa-node.mjs +179 -72
- package/src/qa-test-order-topology.mjs +118 -1
- package/src/qa-verdict-discovery.mjs +21 -4
- package/src/qa-verdict-publish.mjs +2 -1
- package/src/run-record.mjs +46 -34
- package/src/runtime-state-ignore.mjs +7 -5
- package/src/sdk-markup.mjs +41 -1
- package/src/source-html-intake.mjs +46 -4
- package/src/stage-ledger.mjs +18 -4
- package/src/stage-record.mjs +53 -16
package/src/cli.mjs
CHANGED
|
@@ -21,9 +21,10 @@ import { homedir } from "node:os";
|
|
|
21
21
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
22
22
|
import { shellToken } from "./shell-token.mjs";
|
|
23
23
|
import { declaredOrderBumps, declaredSelectorTiers } from "./commercial-journey.mjs";
|
|
24
|
+
import { checkoutPathFrom, entryPages, isCheckoutFlowPage, paymentPageForFunnel, upgradeCampaignSpec } from "../campaign-spec/dist/index.js";
|
|
24
25
|
import { diagnosticExport, diagnosticTextLines } from "./diagnostic.mjs";
|
|
25
26
|
import { observeProgress, PROGRESS_OBSERVATION } from "./progress-node.mjs";
|
|
26
|
-
import { HIDDEN_EAGER_MEDIA_ACTIONS, requiredActionText, substitutePacket } from "./gate-actions.mjs";
|
|
27
|
+
import { HIDDEN_EAGER_MEDIA_ACTIONS, qaRunCommand, requiredActionText, substitutePacket } from "./gate-actions.mjs";
|
|
27
28
|
import { orderPathDepthDriftText, orderPathDepthReconcileAction, orderPathDepthsDisagree, parseOrderPathDepthFlag } from "./proof-policy.mjs";
|
|
28
29
|
import { specMaterialHash, specHashesMatch } from "./spec-identity.mjs";
|
|
29
30
|
// The same predicate stage-ledger.mjs judges a mutator's result with, imported
|
|
@@ -96,6 +97,7 @@ import {
|
|
|
96
97
|
refused,
|
|
97
98
|
refusing,
|
|
98
99
|
runWithRefusalScope,
|
|
100
|
+
timeOperatorWait,
|
|
99
101
|
} from "./lifecycle.mjs";
|
|
100
102
|
import { commandNames, optsOutOfRunSession, runInvocation } from "./invocation.mjs";
|
|
101
103
|
import {
|
|
@@ -251,6 +253,7 @@ import {
|
|
|
251
253
|
readStoreProfile,
|
|
252
254
|
} from "./spec-derive-store.mjs";
|
|
253
255
|
import { ROOT, cmd, asInvocation } from "./install-invocation.mjs";
|
|
256
|
+
import { hostedTemplateNeedsPreviewUrl, hostedTemplateNeedsUsablePreview } from "./local-preview-policy.mjs";
|
|
254
257
|
import {
|
|
255
258
|
requireArg,
|
|
256
259
|
isObject,
|
|
@@ -262,6 +265,7 @@ import {
|
|
|
262
265
|
sha256File,
|
|
263
266
|
relFromDir,
|
|
264
267
|
isLocalAbsolutePath,
|
|
268
|
+
portableArtifactPaths,
|
|
265
269
|
resolveFromFile,
|
|
266
270
|
extractFrontmatterValue,
|
|
267
271
|
addIssue,
|
|
@@ -301,6 +305,7 @@ const HELP = `Campaigns OS toolkit
|
|
|
301
305
|
|
|
302
306
|
Usage:
|
|
303
307
|
campaigns-os help
|
|
308
|
+
campaigns-os --version # print the installed package version; -v is equivalent
|
|
304
309
|
campaigns-os demo --target <new-directory> # offline inert sample; open landing/index.html; no campaign evidence
|
|
305
310
|
campaigns-os start (--spec <json> | --map-id <id>) --source <html-dir> --target <page-kit-dir> --template-family <family>
|
|
306
311
|
[--brief <yaml|json>] [--proxy-base <url>] [--cached-spec] [--theme-policy <inspect_only|auto|off>]
|
|
@@ -335,7 +340,7 @@ Usage:
|
|
|
335
340
|
campaigns-os record brief --packet <campaign-runtime.build.json> [--brief <yaml|json>] [--context <json>] [--report <json>] [--dry-run] [--json]
|
|
336
341
|
campaigns-os record spec --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--dry-run] [--json]
|
|
337
342
|
campaigns-os record setup --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--dry-run] [--json] # record setup complete once the campaign output directory exists: Build Context scaffold.required=false and stages.setup completed, validated against their schemas before either is written
|
|
338
|
-
campaigns-os record build --packet <campaign-runtime.build.json> [--build-environment <development|production>] [--context <json>] [--report <json>] [--dry-run] [--json] # after page-kit build (
|
|
343
|
+
campaigns-os record build --packet <campaign-runtime.build.json> [--build-environment <development|production>] [--adapter-decision <key>=<value>[,<key>=<value>...]] [--context <json>] [--report <json>] [--dry-run] [--json] # after page-kit build: records scalar adapter decisions on the Assembly Report (all pairs in one flag; a repeated flag keeps only the last), stamps stages.assembly.build_fingerprint from doctor's derived.build_output_fingerprint.value (and the Design Source Package material fingerprint when present), and makes Polish required unless its evidence is bound to this output; --build-environment records stages.assembly.evidence.build_environment (local proof mode records development)
|
|
339
344
|
campaigns-os record polish --packet <campaign-runtime.build.json> --evidence <polish-evidence.json> [--context <json>] [--report <json>] [--dry-run] [--json] # after polish capture: stages.polish from the file's status (completed, completed_with_warnings, blocked with blockers, or skipped with skip_reason), evidence and optional repair_loop_defect, bound to doctor's current fingerprint; a completed status is refused, writing nothing, unless the polish gate doctor evaluates would pass. --dry-run runs every check and writes nothing
|
|
340
345
|
campaigns-os record theme --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--dry-run] [--json] # after the brand layer is linked and build is recorded: report.theme becomes applied with load_order after-next-core, css_path, commerce_pages and per-page evidence, only when each built commerce page that loads next-core.css loads brand-theme.css (or checkout-brand.css) after it and at least one does; a page loading neither is left out as the design's own markup; refused, writing nothing, otherwise. --dry-run runs every check and writes nothing
|
|
341
346
|
campaigns-os record deploy --packet <campaign-runtime.build.json> --base-url <served url> [--context <json>] [--report <json>] [--dry-run] [--json] # a local preview (deploy.target local-serve) after polish is recorded: GETs every built page under the loopback URL (the campaign route root), then records deploy.preview_url on the packet and stages.deploy completed with the URL in outputs and the build fingerprint it probed (a later record build of other output makes deploy required again); refused, writing nothing, when the URL is not loopback or not the route root, a page does not answer 2xx, the build changed since it was recorded, or the theme gate is blocked. --dry-run runs every check, the requests included, and writes nothing
|
|
@@ -356,7 +361,7 @@ Usage:
|
|
|
356
361
|
campaigns-os next deploy --packet <json> --report <json> [--json]
|
|
357
362
|
campaigns-os next qa --packet <json> --report <json> [--json]
|
|
358
363
|
campaigns-os qa resolve --packet <json> [--base-url <url>] [--no-probe] [--probe-timeout-ms <ms>] [--json] # probes the derived entry URLs; a dead route set reports routes_unresolved, an unprobed one ready_unprobed
|
|
359
|
-
campaigns-os qa run --packet <json> [--base-url <url>] [--browser] [--test-order <mode>] [--select-package <ref[:qty],...>] [--apply-coupon <code>] [--no-post-verdict] [--no-remit] [--no-live-refs] [--output-dir <dir>] [--json]
|
|
364
|
+
campaigns-os qa run --packet <json> [--base-url <url>] [--currency <code>] [--browser] [--test-order <mode>] [--select-package <ref[:qty],...>] [--apply-coupon <code>] [--no-post-verdict] [--no-remit] [--no-live-refs] [--output-dir <dir>] [--json]
|
|
360
365
|
campaigns-os qa promote --packet <json> --verdict <full-verdict.json> [--json] # project one explicit qa-output verdict to the committed .campaign-runtime/qa-verdict.json sidecar
|
|
361
366
|
campaigns-os qa publish --packet <json> [--verdict <full-verdict.json>] [--republish] [--proxy-base <url>] [--dry-run] [--json] # post an already-stored verdict (the sidecar's run, or --verdict) to the QA portal without a re-run or an order; refuses a stale spec_hash or an already-published verdict. --dry-run runs every one of those refusal checks and prints what would be posted (endpoint, verdict run id, payload bytes) without the POST
|
|
362
367
|
campaigns-os qa policy set --packet <json> [--allowed-domains-confirmed true|false] [--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--order-path-depth <off|common|full>] [--json] # --order-path-depth writes qa.proof_policy.order_path_depth and refreshes the assembly report's proof_policy mirror
|
|
@@ -364,7 +369,7 @@ Usage:
|
|
|
364
369
|
campaigns-os findings harvest --packet <json> [--context <json>] [--report <json>] [--journal <path>] [--run-id <id>] [--write] [--json]
|
|
365
370
|
campaigns-os findings list [--packet <json>] [--journal <path>] [--json]
|
|
366
371
|
campaigns-os findings export [--summary | --json] [--packet <json>] [--journal <path>]
|
|
367
|
-
campaigns-os run-record --packet <json> [--context <json>] [--report <json>] [--qa-verdict <path>] [--run-id <id>] [--new-run] [--journal <path>] [--lifecycle-journal <path>] [--surfaces <a,b>] [--primary-surface <s>] [--surface-confidence <text>] [--
|
|
372
|
+
campaigns-os run-record --packet <json> [--context <json>] [--report <json>] [--qa-verdict <path>] [--run-id <id>] [--new-run] [--journal <path>] [--lifecycle-journal <path>] [--surfaces <a,b>] [--primary-surface <s>] [--surface-confidence <text>] [--proxy-base <url>] [--no-remit] [--no-write] [--dry-run] [--list] [--json]
|
|
368
373
|
run_id: --run-id > the active run session > the most recent Run Record for this packet's campaign (re-emitted in place; a remitted one is left as written) > freshly minted. --new-run always mints; --list prints the run ids on disk for this packet (id, created_at, remit state, path) and, like --no-write, writes and sends nothing.
|
|
369
374
|
--dry-run assembles the record and prints it (with --json: dry_run, would_write, would_remit), then writes no file and sends nothing — where --no-write skips the assembly's reads too. Combining them is allowed and still writes nothing. \`run end --dry-run\` hands the flag on to run-record and leaves the run session open, so the close can still be made for real afterwards.
|
|
370
375
|
|
|
@@ -378,7 +383,7 @@ Usage:
|
|
|
378
383
|
campaigns-os run status [--json] # active session + incomplete stages + deviation count + exact next command; read-only — it never sweeps and never journals
|
|
379
384
|
campaigns-os run end [--packet <json>] [--no-remit] [--no-write] [--proxy-base <url>] [--json] # assemble the aggregated Run Record for the session, then clear it (also closes out a stale session at cwd); --proxy-base is handed to run-record, so the session record remits to that receiver under the consent scoped to it
|
|
380
385
|
|
|
381
|
-
Gates: when theme inspect finds a generatable brand theme and the campaign ships commerce pages, \`next polish|deploy|qa\` and \`qa run\` BLOCK until the brand layer is applied after next-core.css or explicitly waived (\`theme waive
|
|
386
|
+
Gates: when theme inspect finds a generatable brand theme and the campaign ships commerce pages, \`next polish|deploy|qa\` and \`qa run\` BLOCK until the brand layer is applied after next-core.css or explicitly waived (recorded: \`theme waive --packet <p> --reason "<why>" --waived-by "<named human>"\`; one-run ephemeral: \`qa run --theme-waive "<reason>"\`).
|
|
382
387
|
Commercial parity: \`qa run\` automatically compares contract-governed authored price/cadence/voucher claims with fresh \`/api/price-preview\` evidence; no extra catalog flag is required.
|
|
383
388
|
Local proof mode: under deploy.target local-serve the build stage renders the DEVELOPMENT environment (\`CPK_ENV=development npx campaign-build --json > .campaign-runtime/page-kit-build-summary.json\`, recorded as stages.assembly.evidence.build_environment) into _site/, and polish capture, browser QA and typed-card orders run against that served output; starter templates gate every vendor loader on the environment, and a production build's protocol-relative loaders (//host/...) fail over a plain-HTTP local serve. \`page-kit parity\` then proves the pin on the production render before commit; the PR preview is the second check. The toolkit never proposes editing a generated include to make a local capture pass.
|
|
384
389
|
Wrapper policy: \`start\`/\`prepare-build\`/\`build\` seed source_html.adapter_contract.wrapper_policy from --wrapper-policy, else the source-html manifest's wrapper_policy key, else strip_document_wrappers. Selecting preserve_document_wrappers reports source_html.prep.document_wrapper as a warning instead of blocking, so raw-HTML source can be handed over without a wrapper-stripping pass (docs/source-adapters.md).
|
|
@@ -475,6 +480,12 @@ function closestCommand(input) {
|
|
|
475
480
|
// (src/qc-test-factories.mjs). The bin entry point never passes it, and no
|
|
476
481
|
// flag, environment variable or file can supply one.
|
|
477
482
|
export async function main(argv, { authentication, qcStandIns } = {}) {
|
|
483
|
+
// Global version flags bypass invocation policy, including session reads and
|
|
484
|
+
// lifecycle capture, just as help bypasses campaign effects.
|
|
485
|
+
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
486
|
+
console.log(packageVersion());
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
478
489
|
return runInvocation(parseArgs(argv), {
|
|
479
490
|
dispatch: (command, args, context) => dispatch(command, args, { ...context, argv, authentication, qcStandIns }),
|
|
480
491
|
closeOutStaleRunSessions,
|
|
@@ -728,10 +739,10 @@ export function recordQaStageOutcome(args, result) {
|
|
|
728
739
|
disposition: verdict.disposition,
|
|
729
740
|
timestamp: verdict.completed_at,
|
|
730
741
|
command: `campaigns-os ${QA_RUN_PRODUCER}`,
|
|
731
|
-
outputs: [result.local_path, result.qa_sidecar?.path].filter(isNonEmptyString),
|
|
742
|
+
outputs: portableArtifactPaths([result.local_path, result.qa_sidecar?.path].filter(isNonEmptyString), workspace.targetRepo),
|
|
732
743
|
blockers: verdict.disposition === "blocked" ? failed : [],
|
|
733
744
|
warnings: verdict.disposition === "ready_with_exceptions"
|
|
734
|
-
? ["QA
|
|
745
|
+
? ["QA passed with explicitly attributed exceptions. Report them to the operator; do not clear or waive them, or change markup just to make them pass."]
|
|
735
746
|
: [],
|
|
736
747
|
// The producer knows its own run id and must restate it, or the stage
|
|
737
748
|
// keeps a previous run's identity beside this run's status and outputs.
|
|
@@ -897,6 +908,7 @@ async function autoEndRunSessionAfterTerminalQa(args, sessionHolder, thrown, imp
|
|
|
897
908
|
|
|
898
909
|
const summary = await closeRunSession(updatedFound, {
|
|
899
910
|
packet,
|
|
911
|
+
closedBy: "qa_auto_end",
|
|
900
912
|
extraArgs,
|
|
901
913
|
silent: true,
|
|
902
914
|
promptForConsent: false,
|
|
@@ -1020,7 +1032,13 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1020
1032
|
args.spec = resolved.specPath;
|
|
1021
1033
|
// `command` rides along for the doctor sidecar's generated_by stamp when
|
|
1022
1034
|
// the mode runs doctor (#312): threaded from here, not re-read from argv.
|
|
1023
|
-
|
|
1035
|
+
// Doctor runs inside prepare-build, so its codes are recorded while that
|
|
1036
|
+
// phase is active and attach to it.
|
|
1037
|
+
const result = await recorder.time("prepare-build", async () => {
|
|
1038
|
+
const prepared = await prepareBuild(args, { ...mode, command, specInput, publishSpec, sourceKind, wrapperPolicyFlag, orderPathDepthFlag });
|
|
1039
|
+
if (prepared.doctor && !prepared.doctor.ok) recorder.recordFindingCodes(prepared.doctor.errors?.map((issue) => issue.code));
|
|
1040
|
+
return prepared;
|
|
1041
|
+
});
|
|
1024
1042
|
result.spec_source = resolved;
|
|
1025
1043
|
autoStartRunSession(result, args, ambient, sessionHolder);
|
|
1026
1044
|
printPrepareResult(result, args);
|
|
@@ -1032,6 +1050,7 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1032
1050
|
// synchronous inspection; see readDoctorLiveCampaign.
|
|
1033
1051
|
const liveCampaign = await readDoctorLiveCampaign(args);
|
|
1034
1052
|
const result = doctorCommand(args, { liveCampaign, qcStandIns });
|
|
1053
|
+
if (!result.ok) recorder.recordFindingCodes(result.errors?.map((issue) => issue.code));
|
|
1035
1054
|
writeResult(result, args, result.ok ? 0 : 2);
|
|
1036
1055
|
printDoctorTinyPrompt(result, args);
|
|
1037
1056
|
return;
|
|
@@ -1225,6 +1244,10 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1225
1244
|
// command a QA run prints has to agree with the run_id this session will
|
|
1226
1245
|
// later close and remit under.
|
|
1227
1246
|
const result = await runQaCli(args, { ambient });
|
|
1247
|
+
if (result?.verdict?.disposition === "blocked") {
|
|
1248
|
+
recorder.recordFindingCodes(result.verdict.assertions?.filter((item) => item.status === "fail" || item.severity === "blocker")
|
|
1249
|
+
.map((item) => item.evidence?.code || item.id));
|
|
1250
|
+
}
|
|
1228
1251
|
// nextStage requires a packet. Guard its optional, swallowed progress
|
|
1229
1252
|
// probe explicitly so it cannot construct a refusal in that try block.
|
|
1230
1253
|
if (args._[1] === "run" && result?.verdict && isNonEmptyString(args.packet) && recordQaStageOutcome(args, result)) {
|
|
@@ -1968,7 +1991,7 @@ function prepareBuildUnderLock({
|
|
|
1968
1991
|
},
|
|
1969
1992
|
qa: {
|
|
1970
1993
|
proof_policy: proofPolicy,
|
|
1971
|
-
test_order_policy_notes: "Test Orders use global test cards that bypass the gateway and create no transactions.
|
|
1994
|
+
test_order_policy_notes: "Test Orders use global test cards that bypass the gateway and create no transactions, but each creates a real store order record. Unless the operator has already said test orders are fine for this campaign, ask once, up front, before placing test orders; then run `qa run --test-order common`: when every actual terminal path in the selected checkout topology fits under the flood cap, common runs them all (effective depth `full`, reason `under_cap`); above the cap it runs checkout, first-offer accept/decline and a deduplicated shortest real receipt path, then adds the shortest path that clicks the decline on each offer or downsell page no planned path declines yet, up to the cap, and names any page left out. The `browser-test-order:upsell-action-coverage` verdict row warns naming each offer page whose decline no executed order clicked. Use `--test-order full` for every actual terminal path in the selected checkout topology; cycles, missing routes, and reachable nonterminals block exhaustive proof before browser launch. Use `--test-order tiers` (or `tiers:common` / `tiers:full`) to drive one strict-selection order per selector tier the CampaignSpec declares on the checkout page, crossed with those path shapes; order-bump rows marked `is_order_bump` or `is_upsell` are add-ons, not tiers, so a three-tier checkout with one bump plans 3 tiers, and `--select-package <ref[:qty],...>` narrows a tiers run to the listed tiers. The default accidental-flood cap is 6, and an overflow names the exact explicit `--max-test-orders` raise and lists the planned paths (up to 40 ids, the remainder counted). That cap bounds planned paths; `--max-order-creations` bounds actual order creations, defaults to the planned path count, and is reserved before each submit. Localhost on any port is a globally allowed Development domain; non-localhost preview/production origins still need SDK origin allowlist confirmation. There is no permission flag; depth controls coverage after the operator has answered.",
|
|
1972
1995
|
},
|
|
1973
1996
|
notes: "Generated by campaigns-os prepare-build. Replace demo refs from CampaignSpec/API before launch.",
|
|
1974
1997
|
};
|
|
@@ -3436,7 +3459,7 @@ export function pageKitSyncCommand(args) {
|
|
|
3436
3459
|
result.written = true;
|
|
3437
3460
|
if (stageIsTerminal(report?.stages?.assembly?.status)) {
|
|
3438
3461
|
addIssue(result.warnings, "page_kit.sync.build_stale", `The Assembly Report records a terminal build (stages.assembly.status ${report.stages.assembly.status}), and the built output was rendered from the entry just rewritten: its store profile links, phone and Campaign Cart loader pin are now stale. Re-run the build stage before polish, deploy or QA; doctor's page-kit gates read ${PAGE_KIT_CAMPAIGNS_REL_PATH}, not _site/.`);
|
|
3439
|
-
result.next = `${cmd("doctor")} --packet ${shellToken(packetPath)}, then rebuild
|
|
3462
|
+
result.next = `${cmd("doctor")} --packet ${shellToken(packetPath)}, then rebuild with page-kit, record it with ${cmd("record")} build --packet ${shellToken(packetPath)} and run ${cmd("next")} --packet ${shellToken(packetPath)}`;
|
|
3440
3463
|
}
|
|
3441
3464
|
// The retained doctor snapshot (if any) now predates the entry it judged.
|
|
3442
3465
|
// The data file is already written, so a failure here is a warning on the
|
|
@@ -4204,11 +4227,11 @@ const RUN_RECORD_QA_DIGEST_LIMIT = 8;
|
|
|
4204
4227
|
// Digests of the verdicts the report records, best-effort: an unreadable
|
|
4205
4228
|
// hint contributes no digest, which fails open to "outdated" rather than to
|
|
4206
4229
|
// a false match.
|
|
4207
|
-
function currentQaVerdictDigestsForReport(report, reportPath) {
|
|
4230
|
+
function currentQaVerdictDigestsForReport(report, reportPath, targetRepo) {
|
|
4208
4231
|
const digests = new Set();
|
|
4209
4232
|
// Lazy on purpose, and the check follows the add: once the limit is
|
|
4210
4233
|
// reached the next candidate is never pulled, read or hashed.
|
|
4211
|
-
for (const candidate of iterateQaVerdicts({ report, reportPath, withDigest: true })) {
|
|
4234
|
+
for (const candidate of iterateQaVerdicts({ report, reportPath, roots: [targetRepo], withDigest: true })) {
|
|
4212
4235
|
if (candidate.source === "assembly_report" && candidate.sha256) digests.add(candidate.sha256);
|
|
4213
4236
|
if (digests.size >= RUN_RECORD_QA_DIGEST_LIMIT) break;
|
|
4214
4237
|
}
|
|
@@ -4289,7 +4312,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4289
4312
|
// Atomic like the assembly report: a torn sidecar would be a corrupted
|
|
4290
4313
|
// freshness artifact — the exact green-lie shape this refresh exists to
|
|
4291
4314
|
// prevent (Kilo review, PR #176). Stamped generated_by: "next" (#312).
|
|
4292
|
-
writeDoctorSidecar(doctorOutPath, doctor, { command: NEXT_PRODUCER });
|
|
4315
|
+
writeDoctorSidecar(doctorOutPath, doctor, { command: NEXT_PRODUCER, targetRepo });
|
|
4293
4316
|
} catch {
|
|
4294
4317
|
// sidecar refresh is best-effort; orchestration must not fail on it
|
|
4295
4318
|
}
|
|
@@ -4305,7 +4328,10 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4305
4328
|
// participate in divergence detection or override prepare-only recovery.
|
|
4306
4329
|
const divergences = prepareBuildGate?.binding_failure
|
|
4307
4330
|
? []
|
|
4308
|
-
: detectLedgerDivergence(report, packet, targetRepo
|
|
4331
|
+
: detectLedgerDivergence(report, packet, targetRepo, {
|
|
4332
|
+
allowUnrecordedHostedPreview: polishCheckpointGate?.carried_forward?.policy === "hosted_template_preview"
|
|
4333
|
+
|| hostedTemplateNeedsPreviewUrl(packet, polishCheckpointGate),
|
|
4334
|
+
});
|
|
4309
4335
|
// Every return path runs through this finalizer so the machine-readable
|
|
4310
4336
|
// contract is uniform: `gates` (pass/blocked/waived/not_applicable per
|
|
4311
4337
|
// gate) and `next_actions` (exact commands — not prose) are always present,
|
|
@@ -4329,7 +4355,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4329
4355
|
records: readRunRecordsForTarget(dirname(packetPath)),
|
|
4330
4356
|
packet,
|
|
4331
4357
|
report,
|
|
4332
|
-
currentQaVerdictDigests: currentQaVerdictDigestsForReport(report, report ? reportPath : null),
|
|
4358
|
+
currentQaVerdictDigests: currentQaVerdictDigestsForReport(report, report ? reportPath : null, targetRepo),
|
|
4333
4359
|
qaVerdictRecorded: qaVerdictPathHints(report).length > 0,
|
|
4334
4360
|
});
|
|
4335
4361
|
} catch {
|
|
@@ -4439,7 +4465,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4439
4465
|
errors,
|
|
4440
4466
|
warnings,
|
|
4441
4467
|
ready,
|
|
4442
|
-
prompt:
|
|
4468
|
+
prompt: `${picked.outcome === "hosted_preview_satisfies_deploy" ? picked.reason : "Pipeline complete. All stages in the assembly report are in a terminal status."} To repeat build work, do the work and run \`${cmd("record")} build --packet <path>\`; this makes downstream stages owed as needed. Run \`${cmd("qa")} run\` for QA.${picked.outcome === "hosted_preview_satisfies_deploy" ? "" : " For a hosted deploy, record the URL and stage outcome as the deploy prompt describes."} Then call \`${cmd("next")}\` again. If a run session is active, finish it with \`${cmd("run")} end\` so the Run Record is assembled and the session closes.${report?.stages?.qa?.status === "completed_with_warnings" ? " QA passed with exceptions; report them to the operator without clearing or waiving them or changing markup just to make them pass." : ""}`,
|
|
4443
4469
|
});
|
|
4444
4470
|
}
|
|
4445
4471
|
stage = picked.stage;
|
|
@@ -4462,7 +4488,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4462
4488
|
if (!assemblyStatus.startsWith("completed")) addIssue(errors, "next.polish.assembly", `Assembly status is "${assemblyStatus || "missing"}"; polish expects completed assembly or an explicit blocked/skipped handoff.`);
|
|
4463
4489
|
if (polishGateRequiresBuild(polishGate)) addPolishGateErrors(errors, polishGate, "polish");
|
|
4464
4490
|
addThemeGateErrors(errors, themeGate, "polish");
|
|
4465
|
-
prompt = polishPrompt(packetPath, reportPath, packet, intentSummary);
|
|
4491
|
+
prompt = polishPrompt(packetPath, reportPath, packet, intentSummary, polishCheckpointGate);
|
|
4466
4492
|
} else if (stage === "deploy") {
|
|
4467
4493
|
addPrepareBuildGateErrors(errors, report);
|
|
4468
4494
|
// Slice 3 Phase 2: deploy is an out-of-band step (Netlify / CF Pages /
|
|
@@ -4472,7 +4498,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4472
4498
|
addPolishGateErrors(errors, polishGate, "deploy");
|
|
4473
4499
|
addPolishCheckpointGateErrors(errors, polishCheckpointGate, "deploy");
|
|
4474
4500
|
addThemeGateErrors(errors, themeGate, "deploy");
|
|
4475
|
-
prompt = deployPrompt(packetPath, reportPath, packet);
|
|
4501
|
+
prompt = deployPrompt(packetPath, reportPath, packet, polishGate, polishCheckpointGate);
|
|
4476
4502
|
} else if (stage === "qa") {
|
|
4477
4503
|
addPrepareBuildGateErrors(errors, report);
|
|
4478
4504
|
if (!report) addIssue(errors, "next.qa.report", "Assembly report is required before QA.");
|
|
@@ -4481,7 +4507,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4481
4507
|
addPolishGateErrors(errors, polishGate, "qa");
|
|
4482
4508
|
addPolishCheckpointGateErrors(errors, polishCheckpointGate, "qa");
|
|
4483
4509
|
addThemeGateErrors(errors, themeGate, "qa");
|
|
4484
|
-
prompt = qaPrompt(packetPath, reportPath, packet, spec, intentSummary);
|
|
4510
|
+
prompt = qaPrompt(packetPath, reportPath, packet, spec, intentSummary, polishCheckpointGate);
|
|
4485
4511
|
}
|
|
4486
4512
|
const status = errors.length
|
|
4487
4513
|
? "blocked"
|
|
@@ -4700,7 +4726,7 @@ function themeStarterPaletteAdvisory(themeGate, packetPath, residueState) {
|
|
|
4700
4726
|
+ "runs the template-residue checks at blocker severity, so `qa run` will block on "
|
|
4701
4727
|
+ "template-residue:<page>:style:* rows for the starter call-to-action colour. Decide before QA, "
|
|
4702
4728
|
+ "not after a blocked verdict. Either record an explicit operator waiver — "
|
|
4703
|
-
+ `\`${cmd("theme")} waive --packet ${packetArg} --reason "<why the starter palette is acceptable>"\` `
|
|
4729
|
+
+ `\`${cmd("theme")} waive --packet ${packetArg} --reason "<why the starter palette is acceptable>" --waived-by "<named human>"\` `
|
|
4704
4730
|
+ "— which downgrades those rows to warn severity and keeps the shipped palette visible in the "
|
|
4705
4731
|
+ "verdict; or hand-author the brand layer (write brand-theme.css, list it after next-core.css in "
|
|
4706
4732
|
+ "commerce-page frontmatter styles, rebuild, then run "
|
|
@@ -4730,18 +4756,18 @@ function quoteDivergences(divergences) {
|
|
|
4730
4756
|
.join(" ");
|
|
4731
4757
|
}
|
|
4732
4758
|
|
|
4733
|
-
function divergenceInspectAction(divergences, packetPath) {
|
|
4759
|
+
function divergenceInspectAction(divergences, packetPath, { packet = null, polishCheckpointGate = null } = {}) {
|
|
4734
4760
|
const divergedStages = divergences.map((divergence) => divergence.stage);
|
|
4735
4761
|
const forwardHint = divergedStages.includes("qa")
|
|
4736
4762
|
? "The artifacts include a QA verdict for this campaign, so the campaign may already be built, deployed, and QA'd — verify the artifacts before redoing any stage."
|
|
4737
4763
|
: divergedStages.includes("deploy")
|
|
4738
|
-
? `If the recorded deploy URL is real and current, the forward path is QA (${cmd("next")} qa --packet ${packetPath}), not re-running earlier stages.`
|
|
4764
|
+
? `If the recorded deploy URL is real and current, the forward path is QA (${cmd("next")} qa --packet ${shellToken(packetPath)}), not re-running earlier stages.`
|
|
4739
4765
|
: "If the built output is real and current, the forward path is polish/deploy/QA, not re-running setup or build.";
|
|
4740
4766
|
return {
|
|
4741
4767
|
id: "divergence_inspect",
|
|
4742
4768
|
kind: "manual",
|
|
4743
4769
|
command: null,
|
|
4744
|
-
description: `Ledger and artifacts disagree — ${divergences.length} divergence(s): ${quoteDivergences(divergences)} The same entries are the divergences[] field of \`${cmd("next")} --json\` output; they are not written to any file. This is the ONLY next action: stage actions are suppressed while the disagreement stands, because every one of them would be derived from the same contradictory evidence. Inspect both sides and decide which is right; update the assembly report only after inspection. Do not rerun start/prepare-build or redo completed-looking work on the strength of the ledger alone, and do not treat artifact presence as proof a stage is complete. ${forwardHint} Re-run \`${cmd("next")} --packet ${packetPath} --json\` once the report matches the artifacts to get the normal action list.`,
|
|
4770
|
+
description: `Ledger and artifacts disagree — ${divergences.length} divergence(s): ${quoteDivergences(divergences)} The same entries are the divergences[] field of \`${cmd("next")} --json\` output; they are not written to any file. This is the ONLY next action: stage actions are suppressed while the disagreement stands, because every one of them would be derived from the same contradictory evidence. Inspect both sides and decide which is right; update the assembly report only after inspection. Do not rerun start/prepare-build or redo completed-looking work on the strength of the ledger alone, and do not treat artifact presence as proof a stage is complete. ${forwardHint}${hostedTemplateNeedsUsablePreview(packet, polishCheckpointGate) ? ` After reconciliation, set a hosted preview URL that differs from the production URL with \`${cmd("qa")} policy set --packet ${shellToken(packetPath)} --preview-url <url>\`.` : ""} Re-run \`${cmd("next")} --packet ${shellToken(packetPath)} --json\` once the report matches the artifacts to get the normal action list.`,
|
|
4745
4771
|
required: true,
|
|
4746
4772
|
};
|
|
4747
4773
|
}
|
|
@@ -4772,7 +4798,7 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4772
4798
|
// of the stale ledger. Suppressing branch-by-branch would leave the next
|
|
4773
4799
|
// branch someone adds unguarded; returning here cannot rot that way.
|
|
4774
4800
|
// The operator reconciles, then re-runs `next` for the normal list.
|
|
4775
|
-
const inspect = divergenceInspectAction(divergences, packetPath);
|
|
4801
|
+
const inspect = divergenceInspectAction(divergences, packetPath, { packet, polishCheckpointGate });
|
|
4776
4802
|
push(inspect.id, inspect.kind, asInvocation(inspect.command), inspect.description, { required: inspect.required });
|
|
4777
4803
|
return actions;
|
|
4778
4804
|
}
|
|
@@ -4798,7 +4824,7 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4798
4824
|
);
|
|
4799
4825
|
}
|
|
4800
4826
|
}
|
|
4801
|
-
push("doctor_recheck", "command", `${cmd("doctor")} --packet ${packetPath} --write --json`, "Record a fresh doctor result after resolving the listed errors.");
|
|
4827
|
+
push("doctor_recheck", "command", `${cmd("doctor")} --packet ${shellToken(packetPath)} --write --json`, "Record a fresh doctor result after resolving the listed errors.");
|
|
4802
4828
|
return actions;
|
|
4803
4829
|
}
|
|
4804
4830
|
if (result.stage === "prepare-build") {
|
|
@@ -4832,16 +4858,30 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4832
4858
|
for (const action of themeGate.required_actions) {
|
|
4833
4859
|
push(`theme_gate.${action.id}`, action.kind, asInvocation(action.command), action.description);
|
|
4834
4860
|
}
|
|
4835
|
-
push("recheck", "command", `${cmd("next")} --packet ${packetPath} --json`, "Re-run next after resolving the theme gate to advance.");
|
|
4861
|
+
push("recheck", "command", `${cmd("next")} --packet ${shellToken(packetPath)} --json`, "Re-run next after resolving the theme gate to advance.");
|
|
4836
4862
|
return actions;
|
|
4837
4863
|
}
|
|
4838
4864
|
if (polishGateRequiresBuild(polishGate) && ["build", "polish", "deploy", "qa"].includes(result.stage)) {
|
|
4839
4865
|
for (const action of polishGate?.required_actions || []) {
|
|
4840
4866
|
push(`polish_gate.${action.id}`, action.kind, asInvocation(action.command), action.description);
|
|
4841
4867
|
}
|
|
4842
|
-
push("recheck", "command", `${cmd("next")} --packet ${packetPath} --json`, "Re-run next after rebuilding against the current Design Source Package.");
|
|
4868
|
+
push("recheck", "command", `${cmd("next")} --packet ${shellToken(packetPath)} --json`, "Re-run next after rebuilding against the current Design Source Package.");
|
|
4843
4869
|
return actions;
|
|
4844
4870
|
}
|
|
4871
|
+
const hostedTemplate = packet?.deploy?.target && packet.deploy.target !== LOCAL_SERVE_DEPLOY_TARGET
|
|
4872
|
+
&& polishCheckpointGate?.code === "polish.hidden_eager_media.no_capturable_routes";
|
|
4873
|
+
if (hostedTemplate && ["polish", "deploy", "qa"].includes(result.stage)) {
|
|
4874
|
+
if (hostedTemplateNeedsUsablePreview(packet, polishCheckpointGate)) {
|
|
4875
|
+
pushPolishCheckpointActions();
|
|
4876
|
+
push("recheck", "command", `${cmd("next")} --packet ${shellToken(packetPath)} --json`, "Re-run next after recording a hosted preview URL that differs from the production URL.");
|
|
4877
|
+
return actions;
|
|
4878
|
+
}
|
|
4879
|
+
if (result.stage === "polish" && polishGate?.carried_forward?.policy === "hosted_template_preview") {
|
|
4880
|
+
push("next_qa", "command", `${cmd("next")} qa --packet ${shellToken(packetPath)}`, "Inspect QA for the hosted preview; missing Polish evidence remains a warning.");
|
|
4881
|
+
push("qa_run", "command", qaRunCommand(packetPath, packet.deploy.preview_url), "Run browser and typed-card QA against the hosted preview URL.");
|
|
4882
|
+
return actions;
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4845
4885
|
if ((polishGate?.status === "blocked" || polishCheckpointGate?.status === "blocked")
|
|
4846
4886
|
&& ["deploy", "qa"].includes(result.stage)) {
|
|
4847
4887
|
const checkpointActionIds = new Set(
|
|
@@ -4854,7 +4894,7 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4854
4894
|
push(`polish_gate.${action.id}`, action.kind, asInvocation(action.command), action.description);
|
|
4855
4895
|
}
|
|
4856
4896
|
pushPolishCheckpointActions();
|
|
4857
|
-
push("recheck", "command", `${cmd("next")} --packet ${packetPath} --json`, "Re-run next after recording valid Polish evidence.");
|
|
4897
|
+
push("recheck", "command", `${cmd("next")} --packet ${shellToken(packetPath)} --json`, "Re-run next after recording valid Polish evidence.");
|
|
4858
4898
|
return actions;
|
|
4859
4899
|
}
|
|
4860
4900
|
// Ahead of every stage that still has QA in front of it, say what QA will do
|
|
@@ -4875,9 +4915,9 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4875
4915
|
}
|
|
4876
4916
|
}
|
|
4877
4917
|
if (result.stage === "setup") {
|
|
4878
|
-
push("setup_skill", "skill", "next-campaigns-os-setup", `Prepare the target page-kit structure and agent context, then record setup with ${cmd("record")} setup --packet ${packetPath}.`);
|
|
4918
|
+
push("setup_skill", "skill", "next-campaigns-os-setup", `Prepare the target page-kit structure and agent context, then record setup with ${cmd("record")} setup --packet ${shellToken(packetPath)}.`);
|
|
4879
4919
|
} else if (result.stage === "build") {
|
|
4880
|
-
const recordBuild = `${cmd("record")} build --packet ${packetPath}${isLocalServePacket(packet) ? ` --build-environment ${LOCAL_PROOF_BUILD_ENVIRONMENT}` : ""}`;
|
|
4920
|
+
const recordBuild = `${cmd("record")} build --packet ${shellToken(packetPath)}${isLocalServePacket(packet) ? ` --build-environment ${LOCAL_PROOF_BUILD_ENVIRONMENT}` : ""}`;
|
|
4881
4921
|
push("build_skill", "skill", "next-campaigns-build", `Assemble the campaign per the build prompt, run the page-kit build, then record build with ${recordBuild}.`);
|
|
4882
4922
|
if (isLocalServePacket(packet)) {
|
|
4883
4923
|
push("build_local_proof", "command", LOCAL_PROOF_BUILD_COMMAND, `Local proof mode (deploy.target is local-serve): build page-kit in the ${LOCAL_PROOF_BUILD_ENVIRONMENT} environment into _site/, then record it with ${recordBuild}, which sets ${LOCAL_PROOF_BUILD_ENVIRONMENT_FIELD} to "${LOCAL_PROOF_BUILD_ENVIRONMENT}" (never hand-edit it). Vendor loaders are environment-gated out of this render (their protocol-relative //host/... URLs fail over a plain-HTTP local serve); SDK dl_* events still fire. ${LOCAL_PROOF_NEVER_EDIT_RULE}`);
|
|
@@ -4889,16 +4929,25 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4889
4929
|
}
|
|
4890
4930
|
}
|
|
4891
4931
|
} else if (result.stage === "polish") {
|
|
4892
|
-
push("polish_skill", "skill", "next-campaigns-polish",
|
|
4932
|
+
push("polish_skill", "skill", "next-campaigns-polish", polishCheckpointGate?.code === "polish.hidden_eager_media.no_capturable_routes"
|
|
4933
|
+
? "Every mapped page is template stock, so there is no design route to capture. Follow the preview guidance for missing Polish evidence."
|
|
4934
|
+
: `Run the visual polish pass and ${cmd("polish")} capture, then record polish with ${cmd("record")} polish --packet ${shellToken(packetPath)} --evidence <polish-evidence.json>.`);
|
|
4893
4935
|
if (polishCheckpointGate?.status === "blocked") pushPolishCheckpointActions();
|
|
4894
4936
|
} else if (result.stage === "deploy") {
|
|
4895
|
-
if (packet.deploy?.
|
|
4937
|
+
if (polishGate?.carried_forward?.policy === "hosted_template_preview" && packet.deploy?.preview_url) {
|
|
4938
|
+
push("next_qa", "command", `${cmd("next")} qa --packet ${shellToken(packetPath)}`, "Inspect the QA stage for this hosted preview without recording a hosted deploy.");
|
|
4939
|
+
push("qa_run", "command", qaRunCommand(packetPath, packet.deploy.preview_url), "Run browser and typed-card QA against the hosted preview; its missing Polish evidence remains a warning.");
|
|
4940
|
+
} else if (packet.deploy?.target === LOCAL_SERVE_DEPLOY_TARGET) {
|
|
4896
4941
|
const plan = localServePlan(packet);
|
|
4897
|
-
push("deploy", "manual", null, `Serve the built ${plan.dir} output locally as the origin root (deploy.target is local-serve)${plan.rewrite ? ` — ${plan.rewrite}` : ""}, then run ${cmd("record")} deploy --packet ${packetPath} --base-url <served url>: it checks every built page answers and records deploy.preview_url and stages.deploy. Localhost on any port is a Development domain: SDK allowed, analytics suppressed.`);
|
|
4942
|
+
push("deploy", "manual", null, `Serve the built ${plan.dir} output locally as the origin root (deploy.target is local-serve)${plan.rewrite ? ` — ${plan.rewrite}` : ""}, then run ${cmd("record")} deploy --packet ${shellToken(packetPath)} --base-url <served url>: it checks every built page answers and records deploy.preview_url and stages.deploy. Localhost on any port is a Development domain: SDK allowed, analytics suppressed.`);
|
|
4898
4943
|
} else {
|
|
4899
4944
|
push("deploy", "manual", null, `Deploy _site/ output to ${packet.deploy?.target || "the deploy target"}, then record deploy.preview_url (or production_url) on the packet and stages.deploy in the assembly report.`);
|
|
4900
4945
|
}
|
|
4901
|
-
|
|
4946
|
+
if (polishGate?.carried_forward?.policy === "hosted_template_preview" && packet.deploy?.preview_url) {
|
|
4947
|
+
push("advance", "command", `${cmd("next")} qa --packet ${shellToken(packetPath)}`, "Inspect QA for the hosted preview without a deploy record.");
|
|
4948
|
+
} else {
|
|
4949
|
+
push("advance", "command", `${cmd("next")} --packet ${shellToken(packetPath)} --json`, "Advance to QA once the deploy URL is recorded.");
|
|
4950
|
+
}
|
|
4902
4951
|
} else if (result.stage === "qa") {
|
|
4903
4952
|
const url = packet.deploy?.preview_url || packet.deploy?.production_url || "<preview-url>";
|
|
4904
4953
|
push("install_browser", "command", `${cmd("qa")} install-browser`, "Install the Playwright browser once after install/update (npm run qa:install-browser from a checkout).");
|
|
@@ -5059,9 +5108,10 @@ Rules:
|
|
|
5059
5108
|
- For one-time prepurchase/order-bump packages outside the main bundles, default package_sync=false and show_line_total_price=false unless the spec explicitly requires quantity sync.
|
|
5060
5109
|
- Record spec-driven removals, especially unsupported payment methods, so polish does not reintroduce them.
|
|
5061
5110
|
- Replace demo refs; do not copy Olympus-style shipping_methods into shop-three-step.
|
|
5062
|
-
-
|
|
5111
|
+
- Multi-step checkout paths run select -> checkout_step... -> checkout; only the checkout page takes payment. On a select page the swap-mode bundle selector (data-next-bundle-selector with data-next-selection-mode="swap") writes the cart and the SDK persists it between pages; a checkout button or link (href from the page's next_url) goes to the next page. Use forcePackageId only on a page with no selector, such as a landing call-to-action into a checkout that renders none.
|
|
5112
|
+
- Each checkout_step page's checkout form (<form data-next-checkout="form">) carries data-next-checkout-step="<its next_url>" and data-next-step-number="<step_number>" (Build Context page_kit frontmatter step_number); the checkout page's form carries neither. On every page that feeds a Checkout (select, checkout_step, or a landing page with express checkout buttons), next-success-url is that Checkout's post-payment destination (frontmatter success_url), not the next step, because the SDK reads it for express orders; forward navigation uses the step form attribute or a plain link, never that meta tag.
|
|
5063
5113
|
- After page-kit build, inspect rendered _site output before handoff: each active page should have a body, Campaign Cart runtime markers, SDK meta tags from CampaignSpec sdk_hints.meta_tags, and no stale copied funnel attribution.
|
|
5064
|
-
- Run page-kit build and SDK/template lint, then record build before polish: \`${cmd("record")} build --packet ${packetPath}${isLocalServePacket(packet) ? ` --build-environment ${LOCAL_PROOF_BUILD_ENVIRONMENT}` : ""}\`. It stamps stages.assembly.build_fingerprint with the fingerprint doctor computes from the built output (derived.build_output_fingerprint.value, sha256 over the sorted path+sha256 manifest of _site/<slug>/; doctor reports built_output.fingerprint_stale whenever the output on disk stops matching the recorded value), records report.design_source_package.material_fingerprint on stages.assembly.source_package_material_fingerprint when present, and sets stages.polish to "required" (required_by="build", required_for=["qa"]). Re-run it after every rebuild; never hand-edit these fields. Build must not mark stages.polish as completed/completed_with_warnings/skipped. If you applied a brand theme, run \`${cmd("record")} theme --packet ${packetPath}\` after record build: it reads each built commerce page's stylesheet links and records report.theme (status applied, load_order=after-next-core, css_path, commerce_pages, evidence), and refuses, writing nothing, when a page that loads next-core.css does not load the brand layer after it. Never hand-edit report.theme.
|
|
5114
|
+
- Run page-kit build and SDK/template lint, then record build before polish: \`${cmd("record")} build --packet ${packetPath}${isLocalServePacket(packet) ? ` --build-environment ${LOCAL_PROOF_BUILD_ENVIRONMENT}` : ""} --adapter-decision <key>=<value>[,<key>=<value>...]\`. Record the value that is true for this build: for example, raw_html_conversion_status=completed once source HTML is converted, or not_required when there is none. Put all scalar adapter decisions in one comma-separated flag; a repeated flag keeps only the last. Use the allowed values in docs/build-packet.md. Record the nine recordable scalar decisions with the command, never by editing those fields in the Assembly Report. wrapper_policy is selected at intake with prepare-build --wrapper-policy or the source-html manifest's wrapper_policy option; the object-valued template_files_copied proof has no flag. It stamps stages.assembly.build_fingerprint with the fingerprint doctor computes from the built output (derived.build_output_fingerprint.value, sha256 over the sorted path+sha256 manifest of _site/<slug>/; doctor reports built_output.fingerprint_stale whenever the output on disk stops matching the recorded value), records report.design_source_package.material_fingerprint on stages.assembly.source_package_material_fingerprint when present, and sets stages.polish to "required" (required_by="build", required_for=["qa"]). Re-run it after every rebuild; never hand-edit these fields. Build must not mark stages.polish as completed/completed_with_warnings/skipped. If you applied a brand theme, run \`${cmd("record")} theme --packet ${packetPath}\` after record build: it reads each built commerce page's stylesheet links and records report.theme (status applied, load_order=after-next-core, css_path, commerce_pages, evidence), and refuses, writing nothing, when a page that loads next-core.css does not load the brand layer after it. Never hand-edit report.theme.
|
|
5065
5115
|
- Capture the machine-readable build summary as an artifact: \`${PAGE_KIT_BUILD_SUMMARY_CAPTURE_COMMAND}\` (requires next-campaign-page-kit >= 0.1.4). Doctor verifies it for per-page build errors and Page Kit shape warnings (NESTED_NO_PERMALINK, DUPLICATE_OUTPUT, MISSING_FRONTMATTER, LAYOUT_NOT_FOUND). If the installed page-kit predates --json, record that in the assembly report instead of skipping silently.${localProofPromptLines(packet, packetPath)}`;
|
|
5066
5116
|
}
|
|
5067
5117
|
|
|
@@ -5130,8 +5180,22 @@ When copying a starter template family, copy the family as an atomic page-kit sl
|
|
|
5130
5180
|
Do not wire checkout, upsell, receipt, payment, package, voucher, or shipping behavior during setup.`;
|
|
5131
5181
|
}
|
|
5132
5182
|
|
|
5133
|
-
function polishPrompt(packetPath, reportPath, packet, intent) {
|
|
5183
|
+
function polishPrompt(packetPath, reportPath, packet, intent, polishCheckpointGate = null) {
|
|
5134
5184
|
const briefPath = packet.build_brief?.normalized_path || "(missing)";
|
|
5185
|
+
if (polishCheckpointGate?.code === "polish.hidden_eager_media.no_capturable_routes") {
|
|
5186
|
+
if (packet.deploy?.target === LOCAL_SERVE_DEPLOY_TARGET && polishCheckpointGate?.status !== "carried_forward") {
|
|
5187
|
+
return `Every mapped page is template stock, so there is no design route to capture. Missing Polish evidence is carried forward only on the local preview served from a loopback host (localhost, 127.0.0.1 or [::1]); this packet's deploy.preview_url is not one. Serve the build locally and record it with \`${cmd("record")} deploy --packet ${shellToken(packetPath)} --base-url <served url>\`, then run \`${cmd("next")} --packet ${shellToken(packetPath)}\`.`;
|
|
5188
|
+
}
|
|
5189
|
+
if (packet.deploy?.target === LOCAL_SERVE_DEPLOY_TARGET) {
|
|
5190
|
+
return `Every mapped page is template stock, so there is no design route to capture. On the local loopback preview, missing Polish evidence is carried forward as a warning. Run \`${cmd("next")} --packet ${shellToken(packetPath)}\` to continue to deploy and QA. Production QA still requires the evidence.`;
|
|
5191
|
+
}
|
|
5192
|
+
if (polishCheckpointGate?.status !== "carried_forward" && packet.deploy?.preview_url) {
|
|
5193
|
+
return `Every mapped page is template stock, so there is no design route to capture. Missing Polish evidence is carried forward only on a hosted preview whose deploy.preview_url is a non-loopback http(s) URL different from deploy.production_url; this packet's is not. Set one with \`${cmd("qa")} policy set --packet ${shellToken(packetPath)} --preview-url <url>\`, then \`${cmd("next")} --packet ${shellToken(packetPath)}\`.`;
|
|
5194
|
+
}
|
|
5195
|
+
return packet.deploy?.preview_url
|
|
5196
|
+
? `Every mapped page is template stock, so there is no design route to capture. Missing Polish evidence is carried forward on the hosted preview. Run \`${cmd("next")} qa --packet ${shellToken(packetPath)}\`, then \`${qaRunCommand(packetPath, packet.deploy.preview_url)}\`. Production QA remains blocked without the evidence.`
|
|
5197
|
+
: `Every mapped page is template stock, so there is no design route to capture. Run \`${cmd("qa")} policy set --packet ${shellToken(packetPath)} --preview-url <url>\` to record the hosted preview URL, then \`${cmd("next")} --packet ${shellToken(packetPath)}\`.`;
|
|
5198
|
+
}
|
|
5135
5199
|
return `${campaignIntentPromptHeader(intent)}Use next-campaigns-polish for this built campaign.
|
|
5136
5200
|
|
|
5137
5201
|
Read first:
|
|
@@ -5190,7 +5254,12 @@ function localServePlan(packet) {
|
|
|
5190
5254
|
};
|
|
5191
5255
|
}
|
|
5192
5256
|
|
|
5193
|
-
function deployPrompt(packetPath, reportPath, packet) {
|
|
5257
|
+
function deployPrompt(packetPath, reportPath, packet, polishGate = null, polishCheckpointGate = null) {
|
|
5258
|
+
if (packet.deploy?.target !== LOCAL_SERVE_DEPLOY_TARGET
|
|
5259
|
+
&& polishCheckpointGate?.code === "polish.hidden_eager_media.no_capturable_routes"
|
|
5260
|
+
&& !packet.deploy?.preview_url) {
|
|
5261
|
+
return `Every mapped page is template stock, and the hosted preview URL is not recorded. Run \`${cmd("qa")} policy set --packet ${shellToken(packetPath)} --preview-url <url>\` after the preview is deployed, then \`${cmd("next")} --packet ${shellToken(packetPath)}\`. There is no hosted record deploy command.`;
|
|
5262
|
+
}
|
|
5194
5263
|
const target = packet.deploy?.target || "unknown";
|
|
5195
5264
|
const liveUrlPath = packet.deploy?.live_url_path || packet.campaign?.live_url_path || campaignRouteRoot(packet) || "/<slug>/";
|
|
5196
5265
|
if (target === LOCAL_SERVE_DEPLOY_TARGET) {
|
|
@@ -5212,6 +5281,9 @@ Once the server is up:
|
|
|
5212
5281
|
|
|
5213
5282
|
If the served build cannot be reached, set stages.deploy.status to "blocked" with a clear reason in outputs so the orchestration loop surfaces it rather than skipping past.`;
|
|
5214
5283
|
}
|
|
5284
|
+
if (polishGate?.carried_forward?.policy === "hosted_template_preview" && packet.deploy?.preview_url) {
|
|
5285
|
+
return `The hosted preview URL is recorded at deploy.preview_url. Every mapped page is template stock, so missing Polish evidence is carried forward as a warning for this preview. Run \`${cmd("next")} qa --packet ${shellToken(packetPath)}\` to inspect the QA stage, then \`${qaRunCommand(packetPath, packet.deploy.preview_url)}\` to test the preview. The recorded hosted preview satisfies this deploy handoff; after QA records its verdict, run \`${cmd("next")} --packet ${shellToken(packetPath)}\` for closeout. Production QA remains strict.`;
|
|
5286
|
+
}
|
|
5215
5287
|
return `Deploy the built campaign to ${target}.
|
|
5216
5288
|
|
|
5217
5289
|
Read first:
|
|
@@ -5234,36 +5306,40 @@ If the deploy is blocked (non-localhost allowed-domain not yet added, CI permiss
|
|
|
5234
5306
|
// The QA command the QA stage hands over. Given a bump cart it is the
|
|
5235
5307
|
// order-bump run: the same command with --cart selecting the base tier and
|
|
5236
5308
|
// toggling each declared bump.
|
|
5237
|
-
function qaRunCommand(packetPath, url, bumpCart = null) {
|
|
5238
|
-
return `${cmd("qa")} run --packet ${packetPath} --base-url ${url} --browser --test-order common${bumpCart ? ` --cart ${shellToken(bumpCart.cart)}` : ""}`;
|
|
5239
|
-
}
|
|
5240
|
-
|
|
5241
5309
|
// `qa run --test-order common` never puts a checkout order bump in a test
|
|
5242
5310
|
// order: the tier planner skips bump rows by design and bump coverage comes
|
|
5243
5311
|
// from --cart. So when the spec declares one, `next` names a second run with
|
|
5244
|
-
// the bump in the cart. It reads the one checkout QA drives, with QA's own
|
|
5245
|
-
// classifiers: QA's test orders run on
|
|
5246
|
-
//
|
|
5247
|
-
// to enabled and sorted by `order || 0`
|
|
5248
|
-
//
|
|
5249
|
-
//
|
|
5250
|
-
//
|
|
5251
|
-
//
|
|
5252
|
-
//
|
|
5253
|
-
//
|
|
5312
|
+
// the bump in the cart. It reads the one checkout path QA drives, with QA's own
|
|
5313
|
+
// row classifiers: QA's test orders run on the Checkout (the page that takes
|
|
5314
|
+
// payment) of the first entry path of the first funnel that has one, each
|
|
5315
|
+
// funnel's pages filtered to enabled and sorted by `order || 0`
|
|
5316
|
+
// (extractTopologies, findPaymentPage). The rows are read from every
|
|
5317
|
+
// checkout-flow page on that path — the select page or first step of a
|
|
5318
|
+
// multi-step checkout declares the cart, not the Checkout (campaigns-os#641).
|
|
5319
|
+
// When that path declares no bump it returns null rather than looking at a
|
|
5320
|
+
// later funnel: QA never drives a later funnel's checkout, so that funnel's
|
|
5321
|
+
// bump ref in --cart would land on a checkout without it. The base is the
|
|
5322
|
+
// first selector tier QA would plan: QA's default keeps the page's
|
|
5323
|
+
// pre-selected card, which a spec does not name. A path that declares no tier
|
|
5324
|
+
// (the cart is filled on an entry page) gets the bump alone, on whatever
|
|
5325
|
+
// selection the entry page made.
|
|
5254
5326
|
export function checkoutOrderBumpCart(spec) {
|
|
5255
|
-
const
|
|
5256
|
-
|
|
5257
|
-
|
|
5327
|
+
const upgraded = upgradeCampaignSpec(spec);
|
|
5328
|
+
const funnels = Array.isArray(upgraded?.funnels)
|
|
5329
|
+
? upgraded.funnels
|
|
5330
|
+
: Array.isArray(upgraded?.funnel_pages) ? [{ pages: upgraded.funnel_pages }] : [];
|
|
5258
5331
|
for (const funnel of funnels) {
|
|
5259
|
-
const
|
|
5332
|
+
const pages = (Array.isArray(funnel?.pages) ? funnel.pages : [])
|
|
5260
5333
|
.filter((page) => page && page.enabled !== false)
|
|
5261
|
-
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
|
5262
|
-
|
|
5334
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
5335
|
+
const sorted = { ...funnel, pages };
|
|
5336
|
+
const checkout = paymentPageForFunnel(sorted);
|
|
5263
5337
|
if (!checkout) continue;
|
|
5264
|
-
const
|
|
5338
|
+
const entryPath = entryPages(sorted).map((entry) => checkoutPathFrom(pages, entry)).find((path) => path?.includes(checkout));
|
|
5339
|
+
const pathPages = (entryPath || [checkout]).filter((page) => isCheckoutFlowPage(page));
|
|
5340
|
+
const bumps = [...new Set(pathPages.flatMap((page) => declaredOrderBumps(page)))];
|
|
5265
5341
|
if (!bumps.length) return null;
|
|
5266
|
-
const base = declaredSelectorTiers(
|
|
5342
|
+
const base = pathPages.map((page) => declaredSelectorTiers(page)[0]?.ref).find(Boolean) || null;
|
|
5267
5343
|
return { base, bumps, cart: [base, ...bumps].filter(Boolean).map((ref) => `${ref}:1`).join(",") };
|
|
5268
5344
|
}
|
|
5269
5345
|
return null;
|
|
@@ -5275,7 +5351,12 @@ function orderBumpRunReason(bumpCart) {
|
|
|
5275
5351
|
return `The checkout declares order bump ${bumpCart.bumps.join(", ")} (is_order_bump or is_upsell), and the default run never puts it in a test order. This run repeats QA with ${cart} in the cart.`;
|
|
5276
5352
|
}
|
|
5277
5353
|
|
|
5278
|
-
function qaPrompt(packetPath, reportPath, packet, spec = null, intent) {
|
|
5354
|
+
function qaPrompt(packetPath, reportPath, packet, spec = null, intent, polishCheckpointGate = null) {
|
|
5355
|
+
if (packet.deploy?.target !== LOCAL_SERVE_DEPLOY_TARGET
|
|
5356
|
+
&& polishCheckpointGate?.code === "polish.hidden_eager_media.no_capturable_routes"
|
|
5357
|
+
&& !packet.deploy?.preview_url) {
|
|
5358
|
+
return `Every mapped page is template stock, and QA needs the hosted preview URL. Run \`${cmd("qa")} policy set --packet ${shellToken(packetPath)} --preview-url <url>\`, then \`${cmd("next")} qa --packet ${shellToken(packetPath)}\`.`;
|
|
5359
|
+
}
|
|
5279
5360
|
const url = packet.deploy?.preview_url || packet.deploy?.production_url || "<preview-url>";
|
|
5280
5361
|
const briefPath = packet.build_brief?.normalized_path || "(missing)";
|
|
5281
5362
|
const bumpCart = checkoutOrderBumpCart(spec);
|
|
@@ -5295,7 +5376,7 @@ ${cmd("qa")} install-browser
|
|
|
5295
5376
|
Node QA command:
|
|
5296
5377
|
${qaRunCommand(packetPath, url)}${bumpCommand}
|
|
5297
5378
|
|
|
5298
|
-
Run the browser install once after install/update before --browser or --test-order. Test-order proof must exercise the campaign through the Campaign Cart SDK with the browser typed-card flow. Do not create hand-built backend API orders as launch proof. Compare visible placeholders, payment methods and template trust badges, variant media, the template's own promo placeholders (demo timers, promo banners, placeholder voucher codes, exit-pop offers), and pricing presentation against the Campaign Build Brief. Do not flag the source design's own proof, urgency or guarantee elements: they are the merchant's content. Test Orders use global test cards that bypass the payment gateway and create no transactions,
|
|
5379
|
+
Run the browser install once after install/update before --browser or --test-order. Test-order proof must exercise the campaign through the Campaign Cart SDK with the browser typed-card flow. Do not create hand-built backend API orders as launch proof. Compare visible placeholders, payment methods and template trust badges, variant media, the template's own promo placeholders (demo timers, promo banners, placeholder voucher codes, exit-pop offers), and pricing presentation against the Campaign Build Brief. Do not flag the source design's own proof, urgency or guarantee elements: they are the merchant's content. Test Orders use global test cards that bypass the payment gateway and create no transactions, but each creates a real order record on the store. Unless the operator has already said test orders are fine for this campaign, ask once, up front, before placing test orders; then proceed. No permission flag, packet policy, or merchant setup is required. Localhost on any port is a globally allowed Development domain for SDK initialization and suppresses Campaigns analytics events; non-localhost preview/production origins still need the SDK origin allowlist. Use --test-order common for the default depth: every actual terminal path when they fit under the flood cap, otherwise checkout, first-offer accept/decline and a deduplicated shortest real receipt path plus the shortest path that clicks the decline on each offer or downsell page not yet declined, up to the cap (the verdict row browser-test-order:upsell-action-coverage warns naming each offer page whose decline no order clicked); use an explicit path such as accept-decline-accept for a targeted matrix; or use --test-order full for every actual terminal path in the selected checkout topology. Cycles, missing routes, and reachable nonterminals block exhaustive proof before browser launch. The default accidental-flood cap is 6, and an overflow names the exact explicit --max-test-orders raise. That cap bounds planned paths; --max-order-creations bounds actual order creations and is reserved before each submit click, defaulting to the planned path count. A path whose failure is classified as created (the order is already placed) is inspected read-only and never resubmitted. A not_created failure may be re-run once, if the creation budget has a slot no still-unrun planned path needs; an ambiguous failure stops that path with an explicit operator check instead of buying again. Read evidence.recovery to tell a recovered pass from a first-attempt pass. Click rendered SDK upsell accept/decline controls for upsell proof. For multi-tier package selectors, drive a specific card with --select-package <ref[:qty],...> (strict: the path fails if the requested card cannot be found or selected, unlike best-effort --cart), or use --test-order tiers / tiers:common / tiers:full to drive every selector tier the CampaignSpec declares on the checkout page in one run (order-bump rows marked is_order_bump or is_upsell are add-ons, not tiers; --select-package narrows a tiers run to the listed tiers); prove coupon-bearing orders with --apply-coupon <code> (typed into the rendered promo input, verified against the persisted-order voucher read-back). Reuse one test customer email via --test-email or CAMPAIGNS_OS_QA_TEST_EMAIL (a real monitored inbox in internal runs) so repeated QA does not litter the customer list.
|
|
5299
5380
|
|
|
5300
5381
|
Launch readiness note: Campaigns OS can prove the campaign build, SDK wiring, browser behavior, and typed-card order paths. It does not prove the merchant is ready for real shoppers. Before launch, confirm the production storefront URL, live payment methods, shipping markets, legal/support URLs, analytics expectations, and any merchant-side configuration. Treat those as real-shopper readiness items, not Campaigns OS build blockers.
|
|
5301
5382
|
|
|
@@ -5343,7 +5424,7 @@ function qaVerdictArtifactsForCampaign(packet, targetRepo) {
|
|
|
5343
5424
|
// Presence-based only: a stale built output still diverges from a pending
|
|
5344
5425
|
// ledger, and the entry says the two disagree — it never asserts the stage
|
|
5345
5426
|
// is complete or the artifact current. This function writes nothing.
|
|
5346
|
-
export function detectLedgerDivergence(report, packet, targetRepo) {
|
|
5427
|
+
export function detectLedgerDivergence(report, packet, targetRepo, { allowUnrecordedHostedPreview = false } = {}) {
|
|
5347
5428
|
const divergences = [];
|
|
5348
5429
|
const stages = report?.stages;
|
|
5349
5430
|
if (!isObject(stages) || !isNonEmptyString(targetRepo)) return divergences;
|
|
@@ -5376,7 +5457,7 @@ export function detectLedgerDivergence(report, packet, targetRepo) {
|
|
|
5376
5457
|
const packetPreviewUrl = optionalString(packet?.deploy?.preview_url);
|
|
5377
5458
|
const packetProductionUrl = optionalString(packet?.deploy?.production_url);
|
|
5378
5459
|
const url = reportUrl || packetPreviewUrl || packetProductionUrl;
|
|
5379
|
-
if (url) {
|
|
5460
|
+
if (url && !(allowUnrecordedHostedPreview && !reportUrl)) {
|
|
5380
5461
|
const source = reportUrl
|
|
5381
5462
|
? "report.stages.deploy.outputs"
|
|
5382
5463
|
: packetPreviewUrl
|
|
@@ -6621,10 +6702,10 @@ async function promptForFinding(current) {
|
|
|
6621
6702
|
const { createInterface } = await import("node:readline/promises");
|
|
6622
6703
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
6623
6704
|
try {
|
|
6624
|
-
const stage = current.stage || (await rl.question(`Stage (${FINDING_STAGES.join("/")}): `)).trim();
|
|
6625
|
-
const kind = current.kind || (await rl.question(`Kind (${FINDING_KINDS.join("/")}): `)).trim();
|
|
6626
|
-
const summary = current.summary || (await rl.question("Summary: ")).trim();
|
|
6627
|
-
const details = current.details || (await rl.question("Details (optional): ")).trim() || null;
|
|
6705
|
+
const stage = current.stage || (await timeOperatorWait(() => rl.question(`Stage (${FINDING_STAGES.join("/")}): `))).trim();
|
|
6706
|
+
const kind = current.kind || (await timeOperatorWait(() => rl.question(`Kind (${FINDING_KINDS.join("/")}): `))).trim();
|
|
6707
|
+
const summary = current.summary || (await timeOperatorWait(() => rl.question("Summary: "))).trim();
|
|
6708
|
+
const details = current.details || (await timeOperatorWait(() => rl.question("Details (optional): "))).trim() || null;
|
|
6628
6709
|
return { stage, kind, summary, details };
|
|
6629
6710
|
} finally {
|
|
6630
6711
|
rl.close();
|
|
@@ -7099,7 +7180,7 @@ async function runSessionEnd(args, ambient = null, sessionHolder = null) {
|
|
|
7099
7180
|
// `run end` is "assemble the Run Record for this session and stop logging to
|
|
7100
7181
|
// it". A throw leaves the session active so the operator can fix the packet
|
|
7101
7182
|
// and re-run `run end`. In text mode run-record prints its own summary.
|
|
7102
|
-
const summary = await closeRunSession(found, { packet, extraArgs: args, silent: args.json === true });
|
|
7183
|
+
const summary = await closeRunSession(found, { packet, extraArgs: args, silent: args.json === true, closedBy: "run_end" });
|
|
7103
7184
|
return { result: summary, exitCode: 0 };
|
|
7104
7185
|
}
|
|
7105
7186
|
|
|
@@ -7114,13 +7195,9 @@ async function runSessionEnd(args, ambient = null, sessionHolder = null) {
|
|
|
7114
7195
|
// `dryRun` entries in src/invocation.mjs and autoEndRunSessionAfterTerminalQa.
|
|
7115
7196
|
export const RUN_RECORD_INHERITABLE_FLAGS = Object.freeze([
|
|
7116
7197
|
"context", "report", "qa-verdict", "journal", "surfaces", "primary-surface", "surface-confidence",
|
|
7117
|
-
"agent-input-tokens", "agent-output-tokens", "agent-tool-output-tokens", "agent-total-tokens", "agent-elapsed-ms", "agent-model", "agent-usage-source",
|
|
7118
7198
|
"no-remit", "no-write", "proxy-base", "dry-run", "json",
|
|
7119
7199
|
]);
|
|
7120
7200
|
const RUN_RECORD_BOOLEAN_INHERITABLE_FLAGS = new Set(["no-remit", "no-write", "dry-run", "json"]);
|
|
7121
|
-
const RUN_RECORD_INTEGER_INHERITABLE_FLAGS = new Set([
|
|
7122
|
-
"agent-input-tokens", "agent-output-tokens", "agent-tool-output-tokens", "agent-total-tokens", "agent-elapsed-ms",
|
|
7123
|
-
]);
|
|
7124
7201
|
|
|
7125
7202
|
// The run-record argv that closes `session`: its run_id and journal, the
|
|
7126
7203
|
// packet the closer resolved, and only the inheritable flags of `extraArgs`.
|
|
@@ -7144,7 +7221,7 @@ export function runSessionEndArgs(session, packet, extraArgs = {}) {
|
|
|
7144
7221
|
// session is cleared only AFTER run-record succeeds: on a throw it stays on
|
|
7145
7222
|
// disk, and the error is rethrown unless `onError` takes it, in which case
|
|
7146
7223
|
// the closer returns null.
|
|
7147
|
-
async function closeRunSession(found, { packet, extraArgs = {}, silent = false, promptForConsent = true, onError = null } = {}) {
|
|
7224
|
+
async function closeRunSession(found, { packet, extraArgs = {}, silent = false, promptForConsent = true, onError = null, closedBy } = {}) {
|
|
7148
7225
|
const endArgs = runSessionEndArgs(found.session, packet, extraArgs);
|
|
7149
7226
|
try {
|
|
7150
7227
|
// No internal closeout currently constructs a refusal before its invoking
|
|
@@ -7152,7 +7229,7 @@ async function closeRunSession(found, { packet, extraArgs = {}, silent = false,
|
|
|
7152
7229
|
// run-record, run end validates its own argv first, and QA auto-end runs
|
|
7153
7230
|
// after QA persistence. Keep the scope at this boundary so a future
|
|
7154
7231
|
// closeout refusal swallowed by onError cannot mark the outer invocation.
|
|
7155
|
-
const summary = await runWithRefusalScope(() => runRecordCommand(endArgs, found, { silent, promptForConsent }));
|
|
7232
|
+
const summary = await runWithRefusalScope(() => runRecordCommand(endArgs, found, { silent, promptForConsent, closedBy }));
|
|
7156
7233
|
// Clearing the session is a write like any other, so a closer carrying
|
|
7157
7234
|
// --dry-run leaves it open: the operator sees the record the close would
|
|
7158
7235
|
// assemble and can still close for real afterwards.
|
|
@@ -7233,6 +7310,7 @@ async function closeOutStaleRunSession(rootDir, inherited = {}) {
|
|
|
7233
7310
|
if (packet && existsSync(packet)) {
|
|
7234
7311
|
const summary = await closeRunSession(stale, {
|
|
7235
7312
|
packet,
|
|
7313
|
+
closedBy: "stale_sweep",
|
|
7236
7314
|
extraArgs: { ...inherited, json: true },
|
|
7237
7315
|
silent: true,
|
|
7238
7316
|
promptForConsent: false,
|
|
@@ -7261,12 +7339,11 @@ async function closeOutStaleRunSession(rootDir, inherited = {}) {
|
|
|
7261
7339
|
// run-record.mjs to assemble the manifest, then (consent-gated, non-fatal)
|
|
7262
7340
|
// remit it. Capture is ALWAYS local; consent gates only the remit. See
|
|
7263
7341
|
// docs/workflow-findings-sidecar.md.
|
|
7264
|
-
async function runRecordCommand(args, ambient = null, { silent = false, promptForConsent = true } = {}) {
|
|
7342
|
+
async function runRecordCommand(args, ambient = null, { silent = false, promptForConsent = true, closedBy = "manual" } = {}) {
|
|
7265
7343
|
const packetPath = resolve(requireArg(args, "packet"));
|
|
7266
7344
|
if (args["new-run"] === true && optionalString(args["run-id"])) {
|
|
7267
7345
|
throw refused("run-record: --new-run and --run-id are exclusive; --run-id names the run to re-emit, --new-run mints a fresh one.");
|
|
7268
7346
|
}
|
|
7269
|
-
const agentUsage = refusing(() => parseAgentUsageArgs(args));
|
|
7270
7347
|
// --dry-run assembles the record and shows it, then writes and sends
|
|
7271
7348
|
// nothing. It differs from --no-write, which skips the assembly's reads
|
|
7272
7349
|
// as well; combining the two is allowed and still writes nothing.
|
|
@@ -7503,6 +7580,7 @@ async function runRecordCommand(args, ambient = null, { silent = false, promptFo
|
|
|
7503
7580
|
packageVersion: packageVersion(),
|
|
7504
7581
|
...toolkitProvenance({ silent }),
|
|
7505
7582
|
qaVerdictPublish,
|
|
7583
|
+
closedBy,
|
|
7506
7584
|
command: "run-record",
|
|
7507
7585
|
argvShape: argvShape(args),
|
|
7508
7586
|
consent: { state: consent.state, source: consent.source },
|
|
@@ -7524,7 +7602,6 @@ async function runRecordCommand(args, ambient = null, { silent = false, promptFo
|
|
|
7524
7602
|
primarySurface: optionalString(args["primary-surface"]),
|
|
7525
7603
|
surfaceConfidence: optionalString(args["surface-confidence"]),
|
|
7526
7604
|
lifecycle,
|
|
7527
|
-
agentUsage,
|
|
7528
7605
|
});
|
|
7529
7606
|
|
|
7530
7607
|
const shouldAttemptRemit = !remitDisabled && consent.state === "on";
|
|
@@ -7886,9 +7963,7 @@ function parseRunRecordSurfaces(value) {
|
|
|
7886
7963
|
|
|
7887
7964
|
function validateRunRecordArgv(args) {
|
|
7888
7965
|
for (const flag of RUN_RECORD_INHERITABLE_FLAGS) {
|
|
7889
|
-
|
|
7890
|
-
// their non-negative-integer diagnostics for bare and blank values.
|
|
7891
|
-
if (!RUN_RECORD_BOOLEAN_INHERITABLE_FLAGS.has(flag) && !RUN_RECORD_INTEGER_INHERITABLE_FLAGS.has(flag) && Object.hasOwn(args, flag)) requireArg(args, flag);
|
|
7966
|
+
if (!RUN_RECORD_BOOLEAN_INHERITABLE_FLAGS.has(flag) && Object.hasOwn(args, flag)) requireArg(args, flag);
|
|
7892
7967
|
}
|
|
7893
7968
|
if (Object.hasOwn(args, "run-id")) requireArg(args, "run-id");
|
|
7894
7969
|
if (Object.hasOwn(args, "new-run") && args["new-run"] !== true) {
|
|
@@ -7898,41 +7973,11 @@ function validateRunRecordArgv(args) {
|
|
|
7898
7973
|
throw refused("run-record: --new-run and --run-id are exclusive; --run-id names the run to re-emit, --new-run mints a fresh one.");
|
|
7899
7974
|
}
|
|
7900
7975
|
return {
|
|
7901
|
-
agentUsage: refusing(() => parseAgentUsageArgs(args)),
|
|
7902
7976
|
dryRun: isDryRun(args),
|
|
7903
7977
|
parsedSurfaces: parseRunRecordSurfaces(args.surfaces),
|
|
7904
7978
|
};
|
|
7905
7979
|
}
|
|
7906
7980
|
|
|
7907
|
-
function parseAgentUsageArgs(args) {
|
|
7908
|
-
const fields = {
|
|
7909
|
-
"agent-input-tokens": "input_tokens",
|
|
7910
|
-
"agent-output-tokens": "output_tokens",
|
|
7911
|
-
"agent-tool-output-tokens": "tool_output_tokens",
|
|
7912
|
-
"agent-total-tokens": "total_tokens",
|
|
7913
|
-
"agent-elapsed-ms": "elapsed_ms",
|
|
7914
|
-
};
|
|
7915
|
-
const usage = {};
|
|
7916
|
-
for (const [flag, field] of Object.entries(fields)) {
|
|
7917
|
-
if (!(flag in args)) continue;
|
|
7918
|
-
usage[field] = parseNonNegativeIntegerFlag(args[flag], flag);
|
|
7919
|
-
}
|
|
7920
|
-
if (isNonEmptyString(args["agent-model"])) usage.model = args["agent-model"].trim();
|
|
7921
|
-
if (isNonEmptyString(args["agent-usage-source"])) usage.source = args["agent-usage-source"].trim();
|
|
7922
|
-
return Object.keys(usage).length ? usage : null;
|
|
7923
|
-
}
|
|
7924
|
-
|
|
7925
|
-
function parseNonNegativeIntegerFlag(value, flag) {
|
|
7926
|
-
if (value === true || value === false || value == null || String(value).trim() === "") {
|
|
7927
|
-
throw new Error(`--${flag} requires a non-negative integer value.`);
|
|
7928
|
-
}
|
|
7929
|
-
const parsed = Number(value);
|
|
7930
|
-
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
7931
|
-
throw new Error(`--${flag} must be a non-negative integer.`);
|
|
7932
|
-
}
|
|
7933
|
-
return parsed;
|
|
7934
|
-
}
|
|
7935
|
-
|
|
7936
7981
|
function packageVersion() {
|
|
7937
7982
|
return readJson(join(ROOT, "package.json")).version;
|
|
7938
7983
|
}
|