@nextcommerce/campaigns-os 1.43.1 → 1.43.2

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 (56) hide show
  1. package/AGENTS.md +4 -2
  2. package/CHANGELOG.md +451 -0
  3. package/README.md +2 -2
  4. package/contracts/agent-relevant-change-policy.v1.json +5 -0
  5. package/contracts/effects.v1.json +8 -8
  6. package/contracts/release-ledger.json +672 -0
  7. package/contracts/supported-surface.json +2 -2
  8. package/docs/build-packet.md +64 -2
  9. package/docs/campaigns-os-build-flow.md +1 -0
  10. package/docs/design-source-package.md +89 -15
  11. package/docs/effects.md +16 -4
  12. package/docs/local-setup.md +1 -1
  13. package/docs/orientation-contract-reference.md +1 -1
  14. package/docs/progress-snapshots.md +10 -6
  15. package/docs/qa-and-test-orders.md +131 -7
  16. package/docs/release-ledger-authoring-guide.md +6 -4
  17. package/docs/runtime-readiness.md +1 -1
  18. package/docs/skills-revision.md +10 -10
  19. package/package.json +1 -1
  20. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  21. package/skills/campaign-readback-classification/SKILL.md +3 -3
  22. package/skills/campaign-run-evidence/SKILL.md +3 -3
  23. package/skills/contribution-intake/SKILL.md +3 -3
  24. package/skills/next-campaigns-build/SKILL.md +4 -4
  25. package/skills/next-campaigns-os/SKILL.md +3 -3
  26. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  27. package/skills/next-campaigns-polish/SKILL.md +3 -3
  28. package/skills/next-campaigns-qa/SKILL.md +3 -3
  29. package/skills.json +10 -10
  30. package/src/build-brief.mjs +6 -4
  31. package/src/built-script-syntax.mjs +480 -0
  32. package/src/campaigns-api-key.mjs +99 -0
  33. package/src/cli-helpers.mjs +118 -0
  34. package/src/cli.mjs +1211 -7495
  35. package/src/design-source-package.mjs +1 -1
  36. package/src/design-source-publication.mjs +898 -0
  37. package/src/diagnostic.mjs +2 -1
  38. package/src/directory-lock.mjs +270 -0
  39. package/src/doctor/checks.mjs +4415 -0
  40. package/src/doctor/inspect.mjs +636 -0
  41. package/src/doctor/next-step.mjs +731 -0
  42. package/src/install-invocation.mjs +29 -0
  43. package/src/invocation.mjs +179 -0
  44. package/src/private-template-source.mjs +1 -1
  45. package/src/progress-node.mjs +6 -35
  46. package/src/proof-policy.mjs +1 -1
  47. package/src/qa-analytics-correctness.mjs +3 -0
  48. package/src/qa-binding-evidence.mjs +76 -11
  49. package/src/qa-browser.mjs +778 -77
  50. package/src/qa-build-scope.mjs +47 -0
  51. package/src/qa-node.mjs +218 -13
  52. package/src/source-html-intake.mjs +1 -1
  53. package/src/source-html-manifest.mjs +9 -2
  54. package/src/stage-ledger.mjs +28 -0
  55. package/src/target-lock.mjs +54 -0
  56. package/src/template-brand-contract.mjs +17 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_note": "The downstream contract manifest. Everything listed here is SUPPORTED SURFACE: consumers (campaigns-agent, campaign-builder, the private ops repo, page-kit campaign repos) may depend on it, and changing it is a deliberate act — hashed entries require a surface_version bump in the same change (check-supported-surface.mjs --base, mirroring the skills.json bump gate), named entries must keep existing at their path, cli_commands must keep resolving in the CLI dispatch, package_exports must stay exported, and every entry must ship in the npm pack (files[] coverage). Anything NOT listed here — src/** internals, scripts/** checkers, examples/**, prompts/**, contracts/** other than this file and the entries named[] below (the orientation contract, the release ledger, and the consumer-facing orientation fixtures) — is implementation: consumers may read it for context but must not build on it, and it can change without notice. Rationale and the compatibility promise: docs/supported-surface.md.",
3
- "surface_version": "1.43.1",
3
+ "surface_version": "1.43.2",
4
4
  "package_exports": [
5
5
  "./commercial-journey",
6
6
  "./commercial-parity",
@@ -119,7 +119,7 @@
119
119
  "sha256": "dfc9abed38d456969e47a21f606d308a03bdf036f3466f7d6e47a602747dacf4"
120
120
  },
121
121
  "contracts/effects.v1.json": {
122
- "sha256": "121779835c14d8d48390c57268b50a60fe808f5d9ba91ea2dde5df22a64f8aaf"
122
+ "sha256": "1dae66e70bb5d167ca407d4ad03e70a1e2e761407b8ad7b85c8a7a51456df858"
123
123
  },
124
124
  "schemas/campaigns-os-effects.v1.schema.json": {
125
125
  "sha256": "3eadd22169ab98bc7c2f682af2751267605170581182158d96be035b0cfe44dc"
@@ -783,6 +783,64 @@ advisories, `unknown_attributes[]`, `pages_scanned`,
783
783
  It passes, with no advisory, on the canonical rendered output of every
784
784
  certified starter family (`fixtures/certified-families/`).
785
785
 
786
+ ### Built-output script syntax gate (`built_output.script_syntax`)
787
+
788
+ Every doctor run that sees built output (the packet path and `doctor --built`
789
+ alike) parses each campaign-owned `.js` file a built page loads by a local
790
+ `<script src>`. A script that does not parse throws a `SyntaxError` on every
791
+ load of every page that references it, and nothing it defines runs; every
792
+ HTML-reading gate passes over it. The shape that shipped was a template-family
793
+ checkout script, copied and hand-edited, left with one closing `});` too many.
794
+
795
+ Parsing uses Acorn at the latest `ecmaVersion`: `sourceType: 'script'` for
796
+ classic scripts and `'module'` for `type="module"`, which is how the browser
797
+ reads each. Remote scripts (an `http(s):` URL, a protocol-relative `//` URL,
798
+ `data:`) are not campaign-owned and are not read, and neither are data blocks
799
+ such as JSON-LD. The type is compared as the browser compares it, with
800
+ surrounding ASCII whitespace stripped and case ignored. A classic `nomodule`
801
+ script is skipped: a module-capable browser never fetches or runs it. A
802
+ `type="module"` script ignores `nomodule` and is still parsed. Each src
803
+ resolves the way the browser resolves it: against the base in effect when the
804
+ parser prepares the script at its end tag, which is the first HTML `<base
805
+ href>` in tree order among those already parsed, or else the page. A `<base>`
806
+ parsed after a script does not move it, whether it is async, deferred or a
807
+ module: its URL is fixed when it is prepared, not when it is fetched. Parse
808
+ order decides, not final tree position, so a base that table foster parenting
809
+ moves ahead of an earlier script still does not apply to it. A `base` inside
810
+ SVG or MathML is not a base element, and only HTML-namespace `<script>`
811
+ elements are read: an SVG `<script>` never loads a `src` attribute. The href is read as the URL parser reads
812
+ it: only leading and trailing ASCII control characters and spaces are
813
+ stripped. A base
814
+ the browser refuses (one that does not parse, or a `data:` or `javascript:`
815
+ URL) falls back to the page, as the HTML "set the frozen base URL" steps
816
+ require. The percent-decoded path maps under the site root first, then the
817
+ campaign directory, never outside either. A base on another origin makes
818
+ relative srcs remote. Imports inside a module are not followed.
819
+
820
+ A parse failure blocks (not waivable — a script that cannot be parsed cannot be
821
+ intended to ship) under `built_output.script_syntax.parse_failure`, one error
822
+ per file. The message leads with `<file>:<line>:<column>` and a fixed
823
+ diagnostic category (for example `Unexpected token` or `Invalid regular
824
+ expression`), never text from the script, and names the pages that load the
825
+ file. A referenced local script that is not in the built output is a warning,
826
+ not a blocker, under `built_output.script_syntax.missing_script`, one warning
827
+ per src naming the pages that load it: the browser gets a 404 for it and
828
+ nothing it would define runs, but whether the page needs it is not known here.
829
+ The src is also listed in `scripts_unresolved[]`. While a parse failure blocks
830
+ the gate, the missing scripts stay on the gate's `warned[]` rather than also
831
+ surfacing as warnings.
832
+
833
+ The gate's evidence lands beside the other checkpoint gates at
834
+ `derived.checkpoint_gates[]` (`id: built_output.script_syntax`, status `pass` |
835
+ `blocked` | `not_applicable`, `findings[]` with `file`, `line`, `column`,
836
+ `source_type` and `pages`, `warned[]` with `src` and `pages`,
837
+ `scripts_scanned`, `scripts_unresolved[]`, `pages_scanned`). Fixtures: `fixtures/script-syntax/{good,bad}`. It passes, parsing every
838
+ local script the pages load and with no missing-script warning, on the
839
+ canonical rendered output of every certified starter family
840
+ (`fixtures/certified-families/`). QA applies the same rule to the page scripts
841
+ it reads for credential declarations (`script-parse:<page_id>`; see
842
+ [QA and test orders](qa-and-test-orders.md)).
843
+
786
844
  > **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
845
 
788
846
  ## Artifact Locations
@@ -1172,6 +1230,7 @@ When `--map-id <id>` is set, the CLI fetches `GET <proxy>/api/spec/<id>` (defaul
1172
1230
  Saved-Map retrieval behavior (`--map-id`):
1173
1231
 
1174
1232
  - **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
+ - **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.
1175
1234
  - **`--cached-spec`** reuses the cache without a network call. Use for offline iteration or when the proxy is temporarily unreachable.
1176
1235
  - **`--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
1236
  - Failure modes (HTTP error, `{ok: false}` response, network timeout) surface as clean CLI errors before any packet is written.
@@ -1180,7 +1239,7 @@ The fetched spec is treated identically to a `--spec`-supplied local file from t
1180
1239
 
1181
1240
  ## Source HTML Manifest Auto-Population
1182
1241
 
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)).
1242
+ 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
1243
 
1185
1244
  The source-html manifest remains a producer/source-HTML adapter input. It is not
1186
1245
  renamed into the Design Source Package. In the normalized source workflow,
@@ -1190,7 +1249,10 @@ contributions, coverage, gaps/TODOs, Surface Identity, references, and readback.
1190
1249
  When source-html data is the available input and the default package path is
1191
1250
  missing, current v0 `prepare-build` synthesizes the package. If a package already
1192
1251
  exists, it is validated against the current material inputs and reused byte for
1193
- byte or refused; it is never silently regenerated. Downstream Build and Polish
1252
+ byte or refused; it is never silently regenerated. The one exception is a stale
1253
+ package an earlier `prepare-build` synthesized and nobody has changed since:
1254
+ `--force` regenerates it from the current inputs
1255
+ ([Design Source Package: stale packages](design-source-package.md#prepare-build-emit-validate-or-refuse)). Downstream Build and Polish
1194
1256
  consume the package concept rather than branching back to
1195
1257
  `packet.source_html` as a second source model. The emitted package lives at
1196
1258
  `.campaign-runtime/input/design-source-package.json` by default and is referenced
@@ -84,6 +84,7 @@ the assembly report.
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
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`).
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
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
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.
@@ -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; source preparation must
339
- reconcile the package and its references explicitly before retrying.
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. The build stage
508
- materialises it from the locked family's own page of that role — the `next
509
- build` prompt names every template-stock page and the family to copy it from —
510
- and the family decides only *how* the package records its coverage:
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. `next-campaigns-build`
537
- materialises each template-stock page from the locked family's own page of
538
- that role (the pre-checkout `select` step first, because it seeds the cart the
539
- runtime pages read). Once the page's built HTML exists at its route under
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 refreshes a
569
- package it did not just create. So a first run that blocked leaves a package
570
- whose provenance names the *old* manifest, and simply rerunning with a new
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
package/docs/effects.md CHANGED
@@ -134,7 +134,9 @@ a flag the command rejects up front. It writes nothing, journals nothing, and is
134
134
  the row to read when you want to know what a typo costs. The one exception is
135
135
  declared on the rows it belongs to: `start`, `prepare-build`, `build`,
136
136
  `run start` and `run end` close out a **stale** run session at the root they are
137
- about to act on *before* argv is refused.
137
+ about to act on *before* argv is refused. Commands that implement `--dry-run`
138
+ skip this closeout whenever that flag is present, including a valued flag that
139
+ will be refused: `run end --dry-run yes` writes, sends, and deletes nothing.
138
140
 
139
141
  A refusal is decided by argv alone. When file content or state on disk decides
140
142
  the outcome, the command has reached a handler failure and journals it.
@@ -143,9 +145,13 @@ For intake, run-record, built-site QA, and `next`, argv-only checks run before
143
145
  their handler reads the target; invalid values are refused without a journal
144
146
  entry. For `start`, `prepare-build`, and `build`, bare, empty, and whitespace-only
145
147
  values of `--spec`, `--map-id`, `--source`, `--target`, `--source-kind`,
146
- `--proxy-base`, `--wrapper-policy`, `--design-manifest`, and
147
- `--order-path-depth` are refused before local spec reads, Map fetches, or cache
148
- writes on the `--spec`, `--map-id`, and `--map-id --cached-spec` paths.
148
+ `--proxy-base`, `--wrapper-policy`, `--design-manifest`, `--order-path-depth`,
149
+ `--template-family`, `--allow-uncertified-template`, `--theme-policy`, and
150
+ `--brief` are refused before local spec reads, Map fetches, or cache writes on
151
+ the `--spec`, `--map-id`, and `--map-id --cached-spec` paths. So is a
152
+ `--theme-policy` outside `inspect_only`, `auto`, and `off`. Whether a named
153
+ template family is certified, and whether a named brief can be read, depend on
154
+ file content: those checks still run in the handler and are journaled.
149
155
  The operator-facing `run-record` and `run end` commands refuse bare, empty, or
150
156
  whitespace-only values for every value-taking inherited run-record flag before
151
157
  packet work. The five agent
@@ -189,6 +195,12 @@ flag without a value is refused with "Missing value for --<flag>". If a named
189
195
  packet yields neither a Map ID nor a valid local-spec identity after checkpoint
190
196
  preflight reads the packet, spec, and report, the requirement is a journaled
191
197
  handler failure. A conflicting local/Map identity is also a handler failure.
198
+ When `qa run` selects `--legacy-api-test-order`, a missing, bare, empty, or
199
+ unusable `--cart` and an unknown legacy mode are argv-only refusals before QA
200
+ input resolution. They append no lifecycle entry. Accepted modes remain
201
+ `accept`, `decline`, and `both` (case-insensitive); browser `--test-order` still
202
+ takes precedence and does not require the legacy cart. API credentials are
203
+ still checked only inside the legacy handler and failures there are journaled.
192
204
  The nested run-record refusal scope in session closeout guards against future
193
205
  changes. No internal closeout can currently create a refusal before its
194
206
  invoking command journals.
@@ -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.1 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.43.2 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
@@ -25,7 +25,7 @@ Ledger schema id: `campaigns-os-release-ledger/v1`
25
25
  Change policy version: `1.0.0`
26
26
  Reason-code vocabulary version: `1.0.0`
27
27
  Limits version: `1.0.0`
28
- Supported surface at generation time: `1.43.1`
28
+ Supported surface at generation time: `1.43.2`
29
29
 
30
30
  ## Forward compatibility
31
31
 
@@ -59,12 +59,16 @@ across them. A progress stream is independent of a run-session ID.
59
59
 
60
60
  Sanitized immutable snapshots are written under the target repository's
61
61
  `.campaign-runtime/progress/` before any request. Allocation uses an exclusive
62
- local lock with a process owner. Dead owners are recovered through an exclusive
63
- recovery claim and an atomic rename; a live process is never evicted. An ownerless
64
- crash gap is recoverable after ten seconds. If recovery itself is interrupted,
65
- capture fails closed: stop all Campaigns OS writers for that target, then remove
66
- the abandoned `.allocation-lock` directory in the affected progress scope before
67
- retrying `next`. Do not remove a lock while a writer is active.
62
+ local lock with a process owner; the lock directory and its owner record are
63
+ published together in one rename, so a lock never exists without its owner.
64
+ Dead owners are recovered through an exclusive recovery claim and an atomic
65
+ rename; a live process is never evicted. A lock directory with no owner record
66
+ (left by an older release) is never taken over: capture refuses it after about
67
+ a second. If one is found, or if recovery itself is interrupted, capture fails
68
+ closed and the warning names the affected `.allocation-lock` directory: stop
69
+ all Campaigns OS writers for that target, then remove that directory before
70
+ retrying `next`. Do not remove a lock while a writer is active. Do not run an
71
+ older Campaigns OS release against the same target at the same time.
68
72
 
69
73
  An unchanged projection reuses its ID, timestamp and sequence. Identity
70
74
  changes start a new stream. Each local scope retains at most 32 snapshots and
@@ -1,5 +1,13 @@
1
1
  # QA And Test Orders
2
2
 
3
+ For packet-based partial builds, QA honors the Assembly Report's recorded
4
+ `stages.prepare_build.declared_out_of_scope` declarations together with the
5
+ packet's skip mappings. Unbuilt declared pages emit `skipped` evidence with
6
+ reason `out_of_build_scope`; HTTP, browser and commercial checks do not request
7
+ those routes, and entry URLs come from the remaining pages. A materialized
8
+ stock page rejoins QA. Missing in-scope pages still fail normally. A raw skip
9
+ mapping without a recorded declaration does not suppress checks.
10
+
3
11
  The public v0 QA runner is Node/npm-based and does not require access to a private runtime repo.
4
12
 
5
13
  > **Commerce QA requires network; it cannot run in a no-outbound sandbox.** The SDK, product images, fonts, the Netlify preview, and the Playwright typed-card test order all need outbound network. A build environment without it can only validate markup/build/CSS — the commerce runtime and the typed-card test order (the Campaigns OS control) must be deferred to a deployed preview. Always run the QA runner against a `--base-url` preview/production origin (e.g. `npm run campaigns-os -- qa run --packet campaign-runtime.build.json --base-url https://deploy-preview-7--your-site.netlify.app/ --browser --test-order common`); never report commerce-runtime QA as passed from an offline build.
@@ -793,9 +801,12 @@ retire that guard once #36 ships and `cartLines` is populated.
793
801
  Analytics correctness has two deliberately separate evidence phases in one QA
794
802
  run:
795
803
 
796
- 1. The campaign-root visit inventories declared providers, containers, pixels,
797
- and other observable tags. It does not prove or disprove Purchase, even if a
798
- stray Purchase-shaped event appears there.
804
+ 1. The inventory visit inventories declared providers, containers, pixels,
805
+ and other observable tags on one page: the campaign root, or a built entry
806
+ when the root cannot be captured (see
807
+ [Which page the inventory captures](#which-page-the-inventory-captures)).
808
+ It does not prove or disprove Purchase, even if a stray Purchase-shaped
809
+ event appears there.
799
810
  2. The existing canonical typed-card order run supplies Purchase evidence. For
800
811
  each planned order, the topology classifier must recognize the final URL as
801
812
  that plan's receipt, then the runner waits the full `--analytics-settle`
@@ -839,6 +850,98 @@ analytics block to gate. The SDK's own data layer is a separate, always-on
839
850
  reading taken on the same order — see [Purchase data layer](#purchase-data-layer-dl_purchase)
840
851
  under Test Orders.
841
852
 
853
+ ### Which page the inventory captures
854
+
855
+ The inventory starts at the campaign root composed from the campaign identity
856
+ (`public_route_slug` plus `route_root`). A partial build (a topology with a
857
+ partial build scope, or pages excluded from the build) may have no page there:
858
+ the root is then whatever the host answers, such as a directory index or a
859
+ generic fallback. So the root is visited only when it is in scope. On a full
860
+ build it always is; on a partial build it is in scope only when a built,
861
+ in-scope topology page is served at the root. When the root is out of scope,
862
+ answers non-2xx, or fails to load (a timeout, a refused connection), the leg
863
+ tries each funnel's built entry in turn: the first in-scope page on a partial
864
+ build (the same entry the partial-scope planner selects), otherwise the first
865
+ entry-like page. It captures the first page that answers 2xx. A response with no
866
+ HTTP status counts as an answer. A closed page or a disconnected browser is not
867
+ a per-page failure: it is the `analytics-correctness:runner` blocker.
868
+
869
+ `analytics-correctness:capture` records the page it used:
870
+ `evidence.capture_page` holds `url` (the URL requested, query redacted),
871
+ `source` (`campaign_root` or `built_entry`), `page_id`, `funnel_id` and
872
+ `http_status`. `evidence.final_url` is the page URL after redirects and
873
+ settling. When a built entry was used, `evidence.root_fallback` says why the
874
+ root was not: `reason` is `out_of_built_scope`, `non_2xx` (with the root's
875
+ `http_status`), or `navigation_error` (with its `error_code`).
876
+
877
+ When no page is captured, the leg emits `analytics-correctness:capture` alone,
878
+ with no per-vendor assertion measured against an empty page. There are two
879
+ outcomes, and `evidence.attempts` lists each page tried:
880
+
881
+ | `evidence.reason` | When | Result |
882
+ |---|---|---|
883
+ | `no_in_scope_page_captured` | The root is out of the built scope and no built entry other than the root is left to try, so nothing was loaded | `skipped`: there is no page whose tags could be measured |
884
+ | `no_capture_page_answered` | At least one page was tried, and every one answered non-2xx or failed to load | `FAIL`/`BLOCKER`: the declared analytics went unmeasured, so a later passing order cannot report the run ready |
885
+
886
+ Step routing is path-based in every certified family. Page-kit builds each
887
+ page to its own `<route>/index.html`, and QA strips the query string from a
888
+ CampaignSpec route. So `/campaign`, `/campaign/` and `/campaign/index.html` are
889
+ one page, and a query string does not name a different page. A topology page
890
+ whose own URL declares a query (for example `/campaign/?step=checkout`) is
891
+ still never merged into the root on its path alone. Unless its query is
892
+ exactly the root's own (parameter order aside), it does not put the root in
893
+ scope, it is captured as its own entry, and its `capture_page` carries
894
+ `query_routed: true`, since the redacted URL alone would read as the root. An
895
+ entry on a different path is never marked `query_routed`, whatever query it
896
+ carries. A URL with no query of its own names the page at that path whatever
897
+ query the other URL carries.
898
+
899
+ ### Local-serve review (`manual_review`)
900
+
901
+ A local proof run renders the development environment on purpose (see
902
+ [Local proof mode](#local-proof-mode-deploytarget-local-serve)), and the
903
+ starter templates gate every vendor loader out of that render. A pixel that
904
+ did not fire there is the render's design, not a campaign defect. So on a
905
+ local-serve run, a failing fire-dependent check becomes `manual_review` at
906
+ `warn` instead of a blocker. The fire-dependent checks are
907
+ `analytics-correctness:tag:*`, `analytics-correctness:oob:*` and
908
+ `analytics-correctness:purchase-fires`.
909
+
910
+ The run qualifies only when all of these hold:
911
+
912
+ - the packet's `deploy.target` is `local-serve`;
913
+ - the analytics capture target (else the base URL) is a loopback URL;
914
+ - the Assembly Report records the development render:
915
+ `stages.assembly.evidence.build_environment` is `development`. A production
916
+ build served on localhost, or a build whose environment was never recorded,
917
+ keeps its blockers.
918
+
919
+ Each failing check is then downgraded only when the page it measured is on
920
+ record as loopback. For `tag:*` and `oob:*`, the check's own URL, the passing
921
+ capture's `capture_page.url` and its `final_url` must all be loopback, so a
922
+ built-entry fallback on a remote host, or a localhost root that redirected to
923
+ a production host, keeps the blocker. For `purchase-fires`, there must be at
924
+ least one judged receipt, and every one needs a loopback `receipt_url` and
925
+ `receipt_document_url` (the page URL read after the receipt analytics
926
+ settled). A missing or unparseable URL keeps the blocker.
927
+
928
+ What always stays a blocker:
929
+
930
+ - `analytics-correctness:data-layer-purchase:<path>`. It counts the SDK's own
931
+ `dl_purchase`, which the development render still pushes, so a miss on
932
+ localhost can be a real defect.
933
+ - A capture or runner failure: `analytics-correctness:runner`, a check whose
934
+ evidence carries an `error_code`, and a `purchase-fires` reading whose
935
+ `capture_error_plan_ids` is missing or not empty. The environment explains a
936
+ silent pixel, not an unmeasured one.
937
+
938
+ A downgraded check keeps its evidence and adds `reason:
939
+ local_serve_development_render`, `local_serve_status: fail`, the recorded
940
+ `build_environment`, the recorded `production_parity` (`status:
941
+ not_recorded` when none is on the report, with a note unless it passed), and a
942
+ `follow_up`: re-run `qa run` against the PR preview (a production render) with
943
+ `--base-url <preview-url>`. That run gates these checks.
944
+
842
945
  ## Analytics parity (dataLayer / GTM)
843
946
 
844
947
  The analytics-parity leg proves the live **dataLayer event stream + GTM/pixel
@@ -858,14 +961,22 @@ npm run campaigns-os -- qa run \
858
961
  ```
859
962
 
860
963
  This receipt-to-receipt parity example names `--analytics-candidate`
861
- explicitly. When that flag is omitted, the candidate is the campaign identity's
862
- composed root (`public_route_slug` plus `route_root`), not the raw
863
- `--base-url` value.
964
+ explicitly, and that URL is captured as given. When that flag is omitted, the
965
+ candidate is chosen the same way as the correctness inventory (see
966
+ [Which page the inventory captures](#which-page-the-inventory-captures)): the
967
+ campaign identity's composed root (`public_route_slug` plus `route_root`), not
968
+ the raw `--base-url` value, when it is in scope and answers 2xx, else the first
969
+ built entry that does. `analytics-parity:capture` then records the same
970
+ `capture_page` and `root_fallback` evidence. The candidate is captured before
971
+ the baseline, and when no candidate page is captured the leg emits
972
+ `analytics-parity:capture` alone, without loading the baseline:
973
+ `no_in_scope_page_captured` (skipped) or `no_capture_page_answered`
974
+ (`FAIL`/`BLOCKER`).
864
975
 
865
976
  | Flag | Meaning |
866
977
  |---|---|
867
978
  | `--analytics-baseline <url>` | Legacy funnel URL to capture as the parity baseline (enables the leg) |
868
- | `--analytics-candidate <url>` | Migrated URL to capture; defaults to the identity-composed campaign root |
979
+ | `--analytics-candidate <url>` | Migrated URL to capture as given; defaults to the identity-composed campaign root, or the first built entry when that root is out of the built scope or does not answer |
869
980
  | `--analytics-hosts a,b` | Extra host substrings to treat as analytics tag-fires (Everflow is built in) |
870
981
  | `--analytics-settle <ms>` | Wait after analytics page loads and after a recognized typed-order receipt for async tags to fire (default 5000); receipt settling must fit inside the order deadline |
871
982
 
@@ -1690,6 +1801,19 @@ review. Nested Google Maps/payment keys and inert HTML do not count as campaign
1690
1801
  credentials. This small static grammar deliberately leaves many real pages
1691
1802
  unknown; a literal inside arbitrary code is not proof of effective configuration.
1692
1803
 
1804
+ A page script that does not parse is not treated as dynamic. The browser throws
1805
+ a `SyntaxError` on it and nothing in it runs, so the binding reads its
1806
+ declarations as unavailable (`script_unavailable_or_limit`) and QA adds a
1807
+ separate `script-parse:<page_id>` blocker in the same `api-metadata` family.
1808
+ Its `actual` names each script by path (inline scripts as `inline script`)
1809
+ with the line and column, and its evidence lists a fixed diagnostic category
1810
+ per script, never text from the script. Classic scripts are parsed as scripts
1811
+ and `type="module"` scripts as modules, with the type stripped of surrounding
1812
+ ASCII whitespace and compared case-insensitively as the browser does; classic
1813
+ `nomodule` scripts are not fetched or parsed (a module script ignores
1814
+ `nomodule` and is parsed), and script srcs resolve against the page's first `<base href>`. Doctor runs the same parse over the built output before deploy; see
1815
+ `built_output.script_syntax` in [the Build Packet doc](build-packet.md).
1816
+
1693
1817
  External executable scripts other than the recognized jsDelivr Campaign Cart
1694
1818
  loader/index are inspected only on the page's origin. Each page admits at most
1695
1819
  6 such references; each run fetches at most 24 distinct URLs (deduplicated),
@@ -118,7 +118,8 @@ be recorded without the bytes actually moving somewhere.
118
118
  ### Fixes that touch only policy-ignored paths
119
119
 
120
120
  A fix living entirely in paths the policy ignores — `src/` other than
121
- `src/cli.mjs`, `scripts/`, tests and fixtures — carries a same-surface CHANGELOG
121
+ `src/cli.mjs`, `src/agent/` and `src/doctor/`, `scripts/`, tests and
122
+ fixtures — carries a same-surface CHANGELOG
122
123
  section (`X.Y.Z+agent.N`) and **no ledger entry**. There is nothing for an entry
123
124
  to claim: every change item must map to a classified changed path in the range,
124
125
  and an ignored path is never classified, so an entry written for such a PR is
@@ -127,9 +128,10 @@ path-less item fails the same way, because no classified change of its class
127
128
  exists in the range. The ignore list and its stated reasons are in
128
129
  [`contracts/agent-relevant-change-policy.v1.json`](../contracts/agent-relevant-change-policy.v1.json).
129
130
 
130
- The dividing line inside `src/` is `src/cli.mjs`: an explicit rule classifies it
131
- as `cli_surface`, so any change to it is agent-relevant and owes an entry, even
132
- when the behaviour change originates in a helper module beside it.
131
+ The classified paths inside `src/` are `src/cli.mjs`, `src/agent/` and
132
+ `src/doctor/`: an explicit rule classifies each as `cli_surface`, so any change
133
+ there is agent-relevant and owes an entry, even when the behaviour change
134
+ originates in a helper module beside it.
133
135
 
134
136
  ### Amendments
135
137
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  How a checkout of this repository at one commit becomes a usable installed runtime, and how a consumer decides whether a prepared one is still trustworthy. Everything below is generated from `contracts/runtime-recipe.campaigns-os-node-v1.json`, which is the only authority for these values.
10
10
 
11
- Recipe kind `campaigns-os-node-v1`, revision `1.0.2`, validated by `schemas/campaigns-os-runtime-recipe.v1.schema.json` (`Campaigns OS Runtime Recipe v1`). Supported surface at generation time: `1.43.1`.
11
+ Recipe kind `campaigns-os-node-v1`, revision `1.0.2`, validated by `schemas/campaigns-os-runtime-recipe.v1.schema.json` (`Campaigns OS Runtime Recipe v1`). Supported surface at generation time: `1.43.2`.
12
12
 
13
13
  ## What this is
14
14