@nextcommerce/campaigns-os 1.43.2 → 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.
Files changed (72) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +648 -5103
  3. package/README.md +32 -11
  4. package/agents/claude/CLAUDE.md +1 -1
  5. package/agents/codex/AGENTS.md +1 -1
  6. package/agents/copilot/copilot-instructions.md +1 -1
  7. package/agents/cursor/campaigns-os.mdc +1 -1
  8. package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +2 -2
  9. package/campaign-spec/dist/rules/analytics-contract-shape.js +2 -2
  10. package/campaign-spec/dist/rules/store-profile-shape.d.ts +5 -1
  11. package/campaign-spec/dist/rules/store-profile-shape.js +8 -10
  12. package/campaign-spec/dist/types.d.ts +2 -2
  13. package/contracts/archive/CHANGELOG.2026-09-30.md +5111 -0
  14. package/contracts/archive/release-ledger.2026-09-30.json +5068 -0
  15. package/contracts/effects.v1.json +1176 -113
  16. package/contracts/orientation-reason-codes.v1.json +7 -0
  17. package/contracts/release-ledger.json +2345 -6087
  18. package/contracts/supported-surface.json +7 -4
  19. package/contracts/template-slot-manifest.shared-content-core.v0.json +403 -0
  20. package/docs/brand-theme-bridge.md +81 -0
  21. package/docs/build-packet.md +158 -21
  22. package/docs/campaigns-os-build-flow.md +3 -3
  23. package/docs/design-source-package.md +73 -0
  24. package/docs/effects.md +50 -8
  25. package/docs/gateway-login.md +3 -0
  26. package/docs/local-setup.md +1 -1
  27. package/docs/orientation-contract-reference.md +42 -2
  28. package/docs/polish-evidence.md +74 -0
  29. package/docs/qa-and-test-orders.md +99 -13
  30. package/docs/release-ledger-authoring-guide.md +64 -4
  31. package/docs/runtime-readiness.md +1 -1
  32. package/docs/sdk-storage-compatibility.md +1 -1
  33. package/docs/skills-revision.md +10 -10
  34. package/docs/supported-surface.md +2 -2
  35. package/docs/versioning.md +4 -1
  36. package/package.json +1 -1
  37. package/schemas/campaigns-os-release-ledger.v1.schema.json +32 -2
  38. package/schemas/campaigns-os-tooling-orientation.v1.schema.json +1 -0
  39. package/skills/campaign-lifecycle-orientation/SKILL.md +16 -5
  40. package/skills/campaign-readback-classification/SKILL.md +3 -3
  41. package/skills/campaign-run-evidence/SKILL.md +7 -6
  42. package/skills/contribution-intake/SKILL.md +3 -3
  43. package/skills/next-campaigns-build/SKILL.md +7 -6
  44. package/skills/next-campaigns-os/SKILL.md +7 -7
  45. package/skills/next-campaigns-os/references/session-intake.md +9 -3
  46. package/skills/next-campaigns-os-setup/SKILL.md +5 -5
  47. package/skills/next-campaigns-polish/SKILL.md +28 -9
  48. package/skills/next-campaigns-qa/SKILL.md +7 -4
  49. package/skills.json +10 -10
  50. package/src/brand-theme.mjs +320 -20
  51. package/src/built-site-scope.mjs +16 -4
  52. package/src/cli.mjs +280 -46
  53. package/src/commercial-parity.mjs +48 -2
  54. package/src/deviation.mjs +13 -1
  55. package/src/diagnostic.mjs +5 -2
  56. package/src/doctor/checks.mjs +320 -81
  57. package/src/doctor/inspect.mjs +55 -13
  58. package/src/doctor/source-provenance.mjs +184 -0
  59. package/src/invocation.mjs +4 -0
  60. package/src/live-campaign-refs.mjs +466 -0
  61. package/src/login.mjs +2 -2
  62. package/src/page-kit-store-profile.mjs +69 -12
  63. package/src/page-kit-sync.mjs +31 -12
  64. package/src/progress-node.mjs +3 -1
  65. package/src/qa-browser.mjs +538 -28
  66. package/src/qa-commercial-parity.mjs +48 -5
  67. package/src/qa-node.mjs +122 -7
  68. package/src/qa-test-order-topology.mjs +148 -0
  69. package/src/sdk-markup.mjs +72 -8
  70. package/src/source-html-intake.mjs +116 -0
  71. package/src/stage-record.mjs +551 -0
  72. package/src/upsell-selector-scope.mjs +112 -2
