@nextcommerce/campaigns-os 1.57.0 → 1.60.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 +176 -0
- package/README.md +6 -5
- package/campaign-spec/dist/checkout-flow.d.ts +7 -0
- package/campaign-spec/dist/checkout-flow.js +17 -1
- package/compatibility.json +1 -1
- package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
- package/contracts/commerce-surface-catalog.json +27 -28
- package/contracts/effects.v1.json +5 -5
- package/contracts/release-ledger.json +975 -0
- package/contracts/supported-surface.json +4 -4
- package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
- package/docs/build-packet.md +35 -19
- package/docs/campaign-build-brief.md +1 -1
- package/docs/campaigns-os-build-flow.md +1 -1
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/polish-evidence.md +1 -1
- package/docs/qa-and-test-orders.md +54 -12
- package/docs/runtime-readiness.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/package.json +1 -1
- package/schemas/campaign-runtime-build-context.v0.schema.json +2 -1
- package/schemas/campaign-runtime-build-packet.v0.schema.json +2 -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 +7 -7
- package/skills/next-campaigns-os/SKILL.md +3 -3
- package/skills/next-campaigns-os-setup/SKILL.md +3 -3
- package/skills/next-campaigns-polish/SKILL.md +9 -10
- package/skills/next-campaigns-qa/SKILL.md +3 -3
- package/skills/next-campaigns-sdk-bump/SKILL.md +193 -0
- package/skills.json +18 -10
- package/src/built-site-scope.mjs +96 -12
- package/src/campaign-identity.mjs +6 -4
- package/src/cli.mjs +50 -81
- package/src/doctor/checks.mjs +79 -6
- package/src/doctor/inspect.mjs +2 -8
- package/src/doctor/next-step.mjs +9 -17
- package/src/orchestration-stage-contract.mjs +6 -3
- package/src/page-kit-build-summary.mjs +21 -1
- package/src/page-kit-campaign-config.mjs +19 -0
- package/src/polish-gate.mjs +1 -5
- package/src/polish-node.mjs +5 -4
- package/src/progress-node.mjs +2 -1
- package/src/qa-browser.mjs +137 -2
- package/src/qa-cart-entry.mjs +105 -0
- package/src/qa-node.mjs +3 -1
- package/src/qa-verdict.mjs +24 -1
- package/src/sdk-attribute-index.mjs +8 -7
- package/src/source-html-intake.mjs +27 -18
- package/src/spec-derive.mjs +24 -9
- package/src/stage-ledger.mjs +27 -1
- package/src/stage-record.mjs +218 -35
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: next-campaigns-os-setup
|
|
3
|
-
version: 2.0.
|
|
3
|
+
version: 2.0.44
|
|
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.60.0+skills.1
|
|
8
|
+
Run `npx --no-install campaigns-os tooling status --skills-revision 1.60.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.46
|
|
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.60.0+skills.1
|
|
8
|
+
Run `npx --no-install campaigns-os tooling status --skills-revision 1.60.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
|
|
@@ -87,13 +87,12 @@ Responsibilities:
|
|
|
87
87
|
evidence forward as a warning. Query or fragment text on the production
|
|
88
88
|
page does not make it a distinct preview. If the hosted preview URL is not
|
|
89
89
|
yet recorded, run
|
|
90
|
-
`campaigns-os qa policy set --packet <packet> --preview-url <url>` first
|
|
91
|
-
|
|
92
|
-
`campaigns-os
|
|
93
|
-
`campaigns-os qa run --packet <packet>
|
|
94
|
-
--test-order common
|
|
95
|
-
|
|
96
|
-
closeout. QA against the packet's production URL keeps the checkpoint blocked.
|
|
90
|
+
`campaigns-os qa policy set --packet <packet> --preview-url <url>` first.
|
|
91
|
+
Then follow `campaigns-os next --packet <packet>`: it hands off
|
|
92
|
+
`campaigns-os record deploy --packet <packet> --base-url
|
|
93
|
+
<preview route root>`, then `campaigns-os qa run --packet <packet>
|
|
94
|
+
--base-url <preview-url> --browser --test-order common`, then closeout. QA
|
|
95
|
+
against the packet's production URL keeps the checkpoint blocked.
|
|
97
96
|
- When a readability warning appears, report it with its colours, ratio, requirement and pages. Offer the operator options inside the brand palette (a darker or lighter shade of the same hue, the palette's text colour, or a larger, bold label where the design allows). Do not recolour the merchant's design until the operator chooses. Accept a warning only with the refs, reason and name the operator gives you at the QC handoff.
|
|
98
97
|
- Record Polish as `completed`, `skipped`, or `blocked` with
|
|
99
98
|
`campaigns-os record polish --packet <packet> --evidence <polish-evidence.json>`
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: next-campaigns-qa
|
|
3
|
-
version: 1.3.
|
|
3
|
+
version: 1.3.45
|
|
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.60.0+skills.1
|
|
8
|
+
Run `npx --no-install campaigns-os tooling status --skills-revision 1.60.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
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: next-campaigns-sdk-bump
|
|
3
|
+
version: 1.0.1
|
|
4
|
+
description: Bump a live campaign's Campaign Cart SDK from one 0.4.x release to a later 0.4.x on a page-kit or static repo. Use for any request to bump, upgrade or move a campaign to SDK 0.4.x, including when the repository has its own BUMP.md. Find every pin, check the release diff and storage against the campaign's own code, prove on a preview with approved test orders, and report what is not proven.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Bundle revision: 1.60.0+skills.1
|
|
8
|
+
Run `npx --no-install campaigns-os tooling status --skills-revision 1.60.0+skills.1`
|
|
9
|
+
from the campaign's Page Kit folder, where it runs the project's pinned copy and
|
|
10
|
+
never installs one, at the start of each task. Start a fresh session if it
|
|
11
|
+
reports `mismatch`: this text is already in your context and is never re-read
|
|
12
|
+
while the CLI on disk can move under it. If the output has no `Skills revision:`
|
|
13
|
+
line (no `revision_check` under `--json`), a campaigns-os older than this check
|
|
14
|
+
answered; follow none of its actions and run the pinned copy.
|
|
15
|
+
|
|
16
|
+
# Next Campaigns SDK Bump
|
|
17
|
+
|
|
18
|
+
Use this when someone says "bump this campaign to SDK 0.4.x". The developer
|
|
19
|
+
installs the toolkit, asks for the bump, approves each push, the pull request
|
|
20
|
+
and each test order, then reviews and merges. Everything here uses existing
|
|
21
|
+
commands; there is no new write or order capability.
|
|
22
|
+
|
|
23
|
+
If `tooling status` prints `Refresh installed skills` for `--platform codex` or
|
|
24
|
+
`--platform agents` only, that refresh is for those harnesses; it is not
|
|
25
|
+
required in a session that uses neither.
|
|
26
|
+
|
|
27
|
+
In the instructions below, bare `campaigns-os …` means `npx --no-install
|
|
28
|
+
campaigns-os …` from the campaign folder. The effect class in each
|
|
29
|
+
parenthetical is the declared row of `contracts/effects.v1.json`.
|
|
30
|
+
|
|
31
|
+
## Scope: a current-SDK bump only
|
|
32
|
+
|
|
33
|
+
A bump moves a funnel from one Campaign Cart 0.4.x release to a later 0.4.x.
|
|
34
|
+
Before anything else, confirm every page in scope loads a 0.4.x loader. Stop
|
|
35
|
+
and say so if you find a 0.3.x pin or a legacy page: one whose own code calls
|
|
36
|
+
the Campaigns JS or the Campaigns API directly for cart and checkout. Moving
|
|
37
|
+
those changes bundle selection, cart calculation and more, and is migration
|
|
38
|
+
work, not a bump.
|
|
39
|
+
|
|
40
|
+
Judge a legacy page by what drives its cart and checkout, not by a script URL.
|
|
41
|
+
The 0.4.x SDK loads the Campaigns analytics script (`/js/v1/campaign/`) itself
|
|
42
|
+
for analytics, and some layouts also load it by hand; on a page the 0.4.x SDK
|
|
43
|
+
drives, that script alone is not a reason to stop.
|
|
44
|
+
|
|
45
|
+
A bump does not adopt new SDK components. Hand-written address or phone fields
|
|
46
|
+
stay hand-written; adopting `data-next-address` or another new component is
|
|
47
|
+
separate work.
|
|
48
|
+
|
|
49
|
+
## Approval gates
|
|
50
|
+
|
|
51
|
+
Ask before every push, before opening the pull request, and before test
|
|
52
|
+
orders. Test orders are real orders in the store that someone may have to
|
|
53
|
+
cancel; follow the `next-campaigns-qa` rule: ask once up front, then follow the
|
|
54
|
+
answer. Never write evidence or notes into the served tree: many static repos
|
|
55
|
+
publish from the repository root. The hand-back goes on the ticket or the pull
|
|
56
|
+
request.
|
|
57
|
+
|
|
58
|
+
`qa run` against a preview that is not a local address posts its verdict to the
|
|
59
|
+
QA portal on campaign-map.nextcommerce.com and prints the link. Say so when you
|
|
60
|
+
ask about test orders, and pass `--local-only` unless the developer wants the
|
|
61
|
+
verdict there. `qa run` writes its local verdict under `qa-output/` in the
|
|
62
|
+
current folder (or `--output-dir <path>`). Keep `qa-output/` out of commits:
|
|
63
|
+
check `.gitignore`, and never stage it.
|
|
64
|
+
|
|
65
|
+
## 1. Branch
|
|
66
|
+
|
|
67
|
+
Branch from an up-to-date `main`. List open pull requests that touch the
|
|
68
|
+
campaign folder and ask the developer about merge order before branching.
|
|
69
|
+
|
|
70
|
+
## 2. Find every pin
|
|
71
|
+
|
|
72
|
+
**Page-kit repo** (`_data/campaigns.json` exists):
|
|
73
|
+
|
|
74
|
+
- `sdk_version` on each slug's entry in `_data/campaigns.json`.
|
|
75
|
+
- Pages that type the version into their own loader. Search with
|
|
76
|
+
`grep -rn "campaign-cart@v" src`. Switch each one to
|
|
77
|
+
`campaign-cart@v{{ campaign.sdk_version }}` so the data file is the only pin.
|
|
78
|
+
A data-file-only bump leaves those pages on the old SDK. The starter
|
|
79
|
+
templates render with Liquid and expose the entry as `campaign`; on a page
|
|
80
|
+
that does not, keep a literal version instead and say so. Step 5 checks the
|
|
81
|
+
built pages for an unrendered `{{`.
|
|
82
|
+
- With a Build Packet, `campaigns-os page-kit sync --packet <packet>` (tier `B`)
|
|
83
|
+
writes the CampaignSpec's pin into the entry; without one, edit the entry.
|
|
84
|
+
|
|
85
|
+
**Static repo** (no `_data/campaigns.json` anywhere in the repository, root
|
|
86
|
+
included):
|
|
87
|
+
|
|
88
|
+
- Preview with `campaigns-os sdk repin --target <funnel folder> --target-sdk <x.y.z>`
|
|
89
|
+
(tier `none` without `--apply`). It lists every loader and CSS pin with path
|
|
90
|
+
and line.
|
|
91
|
+
- Scope it to the funnel folder, not the repository root. Repin rewrites every
|
|
92
|
+
semver pin below `--target`, so in a repository where funnels sit side by
|
|
93
|
+
side, a root-level `--apply` moves pages outside the bump. Check every listed
|
|
94
|
+
file belongs to the funnel, then run the same command with `--apply` (tier
|
|
95
|
+
`B`). It writes `.campaign-runtime/sdk-repin.json` under the target; that file
|
|
96
|
+
is served if the publish root includes it, so say so or keep it out of the
|
|
97
|
+
deploy.
|
|
98
|
+
- `sdk repin` refuses a target with `_data/campaigns.json` at or below it,
|
|
99
|
+
but it does not look above `--target`. A funnel folder inside a page-kit
|
|
100
|
+
repository is not refused, so never point `sdk repin` at one; use the
|
|
101
|
+
page-kit steps above.
|
|
102
|
+
|
|
103
|
+
## 3. Read the release diff against the campaign
|
|
104
|
+
|
|
105
|
+
Read the release notes and the source diff between the two Campaign Cart tags.
|
|
106
|
+
For each change, search the campaign's own JavaScript, includes and markup, and
|
|
107
|
+
report file and line where the campaign depends on the changed behaviour. In
|
|
108
|
+
particular:
|
|
109
|
+
|
|
110
|
+
- Trace where the funnel's purchase event comes from before the bump. Static
|
|
111
|
+
funnels often hand-wire `order:completed` listeners and their own `dataLayer`
|
|
112
|
+
pushes; check that event's page and timing against the release you are
|
|
113
|
+
moving to.
|
|
114
|
+
- If a page shows a discount before the SDK confirms a coupon, check that the
|
|
115
|
+
coupon exists on every campaign the funnel uses.
|
|
116
|
+
- Note whether the range changes the card fields or the 3-D Secure flow; if it
|
|
117
|
+
does, plan a 3-D Secure test order in step 6.
|
|
118
|
+
- When the range removes a feature the campaign uses, say so, and do not just
|
|
119
|
+
delete the call: that silently drops the feature from a live funnel. Look for
|
|
120
|
+
a documented replacement in the repository's own docs (`CLAUDE.md`,
|
|
121
|
+
`BUMP.md`) and the release notes. If none is documented, stop and ask.
|
|
122
|
+
- Classify every `next:initialized` listener. From 0.4.42 the SDK fires that
|
|
123
|
+
event once and does not replay it, so a deferred script that only listens for
|
|
124
|
+
it can miss it on a cached reload. Guard each one in the bump: run now if
|
|
125
|
+
`window.next` exists and `document.body.dataset.nextSdkLoading === "false"`,
|
|
126
|
+
otherwise wait for the event, and run once either way. Step 6 proves it.
|
|
127
|
+
|
|
128
|
+
## 4. Storage check
|
|
129
|
+
|
|
130
|
+
Run `campaigns-os sdk storage-check --target <git-root> --target-sdk <x.y.z> --manifest <storage-migrations.v1.json> --scope <funnel folder,shared js>`
|
|
131
|
+
(tier `none`). Take the manifest from an unmodified checkout of the target
|
|
132
|
+
Campaign Cart tag (`docs/compatibility/storage-migrations.v1.json`, v0.4.40 or
|
|
133
|
+
later), at the highest version its supported range declares. Read each
|
|
134
|
+
`incompatible` and `unknown` line and say whose key it is. See
|
|
135
|
+
`docs/sdk-storage-compatibility.md`.
|
|
136
|
+
|
|
137
|
+
The manifest's `supportedSdkVersions` can stop below the tag it ships in: tags
|
|
138
|
+
v0.4.39 to v0.4.42 declare 0.4.38 to 0.4.38, so `--target-sdk 0.4.42` returns
|
|
139
|
+
`target-outside-manifest-range`. Run the check at the manifest's highest
|
|
140
|
+
version and report that it does not speak for the versions above it.
|
|
141
|
+
|
|
142
|
+
## 5. Build and check
|
|
143
|
+
|
|
144
|
+
**Page-kit:** run a development build, confirm every built page loads only the
|
|
145
|
+
target version and that no built loader URL still holds an unrendered `{{`,
|
|
146
|
+
then `campaigns-os doctor --built <repo> --family <family> --slug <slug>`
|
|
147
|
+
(tier `none`) for a repository with no Build Packet, or `campaigns-os doctor --packet <packet>`
|
|
148
|
+
with one. Errors that are already on `main` are recorded, not fixed. Report doctor's
|
|
149
|
+
advisories with the rest; an attribute flagged as missing from the SDK's index
|
|
150
|
+
that the page does set (for example `data-next-page-type` on `<body>`) goes in
|
|
151
|
+
the hand-back as unchecked, not fixed by guesswork.
|
|
152
|
+
|
|
153
|
+
**Static:** there is no build and no doctor. Re-run the repin preview (it
|
|
154
|
+
should list nothing below the target) and search the served pages for any
|
|
155
|
+
remaining old version.
|
|
156
|
+
|
|
157
|
+
## 6. Preview proof
|
|
158
|
+
|
|
159
|
+
Deploy a preview and prove the bump with browser test orders under the
|
|
160
|
+
approval rule above. `qa run --browser` needs the browser once per machine:
|
|
161
|
+
`campaigns-os qa install-browser` downloads about 100 MB, so ask before running
|
|
162
|
+
it. Pin the browser to the store's main market: a run
|
|
163
|
+
geolocated elsewhere can order in another currency and skip a currency-gated
|
|
164
|
+
upsell.
|
|
165
|
+
|
|
166
|
+
- With a Build Packet: `campaigns-os qa run --packet <packet> --base-url <preview-url> --browser --test-order common`
|
|
167
|
+
(tier `C`; see `next-campaigns-qa`).
|
|
168
|
+
- A built page-kit repo with no packet: `campaigns-os qa run --site <repo> --base-url <preview-url> --family <family> --browser`
|
|
169
|
+
(tier `C`) checks the served pages. Place the approved test orders in the
|
|
170
|
+
browser and record each order number.
|
|
171
|
+
- A static repo: place the approved test orders in the browser on the preview
|
|
172
|
+
and record each order number.
|
|
173
|
+
|
|
174
|
+
Cover:
|
|
175
|
+
|
|
176
|
+
- one card order per live checkout;
|
|
177
|
+
- PayPal cancelled, then a card order, where PayPal is offered;
|
|
178
|
+
- an upsell at quantity 2 or more on a page that shows a quantity picker;
|
|
179
|
+
- a 3-D Secure card when step 3 found card-field or 3-D Secure changes, and whether it challenged;
|
|
180
|
+
- the coupon path, if the funnel has one;
|
|
181
|
+
- the purchase event fires once, on the page the release says it should;
|
|
182
|
+
- each SDK page on a fresh load and on a cached reload, with every
|
|
183
|
+
`next:initialized` listener from step 3 running once on both.
|
|
184
|
+
|
|
185
|
+
Read each order back in the store admin (the packet run's verdict under
|
|
186
|
+
`qa-output/` also lists the orders it created) and keep the order numbers for
|
|
187
|
+
the hand-back.
|
|
188
|
+
|
|
189
|
+
## 7. Hand back
|
|
190
|
+
|
|
191
|
+
On the ticket or the pull request, not in the repository: what changed (pins,
|
|
192
|
+
loader rewrites), the storage-check and doctor results, a table of test orders,
|
|
193
|
+
what is not proven, and which orders to cancel.
|
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.60.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.61",
|
|
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.44",
|
|
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.46",
|
|
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.46",
|
|
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.45",
|
|
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.41",
|
|
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.41",
|
|
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.41",
|
|
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,10 +80,18 @@
|
|
|
80
80
|
{
|
|
81
81
|
"id": "contribution-intake",
|
|
82
82
|
"name": "Contribution Intake",
|
|
83
|
-
"version": "1.0.
|
|
83
|
+
"version": "1.0.41",
|
|
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."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "next-campaigns-sdk-bump",
|
|
90
|
+
"name": "Campaign SDK Bump",
|
|
91
|
+
"version": "1.0.1",
|
|
92
|
+
"path": "skills/next-campaigns-sdk-bump/SKILL.md",
|
|
93
|
+
"domain": "campaigns",
|
|
94
|
+
"description": "Bump a live campaign from one Campaign Cart 0.4.x release to a later 0.4.x on a page-kit or static repo: find every pin, check the release diff and storage against the campaign code, prove on a preview with approved test orders, and report what is not proven."
|
|
87
95
|
}
|
|
88
96
|
]
|
|
89
97
|
}
|
package/src/built-site-scope.mjs
CHANGED
|
@@ -290,9 +290,8 @@ export function synthesizeMinimalBuildPacket({
|
|
|
290
290
|
// the same build. The manifest is `<path>\n<sha256>\n` per file, paths
|
|
291
291
|
// relative to the output root with `/` separators and sorted by code point,
|
|
292
292
|
// so directory walk order and the absolute location never leak into the
|
|
293
|
-
// value. Page Kit (0.2.0) writes only rendered HTML and copied
|
|
294
|
-
// _site/, nothing it timestamps, so
|
|
295
|
-
// takes root-relative paths for a consumer whose build does stamp a file.
|
|
293
|
+
// value. Page Kit (0.2.x and 0.3.x) writes only rendered HTML and copied
|
|
294
|
+
// assets into _site/, nothing it timestamps, so every file is hashed.
|
|
296
295
|
// Symbolic links are never build output and are skipped, not followed.
|
|
297
296
|
export const BUILD_FINGERPRINT_ALGORITHM = "sha256-manifest/v1";
|
|
298
297
|
|
|
@@ -326,22 +325,17 @@ function sha256Hex(buffer) {
|
|
|
326
325
|
* hand (`sha256sum` over each file, sort by path, hash the lines). `ok: false`
|
|
327
326
|
* with `error` when the root is not a directory; an empty tree still hashes
|
|
328
327
|
* (the empty manifest) so a wiped _site/ reads as a changed build, not a
|
|
329
|
-
* missing one.
|
|
328
|
+
* missing one. `excluded` is always empty; doctor's build_output_fingerprint
|
|
329
|
+
* still reports it.
|
|
330
330
|
*/
|
|
331
|
-
export function computeBuildFingerprint(outputDir
|
|
331
|
+
export function computeBuildFingerprint(outputDir) {
|
|
332
332
|
const root = String(outputDir || "");
|
|
333
333
|
const base = { ok: false, algorithm: BUILD_FINGERPRINT_ALGORITHM, root, fingerprint: null, file_count: 0, excluded: [], manifest: null };
|
|
334
334
|
if (!root || !existsSync(root) || !statSync(root).isDirectory()) {
|
|
335
335
|
return { ...base, error: `Build output directory does not exist: ${root || "(empty)"}` };
|
|
336
336
|
}
|
|
337
|
-
const excludeSet = new Set((Array.isArray(exclude) ? exclude : []).map((path) => String(path).split(sep).join("/")));
|
|
338
|
-
const excluded = [];
|
|
339
337
|
const lines = [];
|
|
340
338
|
for (const path of listFilesRelative(root)) {
|
|
341
|
-
if (excludeSet.has(path)) {
|
|
342
|
-
excluded.push(path);
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
339
|
lines.push(`${path}\n${sha256Hex(readFileSync(join(root, path)))}\n`);
|
|
346
340
|
}
|
|
347
341
|
const manifest = lines.join("");
|
|
@@ -350,8 +344,98 @@ export function computeBuildFingerprint(outputDir, { exclude = [] } = {}) {
|
|
|
350
344
|
ok: true,
|
|
351
345
|
fingerprint: `sha256:${sha256Hex(Buffer.from(manifest, "utf8"))}`,
|
|
352
346
|
file_count: lines.length,
|
|
353
|
-
excluded,
|
|
354
347
|
manifest,
|
|
355
348
|
error: null,
|
|
356
349
|
};
|
|
357
350
|
}
|
|
351
|
+
|
|
352
|
+
// The manifest as `{ path, sha256 }` entries, the shape `record build` keeps
|
|
353
|
+
// on stages.assembly.build_manifest beside build_fingerprint so a later
|
|
354
|
+
// mismatch can name the files that differ instead of only two hashes.
|
|
355
|
+
export function buildManifestEntries(fingerprint) {
|
|
356
|
+
const lines = String(fingerprint?.manifest || "").split("\n");
|
|
357
|
+
const entries = [];
|
|
358
|
+
for (let index = 0; index + 1 < lines.length; index += 2) {
|
|
359
|
+
if (lines[index]) entries.push({ path: lines[index], sha256: lines[index + 1] });
|
|
360
|
+
}
|
|
361
|
+
return entries;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
365
|
+
|
|
366
|
+
// The recorded manifest, only when it is evidence about the recorded build:
|
|
367
|
+
// well-formed entries whose canonical manifest hashes to the recorded
|
|
368
|
+
// fingerprint. A record made before build kept a manifest, or one rewritten
|
|
369
|
+
// by hand, reads as unavailable (null) rather than as a misleading diff.
|
|
370
|
+
export function recordedBuildManifest(assembly) {
|
|
371
|
+
const entries = assembly?.build_manifest;
|
|
372
|
+
const recorded = assembly?.build_fingerprint;
|
|
373
|
+
if (!Array.isArray(entries) || typeof recorded !== "string") return null;
|
|
374
|
+
if (!entries.every((entry) => typeof entry?.path === "string" && entry.path && typeof entry?.sha256 === "string" && SHA256_HEX.test(entry.sha256))) return null;
|
|
375
|
+
const manifest = entries.map((entry) => `${entry.path}\n${entry.sha256}\n`).join("");
|
|
376
|
+
return `sha256:${sha256Hex(Buffer.from(manifest, "utf8"))}` === recorded ? entries : null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// macOS and iCloud name a sync conflict copy `<name> 2.<ext>` (then ` 3`,
|
|
380
|
+
// ...), on files and on folders. A trailing " N" alone is too common in real
|
|
381
|
+
// names ("chapter 12.md"), so a path counts as a copy only when the same path
|
|
382
|
+
// without the suffix also exists. The flag is a hint, never a verdict.
|
|
383
|
+
const CONFLICT_COPY_SEGMENT = /^(.+) (?:[2-9]|[1-9]\d+)(\.[^.]+)?$/;
|
|
384
|
+
export function isSyncConflictCopyPath(path, knownPaths) {
|
|
385
|
+
const segments = String(path).split("/");
|
|
386
|
+
const known = [...knownPaths];
|
|
387
|
+
return segments.some((segment, index) => {
|
|
388
|
+
const match = CONFLICT_COPY_SEGMENT.exec(segment);
|
|
389
|
+
if (!match) return false;
|
|
390
|
+
const original = [...segments.slice(0, index), `${match[1]}${match[2] ?? ""}`].join("/");
|
|
391
|
+
return index === segments.length - 1
|
|
392
|
+
? known.includes(original)
|
|
393
|
+
: known.some((candidate) => candidate.startsWith(`${original}/`));
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export const BUILD_DRIFT_PATH_LIMIT = 20;
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Compare the output on disk (a computeBuildFingerprint result) with the
|
|
401
|
+
* manifest stages.assembly recorded. Returns
|
|
402
|
+
* { manifest: "recorded", extra, missing, changed, conflict_copies, summary }
|
|
403
|
+
* with sorted path lists, or { manifest: "unavailable", summary } when the
|
|
404
|
+
* record carries no usable manifest. `summary` is one sentence for a
|
|
405
|
+
* refusal: each list capped at BUILD_DRIFT_PATH_LIMIT with "+N more".
|
|
406
|
+
*/
|
|
407
|
+
export function describeBuildOutputDrift(assembly, current) {
|
|
408
|
+
const recorded = recordedBuildManifest(assembly);
|
|
409
|
+
if (!recorded) {
|
|
410
|
+
return {
|
|
411
|
+
manifest: "unavailable",
|
|
412
|
+
summary: "The build record carries no file manifest (it was recorded before record build kept one), so the differing paths cannot be named; re-run record build to record one.",
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const before = new Map(recorded.map((entry) => [entry.path, entry.sha256]));
|
|
416
|
+
const now = new Map(buildManifestEntries(current).map((entry) => [entry.path, entry.sha256]));
|
|
417
|
+
const extra = [...now.keys()].filter((path) => !before.has(path));
|
|
418
|
+
const missing = [...before.keys()].filter((path) => !now.has(path));
|
|
419
|
+
const changed = [...now.keys()].filter((path) => before.has(path) && before.get(path) !== now.get(path));
|
|
420
|
+
const knownPaths = new Set([...before.keys(), ...now.keys()]);
|
|
421
|
+
const isCopy = (path) => isSyncConflictCopyPath(path, knownPaths);
|
|
422
|
+
const conflictCopies = extra.filter(isCopy);
|
|
423
|
+
const list = (label, paths) => {
|
|
424
|
+
if (paths.length === 0) return null;
|
|
425
|
+
const shown = paths.slice(0, BUILD_DRIFT_PATH_LIMIT).map((path) => (isCopy(path) ? `${path} [sync conflict copy]` : path));
|
|
426
|
+
const more = paths.length > BUILD_DRIFT_PATH_LIMIT ? ` (+${paths.length - BUILD_DRIFT_PATH_LIMIT} more)` : "";
|
|
427
|
+
return `${paths.length} ${label}: ${shown.join(", ")}${more}`;
|
|
428
|
+
};
|
|
429
|
+
const parts = [list("extra", extra), list("missing", missing), list("changed", changed)].filter(Boolean);
|
|
430
|
+
const hint = conflictCopies.length
|
|
431
|
+
? ` ${conflictCopies.length} extra file name(s) look like macOS/iCloud sync conflict copies ("<name> 2.<ext>" beside "<name>.<ext>"); if they are, remove them (or move the repo out of the synced folder) and rebuild.`
|
|
432
|
+
: "";
|
|
433
|
+
return {
|
|
434
|
+
manifest: "recorded",
|
|
435
|
+
extra,
|
|
436
|
+
missing,
|
|
437
|
+
changed,
|
|
438
|
+
conflict_copies: conflictCopies,
|
|
439
|
+
summary: `Against the manifest build recorded: ${parts.length ? parts.join("; ") : "no path differs"}.${hint}`,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
@@ -263,9 +263,11 @@ function gateBase(subject) {
|
|
|
263
263
|
* A page marked `spec_page: false` is built output that no CampaignSpec page
|
|
264
264
|
* builds to. It is still scanned, because it is still served, but a finding
|
|
265
265
|
* that names it says so: such a file is usually left in `_site/` by an earlier
|
|
266
|
-
* build, or built from an HTML file copied into the source
|
|
267
|
-
*
|
|
268
|
-
*
|
|
266
|
+
* build, or built from an HTML file copied into the source, and the repair is
|
|
267
|
+
* to remove it, not to retag it. Both are page-kit 0.2.x behaviour (no
|
|
268
|
+
* `_site/` pruning; a design export's `index.html` under `assets/` builds as
|
|
269
|
+
* a page). 0.3.0 cleans `_site/<slug>/` and ignores `.html` under `assets/`,
|
|
270
|
+
* so there a stray is a source page no spec page claims.
|
|
269
271
|
*
|
|
270
272
|
* @param {{ subject: object, pages: Array<{ page_id: string, route?: string|null,
|
|
271
273
|
* file?: string|null, content: string, spec_page?: boolean,
|
|
@@ -415,7 +417,7 @@ export function evaluateCampaignIdentity({ subject, pages = [], expectedApiKey =
|
|
|
415
417
|
// own instruction (retag, or add a missing tag to a real page) is dropped:
|
|
416
418
|
// that edit would land on the wrong file.
|
|
417
419
|
const named = stray.length > 2 ? `${stray.slice(0, -1).join(", ")}, and ${stray.at(-1)}` : stray.join(" and ");
|
|
418
|
-
finding.message = `${named} ${stray.length === 1 ? "is" : "are"} not the built page of any CampaignSpec page: built output left by an earlier build, or an HTML file copied into the source (a design export's index.html under assets/ builds as its own page). Remove the source file if there is one, delete the built file, then rebuild and record the build again; do not retag either page. What it causes: ${finding.evidence}`;
|
|
420
|
+
finding.message = `${named} ${stray.length === 1 ? "is" : "are"} not the built page of any CampaignSpec page: built output left by an earlier build, or an HTML file copied into the source (on page-kit 0.2.x a design export's index.html under assets/ builds as its own page). Remove the source file if there is one, delete the built file, then rebuild and record the build again; do not retag either page. What it causes: ${finding.evidence}`;
|
|
419
421
|
}
|
|
420
422
|
|
|
421
423
|
// evidence is the message's first sentence, kept apart only so a stray
|