@nextcommerce/campaigns-os 1.52.0 → 1.53.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 +48 -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/effects.v1.json +201 -12
- package/contracts/release-ledger.json +327 -0
- package/contracts/supported-surface.json +2 -2
- package/docs/brand-theme-bridge.md +2 -0
- package/docs/build-packet.md +56 -15
- package/docs/campaign-build-brief.md +48 -17
- package/docs/effects.md +8 -0
- 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 +2 -4
- package/docs/qa-and-test-orders.md +2 -0
- package/docs/readback.md +19 -2
- package/docs/runtime-readiness.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/package.json +1 -1
- package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +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/cli.mjs +160 -112
- package/src/contrast.mjs +649 -0
- package/src/design-source-publication.mjs +110 -3
- package/src/doctor/checks.mjs +129 -14
- 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 +3 -1
- 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 +15 -3
- package/src/qa-browser.mjs +131 -84
- package/src/qa-node.mjs +14 -3
- 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/stage-ledger.mjs +235 -44
- package/src/stage-record.mjs +119 -26
- 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";
|
|
@@ -328,6 +331,8 @@ Usage:
|
|
|
328
331
|
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
332
|
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
333
|
campaigns-os polish capture --packet <campaign-runtime.build.json> --base-url <url> [--report <json>] [--headed] [--auth-cookie <cookie>] [--json]
|
|
334
|
+
campaigns-os record brief --packet <campaign-runtime.build.json> [--brief <yaml|json>] [--context <json>] [--report <json>] [--dry-run] [--json]
|
|
335
|
+
campaigns-os record spec --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--dry-run] [--json]
|
|
331
336
|
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
337
|
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
338
|
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
|
|
@@ -709,6 +714,10 @@ export function recordQaStageOutcome(args, result) {
|
|
|
709
714
|
const failed = (Array.isArray(verdict.assertions) ? verdict.assertions : [])
|
|
710
715
|
.filter((assertion) => assertion?.status === "fail")
|
|
711
716
|
.map((assertion) => `${assertion.id}: ${assertion.actual || "assertion failed"}`);
|
|
717
|
+
// The QA stamps are the verdict's: the brief material qa run bound at run
|
|
718
|
+
// start and the spec material it judged, never the inputs at write time.
|
|
719
|
+
const qaStamps = inputStamps({ briefMaterial: verdict.source_brief_material, specMaterial: verdict.spec_hash });
|
|
720
|
+
const qaInputs = stageWriteInputs("qa", currentPacketInputs({ packet, packetPath }));
|
|
712
721
|
const committed = commitAssemblyReport(workspace, (report) => {
|
|
713
722
|
if (hasLocalIdentity && !specHashesMatch(verdict.spec_hash, report.identity?.spec_material_hash)) {
|
|
714
723
|
throw new Error("Local-spec QA verdict belongs to a different material revision; report evidence was not changed.");
|
|
@@ -734,6 +743,8 @@ export function recordQaStageOutcome(args, result) {
|
|
|
734
743
|
// summarizePurchaseProof). This is what lets `next` tell a real purchase
|
|
735
744
|
// path from a `--test-order off` diagnostic.
|
|
736
745
|
proof: summarizePurchaseProof({ verdict, proofPolicy: packet.qa?.proof_policy }),
|
|
746
|
+
stamps: qaStamps,
|
|
747
|
+
inputs: qaInputs,
|
|
737
748
|
});
|
|
738
749
|
}, {
|
|
739
750
|
stage: "qa",
|
|
@@ -1438,8 +1449,7 @@ function createInitialAssemblyReportStages({ scaffoldRequired, blockers, outputs
|
|
|
1438
1449
|
// (STAGE_TERMINAL_STATUS_PREFIXES): declared out-of-scope pages do not hold
|
|
1439
1450
|
// the ladder, they are recorded on the stage so downstream consumers can see
|
|
1440
1451
|
// exactly which pages assemble from the template family instead of source HTML.
|
|
1441
|
-
|
|
1442
|
-
stages.prepare_build = createStage("prepare_build", blockers.length ? "blocked" : terminalStatus, {
|
|
1452
|
+
stages.prepare_build = createStage("prepare_build", prepareBuildStageStatus({ blockers, declaredScopeSkips }), {
|
|
1443
1453
|
outputs,
|
|
1444
1454
|
blockers,
|
|
1445
1455
|
...(declaredScopeSkips.length ? { declared_out_of_scope: declaredScopeSkips } : {}),
|
|
@@ -1790,6 +1800,10 @@ function prepareBuildUnderLock({
|
|
|
1790
1800
|
sourceRoot,
|
|
1791
1801
|
targetRepo,
|
|
1792
1802
|
});
|
|
1803
|
+
// The report and normalized brief this run replaces: the report's brief
|
|
1804
|
+
// mode keeps an existing prepared campaign prepared (when it is this
|
|
1805
|
+
// campaign's report), and the previous brief carries field provenance.
|
|
1806
|
+
const priorBriefInputs = priorBriefInputsForIntake({ reportPath, briefPath, identity: { spec: { map_id: mapId, ...(localSpecId ? { local_spec_id: localSpecId } : {}) }, campaign: { public_route_slug: publicRouteSlug } } });
|
|
1793
1807
|
const buildBrief = createCampaignBuildBriefArtifact({
|
|
1794
1808
|
inputPath: briefDiscovery?.path || null,
|
|
1795
1809
|
inputSource: briefDiscovery?.source || null,
|
|
@@ -1799,10 +1813,19 @@ function prepareBuildUnderLock({
|
|
|
1799
1813
|
templateFamily,
|
|
1800
1814
|
sourceAssetCrawl,
|
|
1801
1815
|
commerceZoneFindings,
|
|
1816
|
+
boundReportMode: priorBriefInputs.boundReportMode,
|
|
1817
|
+
previousNormalizedBrief: priorBriefInputs.previousNormalizedBrief,
|
|
1802
1818
|
});
|
|
1803
1819
|
if (buildBrief.inputPath && buildBrief.artifact?._meta) {
|
|
1804
1820
|
buildBrief.artifact._meta.input_path = relFromFile(briefPath, buildBrief.inputPath);
|
|
1805
1821
|
}
|
|
1822
|
+
const briefBindings = briefIntakeBindings(buildBrief);
|
|
1823
|
+
// The packet settings this run writes, checked against the packet on disk
|
|
1824
|
+
// before anything is published.
|
|
1825
|
+
const deploySettings = {
|
|
1826
|
+
target: optionalString(args["deploy-target"], "unknown"),
|
|
1827
|
+
preview_url: optionalString(args["preview-url"]),
|
|
1828
|
+
};
|
|
1806
1829
|
const buildBriefPrompts = buildBrief.mode === "prepared" ? [] : buildBrief.questions.map((question) => ({
|
|
1807
1830
|
code: `BUILD_BRIEF_${toConstantCase(question.id)}`,
|
|
1808
1831
|
stage: "prepare_build",
|
|
@@ -1836,6 +1859,11 @@ function prepareBuildUnderLock({
|
|
|
1836
1859
|
}))
|
|
1837
1860
|
: [];
|
|
1838
1861
|
const designSourcePackage = publication.package({
|
|
1862
|
+
plannedPacket: {
|
|
1863
|
+
campaign: { allowed_domains_confirmed: args["allowed-domains-confirmed"] === true },
|
|
1864
|
+
deploy: { ...deploySettings, production_url: optionalString(args["production-url"]) },
|
|
1865
|
+
qa: { proof_policy: { order_path_depth: orderPathDepthFlag || "common" } },
|
|
1866
|
+
},
|
|
1839
1867
|
activePages,
|
|
1840
1868
|
mappings: matched.mappings,
|
|
1841
1869
|
manifestResult,
|
|
@@ -1887,6 +1915,7 @@ function prepareBuildUnderLock({
|
|
|
1887
1915
|
normalized_path: relFromFile(packetPath, briefPath),
|
|
1888
1916
|
question_count: buildBrief.questions.length,
|
|
1889
1917
|
gate_count: buildBrief.gates.length,
|
|
1918
|
+
...briefBindings,
|
|
1890
1919
|
},
|
|
1891
1920
|
assembly: {
|
|
1892
1921
|
implementation: "next-campaigns-build",
|
|
@@ -1914,8 +1943,7 @@ function prepareBuildUnderLock({
|
|
|
1914
1943
|
compatible_outputs: ["static-html", "campaign-cart-sdk"],
|
|
1915
1944
|
},
|
|
1916
1945
|
deploy: {
|
|
1917
|
-
|
|
1918
|
-
preview_url: optionalString(args["preview-url"]),
|
|
1946
|
+
...deploySettings,
|
|
1919
1947
|
production_url: optionalString(args["production-url"]),
|
|
1920
1948
|
live_url_path: liveUrlPath,
|
|
1921
1949
|
},
|
|
@@ -2002,6 +2030,7 @@ function prepareBuildUnderLock({
|
|
|
2002
2030
|
gate_count: buildBrief.gates.length,
|
|
2003
2031
|
questions: buildBrief.questions,
|
|
2004
2032
|
gates: buildBrief.gates,
|
|
2033
|
+
...briefBindings,
|
|
2005
2034
|
},
|
|
2006
2035
|
page_map: matched.mappings.map((mapping) => ({
|
|
2007
2036
|
page_id: mapping.page_id,
|
|
@@ -2055,7 +2084,8 @@ function prepareBuildUnderLock({
|
|
|
2055
2084
|
|
|
2056
2085
|
// The top-level status/next/blockers are derived from the stages by the same
|
|
2057
2086
|
// 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
|
|
2087
|
+
// prepare-build's first write and a producer's last write spell them alike,
|
|
2088
|
+
// against the brief and CampaignSpec material this run binds.
|
|
2059
2089
|
const report = applyDerivedAssemblyReportSummary(createAssemblyReport({
|
|
2060
2090
|
packetPath,
|
|
2061
2091
|
contextPath,
|
|
@@ -2072,7 +2102,7 @@ function prepareBuildUnderLock({
|
|
|
2072
2102
|
buildScopeReasonsInvalid,
|
|
2073
2103
|
templateSelection,
|
|
2074
2104
|
evidence: strippedSpecBytes == null ? [] : hostStripped.evidence,
|
|
2075
|
-
}));
|
|
2105
|
+
}), { briefMaterial: briefBindings.material, specMaterial: specMaterialHash(spec) });
|
|
2076
2106
|
|
|
2077
2107
|
// Values a spec left as it is still holds that doctor blocks on. An
|
|
2078
2108
|
// absolute http(s) page_url is not among them: projection takes its path.
|
|
@@ -2643,7 +2673,7 @@ export function commitWaiverToAssemblyReport(workspace, mutate, options, { dryRu
|
|
|
2643
2673
|
// run's own "do not write" is this wrapper's null below, not the mutator's.
|
|
2644
2674
|
if (!isPlainObject(mutated)) throw new TypeError("commitAssemblyReport mutate(report) must return an Assembly Report object.");
|
|
2645
2675
|
if (!dryRun) return mutated;
|
|
2646
|
-
applyDerivedAssemblyReportSummary(mutated);
|
|
2676
|
+
applyDerivedAssemblyReportSummary(mutated, currentPacketInputs({ packet: workspace.packet, packetPath: workspace.packetPath }));
|
|
2647
2677
|
return null;
|
|
2648
2678
|
};
|
|
2649
2679
|
// A preview writes nothing, so it does not take the target lock either: the
|
|
@@ -2698,7 +2728,19 @@ export async function polishCaptureCommand(args, options = {}) {
|
|
|
2698
2728
|
throw new Error(`polish capture needs an existing Assembly Report at ${reportPath}; run prepare-build/start first.`);
|
|
2699
2729
|
}
|
|
2700
2730
|
requireValidPolishCaptureReport(report, reportPath);
|
|
2701
|
-
|
|
2731
|
+
// A campaign whose every mapping is template stock has no page_load route
|
|
2732
|
+
// to capture: its plan error is held, and the capture measures readability
|
|
2733
|
+
// on the built pages only, leaving page_load and media_weight as they are.
|
|
2734
|
+
const { capturePolishReadability, isNoCapturableRoutesError } = await import("./polish-node.mjs");
|
|
2735
|
+
const planOrHeld = (currentPacket) => {
|
|
2736
|
+
try {
|
|
2737
|
+
return { plan: planPolishCapture({ packet: currentPacket, baseUrl }), held: null };
|
|
2738
|
+
} catch (error) {
|
|
2739
|
+
if (!isNoCapturableRoutesError(error)) throw error;
|
|
2740
|
+
return { plan: null, held: error };
|
|
2741
|
+
}
|
|
2742
|
+
};
|
|
2743
|
+
const { plan, held: heldPlanError } = planOrHeld(packet);
|
|
2702
2744
|
const initialBinding = createPolishCaptureBinding({
|
|
2703
2745
|
packet,
|
|
2704
2746
|
report,
|
|
@@ -2711,7 +2753,7 @@ export async function polishCaptureCommand(args, options = {}) {
|
|
|
2711
2753
|
if (typeof createBrowserAdapter !== "function") {
|
|
2712
2754
|
({ createPolishBrowserAdapter: createBrowserAdapter } = await import("./polish-browser.mjs"));
|
|
2713
2755
|
}
|
|
2714
|
-
const capture = await capturePolishPageLoad({
|
|
2756
|
+
const capture = await (heldPlanError ? capturePolishReadability : capturePolishPageLoad)({
|
|
2715
2757
|
packet,
|
|
2716
2758
|
report,
|
|
2717
2759
|
baseUrl,
|
|
@@ -2721,7 +2763,11 @@ export async function polishCaptureCommand(args, options = {}) {
|
|
|
2721
2763
|
adapterStartupDeadlineMs: options.adapterStartupDeadlineMs,
|
|
2722
2764
|
captureCellDeadlineMs: options.captureCellDeadlineMs,
|
|
2723
2765
|
adapterCloseDeadlineMs: options.adapterCloseDeadlineMs,
|
|
2766
|
+
probeClock: options.probeClock,
|
|
2767
|
+
targetRepo,
|
|
2724
2768
|
});
|
|
2769
|
+
// Nothing was measured: the held plan error stands.
|
|
2770
|
+
if (heldPlanError && !capture.readability) throw heldPlanError;
|
|
2725
2771
|
|
|
2726
2772
|
if (typeof options.afterCapture === "function") {
|
|
2727
2773
|
await options.afterCapture({ packetPath, reportPath, targetRepo, capture });
|
|
@@ -2734,7 +2780,7 @@ export async function polishCaptureCommand(args, options = {}) {
|
|
|
2734
2780
|
let checkpoint = null;
|
|
2735
2781
|
commitAssemblyReport(workspace, (currentReport) => {
|
|
2736
2782
|
requireValidPolishCaptureReport(currentReport, reportPath);
|
|
2737
|
-
const currentPlan =
|
|
2783
|
+
const { plan: currentPlan } = planOrHeld(currentPacket);
|
|
2738
2784
|
const currentBinding = createPolishCaptureBinding({
|
|
2739
2785
|
packet: currentPacket,
|
|
2740
2786
|
report: currentReport,
|
|
@@ -2744,7 +2790,12 @@ export async function polishCaptureCommand(args, options = {}) {
|
|
|
2744
2790
|
});
|
|
2745
2791
|
assertPolishCaptureBindingUnchanged(initialBinding, currentBinding);
|
|
2746
2792
|
|
|
2747
|
-
const merged = mergePolishCaptureEvidence(currentReport, {
|
|
2793
|
+
const merged = mergePolishCaptureEvidence(currentReport, {
|
|
2794
|
+
pageLoad: capture.page_load,
|
|
2795
|
+
mediaWeight: capture.media_weight,
|
|
2796
|
+
readability: capture.readability,
|
|
2797
|
+
now: options.now ?? new Date(),
|
|
2798
|
+
});
|
|
2748
2799
|
checkpoint = evaluateRecordedHiddenEagerMediaCheckpoint({
|
|
2749
2800
|
packet: currentPacket,
|
|
2750
2801
|
report: merged,
|
|
@@ -2755,19 +2806,39 @@ export async function polishCaptureCommand(args, options = {}) {
|
|
|
2755
2806
|
staleReason: `Package-owned polish page-load evidence changed after this doctor snapshot. Re-run ${cmd("doctor")} (or next) for current state.`,
|
|
2756
2807
|
});
|
|
2757
2808
|
|
|
2809
|
+
if (heldPlanError) {
|
|
2810
|
+
// The readability capture completed; page_load's own outcome stays the
|
|
2811
|
+
// held no_capturable_routes message, reported as a warning.
|
|
2812
|
+
return {
|
|
2813
|
+
ok: true,
|
|
2814
|
+
status: "captured_with_warnings",
|
|
2815
|
+
action: "polish-capture",
|
|
2816
|
+
report_path: reportPath,
|
|
2817
|
+
measurement: null,
|
|
2818
|
+
checkpoint,
|
|
2819
|
+
observed_findings: [],
|
|
2820
|
+
warnings: [{ code: heldPlanError.code, message: heldPlanError.message }],
|
|
2821
|
+
capture: {
|
|
2822
|
+
route_scope: null,
|
|
2823
|
+
routes: [],
|
|
2824
|
+
viewports: [],
|
|
2825
|
+
readability_routes: capture.readability?.subject?.routes ?? [],
|
|
2826
|
+
},
|
|
2827
|
+
};
|
|
2828
|
+
}
|
|
2758
2829
|
const ok = checkpoint.status === "pass" || checkpoint.status === "waived";
|
|
2759
2830
|
return {
|
|
2760
2831
|
ok,
|
|
2761
2832
|
status: ok ? (checkpoint.status === "waived" ? "ready_with_waivers" : "ready") : "blocked",
|
|
2762
2833
|
action: "polish-capture",
|
|
2763
2834
|
report_path: reportPath,
|
|
2764
|
-
measurement: capture.page_load
|
|
2835
|
+
measurement: capture.page_load?.measurement ?? null,
|
|
2765
2836
|
checkpoint,
|
|
2766
|
-
observed_findings: capture.page_load
|
|
2837
|
+
observed_findings: capture.page_load?.findings ?? [],
|
|
2767
2838
|
capture: {
|
|
2768
|
-
route_scope: capture.plan
|
|
2769
|
-
routes: capture.plan
|
|
2770
|
-
viewports: capture.plan
|
|
2839
|
+
route_scope: capture.plan?.route_scope ?? null,
|
|
2840
|
+
routes: (capture.plan?.routes ?? []).map((route) => route.requested_route),
|
|
2841
|
+
viewports: (capture.plan?.viewports ?? []).map((viewport) => viewport.key),
|
|
2771
2842
|
},
|
|
2772
2843
|
};
|
|
2773
2844
|
}
|
|
@@ -3509,7 +3580,6 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3509
3580
|
store: storeFlags
|
|
3510
3581
|
? { 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
3582
|
: null,
|
|
3512
|
-
rebound: { build_context: null, assembly_report: null },
|
|
3513
3583
|
errors: [],
|
|
3514
3584
|
warnings: [],
|
|
3515
3585
|
next: `${cmd("doctor")} --packet ${shellToken(packetPath)}`,
|
|
@@ -3743,17 +3813,15 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3743
3813
|
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
3814
|
}
|
|
3745
3815
|
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)}`;
|
|
3816
|
+
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
3817
|
}
|
|
3818
|
+
// spec derive writes the spec alone: the Build Context and the Assembly
|
|
3819
|
+
// Report keep the identity they bound, so every recorded stage reads its
|
|
3820
|
+
// stamps against the new content and `record spec` is what binds it.
|
|
3821
|
+
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
3822
|
}
|
|
3750
3823
|
|
|
3751
3824
|
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
3825
|
// The plan already refused every path its containers cannot take, so a
|
|
3758
3826
|
// throw here is a defect in this toolkit rather than in the spec; it is
|
|
3759
3827
|
// still a structured error, never a crash past the --json contract.
|
|
@@ -3791,82 +3859,7 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3791
3859
|
rmSync(tmpPath, { force: true });
|
|
3792
3860
|
}
|
|
3793
3861
|
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
3862
|
// 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
3863
|
try {
|
|
3871
3864
|
markDoctorSidecarStale(targetRepo, {
|
|
3872
3865
|
command: "spec derive",
|
|
@@ -4245,6 +4238,8 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4245
4238
|
followContextPointer: true,
|
|
4246
4239
|
});
|
|
4247
4240
|
const report = readJsonIfExists(reportPath);
|
|
4241
|
+
// The campaign intent summary, computed once per call and only printed.
|
|
4242
|
+
const intentSummary = readIntentSummary(packetPath, packet, contextPath);
|
|
4248
4243
|
// Doctor reads the same sidecars through the same resolver (an operator's
|
|
4249
4244
|
// explicit --context / --report passed through, the defaults derived), so
|
|
4250
4245
|
// its prepare-build gate and its stage pick are this command's: `next`
|
|
@@ -4330,6 +4325,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4330
4325
|
} });
|
|
4331
4326
|
if (divergences.length) result.divergences = divergences;
|
|
4332
4327
|
result.gates = buildNextGates({ doctor, report, themeGate, polishGate, prepareBuildGate, packetPath });
|
|
4328
|
+
if (doctor.derived?.input_currency) result.input_currency = doctor.derived.input_currency;
|
|
4333
4329
|
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
4330
|
// The QC handoff: read from data already loaded plus the
|
|
4335
4331
|
// full QA verdict the QA stage names. It adds nothing to errors[],
|
|
@@ -4338,7 +4334,11 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4338
4334
|
// one (from --report or the Build Context pointer), so it records there.
|
|
4339
4335
|
const qc = readCurrentQcResults({ report, doctor, spec, targetRepo, packetPath, reportPath, qcStandIns, rederivers: qcRederivers });
|
|
4340
4336
|
result.qc_handoff = buildQcHandoff({ results: qc.results, coverage: qc.coverage, accepts: report?.qc_accepts, packetPath, reportPath: explicitReportPath(reportPath, targetRepo) });
|
|
4337
|
+
// One note per operator decision that kept the build output unchanged
|
|
4338
|
+
// after an input change, quoting the operator's reason.
|
|
4339
|
+
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
4340
|
recordNextRecommendation(ambient, result);
|
|
4341
|
+
result.intent_summary = intentSummary;
|
|
4342
4342
|
return result;
|
|
4343
4343
|
};
|
|
4344
4344
|
const doctorHasOnlyPolishGateErrors = doctorErrorsAreOnlyPolishGate(doctor.errors);
|
|
@@ -4430,12 +4430,12 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4430
4430
|
if (stage === "setup") {
|
|
4431
4431
|
addPrepareBuildGateErrors(errors, report);
|
|
4432
4432
|
if (!doctor.ok && !doctorHasOnlyPolishGateErrors) addIssue(errors, "next.setup.doctor", "Doctor is blocked; resolve packet errors before setup.");
|
|
4433
|
-
prompt = setupPrompt(packetPath, contextPath, reportPath, packet);
|
|
4433
|
+
prompt = setupPrompt(packetPath, contextPath, reportPath, packet, intentSummary);
|
|
4434
4434
|
} else if (stage === "build") {
|
|
4435
4435
|
addPrepareBuildGateErrors(errors, report);
|
|
4436
4436
|
if (!doctor.ok && !doctorHasOnlyPolishGateErrors) addIssue(errors, "next.build.doctor", "Doctor is blocked; resolve packet errors before build.");
|
|
4437
4437
|
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);
|
|
4438
|
+
prompt = buildPrompt(packetPath, contextPath, reportPath, packet, doctor.derived, intentSummary);
|
|
4439
4439
|
} else if (stage === "polish") {
|
|
4440
4440
|
addPrepareBuildGateErrors(errors, report);
|
|
4441
4441
|
if (!report) addIssue(errors, "next.polish.report", "Assembly report is required before polish.");
|
|
@@ -4443,7 +4443,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4443
4443
|
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
4444
|
if (polishGateRequiresBuild(polishGate)) addPolishGateErrors(errors, polishGate, "polish");
|
|
4445
4445
|
addThemeGateErrors(errors, themeGate, "polish");
|
|
4446
|
-
prompt = polishPrompt(packetPath, reportPath, packet);
|
|
4446
|
+
prompt = polishPrompt(packetPath, reportPath, packet, intentSummary);
|
|
4447
4447
|
} else if (stage === "deploy") {
|
|
4448
4448
|
addPrepareBuildGateErrors(errors, report);
|
|
4449
4449
|
// Slice 3 Phase 2: deploy is an out-of-band step (Netlify / CF Pages /
|
|
@@ -4462,7 +4462,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4462
4462
|
addPolishGateErrors(errors, polishGate, "qa");
|
|
4463
4463
|
addPolishCheckpointGateErrors(errors, polishCheckpointGate, "qa");
|
|
4464
4464
|
addThemeGateErrors(errors, themeGate, "qa");
|
|
4465
|
-
prompt = qaPrompt(packetPath, reportPath, packet, spec);
|
|
4465
|
+
prompt = qaPrompt(packetPath, reportPath, packet, spec, intentSummary);
|
|
4466
4466
|
}
|
|
4467
4467
|
const status = errors.length
|
|
4468
4468
|
? "blocked"
|
|
@@ -4757,6 +4757,11 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4757
4757
|
push(inspect.id, inspect.kind, asInvocation(inspect.command), inspect.description, { required: inspect.required });
|
|
4758
4758
|
return actions;
|
|
4759
4759
|
}
|
|
4760
|
+
for (const subcommand of inputRefreshCommands(result.input_currency)) {
|
|
4761
|
+
push("refresh_inputs", "command", `${cmd("record")} ${subcommand} --packet ${shellToken(packetPath)} --json`, subcommand === "brief"
|
|
4762
|
+
? "A recorded stage was made against earlier Campaign Build Brief content: save the brief with record brief (keeps stage history), then re-run next."
|
|
4763
|
+
: "A recorded stage was made against earlier CampaignSpec content: bind it with record spec (keeps stage history), then re-run next.");
|
|
4764
|
+
}
|
|
4760
4765
|
if (result.stage === "doctor-blocked") {
|
|
4761
4766
|
const checkpointGates = (result.gates || []).filter(
|
|
4762
4767
|
(gate) => gate?.status === "blocked" && Object.hasOwn(CHECKPOINT_EVALUATORS, gate?.id || ""),
|
|
@@ -5008,9 +5013,9 @@ function templateStockPromptLine(packet, derived = {}) {
|
|
|
5008
5013
|
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
5014
|
}
|
|
5010
5015
|
|
|
5011
|
-
function buildPrompt(packetPath, contextPath, reportPath, packet, derived = {}) {
|
|
5016
|
+
function buildPrompt(packetPath, contextPath, reportPath, packet, derived = {}, intent) {
|
|
5012
5017
|
const briefPath = packet.build_brief?.normalized_path || "(missing; generate or confirm Campaign Build Brief before business-sensitive assembly)";
|
|
5013
|
-
return
|
|
5018
|
+
return `${campaignIntentPromptHeader(intent)}Use next-campaigns-build for this Campaigns OS handoff.
|
|
5014
5019
|
|
|
5015
5020
|
Read first:
|
|
5016
5021
|
- Build Packet: ${packetPath}
|
|
@@ -5023,6 +5028,7 @@ Read first:
|
|
|
5023
5028
|
Rules:
|
|
5024
5029
|
- Treat CampaignSpec/API as the source for package, shipping, voucher, payment, tracking, footer, and SEO values.
|
|
5025
5030
|
- 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.
|
|
5031
|
+
- 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
5032
|
- Read the selected template family's agentContract and sharedFrontmatterVocabulary before commerce wiring.
|
|
5027
5033
|
- 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
5034
|
- Preserve prepared source HTML for landing/presell pages when it is a real standalone design.
|
|
@@ -5049,9 +5055,44 @@ function localProofPromptLines(packet, packetPath = "<packet>") {
|
|
|
5049
5055
|
- 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
5056
|
}
|
|
5051
5057
|
|
|
5052
|
-
|
|
5058
|
+
// Read the normalized brief the packet records, resolved against the packet
|
|
5059
|
+
// file as doctor resolves it, and summarize it over the Build Context's active
|
|
5060
|
+
// pages in recorded order. A missing, unreadable or invalid brief reads
|
|
5061
|
+
// unavailable; this never fails `next`. Each read or summarising failure is
|
|
5062
|
+
// named on stderr, one line per failure, never on stdout.
|
|
5063
|
+
function readIntentSummary(packetPath, packet, contextPath) {
|
|
5064
|
+
const reportFailure = (error) => process.stderr.write(`campaigns-os: intent summary unavailable: ${singleLineFragment(error?.message ?? error, "unknown error")}\n`);
|
|
5065
|
+
const normalizedPath = resolveFromFile(packetPath, packet?.build_brief?.normalized_path);
|
|
5066
|
+
let brief = null;
|
|
5067
|
+
try {
|
|
5068
|
+
brief = readJsonIfExists(normalizedPath);
|
|
5069
|
+
} catch (error) {
|
|
5070
|
+
reportFailure(error);
|
|
5071
|
+
brief = null;
|
|
5072
|
+
}
|
|
5073
|
+
let activePageIds = [];
|
|
5074
|
+
try {
|
|
5075
|
+
const activePages = readJsonIfExists(contextPath)?.spec?.active_pages;
|
|
5076
|
+
activePageIds = Array.isArray(activePages) ? activePages.map((page) => page?.id) : [];
|
|
5077
|
+
} catch (error) {
|
|
5078
|
+
reportFailure(error);
|
|
5079
|
+
activePageIds = [];
|
|
5080
|
+
}
|
|
5081
|
+
return { ...summarizeCampaignBrief({ brief, activePageIds, onError: reportFailure }), normalized_path: normalizedPath };
|
|
5082
|
+
}
|
|
5083
|
+
|
|
5084
|
+
// The setup, build, Polish and QA prompts start with the campaign intent
|
|
5085
|
+
// summary, framed as orientation only.
|
|
5086
|
+
function campaignIntentPromptHeader(intent) {
|
|
5087
|
+
return `Campaign intent (from the Campaign Build Brief at ${intent.normalized_path || "(not recorded)"}; orientation only, never a source of prices or commerce behaviour):
|
|
5088
|
+
${intent.text}
|
|
5089
|
+
|
|
5090
|
+
`;
|
|
5091
|
+
}
|
|
5092
|
+
|
|
5093
|
+
function setupPrompt(packetPath, contextPath, reportPath, packet, intent) {
|
|
5053
5094
|
const briefPath = packet.build_brief?.normalized_path || "(missing)";
|
|
5054
|
-
return
|
|
5095
|
+
return `${campaignIntentPromptHeader(intent)}Use next-campaigns-os-setup for this Campaigns OS handoff.
|
|
5055
5096
|
|
|
5056
5097
|
Read first:
|
|
5057
5098
|
- Build Packet: ${packetPath}
|
|
@@ -5070,9 +5111,9 @@ When copying a starter template family, copy the family as an atomic page-kit sl
|
|
|
5070
5111
|
Do not wire checkout, upsell, receipt, payment, package, voucher, or shipping behavior during setup.`;
|
|
5071
5112
|
}
|
|
5072
5113
|
|
|
5073
|
-
function polishPrompt(packetPath, reportPath, packet) {
|
|
5114
|
+
function polishPrompt(packetPath, reportPath, packet, intent) {
|
|
5074
5115
|
const briefPath = packet.build_brief?.normalized_path || "(missing)";
|
|
5075
|
-
return
|
|
5116
|
+
return `${campaignIntentPromptHeader(intent)}Use next-campaigns-polish for this built campaign.
|
|
5076
5117
|
|
|
5077
5118
|
Read first:
|
|
5078
5119
|
- Build Packet: ${packetPath}
|
|
@@ -5215,14 +5256,14 @@ function orderBumpRunReason(bumpCart) {
|
|
|
5215
5256
|
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
5257
|
}
|
|
5217
5258
|
|
|
5218
|
-
function qaPrompt(packetPath, reportPath, packet, spec = null) {
|
|
5259
|
+
function qaPrompt(packetPath, reportPath, packet, spec = null, intent) {
|
|
5219
5260
|
const url = packet.deploy?.preview_url || packet.deploy?.production_url || "<preview-url>";
|
|
5220
5261
|
const briefPath = packet.build_brief?.normalized_path || "(missing)";
|
|
5221
5262
|
const bumpCart = checkoutOrderBumpCart(spec);
|
|
5222
5263
|
const bumpCommand = bumpCart
|
|
5223
5264
|
? `\n\nOrder bump QA command (proves the charged add-on):\n${qaRunCommand(packetPath, url, bumpCart)}\n${orderBumpRunReason(bumpCart)}`
|
|
5224
5265
|
: "";
|
|
5225
|
-
return
|
|
5266
|
+
return `${campaignIntentPromptHeader(intent)}Use next-campaigns-qa for this deployed campaign.
|
|
5226
5267
|
|
|
5227
5268
|
${packet.spec.local_spec_id ? "Local spec ID" : "Map ID"}: ${packet.spec.local_spec_id || packet.spec.map_id}
|
|
5228
5269
|
Base URL: ${url}
|
|
@@ -6396,7 +6437,7 @@ function safePolishByteCount(value) {
|
|
|
6396
6437
|
}
|
|
6397
6438
|
|
|
6398
6439
|
export function formatPolishCaptureText(result) {
|
|
6399
|
-
const status = ["ready", "ready_with_waivers", "blocked"].includes(result?.status)
|
|
6440
|
+
const status = ["ready", "ready_with_waivers", "captured_with_warnings", "blocked"].includes(result?.status)
|
|
6400
6441
|
? result.status
|
|
6401
6442
|
: "unknown";
|
|
6402
6443
|
const measurementStatus = ["complete", "incomplete"].includes(result?.measurement?.status)
|
|
@@ -6410,6 +6451,11 @@ export function formatPolishCaptureText(result) {
|
|
|
6410
6451
|
`Status: ${status.toUpperCase()}`,
|
|
6411
6452
|
`Measurement: ${measurementStatus.toUpperCase()}`,
|
|
6412
6453
|
];
|
|
6454
|
+
// Outcomes that did not stop the capture, printed with their own message
|
|
6455
|
+
// (the held page-load plan message on a campaign with no mapped pages).
|
|
6456
|
+
for (const warning of Array.isArray(result?.warnings) ? result.warnings : []) {
|
|
6457
|
+
if (warning?.code === "no_capturable_routes" && typeof warning.message === "string") lines.push(`Warning: ${warning.message}`);
|
|
6458
|
+
}
|
|
6413
6459
|
const incomplete = Array.isArray(result?.measurement?.incomplete)
|
|
6414
6460
|
? result.measurement.incomplete
|
|
6415
6461
|
: [];
|
|
@@ -8411,6 +8457,8 @@ const WAIVE_ACTIONS = new Set(["theme-waive", "checkpoint-waive"]);
|
|
|
8411
8457
|
// check's readiness line); they are text-only and never enter the JSON result.
|
|
8412
8458
|
export function resultTextLines(result, { headerLines = [] } = {}) {
|
|
8413
8459
|
const lines = [`Status: ${String(result.status || "unknown").toUpperCase()}`, ...headerLines];
|
|
8460
|
+
// `next` only: the campaign intent summary, directly under the status.
|
|
8461
|
+
if (result.intent_summary) lines.push("Campaign intent:", ...result.intent_summary.text.split("\n"));
|
|
8414
8462
|
// A waive command's second line names what it recorded; the third is the
|
|
8415
8463
|
// stage doctor now picks for the report the waiver was written to.
|
|
8416
8464
|
if (WAIVE_ACTIONS.has(result.action) && result.gate) {
|