@nextcommerce/campaigns-os 1.61.0 → 1.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +127 -0
- package/README.md +86 -438
- package/agents/claude/CLAUDE.md +9 -4
- package/agents/codex/AGENTS.md +8 -4
- package/agents/copilot/copilot-instructions.md +8 -4
- package/agents/cursor/campaigns-os.mdc +10 -4
- package/campaign-spec/README.md +7 -0
- package/campaign-spec/dist/analytics-vendors.d.ts +40 -0
- package/campaign-spec/dist/analytics-vendors.js +44 -0
- package/campaign-spec/dist/checkout-flow.d.ts +18 -0
- package/campaign-spec/dist/checkout-flow.js +28 -0
- package/campaign-spec/dist/index.d.ts +4 -2
- package/campaign-spec/dist/index.js +5 -1
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +8 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.js +39 -0
- package/campaign-spec/dist/rules/campaign-currencies.d.ts +30 -0
- package/campaign-spec/dist/rules/campaign-currencies.js +130 -0
- package/campaign-spec/dist/rules/checkout-path-has-packages.d.ts +31 -0
- package/campaign-spec/dist/rules/checkout-path-has-packages.js +79 -0
- package/campaign-spec/dist/rules/index.d.ts +3 -0
- package/campaign-spec/dist/rules/index.js +9 -0
- package/campaign-spec/dist/rules/package-currency-prices.d.ts +29 -0
- package/campaign-spec/dist/rules/package-currency-prices.js +137 -0
- package/campaign-spec/dist/types.d.ts +59 -9
- package/compatibility.json +1 -1
- package/contracts/commerce-surface-catalog.json +59 -53
- package/contracts/effects.v1.json +549 -165
- package/contracts/fixtures/runtime-recipe/accept/current.json +4 -0
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +4 -0
- package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +4 -0
- package/contracts/release-ledger.json +1260 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +4 -0
- package/contracts/supported-surface.json +8 -8
- package/contracts/template-brand-contract.shared-commerce.v0.json +23 -1
- package/docs/build-packet.md +430 -37
- package/docs/campaign-standardization-report.md +4 -4
- package/docs/campaigns-os-build-flow.md +54 -3
- package/docs/campaignspec-authoring-examples.md +59 -4
- package/docs/design-source-package.md +18 -6
- package/docs/effects.md +17 -9
- 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 +7 -4
- package/docs/qa-and-test-orders.md +147 -1
- package/docs/readback.md +40 -5
- package/docs/runtime-readiness.md +7 -3
- package/docs/sdk-storage-compatibility.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/package.json +1 -1
- package/schemas/campaign-runtime-build-context.v0.schema.json +9 -0
- package/schemas/campaign-runtime-build-packet.v0.schema.json +9 -0
- package/schemas/campaign-spec.v4.schema.json +34 -3
- package/schemas/campaign-spec.v5.schema.json +34 -3
- package/schemas/campaigns-os-readback.v2.schema.json +62 -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 +4 -3
- package/skills/next-campaigns-os/SKILL.md +6 -4
- package/skills/next-campaigns-os-setup/SKILL.md +5 -3
- package/skills/next-campaigns-polish/SKILL.md +5 -4
- package/skills/next-campaigns-qa/SKILL.md +7 -4
- package/skills/next-campaigns-sdk-bump/SKILL.md +42 -9
- package/skills.json +11 -11
- package/src/brand-theme.mjs +2 -1
- package/src/build-brief.mjs +4 -1
- package/src/campaign-workspace.mjs +288 -15
- package/src/change-scope.mjs +284 -0
- package/src/cli.mjs +308 -86
- package/src/commercial-journey.mjs +16 -3
- package/src/commercial-parity.mjs +15 -7
- package/src/content-residue.mjs +4 -2
- package/src/design-source-package.mjs +36 -0
- package/src/design-source-publication.mjs +9 -6
- package/src/directory-lock.mjs +73 -19
- package/src/doctor/checks.mjs +79 -7
- package/src/doctor/ci.mjs +75 -10
- package/src/doctor/inspect.mjs +23 -6
- package/src/doctor/next-step.mjs +7 -0
- package/src/doctor-sidecar.mjs +3 -2
- package/src/evidence-debt.mjs +47 -7
- package/src/finding-cause.mjs +12 -11
- package/src/input-refresh.mjs +339 -32
- package/src/invocation.mjs +7 -1
- package/src/live-campaign-refs.mjs +7 -2
- package/src/local-preview-policy.mjs +35 -2
- package/src/page-kit-analytics-vendors.mjs +132 -0
- package/src/polish-browser.mjs +3 -1
- package/src/polish-readability.mjs +221 -9
- package/src/polish-visual.mjs +9 -2
- package/src/progress-node.mjs +3 -2
- package/src/qa-analytics-parity.mjs +17 -1
- package/src/qa-browser.mjs +40 -4
- package/src/qa-commercial-parity.mjs +50 -3
- package/src/qa-node.mjs +196 -16
- package/src/qa-publish.mjs +9 -8
- package/src/qa-sidecar.mjs +16 -4
- package/src/qa-vendor-beacons.mjs +232 -0
- package/src/qa-vendor-endpoints.mjs +306 -0
- package/src/qc-accept.mjs +29 -1
- package/src/qc-check-registry.mjs +5 -1
- package/src/qc-results.mjs +73 -26
- package/src/readback.mjs +58 -10
- package/src/route-identity.mjs +10 -0
- package/src/run-record.mjs +18 -10
- package/src/runtime-state-ignore.mjs +6 -2
- package/src/sdk-storage-compatibility.mjs +21 -2
- package/src/source-html-intake.mjs +15 -1
- package/src/spec-derive-campaign.mjs +216 -0
- package/src/spec-derive.mjs +473 -53
- package/src/spec-identity.mjs +14 -0
- package/src/stage-ledger.mjs +40 -1
- package/src/stage-record.mjs +58 -13
- package/src/target-lock.mjs +11 -1
package/src/cli.mjs
CHANGED
|
@@ -28,11 +28,11 @@ import { diagnosticExport, diagnosticTextLines } from "./diagnostic.mjs";
|
|
|
28
28
|
import { observeProgress, PROGRESS_OBSERVATION } from "./progress-node.mjs";
|
|
29
29
|
import { HIDDEN_EAGER_MEDIA_ACTIONS, qaRunCommand, requiredActionText, substitutePacket } from "./gate-actions.mjs";
|
|
30
30
|
import { orderPathDepthDriftText, orderPathDepthReconcileAction, orderPathDepthsDisagree, parseOrderPathDepthFlag } from "./proof-policy.mjs";
|
|
31
|
-
import { specMaterialHash, specHashesMatch } from "./spec-identity.mjs";
|
|
31
|
+
import { routeMaskedSpecMaterialHash, specMaterialHash, specHashesMatch } from "./spec-identity.mjs";
|
|
32
32
|
// The same predicate stage-ledger.mjs judges a mutator's result with, imported
|
|
33
33
|
// rather than re-stated so the waive preview and the commit agree by identity.
|
|
34
34
|
import { isPlainObject } from "./repo-scan.mjs";
|
|
35
|
-
import { applyDerivedAssemblyReportSummary, commitAssemblyReport, QA_GATE_PLACEHOLDER_TEXT_RESIDUE, recordProducerStageOutcome } from "./stage-ledger.mjs";
|
|
35
|
+
import { applyDerivedAssemblyReportSummary, campaignHandedOff, commitAssemblyReport, QA_GATE_PLACEHOLDER_TEXT_RESIDUE, recordProducerStageOutcome } from "./stage-ledger.mjs";
|
|
36
36
|
import { SESSION_ENDING_DISPOSITIONS, summarizePlaceholderTextGate, summarizePurchaseProof } from "./qa-verdict.mjs";
|
|
37
37
|
import { assessRunRecordCloseout, identityMatches, latestMatchingRunRecord, reasonIsRemitRecovery } from "./run-record-closeout.mjs";
|
|
38
38
|
import {
|
|
@@ -82,7 +82,17 @@ import { markDoctorSidecarStale, writeDoctorSidecar, writeJsonAtomic } from "./d
|
|
|
82
82
|
import { QcAcceptRefusal, buildQcHandoff, parseQcResultRef, planQcAccepts, projectQcAccept, qcAcceptAttribution, qcHandoffTextLines } from "./qc-accept.mjs";
|
|
83
83
|
import { loadQcRederivers } from "./qc-check-registry.mjs";
|
|
84
84
|
import { fingerprint12, readCurrentQcResults } from "./qc-results.mjs";
|
|
85
|
-
import {
|
|
85
|
+
import {
|
|
86
|
+
DEFAULT_PACKET_FILENAME,
|
|
87
|
+
RUNTIME_DIR_REL_PATH,
|
|
88
|
+
campaignSidecarPaths,
|
|
89
|
+
explicitReportPath,
|
|
90
|
+
packetRuntimeRelPath,
|
|
91
|
+
planFunnelWorkspace,
|
|
92
|
+
resolveCampaignWorkspace,
|
|
93
|
+
soleProjectPacket,
|
|
94
|
+
targetRepoFor,
|
|
95
|
+
} from "./campaign-workspace.mjs";
|
|
86
96
|
import { canonicalPath, sameFile } from "./fs-identity.mjs";
|
|
87
97
|
import { DEFAULT_PROXY_BASE, fetchSpecByMapId } from "./spec-fetch.mjs";
|
|
88
98
|
import { writeMapSdkPin } from "./map-pin-writeback.mjs";
|
|
@@ -101,7 +111,7 @@ import {
|
|
|
101
111
|
runWithRefusalScope,
|
|
102
112
|
timeOperatorWait,
|
|
103
113
|
} from "./lifecycle.mjs";
|
|
104
|
-
import { commandNames, optsOutOfRunSession, runInvocation } from "./invocation.mjs";
|
|
114
|
+
import { JOINED_FLAGS, commandNames, optsOutOfRunSession, runInvocation } from "./invocation.mjs";
|
|
105
115
|
import {
|
|
106
116
|
clearRunSession,
|
|
107
117
|
findRunSession,
|
|
@@ -141,6 +151,7 @@ import { derivePackagePin, LOCAL_INVOCATION_PREFIX, localInstallStatus, resolveI
|
|
|
141
151
|
import {
|
|
142
152
|
campaignRouteRoot,
|
|
143
153
|
normalizePublicRouteSlug,
|
|
154
|
+
specPublicRouteSlug,
|
|
144
155
|
} from "./route-identity.mjs";
|
|
145
156
|
import {
|
|
146
157
|
PAGE_KIT_BUILD_SUMMARY_CAPTURE_COMMAND,
|
|
@@ -240,11 +251,13 @@ import {
|
|
|
240
251
|
formatDeriveValue,
|
|
241
252
|
isPageTreeIgnoredDir,
|
|
242
253
|
isPageTreeIgnoredPath,
|
|
243
|
-
|
|
254
|
+
frontmatterScalars,
|
|
244
255
|
pageRouteForFile,
|
|
245
256
|
permalinkRoute,
|
|
246
257
|
planSpecDerive,
|
|
247
258
|
} from "./spec-derive.mjs";
|
|
259
|
+
import { planCampaignMirrorDerive } from "./spec-derive-campaign.mjs";
|
|
260
|
+
import { disabledLiveRead, liveRefsDisabled, readLiveCampaignForPacket } from "./live-campaign-refs.mjs";
|
|
248
261
|
import {
|
|
249
262
|
adminApiBaseForStore,
|
|
250
263
|
normalizeStoreSubdomain,
|
|
@@ -311,7 +324,7 @@ Usage:
|
|
|
311
324
|
[--brief <yaml|json>] [--proxy-base <url>] [--cached-spec] [--theme-policy <inspect_only|auto|off>]
|
|
312
325
|
[--wrapper-policy <strip_document_wrappers|preserve_document_wrappers|not_required|unknown>] [--design-manifest <path>]
|
|
313
326
|
[--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>]
|
|
314
|
-
[--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--no-run-session] [--force] # --force overwrites an assembly report that carries stage evidence (destructive; prints the cleared stage keys) and regenerates a stale Design Source Package an earlier intake synthesized and nobody changed
|
|
327
|
+
[--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--no-run-session] [--force] # --force overwrites an assembly report that carries stage evidence (destructive; prints the cleared stage keys) and regenerates a stale Design Source Package an earlier intake synthesized and nobody changed; with --out naming another route's packet it replaces that funnel (renaming its route)
|
|
315
328
|
campaigns-os prepare-build (--spec <json> | --map-id <id>) --source <html-dir> --target <page-kit-dir> --template-family <family>
|
|
316
329
|
[--brief <yaml|json>] [--proxy-base <url>] [--cached-spec] [--theme-policy <inspect_only|auto|off>]
|
|
317
330
|
[--wrapper-policy <strip_document_wrappers|preserve_document_wrappers|not_required|unknown>] [--design-manifest <path>]
|
|
@@ -334,7 +347,7 @@ Usage:
|
|
|
334
347
|
campaigns-os checkpoint waive --packet <campaign-runtime.build.json> --gate <checkpoint-id> [--page <page_id>] --reason "<why>" --waived-by "<named human>" [--expires-at <ISO>] [--review-condition "<trigger>"] [--report <json>] [--dry-run] [--json] # one bound is required; registered gates: page_kit.store_profile, page_kit.sdk_version, polish.hidden_eager_media, built_output.upsell_selector_scope, source_html.producer_provenance (per page: --page <page_id> is required, for a Figma-typed page whose approved source is hand-written HTML). --dry-run runs every check (named human, bounds, registered and waivable gate) and prints the waiver it would write, without touching the report
|
|
335
348
|
campaigns-os checkpoint accept --packet <campaign-runtime.build.json> --result <result_id>@<fingerprint12> [--result ...] --reason "<the operator's reason>" --accepted-by "<named human>" [--expires-at <ISO>] [--review-condition "<trigger>"] [--report <json>] [--dry-run] [--json] # record the operator's accept of a measured QC warning, as the QC handoff of next prints it; changes no readiness and lapses when the measured state or the build changes. --dry-run runs every check and prints the accepts it would record, without touching the report
|
|
336
349
|
campaigns-os page-kit sync --packet <campaign-runtime.build.json> [--dry-run] [--json] # write the CampaignSpec's Store Profile fields (campaign.store_*) and SDK pin (global_config.sdk_version, runtime.sdk_version alias) into the target's _data/campaigns.json entry for the packet's route, printing a field-by-field diff; the recovery for a doctor blocked on page_kit.store_profile / page_kit.sdk_version after a fresh scaffold. Writes only those ten fields, only from usable spec values (a bad pin, a non-http URL, a non-tel: phone URI or the demo value itself is reported as not synced, status PARTIAL); exit 2 when the entry or the spec is missing, or the spec identifies another campaign.
|
|
337
|
-
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.
|
|
350
|
+
campaigns-os spec derive --packet <campaign-runtime.build.json> [--dry-run] [--json] [--report <json>] [--no-live-refs] [--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, and each starter-template vendor's id key, such as ga4_id or triplewhale_name -> analytics.vendors.<vendor>.id); prints a field-by-field before -> after diff and writes nothing else. On a handed-off campaign (deploy or QA completed, now or in the stage's history) the page tree also adds pages (a file with a page-kit page_type no spec page binds; id from the file name, appended to the funnel, routing from its next_url/decline_url/next_step frontmatter) and removes pages (one whose built file the packet named is gone), matched by page id; page order is reported as not derived. When the packet's built _site/<route>/ exists and a public Campaigns API key resolves, it also makes the one read-only GET of {proxy-base}/api/campaign doctor makes and refreshes the fields the spec mirrors from the Campaigns App (each page package's name, price, product, variant and availability; the offer records and each page's offer copies; live offers missing from offers[] are added) — a price is never written from the repo; --no-live-refs sends nothing and leaves those fields as they are; --proxy-base names the proxy. Otherwise repo-derived fields only; --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.
|
|
338
351
|
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.
|
|
339
352
|
campaigns-os polish capture --packet <campaign-runtime.build.json> --base-url <url> [--report <json>] [--headed] [--auth-cookie <cookie>] [--json]
|
|
340
353
|
campaigns-os record brief --packet <campaign-runtime.build.json> [--brief <yaml|json>] [--context <json>] [--report <json>] [--dry-run] [--json]
|
|
@@ -361,7 +374,7 @@ Usage:
|
|
|
361
374
|
campaigns-os next deploy --packet <json> --report <json> [--json]
|
|
362
375
|
campaigns-os next qa --packet <json> --report <json> [--json]
|
|
363
376
|
campaigns-os qa resolve --packet <json> [--base-url <url>] [--no-probe] [--probe-timeout-ms <ms>] [--json] # probes the derived entry URLs; a dead route set reports routes_unresolved, an unprobed one ready_unprobed
|
|
364
|
-
campaigns-os qa run --packet <json> [--base-url <url>] [--currency <code>] [--browser] [--test-order <mode>] [--select-package <ref[:qty],...>] [--apply-coupon <code>] [--test-email <email>] [--test-country <code> --test-address1 <line> --test-city <city> --test-province <code> --test-postal <code>] [--no-post-verdict] [--no-remit] [--no-live-refs] [--output-dir <dir>] [--json]
|
|
377
|
+
campaigns-os qa run --packet <json> [--base-url <url>] [--currency <code>] [--page <page_id>]... [--browser] [--test-order <mode>] [--select-package <ref[:qty],...>] [--apply-coupon <code>] [--test-email <email>] [--test-country <code> --test-address1 <line> --test-city <city> --test-province <code> --test-postal <code>] [--vendor-beacons live] [--no-post-verdict] [--no-remit] [--no-live-refs] [--output-dir <dir>] [--json]
|
|
365
378
|
campaigns-os qa promote --packet <json> --verdict <full-verdict.json> [--json] # project one explicit qa-output verdict to the committed .campaign-runtime/qa-verdict.json sidecar
|
|
366
379
|
campaigns-os qa publish --packet <json> [--verdict <full-verdict.json>] [--republish] [--proxy-base <url>] [--dry-run] [--json] # post an already-stored verdict (the sidecar's run, or --verdict) to the QA portal without a re-run or an order; refuses a stale spec_hash or an already-published verdict. --dry-run runs every one of those refusal checks and prints what would be posted (endpoint, verdict run id, payload bytes) without the POST
|
|
367
380
|
campaigns-os qa policy set --packet <json> [--allowed-domains-confirmed true|false] [--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--order-path-depth <off|common|full>] [--json] # --order-path-depth writes qa.proof_policy.order_path_depth and refreshes the assembly report's proof_policy mirror
|
|
@@ -375,7 +388,7 @@ Usage:
|
|
|
375
388
|
|
|
376
389
|
Commands other than login, logout, demo, and tooling diagnose accept [--lifecycle-journal <path>] (or env CAMPAIGNS_OS_LIFECYCLE_LOG) to append a command-lifecycle entry (command, argv shape, exit status, timing) for the run; pair with --run-id so run-record can embed it.
|
|
377
390
|
--no-write suppresses that append for every command, however the journal was selected (flag, env, or the active run session); a refused invocation (unknown command, an unknown subcommand refused before its handler runs, or a flag the command refuses up front) and \`run status\` never append one at all.
|
|
378
|
-
A refused invocation writes no file of its own. One effect still precedes argument refusal: \`start\`, \`prepare-build\`, \`build\`, \`run start\` and \`run end\` close out a STALE run session at the root they are about to act on (Run Record assembled and remitted under the usual consent, session file cleared) before argv is refused — a declared effect of those commands. --no-write suppresses that closeout too, so a --no-write invocation leaves the target byte-identical.
|
|
391
|
+
A refused invocation writes no file of its own. One effect still precedes argument refusal: \`start\`, \`prepare-build\`, \`build\`, \`run start\` and \`run end\` close out a STALE run session at the root they are about to act on (Run Record assembled and remitted under the usual consent, session file cleared) before argv is refused — a declared effect of those commands. --no-write suppresses that closeout too, so a --no-write invocation leaves the target byte-identical; a --flag=value token, which every command refuses, skips it as well.
|
|
379
392
|
campaigns-os telemetry status|on [--proxy-base <url>] [--json] # machine-level Run Telemetry consent (gates remit only; capture is always local). \`on\` records consent for ONE endpoint: the canonical NEXT endpoint by default, or the --proxy-base you name (a loopback or staging receiver); \`status\` reports the stored scope and checks it against the canonical endpoint or the --proxy-base you name
|
|
380
393
|
campaigns-os telemetry off [--json] # turn remit off for every endpoint (takes no --proxy-base)
|
|
381
394
|
campaigns-os telemetry list [--packet <json> | --admin-key-env <VAR>] [--since <ISO>] [--package <v>] [--surface <s>] [--trusted] [--limit <n>] [--proxy-base <url>] [--trust-proxy-base] [--json] # read stored Run Records: tenant scope via the packet's campaign key, or cross-tenant via the ops admin key (default env CAMPAIGN_OPS_ADMIN_KEY). --proxy-base must be https unless it is a loopback host (allowed over http, with a warning that the credential is in clear).
|
|
@@ -487,7 +500,10 @@ export async function main(argv, { authentication, qcStandIns } = {}) {
|
|
|
487
500
|
return;
|
|
488
501
|
}
|
|
489
502
|
return withDoctorCiReport(parseArgs(argv), (args) => runInvocation(args, {
|
|
490
|
-
dispatch: (command, args, context) =>
|
|
503
|
+
dispatch: (command, args, context) => {
|
|
504
|
+
refuseJoinedFlag(args);
|
|
505
|
+
return dispatch(command, args, { ...context, argv, authentication, qcStandIns });
|
|
506
|
+
},
|
|
491
507
|
closeOutStaleRunSessions,
|
|
492
508
|
ambientRunSession,
|
|
493
509
|
lifecycleIdentity,
|
|
@@ -730,10 +746,17 @@ export function recordQaStageOutcome(args, result) {
|
|
|
730
746
|
// start and the spec material it judged, never the inputs at write time.
|
|
731
747
|
const qaStamps = inputStamps({ briefMaterial: verdict.source_brief_material, specMaterial: verdict.spec_hash });
|
|
732
748
|
const qaInputs = stageWriteInputs("qa", currentPacketInputs({ packet, packetPath }));
|
|
749
|
+
const pageScope = isObject(result.page_scope) ? result.page_scope : null;
|
|
733
750
|
const committed = commitAssemblyReport(workspace, (report) => {
|
|
734
751
|
if (hasLocalIdentity && !specHashesMatch(verdict.spec_hash, report.identity?.spec_material_hash)) {
|
|
735
752
|
throw new Error("Local-spec QA verdict belongs to a different material revision; report evidence was not changed.");
|
|
736
753
|
}
|
|
754
|
+
// A page-scoped run (#720) is bound to the build it checked; a report
|
|
755
|
+
// rebuilt since then keeps the QA it had.
|
|
756
|
+
if (pageScope && currentBuildFingerprint(report) !== pageScope.build_fingerprint) {
|
|
757
|
+
throw new Error("The build was recorded again during this page-scoped QA run; report evidence was not changed. Re-run qa run --page against the recorded build.");
|
|
758
|
+
}
|
|
759
|
+
const evidence = qaStageGateEvidence(verdict, report, result);
|
|
737
760
|
return recordProducerStageOutcome(report, {
|
|
738
761
|
stage: "qa",
|
|
739
762
|
disposition: verdict.disposition,
|
|
@@ -750,11 +773,19 @@ export function recordQaStageOutcome(args, result) {
|
|
|
750
773
|
// Which build this verdict judged, and the gates whose browser outcome
|
|
751
774
|
// the doctor's static scan defers to (qaGatePassedForCurrentBuild). A
|
|
752
775
|
// gate that never ran is left out, so silence never reads as a pass.
|
|
753
|
-
|
|
776
|
+
// A page-scoped record names its pages, the changed pages it pays
|
|
777
|
+
// for and the build whose full QA it extends.
|
|
778
|
+
evidence: pageScope ? { ...evidence, page_scope: {
|
|
779
|
+
pages: pageScope.pages, owed_pages: pageScope.owed_pages, base_build_fingerprint: pageScope.base_build_fingerprint,
|
|
780
|
+
} } : evidence,
|
|
754
781
|
// Counts-only: never order ids, refs, emails or URLs (see
|
|
755
782
|
// summarizePurchaseProof). This is what lets `next` tell a real purchase
|
|
756
|
-
// path from a `--test-order off` diagnostic.
|
|
757
|
-
|
|
783
|
+
// path from a `--test-order off` diagnostic. A page-scoped run places
|
|
784
|
+
// no orders and the change it pays touches no commerce, so the
|
|
785
|
+
// purchase proof of the full run it extends is kept.
|
|
786
|
+
proof: pageScope && isObject(report.stages?.qa?.purchase_proof)
|
|
787
|
+
? report.stages.qa.purchase_proof
|
|
788
|
+
: summarizePurchaseProof({ verdict, proofPolicy: packet.qa?.proof_policy }),
|
|
758
789
|
stamps: qaStamps,
|
|
759
790
|
inputs: qaInputs,
|
|
760
791
|
});
|
|
@@ -1024,7 +1055,7 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1024
1055
|
// A --map-id fetch runs here, but the fetched spec is written to the
|
|
1025
1056
|
// shared cache file inside prepareBuild, under the per-target lock, so it
|
|
1026
1057
|
// never replaces the spec another run is reading and hashing (#501).
|
|
1027
|
-
const { publishSpec, ...resolved } = await recorder.time("resolve-spec", () => resolveSpecPath(args, { deferCacheWrite: true }));
|
|
1058
|
+
const { publishSpec, fetchedSpec, ...resolved } = await recorder.time("resolve-spec", () => resolveSpecPath(args, { deferCacheWrite: true }));
|
|
1028
1059
|
// The spec as the operator named it, before resolution, so the build
|
|
1029
1060
|
// context can record which input to replay (a local file, or a map id
|
|
1030
1061
|
// fetched from a given store).
|
|
@@ -1035,7 +1066,7 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1035
1066
|
// Doctor runs inside prepare-build, so its codes are recorded while that
|
|
1036
1067
|
// phase is active and attach to it.
|
|
1037
1068
|
const result = await recorder.time("prepare-build", async () => {
|
|
1038
|
-
const prepared = await prepareBuild(args, { ...mode, command, specInput, publishSpec, sourceKind, wrapperPolicyFlag, orderPathDepthFlag });
|
|
1069
|
+
const prepared = await prepareBuild(args, { ...mode, command, specInput, publishSpec, fetchedSpec, sourceKind, wrapperPolicyFlag, orderPathDepthFlag });
|
|
1039
1070
|
if (prepared.doctor && !prepared.doctor.ok) recorder.recordFindingCodes(prepared.doctor.errors?.map((issue) => issue.code));
|
|
1040
1071
|
return prepared;
|
|
1041
1072
|
});
|
|
@@ -1257,6 +1288,8 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1257
1288
|
// The ambient session is handed over, not re-discovered: the closeout
|
|
1258
1289
|
// command a QA run prints has to agree with the run_id this session will
|
|
1259
1290
|
// later close and remit under.
|
|
1291
|
+
// --page repeats; the shared parser keeps only the last value.
|
|
1292
|
+
if (args._[1] === "run" && Array.isArray(argv) && argv.includes("--page")) args = { ...args, page: repeatedFlagValues(argv, "page") };
|
|
1260
1293
|
const result = await runQaCli(args, { ambient });
|
|
1261
1294
|
if (result?.verdict?.disposition === "blocked") {
|
|
1262
1295
|
recorder.recordFindingCodes(result.verdict.assertions?.filter((item) => item.status === "fail" || item.severity === "blocker")
|
|
@@ -1307,6 +1340,20 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1307
1340
|
);
|
|
1308
1341
|
}
|
|
1309
1342
|
|
|
1343
|
+
// A flag takes its value as the next token. `--flag=value` would otherwise be
|
|
1344
|
+
// an unknown key, silently ignored (`qa run --page=x` widening to a full run),
|
|
1345
|
+
// so it is refused at dispatch, like every other argv refusal; the stale
|
|
1346
|
+
// sweep before dispatch is skipped for it (JOINED_FLAGS). Only the name is
|
|
1347
|
+
// quoted: the value can be a secret, such as an --auth-cookie.
|
|
1348
|
+
function refuseJoinedFlag(args) {
|
|
1349
|
+
const flags = args?.[JOINED_FLAGS];
|
|
1350
|
+
if (!flags?.length) return;
|
|
1351
|
+
const [flag] = flags;
|
|
1352
|
+
if (!flag) throw refused("--=… is not accepted: the flag name is missing before =. Write the flag, then its value as the next argument (--flag value).");
|
|
1353
|
+
const name = `--${flag}`;
|
|
1354
|
+
throw refused(`${name}=… is not accepted: a flag takes its value as the next argument, not --flag=value. Write \`${name}\` alone for an on/off flag, or \`${name} <value>\` for a flag that takes a value.`);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1310
1357
|
export function parseArgs(argv) {
|
|
1311
1358
|
const args = { _: [] };
|
|
1312
1359
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -1315,6 +1362,12 @@ export function parseArgs(argv) {
|
|
|
1315
1362
|
args._.push(token);
|
|
1316
1363
|
continue;
|
|
1317
1364
|
}
|
|
1365
|
+
const equal = token.indexOf("=");
|
|
1366
|
+
if (equal >= 2) {
|
|
1367
|
+
if (!args[JOINED_FLAGS]) Object.defineProperty(args, JOINED_FLAGS, { value: [] });
|
|
1368
|
+
args[JOINED_FLAGS].push(token.slice(2, equal));
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1318
1371
|
const key = token.slice(2);
|
|
1319
1372
|
const next = argv[index + 1];
|
|
1320
1373
|
if (!next || next.startsWith("--")) {
|
|
@@ -1403,7 +1456,9 @@ async function resolveSpecPath(args, opts = {}) {
|
|
|
1403
1456
|
return { specPath: cachePath, source: "remote", mapId, proxyBase,
|
|
1404
1457
|
savedMapRevision: { map_id: mapId, hash: spec.spec_identity?.spec_hash || spec.spec_hash || null,
|
|
1405
1458
|
algorithm: "map-store-v1", local_spec_material_hash: specMaterialHash(spec) },
|
|
1406
|
-
|
|
1459
|
+
// The fetched spec itself, for a caller that writes the cache later and
|
|
1460
|
+
// must know the funnel's route first. Not part of the reported source.
|
|
1461
|
+
...(opts.deferCacheWrite ? { publishSpec, fetchedSpec: spec } : {}),
|
|
1407
1462
|
};
|
|
1408
1463
|
}
|
|
1409
1464
|
throw refused(
|
|
@@ -1532,11 +1587,13 @@ function createProofPolicy({ orderPathDepth = null } = {}) {
|
|
|
1532
1587
|
// channel that supplies it. Source screenshot proof is seeded only by the
|
|
1533
1588
|
// source-html manifest's pages[].screenshots[], which is documented nowhere in
|
|
1534
1589
|
// the blocking reason itself.
|
|
1535
|
-
|
|
1590
|
+
// `packagePath` is where this run emitted the package, relative to the target
|
|
1591
|
+
// repo: under the funnel's runtime directory.
|
|
1592
|
+
const designSourcePackageRemedy = (packagePath = DESIGN_SOURCE_PACKAGE_REL_PATH) => [
|
|
1536
1593
|
`Supply the missing source proof through pages[].screenshots[] in ${SOURCE_HTML_MANIFEST_REL_PATH}`,
|
|
1537
1594
|
"under the source root, or in a manifest anywhere else named by --design-manifest <path>",
|
|
1538
1595
|
"(one available desktop record and one available mobile record per renderable page);",
|
|
1539
|
-
`then, if no downstream stage has consumed it, remove the Design Source Package this blocked run emitted at ${
|
|
1596
|
+
`then, if no downstream stage has consumed it, remove the Design Source Package this blocked run emitted at ${packagePath}`,
|
|
1540
1597
|
"and rerun prepare-build/start.",
|
|
1541
1598
|
'See "Clearing DESIGN_SOURCE_PACKAGE_NOT_READY" in docs/design-source-package.md.',
|
|
1542
1599
|
].join(" ");
|
|
@@ -1547,12 +1604,13 @@ const DESIGN_SOURCE_PACKAGE_REMEDY = [
|
|
|
1547
1604
|
// exception reasons keep their own wording.
|
|
1548
1605
|
const DESIGN_SOURCE_PACKAGE_REMEDY_REASON = /source_screenshot proof\.$|^Source TODO "capture-(?![^"]*-template-)[^"]*" is /;
|
|
1549
1606
|
|
|
1550
|
-
function designSourcePackageBlockers(prepared) {
|
|
1607
|
+
function designSourcePackageBlockers(prepared, targetRepo) {
|
|
1551
1608
|
if (!["blocked", "pending"].includes(prepared.value?.readiness?.status)) return [];
|
|
1609
|
+
const remedy = designSourcePackageRemedy(prepared.path && targetRepo ? relFromDir(targetRepo, prepared.path) : undefined);
|
|
1552
1610
|
return (prepared.value.readiness.blocking_reasons || []).map((message, index) => ({
|
|
1553
1611
|
code: "DESIGN_SOURCE_PACKAGE_NOT_READY",
|
|
1554
1612
|
stage: "prepare_build",
|
|
1555
|
-
message: DESIGN_SOURCE_PACKAGE_REMEDY_REASON.test(message) ? `${message} ${
|
|
1613
|
+
message: DESIGN_SOURCE_PACKAGE_REMEDY_REASON.test(message) ? `${message} ${remedy}` : message,
|
|
1556
1614
|
detail: {
|
|
1557
1615
|
blocker_index: index,
|
|
1558
1616
|
readiness_status: prepared.value.readiness.status,
|
|
@@ -1657,18 +1715,44 @@ async function prepareBuild(args, options = {}) {
|
|
|
1657
1715
|
if (publishSpec && !existsSync(targetRepo)) mkdirSync(targetRepo, { recursive: true });
|
|
1658
1716
|
if (!existsSync(targetRepo) || !statSync(targetRepo).isDirectory()) throw new Error(`Target repo is not a directory: ${targetRepo}`);
|
|
1659
1717
|
|
|
1660
|
-
|
|
1661
|
-
|
|
1718
|
+
// Which funnel this is decides where its packet and records go (#719): a
|
|
1719
|
+
// project's first funnel uses the default packet and the project-level
|
|
1720
|
+
// .campaign-runtime/, every later one its own (planFunnelWorkspace). The
|
|
1721
|
+
// route is read here, before the lock, and checked again under it.
|
|
1722
|
+
const plannedRoute = intakeRouteSlug(options.fetchedSpec || readSpecForRoute(specPath), args);
|
|
1723
|
+
const planFunnel = () => plannedRoute
|
|
1724
|
+
? refusing(() => planFunnelWorkspace(targetRepo, plannedRoute, { packetPath: args.out || null, force: args.force === true }))
|
|
1725
|
+
: { packetPath: resolve(args.out || join(targetRepo, DEFAULT_PACKET_FILENAME)), runtimeRelPath: RUNTIME_DIR_REL_PATH, scoped: false, addedBeside: null };
|
|
1726
|
+
const funnel = planFunnel();
|
|
1727
|
+
const sidecars = campaignSidecarPaths(targetRepo, funnel.runtimeRelPath);
|
|
1728
|
+
const packetPath = funnel.packetPath;
|
|
1662
1729
|
const contextPath = resolve(args["context-out"] || sidecars.contextPath);
|
|
1663
1730
|
const reportPath = resolve(args["report-out"] || sidecars.reportPath);
|
|
1664
1731
|
const doctorOutPath = resolve(args["doctor-out"] || sidecars.doctorOutPath);
|
|
1665
|
-
const briefPath = resolve(args["brief-out"] || join(
|
|
1732
|
+
const briefPath = resolve(args["brief-out"] || join(sidecars.runtimeDir, relative(RUNTIME_DIR_REL_PATH, BUILD_BRIEF_NORMALIZED_REL_PATH)));
|
|
1666
1733
|
return withDesignSourcePublication({
|
|
1667
1734
|
targetRepo,
|
|
1735
|
+
runtimeDir: sidecars.runtimeDir,
|
|
1668
1736
|
outputs: { packetPath, contextPath, reportPath, doctorOutPath, briefPath },
|
|
1669
1737
|
force: args.force === true,
|
|
1670
1738
|
publishSpec,
|
|
1671
|
-
}, (publication) => prepareBuildUnderLock({ args, options, specPath, sourceRoot, targetRepo, publication }));
|
|
1739
|
+
}, (publication) => prepareBuildUnderLock({ args, options, specPath, sourceRoot, targetRepo, publication, funnel: { ...funnel, route: plannedRoute }, planFunnel }));
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// A spec that cannot be read here is read again, and refused, under the lock.
|
|
1743
|
+
function readSpecForRoute(specPath) {
|
|
1744
|
+
try {
|
|
1745
|
+
return readJson(specPath);
|
|
1746
|
+
} catch {
|
|
1747
|
+
return null;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// The public route slug intake will use, from the flag or the spec; null when
|
|
1752
|
+
// neither names one (prepare-build then refuses under the lock).
|
|
1753
|
+
function intakeRouteSlug(spec, args) {
|
|
1754
|
+
if (!isObject(spec)) return optionalString(args["public-route-slug"]) || null;
|
|
1755
|
+
return normalizePublicRouteSlug(campaignIdentity(spec, args).publicRouteSlug) || null;
|
|
1672
1756
|
}
|
|
1673
1757
|
|
|
1674
1758
|
function prepareBuildUnderLock({
|
|
@@ -1678,6 +1762,8 @@ function prepareBuildUnderLock({
|
|
|
1678
1762
|
sourceRoot,
|
|
1679
1763
|
targetRepo,
|
|
1680
1764
|
publication,
|
|
1765
|
+
funnel,
|
|
1766
|
+
planFunnel,
|
|
1681
1767
|
}) {
|
|
1682
1768
|
const { packetPath, contextPath, reportPath, doctorOutPath, briefPath, designSourcePackagePath } = publication.paths;
|
|
1683
1769
|
// #531: a host-prefixed route ("shop.example.com/route/upsell/") in a Map
|
|
@@ -1701,6 +1787,22 @@ function prepareBuildUnderLock({
|
|
|
1701
1787
|
throw new Error("CampaignSpec requires exactly one identity: a saved spec_identity.map_id, or an agent-authored spec_identity.local_spec_id (1–64 letters, digits, underscores or hyphens). Keep the local ID stable across revisions; do not invent a Map ID.");
|
|
1702
1788
|
}
|
|
1703
1789
|
if (!publicRouteSlug) throw new Error("CampaignSpec has no public route slug. Set spec_identity.public_route_slug or campaign.slug.");
|
|
1790
|
+
// The packet and runtime directory were chosen for the route read before
|
|
1791
|
+
// the lock; a spec that changed meanwhile would file this funnel's records
|
|
1792
|
+
// under another route's.
|
|
1793
|
+
if (normalizePublicRouteSlug(publicRouteSlug) !== funnel.route) {
|
|
1794
|
+
throw new Error(`The CampaignSpec's public route slug changed while prepare-build waited for the target lock (${JSON.stringify(funnel.route)} became ${JSON.stringify(publicRouteSlug)}). Re-run prepare-build.`);
|
|
1795
|
+
}
|
|
1796
|
+
// Another funnel set up while this run waited can change where this one
|
|
1797
|
+
// belongs (two first funnels started at once in an empty project). The
|
|
1798
|
+
// paths this run writes were fixed before the lock, so it stops instead.
|
|
1799
|
+
const replanned = planFunnel();
|
|
1800
|
+
if (replanned.packetPath !== funnel.packetPath || replanned.runtimeRelPath !== funnel.runtimeRelPath) {
|
|
1801
|
+
throw new Error(
|
|
1802
|
+
`Another funnel was set up in ${targetRepo} while prepare-build waited for the target lock, so route ${JSON.stringify(funnel.route)} now belongs at `
|
|
1803
|
+
+ `${replanned.packetPath} with its records under ${replanned.runtimeRelPath}/, not ${funnel.packetPath} and ${funnel.runtimeRelPath}/. Re-run prepare-build.`,
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1704
1806
|
|
|
1705
1807
|
// Dispatch validated the argv-only flags before spec resolution, including
|
|
1706
1808
|
// the value forms of --template-family, --allow-uncertified-template,
|
|
@@ -1932,7 +2034,7 @@ function prepareBuildUnderLock({
|
|
|
1932
2034
|
mapId,
|
|
1933
2035
|
publicRouteSlug,
|
|
1934
2036
|
});
|
|
1935
|
-
const designSourceBlockers = designSourcePackageBlockers(designSourcePackage);
|
|
2037
|
+
const designSourceBlockers = designSourcePackageBlockers(designSourcePackage, targetRepo);
|
|
1936
2038
|
const blockers = [...sourceBlockers, ...briefBlockers, ...briefQuestionBlockers, ...designSourceBlockers];
|
|
1937
2039
|
const adapterDecisions = createAdapterDecisions({ commerceZoneFindings, wrapperPolicy: wrapperPolicy.value });
|
|
1938
2040
|
const proofPolicy = createProofPolicy({ orderPathDepth: orderPathDepthFlag });
|
|
@@ -1977,6 +2079,9 @@ function prepareBuildUnderLock({
|
|
|
1977
2079
|
assembly: {
|
|
1978
2080
|
implementation: "next-campaigns-build",
|
|
1979
2081
|
target_repo: relFromFile(packetPath, targetRepo),
|
|
2082
|
+
// Recorded only for a funnel that is not the project's first; absent,
|
|
2083
|
+
// the records are under the project-level .campaign-runtime/.
|
|
2084
|
+
...(funnel.scoped ? { runtime_dir: funnel.runtimeRelPath } : {}),
|
|
1980
2085
|
output_dir: outputDir,
|
|
1981
2086
|
template_family: isUnresolvedTemplateFamily(templateFamily) ? "undecided" : templateFamily,
|
|
1982
2087
|
template_decision_notes: templateLocked
|
|
@@ -2050,6 +2155,7 @@ function prepareBuildUnderLock({
|
|
|
2050
2155
|
path: portable(specPath),
|
|
2051
2156
|
hash: specFileHash,
|
|
2052
2157
|
material_hash: specMaterialHash(spec),
|
|
2158
|
+
route_masked_material_hash: routeMaskedSpecMaterialHash(spec),
|
|
2053
2159
|
active_pages: activePages.map((page) => ({
|
|
2054
2160
|
id: page.id,
|
|
2055
2161
|
type: page.type || null,
|
|
@@ -2102,7 +2208,7 @@ function prepareBuildUnderLock({
|
|
|
2102
2208
|
target_repo: ".",
|
|
2103
2209
|
output_dir: portable(resolve(targetRepo, outputDir)),
|
|
2104
2210
|
handoff_skill: existsSync(resolve(targetRepo, outputDir)) ? "next-campaigns-build" : "next-campaigns-os-setup",
|
|
2105
|
-
handoff_artifact:
|
|
2211
|
+
handoff_artifact: `${funnel.runtimeRelPath}/setup-handoff.json`,
|
|
2106
2212
|
reason: existsSync(resolve(targetRepo, outputDir))
|
|
2107
2213
|
? "Target campaign output directory already exists."
|
|
2108
2214
|
: "Target campaign output directory is missing; scaffold before build.",
|
|
@@ -2222,6 +2328,7 @@ function prepareBuildUnderLock({
|
|
|
2222
2328
|
context,
|
|
2223
2329
|
report,
|
|
2224
2330
|
doctor,
|
|
2331
|
+
...(funnel.addedBeside ? { funnelAddedBeside: funnel.addedBeside } : {}),
|
|
2225
2332
|
};
|
|
2226
2333
|
}
|
|
2227
2334
|
|
|
@@ -2505,11 +2612,8 @@ function rejectUnknownStandardizeFlags(args) {
|
|
|
2505
2612
|
const known = new Set(STANDARDIZE_FLAGS);
|
|
2506
2613
|
const unknown = Object.keys(args).filter((key) => key !== "_" && !known.has(key));
|
|
2507
2614
|
if (!unknown.length) return;
|
|
2508
|
-
const valueHint = unknown.some((key) => key.includes("="))
|
|
2509
|
-
? " A flag takes its value as the next argument (--flag value), not --flag=value."
|
|
2510
|
-
: "";
|
|
2511
2615
|
throw refused(
|
|
2512
|
-
`Unknown flag${unknown.length > 1 ? "s" : ""} for standardize: ${unknown.map((key) => `--${key}`).join(", ")}
|
|
2616
|
+
`Unknown flag${unknown.length > 1 ? "s" : ""} for standardize: ${unknown.map((key) => `--${key}`).join(", ")}. Known flags: ${STANDARDIZE_FLAGS.map((key) => `--${key}`).join(", ")}.`,
|
|
2513
2617
|
);
|
|
2514
2618
|
}
|
|
2515
2619
|
|
|
@@ -2601,7 +2705,9 @@ function themeCommand(args) {
|
|
|
2601
2705
|
// (blocked, already-current CSS) leaves doctor's inputs untouched (Kilo
|
|
2602
2706
|
// review, PR #176).
|
|
2603
2707
|
if (written.wrote?.report || written.wrote?.css) {
|
|
2604
|
-
|
|
2708
|
+
const staleTarget = resolveFromFile(packetPath, packet?.assembly?.target_repo) || dirname(packetPath);
|
|
2709
|
+
markDoctorSidecarStale(staleTarget, {
|
|
2710
|
+
path: campaignSidecarPaths(staleTarget, packetRuntimeRelPath(packet)).doctorOutPath,
|
|
2605
2711
|
command: "theme generate",
|
|
2606
2712
|
reason: `Theme artifacts were generated after this doctor snapshot. Re-run ${cmd("doctor")} (or next) for current state.`,
|
|
2607
2713
|
});
|
|
@@ -3286,8 +3392,7 @@ export function pageKitSyncCommand(args) {
|
|
|
3286
3392
|
// Assembly Report whose waivers are honoured below.
|
|
3287
3393
|
const unknown = Object.keys(args).filter((key) => key !== "_" && !PAGE_KIT_SYNC_FLAGS.includes(key));
|
|
3288
3394
|
if (unknown.length) {
|
|
3289
|
-
|
|
3290
|
-
throw refused(`Unknown flag${unknown.length > 1 ? "s" : ""} for page-kit sync: ${unknown.map((key) => `--${key}`).join(", ")}.${valueHint} Known flags: ${PAGE_KIT_SYNC_FLAGS.map((key) => `--${key}`).join(", ")}.`);
|
|
3395
|
+
throw refused(`Unknown flag${unknown.length > 1 ? "s" : ""} for page-kit sync: ${unknown.map((key) => `--${key}`).join(", ")}. Known flags: ${PAGE_KIT_SYNC_FLAGS.map((key) => `--${key}`).join(", ")}.`);
|
|
3291
3396
|
}
|
|
3292
3397
|
const packetPath = resolve(requireArg(args, "packet"));
|
|
3293
3398
|
const dryRun = isDryRun(args);
|
|
@@ -3364,11 +3469,7 @@ export function pageKitSyncCommand(args) {
|
|
|
3364
3469
|
// spec.local_path would write another campaign's store profile into this
|
|
3365
3470
|
// route with a plausible-looking diff.
|
|
3366
3471
|
if (spec && publicRouteSlug) {
|
|
3367
|
-
const specSlug =
|
|
3368
|
-
optionalString(spec.spec_identity?.public_route_slug)
|
|
3369
|
-
|| optionalString(spec.campaign?.slug)
|
|
3370
|
-
|| optionalString(spec.campaign?.id),
|
|
3371
|
-
);
|
|
3472
|
+
const specSlug = specPublicRouteSlug(spec);
|
|
3372
3473
|
const specMapId = optionalString(spec.spec_identity?.map_id) || optionalString(spec.map_id);
|
|
3373
3474
|
const packetMapId = optionalString(packet.spec?.map_id);
|
|
3374
3475
|
if (specSlug && specSlug !== publicRouteSlug) {
|
|
@@ -3494,6 +3595,7 @@ export function pageKitSyncCommand(args) {
|
|
|
3494
3595
|
// result, never a thrown error that hides the write.
|
|
3495
3596
|
try {
|
|
3496
3597
|
markDoctorSidecarStale(targetRepo, {
|
|
3598
|
+
path: campaignSidecarPaths(targetRepo, packetRuntimeRelPath(packet)).doctorOutPath,
|
|
3497
3599
|
command: "page-kit sync",
|
|
3498
3600
|
reason: `page-kit sync rewrote ${PAGE_KIT_CAMPAIGNS_REL_PATH}[${publicRouteSlug}] after this doctor snapshot. Re-run ${cmd("doctor")} (or next) for current state.`,
|
|
3499
3601
|
});
|
|
@@ -3524,7 +3626,7 @@ export function pageKitSyncCommand(args) {
|
|
|
3524
3626
|
// the default run stays offline. --dry-run prints the same diff and writes
|
|
3525
3627
|
// nothing. Exit 2 when the packet, the spec or the target entry is missing,
|
|
3526
3628
|
// or the store cannot be read.
|
|
3527
|
-
const SPEC_DERIVE_FLAGS = Object.freeze(["packet", "dry-run", "json", "report", "from-store", "store-token-source"]);
|
|
3629
|
+
const SPEC_DERIVE_FLAGS = Object.freeze(["packet", "dry-run", "json", "report", "from-store", "store-token-source", "no-live-refs"]);
|
|
3528
3630
|
|
|
3529
3631
|
// The store flags, parsed once for both the async reader and the sync
|
|
3530
3632
|
// command: `{ subdomain, token_env }` when --from-store is given, null when
|
|
@@ -3552,9 +3654,9 @@ function parseSpecDeriveStoreFlags(args) {
|
|
|
3552
3654
|
// run the same command with the store read in hand. The only network the
|
|
3553
3655
|
// command ever does happens here, and the token never leaves this function:
|
|
3554
3656
|
// the result names the credential source, never its value.
|
|
3555
|
-
export async function specDeriveFromStoreCommand(args, { fetchImpl = globalThis.fetch, env = process.env, credentials, warn = console.warn } = {}) {
|
|
3657
|
+
export async function specDeriveFromStoreCommand(args, { fetchImpl = globalThis.fetch, env = process.env, credentials, warn = console.warn, liveCampaign = undefined } = {}) {
|
|
3556
3658
|
const store = parseSpecDeriveStoreFlags(args);
|
|
3557
|
-
if (!store) return specDeriveCommand(args);
|
|
3659
|
+
if (!store) return specDeriveCommand(args, { liveCampaign });
|
|
3558
3660
|
const preflight = specDeriveCommand(args, { store: { ...store, status: "preflight" } });
|
|
3559
3661
|
if (preflight.errors.length) return preflight;
|
|
3560
3662
|
let read;
|
|
@@ -3567,7 +3669,36 @@ export async function specDeriveFromStoreCommand(args, { fetchImpl = globalThis.
|
|
|
3567
3669
|
read = await readGatewayStoreProfile({ subdomain: store.subdomain, credentials, fetchImpl });
|
|
3568
3670
|
}
|
|
3569
3671
|
// Recheck the original packet/spec identity after the network operation.
|
|
3570
|
-
return specDeriveCommand(args, { store: { ...store, ...read, expected: { spec_path: preflight.spec_path, public_route_slug: preflight.public_route_slug } } });
|
|
3672
|
+
return specDeriveCommand(args, { liveCampaign, store: { ...store, ...read, expected: { spec_path: preflight.spec_path, public_route_slug: preflight.public_route_slug } } });
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
// The live campaign read `spec derive` makes (#721): the one read-only GET of
|
|
3676
|
+
// {proxy-base}/api/campaign doctor makes, under the same rule (packet mode,
|
|
3677
|
+
// the packet's built _site/<route>/ exists, a public Campaigns API key
|
|
3678
|
+
// resolves), with the campaign body kept so the mirrored offer and package
|
|
3679
|
+
// fields can be refreshed from it. --no-live-refs sends nothing. `undefined`
|
|
3680
|
+
// means no read was due.
|
|
3681
|
+
export async function readSpecDeriveLiveCampaign(args, { fetchImpl = globalThis.fetch, env = process.env, warn = undefined } = {}) {
|
|
3682
|
+
if (liveRefsDisabled(args)) return disabledLiveRead();
|
|
3683
|
+
if (!isNonEmptyString(args.packet)) return undefined;
|
|
3684
|
+
let workspace;
|
|
3685
|
+
try {
|
|
3686
|
+
workspace = resolveCampaignWorkspace(resolve(args.packet), { followContextPointer: false });
|
|
3687
|
+
} catch {
|
|
3688
|
+
// An unreadable packet is the derive's own error; there is nothing to read for.
|
|
3689
|
+
return undefined;
|
|
3690
|
+
}
|
|
3691
|
+
const slug = normalizePublicRouteSlug(workspace.packet?.campaign?.public_route_slug);
|
|
3692
|
+
if (!workspace.targetRepo || !slug || !existsSync(join(workspace.targetRepo, "_site", slug))) return undefined;
|
|
3693
|
+
return readLiveCampaignForPacket({
|
|
3694
|
+
packet: workspace.packet,
|
|
3695
|
+
packetPath: workspace.packetPath,
|
|
3696
|
+
env,
|
|
3697
|
+
fetchImpl,
|
|
3698
|
+
proxyBase: optionalString(args["proxy-base"]) || DEFAULT_PROXY_BASE,
|
|
3699
|
+
includeCampaign: true,
|
|
3700
|
+
...(warn ? { warn } : {}),
|
|
3701
|
+
});
|
|
3571
3702
|
}
|
|
3572
3703
|
|
|
3573
3704
|
// The page files page-kit renders under the campaign's source directory, with
|
|
@@ -3595,30 +3726,32 @@ function listPageKitPageFiles(outputDir, publicRouteSlug, { assetsHtmlIgnored =
|
|
|
3595
3726
|
// page-kit honours a permalink before its filename rule, so the
|
|
3596
3727
|
// frontmatter is read first (quoting, comments, BOM and CRLF as
|
|
3597
3728
|
// page-kit's reader takes them); only a file with no usable permalink
|
|
3598
|
-
// falls back to the filename route.
|
|
3599
|
-
|
|
3729
|
+
// falls back to the filename route. The page_type and the links to
|
|
3730
|
+
// other pages are read too: after handoff they say which files are
|
|
3731
|
+
// funnel pages and how they link (#721).
|
|
3732
|
+
const { permalink, page_type: pageType, next_url: nextUrl, decline_url: declineUrl, next_step: nextStep, step_number: stepNumber } = frontmatterScalars(readFileSync(fullPath, "utf8"));
|
|
3733
|
+
const facts = { page_type: pageType, next_url: nextUrl, decline_url: declineUrl, next_step: nextStep, step_number: stepNumber };
|
|
3600
3734
|
if (permalink === null) {
|
|
3601
3735
|
const filenameRoute = pageRouteForFile(path);
|
|
3602
3736
|
if (filenameRoute === null) continue;
|
|
3603
|
-
files.push({ path, basename: basename(entry.name, ".html"), route: filenameRoute, permalink: null, problem: null });
|
|
3737
|
+
files.push({ path, basename: basename(entry.name, ".html"), route: filenameRoute, permalink: null, problem: null, ...facts });
|
|
3604
3738
|
continue;
|
|
3605
3739
|
}
|
|
3606
3740
|
const resolved = permalinkRoute(permalink, publicRouteSlug);
|
|
3607
|
-
files.push({ path, basename: basename(entry.name, ".html"), route: resolved.route ?? null, permalink, problem: resolved.problem ?? null });
|
|
3741
|
+
files.push({ path, basename: basename(entry.name, ".html"), route: resolved.route ?? null, permalink, problem: resolved.problem ?? null, ...facts });
|
|
3608
3742
|
}
|
|
3609
3743
|
};
|
|
3610
3744
|
walk(outputDir);
|
|
3611
3745
|
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
3612
3746
|
}
|
|
3613
3747
|
|
|
3614
|
-
export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
3748
|
+
export function specDeriveCommand(args, { store: storeRead = null, liveCampaign = undefined } = {}) {
|
|
3615
3749
|
// A command that rewrites the spec rejects flags it does not know: a
|
|
3616
3750
|
// mistyped --dryrun must not fall through to a real write. --report names
|
|
3617
3751
|
// the Assembly Report whose waivers are honoured below.
|
|
3618
3752
|
const unknown = Object.keys(args).filter((key) => key !== "_" && !SPEC_DERIVE_FLAGS.includes(key));
|
|
3619
3753
|
if (unknown.length) {
|
|
3620
|
-
|
|
3621
|
-
throw refused(`Unknown flag${unknown.length > 1 ? "s" : ""} for spec derive: ${unknown.map((key) => `--${key}`).join(", ")}.${valueHint} Known flags: ${SPEC_DERIVE_FLAGS.map((key) => `--${key}`).join(", ")}.`);
|
|
3754
|
+
throw refused(`Unknown flag${unknown.length > 1 ? "s" : ""} for spec derive: ${unknown.map((key) => `--${key}`).join(", ")}. Known flags: ${SPEC_DERIVE_FLAGS.map((key) => `--${key}`).join(", ")}.`);
|
|
3622
3755
|
}
|
|
3623
3756
|
// The store read is supplied by specDeriveFromStoreCommand; this function
|
|
3624
3757
|
// never touches the network itself, so a --from-store call that reaches it
|
|
@@ -3646,6 +3779,8 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3646
3779
|
not_derived: [],
|
|
3647
3780
|
not_in_target: [],
|
|
3648
3781
|
stale_hints: [],
|
|
3782
|
+
page_scope: null,
|
|
3783
|
+
live_campaign: null,
|
|
3649
3784
|
store: storeFlags
|
|
3650
3785
|
? { 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 }
|
|
3651
3786
|
: null,
|
|
@@ -3709,11 +3844,7 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3709
3844
|
// (campaign.route_slug_identity); without it a stale or copy-pasted
|
|
3710
3845
|
// spec.local_path would receive another campaign's routes and pin.
|
|
3711
3846
|
if (spec && publicRouteSlug) {
|
|
3712
|
-
const specSlug =
|
|
3713
|
-
optionalString(spec.spec_identity?.public_route_slug)
|
|
3714
|
-
|| optionalString(spec.campaign?.slug)
|
|
3715
|
-
|| optionalString(spec.campaign?.id),
|
|
3716
|
-
);
|
|
3847
|
+
const specSlug = specPublicRouteSlug(spec);
|
|
3717
3848
|
const specMapId = optionalString(spec.spec_identity?.map_id) || optionalString(spec.map_id);
|
|
3718
3849
|
const packetMapId = optionalString(packet.spec?.map_id);
|
|
3719
3850
|
if (specSlug && specSlug !== publicRouteSlug) {
|
|
@@ -3836,7 +3967,24 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3836
3967
|
addIssue(result.errors, `spec.derive.${codes[storeRead.status] || "store_unreachable"}`, `${storeRead.detail} Nothing was written.`, { subdomain: storeRead.subdomain, token_source: storeRead.token_env ? `env:${storeRead.token_env}` : "gateway:login" });
|
|
3837
3968
|
return result;
|
|
3838
3969
|
}
|
|
3839
|
-
|
|
3970
|
+
// After handoff the page tree also says which pages exist (#721).
|
|
3971
|
+
const handedOff = campaignHandedOff(report);
|
|
3972
|
+
const plan = planSpecDerive({ spec, entry, pageFiles, packetBindings, waivedGates, waiversUnknown, publicRouteSlug, handedOff });
|
|
3973
|
+
result.page_scope = plan.page_scope;
|
|
3974
|
+
// The mirrored offer and package fields, from the live campaign read
|
|
3975
|
+
// (#721). A read that did not happen leaves them as they are and says why.
|
|
3976
|
+
if (liveCampaign?.status === "read" && isObject(liveCampaign.campaign)) {
|
|
3977
|
+
const mirror = planCampaignMirrorDerive({ spec, campaign: liveCampaign.campaign, campaignRef: liveCampaign.campaign_ref_id || null });
|
|
3978
|
+
plan.changes.push(...mirror.changes);
|
|
3979
|
+
plan.unchanged.push(...mirror.unchanged);
|
|
3980
|
+
plan.not_derived.push(...mirror.not_derived);
|
|
3981
|
+
result.live_campaign = { status: "read", key_source: liveCampaign.key_source || null, campaign_ref_id: liveCampaign.campaign_ref_id || null, ...mirror.summary };
|
|
3982
|
+
} else if (liveCampaign) {
|
|
3983
|
+
result.live_campaign = { status: "not_run", reason_code: liveCampaign.reason_code || null, reason: liveCampaign.reason || null, key_source: liveCampaign.key_source || null };
|
|
3984
|
+
if (!["disabled", "no_key", "not_read"].includes(liveCampaign.reason_code)) {
|
|
3985
|
+
addIssue(result.warnings, "spec.derive.live_campaign_not_read", `The live campaign was not read (${singleLineDetail(liveCampaign.reason || liveCampaign.reason_code || "unknown reason")}); the spec's offer and package fields were left as they are.`, { reason_code: liveCampaign.reason_code || null });
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3840
3988
|
let storeChanged = false;
|
|
3841
3989
|
if (storeRead) {
|
|
3842
3990
|
const storePlan = planStoreProfileDerive({ spec, store: storeRead.store, pages: storeRead.pages, pagesStatus: storeRead.pages_status, pagesDetail: storeRead.pages_detail, subdomain: storeRead.subdomain });
|
|
@@ -3860,11 +4008,18 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3860
4008
|
addIssue(result.warnings, `spec.derive.${row.reason}`, `${row.field} was not derived: ${row.detail}`, { field: row.field, reason: row.reason, ...(row.page_id ? { page_id: row.page_id } : {}) });
|
|
3861
4009
|
}
|
|
3862
4010
|
for (const hint of plan.stale_hints) {
|
|
3863
|
-
addIssue(result.warnings, "spec.derive.routing_hint_stale", `page "${hint.page_id}" carries sdk_hints.meta_tags.${hint.tag} ${JSON.stringify(hint.value)}, but page "${hint.target_page_id}" now derives to ${JSON.stringify(hint.derived_route)}.
|
|
4011
|
+
addIssue(result.warnings, "spec.derive.routing_hint_stale", `page "${hint.page_id}" carries sdk_hints.meta_tags.${hint.tag} ${JSON.stringify(hint.value)}, but page "${hint.target_page_id}" now derives to ${JSON.stringify(hint.derived_route)}. Derive rewrites a hint only on an edge it re-points after handoff; edit the hint to that route in the spec (or, while the Map is still the spec's source, re-save the Map) so the built meta tag and doctor's expectation agree. Doctor blocks and record spec refuses until then.`, hint);
|
|
3864
4012
|
}
|
|
3865
4013
|
for (const block of plan.created_blocks || []) {
|
|
3866
4014
|
addIssue(result.warnings, "spec.derive.analytics_block_created", `${block} ${dryRun ? "would be" : "is"} created from the repo's id: the spec ${dryRun ? "would then declare" : "now declares"} an analytics contract, so QA expects that tag to fire instead of treating analytics as advisory.`);
|
|
3867
4015
|
}
|
|
4016
|
+
// A vendor block changes what doctor reads; it turns on QA's analytics leg
|
|
4017
|
+
// only when it is the first analytics the spec carries (a provider block
|
|
4018
|
+
// created beside it already says so).
|
|
4019
|
+
const vendorOpensAnalytics = plan.analytics_created === true && !(plan.created_blocks || []).length;
|
|
4020
|
+
for (const block of plan.created_vendor_blocks || []) {
|
|
4021
|
+
addIssue(result.warnings, "spec.derive.analytics_vendor_created", `${block} ${dryRun ? "would be" : "is"} created from the repo's id: doctor ${dryRun ? "would then read" : "now reads"} that vendor's campaigns.json key against the spec${vendorOpensAnalytics ? `, and the spec ${dryRun ? "would then carry" : "now carries"} an analytics block, so QA runs its analytics leg and expects the typed-card order to fire a Purchase` : ""}.`);
|
|
4022
|
+
}
|
|
3868
4023
|
|
|
3869
4024
|
// What a write entails is said in both modes, so a dry run previews the
|
|
3870
4025
|
// warnings a real run would carry: a lossy round trip, a build now stale,
|
|
@@ -3878,8 +4033,9 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3878
4033
|
if (serialize(spec) !== text) {
|
|
3879
4034
|
addIssue(result.warnings, "spec.derive.file_reformatted", `${result.spec_path} ${would} re-serialized with ${indent === "\t" ? "tab" : `${indent.length}-space`} indentation; formatting outside the derived fields (key order, whitespace, number spelling) may differ from the original. Review the file diff before committing.`);
|
|
3880
4035
|
}
|
|
3881
|
-
if (plan.changes.some((row) => row.page_id)) {
|
|
3882
|
-
|
|
4036
|
+
if (plan.changes.some((row) => row.page_id && (row.op || /\.page_url$/.test(row.field)))) {
|
|
4037
|
+
const pageSetMoved = plan.changes.some((row) => row.op && row.page_id);
|
|
4038
|
+
addIssue(result.warnings, "spec.derive.projection_stale", `${pageSetMoved ? "A page added or removed" : "A page route moved"} ${dryRun ? "would leave" : "leaves"} the packet's page-kit projection (source_html.pages[].page_kit) and the Build Context describing the old pages. Bind it with ${cmd("record")} spec, which re-projects them for a routes-only change and, on a handed-off campaign, for pages added or removed; any other page-scope change needs ${cmd("prepare-build")}.`);
|
|
3883
4039
|
}
|
|
3884
4040
|
if (stageIsTerminal(report?.stages?.assembly?.status)) {
|
|
3885
4041
|
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.`);
|
|
@@ -3931,6 +4087,7 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3931
4087
|
// The retained doctor snapshot (if any) now predates the spec it judged.
|
|
3932
4088
|
try {
|
|
3933
4089
|
markDoctorSidecarStale(targetRepo, {
|
|
4090
|
+
path: campaignSidecarPaths(targetRepo, packetRuntimeRelPath(packet)).doctorOutPath,
|
|
3934
4091
|
command: "spec derive",
|
|
3935
4092
|
reason: `spec derive rewrote the CampaignSpec after this doctor snapshot. Re-run ${cmd("doctor")} (or next) for current state.`,
|
|
3936
4093
|
});
|
|
@@ -3981,6 +4138,14 @@ export function specDeriveTextLines(result) {
|
|
|
3981
4138
|
}
|
|
3982
4139
|
if (result.unchanged?.length) lines.push(`Unchanged: ${result.unchanged.map((row) => row.field).join(", ")}`);
|
|
3983
4140
|
if (result.not_in_target?.length) lines.push(`Not in target (left as they are): ${result.not_in_target.join(", ")}`);
|
|
4141
|
+
if (result.page_scope?.added?.length) lines.push(`Pages added from the page tree: ${result.page_scope.added.map((page) => `${singleLineField(page.page_id)} (${singleLineField(page.file)})`).join(", ")}`);
|
|
4142
|
+
if (result.page_scope?.removed?.length) lines.push(`Pages removed (their built file is gone): ${result.page_scope.removed.map((page) => `${singleLineField(page.page_id)} (${singleLineField(page.file)})`).join(", ")}`);
|
|
4143
|
+
if (result.page_scope?.unclaimed?.length) lines.push(`Page files no spec page binds (pages are added from the tree only after handoff): ${result.page_scope.unclaimed.map(singleLineField).join(", ")}`);
|
|
4144
|
+
if (result.live_campaign) {
|
|
4145
|
+
lines.push(result.live_campaign.status === "read"
|
|
4146
|
+
? `Live campaign: read${result.live_campaign.campaign_ref_id ? ` (campaign ${singleLineField(result.live_campaign.campaign_ref_id)})` : ""}; offer and package fields refreshed from it`
|
|
4147
|
+
: `Live campaign: not read (${singleLineField(result.live_campaign.reason_code || "unknown")}); offer and package fields left as they are`);
|
|
4148
|
+
}
|
|
3984
4149
|
if (result.warnings?.length) {
|
|
3985
4150
|
lines.push("Warnings:");
|
|
3986
4151
|
for (const issue of result.warnings) lines.push(`- ${formatIssueSummary(issue)}`);
|
|
@@ -4379,7 +4544,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4379
4544
|
let runRecordCloseout = null;
|
|
4380
4545
|
try {
|
|
4381
4546
|
runRecordCloseout = assessRunRecordCloseout({
|
|
4382
|
-
records: readRunRecordsForTarget(dirname(packetPath)),
|
|
4547
|
+
records: readRunRecordsForTarget(dirname(packetPath), packetRuntimeRelPath(packet)),
|
|
4383
4548
|
packet,
|
|
4384
4549
|
report,
|
|
4385
4550
|
currentQaVerdictDigests: currentQaVerdictDigestsForReport(report, report ? reportPath : null, targetRepo),
|
|
@@ -4405,7 +4570,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4405
4570
|
// command names the report this run read when that is not the default
|
|
4406
4571
|
// one (from --report or the Build Context pointer), so it records there.
|
|
4407
4572
|
const qc = readCurrentQcResults({ report, doctor, spec, targetRepo, packetPath, reportPath, qcStandIns, rederivers: qcRederivers });
|
|
4408
|
-
result.qc_handoff = buildQcHandoff({ results: qc.results, coverage: qc.coverage, accepts: report?.qc_accepts, packetPath, reportPath: explicitReportPath(reportPath, targetRepo) });
|
|
4573
|
+
result.qc_handoff = buildQcHandoff({ results: qc.results, coverage: qc.coverage, accepts: report?.qc_accepts, packetPath, reportPath: explicitReportPath(reportPath, targetRepo, packetRuntimeRelPath(packet)) });
|
|
4409
4574
|
// One note per operator decision that kept the build output unchanged
|
|
4410
4575
|
// after an input change, quoting the operator's reason.
|
|
4411
4576
|
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}"`);
|
|
@@ -5083,14 +5248,15 @@ function templateStockPromptLine(packet, derived = {}) {
|
|
|
5083
5248
|
|
|
5084
5249
|
function buildPrompt(packetPath, contextPath, reportPath, packet, derived = {}, intent) {
|
|
5085
5250
|
const briefPath = packet.build_brief?.normalized_path || "(missing; generate or confirm Campaign Build Brief before business-sensitive assembly)";
|
|
5251
|
+
const runtime = runtimeRelPathOf(packet);
|
|
5086
5252
|
return `${campaignIntentPromptHeader(intent)}Use next-campaigns-build for this Campaigns OS handoff.
|
|
5087
5253
|
|
|
5088
5254
|
Read first:
|
|
5089
5255
|
- Build Packet: ${packetPath}
|
|
5090
|
-
- Build Context: ${contextPath ||
|
|
5091
|
-
- Assembly Report: ${reportPath ||
|
|
5256
|
+
- Build Context: ${contextPath || `(use packet-adjacent ${runtime}/build-context.json if present)`}
|
|
5257
|
+
- Assembly Report: ${reportPath || `(use packet-adjacent ${runtime}/assembly-report.json if present)`}
|
|
5092
5258
|
- Campaign Build Brief: ${briefPath}
|
|
5093
|
-
- Design Source Package:
|
|
5259
|
+
- Design Source Package: ${runtime}/input/design-source-package.json when present; use report.design_source_package.material_fingerprint as the source context fingerprint.
|
|
5094
5260
|
- Template family: ${packet.assembly.template_family}${templateStockPromptLine(packet, derived)}
|
|
5095
5261
|
|
|
5096
5262
|
Rules:
|
|
@@ -5101,7 +5267,7 @@ Rules:
|
|
|
5101
5267
|
- 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.
|
|
5102
5268
|
- Preserve prepared source HTML for landing/presell pages when it is a real standalone design.
|
|
5103
5269
|
- For checkout/upsell/downsell/receipt, use the starter template as the SDK contract reference only: preserve required data-next controls and runtime wiring, but let the campaign/source own visual chrome, copy hierarchy, imagery, and brand layer.
|
|
5104
|
-
- Read context.theme and
|
|
5270
|
+
- Read context.theme and ${runtime}/theme/theme-report.json when present. If a fresh brand-theme.css exists, copy it into the campaign assets/css folder and list it after next-core.css in checkout/upsell/downsell/receipt frontmatter styles; when theme generate also wrote brand-fonts.css (the design's own faces), copy it beside brand-theme.css and list it on the same pages; if policy is inspect_only, generate or skip explicitly before applying a new brand layer.
|
|
5105
5271
|
- Generated brand-theme.css v0 is root-variable-only. Do not edit SDK-owned selectors, package controls, payment fields, totals, submit controls, receipt templates, route meta tags, or SDK JavaScript as part of theme application.
|
|
5106
5272
|
- If you copy starter-template files, copy the selected family atomically with dependent pages, _includes, _layouts, assets/css, and assets/js; do not copy only checkout.html and receipt.html.
|
|
5107
5273
|
- Resolve SDK routing meta tags to campaign-root paths such as ${campaignRouteRoot(packet) || `/${packet.campaign.public_route_slug}/`}upsell/, not source filenames or unrooted spec literals.
|
|
@@ -5173,7 +5339,7 @@ Read first:
|
|
|
5173
5339
|
|
|
5174
5340
|
Prepare the target page-kit structure and agent context, then record setup:
|
|
5175
5341
|
- ${cmd("record")} setup --packet ${packetPath}
|
|
5176
|
-
It sets
|
|
5342
|
+
It sets the Build Context's scaffold.required=false and the Assembly Report's stages.setup to completed, validated against their schemas; do not hand-edit either file.
|
|
5177
5343
|
|
|
5178
5344
|
When copying a starter template family, copy the family as an atomic page-kit slice: pages plus required _includes, _layouts, assets/css, and assets/js. Do not copy only checkout.html and receipt.html.
|
|
5179
5345
|
|
|
@@ -5875,11 +6041,22 @@ export function resolvePinSources(args, { cwd = process.cwd() } = {}) {
|
|
|
5875
6041
|
// An explicit packet names its project: its target repo, whatever the cwd.
|
|
5876
6042
|
const projectStart = packet ? targetRepoFor(packetPath, packet) : cwd;
|
|
5877
6043
|
const packageJsonPath = nearestPackageJson(projectStart);
|
|
6044
|
+
let packetUnread = false;
|
|
5878
6045
|
if (!packetPath) {
|
|
5879
6046
|
// The contracted home of the packet is the project root beside package.json
|
|
5880
6047
|
// (prepare-build's default --out), the same place readback discovers it.
|
|
5881
|
-
|
|
5882
|
-
|
|
6048
|
+
// A project holding several funnels has one packet each; reading any one
|
|
6049
|
+
// of them would be a guess, so none is read and the warning names them.
|
|
6050
|
+
const projectRoot = packageJsonPath ? dirname(packageJsonPath) : resolve(cwd);
|
|
6051
|
+
packetPath = join(projectRoot, DEFAULT_PACKET_FILENAME);
|
|
6052
|
+
try {
|
|
6053
|
+
packetPath = soleProjectPacket(projectRoot, { command: "tooling status" }) || packetPath;
|
|
6054
|
+
} catch (error) {
|
|
6055
|
+
if (error?.code !== "MULTIPLE_FUNNEL_PACKETS") throw error;
|
|
6056
|
+
packetUnread = true;
|
|
6057
|
+
warnings.push(`Packet pin not read: ${error.message}`);
|
|
6058
|
+
}
|
|
6059
|
+
if (!packetUnread && existsSync(packetPath)) packet = readPinJson(packetPath, packetProblem);
|
|
5883
6060
|
}
|
|
5884
6061
|
|
|
5885
6062
|
const { projectSpec, projectManifest, projectKey } = resolveProjectPin(packageJsonPath, warnings);
|
|
@@ -5896,7 +6073,7 @@ export function resolvePinSources(args, { cwd = process.cwd() } = {}) {
|
|
|
5896
6073
|
packetVersionIgnored = typeof recorded === "string" ? recorded : JSON.stringify(recorded);
|
|
5897
6074
|
warnings.push(`Packet campaigns_os_version ignored: ${JSON.stringify(recorded)} in ${packetPath} is not an exact version.`);
|
|
5898
6075
|
}
|
|
5899
|
-
} else if (existsSync(packetPath) && !packet) {
|
|
6076
|
+
} else if (!packetUnread && existsSync(packetPath) && !packet) {
|
|
5900
6077
|
warnings.push(`Packet pin unavailable: ${packetPath} ${packetProblem.reason}.`);
|
|
5901
6078
|
}
|
|
5902
6079
|
return {
|
|
@@ -7359,7 +7536,7 @@ async function runRecordCommand(args, ambient = null, { silent = false, promptFo
|
|
|
7359
7536
|
const parsedSurfaces = parseRunRecordSurfaces(args.surfaces);
|
|
7360
7537
|
const packet = readJson(packetPath);
|
|
7361
7538
|
const explicitTargetRepo = resolveFromFile(packetPath, packet.assembly?.target_repo);
|
|
7362
|
-
const { baseDir, targetRepo, contextPath, reportPath } = resolveCampaignWorkspace(packetPath, {
|
|
7539
|
+
const { baseDir, runtimeRelPath, targetRepo, contextPath, reportPath } = resolveCampaignWorkspace(packetPath, {
|
|
7363
7540
|
packet,
|
|
7364
7541
|
contextPath: args.context ? resolve(args.context) : undefined,
|
|
7365
7542
|
reportPath: args.report ? resolve(args.report) : undefined,
|
|
@@ -7403,7 +7580,7 @@ async function runRecordCommand(args, ambient = null, { silent = false, promptFo
|
|
|
7403
7580
|
// session decides without it, and a target with a long history pays no I/O
|
|
7404
7581
|
// for a decision already made.
|
|
7405
7582
|
const needsDiskScan = listOnly || (!optionalString(args["run-id"]) && args["new-run"] !== true && !isNonEmptyString(ambient?.session?.run_id));
|
|
7406
|
-
const targetRecords = needsDiskScan ? readRunRecordsForTarget(baseDir) : [];
|
|
7583
|
+
const targetRecords = needsDiskScan ? readRunRecordsForTarget(baseDir, runtimeRelPath) : [];
|
|
7407
7584
|
const latestRecordEntry = needsDiskScan ? latestMatchingRunRecord(targetRecords, packet) : null;
|
|
7408
7585
|
let runId;
|
|
7409
7586
|
let runIdSource;
|
|
@@ -7544,7 +7721,7 @@ async function runRecordCommand(args, ambient = null, { silent = false, promptFo
|
|
|
7544
7721
|
// may not. A pure --no-write run reads nothing: it writes and sends nothing.
|
|
7545
7722
|
// A --dry-run does read it, because what a real run would do here is the
|
|
7546
7723
|
// very thing the dry run is being asked to report.
|
|
7547
|
-
const prior = write || dryRun ? readPriorRunRecord(runId, baseDir) : null;
|
|
7724
|
+
const prior = write || dryRun ? readPriorRunRecord(runId, baseDir, runtimeRelPath) : null;
|
|
7548
7725
|
const priorRemit = priorRemitOutcome(prior?.record);
|
|
7549
7726
|
const storedRemotely = priorRemit?.state === "ok";
|
|
7550
7727
|
// A pure local-inspection run (--no-write), an explicit --no-remit, or a
|
|
@@ -7743,7 +7920,7 @@ async function runRecordCommand(args, ambient = null, { silent = false, promptFo
|
|
|
7743
7920
|
// carries this run's remit_* outcome rather than the placeholders it was
|
|
7744
7921
|
// assembled with. Validated above, ahead of the send; nothing is written
|
|
7745
7922
|
// under --dry-run or --no-write.
|
|
7746
|
-
const recordPath = write ? writeRunRecord(record, { baseDir }) : null;
|
|
7923
|
+
const recordPath = write ? writeRunRecord(record, { baseDir, runtimeRelPath }) : null;
|
|
7747
7924
|
|
|
7748
7925
|
const summary = {
|
|
7749
7926
|
ok: true,
|
|
@@ -7775,7 +7952,7 @@ async function runRecordCommand(args, ambient = null, { silent = false, promptFo
|
|
|
7775
7952
|
// stopped the send anyway, and null with `would_remit_refused` set when the
|
|
7776
7953
|
// transport's destination gate refuses the base before any socket opens).
|
|
7777
7954
|
// Envelope only; never on the record.
|
|
7778
|
-
...(dryRun ? { dry_run: true, would_write: resolveRunRecordPath(runId, baseDir), would_remit: wouldRemitEndpoint, would_remit_refused: wouldRemitRefusal } : {}),
|
|
7955
|
+
...(dryRun ? { dry_run: true, would_write: resolveRunRecordPath(runId, baseDir, runtimeRelPath), would_remit: wouldRemitEndpoint, would_remit_refused: wouldRemitRefusal } : {}),
|
|
7779
7956
|
};
|
|
7780
7957
|
if (silent) return summary;
|
|
7781
7958
|
if (args.json) {
|
|
@@ -7828,10 +8005,10 @@ function assertRunRecordValid(record) {
|
|
|
7828
8005
|
// `writeRunRecord` could have written is trusted as a prior — the same
|
|
7829
8006
|
// validator gates both — so a file that merely says `remit_state: "ok"` is
|
|
7830
8007
|
// replaced like a corrupt one, never preserved or handed back as the record.
|
|
7831
|
-
function readPriorRunRecord(runId, baseDir) {
|
|
8008
|
+
function readPriorRunRecord(runId, baseDir, runtimeRelPath) {
|
|
7832
8009
|
let path;
|
|
7833
8010
|
try {
|
|
7834
|
-
path = resolveRunRecordPath(runId, baseDir);
|
|
8011
|
+
path = resolveRunRecordPath(runId, baseDir, runtimeRelPath);
|
|
7835
8012
|
} catch {
|
|
7836
8013
|
return null;
|
|
7837
8014
|
}
|
|
@@ -7913,6 +8090,26 @@ function artifactRefPath(kind, filePath, baseDir) {
|
|
|
7913
8090
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
7914
8091
|
}
|
|
7915
8092
|
|
|
8093
|
+
// The packet's runtime directory for a printed prompt; a packet that records
|
|
8094
|
+
// an unusable one names the project-level directory (doctor refuses it).
|
|
8095
|
+
function runtimeRelPathOf(packet) {
|
|
8096
|
+
try {
|
|
8097
|
+
return packetRuntimeRelPath(packet);
|
|
8098
|
+
} catch {
|
|
8099
|
+
return RUNTIME_DIR_REL_PATH;
|
|
8100
|
+
}
|
|
8101
|
+
}
|
|
8102
|
+
|
|
8103
|
+
// The runtime directory of the packet at `packetPath`, or the project-level
|
|
8104
|
+
// one when it cannot be read (the printed command then carries --report).
|
|
8105
|
+
function runtimeRelPathAt(packetPath) {
|
|
8106
|
+
try {
|
|
8107
|
+
return packetPath ? packetRuntimeRelPath(readJson(packetPath)) : RUNTIME_DIR_REL_PATH;
|
|
8108
|
+
} catch {
|
|
8109
|
+
return RUNTIME_DIR_REL_PATH;
|
|
8110
|
+
}
|
|
8111
|
+
}
|
|
8112
|
+
|
|
7916
8113
|
// The verdict a Run Record should carry when none was named: every candidate
|
|
7917
8114
|
// the report records or the qa-output directories hold, minus any the QA
|
|
7918
8115
|
// verdict receiver stamped `trusted: false` (an anonymous submission is
|
|
@@ -7925,7 +8122,7 @@ function inferQaVerdictPath({ packet, report, reportPath = null, targetRepo = nu
|
|
|
7925
8122
|
const eligible = discoverQaVerdicts({
|
|
7926
8123
|
packet,
|
|
7927
8124
|
report,
|
|
7928
|
-
reportPath: reportPath || (targetRepo ? campaignSidecarPaths(targetRepo).reportPath : null),
|
|
8125
|
+
reportPath: reportPath || (targetRepo ? campaignSidecarPaths(targetRepo, packetRuntimeRelPath(packet)).reportPath : null),
|
|
7929
8126
|
roots: [targetRepo, baseDir],
|
|
7930
8127
|
}).filter((candidate) => candidate.verdict && candidate.trusted
|
|
7931
8128
|
&& ((packet?.spec?.local_spec_id == null && candidate.verdict.local_spec_id == null) || candidate.identityMatch));
|
|
@@ -7941,16 +8138,14 @@ const ARGV_SHAPE_PRIVATE_FLAGS = new Set(["auth-cookie", "no-remit", "no-write",
|
|
|
7941
8138
|
|
|
7942
8139
|
// argv SHAPE = selected flag NAMES present, never their values (minimization).
|
|
7943
8140
|
// Sorted + de-duplicated for deterministic records; opt-out and endpoint flags
|
|
7944
|
-
// stay private.
|
|
7945
|
-
//
|
|
7946
|
-
// leak into a persisted, potentially-remitted shape, breaking the guarantee.
|
|
8141
|
+
// stay private. parseArgs keeps a `--flag=value` token off the keys, so no
|
|
8142
|
+
// value (e.g. --auth-cookie=SECRET) can reach a persisted, remitted shape.
|
|
7947
8143
|
function argvShape(args) {
|
|
7948
8144
|
const names = new Set();
|
|
7949
8145
|
for (const key of Object.keys(args)) {
|
|
7950
8146
|
if (key === "_") continue;
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
names.add(`--${name}`);
|
|
8147
|
+
if (ARGV_SHAPE_PRIVATE_FLAGS.has(key)) continue;
|
|
8148
|
+
names.add(`--${key}`);
|
|
7954
8149
|
}
|
|
7955
8150
|
return [...names].sort();
|
|
7956
8151
|
}
|
|
@@ -8466,7 +8661,7 @@ export function doctorRequiredActionLines(result) {
|
|
|
8466
8661
|
// cannot be ruled out.
|
|
8467
8662
|
const inspectedReportPath = typeof derived?.assembly_report_path === "string" ? derived.assembly_report_path : null;
|
|
8468
8663
|
const inferredReportPath = typeof derived?.target_repo === "string"
|
|
8469
|
-
? campaignSidecarPaths(derived.target_repo).reportPath
|
|
8664
|
+
? campaignSidecarPaths(derived.target_repo, runtimeRelPathAt(packetPath)).reportPath
|
|
8470
8665
|
: null;
|
|
8471
8666
|
const reportPath = inspectedReportPath && inspectedReportPath !== inferredReportPath ? inspectedReportPath : null;
|
|
8472
8667
|
const lines = [];
|
|
@@ -8503,6 +8698,19 @@ function printPrepareResult(result, args) {
|
|
|
8503
8698
|
}
|
|
8504
8699
|
}
|
|
8505
8700
|
console.log(`Packet: ${result.packetPath}`);
|
|
8701
|
+
if (result.funnelAddedBeside) {
|
|
8702
|
+
// A route differing from the default packet's is either another funnel
|
|
8703
|
+
// or a rename of that one; only the operator knows which, so say what was
|
|
8704
|
+
// done and how to undo it.
|
|
8705
|
+
const beside = result.funnelAddedBeside;
|
|
8706
|
+
const route = result.packet?.campaign?.public_route_slug;
|
|
8707
|
+
const runtimeDir = result.packet?.assembly?.runtime_dir;
|
|
8708
|
+
console.log(
|
|
8709
|
+
`Funnel: route ${route} was set up as another funnel beside ${beside.packetPath} (route ${beside.route || "unnamed"})`
|
|
8710
|
+
+ `${runtimeDir ? `, with its records under ${runtimeDir}/` : ""}. To rename that funnel's route instead, delete ${result.packetPath}`
|
|
8711
|
+
+ `${runtimeDir ? ` and ${runtimeDir}/` : ""}, then re-run prepare-build with --out ${beside.packetPath} --force.`,
|
|
8712
|
+
);
|
|
8713
|
+
}
|
|
8506
8714
|
if (result.briefPath) {
|
|
8507
8715
|
const brief = result.context?.build_brief;
|
|
8508
8716
|
console.log(`Brief: ${result.briefPath}${brief ? ` (${brief.mode}, ${brief.status}, questions=${brief.question_count})` : ""}`);
|
|
@@ -8650,16 +8858,30 @@ export async function specDeriveWithMapWriteback(args, { fetchImpl = undefined,
|
|
|
8650
8858
|
if (Object.hasOwn(args, "proxy-base") && !optionalString(args["proxy-base"])) {
|
|
8651
8859
|
throw refused("spec derive: --proxy-base needs a URL (https, or a loopback host); nothing was written.");
|
|
8652
8860
|
}
|
|
8653
|
-
if (
|
|
8654
|
-
throw refused(
|
|
8861
|
+
if (Object.hasOwn(args, "no-live-refs") && args["no-live-refs"] !== true) {
|
|
8862
|
+
throw refused(`--no-live-refs takes no value (got ${JSON.stringify(args["no-live-refs"])}); write \`--no-live-refs\` on its own, after the other flags.`);
|
|
8655
8863
|
}
|
|
8656
8864
|
const writeMap = args["write-map"] === true;
|
|
8657
8865
|
const localArgs = Object.fromEntries(Object.entries(args).filter(([key]) => !SPEC_DERIVE_MAP_FLAGS.includes(key)));
|
|
8866
|
+
// The live campaign read (#721), under the proxy --proxy-base names, is
|
|
8867
|
+
// made only once the local checks pass: a dry run of the repo half first,
|
|
8868
|
+
// so a missing packet, spec or entry (or an unknown flag) sends nothing.
|
|
8869
|
+
// --no-live-refs sends nothing at all.
|
|
8870
|
+
const unknownFlags = Object.keys(localArgs).filter((key) => key !== "_" && !SPEC_DERIVE_FLAGS.includes(key));
|
|
8871
|
+
// The store flags are judged first, as the local command judges them, so
|
|
8872
|
+
// the preflight below (which leaves them out) never changes which refusal
|
|
8873
|
+
// a malformed invocation gets.
|
|
8874
|
+
if (!unknownFlags.length) parseSpecDeriveStoreFlags(localArgs);
|
|
8875
|
+
const preflightArgs = Object.fromEntries(Object.entries(localArgs).filter(([key]) => key !== "from-store" && key !== "store-token-source"));
|
|
8876
|
+
const preflight = unknownFlags.length || liveRefsDisabled(args) ? null : specDeriveCommand({ ...preflightArgs, "dry-run": true });
|
|
8877
|
+
const liveCampaign = liveRefsDisabled(args)
|
|
8878
|
+
? disabledLiveRead()
|
|
8879
|
+
: preflight && !preflight.errors.length ? await readSpecDeriveLiveCampaign(args, { ...(fetchImpl ? { fetchImpl } : {}), env, ...(warn ? { warn } : {}) }) : undefined;
|
|
8658
8880
|
// A copy, so nothing here depends on the local command handing out a
|
|
8659
8881
|
// mutable object; the Map outcome is added beside its fields, not into them.
|
|
8660
8882
|
// The store source (--from-store) is the other network read; it runs
|
|
8661
8883
|
// before the Map write-back so the pin it reports is the one written.
|
|
8662
|
-
const local = Object.hasOwn(localArgs, "from-store") ? await specDeriveFromStoreCommand(localArgs) : specDeriveCommand(localArgs);
|
|
8884
|
+
const local = Object.hasOwn(localArgs, "from-store") ? await specDeriveFromStoreCommand(localArgs, { liveCampaign }) : specDeriveCommand(localArgs, { liveCampaign });
|
|
8663
8885
|
const result = { ...local, write_map: writeMap, map: null };
|
|
8664
8886
|
if (!writeMap) return result;
|
|
8665
8887
|
const skipped = (reason, detail) => {
|