@@ -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-ruggie`, `/receipt` with no slug prefix,
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 (doctor's
146
- `target_only` warning still applies), non-governed keys keep their values and
147
- order, other routes and other files are not written. The file is edited in
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: six shapes of `data-next-*` markup that the Campaign Cart SDK binds
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 used, kept so
732
- the two vocabularies line up; each doctor issue is `built_output.sdk_markup.`
733
- plus the code lower-cased, and its message leads with the code.
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
 
@@ -1038,9 +1070,9 @@ fingerprint is `sha256:` plus the SHA-256 of that manifest. Nothing is excluded
1038
1070
  default (Page Kit writes only rendered HTML and copied assets into `_site/`, nothing
1039
1071
  it timestamps); `.campaign-runtime/page-kit-build-summary.json` lives outside the
1040
1072
  root and is not hashed. Build does not type the value: after page-kit build it runs
1041
- `campaigns-os doctor --packet <packet> --json` and copies
1042
- `derived.build_output_fingerprint.value` (with `.file_count` and `.status`) onto
1043
- `stages.assembly.build_fingerprint`. Doctor recomputes the value on every run
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
1044
1076
  (`built_output.fingerprint`): a match is a ready line, a missing record is the
1045
1077
  warning `built_output.fingerprint_missing` carrying the value to record, and a
1046
1078
  recorded value the output no longer matches is `built_output.fingerprint_stale`
@@ -1064,9 +1096,11 @@ Design Source Package exists. The waiver must remain visible in Campaign
1064
1096
  Readiness Readback and downstream QA evidence; it is not a silent pass.
1065
1097
  In v0, write accepted Source Freshness Waivers directly into `waivers[]`.
1066
1098
  `campaigns-os checkpoint waive` is a staged generic registry and currently
1067
- accepts four gates: `page_kit.store_profile`, `page_kit.sdk_version`,
1068
- `polish.hidden_eager_media`, and `built_output.upsell_selector_scope`; an
1069
- unregistered gate id is refused with that list. `theme waive` applies the same
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
1070
1104
  attribution rule (a named human, no placeholder, an optional future
1071
1105
  `--expires-at`) on its own lane. Within Polish, only the broader Source Freshness
1072
1106
  waiver retains its existing report path; theme and QA decisions retain their
@@ -1225,13 +1259,13 @@ and report proof policy fields above.
1225
1259
  | `--spec <path>` | Local JSON file | Agent-authored local specs, saved-Map exports, offline work or CI fixtures |
1226
1260
  | `--map-id <id>` | Map Builder proxy (KV-backed) | Saved-Map intake from the current KV revision |
1227
1261
 
1228
- 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.
1229
1263
 
1230
1264
  Saved-Map retrieval behavior (`--map-id`):
1231
1265
 
1232
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.
1233
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.
1234
- - **`--cached-spec`** reuses the cache without a network call. Use for offline iteration or when the proxy is temporarily unreachable.
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.
1235
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).
1236
1270
  - Failure modes (HTTP error, `{ok: false}` response, network timeout) surface as clean CLI errors before any packet is written.
1237
1271
 
@@ -1321,6 +1355,70 @@ routes during projection. That normalization strips `.html`/`index.html`,
1321
1355
  removes query/fragment values, converts absolute preview URLs to their path, and
1322
1356
  normalizes trailing slashes before deriving target files and frontmatter routes.
1323
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
+
1324
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.
1325
1423
 
1326
1424
  `target_path` intentionally uses the terminal route segment (`checkout/step-1/`
@@ -1477,10 +1575,49 @@ The motion:
1477
1575
 
1478
1576
  ```text
