@nextcommerce/campaigns-os 1.50.0 → 1.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +426 -0
- package/agents/claude/CLAUDE.md +2 -2
- package/agents/codex/AGENTS.md +1 -1
- package/agents/copilot/copilot-instructions.md +1 -1
- package/agents/cursor/campaigns-os.mdc +1 -1
- package/campaign-spec/dist/rules/campaign-metadata.d.ts +5 -1
- package/campaign-spec/dist/rules/campaign-metadata.js +9 -2
- package/campaign-spec/dist/rules/design-source-shape.js +13 -3
- package/campaign-spec/dist/rules/sdk-version.js +2 -1
- package/compatibility.json +1 -1
- package/contracts/commerce-surface-catalog.json +26 -46
- package/contracts/effects.v1.json +81 -2
- package/contracts/release-ledger.json +906 -0
- package/contracts/supported-surface.json +2 -2
- package/contracts/template-brand-contract.shared-commerce.v0.json +2 -2
- package/contracts/template-slot-manifest.shared-content-core.v0.json +24 -0
- package/docs/build-packet.md +93 -9
- package/docs/campaign-build-brief.md +25 -1
- package/docs/effects.md +6 -0
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/polish-evidence.md +10 -0
- package/docs/qa-and-test-orders.md +45 -4
- package/docs/runtime-readiness.md +1 -1
- package/docs/sdk-storage-compatibility.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/package.json +1 -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 +3 -3
- package/skills/next-campaigns-os/SKILL.md +4 -4
- package/skills/next-campaigns-os/references/session-intake.md +7 -3
- package/skills/next-campaigns-os-setup/SKILL.md +3 -3
- package/skills/next-campaigns-polish/SKILL.md +3 -3
- package/skills/next-campaigns-qa/SKILL.md +6 -5
- package/skills.json +10 -10
- package/src/adapter-decision-contract.mjs +1 -1
- package/src/brand-theme.mjs +12 -0
- package/src/build-brief.mjs +68 -21
- package/src/built-site-scope.mjs +39 -6
- package/src/built-smoke-qc.mjs +1117 -0
- package/src/campaign-identity.mjs +36 -2
- package/src/cart-placeholders.mjs +730 -0
- package/src/cli.mjs +310 -34
- package/src/commercial-journey.mjs +65 -4
- package/src/commercial-parity.mjs +6 -1
- package/src/doctor/checks.mjs +291 -24
- package/src/doctor/inspect.mjs +53 -2
- package/src/doctor/next-step.mjs +1 -1
- package/src/invocation.mjs +2 -1
- package/src/local-preview-policy.mjs +1 -1
- package/src/local-proof.mjs +4 -1
- package/src/polish-browser.mjs +218 -1
- package/src/polish-capture.mjs +1 -1
- package/src/polish-media-weight.mjs +492 -0
- package/src/polish-node.mjs +96 -4
- package/src/progress-node.mjs +5 -1
- package/src/qa-browser.mjs +308 -96
- package/src/qa-content-params.mjs +889 -0
- package/src/qa-node.mjs +104 -12
- package/src/qa-order-bump.mjs +22 -1
- package/src/qa-policy-links.mjs +1019 -0
- package/src/qa-tracking-params.mjs +1389 -0
- package/src/qa-url-privacy.mjs +168 -0
- package/src/qc-accept.mjs +446 -0
- package/src/qc-check-registry.mjs +83 -0
- package/src/qc-results.mjs +1049 -0
- package/src/sdk-attribute-index.mjs +71 -0
- package/src/sdk-markup.mjs +2 -2
- package/src/sdk-storage-compatibility.mjs +63 -3
- package/src/source-prep.mjs +37 -7
- package/src/stage-record.mjs +56 -17
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: next-campaigns-os
|
|
3
|
-
version: 1.0.
|
|
3
|
+
version: 1.0.47
|
|
4
4
|
description: Coordinate Campaigns OS lifecycle workflows from CampaignSpec, Build Packet, starter-template contracts, stage reports, deploy evidence, and QA proof depth.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
Bundle revision: 1.
|
|
8
|
-
Run `npx --no-install campaigns-os tooling status --skills-revision 1.
|
|
7
|
+
Bundle revision: 1.52.0+skills.1
|
|
8
|
+
Run `npx --no-install campaigns-os tooling status --skills-revision 1.52.0+skills.1`
|
|
9
9
|
from the campaign's Page Kit folder, where it runs the project's pinned copy and
|
|
10
10
|
never installs one, at the start of each task. Start a fresh session if it
|
|
11
11
|
reports `mismatch`: this text is already in your context and is never re-read
|
|
@@ -95,7 +95,7 @@ Map and run endpoints) with a local CampaignSpec, prepared HTML/assets source, t
|
|
|
95
95
|
retains each attributed exception as `ready_with_exceptions`, and one
|
|
96
96
|
exception never suppresses another blocker.
|
|
97
97
|
10. Run the package-owned proof path in sequence: ensure `npx --no-install campaigns-os qa install-browser` has completed, run `campaigns-os qa resolve --packet <packet>` (tier `A`: it fetches `--base-url`; `--no-probe` is tier `B` and local), then `campaigns-os qa run --packet <packet> --base-url <url> --browser --test-order common` (tier `C`: it overwrites the stored verdict and assembly report, places real typed-card test orders against the campaign, and posts the verdict and progress; `--no-post-verdict` drops the verdict POST and `--no-remit` the Run Record remit, but both stay tier `C`).
|
|
98
|
-
11. Treat typed-card proof coverage as the control.
|
|
98
|
+
11. Treat typed-card proof coverage as the control: `qa run` has no permission flag. Test orders still land in the store as real orders (global test cards: no charge, no transaction) that someone may have to cancel. Unless the operator has already said test orders are fine for this campaign, ask once, up front in your first turn with your other setup questions, so the answer covers the whole build and QA and neither stops for it later. `common` runs every actual terminal path when they fit under the flood cap (`--max-test-orders`, 6 by default); above the cap it runs checkout, first-offer accept and decline, and a deduplicated shortest real receipt path, then adds one decline path per offer or downsell page not yet declined, up to the cap, and names any page left out. A page counts as covered only when an order clicks its decline; the `browser-test-order:upsell-action-coverage` verdict row warns naming each page whose decline no order clicked. `full` walks every actual terminal path in the selected checkout topology; cycles, missing routes, and reachable nonterminals block exhaustive proof before browser launch. The accidental-flood cap remains `6`, and an overflow names the exact explicit `--max-test-orders` raise. Localhost on any port is a Campaigns App Development domain for SDK QA with analytics suppressed; non-localhost preview/production origins still need SDK origin allowlist confirmation.
|
|
99
99
|
12. Discuss launch only from recorded build, polish, deploy, browser QA, and test-order evidence, or from explicit blockers.
|
|
100
100
|
|
|
101
101
|
## Session Intake
|
|
@@ -141,9 +141,13 @@ hand-edit deployed routing config as the primary promotion path.
|
|
|
141
141
|
|
|
142
142
|
## Test-Order Proof Policy
|
|
143
143
|
|
|
144
|
-
Treat test orders as
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
Treat test orders as repeatable proof. `qa run` has no permission flag:
|
|
145
|
+
coverage is its only control. Test orders still land in the store as real
|
|
146
|
+
orders (global test cards: no charge, no transaction) that someone may have to
|
|
147
|
+
cancel. Unless the operator has already said test orders are fine for this
|
|
148
|
+
campaign, ask once, up front in your first turn with your other setup questions,
|
|
149
|
+
so the answer covers the whole build and QA and neither stops for it later.
|
|
150
|
+
Record:
|
|
147
151
|
|
|
148
152
|
- Coverage: `common` (every actual terminal path when they fit under the flood cap; above it, checkout, first-offer accept/decline, a deduplicated shortest real receipt path, and one decline path per offer or downsell page not yet declined, up to the cap), `off`, `checkout`, `decline`, `accept`, `both`, `full`, or explicit paths such as `decline-decline-accept`.
|
|
149
153
|
- Cart matrix: base cart, base plus bump, specific package refs/quantities.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: next-campaigns-os-setup
|
|
3
|
-
version: 2.0.
|
|
3
|
+
version: 2.0.30
|
|
4
4
|
description: Bootstrap or prepare a target page-kit campaign repo from a doctor-cleared Campaigns OS Build Packet before full build wiring. Formerly installed as next-campaigns-setup; renamed 2026-08 to stop colliding with the published NextCommerceCo/skills scaffolder of that name.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
Bundle revision: 1.
|
|
8
|
-
Run `npx --no-install campaigns-os tooling status --skills-revision 1.
|
|
7
|
+
Bundle revision: 1.52.0+skills.1
|
|
8
|
+
Run `npx --no-install campaigns-os tooling status --skills-revision 1.52.0+skills.1`
|
|
9
9
|
from the campaign's Page Kit folder, where it runs the project's pinned copy and
|
|
10
10
|
never installs one, at the start of each task. Start a fresh session if it
|
|
11
11
|
reports `mismatch`: this text is already in your context and is never re-read
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: next-campaigns-polish
|
|
3
|
-
version: 1.1.
|
|
3
|
+
version: 1.1.31
|
|
4
4
|
description: Run the visual/runtime polish pass after build and before QA for a Campaigns OS campaign.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
Bundle revision: 1.
|
|
8
|
-
Run `npx --no-install campaigns-os tooling status --skills-revision 1.
|
|
7
|
+
Bundle revision: 1.52.0+skills.1
|
|
8
|
+
Run `npx --no-install campaigns-os tooling status --skills-revision 1.52.0+skills.1`
|
|
9
9
|
from the campaign's Page Kit folder, where it runs the project's pinned copy and
|
|
10
10
|
never installs one, at the start of each task. Start a fresh session if it
|
|
11
11
|
reports `mismatch`: this text is already in your context and is never re-read
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: next-campaigns-qa
|
|
3
|
-
version: 1.3.
|
|
3
|
+
version: 1.3.31
|
|
4
4
|
description: Run spec-aware QA from a saved Map or local-spec Build Packet and tested campaign URL after build, polish, and deploy/local evidence exist, including Playwright typed-card test-order proof.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
Bundle revision: 1.
|
|
8
|
-
Run `npx --no-install campaigns-os tooling status --skills-revision 1.
|
|
7
|
+
Bundle revision: 1.52.0+skills.1
|
|
8
|
+
Run `npx --no-install campaigns-os tooling status --skills-revision 1.52.0+skills.1`
|
|
9
9
|
from the campaign's Page Kit folder, where it runs the project's pinned copy and
|
|
10
10
|
never installs one, at the start of each task. Start a fresh session if it
|
|
11
11
|
reports `mismatch`: this text is already in your context and is never re-read
|
|
@@ -89,7 +89,7 @@ Rules:
|
|
|
89
89
|
- A typed-card path that fails is classified by **what it did to the store** before the runner decides what to do about it. A failure the runner can prove happened before submit (`not_created`) is **re-run once, if the creation budget has a slot no still-unrun planned path needs** — so a transient miss is not reported as a defect in the build, without an early path eating budget the last planned paths need. Under the default budget a path whose submit was *rejected* has already spent its own slot, so it is not re-run and records `evidence.order_creation.rerun_skipped` instead. When the re-run does happen, both attempts appear in `test_orders[]` and `evidence.retry` names the first attempt's error and ref id. A failure that happened **after** the order was created (`created` — most often a receipt that did not render) is **never resubmitted**: the runner reloads that order's receipt and re-runs only the read-only checks, and `evidence.recovery` carries the original failure, the checks re-run, and whether it cleared. Read `evidence.order_creation` for two separate counts: `submissions_reserved` (platform-side creation slots charged to this path — reserved before a submit click, or charged for a hosted-checkout redirect where no submit click happens — which stand even when the create then failed) and `orders_confirmed_created` (creates the platform was observed to accept) — a spent slot with no confirmed order is the ambiguous case, not an order to reconcile. Recovery clears only on persisted evidence it re-read on that pass: a failed or absent order read-back stops it honestly rather than re-deciding against the original attempt's numbers. An outcome it cannot prove either way (`ambiguous` — an unusable read-back, a lost create response, a network-failed create, a 4xx after an earlier 2xx) stops the path and names the operator check instead of buying again. A pass that only came back after recovery is never indistinguishable from a first-attempt pass, and a failure that survives recovery still blocks.
|
|
90
90
|
- Analytics correctness is two-phase in the same run: the campaign-root visit inventories declared providers/tags only, then the one canonical typed-card run proves Purchase for each topology-recognized receipt from the signals emitted across every page the path loaded after checkout, read after the full `--analytics-settle` window. The receipt qualifies the order; the journey is measured, because the SDK fires `dl_purchase` (and the outbound Purchase) on the first `?ref_id=` page — the upsell page when the funnel has one — and dedupes it on the receipt. It never places a second analytics order. The receipt document's own reading stays in evidence (`receipt_signals`, `fired_on`) as the diagnostic of which document fired.
|
|
91
91
|
- A missing or topology-unrecognized receipt is `MANUAL_REVIEW`/`WARN`; a recognized receipt with no dataLayer, outbound Meta, or outbound GA4 Purchase is `FAIL`/`BLOCKER`. Capture, unreadable-page, and settle-deadline errors on a recognized receipt are explicit non-waivable blockers. The `analytics-correctness:purchase-fires` waiver applies only to a genuine recognized-receipt/no-signal failure, and is recorded with `campaigns-os qa waive --assertion analytics-correctness:purchase-fires --reason "<why>"` (tier `C`: it overwrites the assembly report and doctor output; the lane is scoped to that one assertion and every other is refused).
|
|
92
|
-
- Keep QA in a tight sequence: install the Playwright browser, resolve topology, run browser QA plus typed-card proof with `--test-order common` by default.
|
|
92
|
+
- Keep QA in a tight sequence: install the Playwright browser, resolve topology, run browser QA plus typed-card proof with `--test-order common` by default. `qa run` has no permission flag: coverage is its only control. Test orders still land in the store as real orders that someone may have to cancel, so unless the operator has already said test orders are fine for this campaign, ask once, up front in your first turn with your other setup questions. When the operator has answered, at the start of this session or earlier in the build, follow that answer and do not pause QA to ask again. When nobody asked, ask once before the first test order rather than skip the question. Otherwise pause only for missing inputs, out-of-scope runtime pages that block checkout proof, or merchant-specific uncertainty.
|
|
93
93
|
- Use `--browser` for rendered browser evidence. Browser QA must use the package-owned Playwright flow, not external agent/browser skills.
|
|
94
94
|
- Saved-Map QA publishes to the QA portal under the existing consent and flag controls; report the portal link only when publication succeeds. Pass `--no-post-verdict` (or `--local-only`) to keep that verdict local. This stays tier `C`: served-page probes, requested orders, Run Telemetry and progress retain their own controls. See `docs/qa-and-test-orders.md` for the saved-Map publication policy.
|
|
95
95
|
- Local-spec QA always keeps verdicts and progress local. `qa run` suppresses portal publication even with `--post-verdict`; `qa publish` refuses local-spec packets with `local_spec`. Report the local verdict/sidecar as evidence, never a dashboard link. A matching route cannot replace a matching `local_spec_id`, and QA refuses a foreign or stale local Assembly Report. Run Telemetry still follows its consent controls.
|
|
@@ -116,11 +116,12 @@ Rules:
|
|
|
116
116
|
- A path with remaining actions may stop cleanly only at a terminal recognized in that selected topology. Missing accept/decline controls on any other page are blockers. A cross-origin handoff is a valid terminal navigation but is not receipt-rendering or persisted-receipt proof.
|
|
117
117
|
- Keep multi-funnel and `tiers:common` / `tiers:full` plans isolated to each selected checkout's own funnel graph, tiers, and recognized terminals; never borrow an unrelated funnel's receipt page.
|
|
118
118
|
- Accepted-upsell proof is valid only when the browser observes the order upsell API mutation and the final order evidence contains the selected upsell package. A checkout bump line marked `is_upsell` is not accepted-upsell proof.
|
|
119
|
-
- Test orders are
|
|
119
|
+
- Test orders are real orders in the store, but global test cards bypass the gateway: no charge, no transaction, and no merchant-specific sandbox routing to confirm. Localhost on any port is globally available as a Campaigns App Development domain and suppresses Campaigns analytics; non-localhost preview/production origins must be allowlisted for the campaign API key so the SDK loads — that is about SDK initialization, not test-order permission.
|
|
120
120
|
- Launch readiness is separate from Campaigns OS proof. If QA passes on local/preview, still surface production storefront URL, live payment methods, shipping markets, legal/support URLs, analytics expectations, and merchant-side configuration as real-shopper readiness items before launch.
|
|
121
121
|
- For multi-market campaigns, verify at least one non-default currency/country path: currency display, shipping method names/prices, available payment methods, and market-specific copy.
|
|
122
122
|
- Treat missing deploy URL, missing polish status, or unresolved doctor blockers as launch blockers.
|
|
123
123
|
- Report blockers, warnings, and residual risks.
|
|
124
|
+
- At the end of QA, run `next` and present the QC handoff once. Ask the operator which open warnings to accept and why. Run `checkpoint accept` only with the refs, reason, and name the operator gave you in this conversation. Never write `qc_accepts`, `qc_results`, or QA verdict files by hand. The up-front test-order permission does not cover accepts.
|
|
124
125
|
- QA follows build and polish; it does not edit campaign code.
|
|
125
126
|
|
|
126
127
|
Canonical test-order flow:
|
package/skills.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "campaigns-os-skills",
|
|
3
3
|
"description": "Skills bundled with Campaigns OS. `skills.sh` installs these into the shared agent skill directories (~/.claude/skills, ~/.codex/skills), so each one is a versioned package: bump the version whenever a package changes. `bundle_revision` identifies the bundle as a whole — `<package version>+skills.<n>`, where the prefix is this package's version and `<n>` counts the skill-text revisions published against it. It advances whenever ANY bundled skill changes, every SKILL.md states it on its first body line, and `campaigns-os tooling status --skills-revision <value>` compares the value an agent read from a skill against the bundle on disk. See docs/skills-revision.md.",
|
|
4
|
-
"bundle_revision": "1.
|
|
4
|
+
"bundle_revision": "1.52.0+skills.1",
|
|
5
5
|
"homepage": "https://github.com/NextCommerceCo/campaigns-os",
|
|
6
6
|
"retired_skills": [
|
|
7
7
|
{
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
{
|
|
17
17
|
"id": "next-campaigns-os",
|
|
18
18
|
"name": "Campaigns OS Lifecycle",
|
|
19
|
-
"version": "1.0.
|
|
19
|
+
"version": "1.0.47",
|
|
20
20
|
"path": "skills/next-campaigns-os/SKILL.md",
|
|
21
21
|
"domain": "campaigns",
|
|
22
22
|
"description": "Coordinate Campaigns OS lifecycle workflows from CampaignSpec, Build Packet, starter-template contracts, stage reports, deploy evidence, and QA proof depth."
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
{
|
|
25
25
|
"id": "next-campaigns-os-setup",
|
|
26
26
|
"name": "Campaigns OS Setup",
|
|
27
|
-
"version": "2.0.
|
|
27
|
+
"version": "2.0.30",
|
|
28
28
|
"path": "skills/next-campaigns-os-setup/SKILL.md",
|
|
29
29
|
"domain": "campaigns",
|
|
30
30
|
"description": "Bootstrap or prepare a target page-kit campaign repo from a doctor-cleared Campaigns OS Build Packet before full build wiring. Formerly next-campaigns-setup; renamed to release that name to the published NextCommerceCo/skills scaffolder."
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
{
|
|
33
33
|
"id": "next-campaigns-build",
|
|
34
34
|
"name": "Campaign Build",
|
|
35
|
-
"version": "1.0.
|
|
35
|
+
"version": "1.0.32",
|
|
36
36
|
"path": "skills/next-campaigns-build/SKILL.md",
|
|
37
37
|
"domain": "campaigns",
|
|
38
38
|
"description": "Assemble a NEXT campaign from a doctor-cleared Build Packet, CampaignSpec/API values, prepared HTML/assets, page-kit, and starter-template contracts."
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
{
|
|
41
41
|
"id": "next-campaigns-polish",
|
|
42
42
|
"name": "Campaign Polish",
|
|
43
|
-
"version": "1.1.
|
|
43
|
+
"version": "1.1.31",
|
|
44
44
|
"path": "skills/next-campaigns-polish/SKILL.md",
|
|
45
45
|
"domain": "campaigns",
|
|
46
46
|
"description": "Run the visual/runtime polish pass after build and before QA for a Campaigns OS campaign."
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
{
|
|
49
49
|
"id": "next-campaigns-qa",
|
|
50
50
|
"name": "Campaign QA",
|
|
51
|
-
"version": "1.3.
|
|
51
|
+
"version": "1.3.31",
|
|
52
52
|
"path": "skills/next-campaigns-qa/SKILL.md",
|
|
53
53
|
"domain": "campaigns",
|
|
54
54
|
"description": "Run spec-aware QA from a saved Map or local-spec Build Packet and tested campaign URL after build, polish, and deploy/local evidence exist, including Playwright typed-card test-order proof."
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
{
|
|
57
57
|
"id": "campaign-lifecycle-orientation",
|
|
58
58
|
"name": "Campaign Lifecycle Orientation",
|
|
59
|
-
"version": "1.0.
|
|
59
|
+
"version": "1.0.27",
|
|
60
60
|
"path": "skills/campaign-lifecycle-orientation/SKILL.md",
|
|
61
61
|
"domain": "campaigns",
|
|
62
62
|
"description": "Orient a reader to the Campaigns OS lifecycle artifacts a run has already emitted, without advancing their state: the shared vocabulary, doctor as a gate, the stage record, and the store-theme/Page Kit two-worlds trap."
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
{
|
|
65
65
|
"id": "campaign-run-evidence",
|
|
66
66
|
"name": "Campaign Run Evidence",
|
|
67
|
-
"version": "1.0.
|
|
67
|
+
"version": "1.0.27",
|
|
68
68
|
"path": "skills/campaign-run-evidence/SKILL.md",
|
|
69
69
|
"domain": "campaigns",
|
|
70
70
|
"description": "Interpret existing doctor, QA verdict and proof-depth evidence without claiming more proof than the artifacts contain, and keep funnel proof apart from merchant launch readiness."
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
{
|
|
73
73
|
"id": "campaign-readback-classification",
|
|
74
74
|
"name": "Campaign Readback Classification",
|
|
75
|
-
"version": "1.0.
|
|
75
|
+
"version": "1.0.27",
|
|
76
76
|
"path": "skills/campaign-readback-classification/SKILL.md",
|
|
77
77
|
"domain": "campaigns",
|
|
78
78
|
"description": "Classify a selected campaign from the campaigns-os-readback/v2 fields (artifacts, staleness.stale_keys, clean, doctor, divergences, skip_cascades) and write a read-only handoff without turning diagnosis into permission."
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
{
|
|
81
81
|
"id": "contribution-intake",
|
|
82
82
|
"name": "Contribution Intake",
|
|
83
|
-
"version": "1.0.
|
|
83
|
+
"version": "1.0.27",
|
|
84
84
|
"path": "skills/contribution-intake/SKILL.md",
|
|
85
85
|
"domain": "campaigns",
|
|
86
86
|
"description": "Turn a suggestion about the agent surface into a classified, evidence-checked and redacted proposal, filed on this repository tracker only with attended approval."
|
|
@@ -317,7 +317,7 @@ function validateTemplateSlicePaths(copied, location, targetRepo, warnings, read
|
|
|
317
317
|
|
|
318
318
|
function checkEnum(value, allowed, code, warnings, addIssue) {
|
|
319
319
|
if (value == null) return;
|
|
320
|
-
if (!allowed.has(value)) addIssue(warnings, code, `${code} has unknown value
|
|
320
|
+
if (!allowed.has(value)) addIssue(warnings, code, `${code} has unknown value ${JSON.stringify(value)}; allowed values: ${[...allowed].join(", ")}.`);
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
function isObject(value) {
|
package/src/brand-theme.mjs
CHANGED
|
@@ -88,6 +88,18 @@ function loadContracts(repoRoot = ROOT) {
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// A theme write error as the Assembly Report's theme.warnings[] carries it
|
|
92
|
+
// (schema $defs.themeIssue): detail, when present, must be an object, so an
|
|
93
|
+
// issue without one omits the key rather than writing null.
|
|
94
|
+
export function themeIssueForReport(error) {
|
|
95
|
+
const detail = error?.detail;
|
|
96
|
+
return {
|
|
97
|
+
code: error?.code,
|
|
98
|
+
message: error?.message,
|
|
99
|
+
...(detail && typeof detail === "object" && !Array.isArray(detail) ? { detail } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
91
103
|
function issue(code, message, detail = null) {
|
|
92
104
|
return detail ? { code, message, detail } : { code, message };
|
|
93
105
|
}
|
package/src/build-brief.mjs
CHANGED
|
@@ -12,11 +12,15 @@ export const BUILD_BRIEF_CANDIDATE_FILENAMES = Object.freeze([
|
|
|
12
12
|
"campaign-build-brief.json",
|
|
13
13
|
]);
|
|
14
14
|
|
|
15
|
+
// answer_fields: the brief fields whose values close the question, as
|
|
16
|
+
// evaluateCampaignBuildBrief reads them. The guided-questions warning names
|
|
17
|
+
// them so an answer given in conversation can be written where it counts.
|
|
15
18
|
const REQUIRED_HIGH_IMPACT_FIELDS = Object.freeze([
|
|
16
19
|
{
|
|
17
20
|
id: "page_design_authority",
|
|
18
21
|
priority: 1,
|
|
19
22
|
field: "design_authority",
|
|
23
|
+
answer_fields: ["design_authority.<page_id>.source"],
|
|
20
24
|
question: "Which source controls each campaign page: the provided design export, the selected template, or a template adapted to another page?",
|
|
21
25
|
reason: "Page-by-page authority prevents checkout, OTO, and receipt pages from drifting into unrelated starter-template composition.",
|
|
22
26
|
},
|
|
@@ -24,6 +28,7 @@ const REQUIRED_HIGH_IMPACT_FIELDS = Object.freeze([
|
|
|
24
28
|
id: "brand_palette_cta",
|
|
25
29
|
priority: 2,
|
|
26
30
|
field: "brand",
|
|
31
|
+
answer_fields: ["brand.commerce_palette_source", "brand.cta_style"],
|
|
27
32
|
question: "Which palette and CTA style should commerce pages use?",
|
|
28
33
|
reason: "Commerce pages need a business-approved brand layer instead of silent template defaults.",
|
|
29
34
|
},
|
|
@@ -31,6 +36,7 @@ const REQUIRED_HIGH_IMPACT_FIELDS = Object.freeze([
|
|
|
31
36
|
id: "variant_media_rules",
|
|
32
37
|
priority: 3,
|
|
33
38
|
field: "media",
|
|
39
|
+
answer_fields: ["media.sold_variants", "media.allow_other_variant_colors"],
|
|
34
40
|
question: "Which product variants or colors are actually sold, and may media show other variants?",
|
|
35
41
|
reason: "Variant ambiguity often creates wrong-color carousels and unavailable-product claims.",
|
|
36
42
|
},
|
|
@@ -38,6 +44,7 @@ const REQUIRED_HIGH_IMPACT_FIELDS = Object.freeze([
|
|
|
38
44
|
id: "bundle_pricing_presentation",
|
|
39
45
|
priority: 4,
|
|
40
46
|
field: "offer_presentation.bundle_cards",
|
|
47
|
+
answer_fields: ["offer_presentation.bundle_cards.primary_price"],
|
|
41
48
|
question: "How should bundle cards present pricing: simple unit price, savings-led, or full accounting?",
|
|
42
49
|
reason: "CampaignSpec owns prices; the brief owns which shopper-facing price story is appropriate.",
|
|
43
50
|
},
|
|
@@ -45,13 +52,16 @@ const REQUIRED_HIGH_IMPACT_FIELDS = Object.freeze([
|
|
|
45
52
|
id: "promo_urgency_copy",
|
|
46
53
|
priority: 5,
|
|
47
54
|
field: "promo_urgency",
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
answer_fields: ["promo_urgency.header_claim_source", "promo_urgency.timer_label"],
|
|
56
|
+
answer_hint: 'promo_urgency.header_claim_source "campaign_offers" and promo_urgency.timer_label the template timer\'s label to fill them, or both "none" to remove them',
|
|
57
|
+
question: "Should the starter template's own promo placeholders (demo countdown timers, promo banners, placeholder voucher codes, exit-pop offers) be filled from this campaign's promo codes and offers, or removed?",
|
|
58
|
+
reason: "Template promo placeholders must not go live with demo values. The source design's own promo, proof and urgency copy is the merchant's content: it is built as designed and is not part of this question.",
|
|
50
59
|
},
|
|
51
60
|
{
|
|
52
61
|
id: "payment_methods_trust",
|
|
53
62
|
priority: 6,
|
|
54
63
|
field: "commerce_surfaces.payment_methods_allowed",
|
|
64
|
+
answer_fields: ["commerce_surfaces.payment_methods_allowed"],
|
|
55
65
|
question: "Which payment methods and trust badges may appear?",
|
|
56
66
|
reason: "Templates often carry demo wallets or badges that must not survive without approval.",
|
|
57
67
|
},
|
|
@@ -59,6 +69,7 @@ const REQUIRED_HIGH_IMPACT_FIELDS = Object.freeze([
|
|
|
59
69
|
id: "canonical_display_names",
|
|
60
70
|
priority: 7,
|
|
61
71
|
field: "canonical_display.product_name_source",
|
|
72
|
+
answer_fields: ["canonical_display.product_name_source"],
|
|
62
73
|
question: "Should CampaignSpec display names win, or may runtime/catalog names override them?",
|
|
63
74
|
reason: "Name drift across source, spec, and runtime data is hard to spot after assembly.",
|
|
64
75
|
},
|
|
@@ -66,6 +77,7 @@ const REQUIRED_HIGH_IMPACT_FIELDS = Object.freeze([
|
|
|
66
77
|
id: "regulated_claims",
|
|
67
78
|
priority: 8,
|
|
68
79
|
field: "campaign_intent.compliance",
|
|
80
|
+
answer_fields: ["campaign_intent.compliance.approved_benefit_language", "campaign_intent.compliance.forbidden_claims"],
|
|
69
81
|
question: "Are there regulated claims, forbidden phrases, or approved benefit statements the build must follow?",
|
|
70
82
|
reason: "Health, financial, and other regulated offers need explicit copy boundaries.",
|
|
71
83
|
conditional: "regulated",
|
|
@@ -227,7 +239,18 @@ export function createCampaignBuildBriefArtifact({
|
|
|
227
239
|
};
|
|
228
240
|
}
|
|
229
241
|
|
|
230
|
-
|
|
242
|
+
// "brand_palette_cta (brand.commerce_palette_source, brand.cta_style)": each
|
|
243
|
+
// open question with the brief fields that close it.
|
|
244
|
+
function describeOpenQuestions(questions) {
|
|
245
|
+
return questions.map((question) => {
|
|
246
|
+
const entry = REQUIRED_HIGH_IMPACT_FIELDS.find((candidate) => candidate.id === question?.id);
|
|
247
|
+
if (isNonEmptyString(entry?.answer_hint)) return `${question?.id} (${entry.answer_hint})`;
|
|
248
|
+
const fields = entry?.answer_fields || (isNonEmptyString(question?.field) ? [question.field] : []);
|
|
249
|
+
return fields.length ? `${question?.id} (${fields.join(", ")})` : String(question?.id);
|
|
250
|
+
}).join(", ");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function validateCampaignBuildBriefArtifact(brief, { spec = null, normalizedPath = BUILD_BRIEF_NORMALIZED_REL_PATH } = {}) {
|
|
231
254
|
const errors = [];
|
|
232
255
|
const warnings = [];
|
|
233
256
|
const ready = [];
|
|
@@ -255,14 +278,20 @@ export function validateCampaignBuildBriefArtifact(brief, { spec = null } = {})
|
|
|
255
278
|
if (questions.length) {
|
|
256
279
|
errors.push({
|
|
257
280
|
code: "build_brief.questions_unanswered",
|
|
258
|
-
message: `Prepared Campaign Build Brief has ${questions.length} unresolved business question(s): ${questions.
|
|
281
|
+
message: `Prepared Campaign Build Brief has ${questions.length} unresolved business question(s): ${describeOpenQuestions(questions)}. Set those fields in the brief file and re-run start or prepare-build.`,
|
|
259
282
|
});
|
|
260
283
|
}
|
|
261
284
|
} else {
|
|
262
285
|
if (questions.length) {
|
|
286
|
+
// An answer given in conversation is not recorded until it is in a
|
|
287
|
+
// brief file that start/prepare-build reads: the guided draft is
|
|
288
|
+
// regenerated on every run, and a brief file replaces it whole.
|
|
263
289
|
warnings.push({
|
|
264
290
|
code: "build_brief.guided_questions",
|
|
265
|
-
message: `Generated Campaign Build Brief draft has ${questions.length} high-impact business question(s) to confirm: ${questions
|
|
291
|
+
message: `Generated Campaign Build Brief draft has ${questions.length} high-impact business question(s) to confirm: ${describeOpenQuestions(questions)}. `
|
|
292
|
+
+ `An answer counts only once it is in a brief file: copy ${normalizedPath} to campaign-build-brief.json in the target repo, set those fields, and re-run start or prepare-build with the same arguments `
|
|
293
|
+
+ "(the file is found there automatically, or pass --brief <file>). The file replaces the draft, so start from the copy to keep the fields the draft already filled. "
|
|
294
|
+
+ "Once a stage has recorded evidence, that re-run needs --force, which clears the evidence.",
|
|
266
295
|
});
|
|
267
296
|
}
|
|
268
297
|
for (const gate of blockerGates) {
|
|
@@ -358,6 +387,9 @@ function draftCampaignBuildBrief({ spec, activePages, pageMappings, templateFami
|
|
|
358
387
|
const paymentMethods = collectSpecPaymentMethods(spec);
|
|
359
388
|
const hasExitPop = activePages?.some((page) => page?.type === "checkout" && page?.exit_intent?.enabled === true) === true;
|
|
360
389
|
const hasOrderBump = hasOrderBumpSignals(spec);
|
|
390
|
+
// With no CampaignSpec surface to fill them, the template's promo
|
|
391
|
+
// placeholders are removed: "none" for both.
|
|
392
|
+
const fillsTemplatePromo = templatePromoSurfaces({ spec, activePages }).length > 0;
|
|
361
393
|
|
|
362
394
|
return {
|
|
363
395
|
schema_version: BUILD_BRIEF_SCHEMA,
|
|
@@ -394,8 +426,8 @@ function draftCampaignBuildBrief({ spec, activePages, pageMappings, templateFami
|
|
|
394
426
|
},
|
|
395
427
|
},
|
|
396
428
|
promo_urgency: {
|
|
397
|
-
header_claim_source:
|
|
398
|
-
timer_label:
|
|
429
|
+
header_claim_source: fillsTemplatePromo ? "campaign_offers" : "none",
|
|
430
|
+
timer_label: fillsTemplatePromo ? null : "none",
|
|
399
431
|
show_promo_code_in_timer: false,
|
|
400
432
|
exit_pop: {
|
|
401
433
|
enabled: hasExitPop,
|
|
@@ -427,7 +459,7 @@ function draftCampaignBuildBrief({ spec, activePages, pageMappings, templateFami
|
|
|
427
459
|
brand: "low",
|
|
428
460
|
media: variantSignals.length === 1 ? "medium" : "low",
|
|
429
461
|
offer_presentation: "low",
|
|
430
|
-
promo_urgency:
|
|
462
|
+
promo_urgency: fillsTemplatePromo ? "low" : "medium",
|
|
431
463
|
commerce_surfaces: paymentMethods.length ? "medium" : "low",
|
|
432
464
|
template_family: templateFamily || null,
|
|
433
465
|
},
|
|
@@ -466,10 +498,11 @@ function evaluateCampaignBuildBrief(brief, { spec, activePages, pageMappings, so
|
|
|
466
498
|
options: ["discounted unit price", "savings-led", "full accounting"],
|
|
467
499
|
});
|
|
468
500
|
}
|
|
469
|
-
|
|
501
|
+
const promoSurfaces = templatePromoSurfaces({ spec, activePages });
|
|
502
|
+
if (promoSurfaces.length && (!isNonEmptyString(brief.promo_urgency?.header_claim_source) || !isNonEmptyString(brief.promo_urgency?.timer_label))) {
|
|
470
503
|
addQuestion(questions, "promo_urgency_copy", {
|
|
471
|
-
detail:
|
|
472
|
-
options: ["
|
|
504
|
+
detail: `CampaignSpec maps ${promoSurfaces.join(" and ")}, which would fill the template's promo placeholders. Missing promo_urgency.header_claim_source ("campaign_offers" or "none") or promo_urgency.timer_label (the template timer's label, or "none").`,
|
|
505
|
+
options: ["fill from the campaign's promo codes and offers", "remove the template's promo placeholders"],
|
|
473
506
|
});
|
|
474
507
|
}
|
|
475
508
|
if (!normalizePaymentList(brief.commerce_surfaces?.payment_methods_allowed).length) {
|
|
@@ -638,16 +671,30 @@ export function collectSpecPaymentMethods(spec) {
|
|
|
638
671
|
return [...methods].sort();
|
|
639
672
|
}
|
|
640
673
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
674
|
+
// The CampaignSpec surfaces that fill a starter template's own promo
|
|
675
|
+
// placeholders: the template's promo banner and countdown timer read
|
|
676
|
+
// funnels[].promo_codes, and its exit-pop offer reads a page's exit_intent or
|
|
677
|
+
// promo_code_input. The campaign's offer catalog and discount pricing are not
|
|
678
|
+
// among them (bundle_pricing_presentation covers those), and neither is any
|
|
679
|
+
// promo, proof or urgency copy of the source design: that is the merchant's
|
|
680
|
+
// content, built as designed.
|
|
681
|
+
function templatePromoSurfaces({ spec = null, activePages = [] } = {}) {
|
|
682
|
+
const surfaces = [];
|
|
683
|
+
const funnels = Array.isArray(spec?.funnels) ? spec.funnels : [];
|
|
684
|
+
if (funnels.some((funnel) => Array.isArray(funnel?.promo_codes) && funnel.promo_codes.length > 0)) {
|
|
685
|
+
surfaces.push("promo codes (funnels[].promo_codes)");
|
|
686
|
+
}
|
|
687
|
+
const pages = Array.isArray(activePages) ? activePages : [];
|
|
688
|
+
// Checkout-only and enabled: true, the rule hasExitPop, doctor's exit-pop
|
|
689
|
+
// contract and QA's coupon orders use for these surfaces.
|
|
690
|
+
const checkoutSurface = (page, key) => page?.type === "checkout" && page?.[key]?.enabled === true;
|
|
691
|
+
if (pages.some((page) => checkoutSurface(page, "exit_intent"))) {
|
|
692
|
+
surfaces.push("an exit-intent offer (exit_intent)");
|
|
693
|
+
}
|
|
694
|
+
if (pages.some((page) => checkoutSurface(page, "promo_code_input"))) {
|
|
695
|
+
surfaces.push("a promo-code input (promo_code_input)");
|
|
696
|
+
}
|
|
697
|
+
return surfaces;
|
|
651
698
|
}
|
|
652
699
|
|
|
653
700
|
function hasOrderBumpSignals(value, keyPath = []) {
|
package/src/built-site-scope.mjs
CHANGED
|
@@ -16,6 +16,8 @@ import { createHash } from "node:crypto";
|
|
|
16
16
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
17
17
|
import { basename, join, relative, sep } from "node:path";
|
|
18
18
|
|
|
19
|
+
import { isFileReadFailure } from "./cart-placeholders.mjs";
|
|
20
|
+
|
|
19
21
|
const HTML_EXT = ".html";
|
|
20
22
|
|
|
21
23
|
// The route tokens inferPageType reads for the funnel roles, exported so a
|
|
@@ -56,7 +58,26 @@ export function inferPageType(routeOrName) {
|
|
|
56
58
|
return "page";
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
// Whether a symbolic link may stand for a built page: its name ends in .html,
|
|
62
|
+
// or its target is a directory, or its target cannot be inspected (missing,
|
|
63
|
+
// EACCES, ELOOP, any file-system error). A link to a regular file (or any
|
|
64
|
+
// other non-directory) not named .html is no page. Only file-system errors
|
|
65
|
+
// are caught; anything else throws.
|
|
66
|
+
function linkMayBePage(full, name) {
|
|
67
|
+
if (name.toLowerCase().endsWith(HTML_EXT)) return true;
|
|
68
|
+
try {
|
|
69
|
+
return statSync(full).isDirectory();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (!isFileReadFailure(error)) throw error;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// `links`, when given, collects every symbolic link that may stand for a page
|
|
77
|
+
// (see linkMayBePage). They are never pages themselves (build output holds no
|
|
78
|
+
// links, and a link is not followed), but a caller that must account for every
|
|
79
|
+
// built page can name them.
|
|
80
|
+
function listHtmlFiles(root, links = null) {
|
|
60
81
|
const files = [];
|
|
61
82
|
if (!existsSync(root) || !statSync(root).isDirectory()) return files;
|
|
62
83
|
const walk = (dir) => {
|
|
@@ -67,9 +88,11 @@ function listHtmlFiles(root) {
|
|
|
67
88
|
const full = join(dir, entry.name);
|
|
68
89
|
if (entry.isDirectory()) walk(full);
|
|
69
90
|
else if (entry.isFile() && entry.name.toLowerCase().endsWith(HTML_EXT)) files.push(full);
|
|
91
|
+
else if (links && entry.isSymbolicLink() && linkMayBePage(full, entry.name)) links.push(full);
|
|
70
92
|
}
|
|
71
93
|
};
|
|
72
94
|
walk(root);
|
|
95
|
+
links?.sort();
|
|
73
96
|
return files.sort();
|
|
74
97
|
}
|
|
75
98
|
|
|
@@ -101,7 +124,13 @@ function resolveSiteRoot(targetRepo) {
|
|
|
101
124
|
*
|
|
102
125
|
* @param {string} targetRepo Absolute path to the page-kit target repo (or a
|
|
103
126
|
* `_site/` directory, or a campaign directory).
|
|
104
|
-
* @param {{ slug?: string|null }} [options]
|
|
127
|
+
* @param {{ slug?: string|null, includeLinkedPages?: boolean }} [options]
|
|
128
|
+
* `includeLinkedPages` adds `linked_pages`: every symbolic link under the
|
|
129
|
+
* campaign directory that may stand for a page (named .html, or its target
|
|
130
|
+
* a directory, or its target not inspectable; each one entry, its
|
|
131
|
+
* `built_path` the link itself; skipped as pages, not followed), in the
|
|
132
|
+
* same shape as `pages`. A link to a regular file not named .html is
|
|
133
|
+
* dropped. Off by default; without it the result is unchanged.
|
|
105
134
|
* @returns {{
|
|
106
135
|
* ok: boolean,
|
|
107
136
|
* error?: string,
|
|
@@ -114,7 +143,7 @@ function resolveSiteRoot(targetRepo) {
|
|
|
114
143
|
* slug_candidates?: string[],
|
|
115
144
|
* }}
|
|
116
145
|
*/
|
|
117
|
-
export function resolveBuiltSiteScope(targetRepo, { slug = null } = {}) {
|
|
146
|
+
export function resolveBuiltSiteScope(targetRepo, { slug = null, includeLinkedPages = false } = {}) {
|
|
118
147
|
const base = { ok: false, target_repo: targetRepo, site_root: null, slug: "", campaign_dir: null, pages: [], html_count: 0 };
|
|
119
148
|
if (!targetRepo || !existsSync(targetRepo) || !statSync(targetRepo).isDirectory()) {
|
|
120
149
|
return { ...base, error: `Built campaign directory does not exist: ${targetRepo}` };
|
|
@@ -149,7 +178,7 @@ export function resolveBuiltSiteScope(targetRepo, { slug = null } = {}) {
|
|
|
149
178
|
return { ...base, site_root: siteRoot, slug: resolvedSlug, error: `Campaign directory does not exist: ${campaignDir}` };
|
|
150
179
|
}
|
|
151
180
|
|
|
152
|
-
const
|
|
181
|
+
const toPage = (file) => {
|
|
153
182
|
const route = routeForFile(campaignDir, file);
|
|
154
183
|
return {
|
|
155
184
|
page_id: pageIdForRoute(route),
|
|
@@ -157,10 +186,13 @@ export function resolveBuiltSiteScope(targetRepo, { slug = null } = {}) {
|
|
|
157
186
|
route,
|
|
158
187
|
built_path: file,
|
|
159
188
|
};
|
|
160
|
-
}
|
|
189
|
+
};
|
|
190
|
+
const links = includeLinkedPages ? [] : null;
|
|
191
|
+
const pages = listHtmlFiles(campaignDir, links).map(toPage);
|
|
192
|
+
const linked = links ? { linked_pages: links.map(toPage) } : {};
|
|
161
193
|
|
|
162
194
|
if (!pages.length) {
|
|
163
|
-
return { ...base, site_root: siteRoot, slug: resolvedSlug, campaign_dir: campaignDir, error: `No built HTML pages found under ${campaignDir}.` };
|
|
195
|
+
return { ...base, site_root: siteRoot, slug: resolvedSlug, campaign_dir: campaignDir, ...linked, error: `No built HTML pages found under ${campaignDir}.` };
|
|
164
196
|
}
|
|
165
197
|
|
|
166
198
|
return {
|
|
@@ -171,6 +203,7 @@ export function resolveBuiltSiteScope(targetRepo, { slug = null } = {}) {
|
|
|
171
203
|
campaign_dir: campaignDir,
|
|
172
204
|
pages,
|
|
173
205
|
html_count: pages.length,
|
|
206
|
+
...linked,
|
|
174
207
|
};
|
|
175
208
|
}
|
|
176
209
|
|