@nextcommerce/campaigns-os 1.59.0 → 1.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +242 -0
- package/README.md +11 -10
- package/agents/claude/CLAUDE.md +15 -0
- package/agents/codex/AGENTS.md +15 -0
- package/agents/copilot/copilot-instructions.md +15 -0
- package/agents/cursor/campaigns-os.mdc +15 -0
- package/compatibility.json +1 -1
- package/contracts/brand-theme-target-tokens.next-core.v0.json +95 -25
- package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
- package/contracts/commerce-surface-catalog.json +27 -26
- package/contracts/release-ledger.json +1311 -0
- package/contracts/supported-surface.json +4 -4
- package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
- package/docs/access-model.md +20 -0
- package/docs/brand-theme-bridge.md +62 -3
- package/docs/build-packet.md +42 -13
- package/docs/campaign-build-brief.md +17 -1
- package/docs/campaigns-os-build-flow.md +62 -3
- package/docs/campaignspec-authoring-examples.md +116 -0
- package/docs/design-source-package.md +46 -2
- package/docs/entry-points.md +188 -0
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/polish-evidence.md +41 -3
- package/docs/qa-and-test-orders.md +50 -2
- package/docs/quickstart.md +438 -0
- package/docs/readback.md +119 -6
- package/docs/runtime-readiness.md +1 -1
- package/docs/sdk-storage-compatibility.md +3 -1
- package/docs/skills-revision.md +10 -10
- package/docs/source-adapters.md +213 -0
- package/package.json +10 -3
- package/schemas/campaign-design-source-package.v0.schema.json +8 -0
- package/schemas/campaigns-os-readback.v2.schema.json +177 -1
- package/schemas/source-html-manifest.v0.schema.json +3 -0
- package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +3 -3
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +5 -5
- package/skills/next-campaigns-os/SKILL.md +4 -3
- package/skills/next-campaigns-os-setup/SKILL.md +3 -3
- package/skills/next-campaigns-polish/SKILL.md +12 -16
- package/skills/next-campaigns-qa/SKILL.md +3 -3
- package/skills/next-campaigns-sdk-bump/SKILL.md +286 -0
- package/skills.json +18 -10
- package/src/brand-theme.mjs +413 -8
- package/src/brief-summary.mjs +6 -2
- package/src/browser-launch.mjs +2 -1
- package/src/build-brief.mjs +9 -2
- package/src/built-site-scope.mjs +7 -14
- package/src/campaign-identity.mjs +6 -4
- package/src/cli.mjs +89 -73
- package/src/design-file-reference.mjs +93 -0
- package/src/design-source-package.mjs +55 -1
- package/src/doctor/checks.mjs +14 -77
- package/src/doctor/ci.mjs +112 -0
- package/src/doctor/inspect.mjs +20 -8
- package/src/doctor/next-step.mjs +6 -3
- package/src/doctor/source-provenance.mjs +74 -0
- package/src/evidence-debt.mjs +119 -0
- package/src/orchestration-stage-contract.mjs +6 -3
- package/src/page-kit-build-summary.mjs +21 -1
- package/src/page-kit-campaign-config.mjs +19 -0
- package/src/parity/compare.mjs +316 -0
- package/src/parity/design-access.mjs +54 -0
- package/src/parity/element-parity-record.mjs +569 -0
- package/src/parity/probe.mjs +205 -0
- package/src/parity/snapshot.mjs +110 -0
- package/src/png-header.mjs +87 -0
- package/src/polish-browser.mjs +186 -9
- package/src/polish-captures-record.mjs +52 -0
- package/src/polish-deadline.mjs +14 -0
- package/src/polish-gate.mjs +26 -11
- package/src/polish-node.mjs +81 -7
- package/src/polish-visual-fixtures.mjs +74 -0
- package/src/polish-visual.mjs +606 -0
- package/src/progress-node.mjs +2 -1
- package/src/qa-analytics-correctness.mjs +14 -2
- package/src/qa-analytics-parity.mjs +94 -5
- package/src/qa-node.mjs +39 -6
- package/src/qc-accept.mjs +148 -6
- package/src/qc-check-registry.mjs +37 -1
- package/src/qc-results.mjs +404 -16
- package/src/readback.mjs +57 -13
- package/src/sdk-attribute-index.mjs +11 -11
- package/src/sdk-markup.mjs +77 -2
- package/src/sdk-storage-compatibility.mjs +94 -4
- package/src/source-html-manifest.mjs +7 -0
- package/src/source-html-server.mjs +129 -0
- package/src/spec-derive.mjs +24 -9
- package/src/stage-ledger.mjs +27 -1
- package/src/stage-record.mjs +63 -9
- package/src/theme-gate.mjs +2 -2
- package/src/visual-diff.mjs +296 -0
- package/src/visual-reference.mjs +126 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "The downstream contract manifest. Everything listed here is SUPPORTED SURFACE: consumers (campaigns-agent, campaign-builder, the private ops repo, page-kit campaign repos) may depend on it, and changing it is a deliberate act — hashed entries require a surface_version bump in the same change (check-supported-surface.mjs --base, mirroring the skills.json bump gate), named entries must keep existing at their path, cli_commands must keep resolving in the CLI dispatch, package_exports must stay exported, and every entry must ship in the npm pack (files[] coverage). Anything NOT listed here — src/** internals, scripts/** checkers, examples/**, prompts/**, contracts/** other than this file and the entries named[] below (the orientation contract, the release ledger, and the consumer-facing orientation fixtures) — is implementation: consumers may read it for context but must not build on it, and it can change without notice. Rationale and the compatibility promise: docs/supported-surface.md.",
|
|
3
|
-
"surface_version": "1.
|
|
3
|
+
"surface_version": "1.61.0",
|
|
4
4
|
"package_exports": [
|
|
5
5
|
"./commercial-journey",
|
|
6
6
|
"./commercial-parity",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"sha256": "d524003184131398dc15fdb84385af1be0de0c69d6c5ede7278689a5b6101b81"
|
|
59
59
|
},
|
|
60
60
|
"schemas/campaign-design-source-package.v0.schema.json": {
|
|
61
|
-
"sha256": "
|
|
61
|
+
"sha256": "ecb8fd534977826541aa7110dad3483b7b9215dd60ed30b6370083848b957f13"
|
|
62
62
|
},
|
|
63
63
|
"schemas/campaign-runtime-assembly-report.v0.schema.json": {
|
|
64
64
|
"sha256": "625a6f2ed6bee726f9cddad750e64a54a7e03744c777f5a70afe4a39afdd9848"
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"sha256": "41ff370de4395149781d7e8c4ad6d7d4e2f896eb2030944399d36f1237726a20"
|
|
113
113
|
},
|
|
114
114
|
"schemas/source-html-manifest.v0.schema.json": {
|
|
115
|
-
"sha256": "
|
|
115
|
+
"sha256": "58bc98b3fd07c87c6ba92d29b26941ca97200ea433d73f7418966f679841df49"
|
|
116
116
|
},
|
|
117
117
|
"schemas/campaigns-os-progress-snapshot.v0.schema.json": {
|
|
118
118
|
"sha256": "ef31b4f20e39dfc1614dd4b45e4cf1692a51760b008cb51d133659307bd2a954"
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"sha256": "2e440be6513fe06b351c3a72c35424a04f807792621cb01e89803441fd925e06"
|
|
122
122
|
},
|
|
123
123
|
"schemas/campaigns-os-readback.v2.schema.json": {
|
|
124
|
-
"sha256": "
|
|
124
|
+
"sha256": "443676656a7d0db4c8c45301dad5c925cd1b03d4d0e3dccdf0132d9a4e6005f2"
|
|
125
125
|
},
|
|
126
126
|
"contracts/effects.v1.json": {
|
|
127
127
|
"sha256": "9c1d69e44eab7403ca3ff80f850214d89b0a02fdc51544beef37dbd0a653eeaa"
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"asset_pin": {
|
|
57
57
|
"repo": "NextCommerceCo/campaign-cart-starter-templates",
|
|
58
|
-
"sha": "
|
|
58
|
+
"sha": "b38343e85a05ec2fe949e13e783057d4c717a8d6",
|
|
59
59
|
"path": "src/<family>/assets/<asset>",
|
|
60
60
|
"note": "asset_sha256 is the sha256 of each shipped starter asset at this commit (the commerce-surface-catalog _synced_from_sha pin; the bytes are identical across every family). A served asset whose bytes hash to the shipped value is the untouched starter asset and counts as residue even though its markup names no method; different bytes mean it was edited in place. check-template-doctrine verifies these hashes against the pinned checkout."
|
|
61
61
|
},
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Access Model
|
|
2
|
+
|
|
3
|
+
Campaign developers should not need access to the private Campaigns OS runtime development repo.
|
|
4
|
+
|
|
5
|
+
This repo is the developer-facing surface:
|
|
6
|
+
|
|
7
|
+
- docs
|
|
8
|
+
- schemas
|
|
9
|
+
- examples
|
|
10
|
+
- starter-template contract snapshots
|
|
11
|
+
- agent skills
|
|
12
|
+
- Build Packet doctor
|
|
13
|
+
- checkpoint wrappers
|
|
14
|
+
- issue templates
|
|
15
|
+
|
|
16
|
+
Private platform/runtime development remains outside this repo. Do not copy private repo paths, client-specific examples, secrets, dashboard internals, or unpublished platform mutation procedures here.
|
|
17
|
+
|
|
18
|
+
## Distribution
|
|
19
|
+
|
|
20
|
+
This repo should stay suitable for public or partner access. Private runtime details, private repo paths, secrets, and client-specific implementation notes belong outside this surface.
|
|
@@ -37,12 +37,66 @@ the accent fallback and the warning asks the operator to confirm it.
|
|
|
37
37
|
```text
|
|
38
38
|
.campaign-runtime/theme/theme-report.json
|
|
39
39
|
.campaign-runtime/theme/brand-theme.css
|
|
40
|
+
.campaign-runtime/theme/brand-fonts.css (only when the source ships brand faces)
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
The generated CSS is v0 root-variable-only. It can override next-core custom
|
|
43
44
|
properties such as `--brand--color--primary` and
|
|
44
45
|
`--brand--color--cta-primary`, but it must not emit selectors, `data-next`
|
|
45
46
|
selectors, payment/package/cart selectors, JavaScript, or remote URL fetches.
|
|
47
|
+
The one exception is a `:root, body` block for the font-family tokens (see
|
|
48
|
+
Typography below).
|
|
49
|
+
|
|
50
|
+
### Typography
|
|
51
|
+
|
|
52
|
+
Commerce pages keep the design's type, not the template's system stack.
|
|
53
|
+
Inspection reads every CSS file the source pages link:
|
|
54
|
+
|
|
55
|
+
- the `@font-face` rules the source ships;
|
|
56
|
+
- which family its headings use (`h1`–`h3`, then `heading`/`title` classes);
|
|
57
|
+
- which family its body copy uses (`html`, `body`, `p`, or the class a
|
|
58
|
+
stylesheet scopes itself under, such as the importer's `.cc-import`);
|
|
59
|
+
- which family its buttons use, and the button `border-radius`.
|
|
60
|
+
|
|
61
|
+
`var()` references resolve through custom properties on `:root`, `html`, `body`
|
|
62
|
+
and that scope class, where Shopify themes keep `--font-body-family` and
|
|
63
|
+
`--buttons-radius`. A family is mapped only when the source ships a face for
|
|
64
|
+
it: a family the page can't load would only change the fallback, so it is
|
|
65
|
+
left out, reported as a `theme.typography.face_unavailable` warning, and
|
|
66
|
+
listed in the theme report's `typography.unavailable`.
|
|
67
|
+
|
|
68
|
+
| Source | next-core token |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| heading family, then the generic the source gave it | `--_text---font-family--heading` |
|
|
71
|
+
| body family (else the button family), then its generic | `--_text---font-family--primary` |
|
|
72
|
+
| button radius (`0`, or one to four lengths or percentages) | `--radius-size--buttons` |
|
|
73
|
+
|
|
74
|
+
A button radius in another form (`10px / 20px`, an unresolved `var()`) is not
|
|
75
|
+
mapped and is listed in `typography.radius_rejected`.
|
|
76
|
+
|
|
77
|
+
The targets are in the `typography` block of
|
|
78
|
+
`contracts/brand-theme-target-tokens.next-core.v0.json`. next-core re-declares
|
|
79
|
+
both font-family tokens on `body` inside its 991px, 767px and 479px media
|
|
80
|
+
queries, so a `:root` value alone holds only on desktop. The generator emits
|
|
81
|
+
them in a `:root, body` block. `brand-theme.css` loads after next-core, so its
|
|
82
|
+
`body` declaration wins at every width.
|
|
83
|
+
|
|
84
|
+
`brand-fonts.css` holds only `@font-face` rules for the mapped families. It
|
|
85
|
+
always ships `font-display: swap`, whatever the source declared. A face the
|
|
86
|
+
source declares once per format keeps every file, in source order. Its URLs are relative to `assets/css/`, pointing at the
|
|
87
|
+
source's own font files, which build copies into the campaign's `assets/`
|
|
88
|
+
tree with the rest of the source assets. A face whose file is remote, a
|
|
89
|
+
`data:` URL, root-relative, outside the source root or missing is left out with
|
|
90
|
+
`theme.typography.face_src_dropped`; containment is checked on real paths, so
|
|
91
|
+
a symlink out of the source root is dropped. Family names are stripped of
|
|
92
|
+
characters that could not appear in a quoted family. Button radius is mapped only alongside a
|
|
93
|
+
mapped family, so a source with no brand faces generates exactly what it did
|
|
94
|
+
before.
|
|
95
|
+
|
|
96
|
+
Typography covers family and button radius only, because those are the only
|
|
97
|
+
type and shape tokens next-core exposes. Heading weight, letter-spacing and
|
|
98
|
+
text-transform stay at next-core's values: next-core renders headings at 700,
|
|
99
|
+
so a design that sets headings at 600 shows its bold face on commerce pages.
|
|
46
100
|
|
|
47
101
|
### Foreground tokens are derived from background luminance
|
|
48
102
|
|
|
@@ -205,8 +259,10 @@ downsell, or receipt pages.
|
|
|
205
259
|
|
|
206
260
|
If a fresh `brand-theme.css` exists:
|
|
207
261
|
|
|
208
|
-
1. Copy it into the campaign asset tree
|
|
209
|
-
|
|
262
|
+
1. Copy it into the campaign asset tree (`assets/css/`), with
|
|
263
|
+
`brand-fonts.css` beside it when theme generate wrote one.
|
|
264
|
+
2. Add it to commerce page frontmatter styles after `next-core.css`. List
|
|
265
|
+
`brand-fonts.css` on the same pages; its position does not matter.
|
|
210
266
|
3. Preserve SDK-owned runtime surfaces: `data-next-*`, package selectors,
|
|
211
267
|
payment fields, totals, submit controls, receipt templates, route meta tags,
|
|
212
268
|
and SDK JavaScript.
|
|
@@ -215,7 +271,10 @@ If a fresh `brand-theme.css` exists:
|
|
|
215
271
|
page's stylesheet links and records `report.theme.status`, `css_path`,
|
|
216
272
|
`commerce_pages`, `load_order` and evidence. It refuses, writing nothing,
|
|
217
273
|
when a page that loads `next-core.css` does not load the brand layer after
|
|
218
|
-
it,
|
|
274
|
+
it, when theme generate wrote `brand-fonts.css` (at the Build Context's
|
|
275
|
+
`theme.generated.fonts_css_path`) and a styled page does not load an
|
|
276
|
+
identical copy from inside the built site, or when no built commerce page
|
|
277
|
+
loads `next-core.css`.
|
|
219
278
|
|
|
220
279
|
### Where next-core.css belongs
|
|
221
280
|
|
package/docs/build-packet.md
CHANGED
|
@@ -788,9 +788,13 @@ One error per finding; each names the two files and the two values, so the
|
|
|
788
788
|
repair is a one-line edit. Every built HTML file under `_site/<slug>/` is
|
|
789
789
|
scanned, because every one is served. When doctor has the CampaignSpec, a
|
|
790
790
|
finding that names a file no active spec page builds to says so and lists it
|
|
791
|
-
under `stray_files`. Such a file is leftover output
|
|
792
|
-
|
|
793
|
-
`index.html` under `assets/`.
|
|
791
|
+
under `stray_files`. Such a file is leftover output or an HTML file copied
|
|
792
|
+
into the source. On page-kit 0.2.x both happen: it does not prune `_site/`,
|
|
793
|
+
and it builds and copies a design export's `index.html` under `assets/`. From
|
|
794
|
+
0.3.0 page-kit removes `_site/<slug>/` before each build and neither builds
|
|
795
|
+
nor copies `.html` under `assets/` (the build summary lists each one as
|
|
796
|
+
`ASSET_IGNORED`), so a stray there is a source page no spec page claims or
|
|
797
|
+
output written outside a build. The repair is to remove the source file if
|
|
794
798
|
there is one, delete the built file, and rebuild and record the build again,
|
|
795
799
|
not to retag it. Pages whose route contains a `-backup-` or `-old-`
|
|
796
800
|
segment are parked copies: skipped and listed on the gate as `pages_skipped`,
|
|
@@ -828,7 +832,7 @@ Blockers (not waivable — the markup provably does not do what it says):
|
|
|
828
832
|
- `CHECKOUT_NOT_FORM` — `data-next-checkout` on an element that is not `<form>`.
|
|
829
833
|
- `WRONG_FIELD_NAME` — `data-next-checkout-field` with a value the SDK does not
|
|
830
834
|
map. The set is vendored from the SDK at a named tag
|
|
831
|
-
(`src/sdk-attribute-index.mjs`, currently v0.4.
|
|
835
|
+
(`src/sdk-attribute-index.mjs`, currently v0.4.43: `email`, `fname`, `lname`,
|
|
832
836
|
`phone`, `address1`, `address2`, `city`, `province`, `postal`, `country`,
|
|
833
837
|
`payment-method`, `accepts_marketing`, `cc-number`, `cc-month`, `cc-year`,
|
|
834
838
|
`exp-month`, `exp-year`, `cvv`, the legacy `card-*` spellings, and any
|
|
@@ -863,6 +867,23 @@ Warnings (advisory):
|
|
|
863
867
|
`data-next-package-toggle`), or one a `*-template-id` attribute points at.
|
|
864
868
|
SDK tokens are single-brace; a template nothing in the SDK reads, including
|
|
865
869
|
a vendor template nested deeper inside SDK chrome, may use any syntax.
|
|
870
|
+
- `PAGE_TYPE_ATTRIBUTE` — `data-next-page-type` on a page with no
|
|
871
|
+
`<meta name="next-page-type">`. The SDK never reads the attribute, so the
|
|
872
|
+
page runs as the default `product` step; on an upsell page no upsell page
|
|
873
|
+
view is recorded. `window.nextConfig.pageType` does not silence it: it
|
|
874
|
+
reaches the SDK's config store but not the upsell page view. The message
|
|
875
|
+
names the meta value to add: `receipt` on a receipt, `upsell` on a page with
|
|
876
|
+
a `data-next-upsell` offer, otherwise the attribute's SDK page type
|
|
877
|
+
(`product`, `cart`, `checkout`, `upsell`, `receipt`; `downsell` reads as
|
|
878
|
+
`upsell`, `thank-you`, `thankyou` and `confirmation` as `receipt`, and
|
|
879
|
+
`presell`, `landing` and `advertorial` as `product`). Any other value is not
|
|
880
|
+
guessed at: the message lists the five SDK page types.
|
|
881
|
+
- `UPSELL_PAGE_TYPE_MISSING` — a `data-next-upsell` offer on a page whose
|
|
882
|
+
`next-page-type` meta is missing or is not `upsell` (the SDK has no
|
|
883
|
+
`downsell` page type). The SDK records the upsell page view only from
|
|
884
|
+
`<meta name="next-page-type" content="upsell">`, so reports show the
|
|
885
|
+
purchase with no upsell view before it. Not raised on a receipt, or when
|
|
886
|
+
`PAGE_TYPE_ATTRIBUTE` already names the page.
|
|
866
887
|
|
|
867
888
|
Information: `data-next-*` names the vendored attribute index does not list are
|
|
868
889
|
collected on the gate (`unknown_attributes[]`) and printed as one advisory ready
|
|
@@ -1025,6 +1046,16 @@ Commit durable packet/context/report artifacts when they represent a real build
|
|
|
1025
1046
|
|
|
1026
1047
|
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.
|
|
1027
1048
|
|
|
1049
|
+
Add `--ci` for the [standing PR evidence check](campaigns-os-build-flow.md#live-campaign-changes):
|
|
1050
|
+
`campaigns-os doctor --packet <packet> --ci --no-live-refs`. CI mode assesses the
|
|
1051
|
+
same build-bound Polish/QA debt as readback, exits 0 for current evidence, 3 for
|
|
1052
|
+
owed evidence, 4 for unknown debt, and 2 for a missing/unreadable packet. Any owed
|
|
1053
|
+
stage returns 3; otherwise an unknown stage returns 4. Other doctor errors remain reported but do not
|
|
1054
|
+
change the CI exit. It prints Markdown for a PR comment, or adds `ci.status`,
|
|
1055
|
+
`ci.exit_code`, `ci.polish`, `ci.qa` and `ci.changes` under `--json`.
|
|
1056
|
+
`--ci` adds no writes or network reads; pass `--no-live-refs` in CI to disable
|
|
1057
|
+
doctor's existing live campaign read. Without `--ci`, inspection behavior is unchanged.
|
|
1058
|
+
|
|
1028
1059
|
`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.
|
|
1029
1060
|
|
|
1030
1061
|
`campaigns-os start` / `campaigns-os prepare-build` writes portable packet, context, report, and generated doctor-output paths. Resolve each path from its reader's base:
|
|
@@ -1754,7 +1785,7 @@ own stages) → calls `next` again → repeat until stage="done"
|
|
|
1754
1785
|
|
|
1755
1786
|
### Refreshing after a CampaignSpec change
|
|
1756
1787
|
|
|
1757
|
-
When the CampaignSpec's material content changes, every build, Polish and QA record made against the earlier content stops counting as current, in local-spec, saved-Map and gateway packets alike. For a saved Map or gateway packet, Campaigns OS compares the copy intake fetched; it does not check whether that copy is the latest remote revision, and reports the remote as unconfirmed. Doctor warns `spec.material_stale`, and `next`, progress, readback, the Assembly Report's own status and the QA gate show the stages that are owed again; `next` routes to the first one. Run `campaigns-os record spec --packet <packet>` to bind the new content: it updates the build context and report identity, re-reads the brief file last saved (by intake or `record brief`), keeps waivers, warning accepts, the applied theme and deploy settings, and moves each superseded stage record into that stage's `history`, which is never used as current proof. `spec derive` no longer rebinds identity; run `record spec` after it. A change to the set, order or routes of active pages still needs intake. Re-recording an unchanged build after a spec change keeps the build owed, because the change has not reached the pages, unless the operator records a reason with `--deviation-reason`; this holds even when `record spec` was not run, and that `record build` also marks Polish and QA owed again. Polish stays owed until a new `polish capture`, and QA must produce a new verdict against the current content. Polish measurements of byte-identical output keep their values, but the Polish stage is owed again. These checks are tamper evidence, not proof: a hand-written record that copies the current values, or a packet pointed at an older copy of the spec, is not detected.
|
|
1788
|
+
When the CampaignSpec's material content changes, every build, Polish and QA record made against the earlier content stops counting as current, in local-spec, saved-Map and gateway packets alike. For a saved Map or gateway packet, Campaigns OS compares the copy intake fetched; it does not check whether that copy is the latest remote revision, and reports the remote as unconfirmed. Doctor warns `spec.material_stale`, and `next`, progress, readback, the Assembly Report's own status and the QA gate show the stages that are owed again; `next` routes to the first one. Run `campaigns-os record spec --packet <packet>` to bind the new content: it updates the build context and report identity, re-reads the brief file last saved (by intake or `record brief`), keeps waivers, warning accepts, the applied theme and deploy settings, and moves each superseded stage record into that stage's `history`, which is never used as current proof. `spec derive` no longer rebinds identity; run `record spec` after it. A change to the set, order or routes of active pages still needs intake. Re-recording an unchanged build after a spec change keeps the build owed, because the change has not reached the pages, unless the operator records a reason with `--deviation-reason`; this holds even when `record spec` was not run, and that `record build` also marks Polish and QA owed again. Polish stays owed until a new `polish capture`, with one exception: when `record build --deviation-reason` records the very output a completed Polish reviewed (same build fingerprint and source package) and only the CampaignSpec changed, not the brief's presentation, that `record build` restores the Polish record the change moved to history, stamped with the current inputs. It is the same review of the same pages. QA must produce a new verdict against the current content. Polish measurements of byte-identical output keep their values, but the Polish stage is owed again. These checks are tamper evidence, not proof: a hand-written record that copies the current values, or a packet pointed at an older copy of the spec, is not detected.
|
|
1758
1789
|
|
|
1759
1790
|
The CampaignSpec's material content is the whole spec except `spec_identity`,
|
|
1760
1791
|
`slug`, `map_id` and `saved_at`; a page `label` edit is material. A spec
|
|
@@ -1813,13 +1844,11 @@ On a hosted target, record an unset hosted preview URL with
|
|
|
1813
1844
|
`campaigns-os qa policy set --packet <p> --preview-url <url>`, then record the
|
|
1814
1845
|
deploy with `campaigns-os record deploy --packet <p> --base-url <preview url>`. The hosted preview must be a non-loopback
|
|
1815
1846
|
HTTP(S) URL whose origin or QA-resolved campaign route path differs from `deploy.production_url`;
|
|
1816
|
-
query and fragment text does not make the production page a preview. With
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
`campaigns-os next --packet <p>` reaches closeout without a hosted deploy
|
|
1822
|
-
record. QA against the packet's production URL retains the strict gate.
|
|
1847
|
+
query and fragment text does not make the production page a preview. With the
|
|
1848
|
+
deploy recorded, run `campaigns-os qa run --packet <p> --base-url <preview-url>
|
|
1849
|
+
--browser --test-order common`; after QA records a verdict for the current
|
|
1850
|
+
build, `campaigns-os next --packet <p>` reaches closeout. QA against the
|
|
1851
|
+
packet's production URL retains the strict gate.
|
|
1823
1852
|
|
|
1824
1853
|
Each command reads the same packet, Build Context and Assembly Report `next`
|
|
1825
1854
|
reads (`--context` / `--report` override them the same way), validates what it
|
|
@@ -1882,7 +1911,7 @@ Result shape (with `--json`):
|
|
|
1882
1911
|
Terminal states:
|
|
1883
1912
|
|
|
1884
1913
|
- **`stage: "doctor-blocked"`** — doctor returned errors. Resolve the blockers and re-run `campaigns-os doctor` to confirm before calling `next` again.
|
|
1885
|
-
- **`stage: "done"`** — every stage is terminal. Pipeline is complete. To repeat build work, do the work and use `record build`; it makes downstream stages owed as needed. Use `record setup` or `record polish` after repeating those stages
|
|
1914
|
+
- **`stage: "done"`** — every stage is terminal. Pipeline is complete. To repeat build work, do the work and use `record build`; it makes downstream stages owed as needed. Use `record setup` or `record polish` after repeating those stages. `record deploy` records the served deploy, a loopback URL for a local-serve target or an https URL for a hosted one. Use `qa run` for QA. Then call `next` again.
|
|
1886
1915
|
- **`stage_blocked: true`** — the picker returned a stage whose recorded status is `blocked`. Don't run the prompt as-is; clear the blocker first.
|
|
1887
1916
|
|
|
1888
1917
|
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.
|
|
@@ -4,6 +4,22 @@ The Campaign Build Brief is the merchandising and design-presentation truth for
|
|
|
4
4
|
|
|
5
5
|
CampaignSpec remains the operational truth: packages, offers, shipping, routing, SDK hints, payment/runtime values, and API identity. The Build Brief answers business-owned presentation questions that agents should not infer silently: page authority, palette and CTA treatment, product media rules, pricing display, promo language, payment/trust surfaces, canonical names, residue policy, and QA expectations.
|
|
6
6
|
|
|
7
|
+
## Design Authority
|
|
8
|
+
|
|
9
|
+
For a page built from an HTML export of a named design file, the drafted brief
|
|
10
|
+
records `design_authority.<page>.source: design_file`, with `reference` set to
|
|
11
|
+
the design file link, `frames` set to the declared frame links, and
|
|
12
|
+
`derived_artifact` naming the export (`provided_design_export` and its HTML
|
|
13
|
+
path). The summary reads "follow the design file, built from its HTML export".
|
|
14
|
+
A Figma spec `design_source` supplies this lineage only with
|
|
15
|
+
figma-sections-export provenance that passes doctor's
|
|
16
|
+
`source_html.producer_provenance` check without a waiver; a manifest
|
|
17
|
+
`design_file` is its own declaration.
|
|
18
|
+
A value you state in the brief file is kept as you wrote it. This records
|
|
19
|
+
authority; it does not read the design file or compare elements. Doctor's
|
|
20
|
+
`design.access` reports that access as unexercised, with guidance to continue or
|
|
21
|
+
compare the export and file by hand.
|
|
22
|
+
|
|
7
23
|
## Artifact Locations
|
|
8
24
|
|
|
9
25
|
Campaigns OS accepts YAML or JSON:
|
|
@@ -64,7 +80,7 @@ A brief is prepared, where every open question blocks, only when the file sets `
|
|
|
64
80
|
|
|
65
81
|
Saving a brief whose content is unchanged keeps all stage evidence and statuses. Reformatting, reordering keys, writing a default value explicitly, or changing only `_meta` also keeps all stage evidence and statuses; the save may rewrite the brief files. When the presentation sections change (`campaign_intent`, `design_authority`, `brand`, `media`, `offer_presentation`, `promo_urgency`, `commerce_surfaces`, `canonical_display`, `template_residue_policy`, or any section you add), build, Polish and QA are owed again. When only `qa_policy` changes, QA alone is owed again. Setup and deploy records are kept. Each affected stage records when its inputs changed. Each replaced record is kept under the stage's `history`; the last five superseded records per stage are kept, and older ones are dropped. History is display only and is never used as current proof.
|
|
66
82
|
|
|
67
|
-
Recording build again over output that has not changed since the brief or CampaignSpec changed does not make the build current: `next` keeps asking for a build, because the change has not reached the pages. If the operator decides the change needs no change to the built pages, record build with `--deviation-reason "<the operator's reason>"`; the build is then recorded with a warning, and the reason is shown in `next` and the QC handoff. `--deviation-reason` is a general flag any caller can pass: Campaigns OS records the reason, but cannot tell whether the operator chose it. A CampaignSpec change that was never saved with `record spec` is recorded by the next `record build`, `record polish` or QA run, with the same effect; `record build` then also marks Polish and QA owed again. An edit to the brief file is not read until it is saved with `record brief`: until then the build reads the saved brief, and doctor warns `build_brief.input_unsaved`. The same applies after `prepare-build --force`, which now keeps stage history, archives the build, Polish and QA records it clears, and keeps the record that the inputs changed. Recording Polish again does not make it current until a new `polish capture` runs after the change. Recording Polish as skipped, with a skip reason, while it is owed is the operator's decision to skip it: the input change no longer makes Polish owed. The Polish gate still passes only a completed Polish, so `next` stays at Polish until one is recorded (see `docs/polish-evidence.md`).
|
|
83
|
+
Recording build again over output that has not changed since the brief or CampaignSpec changed does not make the build current: `next` keeps asking for a build, because the change has not reached the pages. If the operator decides the change needs no change to the built pages, record build with `--deviation-reason "<the operator's reason>"`; the build is then recorded with a warning, and the reason is shown in `next` and the QC handoff. `--deviation-reason` is a general flag any caller can pass: Campaigns OS records the reason, but cannot tell whether the operator chose it. A CampaignSpec change that was never saved with `record spec` is recorded by the next `record build`, `record polish` or QA run, with the same effect; `record build` then also marks Polish and QA owed again. An edit to the brief file is not read until it is saved with `record brief`: until then the build reads the saved brief, and doctor warns `build_brief.input_unsaved`. The same applies after `prepare-build --force`, which now keeps stage history, archives the build, Polish and QA records it clears, and keeps the record that the inputs changed. Recording Polish again does not make it current until a new `polish capture` runs after the change. The exception is a CampaignSpec-only change recorded with `record build --deviation-reason` over the exact output a completed Polish reviewed: that `record build` restores the Polish record, since it reviewed the same pages. A brief presentation change still owes a new Polish. Recording Polish as skipped, with a skip reason, while it is owed is the operator's decision to skip it: the input change no longer makes Polish owed. The Polish gate still passes only a completed Polish, so `next` stays at Polish until one is recorded (see `docs/polish-evidence.md`).
|
|
68
84
|
|
|
69
85
|
A build, Polish or QA record that does not say which brief and CampaignSpec content it was made against counts as unconfirmed, not current. Records made by earlier releases are unconfirmed, so after upgrading, `next` routes back to build once; record build and Polish again and run QA again. These checks are tamper evidence, not proof: a record written by hand that copies the current values or removes the record of an input change, or a packet pointed at an older copy of the spec or brief, is not detected.
|
|
70
86
|
|
|
@@ -17,6 +17,65 @@ The happy path is intentionally tight:
|
|
|
17
17
|
|
|
18
18
|
Pause only for missing inputs, doctor blockers, blocked deploys, out-of-scope runtime pages that block checkout proof, or merchant-specific uncertainty. The default path should not branch into external browser skills or hand-built backend order creation.
|
|
19
19
|
|
|
20
|
+
## Live-campaign changes
|
|
21
|
+
|
|
22
|
+
Keep a standing pull-request check after launch: install locked dependencies
|
|
23
|
+
with `npm ci`, build the campaign site with
|
|
24
|
+
`CPK_ENV=development npx --no-install campaign-build` (the check does not read the build summary),
|
|
25
|
+
then run `npx --no-install campaigns-os doctor --packet <packet> --ci --no-live-refs`.
|
|
26
|
+
Run these from the Page Kit folder containing the pinned tools and committed
|
|
27
|
+
packet, local CampaignSpec, Build Context and Assembly Report. The site build
|
|
28
|
+
must produce `_site/<public_route_slug>/`; `campaigns-os build` prepares inputs
|
|
29
|
+
and does not build the site. Do not record a new build merely to clear the check.
|
|
30
|
+
Match the Assembly Report's build environment: the documented local-proof flow
|
|
31
|
+
uses `development`; set `CPK_ENV=production` when the recorded evidence reviewed
|
|
32
|
+
a production render instead. Environment differences also change fingerprints.
|
|
33
|
+
|
|
34
|
+
| CI status | Exit code | Meaning |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `current` | `0` | Polish and QA both bind to the build on disk and neither is lifecycle-required. |
|
|
37
|
+
| `owed` | `3` | Polish or QA binds to another build or is lifecycle-required; evidence is owed. |
|
|
38
|
+
| `unknown` | `4` | Output or assembly fingerprints are missing, a stage fingerprint is missing without a lifecycle requirement, output is unreadable, or inspection throws; debt cannot be determined. |
|
|
39
|
+
| `usage` | `2` | The packet is missing, unreadable or invalid JSON. |
|
|
40
|
+
|
|
41
|
+
Any owed stage returns 3; otherwise an unknown stage returns 4. Normal
|
|
42
|
+
doctor errors remain visible but do not determine the CI exit code. Without
|
|
43
|
+
`--ci`, doctor retains its usual inspection output and 0/2 result. `--json`
|
|
44
|
+
adds a `ci` object containing `status`, `exit_code`, `polish`, `qa`, and
|
|
45
|
+
`changes` when assessment completes. Without `--json`, every CI outcome prints
|
|
46
|
+
an escaped Markdown report, including a "could not evaluate" report for packet
|
|
47
|
+
usage errors (exit 2) or inspection failures (exit 4). Page changes are compared
|
|
48
|
+
with the retained assembly manifest, so the page list may be empty if that
|
|
49
|
+
manifest was already updated; old Polish/QA bindings still show debt.
|
|
50
|
+
Without a usable manifest, page changes are unknown, but matching stage
|
|
51
|
+
bindings remain current unless lifecycle-required, and differing bindings remain owed.
|
|
52
|
+
The check reads recorded fingerprints, not machine-local capture files.
|
|
53
|
+
Readback's `evidence_debt` uses the same assessment, showing an agent this debt
|
|
54
|
+
before it edits; its separate time-based staleness assessment is not the CI signal.
|
|
55
|
+
|
|
56
|
+
Copy the [workflow example](https://github.com/NextCommerceCo/campaigns-os/blob/main/docs/examples/campaign-repo-evidence-check.yml) into the
|
|
57
|
+
campaign repository and adjust the packet path. It posts or updates one PR
|
|
58
|
+
comment using the fixed `<!-- campaign-evidence-check -->` marker. Evidence
|
|
59
|
+
reports use stdout only; stderr stays in the job log. If the command produces
|
|
60
|
+
no stdout, the comment points to the job log with a fixed fallback message.
|
|
61
|
+
If install or build fails before doctor runs, the comment is updated with
|
|
62
|
+
"Campaign build failed; evidence not evaluated. See the job log."
|
|
63
|
+
Evidence findings are **report-only by default**. Opt in to failing on a nonzero doctor
|
|
64
|
+
exit by setting the repository Actions variable
|
|
65
|
+
`CAMPAIGNS_OS_EVIDENCE_CHECK` to `fail`. Install/build failures still fail normally.
|
|
66
|
+
Fork pull requests may lack comment-write permission; comment failures are
|
|
67
|
+
nonfatal and the report remains in the job log. The example uses `pull_request`
|
|
68
|
+
and does not run untrusted code with `pull_request_target` privileges.
|
|
69
|
+
|
|
70
|
+
To repay fingerprint-drift debt, rebuild and record the output, re-run `polish capture` against
|
|
71
|
+
that served build, record Polish with `record polish --evidence <evidence.json>`,
|
|
72
|
+
then run `qa run` to record QA for the current build. For lifecycle-required
|
|
73
|
+
stages with matching build bindings, re-run the required stages against the
|
|
74
|
+
existing build: `polish capture` and `record polish` when Polish is owed, and
|
|
75
|
+
`qa run` when QA is owed. The CI report names each owed stage's cause and
|
|
76
|
+
selects the remedy accordingly. The workflow only inspects;
|
|
77
|
+
it does not renew evidence or deploy.
|
|
78
|
+
|
|
20
79
|
## Partial Builds
|
|
21
80
|
|
|
22
81
|
Partial builds are valid campaign work. A pass may build only new presell pages,
|
|
@@ -41,7 +100,7 @@ URL.
|
|
|
41
100
|
- Starter template contracts own the SDK attribute contract and protected runtime surfaces: checkout/cart/upsell/receipt/payment/address/totals/submit controls and their required data attributes.
|
|
42
101
|
- Designed source owns visual composition, content hierarchy, imagery, and page-level copy.
|
|
43
102
|
|
|
44
|
-
Packages identify sellable products or variants, and Offers own campaign price changes. Package Retail Price/Quantity fields
|
|
103
|
+
Packages identify sellable products or variants, and Offers own campaign price changes. The Campaigns App dashboard calls packages **Products** and creates one per variant when an Offer that selects it is saved, so a campaign set up there has one package per variant and a separate Offer for each Buy 1/2/3 tier. Package Retail Price/Quantity fields and multi-unit packages exist on older campaigns that have the legacy **Enable Package Retail Price & Quantity** setting on (see the "Older Campaigns" section of [Campaign Products](https://docs.nextcommerce.com/docs/apps/campaigns-app/build-campaign-packages)), but assembly should not introduce them for new tier pricing.
|
|
45
104
|
|
|
46
105
|
## Offer Application Surfaces
|
|
47
106
|
|
|
@@ -83,11 +142,11 @@ the assembly report.
|
|
|
83
142
|
- Landing and presell pages should preserve prepared HTML when it is a real standalone design. Use page-kit passthrough structure, inject the SDK/config requirements, and repoint CTAs into the CampaignSpec flow.
|
|
84
143
|
- **Pre-checkout pages must ship the same SDK bootstrap as the checkout layout.** Presell and landing pages are SDK `page_type: product`; they need `config.js` (before the loader), the `campaign-cart@v{sdk_version}/dist/loader.js` module script, and the `next-funnel` + `next-page-type` meta tags — not just inert `data-next-*` attributes. Without the loader the SDK silently no-ops: `data-next-hide` conditional visibility (`param.banner` / `param.seen`), `utmTransfer` UTM/query carry-through to checkout (top-of-funnel ad attribution), and SDK analytics never fire. Treat `param.banner` / `param.seen` visibility and `utmTransfer` as standard pre-checkout wiring, not per-campaign discoveries. Doctor enforces this with `built_output.pre_checkout_sdk_bootstrap`.
|
|
85
144
|
- **Every page names the same campaign.** A page borrowed from another funnel (a copied upsell or receipt) must not keep the other campaign's `next-api-key` / `config.js` API key, `next-funnel` meta, or `setAttribution({ funnel })` call; the SDK reads these per page and reconciles nothing, so the order lands on or attributes to the wrong campaign with no visible error. Doctor blocks this, unwaivably, with `built_output.campaign_identity` (one error per drift, naming both files and both values).
|
|
86
|
-
- **SDK markup must do what it says.** `data-next-checkout` goes on the `<form>`; `data-next-checkout-field` values are the SDK's fixed names (`fname`, `lname`, `postal` — never `firstName`, `lastName`, `zip`); an `add-to-cart` button linked by `data-next-selector-id` needs a selector with that id and that selector in `select` mode (swap mode plus the button writes the cart twice); every `data-next-upsell-action` sits inside its `data-next-upsell` container, on any page type; one default-selected card per selector; single-brace tokens inside SDK templates. Doctor enforces the first five as blockers and the last
|
|
145
|
+
- **SDK markup must do what it says.** `data-next-checkout` goes on the `<form>`; `data-next-checkout-field` values are the SDK's fixed names (`fname`, `lname`, `postal` — never `firstName`, `lastName`, `zip`); an `add-to-cart` button linked by `data-next-selector-id` needs a selector with that id and that selector in `select` mode (swap mode plus the button writes the cart twice); every `data-next-upsell-action` sits inside its `data-next-upsell` container, on any page type; one default-selected card per selector; single-brace tokens inside SDK templates; the page type in `<meta name="next-page-type">` (`upsell` on every upsell offer page), never only in a `data-next-page-type` attribute. Doctor enforces the first five as blockers and the last three as warnings under `built_output.sdk_markup` (codes `SWAP_WITH_ADD_TO_CART`, `CHECKOUT_NOT_FORM`, `WRONG_FIELD_NAME`, `MISSING_SELECTOR_ID_MATCH`, `ORPHANED_UPSELL_ACTION`, `DOUBLE_SELECTED`, `TEMPLATE_DOUBLE_BRACE`, `PAGE_TYPE_ATTRIBUTE`, `UPSELL_PAGE_TYPE_MISSING`).
|
|
87
146
|
- **Campaign scripts must parse.** A hand-edited script with a stray or missing bracket throws a `SyntaxError` on every page that loads it, and nothing in it runs. Doctor parses every campaign-owned `.js` file a built page loads by a local `<script src>` and blocks, unwaivably, with `built_output.script_syntax.parse_failure`, naming the file, line and column.
|
|
88
147
|
- Checkout, upsell, downsell, and receipt pages should preserve starter-template SDK contracts while keeping the campaign/source visual language. Treat starter templates as the reference for required `data-next-*` controls and wiring, not as a mandate to carry their visual chrome into the final campaign.
|
|
89
148
|
- If source HTML declares SDK-owned zones such as `data-commerce-zone="checkout-form"` or `data-commerce-zone="order-summary"`, keep the selected starter-template family's SDK wiring for that zone (the checkout form, bound fields, payment submit and cart summary). The wrapper and page composition around it stay source-owned; do not add family class names to the campaign's own layout to satisfy QA.
|
|
90
|
-
- If `context.theme` names a generated `brand-theme.css`, copy it into campaign assets and load it after `next-core.css` on checkout, upsell, downsell, and receipt pages. Generated brand-theme v0 is root-variable-only; do not use it as permission to edit SDK-owned selectors or runtime structure.
|
|
149
|
+
- If `context.theme` names a generated `brand-theme.css`, copy it into campaign assets and load it after `next-core.css` on checkout, upsell, downsell, and receipt pages. When `context.theme.generated.fonts_css_path` names a `brand-fonts.css`, copy it beside the brand layer and load it on the same pages, so headings and body copy use the design's faces (`record theme` refuses a styled page that skips it). Generated brand-theme v0 is root-variable-only; do not use it as permission to edit SDK-owned selectors or runtime structure.
|
|
91
150
|
- Buy-more-save-more selectors should use selected quantity plus Offer-aware price displays. Do not swap in stale package-per-tier IDs unless the CampaignSpec explicitly represents an older campaign that still owns separate packages for each option.
|
|
92
151
|
- SDK routing meta tags should be emitted as campaign-root paths, for example `/campaign-slug/upsell/`, even when the CampaignSpec source value is slug-relative like `upsell/`.
|
|
93
152
|
- One-time prepurchase/order-bump packages outside the main bundle should default to fixed quantity and fixed line total display unless the spec explicitly requires syncing quantity with the main bundle.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# CampaignSpec Authoring Examples
|
|
2
|
+
|
|
3
|
+
Use these examples when authoring or generating common Campaign Map shapes. They
|
|
4
|
+
are public-package guidance: the goal is clearer exported CampaignSpec fields,
|
|
5
|
+
not a private merchant workflow.
|
|
6
|
+
|
|
7
|
+
## Hero Product + Order Bump + Upsell + Receipt
|
|
8
|
+
|
|
9
|
+
Reference fixture: [single-step checkout, upsell and receipt](https://github.com/NextCommerceCo/campaigns-os/blob/main/contracts/fixtures/campaign-specs/shop-single-step-upsell-receipt.json).
|
|
10
|
+
Fixtures are repository test data and are not included in the installed package.
|
|
11
|
+
|
|
12
|
+
For CampaignSpec 5.0 multi-step shapes, compare the [three-step checkout](https://github.com/NextCommerceCo/campaigns-os/blob/main/contracts/fixtures/campaign-specs/shop-three-step-dynamic-shipping.json)
|
|
13
|
+
and [select-to-checkout](https://github.com/NextCommerceCo/campaigns-os/blob/main/contracts/fixtures/campaign-specs/olympus-mv-two-step-configurable.json) examples.
|
|
14
|
+
Author new specs against the shipped [v5 schema](../schemas/campaign-spec.v5.schema.json).
|
|
15
|
+
|
|
16
|
+
Canonical page shape:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
checkout -> upsell -> receipt
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Authoring guidance:
|
|
23
|
+
|
|
24
|
+
- Checkout page:
|
|
25
|
+
- `type: "checkout"` with `sdk_hints.sdk_page_type: "checkout"`
|
|
26
|
+
- `page_url: "/checkout/"` — `page_url` is the route field.
|
|
27
|
+
- `packages[]` entries use `ref_id` + `qty` (the real export key is `qty`,
|
|
28
|
+
not `quantity`). A plain entry is the main package; add-ons carry the
|
|
29
|
+
boolean role flags `is_order_bump: true` / `is_upsell: true` — there is
|
|
30
|
+
no `role` field in exports.
|
|
31
|
+
- `sdk_hints.template_family` names the intended starter family only as a
|
|
32
|
+
hint; the Build Packet still locks the family.
|
|
33
|
+
- `sdk_hints.meta_tags.next-success-url` points to the next runtime route.
|
|
34
|
+
- `success_url` holds the next PAGE ID (never a URL, despite the name).
|
|
35
|
+
- A checkout may equally declare its forward step as `next_page`; twelve
|
|
36
|
+
pages across ten certified fixtures do. Forward links resolve from
|
|
37
|
+
whichever routing field a page declares — `on_accept`, then `success_url`,
|
|
38
|
+
then `next_page` — so an unconventional journey is wired rather than
|
|
39
|
+
dropped.
|
|
40
|
+
- `next_page` is the generic "wherever this page goes next" and is honoured on
|
|
41
|
+
every page type. The other two forward fields carry a page-shaped meaning
|
|
42
|
+
and are honoured only where that meaning exists:
|
|
43
|
+
- `success_url` means "after payment succeeds" — only on `type: "checkout"`.
|
|
44
|
+
- `on_accept` means "after accepting the offer on this page" — only on
|
|
45
|
+
`type: "upsell"` or `"downsell"`.
|
|
46
|
+
|
|
47
|
+
Declared anywhere else they are inert, `next_page` wins, and the
|
|
48
|
+
`RouteFieldIgnoredForPageType` warning says which field was skipped and
|
|
49
|
+
where the shopper actually goes. A `select` page is the first step of a
|
|
50
|
+
two-step checkout and a `checkout_step` is a non-final page of a split
|
|
51
|
+
checkout: neither takes payment nor presents an accept/decline offer, so a
|
|
52
|
+
copy-pasted `success_url` or `on_accept` there does not route.
|
|
53
|
+
- Multi-step checkout (CampaignSpec 5.0): `checkout` is the page that takes
|
|
54
|
+
payment, and it forwards to an upsell, downsell or thank-you page. A page
|
|
55
|
+
with a checkout form that collects details and moves on without placing an
|
|
56
|
+
order is `type: "checkout_step"` and routes with `next_page`; a bundle
|
|
57
|
+
picker before checkout is `type: "select"`. Every path from an entry page
|
|
58
|
+
to an upsell passes exactly one `checkout`; a campaign may hold several, one
|
|
59
|
+
per split-test path. A 4.2/4.3 spec that chained `checkout → checkout` is
|
|
60
|
+
read as `checkout_step` pages ahead of the last one.
|
|
61
|
+
See the [v5 schema](../schemas/campaign-spec.v5.schema.json) for the
|
|
62
|
+
authoring fields; runtime routing is projected by Campaigns OS.
|
|
63
|
+
|
|
64
|
+
- Upsell page:
|
|
65
|
+
- `type: "upsell"` or `type: "downsell"`, with
|
|
66
|
+
`sdk_hints.sdk_page_type: "upsell"`
|
|
67
|
+
- `page_url` is a Page Kit route, not a source filename.
|
|
68
|
+
- `packages[]` (with `is_upsell: true`) or `offers[]` names the
|
|
69
|
+
post-purchase offer refs. Offers are identified by `ref_id` into the
|
|
70
|
+
root `offers[]` catalog (`package_ref_id` is not part of the contract).
|
|
71
|
+
- `on_accept` and `on_decline` hold the next PAGE IDs; route paths live
|
|
72
|
+
in `page_url` / derived `resolved_routing`.
|
|
73
|
+
|
|
74
|
+
- Receipt page:
|
|
75
|
+
- `type: "thankyou"` — the canonical terminal page type. The SDK-layer
|
|
76
|
+
`receipt` projection is expressed as `sdk_hints.sdk_page_type: "receipt"`
|
|
77
|
+
(and `meta_tags["next-page-type"]`), never as `page.type`.
|
|
78
|
+
- `page_url: "/receipt/"`
|
|
79
|
+
- Receipt summary/frontmatter hints preserve SDK-owned order item templates.
|
|
80
|
+
|
|
81
|
+
Generator checklist:
|
|
82
|
+
|
|
83
|
+
- Always emit stable `page.id` values; page IDs are the join key for
|
|
84
|
+
`source_html.pages[]`.
|
|
85
|
+
- Prefer Page Kit public routes (`checkout/`, `upsell/`, `receipt/`) over
|
|
86
|
+
filenames (`checkout.html`).
|
|
87
|
+
- Keep package, shipping, voucher, and offer refs in CampaignSpec/API fields,
|
|
88
|
+
not in source HTML.
|
|
89
|
+
- Put template-family preference in `campaign.preferred_template_family` when
|
|
90
|
+
useful, but require the Build Packet/template lock before commerce wiring.
|
|
91
|
+
- Keep checkout/upsell/downsell/receipt runtime surfaces template-clone-first;
|
|
92
|
+
source HTML can own visual composition and copy, not payment/order mutation.
|
|
93
|
+
|
|
94
|
+
## Order Bump Notes
|
|
95
|
+
|
|
96
|
+
An order bump is still a checkout package/offer surface. It should be encoded as
|
|
97
|
+
a checkout page package with `is_order_bump: true`, a `qty`, and its source
|
|
98
|
+
`ref_id`.
|
|
99
|
+
When the selected starter family does not support a bump, the build should
|
|
100
|
+
remove the bump surface and record the decision in the Assembly Report instead
|
|
101
|
+
of leaving demo bump refs in copied frontmatter.
|
|
102
|
+
|
|
103
|
+
## Multi-Upsell Depth
|
|
104
|
+
|
|
105
|
+
For more than one post-purchase page, keep each upsell/downsell page explicit in
|
|
106
|
+
`funnels[].pages[]` and wire `on_accept` / `on_decline` to the next page. QA
|
|
107
|
+
then derives typed-card proof coverage from topology. `common` runs every
|
|
108
|
+
actual terminal path when they fit under the flood cap; above it, it runs
|
|
109
|
+
checkout, first-offer accept and decline, and a deduplicated shortest real
|
|
110
|
+
receipt path, then adds the shortest path that clicks the decline on each offer
|
|
111
|
+
or downsell page not yet declined, up to the cap. Explicit paths support targeted
|
|
112
|
+
repair, while `full` walks every actual terminal path in the selected checkout
|
|
113
|
+
topology. Cycles, missing routes, and reachable nonterminals block exhaustive
|
|
114
|
+
proof before browser launch. The accidental-flood cap remains `6`; when the
|
|
115
|
+
planned count exceeds it, the command names the exact explicit
|
|
116
|
+
`--max-test-orders` raise.
|
|
@@ -32,6 +32,48 @@ Unknown top-level fields are rejected.
|
|
|
32
32
|
|
|
33
33
|
## Contributions and coverage
|
|
34
34
|
|
|
35
|
+
**A design file behind an HTML export.** Name it on the page's source-html
|
|
36
|
+
manifest entry as `design_file: {type, file_url, breakpoints}`. `breakpoints`
|
|
37
|
+
holds optional desktop, tablet and mobile frame links. Intake records a separate,
|
|
38
|
+
non-renderable contribution (`figma_frames` with native trust for Figma, otherwise
|
|
39
|
+
`other` with opaque trust), with `reference_only` coverage. The page's
|
|
40
|
+
`html_funnel` mapping links to it through `derived_from`. Pages sharing a file
|
|
41
|
+
share one contribution. A CampaignSpec page's Figma `design_source` records lineage
|
|
42
|
+
only with figma-sections-export provenance that passes doctor's
|
|
43
|
+
`source_html.producer_provenance` check without a waiver; then it wins when both
|
|
44
|
+
channels are present. Otherwise, a spec pointer alone adds no contribution or
|
|
45
|
+
lineage at intake, and no `design.access` row unless the package already records
|
|
46
|
+
a spec contribution. A Figma `design_source` that names a different file or
|
|
47
|
+
frames from the manifest's `design_file` is reported as a conflict for review
|
|
48
|
+
even when it records no lineage, as is a file key that differs from the
|
|
49
|
+
exporter's. A file shared by several pages keeps the manifest's provenance if
|
|
50
|
+
any page declared it in the manifest.
|
|
51
|
+
|
|
52
|
+
Malformed `design_file` values remain non-fatal manifest warnings; the schema
|
|
53
|
+
accepts them so validation agrees with intake. File URL schemes are
|
|
54
|
+
case-insensitive. Explicit tool types are lowercased for identity. For a type
|
|
55
|
+
other than `figma` or `other`, the design-file source ref's optional
|
|
56
|
+
`design_file_type` string preserves the type doctor needs to reconstruct the
|
|
57
|
+
recorded identity even after a declaration edit. This new field is material;
|
|
58
|
+
source-reference `notes` remain nonmaterial, including on `design_file` refs.
|
|
59
|
+
|
|
60
|
+
Recording a file adds no readiness requirement, Source TODO or blocker. A
|
|
61
|
+
manifest edit, including `design_file`, follows the existing stale-package rule:
|
|
62
|
+
rerun intake with `--force`. A later spec declaration leaves the package current;
|
|
63
|
+
when eligible under the Figma provenance rule above, doctor reports the
|
|
64
|
+
unrecorded file with the same recovery guidance.
|
|
65
|
+
`design.access` reports `unexercised` / `design_access_missing` when the recorded
|
|
66
|
+
file agrees with the declaration. For a recorded spec contribution, doctor
|
|
67
|
+
compares the current spec declaration even if today's manifest or provenance is
|
|
68
|
+
unavailable. This version cannot read design files. This
|
|
69
|
+
records authority only; Polish element parity compares the built page with its
|
|
70
|
+
source HTML, not the design file (see [Polish evidence](polish-evidence.md)).
|
|
71
|
+
A changed design file is listed for review; changing only breakpoint frames on
|
|
72
|
+
that file does not change its identity. If the package records a file but the
|
|
73
|
+
current declaration is absent, `design.access` reports `review` /
|
|
74
|
+
`design_reference_missing`: restore the declaration, or rerun intake with
|
|
75
|
+
`--force` to drop the recorded file.
|
|
76
|
+
|
|
35
77
|
Each `contributions[]` record identifies one source contribution and records:
|
|
36
78
|
|
|
37
79
|
- `id`, `kind`, `trust`, and whether it is `renderable`;
|
|
@@ -275,10 +317,10 @@ The projected fields are explicit:
|
|
|
275
317
|
| Contribution core | `id`, `kind`, `trust`, `renderable`. |
|
|
276
318
|
| Provenance | `source_type`, `adapter`, `producer`, `generator`, `generator_version`, `source_root`, `manifest_schema_version`, `manifest_path`, `manifest_sha256`, `producer_material_fingerprint`, `asset_crawl_schema_version`. |
|
|
277
319
|
| Presentation intent | `summary`, `composition`, `content_hierarchy`, `imagery`, `copy`, `brand`, `responsive_behavior`. |
|
|
278
|
-
| Source reference | `id`, `kind`, `path`, `url`, `sha256`, `role`, `media_type
|
|
320
|
+
| Source reference | `id`, `kind`, `path`, `url`, `sha256`, `role`, `media_type`, optional `design_file_type` (only on `design_file` refs). |
|
|
279
321
|
| Visual reference | `id`, `kind`, `viewport`, `availability`, `url`, `path`, `sha256`, `width`, `height`, `device_profile`, `scale_factor`, `browser`, `source_ref_id`, `unavailable_reason`. |
|
|
280
322
|
| Template Reference | `id`, `family`, `version`, `contract_path`, `artifact_path`, `sha256`, `standard_viewport_refs` using the visual projection above. |
|
|
281
|
-
| Coverage mapping | `id`, `surface_id`, `coverage_role`, `confidence`, `source_refs`, `screenshot_refs`, `reference_refs`, `template_reference_id`. |
|
|
323
|
+
| Coverage mapping | `id`, `surface_id`, `coverage_role`, `confidence`, `source_refs`, `screenshot_refs`, `reference_refs`, `template_reference_id`, optional `derived_from`. |
|
|
282
324
|
| Source Gap | `id`, `kind`, `scope`, `applies_to`, `reason`, `status`, `attributed_by`, `attributed_at`, `evidence_refs`. |
|
|
283
325
|
| Source TODO | `id`, `kind`, `scope`, `applies_to`, `description`, `status`, `owner`, `required_viewports`, `source_ref_ids`. |
|
|
284
326
|
| Approved waiver | `id`, `scope`, `applies_to`, `reason`, `status`, `waived_by`, `waived_at`, `expires_at`, `review_condition`, `evidence_refs`. |
|
|
@@ -458,6 +500,8 @@ only when filename matching was ambiguous, and is `null` otherwise.)
|
|
|
458
500
|
|
|
459
501
|
### The record shape
|
|
460
502
|
|
|
503
|
+
Polish now reads these files. Record `scale_factor` (the device pixel ratio the screenshot was taken at) and keep `width`/`height` equal to the image's pixel size: a screenshot is compared with the built page only when its width divided by `scale_factor` equals the capture width, and a screenshot without `scale_factor` is not compared. Link it to its source page (`source_ref_id`, filled in for you) and record `captured_at`, or it is listed for review.
|
|
504
|
+
|
|
461
505
|
One manifest page entry with its proof, inside the envelope the validator requires
|
|
462
506
|
(the mobile record is elided):
|
|
463
507
|
|