1479
1577
  agent calls `next` → gets { stage, prompt, picked_reason } → does the work →
1480
- updates assembly report's stages.<name>.status → calls `next` again →
1481
- 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"
1482
1580
  ```
1483
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
+
1484
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.
1485
1622
 
1486
1623
  | Stage | Report key | Owner |
@@ -1523,8 +1660,8 @@ The legacy form `campaigns-os next <stage>` (e.g. `next build`) still works and
1523
1660
 
1524
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:
1525
1662
 
1526
- - `design_source.type === "figma"` with `file_url`: doctor calls out the Figma file and the figma-sections-export handoff command (`npm run handoff -- <slug>`).
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.
1527
1664
  - `design_source` set without `file_url`: doctor flags the missing `file_url` so the spec can be corrected.
1528
- - `design_source` unset: doctor keeps the original generic coverage error.
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.
1529
1666
 
1530
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,10 +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 four 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`, `DOUBLE_SELECTED`, `TEMPLATE_DOUBLE_BRACE`).
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
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.
88
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.
89
- - If source HTML declares SDK-owned zones such as `data-commerce-zone="checkout-form"` or `data-commerce-zone="order-summary"`, adopt the selected starter-template family shell for that runtime page. Do not build a custom checkout/upsell structure around a few borrowed includes; browser QA will check declared family structure where `agentContract.qaStructure` exists.
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.
90
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.
91
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.
92
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/`.
@@ -95,7 +95,7 @@ the assembly report.
95
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.
96
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.
97
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.
98
- - Browser QA also checks template-family commerce structure when the family contract declares machine-checkable selectors. Missing required Limos checkout shell markers, for example, are treated as a warning-severity failure: the checkout may load, but it is not proven as a conformant Limos checkout.
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).
99
99
 
100
100
  ## Synthetic Campaigns
101
101
 
@@ -728,6 +728,79 @@ the packet file, so a packet resolves its source from the location it was
728
728
  written at: replay a run from the same place, or expect doctor to report
729
729
  `source_html.root` as missing.
730
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
+
731
804
  ## Lifecycle ownership and freshness
732
805
 
733
806
  Prepare owns source normalization and the three package references. It records
package/docs/effects.md CHANGED
@@ -38,8 +38,10 @@ that already understands those hints needs no translation layer.
38
38
  **`readOnlyHint` counts the command-lifecycle journal.** A journal append is a
39
39
  write like any other, so every `readOnlyHint: true` row is an invocation the
40
40
  CLI exempts from lifecycle capture (the converse does not hold: `demo` and the
41
- `--no-write` forms skip the journal but still write other declared files): `help`, `readback`,
42
- `run status`, `doctor` inspection, `doctor --no-write`, `sdk storage-check`,
41
+ `--no-write` forms skip the journal but still write other declared files, and
42
+ `doctor` inspection and `doctor --no-write` skip it and write nothing but may
43
+ send the one live campaign read): `help`, `readback`,
44
+ `run status`, `doctor --no-live-refs` inspection, `sdk storage-check`,
43
45
  `tooling diagnose`, a refused invocation, `run-record --no-write`, and every
44
46
  `--dry-run` form on the commands that implement the flag. Everything else
45
47
  appends an entry when a journal is selected — an active run session,
@@ -81,6 +83,46 @@ writes your **home** directory, not the campaign. `telemetry on` writes your
81
83
  **machine** config. `run-record` writes beside the **working directory**, not the
82
84
  target repo. A row that said "writes the target" would be wrong about all three.
83
85
 
86
+ ### The Map Builder fetch
87
+
88
+ `start`, `prepare-build` and `build` take their CampaignSpec from `--spec` or
89
+ from `--map-id`. With `--map-id` (and no `--cached-spec`) the invocation sends
90
+ `{proxy-base}/api/spec/{map-id}` and writes what comes back to
91
+ `{target}/.campaign-runtime/fetched-specs/<map-id>.json`, replacing any earlier
92
+ copy of that Map; the intake then reads the spec from that file. Both effects
93
+ are on every row for those three commands. The effect test runs each row with
94
+ `--spec` under four conditions and with `--map-id` under `persisted_consent`,
95
+ the one condition whose `--proxy-base` is the loopback receiver, so the fetch
96
+ and the write are observed rather than taken on trust.
97
+
98
+ ### The live campaign read
99
+
100
+ `doctor --packet` (with or without `--write` / `--no-write`) and every `qa run`
101
+ form check each page's shipping and package refs against the live campaign.
102
+ Doctor reads when the packet's built `_site/<route>/` exists; QA reads when it
103
+ has read at least one served page. Both need a public Campaigns API key from
104
+ the packet, its local CampaignSpec or the declared campaign-key env var. The
105
+ read is one `GET {proxy-base}/api/campaign` with the key in `X-Campaign-Key`,
106
+ plus `?ref_id=<id>` when the CampaignSpec's `campaign.ref_id` names the
107
+ campaign. No store or Admin credential is used. The proxy's answer is an
108
+ envelope whose `data` field holds the campaign. A failed read, an `ok: false`
109
+ envelope, one with an `error` and no campaign, or several campaigns with no
110
+ `campaign.ref_id` to pick one is recorded as `not_run` with its reason, never
111
+ as a pass. `--no-live-refs` on `doctor` or `qa run` skips only this read and
112
+ records `not_run` with reason `disabled`; every other declared write and send
113
+ is unchanged. Each form has its own row, including `doctor --write
114
+ --no-live-refs` and `qa run --no-live-refs` with each `qa run` modifier.
115
+ `doctor --no-write --no-live-refs` has the effects of the `doctor
116
+ --no-live-refs` row.
117
+
118
+ Only `doctor` and `qa run` make this read. The other commands that run doctor
119
+ internally make no request and record `derived.live_campaign_refs` as
120
+ `not_run` with reason `not_read`, with no warning: `start`, `prepare-build`
121
+ and `build` (the intake alias for prepare-build + doctor), `theme waive`,
122
+ `checkpoint`, `findings harvest`, `run-record`, `next`, and the doctor
123
+ sidecar refresh `qa run` makes after it records the QA stage (the verdict
124
+ itself carries QA's own read).
125
+
84
126
  ## How to read a row
85
127
 
86
128
  ```jsonc
