@nextcommerce/campaigns-os 1.47.0 → 1.50.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 +307 -0
- package/README.md +30 -3
- package/agents/claude/CLAUDE.md +6 -3
- package/agents/codex/AGENTS.md +6 -4
- package/agents/copilot/copilot-instructions.md +3 -2
- package/agents/cursor/campaigns-os.mdc +3 -3
- package/compatibility.json +1 -1
- package/contracts/commerce-surface-catalog.json +17 -17
- package/contracts/effects.v1.json +173 -0
- package/contracts/release-ledger.json +798 -0
- package/contracts/supported-surface.json +4 -4
- package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
- package/docs/brand-theme-bridge.md +12 -6
- package/docs/build-packet.md +9 -4
- package/docs/campaign-build-brief.md +25 -28
- package/docs/local-setup.md +7 -2
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/qa-and-test-orders.md +70 -9
- package/docs/runtime-readiness.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/package.json +1 -1
- package/schemas/campaign-runtime-build-packet.v0.schema.json +4 -0
- package/schemas/campaigns-os-qa-verdict.v0.schema.json +8 -3
- 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 +9 -4
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +5 -4
- 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 +6 -5
- package/skills/next-campaigns-qa/SKILL.md +3 -3
- package/skills.json +10 -10
- package/src/brand-theme.mjs +13 -2
- package/src/cli.mjs +17 -12
- package/src/content-residue.mjs +18 -90
- package/src/doctor/checks.mjs +21 -29
- package/src/doctor/inspect.mjs +13 -3
- package/src/doctor/next-step.mjs +4 -0
- package/src/gate-actions.mjs +8 -0
- package/src/install-mode.mjs +0 -8
- package/src/invocation.mjs +3 -1
- package/src/local-preview-policy.mjs +92 -0
- package/src/page-kit-sdk-version.mjs +8 -1
- package/src/polish-node.mjs +26 -2
- package/src/progress-node.mjs +5 -1
- package/src/qa-binding-evidence.mjs +22 -1
- package/src/qa-browser.mjs +92 -14
- package/src/qa-node.mjs +43 -6
- package/src/readback.mjs +19 -10
- package/src/source-prep.mjs +1 -1
- package/src/stage-record.mjs +303 -22
- package/src/theme-gate.mjs +3 -3
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: next-campaigns-os-setup
|
|
3
|
-
version: 2.0.
|
|
3
|
+
version: 2.0.27
|
|
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.50.0+skills.1
|
|
8
|
+
Run `npx --no-install campaigns-os tooling status --skills-revision 1.50.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.28
|
|
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.50.0+skills.1
|
|
8
|
+
Run `npx --no-install campaigns-os tooling status --skills-revision 1.50.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
|
|
@@ -48,7 +48,8 @@ Use this after build has produced a runnable page-kit campaign.
|
|
|
48
48
|
Theme gate: `campaigns-os next polish` (tier `A`, like every `next` form: additive writes under `.campaign-runtime/` plus a stage-progress POST under Run Telemetry consent; `--no-write` and `--no-remit` are each tier `B`) blocks when theme inspect found a
|
|
49
49
|
generatable brand theme that is not yet applied to commerce pages. Do not work
|
|
50
50
|
around the gate — apply the brand layer (`theme generate`, tier `B`; copy into
|
|
51
|
-
campaign assets, load after `next-core.css`,
|
|
51
|
+
campaign assets, load after `next-core.css`, rebuild, `campaigns-os record build`,
|
|
52
|
+
then `campaigns-os record theme --packet <p>`, tier `C`) or record an
|
|
52
53
|
explicit waiver (`campaigns-os theme waive --packet <p> --reason "<why>" --waived-by "<named human>"` — tier `C`, because the waiver overwrites the assembly report and doctor output; placeholders are refused).
|
|
53
54
|
|
|
54
55
|
Responsibilities:
|
|
@@ -56,7 +57,7 @@ Responsibilities:
|
|
|
56
57
|
- Compare prepared source design against built campaign pages.
|
|
57
58
|
- Scan the prepared source assets for brand marks such as `logo*.png`, `logo*.svg`, and obvious header/logo images before leaving starter-template logos in place.
|
|
58
59
|
- Scan **visible** rendered text inside the family's content/commerce surfaces (the selectors enumerated in `contracts/template-brand-contract.<family>.v0.json`) for placeholder/residue copy the build should have replaced — the *literal starter defaults*: lorem-ipsum, the unmodified starter headings (`Product Name` / `Package Title` / `Your headline`), `[VERIFY …]` author notes, `TODO` markers. Match the literal starter strings, not any authored copy that merely contains those words, and skip `<script>` / `<style>` / JSON-LD / `data-*` attributes. Treat a surviving literal starter default on a content/commerce surface as a polish blocker — replace it from the prepared source / CampaignSpec; do not draft substitute copy (if the design's authored copy is genuinely missing, flag it rather than invent it). This is *copy* residue, complementary to the computed-style / asset residue gated by `next-campaigns-qa` + the brand contract — not a duplicate of it.
|
|
59
|
-
- Flag template *defaults* left where they disagree with the prepared design — e.g. the same benefit icon repeated across a grid *when the design uses distinct icons*, or a guarantee badge/term that disagrees with the design — as polish defects, not just logos. Judge against the prepared source, not taste: copy or imagery the source/CampaignSpec does not supply (e.g. a placeholder testimonial name/quote/role) is residue; "looks generic" on its own is not.
|
|
60
|
+
- Flag template *defaults* left where they disagree with the prepared design — e.g. the same benefit icon repeated across a grid *when the design uses distinct icons*, or a guarantee badge/term that disagrees with the design — as polish defects, not just logos. Judge against the prepared source, not taste: copy or imagery the source/CampaignSpec does not supply (e.g. a placeholder testimonial name/quote/role) is residue; "looks generic" on its own is not. Copy the source does supply, including its proof and urgency elements (reviews, "Verified Purchase" labels, recent-purchase popups, stock counters, countdowns, guarantees), is the merchant's content: keep it as designed and do not record it as an issue or an unconfirmed claim.
|
|
60
61
|
- **Brand-bleed (cloned-source de-brand) pass.** When a campaign is cloned from a proven sibling, the sibling's brand defaults ride along. Inspect the built pages and assets for residual cross-brand bleed and clear it before recording: (1) a residual promo/sale banner or coupon code/copy from the source campaign (including a baked-in *fake* code); (2) a prior-campaign / sibling favicon left in place; (3) scaffold or non-design fonts the design did not specify (e.g. starter `Plus Jakarta`); (4) hardcoded non-token colors — any brand color literal that should be a token, such as next-core's `#C670FE` "Most Popular" pill. Clear each from the prepared source / CampaignSpec and brand theme (tokens, not literals); flag — do not invent — anything the design genuinely doesn't supply. Treat surviving bleed as a polish blocker. This complements the favicon/logo and copy-residue checks above; it is the cross-brand contamination angle, not a duplicate.
|
|
61
62
|
- Read the assembly report decisions before polishing. Do not reintroduce source-HTML elements that build intentionally dropped because CampaignSpec/API did not support them, such as unavailable payment methods.
|
|
62
63
|
- **Remove or rename a contract-listed payment-chrome asset; never edit one in place.** The assets named in `contracts/template-brand-contract.<family>.v0.json` under `default_residue.payment_chrome.assets` are keyed by QA on the *referenced basename*, not on their contents. Stripping the unsupported marks from inside a shared file such as `upsell-payment-logos.svg` leaves the reference in place, so QA reports residue for an asset that no longer carries any — and on 2026-09-06 the repair loop's remedy for that report deleted a cards-only trust strip that was correct. Delete the asset, or write a new one under a new name and repoint the reference. QA downgrades an edited-in-place asset to `manual_review` rather than a blocker, but that is a safety net for a mistake, not the supported way to do this.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: next-campaigns-qa
|
|
3
|
-
version: 1.3.
|
|
3
|
+
version: 1.3.28
|
|
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.50.0+skills.1
|
|
8
|
+
Run `npx --no-install campaigns-os tooling status --skills-revision 1.50.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
|
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.50.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.44",
|
|
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.27",
|
|
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.29",
|
|
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.28",
|
|
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.28",
|
|
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.24",
|
|
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.24",
|
|
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.24",
|
|
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.24",
|
|
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."
|
package/src/brand-theme.mjs
CHANGED
|
@@ -25,7 +25,7 @@ export const THEME_REPORT_STATUSES = new Set(["applied", "skipped", "blocked", "
|
|
|
25
25
|
export const THEME_CONFIDENCES = new Set(["high", "medium", "low", "none"]);
|
|
26
26
|
|
|
27
27
|
const SKIP_DIRS = new Set([".git", "node_modules", "_site", "dist", "build", ".next", "coverage", "qa-output"]);
|
|
28
|
-
const BRAND_LAYER_FILENAMES = new Set(["brand-theme.css", "checkout-brand.css"]);
|
|
28
|
+
export const BRAND_LAYER_FILENAMES = new Set(["brand-theme.css", "checkout-brand.css"]);
|
|
29
29
|
|
|
30
30
|
function isObject(value) {
|
|
31
31
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -574,6 +574,17 @@ function discoverThemeCandidates({ sourceRoot, pageMappings = [], manifest = nul
|
|
|
574
574
|
candidates.push(candidateFromFile(path, role, source, referencedBy));
|
|
575
575
|
}
|
|
576
576
|
|
|
577
|
+
// A stylesheet that pages of different roles link is the funnel's shared
|
|
578
|
+
// layer, not the first linking page's: it ranks as shared.
|
|
579
|
+
function addPageReference(path, role, referencedBy) {
|
|
580
|
+
const existing = seenFiles.has(resolve(path))
|
|
581
|
+
? candidates.find((candidate) => candidate.source === "mapped_html_reference" && resolve(candidate.path) === resolve(path))
|
|
582
|
+
: null;
|
|
583
|
+
if (!existing) return addFile(path, role, "mapped_html_reference", referencedBy);
|
|
584
|
+
existing.referenced_by.push(...referencedBy);
|
|
585
|
+
if (existing.role !== role) existing.role = "shared";
|
|
586
|
+
}
|
|
587
|
+
|
|
577
588
|
for (const conventional of [
|
|
578
589
|
"assets/css/tokens.css",
|
|
579
590
|
"assets/css/landing/tokens.css",
|
|
@@ -595,7 +606,7 @@ function discoverThemeCandidates({ sourceRoot, pageMappings = [], manifest = nul
|
|
|
595
606
|
const content = readFileSync(htmlPath, "utf8");
|
|
596
607
|
for (const ref of extractCssRefs(content)) {
|
|
597
608
|
for (const cssPath of candidateCssPathsForRef(sourceRoot, htmlPath, ref)) {
|
|
598
|
-
|
|
609
|
+
addPageReference(cssPath, candidateRoleFromPath(ref, role), [{ page_id: mapping.page_id || null, path: mapping.path, ref }]);
|
|
599
610
|
}
|
|
600
611
|
}
|
|
601
612
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -296,15 +296,18 @@ Usage:
|
|
|
296
296
|
campaigns-os start (--spec <json> | --map-id <id>) --source <html-dir> --target <page-kit-dir> --template-family <family>
|
|
297
297
|
[--brief <yaml|json>] [--proxy-base <url>] [--cached-spec] [--theme-policy <inspect_only|auto|off>]
|
|
298
298
|
[--wrapper-policy <strip_document_wrappers|preserve_document_wrappers|not_required|unknown>] [--design-manifest <path>]
|
|
299
|
-
[--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>]
|
|
299
|
+
[--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>]
|
|
300
|
+
[--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--no-run-session] [--force] # --force overwrites an assembly report that carries stage evidence (destructive; prints the cleared stage keys) and regenerates a stale Design Source Package an earlier intake synthesized and nobody changed
|
|
300
301
|
campaigns-os prepare-build (--spec <json> | --map-id <id>) --source <html-dir> --target <page-kit-dir> --template-family <family>
|
|
301
302
|
[--brief <yaml|json>] [--proxy-base <url>] [--cached-spec] [--theme-policy <inspect_only|auto|off>]
|
|
302
303
|
[--wrapper-policy <strip_document_wrappers|preserve_document_wrappers|not_required|unknown>] [--design-manifest <path>]
|
|
303
|
-
[--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>]
|
|
304
|
+
[--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>]
|
|
305
|
+
[--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--no-run-session] [--force]
|
|
304
306
|
campaigns-os build (--spec <json> | --map-id <id>) --source <html-dir> --target <page-kit-dir> --template-family <family>
|
|
305
307
|
[--brief <yaml|json>] [--proxy-base <url>] [--cached-spec] [--theme-policy <inspect_only|auto|off>]
|
|
306
308
|
[--wrapper-policy <strip_document_wrappers|preserve_document_wrappers|not_required|unknown>] [--design-manifest <path>]
|
|
307
|
-
[--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>]
|
|
309
|
+
[--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>]
|
|
310
|
+
[--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--no-run-session] [--force] # intake alias for prepare-build + doctor
|
|
308
311
|
campaigns-os doctor --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--strip-paths] [--write] [--no-write] [--doctor-out <path>] [--proxy-base <url>] [--no-live-refs] [--json] # inspection by default; --doctor-out requires --write; --no-write wins. When the packet's built _site/<route>/ exists and a public Campaigns API key resolves (packet, its local CampaignSpec, or the declared campaign-key env var), doctor makes one read-only GET of {proxy-base}/api/campaign under X-Campaign-Key to check each built page's shipping and package refs against the live campaign; --proxy-base overrides the canonical proxy (https, or a loopback host over http). --no-live-refs skips the read and records not_run with reason disabled. No key, no built page, or a failed read records derived.live_campaign_refs as not_run with its reason. Only doctor and qa run make this read; other commands that run doctor record not_read
|
|
309
312
|
campaigns-os doctor --built <page-kit-target-repo> --family <family> [--slug <slug>] [--base-url <url>] [--emit-packet [path]] [--json] # L7: doctor a built _site/ with no Build Packet
|
|
310
313
|
campaigns-os bundle check --packet <campaign-runtime.build.json> [--require-qa] [--json] # validate the canonical migration/readback JSON bundle; never substitutes markdown
|
|
@@ -321,6 +324,8 @@ Usage:
|
|
|
321
324
|
campaigns-os record setup --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--dry-run] [--json] # record setup complete once the campaign output directory exists: Build Context scaffold.required=false and stages.setup completed, validated against their schemas before either is written
|
|
322
325
|
campaigns-os record build --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--dry-run] [--json] # after page-kit build: stages.assembly completed with build_fingerprint = doctor's derived.build_output_fingerprint.value (and the Design Source Package material fingerprint when the report has one); stages.polish becomes required unless its evidence is bound to this exact output
|
|
323
326
|
campaigns-os record polish --packet <campaign-runtime.build.json> --evidence <polish-evidence.json> [--context <json>] [--report <json>] [--dry-run] [--json] # after polish capture: stages.polish from the file's status (completed, completed_with_warnings, blocked with blockers, or skipped with skip_reason), evidence and optional repair_loop_defect, bound to doctor's current fingerprint; a completed status is refused, writing nothing, unless the polish gate doctor evaluates would pass. --dry-run runs every check and writes nothing
|
|
327
|
+
campaigns-os record theme --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--dry-run] [--json] # after the brand layer is linked and build is recorded: report.theme becomes applied with load_order after-next-core, css_path, commerce_pages and per-page evidence, only when each built commerce page that loads next-core.css loads brand-theme.css (or checkout-brand.css) after it and at least one does; a page loading neither is left out as the design's own markup; refused, writing nothing, otherwise. --dry-run runs every check and writes nothing
|
|
328
|
+
campaigns-os record deploy --packet <campaign-runtime.build.json> --base-url <served url> [--context <json>] [--report <json>] [--dry-run] [--json] # a local preview (deploy.target local-serve) after polish is recorded: GETs every built page under the loopback URL (the campaign route root), then records deploy.preview_url on the packet and stages.deploy completed with the URL in outputs; refused, writing nothing, when the URL is not loopback or not the route root, a page does not answer 2xx, the build changed since it was recorded, or the theme gate is blocked. --dry-run runs every check, the requests included, and writes nothing
|
|
324
329
|
campaigns-os readback <target-repo-root> [--json] [--packet <path>] [--doctor <path>] [--context <path>] [--report <path>] [--qa-verdict <path>] [--findings <path>] # read-only projection of one run's emitted artifacts (packet, doctor output, build context, assembly report, QA verdict, findings export): artifact states, per-artifact freshness against the checkout's HEAD reflog, doctor warning grouping, skip cascades and cross-artifact divergences. Writes nothing, starts no process, touches no network, and records no lifecycle entry; --json emits one campaigns-os-readback/v2 object (docs/readback.md). Exit 2 for a missing target root or a Build Packet set freshness cannot single out.
|
|
325
330
|
campaigns-os readback --example [--json] # project the bundled synthetic sample; freshness is not computable for it by design
|
|
326
331
|
campaigns-os validate-assembly-report --report <json> [--json]
|
|
@@ -1066,8 +1071,8 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1066
1071
|
}
|
|
1067
1072
|
|
|
1068
1073
|
if (command === "record") {
|
|
1069
|
-
const {
|
|
1070
|
-
writeResult(
|
|
1074
|
+
const { recordCommand } = await import("./stage-record.mjs");
|
|
1075
|
+
writeResult(await recordCommand(args), args, 0);
|
|
1071
1076
|
return;
|
|
1072
1077
|
}
|
|
1073
1078
|
|
|
@@ -4463,8 +4468,9 @@ function themeStarterPaletteAdvisory(themeGate, packetPath, residueState) {
|
|
|
4463
4468
|
+ `\`${cmd("theme")} waive --packet ${packetArg} --reason "<why the starter palette is acceptable>"\` `
|
|
4464
4469
|
+ "— which downgrades those rows to warn severity and keeps the shipped palette visible in the "
|
|
4465
4470
|
+ "verdict; or hand-author the brand layer (write brand-theme.css, list it after next-core.css in "
|
|
4466
|
-
+ "commerce-page frontmatter styles, rebuild, then
|
|
4467
|
-
+ "
|
|
4471
|
+
+ "commerce-page frontmatter styles, rebuild, then run "
|
|
4472
|
+
+ `\`${cmd("record")} build --packet ${packetArg}\` and \`${cmd("record")} theme --packet ${packetArg}\`), `
|
|
4473
|
+
+ "per docs/brand-theme-bridge.md. This notice waives nothing on its own.",
|
|
4468
4474
|
};
|
|
4469
4475
|
}
|
|
4470
4476
|
|
|
@@ -4647,7 +4653,7 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4647
4653
|
} else if (result.stage === "deploy") {
|
|
4648
4654
|
if (packet.deploy?.target === LOCAL_SERVE_DEPLOY_TARGET) {
|
|
4649
4655
|
const plan = localServePlan(packet);
|
|
4650
|
-
push("deploy", "manual", null, `Serve the built ${plan.dir} output locally as the origin root (deploy.target is local-serve)${plan.rewrite ? ` — ${plan.rewrite}` : ""}, then record
|
|
4656
|
+
push("deploy", "manual", null, `Serve the built ${plan.dir} output locally as the origin root (deploy.target is local-serve)${plan.rewrite ? ` — ${plan.rewrite}` : ""}, then run ${cmd("record")} deploy --packet ${packetPath} --base-url <served url>: it checks every built page answers and records deploy.preview_url and stages.deploy. Localhost on any port is a Development domain: SDK allowed, analytics suppressed.`);
|
|
4651
4657
|
} else {
|
|
4652
4658
|
push("deploy", "manual", null, `Deploy _site/ output to ${packet.deploy?.target || "the deploy target"}, then record deploy.preview_url (or production_url) on the packet and stages.deploy in the assembly report.`);
|
|
4653
4659
|
}
|
|
@@ -4811,7 +4817,7 @@ Rules:
|
|
|
4811
4817
|
- Replace demo refs; do not copy Olympus-style shipping_methods into shop-three-step.
|
|
4812
4818
|
- For two-step package-selection flows, treat the selector page as the pre-checkout step and pass the selected cart to checkout with forcePackageId; preserve normal tracking params and strip forcePackageId from visible checkout URLs after SDK initialization.
|
|
4813
4819
|
- After page-kit build, inspect rendered _site output before handoff: each active page should have a body, Campaign Cart runtime markers, SDK meta tags from CampaignSpec sdk_hints.meta_tags, and no stale copied funnel attribution.
|
|
4814
|
-
- Run page-kit build and SDK/template lint, then record build before polish: \`${cmd("record")} build --packet ${packetPath}\`. It stamps stages.assembly.build_fingerprint with the fingerprint doctor computes from the built output (derived.build_output_fingerprint.value, sha256 over the sorted path+sha256 manifest of _site/<slug>/; doctor reports built_output.fingerprint_stale whenever the output on disk stops matching the recorded value), records report.design_source_package.material_fingerprint on stages.assembly.source_package_material_fingerprint when present, and sets stages.polish to "required" (required_by="build", required_for=["qa"]). Re-run it after every rebuild; never hand-edit these fields. Build must not mark stages.polish as completed/completed_with_warnings/skipped. If you applied a brand theme, record report.theme
|
|
4820
|
+
- Run page-kit build and SDK/template lint, then record build before polish: \`${cmd("record")} build --packet ${packetPath}\`. It stamps stages.assembly.build_fingerprint with the fingerprint doctor computes from the built output (derived.build_output_fingerprint.value, sha256 over the sorted path+sha256 manifest of _site/<slug>/; doctor reports built_output.fingerprint_stale whenever the output on disk stops matching the recorded value), records report.design_source_package.material_fingerprint on stages.assembly.source_package_material_fingerprint when present, and sets stages.polish to "required" (required_by="build", required_for=["qa"]). Re-run it after every rebuild; never hand-edit these fields. Build must not mark stages.polish as completed/completed_with_warnings/skipped. If you applied a brand theme, run \`${cmd("record")} theme --packet ${packetPath}\` after record build: it reads each built commerce page's stylesheet links and records report.theme (status applied, load_order=after-next-core, css_path, commerce_pages, evidence), and refuses, writing nothing, when a page that loads next-core.css does not load the brand layer after it. Never hand-edit report.theme.
|
|
4815
4821
|
- Capture the machine-readable build summary as an artifact: \`${PAGE_KIT_BUILD_SUMMARY_CAPTURE_COMMAND}\` (requires next-campaign-page-kit >= 0.1.4). Doctor verifies it for per-page build errors and Page Kit shape warnings (NESTED_NO_PERMALINK, DUPLICATE_OUTPUT, MISSING_FRONTMATTER, LAYOUT_NOT_FOUND). If the installed page-kit predates --json, record that in the assembly report instead of skipping silently.${localProofPromptLines(packet)}`;
|
|
4816
4822
|
}
|
|
4817
4823
|
|
|
@@ -4922,9 +4928,8 @@ Read first:
|
|
|
4922
4928
|
Nothing ships anywhere: the page-kit build produces _site/ output and you serve ${serveDir} on localhost (any static server, any port) for QA. Localhost on any port is a Campaigns App Development domain, so the SDK initialises there without an origin allowlist entry and Campaigns analytics events are suppressed.
|
|
4923
4929
|
|
|
4924
4930
|
Once the server is up:
|
|
4925
|
-
1.
|
|
4926
|
-
2.
|
|
4927
|
-
3. Run \`${cmd("next")} --packet ${packetPath}\` to advance to QA.
|
|
4931
|
+
1. Run \`${cmd("record")} deploy --packet ${packetPath} --base-url <localhost origin>${liveUrlPath}\`. It requests every built page under that URL, then records the URL on the packet at deploy.preview_url and stages.deploy as completed with the URL in outputs; it refuses, writing nothing, if a page does not answer.
|
|
4932
|
+
2. Run \`${cmd("next")} --packet ${packetPath}\` to advance to QA.
|
|
4928
4933
|
|
|
4929
4934
|
If the served build cannot be reached, set stages.deploy.status to "blocked" with a clear reason in outputs so the orchestration loop surfaces it rather than skipping past.`;
|
|
4930
4935
|
}
|
package/src/content-residue.mjs
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
// Rendered-output content-residue scan + proof-attestation gate.
|
|
2
2
|
//
|
|
3
3
|
// Scans BUILT campaign output (_site HTML), not frontmatter: layout- or
|
|
4
|
-
// script-rendered
|
|
5
|
-
//
|
|
4
|
+
// script-rendered chrome only exists after the build, which is why
|
|
5
|
+
// frontmatter-level checks missed the hardcoded starter countdown chrome.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// in this public package.
|
|
7
|
+
// What it looks for is template residue, never the merchant's own copy: the
|
|
8
|
+
// public starter-template demo strings, bracket-style demo stubs, and the
|
|
9
|
+
// literal needs-merchant-input marker. Proof and urgency content the merchant
|
|
10
|
+
// supplies (reviews, ratings, "Verified Purchase" labels, stock counters,
|
|
11
|
+
// countdowns) is the merchant's responsibility and is not scanned. No
|
|
12
|
+
// merchant-specific fingerprints are carried in this public package.
|
|
14
13
|
//
|
|
15
|
-
// Posture (
|
|
16
|
-
// - hard: the literal needs-merchant-input marker, and
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
14
|
+
// Posture (two tiers):
|
|
15
|
+
// - hard: the literal needs-merchant-input marker, and starter countdown
|
|
16
|
+
// chrome on a brief-backed build whose brief does not verify the offer
|
|
17
|
+
// urgency — blockers (collect-inputs).
|
|
18
|
+
// - review: demo/placeholder residue — warnings; a hit means an unreplaced
|
|
19
|
+
// demo slot or a stale template.
|
|
21
20
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
22
21
|
import { join, relative } from "node:path";
|
|
23
22
|
|
|
@@ -55,63 +54,6 @@ export const DEMO_RESIDUE_TERMS = Object.freeze([
|
|
|
55
54
|
"Sell-Out Risk: High",
|
|
56
55
|
]);
|
|
57
56
|
|
|
58
|
-
// Generic anti-pattern classes (review tier). Each id is stable so the
|
|
59
|
-
// attestation/review UX can key on it.
|
|
60
|
-
export const CONTENT_ANTI_PATTERNS = Object.freeze([
|
|
61
|
-
{
|
|
62
|
-
id: "invented_counts",
|
|
63
|
-
antiPattern: 1,
|
|
64
|
-
rule: "Counts/ratings/percent-recommend claims require a real, brief-sourced basis.",
|
|
65
|
-
// Count branch requires a proof-scaled number (comma groups, 4+ digits, or
|
|
66
|
-
// a trailing +) so ordinary commerce quantities ("Choose 3 pairs and
|
|
67
|
-
// save") never enter the review queue; "pairs"/"sold" dropped as nouns
|
|
68
|
-
// for the same reason. Small invented counts are a knowingly accepted gap.
|
|
69
|
-
regex: /\b(?:\d{1,3}(?:,\d{3})+|\d{4,}|\d+\+)\s+(?:reviews?|ratings?|customers?|users?|famil(?:y|ies)|wearers?|people)\b|\b(?:4\.[5-9]|5\.0)\s*(?:\/\s*5|stars?)|\b(?:9[0-9]|100)%\b[^<]{0,60}\b(?:recommend|reported|said|would)\b/i,
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
id: "verified_buyer_chrome",
|
|
73
|
-
antiPattern: 2,
|
|
74
|
-
rule: "'Verified' labels must resolve to a real approved review source.",
|
|
75
|
-
regex: /Verified\s+(?:Buyer|Customer|Purchase)|What\s+(?:Our\s+)?Customers\s+(?:Think|Say)|Real\s+(?:People|Customers)[^<]{0,20}Real\s+(?:Results|Relief)|5[- ]Star\s+Review/i,
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
id: "byline_persona",
|
|
79
|
-
antiPattern: 3,
|
|
80
|
-
rule: "Advertorial identities must be real, brief-supplied, and authorized.",
|
|
81
|
-
regex: /Mom\s+of\s+Two|Consumer\s+Report|Review\s+Team|Wellness\s+Educator|Licensed\s+(?:Physiotherapist|Professional)/i,
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
id: "borrowed_authority",
|
|
85
|
-
antiPattern: 4,
|
|
86
|
-
rule: "Expert/clinician/institution references require name, credential, permission, and source.",
|
|
87
|
-
regex: /\bDr\.\s+[A-Z]|\bM\.?D\.?\b|doctor[- ]recommended|clinically\s+(?:recognized|recommended)|expert\s+(?:says|recommends)/i,
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
id: "press_marquee",
|
|
91
|
-
antiPattern: 8,
|
|
92
|
-
rule: "Press mentions require a brief-supplied working URL for the exact merchant and product.",
|
|
93
|
-
regex: /As\s+Seen\s+(?:On|In)|Featured\s+(?:On|In)/i,
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
id: "science_theater",
|
|
97
|
-
antiPattern: 9,
|
|
98
|
-
rule: "Study/clinical/certification claims require citation metadata and approved wording.",
|
|
99
|
-
regex: /peer[- ]reviewed|science[- ]backed|backed\s+by\s+science|stud(?:y|ies)\s+(?:show|prove|confirm)|researchers\s+found|clinically\s+(?:proven|shown|tested)|NASA[- ]developed/i,
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
id: "scarcity_theater",
|
|
103
|
-
antiPattern: 10,
|
|
104
|
-
rule: "Urgency renders only from a real, approved promotion window or live inventory source.",
|
|
105
|
-
regex: /ENDS\s+AT\s+MIDNIGHT|Offer\s+Expires|Deal\s+Ending|Only\s+\d+\s+(?:Units\s+)?Left|Stock\s+(?:Levels?\s+)?Low|\d+%\s+Sold|Sell[- ]?Out\s+Risk|supplies\s+are\s+limited/i,
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
id: "fake_comparison",
|
|
109
|
-
antiPattern: 11,
|
|
110
|
-
rule: "Tested-N/showdown framing requires a brief-supplied comparison matrix and test record.",
|
|
111
|
-
regex: /(?:we\s+)?tested\s+\d+\s+(?:contenders|products|devices|gloves|combinations|brands)|only\s+one\s+(?:survived|worked|stood)|Competitor\s+[12]\b/i,
|
|
112
|
-
},
|
|
113
|
-
]);
|
|
114
|
-
|
|
115
57
|
const ENTITIES = new Map([
|
|
116
58
|
["&", "&"], ["<", "<"], [">", ">"], [""", '"'],
|
|
117
59
|
["'", "'"], ["'", "'"], [" ", " "], ["’", "’"], ["—", "—"],
|
|
@@ -168,13 +110,12 @@ function excerptAt(html, index, span = 80) {
|
|
|
168
110
|
}
|
|
169
111
|
|
|
170
112
|
// Pure scan of one rendered HTML document. Returns { hard: [], review: [] };
|
|
171
|
-
// each finding: { id, tier, rule?, excerpt }.
|
|
172
|
-
// view (attributes count, comments
|
|
173
|
-
//
|
|
174
|
-
//
|
|
113
|
+
// each finding: { id, tier, rule?, excerpt }. The hard checks and the demo
|
|
114
|
+
// terms run on the markup view (attributes count, comments and script/style
|
|
115
|
+
// bodies do not), so attribute residue is still caught; bracket stubs run on
|
|
116
|
+
// the same view with tags stripped, so CSS attribute selectors never match.
|
|
175
117
|
export function scanRenderedHtml(html, { urgencyVerified = false } = {}) {
|
|
176
118
|
const markup = markupView(typeof html === "string" ? html : "");
|
|
177
|
-
const text = visibleText(typeof html === "string" ? html : "");
|
|
178
119
|
const hard = [];
|
|
179
120
|
const review = [];
|
|
180
121
|
|
|
@@ -205,19 +146,6 @@ export function scanRenderedHtml(html, { urgencyVerified = false } = {}) {
|
|
|
205
146
|
}
|
|
206
147
|
}
|
|
207
148
|
|
|
208
|
-
for (const pattern of CONTENT_ANTI_PATTERNS) {
|
|
209
|
-
if (pattern.id === "scarcity_theater" && urgencyVerified) continue;
|
|
210
|
-
const match = pattern.regex.exec(text);
|
|
211
|
-
if (match) {
|
|
212
|
-
review.push({
|
|
213
|
-
id: pattern.id,
|
|
214
|
-
tier: "review",
|
|
215
|
-
antiPattern: pattern.antiPattern,
|
|
216
|
-
rule: pattern.rule,
|
|
217
|
-
excerpt: excerptAt(text, match.index),
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
149
|
return { hard, review };
|
|
222
150
|
}
|
|
223
151
|
|
package/src/doctor/checks.mjs
CHANGED
|
@@ -363,7 +363,7 @@ const SPEC_DOCTOR_CHECKS = createDoctorCheckRegistry([
|
|
|
363
363
|
{
|
|
364
364
|
id: PAGE_KIT_SDK_VERSION_SCOPE,
|
|
365
365
|
phase: "target",
|
|
366
|
-
run: ({ spec, errors, warnings, ready, derived, buildState }) => validateTargetSdkVersion(spec, errors, warnings, ready, derived, buildState),
|
|
366
|
+
run: ({ spec, packet, errors, warnings, ready, derived, buildState }) => validateTargetSdkVersion(spec, errors, warnings, ready, derived, buildState, packet),
|
|
367
367
|
},
|
|
368
368
|
{
|
|
369
369
|
id: PAGE_KIT_STORE_PROFILE_SCOPE,
|
|
@@ -1159,17 +1159,22 @@ function validateLocalProof(packet, report, errors, warnings, ready) {
|
|
|
1159
1159
|
addIssue(errors, LOCAL_PROOF_PARITY_SCOPE, `Production parity FAILED${difference ? ` — first non-gated difference: ${singleLineField(String(difference.kind))} at ${singleLineField(String(difference.route))}${difference.line ? ` line ${difference.line}` : ""}: ${singleLineField(String(difference.detail || ""))}` : `: ${singleLineField(String(parity.summary || ""))}`}. Rebuild in development, re-prove, and run ${parityCommand} again. ${LOCAL_PROOF_NEVER_EDIT_RULE}`, difference ? { first_difference: difference } : undefined);
|
|
1160
1160
|
}
|
|
1161
1161
|
|
|
1162
|
-
function validateTargetSdkVersion(spec, errors, warnings, ready, derived, buildState) {
|
|
1162
|
+
function validateTargetSdkVersion(spec, errors, warnings, ready, derived, buildState, packet = null) {
|
|
1163
1163
|
const report = buildState?.report || null;
|
|
1164
1164
|
const required = stageIsTerminal(report?.stages?.setup?.status)
|
|
1165
1165
|
|| stageIsTerminal(report?.stages?.assembly?.status)
|
|
1166
1166
|
|| derived.scaffold_required !== true;
|
|
1167
|
+
const family = packet?.assembly?.template_family;
|
|
1168
|
+
const verified = isNonEmptyString(family) && isCertifiedTemplateFamily(family)
|
|
1169
|
+
? assessTemplateFreshness({ family, catalog: resolveCommerceCatalog(), sdkSupportPolicy: defaultSdkSupportPolicy() }).verified_sdk_version
|
|
1170
|
+
: null;
|
|
1167
1171
|
const gate = evaluatePageKitSdkVersion({
|
|
1168
1172
|
spec,
|
|
1169
1173
|
specStatus: buildState?.specStatus || "ok",
|
|
1170
1174
|
targetLoad: buildState?.pageKitCampaignConfig,
|
|
1171
1175
|
waivers: report?.waivers,
|
|
1172
1176
|
required,
|
|
1177
|
+
familyVerification: verified ? { family, sdk_version: verified } : null,
|
|
1173
1178
|
});
|
|
1174
1179
|
derived.checkpoint_gates.push(gate);
|
|
1175
1180
|
|
|
@@ -3199,7 +3204,7 @@ function validateSourceCoverage(packet, packetPath, spec, errors, warnings, read
|
|
|
3199
3204
|
addIssue(
|
|
3200
3205
|
warnings,
|
|
3201
3206
|
"source_html.pages.source_hash",
|
|
3202
|
-
`Source page "${page.page_id}" hash mismatch — file at ${page.path} has changed since the
|
|
3207
|
+
`Source page "${page.page_id}" hash mismatch — file at ${page.path} has changed since intake recorded it in the Build Packet (recorded sha256=${expectedHash.slice(0, 12)}…, on-disk sha256=${actualHash.slice(0, 12)}…). This warning does not block. Re-running start or prepare-build with --force records the current file and clears recorded stage evidence; editing the source-html manifest alone does not change the hash the packet holds.`,
|
|
3203
3208
|
);
|
|
3204
3209
|
}
|
|
3205
3210
|
}
|
|
@@ -4018,11 +4023,11 @@ function collectPlaceholderTextResidueMatches(root, terms) {
|
|
|
4018
4023
|
}
|
|
4019
4024
|
|
|
4020
4025
|
// Rendered-output content-residue scan (assembly-surfaces prototype): scans
|
|
4021
|
-
// BUILT pages for the needs-merchant-input marker and
|
|
4022
|
-
//
|
|
4023
|
-
//
|
|
4024
|
-
//
|
|
4025
|
-
// only exists after the build.
|
|
4026
|
+
// BUILT pages for the needs-merchant-input marker and, on a brief-backed
|
|
4027
|
+
// build, starter countdown chrome the brief does not verify (blockers), plus
|
|
4028
|
+
// template demo/placeholder residue (warnings). The merchant's own proof and
|
|
4029
|
+
// urgency copy is not scanned. Scans _site output, not frontmatter —
|
|
4030
|
+
// layout/script-rendered chrome only exists after the build.
|
|
4026
4031
|
export function validateBuiltContentResidue(packet, errors, warnings, ready, derived, buildState = {}) {
|
|
4027
4032
|
if (!isStageComplete(buildState.report, "assembly")) return;
|
|
4028
4033
|
// Scan the RENDERED _site output, not the campaign source dir: the Liquid
|
|
@@ -4035,7 +4040,7 @@ export function validateBuiltContentResidue(packet, errors, warnings, ready, der
|
|
|
4035
4040
|
const urgencyVerified = briefUrgencyVerified(brief?.payload);
|
|
4036
4041
|
const findings = scanBuiltOutputContentResidue(siteRoot, { urgencyVerified });
|
|
4037
4042
|
if (!findings.length) {
|
|
4038
|
-
ready.push("Built output carries no needs-input markers, unverified urgency chrome, or
|
|
4043
|
+
ready.push("Built output carries no needs-input markers, unverified urgency chrome, or template demo residue");
|
|
4039
4044
|
return;
|
|
4040
4045
|
}
|
|
4041
4046
|
// One issue per finding id, carrying the FULL file inventory: the id keeps
|
|
@@ -4068,9 +4073,8 @@ export function validateBuiltContentResidue(packet, errors, warnings, ready, der
|
|
|
4068
4073
|
);
|
|
4069
4074
|
} else if (id === "unverified_urgency_countdown") {
|
|
4070
4075
|
// The scanner emits this only when the brief does not verify urgency
|
|
4071
|
-
// (urgencyVerified=false).
|
|
4072
|
-
//
|
|
4073
|
-
// with no brief payload may carry an intentional countdown — warning.
|
|
4076
|
+
// (urgencyVerified=false). Only a readable brief payload (AI-assembled)
|
|
4077
|
+
// makes it a finding.
|
|
4074
4078
|
if (briefReadable) {
|
|
4075
4079
|
addIssue(
|
|
4076
4080
|
errors,
|
|
@@ -4078,30 +4082,18 @@ export function validateBuiltContentResidue(packet, errors, warnings, ready, der
|
|
|
4078
4082
|
`Built output renders countdown chrome without verified offer urgency (${where}). Set offer.urgency.verified in ${BRIEF_PAYLOAD_REL_PATH} from a real promotion window, or blank the urgency slots.`,
|
|
4079
4083
|
);
|
|
4080
4084
|
} else if (brief?.error) {
|
|
4081
|
-
//
|
|
4082
|
-
// proof_attestation.unreadable
|
|
4083
|
-
//
|
|
4084
|
-
// brief parses — "confirm the urgency is real" copy here would point
|
|
4085
|
-
// the operator at the wrong fix.
|
|
4086
|
-
} else {
|
|
4087
|
-
addIssue(
|
|
4088
|
-
warnings,
|
|
4089
|
-
"content_residue.urgency_unattested",
|
|
4090
|
-
`Built output renders countdown chrome and no brief payload attests the promotion window (${where}). Confirm the urgency is real (a genuine offer window or live inventory) before launch.`,
|
|
4091
|
-
);
|
|
4085
|
+
// Deliberately nothing: an unreadable brief payload already blocks
|
|
4086
|
+
// through proof_attestation.unreadable (validateProofAttestation),
|
|
4087
|
+
// and urgency is re-evaluated once the brief parses.
|
|
4092
4088
|
}
|
|
4089
|
+
// No brief payload at all: the countdown is the merchant's content and
|
|
4090
|
+
// is not ours to question.
|
|
4093
4091
|
} else if (id === "demo_residue_term" || id === "bracket_placeholder_stub") {
|
|
4094
4092
|
addIssue(
|
|
4095
4093
|
warnings,
|
|
4096
4094
|
"content_residue.demo_residue",
|
|
4097
4095
|
`Built output carries template demo/placeholder residue (${where}; e.g. "${finding.excerpt}"). Fill or blank the slot — demo values must never ship.`,
|
|
4098
4096
|
);
|
|
4099
|
-
} else {
|
|
4100
|
-
addIssue(
|
|
4101
|
-
warnings,
|
|
4102
|
-
"content_residue.anti_pattern",
|
|
4103
|
-
`Built output matches content anti-pattern "${id}" (${where}; e.g. "${finding.excerpt}"). ${finding.rule || "Remove it or route it through brief-sourced proof."} This is a review warning and nothing downstream blocks on it: the claim is the operator's and the client's responsibility — remove or evidence it, never make it more plausible.`,
|
|
4104
|
-
);
|
|
4105
4097
|
}
|
|
4106
4098
|
}
|
|
4107
4099
|
}
|
package/src/doctor/inspect.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Doctor entry points: the doctor command, packet inspection and built-output inspection.
|
|
2
2
|
import { resolveCampaignIdentity } from "../spec-source-identity.mjs";
|
|
3
|
+
import { applyLocalPreviewToCheckpoint, applyLocalPreviewToPolishGate, carriedForwardMessage, CARRIED_FORWARD } from "../local-preview-policy.mjs";
|
|
3
4
|
import { withHtmlScanSnapshot } from "../html-scan.mjs";
|
|
4
5
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -506,16 +507,23 @@ function inspectDoctorPacket(packetPath, { contextPath = undefined, reportPath =
|
|
|
506
507
|
}
|
|
507
508
|
runPricingCssHideCheck({ packet, derived, warnings, ready, report });
|
|
508
509
|
|
|
509
|
-
|
|
510
|
+
// The local preview policy (local-preview-policy.mjs) carries missing
|
|
511
|
+
// polish and page-load evidence forward as warnings on a local-serve packet.
|
|
512
|
+
const polishCheckpointGate = applyLocalPreviewToCheckpoint(
|
|
513
|
+
evaluateRecordedHiddenEagerMediaCheckpoint({ packet, report }),
|
|
514
|
+
{ packet, report },
|
|
515
|
+
);
|
|
510
516
|
derived.polish_checkpoint_gate = polishCheckpointGate;
|
|
511
|
-
const polishGate = evaluatePolishGate({
|
|
517
|
+
const polishGate = applyLocalPreviewToPolishGate(evaluatePolishGate({
|
|
512
518
|
report,
|
|
513
519
|
hiddenEagerMediaGate: polishCheckpointGate,
|
|
514
520
|
currentOutputFingerprint: derived.build_output_fingerprint?.value || null,
|
|
515
|
-
});
|
|
521
|
+
}), { packet, checkpointGate: polishCheckpointGate });
|
|
516
522
|
derived.polish_gate = polishGate;
|
|
517
523
|
if (polishGate.status === "blocked" && !polishGate.owned_checkpoint_only) {
|
|
518
524
|
pushGateIssue({ errors, warnings }, gateIssue("polish_gate", polishGate));
|
|
525
|
+
} else if (polishGate.status === CARRIED_FORWARD && !polishGate.owned_checkpoint_only) {
|
|
526
|
+
addIssue(warnings, polishGate.code, carriedForwardMessage(polishGate), { polish_gate: polishGate });
|
|
519
527
|
} else if (polishGate.status === "waived" && !polishGate.owned_checkpoint_only) {
|
|
520
528
|
ready.push(`Polish gate passed under waiver: ${polishGate.waiver?.reason || "(no reason recorded)"}`);
|
|
521
529
|
} else if (polishGate.status === "pass") {
|
|
@@ -524,6 +532,8 @@ function inspectDoctorPacket(packetPath, { contextPath = undefined, reportPath =
|
|
|
524
532
|
|
|
525
533
|
if (polishCheckpointGate.status === "blocked") {
|
|
526
534
|
pushGateIssue({ errors, warnings }, gateIssue("polish_checkpoint_gate", polishCheckpointGate));
|
|
535
|
+
} else if (polishCheckpointGate.status === CARRIED_FORWARD) {
|
|
536
|
+
addIssue(warnings, polishCheckpointGate.code, carriedForwardMessage(polishCheckpointGate), { polish_checkpoint_gate: polishCheckpointGate });
|
|
527
537
|
} else if (polishCheckpointGate.status === "waived") {
|
|
528
538
|
addIssue(
|
|
529
539
|
warnings,
|
package/src/doctor/next-step.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The next step doctor recommends, and the gate issues `next` reads from doctor.
|
|
2
2
|
import { campaignIdentitiesMatch } from "../spec-source-identity.mjs";
|
|
3
|
+
import { CARRIED_FORWARD } from "../local-preview-policy.mjs";
|
|
3
4
|
import { resolve } from "node:path";
|
|
4
5
|
import { orderPathDepthDriftText } from "../proof-policy.mjs";
|
|
5
6
|
import { anyAssemblyReportStageBlocked } from "../stage-ledger.mjs";
|
|
@@ -462,6 +463,9 @@ function pickNextStage(report, { errors = [], derived = null }, prepareBuildGate
|
|
|
462
463
|
}
|
|
463
464
|
|
|
464
465
|
for (const cliStage of NEXT_STAGE_ORDER) {
|
|
466
|
+
// On the local preview, missing polish is carried forward as a warning
|
|
467
|
+
// (local-preview-policy.mjs): the ladder moves on to deploy and QA.
|
|
468
|
+
if (cliStage === "polish" && polishGate.status === CARRIED_FORWARD) continue;
|
|
465
469
|
const reportKey = reportKeyForCliStage(cliStage);
|
|
466
470
|
const stage = report.stages[reportKey];
|
|
467
471
|
if (!stage) {
|