@nextcommerce/campaigns-os 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +204 -0
- package/CHANGELOG.md +5002 -0
- package/CONTEXT.md +685 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +368 -0
- package/agents/claude/CLAUDE.md +32 -0
- package/agents/codex/AGENTS.md +27 -0
- package/agents/copilot/copilot-instructions.md +14 -0
- package/agents/cursor/campaigns-os.mdc +13 -0
- package/bin/campaigns-os.mjs +38 -0
- package/campaign-spec/README.md +138 -0
- package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
- package/campaign-spec/dist/analytics-vocabulary.js +74 -0
- package/campaign-spec/dist/index.d.ts +40 -0
- package/campaign-spec/dist/index.js +77 -0
- package/campaign-spec/dist/normalize.d.ts +22 -0
- package/campaign-spec/dist/normalize.js +41 -0
- package/campaign-spec/dist/routing.d.ts +190 -0
- package/campaign-spec/dist/routing.js +263 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
- package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
- package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
- package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
- package/campaign-spec/dist/rules/cycle-detection.js +141 -0
- package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
- package/campaign-spec/dist/rules/design-source-shape.js +142 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
- package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
- package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
- package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
- package/campaign-spec/dist/rules/funnel-count.js +37 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
- package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
- package/campaign-spec/dist/rules/funnel-identity.js +57 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
- package/campaign-spec/dist/rules/index.d.ts +69 -0
- package/campaign-spec/dist/rules/index.js +131 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
- package/campaign-spec/dist/rules/page-count.d.ts +11 -0
- package/campaign-spec/dist/rules/page-count.js +31 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
- package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
- package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
- package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
- package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
- package/campaign-spec/dist/rules/schema-version.js +53 -0
- package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
- package/campaign-spec/dist/rules/sdk-version.js +96 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
- package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
- package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
- package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
- package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
- package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
- package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
- package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
- package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
- package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
- package/campaign-spec/dist/sdk-version-parse.js +62 -0
- package/campaign-spec/dist/types.d.ts +674 -0
- package/campaign-spec/dist/types.js +40 -0
- package/campaign-spec/package.json +12 -0
- package/compatibility.json +25 -0
- package/contracts/agent-relevant-change-policy.v1.json +111 -0
- package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
- package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
- package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
- package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
- package/contracts/commerce-surface-catalog.json +2452 -0
- package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
- package/contracts/fixtures/orientation/envelope/current.json +95 -0
- package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
- package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
- package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
- package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
- package/contracts/fixtures/orientation/envelope/refused.json +100 -0
- package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
- package/contracts/fixtures/orientation/envelope/updated.json +135 -0
- package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
- package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
- package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
- package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
- package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
- package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
- package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
- package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
- package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
- package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
- package/contracts/migration-sidecar-bundle.v0.json +149 -0
- package/contracts/orientation-limits.v1.json +41 -0
- package/contracts/orientation-reason-codes.v1.json +196 -0
- package/contracts/private-template-sources.json +8 -0
- package/contracts/release-ledger.json +4598 -0
- package/contracts/reserved-skill-names.json +13 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
- package/contracts/supported-surface.json +180 -0
- package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.apollo.v0.json +27 -0
- package/contracts/template-brand-contract.demeter.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
- package/contracts/template-brand-contract.olympus.v0.json +27 -0
- package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
- package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
- package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.apollo.v0.json +12 -0
- package/contracts/template-slot-manifest.demeter.v0.json +30 -0
- package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
- package/contracts/template-slot-manifest.olympus.v0.json +12 -0
- package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
- package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
- package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
- package/docs/brand-theme-bridge.md +159 -0
- package/docs/build-packet.md +1300 -0
- package/docs/campaign-build-brief.md +145 -0
- package/docs/campaign-standardization-report.md +329 -0
- package/docs/campaigns-os-build-flow.md +117 -0
- package/docs/design-source-package.md +784 -0
- package/docs/legacy-migration.md +58 -0
- package/docs/migration-sidecar-bundle.md +139 -0
- package/docs/orientation-contract-reference.md +1220 -0
- package/docs/polish-evidence.md +502 -0
- package/docs/qa-and-test-orders.md +1691 -0
- package/docs/release-ledger-authoring-guide.md +274 -0
- package/docs/runtime-readiness.md +211 -0
- package/docs/supported-surface.md +83 -0
- package/docs/versioning.md +55 -0
- package/docs/workflow-findings-sidecar.md +588 -0
- package/package.json +135 -0
- package/prompts/first-build.md +27 -0
- package/prompts/friction-log.md +30 -0
- package/schemas/campaign-build-brief.v1.schema.json +149 -0
- package/schemas/campaign-design-source-package.v0.schema.json +697 -0
- package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
- package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
- package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
- package/schemas/campaign-spec.v4.schema.json +582 -0
- package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
- package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
- package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
- package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
- package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
- package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
- package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
- package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
- package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
- package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
- package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
- package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
- package/schemas/source-html-manifest.v0.schema.json +252 -0
- package/skills/next-campaigns-build/SKILL.md +73 -0
- package/skills/next-campaigns-os/SKILL.md +101 -0
- package/skills/next-campaigns-os/references/session-intake.md +160 -0
- package/skills/next-campaigns-os-setup/SKILL.md +22 -0
- package/skills/next-campaigns-polish/SKILL.md +145 -0
- package/skills/next-campaigns-qa/SKILL.md +92 -0
- package/skills.json +56 -0
- package/skills.sh +64 -0
- package/src/adapter-decision-contract.mjs +333 -0
- package/src/brand-theme.mjs +1151 -0
- package/src/browser-launch.mjs +79 -0
- package/src/build-brief.mjs +781 -0
- package/src/built-site-scope.mjs +312 -0
- package/src/campaign-ecosystem.mjs +734 -0
- package/src/campaign-identity.mjs +405 -0
- package/src/campaign-workspace.mjs +127 -0
- package/src/checkpoint-waiver.mjs +302 -0
- package/src/cli.mjs +13442 -0
- package/src/commercial-journey.mjs +1119 -0
- package/src/commercial-parity.mjs +965 -0
- package/src/consent.mjs +347 -0
- package/src/content-residue.mjs +322 -0
- package/src/deadline.mjs +81 -0
- package/src/design-source-package.mjs +2604 -0
- package/src/deviation.mjs +107 -0
- package/src/doctor-check-registry.mjs +49 -0
- package/src/doctor-sidecar.mjs +106 -0
- package/src/finding-cause.mjs +557 -0
- package/src/findings.mjs +326 -0
- package/src/fs-identity.mjs +68 -0
- package/src/gate-actions.mjs +105 -0
- package/src/html-scan.mjs +53 -0
- package/src/install-mode.mjs +272 -0
- package/src/legacy-migration.d.ts +128 -0
- package/src/legacy-migration.mjs +510 -0
- package/src/lifecycle.mjs +338 -0
- package/src/local-proof.mjs +401 -0
- package/src/map-pin-writeback.mjs +210 -0
- package/src/orchestration-stage-contract.mjs +81 -0
- package/src/package-install-fixture.mjs +33 -0
- package/src/page-kit-build-summary.mjs +175 -0
- package/src/page-kit-campaign-config.mjs +57 -0
- package/src/page-kit-sdk-version.mjs +392 -0
- package/src/page-kit-store-profile.mjs +369 -0
- package/src/page-kit-sync.mjs +162 -0
- package/src/polish-browser.mjs +867 -0
- package/src/polish-capture.mjs +1094 -0
- package/src/polish-deadline.mjs +51 -0
- package/src/polish-gate.mjs +739 -0
- package/src/polish-node.mjs +639 -0
- package/src/polish-page-load.mjs +1100 -0
- package/src/private-template-source.mjs +237 -0
- package/src/proof-policy.mjs +82 -0
- package/src/qa-analytics-correctness.mjs +307 -0
- package/src/qa-analytics-errors.mjs +38 -0
- package/src/qa-analytics-parity.mjs +699 -0
- package/src/qa-binding-evidence.mjs +140 -0
- package/src/qa-browser.mjs +6608 -0
- package/src/qa-cart-entry.mjs +406 -0
- package/src/qa-commercial-parity.mjs +641 -0
- package/src/qa-node.mjs +3620 -0
- package/src/qa-order-bump.mjs +381 -0
- package/src/qa-parity-capture.mjs +428 -0
- package/src/qa-parity-fixture.mjs +359 -0
- package/src/qa-publish.mjs +362 -0
- package/src/qa-purchase-data-layer.mjs +263 -0
- package/src/qa-route-probe.mjs +272 -0
- package/src/qa-sidecar.mjs +188 -0
- package/src/qa-test-order-topology.mjs +207 -0
- package/src/qa-url-privacy.mjs +13 -0
- package/src/qa-verdict-discovery.mjs +192 -0
- package/src/qa-verdict-publish.mjs +105 -0
- package/src/qa-verdict.mjs +287 -0
- package/src/remit.mjs +388 -0
- package/src/repo-scan.mjs +83 -0
- package/src/route-identity.mjs +133 -0
- package/src/run-record-closeout.mjs +229 -0
- package/src/run-record.mjs +839 -0
- package/src/run-session.mjs +226 -0
- package/src/runtime-state-ignore.mjs +113 -0
- package/src/sdk-attribute-index.mjs +212 -0
- package/src/sdk-markup.mjs +358 -0
- package/src/sdk-meta-tags.mjs +52 -0
- package/src/shell-token.mjs +7 -0
- package/src/sidecar-bundle.mjs +399 -0
- package/src/source-asset-crawl.mjs +469 -0
- package/src/source-html-intake.mjs +627 -0
- package/src/source-html-manifest.mjs +276 -0
- package/src/source-prep.mjs +284 -0
- package/src/spec-derive-store.mjs +431 -0
- package/src/spec-derive.mjs +514 -0
- package/src/spec-fetch.mjs +66 -0
- package/src/spec-hash.mjs +48 -0
- package/src/spec-identity.mjs +27 -0
- package/src/stage-ledger.mjs +509 -0
- package/src/standardization-report.mjs +1297 -0
- package/src/template-brand-contract.mjs +473 -0
- package/src/template-freshness.mjs +196 -0
- package/src/template-reference.mjs +81 -0
- package/src/template-slot-manifest.mjs +150 -0
- package/src/text-safety.mjs +62 -0
- package/src/theme-gate.mjs +185 -0
- package/src/upsell-selector-scope.mjs +299 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
function isObject(value) {
|
|
2
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function nonEmptyString(value) {
|
|
6
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const UNRESOLVED_TEMPLATE_FAMILIES = Object.freeze(["auto", "undecided"]);
|
|
10
|
+
|
|
11
|
+
export function isUnresolvedTemplateFamily(value) {
|
|
12
|
+
const family = nonEmptyString(value);
|
|
13
|
+
return !family || UNRESOLVED_TEMPLATE_FAMILIES.includes(family);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const TEMPLATE_FAMILY_FLAG_SOURCE = "--template-family";
|
|
17
|
+
export const TEMPLATE_FAMILY_HINT_SOURCE = "CampaignSpec preferred_template_family";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the template family an entry-point command will build on, from the
|
|
21
|
+
* two channels an operator has: the `--template-family` flag and the
|
|
22
|
+
* CampaignSpec authoring hint. The flag wins — that was always the behaviour
|
|
23
|
+
* (docs/build-packet.md "Authoring-Time Hints") — but it used to win in
|
|
24
|
+
* silence, so a spec hinting one family and a flag naming another produced a
|
|
25
|
+
* packet with no trace of the discarded hint. The resolution is returned as
|
|
26
|
+
* data, including `overridden`, so the caller can say which value won.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveTemplateFamilySelection({ flag = null, hint = null } = {}) {
|
|
29
|
+
const flagValue = nonEmptyString(flag);
|
|
30
|
+
const hintValue = nonEmptyString(hint);
|
|
31
|
+
const value = flagValue || hintValue || "undecided";
|
|
32
|
+
const source = flagValue
|
|
33
|
+
? TEMPLATE_FAMILY_FLAG_SOURCE
|
|
34
|
+
: hintValue
|
|
35
|
+
? TEMPLATE_FAMILY_HINT_SOURCE
|
|
36
|
+
: "default";
|
|
37
|
+
return {
|
|
38
|
+
value,
|
|
39
|
+
source,
|
|
40
|
+
flag: flagValue,
|
|
41
|
+
hint: hintValue,
|
|
42
|
+
// Only a disagreement is an override. A flag that merely repeats the hint
|
|
43
|
+
// confirms it, and warning there would train operators to ignore the line.
|
|
44
|
+
overridden: Boolean(flagValue && hintValue && flagValue !== hintValue),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the selected catalog family into the input shape consumed by the
|
|
50
|
+
* Design Source Package synthesizer. A family without published proof remains
|
|
51
|
+
* a family-only input so the DSP emits its existing missing-reference blocker.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveTemplateFamilyDesignSource(catalog, family) {
|
|
54
|
+
const normalizedFamily = nonEmptyString(family);
|
|
55
|
+
if (isUnresolvedTemplateFamily(normalizedFamily)) return null;
|
|
56
|
+
|
|
57
|
+
const entry = isObject(catalog?.families?.[normalizedFamily])
|
|
58
|
+
? catalog.families[normalizedFamily]
|
|
59
|
+
: null;
|
|
60
|
+
const reference = isObject(entry?.templateReference)
|
|
61
|
+
? entry.templateReference
|
|
62
|
+
: isObject(entry?.template_reference)
|
|
63
|
+
? entry.template_reference
|
|
64
|
+
: null;
|
|
65
|
+
if (!reference) return { family: normalizedFamily };
|
|
66
|
+
|
|
67
|
+
const referenceFamily = nonEmptyString(reference.family);
|
|
68
|
+
if (referenceFamily && referenceFamily !== normalizedFamily) {
|
|
69
|
+
throw new TypeError(
|
|
70
|
+
`Template Reference family ${JSON.stringify(referenceFamily)} does not match selected family ` +
|
|
71
|
+
`${JSON.stringify(normalizedFamily)}.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const version = nonEmptyString(reference.version);
|
|
76
|
+
return {
|
|
77
|
+
family: normalizedFamily,
|
|
78
|
+
...(version ? { version } : {}),
|
|
79
|
+
template_reference: structuredClone(reference),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Template slot manifests: the content-slot contract for certified template
|
|
2
|
+
// families. One shared content-slot core declares every content slot on the
|
|
3
|
+
// shared pages (semantic description, length band, source policy, proof and
|
|
4
|
+
// urgency flags); per-family manifests extend it with family-specific overlay
|
|
5
|
+
// keys and the list of commerce-owned pages the content layer never touches.
|
|
6
|
+
//
|
|
7
|
+
// Files live at contracts/template-slot-manifest.<family>.v0.json and
|
|
8
|
+
// contracts/template-slot-manifest.shared-content-core.v0.json. Family
|
|
9
|
+
// manifests `extends` the shared core; object values merge, arrays and
|
|
10
|
+
// scalars replace (same semantics as template brand contracts).
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
|
+
|
|
17
|
+
export const TEMPLATE_SLOT_MANIFEST_SCHEMA = "template-slot-manifest/v0";
|
|
18
|
+
|
|
19
|
+
export const SLOT_SOURCE_POLICIES = Object.freeze([
|
|
20
|
+
"generated",
|
|
21
|
+
"merchant_asset_required",
|
|
22
|
+
"spec_truth",
|
|
23
|
+
"brand",
|
|
24
|
+
"template_static",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
export function templateSlotManifestPath(family) {
|
|
28
|
+
if (typeof family !== "string" || !family.trim()) return null;
|
|
29
|
+
return join(ROOT, "contracts", `template-slot-manifest.${family.trim()}.v0.json`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sharedContentCorePath() {
|
|
33
|
+
return join(ROOT, "contracts", "template-slot-manifest.shared-content-core.v0.json");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function loadTemplateSlotManifest(family) {
|
|
37
|
+
const path = templateSlotManifestPath(family);
|
|
38
|
+
if (!path || !existsSync(path)) return null;
|
|
39
|
+
const manifest = loadManifestFile(path);
|
|
40
|
+
if (manifest.family !== family) {
|
|
41
|
+
throw slotManifestError(
|
|
42
|
+
"family_mismatch",
|
|
43
|
+
`Template slot manifest ${path} declares family "${manifest.family}"; expected "${family}".`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return manifest;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadManifestFile(path, seen = new Set()) {
|
|
50
|
+
let manifest = null;
|
|
51
|
+
try {
|
|
52
|
+
manifest = JSON.parse(readFileSync(path, "utf8"));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw slotManifestError(
|
|
55
|
+
"parse_error",
|
|
56
|
+
`Template slot manifest ${path} failed to parse: ${error instanceof Error ? error.message : String(error)}.`,
|
|
57
|
+
error,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return resolveExtendsChain(manifest, { dir: dirname(path), label: path, seen });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveExtendsChain(manifest, { dir, label = dir, seen = new Set() } = {}) {
|
|
64
|
+
if (seen.has(label)) throw slotManifestError("extends_cycle", `Template slot manifest extends cycle at ${label}.`);
|
|
65
|
+
seen.add(label);
|
|
66
|
+
if (!isPlainObject(manifest) || manifest.schema_version !== TEMPLATE_SLOT_MANIFEST_SCHEMA) {
|
|
67
|
+
throw slotManifestError(
|
|
68
|
+
"schema_mismatch",
|
|
69
|
+
`Template slot manifest ${label} has schema_version "${manifest?.schema_version}"; expected "${TEMPLATE_SLOT_MANIFEST_SCHEMA}".`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const parentRef = typeof manifest.extends === "string" && manifest.extends.trim() ? manifest.extends.trim() : null;
|
|
73
|
+
if (!parentRef) return manifest;
|
|
74
|
+
const parentPath = join(dir, parentRef);
|
|
75
|
+
if (!existsSync(parentPath)) {
|
|
76
|
+
throw slotManifestError("extends_missing_parent", `Template slot manifest ${label} extends missing file "${parentRef}".`);
|
|
77
|
+
}
|
|
78
|
+
const merged = mergeObjects(loadManifestFile(parentPath, seen), manifest);
|
|
79
|
+
delete merged.extends;
|
|
80
|
+
return merged;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function slotManifestError(code, message, cause = undefined) {
|
|
84
|
+
const error = new Error(message, cause ? { cause } : undefined);
|
|
85
|
+
error.code = code;
|
|
86
|
+
return error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function mergeObjects(parent, child) {
|
|
90
|
+
const merged = { ...parent };
|
|
91
|
+
for (const [key, value] of Object.entries(child)) {
|
|
92
|
+
if (isPlainObject(value) && isPlainObject(parent?.[key])) {
|
|
93
|
+
merged[key] = mergeObjects(parent[key], value);
|
|
94
|
+
} else {
|
|
95
|
+
merged[key] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return merged;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isPlainObject(value) {
|
|
102
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sectionsOf(pageEntry) {
|
|
106
|
+
return Array.isArray(pageEntry?.sections) ? pageEntry.sections : [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Every slot entry declared for a page: shared-core slots plus the family
|
|
110
|
+
// overlay's slots. Returns [] for commerce-owned or unknown pages.
|
|
111
|
+
export function manifestPageSlots(manifest, page) {
|
|
112
|
+
if (!isPlainObject(manifest)) return [];
|
|
113
|
+
const slots = [];
|
|
114
|
+
for (const source of [manifest.pages?.[page], manifest.overlay?.pages?.[page]]) {
|
|
115
|
+
for (const section of sectionsOf(source)) {
|
|
116
|
+
for (const slot of Array.isArray(section.slots) ? section.slots : []) {
|
|
117
|
+
if (isPlainObject(slot) && typeof slot.key === "string") slots.push(slot);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return slots;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function declaredSlotKeys(manifest, page) {
|
|
125
|
+
return new Set(manifestPageSlots(manifest, page).map((slot) => slot.key));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Pages the content layer never validates key-by-key: commerce-owned pages
|
|
129
|
+
// declared by the family manifest plus pages the shared core marks
|
|
130
|
+
// commerce_owned (checkout).
|
|
131
|
+
export function commerceOwnedPages(manifest) {
|
|
132
|
+
const pages = new Set(
|
|
133
|
+
Array.isArray(manifest?.commerce_owned_pages) ? manifest.commerce_owned_pages.map(String) : [],
|
|
134
|
+
);
|
|
135
|
+
for (const [page, entry] of Object.entries(manifest?.pages || {})) {
|
|
136
|
+
if (isPlainObject(entry) && entry.commerce_owned === true) pages.add(page);
|
|
137
|
+
}
|
|
138
|
+
return pages;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Slots whose chrome may render only under verified offer urgency.
|
|
142
|
+
export function urgencySlots(manifest, page) {
|
|
143
|
+
return manifestPageSlots(manifest, page).filter((slot) => slot.enabled_by_verified_urgency === true);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Proof surfaces (counts, ratings, reviews, UGC, byline identity) — the slots
|
|
147
|
+
// the attestation lane and the rendered-residue doctor checks care about.
|
|
148
|
+
export function proofSlots(manifest, page) {
|
|
149
|
+
return manifestPageSlots(manifest, page).filter((slot) => slot.proof_surface === true);
|
|
150
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Text flattening for values this toolkit prints but did not author.
|
|
2
|
+
//
|
|
3
|
+
// A leaf module on purpose: every function here is pure string work with no
|
|
4
|
+
// imports, and all of them are published as the `./text-safety` package export so
|
|
5
|
+
// a consumer that renders toolkit-derived values (a run id, a path, a quoted
|
|
6
|
+
// loader message) into its own single-line notice can flatten them the same
|
|
7
|
+
// way instead of reimplementing the escape set and drifting from it.
|
|
8
|
+
|
|
9
|
+
// One line, no control characters. These values land in operator notices
|
|
10
|
+
// that are single-line by construction, so a run id or a path carrying a
|
|
11
|
+
// newline, a carriage return, or an ANSI escape could split a notice,
|
|
12
|
+
// overwrite it, or dress a fabricated line up as toolkit output. Neither value
|
|
13
|
+
// is toolkit-authored: the path comes from a packet-derived target directory
|
|
14
|
+
// and the id can be handed in with --run-id. Replaced, never dropped, so a
|
|
15
|
+
// mangled value stays visible as mangled rather than silently shortening the
|
|
16
|
+
// message.
|
|
17
|
+
export function singleLineField(value, fallback = "") {
|
|
18
|
+
const raw = typeof value === "string" ? value : value == null ? "" : String(value);
|
|
19
|
+
if (!raw) return fallback;
|
|
20
|
+
// C0, DEL and C1, which covers CR, LF, TAB and the ESC that starts ANSI.
|
|
21
|
+
return raw.replace(/[\u0000-\u001f\u007f-\u009f]/g, "\uFFFD");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// One trimmed line for a value folded INTO a sentence rather than printed as
|
|
25
|
+
// its own field: a gate's repair command quoted in a notice, a loader message
|
|
26
|
+
// quoted in an advisory. The control-character half is `singleLineField`'s and
|
|
27
|
+
// is not duplicated; what is added is the reading a sentence gives whitespace.
|
|
28
|
+
// Line breaks and tabs become spaces — a newline inside a quoted instruction
|
|
29
|
+
// or JSON fragment is a word boundary, and U+FFFD there reads as mojibake —
|
|
30
|
+
// and runs of whitespace collapse, because the value is read as words, not
|
|
31
|
+
// compared as an identifier. ESC, DEL and the rest still become the
|
|
32
|
+
// replacement character, since those have no reading as text. A value that is
|
|
33
|
+
// compared against a known word (a disposition) keeps `singleLineField`.
|
|
34
|
+
export function singleLineFragment(value, fallback = "") {
|
|
35
|
+
const spaced = String(value ?? "").replace(/[\r\n\t\v\f]+/g, " ");
|
|
36
|
+
const flattened = singleLineField(spaced).replace(/\s+/g, " ").trim();
|
|
37
|
+
return flattened || fallback;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const ADVISORY_DETAIL_MAX = 300;
|
|
41
|
+
|
|
42
|
+
// One trimmed line, no control characters, no Markdown that could restyle the
|
|
43
|
+
// rest of the description or a rendered bullet.
|
|
44
|
+
//
|
|
45
|
+
// The folding is `singleLineFragment`'s. Two things are added on top of it,
|
|
46
|
+
// because this input is different in kind from a repair command: a loader
|
|
47
|
+
// message QUOTES FILE CONTENT, so it can be long and can carry Markdown.
|
|
48
|
+
export function singleLineDetail(detail, max = ADVISORY_DETAIL_MAX) {
|
|
49
|
+
const flattened = singleLineFragment(detail).replace(/[`*_[\]<>]/g, "\\$&");
|
|
50
|
+
if (!flattened) return "(no detail reported)";
|
|
51
|
+
// A published export: the result never exceeds `max` characters, and it
|
|
52
|
+
// never fabricates. A non-finite max means the default budget; a derived or
|
|
53
|
+
// mistaken max below one floors at one character, since a budget of zero
|
|
54
|
+
// would print nothing at all in place of a real message. The ellipsis
|
|
55
|
+
// itself costs a character, so a budget of one cannot hold it: at that
|
|
56
|
+
// width the cut is bare rather than an ellipsis standing in for the whole
|
|
57
|
+
// detail.
|
|
58
|
+
const limit = Number.isFinite(max) ? Math.max(1, Math.floor(max)) : ADVISORY_DETAIL_MAX;
|
|
59
|
+
if (flattened.length <= limit) return flattened;
|
|
60
|
+
if (limit < 2) return flattened.slice(0, limit);
|
|
61
|
+
return `${flattened.slice(0, limit - 1).trimEnd()}\u2026`;
|
|
62
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Theme gate: turns the advisory "theme inspect found a generatable brand
|
|
2
|
+
// theme" signal into a deterministic stage gate.
|
|
3
|
+
//
|
|
4
|
+
// Doctrine: when theme inspect proves a brand layer CAN be generated and the
|
|
5
|
+
// campaign ships commerce pages, polish/deploy/QA must not proceed until the
|
|
6
|
+
// brand layer is generated and applied after next-core.css — or an operator
|
|
7
|
+
// records an explicit waiver with a reason. Advisory prose is exactly what
|
|
8
|
+
// agents skipped in the recovery-relief-stack-v1 dogfood run; this module is
|
|
9
|
+
// the single decision point every consumer (next, doctor, qa) shares.
|
|
10
|
+
const COMMERCE_PAGE_TYPES = new Set(["checkout", "upsell", "downsell", "receipt", "thankyou"]);
|
|
11
|
+
|
|
12
|
+
function isCommercePage(page) {
|
|
13
|
+
return COMMERCE_PAGE_TYPES.has(String(page?.type || "")) || String(page?.role || "") === "runtime";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function pageKey(page) {
|
|
17
|
+
return String(page?.page_id || page?.route || page?.type || "");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The campaign's commerce surface, split by whether this build produced it.
|
|
22
|
+
*
|
|
23
|
+
* "Does this campaign ship commerce pages?" is a question about the campaign,
|
|
24
|
+
* not about one build's output. A partial build parks declared checkout/upsell
|
|
25
|
+
* pages in `out_of_scope_pages`, which used to leave `built_pages` with no
|
|
26
|
+
* commerce page and retire the gate on a nine-page commerce funnel (#274).
|
|
27
|
+
* Reading only the built half made the gate's answer depend on build scope,
|
|
28
|
+
* which is the one input it must not key off.
|
|
29
|
+
*/
|
|
30
|
+
export function commerceScopeFromScope(scope) {
|
|
31
|
+
const built = (Array.isArray(scope?.built_pages) ? scope.built_pages : []).filter(isCommercePage);
|
|
32
|
+
const builtKeys = new Set(built.map(pageKey));
|
|
33
|
+
const outOfScope = (Array.isArray(scope?.out_of_scope_pages) ? scope.out_of_scope_pages : [])
|
|
34
|
+
.filter(isCommercePage)
|
|
35
|
+
.filter((page) => !builtKeys.has(pageKey(page)));
|
|
36
|
+
return { built, out_of_scope: outOfScope, all: [...built, ...outOfScope] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// An expiry `theme waive --expires-at` recorded is honoured here: at or after
|
|
40
|
+
// that instant the record no longer waives the gate (inclusive boundary, the
|
|
41
|
+
// same rule the checkpoint and polish lanes apply), so the gate falls back to
|
|
42
|
+
// its normal evaluation and asks for a fresh decision.
|
|
43
|
+
export function themeWaiverExpired(waiver, now = new Date().toISOString()) {
|
|
44
|
+
const expiresAt = waiver?.expires_at;
|
|
45
|
+
if (typeof expiresAt !== "string" || !expiresAt.trim()) return false;
|
|
46
|
+
const expiresMs = Date.parse(expiresAt);
|
|
47
|
+
return !Number.isFinite(expiresMs) || expiresMs <= Date.parse(now);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function themeWaiverFrom(reportTheme, ephemeralWaiver = null, { now = new Date().toISOString() } = {}) {
|
|
51
|
+
if (typeof ephemeralWaiver === "string" && ephemeralWaiver.trim()) {
|
|
52
|
+
return { reason: ephemeralWaiver.trim(), waived_by: "cli_flag", waived_at: null };
|
|
53
|
+
}
|
|
54
|
+
const waiver = reportTheme?.waiver;
|
|
55
|
+
if (waiver && typeof waiver === "object" && typeof waiver.reason === "string" && waiver.reason.trim()) {
|
|
56
|
+
return themeWaiverExpired(waiver, now) ? null : waiver;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Evaluate the theme gate for a campaign.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} options
|
|
65
|
+
* @param {object|null} options.reportTheme Assembly report theme block.
|
|
66
|
+
* @param {object|null} options.contextTheme Build-context theme block (theme inspect result).
|
|
67
|
+
* @param {object|null} options.scope Doctor derived scope ({ built_pages: [...] }).
|
|
68
|
+
* @param {string|null} options.packetPath Build packet path, used to render exact commands.
|
|
69
|
+
* @param {string|null} options.waive Ephemeral waiver reason from a CLI flag (--theme-waive).
|
|
70
|
+
* @param {string} options.now Evaluation instant (ISO); a recorded waiver expired by then is ignored.
|
|
71
|
+
*
|
|
72
|
+
* @returns {{
|
|
73
|
+
* status: "pass"|"blocked"|"waived"|"not_applicable",
|
|
74
|
+
* code: string,
|
|
75
|
+
* reason: string,
|
|
76
|
+
* commerce_pages: string[],
|
|
77
|
+
* commerce_pages_out_of_scope: string[],
|
|
78
|
+
* waiver: object|null,
|
|
79
|
+
* required_actions: Array<{ id: string, kind: "command"|"manual", command: string|null, description: string }>,
|
|
80
|
+
* }}
|
|
81
|
+
*/
|
|
82
|
+
export function evaluateThemeGate({ reportTheme = null, contextTheme = null, scope = null, packetPath = null, waive = null, now = new Date().toISOString() } = {}) {
|
|
83
|
+
const packetArg = packetPath || "<campaign-runtime.build.json>";
|
|
84
|
+
const commerceScope = commerceScopeFromScope(scope);
|
|
85
|
+
const pageLabel = (page) => page.page_id || page.route || page.type;
|
|
86
|
+
const commercePages = commerceScope.all.map(pageLabel);
|
|
87
|
+
const commercePagesOutOfScope = commerceScope.out_of_scope.map(pageLabel);
|
|
88
|
+
const result = (status, code, reason, requiredActions = [], waiver = null) => ({
|
|
89
|
+
status,
|
|
90
|
+
code,
|
|
91
|
+
reason,
|
|
92
|
+
commerce_pages: commercePages,
|
|
93
|
+
commerce_pages_out_of_scope: commercePagesOutOfScope,
|
|
94
|
+
waiver,
|
|
95
|
+
required_actions: requiredActions,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const policy = contextTheme?.policy || null;
|
|
99
|
+
if (policy === "off") {
|
|
100
|
+
return result("not_applicable", "theme_gate.policy_off", "Theme policy is off; brand layer is explicitly out of scope for this run.");
|
|
101
|
+
}
|
|
102
|
+
if (!commercePages.length) {
|
|
103
|
+
return result("not_applicable", "theme_gate.no_commerce_pages", "Campaign ships no commerce pages; the brand-layer gate does not apply.");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const waiver = themeWaiverFrom(reportTheme, waive, { now });
|
|
107
|
+
if (waiver) {
|
|
108
|
+
return result("waived", "theme_gate.waived", `Theme gate waived: ${waiver.reason}`, [], waiver);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const reportStatus = String(reportTheme?.status || "");
|
|
112
|
+
const loadOrder = String(reportTheme?.load_order || "");
|
|
113
|
+
const canGenerate = contextTheme?.generated?.can_generate === true;
|
|
114
|
+
|
|
115
|
+
if (reportStatus === "applied") {
|
|
116
|
+
if (loadOrder !== "after-next-core") {
|
|
117
|
+
return result(
|
|
118
|
+
"blocked",
|
|
119
|
+
"theme_gate.load_order",
|
|
120
|
+
`Brand theme is recorded as applied but load_order is "${loadOrder || "unknown"}"; commerce pages must load brand-theme.css after next-core.css.`,
|
|
121
|
+
[
|
|
122
|
+
{
|
|
123
|
+
id: "fix_load_order",
|
|
124
|
+
kind: "manual",
|
|
125
|
+
command: null,
|
|
126
|
+
description: "List brand-theme.css after next-core.css in commerce-page frontmatter styles, rebuild, then record report.theme.load_order=after-next-core.",
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return result("pass", "theme_gate.applied", "Brand theme is applied after next-core.css on commerce pages.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (reportStatus === "skipped" && !canGenerate) {
|
|
135
|
+
return result("pass", "theme_gate.nothing_generatable", "No generatable brand theme was found; the gate passes without a brand layer.");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!canGenerate && !reportStatus) {
|
|
139
|
+
return result("not_applicable", "theme_gate.no_theme_context", "No theme context exists for this campaign; run prepare-build/theme inspect to populate it.");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!canGenerate) {
|
|
143
|
+
// needs_review/blocked without a generatable artifact: the operator must
|
|
144
|
+
// decide. Treat as blocked so the decision is recorded, not improvised.
|
|
145
|
+
return result(
|
|
146
|
+
"blocked",
|
|
147
|
+
"theme_gate.needs_decision",
|
|
148
|
+
`Assembly report theme status is "${reportStatus || "missing"}" and no generatable brand theme exists; record an applied brand layer or waive the gate with a reason.`,
|
|
149
|
+
[
|
|
150
|
+
{
|
|
151
|
+
id: "waive_theme",
|
|
152
|
+
kind: "command",
|
|
153
|
+
command: `campaigns-os theme waive --packet ${packetArg} --reason "<why the starter palette is acceptable>" --waived-by "<named human>"`,
|
|
154
|
+
description: "Record an explicit operator waiver if shipping without a campaign brand layer is intentional.",
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return result(
|
|
161
|
+
"blocked",
|
|
162
|
+
"theme_gate.generatable_not_applied",
|
|
163
|
+
"theme inspect proved a brand theme can be generated, but it has not been generated and applied to commerce pages. Starter-template commerce families consume --brand--* tokens; without the brand layer they ship the starter palette.",
|
|
164
|
+
[
|
|
165
|
+
{
|
|
166
|
+
id: "theme_generate",
|
|
167
|
+
kind: "command",
|
|
168
|
+
command: `campaigns-os theme generate --packet ${packetArg}`,
|
|
169
|
+
description: "Generate .campaign-runtime/theme/brand-theme.css from the inspected source tokens.",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "apply_brand_layer",
|
|
173
|
+
kind: "manual",
|
|
174
|
+
command: null,
|
|
175
|
+
description: "Copy brand-theme.css into the campaign assets/css folder, list it after next-core.css in checkout/upsell/downsell/receipt frontmatter styles, rebuild, then record report.theme.status=applied, load_order=after-next-core, and commerce_pages.",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: "waive_theme",
|
|
179
|
+
kind: "command",
|
|
180
|
+
command: `campaigns-os theme waive --packet ${packetArg} --reason "<why the starter palette is acceptable>" --waived-by "<named human>"`,
|
|
181
|
+
description: "Or record an explicit operator waiver instead of applying the brand layer.",
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
);
|
|
185
|
+
}
|