@@ -108,7 +150,7 @@ There is **one row per command and per effect-changing flag combination**. The
108
150
  flags that change what the invocation does to the world are listed once, in
109
151
  `vocabulary.effect_changing_flags`: `--browser`, `--built`, `--dry-run`,
110
152
  `--emit-packet`, `--example`, `--force`, `--from-store`, `--list`,
111
- `--no-post-verdict`, `--no-probe`, `--no-remit`, `--no-run-session`,
153
+ `--no-live-refs`, `--no-post-verdict`, `--no-probe`, `--no-remit`, `--no-run-session`,
112
154
  `--no-write`, `--republish`, `--test-order`, `--write`, `--write-map`. Flags
113
155
  that only change the output shape (`--json`, `--report`) deliberately do not.
114
156
 
@@ -217,7 +259,7 @@ sha256) before and after while a loopback `node:http` receiver counts requests.
217
259
  | `ambient_session` | An active ambient run session opened by `run start` at the target. |
218
260
  | `stale_session` | A run session idle past the 12 h TTL, at the target and at the working directory. |
219
261
  | `lifecycle_log` | `CAMPAIGNS_OS_LIFECYCLE_LOG` names a journal outside the runtime directory. |
220
- | `persisted_consent` | Run Telemetry consent **persisted on the machine for the loopback receiver's scope**, a synthetic campaign key in the environment, no run session, and `--proxy-base <loopback>` wherever the command takes it. |
262
+ | `persisted_consent` | Run Telemetry consent **persisted on the machine for the loopback receiver's scope**, a synthetic campaign key in the environment, no run session, and `--proxy-base <loopback>` wherever the command takes it. `start`, `prepare-build` and `build` name their spec by `--map-id` here, and the receiver serves it. |
221
263
 
