@nextcommerce/campaigns-os 1.61.0 → 1.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +127 -0
- package/README.md +86 -438
- package/agents/claude/CLAUDE.md +9 -4
- package/agents/codex/AGENTS.md +8 -4
- package/agents/copilot/copilot-instructions.md +8 -4
- package/agents/cursor/campaigns-os.mdc +10 -4
- package/campaign-spec/README.md +7 -0
- package/campaign-spec/dist/analytics-vendors.d.ts +40 -0
- package/campaign-spec/dist/analytics-vendors.js +44 -0
- package/campaign-spec/dist/checkout-flow.d.ts +18 -0
- package/campaign-spec/dist/checkout-flow.js +28 -0
- package/campaign-spec/dist/index.d.ts +4 -2
- package/campaign-spec/dist/index.js +5 -1
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +8 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.js +39 -0
- package/campaign-spec/dist/rules/campaign-currencies.d.ts +30 -0
- package/campaign-spec/dist/rules/campaign-currencies.js +130 -0
- package/campaign-spec/dist/rules/checkout-path-has-packages.d.ts +31 -0
- package/campaign-spec/dist/rules/checkout-path-has-packages.js +79 -0
- package/campaign-spec/dist/rules/index.d.ts +3 -0
- package/campaign-spec/dist/rules/index.js +9 -0
- package/campaign-spec/dist/rules/package-currency-prices.d.ts +29 -0
- package/campaign-spec/dist/rules/package-currency-prices.js +137 -0
- package/campaign-spec/dist/types.d.ts +59 -9
- package/compatibility.json +1 -1
- package/contracts/commerce-surface-catalog.json +59 -53
- package/contracts/effects.v1.json +549 -165
- package/contracts/fixtures/runtime-recipe/accept/current.json +4 -0
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +4 -0
- package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +4 -0
- package/contracts/release-ledger.json +1260 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +4 -0
- package/contracts/supported-surface.json +8 -8
- package/contracts/template-brand-contract.shared-commerce.v0.json +23 -1
- package/docs/build-packet.md +430 -37
- package/docs/campaign-standardization-report.md +4 -4
- package/docs/campaigns-os-build-flow.md +54 -3
- package/docs/campaignspec-authoring-examples.md +59 -4
- package/docs/design-source-package.md +18 -6
- package/docs/effects.md +17 -9
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/polish-evidence.md +2 -0
- package/docs/progress-snapshots.md +7 -4
- package/docs/qa-and-test-orders.md +147 -1
- package/docs/readback.md +40 -5
- package/docs/runtime-readiness.md +7 -3
- package/docs/sdk-storage-compatibility.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/package.json +1 -1
- package/schemas/campaign-runtime-build-context.v0.schema.json +9 -0
- package/schemas/campaign-runtime-build-packet.v0.schema.json +9 -0
- package/schemas/campaign-spec.v4.schema.json +34 -3
- package/schemas/campaign-spec.v5.schema.json +34 -3
- package/schemas/campaigns-os-readback.v2.schema.json +62 -1
- package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +3 -3
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +4 -3
- package/skills/next-campaigns-os/SKILL.md +6 -4
- package/skills/next-campaigns-os-setup/SKILL.md +5 -3
- package/skills/next-campaigns-polish/SKILL.md +5 -4
- package/skills/next-campaigns-qa/SKILL.md +7 -4
- package/skills/next-campaigns-sdk-bump/SKILL.md +42 -9
- package/skills.json +11 -11
- package/src/brand-theme.mjs +2 -1
- package/src/build-brief.mjs +4 -1
- package/src/campaign-workspace.mjs +288 -15
- package/src/change-scope.mjs +284 -0
- package/src/cli.mjs +308 -86
- package/src/commercial-journey.mjs +16 -3
- package/src/commercial-parity.mjs +15 -7
- package/src/content-residue.mjs +4 -2
- package/src/design-source-package.mjs +36 -0
- package/src/design-source-publication.mjs +9 -6
- package/src/directory-lock.mjs +73 -19
- package/src/doctor/checks.mjs +79 -7
- package/src/doctor/ci.mjs +75 -10
- package/src/doctor/inspect.mjs +23 -6
- package/src/doctor/next-step.mjs +7 -0
- package/src/doctor-sidecar.mjs +3 -2
- package/src/evidence-debt.mjs +47 -7
- package/src/finding-cause.mjs +12 -11
- package/src/input-refresh.mjs +339 -32
- package/src/invocation.mjs +7 -1
- package/src/live-campaign-refs.mjs +7 -2
- package/src/local-preview-policy.mjs +35 -2
- package/src/page-kit-analytics-vendors.mjs +132 -0
- package/src/polish-browser.mjs +3 -1
- package/src/polish-readability.mjs +221 -9
- package/src/polish-visual.mjs +9 -2
- package/src/progress-node.mjs +3 -2
- package/src/qa-analytics-parity.mjs +17 -1
- package/src/qa-browser.mjs +40 -4
- package/src/qa-commercial-parity.mjs +50 -3
- package/src/qa-node.mjs +196 -16
- package/src/qa-publish.mjs +9 -8
- package/src/qa-sidecar.mjs +16 -4
- package/src/qa-vendor-beacons.mjs +232 -0
- package/src/qa-vendor-endpoints.mjs +306 -0
- package/src/qc-accept.mjs +29 -1
- package/src/qc-check-registry.mjs +5 -1
- package/src/qc-results.mjs +73 -26
- package/src/readback.mjs +58 -10
- package/src/route-identity.mjs +10 -0
- package/src/run-record.mjs +18 -10
- package/src/runtime-state-ignore.mjs +6 -2
- package/src/sdk-storage-compatibility.mjs +21 -2
- package/src/source-html-intake.mjs +15 -1
- package/src/spec-derive-campaign.mjs +216 -0
- package/src/spec-derive.mjs +473 -53
- package/src/spec-identity.mjs +14 -0
- package/src/stage-ledger.mjs +40 -1
- package/src/stage-record.mjs +58 -13
- package/src/target-lock.mjs +11 -1
package/docs/build-packet.md
CHANGED
|
@@ -351,6 +351,53 @@ action. A configured campaign whose pin is newer than the spec's is the
|
|
|
351
351
|
advisory case above: no required action, and sync never moves that pin
|
|
352
352
|
backwards; `spec derive` is the command that closes it from the spec side.
|
|
353
353
|
|
|
354
|
+
### Template analytics vendors (`page_kit.analytics_vendors`)
|
|
355
|
+
|
|
356
|
+
The certified starter templates ship a direct adapter for eight analytics
|
|
357
|
+
vendors: GA4, AppLovin Axon, Taboola, Triple Whale, TikTok, Northbeam,
|
|
358
|
+
Snapchat and Pinterest. A vendor is switched on by its id key in the
|
|
359
|
+
campaign's `_data/campaigns.json` entry (`ga4_id`, `axon_event_key`,
|
|
360
|
+
`taboola_account_id`, `triplewhale_name`, `tiktok_pixel_id`,
|
|
361
|
+
`northbeam_client_id`, `snap_pixel_id`, `pinterest_tag_id`); absent or empty
|
|
362
|
+
means off. The adapter forwards the SDK's `dl_*` events to the vendor. These
|
|
363
|
+
vendors are separate from the SDK providers in `analytics.providers` (GTM,
|
|
364
|
+
Meta, RudderStack, a custom endpoint), which reach the page through
|
|
365
|
+
`config.js`.
|
|
366
|
+
|
|
367
|
+
The CampaignSpec declares them under `analytics.vendors`, keyed by vendor,
|
|
368
|
+
in the shape the Map Builder saves:
|
|
369
|
+
|
|
370
|
+
```json
|
|
371
|
+
"analytics": {
|
|
372
|
+
"vendors": {
|
|
373
|
+
"triplewhale": { "enabled": true, "id": "acme.myshopify.com", "platform": "SHOPIFY", "identityEnabled": true, "blockedEvents": ["dl_purchase", "dl_upsell_purchase"] },
|
|
374
|
+
"tiktok": { "enabled": true, "id": "CABCDEFGHIJ1234567890" }
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
`id` is the value of the vendor's id key, `platform` is Triple Whale's
|
|
380
|
+
`triplewhale_platform`, `identityEnabled` turns on the vendor's identity or
|
|
381
|
+
advanced-matching flag (raw email and phone), and `allowedEvents` /
|
|
382
|
+
`blockedEvents` are the `dl_*` events the adapter forwards or drops. The Map's
|
|
383
|
+
spec compiler writes the matching keys into `campaigns.json`, and
|
|
384
|
+
`spec derive` mirrors the id back (see below).
|
|
385
|
+
|
|
386
|
+
Once the target entry exists, doctor compares the two sides and warns, per
|
|
387
|
+
vendor, when:
|
|
388
|
+
|
|
389
|
+
| Code | Meaning |
|
|
390
|
+
|---|---|
|
|
391
|
+
| `page_kit.analytics_vendors.key_missing` | the spec declares the vendor but its id key is absent or empty, so the template never loads it |
|
|
392
|
+
| `page_kit.analytics_vendors.key_placeholder` | the id key holds a placeholder (`XXXX`, `YOUR_`, `CHANGEME`, `<…>`, `EXAMPLE`); the template loads the vendor for any non-empty value |
|
|
393
|
+
| `page_kit.analytics_vendors.undeclared` | the id key switches the vendor on but the spec does not declare it, declares it with `enabled: false`, or turns analytics off |
|
|
394
|
+
| `page_kit.analytics_vendors.id_mismatch` | both sides name the vendor with different ids; the `campaigns.json` value is what ships |
|
|
395
|
+
|
|
396
|
+
Every finding is a warning and names only keys and vendors, never a value
|
|
397
|
+
read from `campaigns.json`. Doctor writes nothing. `spec derive` clears an
|
|
398
|
+
`undeclared` or `id_mismatch` finding from the spec side; the others are
|
|
399
|
+
fixed in `campaigns.json` or in the Map.
|
|
400
|
+
|
|
354
401
|
### Changing a campaign after handoff
|
|
355
402
|
|
|
356
403
|
For page-kit campaigns, the reviewed repository spec determines the next build.
|
|
@@ -401,8 +448,9 @@ This procedure does not grant new merge, store-write or deployment authority.
|
|
|
401
448
|
Do not infer intent from every difference in a stale export. If the repository
|
|
402
449
|
advances during review, repeat the comparison against the new revision.
|
|
403
450
|
5. **Refresh and validate the result.** Preview `spec derive --dry-run`, inspect
|
|
404
|
-
every refused or unresolved field, then apply the accepted derivation.
|
|
405
|
-
|
|
451
|
+
every refused or unresolved field, then apply the accepted derivation. Derive
|
|
452
|
+
writes repo-derived fields and the offer and package fields mirrored from the
|
|
453
|
+
live campaign; it does not merge authored intent. Follow the packet's normal
|
|
406
454
|
`page-kit sync`, build, doctor and relevant QA steps. Resolve stale routing
|
|
407
455
|
hints and rebuild affected pages. A successful derive exit alone is not build
|
|
408
456
|
or QA approval. After an authored-only edit, derive may return `unchanged`
|
|
@@ -442,6 +490,45 @@ routing references and rebuild the affected upsell page. If the developer also
|
|
|
442
490
|
changed the selected package to C, record a conflict and obtain a choice before
|
|
443
491
|
merging. Do not apply B merely because the Map was saved later.
|
|
444
492
|
|
|
493
|
+
#### Pages, offers and packages after handoff
|
|
494
|
+
|
|
495
|
+
A campaign is **handed off** once its deploy or its QA has been completed: the
|
|
496
|
+
Assembly Report's `stages.deploy` or `stages.qa` holds a completed record, now
|
|
497
|
+
or in that stage's `history` (a deploy or QA made owed again by a later change
|
|
498
|
+
still happened). From then on the repo's page tree, not the design source, is
|
|
499
|
+
the authority for which pages the funnel has, and the Campaigns App is the
|
|
500
|
+
authority for the offers and packages the spec mirrors. Two commands keep the
|
|
501
|
+
spec in step without running setup or intake again:
|
|
502
|
+
|
|
503
|
+
1. `campaigns-os spec derive --packet <packet> --dry-run` shows the change, field
|
|
504
|
+
by field; run it again without `--dry-run` to write it. It adds a page the
|
|
505
|
+
tree builds that no spec page binds, removes a page whose built file is gone,
|
|
506
|
+
and refreshes the offer and package fields the spec mirrors from the live
|
|
507
|
+
campaign (see [Deriving the spec from the repo](#deriving-the-spec-from-the-repo-spec-derive)).
|
|
508
|
+
2. `campaigns-os record spec --packet <packet>` binds the result. A page added
|
|
509
|
+
or removed is bound without intake: the packet's and the Build Context's
|
|
510
|
+
page-kit projection are re-projected, the added page is recorded with
|
|
511
|
+
`origin: "repo"` (its source is its own page file), and the build, Polish
|
|
512
|
+
and QA stages it affects are owed again; deploy follows at the next
|
|
513
|
+
`record build` (see
|
|
514
|
+
[Binding a page change](#binding-a-page-change)).
|
|
515
|
+
|
|
516
|
+
Then rebuild, record the build, and run Polish, deploy and QA as `next` asks.
|
|
517
|
+
Before handoff, `spec derive` lists a page file no spec page binds as
|
|
518
|
+
unclaimed and adds nothing, and `record spec` refuses a page added or removed
|
|
519
|
+
(`page_scope_changed`): the design source decides the pages until the first
|
|
520
|
+
deploy or QA, and a new page there needs intake.
|
|
521
|
+
|
|
522
|
+
To add an upsell after handoff, copy an existing upsell page file to a new
|
|
523
|
+
name (the file name becomes the page id, so `upsell-2.html` is page
|
|
524
|
+
`upsell-2`), keep its `page_type: upsell` frontmatter, and set its `next_url`
|
|
525
|
+
and `decline_url` to the routes it leads to. Point the page before it at the
|
|
526
|
+
new route (for example the first upsell's `next_url`). `spec derive` then
|
|
527
|
+
appends the page to the funnel, sets its `on_accept` and `on_decline` from
|
|
528
|
+
those links, and moves the first upsell's `on_accept` to the new page. The
|
|
529
|
+
repo states links, not positions, so the new page's `order` is reported as not
|
|
530
|
+
derived; set it by hand if the funnel's pages carry one.
|
|
531
|
+
|
|
445
532
|
#### Map write-back and future portal saves
|
|
446
533
|
|
|
447
534
|
`spec derive --write-map` remains an explicit, pin-only action. A developer or
|
|
@@ -469,11 +556,13 @@ this command generates the repo-derived ones so doctor compares generated
|
|
|
469
556
|
against generated instead of refereeing a hand-typed value against the repo:
|
|
470
557
|
|
|
471
558
|
```bash
|
|
472
|
-
campaigns-os spec derive --packet campaign-runtime.build.json [--dry-run] [--json] [--report <json>] [--from-store <subdomain> [--store-token-source env:<VAR>]] [--write-map] [--proxy-base <url>]
|
|
559
|
+
campaigns-os spec derive --packet campaign-runtime.build.json [--dry-run] [--json] [--report <json>] [--no-live-refs] [--from-store <subdomain> [--store-token-source env:<VAR>]] [--write-map] [--proxy-base <url>]
|
|
473
560
|
```
|
|
474
561
|
|
|
475
|
-
|
|
476
|
-
|
|
562
|
+
It reads the target repo and, once the campaign is built, the live campaign
|
|
563
|
+
in the Campaigns App (below), and writes into the packet's local spec
|
|
564
|
+
(`spec.local_path`). `--from-store` and `--write-map` are the other two
|
|
565
|
+
network paths. The repo-derived fields:
|
|
477
566
|
|
|
478
567
|
| Spec field | Repo authority |
|
|
479
568
|
|---|---|
|
|
@@ -481,15 +570,19 @@ By default it reads the target repo only (no network unless `--from-store` or
|
|
|
481
570
|
| `funnels[].pages[].page_url` (the legacy `funnel_pages[].page_url` mirror, when present, is reconciled to the same route on every run) | the page tree under `assembly.output_dir` (default `src/<public_route_slug>/`), read by page-kit's own rule: the file's basename alone (`checkout.html` → `checkout/` wherever it sits; `index.html` → the entry route; a nested `index.html` collides with the root and is not read), or a frontmatter `permalink` in the `/<slug>/<route>/` form prepare-build writes (page-kit serves a permalink verbatim, so any other spelling is a repo defect, refused) |
|
|
482
571
|
| `analytics.providers.gtm.containerId` | `_data/campaigns.json[public_route_slug].gtm_id` |
|
|
483
572
|
| `analytics.providers.facebook.pixelId` | `_data/campaigns.json[public_route_slug].fb_pixel_id` |
|
|
573
|
+
| `analytics.vendors.<vendor>.id`, for each starter-template vendor adapter: `ga4`, `axon`, `taboola`, `triplewhale`, `tiktok`, `northbeam`, `snap`, `pinterest` | the vendor's id key in the same entry: `ga4_id`, `axon_event_key`, `taboola_account_id`, `triplewhale_name`, `tiktok_pixel_id`, `northbeam_client_id`, `snap_pixel_id`, `pinterest_tag_id` |
|
|
484
574
|
|
|
485
|
-
Nothing else is written: not the store profile (store-derived;
|
|
486
|
-
`--from-store` below), not any authored or mirrored field, not the packet,
|
|
487
|
-
not the repo. A
|
|
575
|
+
Nothing else is written from the repo: not the store profile (store-derived;
|
|
576
|
+
see `--from-store` below), not any authored or mirrored field, not the packet,
|
|
577
|
+
not the repo, with the one exception of pages after handoff (below). A
|
|
488
578
|
derived field the spec carries with a different, authored-looking value is
|
|
489
579
|
overwritten, and the printed `before -> after` line shows it: that is the
|
|
490
580
|
class doing its job. A block the spec lacks is created (`global_config`,
|
|
491
|
-
`analytics.providers.gtm` as `{ "enabled": true, "containerId": … }
|
|
492
|
-
|
|
581
|
+
`analytics.providers.gtm` as `{ "enabled": true, "containerId": … }`,
|
|
582
|
+
`analytics.vendors.ga4` as `{ "enabled": true, "id": … }`); an array element
|
|
583
|
+
is never invented. For a vendor only the id is derived: its platform,
|
|
584
|
+
identity flag and allowed or blocked events stay authored in the Map, whose
|
|
585
|
+
spec compiler writes them into `campaigns.json`.
|
|
493
586
|
|
|
494
587
|
Each page is bound to one file: the packet's own projection first
|
|
495
588
|
(`source_html.pages[].page_kit.target_path`, the file the build stage wrote
|
|
@@ -501,9 +594,96 @@ the tree's route (`checkout`, `/<slug>/checkout/`) is not a change, and a
|
|
|
501
594
|
value nested differently from the tree (`offers/upsell/` against
|
|
502
595
|
`upsell.html`) is. A routing hint in `sdk_hints.meta_tags` (`next-success-url`,
|
|
503
596
|
`next-upsell-accept-url`, `next-upsell-decline-url`) that no longer matches
|
|
504
|
-
the
|
|
505
|
-
`spec.derive.routing_hint_stale` on every run until the
|
|
506
|
-
|
|
597
|
+
the route of the page it names is reported as
|
|
598
|
+
`spec.derive.routing_hint_stale` on every run until the hint is fixed. The
|
|
599
|
+
list is read off the spec as the run leaves it. Hints are a spec projection
|
|
600
|
+
the Map regenerates, so derive rewrites one only on an edge it re-points after
|
|
601
|
+
handoff (below); any other stale hint is edited in the spec (or, while the Map
|
|
602
|
+
is still the spec's source, fixed by re-saving the Map).
|
|
603
|
+
|
|
604
|
+
A routing hint that is stale is also a doctor blocker (`spec.routing_hint_stale`,
|
|
605
|
+
on every read), and `record spec` refuses to bind a page change while one is
|
|
606
|
+
(`routing_hint_stale`): the built meta tag follows the hint, so a moved upsell
|
|
607
|
+
with checkout's hint on the old path sends shoppers to a 404 after payment.
|
|
608
|
+
The three share one function (`staleRoutingHints` in `src/spec-derive.mjs`).
|
|
609
|
+
|
|
610
|
+
**Pages after handoff (#721).** On a handed-off campaign (see
|
|
611
|
+
[Pages, offers and packages after handoff](#pages-offers-and-packages-after-handoff))
|
|
612
|
+
the page tree also says which pages exist, matched by page id and never by
|
|
613
|
+
position:
|
|
614
|
+
|
|
615
|
+
- A file with a page-kit `page_type` in its frontmatter (`product`, `checkout`,
|
|
616
|
+
`upsell`, `receipt`) that no spec page binds is **added**: its id is the file
|
|
617
|
+
name, its type follows `page_type` (`product` reads as `landing`, `receipt` as
|
|
618
|
+
`thankyou`; a `checkout` file that carries `next_step` is a `checkout_step`
|
|
619
|
+
when it also carries `step_number` and a `select` page when it does not, and
|
|
620
|
+
in a v4 spec a step is a `checkout` that forwards to the Checkout), its
|
|
621
|
+
`page_url` is the route the file builds to, and it is
|
|
622
|
+
appended to the funnel's pages (the legacy `funnel_pages[]` mirror too). The
|
|
623
|
+
diff row carries `op: "insert"`.
|
|
624
|
+
- A page whose file the packet's projection named
|
|
625
|
+
(`source_html.pages[].page_kit.target_path`) and the tree no longer has is
|
|
626
|
+
**removed** (`op: "remove"`). A page the build never wrote stays, reported as
|
|
627
|
+
`page_file_not_found`.
|
|
628
|
+
- Routing is read from the page files' frontmatter links, the keys
|
|
629
|
+
prepare-build projects: `next_url` (an upsell's `on_accept`, a checkout's
|
|
630
|
+
`success_url`, or its `next_page` when it forwards through that, a landing
|
|
631
|
+
page's `next_page`), `decline_url` (an upsell's
|
|
632
|
+
`on_decline`) and, on a select or checkout_step page and on a `checkout`
|
|
633
|
+
page whose file carries `next_step` (a v4 step), `next_step` (`next_page`). An added page takes its own links; a kept page whose link now
|
|
634
|
+
names an added page, or whose routing named a removed page, is re-pointed at
|
|
635
|
+
the page its link names. No other routing is touched. When the re-pointed
|
|
636
|
+
page carries the routing hint that goes with that field
|
|
637
|
+
(`next-upsell-accept-url` for `on_accept`, `next-upsell-decline-url` for
|
|
638
|
+
`on_decline`, `next-success-url` for `success_url`), the hint is rewritten
|
|
639
|
+
to the new target's route in the same spelling (`/<slug>/receipt/` becomes
|
|
640
|
+
`/<slug>/upsell-2/`, `receipt/` becomes `upsell-2/`), and so is the legacy
|
|
641
|
+
`funnel_pages[]` copy. An added page gets no hints and no packages: which
|
|
642
|
+
packages it sells is authored, so derive reports `page_packages_not_derived`
|
|
643
|
+
for it. Until each path into the Checkout passes a page that sells a
|
|
644
|
+
main-cart package (and each upsell or downsell has its offer package),
|
|
645
|
+
doctor blocks on `CheckoutPathHasPackages` or `UpsellHasPackages`.
|
|
646
|
+
- A page whose file is gone and a new file of the same page-kit `page_type`
|
|
647
|
+
in the same run read as a rename (`upsell.html` to `upsell-1.html`, or a
|
|
648
|
+
downsell's `downsell.html`, which is `page_type: upsell`, to
|
|
649
|
+
`downsell-1.html`). Derive applies
|
|
650
|
+
neither, so the page's packages, offers and other authored fields are not
|
|
651
|
+
lost, and reports both as `page_renamed`. For a rename, set the page's
|
|
652
|
+
`page_url` to the new route (it keeps its id; `record spec` binds that as a
|
|
653
|
+
move of routes) or rename the file back. For a new page, remove the old page
|
|
654
|
+
from the spec by hand and derive again.
|
|
655
|
+
- Page order is not something the tree states. Where the funnel's pages carry
|
|
656
|
+
an `order`, the added page's is reported as not derived.
|
|
657
|
+
|
|
658
|
+
Before handoff the files no page binds are listed in `page_scope.unclaimed`
|
|
659
|
+
and nothing is added or removed. The result's `page_scope` names what was
|
|
660
|
+
added, removed and left unclaimed.
|
|
661
|
+
|
|
662
|
+
**Offers and packages from the Campaigns App (#721).** When the packet's
|
|
663
|
+
built `_site/<public_route_slug>/` exists and a public Campaigns API key
|
|
664
|
+
resolves (the packet, its local CampaignSpec, or the declared campaign-key env
|
|
665
|
+
var), `spec derive` makes the read-only `GET {proxy-base}/api/campaign` under
|
|
666
|
+
`X-Campaign-Key` that doctor and `qa run` already make, and refreshes the
|
|
667
|
+
fields the spec mirrors from the live campaign:
|
|
668
|
+
|
|
669
|
+
| Spec field (mirrored) | Live campaign authority |
|
|
670
|
+
|---|---|
|
|
671
|
+
| each page package's `name`, `price`, `price_retail`, `product_name`, `product_sku`, `product_variant_name`, `product_purchase_availability`, `product_inventory_availability`, `image`, the recurring fields `is_recurring`, `price_recurring`, `interval`, `interval_count`, and `variant_attributes` (the campaign's `product_variant_attribute_values`) | the campaign's package with the same `ref_id`; a field the spec lacks is not added when the live value is null, `false` or an empty list, as Map Builder leaves it out |
|
|
672
|
+
| `offers[]` records: `name`, `type`, `code`, `condition`, `benefit`, `packages` | the campaign's offer with the same `ref_id`; a live offer missing from `offers[]` is added |
|
|
673
|
+
| each page's `offers[]` copy: `name`, `type`, `code`, `condition`, `benefit` | the same offer |
|
|
674
|
+
|
|
675
|
+
Which packages and offers a page carries, and a package's `qty`,
|
|
676
|
+
`is_upsell`, `is_order_bump`, `default_selected` and `configurable`, are
|
|
677
|
+
authored and never written. A price reaches the spec only from the Campaigns
|
|
678
|
+
App, never from the repo. A record the spec carries that the live campaign
|
|
679
|
+
does not serve is reported (`package_not_live`, `offer_not_live`) and kept.
|
|
680
|
+
Each row names its source, for example
|
|
681
|
+
`- funnels[0].pages[1].packages[0].price: "49.00" -> "39.00" (from the Campaigns App campaign 424242 (GET /api/campaign))`.
|
|
682
|
+
A read that fails leaves those fields as they are, with a
|
|
683
|
+
`spec.derive.live_campaign_not_read` warning; `--no-live-refs` sends nothing
|
|
684
|
+
and leaves them as they are; `--proxy-base` names the proxy. `--dry-run`
|
|
685
|
+
makes the same read, so its diff shows what would change. The result's
|
|
686
|
+
`live_campaign` says whether the read happened.
|
|
507
687
|
|
|
508
688
|
What the repo cannot state is reported as `not_derived[]` with a reason and
|
|
509
689
|
the status is `partial` (exit 0; the fields it could derive are written):
|
|
@@ -511,20 +691,30 @@ the status is `partial` (exit 0; the fields it could derive are written):
|
|
|
511
691
|
| Reason | Meaning |
|
|
512
692
|
|---|---|
|
|
513
693
|
| `scaffold_seed` | the entry still carries the starter demo store profile, so its pin is the starter's seed, not a version anyone chose; `page-kit sync` seeds the pin from the spec in that state |
|
|
514
|
-
| `target_missing`, `target_invalid` | the entry has no `sdk_version`, or it is not a released `MAJOR.MINOR.PATCH`; for an analytics id, the value is not a GTM container id / a digits-only pixel id; for a route, the file's permalink is not in the `/<slug>/<route>/` form (no slug prefix, another prefix, `.html`, `..`, a control character), reported with the URL page-kit would serve |
|
|
694
|
+
| `target_missing`, `target_invalid` | the entry has no `sdk_version`, or it is not a released `MAJOR.MINOR.PATCH`; for an analytics id, the value is not a GTM container id / a digits-only pixel id / a GA4 measurement id (`G-…`) / a digits-only Taboola account id / for the other vendors, one token with no spaces or quotes; for a route, the file's permalink is not in the `/<slug>/<route>/` form (no slug prefix, another prefix, `.html`, `..`, a control character), reported with the URL page-kit would serve |
|
|
515
695
|
| `waived` | an active named-human `page_kit.sdk_version` waiver covers the exact pair; derive leaves the spec as the waiver accepted it |
|
|
516
696
|
| `spec_ahead` | a released pin the spec declares (canonical or alias) is ahead of the repo pin: the state doctor blocks on with `page-kit sync` as its repair (#413); one command owns it, so derive never moves a spec pin backwards |
|
|
517
697
|
| `page_tree_missing`, `page_file_not_found`, `page_file_ambiguous` | no page tree, no file binds to the page, or more than one does |
|
|
518
698
|
| `entry_route_undeclared` | the page binds to the top-level `index.html` (the entry route, `""`) but is not flagged `is_entry`; doctor honours an empty `page_url` only on the entry page, so the flag is asked for in the Map rather than the route written |
|
|
519
699
|
| `waivers_unknown` | the Assembly Report could not be read, so a named-human `page_kit.sdk_version` waiver cannot be ruled out; the pin waits, routes and ids still derive |
|
|
520
700
|
| `page_id_duplicate` | the page id appears more than once; no single route can be derived for it |
|
|
521
|
-
| `spec_container_invalid` | `global_config`, `runtime`, `analytics`, `analytics.providers` or `analytics.
|
|
522
|
-
| `target_empty` | the entry's `gtm_id` / `fb_pixel_id` is empty while the spec declares an id; an empty repo value never deletes a spec id |
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
701
|
+
| `spec_container_invalid` | `global_config`, `runtime`, `analytics`, `analytics.providers`, `analytics.providers.<provider>`, `analytics.vendors` or `analytics.vendors.<vendor>` exists in the spec but is not an object; reported by the plan so `--dry-run` and the write agree |
|
|
702
|
+
| `target_empty` | the entry's `gtm_id` / `fb_pixel_id` / vendor id key is empty while the spec declares an id; an empty repo value never deletes a spec id |
|
|
703
|
+
| `page_type_unknown`, `page_id_unusable`, `page_funnel_ambiguous` | after handoff, a page file the spec lacks was not added: its `page_type` is not one of page-kit's four, its file name is not a usable new page id (taken, `index`, or not letters, digits, `-` and `_`), or the spec has several funnels (or none), so which one it joins is not stated |
|
|
704
|
+
| `routing_not_derived`, `routing_target_removed` | an added page's link names no single page (or the file states none), or a kept page routes to a removed page and its file's link names no page the spec keeps; set the field by hand |
|
|
705
|
+
| `page_order_not_derived` | an added page was appended; its `order` is left for you |
|
|
706
|
+
| `page_packages_not_derived` | an added page (any type but a thank-you page) carries no packages; author them before doctor's `CheckoutPathHasPackages` / `UpsellHasPackages` rules pass |
|
|
707
|
+
| `page_renamed` | a page's file is gone and a new file of the same page-kit `page_type` appeared; read as a rename, neither the remove nor the add is applied |
|
|
708
|
+
| `package_not_live`, `offer_not_live`, `offers_not_in_live_campaign` | a package or offer the spec carries is not in the live campaign, or the live campaign carried no offers list; nothing is changed |
|
|
709
|
+
|
|
710
|
+
A placeholder id (`GTM-XXXXXXX`, a run of one digit; for a vendor also
|
|
711
|
+
`XXXX`, `YOUR_`, `CHANGEME`, `<…>` or `EXAMPLE` anywhere in it, such as
|
|
712
|
+
`G-XXXXXXXXXX` or `store.example.com`) is `target_invalid`: writing it would
|
|
713
|
+
declare an analytics contract QA then blocks on, and the template loads a
|
|
714
|
+
vendor for any non-empty value. A derived field the entry does not carry at
|
|
715
|
+
all is listed under `not_in_target[]` and left as it is. Vendors are opt-in
|
|
716
|
+
(an absent key means the vendor is off), so a vendor neither the entry nor
|
|
717
|
+
the spec carries is not listed.
|
|
528
718
|
|
|
529
719
|
`spec derive` writes the spec alone and no longer rebinds identity: the
|
|
530
720
|
Build Context's `spec.hash` / `spec.material_hash` and the Assembly Report's
|
|
@@ -532,12 +722,16 @@ Build Context's `spec.hash` / `spec.material_hash` and the Assembly Report's
|
|
|
532
722
|
bound to, so every build, Polish and QA record made against the earlier
|
|
533
723
|
content reads owed again. Run `record spec` after it (the result's `next`
|
|
534
724
|
names it) to bind the derived spec; QA's verdict and `bundle check` correlate
|
|
535
|
-
against the bound material hash. A derived route change
|
|
536
|
-
`spec.derive.projection_stale`: the packet's page-kit
|
|
537
|
-
(`source_html.pages[].page_kit`) and the Build Context page map
|
|
538
|
-
|
|
725
|
+
against the bound material hash. A derived route change, or a page added or
|
|
726
|
+
removed, also warns `spec.derive.projection_stale`: the packet's page-kit
|
|
727
|
+
projection (`source_html.pages[].page_kit`) and the Build Context page map
|
|
728
|
+
describe the old pages, and `record spec` re-projects them (see
|
|
729
|
+
[Binding a page change](#binding-a-page-change)). A
|
|
539
730
|
provider block derive creates warns `spec.derive.analytics_block_created`:
|
|
540
|
-
the spec then declares an analytics contract QA gates on.
|
|
731
|
+
the spec then declares an analytics contract QA gates on. A vendor block
|
|
732
|
+
derive creates warns `spec.derive.analytics_vendor_created`: doctor then reads
|
|
733
|
+
that vendor's `campaigns.json` key against the spec, and when the spec had no
|
|
734
|
+
`analytics` block before, QA's analytics leg now runs too. `--dry-run` carries
|
|
541
735
|
the same `file_reformatted`, `projection_stale` and `build_stale` warnings,
|
|
542
736
|
phrased as what a write would do.
|
|
543
737
|
|
|
@@ -1032,6 +1226,109 @@ Record already on disk for this packet's campaign (re-emitted in place), else
|
|
|
1032
1226
|
a freshly minted id; `--new-run` mints on request and `--list` prints the ids
|
|
1033
1227
|
on disk without writing (see docs/workflow-findings-sidecar.md).
|
|
1034
1228
|
|
|
1229
|
+
### Several funnels in one project
|
|
1230
|
+
|
|
1231
|
+
One Page Kit project can hold several funnels, each with its own public route
|
|
1232
|
+
slug (`campaign.public_route_slug`, the `_site/<route>/` directory). Each funnel
|
|
1233
|
+
keeps its own packet and its own records, so setting up, building, recording or
|
|
1234
|
+
running QA for one funnel never changes another funnel's files.
|
|
1235
|
+
|
|
1236
|
+
- The first funnel set up in a project uses the layout above: the packet
|
|
1237
|
+
`campaign-runtime.build.json` and its records directly under
|
|
1238
|
+
`.campaign-runtime/`.
|
|
1239
|
+
- Every later funnel gets a packet named `campaign-runtime-<route>.build.json`
|
|
1240
|
+
at the project root and keeps its records under
|
|
1241
|
+
`.campaign-runtime/funnels/<route>/`. Its packet records that directory as
|
|
1242
|
+
`assembly.runtime_dir`.
|
|
1243
|
+
|
|
1244
|
+
The funnel's runtime directory holds its Build Context, Assembly Report, doctor
|
|
1245
|
+
output, `theme/`, `input/` (normalized brief, Design Source Package, brief
|
|
1246
|
+
payload), `setup-handoff.json`, the committed QA verdict sidecar
|
|
1247
|
+
(`qa-verdict.json`) and its Run Records (`run-records/`). The QA verdict sidecar
|
|
1248
|
+
and Run Records sit under the same relative directory beside the packet, which
|
|
1249
|
+
is the target root unless `--out` placed the packet elsewhere.
|
|
1250
|
+
|
|
1251
|
+
Some things stay shared by the whole project: the run session, the lifecycle,
|
|
1252
|
+
deviation and findings journals, `fetched-specs/`, `agent-context/`,
|
|
1253
|
+
`progress/`, the SDK repin record, the Page Kit build summary, the polish
|
|
1254
|
+
capture images under `.campaign-runtime/polish/` (each file is named by the
|
|
1255
|
+
hash of its bytes, so one funnel's capture never replaces another's), the
|
|
1256
|
+
prepare-build target lock and the full QA verdicts under `qa-output/<map id or
|
|
1257
|
+
route>/`.
|
|
1258
|
+
|
|
1259
|
+
`start` and `prepare-build` choose the packet and directory for you:
|
|
1260
|
+
|
|
1261
|
+
1. `--out` names the packet when given.
|
|
1262
|
+
2. Otherwise, a funnel whose route matches `campaign-runtime.build.json`, or a
|
|
1263
|
+
project with no default packet yet, uses the default packet. A funnel that
|
|
1264
|
+
already has `campaign-runtime-<route>.build.json` keeps it. Any other
|
|
1265
|
+
funnel gets `campaign-runtime-<route>.build.json`.
|
|
1266
|
+
3. A packet that already exists at that path for another route, or whose route
|
|
1267
|
+
cannot be read, is not replaced: the run is refused and names that route.
|
|
1268
|
+
This holds for `--out` too. Only `--force` replaces it, which is how a
|
|
1269
|
+
funnel's route is renamed (below).
|
|
1270
|
+
4. A packet that already records `assembly.runtime_dir` for this route keeps
|
|
1271
|
+
it, and one for this route that records none keeps `.campaign-runtime/`
|
|
1272
|
+
while those records are its own. Otherwise a funnel whose
|
|
1273
|
+
`.campaign-runtime/funnels/<route>/` exists keeps that directory. The
|
|
1274
|
+
project-level `.campaign-runtime/` is used when it holds no Build Context or
|
|
1275
|
+
Assembly Report yet, or when those belong to this route (the report's
|
|
1276
|
+
`identity.public_route_slug`, else the route of the packet the context
|
|
1277
|
+
names). Records that belong to another route, or whose route cannot be
|
|
1278
|
+
read, are never replaced: the funnel gets its own directory instead.
|
|
1279
|
+
|
|
1280
|
+
The choice is made before the prepare-build target lock is taken and checked
|
|
1281
|
+
again once it is held. When another funnel was set up while the run waited
|
|
1282
|
+
(two first funnels started at once in an empty project), the run stops and
|
|
1283
|
+
names where this funnel now belongs; run it again.
|
|
1284
|
+
|
|
1285
|
+
A route used as a directory and file name may contain letters, digits, dots,
|
|
1286
|
+
underscores and hyphens, and may not start with a dot. A packet whose
|
|
1287
|
+
`assembly.runtime_dir` is anything other than `.campaign-runtime` or
|
|
1288
|
+
`.campaign-runtime/funnels/<its own route>` is refused, because following it
|
|
1289
|
+
could read or write another funnel's records.
|
|
1290
|
+
|
|
1291
|
+
Every command that takes `--packet` reads and writes that funnel's records:
|
|
1292
|
+
`doctor` (including `--ci`, the PR evidence check), `next`, `record`, `theme`,
|
|
1293
|
+
`checkpoint`, `polish capture`, `qa`, `run-record`, `page-kit sync` and
|
|
1294
|
+
`spec derive`. Commands that would otherwise pick a packet for you do not
|
|
1295
|
+
guess when the project holds more than one: `readback <root>` refuses with
|
|
1296
|
+
exit 2 and names each packet with its route, and `tooling status` reads no
|
|
1297
|
+
packet pin and says why. Pass `--packet` to name the funnel. In CI, run the
|
|
1298
|
+
evidence check once per packet. `bundle check` still validates only the
|
|
1299
|
+
repository-root bundle of the first funnel; a later funnel's packet reports
|
|
1300
|
+
`bundle.packet.noncanonical_path`.
|
|
1301
|
+
|
|
1302
|
+
A single-funnel project set up before per-funnel records reads exactly as it
|
|
1303
|
+
did. Its packet records no `assembly.runtime_dir`, so every command keeps
|
|
1304
|
+
reading and writing `.campaign-runtime/`, and nothing has to be set up again.
|
|
1305
|
+
Adding a second funnel to that project leaves the first funnel's packet and
|
|
1306
|
+
records where they are.
|
|
1307
|
+
|
|
1308
|
+
A new route beside an existing default packet could be another funnel or a
|
|
1309
|
+
rename of that one, and only the operator knows which. `prepare-build` sets it
|
|
1310
|
+
up as another funnel and prints a `Funnel:` line saying so, with the steps to
|
|
1311
|
+
make it a rename instead (the JSON result carries `funnelAddedBeside`, the
|
|
1312
|
+
default packet's path and route).
|
|
1313
|
+
|
|
1314
|
+
To rename the route of a project's first funnel (the one at
|
|
1315
|
+
`campaign-runtime.build.json`):
|
|
1316
|
+
|
|
1317
|
+
1. Set the new route in the CampaignSpec.
|
|
1318
|
+
2. If setup already ran for the new route, delete what it created:
|
|
1319
|
+
`campaign-runtime-<new route>.build.json` and
|
|
1320
|
+
`.campaign-runtime/funnels/<new route>/`. Do not delete `.campaign-runtime/`
|
|
1321
|
+
itself, which also holds the project's shared records.
|
|
1322
|
+
3. Run `prepare-build --out campaign-runtime.build.json --force` (or `start`
|
|
1323
|
+
with the same flags). The default packet and the records directly under
|
|
1324
|
+
`.campaign-runtime/` are rewritten for the new route in place. Like any
|
|
1325
|
+
`--force` intake, this clears recorded stage evidence and operator state.
|
|
1326
|
+
|
|
1327
|
+
To rename the route of a later funnel, delete its packet
|
|
1328
|
+
(`campaign-runtime-<old route>.build.json`) and its directory
|
|
1329
|
+
(`.campaign-runtime/funnels/<old route>/`), then run setup again for the new
|
|
1330
|
+
route. It is set up as a funnel named for the new route.
|
|
1331
|
+
|
|
1035
1332
|
The packet's top-level `generated_at` (ISO-8601 UTC, `Z` suffix) is stamped by
|
|
1036
1333
|
`prepare-build` on every new packet. Downstream freshness — campaigns-agent's
|
|
1037
1334
|
readback staleness comparison and its multi-packet selection at the repository
|
|
@@ -1042,7 +1339,7 @@ than hand-adding the field. Note that a committed artifact set always reads
|
|
|
1042
1339
|
stale to the readback once HEAD moves past it — freshness proof is a
|
|
1043
1340
|
regeneration at HEAD, not a property a commit can preserve.
|
|
1044
1341
|
|
|
1045
|
-
Commit durable packet/context/report artifacts when they represent a real build handoff. The rest of `.campaign-runtime/` is machine-local and is not for the campaign repository: `run-session.json`, `command-lifecycle.jsonl`, `agent-deviations.jsonl`, `workflow-findings.jsonl`, `run-records/`, `fetched-specs/`, `progress/`, `polish-evidence/`, `evidence/`, and `*.log`/`*.tmp` are per-machine, append-only, or carry live URLs and absolute paths, and so are the full QA verdicts `qa run` writes under the target's `qa-output/`. `start`, `prepare-build`, `install-agent-context`, and `run start` write a managed ignore block for that set into the target's `.gitignore` (keyed on its marker line; edit the list beneath it freely). An existing block gains `qa-output/` and `.campaign-runtime/
|
|
1342
|
+
Commit durable packet/context/report artifacts when they represent a real build handoff. The rest of `.campaign-runtime/` is machine-local and is not for the campaign repository: `run-session.json`, `command-lifecycle.jsonl`, `agent-deviations.jsonl`, `workflow-findings.jsonl`, `run-records/`, `fetched-specs/`, `progress/`, `polish-evidence/`, `evidence/`, and `*.log`/`*.tmp` are per-machine, append-only, or carry live URLs and absolute paths, and so are the full QA verdicts `qa run` writes under the target's `qa-output/`. `start`, `prepare-build`, `install-agent-context`, and `run start` write a managed ignore block for that set into the target's `.gitignore` (keyed on its marker line; edit the list beneath it freely). The block also lists `.campaign-runtime/funnels/*/run-records/` for the Run Records of a project's second and later funnels. An existing block gains `qa-output/`, `.campaign-runtime/progress/` and `.campaign-runtime/funnels/*/run-records/` when they are missing, since those hold live URLs, order references and remit records. The readback bundle (`build-context.json`, `assembly-report.json`, `doctor-output.json`, `qa-verdict.json`), `input/`, `theme/`, `agent-context/`, and `setup-handoff.json` are deliberately not ignored, at the top level or under `funnels/<route>/`. The Campaigns API key is a public, browser-side, domain-allowlisted key and may already be present in the local CampaignSpec as `campaign.campaigns_api_key`; do not duplicate it into the packet unless the spec is unavailable. Do not commit raw private API responses, backend secrets, or temporary media exports.
|
|
1046
1343
|
|
|
1047
1344
|
Packet-mode `doctor` is inspection-only by default and preserves retained evidence and active run journals, even when lifecycle capture is configured. Use `--write` to intentionally record fresh evidence; `--no-write` takes precedence. Inspection still reports current blockers and keeps the same exit status.
|
|
1048
1345
|
|
|
@@ -1050,8 +1347,11 @@ Add `--ci` for the [standing PR evidence check](campaigns-os-build-flow.md#live-
|
|
|
1050
1347
|
`campaigns-os doctor --packet <packet> --ci --no-live-refs`. CI mode assesses the
|
|
1051
1348
|
same build-bound Polish/QA debt as readback, exits 0 for current evidence, 3 for
|
|
1052
1349
|
owed evidence, 4 for unknown debt, and 2 for a missing/unreadable packet. Any owed
|
|
1053
|
-
stage returns 3; otherwise an unknown stage returns 4.
|
|
1054
|
-
|
|
1350
|
+
stage returns 3; otherwise an unknown stage returns 4. Advisory Polish, owed
|
|
1351
|
+
for a page-only change, does not count: such a change owes QA on the changed
|
|
1352
|
+
pages only, which `qa run --packet <packet> --page <page_id>` pays. Other doctor errors remain reported but do not
|
|
1353
|
+
change the CI exit. It prints Markdown for a PR comment that opens with the
|
|
1354
|
+
kind of change it found and what that owes, or adds `ci.status`,
|
|
1055
1355
|
`ci.exit_code`, `ci.polish`, `ci.qa` and `ci.changes` under `--json`.
|
|
1056
1356
|
`--ci` adds no writes or network reads; pass `--no-live-refs` in CI to disable
|
|
1057
1357
|
doctor's existing live campaign read. Without `--ci`, inspection behavior is unchanged.
|
|
@@ -1252,6 +1552,37 @@ fingerprint, refuses as before and says the manifest is unavailable; run
|
|
|
1252
1552
|
`record build` again to record one. Cleaning `_site/` before a build is Page Kit's
|
|
1253
1553
|
job, not this check's.
|
|
1254
1554
|
|
|
1555
|
+
#### What a rebuild changed
|
|
1556
|
+
|
|
1557
|
+
`record build` also keeps two records that let a later check tell a page-only
|
|
1558
|
+
change from a commerce-affecting one
|
|
1559
|
+
([what a change owes](campaigns-os-build-flow.md#what-a-change-owes)):
|
|
1560
|
+
|
|
1561
|
+
- `stages.assembly.commerce_signature`: `{ "algorithm": "commerce-signature/v1",
|
|
1562
|
+
"campaign_config", "pages" }`. `pages` maps each built page's path to a hash
|
|
1563
|
+
of its commerce-bearing markup (every `data-next-*` attribute and other
|
|
1564
|
+
package, offer, shipping or upsell attribute and `os-checkout-*` attribute,
|
|
1565
|
+
every inline event handler, every `next-*` meta tag, every script's `src` or
|
|
1566
|
+
inline body, every link target and form action). Copy,
|
|
1567
|
+
image references and styling are not in the hash. `campaign_config` is a
|
|
1568
|
+
hash of the campaign's own entry in `_data/campaigns.json`, or
|
|
1569
|
+
`unavailable:<status>` when that entry cannot be read. The record build
|
|
1570
|
+
result does not echo it.
|
|
1571
|
+
- `stages.assembly.build_changes`: one entry per rebuild since the builds Polish
|
|
1572
|
+
and QA last reviewed, oldest first, each `{ "from_build_fingerprint",
|
|
1573
|
+
"build_fingerprint", "recorded_at", "kind", "changed_pages",
|
|
1574
|
+
"unmapped_files", "unmapped_file_count", "reasons" }`. `kind` is `page_only`,
|
|
1575
|
+
`commerce_affecting` or `unknown`, and `reasons` names why a change counts as
|
|
1576
|
+
commerce (for example `checkout_page`, `commerce_markup_changed`,
|
|
1577
|
+
`campaign_config_changed`, `unmapped_file`, `page_added`). Entries no stage
|
|
1578
|
+
needs any more are dropped, and at most 25 are kept; a stage that reviewed an
|
|
1579
|
+
older build reads its change as unknown and owes the full stage.
|
|
1580
|
+
|
|
1581
|
+
Readback's `evidence_debt.<stage>.change` and `doctor --ci` read these, so the
|
|
1582
|
+
changed-page list survives `record build`. A record made before these fields
|
|
1583
|
+
existed has no signature, and its next change reads as unknown (full Polish and
|
|
1584
|
+
QA) until `record build` records one.
|
|
1585
|
+
|
|
1255
1586
|
A stale or missing Assembly Source Package Fingerprint is waivable only as an
|
|
1256
1587
|
exceptional Source Freshness Waiver. The waiver must be structured in
|
|
1257
1588
|
`waivers[]`, with `scope: "assembly_source_package_freshness"` or an
|
|
@@ -1785,7 +2116,7 @@ own stages) → calls `next` again → repeat until stage="done"
|
|
|
1785
2116
|
|
|
1786
2117
|
### Refreshing after a CampaignSpec change
|
|
1787
2118
|
|
|
1788
|
-
When the CampaignSpec's material content changes, every build, Polish and QA record made against the earlier content stops counting as current, in local-spec, saved-Map and gateway packets alike. For a saved Map or gateway packet, Campaigns OS compares the copy intake fetched; it does not check whether that copy is the latest remote revision, and reports the remote as unconfirmed. Doctor warns `spec.material_stale`, and `next`, progress, readback, the Assembly Report's own status and the QA gate show the stages that are owed again; `next` routes to the first one. Run `campaigns-os record spec --packet <packet>` to bind the new content: it updates the build context and report identity, re-reads the brief file last saved (by intake or `record brief`), keeps waivers, warning accepts, the applied theme and deploy settings, and moves each superseded stage record into that stage's `history`, which is never used as current proof. `spec derive` no longer rebinds identity; run `record spec` after it. A change to the
|
|
2119
|
+
When the CampaignSpec's material content changes, every build, Polish and QA record made against the earlier content stops counting as current, in local-spec, saved-Map and gateway packets alike. For a saved Map or gateway packet, Campaigns OS compares the copy intake fetched; it does not check whether that copy is the latest remote revision, and reports the remote as unconfirmed. Doctor warns `spec.material_stale`, and `next`, progress, readback, the Assembly Report's own status and the QA gate show the stages that are owed again; `next` routes to the first one. Run `campaigns-os record spec --packet <packet>` to bind the new content: it updates the build context and report identity, re-reads the brief file last saved (by intake or `record brief`), keeps waivers, warning accepts, the applied theme and deploy settings, and moves each superseded stage record into that stage's `history`, which is never used as current proof. `spec derive` no longer rebinds identity; run `record spec` after it. A move of page routes binds with `record spec`, and so does a page added or removed after handoff (see [Binding a page change](#binding-a-page-change)); a change to the order of active pages, or a page added or removed before handoff, still needs intake. Re-recording an unchanged build after a spec change keeps the build owed, because the change has not reached the pages, unless the operator records a reason with `--deviation-reason`; this holds even when `record spec` was not run, and that `record build` also marks Polish and QA owed again. Polish stays owed until a new `polish capture`, with one exception: when `record build --deviation-reason` records the very output a completed Polish reviewed (same build fingerprint and source package) and only the CampaignSpec changed, not the brief's presentation, that `record build` restores the Polish record the change moved to history, stamped with the current inputs. It is the same review of the same pages. QA must produce a new verdict against the current content. Polish measurements of byte-identical output keep their values, but the Polish stage is owed again. These checks are tamper evidence, not proof: a hand-written record that copies the current values, or a packet pointed at an older copy of the spec, is not detected.
|
|
1789
2120
|
|
|
1790
2121
|
The CampaignSpec's material content is the whole spec except `spec_identity`,
|
|
1791
2122
|
`slug`, `map_id` and `saved_at`; a page `label` edit is material. A spec
|
|
@@ -1793,18 +2124,19 @@ change affects the stages as follows:
|
|
|
1793
2124
|
|
|
1794
2125
|
| Stage | After a spec material change |
|
|
1795
2126
|
|---|---|
|
|
1796
|
-
| `prepare_build` | unchanged (`record spec` refuses a page
|
|
2127
|
+
| `prepare_build` | unchanged (`record spec` refuses a page reorder, and a page added or removed before handoff, which need intake) |
|
|
1797
2128
|
| `doctor` | recomputed on every read |
|
|
1798
2129
|
| `setup` | kept |
|
|
1799
2130
|
| `assembly` | owed: `required`, `required_by: "spec"`, `required_for: ["polish", "qa"]` |
|
|
1800
2131
|
| `polish` | owed: `required`, `required_by: "spec"`, `required_for: ["qa"]` |
|
|
1801
|
-
| `deploy` | kept |
|
|
2132
|
+
| `deploy` | kept, unless a page change alters the set of built pages: then owed (`required`, `required_by: "spec"`, `required_for: ["qa"]`, the probe kept in its history) |
|
|
1802
2133
|
| `qa` | owed: `required`, `required_by: "spec"`, `required_for: []` |
|
|
1803
2134
|
|
|
1804
2135
|
`record spec` refuses, writing nothing, in this order: `spec_unreadable` (the
|
|
1805
2136
|
spec does not parse), `spec_identity_changed` (its map id or local spec id
|
|
1806
|
-
names another campaign than the packet or the report), `page_scope_changed
|
|
1807
|
-
|
|
2137
|
+
names another campaign than the packet or the report), `page_scope_changed`
|
|
2138
|
+
(the active pages are in another order, or a page was added or removed before
|
|
2139
|
+
handoff), the page-change refusals below, then, for the brief file last saved (by intake or `record brief`), `brief_too_large`,
|
|
1808
2140
|
`brief_source_is_package_artifact` and `brief_file_missing` (the recorded
|
|
1809
2141
|
path is missing or is not a readable regular file: a directory, a pipe or
|
|
1810
2142
|
socket, a symlink to nothing, or a file without read permission). When it must
|
|
@@ -1817,8 +2149,69 @@ does not record which content it was made against is named in a
|
|
|
1817
2149
|
normalized brief from the brief file last saved, so an edit made to the
|
|
1818
2150
|
normalized brief itself is replaced by what its source gives. A stage whose spec
|
|
1819
2151
|
stamp already equals the new content (a build recorded after the edit) is not
|
|
1820
|
-
demoted. `record spec`
|
|
1821
|
-
check and writes nothing
|
|
2152
|
+
demoted. `record spec` writes the Build Packet only when it binds a page
|
|
2153
|
+
change. `--dry-run` runs every check and writes nothing; its `would_write`
|
|
2154
|
+
lists every file the bind would write.
|
|
2155
|
+
|
|
2156
|
+
#### Binding a page change
|
|
2157
|
+
|
|
2158
|
+
`record spec` compares the spec's active pages with the Build Context's
|
|
2159
|
+
`spec.active_pages` in two parts: page identity (the ids, and the order of the
|
|
2160
|
+
pages both lists keep) and routes. It binds:
|
|
2161
|
+
|
|
2162
|
+
- **A move of routes alone**, whenever it happens (#531). This is what a
|
|
2163
|
+
`spec derive` route change needs.
|
|
2164
|
+
- **A page added or removed** on a handed-off campaign (deploy or QA
|
|
2165
|
+
completed, now or in the stage's history; #721). This is what `spec derive`
|
|
2166
|
+
writes when the page tree gains or loses a page after handoff.
|
|
2167
|
+
|
|
2168
|
+
Binding re-projects every page's page-kit target (a page's `next_url` and
|
|
2169
|
+
`decline_url` read the routes of the pages it links to) and writes, in
|
|
2170
|
+
publication order with the report last: the Design Source Package, when a kept
|
|
2171
|
+
page's Surface Identity changes (route, page-kit target or label); the Build
|
|
2172
|
+
Packet's `source_html.pages[]` (an added page as
|
|
2173
|
+
`{ page_id, page_type, origin: "repo", page_kit }`, a removed page's entry
|
|
2174
|
+
dropped) and its `design_source_package` reference; the normalized brief when
|
|
2175
|
+
it is re-derived; the Build Context's `spec.active_pages`, `page_map` and
|
|
2176
|
+
`design_source_package` reference; and the Assembly Report, whose
|
|
2177
|
+
`design_source_package` reference moves with the other two. The package is
|
|
2178
|
+
rewritten at its own path, with a pending provenance record beside it until
|
|
2179
|
+
the report records its new sha256. A run that stops part way leaves a
|
|
2180
|
+
package the next `record spec` or `prepare-build` still knows as its own, and
|
|
2181
|
+
the next `record spec` finishes the job: when that provenance vouches for the
|
|
2182
|
+
package on disk and any of the packet's, the Build Context's or the report's
|
|
2183
|
+
references names other bytes, it moves them (result field
|
|
2184
|
+
`design_source_references_repaired`), and the build, Polish and QA are owed
|
|
2185
|
+
again, since what the stopped run changed cannot be told from what is left. An
|
|
2186
|
+
added page's own page file is its source, so doctor counts it as mapped, and
|
|
2187
|
+
as built once the file exists (`source_html.pages.repo_page_missing` when it
|
|
2188
|
+
does not), and the brief is not asked to name its design authority.
|
|
2189
|
+
|
|
2190
|
+
The stages: build, Polish and QA are owed again as for any spec change. A
|
|
2191
|
+
deploy is bound to the build fingerprint it probed, so the next `record
|
|
2192
|
+
build` makes it owed when the rebuilt site differs. A deploy recorded before
|
|
2193
|
+
that stamp existed carries no fingerprint; `record spec` makes it owed when
|
|
2194
|
+
the set of built page routes changes. A move that changes nothing the
|
|
2195
|
+
build wrote (every page projects to the same target and links, the routing
|
|
2196
|
+
hints are unchanged, the Design Source Package is unchanged, and nothing but
|
|
2197
|
+
`page_url` differs from the material the Build Context recorded in
|
|
2198
|
+
`spec.route_masked_material_hash` at the last bind) demotes nothing: the
|
|
2199
|
+
build, Polish and QA records made against the bound spec are restamped with
|
|
2200
|
+
the new content. A respelling such as `upsell/` to `/<slug>/upsell/` is that
|
|
2201
|
+
case. A Build Context written before this field existed has no recorded value,
|
|
2202
|
+
so its first move is treated as a full change.
|
|
2203
|
+
|
|
2204
|
+
Refused, writing nothing:
|
|
2205
|
+
|
|
2206
|
+
| Code | When |
|
|
2207
|
+
|---|---|
|
|
2208
|
+
| `route_root_changed` | the spec names another route slug, or declares a `route_root` other than the one the packet serves at; the recorded preview and deploy are rooted there, so that needs intake |
|
|
2209
|
+
| `routing_hint_stale` | a page's `sdk_hints.meta_tags` routing hint names a route its page no longer has; edit the hint in the spec first (`spec derive` rewrites the ones on an edge it re-points after handoff) |
|
|
2210
|
+
| `page_kit_target_conflict` | two pages project to one page-kit target file |
|
|
2211
|
+
| `design_source_package_unreadable` | the package the packet names cannot be read |
|
|
2212
|
+
| `design_source_package_operator_supplied` | the new pages would change the package, and the report does not record it as one prepare-build synthesized (`origin` `adopted`, or no `origin` in a report older than that field), so `record spec` does not rewrite it |
|
|
2213
|
+
| `design_source_package_edited` | the new pages would change the package, and it no longer hashes to the sha256 the packet records (or to one its pending provenance record names) |
|
|
2214
|
+
| `design_source_package_unbindable` | the package with the new page surfaces does not validate |
|
|
1822
2215
|
|
|
1823
2216
|
### Recording stage completion
|
|
1824
2217
|
|
|
@@ -1828,7 +2221,7 @@ hand-editing `.campaign-runtime/` JSON:
|
|
|
1828
2221
|
| Command | Writes | Refused (nothing written) when |
|
|
1829
2222
|
|---|---|---|
|
|
1830
2223
|
| `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 |
|
|
1831
|
-
| `campaigns-os record build --packet <p> [--build-environment <development\|production>] [--adapter-decision <key>=<value>[,<key>=<value>...]]` | `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, `evidence.build_environment` = the `--build-environment` value when given (kept from the last record otherwise), and the specified scalar `report.adapter_decisions`; all recordable decisions go in one comma-separated flag (a repeated flag keeps only the last list); `wrapper_policy` stays an intake choice. `stages.polish` resets to `required` (`required_by` build, `required_for` qa) unless its evidence is bound to this exact output; a completed `stages.deploy` whose `source_build_fingerprint` names other output resets to `required` the same way, without the old probe's `outputs` and `evidence`, and its prior record is kept in `stages.deploy.history` (a deploy with no `source_build_fingerprint`, recorded before deploy stamped it, is kept) | doctor cannot compute the fingerprint (no `_site/<public_route_slug>/`), setup is still required, `stages.setup` is not terminal, or an adapter decision key/value is invalid |
|
|
2224
|
+
| `campaigns-os record build --packet <p> [--build-environment <development\|production>] [--adapter-decision <key>=<value>[,<key>=<value>...]]` | `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, `evidence.build_environment` = the `--build-environment` value when given (kept from the last record otherwise), and the specified scalar `report.adapter_decisions`; all recordable decisions go in one comma-separated flag (a repeated flag keeps only the last list); `wrapper_policy` stays an intake choice. `stages.polish` resets to `required` (`required_by` build, `required_for` qa, `owed_since_build_fingerprint` the build Polish last reviewed) unless its evidence is bound to this exact output; `stages.assembly.commerce_signature` and `stages.assembly.build_changes` record what the rebuild changed (see "What a rebuild changed" below); a completed `stages.deploy` whose `source_build_fingerprint` names other output resets to `required` the same way, without the old probe's `outputs` and `evidence`, and its prior record is kept in `stages.deploy.history` (a deploy with no `source_build_fingerprint`, recorded before deploy stamped it, is kept) | doctor cannot compute the fingerprint (no `_site/<public_route_slug>/`), setup is still required, `stages.setup` is not terminal, or an adapter decision key/value is invalid |
|
|
1832
2225
|
| `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 |
|
|
1833
2226
|
| `campaigns-os record theme --packet <p>` | `report.theme`: status `applied`, `load_order` `after-next-core`, `css_path`, `commerce_pages` and per-page evidence read from each built commerce page's stylesheet links; any earlier theme waiver is cleared | build is not recorded for the current output, the campaign ships no commerce pages, a built commerce page that loads `next-core.css` does not load `brand-theme.css` (or `checkout-brand.css`) after it or links one missing from the built output, or no built commerce page loads `next-core.css` |
|
|
1834
2227
|
| `campaigns-os record deploy --packet <p> --base-url <url>` | the packet's `deploy.preview_url` and `stages.deploy` completed with the URL in `outputs`, one evidence line per built page that answered (each page is requested under the URL first) and, on a hosted target, one per script or stylesheet served as built, and `source_build_fingerprint` = the recorded `stages.assembly.build_fingerprint` it probed, so a later `record build` of different output makes deploy owed again and `next` routes back to `record deploy` before QA; a byte-identical rebuild keeps it current | the URL does not name the campaign's route root; on a `local-serve` packet, the URL is not a loopback origin or a built page does not answer 2xx; on any other deploy target, the URL is not https (plain http only on a loopback host), a built page does not answer 200, or a script or stylesheet the built pages load from that origin is missing or served with other bytes (sha256) than the built output (each one named; HTML is not compared, since hosts inject markup); polish is not recorded, the built output changed since build was recorded, or the theme gate is blocked |
|