@nextcommerce/campaigns-os 1.52.0 → 1.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +109 -0
- package/README.md +1 -0
- package/agents/claude/CLAUDE.md +2 -0
- package/agents/codex/AGENTS.md +2 -0
- package/agents/copilot/copilot-instructions.md +2 -0
- package/agents/cursor/campaigns-os.mdc +2 -0
- package/compatibility.json +1 -1
- package/contracts/campaign-cart-checkout-field-contract.v0.json +10 -0
- package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
- package/contracts/commerce-surface-catalog.json +27 -27
- package/contracts/effects.v1.json +295 -47
- package/contracts/release-ledger.json +660 -0
- package/contracts/supported-surface.json +3 -3
- package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
- package/docs/brand-theme-bridge.md +2 -0
- package/docs/build-packet.md +67 -19
- package/docs/campaign-build-brief.md +48 -17
- package/docs/campaign-standardization-report.md +32 -0
- package/docs/design-source-package.md +7 -3
- package/docs/effects.md +10 -1
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/polish-evidence.md +2 -0
- package/docs/progress-snapshots.md +5 -5
- package/docs/qa-and-test-orders.md +30 -3
- package/docs/readback.md +19 -2
- package/docs/runtime-readiness.md +1 -1
- package/docs/sdk-storage-compatibility.md +15 -0
- package/docs/skills-revision.md +10 -10
- package/docs/supported-surface.md +1 -0
- package/package.json +1 -1
- package/schemas/campaign-runtime-assembly-report.v0.schema.json +5 -1
- package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +3 -3
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +5 -3
- package/skills/next-campaigns-os/SKILL.md +10 -19
- package/skills/next-campaigns-os/references/session-intake.md +10 -6
- package/skills/next-campaigns-os-setup/SKILL.md +4 -3
- package/skills/next-campaigns-polish/SKILL.md +7 -5
- package/skills/next-campaigns-qa/SKILL.md +5 -3
- package/skills.json +10 -10
- package/src/brand-theme.mjs +21 -20
- package/src/brief-summary.mjs +275 -0
- package/src/build-brief.mjs +167 -15
- package/src/campaign-ecosystem.mjs +153 -31
- package/src/cli.mjs +182 -115
- package/src/contrast.mjs +649 -0
- package/src/design-source-publication.mjs +110 -3
- package/src/directory-lock.mjs +79 -7
- package/src/doctor/checks.mjs +141 -16
- package/src/doctor/inspect.mjs +13 -0
- package/src/doctor/next-step.mjs +15 -6
- package/src/input-currency.mjs +394 -0
- package/src/input-refresh.mjs +708 -0
- package/src/invocation.mjs +6 -2
- package/src/polish-browser.mjs +503 -201
- package/src/polish-deadline.mjs +7 -0
- package/src/polish-node.mjs +326 -46
- package/src/polish-readability.mjs +828 -0
- package/src/progress-node.mjs +18 -5
- package/src/qa-browser.mjs +211 -93
- package/src/qa-node.mjs +47 -7
- package/src/qc-accept.mjs +44 -8
- package/src/qc-check-registry.mjs +15 -7
- package/src/qc-results.mjs +326 -34
- package/src/readback.mjs +58 -10
- package/src/sdk-attribute-index.mjs +111 -23
- package/src/sdk-markup.mjs +49 -10
- package/src/sdk-repin.mjs +282 -0
- package/src/stage-ledger.mjs +235 -44
- package/src/stage-record.mjs +154 -30
- package/src/standardization-report.mjs +26 -1
- package/src/target-lock.mjs +9 -1
- package/src/tooling-setup.mjs +1 -1
package/src/cli.mjs
CHANGED
|
@@ -120,6 +120,7 @@ import {
|
|
|
120
120
|
SOURCE_HTML_MANIFEST_REL_PATH,
|
|
121
121
|
} from "./source-html-manifest.mjs";
|
|
122
122
|
import { crawlSourceAssetPaths } from "./source-asset-crawl.mjs";
|
|
123
|
+
import { summarizeCampaignBrief } from "./brief-summary.mjs";
|
|
123
124
|
import {
|
|
124
125
|
THEME_POLICIES,
|
|
125
126
|
inspectBrandTheme,
|
|
@@ -174,6 +175,8 @@ import {
|
|
|
174
175
|
createCampaignBuildBriefArtifact,
|
|
175
176
|
inferBuildBriefPath,
|
|
176
177
|
} from "./build-brief.mjs";
|
|
178
|
+
import { briefIntakeBindings, currentPacketInputs, inputRefreshCommands, inputStamps, stageWriteInputs } from "./input-currency.mjs";
|
|
179
|
+
import { prepareBuildStageStatus, priorBriefInputsForIntake } from "./input-refresh.mjs";
|
|
177
180
|
import {
|
|
178
181
|
DESIGN_SOURCE_PACKAGE_REL_PATH,
|
|
179
182
|
} from "./design-source-package.mjs";
|
|
@@ -315,9 +318,10 @@ Usage:
|
|
|
315
318
|
[--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>]
|
|
316
319
|
[--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--no-run-session] [--force] # intake alias for prepare-build + doctor
|
|
317
320
|
campaigns-os doctor --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--strip-paths] [--write] [--no-write] [--doctor-out <path>] [--proxy-base <url>] [--no-live-refs] [--json] # inspection by default; --doctor-out requires --write; --no-write wins. When the packet's built _site/<route>/ exists and a public Campaigns API key resolves (packet, its local CampaignSpec, or the declared campaign-key env var), doctor makes one read-only GET of {proxy-base}/api/campaign under X-Campaign-Key to check each built page's shipping and package refs against the live campaign; --proxy-base overrides the canonical proxy (https, or a loopback host over http). --no-live-refs skips the read and records not_run with reason disabled. No key, no built page, or a failed read records derived.live_campaign_refs as not_run with its reason. Only doctor and qa run make this read; other commands that run doctor record not_read
|
|
318
|
-
campaigns-os doctor --built <page-kit-target-repo> --family <family> [--slug <slug>] [--base-url <url>] [--emit-packet [path]] [--json] # L7: doctor a built _site/ with no Build Packet
|
|
321
|
+
campaigns-os doctor --built <page-kit-target-repo> --family <family> [--slug <slug>] [--base-url <url>] [--emit-packet [path]] [--json] # L7: doctor a built _site/ with no Build Packet. Reads the local files only: --base-url is not fetched, it fills deploy.preview_url in the minimal packet (check served pages with qa run --site)
|
|
319
322
|
campaigns-os bundle check --packet <campaign-runtime.build.json> [--require-qa] [--json] # validate the canonical migration/readback JSON bundle; never substitutes markdown
|
|
320
323
|
campaigns-os sdk storage-check --target <git-root> --target-sdk <x.y.z> --manifest <SDK-manifest.json> --scope <dir,file> [--exclude <dir,file>] [--json]
|
|
324
|
+
campaigns-os sdk repin --target <static-campaign-repo> [--target-sdk <x.y.z>] [--apply] [--json] # rewrite the version segment of each semver-pinned Campaign Cart loader.js / campaign-cart.css URL (the references standardize's scan finds, @vX.Y.Z or @X.Y.Z) below the target, default the SDK support policy's preferred_minimum; @latest, @main, commit and prerelease refs, and pins at or above the target, are reported and left alone. Preview by default (path:line, old -> new URL, nothing written); --apply writes the files and a change record at .campaign-runtime/sdk-repin.json (campaigns-os-sdk-repin/v0: files touched, reference count, from/to versions) a Run Record can cite. A repo with _data/campaigns.json is refused (exit 2): page-kit sync owns that pin
|
|
321
325
|
campaigns-os standardize --target <campaign-repo> [--family <family>] [--slug <slug>] [--sdk-support-policy <path.json>] [--field-contract <path.json>] [--no-doctor] [--json]
|
|
322
326
|
campaigns-os theme inspect --packet <campaign-runtime.build.json> [--context <json>] [--theme-policy <inspect_only|auto|off>] [--json]
|
|
323
327
|
campaigns-os theme generate --packet <campaign-runtime.build.json> [--context <json>] [--out-dir <dir>] [--force] [--json]
|
|
@@ -328,11 +332,13 @@ Usage:
|
|
|
328
332
|
campaigns-os spec derive --packet <campaign-runtime.build.json> [--dry-run] [--json] [--report <json>] [--from-store <subdomain> [--store-token-source env:<VAR>]] [--write-map] [--proxy-base <url>] # write the fields the target repo already states into the packet's local CampaignSpec (spec.local_path): the SDK pin from _data/campaigns.json[<route>].sdk_version (global_config.sdk_version, and the runtime.sdk_version alias when declared), each page's page_url from the page tree under src/<route>/ (filename or permalink), and the analytics ids the entry carries (gtm_id -> analytics.providers.gtm.containerId, fb_pixel_id -> analytics.providers.facebook.pixelId); prints a field-by-field before -> after diff and writes nothing else. Repo-derived fields only and no network by default; --from-store <subdomain> (the <store> of <store>.29next.store) also reads through campaigns-os login gateway credentials (--store-token-source env:<VAR> explicitly selects the warned break-glass Admin path; a token never goes on the command line) and writes the nine campaign.store_* Store Profile fields: store_name and store_url (primary domain) and store_phone/store_phone_tel from GET /store/, and store_terms/privacy/contact/returns/shipping as https://<primary domain>/<slug>/ from the one storefront page (GET /pages/) whose slug or title names each policy; an empty store field, no page or several never empties the spec's value. A field the repo or store cannot state (a scaffold's seeded pin, an unbound page, an empty or malformed id, an active page_kit.sdk_version waiver, an empty store field, an unbound policy page) is reported as not derived, status PARTIAL; exit 2 when the packet, the spec or the target entry is missing, the spec identifies another campaign, or the store cannot be read (credential missing, 401/403, no such store, unreachable). --write-map also records the derived pin into the saved Map's Build hints (Campaign Cart SDK version) through the proxy Worker (PUT /api/maps/<spec.map_id> under X-Campaign-Key, the packet's Campaigns API key, with the Map's spec_hash as the X-Spec-Hash precondition): written when the Map declares no pin or one behind the repo, unchanged when equal, refused (warning, exit 0) when the Map pin is ahead or cannot be ordered, failed (error, exit 2) when the key is missing or mismatched, the Map is gone, was saved in between, or the proxy refuses the body; the write is recorded on the Assembly Report evidence[] and in the result's map object. --proxy-base overrides the canonical proxy (https, or a loopback host over http); --dry-run reads the Map and reports would_write without a PUT.
|
|
329
333
|
campaigns-os page-kit parity --packet <campaign-runtime.build.json> [--report <json>] [--json] # local proof mode (deploy.target local-serve): render the current source in development and production through the target's page-kit into temp dirs, assert the served _site/ is the current development render and that production differs from it only in environment-gated output (same page set, same route slugs, same Campaign Cart pin and next-api-key); records stages.assembly.evidence.local_proof.production_parity, which doctor reads as local_proof.production_parity. Exit 2 on a non-gated difference.
|
|
330
334
|
campaigns-os polish capture --packet <campaign-runtime.build.json> --base-url <url> [--report <json>] [--headed] [--auth-cookie <cookie>] [--json]
|
|
335
|
+
campaigns-os record brief --packet <campaign-runtime.build.json> [--brief <yaml|json>] [--context <json>] [--report <json>] [--dry-run] [--json]
|
|
336
|
+
campaigns-os record spec --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--dry-run] [--json]
|
|
331
337
|
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
|
|
332
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 (--build-environment records stages.assembly.evidence.build_environment; local proof mode records development): stages.assembly completed with build_fingerprint = doctor's derived.build_output_fingerprint.value (and the Design Source Package material fingerprint when the report has one); stages.polish becomes required unless its evidence is bound to this exact output
|
|
333
339
|
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
|
|
334
340
|
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
|
|
335
|
-
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; 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
|
|
341
|
+
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
|
|
336
342
|
campaigns-os readback <target-repo-root> [--json] [--packet <path>] [--doctor <path>] [--context <path>] [--report <path>] [--qa-verdict <path>] [--findings <path>] # read-only projection of one run's emitted artifacts (packet, doctor output, build context, assembly report, QA verdict, findings export): artifact states, per-artifact freshness against the checkout's HEAD reflog, doctor warning grouping, skip cascades and cross-artifact divergences. Writes nothing, starts no process, touches no network, and records no lifecycle entry; --json emits one campaigns-os-readback/v2 object (docs/readback.md). Exit 2 for a missing target root or a Build Packet set freshness cannot single out.
|
|
337
343
|
campaigns-os readback --example [--json] # project the bundled synthetic sample; freshness is not computable for it by design
|
|
338
344
|
campaigns-os validate-assembly-report --report <json> [--json]
|
|
@@ -709,6 +715,10 @@ export function recordQaStageOutcome(args, result) {
|
|
|
709
715
|
const failed = (Array.isArray(verdict.assertions) ? verdict.assertions : [])
|
|
710
716
|
.filter((assertion) => assertion?.status === "fail")
|
|
711
717
|
.map((assertion) => `${assertion.id}: ${assertion.actual || "assertion failed"}`);
|
|
718
|
+
// The QA stamps are the verdict's: the brief material qa run bound at run
|
|
719
|
+
// start and the spec material it judged, never the inputs at write time.
|
|
720
|
+
const qaStamps = inputStamps({ briefMaterial: verdict.source_brief_material, specMaterial: verdict.spec_hash });
|
|
721
|
+
const qaInputs = stageWriteInputs("qa", currentPacketInputs({ packet, packetPath }));
|
|
712
722
|
const committed = commitAssemblyReport(workspace, (report) => {
|
|
713
723
|
if (hasLocalIdentity && !specHashesMatch(verdict.spec_hash, report.identity?.spec_material_hash)) {
|
|
714
724
|
throw new Error("Local-spec QA verdict belongs to a different material revision; report evidence was not changed.");
|
|
@@ -734,6 +744,8 @@ export function recordQaStageOutcome(args, result) {
|
|
|
734
744
|
// summarizePurchaseProof). This is what lets `next` tell a real purchase
|
|
735
745
|
// path from a `--test-order off` diagnostic.
|
|
736
746
|
proof: summarizePurchaseProof({ verdict, proofPolicy: packet.qa?.proof_policy }),
|
|
747
|
+
stamps: qaStamps,
|
|
748
|
+
inputs: qaInputs,
|
|
737
749
|
});
|
|
738
750
|
}, {
|
|
739
751
|
stage: "qa",
|
|
@@ -1041,7 +1053,25 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1041
1053
|
}
|
|
1042
1054
|
|
|
1043
1055
|
if (command === "sdk") {
|
|
1044
|
-
if (args._[1]
|
|
1056
|
+
if (args._[1] === "repin" && args._.length === 2) {
|
|
1057
|
+
const known = new Set(["_", "target", "target-sdk", "apply", "json"]);
|
|
1058
|
+
for (const key of Object.keys(args)) if (!known.has(key)) throw refused(`Unknown SDK repin flag: --${key}`);
|
|
1059
|
+
for (const flag of ["apply", "json"]) {
|
|
1060
|
+
if (args[flag] !== undefined && args[flag] !== true) throw refused(`--${flag} is a boolean flag and takes no value.`);
|
|
1061
|
+
}
|
|
1062
|
+
const { runSdkRepin, formatSdkRepinReport, isRepinTargetVersion } = await import("./sdk-repin.mjs");
|
|
1063
|
+
const targetSdk = args["target-sdk"] === undefined ? null : requireArg(args, "target-sdk");
|
|
1064
|
+
if (targetSdk !== null && !isRepinTargetVersion(targetSdk)) throw refused(`--target-sdk ${targetSdk} is not a released SDK version (x.y.z).`);
|
|
1065
|
+
const result = runSdkRepin({
|
|
1066
|
+
targetRepo: requireArg(args, "target"),
|
|
1067
|
+
targetSdk,
|
|
1068
|
+
apply: args.apply === true,
|
|
1069
|
+
});
|
|
1070
|
+
console.log(args.json ? JSON.stringify(result, null, 2) : formatSdkRepinReport(result));
|
|
1071
|
+
process.exitCode = result.ok ? 0 : 2;
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if (args._[1] !== "storage-check" || args._.length !== 2) throw refused("Use: campaigns-os sdk storage-check --target <git-root> --target-sdk <x.y.z> --manifest <SDK-manifest.json> --scope <dir,file> [--exclude <dir,file>] [--json], or campaigns-os sdk repin --target <static-campaign-repo> [--target-sdk <x.y.z>] [--apply] [--json].");
|
|
1045
1075
|
const known = new Set(["_", "target", "target-sdk", "manifest", "scope", "exclude", "json"]);
|
|
1046
1076
|
if (args.json !== undefined && args.json !== true) throw refused("--json is a boolean flag and takes no value.");
|
|
1047
1077
|
for (const key of Object.keys(args)) if (!known.has(key)) throw refused(`Unknown SDK storage-check flag: --${key}`);
|
|
@@ -1438,8 +1468,7 @@ function createInitialAssemblyReportStages({ scaffoldRequired, blockers, outputs
|
|
|
1438
1468
|
// (STAGE_TERMINAL_STATUS_PREFIXES): declared out-of-scope pages do not hold
|
|
1439
1469
|
// the ladder, they are recorded on the stage so downstream consumers can see
|
|
1440
1470
|
// exactly which pages assemble from the template family instead of source HTML.
|
|
1441
|
-
|
|
1442
|
-
stages.prepare_build = createStage("prepare_build", blockers.length ? "blocked" : terminalStatus, {
|
|
1471
|
+
stages.prepare_build = createStage("prepare_build", prepareBuildStageStatus({ blockers, declaredScopeSkips }), {
|
|
1443
1472
|
outputs,
|
|
1444
1473
|
blockers,
|
|
1445
1474
|
...(declaredScopeSkips.length ? { declared_out_of_scope: declaredScopeSkips } : {}),
|
|
@@ -1790,6 +1819,10 @@ function prepareBuildUnderLock({
|
|
|
1790
1819
|
sourceRoot,
|
|
1791
1820
|
targetRepo,
|
|
1792
1821
|
});
|
|
1822
|
+
// The report and normalized brief this run replaces: the report's brief
|
|
1823
|
+
// mode keeps an existing prepared campaign prepared (when it is this
|
|
1824
|
+
// campaign's report), and the previous brief carries field provenance.
|
|
1825
|
+
const priorBriefInputs = priorBriefInputsForIntake({ reportPath, briefPath, identity: { spec: { map_id: mapId, ...(localSpecId ? { local_spec_id: localSpecId } : {}) }, campaign: { public_route_slug: publicRouteSlug } } });
|
|
1793
1826
|
const buildBrief = createCampaignBuildBriefArtifact({
|
|
1794
1827
|
inputPath: briefDiscovery?.path || null,
|
|
1795
1828
|
inputSource: briefDiscovery?.source || null,
|
|
@@ -1799,10 +1832,19 @@ function prepareBuildUnderLock({
|
|
|
1799
1832
|
templateFamily,
|
|
1800
1833
|
sourceAssetCrawl,
|
|
1801
1834
|
commerceZoneFindings,
|
|
1835
|
+
boundReportMode: priorBriefInputs.boundReportMode,
|
|
1836
|
+
previousNormalizedBrief: priorBriefInputs.previousNormalizedBrief,
|
|
1802
1837
|
});
|
|
1803
1838
|
if (buildBrief.inputPath && buildBrief.artifact?._meta) {
|
|
1804
1839
|
buildBrief.artifact._meta.input_path = relFromFile(briefPath, buildBrief.inputPath);
|
|
1805
1840
|
}
|
|
1841
|
+
const briefBindings = briefIntakeBindings(buildBrief);
|
|
1842
|
+
// The packet settings this run writes, checked against the packet on disk
|
|
1843
|
+
// before anything is published.
|
|
1844
|
+
const deploySettings = {
|
|
1845
|
+
target: optionalString(args["deploy-target"], "unknown"),
|
|
1846
|
+
preview_url: optionalString(args["preview-url"]),
|
|
1847
|
+
};
|
|
1806
1848
|
const buildBriefPrompts = buildBrief.mode === "prepared" ? [] : buildBrief.questions.map((question) => ({
|
|
1807
1849
|
code: `BUILD_BRIEF_${toConstantCase(question.id)}`,
|
|
1808
1850
|
stage: "prepare_build",
|
|
@@ -1836,6 +1878,11 @@ function prepareBuildUnderLock({
|
|
|
1836
1878
|
}))
|
|
1837
1879
|
: [];
|
|
1838
1880
|
const designSourcePackage = publication.package({
|
|
1881
|
+
plannedPacket: {
|
|
1882
|
+
campaign: { allowed_domains_confirmed: args["allowed-domains-confirmed"] === true },
|
|
1883
|
+
deploy: { ...deploySettings, production_url: optionalString(args["production-url"]) },
|
|
1884
|
+
qa: { proof_policy: { order_path_depth: orderPathDepthFlag || "common" } },
|
|
1885
|
+
},
|
|
1839
1886
|
activePages,
|
|
1840
1887
|
mappings: matched.mappings,
|
|
1841
1888
|
manifestResult,
|
|
@@ -1887,6 +1934,7 @@ function prepareBuildUnderLock({
|
|
|
1887
1934
|
normalized_path: relFromFile(packetPath, briefPath),
|
|
1888
1935
|
question_count: buildBrief.questions.length,
|
|
1889
1936
|
gate_count: buildBrief.gates.length,
|
|
1937
|
+
...briefBindings,
|
|
1890
1938
|
},
|
|
1891
1939
|
assembly: {
|
|
1892
1940
|
implementation: "next-campaigns-build",
|
|
@@ -1914,8 +1962,7 @@ function prepareBuildUnderLock({
|
|
|
1914
1962
|
compatible_outputs: ["static-html", "campaign-cart-sdk"],
|
|
1915
1963
|
},
|
|
1916
1964
|
deploy: {
|
|
1917
|
-
|
|
1918
|
-
preview_url: optionalString(args["preview-url"]),
|
|
1965
|
+
...deploySettings,
|
|
1919
1966
|
production_url: optionalString(args["production-url"]),
|
|
1920
1967
|
live_url_path: liveUrlPath,
|
|
1921
1968
|
},
|
|
@@ -2002,6 +2049,7 @@ function prepareBuildUnderLock({
|
|
|
2002
2049
|
gate_count: buildBrief.gates.length,
|
|
2003
2050
|
questions: buildBrief.questions,
|
|
2004
2051
|
gates: buildBrief.gates,
|
|
2052
|
+
...briefBindings,
|
|
2005
2053
|
},
|
|
2006
2054
|
page_map: matched.mappings.map((mapping) => ({
|
|
2007
2055
|
page_id: mapping.page_id,
|
|
@@ -2055,7 +2103,8 @@ function prepareBuildUnderLock({
|
|
|
2055
2103
|
|
|
2056
2104
|
// The top-level status/next/blockers are derived from the stages by the same
|
|
2057
2105
|
// function every later commit of the report runs (stage-ledger.mjs), so
|
|
2058
|
-
// prepare-build's first write and a producer's last write spell them alike
|
|
2106
|
+
// prepare-build's first write and a producer's last write spell them alike,
|
|
2107
|
+
// against the brief and CampaignSpec material this run binds.
|
|
2059
2108
|
const report = applyDerivedAssemblyReportSummary(createAssemblyReport({
|
|
2060
2109
|
packetPath,
|
|
2061
2110
|
contextPath,
|
|
@@ -2072,7 +2121,7 @@ function prepareBuildUnderLock({
|
|
|
2072
2121
|
buildScopeReasonsInvalid,
|
|
2073
2122
|
templateSelection,
|
|
2074
2123
|
evidence: strippedSpecBytes == null ? [] : hostStripped.evidence,
|
|
2075
|
-
}));
|
|
2124
|
+
}), { briefMaterial: briefBindings.material, specMaterial: specMaterialHash(spec) });
|
|
2076
2125
|
|
|
2077
2126
|
// Values a spec left as it is still holds that doctor blocks on. An
|
|
2078
2127
|
// absolute http(s) page_url is not among them: projection takes its path.
|
|
@@ -2643,7 +2692,7 @@ export function commitWaiverToAssemblyReport(workspace, mutate, options, { dryRu
|
|
|
2643
2692
|
// run's own "do not write" is this wrapper's null below, not the mutator's.
|
|
2644
2693
|
if (!isPlainObject(mutated)) throw new TypeError("commitAssemblyReport mutate(report) must return an Assembly Report object.");
|
|
2645
2694
|
if (!dryRun) return mutated;
|
|
2646
|
-
applyDerivedAssemblyReportSummary(mutated);
|
|
2695
|
+
applyDerivedAssemblyReportSummary(mutated, currentPacketInputs({ packet: workspace.packet, packetPath: workspace.packetPath }));
|
|
2647
2696
|
return null;
|
|
2648
2697
|
};
|
|
2649
2698
|
// A preview writes nothing, so it does not take the target lock either: the
|
|
@@ -2698,7 +2747,19 @@ export async function polishCaptureCommand(args, options = {}) {
|
|
|
2698
2747
|
throw new Error(`polish capture needs an existing Assembly Report at ${reportPath}; run prepare-build/start first.`);
|
|
2699
2748
|
}
|
|
2700
2749
|
requireValidPolishCaptureReport(report, reportPath);
|
|
2701
|
-
|
|
2750
|
+
// A campaign whose every mapping is template stock has no page_load route
|
|
2751
|
+
// to capture: its plan error is held, and the capture measures readability
|
|
2752
|
+
// on the built pages only, leaving page_load and media_weight as they are.
|
|
2753
|
+
const { capturePolishReadability, isNoCapturableRoutesError } = await import("./polish-node.mjs");
|
|
2754
|
+
const planOrHeld = (currentPacket) => {
|
|
2755
|
+
try {
|
|
2756
|
+
return { plan: planPolishCapture({ packet: currentPacket, baseUrl }), held: null };
|
|
2757
|
+
} catch (error) {
|
|
2758
|
+
if (!isNoCapturableRoutesError(error)) throw error;
|
|
2759
|
+
return { plan: null, held: error };
|
|
2760
|
+
}
|
|
2761
|
+
};
|
|
2762
|
+
const { plan, held: heldPlanError } = planOrHeld(packet);
|
|
2702
2763
|
const initialBinding = createPolishCaptureBinding({
|
|
2703
2764
|
packet,
|
|
2704
2765
|
report,
|
|
@@ -2711,7 +2772,7 @@ export async function polishCaptureCommand(args, options = {}) {
|
|
|
2711
2772
|
if (typeof createBrowserAdapter !== "function") {
|
|
2712
2773
|
({ createPolishBrowserAdapter: createBrowserAdapter } = await import("./polish-browser.mjs"));
|
|
2713
2774
|
}
|
|
2714
|
-
const capture = await capturePolishPageLoad({
|
|
2775
|
+
const capture = await (heldPlanError ? capturePolishReadability : capturePolishPageLoad)({
|
|
2715
2776
|
packet,
|
|
2716
2777
|
report,
|
|
2717
2778
|
baseUrl,
|
|
@@ -2721,7 +2782,11 @@ export async function polishCaptureCommand(args, options = {}) {
|
|
|
2721
2782
|
adapterStartupDeadlineMs: options.adapterStartupDeadlineMs,
|
|
2722
2783
|
captureCellDeadlineMs: options.captureCellDeadlineMs,
|
|
2723
2784
|
adapterCloseDeadlineMs: options.adapterCloseDeadlineMs,
|
|
2785
|
+
probeClock: options.probeClock,
|
|
2786
|
+
targetRepo,
|
|
2724
2787
|
});
|
|
2788
|
+
// Nothing was measured: the held plan error stands.
|
|
2789
|
+
if (heldPlanError && !capture.readability) throw heldPlanError;
|
|
2725
2790
|
|
|
2726
2791
|
if (typeof options.afterCapture === "function") {
|
|
2727
2792
|
await options.afterCapture({ packetPath, reportPath, targetRepo, capture });
|
|
@@ -2734,7 +2799,7 @@ export async function polishCaptureCommand(args, options = {}) {
|
|
|
2734
2799
|
let checkpoint = null;
|
|
2735
2800
|
commitAssemblyReport(workspace, (currentReport) => {
|
|
2736
2801
|
requireValidPolishCaptureReport(currentReport, reportPath);
|
|
2737
|
-
const currentPlan =
|
|
2802
|
+
const { plan: currentPlan } = planOrHeld(currentPacket);
|
|
2738
2803
|
const currentBinding = createPolishCaptureBinding({
|
|
2739
2804
|
packet: currentPacket,
|
|
2740
2805
|
report: currentReport,
|
|
@@ -2744,7 +2809,12 @@ export async function polishCaptureCommand(args, options = {}) {
|
|
|
2744
2809
|
});
|
|
2745
2810
|
assertPolishCaptureBindingUnchanged(initialBinding, currentBinding);
|
|
2746
2811
|
|
|
2747
|
-
const merged = mergePolishCaptureEvidence(currentReport, {
|
|
2812
|
+
const merged = mergePolishCaptureEvidence(currentReport, {
|
|
2813
|
+
pageLoad: capture.page_load,
|
|
2814
|
+
mediaWeight: capture.media_weight,
|
|
2815
|
+
readability: capture.readability,
|
|
2816
|
+
now: options.now ?? new Date(),
|
|
2817
|
+
});
|
|
2748
2818
|
checkpoint = evaluateRecordedHiddenEagerMediaCheckpoint({
|
|
2749
2819
|
packet: currentPacket,
|
|
2750
2820
|
report: merged,
|
|
@@ -2755,19 +2825,39 @@ export async function polishCaptureCommand(args, options = {}) {
|
|
|
2755
2825
|
staleReason: `Package-owned polish page-load evidence changed after this doctor snapshot. Re-run ${cmd("doctor")} (or next) for current state.`,
|
|
2756
2826
|
});
|
|
2757
2827
|
|
|
2828
|
+
if (heldPlanError) {
|
|
2829
|
+
// The readability capture completed; page_load's own outcome stays the
|
|
2830
|
+
// held no_capturable_routes message, reported as a warning.
|
|
2831
|
+
return {
|
|
2832
|
+
ok: true,
|
|
2833
|
+
status: "captured_with_warnings",
|
|
2834
|
+
action: "polish-capture",
|
|
2835
|
+
report_path: reportPath,
|
|
2836
|
+
measurement: null,
|
|
2837
|
+
checkpoint,
|
|
2838
|
+
observed_findings: [],
|
|
2839
|
+
warnings: [{ code: heldPlanError.code, message: heldPlanError.message }],
|
|
2840
|
+
capture: {
|
|
2841
|
+
route_scope: null,
|
|
2842
|
+
routes: [],
|
|
2843
|
+
viewports: [],
|
|
2844
|
+
readability_routes: capture.readability?.subject?.routes ?? [],
|
|
2845
|
+
},
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2758
2848
|
const ok = checkpoint.status === "pass" || checkpoint.status === "waived";
|
|
2759
2849
|
return {
|
|
2760
2850
|
ok,
|
|
2761
2851
|
status: ok ? (checkpoint.status === "waived" ? "ready_with_waivers" : "ready") : "blocked",
|
|
2762
2852
|
action: "polish-capture",
|
|
2763
2853
|
report_path: reportPath,
|
|
2764
|
-
measurement: capture.page_load
|
|
2854
|
+
measurement: capture.page_load?.measurement ?? null,
|
|
2765
2855
|
checkpoint,
|
|
2766
|
-
observed_findings: capture.page_load
|
|
2856
|
+
observed_findings: capture.page_load?.findings ?? [],
|
|
2767
2857
|
capture: {
|
|
2768
|
-
route_scope: capture.plan
|
|
2769
|
-
routes: capture.plan
|
|
2770
|
-
viewports: capture.plan
|
|
2858
|
+
route_scope: capture.plan?.route_scope ?? null,
|
|
2859
|
+
routes: (capture.plan?.routes ?? []).map((route) => route.requested_route),
|
|
2860
|
+
viewports: (capture.plan?.viewports ?? []).map((viewport) => viewport.key),
|
|
2771
2861
|
},
|
|
2772
2862
|
};
|
|
2773
2863
|
}
|
|
@@ -3509,7 +3599,6 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3509
3599
|
store: storeFlags
|
|
3510
3600
|
? { subdomain: storeFlags.subdomain, admin_api: adminApiBaseForStore(storeFlags.subdomain), ...(storeFlags.token_env ? {} : { transport: "gateway", endpoint: "https://mcp.nextcommerce.com/admin/" }), token_source: storeFlags.token_env ? `env:${storeFlags.token_env}` : "gateway:login", store_read: null, pages_read: null, primary_domain: null }
|
|
3511
3601
|
: null,
|
|
3512
|
-
rebound: { build_context: null, assembly_report: null },
|
|
3513
3602
|
errors: [],
|
|
3514
3603
|
warnings: [],
|
|
3515
3604
|
next: `${cmd("doctor")} --packet ${shellToken(packetPath)}`,
|
|
@@ -3743,17 +3832,15 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3743
3832
|
addIssue(result.warnings, "spec.derive.projection_stale", `A page route ${dryRun ? "would move" : "moved"}, and the packet's page-kit projection (source_html.pages[].page_kit) and the Build Context were prepared from the old routes. Re-run ${cmd("prepare-build")} (or start) before the next build so they describe the routes the spec carries.`);
|
|
3744
3833
|
}
|
|
3745
3834
|
if (stageIsTerminal(report?.stages?.assembly?.status)) {
|
|
3746
|
-
addIssue(result.warnings, "spec.derive.build_stale", `The Assembly Report records a terminal build (stages.assembly.status ${report.stages.assembly.status}) rendered from the spec ${dryRun ? "this would rewrite" : "just rewritten"}.
|
|
3747
|
-
result.next = `${cmd("doctor")} --packet ${shellToken(packetPath)}, then rebuild: set stages.assembly.status back to "pending" on the Assembly Report and run ${cmd("next")} --packet ${shellToken(packetPath)}`;
|
|
3835
|
+
addIssue(result.warnings, "spec.derive.build_stale", `The Assembly Report records a terminal build (stages.assembly.status ${report.stages.assembly.status}) rendered from the spec ${dryRun ? "this would rewrite" : "just rewritten"}. Bind the rewritten spec with ${cmd("record")} spec (it keeps stage history); next then routes back to the build, and Polish and QA are owed again.`);
|
|
3748
3836
|
}
|
|
3837
|
+
// spec derive writes the spec alone: the Build Context and the Assembly
|
|
3838
|
+
// Report keep the identity they bound, so every recorded stage reads its
|
|
3839
|
+
// stamps against the new content and `record spec` is what binds it.
|
|
3840
|
+
if (report) result.next = `${cmd("record")} spec --packet ${shellToken(packetPath)} ${dryRun ? "after the write " : ""}to bind the derived spec (it keeps stage history), then ${cmd("next")} --packet ${shellToken(packetPath)}`;
|
|
3749
3841
|
}
|
|
3750
3842
|
|
|
3751
3843
|
if (plan.changes.length && !dryRun) {
|
|
3752
|
-
// The identity the sidecars bound to the spec BEFORE this write, so the
|
|
3753
|
-
// re-bind below can tell "bound to the spec being replaced" from "already
|
|
3754
|
-
// drifted" and only ever moves the former.
|
|
3755
|
-
const beforeRawHash = createHash("sha256").update(text).digest("hex");
|
|
3756
|
-
const beforeMaterialHash = specMaterialHash(spec);
|
|
3757
3844
|
// The plan already refused every path its containers cannot take, so a
|
|
3758
3845
|
// throw here is a defect in this toolkit rather than in the spec; it is
|
|
3759
3846
|
// still a structured error, never a crash past the --json contract.
|
|
@@ -3791,82 +3878,7 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3791
3878
|
rmSync(tmpPath, { force: true });
|
|
3792
3879
|
}
|
|
3793
3880
|
result.written = true;
|
|
3794
|
-
// The Build Context and the Assembly Report carry the spec's identity
|
|
3795
|
-
// (raw and material hashes) from prepare-build, and QA's verdict is
|
|
3796
|
-
// correlated against the material hash by the bundle check. Each sidecar
|
|
3797
|
-
// that was bound to the spec just replaced is re-bound to the new one;
|
|
3798
|
-
// one that already carried another identity is left as it is and named.
|
|
3799
|
-
const afterRawHash = createHash("sha256").update(serialized).digest("hex");
|
|
3800
|
-
const afterMaterialHash = specMaterialHash(spec);
|
|
3801
|
-
const boundToOld = (raw, material) => raw === beforeRawHash || material === beforeMaterialHash;
|
|
3802
|
-
// A sidecar is re-bound only when it names the spec being written: two
|
|
3803
|
-
// packets sharing a target repo can carry byte-identical spec exports,
|
|
3804
|
-
// and a matching hash alone would let one packet's derive re-bind the
|
|
3805
|
-
// other's sidecar to a spec it never used.
|
|
3806
|
-
const namesThisSpec = (recorded) => {
|
|
3807
|
-
if (!isNonEmptyString(recorded)) return false;
|
|
3808
|
-
try {
|
|
3809
|
-
return realpathSync(resolve(targetRepo, recorded)) === realSpecPath;
|
|
3810
|
-
} catch {
|
|
3811
|
-
return false;
|
|
3812
|
-
}
|
|
3813
|
-
};
|
|
3814
|
-
const contextPath = workspace?.contextPath || null;
|
|
3815
|
-
let context = null;
|
|
3816
|
-
try {
|
|
3817
|
-
context = contextPath ? readJsonIfExists(contextPath) : null;
|
|
3818
|
-
} catch (error) {
|
|
3819
|
-
result.rebound.build_context = false;
|
|
3820
|
-
addIssue(result.warnings, "spec.derive.identity_not_rebound", `The Build Context could not be read (${singleLineDetail(error.message)}); its spec identity was not updated. Re-run prepare-build before QA so the bundle correlates.`);
|
|
3821
|
-
}
|
|
3822
|
-
if (isObject(context?.spec)) {
|
|
3823
|
-
if (!namesThisSpec(context.spec.path)) {
|
|
3824
|
-
result.rebound.build_context = false;
|
|
3825
|
-
addIssue(result.warnings, "spec.derive.identity_not_rebound", "The Build Context names a different spec file than the one derive wrote (another packet's, or a moved export); it was left as it is. Re-run prepare-build before QA so the bundle correlates.");
|
|
3826
|
-
} else if (boundToOld(context.spec.hash, context.spec.material_hash)) {
|
|
3827
|
-
try {
|
|
3828
|
-
writeJsonAtomic(contextPath, { ...context, spec: { ...context.spec, hash: afterRawHash, material_hash: afterMaterialHash } });
|
|
3829
|
-
result.rebound.build_context = true;
|
|
3830
|
-
} catch (error) {
|
|
3831
|
-
result.rebound.build_context = false;
|
|
3832
|
-
addIssue(result.warnings, "spec.derive.identity_not_rebound", `The Build Context's spec identity could not be updated (${singleLineDetail(error.message)}); re-run prepare-build before QA so the bundle correlates.`);
|
|
3833
|
-
}
|
|
3834
|
-
} else {
|
|
3835
|
-
result.rebound.build_context = false;
|
|
3836
|
-
addIssue(result.warnings, "spec.derive.identity_not_rebound", `The Build Context's spec identity was already bound to a different spec than the one derive replaced; it was left as it is. Re-run prepare-build before QA so the bundle correlates.`);
|
|
3837
|
-
}
|
|
3838
|
-
}
|
|
3839
|
-
if (report && isObject(report.identity) && workspace) {
|
|
3840
|
-
if (!namesThisSpec(report.inputs?.spec_path)) {
|
|
3841
|
-
result.rebound.assembly_report = false;
|
|
3842
|
-
addIssue(result.warnings, "spec.derive.identity_not_rebound", "The Assembly Report names a different spec file than the one derive wrote (another packet's, or a moved export); it was left as it is. Re-run prepare-build before QA so the bundle correlates.");
|
|
3843
|
-
} else if (boundToOld(report.identity.spec_hash, report.identity.spec_material_hash)) {
|
|
3844
|
-
try {
|
|
3845
|
-
// The identity is re-checked on the report as it is re-read for
|
|
3846
|
-
// the commit, so a prepare-build that re-bound it in the meantime
|
|
3847
|
-
// is left alone.
|
|
3848
|
-
const committed = commitAssemblyReport(workspace, (current) => (
|
|
3849
|
-
isObject(current.identity) && boundToOld(current.identity.spec_hash, current.identity.spec_material_hash)
|
|
3850
|
-
? { ...current, identity: { ...current.identity, spec_hash: afterRawHash, spec_material_hash: afterMaterialHash } }
|
|
3851
|
-
: null
|
|
3852
|
-
), {
|
|
3853
|
-
command: "spec derive",
|
|
3854
|
-
staleReason: `spec derive rewrote the CampaignSpec after this doctor snapshot. Re-run ${cmd("doctor")} (or next) for current state.`,
|
|
3855
|
-
});
|
|
3856
|
-
result.rebound.assembly_report = committed.written;
|
|
3857
|
-
if (!committed.written) addIssue(result.warnings, "spec.derive.identity_not_rebound", "The Assembly Report's spec identity moved while spec derive was running; it was left as it is. Re-run prepare-build before QA so the bundle correlates.");
|
|
3858
|
-
} catch (error) {
|
|
3859
|
-
result.rebound.assembly_report = false;
|
|
3860
|
-
addIssue(result.warnings, "spec.derive.identity_not_rebound", `The Assembly Report's spec identity could not be updated (${singleLineDetail(error.message)}); re-run prepare-build before QA so the bundle correlates.`);
|
|
3861
|
-
}
|
|
3862
|
-
} else {
|
|
3863
|
-
result.rebound.assembly_report = false;
|
|
3864
|
-
addIssue(result.warnings, "spec.derive.identity_not_rebound", `The Assembly Report's spec identity was already bound to a different spec than the one derive replaced; it was left as it is. Re-run prepare-build before QA so the bundle correlates.`);
|
|
3865
|
-
}
|
|
3866
|
-
}
|
|
3867
3881
|
// The retained doctor snapshot (if any) now predates the spec it judged.
|
|
3868
|
-
// commitAssemblyReport stamps it when the report was re-bound; every
|
|
3869
|
-
// other path stamps it here.
|
|
3870
3882
|
try {
|
|
3871
3883
|
markDoctorSidecarStale(targetRepo, {
|
|
3872
3884
|
command: "spec derive",
|
|
@@ -4245,6 +4257,8 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4245
4257
|
followContextPointer: true,
|
|
4246
4258
|
});
|
|
4247
4259
|
const report = readJsonIfExists(reportPath);
|
|
4260
|
+
// The campaign intent summary, computed once per call and only printed.
|
|
4261
|
+
const intentSummary = readIntentSummary(packetPath, packet, contextPath);
|
|
4248
4262
|
// Doctor reads the same sidecars through the same resolver (an operator's
|
|
4249
4263
|
// explicit --context / --report passed through, the defaults derived), so
|
|
4250
4264
|
// its prepare-build gate and its stage pick are this command's: `next`
|
|
@@ -4330,6 +4344,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4330
4344
|
} });
|
|
4331
4345
|
if (divergences.length) result.divergences = divergences;
|
|
4332
4346
|
result.gates = buildNextGates({ doctor, report, themeGate, polishGate, prepareBuildGate, packetPath });
|
|
4347
|
+
if (doctor.derived?.input_currency) result.input_currency = doctor.derived.input_currency;
|
|
4333
4348
|
result.next_actions = buildNextActions({ result, packetPath, packet, themeGate, polishGate, polishCheckpointGate, prepareBuildGate, ambient, runRecordCloseout, purchaseProof, brandContract: doctor.derived?.brand_contract || null, context: readJsonIfExists(contextPath), targetRepo, spec });
|
|
4334
4349
|
// The QC handoff: read from data already loaded plus the
|
|
4335
4350
|
// full QA verdict the QA stage names. It adds nothing to errors[],
|
|
@@ -4338,7 +4353,11 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4338
4353
|
// one (from --report or the Build Context pointer), so it records there.
|
|
4339
4354
|
const qc = readCurrentQcResults({ report, doctor, spec, targetRepo, packetPath, reportPath, qcStandIns, rederivers: qcRederivers });
|
|
4340
4355
|
result.qc_handoff = buildQcHandoff({ results: qc.results, coverage: qc.coverage, accepts: report?.qc_accepts, packetPath, reportPath: explicitReportPath(reportPath, targetRepo) });
|
|
4356
|
+
// One note per operator decision that kept the build output unchanged
|
|
4357
|
+
// after an input change, quoting the operator's reason.
|
|
4358
|
+
for (const issue of doctor.warnings) if (issue.code === "assembly.output_unchanged_by_operator_decision") (result.qc_handoff.notes ||= []).push(`Build output kept unchanged after an input change by the operator's decision: "${issue.detail.reason}"`);
|
|
4341
4359
|
recordNextRecommendation(ambient, result);
|
|
4360
|
+
result.intent_summary = intentSummary;
|
|
4342
4361
|
return result;
|
|
4343
4362
|
};
|
|
4344
4363
|
const doctorHasOnlyPolishGateErrors = doctorErrorsAreOnlyPolishGate(doctor.errors);
|
|
@@ -4430,12 +4449,12 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4430
4449
|
if (stage === "setup") {
|
|
4431
4450
|
addPrepareBuildGateErrors(errors, report);
|
|
4432
4451
|
if (!doctor.ok && !doctorHasOnlyPolishGateErrors) addIssue(errors, "next.setup.doctor", "Doctor is blocked; resolve packet errors before setup.");
|
|
4433
|
-
prompt = setupPrompt(packetPath, contextPath, reportPath, packet);
|
|
4452
|
+
prompt = setupPrompt(packetPath, contextPath, reportPath, packet, intentSummary);
|
|
4434
4453
|
} else if (stage === "build") {
|
|
4435
4454
|
addPrepareBuildGateErrors(errors, report);
|
|
4436
4455
|
if (!doctor.ok && !doctorHasOnlyPolishGateErrors) addIssue(errors, "next.build.doctor", "Doctor is blocked; resolve packet errors before build.");
|
|
4437
4456
|
if (doctor.derived?.scaffold_required) addIssue(errors, "next.build.setup", doctor.derived.scaffold_reason || "Setup is required before build.");
|
|
4438
|
-
prompt = buildPrompt(packetPath, contextPath, reportPath, packet, doctor.derived);
|
|
4457
|
+
prompt = buildPrompt(packetPath, contextPath, reportPath, packet, doctor.derived, intentSummary);
|
|
4439
4458
|
} else if (stage === "polish") {
|
|
4440
4459
|
addPrepareBuildGateErrors(errors, report);
|
|
4441
4460
|
if (!report) addIssue(errors, "next.polish.report", "Assembly report is required before polish.");
|
|
@@ -4443,7 +4462,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4443
4462
|
if (!assemblyStatus.startsWith("completed")) addIssue(errors, "next.polish.assembly", `Assembly status is "${assemblyStatus || "missing"}"; polish expects completed assembly or an explicit blocked/skipped handoff.`);
|
|
4444
4463
|
if (polishGateRequiresBuild(polishGate)) addPolishGateErrors(errors, polishGate, "polish");
|
|
4445
4464
|
addThemeGateErrors(errors, themeGate, "polish");
|
|
4446
|
-
prompt = polishPrompt(packetPath, reportPath, packet);
|
|
4465
|
+
prompt = polishPrompt(packetPath, reportPath, packet, intentSummary);
|
|
4447
4466
|
} else if (stage === "deploy") {
|
|
4448
4467
|
addPrepareBuildGateErrors(errors, report);
|
|
4449
4468
|
// Slice 3 Phase 2: deploy is an out-of-band step (Netlify / CF Pages /
|
|
@@ -4462,7 +4481,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4462
4481
|
addPolishGateErrors(errors, polishGate, "qa");
|
|
4463
4482
|
addPolishCheckpointGateErrors(errors, polishCheckpointGate, "qa");
|
|
4464
4483
|
addThemeGateErrors(errors, themeGate, "qa");
|
|
4465
|
-
prompt = qaPrompt(packetPath, reportPath, packet, spec);
|
|
4484
|
+
prompt = qaPrompt(packetPath, reportPath, packet, spec, intentSummary);
|
|
4466
4485
|
}
|
|
4467
4486
|
const status = errors.length
|
|
4468
4487
|
? "blocked"
|
|
@@ -4757,6 +4776,11 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4757
4776
|
push(inspect.id, inspect.kind, asInvocation(inspect.command), inspect.description, { required: inspect.required });
|
|
4758
4777
|
return actions;
|
|
4759
4778
|
}
|
|
4779
|
+
for (const subcommand of inputRefreshCommands(result.input_currency)) {
|
|
4780
|
+
push("refresh_inputs", "command", `${cmd("record")} ${subcommand} --packet ${shellToken(packetPath)} --json`, subcommand === "brief"
|
|
4781
|
+
? "A recorded stage was made against earlier Campaign Build Brief content: save the brief with record brief (keeps stage history), then re-run next."
|
|
4782
|
+
: "A recorded stage was made against earlier CampaignSpec content: bind it with record spec (keeps stage history), then re-run next.");
|
|
4783
|
+
}
|
|
4760
4784
|
if (result.stage === "doctor-blocked") {
|
|
4761
4785
|
const checkpointGates = (result.gates || []).filter(
|
|
4762
4786
|
(gate) => gate?.status === "blocked" && Object.hasOwn(CHECKPOINT_EVALUATORS, gate?.id || ""),
|
|
@@ -5008,9 +5032,9 @@ function templateStockPromptLine(packet, derived = {}) {
|
|
|
5008
5032
|
return `\n- Template-stock pages (declared out of source scope; no design source exists for them): ${pages.join(", ")}. Keep these routes unbuilt by default, especially presell/landing pages staying on another host; never publish stock placeholder copy to fill a partial build. Materialise a page only with explicit operator opt-in for that page (including a pre-checkout select stand-in required by the runtime). For opted-in pages, use the ${packet.assembly.template_family} family's own page for that role with its dependent _includes, _layouts, and assets, wire it from CampaignSpec, and build a required select step first. Once its built HTML exists, doctor lists it among the previewable routes.`;
|
|
5009
5033
|
}
|
|
5010
5034
|
|
|
5011
|
-
function buildPrompt(packetPath, contextPath, reportPath, packet, derived = {}) {
|
|
5035
|
+
function buildPrompt(packetPath, contextPath, reportPath, packet, derived = {}, intent) {
|
|
5012
5036
|
const briefPath = packet.build_brief?.normalized_path || "(missing; generate or confirm Campaign Build Brief before business-sensitive assembly)";
|
|
5013
|
-
return
|
|
5037
|
+
return `${campaignIntentPromptHeader(intent)}Use next-campaigns-build for this Campaigns OS handoff.
|
|
5014
5038
|
|
|
5015
5039
|
Read first:
|
|
5016
5040
|
- Build Packet: ${packetPath}
|
|
@@ -5023,6 +5047,7 @@ Read first:
|
|
|
5023
5047
|
Rules:
|
|
5024
5048
|
- Treat CampaignSpec/API as the source for package, shipping, voucher, payment, tracking, footer, and SEO values.
|
|
5025
5049
|
- Treat the Campaign Build Brief as the merchandising/design presentation truth: page authority, palette/CTA style, variant media rules, pricing display strategy, the template's own promo placeholders, payment/trust surfaces, display-name policy, residue policy, and QA expectations. Agents may resolve implementation uncertainty; unresolved brief questions are business uncertainty and should be asked or recorded, not guessed.
|
|
5050
|
+
- Refresh a changed Campaign Build Brief with \`${cmd("record")} brief --packet ${packetPath}\` and a changed CampaignSpec with \`${cmd("record")} spec --packet ${packetPath}\` before building on the change.
|
|
5026
5051
|
- Read the selected template family's agentContract and sharedFrontmatterVocabulary before commerce wiring.
|
|
5027
5052
|
- Prepared AI/exported HTML must be converted into page-kit-ready source first: keep page-owned body markup, strip document wrappers, add YAML frontmatter, move shared CSS/assets into the campaign structure, and use Liquid helpers only for page-kit links/assets/includes. Page Kit publishes src/<slug>/assets/config.js as /<slug>/config.js and src/<slug>/assets/products/foo.png as /<slug>/products/foo.png; do not leave raw /assets/... or /<slug>/assets/... references in rendered pages.
|
|
5028
5053
|
- Preserve prepared source HTML for landing/presell pages when it is a real standalone design.
|
|
@@ -5049,9 +5074,44 @@ function localProofPromptLines(packet, packetPath = "<packet>") {
|
|
|
5049
5074
|
- Local proof mode (deploy.target is local-serve): run the page-kit build in the ${LOCAL_PROOF_BUILD_ENVIRONMENT} environment — \`${LOCAL_PROOF_BUILD_COMMAND}\` — so _site/ is the development render, and record it with \`${cmd("record")} build --packet ${packetPath} --build-environment ${LOCAL_PROOF_BUILD_ENVIRONMENT}\`, which sets ${LOCAL_PROOF_BUILD_ENVIRONMENT_FIELD} to "${LOCAL_PROOF_BUILD_ENVIRONMENT}" on the assembly report (never hand-edit it). The starter templates gate every vendor loader on the environment and several loaders are protocol-relative (//host/...), which fail over a plain-HTTP local serve; the SDK's dl_* events still fire in development. Polish capture, browser QA and typed-card orders run against this served output. Before committing, run \`${asInvocation(LOCAL_PROOF_PARITY_COMMAND)}\` to prove the production render differs only in environment-gated output and pins the same Campaign Cart version; the PR preview is the second check. ${LOCAL_PROOF_NEVER_EDIT_RULE}`;
|
|
5050
5075
|
}
|
|
5051
5076
|
|
|
5052
|
-
|
|
5077
|
+
// Read the normalized brief the packet records, resolved against the packet
|
|
5078
|
+
// file as doctor resolves it, and summarize it over the Build Context's active
|
|
5079
|
+
// pages in recorded order. A missing, unreadable or invalid brief reads
|
|
5080
|
+
// unavailable; this never fails `next`. Each read or summarising failure is
|
|
5081
|
+
// named on stderr, one line per failure, never on stdout.
|
|
5082
|
+
function readIntentSummary(packetPath, packet, contextPath) {
|
|
5083
|
+
const reportFailure = (error) => process.stderr.write(`campaigns-os: intent summary unavailable: ${singleLineFragment(error?.message ?? error, "unknown error")}\n`);
|
|
5084
|
+
const normalizedPath = resolveFromFile(packetPath, packet?.build_brief?.normalized_path);
|
|
5085
|
+
let brief = null;
|
|
5086
|
+
try {
|
|
5087
|
+
brief = readJsonIfExists(normalizedPath);
|
|
5088
|
+
} catch (error) {
|
|
5089
|
+
reportFailure(error);
|
|
5090
|
+
brief = null;
|
|
5091
|
+
}
|
|
5092
|
+
let activePageIds = [];
|
|
5093
|
+
try {
|
|
5094
|
+
const activePages = readJsonIfExists(contextPath)?.spec?.active_pages;
|
|
5095
|
+
activePageIds = Array.isArray(activePages) ? activePages.map((page) => page?.id) : [];
|
|
5096
|
+
} catch (error) {
|
|
5097
|
+
reportFailure(error);
|
|
5098
|
+
activePageIds = [];
|
|
5099
|
+
}
|
|
5100
|
+
return { ...summarizeCampaignBrief({ brief, activePageIds, onError: reportFailure }), normalized_path: normalizedPath };
|
|
5101
|
+
}
|
|
5102
|
+
|
|
5103
|
+
// The setup, build, Polish and QA prompts start with the campaign intent
|
|
5104
|
+
// summary, framed as orientation only.
|
|
5105
|
+
function campaignIntentPromptHeader(intent) {
|
|
5106
|
+
return `Campaign intent (from the Campaign Build Brief at ${intent.normalized_path || "(not recorded)"}; orientation only, never a source of prices or commerce behaviour):
|
|
5107
|
+
${intent.text}
|
|
5108
|
+
|
|
5109
|
+
`;
|
|
5110
|
+
}
|
|
5111
|
+
|
|
5112
|
+
function setupPrompt(packetPath, contextPath, reportPath, packet, intent) {
|
|
5053
5113
|
const briefPath = packet.build_brief?.normalized_path || "(missing)";
|
|
5054
|
-
return
|
|
5114
|
+
return `${campaignIntentPromptHeader(intent)}Use next-campaigns-os-setup for this Campaigns OS handoff.
|
|
5055
5115
|
|
|
5056
5116
|
Read first:
|
|
5057
5117
|
- Build Packet: ${packetPath}
|
|
@@ -5070,9 +5130,9 @@ When copying a starter template family, copy the family as an atomic page-kit sl
|
|
|
5070
5130
|
Do not wire checkout, upsell, receipt, payment, package, voucher, or shipping behavior during setup.`;
|
|
5071
5131
|
}
|
|
5072
5132
|
|
|
5073
|
-
function polishPrompt(packetPath, reportPath, packet) {
|
|
5133
|
+
function polishPrompt(packetPath, reportPath, packet, intent) {
|
|
5074
5134
|
const briefPath = packet.build_brief?.normalized_path || "(missing)";
|
|
5075
|
-
return
|
|
5135
|
+
return `${campaignIntentPromptHeader(intent)}Use next-campaigns-polish for this built campaign.
|
|
5076
5136
|
|
|
5077
5137
|
Read first:
|
|
5078
5138
|
- Build Packet: ${packetPath}
|
|
@@ -5215,14 +5275,14 @@ function orderBumpRunReason(bumpCart) {
|
|
|
5215
5275
|
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.`;
|
|
5216
5276
|
}
|
|
5217
5277
|
|
|
5218
|
-
function qaPrompt(packetPath, reportPath, packet, spec = null) {
|
|
5278
|
+
function qaPrompt(packetPath, reportPath, packet, spec = null, intent) {
|
|
5219
5279
|
const url = packet.deploy?.preview_url || packet.deploy?.production_url || "<preview-url>";
|
|
5220
5280
|
const briefPath = packet.build_brief?.normalized_path || "(missing)";
|
|
5221
5281
|
const bumpCart = checkoutOrderBumpCart(spec);
|
|
5222
5282
|
const bumpCommand = bumpCart
|
|
5223
5283
|
? `\n\nOrder bump QA command (proves the charged add-on):\n${qaRunCommand(packetPath, url, bumpCart)}\n${orderBumpRunReason(bumpCart)}`
|
|
5224
5284
|
: "";
|
|
5225
|
-
return
|
|
5285
|
+
return `${campaignIntentPromptHeader(intent)}Use next-campaigns-qa for this deployed campaign.
|
|
5226
5286
|
|
|
5227
5287
|
${packet.spec.local_spec_id ? "Local spec ID" : "Map ID"}: ${packet.spec.local_spec_id || packet.spec.map_id}
|
|
5228
5288
|
Base URL: ${url}
|
|
@@ -6396,7 +6456,7 @@ function safePolishByteCount(value) {
|
|
|
6396
6456
|
}
|
|
6397
6457
|
|
|
6398
6458
|
export function formatPolishCaptureText(result) {
|
|
6399
|
-
const status = ["ready", "ready_with_waivers", "blocked"].includes(result?.status)
|
|
6459
|
+
const status = ["ready", "ready_with_waivers", "captured_with_warnings", "blocked"].includes(result?.status)
|
|
6400
6460
|
? result.status
|
|
6401
6461
|
: "unknown";
|
|
6402
6462
|
const measurementStatus = ["complete", "incomplete"].includes(result?.measurement?.status)
|
|
@@ -6410,6 +6470,11 @@ export function formatPolishCaptureText(result) {
|
|
|
6410
6470
|
`Status: ${status.toUpperCase()}`,
|
|
6411
6471
|
`Measurement: ${measurementStatus.toUpperCase()}`,
|
|
6412
6472
|
];
|
|
6473
|
+
// Outcomes that did not stop the capture, printed with their own message
|
|
6474
|
+
// (the held page-load plan message on a campaign with no mapped pages).
|
|
6475
|
+
for (const warning of Array.isArray(result?.warnings) ? result.warnings : []) {
|
|
6476
|
+
if (warning?.code === "no_capturable_routes" && typeof warning.message === "string") lines.push(`Warning: ${warning.message}`);
|
|
6477
|
+
}
|
|
6413
6478
|
const incomplete = Array.isArray(result?.measurement?.incomplete)
|
|
6414
6479
|
? result.measurement.incomplete
|
|
6415
6480
|
: [];
|
|
@@ -8411,6 +8476,8 @@ const WAIVE_ACTIONS = new Set(["theme-waive", "checkpoint-waive"]);
|
|
|
8411
8476
|
// check's readiness line); they are text-only and never enter the JSON result.
|
|
8412
8477
|
export function resultTextLines(result, { headerLines = [] } = {}) {
|
|
8413
8478
|
const lines = [`Status: ${String(result.status || "unknown").toUpperCase()}`, ...headerLines];
|
|
8479
|
+
// `next` only: the campaign intent summary, directly under the status.
|
|
8480
|
+
if (result.intent_summary) lines.push("Campaign intent:", ...result.intent_summary.text.split("\n"));
|
|
8414
8481
|
// A waive command's second line names what it recorded; the third is the
|
|
8415
8482
|
// stage doctor now picks for the report the waiver was written to.
|
|
8416
8483
|
if (WAIVE_ACTIONS.has(result.action) && result.gate) {
|