222
264
  Five conditions rather than one, because the CLI's effects are not a function of
223
265
  argv alone: an ambient session redirects the journal and is itself touched by
@@ -319,10 +361,10 @@ loopback receiver only stands in for (`{base-url}`, the login gateway) matched
319
361
  refuses the contradiction rather than letting the test find it.
320
362
 
321
363
  A `full` row may still carry an individual effect the offline fixture cannot
322
- reach — the Map Builder spec fetch behind `--map-id`, the `codex` and `agents`
323
- destinations of `install-skills`. Each such entry has an empty `observed_in`
324
- **and** a `not_observed_reason`, and `check-effects.mjs` refuses one without the
325
- reason. What it may not be is silent.
364
+ reach — the `codex` and `agents` destinations of `install-skills`. Each such
365
+ entry has an empty `observed_in` **and** a `not_observed_reason`, and
366
+ `check-effects.mjs` refuses one without the reason. What it may not be is
367
+ silent.
326
368
 
327
369
  ## The rule
328
370
 
@@ -17,6 +17,9 @@ campaigns-os login --store example
17
17
  There is no discovery or guess from the current project. Noninteractive calls
18
18
  must supply `--store`. URLs, paths and unrelated hosts are refused before any
19
19
  request. Login uses the fixed `https://mcp.nextcommerce.com` gateway.
20
+ It asks for the `https://mcp.nextcommerce.com/mcp` resource and the
21
+ `campaigns.read` capability. A login saved before 1.43.2+agent.3, which named
22
+ the earlier `/campaigns` resource, is refused; sign in again.
20
23
 
21
24
  Open the displayed device page in one browser tab and enter the displayed code.
22
25
  Keep that tab: if installation is needed, follow its Install Campaigns link,
@@ -3,7 +3,7 @@
3
3
  For a new campaign, choose its working folder and run this from that folder:
4
4
 
5
5
  ```sh
6
- npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.43.2 next-campaign-page-kit@0.2.0 && npx --no-install campaigns-os tooling setup --target . --platform claude
6
+ npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.46.0 next-campaign-page-kit@0.2.0 && npx --no-install campaigns-os tooling setup --target . --platform claude
7
7
  ```
8
8
 
9
9
  Review the release source/provenance before installation as described in
@@ -9,6 +9,7 @@
9
9
  contracts/orientation-reason-codes.v1.json
10
10
  contracts/orientation-limits.v1.json
11
11
  contracts/supported-surface.json
12
+ contracts/release-ledger.json (baseline_floor only)
12
13
  contracts/fixtures/orientation/canonicalization/v1.json
13
14
  Regenerate: node ./scripts/generate-orientation-reference.mjs --write
14
15
  CI runs the same script with --check, so a stale copy of this file fails the build.
@@ -25,7 +26,7 @@ Ledger schema id: `campaigns-os-release-ledger/v1`
25
26
  Change policy version: `1.0.0`
26
27
  Reason-code vocabulary version: `1.0.0`
27
28
  Limits version: `1.0.0`
28
- Supported surface at generation time: `1.43.2`
29
+ Supported surface at generation time: `1.46.0`
29
30
 
30
31
  ## Forward compatibility
31
32
 
@@ -60,6 +61,43 @@ position, so a consumer reading history in array order reads it in `sequence` or
60
61
  its number to `sequence`; the two diverge legitimately, because when concurrent pull requests
61
62
  land the later one restamps its `sequence` to follow the earlier while keeping the id it was
