@nextcommerce/campaigns-os 1.43.1 → 1.46.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 +9 -2
- package/CHANGELOG.md +1099 -5103
- package/README.md +34 -13
- package/agents/claude/CLAUDE.md +1 -1
- package/agents/codex/AGENTS.md +1 -1
- package/agents/copilot/copilot-instructions.md +1 -1
- package/agents/cursor/campaigns-os.mdc +1 -1
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +2 -2
- package/campaign-spec/dist/rules/analytics-contract-shape.js +2 -2
- package/campaign-spec/dist/rules/store-profile-shape.d.ts +5 -1
- package/campaign-spec/dist/rules/store-profile-shape.js +8 -10
- package/campaign-spec/dist/types.d.ts +2 -2
- package/contracts/agent-relevant-change-policy.v1.json +5 -0
- package/contracts/archive/CHANGELOG.2026-09-30.md +5111 -0
- package/contracts/archive/release-ledger.2026-09-30.json +5068 -0
- package/contracts/effects.v1.json +1184 -121
- package/contracts/orientation-reason-codes.v1.json +7 -0
- package/contracts/release-ledger.json +2190 -5260
- package/contracts/supported-surface.json +7 -4
- package/contracts/template-slot-manifest.shared-content-core.v0.json +403 -0
- package/docs/brand-theme-bridge.md +81 -0
- package/docs/build-packet.md +222 -23
- package/docs/campaigns-os-build-flow.md +4 -3
- package/docs/design-source-package.md +162 -15
- package/docs/effects.md +66 -12
- package/docs/gateway-login.md +3 -0
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +42 -2
- package/docs/polish-evidence.md +74 -0
- package/docs/progress-snapshots.md +10 -6
- package/docs/qa-and-test-orders.md +230 -20
- package/docs/release-ledger-authoring-guide.md +70 -8
- package/docs/runtime-readiness.md +1 -1
- package/docs/sdk-storage-compatibility.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/docs/supported-surface.md +2 -2
- package/docs/versioning.md +4 -1
- package/package.json +1 -1
- package/schemas/campaigns-os-release-ledger.v1.schema.json +32 -2
- package/schemas/campaigns-os-tooling-orientation.v1.schema.json +1 -0
- package/skills/campaign-lifecycle-orientation/SKILL.md +16 -5
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +7 -6
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +7 -6
- package/skills/next-campaigns-os/SKILL.md +7 -7
- package/skills/next-campaigns-os/references/session-intake.md +9 -3
- package/skills/next-campaigns-os-setup/SKILL.md +5 -5
- package/skills/next-campaigns-polish/SKILL.md +28 -9
- package/skills/next-campaigns-qa/SKILL.md +7 -4
- package/skills.json +10 -10
- package/src/brand-theme.mjs +320 -20
- package/src/build-brief.mjs +6 -4
- package/src/built-script-syntax.mjs +480 -0
- package/src/built-site-scope.mjs +16 -4
- package/src/campaigns-api-key.mjs +99 -0
- package/src/cli-helpers.mjs +118 -0
- package/src/cli.mjs +1530 -7580
- package/src/commercial-parity.mjs +48 -2
- package/src/design-source-package.mjs +1 -1
- package/src/design-source-publication.mjs +898 -0
- package/src/deviation.mjs +13 -1
- package/src/diagnostic.mjs +6 -2
- package/src/directory-lock.mjs +270 -0
- package/src/doctor/checks.mjs +4654 -0
- package/src/doctor/inspect.mjs +678 -0
- package/src/doctor/next-step.mjs +731 -0
- package/src/doctor/source-provenance.mjs +184 -0
- package/src/install-invocation.mjs +29 -0
- package/src/invocation.mjs +183 -0
- package/src/live-campaign-refs.mjs +466 -0
- package/src/login.mjs +2 -2
- package/src/page-kit-store-profile.mjs +69 -12
- package/src/page-kit-sync.mjs +31 -12
- package/src/private-template-source.mjs +1 -1
- package/src/progress-node.mjs +9 -36
- package/src/proof-policy.mjs +1 -1
- package/src/qa-analytics-correctness.mjs +3 -0
- package/src/qa-binding-evidence.mjs +76 -11
- package/src/qa-browser.mjs +1316 -105
- package/src/qa-build-scope.mjs +47 -0
- package/src/qa-commercial-parity.mjs +48 -5
- package/src/qa-node.mjs +339 -19
- package/src/qa-test-order-topology.mjs +148 -0
- package/src/sdk-markup.mjs +72 -8
- package/src/source-html-intake.mjs +117 -1
- package/src/source-html-manifest.mjs +9 -2
- package/src/stage-ledger.mjs +28 -0
- package/src/stage-record.mjs +551 -0
- package/src/target-lock.mjs +54 -0
- package/src/template-brand-contract.mjs +17 -1
- package/src/upsell-selector-scope.mjs +112 -2
package/docs/build-packet.md
CHANGED
|
@@ -69,7 +69,7 @@ localhost readiness is not production approval.
|
|
|
69
69
|
|
|
70
70
|
Most campaigns are served under a slug prefix (`/<public_route_slug>/...`), and
|
|
71
71
|
that stays the default. A campaign whose whole funnel is served from the **site
|
|
72
|
-
root** — pages at `/checkout-v2`, `/oto-
|
|
72
|
+
root** — pages at `/checkout-v2`, `/oto-rootfunnel`, `/receipt` with no slug prefix,
|
|
73
73
|
the normal shape for a single-campaign site or an in-place static deploy —
|
|
74
74
|
declares `campaign.route_root: "/"`. Rules:
|
|
75
75
|
|
|
@@ -85,7 +85,10 @@ declares `campaign.route_root: "/"`. Rules:
|
|
|
85
85
|
- Doctor's routing-meta checks (`routing_meta.runtime_root`,
|
|
86
86
|
`sdk_hints.meta_tags.route_mismatch`) and route displays validate against the
|
|
87
87
|
declared route root instead of assuming slug-as-prefix, so a root-served
|
|
88
|
-
funnel's correct `/receipt`-style metas pass without waivers.
|
|
88
|
+
funnel's correct `/receipt`-style metas pass without waivers. A routing
|
|
89
|
+
meta value with a bare or `//` host in front of its path is not a
|
|
90
|
+
`runtime_root` warning; it is the `routing_meta.host_prefixed` blocker (see "Page Kit
|
|
91
|
+
Target Projection" below).
|
|
89
92
|
- The CampaignSpec may carry the same declaration at `campaign.route_root` (or
|
|
90
93
|
`spec_identity.route_root`); `prepare-build` copies it onto the packet and
|
|
91
94
|
defaults `live_url_path` to `/`.
|
|
@@ -112,6 +115,22 @@ Before scaffold, a missing target entry is `not_applicable`; once setup or
|
|
|
112
115
|
assembly is terminal, or the target output already exists, missing or malformed
|
|
113
116
|
target evidence is a non-waivable blocker. Target-only values remain warnings.
|
|
114
117
|
Mismatches, missing required target values, and known demo residue block.
|
|
118
|
+
An absent or null spec field means "not provided". An explicit empty (or
|
|
119
|
+
whitespace-only) string in one of the eight optional fields (`store_name`,
|
|
120
|
+
`store_terms`, `store_privacy`, `store_contact`, `store_returns`,
|
|
121
|
+
`store_shipping`, `store_phone`, `store_phone_tel`) means the merchant has no
|
|
122
|
+
such value: `page-kit sync` blanks a recognised starter demo value with it
|
|
123
|
+
(the placeholder storefront URLs and phone number; the starter's demo store
|
|
124
|
+
name is not recognised and stays a `target_only` warning), and a blank or
|
|
125
|
+
absent target field then reads as `intentionally_empty` (clean, and named in
|
|
126
|
+
the gate reason and the doctor line). `campaign.store_url` stays required: a
|
|
127
|
+
`""` there still raises doctor's `spec.store_profile` error, and the gate
|
|
128
|
+
reason says so. A real, non-demo target value against a spec `""` is left as
|
|
129
|
+
it is and stays a `target_only` warning, because Maps saved `""` for every
|
|
130
|
+
cleared store field before it meant empty; the warning and the sync output
|
|
131
|
+
(`spec_empty_not_applied[]`) say the `""` was not applied, so remove that value
|
|
132
|
+
by hand if the merchant has none. An empty string outside these fields
|
|
133
|
+
carries no such meaning.
|
|
115
134
|
Demo residue (a `demo.29next.com` URL or the demo phone number still in the
|
|
116
135
|
target) is never waivable: the gate names the residue fields, offers no waive
|
|
117
136
|
command for them, and `checkpoint waive` refuses with those fields until the
|
|
@@ -142,9 +161,15 @@ re-saving the Map). The repo pin is the authority for what ships and the Map
|
|
|
142
161
|
field is a build hint, so that state is a doctor warning, not a blocker (see
|
|
143
162
|
the SDK version checkpoint below). Both go into `_data/campaigns.json[public_route_slug]`,
|
|
144
163
|
prints a field-by-field before/after diff, and touches nothing else: a
|
|
145
|
-
governed field the spec does not carry is left as it is
|
|
146
|
-
`target_only` warning still applies),
|
|
147
|
-
|
|
164
|
+
governed field the spec does not carry (absent or null) is left as it is
|
|
165
|
+
(doctor's `target_only` warning still applies), a field the spec sets to `""`
|
|
166
|
+
(or whitespace only) blanks a recognised starter demo value (the placeholder
|
|
167
|
+
storefront URLs and phone number; the starter's demo store name is not
|
|
168
|
+
recognised and stays a `target_only` warning) and otherwise leaves the target
|
|
169
|
+
value as it is (listed in `not_in_spec[]` as before and also in
|
|
170
|
+
`spec_empty_not_applied[]`, printed as `Spec "" not applied`), non-governed
|
|
171
|
+
keys keep their values and order, other routes and other files are not
|
|
172
|
+
written. The file is edited in
|
|
148
173
|
place and re-serialized with its own top-level indentation, line ending and
|
|
149
174
|
trailing newline; when that round trip would not have reproduced the file
|
|
150
175
|
byte for byte (a minified file, mixed indentation), a
|
|
@@ -724,13 +749,14 @@ built-output gate now carries.
|
|
|
724
749
|
### Built-output SDK markup gate (`built_output.sdk_markup`)
|
|
725
750
|
|
|
726
751
|
Every doctor run that sees built output also runs the static SDK markup
|
|
727
|
-
family:
|
|
752
|
+
family: seven shapes of `data-next-*` markup that the Campaign Cart SDK binds
|
|
728
753
|
without complaint and that then either do nothing (a field that never reaches
|
|
729
754
|
the order, a button that never enables) or write the cart twice. They sit
|
|
730
755
|
beside `built_output.upsell_selector_scope`, which is the same kind of check
|
|
731
|
-
for one shape. The codes are the ones a partner Campaign Cart kit
|
|
732
|
-
the two vocabularies line up; each doctor issue is
|
|
733
|
-
plus the code lower-cased, and its message leads
|
|
756
|
+
for one shape. The first six codes are the ones a partner Campaign Cart kit
|
|
757
|
+
used, kept so the two vocabularies line up; each doctor issue is
|
|
758
|
+
`built_output.sdk_markup.` plus the code lower-cased, and its message leads
|
|
759
|
+
with the code.
|
|
734
760
|
|
|
735
761
|
Blockers (not waivable — the markup provably does not do what it says):
|
|
736
762
|
|
|
@@ -752,6 +778,12 @@ Blockers (not waivable — the markup provably does not do what it says):
|
|
|
752
778
|
`data-next-selector-id` names no selector on the page (an element that is a
|
|
753
779
|
bundle, package, cart or upsell selector; another element echoing the id
|
|
754
780
|
does not count). One finding per dead id, however many buttons link to it.
|
|
781
|
+
- `ORPHANED_UPSELL_ACTION` — an element carrying `data-next-upsell-action` with
|
|
782
|
+
no ancestor carrying `data-next-upsell`. The SDK binds upsell actions only
|
|
783
|
+
inside that container, so a "No thanks" link placed beside the offer
|
|
784
|
+
container, not inside it, goes nowhere and the shopper cannot decline.
|
|
785
|
+
Checked on every page type, not only upsell and downsell pages. Move the
|
|
786
|
+
element inside its `data-next-upsell` container.
|
|
755
787
|
|
|
756
788
|
Warnings (advisory):
|
|
757
789
|
|
|
@@ -783,6 +815,64 @@ advisories, `unknown_attributes[]`, `pages_scanned`,
|
|
|
783
815
|
It passes, with no advisory, on the canonical rendered output of every
|
|
784
816
|
certified starter family (`fixtures/certified-families/`).
|
|
785
817
|
|
|
818
|
+
### Built-output script syntax gate (`built_output.script_syntax`)
|
|
819
|
+
|
|
820
|
+
Every doctor run that sees built output (the packet path and `doctor --built`
|
|
821
|
+
alike) parses each campaign-owned `.js` file a built page loads by a local
|
|
822
|
+
`<script src>`. A script that does not parse throws a `SyntaxError` on every
|
|
823
|
+
load of every page that references it, and nothing it defines runs; every
|
|
824
|
+
HTML-reading gate passes over it. The shape that shipped was a template-family
|
|
825
|
+
checkout script, copied and hand-edited, left with one closing `});` too many.
|
|
826
|
+
|
|
827
|
+
Parsing uses Acorn at the latest `ecmaVersion`: `sourceType: 'script'` for
|
|
828
|
+
classic scripts and `'module'` for `type="module"`, which is how the browser
|
|
829
|
+
reads each. Remote scripts (an `http(s):` URL, a protocol-relative `//` URL,
|
|
830
|
+
`data:`) are not campaign-owned and are not read, and neither are data blocks
|
|
831
|
+
such as JSON-LD. The type is compared as the browser compares it, with
|
|
832
|
+
surrounding ASCII whitespace stripped and case ignored. A classic `nomodule`
|
|
833
|
+
script is skipped: a module-capable browser never fetches or runs it. A
|
|
834
|
+
`type="module"` script ignores `nomodule` and is still parsed. Each src
|
|
835
|
+
resolves the way the browser resolves it: against the base in effect when the
|
|
836
|
+
parser prepares the script at its end tag, which is the first HTML `<base
|
|
837
|
+
href>` in tree order among those already parsed, or else the page. A `<base>`
|
|
838
|
+
parsed after a script does not move it, whether it is async, deferred or a
|
|
839
|
+
module: its URL is fixed when it is prepared, not when it is fetched. Parse
|
|
840
|
+
order decides, not final tree position, so a base that table foster parenting
|
|
841
|
+
moves ahead of an earlier script still does not apply to it. A `base` inside
|
|
842
|
+
SVG or MathML is not a base element, and only HTML-namespace `<script>`
|
|
843
|
+
elements are read: an SVG `<script>` never loads a `src` attribute. The href is read as the URL parser reads
|
|
844
|
+
it: only leading and trailing ASCII control characters and spaces are
|
|
845
|
+
stripped. A base
|
|
846
|
+
the browser refuses (one that does not parse, or a `data:` or `javascript:`
|
|
847
|
+
URL) falls back to the page, as the HTML "set the frozen base URL" steps
|
|
848
|
+
require. The percent-decoded path maps under the site root first, then the
|
|
849
|
+
campaign directory, never outside either. A base on another origin makes
|
|
850
|
+
relative srcs remote. Imports inside a module are not followed.
|
|
851
|
+
|
|
852
|
+
A parse failure blocks (not waivable — a script that cannot be parsed cannot be
|
|
853
|
+
intended to ship) under `built_output.script_syntax.parse_failure`, one error
|
|
854
|
+
per file. The message leads with `<file>:<line>:<column>` and a fixed
|
|
855
|
+
diagnostic category (for example `Unexpected token` or `Invalid regular
|
|
856
|
+
expression`), never text from the script, and names the pages that load the
|
|
857
|
+
file. A referenced local script that is not in the built output is a warning,
|
|
858
|
+
not a blocker, under `built_output.script_syntax.missing_script`, one warning
|
|
859
|
+
per src naming the pages that load it: the browser gets a 404 for it and
|
|
860
|
+
nothing it would define runs, but whether the page needs it is not known here.
|
|
861
|
+
The src is also listed in `scripts_unresolved[]`. While a parse failure blocks
|
|
862
|
+
the gate, the missing scripts stay on the gate's `warned[]` rather than also
|
|
863
|
+
surfacing as warnings.
|
|
864
|
+
|
|
865
|
+
The gate's evidence lands beside the other checkpoint gates at
|
|
866
|
+
`derived.checkpoint_gates[]` (`id: built_output.script_syntax`, status `pass` |
|
|
867
|
+
`blocked` | `not_applicable`, `findings[]` with `file`, `line`, `column`,
|
|
868
|
+
`source_type` and `pages`, `warned[]` with `src` and `pages`,
|
|
869
|
+
`scripts_scanned`, `scripts_unresolved[]`, `pages_scanned`). Fixtures: `fixtures/script-syntax/{good,bad}`. It passes, parsing every
|
|
870
|
+
local script the pages load and with no missing-script warning, on the
|
|
871
|
+
canonical rendered output of every certified starter family
|
|
872
|
+
(`fixtures/certified-families/`). QA applies the same rule to the page scripts
|
|
873
|
+
it reads for credential declarations (`script-parse:<page_id>`; see
|
|
874
|
+
[QA and test orders](qa-and-test-orders.md)).
|
|
875
|
+
|
|
786
876
|
> **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`.
|
|
787
877
|
|
|
788
878
|
## Artifact Locations
|
|
@@ -980,9 +1070,9 @@ fingerprint is `sha256:` plus the SHA-256 of that manifest. Nothing is excluded
|
|
|
980
1070
|
default (Page Kit writes only rendered HTML and copied assets into `_site/`, nothing
|
|
981
1071
|
it timestamps); `.campaign-runtime/page-kit-build-summary.json` lives outside the
|
|
982
1072
|
root and is not hashed. Build does not type the value: after page-kit build it runs
|
|
983
|
-
`campaigns-os
|
|
984
|
-
`derived.build_output_fingerprint.value`
|
|
985
|
-
|
|
1073
|
+
`campaigns-os record build --packet <packet>`, which stamps doctor's
|
|
1074
|
+
`derived.build_output_fingerprint.value` onto `stages.assembly.build_fingerprint`
|
|
1075
|
+
(see "Recording stage completion" below). Doctor recomputes the value on every run
|
|
986
1076
|
(`built_output.fingerprint`): a match is a ready line, a missing record is the
|
|
987
1077
|
warning `built_output.fingerprint_missing` carrying the value to record, and a
|
|
988
1078
|
recorded value the output no longer matches is `built_output.fingerprint_stale`
|
|
@@ -1006,9 +1096,11 @@ Design Source Package exists. The waiver must remain visible in Campaign
|
|
|
1006
1096
|
Readiness Readback and downstream QA evidence; it is not a silent pass.
|
|
1007
1097
|
In v0, write accepted Source Freshness Waivers directly into `waivers[]`.
|
|
1008
1098
|
`campaigns-os checkpoint waive` is a staged generic registry and currently
|
|
1009
|
-
accepts
|
|
1010
|
-
`polish.hidden_eager_media`,
|
|
1011
|
-
|
|
1099
|
+
accepts five gates: `page_kit.store_profile`, `page_kit.sdk_version`,
|
|
1100
|
+
`polish.hidden_eager_media`, `built_output.upsell_selector_scope`, and
|
|
1101
|
+
`source_html.producer_provenance`, which is waived per page with
|
|
1102
|
+
`--page <page_id>` (see the [Design Source Package](./design-source-package.md)
|
|
1103
|
+
hand-written HTML route); an unregistered gate id is refused with that list. `theme waive` applies the same
|
|
1012
1104
|
attribution rule (a named human, no placeholder, an optional future
|
|
1013
1105
|
`--expires-at`) on its own lane. Within Polish, only the broader Source Freshness
|
|
1014
1106
|
waiver retains its existing report path; theme and QA decisions retain their
|
|
@@ -1167,12 +1259,13 @@ and report proof policy fields above.
|
|
|
1167
1259
|
| `--spec <path>` | Local JSON file | Agent-authored local specs, saved-Map exports, offline work or CI fixtures |
|
|
1168
1260
|
| `--map-id <id>` | Map Builder proxy (KV-backed) | Saved-Map intake from the current KV revision |
|
|
1169
1261
|
|
|
1170
|
-
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.
|
|
1262
|
+
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. When the Map holds routes with a host in front of the path, the cached file holds the rooted routes and that run's Assembly Report `evidence[]` holds the values as fetched (see "Page Kit Target Projection" below). The cache is written only inside a real `fetched-specs/` directory: if `.campaign-runtime/`, `fetched-specs/` or the cache file is a symlink, the command stops with an error before fetching and writes nothing. Each write goes to a new file renamed over the cache file, so a hard link to the old file keeps its bytes.
|
|
1171
1263
|
|
|
1172
1264
|
Saved-Map retrieval behavior (`--map-id`):
|
|
1173
1265
|
|
|
1174
1266
|
- **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.
|
|
1175
|
-
-
|
|
1267
|
+
- **One writer at a time.** The fetch happens first, but the fetched spec is written to the cache file only once the run holds the per-target prepare-build lock. A second run against the same target, fetching a newer Map revision, waits for the lock before replacing the cache, so the run holding it records the hash of the revision it actually parsed.
|
|
1268
|
+
- **`--cached-spec`** reuses the cache without a network call. Use for offline iteration or when the proxy is temporarily unreachable. The cached copy is read as it is and never rewritten.
|
|
1176
1269
|
- **`--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).
|
|
1177
1270
|
- Failure modes (HTTP error, `{ok: false}` response, network timeout) surface as clean CLI errors before any packet is written.
|
|
1178
1271
|
|
|
@@ -1180,7 +1273,7 @@ The fetched spec is treated identically to a `--spec`-supplied local file from t
|
|
|
1180
1273
|
|
|
1181
1274
|
## Source HTML Manifest Auto-Population
|
|
1182
1275
|
|
|
1183
|
-
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)).
|
|
1276
|
+
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`. Each `pages[]` entry carries exactly one of `path` or `skip_reason`: an entry with both is invalid, and an invalid entry makes prepare-build ignore the whole manifest and fall back to filesystem matching. 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)).
|
|
1184
1277
|
|
|
1185
1278
|
The source-html manifest remains a producer/source-HTML adapter input. It is not
|
|
1186
1279
|
renamed into the Design Source Package. In the normalized source workflow,
|
|
@@ -1190,7 +1283,10 @@ contributions, coverage, gaps/TODOs, Surface Identity, references, and readback.
|
|
|
1190
1283
|
When source-html data is the available input and the default package path is
|
|
1191
1284
|
missing, current v0 `prepare-build` synthesizes the package. If a package already
|
|
1192
1285
|
exists, it is validated against the current material inputs and reused byte for
|
|
1193
|
-
byte or refused; it is never silently regenerated.
|
|
1286
|
+
byte or refused; it is never silently regenerated. The one exception is a stale
|
|
1287
|
+
package an earlier `prepare-build` synthesized and nobody has changed since:
|
|
1288
|
+
`--force` regenerates it from the current inputs
|
|
1289
|
+
([Design Source Package: stale packages](design-source-package.md#prepare-build-emit-validate-or-refuse)). Downstream Build and Polish
|
|
1194
1290
|
consume the package concept rather than branching back to
|
|
1195
1291
|
`packet.source_html` as a second source model. The emitted package lives at
|
|
1196
1292
|
`.campaign-runtime/input/design-source-package.json` by default and is referenced
|
|
@@ -1259,6 +1355,70 @@ routes during projection. That normalization strips `.html`/`index.html`,
|
|
|
1259
1355
|
removes query/fragment values, converts absolute preview URLs to their path, and
|
|
1260
1356
|
normalizes trailing slashes before deriving target files and frontmatter routes.
|
|
1261
1357
|
|
|
1358
|
+
A route value with a host in front of its path (an older saved Map stored
|
|
1359
|
+
`shop.example.com/route/upsell/` where the route is `/route/upsell/`) is
|
|
1360
|
+
reduced to its rooted path before anything reads a spec that `prepare-build`,
|
|
1361
|
+
`start` or `build` fetched with `--map-id` in the same run. This covers every
|
|
1362
|
+
`page_url` and every `next-success-url`, `next-upsell-accept-url` and
|
|
1363
|
+
`next-upsell-decline-url` meta tag value in `funnels[].pages[]` and
|
|
1364
|
+
`funnel_pages[]`:
|
|
1365
|
+
|
|
1366
|
+
- The host is removed and the path is kept with its query and fragment
|
|
1367
|
+
(`shop.example.com/route/x/?v=b#top` becomes `/route/x/?v=b#top`). An
|
|
1368
|
+
absolute `http(s)` `page_url` is rooted the same way; an absolute `http(s)`
|
|
1369
|
+
routing meta value is a valid SDK target and is kept.
|
|
1370
|
+
- The fetched copy at `<target>/.campaign-runtime/fetched-specs/<map-id>.json`
|
|
1371
|
+
then holds the rooted values, not the bytes as fetched. It is replaced only
|
|
1372
|
+
after the Assembly Report recording the changes has been published, so if
|
|
1373
|
+
publishing fails the copy is left exactly as fetched. The rooted spec is
|
|
1374
|
+
written to a new file in `fetched-specs/` and renamed over the copy, so
|
|
1375
|
+
another name for the old file (a hard link) is never changed. A spec with
|
|
1376
|
+
no host-prefixed value is written exactly as fetched, as before.
|
|
1377
|
+
- Each changed value is recorded on that run's Assembly Report `evidence[]` as
|
|
1378
|
+
`{ "code": "routing_meta.host_stripped", "page_id", "field", "from", "to" }`;
|
|
1379
|
+
`from` is the value exactly as the Map returned it. The raw values are kept
|
|
1380
|
+
only there and in the Map itself: a later run with `--cached-spec` reads the
|
|
1381
|
+
rooted copy, finds nothing to strip, and records no evidence. One line on
|
|
1382
|
+
stderr lists the changes.
|
|
1383
|
+
- `intake.saved_map_revision.local_spec_material_hash` in the Build Context is
|
|
1384
|
+
the material hash of the rooted copy, while `hash` stays the fetched Map
|
|
1385
|
+
revision. A progress snapshot's `saved_revision_alignment: "aligned"`
|
|
1386
|
+
therefore means the local spec matches that Map revision after host
|
|
1387
|
+
stripping, not byte for byte.
|
|
1388
|
+
|
|
1389
|
+
A local `--spec` file and a copy reused with `--cached-spec` are never
|
|
1390
|
+
rewritten. When either holds a value doctor blocks on (below), intake reads it
|
|
1391
|
+
as it is and prints one line on stderr naming each value and its rooted form:
|
|
1392
|
+
for a local file, that the file must be edited; for `--cached-spec`, to re-run
|
|
1393
|
+
without `--cached-spec` so the Map is fetched and normalised.
|
|
1394
|
+
|
|
1395
|
+
A value reads as host-prefixed when it is one of:
|
|
1396
|
+
|
|
1397
|
+
- an `http://` or `https://` URL, with any host (stripped from a fresh fetch;
|
|
1398
|
+
never blocked);
|
|
1399
|
+
- `//<host>/...`, where `<host>` is one of the bare host forms below;
|
|
1400
|
+
- `<host>/...`, where `<host>` is `localhost`, a valid IPv4 address (four
|
|
1401
|
+
dot-separated numbers, each 0-255), any name with a `:port`
|
|
1402
|
+
(`localhost:8080`, `shop.example.com:8443`), or a dotted name whose last
|
|
1403
|
+
label is 2-63 letters and is not a page or script extension: `html`, `htm`,
|
|
1404
|
+
`shtml`, `php`, `asp`, `aspx`, `jsp` or `cgi` (`shop.example.com`,
|
|
1405
|
+
any case).
|
|
1406
|
+
|
|
1407
|
+
Everything else stays a route for the existing checks: a rooted `/...` value,
|
|
1408
|
+
a first segment with no dot (`route/x/`), a dotted segment whose last label is
|
|
1409
|
+
not all letters (`v1.2/offer/`), a dotted quad with a number over 255
|
|
1410
|
+
(`300.1.2.3/offer/`), a page or script filename (`checkout.html`,
|
|
1411
|
+
`index.php/checkout/`, `upsell.aspx/`), `//` followed by something that is not
|
|
1412
|
+
a host (`//route/x/`), a bare host with no path, and an empty or missing value.
|
|
1413
|
+
|
|
1414
|
+
If a bare or `//` host-prefixed value still reaches doctor (a local spec that
|
|
1415
|
+
holds one, a copy reused with `--cached-spec`, or a spec edited after intake),
|
|
1416
|
+
doctor blocks with `routing_meta.host_prefixed`, naming each value and its
|
|
1417
|
+
rooted form. An absolute `http(s)` value never raises it: projection converts
|
|
1418
|
+
an absolute `page_url` to its path, as above. `page_url` is checked whether or
|
|
1419
|
+
not the site is built; routing meta values follow the same built-output
|
|
1420
|
+
deferral as `routing_meta.runtime_root`.
|
|
1421
|
+
|
|
1262
1422
|
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.
|
|
1263
1423
|
|
|
1264
1424
|
`target_path` intentionally uses the terminal route segment (`checkout/step-1/`
|
|
@@ -1415,10 +1575,49 @@ The motion:
|
|
|
1415
1575
|
|
|
1416
1576
|
```text
|
|
1417
1577
|
agent calls `next` → gets { stage, prompt, picked_reason } → does the work →
|
|
1418
|
-
|
|
1419
|
-
repeat until stage="done"
|
|
1578
|
+
records it (`campaigns-os record setup|build|polish`; deploy and QA record their
|
|
1579
|
+
own stages) → calls `next` again → repeat until stage="done"
|
|
1420
1580
|
```
|
|
1421
1581
|
|
|
1582
|
+
### Recording stage completion
|
|
1583
|
+
|
|
1584
|
+
Setup, build and polish completion is recorded with one command each, never by
|
|
1585
|
+
hand-editing `.campaign-runtime/` JSON:
|
|
1586
|
+
|
|
1587
|
+
| Command | Writes | Refused (nothing written) when |
|
|
1588
|
+
|---|---|---|
|
|
1589
|
+
| `campaigns-os record setup --packet <p>` | Build Context `scaffold.required=false` (`handoff_skill` next-campaigns-build) and `stages.setup` completed | the campaign output directory (`assembly.output_dir`) does not exist, or there is no Build Context or Assembly Report |
|
|
1590
|
+
| `campaigns-os record build --packet <p>` | `stages.assembly` completed with `build_fingerprint` = doctor's `derived.build_output_fingerprint.value`, `source_package_material_fingerprint` = the report's Design Source Package material fingerprint when present, and `stages.polish` reset to `required` (`required_by` build, `required_for` qa) unless its evidence is bound to this exact output | doctor cannot compute the fingerprint (no `_site/<public_route_slug>/`), setup is still required, or `stages.setup` is not terminal |
|
|
1591
|
+
| `campaigns-os record polish --packet <p> --evidence <file>` | `stages.polish` from the file (`docs/polish-evidence.md` §7: completed, blocked or skipped), bound to doctor's current fingerprint; `report.theme.repair_loop_defect` when the file sets it | build is not recorded for the current output, the file has a shape error (named by field), or, for a completed status, the polish gate doctor evaluates would not pass on the result |
|
|
1592
|
+
|
|
1593
|
+
Each command also refuses a stage `next` has not reached: while doctor's
|
|
1594
|
+
prepare-build gate is set (`next` answers prepare-build) or while an earlier
|
|
1595
|
+
stage in the order below is not terminal.
|
|
1596
|
+
|
|
1597
|
+
Each command reads the same packet, Build Context and Assembly Report `next`
|
|
1598
|
+
reads (`--context` / `--report` override them the same way), validates what it
|
|
1599
|
+
would write against `schemas/campaign-runtime-build-context.v0.schema.json` and
|
|
1600
|
+
`schemas/campaign-runtime-assembly-report.v0.schema.json` plus doctor's report
|
|
1601
|
+
checks, and then writes under the target lock, stamping any retained doctor
|
|
1602
|
+
output stale. The packet is read once first, only to name the target lock, and
|
|
1603
|
+
re-read and re-checked under it: which report the Build Context binds, the
|
|
1604
|
+
report itself, the Build Context and doctor's reading (the fingerprint and the
|
|
1605
|
+
binding) are all read inside the same target lock as the write, and the
|
|
1606
|
+
fingerprint is computed once more just before the write; output
|
|
1607
|
+
that changed in between is refused, never recorded with the old value. Every
|
|
1608
|
+
command refuses a report that is not bound to the packet: whenever doctor's
|
|
1609
|
+
prepare-build binding gate fails (`next.prepare_build.context_missing`,
|
|
1610
|
+
`context_packet_mismatch`, `context_dsp_mismatch`, `context_report_missing`,
|
|
1611
|
+
`report_packet_mismatch`, `report_context_mismatch`, `report_campaign_mismatch`
|
|
1612
|
+
or `report_dsp_mismatch`, the refusals `next` answers with prepare-build), and
|
|
1613
|
+
whenever the report's campaign identity does not match the packet, including
|
|
1614
|
+
for packets with no Design Source Package. Every command also adds
|
|
1615
|
+
`recorded_by`, and `completed_at` unless it records a blocked Polish, to the
|
|
1616
|
+
stage it records. `--dry-run` runs every check, takes no lock and writes nothing. A failed
|
|
1617
|
+
check exits non-zero with the problems listed, one per line. Re-run `record
|
|
1618
|
+
build` after every page-kit build; a rebuild that changes the output needs
|
|
1619
|
+
`polish capture` and `record polish` again.
|
|
1620
|
+
|
|
1422
1621
|
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.
|
|
1423
1622
|
|
|
1424
1623
|
| Stage | Report key | Owner |
|
|
@@ -1461,8 +1660,8 @@ The legacy form `campaigns-os next <stage>` (e.g. `next build`) still works and
|
|
|
1461
1660
|
|
|
1462
1661
|
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:
|
|
1463
1662
|
|
|
1464
|
-
- `design_source.type === "figma"` with `file_url`: doctor calls out the Figma file and the figma-sections-export handoff
|
|
1663
|
+
- `design_source.type === "figma"` with `file_url`: doctor calls out the Figma file and says the Figma provenance gate (`source_html.producer_provenance`) needs the figma-sections-export handoff manifest (`npm run handoff -- <slug>`); a hand-written manifest cannot pass that gate.
|
|
1465
1664
|
- `design_source` set without `file_url`: doctor flags the missing `file_url` so the spec can be corrected.
|
|
1466
|
-
- `design_source` unset:
|
|
1665
|
+
- `design_source` unset, `ai-generated`, or another producer type: the message names the manifest path (`<source-root>/.campaigns-os/source-html-manifest.json`), the schema, and a minimal page entry to write by hand (no exporter is needed), plus `"wrapper_policy": "preserve_document_wrappers"` for standalone documents kept whole. When any active page's `design_source` is Figma, it instead says the manifest must pass the Figma provenance gate.
|
|
1467
1666
|
|
|
1468
1667
|
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.
|
|
@@ -83,9 +83,10 @@ the assembly report.
|
|
|
83
83
|
- 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
84
|
- **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
85
|
- **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); one default-selected card per selector; single-brace tokens inside SDK templates. Doctor enforces the first
|
|
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 two 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`).
|
|
87
|
+
- **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.
|
|
87
88
|
- 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.
|
|
88
|
-
- If source HTML declares SDK-owned zones such as `data-commerce-zone="checkout-form"` or `data-commerce-zone="order-summary"`,
|
|
89
|
+
- 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.
|
|
89
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.
|
|
90
91
|
- 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.
|
|
91
92
|
- 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/`.
|
|
@@ -94,7 +95,7 @@ the assembly report.
|
|
|
94
95
|
- Any source element dropped because the spec does not support it, such as PayPal when `available_payment_methods` excludes PayPal, should be recorded in the assembly report for polish.
|
|
95
96
|
- After page-kit build, doctor checks rendered local script references plus rendered package and shipping refs against the CampaignSpec. Missing built scripts, stale package IDs, stale shipping IDs, and unavailable package refs must be fixed or intentionally blocked before QA.
|
|
96
97
|
- Browser QA opens SDK-owned runtime pages once as a shopper and once with `?debugger=true`. The debugger pass should prove the Campaign Cart debugger overlay and selector controls mount without changing the normal checkout/test-order flow.
|
|
97
|
-
- Browser QA also checks template-family commerce structure when the family contract declares machine-checkable selectors.
|
|
98
|
+
- Browser QA also checks template-family commerce structure when the family contract declares machine-checkable selectors. It checks what the checkout does, not family class names: a missing family shell selector (a wrapper or column class, or the shipping field row marker) is a warning when the checkout form, its bound fields and a visible total all pass, and a failure when any of them does not. A missing SDK selector is always a warning-severity failure. See [QA and test orders](qa-and-test-orders.md).
|
|
98
99
|
|
|
99
100
|
## Synthetic Campaigns
|
|
100
101
|
|
|
@@ -335,14 +335,80 @@ If any current campaign, active/mapped page, source material, manifest or crawl
|
|
|
335
335
|
provenance, coverage, template family/reference, material fingerprint,
|
|
336
336
|
readiness, or readback check fails, `prepare-build` refuses. It leaves the
|
|
337
337
|
existing package and packet/context/report/brief sidecars byte-identical. It
|
|
338
|
-
does not silently regenerate or overwrite the package
|
|
339
|
-
|
|
338
|
+
does not silently regenerate or overwrite the package. The refusal names the
|
|
339
|
+
package file and says which recovery applies:
|
|
340
|
+
|
|
341
|
+
- **A package `prepare-build` synthesized itself.** The Assembly Report's
|
|
342
|
+
`design_source_package` reference records `origin`: `"synthesized"` when
|
|
343
|
+
`prepare-build` wrote the package bytes (or reused bytes it had written), and
|
|
344
|
+
`"adopted"` when it validated and reused a package it did not write. When the
|
|
345
|
+
previous report at this run's report path says `"synthesized"` and the
|
|
346
|
+
package bytes still hash to that report's `sha256`, the stale package is the
|
|
347
|
+
producer's own output: rerun with `--force` and `prepare-build` regenerates
|
|
348
|
+
it from the current inputs (mode `regenerated`), announcing the replacement
|
|
349
|
+
on stderr. `--force` also resets any stage evidence the Assembly Report
|
|
350
|
+
carries, exactly as it does for the report alone.
|
|
351
|
+
- **Any other package** — placed by an operator, edited by hand after it was
|
|
352
|
+
written, adopted by an earlier run, or recorded by a report that predates
|
|
353
|
+
`origin` — is never overwritten, `--force` or not. Reconcile it with the
|
|
354
|
+
current inputs, or, if no downstream stage has consumed it, delete it and
|
|
355
|
+
rerun so `prepare-build` synthesizes a fresh one.
|
|
356
|
+
|
|
357
|
+
Only the Assembly Report carries `origin`; the packet and context references
|
|
358
|
+
keep their four strict fields.
|
|
359
|
+
|
|
360
|
+
The package is published before the report that records it. So that a run
|
|
361
|
+
which fails or dies between the two does not leave its own package unprovable,
|
|
362
|
+
`prepare-build` first writes a pending provenance record beside the package
|
|
363
|
+
(`.campaign-runtime/input/.design-source-package.json.pending-provenance.json`)
|
|
364
|
+
naming the sha256 of the bytes it is about to publish, and removes it once the
|
|
365
|
+
Assembly Report is written. A `--force` run that replaces a stale package it
|
|
366
|
+
proved its own keeps that package's sha256 in the record too, so a
|
|
367
|
+
regeneration that fails or dies part way leaves whichever package is on disk
|
|
368
|
+
provable. A run that finds another writer's package at the path and adopts it
|
|
369
|
+
instead of publishing drops its own candidate from the record. A retry that
|
|
370
|
+
finds the record treats a package whose bytes still hash to one of its entries
|
|
371
|
+
as `"synthesized"`, exactly as if the report had recorded it; bytes changed
|
|
372
|
+
since are not vouched for, and a malformed record vouches for nothing. Just
|
|
373
|
+
before the report is published the record is narrowed to what the report
|
|
374
|
+
records (the package's hash when synthesized, nothing when adopted), so no
|
|
375
|
+
unpublished candidate outlives the report. The record's path is
|
|
376
|
+
reserved like the other outputs, so no configurable output may point at it.
|
|
377
|
+
|
|
378
|
+
Runs against the same target take turns. From the stage-evidence check and
|
|
379
|
+
the reading of its inputs (CampaignSpec, source manifest, page mappings, asset
|
|
380
|
+
crawl) through the packet, context and report that record them,
|
|
381
|
+
`prepare-build` holds a lock directory beside the package
|
|
382
|
+
(`.campaign-runtime/input/.design-source-package.json.lock`), so each run judges
|
|
383
|
+
provenance against the report and package the previous run left, and records
|
|
384
|
+
the inputs it actually read. With `--map-id`, the fetched spec is written to the
|
|
385
|
+
shared `fetched-specs/` cache file inside the same lock, so a run waiting for
|
|
386
|
+
the lock cannot replace the spec the lock holder is reading. The source-html
|
|
387
|
+
manifest is read once: the `manifest_sha256` the package records is the hash of
|
|
388
|
+
the exact bytes source intake parsed. Every command that edits the Assembly Report
|
|
389
|
+
(`doctor`, `qa run`, waivers, the polish merge and the other stage producers)
|
|
390
|
+
takes the same lock for its read-modify-write, so no stage evidence lands
|
|
391
|
+
between `prepare-build`'s final stage-evidence check and its publication; a
|
|
392
|
+
producer that `prepare-build` reaches from inside its own run enters directly.
|
|
393
|
+
Of several concurrent `--force` runs, the first regenerates the package and the
|
|
394
|
+
rest reuse it as `"synthesized"`. A command that finds the lock held waits up to
|
|
395
|
+
a minute. The lock directory and its owner record appear together, so a lock
|
|
396
|
+
left by a process that died is recovered automatically, and a lock directory
|
|
397
|
+
without an owner record (only an older release leaves one) is never taken
|
|
398
|
+
over: the command refuses it after about a second and names it. Confirm no
|
|
399
|
+
campaigns-os process is working on the target, then remove it. Do not run an
|
|
400
|
+
older Campaigns OS release against the same target at the same time. A
|
|
401
|
+
waiver's `--dry-run` preview takes no lock.
|
|
340
402
|
|
|
341
403
|
Before writing any output, `prepare-build` also requires distinct paths for the
|
|
342
404
|
Build Packet, Build Context, Assembly Report, Doctor output, normalized Build
|
|
343
405
|
Brief, and fixed Design Source Package. Equal paths and filesystem aliases are
|
|
344
406
|
rejected, including symlinks, hard links, dangling leaf symlinks, and symlinked
|
|
345
|
-
parent directories.
|
|
407
|
+
parent directories. No output may be placed inside the lock directory, or
|
|
408
|
+
inside the staging and tomb directories the lock creates beside it
|
|
409
|
+
(`.lock.staging-*`, `.lock.recovery-staging-*`, `.lock.released-*`,
|
|
410
|
+
`.lock.abandoned-*`), directly or through a directory alias: they are removed
|
|
411
|
+
with their contents when the run finishes.
|
|
346
412
|
|
|
347
413
|
This behavior is the implemented v0 compatibility boundary. It does not promise
|
|
348
414
|
that a separate future workflow command will generate, repair, approve, or
|
|
@@ -504,10 +570,12 @@ Either declaration records the page on the assembly report's
|
|
|
504
570
|
`template_family` set to the family the packet locks, lists it under
|
|
505
571
|
`stages.prepare_build.declared_out_of_scope`, and reaches
|
|
506
572
|
`stages.prepare_build.status: "completed_partial"`. Intake demands no design
|
|
507
|
-
source for the page: no `capture-*` TODO, no `link-*` TODO.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
573
|
+
source for the page: no `capture-*` TODO, no `link-*` TODO. This source-scope
|
|
574
|
+
classification does **not** authorize publishing a stock page. `next build`
|
|
575
|
+
keeps these routes unbuilt by default and requires explicit operator opt-in
|
|
576
|
+
per page before materializing a stand-in from the locked family. In particular,
|
|
577
|
+
presell and landing pages staying on another host must not be replaced with
|
|
578
|
+
placeholder copy. The family decides only *how* intake records coverage:
|
|
511
579
|
|
|
512
580
|
- **A family that publishes complete Template Reference proof — today `apollo`
|
|
513
581
|
alone** — covers the page with synthesized `template_baseline` coverage from
|
|
@@ -533,10 +601,10 @@ not built yet`, and keeps checkout launch and test-order proof blocked while a
|
|
|
533
601
|
runtime page (`select`, `checkout`, `upsell`, `receipt`) is among them. You get
|
|
534
602
|
a terminal, honest intake — not a fully proven campaign.
|
|
535
603
|
|
|
536
|
-
The build stage lifts those limits page by page.
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
604
|
+
The build stage lifts those limits page by page for opted-in pages. It uses
|
|
605
|
+
the locked family's own page of that role (an opted-in pre-checkout `select`
|
|
606
|
+
stand-in first, because it seeds the cart the runtime pages read).
|
|
607
|
+
Once the page's built HTML exists at its route under
|
|
540
608
|
`_site/<slug>/`, doctor reads the `template_stock` marker on the scope decision
|
|
541
609
|
and counts the page as built: it moves into `derived.scope.built_pages` (with
|
|
542
610
|
`template_stock: true`, `template_family`, and no `source_path`), joins the
|
|
@@ -565,10 +633,10 @@ family the `template-baseline` contribution's `presentation_intent` names.
|
|
|
565
633
|
|
|
566
634
|
### Recovery after a blocked first run
|
|
567
635
|
|
|
568
|
-
A blocked run still emits the package, and `prepare-build` never
|
|
569
|
-
package it did not just create. So a first run that blocked leaves a
|
|
570
|
-
whose provenance names the *old* manifest, and simply rerunning with a
|
|
571
|
-
manifest fails closed:
|
|
636
|
+
A blocked run still emits the package, and a plain `prepare-build` never
|
|
637
|
+
refreshes a package it did not just create. So a first run that blocked leaves a
|
|
638
|
+
package whose provenance names the *old* manifest, and simply rerunning with a
|
|
639
|
+
new manifest fails closed:
|
|
572
640
|
|
|
573
641
|
```
|
|
574
642
|
campaigns-os: Design Source Package at <target>/.campaign-runtime/input/design-source-package.json
|
|
@@ -597,6 +665,12 @@ npm run campaigns-os -- start \
|
|
|
597
665
|
--template-family <family>
|
|
598
666
|
```
|
|
599
667
|
|
|
668
|
+
When the refusal says the package was synthesized by an earlier
|
|
669
|
+
`prepare-build` and is unchanged since, skip step 2 and add `--force` to step 3
|
|
670
|
+
instead: `prepare-build` (and `start` and `build`, which run the same intake)
|
|
671
|
+
then regenerates the package itself. `--force` resets any stage evidence the
|
|
672
|
+
Assembly Report carries, so the same caution applies.
|
|
673
|
+
|
|
600
674
|
Step 2 is only ever correct for a package emitted by a blocked run that no
|
|
601
675
|
downstream stage has consumed. Once Build or Polish has bound its evidence to a
|
|
602
676
|
package fingerprint, deleting it invalidates that evidence; reconcile through
|
|
@@ -654,6 +728,79 @@ the packet file, so a packet resolves its source from the location it was
|
|
|
654
728
|
written at: replay a run from the same place, or expect doctor to report
|
|
655
729
|
`source_html.root` as missing.
|
|
656
730
|
|
|
731
|
+
### Hand-written HTML behind a Figma design source
|
|
732
|
+
|
|
733
|
+
A saved Map page with `design_source.type: figma` (or a `figma.com` file URL)
|
|
734
|
+
makes doctor demand figma-sections-export provenance from the source-html
|
|
735
|
+
manifest: a semantic `producer_provenance` block, section exports, a
|
|
736
|
+
material fingerprint, and the export's section partials and assets in
|
|
737
|
+
`files[]`. When the approved source for that page is hand-written HTML and the
|
|
738
|
+
Figma file only renders it, no export exists, and doctor reports the
|
|
739
|
+
`source_html.producer_provenance*`, `source_html.files.partial` and
|
|
740
|
+
`source_html.files.asset` errors. Leave `design_source` on the Map and take
|
|
741
|
+
this route instead:
|
|
742
|
+
|
|
743
|
+
1. Write the source-html manifest by hand (the record shape and the
|
|
744
|
+
`screenshots[]` proof are above). Give each page its `page_id` and `path`
|
|
745
|
+
and its desktop and mobile screenshot records, and list the page files in
|
|
746
|
+
`files[]` with `role: "page"`. Do not add `partial` or `asset` entries the
|
|
747
|
+
HTML does not have: the waiver covers their absence. Set `generator` to
|
|
748
|
+
the tool or person that wrote the HTML, never to `figma-sections-export`.
|
|
749
|
+
2. If the HTML files are full documents (`<!doctype>`, `<html>`, `<head>`,
|
|
750
|
+
`<body>`), set `"wrapper_policy": "preserve_document_wrappers"` in the
|
|
751
|
+
manifest so the wrapper finding is a recorded decision, not a blocker (see
|
|
752
|
+
[docs/source-adapters.md](./source-adapters.md#selecting-the-wrapper-policy-at-intake)).
|
|
753
|
+
3. Record a named-human waiver for each such page:
|
|
754
|
+
|
|
755
|
+
```bash
|
|
756
|
+
campaigns-os checkpoint waive \
|
|
757
|
+
--packet campaign-runtime.build.json \
|
|
758
|
+
--gate source_html.producer_provenance \
|
|
759
|
+
--page <page_id> \
|
|
760
|
+
--reason "<why the approved source is hand-written HTML>" \
|
|
761
|
+
--waived-by "<named human>" \
|
|
762
|
+
--expires-at <ISO timestamp>
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
`--page` is required, and it must name an active CampaignSpec page with a
|
|
766
|
+
Figma design source; any other id is refused and the refusal lists the pages
|
|
767
|
+
that qualify. The `<gate>:<page_id>` spelling is refused too. As with every
|
|
768
|
+
`checkpoint waive` gate, a placeholder name is refused, one bound
|
|
769
|
+
(`--expires-at` or `--review-condition`) is required, and `--dry-run`
|
|
770
|
+
validates the waiver and writes nothing. A value-taking flag given without
|
|
771
|
+
a value is refused, never read as the text `true`.
|
|
772
|
+
|
|
773
|
+
Doctor reports the Figma-export findings (the `source_html.producer_provenance*`
|
|
774
|
+
codes, `source_html.files.partial` and `source_html.files.asset`) once for each
|
|
775
|
+
Figma-typed page, with the page in `detail.page_id`. While a page's waiver is
|
|
776
|
+
active, its findings are reported as warnings carrying `waived: true`, its
|
|
777
|
+
checkpoint gate reports `waived`, and doctor and `next` report
|
|
778
|
+
`ready_with_waivers`, never clean. A waiver covers only its own page: another
|
|
779
|
+
Figma-typed page without one still gets the findings as errors, and doctor
|
|
780
|
+
stays blocked.
|
|
781
|
+
|
|
782
|
+
When the manifest's `generator` names `figma-sections-export` in any form
|
|
783
|
+
(with or without an `@<version>`, in any case), the manifest claims to be a
|
|
784
|
+
real export, so the missing provenance is the export's own defect: doctor
|
|
785
|
+
reports the findings once, manifest-wide, as errors, and each page's
|
|
786
|
+
checkpoint gate reports `blocked` with the code
|
|
787
|
+
`source_html.producer_provenance.exporter_claim` and the repair action.
|
|
788
|
+
`checkpoint waive` refuses the gate in that state, and a waiver recorded
|
|
789
|
+
before the generator changed is reported as inert.
|
|
790
|
+
|
|
791
|
+
The waiver covers the Figma-export findings only. Manifest validation, the
|
|
792
|
+
wrapper policy, source-preparation findings, page mappings and the screenshot
|
|
793
|
+
proof at `prepare-build` keep their own severity.
|
|
794
|
+
|
|
795
|
+
The waiver is recorded against the page's exact state: its design source and
|
|
796
|
+
the provenance findings doctor reported when it was recorded. A change to either
|
|
797
|
+
makes it stale, an expired waiver no longer applies, and doctor blocks again. A
|
|
798
|
+
waiver for a page that no longer has a Figma design source (its `design_source`
|
|
799
|
+
changed, or it left the spec), or one under a manifest whose generator claims
|
|
800
|
+
figma-sections-export, is reported as
|
|
801
|
+
`source_html.producer_provenance.waiver_inert`, a warning, like the other
|
|
802
|
+
checkpoint gates' inert waiver history.
|
|
803
|
+
|
|
657
804
|
## Lifecycle ownership and freshness
|
|
658
805
|
|
|
659
806
|
Prepare owns source normalization and the three package references. It records
|