@nextcommerce/campaigns-os 1.33.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 +204 -0
- package/CHANGELOG.md +5002 -0
- package/CONTEXT.md +685 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +368 -0
- package/agents/claude/CLAUDE.md +32 -0
- package/agents/codex/AGENTS.md +27 -0
- package/agents/copilot/copilot-instructions.md +14 -0
- package/agents/cursor/campaigns-os.mdc +13 -0
- package/bin/campaigns-os.mjs +38 -0
- package/campaign-spec/README.md +138 -0
- package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
- package/campaign-spec/dist/analytics-vocabulary.js +74 -0
- package/campaign-spec/dist/index.d.ts +40 -0
- package/campaign-spec/dist/index.js +77 -0
- package/campaign-spec/dist/normalize.d.ts +22 -0
- package/campaign-spec/dist/normalize.js +41 -0
- package/campaign-spec/dist/routing.d.ts +190 -0
- package/campaign-spec/dist/routing.js +263 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
- package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
- package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
- package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
- package/campaign-spec/dist/rules/cycle-detection.js +141 -0
- package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
- package/campaign-spec/dist/rules/design-source-shape.js +142 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
- package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
- package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
- package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
- package/campaign-spec/dist/rules/funnel-count.js +37 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
- package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
- package/campaign-spec/dist/rules/funnel-identity.js +57 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
- package/campaign-spec/dist/rules/index.d.ts +69 -0
- package/campaign-spec/dist/rules/index.js +131 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
- package/campaign-spec/dist/rules/page-count.d.ts +11 -0
- package/campaign-spec/dist/rules/page-count.js +31 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
- package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
- package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
- package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
- package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
- package/campaign-spec/dist/rules/schema-version.js +53 -0
- package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
- package/campaign-spec/dist/rules/sdk-version.js +96 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
- package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
- package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
- package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
- package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
- package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
- package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
- package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
- package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
- package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
- package/campaign-spec/dist/sdk-version-parse.js +62 -0
- package/campaign-spec/dist/types.d.ts +674 -0
- package/campaign-spec/dist/types.js +40 -0
- package/campaign-spec/package.json +12 -0
- package/compatibility.json +25 -0
- package/contracts/agent-relevant-change-policy.v1.json +111 -0
- package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
- package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
- package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
- package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
- package/contracts/commerce-surface-catalog.json +2452 -0
- package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
- package/contracts/fixtures/orientation/envelope/current.json +95 -0
- package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
- package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
- package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
- package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
- package/contracts/fixtures/orientation/envelope/refused.json +100 -0
- package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
- package/contracts/fixtures/orientation/envelope/updated.json +135 -0
- package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
- package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
- package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
- package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
- package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
- package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
- package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
- package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
- package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
- package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
- package/contracts/migration-sidecar-bundle.v0.json +149 -0
- package/contracts/orientation-limits.v1.json +41 -0
- package/contracts/orientation-reason-codes.v1.json +196 -0
- package/contracts/private-template-sources.json +8 -0
- package/contracts/release-ledger.json +4598 -0
- package/contracts/reserved-skill-names.json +13 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
- package/contracts/supported-surface.json +180 -0
- package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.apollo.v0.json +27 -0
- package/contracts/template-brand-contract.demeter.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
- package/contracts/template-brand-contract.olympus.v0.json +27 -0
- package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
- package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
- package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.apollo.v0.json +12 -0
- package/contracts/template-slot-manifest.demeter.v0.json +30 -0
- package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
- package/contracts/template-slot-manifest.olympus.v0.json +12 -0
- package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
- package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
- package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
- package/docs/brand-theme-bridge.md +159 -0
- package/docs/build-packet.md +1300 -0
- package/docs/campaign-build-brief.md +145 -0
- package/docs/campaign-standardization-report.md +329 -0
- package/docs/campaigns-os-build-flow.md +117 -0
- package/docs/design-source-package.md +784 -0
- package/docs/legacy-migration.md +58 -0
- package/docs/migration-sidecar-bundle.md +139 -0
- package/docs/orientation-contract-reference.md +1220 -0
- package/docs/polish-evidence.md +502 -0
- package/docs/qa-and-test-orders.md +1691 -0
- package/docs/release-ledger-authoring-guide.md +274 -0
- package/docs/runtime-readiness.md +211 -0
- package/docs/supported-surface.md +83 -0
- package/docs/versioning.md +55 -0
- package/docs/workflow-findings-sidecar.md +588 -0
- package/package.json +135 -0
- package/prompts/first-build.md +27 -0
- package/prompts/friction-log.md +30 -0
- package/schemas/campaign-build-brief.v1.schema.json +149 -0
- package/schemas/campaign-design-source-package.v0.schema.json +697 -0
- package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
- package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
- package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
- package/schemas/campaign-spec.v4.schema.json +582 -0
- package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
- package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
- package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
- package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
- package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
- package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
- package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
- package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
- package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
- package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
- package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
- package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
- package/schemas/source-html-manifest.v0.schema.json +252 -0
- package/skills/next-campaigns-build/SKILL.md +73 -0
- package/skills/next-campaigns-os/SKILL.md +101 -0
- package/skills/next-campaigns-os/references/session-intake.md +160 -0
- package/skills/next-campaigns-os-setup/SKILL.md +22 -0
- package/skills/next-campaigns-polish/SKILL.md +145 -0
- package/skills/next-campaigns-qa/SKILL.md +92 -0
- package/skills.json +56 -0
- package/skills.sh +64 -0
- package/src/adapter-decision-contract.mjs +333 -0
- package/src/brand-theme.mjs +1151 -0
- package/src/browser-launch.mjs +79 -0
- package/src/build-brief.mjs +781 -0
- package/src/built-site-scope.mjs +312 -0
- package/src/campaign-ecosystem.mjs +734 -0
- package/src/campaign-identity.mjs +405 -0
- package/src/campaign-workspace.mjs +127 -0
- package/src/checkpoint-waiver.mjs +302 -0
- package/src/cli.mjs +13442 -0
- package/src/commercial-journey.mjs +1119 -0
- package/src/commercial-parity.mjs +965 -0
- package/src/consent.mjs +347 -0
- package/src/content-residue.mjs +322 -0
- package/src/deadline.mjs +81 -0
- package/src/design-source-package.mjs +2604 -0
- package/src/deviation.mjs +107 -0
- package/src/doctor-check-registry.mjs +49 -0
- package/src/doctor-sidecar.mjs +106 -0
- package/src/finding-cause.mjs +557 -0
- package/src/findings.mjs +326 -0
- package/src/fs-identity.mjs +68 -0
- package/src/gate-actions.mjs +105 -0
- package/src/html-scan.mjs +53 -0
- package/src/install-mode.mjs +272 -0
- package/src/legacy-migration.d.ts +128 -0
- package/src/legacy-migration.mjs +510 -0
- package/src/lifecycle.mjs +338 -0
- package/src/local-proof.mjs +401 -0
- package/src/map-pin-writeback.mjs +210 -0
- package/src/orchestration-stage-contract.mjs +81 -0
- package/src/package-install-fixture.mjs +33 -0
- package/src/page-kit-build-summary.mjs +175 -0
- package/src/page-kit-campaign-config.mjs +57 -0
- package/src/page-kit-sdk-version.mjs +392 -0
- package/src/page-kit-store-profile.mjs +369 -0
- package/src/page-kit-sync.mjs +162 -0
- package/src/polish-browser.mjs +867 -0
- package/src/polish-capture.mjs +1094 -0
- package/src/polish-deadline.mjs +51 -0
- package/src/polish-gate.mjs +739 -0
- package/src/polish-node.mjs +639 -0
- package/src/polish-page-load.mjs +1100 -0
- package/src/private-template-source.mjs +237 -0
- package/src/proof-policy.mjs +82 -0
- package/src/qa-analytics-correctness.mjs +307 -0
- package/src/qa-analytics-errors.mjs +38 -0
- package/src/qa-analytics-parity.mjs +699 -0
- package/src/qa-binding-evidence.mjs +140 -0
- package/src/qa-browser.mjs +6608 -0
- package/src/qa-cart-entry.mjs +406 -0
- package/src/qa-commercial-parity.mjs +641 -0
- package/src/qa-node.mjs +3620 -0
- package/src/qa-order-bump.mjs +381 -0
- package/src/qa-parity-capture.mjs +428 -0
- package/src/qa-parity-fixture.mjs +359 -0
- package/src/qa-publish.mjs +362 -0
- package/src/qa-purchase-data-layer.mjs +263 -0
- package/src/qa-route-probe.mjs +272 -0
- package/src/qa-sidecar.mjs +188 -0
- package/src/qa-test-order-topology.mjs +207 -0
- package/src/qa-url-privacy.mjs +13 -0
- package/src/qa-verdict-discovery.mjs +192 -0
- package/src/qa-verdict-publish.mjs +105 -0
- package/src/qa-verdict.mjs +287 -0
- package/src/remit.mjs +388 -0
- package/src/repo-scan.mjs +83 -0
- package/src/route-identity.mjs +133 -0
- package/src/run-record-closeout.mjs +229 -0
- package/src/run-record.mjs +839 -0
- package/src/run-session.mjs +226 -0
- package/src/runtime-state-ignore.mjs +113 -0
- package/src/sdk-attribute-index.mjs +212 -0
- package/src/sdk-markup.mjs +358 -0
- package/src/sdk-meta-tags.mjs +52 -0
- package/src/shell-token.mjs +7 -0
- package/src/sidecar-bundle.mjs +399 -0
- package/src/source-asset-crawl.mjs +469 -0
- package/src/source-html-intake.mjs +627 -0
- package/src/source-html-manifest.mjs +276 -0
- package/src/source-prep.mjs +284 -0
- package/src/spec-derive-store.mjs +431 -0
- package/src/spec-derive.mjs +514 -0
- package/src/spec-fetch.mjs +66 -0
- package/src/spec-hash.mjs +48 -0
- package/src/spec-identity.mjs +27 -0
- package/src/stage-ledger.mjs +509 -0
- package/src/standardization-report.mjs +1297 -0
- package/src/template-brand-contract.mjs +473 -0
- package/src/template-freshness.mjs +196 -0
- package/src/template-reference.mjs +81 -0
- package/src/template-slot-manifest.mjs +150 -0
- package/src/text-safety.mjs +62 -0
- package/src/theme-gate.mjs +185 -0
- package/src/upsell-selector-scope.mjs +299 -0
|
@@ -0,0 +1,1300 @@
|
|
|
1
|
+
# Build Packet
|
|
2
|
+
|
|
3
|
+
The Build Packet is the campaign assembly handoff. It wraps, but does not replace, the CampaignSpec.
|
|
4
|
+
|
|
5
|
+
It answers:
|
|
6
|
+
|
|
7
|
+
- Which CampaignSpec and Map ID are we building?
|
|
8
|
+
- Which public route slug and campaign directory are expected?
|
|
9
|
+
- Where are the prepared HTML/assets?
|
|
10
|
+
- Which Campaign Build Brief is the merchandising/design presentation truth?
|
|
11
|
+
- Which target page-kit repo should be updated?
|
|
12
|
+
- Which starter template family is locked?
|
|
13
|
+
- Which commerce catalog/contract should the agent read?
|
|
14
|
+
- Which deploy target, SDK origin state, and QA proof depth apply?
|
|
15
|
+
|
|
16
|
+
The current schema is `schemas/campaign-runtime-build-packet.v0.schema.json`.
|
|
17
|
+
|
|
18
|
+
## Root-Served Campaigns (`campaign.route_root`)
|
|
19
|
+
|
|
20
|
+
Most campaigns are served under a slug prefix (`/<public_route_slug>/...`), and
|
|
21
|
+
that stays the default. A campaign whose whole funnel is served from the **site
|
|
22
|
+
root** — pages at `/checkout-v2`, `/oto-ruggie`, `/receipt` with no slug prefix,
|
|
23
|
+
the normal shape for a single-campaign site or an in-place static deploy —
|
|
24
|
+
declares `campaign.route_root: "/"`. Rules:
|
|
25
|
+
|
|
26
|
+
- `public_route_slug` stays **required** either way: it is the campaign
|
|
27
|
+
identity and the `_site/<public_route_slug>/` built-output directory name.
|
|
28
|
+
`route_root` describes the *served* path shape only.
|
|
29
|
+
- When present, `route_root` must be `"/"` or `"/<public_route_slug>/"`; any
|
|
30
|
+
other prefix is a doctor blocker (`campaign.route_root`) because it would
|
|
31
|
+
contradict the slug identity the built-output checks root on. `qa run`
|
|
32
|
+
reads the packet by the same rule: a value doctor blocks never roots a QA
|
|
33
|
+
check either — QA audits the slug-prefixed default instead — so a
|
|
34
|
+
hand-edited packet cannot pass QA at a root doctor refuses.
|
|
35
|
+
- Doctor's routing-meta checks (`routing_meta.runtime_root`,
|
|
36
|
+
`sdk_hints.meta_tags.route_mismatch`) and route displays validate against the
|
|
37
|
+
declared route root instead of assuming slug-as-prefix, so a root-served
|
|
38
|
+
funnel's correct `/receipt`-style metas pass without waivers.
|
|
39
|
+
- The CampaignSpec may carry the same declaration at `campaign.route_root` (or
|
|
40
|
+
`spec_identity.route_root`); `prepare-build` copies it onto the packet and
|
|
41
|
+
defaults `live_url_path` to `/`.
|
|
42
|
+
- Two `sdk_hints.meta_tags` keys older Map exports still carry, `next-currency`
|
|
43
|
+
and `next-predictive-address`, are not read by the Campaign Cart SDK (the
|
|
44
|
+
list is `src/sdk-meta-tags.mjs`; QA reads the same one). Doctor never
|
|
45
|
+
requires them from the built page: a spec that lists one gets a single
|
|
46
|
+
advisory warning per page, `sdk_hints.meta_tags.ignored_by_sdk`, naming the
|
|
47
|
+
key and the reason (`remove from the Map's page hints; the SDK does not read
|
|
48
|
+
it`), whether or not the tag rendered and before `_site/` exists. They are
|
|
49
|
+
never `sdk_hints.meta_tags.missing` and never appear in the pre-build
|
|
50
|
+
"CampaignSpec expects SDK meta tags (...)" list. The fix is an edit to the
|
|
51
|
+
Map's page hints, not to the build.
|
|
52
|
+
|
|
53
|
+
Page-kit also needs `campaign.store_url` for `_data/campaigns.json`. Additional Store Profile fields live under `campaign.store_*` as optional storefront/legal metadata because they are not Campaigns API data: the operator enters them, or `spec derive --from-store` derives them from the store (see "Deriving the spec from the repo" below).
|
|
54
|
+
|
|
55
|
+
### Page Kit Store Profile checkpoint
|
|
56
|
+
|
|
57
|
+
Doctor compares the packet-local CampaignSpec to exactly nine governed fields
|
|
58
|
+
in `_data/campaigns.json[public_route_slug]`, in this order:
|
|
59
|
+
`store_name`, `store_url`, `store_terms`, `store_privacy`, `store_contact`,
|
|
60
|
+
`store_returns`, `store_shipping`, `store_phone`, and `store_phone_tel`.
|
|
61
|
+
Before scaffold, a missing target entry is `not_applicable`; once setup or
|
|
62
|
+
assembly is terminal, or the target output already exists, missing or malformed
|
|
63
|
+
target evidence is a non-waivable blocker. Target-only values remain warnings.
|
|
64
|
+
Mismatches, missing required target values, and known demo residue block.
|
|
65
|
+
Demo residue (a `demo.29next.com` URL or the demo phone number still in the
|
|
66
|
+
target) is never waivable: the gate names the residue fields, offers no waive
|
|
67
|
+
command for them, and `checkpoint waive` refuses with those fields until the
|
|
68
|
+
values are replaced.
|
|
69
|
+
|
|
70
|
+
The repair is a command. A fresh page-kit scaffold (`campaign-init`) seeds the
|
|
71
|
+
route's entry with the starter family's demo profile and pin, so this gate and
|
|
72
|
+
the SDK-version gate below block on every first run; the values that clear
|
|
73
|
+
them already exist in the CampaignSpec, and doctor and `next` print the
|
|
74
|
+
reconcile as the gate's `repair_target` action:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
campaigns-os page-kit sync --packet campaign-runtime.build.json [--dry-run] [--json]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The CampaignSpec is the authority for the Store Profile: those values are
|
|
81
|
+
authored in the Map, never in the repo, so `page-kit sync` writes the nine
|
|
82
|
+
fields the spec carries (`campaign.store_*`) unconditionally. The SDK pin is
|
|
83
|
+
different. On an existing campaign the repo pin moves first and the Map/spec
|
|
84
|
+
is stale until someone re-saves it, so a spec → repo write would undo a bump
|
|
85
|
+
silently; sync therefore **seeds** `sdk_version` (`global_config.sdk_version`,
|
|
86
|
+
or the `runtime.sdk_version` alias when the canonical key is absent): it
|
|
87
|
+
writes the pin while the entry is still in scaffold state (the starter demo
|
|
88
|
+
store profile is still in it) or when the target pin is older than the
|
|
89
|
+
spec's, and refuses to move a configured campaign's pin backwards
|
|
90
|
+
(`not_synced`, reason `target_newer`, naming both versions and pointing at
|
|
91
|
+
re-saving the Map). The repo pin is the authority for what ships and the Map
|
|
92
|
+
field is a build hint, so that state is a doctor warning, not a blocker (see
|
|
93
|
+
the SDK version checkpoint below). Both go into `_data/campaigns.json[public_route_slug]`,
|
|
94
|
+
prints a field-by-field before/after diff, and touches nothing else: a
|
|
95
|
+
governed field the spec does not carry is left as it is (doctor's
|
|
96
|
+
`target_only` warning still applies), non-governed keys keep their values and
|
|
97
|
+
order, other routes and other files are not written. The file is edited in
|
|
98
|
+
place and re-serialized with its own top-level indentation, line ending and
|
|
99
|
+
trailing newline; when that round trip would not have reproduced the file
|
|
100
|
+
byte for byte (a minified file, mixed indentation), a
|
|
101
|
+
`page_kit.sync.file_reformatted` warning says so, because the printed diff
|
|
102
|
+
covers only the governed fields. `--dry-run` prints the same diff without
|
|
103
|
+
writing. Exit 0 on success (including a no-op re-run); exit 2 with
|
|
104
|
+
`page_kit.sync.*` error codes and nothing written when the packet cannot be
|
|
105
|
+
read, the target entry or the spec is missing or not an object, the spec
|
|
106
|
+
identifies another campaign (`spec_identity.public_route_slug` or `map_id`
|
|
107
|
+
disagreeing with the packet: `page_kit.sync.spec_identity_mismatch`), or the
|
|
108
|
+
resolved `_data/campaigns.json` lies outside the target repo through a symlink
|
|
109
|
+
(`page_kit.sync.target_escapes_repo`).
|
|
110
|
+
|
|
111
|
+
The spec is the authority, but the target is made authoritative only from a
|
|
112
|
+
usable spec value. States the target cannot be made authoritative for are
|
|
113
|
+
reported as `not_synced` with a reason, never written, and the run's status
|
|
114
|
+
is `partial` (exit 0, since the writes that could happen did; doctor will
|
|
115
|
+
still block): a conflicting or non-released spec pin; a spec field of the
|
|
116
|
+
wrong type (`spec_invalid_type`, doctor's own blocker); a URL field that is
|
|
117
|
+
not an http(s) URL or a `store_phone_tel` that is not a `tel:` URI of digits,
|
|
118
|
+
spaces, dashes, parens and dots (templates put both into `href` attributes,
|
|
119
|
+
where escaping does not neutralize another scheme); a value with control
|
|
120
|
+
characters; the starter demo value itself in the spec; and starter demo
|
|
121
|
+
residue in a governed field the spec does not carry (doctor blocks on that
|
|
122
|
+
residue without a waiver, and sync has no spec value to write over it). For
|
|
123
|
+
every one of those the gate's `repair_target` action is an edit naming the
|
|
124
|
+
spec field, not the sync command, so `next` never loops on a repair that
|
|
125
|
+
cannot make progress. Fix the spec, then sync again. (`store_contact` may
|
|
126
|
+
also be a `mailto:` address, the one non-http value templates render as a
|
|
127
|
+
contact link.) A gate under an active named-human waiver is a human decision
|
|
128
|
+
sync does not reverse: its fields are reported `not_synced` with reason
|
|
129
|
+
`waived` naming who waived, and the target stays as the waiver accepted it
|
|
130
|
+
until the waiver is withdrawn from the Assembly Report. Sync reads the report
|
|
131
|
+
doctor would (the one the Build Context binds, or `--report <path>`, which
|
|
132
|
+
doctor appends to the printed command when it inspected a non-default
|
|
133
|
+
report); unknown flags are rejected rather than ignored, so a mistyped
|
|
134
|
+
`--dry-run` cannot become a write. After a write the retained doctor
|
|
135
|
+
snapshot is marked stale; when the report records a terminal build, a
|
|
136
|
+
`page_kit.sync.build_stale` warning says the rendered `_site/` was built
|
|
137
|
+
from the old entry and points at the rebuild, because doctor's page-kit gates
|
|
138
|
+
read `_data/campaigns.json`, not the built output. Re-run `doctor` and both
|
|
139
|
+
gates report `pass` without a waiver.
|
|
140
|
+
|
|
141
|
+
An intentional, evidence-backed mismatch or missing value may be accepted with
|
|
142
|
+
the first gate in the staged checkpoint registry:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
campaigns-os checkpoint waive \
|
|
146
|
+
--packet campaign-runtime.build.json \
|
|
147
|
+
--gate page_kit.store_profile \
|
|
148
|
+
--reason "<why correction is intentionally deferred>" \
|
|
149
|
+
--waived-by "<named human>" \
|
|
150
|
+
--review-condition "<specific re-evaluation trigger>"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Use `--expires-at <canonical-ISO-timestamp>` instead of, or alongside,
|
|
154
|
+
`--review-condition`; at least one bound is required and an expiry must be later
|
|
155
|
+
than `waived_at`. The decision is appended to the Assembly Report's top-level
|
|
156
|
+
`waivers[]` and fingerprints the exact slug, relative target path, and governed
|
|
157
|
+
discrepant field/value set. Stale, foreign, expired, or malformed records are
|
|
158
|
+
inert. A current waiver produces checkpoint status `waived` and doctor/next
|
|
159
|
+
readiness `ready_with_waivers`; it never becomes a clean pass. Raw arbitrary
|
|
160
|
+
campaign entry fields are validation-private and never belong in doctor, next,
|
|
161
|
+
sidecar, or QA evidence. The same boundary applies to waiver history: public
|
|
162
|
+
gate/readback/QA output whitelists the active decision's scope, current safe
|
|
163
|
+
subject, fingerprint, attribution, timestamps, and bound, while inert history
|
|
164
|
+
is exposed only as stale/foreign/malformed/expired counts. Raw report records
|
|
165
|
+
remain private to evaluation. Once correction removes all blocker fields,
|
|
166
|
+
waiver history is not evaluated or surfaced as an inert warning.
|
|
167
|
+
|
|
168
|
+
### Page Kit SDK version checkpoint
|
|
169
|
+
|
|
170
|
+
The second registered checkpoint requires a canonical released semantic version
|
|
171
|
+
in CampaignSpec and a released target pin in
|
|
172
|
+
`_data/campaigns.json[public_route_slug].sdk_version`. CampaignSpec's
|
|
173
|
+
`global_config.sdk_version` is canonical, with `runtime.sdk_version` as an
|
|
174
|
+
accepted alias; declaring both is valid only when their released
|
|
175
|
+
versions are equal. Missing, malformed, present-but-empty, non-string,
|
|
176
|
+
prerelease, non-canonical, or conflicting dual declarations are non-waivable.
|
|
177
|
+
Missing or invalid target evidence is also non-waivable.
|
|
178
|
+
|
|
179
|
+
The two pins have a direction of authority. The repo pin is the version the
|
|
180
|
+
funnel serves, so it is the only value a bump can be proven against; the spec
|
|
181
|
+
field is a build hint whose job is to seed a fresh scaffold. The gate compares
|
|
182
|
+
them accordingly:
|
|
183
|
+
|
|
184
|
+
- **Equal** — pass.
|
|
185
|
+
- **Target newer than the spec, both released, entry configured** (the
|
|
186
|
+
starter demo store profile is gone) — a completed bump the Map has not been
|
|
187
|
+
re-saved for. Doctor passes the gate with the `page_kit.sdk_version.repo_newer`
|
|
188
|
+
warning and a ready line naming what ships; QA projects it as a `warn`
|
|
189
|
+
assertion; `next` does not stop. The gate's `advisory_actions` carry one
|
|
190
|
+
`refresh_spec` command, `campaigns-os spec derive --packet <packet>`
|
|
191
|
+
(below), which writes the repo pin into the spec; with `--write-map` it
|
|
192
|
+
also records the pin in the Map's Build hints field (Campaign Cart SDK
|
|
193
|
+
version), which is otherwise re-saved by hand to make the exported spec
|
|
194
|
+
stop reading stale. Nothing in the repo needs to change, and there is
|
|
195
|
+
nothing to waive.
|
|
196
|
+
- **Target behind the spec, or still the scaffold's seeded pin beside the demo
|
|
197
|
+
store profile** — blocked, repaired by `page-kit sync` (below) or waived.
|
|
198
|
+
- **Target pin not a released version** — blocked, non-waivable, whatever the
|
|
199
|
+
spec says.
|
|
200
|
+
|
|
201
|
+
Only the blocked mismatch between two valid released versions has a waiver
|
|
202
|
+
lane, and the decision fingerprints that exact expected/observed pair:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
campaigns-os checkpoint waive \
|
|
206
|
+
--packet campaign-runtime.build.json \
|
|
207
|
+
--gate page_kit.sdk_version \
|
|
208
|
+
--reason "<why this exact target pin is intentional>" \
|
|
209
|
+
--waived-by "<named human>" \
|
|
210
|
+
--review-condition "<specific re-evaluation trigger>"
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Changing either version makes the decision stale. The same named-human,
|
|
214
|
+
bounded-decision, visibility, and privacy rules described for Store Profile
|
|
215
|
+
apply. A target pin that is missing, malformed, or behind a valid spec pin
|
|
216
|
+
(or still the scaffold's seeded pin beside the demo store profile) is repaired
|
|
217
|
+
by the same `campaigns-os page-kit sync --packet <campaign-runtime.build.json>`
|
|
218
|
+
described above, which doctor and `next` print as the gate's `repair_target`
|
|
219
|
+
action. A configured campaign whose pin is newer than the spec's is the
|
|
220
|
+
advisory case above: no required action, and sync never moves that pin
|
|
221
|
+
backwards; `spec derive` is the command that closes it from the spec side.
|
|
222
|
+
|
|
223
|
+
### Deriving the spec from the repo (`spec derive`)
|
|
224
|
+
|
|
225
|
+
Every CampaignSpec field has a class: **authored** (a human writes it: offers,
|
|
226
|
+
funnel shape, copy intent), **mirrored** (pulled from the Campaigns API or the
|
|
227
|
+
store: packages, prices, shipping) or **derived** (the repo or the store
|
|
228
|
+
already states it: the SDK pin, page routes, the store profile, analytics
|
|
229
|
+
ids). The table is on #432. Derived fields are generated, never typed, and
|
|
230
|
+
this command generates the repo-derived ones so doctor compares generated
|
|
231
|
+
against generated instead of refereeing a hand-typed value against the repo:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
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>]
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
By default it reads the target repo only (no network unless `--from-store` or
|
|
238
|
+
`--write-map`, below) and writes into the packet's local spec (`spec.local_path`):
|
|
239
|
+
|
|
240
|
+
| Spec field | Repo authority |
|
|
241
|
+
|---|---|
|
|
242
|
+
| `global_config.sdk_version`, and `runtime.sdk_version` when the spec declares the alias (so the two never conflict) | `_data/campaigns.json[public_route_slug].sdk_version` |
|
|
243
|
+
| `funnels[].pages[].page_url` (the legacy `funnel_pages[].page_url` mirror, when present, is reconciled to the same route on every run) | the page tree under `assembly.output_dir` (default `src/<public_route_slug>/`), read by page-kit's own rule: the file's basename alone (`checkout.html` → `checkout/` wherever it sits; `index.html` → the entry route; a nested `index.html` collides with the root and is not read), or a frontmatter `permalink` in the `/<slug>/<route>/` form prepare-build writes (page-kit serves a permalink verbatim, so any other spelling is a repo defect, refused) |
|
|
244
|
+
| `analytics.providers.gtm.containerId` | `_data/campaigns.json[public_route_slug].gtm_id` |
|
|
245
|
+
| `analytics.providers.facebook.pixelId` | `_data/campaigns.json[public_route_slug].fb_pixel_id` |
|
|
246
|
+
|
|
247
|
+
Nothing else is written: not the store profile (store-derived; see
|
|
248
|
+
`--from-store` below), not any authored or mirrored field, not the packet,
|
|
249
|
+
not the repo. A
|
|
250
|
+
derived field the spec carries with a different, authored-looking value is
|
|
251
|
+
overwritten, and the printed `before -> after` line shows it: that is the
|
|
252
|
+
class doing its job. A block the spec lacks is created (`global_config`,
|
|
253
|
+
`analytics.providers.gtm` as `{ "enabled": true, "containerId": … }`); an
|
|
254
|
+
array element is never invented.
|
|
255
|
+
|
|
256
|
+
Each page is bound to one file: the packet's own projection first
|
|
257
|
+
(`source_html.pages[].page_kit.target_path`, the file the build stage wrote
|
|
258
|
+
for that page id), else a file whose route equals the page's current route,
|
|
259
|
+
whose terminal segment equals it, or whose filename is the page id. The
|
|
260
|
+
derived route is compared the way prepare-build projects a route
|
|
261
|
+
(normalized, slug prefix stripped), so a spelling that already resolves to
|
|
262
|
+
the tree's route (`checkout`, `/<slug>/checkout/`) is not a change, and a
|
|
263
|
+
value nested differently from the tree (`offers/upsell/` against
|
|
264
|
+
`upsell.html`) is. A routing hint in `sdk_hints.meta_tags` (`next-success-url`,
|
|
265
|
+
`next-upsell-accept-url`, `next-upsell-decline-url`) that no longer matches
|
|
266
|
+
the derived route of the page it names is reported as
|
|
267
|
+
`spec.derive.routing_hint_stale` on every run until the Map is re-saved;
|
|
268
|
+
hints are a Map projection the editor regenerates and are not rewritten.
|
|
269
|
+
|
|
270
|
+
What the repo cannot state is reported as `not_derived[]` with a reason and
|
|
271
|
+
the status is `partial` (exit 0; the fields it could derive are written):
|
|
272
|
+
|
|
273
|
+
| Reason | Meaning |
|
|
274
|
+
|---|---|
|
|
275
|
+
| `scaffold_seed` | the entry still carries the starter demo store profile, so its pin is the starter's seed, not a version anyone chose; `page-kit sync` seeds the pin from the spec in that state |
|
|
276
|
+
| `target_missing`, `target_invalid` | the entry has no `sdk_version`, or it is not a released `MAJOR.MINOR.PATCH`; for an analytics id, the value is not a GTM container id / a digits-only pixel id; for a route, the file's permalink is not in the `/<slug>/<route>/` form (no slug prefix, another prefix, `.html`, `..`, a control character), reported with the URL page-kit would serve |
|
|
277
|
+
| `waived` | an active named-human `page_kit.sdk_version` waiver covers the exact pair; derive leaves the spec as the waiver accepted it |
|
|
278
|
+
| `spec_ahead` | a released pin the spec declares (canonical or alias) is ahead of the repo pin: the state doctor blocks on with `page-kit sync` as its repair (#413); one command owns it, so derive never moves a spec pin backwards |
|
|
279
|
+
| `page_tree_missing`, `page_file_not_found`, `page_file_ambiguous` | no page tree, no file binds to the page, or more than one does |
|
|
280
|
+
| `entry_route_undeclared` | the page binds to the top-level `index.html` (the entry route, `""`) but is not flagged `is_entry`; doctor honours an empty `page_url` only on the entry page, so the flag is asked for in the Map rather than the route written |
|
|
281
|
+
| `waivers_unknown` | the Assembly Report could not be read, so a named-human `page_kit.sdk_version` waiver cannot be ruled out; the pin waits, routes and ids still derive |
|
|
282
|
+
| `page_id_duplicate` | the page id appears more than once; no single route can be derived for it |
|
|
283
|
+
| `spec_container_invalid` | `global_config`, `runtime`, `analytics`, `analytics.providers` or `analytics.providers.<provider>` exists in the spec but is not an object; reported by the plan so `--dry-run` and the write agree |
|
|
284
|
+
| `target_empty` | the entry's `gtm_id` / `fb_pixel_id` is empty while the spec declares an id; an empty repo value never deletes a spec id |
|
|
285
|
+
|
|
286
|
+
A placeholder id (`GTM-XXXXXXX`, a run of one digit) is `target_invalid`:
|
|
287
|
+
writing it would declare an analytics contract QA then blocks on. A derived
|
|
288
|
+
field the entry does not carry at all is listed under `not_in_target[]` and
|
|
289
|
+
left as it is.
|
|
290
|
+
|
|
291
|
+
After a write, the sidecars that carry the spec's identity are re-bound. The
|
|
292
|
+
Build Context's `spec.hash` / `spec.material_hash` and the Assembly Report's
|
|
293
|
+
`identity.spec_hash` / `identity.spec_material_hash` move to the new spec
|
|
294
|
+
when they were bound to the one derive replaced and name that spec file
|
|
295
|
+
(`rebound` on the result; two packets sharing a target repo never re-bind
|
|
296
|
+
each other's sidecars);
|
|
297
|
+
QA's verdict and `bundle check` correlate against the material hash, so this
|
|
298
|
+
is what keeps a derive-then-QA run conformant. A sidecar already carrying
|
|
299
|
+
another identity is left alone with a `spec.derive.identity_not_rebound`
|
|
300
|
+
warning naming `prepare-build`. A derived route change also warns
|
|
301
|
+
`spec.derive.projection_stale`: the packet's page-kit projection
|
|
302
|
+
(`source_html.pages[].page_kit`) and the Build Context page map were prepared
|
|
303
|
+
from the old routes, and `prepare-build` (or `start`) regenerates them. A
|
|
304
|
+
provider block derive creates warns `spec.derive.analytics_block_created`:
|
|
305
|
+
the spec then declares an analytics contract QA gates on. `--dry-run` carries
|
|
306
|
+
the same `file_reformatted`, `projection_stale` and `build_stale` warnings,
|
|
307
|
+
phrased as what a write would do.
|
|
308
|
+
|
|
309
|
+
Write discipline is `page-kit sync`'s: one read serves the plan and the
|
|
310
|
+
write; the file is edited in place and re-serialized with its own top-level
|
|
311
|
+
indentation, line ending and trailing newline (`spec.derive.file_reformatted`
|
|
312
|
+
when that round trip would not reproduce the file byte for byte); staged
|
|
313
|
+
through a temp file created with the spec's own mode bits and renamed over it,
|
|
314
|
+
after re-reading the spec and refusing (`spec.derive.spec_changed_underneath`,
|
|
315
|
+
exit 2) when it changed since the single read; written only at the path
|
|
316
|
+
the spec resolves to, which must lie inside the spec's own directory or the
|
|
317
|
+
target repo (`spec.derive.spec_escapes_boundary` otherwise, so a symlinked
|
|
318
|
+
`spec.local_path` cannot redirect the write); the retained doctor sidecar is
|
|
319
|
+
marked stale after a write, and a terminal build gets a
|
|
320
|
+
`spec.derive.build_stale` warning naming the rebuild (doctor does not
|
|
321
|
+
fingerprint the spec itself; the re-bound sidecar identity is what QA reads). `--dry-run` prints the
|
|
322
|
+
same diff and writes nothing; unknown flags and a valued `--dry-run` are
|
|
323
|
+
rejected. Exit 2 with `spec.derive.*` error codes and nothing written when the
|
|
324
|
+
packet cannot be read, `spec.local_path` is absent or not a file, the spec is
|
|
325
|
+
not a JSON object, the spec identifies another campaign
|
|
326
|
+
(`spec.derive.spec_identity_mismatch`), or the target entry is missing
|
|
327
|
+
(`spec.derive.entry_missing`; scaffold first).
|
|
328
|
+
|
|
329
|
+
#### Deriving the store profile from the store (`--from-store`)
|
|
330
|
+
|
|
331
|
+
The nine `campaign.store_*` Store Profile fields are derived too, and their
|
|
332
|
+
authority is the store: `page-kit sync` writes them spec → repo, and this is
|
|
333
|
+
the generator for the store → spec half. It needs a credential and the
|
|
334
|
+
network, which the default run never touches, so it is opt-in:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
campaigns-os spec derive --packet campaign-runtime.build.json --from-store <subdomain> [--store-token-source env:<VAR>] [--dry-run] [--json]
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`<subdomain>` is the store's `<store>.29next.store` subdomain (the Admin API
|
|
341
|
+
lives at `https://<subdomain>.29next.store/api/admin/`). The read token is
|
|
342
|
+
taken from the environment, never from the command line: from
|
|
343
|
+
`<SUBDOMAIN>_ADMIN_TOKEN` (upper-cased, dashes as underscores) by default,
|
|
344
|
+
or from the variable `--store-token-source env:<VAR>` names. An Admin API
|
|
345
|
+
access token with the `store:read` and `content:read` scopes (Settings >
|
|
346
|
+
API Access) is enough; the token is sent as a bearer and appears nowhere in
|
|
347
|
+
the output, which names the variable instead (a value that is not one line
|
|
348
|
+
of printable ASCII is refused unsent, `spec.derive.store_credential_invalid`,
|
|
349
|
+
and a transport error that quotes a header is redacted). The store is only
|
|
350
|
+
read.
|
|
351
|
+
|
|
352
|
+
| Spec field | Store authority |
|
|
353
|
+
|---|---|
|
|
354
|
+
| `campaign.store_name` | `GET /store/` `name` (Admin API version `2024-04-01`) |
|
|
355
|
+
| `campaign.store_url` | `GET /store/` `primary_domain`, as `https://<primary_domain>` |
|
|
356
|
+
| `campaign.store_phone` | `GET /store/` `contact_address.phone_number`, verbatim |
|
|
357
|
+
| `campaign.store_phone_tel` | the same phone as a `tel:` URI (digits, a leading `+` kept), only when the display phone is one plain number: an extension, a second number or a vanity word would fold into the digits and dial something else, so those leave the field not derived (`target_invalid`) |
|
|
358
|
+
| `campaign.store_terms`, `store_privacy`, `store_contact`, `store_returns`, `store_shipping` | `GET /pages/` (Admin API version `unstable`, followed cursor by cursor under the store's own pages endpoint): the one storefront page that carries the policy, as `https://<primary_domain>/<slug>/`, which is where the storefront serves it. A page whose slug is one of the policy's conventional slugs (`terms`, `terms-of-service`, `privacy-policy`, `contact-us`, `return-policy`, `shipping-policy`, `shipping-returns`, …) binds first; only when no page has a conventional slug does the wider match by slug or title words apply (terms/tos/conditions; privacy; contact; return(s)/refund(s); shipping/delivery), so a "free shipping" promo page never outranks the policy. One page may carry two policies (`shipping-returns`) |
|
|
359
|
+
|
|
360
|
+
Rows join the same `before -> after` diff in the Store Profile's field
|
|
361
|
+
order, each with its store source, and are compared NFC-normalized and
|
|
362
|
+
trimmed as `page-kit sync` compares them, plus one leniency of derive's own:
|
|
363
|
+
URL fields compare without a trailing slash, so a spec that carries
|
|
364
|
+
`https://x.example/` is left alone when the store says `https://x.example`
|
|
365
|
+
rather than churned (sync then writes the spec's spelling into the repo as
|
|
366
|
+
it is). Every value
|
|
367
|
+
passes the Store Profile shape rule before it is planned: the starter demo
|
|
368
|
+
store's URL or phone, a non-http(s) URL, a malformed `tel:` or a control
|
|
369
|
+
character is `target_invalid`, never written. A field the store cannot state
|
|
370
|
+
is `not_derived[]` and **the spec's value is left as it is** (a store never
|
|
371
|
+
empties a spec field): `store_field_missing` (an empty name, domain or
|
|
372
|
+
phone), `store_domain_missing` (no primary domain, so no page URL can be
|
|
373
|
+
formed), `store_page_not_found`, `store_page_ambiguous` (several pages read
|
|
374
|
+
as the policy; the slugs are named), `store_pages_unavailable` (the pages
|
|
375
|
+
endpoint failed, with the reason: a token without `content:read`, a version
|
|
376
|
+
that does not serve `/pages/`, a body that is not the page list, a cursor
|
|
377
|
+
outside the store's pages endpoint that was not followed) and
|
|
378
|
+
`store_pages_truncated` (more pages than ten requests or two thousand rows
|
|
379
|
+
return). A slug that is not one honest path segment (a separator, `.` or
|
|
380
|
+
`..`, malformed text) is `target_invalid`. When the store's primary domain is not the host the spec's
|
|
381
|
+
`store_url` named, `spec.derive.store_domain_changed` says so: either the
|
|
382
|
+
spec was stale and the diff is the correction, or `--from-store` names
|
|
383
|
+
another merchant's store and the spec should be restored.
|
|
384
|
+
|
|
385
|
+
The result carries a `store` block (`subdomain`, `admin_api`,
|
|
386
|
+
`token_source`, `store_read`, `pages_read`, `primary_domain`), and the text
|
|
387
|
+
output a `Store:` line. After a write that moved a store field, `next` is
|
|
388
|
+
`page-kit sync` first (doctor's `page_kit.store_profile` gate now sees the
|
|
389
|
+
spec ahead of the repo and names sync as its repair), then doctor. A store
|
|
390
|
+
that cannot be read is a refusal with nothing written, repo fields included,
|
|
391
|
+
exit 2: `spec.derive.store_credential_missing` (the variable is unset or
|
|
392
|
+
empty), `store_unauthorized` (401/403), `store_not_found` (404: no store at
|
|
393
|
+
that subdomain), `store_unreachable` (transport, timeout, 5xx) or
|
|
394
|
+
`store_response_invalid`. Local preconditions (packet, spec, target entry, spec boundary, page tree)
|
|
395
|
+
are checked before the store is contacted, and a packet that names another
|
|
396
|
+
spec or route by the time the read returns is refused
|
|
397
|
+
(`spec.derive.packet_changed_underneath`). `--store-token-source` without
|
|
398
|
+
`--from-store`, a subdomain that is not one (a URL, a path), or a token
|
|
399
|
+
source that is not `env:<VAR>` is rejected before anything is read.
|
|
400
|
+
|
|
401
|
+
#### Recording the pin in the Map (`--write-map`)
|
|
402
|
+
|
|
403
|
+
The local derive fixes the exported spec; the Map itself still shows the old
|
|
404
|
+
pin until someone re-saves Build hints, so anyone opening the Map or a fresh
|
|
405
|
+
export reads stale. `--write-map` closes that half (#415): after the local
|
|
406
|
+
write, the pin the plan derived is recorded into the saved Map's Build hints
|
|
407
|
+
field (Campaign Cart SDK version) through the proxy Worker, with the same
|
|
408
|
+
direction of authority as everything else on this gate: the write goes
|
|
409
|
+
forward or not at all.
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
campaigns-os spec derive --packet campaign-runtime.build.json --write-map [--dry-run] [--proxy-base <url>]
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
The Map named by the packet's `spec.map_id` is read back (`GET
|
|
416
|
+
/api/spec/<map-id>`) and re-stated with exactly the pin fields moved
|
|
417
|
+
(`global_config.sdk_version`, and `runtime.sdk_version` only when the Map
|
|
418
|
+
already declares the alias): every other field is the Map's own read-back,
|
|
419
|
+
never the local spec, so an authored field is not rewritten from a local copy
|
|
420
|
+
and the routes or analytics ids derive wrote locally do not travel. The `PUT
|
|
421
|
+
/api/maps/<map-id>` carries the packet's Campaigns API key as
|
|
422
|
+
`X-Campaign-Key` (the receiver refuses a key that is not the Map's; the key
|
|
423
|
+
is the public-by-design one the packet, its local spec or the declared env
|
|
424
|
+
source already holds) and the Map's `spec_hash` as `X-Spec-Hash`, so a save
|
|
425
|
+
that landed in between is a conflict, not an overwrite. The proxy base is the
|
|
426
|
+
canonical `https://campaign-map.nextcommerce.com` unless `--proxy-base` names
|
|
427
|
+
another; it must be https, or a loopback host over http (allowed for a local
|
|
428
|
+
receiver, with a stderr warning that the key travels in clear).
|
|
429
|
+
|
|
430
|
+
The decision, reported on the result's `map` object and as one text line:
|
|
431
|
+
|
|
432
|
+
| `map.status` | Meaning |
|
|
433
|
+
|---|---|
|
|
434
|
+
| `written` | the Map declared no pin, or one behind the repo pin; it now records the repo pin (`map.spec_identity.before` / `.after` carry the Map's `spec_hash` and `saved_at` either side) |
|
|
435
|
+
| `unchanged` | the Map already records the repo pin; nothing sent |
|
|
436
|
+
| `would_write` | `--dry-run`: the Map was read and the write previewed; nothing sent |
|
|
437
|
+
| `refused` | a warning, exit 0, the local derive stands: `ahead` (the Map pin is newer than the repo pin — a bump the repo never received, doctor's blocked state and `page-kit sync`'s repair; the Map is never moved backwards) or `pin_unreadable` (the Map's pin is not a released version, or two declarations disagree; a value the rule cannot order is not overwritten silently) |
|
|
438
|
+
| `skipped` | the pin was not derived (`pin_<reason>`, the `not_derived` reason: a scaffold's seed, a waiver, `spec_ahead`, …) or the local derive was blocked; nothing was read or sent |
|
|
439
|
+
| `failed` | an error, exit 2, the local derive stands: `key_missing` (no Campaigns API key anywhere), `key_mismatch` (403), `not_found` (404), `changed_underneath` (409: derive again against the current save), `rejected` (the proxy's spec validation refused the re-stated Map: re-save it in the builder first), `proxy_base_insecure`, `network_error`, `http_error`, `response_invalid` (an answer that says neither yes nor no: read the Map back before deriving again) |
|
|
440
|
+
|
|
441
|
+
A write is traceable from the campaign's own record: one line is appended to
|
|
442
|
+
the Assembly Report's `evidence[]` (`Map write-back: global_config.sdk_version
|
|
443
|
+
<before> -> <after> on Map <id> at <time> via spec derive --write-map (Map
|
|
444
|
+
spec_hash <before> -> <after>)`), the retained doctor sidecar is marked stale
|
|
445
|
+
by `spec derive --write-map`, and the run's lifecycle journal carries the
|
|
446
|
+
command with its argv shape, so the Run Record (which references the report by
|
|
447
|
+
hash) shows both that the write ran and what it changed. A report that does
|
|
448
|
+
not exist yet (a derive before `prepare-build`) leaves a
|
|
449
|
+
`spec.derive.map_not_recorded` warning carrying the same line; a report that
|
|
450
|
+
took the line while the doctor stamp failed leaves
|
|
451
|
+
`spec.derive.map_doctor_sidecar_not_marked` instead, and one that could not be
|
|
452
|
+
read back after the failure leaves `spec.derive.map_recorded_status_unknown`
|
|
453
|
+
(`map.recorded: "unknown"`) rather than a claim either way. A 403 on the read
|
|
454
|
+
is `key_mismatch`, as on the write. Without
|
|
455
|
+
`--write-map` nothing is read from or sent to the Map; `--proxy-base` is
|
|
456
|
+
refused on its own.
|
|
457
|
+
|
|
458
|
+
### Polish hidden eager-media checkpoint
|
|
459
|
+
|
|
460
|
+
The third registered checkpoint is package-owned page-load evidence recorded at
|
|
461
|
+
`stages.polish.evidence.visual_review.page_load`. Install the package-owned
|
|
462
|
+
browser, serve the current build, and run this producer before marking Polish
|
|
463
|
+
complete, deploying, or starting QA:
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
npm run qa:install-browser
|
|
467
|
+
campaigns-os polish capture \
|
|
468
|
+
--packet campaign-runtime.build.json \
|
|
469
|
+
--base-url <served-current-build-url>
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
The producer derives every mapped, non-skipped route from the packet and
|
|
473
|
+
captures desktop `1440x1200` and mobile `390x844`. It blocks when a
|
|
474
|
+
computed-hidden `video` or `audio` element transfers strictly more than
|
|
475
|
+
`1,048,576` bytes, unless its content attribute is exactly
|
|
476
|
+
ASCII-case-insensitive `none` or `metadata`. Evidence exactly at the byte
|
|
477
|
+
threshold passes. The command owns the versioned capture format and its
|
|
478
|
+
integrity binding; never hand-author or copy `page_load`.
|
|
479
|
+
|
|
480
|
+
The operator supplies the URL of the current served output. The evidence binds
|
|
481
|
+
to packet/report authority, but the URL itself is not cryptographic proof that
|
|
482
|
+
the server is hosting those exact build bytes. The durable field map, bounded
|
|
483
|
+
measurement semantics, and attachment race boundary are documented in
|
|
484
|
+
[Polish evidence](./polish-evidence.md#durable-page_load-field-map).
|
|
485
|
+
|
|
486
|
+
Missing, malformed, stale, incomplete, or contradictory measurement evidence
|
|
487
|
+
is nonwaivable. A complete real finding may receive an exact named-human
|
|
488
|
+
decision:
|
|
489
|
+
|
|
490
|
+
```bash
|
|
491
|
+
campaigns-os checkpoint waive \
|
|
492
|
+
--packet campaign-runtime.build.json \
|
|
493
|
+
--gate polish.hidden_eager_media \
|
|
494
|
+
--reason "<why this exact finding is accepted>" \
|
|
495
|
+
--waived-by "<named human>" \
|
|
496
|
+
--review-condition "<specific re-evaluation trigger>"
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
The decision binds the build fingerprint, campaign slug, route scope, routes,
|
|
500
|
+
fixed viewports, and stable finding state. Any change makes it inert. Doctor and
|
|
501
|
+
`next` report `ready_with_waivers`; QA reports `ready_with_exceptions` and keeps
|
|
502
|
+
the warning visible.
|
|
503
|
+
|
|
504
|
+
QA evaluates all three registered gates from one packet/spec/target/report
|
|
505
|
+
snapshot; a waiver for one never hides a blocker in another. See
|
|
506
|
+
[QA checkpoint preflight](./qa-and-test-orders.md#packet-local-checkpoint-preflight)
|
|
507
|
+
for the downstream runtime boundary.
|
|
508
|
+
|
|
509
|
+
Every gate that still owes work carries `required_actions[]` — the exact repair
|
|
510
|
+
command or manual step, plus the waiver command. `campaigns-os doctor` prints
|
|
511
|
+
the same actions in its text report, under a `Required actions:` block below the
|
|
512
|
+
errors and warnings, so an operator reading stdout gets the remediation without
|
|
513
|
+
re-running with `--json`.
|
|
514
|
+
|
|
515
|
+
### Built-output campaign identity gate (`built_output.campaign_identity`)
|
|
516
|
+
|
|
517
|
+
Every doctor run that sees built output (the packet path and `doctor --built`
|
|
518
|
+
alike) checks that the pages agree about which campaign they belong to. The
|
|
519
|
+
SDK reads three identity signals per page and reconciles nothing across
|
|
520
|
+
pages: the API key (`<meta name="next-api-key">` beats `window.nextConfig.apiKey`,
|
|
521
|
+
whether inline or in the `config.js` the page loads), the `next-funnel` meta,
|
|
522
|
+
and any `setAttribution({ funnel })` call. A page copied from another funnel
|
|
523
|
+
that still carries the other campaign's key, tag, or attribution call binds,
|
|
524
|
+
builds, and renders without complaint, and creates or attributes the order
|
|
525
|
+
against the wrong campaign. It has shipped twice.
|
|
526
|
+
|
|
527
|
+
The gate blocks (not waivable — two identities on one funnel cannot both be
|
|
528
|
+
intended) when:
|
|
529
|
+
|
|
530
|
+
- any two observed API keys differ, from any source on any page, including a
|
|
531
|
+
page whose meta names one key while its `config.js` names another
|
|
532
|
+
(`built_output.campaign_identity.api_key_drift`);
|
|
533
|
+
- `next-funnel` differs across pages (`…funnel_drift`), or, once any page
|
|
534
|
+
carries the tag, a page that declares `next-page-type` has no `next-funnel`
|
|
535
|
+
(`…funnel_missing`; a campaign that tags no page at all is consistent, and
|
|
536
|
+
the platform fills the campaign name when the tag is absent);
|
|
537
|
+
- a `setAttribution({ funnel })` string disagrees with the `next-funnel` of the
|
|
538
|
+
page that calls it, or with the campaign's tag when that page has none
|
|
539
|
+
(`…attribution_drift`).
|
|
540
|
+
|
|
541
|
+
One error per finding; each names the two files and the two values, so the
|
|
542
|
+
repair is a one-line edit. Pages whose route contains a `-backup-` or `-old-`
|
|
543
|
+
segment are parked copies: skipped and listed on the gate as `pages_skipped`,
|
|
544
|
+
never scanned. Presence is not asserted: a campaign whose pages carry no key
|
|
545
|
+
at all, or no `setAttribution` anywhere, passes on the funnel tag alone.
|
|
546
|
+
`checkpoint waive` does not register this gate; the repair is the only route.
|
|
547
|
+
|
|
548
|
+
The gate's evidence lands beside the other checkpoint gates at
|
|
549
|
+
`derived.checkpoint_gates[]` (`id: built_output.campaign_identity`, status
|
|
550
|
+
`pass` | `blocked` | `not_applicable`, `identity: { api_key, api_key_source,
|
|
551
|
+
funnel }`, `findings[]`, `pages_scanned`, `pages_skipped`). It is proven to
|
|
552
|
+
pass on the canonical rendered output of every certified starter family
|
|
553
|
+
(`fixtures/certified-families/`), the reachability bar every static
|
|
554
|
+
built-output gate now carries.
|
|
555
|
+
|
|
556
|
+
### Built-output SDK markup gate (`built_output.sdk_markup`)
|
|
557
|
+
|
|
558
|
+
Every doctor run that sees built output also runs the static SDK markup
|
|
559
|
+
family: six shapes of `data-next-*` markup that the Campaign Cart SDK binds
|
|
560
|
+
without complaint and that then either do nothing (a field that never reaches
|
|
561
|
+
the order, a button that never enables) or write the cart twice. They sit
|
|
562
|
+
beside `built_output.upsell_selector_scope`, which is the same kind of check
|
|
563
|
+
for one shape. The codes are the ones a partner Campaign Cart kit used, kept so
|
|
564
|
+
the two vocabularies line up; each doctor issue is `built_output.sdk_markup.`
|
|
565
|
+
plus the code lower-cased, and its message leads with the code.
|
|
566
|
+
|
|
567
|
+
Blockers (not waivable — the markup provably does not do what it says):
|
|
568
|
+
|
|
569
|
+
- `SWAP_WITH_ADD_TO_CART` — a bundle selector in swap mode (explicit
|
|
570
|
+
`data-next-selection-mode="swap"`, or the SDK default when the attribute is
|
|
571
|
+
absent) with an `add-to-cart` button linked to it by `data-next-selector-id`.
|
|
572
|
+
Both write the cart. An upsell-context selector is exempt: it is select mode
|
|
573
|
+
by construction.
|
|
574
|
+
- `CHECKOUT_NOT_FORM` — `data-next-checkout` on an element that is not `<form>`.
|
|
575
|
+
- `WRONG_FIELD_NAME` — `data-next-checkout-field` with a value the SDK does not
|
|
576
|
+
map. The set is vendored from the SDK at a named tag
|
|
577
|
+
(`src/sdk-attribute-index.mjs`, currently v0.4.38: `email`, `fname`, `lname`,
|
|
578
|
+
`phone`, `address1`, `address2`, `city`, `province`, `postal`, `country`,
|
|
579
|
+
`payment-method`, `accepts_marketing`, `cc-number`, `cc-month`, `cc-year`,
|
|
580
|
+
`exp-month`, `exp-year`, `cvv`, the legacy `card-*` spellings, and any
|
|
581
|
+
`billing-` prefixed name). The message names the SDK spelling for the usual
|
|
582
|
+
offenders (`firstName` → `fname`, `zip` → `postal`).
|
|
583
|
+
- `MISSING_SELECTOR_ID_MATCH` — an `add-to-cart` button whose
|
|
584
|
+
`data-next-selector-id` names no selector on the page (an element that is a
|
|
585
|
+
bundle, package, cart or upsell selector; another element echoing the id
|
|
586
|
+
does not count). One finding per dead id, however many buttons link to it.
|
|
587
|
+
|
|
588
|
+
Warnings (advisory):
|
|
589
|
+
|
|
590
|
+
- `DOUBLE_SELECTED` — more than one `data-next-selected="true"` card inside one
|
|
591
|
+
selector.
|
|
592
|
+
- `TEMPLATE_DOUBLE_BRACE` — `{{` inside an SDK-owned `<template>`: the direct
|
|
593
|
+
child of a container the SDK clones from (`data-next-cart-summary`,
|
|
594
|
+
`data-summary-lines`, `data-next-discounts`, `data-next-bundle-selector`,
|
|
595
|
+
`data-next-bundle-slots`, `data-next-package-selector`,
|
|
596
|
+
`data-next-package-toggle`), or one a `*-template-id` attribute points at.
|
|
597
|
+
SDK tokens are single-brace; a template nothing in the SDK reads, including
|
|
598
|
+
a vendor template nested deeper inside SDK chrome, may use any syntax.
|
|
599
|
+
|
|
600
|
+
Information: `data-next-*` names the vendored attribute index does not list are
|
|
601
|
+
collected on the gate (`unknown_attributes[]`) and printed as one advisory ready
|
|
602
|
+
line, never as a warning. That is where an invented attribute such as
|
|
603
|
+
`data-next-coupon-input` shows up; it is information rather than a warning
|
|
604
|
+
because the certified templates carry a handful of their own `data-next-*`
|
|
605
|
+
hooks the SDK never reads.
|
|
606
|
+
|
|
607
|
+
Markup inside SDK templates is scanned too, since the SDK clones it into the
|
|
608
|
+
live DOM. A gate reports one disposition: while blockers stand, advisories
|
|
609
|
+
stay on the gate's `warned[]` and become doctor warnings only once the
|
|
610
|
+
blockers clear; the gate `reason` names the first five findings and a count. The gate's evidence lands beside the other checkpoint gates at
|
|
611
|
+
`derived.checkpoint_gates[]` (`id: built_output.sdk_markup`, status `pass` |
|
|
612
|
+
`blocked` | `not_applicable`, `findings[]` for blockers, `warned[]` for
|
|
613
|
+
advisories, `unknown_attributes[]`, `pages_scanned`,
|
|
614
|
+
`sdk_attribute_index_version`). Fixtures: `fixtures/sdk-markup/<code>/{bad,good}`.
|
|
615
|
+
It passes, with no advisory, on the canonical rendered output of every
|
|
616
|
+
certified starter family (`fixtures/certified-families/`).
|
|
617
|
+
|
|
618
|
+
> **Where does the source HTML come from?** See [docs/entry-points.md](./entry-points.md) for the five recognized entry points (template-stock, Figma-driven, AI-generated, hand-authored, mixed) and how each populates `source_html.pages[]` + `design_source`.
|
|
619
|
+
|
|
620
|
+
## Artifact Locations
|
|
621
|
+
|
|
622
|
+
By default `campaigns-os start` writes into the target repo:
|
|
623
|
+
|
|
624
|
+
```text
|
|
625
|
+
campaign-runtime.build.json
|
|
626
|
+
.campaign-runtime/build-context.json
|
|
627
|
+
.campaign-runtime/assembly-report.json
|
|
628
|
+
.campaign-runtime/doctor-output.json
|
|
629
|
+
.campaign-runtime/theme/theme-report.json
|
|
630
|
+
.campaign-runtime/input/campaign-build-brief.normalized.json
|
|
631
|
+
.campaign-runtime/input/design-source-package.json
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
Those `.campaign-runtime/` paths are relative to the target repo
|
|
635
|
+
(`packet.assembly.target_repo`, resolved against the packet's directory) even
|
|
636
|
+
when `--out` keeps the packet somewhere else, and every stage — `doctor`
|
|
637
|
+
included — reads and writes them there. When `prepare-build --report-out`
|
|
638
|
+
puts the Assembly Report elsewhere, the Build Context records that path
|
|
639
|
+
(`report_path`, relative to the target repo) and `doctor`, `next`, `qa run`,
|
|
640
|
+
`qa waive` and the QA stage record follow it, so the report `next` reads is the
|
|
641
|
+
one QA writes into — provided the context's `packet_path` names that packet;
|
|
642
|
+
a context naming another packet binds nothing. `theme waive`, `checkpoint
|
|
643
|
+
waive`, `polish capture`,
|
|
644
|
+
`findings harvest`, `run-record` and `run status` act on the default location
|
|
645
|
+
unless `--report` names another. `run-record` keys its record on a `run_id`
|
|
646
|
+
resolved as `--run-id`, else the active run session, else the most recent Run
|
|
647
|
+
Record already on disk for this packet's campaign (re-emitted in place), else
|
|
648
|
+
a freshly minted id; `--new-run` mints on request and `--list` prints the ids
|
|
649
|
+
on disk without writing (see docs/workflow-findings-sidecar.md).
|
|
650
|
+
|
|
651
|
+
The packet's top-level `generated_at` (ISO-8601 UTC, `Z` suffix) is stamped by
|
|
652
|
+
`prepare-build` on every new packet. Downstream freshness — campaigns-agent's
|
|
653
|
+
readback staleness comparison and its multi-packet selection at the repository
|
|
654
|
+
root — reads this field, never file mtime. Packets generated before the field
|
|
655
|
+
existed remain schema-valid without it, but they cannot win freshness selection
|
|
656
|
+
and never satisfy a fresh-artifact readback on their own; regenerate rather
|
|
657
|
+
than hand-adding the field. Note that a committed artifact set always reads
|
|
658
|
+
stale to the readback once HEAD moves past it — freshness proof is a
|
|
659
|
+
regeneration at HEAD, not a property a commit can preserve.
|
|
660
|
+
|
|
661
|
+
Commit durable packet/context/report artifacts when they represent a real build handoff. The rest of `.campaign-runtime/` is machine-local and is not for the campaign repository: `run-session.json`, `command-lifecycle.jsonl`, `agent-deviations.jsonl`, `workflow-findings.jsonl`, `run-records/`, `fetched-specs/`, `polish-evidence/`, `evidence/`, and `*.log`/`*.tmp` are per-machine, append-only, or carry live URLs and absolute paths, and so are the full QA verdicts `qa run` writes under the target's `qa-output/`. `start`, `prepare-build`, `install-agent-context`, and `run start` write a managed ignore block for exactly that set into the target's `.gitignore` once (keyed on its marker line; edit the list beneath it freely). The readback bundle (`build-context.json`, `assembly-report.json`, `doctor-output.json`, `qa-verdict.json`), `input/`, `theme/`, `agent-context/`, and `setup-handoff.json` are deliberately not ignored. The Campaigns API key is a public, browser-side, domain-allowlisted key and may already be present in the local CampaignSpec as `campaign.campaigns_api_key`; do not duplicate it into the packet unless the spec is unavailable. Do not commit raw private API responses, backend secrets, or temporary media exports.
|
|
662
|
+
|
|
663
|
+
Packet-mode `doctor` is inspection-only by default and preserves retained evidence and active run journals, even when lifecycle capture is configured. Use `--write` to intentionally record fresh evidence; `--no-write` takes precedence. Inspection still reports current blockers and keeps the same exit status.
|
|
664
|
+
|
|
665
|
+
`campaigns-os doctor --packet <packet> --write` restates its outcome on the Assembly Report's `stages.doctor` (status, command, outputs, blockers, warnings, `checked_at`). A re-run that reaches the same outcome leaves the report's bytes unchanged rather than refreshing the timestamp alone, so a digest taken of the report — a Run Record's `assembly_report` sha256 — keeps verifying across repeated doctor runs; a changed outcome still rewrites the file.
|
|
666
|
+
|
|
667
|
+
`campaigns-os start` / `campaigns-os prepare-build` writes packet, context, report, and generated doctor-output paths as relative paths by default, including sibling CampaignSpec/source directories such as `../campaign-source`. `campaigns-os doctor` continues to accept older absolute-path packets; use `campaigns-os doctor --packet <packet> --write --strip-paths` when regenerating a commit-ready doctor output from an older packet. Committed handoff artifacts should not contain machine-local absolute paths unless no relative form is possible.
|
|
668
|
+
|
|
669
|
+
`start` / `prepare-build` also run the [Brand Theme Bridge](./brand-theme-bridge.md)
|
|
670
|
+
in `inspect_only` mode. The optional theme evidence lives in `context.theme`,
|
|
671
|
+
`report.theme`, and `.campaign-runtime/theme/theme-report.json`. The Build
|
|
672
|
+
Packet itself does not gain required theme fields in v0.
|
|
673
|
+
|
|
674
|
+
A campaign whose source carries no brand tokens has one more decision to make,
|
|
675
|
+
and it is due before QA rather than after it. With nothing to generate, the
|
|
676
|
+
theme gate passes (`theme_gate.nothing_generatable`) and no brand layer is
|
|
677
|
+
applied, so the commerce pages ship the starter family's own palette — and
|
|
678
|
+
browser QA, with the gate unwaived, runs the template-residue checks at blocker
|
|
679
|
+
severity, so `qa run` blocks on `template-residue:<page>:style:*` rows for the
|
|
680
|
+
starter call-to-action colour. That is deliberate on both sides: a passing gate
|
|
681
|
+
means "nothing could be generated", not "this palette was reviewed". Two lanes
|
|
682
|
+
clear it, and `campaigns-os next` names them from the build stage onward so the
|
|
683
|
+
choice is made before a blocked verdict forces it — for the families this
|
|
684
|
+
applies to. Palette residue is a certified-family check: it runs only where the
|
|
685
|
+
selected `template_family` has a brand contract listing both the starter colours
|
|
686
|
+
and the commerce selectors to inspect them on, so a `custom` or `undecided`
|
|
687
|
+
family produces no `template-residue:*:style:*` rows and `next` stays quiet
|
|
688
|
+
rather than asking for a waiver it does not need. Either record an explicit
|
|
689
|
+
operator waiver (`campaigns-os theme waive --packet <packet> --reason "<why the
|
|
690
|
+
starter palette is acceptable>" --waived-by "<named human>"`, optionally
|
|
691
|
+
`--expires-at <canonical ISO timestamp>`), which downgrades those rows to `warn`
|
|
692
|
+
(status and severity — never `fail`) and keeps the shipped palette visible in
|
|
693
|
+
the verdict; or hand-author the brand
|
|
694
|
+
layer — write `brand-theme.css`, list it after `next-core.css` in commerce-page
|
|
695
|
+
frontmatter styles, rebuild, and record `report.theme.status: applied` with
|
|
696
|
+
`load_order: after-next-core`. Nothing waives the gate on the operator's
|
|
697
|
+
behalf. See [Brand Theme Bridge](./brand-theme-bridge.md) for both lanes in
|
|
698
|
+
full.
|
|
699
|
+
|
|
700
|
+
`start` / `prepare-build` also accept `--brief <campaign-build-brief.yaml|json>`
|
|
701
|
+
and auto-discover `campaign-build-brief.yaml`, `.yml`, or `.json` from the
|
|
702
|
+
source root or target repo. When none is present, Campaigns OS creates a guided
|
|
703
|
+
draft at `.campaign-runtime/input/campaign-build-brief.normalized.json`.
|
|
704
|
+
See [Campaign Build Brief](./campaign-build-brief.md) for the schema and
|
|
705
|
+
prepared/guided behavior.
|
|
706
|
+
|
|
707
|
+
`start` / `prepare-build` also prepares the normalized Design Source Package at
|
|
708
|
+
`.campaign-runtime/input/design-source-package.json`. When that path is absent,
|
|
709
|
+
the command synthesizes and writes the package; when it exists, the command
|
|
710
|
+
validates it against the current campaign/page/source/template inputs and reuses
|
|
711
|
+
its exact bytes. It refuses stale or contradictory packages instead of silently
|
|
712
|
+
regenerating them. Its schema version is
|
|
713
|
+
`campaign-design-source-package/v0` (schema file:
|
|
714
|
+
`schemas/campaign-design-source-package.v0.schema.json`). The Build Packet,
|
|
715
|
+
Build Context, and Assembly Report reference that artifact by path, full artifact
|
|
716
|
+
hash, and material fingerprint instead of embedding it,
|
|
717
|
+
matching the normalized Build Brief handoff pattern. The full hash supports audit
|
|
718
|
+
and reproduction; the material fingerprint drives freshness gates. The package
|
|
719
|
+
includes a generated top-level `readiness` summary with
|
|
720
|
+
`status`, `blocking_reasons`, `gap_count`, `todo_count`, `waiver_count`, and
|
|
721
|
+
`generated_at`; detailed gaps, TODOs, and waivers remain authoritative.
|
|
722
|
+
See the dedicated [Design Source Package v0 guide](./design-source-package.md)
|
|
723
|
+
for the exact reference shape, material projection, emit/reuse/refusal boundary,
|
|
724
|
+
and lifecycle ownership contract. This section keeps the Build Packet handoff
|
|
725
|
+
context and does not replace that consumer guide.
|
|
726
|
+
|
|
727
|
+
`readiness.status` uses `pending`, `blocked`, `ready`, `ready_with_gaps`, or
|
|
728
|
+
`ready_with_waivers`, not `ready_with_warnings`. The package may include
|
|
729
|
+
free-form `notes`, but notes do not affect readiness; any concern that affects
|
|
730
|
+
whether Build or Polish can proceed must be typed as a gap, TODO, proposed
|
|
731
|
+
exception, or waiver. Source gaps and TODOs require `scope` and `applies_to`;
|
|
732
|
+
attach them to Surface Identity when possible. The package reserves a top-level
|
|
733
|
+
Surface Identity entry `campaign` with `kind: "campaign"`; use
|
|
734
|
+
`applies_to: ["campaign"]` for legitimate campaign-level gaps/TODOs. Surface
|
|
735
|
+
Identity IDs should be stable human-semantic strings such as `campaign`,
|
|
736
|
+
`landing`, `landing.hero`, `checkout`, `checkout.payment`, or
|
|
737
|
+
`upsell.offer-card`, with labels and aliases for source-specific, DOM, or Page
|
|
738
|
+
Kit names. v0 requires `campaign` plus page-level Surface Identities for active
|
|
739
|
+
or mapped CampaignSpec pages; section and runtime-surface IDs are optional until
|
|
740
|
+
Build or Polish needs them. Do not derive the primary Surface Identity solely
|
|
741
|
+
from CPK `page_type`, Map Builder custom labels, public routes, or producer page
|
|
742
|
+
types. Preserve those as mapped attributes or aliases alongside the Surface
|
|
743
|
+
Identity. For page-level IDs, prefer the CampaignSpec page ID when it is stable
|
|
744
|
+
and human-readable; otherwise derive from normalized page role plus order
|
|
745
|
+
(`landing`, `checkout`, `upsell-1`, `downsell-1`, `receipt`). Always preserve the
|
|
746
|
+
CampaignSpec page ID, Map Builder label/custom name, public route, source aliases,
|
|
747
|
+
and CPK `page_type` separately. `surface_identity[]` is a structured catalog,
|
|
748
|
+
not a simple list of strings. Minimum fields are `id`, `kind`, `label`,
|
|
749
|
+
`aliases`, and `mappings`; page-surface `mappings` preserve CampaignSpec page ID,
|
|
750
|
+
Map Builder label/custom name, public route, producer page type, and Page Kit
|
|
751
|
+
projection. Contribution mappings should reference `surface_identity[].id`
|
|
752
|
+
values and carry relationship metadata such as `coverage_role`, `confidence`,
|
|
753
|
+
`source_refs`, and `notes`. They should not define competing CampaignSpec route
|
|
754
|
+
or Page Kit projection maps. `coverage_role` is a small enum:
|
|
755
|
+
`primary_design`, `partial_design`, `brand_tokens`, `asset_source`,
|
|
756
|
+
`copy_source`, `template_baseline`, `reference_only`, or `fallback_legacy`;
|
|
757
|
+
use `notes` for unusual cases. Mapping `confidence` is also a coarse enum:
|
|
758
|
+
`high`, `medium`, `low`, or `unknown`. It describes confidence in the
|
|
759
|
+
surface/coverage mapping, not design quality or approval. Low confidence blocks
|
|
760
|
+
source readiness only when it affects required page-level `primary_design`
|
|
761
|
+
coverage; represent that as a Source TODO unless waived. Low confidence on
|
|
762
|
+
brand-token, reference-only, or other non-primary coverage does not by itself
|
|
763
|
+
block the v0 readiness evaluator; any gap or note is a separate explicit record.
|
|
764
|
+
|
|
765
|
+
Screenshot references in the Design Source Package are source-side or
|
|
766
|
+
reference-side proof only: canonical URLs, exports, captured source renders, or
|
|
767
|
+
explicit records that a render is unavailable. Built-output screenshots for the
|
|
768
|
+
current implementation belong in Polish Evidence or later QA evidence, tied to
|
|
769
|
+
the current build fingerprint. Polish should compare against Design Source
|
|
770
|
+
Package refs and Template Reference refs without mutating either source artifact.
|
|
771
|
+
If Polish can capture a missing canonical source render, it should emit a
|
|
772
|
+
proposed source-reference update or Source TODO rather than silently updating the
|
|
773
|
+
Design Source Package. Source preparation owns any package mutation and must
|
|
774
|
+
record it explicitly with attribution.
|
|
775
|
+
Material source-reference refreshes create a new Design Source Package
|
|
776
|
+
fingerprint. Any Build, Polish, or QA evidence tied to the previous source
|
|
777
|
+
fingerprint is stale until refreshed or explicitly waived. v0 determines
|
|
778
|
+
materiality through its explicit projection, not a marker in the package.
|
|
779
|
+
Top-level `generated_at`, generated readiness/readback, notes, visual
|
|
780
|
+
`captured_at` alone, formatting, key order, and normalized record/set order are
|
|
781
|
+
non-material; the exact artifact-byte hash still changes when their serialized
|
|
782
|
+
bytes change.
|
|
783
|
+
Polish Evidence must record both the current build fingerprint and the current
|
|
784
|
+
Design Source Package material fingerprint, conventionally as
|
|
785
|
+
`source_build_fingerprint` for the assembly/build artifact and
|
|
786
|
+
`source_package_material_fingerprint` for the design source context. Freshness
|
|
787
|
+
gates consider Polish current only when both match the latest artifacts;
|
|
788
|
+
if either changes materially, Polish is stale unless a structured waiver explains
|
|
789
|
+
the exception. During the v0 transition, the polish gate enforces
|
|
790
|
+
`source_package_material_fingerprint` only when the Assembly Report exposes a
|
|
791
|
+
current Design Source Package material fingerprint, such as
|
|
792
|
+
`design_source_package.material_fingerprint`. Legacy reports without a current
|
|
793
|
+
source package keep the build-fingerprint gate and emit a readiness warning
|
|
794
|
+
instead of blocking.
|
|
795
|
+
|
|
796
|
+
Build must also record the Design Source Package material fingerprint it
|
|
797
|
+
consumed on `stages.assembly.source_package_material_fingerprint`; Prepare does
|
|
798
|
+
not populate that consumption field. If the current
|
|
799
|
+
`design_source_package.material_fingerprint` is missing from Assembly or differs
|
|
800
|
+
from the Assembly-recorded value, `campaigns-os next` routes back to Build before
|
|
801
|
+
Polish. Polish must review a build made from the current material source context;
|
|
802
|
+
it should not repair or certify a build made from stale design inputs.
|
|
803
|
+
|
|
804
|
+
`stages.assembly.build_fingerprint` is the fingerprint of the built OUTPUT, not
|
|
805
|
+
of its inputs: it changes exactly when the bytes under `_site/<public_route_slug>/`
|
|
806
|
+
change, so a toolkit or template upgrade that renders different output from
|
|
807
|
+
identical source reads as a different build, and identical output on any machine
|
|
808
|
+
at any path yields the same value. The algorithm (`sha256-manifest/v1`): list every
|
|
809
|
+
file under the built route root, path relative to that root with `/` separators,
|
|
810
|
+
sorted by code point; for each file emit one `<path>\n<sha256-hex>\n` pair; the
|
|
811
|
+
fingerprint is `sha256:` plus the SHA-256 of that manifest. Nothing is excluded by
|
|
812
|
+
default (Page Kit writes only rendered HTML and copied assets into `_site/`, nothing
|
|
813
|
+
it timestamps); `.campaign-runtime/page-kit-build-summary.json` lives outside the
|
|
814
|
+
root and is not hashed. Build does not type the value: after page-kit build it runs
|
|
815
|
+
`campaigns-os doctor --packet <packet> --json` and copies
|
|
816
|
+
`derived.build_output_fingerprint.value` (with `.file_count` and `.status`) onto
|
|
817
|
+
`stages.assembly.build_fingerprint`. Doctor recomputes the value on every run
|
|
818
|
+
(`built_output.fingerprint`): a match is a ready line, a missing record is the
|
|
819
|
+
warning `built_output.fingerprint_missing` carrying the value to record, and a
|
|
820
|
+
recorded value the output no longer matches is `built_output.fingerprint_stale`
|
|
821
|
+
(blocking once assembly is complete). The polish gate, QA, and `polish capture`
|
|
822
|
+
compare evidence against that recomputed value, so evidence bound to a build whose
|
|
823
|
+
output has since changed is `polish.output_drift` even when the recorded string
|
|
824
|
+
still matches (`polish.stale` stays the code for evidence stamped against an older
|
|
825
|
+
recorded build). `polish capture` refuses by name when the built route root is
|
|
826
|
+
missing, unreadable, or drifted; symbolic links are never build output and are
|
|
827
|
+
skipped by the walk.
|
|
828
|
+
|
|
829
|
+
A stale or missing Assembly Source Package Fingerprint is waivable only as an
|
|
830
|
+
exceptional Source Freshness Waiver. The waiver must be structured in
|
|
831
|
+
`waivers[]`, with `scope: "assembly_source_package_freshness"` or an
|
|
832
|
+
`applies_to` reference such as
|
|
833
|
+
`stages.assembly.source_package_material_fingerprint`, plus reason, owner or
|
|
834
|
+
waived_by, timestamp, and expiry/review condition. The waiver allows the
|
|
835
|
+
orchestration loop to proceed to Polish, but Polish still must record current
|
|
836
|
+
Polish Evidence, including `source_package_material_fingerprint` when a current
|
|
837
|
+
Design Source Package exists. The waiver must remain visible in Campaign
|
|
838
|
+
Readiness Readback and downstream QA evidence; it is not a silent pass.
|
|
839
|
+
In v0, write accepted Source Freshness Waivers directly into `waivers[]`.
|
|
840
|
+
`campaigns-os checkpoint waive` is a staged generic registry and currently
|
|
841
|
+
accepts four gates: `page_kit.store_profile`, `page_kit.sdk_version`,
|
|
842
|
+
`polish.hidden_eager_media`, and `built_output.upsell_selector_scope`; an
|
|
843
|
+
unregistered gate id is refused with that list. `theme waive` applies the same
|
|
844
|
+
attribution rule (a named human, no placeholder, an optional future
|
|
845
|
+
`--expires-at`) on its own lane. Within Polish, only the broader Source Freshness
|
|
846
|
+
waiver retains its existing report path; theme and QA decisions retain their
|
|
847
|
+
existing artifact or waiver paths until each is explicitly registered.
|
|
848
|
+
|
|
849
|
+
In v0, material source fingerprint fields include contribution identity/kind,
|
|
850
|
+
provenance, presentation intent, Surface Identity catalog and mappings,
|
|
851
|
+
contribution coverage roles, mapping confidence, source refs, source
|
|
852
|
+
screenshot/reference refs, Template Reference linkage, Source Gaps, Source
|
|
853
|
+
TODOs, accepted waivers, and any source divergence or proposed exception that
|
|
854
|
+
has `readiness_affecting: true`. Generated readback prose, formatting/key order,
|
|
855
|
+
and administrative notes are non-material when they do not alter readiness,
|
|
856
|
+
coverage, provenance, or comparison basis. Capture timestamps alone may be
|
|
857
|
+
non-material, but changing the viewport key, URL, dimensions, artifact path, or
|
|
858
|
+
visual artifact hash is material.
|
|
859
|
+
|
|
860
|
+
For renderable contributions that provide page-level `primary_design` coverage,
|
|
861
|
+
source readiness requires at least desktop and mobile screenshot refs. Tablet is
|
|
862
|
+
optional in v0. If a source is renderable but cannot be captured, record a
|
|
863
|
+
Source TODO unless the absence is explicitly accepted as a Source Gap or covered
|
|
864
|
+
by an approved Checkpoint Waiver. Template-baseline pages use the selected
|
|
865
|
+
Template Reference standard viewport refs rather than source-specific captures.
|
|
866
|
+
Use shared viewport keys across source refs, Polish Evidence, and QA evidence:
|
|
867
|
+
`mobile`, `desktop`, and optional `tablet` in v0. Exact width, height, device
|
|
868
|
+
profile, scale factor, browser, capture time, and URL are capture metadata, not
|
|
869
|
+
new viewport names. Avoid stage-specific aliases such as `iphone`, `small`,
|
|
870
|
+
`wide`, or `1440`; keep those details in metadata so cross-stage comparisons can
|
|
871
|
+
join on the same keys.
|
|
872
|
+
|
|
873
|
+
Required page-level coverage applies to every active or mapped page in the
|
|
874
|
+
current build scope. A page is covered by a non-low-confidence `primary_design`
|
|
875
|
+
contribution, an explicit `template_baseline` contribution for template-stock
|
|
876
|
+
pages, or an attributed Source Gap / approved Checkpoint Waiver explaining why
|
|
877
|
+
no primary design source exists. `template_baseline` must reference the selected
|
|
878
|
+
template family/version and Template Reference artifact or contract. If the
|
|
879
|
+
Template Reference proof is missing, record a Source TODO, Source Gap, or waiver
|
|
880
|
+
according to whether the missing proof represents unfinished preparation, an
|
|
881
|
+
accepted source absence, or an approved run exception. Missing page-level
|
|
882
|
+
coverage blocks source readiness.
|
|
883
|
+
|
|
884
|
+
## Adapter And Proof Fields
|
|
885
|
+
|
|
886
|
+
Fresh packets now include `source_html.adapter_contract`. Build Context and
|
|
887
|
+
Assembly Report carry the same values as `adapter_decisions`, and build agents
|
|
888
|
+
should update the report as they complete work.
|
|
889
|
+
|
|
890
|
+
Required adapter decisions:
|
|
891
|
+
|
|
892
|
+
| Field | Purpose |
|
|
893
|
+
| --- | --- |
|
|
894
|
+
| `raw_html_conversion_status` | Whether prepared HTML has been converted into page-kit-ready source. |
|
|
895
|
+
| `source_asset_strategy` | How images/fonts/CSS/JS are moved and referenced. Prefer `pagekit_campaign_asset_root`. |
|
|
896
|
+
| `commerce_shell_adoption` | Whether checkout/upsell/downsell/receipt use a template-clone-first SDK surface. |
|
|
897
|
+
| `route_rewrite_policy` | How page links, CTAs, and SDK routing values were rewritten from CampaignSpec routes. |
|
|
898
|
+
| `template_files_copied` | Whether the selected template family was copied/verified as one atomic page-kit slice. |
|
|
899
|
+
| `config_script_strategy` | How campaign config scripts are loaded. |
|
|
900
|
+
| `wrapper_policy` | Whether document wrappers are stripped, preserved, or not required. |
|
|
901
|
+
| `frontmatter_policy` | How Page Kit YAML frontmatter is created or preserved. |
|
|
902
|
+
| `script_style_reference_policy` | How scripts/styles move into frontmatter, campaign assets, inline blocks, or passthrough. |
|
|
903
|
+
| `cta_rewrite_policy` | How CTA destinations are rewritten from CampaignSpec routes. |
|
|
904
|
+
| `layout_choice` | Which Page Kit layout strategy wraps the prepared source. |
|
|
905
|
+
|
|
906
|
+
Fresh build context also includes `source.asset_crawl`
|
|
907
|
+
(`source-asset-crawl/v0`). `prepare-build` scans the source HTML files and
|
|
908
|
+
referenced local CSS, then records each local image/font/CSS/JS asset ref with:
|
|
909
|
+
|
|
910
|
+
- `raw` and `normalized` source refs;
|
|
911
|
+
- `source_path` / `source_exists` resolution under the source root;
|
|
912
|
+
- `pagekit_asset_path` for the campaign asset-root ref to use during assembly;
|
|
913
|
+
- summarized warnings for raw `/assets/...` refs, missing local files, and
|
|
914
|
+
refs that escape the source root.
|
|
915
|
+
|
|
916
|
+
Use this inventory before moving assets into Page Kit. It is deliberately a
|
|
917
|
+
context/report aid, not part of `source_html.pages[]` page binding.
|
|
918
|
+
|
|
919
|
+
`template_files_copied` is intentionally group-based rather than prose-only:
|
|
920
|
+
`pages`, `_includes`, `_layouts`, `assets/css`, `assets/js`, and
|
|
921
|
+
`frontmatter_vocabulary`. Doctor warns when an assembly-complete report still
|
|
922
|
+
shows `pending`/`partial` template copying or misses one of those groups. When
|
|
923
|
+
the status is `complete` or `verified_existing_slice`, `paths` must name
|
|
924
|
+
target-repo-relative proof paths and doctor verifies those paths exist.
|
|
925
|
+
|
|
926
|
+
Fresh packets also include `qa.proof_policy`, mirrored into
|
|
927
|
+
`report.proof_policy`. It records browser QA requirement, typed-card depth,
|
|
928
|
+
localhost Development-domain behavior, non-localhost SDK allowlist requirement,
|
|
929
|
+
order path depth, and operator approval state. Test cards still need no
|
|
930
|
+
permission gate; the explicit field prevents agents from re-litigating proof
|
|
931
|
+
depth in chat. Doctor checks the full field set in both packet and report
|
|
932
|
+
artifacts when present. `order_path_depth` is seeded `common` and set with
|
|
933
|
+
`--order-path-depth <off|common|full>` on `prepare-build`/`start` or later
|
|
934
|
+
with `qa policy set --order-path-depth <depth>`, which also refreshes the
|
|
935
|
+
report mirror; a packet whose depth disagrees with its report mirror draws the
|
|
936
|
+
advisory `qa.proof_policy.order_path_depth_drift` warning naming that command
|
|
937
|
+
(see `docs/qa-and-test-orders.md`, "Purchase-proof coverage"). The `qa` block carries no permission booleans:
|
|
938
|
+
`qa.test_orders_allowed` and `qa.sandbox_test_card_confirmed`, which no command
|
|
939
|
+
read, were removed in supported surface 1.28.0, and doctor warns
|
|
940
|
+
(`qa.removed_policy_fields`) on a packet that still carries either.
|
|
941
|
+
|
|
942
|
+
### Deploy target
|
|
943
|
+
|
|
944
|
+
`deploy.target` names where the built `_site/` output is served for QA. The
|
|
945
|
+
schema enum is `netlify`, `cloudflare-pages`, `vercel`, `shopify-proxy`,
|
|
946
|
+
`agency-ci`, `local-serve` and `unknown`; doctor blocks (`deploy.target`) on
|
|
947
|
+
any other value. `prepare-build`/`start` take `--deploy-target <target>` and
|
|
948
|
+
default to `unknown`; `qa policy set --deploy-target <target>` changes it later.
|
|
949
|
+
|
|
950
|
+
`local-serve` (added in 1.28.0) is the localhost QA path: nothing is deployed,
|
|
951
|
+
the built output is served on localhost by any static server, and
|
|
952
|
+
`deploy.preview_url` records that origin. Localhost on any port is a Campaigns
|
|
953
|
+
App Development domain (SDK allowed, analytics suppressed), so under
|
|
954
|
+
`local-serve` doctor does not raise `campaign.allowed_domains_confirmed`, reads
|
|
955
|
+
a recorded localhost URL as the intended state (a `ready` line), accepts a
|
|
956
|
+
loopback host (`127.0.0.1`, `[::1]`) with a ready line naming the
|
|
957
|
+
`http://localhost:<port>/` fallback, and warns (`deploy.local_serve_url`) when
|
|
958
|
+
the recorded URL is neither.
|
|
959
|
+
`next` at the deploy stage then hands off a serve-locally prompt and action
|
|
960
|
+
instead of a ship-to-host one. The directory to serve is `_site/`; for a
|
|
961
|
+
root-served campaign (`campaign.route_root: "/"`) the handoff adds that pages
|
|
962
|
+
are served at site-root paths while assets keep the `/<public_route_slug>/`
|
|
963
|
+
prefix, so `_site/` needs the same rewrite of root-level page routes onto
|
|
964
|
+
`/<public_route_slug>/<route>` the production host applies — no single
|
|
965
|
+
directory serves both. The QA stage is unchanged and runs against the recorded
|
|
966
|
+
URL.
|
|
967
|
+
|
|
968
|
+
`local-serve` also selects **local proof mode** for the build stage: page-kit
|
|
969
|
+
is built in the development environment (`CPK_ENV=development npx
|
|
970
|
+
campaign-build --json > .campaign-runtime/page-kit-build-summary.json`) into
|
|
971
|
+
`_site/`, and the build records `stages.assembly.evidence.build_environment:
|
|
972
|
+
"development"` on the Assembly Report (a free-form stage field; no schema
|
|
973
|
+
change). The starter templates gate every vendor loader on the environment,
|
|
974
|
+
and a production build's protocol-relative loaders (`//host/...`) fail over a
|
|
975
|
+
plain-HTTP local serve, voiding polish capture unwaivably; the SDK's `dl_*`
|
|
976
|
+
events still fire in development. Before commit, `campaigns-os page-kit parity
|
|
977
|
+
--packet <packet>` renders the current source in both environments to temp
|
|
978
|
+
directories and proves the served output is the current development render
|
|
979
|
+
and that production differs from it only in environment-gated output, with
|
|
980
|
+
the same page set, route slugs, Campaign Cart pin and `next-api-key`; the
|
|
981
|
+
result is recorded on `stages.assembly.evidence.local_proof.production_parity`
|
|
982
|
+
and doctor reports it as `local_proof.production_parity` (with
|
|
983
|
+
`local_proof.build_environment` for the build record). The PR preview is the
|
|
984
|
+
second check. The toolkit never proposes editing a generated include to make a
|
|
985
|
+
local capture pass. Details and the step order:
|
|
986
|
+
[qa-and-test-orders.md](./qa-and-test-orders.md#local-proof-mode-deploytarget-local-serve).
|
|
987
|
+
|
|
988
|
+
Campaign Build Brief `qa_policy` is deliberately scoped as
|
|
989
|
+
`documented_expectation` metadata. Use it to preserve business QA intent, but
|
|
990
|
+
do not treat it as the enforced gate; doctor/QA enforcement reads the packet
|
|
991
|
+
and report proof policy fields above.
|
|
992
|
+
|
|
993
|
+
## CampaignSpec Retrieval (`--map-id`)
|
|
994
|
+
|
|
995
|
+
`campaigns-os start` / `campaigns-os prepare-build` accept the CampaignSpec via either of two routes:
|
|
996
|
+
|
|
997
|
+
| Flag | Source | When to use |
|
|
998
|
+
| --- | --- | --- |
|
|
999
|
+
| `--spec <path>` | Local JSON file | Offline work, CI runs against a fixture, or hand-edited spec drafts |
|
|
1000
|
+
| `--map-id <id>` | Map Builder proxy (KV-backed) | Default agentic flow — KV is the source of truth, no file shuttling |
|
|
1001
|
+
|
|
1002
|
+
When `--map-id <id>` is set, the CLI fetches `GET <proxy>/api/spec/<id>` (default `<proxy>` is `https://campaign-map.nextcommerce.com`) and caches the response to `<target>/.campaign-runtime/fetched-specs/<id>.json`. The cached file is what downstream stages read, so the packet's `spec.local_path` always resolves to an on-disk artifact regardless of intake mode.
|
|
1003
|
+
|
|
1004
|
+
Retrieval behavior:
|
|
1005
|
+
|
|
1006
|
+
- **Re-fetch by default.** Every `start` / `prepare-build` invocation re-fetches from KV. KV is the source of truth; the cache file is a debug/inspection artifact, not a performance optimization.
|
|
1007
|
+
- **`--cached-spec`** reuses the cache without a network call. Use for offline iteration or when the proxy is temporarily unreachable.
|
|
1008
|
+
- **`--proxy-base <url>`** overrides the default origin. Use for staging environments or local Worker dev (`wrangler dev`). Spec retrieval carries no credential, so any reachable origin works here — but the same flag also aims the credential-bearing rails (Run Telemetry remit, QA verdict publish, `telemetry list`), and those require `https:` unless the host is loopback (`localhost`, `127.0.0.1`, `[::1]`), which is allowed over plain http with a stderr warning. A plain-http remote proxy is refused before the request. See docs/workflow-findings-sidecar.md (Remit Channel).
|
|
1009
|
+
- Failure modes (HTTP error, `{ok: false}` response, network timeout) surface as clean CLI errors before any packet is written.
|
|
1010
|
+
|
|
1011
|
+
The fetched spec is treated identically to a `--spec`-supplied local file from this point forward — same identity validation, same `prepareBuild` pipeline, same idempotency semantics. Re-running `start --map-id` on the same campaign re-fetches the spec, regenerates the packet, and re-runs doctor. If `design_source` was newly populated since the last run, the doctor's design_source-aware blocker logic surfaces it; if nothing changed, the run is a no-op as far as downstream stages are concerned.
|
|
1012
|
+
|
|
1013
|
+
## Source HTML Manifest Auto-Population
|
|
1014
|
+
|
|
1015
|
+
When the source HTML root carries a source-html manifest at `<source>/.campaigns-os/source-html-manifest.json` (schema `source-html-manifest/v0`, published at `schemas/source-html-manifest.v0.schema.json`) — or `--design-manifest <path>` names a manifest of that schema anywhere else, for a source root nobody can write to — `campaigns-os prepare-build` reads it and uses its `pages[]` block to populate `packet.source_html.pages[]` directly — bypassing the legacy filesystem-name slug matching. Wherever the manifest lives, its `pages[].path` entries stay relative to `--source`. A `pages[]` entry with `skip_reason` and no `path` declares a template-stock page: its assembly decision carries `template_stock: true` and the locked family, and intake demands no design source for it ([Template-stock pages](design-source-package.md#template-stock-pages-the-family-decides)).
|
|
1016
|
+
|
|
1017
|
+
The source-html manifest remains a producer/source-HTML adapter input. It is not
|
|
1018
|
+
renamed into the Design Source Package. In the normalized source workflow,
|
|
1019
|
+
`prepare-build` uses source-html manifests, filesystem fallback, template-stock
|
|
1020
|
+
inputs, and other adapters to emit a separate public Design Source Package with
|
|
1021
|
+
contributions, coverage, gaps/TODOs, Surface Identity, references, and readback.
|
|
1022
|
+
When source-html data is the available input and the default package path is
|
|
1023
|
+
missing, current v0 `prepare-build` synthesizes the package. If a package already
|
|
1024
|
+
exists, it is validated against the current material inputs and reused byte for
|
|
1025
|
+
byte or refused; it is never silently regenerated. Downstream Build and Polish
|
|
1026
|
+
consume the package concept rather than branching back to
|
|
1027
|
+
`packet.source_html` as a second source model. The emitted package lives at
|
|
1028
|
+
`.campaign-runtime/input/design-source-package.json` by default and is referenced
|
|
1029
|
+
from packet/context/report by path, full artifact hash, and material fingerprint.
|
|
1030
|
+
|
|
1031
|
+
Behavior:
|
|
1032
|
+
|
|
1033
|
+
- The manifest is consumed only when it passes the `source-html-manifest/v0`
|
|
1034
|
+
validator. Unknown schema versions, missing `page_id`, entries with neither
|
|
1035
|
+
(or both) `path` and `skip_reason`, or malformed `source_hash` values log a
|
|
1036
|
+
warning and fall back to filesystem matching so out-of-band tools cannot
|
|
1037
|
+
silently corrupt the packet. Doctor also validates a present manifest at the
|
|
1038
|
+
source root.
|
|
1039
|
+
- A partial-source build is declarable (#238). A page entry may carry
|
|
1040
|
+
`skip_reason` instead of `path` to declare that active page out of source
|
|
1041
|
+
scope (a template-derived page has no source HTML by design), and
|
|
1042
|
+
CampaignSpec `build_scope.mode: "partial"` declares the same thing as a
|
|
1043
|
+
blanket for active pages with no manifest entry and no `design_source`.
|
|
1044
|
+
Declared pages are recorded on the packet as `skip_reason` mappings and on
|
|
1045
|
+
the assembly report under `stages.prepare_build.declared_out_of_scope`;
|
|
1046
|
+
prepare-build reaches `completed_partial` instead of blocking on
|
|
1047
|
+
`MISSING_SOURCE_PAGE`, and the declaration regenerates identically on every
|
|
1048
|
+
`start`/`prepare-build` run because it derives from the spec and manifest.
|
|
1049
|
+
A page that declares `design_source` still blocks without a per-page skip
|
|
1050
|
+
entry, and full/undeclared scope keeps the blocking behavior exactly.
|
|
1051
|
+
- The manifest's `page_id` must match an active CampaignSpec page id. Manifest entries with no matching spec page surface as a `MANIFEST_EXTRA_PAGE` prompt (analogous to the existing `MISSING_SOURCE_PAGE` prompt) so the operator reconciles either the spec or the manifest before build.
|
|
1052
|
+
- Optional manifest `page_url` values must be unique after Page Kit route normalization. Duplicate values surface as `MANIFEST_DUPLICATE_PAGE_URL`; prepare-build keeps the first value for route fallback matching and asks the operator to deduplicate before build.
|
|
1053
|
+
- Path values are relative to the source HTML root (`<source>`), not to the `.campaigns-os/` directory that contains the manifest. For example, use `checkout/index.html`, not `../checkout/index.html`.
|
|
1054
|
+
- An optional top-level `wrapper_policy` key declares the document-wrapper policy for the handed-over source, in the same vocabulary the packet records at `source_html.adapter_contract.wrapper_policy`. prepare-build seeds the adapter contract from it; the `--wrapper-policy` flag overrides it, and with neither the default stays `strip_document_wrappers`. Unlike the keys above, a value outside the vocabulary does not invalidate the manifest — the key is ignored with a warning and the rest of the manifest is used as written. See [docs/source-adapters.md](source-adapters.md#selecting-the-wrapper-policy-at-intake).
|
|
1055
|
+
- The build context records `source.manifest` with `schema_version`, `generator`, `generated_at`, and `page_count`, and the assembly decision log records evidence citing the manifest file.
|
|
1056
|
+
|
|
1057
|
+
When the manifest is absent, prepare-build falls back to filesystem-name slug
|
|
1058
|
+
matching. If exactly one candidate matches an active page, the mapping is
|
|
1059
|
+
recorded as before. If multiple HTML files can satisfy the same page, or if all
|
|
1060
|
+
matching files were already assigned to sibling pages, prepare-build blocks with
|
|
1061
|
+
`AMBIGUOUS_SOURCE_PAGE`, records `context.source.ambiguous_candidates`, and
|
|
1062
|
+
drafts `context.source.manifest_draft` so the operator can write
|
|
1063
|
+
`.campaigns-os/source-html-manifest.json` and choose the intended paths before
|
|
1064
|
+
build.
|
|
1065
|
+
|
|
1066
|
+
### Page Kit Target Projection
|
|
1067
|
+
|
|
1068
|
+
`source_html.pages[].path` is source provenance. It names the producer/source-root-relative HTML file that should be consumed; it is not necessarily the file path to write under the Page Kit campaign directory.
|
|
1069
|
+
|
|
1070
|
+
Fresh `prepare-build` output also writes `source_html.pages[].page_kit` for mapped pages. This block is the Page Kit target projection:
|
|
1071
|
+
|
|
1072
|
+
- `target_path` is the page file relative to `assembly.output_dir` (`checkout.html`, `receipt.html`, etc.).
|
|
1073
|
+
- `output_path` is the same target file relative to `assembly.target_repo`.
|
|
1074
|
+
- `public_route` is the rendered campaign-rooted route Page Kit should produce.
|
|
1075
|
+
- `page_type` is the CPK runtime/analytics vocabulary (`product`, `checkout`, `upsell`, `receipt`), not the richer CampaignSpec or producer page type. CampaignSpec `select` pages project as CPK `checkout` because they are pre-checkout runtime selection surfaces.
|
|
1076
|
+
- `frontmatter` names the Page Kit frontmatter fields the build should write or preserve.
|
|
1077
|
+
- `permalink_required` is true when Page Kit's filename-derived route would not match `public_route`.
|
|
1078
|
+
|
|
1079
|
+
The Design Source Package should reference this projection without confusing it
|
|
1080
|
+
with Surface Identity. Surface Identity is the campaign-facing join key; Page Kit
|
|
1081
|
+
`page_type`, public routes, output paths, CampaignSpec/Map Builder page IDs,
|
|
1082
|
+
custom labels, and producer page types stay as mapped attributes or aliases.
|
|
1083
|
+
|
|
1084
|
+
`page_map[].output_path` in the Build Context is the same Page Kit target path,
|
|
1085
|
+
not `source_html.pages[].path` appended under `assembly.output_dir`. Build agents
|
|
1086
|
+
should read `source_path` for producer provenance and `page_kit.output_path` for
|
|
1087
|
+
the file to write.
|
|
1088
|
+
|
|
1089
|
+
CampaignSpec `page_url` and legacy `url` values are interpreted as Page Kit
|
|
1090
|
+
routes during projection. That normalization strips `.html`/`index.html`,
|
|
1091
|
+
removes query/fragment values, converts absolute preview URLs to their path, and
|
|
1092
|
+
normalizes trailing slashes before deriving target files and frontmatter routes.
|
|
1093
|
+
|
|
1094
|
+
This prevents mixed-source manifests such as `checkout/index.html` from leaking producer folder structure into `src/<slug>/checkout/index.html`. Campaigns OS owns the Adapter from source/manifest/CampaignSpec into Page Kit shape; Page Kit remains the target.
|
|
1095
|
+
|
|
1096
|
+
`target_path` intentionally uses the terminal route segment (`checkout/step-1/`
|
|
1097
|
+
projects to `step-1.html`). If two routes collapse to the same target filename,
|
|
1098
|
+
prepare-build emits `PAGE_KIT_TARGET_CONFLICT`; change one CampaignSpec route
|
|
1099
|
+
before build instead of letting an agent choose a destination.
|
|
1100
|
+
|
|
1101
|
+
### Per-page `source_hash` (Slice 6 drift detection)
|
|
1102
|
+
|
|
1103
|
+
Each `manifest.pages[]` entry MAY carry a `source_hash` field — the sha256 hex digest of the source HTML file's contents at the moment the producer wrote the manifest. When present, prepare-build threads the hash onto the matching `packet.source_html.pages[]` mapping. Doctor reads the packet mapping at validate time, computes the current on-disk sha256 of the same file, and warns (`source_html.pages.source_hash`) when they diverge.
|
|
1104
|
+
|
|
1105
|
+
Behavior:
|
|
1106
|
+
|
|
1107
|
+
- Optional on the producer side. Producers that don't emit `source_hash` (pre-Slice-6 manifests, template-stock, hand-authored) keep working; doctor's drift check is silent without a hash to compare.
|
|
1108
|
+
- Warning severity only. A drift never blocks a build — the operator decides whether to re-run the producer to refresh the manifest or accept the local edits.
|
|
1109
|
+
- The warning names the file path and includes both hashes (truncated to 12 chars) so the operator can confirm which file diverged without re-running the producer.
|
|
1110
|
+
|
|
1111
|
+
### Reference AI-generated producer
|
|
1112
|
+
|
|
1113
|
+
`scripts/reference-ai-producer.mjs` ships in this repo as the smallest possible producer reference. It walks a folder of HTML files (auto-discovery) or accepts explicit `--page page_id=path` mappings, computes sha256 per file, and emits the `source-html-manifest/v0` at the canonical location. Auto-discovery maps `landing.html` to `landing` and nested `checkout/index.html` to `checkout`; duplicate inferred page ids fail fast, so use explicit `--page` mappings for ambiguous layouts.
|
|
1114
|
+
|
|
1115
|
+
Usage:
|
|
1116
|
+
|
|
1117
|
+
```bash
|
|
1118
|
+
node scripts/reference-ai-producer.mjs \
|
|
1119
|
+
--source <source-root> \
|
|
1120
|
+
--campaign-slug <slug> \
|
|
1121
|
+
[--generator <name@version>] \
|
|
1122
|
+
[--page landing=presell-a.html --page checkout=checkout/step.html]
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
Real AI agents (Claude, Codex, etc.) that produce campaign source HTML should adopt this manifest shape so doctor's design_source-aware error messages and Slice 6 drift detection work uniformly across producers. The script generates only the manifest; it does not write any HTML.
|
|
1126
|
+
|
|
1127
|
+
## Authoring-Time Hints (Template Family + Upsell Pattern)
|
|
1128
|
+
|
|
1129
|
+
The CampaignSpec carries two optional **hints** the build agent uses
|
|
1130
|
+
as defaults. Both are hints, not contracts: CLI / operator overrides
|
|
1131
|
+
always win.
|
|
1132
|
+
|
|
1133
|
+
**Campaign-level:** `campaign.preferred_template_family` declares
|
|
1134
|
+
which starter family the campaign was authored against (one of
|
|
1135
|
+
`apollo`, `apollo-mv-single-step`, `olympus`, `limos`, `demeter`,
|
|
1136
|
+
`olympus-mv-single-step`, `olympus-mv-two-step`, `shop-single-step`,
|
|
1137
|
+
`shop-three-step`). The
|
|
1138
|
+
consumer (`preferredTemplateFamily()` in `src/cli.mjs`) reads this
|
|
1139
|
+
at three spec locations and uses it as the default template family
|
|
1140
|
+
when no `--template-family` CLI flag is given.
|
|
1141
|
+
|
|
1142
|
+
Resolution order:
|
|
1143
|
+
|
|
1144
|
+
1. `--template-family <family>` CLI flag (sets `template_lock.locked: true`).
|
|
1145
|
+
2. `spec.spec_identity.preferred_template_family`.
|
|
1146
|
+
3. `spec.campaign.preferred_template_family` (the canonical authoring location).
|
|
1147
|
+
4. `spec.preferred_template_family` (legacy fallback).
|
|
1148
|
+
5. `"undecided"`.
|
|
1149
|
+
|
|
1150
|
+
When the flag and the hint disagree, the flag wins and `prepare-build` says
|
|
1151
|
+
so rather than resolving in silence: it prints one stderr line naming the
|
|
1152
|
+
winning flag value, the overridden `preferred_template_family` value, and
|
|
1153
|
+
which channel each came from, and records the same thing on the assembly
|
|
1154
|
+
report as a `prepare_build` warning with code
|
|
1155
|
+
`TEMPLATE_FAMILY_HINT_OVERRIDDEN`. An operator reading the report
|
|
1156
|
+
therefore sees that the packet's family was an override rather than agreement
|
|
1157
|
+
with the spec. A flag that merely repeats the hint is agreement, not an
|
|
1158
|
+
override, and stays quiet. To build on the spec hint instead, re-run without
|
|
1159
|
+
`--template-family`; to remove the disagreement, update the spec so the two
|
|
1160
|
+
match.
|
|
1161
|
+
|
|
1162
|
+
When the hint wins, `template_lock.locked` stays `false` — the family is set as the default but not locked, so a downstream stage (or a follow-up operator pass) can override without contradiction. `template_decision_notes` records the hint source. `template.candidates` in the build context lists the hint with `source: "CampaignSpec preferred_template_family"` for provenance.
|
|
1163
|
+
|
|
1164
|
+
**Per-page:** `Page.upsell_template_pattern` declares the UI variant
|
|
1165
|
+
for an upsell page (one of `mv`, `bundle_tier_pills`,
|
|
1166
|
+
`bundle_tier_cards`, `single`). Flows from the spec page onto
|
|
1167
|
+
`packet.source_html.pages[].upsell_template_pattern` so the build
|
|
1168
|
+
stage can pick the right partial without re-parsing the spec.
|
|
1169
|
+
|
|
1170
|
+
The field is per-page; only upsell pages should carry it. Upstream
|
|
1171
|
+
spec validation warns when it's set on non-upsell pages, but the
|
|
1172
|
+
consumer surfaces it verbatim and lets the build stage decide what
|
|
1173
|
+
to do with it.
|
|
1174
|
+
|
|
1175
|
+
## Commerce Catalog (`assembly.commerce_catalog`)
|
|
1176
|
+
|
|
1177
|
+
`assembly.commerce_catalog` names the commerce-surface catalog the build and
|
|
1178
|
+
doctor read for the locked template family (`required`, `family`, `version`,
|
|
1179
|
+
`path`).
|
|
1180
|
+
|
|
1181
|
+
- `path: null` means the toolkit's own catalog
|
|
1182
|
+
(`contracts/commerce-surface-catalog.json` of the `campaigns-os` that is
|
|
1183
|
+
running). This is what `prepare-build` records by default. The catalog
|
|
1184
|
+
travels with the toolkit, not with the campaign, so the packet does not
|
|
1185
|
+
record where one machine's checkout or package install kept it, and the
|
|
1186
|
+
same packet resolves on any machine and under `npx campaigns-os`.
|
|
1187
|
+
- A string `path` is an operator-supplied `--commerce-catalog <path>`,
|
|
1188
|
+
recorded relative to the packet (keep it inside the campaign repo). Doctor
|
|
1189
|
+
resolves it against the packet's directory and blocks on
|
|
1190
|
+
`assembly.commerce_catalog.path` when it does not exist.
|
|
1191
|
+
- Packets prepared before `null` was recorded carry the toolkit catalog as a
|
|
1192
|
+
packet-relative path that climbs into the checkout that ran `prepare-build`
|
|
1193
|
+
(`../../../campaigns-os/contracts/commerce-surface-catalog.json`). When such a
|
|
1194
|
+
path does not exist but its file name is `commerce-surface-catalog.json`,
|
|
1195
|
+
doctor and QA resolve it to the running toolkit's catalog and doctor prints
|
|
1196
|
+
a `ready` line saying the packet still carries a machine-local path. That
|
|
1197
|
+
is never a blocker; re-running `prepare-build` records `null`.
|
|
1198
|
+
|
|
1199
|
+
## Orchestration Loop (`campaigns-os next`)
|
|
1200
|
+
|
|
1201
|
+
`campaigns-os next` (no stage argument) is the agentic orchestration primitive. It reads the current packet, doctor, and assembly report state from disk and tells you which stage should run next. Each call re-reads state, so the loop is idempotent and recoverable across sessions / machines.
|
|
1202
|
+
|
|
1203
|
+
Treat the loop as a sequence of Readiness Checkpoints, not as a required one-shot
|
|
1204
|
+
campaign build. A one-shot run is the best case where inputs are already complete
|
|
1205
|
+
and every checkpoint can advance in one session; the normal path may take several
|
|
1206
|
+
turns or sessions as source gaps, source TODOs, waivers, polish findings, deploy
|
|
1207
|
+
state, and QA blockers are discovered and resolved.
|
|
1208
|
+
|
|
1209
|
+
For source preparation, the Design Source Package should carry a generated
|
|
1210
|
+
readback summary that names included sources, coverage, gaps/TODOs, mappings,
|
|
1211
|
+
reference availability, and readiness. The readback helps humans and agents pick
|
|
1212
|
+
up the work later; structured package fields remain authoritative. Later stages
|
|
1213
|
+
should write their own stage readbacks or evidence summaries rather than
|
|
1214
|
+
rewriting the Design Source Readback. Those stage readbacks should be surfaced
|
|
1215
|
+
through a consolidated or just-in-time Campaign Readiness Readback so the
|
|
1216
|
+
operator is not expected to discover a patchwork of separate artifacts. Generate
|
|
1217
|
+
that readiness readback from the latest artifacts as the primary behavior; Run
|
|
1218
|
+
Records may snapshot it for audit. `campaigns-os next` should show the concise
|
|
1219
|
+
current-stage readback, while run or campaign status should show the fuller
|
|
1220
|
+
campaign-level readback. The readback should include readable prose plus stable
|
|
1221
|
+
buckets: `current_checkpoint`, `readiness_status`, `handled`, `blocked_by`,
|
|
1222
|
+
`known_gaps`, `proposed_exceptions`, `waivers`, `evidence_refs`, and
|
|
1223
|
+
`next_actions`. `evidence_refs` should point to source package sections,
|
|
1224
|
+
screenshots, Polish Evidence, Assembly Report stages, deploy URLs, QA verdicts,
|
|
1225
|
+
or other owning artifacts; the readback summarizes evidence but does not embed
|
|
1226
|
+
the detailed proof. UI surfaces may render screenshot thumbnails from refs, but
|
|
1227
|
+
CLI/readback data should keep screenshots as references with a short statement of
|
|
1228
|
+
what each proves.
|
|
1229
|
+
|
|
1230
|
+
Checkpoint status should stay boring and shared: `pending`, `blocked`, `ready`,
|
|
1231
|
+
`ready_with_gaps`, `ready_with_waivers`, `completed`,
|
|
1232
|
+
`completed_with_warnings`, or `skipped`. Put stage-specific detail in evidence,
|
|
1233
|
+
gaps, TODOs, waivers, findings, and next actions. `ready_with_waivers` requires
|
|
1234
|
+
structured waiver evidence: owner, reason, scope, applies-to references, created
|
|
1235
|
+
time, and either an expiry or review condition. Stages such as polish may draft
|
|
1236
|
+
or recommend waivers with evidence, but an operator/run decision approves them.
|
|
1237
|
+
Polish must classify every unresolved issue as `repair_needed`, `source_gap`,
|
|
1238
|
+
`source_divergence`, `waiver_recommended`, or `out_of_scope` so the next
|
|
1239
|
+
checkpoint knows whether to fix, carry, approve, or route it. Unresolved
|
|
1240
|
+
`repair_needed` issues block deploy and QA unless repaired, reclassified, or
|
|
1241
|
+
covered by an approved waiver. A `source_divergence` raised by polish is
|
|
1242
|
+
proposed until confirmed by an operator/run decision or the relevant Build or
|
|
1243
|
+
Design Source owner. A `source_gap` raised by polish is proposed too, unless it
|
|
1244
|
+
traces to an accepted Source Gap in the Design Source Package.
|
|
1245
|
+
|
|
1246
|
+
The motion:
|
|
1247
|
+
|
|
1248
|
+
```text
|
|
1249
|
+
agent calls `next` → gets { stage, prompt, picked_reason } → does the work →
|
|
1250
|
+
updates assembly report's stages.<name>.status → calls `next` again →
|
|
1251
|
+
repeat until stage="done"
|
|
1252
|
+
```
|
|
1253
|
+
|
|
1254
|
+
Stage order: `setup → build → polish → deploy → qa`. The picker walks this list and returns the first stage whose recorded status isn't terminal (`completed`, `completed_with_warnings`, `skipped`). During Polish, install the package-owned browser first, then run `campaigns-os polish capture` against the served current build before recording a terminal `stages.polish.status` or proceeding to deploy/QA; the producer attaches package-owned `visual_review.page_load` evidence and never marks the stage complete itself.
|
|
1255
|
+
|
|
1256
|
+
| Stage | Report key | Owner |
|
|
1257
|
+
|---|---|---|
|
|
1258
|
+
| setup | `stages.setup` | scaffold the page-kit campaign repo |
|
|
1259
|
+
| build | `stages.assembly` | assemble the campaign (next-campaigns-build) |
|
|
1260
|
+
| polish | `stages.polish` | source-design fidelity pass (next-campaigns-polish) |
|
|
1261
|
+
| deploy | `stages.deploy` | ship `_site/` to Netlify / CF Pages / Vercel / etc. (out-of-band), or serve it locally under `deploy.target: local-serve` |
|
|
1262
|
+
| qa | `stages.qa` | spec-aware QA (next-campaigns-qa) |
|
|
1263
|
+
|
|
1264
|
+
The CLI stage name is `build` but the report keys the same stage as `assembly` — the picker handles the translation. Both names refer to the same lifecycle step.
|
|
1265
|
+
|
|
1266
|
+
The Assembly Report's top-level `status`, `next` and `blockers` are derived from its `stages` on every write of the report (prepare-build's first write and every stage record after it), never carried forward from an earlier write. `status` is `blocked` while any recorded stage is blocked, `completed` only once every recorded stage (`prepare_build` and `doctor` included) is terminal, and `prepared` otherwise. `next.stage` is the first non-terminal stage in the order above, in the `next <stage>` vocabulary (`setup`, `build`, `polish`, `deploy`, `qa`, then `done`; a blocked prepare-build or doctor names `prepare-build` / `doctor-blocked`; a doctor that never recorded an outcome does not hold the ladder but is named `doctor` once the ladder is exhausted, so a `prepare-build --no-doctor` report never reads `completed`), `next.owner` is the skill that owns it, and `next.blocked` is present and true when that stage is the one holding the ladder. `blockers` is the union of the `blockers[]` of the stages currently blocked, so a blocker cleared by a re-run leaves the top level with its stage. The report's `next` is the ledger's own position; `campaigns-os next` additionally folds in live gates (doctor findings, purchase-proof coverage, the polish gate) and remains the authority for what runs next.
|
|
1267
|
+
|
|
1268
|
+
Result shape (with `--json`):
|
|
1269
|
+
|
|
1270
|
+
```jsonc
|
|
1271
|
+
{
|
|
1272
|
+
"ok": true,
|
|
1273
|
+
"status": "ready",
|
|
1274
|
+
"stage": "build",
|
|
1275
|
+
"picked_reason": "Stage \"assembly\" has status \"pending\"; run \"build\" next.",
|
|
1276
|
+
"prompt": "Use next-campaigns-build for this Campaigns OS handoff. ...",
|
|
1277
|
+
"errors": [],
|
|
1278
|
+
"warnings": [],
|
|
1279
|
+
"ready": [],
|
|
1280
|
+
"stage_blocked": false // present only when the recorded status is "blocked"
|
|
1281
|
+
}
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
Terminal states:
|
|
1285
|
+
|
|
1286
|
+
- **`stage: "doctor-blocked"`** — doctor returned errors. Resolve the blockers and re-run `campaigns-os doctor` to confirm before calling `next` again.
|
|
1287
|
+
- **`stage: "done"`** — every stage is in a terminal status. Pipeline is complete. To re-run a specific stage, set its status back to `"pending"` in the assembly report and call `next` again.
|
|
1288
|
+
- **`stage_blocked: true`** — the picker returned a stage whose recorded status is `blocked`. Don't run the prompt as-is; clear the blocker first.
|
|
1289
|
+
|
|
1290
|
+
The legacy form `campaigns-os next <stage>` (e.g. `next build`) still works and is the way to force a specific stage when you want to override the picker.
|
|
1291
|
+
|
|
1292
|
+
## Design Source-Aware Coverage Error
|
|
1293
|
+
|
|
1294
|
+
CampaignSpec pages may carry an optional `design_source` block on `Page` — a pointer to the design artifact (Figma file + per-breakpoint selection URLs) that supplies prepared HTML for that page. When doctor detects an active spec page with no source mapping, the `source_html.pages.coverage` error now carries a hint that points the operator at the design source:
|
|
1295
|
+
|
|
1296
|
+
- `design_source.type === "figma"` with `file_url`: doctor calls out the Figma file and the figma-sections-export handoff command (`npm run handoff -- <slug>`).
|
|
1297
|
+
- `design_source` set without `file_url`: doctor flags the missing `file_url` so the spec can be corrected.
|
|
1298
|
+
- `design_source` unset: doctor keeps the original generic coverage error.
|
|
1299
|
+
|
|
1300
|
+
The error code (`source_html.pages.coverage`) is unchanged so existing doctor consumers do not need to be updated; only the human-readable `message` and an optional `detail.design_source` payload are added.
|