62
63
  written with. Order by `sequence`, identify by `id`, and do not infer one from the other.
64
+ After a baseline rotation the file starts at `baseline_floor.last_archived_sequence + 1`, not at 1;
65
+ entries are never renumbered, so position plus that offset is still the sequence.
66
+
67
+ ## Baseline rotation
68
+
69
+ The ledger and the changelog are bounded as whole files, so they are rotated rather than allowed to
70
+ outgrow the limits. A rotation moves every entry up to a reviewed cut, and the changelog from the first section
71
+ those entries link to the end of the file (unlinked sections in that range included), byte-for-byte and in order
72
+ into a dated archive pair under `contracts/archive/`, and declares the cut as
73
+ `baseline_floor` in `contracts/release-ledger.json`. Archived entries keep their `sequence`, `entry_sha256` and
74
+ `changelog_sha256`; each section hash verifies against the archive changelog. The floor names the archive
75
+ files with their SHA-256, the last archived entry and its sequence, the first kept entry, the live entry that
76
+ recorded the rotation, and the reason code to refuse with. The floor only moves forward, only with a new
77
+ rotation entry, and an archive file never changes once merged; the next rotation writes a new dated pair.
78
+
79
+ Rotation changes no mandatory read. The reading order in `AGENTS.md` is unchanged, and `source_bytes` measures
80
+ exactly its data files (steps 1 to 8):
81
+
82
+ - `contracts/supported-surface.json`
83
+ - `contracts/release-ledger.json`
84
+ - `CHANGELOG.md`
85
+ - `contracts/orientation-limits.v1.json`
86
+ - `contracts/orientation-reason-codes.v1.json`
87
+ - `schemas/campaigns-os-tooling-orientation.v1.schema.json`
88
+ - `schemas/campaigns-os-release-ledger.v1.schema.json`
89
+ - `contracts/agent-relevant-change-policy.v1.json`
90
+
91
+ The archive files are not among them: they are optional history reads and count against no limit, and their
92
+ sections and entries are not in `section_count` or `ledger_entries`.
93
+
94
+ Current floor: last archived entry `RL-0124` (sequence 124), first kept entry
95
+ `RL-0125`, recorded by `RL-0190`. Archives, oldest rotation first:
96
+
97
+ - [`contracts/archive/release-ledger.2026-09-30.json`](../contracts/archive/release-ledger.2026-09-30.json) and [`contracts/archive/CHANGELOG.2026-09-30.md`](../contracts/archive/CHANGELOG.2026-09-30.md)
98
+
99
+ A consumer whose reviewed baseline's newest ledger entry is older than `RL-0124` refuses with
100
+ `baseline_below_floor`. Adopt a newer reviewed baseline whose ledger reaches the floor's last_archived_id; every commit at or after the floor's first_kept_id qualifies. Do not orient on the partial live window, and do not stitch the archive in as a substitute: the archive holds the history for reference, not for this read.
63
101
 
64
102
  ## Release-ledger digest canonicalization
65
103
 
@@ -120,6 +158,7 @@ consumer's parser tests.
120
158
  |---|---|---|---|---|---|
121
159
  | `ahead_of_upstream` | `refused` | campaigns-agent | `TP-D4-ahead-of-upstream` | The checkout carries commits the upstream default branch does not, so its contracts are not a published generation. | Push or drop the local commits, or run against a managed generation at the published OID. |
122
160
  | `already_current` | `current` | campaigns-agent | `TP-B3-already-current` | The verified target equals the active generation; nothing changed and no restart is required. | None. |
161
+ | `baseline_below_floor` | `refused` | campaigns-os | `A1-baseline-below-floor` | The target ledger declares a baseline_floor, and the reviewed baseline's newest ledger entry is older than the floor's last archived entry. Part of the baseline-to-target window was rotated into the archive files the floor names, which are not mandatory orientation reads, so the window cannot be read from the live ledger and changelog. | Adopt a newer reviewed baseline whose ledger reaches the floor's last_archived_id; every commit at or after the floor's first_kept_id qualifies. Do not orient on the partial live window, and do not stitch the archive in as a substitute: the archive holds the history for reference, not for this read. |
123
162
  | `checkout_acquisition_failed` | `refused` | campaigns-agent | `TP-D1-acquisition-failed` | Managed acquisition did not complete: interrupted clone, remote validation failure, or a broken linked-worktree backpointer. Partial state is quarantined, never exposed as ready. | Rerun acquisition. If it fails repeatedly, inspect the quarantined partial named in the diagnostic and confirm remote reachability. |
124
163
  | `checkout_already_exists` | `refused` | campaigns-agent | `TP-D1-checkout-already-exists` | The managed root reserved for acquisition appeared concurrently and is not an empty reservation this run owns. | Rerun so the existing root is validated as a managed store, or remove the unexpected directory after confirming it holds no needed state. |
125
164
  | `checkout_missing` | `refused` | campaigns-agent | `TP-D1-checkout-missing` | No checkout exists at the configured location and the mode does not authorize acquiring one. | Supply the operator checkout at the configured path, or switch to managed mode so a generation can be acquired. |
@@ -205,7 +244,7 @@ nowhere in the schemas, or in the schemas and not here, is a generation failure.
205
244
  | `$defs.baseline.properties.kind` | `legacy_commit`, `supported_surface` |
206
245
  | `$defs.change_class` | `schema`, `hashed_surface`, `named_surface`, `cli_surface`, `skill`, `package_export`, `compatibility_policy`, `documentation`, `workflow`, `generated_runtime` |
207
246
  | `$defs.disposition` | `current`, `orientation_available`, `updated`, `restart_required`, `recovered_interrupted_update`, `legacy_baseline`, `freshness_unknown`, `refused` |
208
- | `$defs.reason_code` | `already_current`, `ahead_of_upstream`, `checkout_acquisition_failed`, `checkout_already_exists`, `checkout_missing`, `checkout_mutation_not_authorized`, `detached_head`, `dirty_checkout`, `diverged_history`, `evidence_budget_exceeded`, `fetch_failed`, `git_environment_unsafe`, `history_incomplete`, `missing_upstream`, `orientation_contract_missing`, `orientation_in_progress`, `orientation_incomplete`, `orientation_rendered`, `orientation_too_large`, `pointer_race`, `runtime_commit_mismatch`, `runtime_refresh_failed`, `runtime_refresh_required`, `surface_incompatible`, `transaction_incomplete`, `transaction_reconciled`, `wrong_remote` |
247
+ | `$defs.reason_code` | `already_current`, `ahead_of_upstream`, `baseline_below_floor`, `checkout_acquisition_failed`, `checkout_already_exists`, `checkout_missing`, `checkout_mutation_not_authorized`, `detached_head`, `dirty_checkout`, `diverged_history`, `evidence_budget_exceeded`, `fetch_failed`, `git_environment_unsafe`, `history_incomplete`, `missing_upstream`, `orientation_contract_missing`, `orientation_in_progress`, `orientation_incomplete`, `orientation_rendered`, `orientation_too_large`, `pointer_race`, `runtime_commit_mismatch`, `runtime_refresh_failed`, `runtime_refresh_required`, `surface_incompatible`, `transaction_incomplete`, `transaction_reconciled`, `wrong_remote` |
209
248
 
210
249
  ### `schemas/campaigns-os-release-ledger.v1.schema.json`
211
250
 
@@ -225,6 +264,7 @@ so a renamed command fails here as well as at the supported-surface gate.
225
264
  - `campaigns-os prepare-build`
226
265
  - `campaigns-os build`
227
266
  - `campaigns-os polish`
267
+ - `campaigns-os record`
228
268
  - `campaigns-os checkpoint`
229
269
  - `campaigns-os page-kit`
230
270
  - `campaigns-os spec`