@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,369 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assessCheckpointWaivers,
|
|
3
|
+
checkpointStateFingerprint,
|
|
4
|
+
projectCheckpointWaiverAssessment,
|
|
5
|
+
} from "./checkpoint-waiver.mjs";
|
|
6
|
+
import { PAGE_KIT_CAMPAIGNS_REL_PATH } from "./page-kit-campaign-config.mjs";
|
|
7
|
+
import { normalizePublicRouteSlug } from "./route-identity.mjs";
|
|
8
|
+
|
|
9
|
+
// The canonical bare spelling of the reconcile command both page-kit gates
|
|
10
|
+
// name as their target repair (implemented in page-kit-sync.mjs, which imports
|
|
11
|
+
// this module and so cannot be imported here). Printers spell it for the
|
|
12
|
+
// running install (gate-actions.mjs).
|
|
13
|
+
export const PAGE_KIT_SYNC_COMMAND = "campaigns-os page-kit sync --packet <packet>";
|
|
14
|
+
|
|
15
|
+
export const PAGE_KIT_STORE_PROFILE_SCOPE = "page_kit.store_profile";
|
|
16
|
+
export const PAGE_KIT_STORE_PROFILE_FIELDS = Object.freeze([
|
|
17
|
+
"store_name",
|
|
18
|
+
"store_url",
|
|
19
|
+
"store_terms",
|
|
20
|
+
"store_privacy",
|
|
21
|
+
"store_contact",
|
|
22
|
+
"store_returns",
|
|
23
|
+
"store_shipping",
|
|
24
|
+
"store_phone",
|
|
25
|
+
"store_phone_tel",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const URL_FIELDS = new Set([
|
|
29
|
+
"store_url",
|
|
30
|
+
"store_terms",
|
|
31
|
+
"store_privacy",
|
|
32
|
+
"store_contact",
|
|
33
|
+
"store_returns",
|
|
34
|
+
"store_shipping",
|
|
35
|
+
]);
|
|
36
|
+
const PHONE_FIELDS = new Set(["store_phone", "store_phone_tel"]);
|
|
37
|
+
const MISSING_TARGET_STATUSES = new Set(["target_repo_missing", "file_missing", "entry_missing"]);
|
|
38
|
+
|
|
39
|
+
// The comparison form of a Store Profile string: NFC-normalized and trimmed.
|
|
40
|
+
// `page-kit sync` writes exactly this form, so what it writes is what the
|
|
41
|
+
// matrix below reads back as a match.
|
|
42
|
+
export function normalizeStoreProfileValue(value) {
|
|
43
|
+
return value.normalize("NFC").trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeFieldValue(value) {
|
|
47
|
+
if (value == null) return { valid: true, value: "" };
|
|
48
|
+
if (typeof value !== "string") {
|
|
49
|
+
const type = Array.isArray(value) ? "array" : typeof value;
|
|
50
|
+
return { valid: false, value: `[invalid:${type}]` };
|
|
51
|
+
}
|
|
52
|
+
return { valid: true, value: normalizeStoreProfileValue(value) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isDemoResidue(field, value) {
|
|
56
|
+
if (!value) return false;
|
|
57
|
+
if (URL_FIELDS.has(field)) {
|
|
58
|
+
try {
|
|
59
|
+
return new URL(value).hostname.toLocaleLowerCase("en-US") === "demo.29next.com";
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return PHONE_FIELDS.has(field) && value.replace(/\D/g, "") === "18888316810";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A waiver records a named human accepting one of the ratified known
|
|
68
|
+
// divergences. Keep this as a positive allowlist: any future blocker kind is
|
|
69
|
+
// non-waivable until its policy and reachability proof are explicitly added.
|
|
70
|
+
// Starter demo residue (a demo storefront URL or phone still in the target) is
|
|
71
|
+
// deliberately absent: the gate exists to keep those values from shipping, so
|
|
72
|
+
// no named human can switch it off — the value must be replaced.
|
|
73
|
+
const WAIVABLE_DISCREPANCY_KINDS = new Set([
|
|
74
|
+
"target_missing",
|
|
75
|
+
"mismatch",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
// Blocker kinds `page-kit sync` repairs by writing the spec's value over the
|
|
79
|
+
// target's: the target is wrong, missing, malformed, or still the starter
|
|
80
|
+
// demo value. A spec-side defect (spec_invalid_type, both_invalid_type) is
|
|
81
|
+
// repaired in the spec, not the target, so sync is not the recovery there;
|
|
82
|
+
// neither is demo residue in a field the spec does not carry, because sync
|
|
83
|
+
// has nothing to write over it (see syncRepairsRow).
|
|
84
|
+
const SYNC_REPAIRABLE_KINDS = new Set([
|
|
85
|
+
"demo_residue",
|
|
86
|
+
"target_missing",
|
|
87
|
+
"mismatch",
|
|
88
|
+
"target_invalid_type",
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
// The tel: shape the campaign-spec StoreProfileShape rule accepts (optional
|
|
92
|
+
// +, then digits / space / dash / parens / dot); anything else, including an
|
|
93
|
+
// alternative scheme, is refused as a value for an <a href>.
|
|
94
|
+
const TEL_URI_REGEX = /^tel:\+?[0-9 .()\-]{4,}$/i;
|
|
95
|
+
|
|
96
|
+
// Why a spec value cannot be written into the target as it stands, or null
|
|
97
|
+
// when it can. `page-kit sync` refuses these and the gate does not name sync
|
|
98
|
+
// as the repair for them: a URL field must be an http(s) URL and the tel
|
|
99
|
+
// field a tel: URI, because templates put both into href attributes where
|
|
100
|
+
// escaping does not neutralize an executable scheme; no field may carry
|
|
101
|
+
// control characters; and the starter demo value is never a repair.
|
|
102
|
+
export function storeProfileSpecValueProblem(field, value) {
|
|
103
|
+
if (typeof value !== "string" || !value.trim()) return "missing";
|
|
104
|
+
const normalized = normalizeStoreProfileValue(value);
|
|
105
|
+
// eslint-disable-next-line no-control-regex
|
|
106
|
+
if (/[\u0000-\u001f\u007f]/.test(normalized)) return "control_characters";
|
|
107
|
+
if (isDemoResidue(field, normalized)) return "demo_residue";
|
|
108
|
+
if (URL_FIELDS.has(field)) {
|
|
109
|
+
try {
|
|
110
|
+
const url = new URL(normalized);
|
|
111
|
+
// store_contact is the one field templates render as a contact link,
|
|
112
|
+
// where a mailto: address is an ordinary value.
|
|
113
|
+
const allowed = url.protocol === "https:" || url.protocol === "http:" || (field === "store_contact" && url.protocol === "mailto:");
|
|
114
|
+
if (!allowed) return "not_http_url";
|
|
115
|
+
} catch {
|
|
116
|
+
return "not_http_url";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (field === "store_phone_tel" && !TEL_URI_REGEX.test(normalized)) return "not_tel_uri";
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The edit a blocked row needs when sync cannot make it: a target-side
|
|
124
|
+
// defect with no spec value behind it (a demo or malformed target value in a
|
|
125
|
+
// field the spec does not carry) is corrected in the target, or the field is
|
|
126
|
+
// added to the spec and synced; a present-but-unusable spec value is
|
|
127
|
+
// corrected in the spec.
|
|
128
|
+
function repairDescriptionForUnsyncableRows(rows, subject) {
|
|
129
|
+
const where = `${subject.target_path}[${subject.public_route_slug}]`;
|
|
130
|
+
const targetSide = rows.filter((row) => storeProfileSpecValueProblem(row.field, row.spec) === "missing");
|
|
131
|
+
const specSide = rows.filter((row) => storeProfileSpecValueProblem(row.field, row.spec) !== "missing");
|
|
132
|
+
const parts = [];
|
|
133
|
+
if (targetSide.length) {
|
|
134
|
+
parts.push(`Remove or correct ${where}.${targetSide.map((row) => row.field).join(", ")} (the CampaignSpec does not carry ${targetSide.length === 1 ? "this field" : "these fields"}, so page-kit sync has nothing to write over the target), or add campaign.${targetSide.map((row) => row.field).join(", campaign.")} to the spec and run page-kit sync.`);
|
|
135
|
+
}
|
|
136
|
+
if (specSide.length) {
|
|
137
|
+
parts.push(`Repair the CampaignSpec Store Profile field(s) ${specSide.map((row) => `${row.field} (${storeProfileSpecValueProblem(row.field, row.spec)})`).join(", ")}: page-kit sync writes only a well-shaped, non-demo spec value over the target. Fix the spec, then run page-kit sync.`);
|
|
138
|
+
}
|
|
139
|
+
return `${parts.join(" ")} Then re-run doctor.`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Sync repairs a row by writing the spec's value over the target's, so it
|
|
143
|
+
// needs a usable spec value: present, well-shaped for its field, and not the
|
|
144
|
+
// starter demo value itself. matrixRow puts demo_residue ahead of the spec
|
|
145
|
+
// comparison, so a residue row may have an empty (or itself demo) spec value.
|
|
146
|
+
function syncRepairsRow(row) {
|
|
147
|
+
return SYNC_REPAIRABLE_KINDS.has(row.kind) && storeProfileSpecValueProblem(row.field, row.spec) === null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function storeProfileDemoResidueFields(gate) {
|
|
151
|
+
const discrepancies = Array.isArray(gate?.state?.discrepancies) ? gate.state.discrepancies : [];
|
|
152
|
+
return discrepancies.filter((row) => row?.kind === "demo_residue").map((row) => row.field);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function isStoreProfileDiscrepancyWaivable(kind) {
|
|
156
|
+
return typeof kind === "string" && WAIVABLE_DISCREPANCY_KINDS.has(kind);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function matrixRow(field, rawSpec, rawTarget) {
|
|
160
|
+
const spec = normalizeFieldValue(rawSpec);
|
|
161
|
+
const target = normalizeFieldValue(rawTarget);
|
|
162
|
+
if (!spec.valid && !target.valid) {
|
|
163
|
+
return { field, kind: "both_invalid_type", spec: spec.value, target: target.value, severity: "blocker" };
|
|
164
|
+
}
|
|
165
|
+
if (!spec.valid) {
|
|
166
|
+
return { field, kind: "spec_invalid_type", spec: spec.value, target: target.value, severity: "blocker" };
|
|
167
|
+
}
|
|
168
|
+
if (!target.valid) {
|
|
169
|
+
return { field, kind: "target_invalid_type", spec: spec.value, target: target.value, severity: "blocker" };
|
|
170
|
+
}
|
|
171
|
+
if (isDemoResidue(field, target.value)) {
|
|
172
|
+
return { field, kind: "demo_residue", spec: spec.value, target: target.value, severity: "blocker" };
|
|
173
|
+
}
|
|
174
|
+
if (spec.value && !target.value) {
|
|
175
|
+
return { field, kind: "target_missing", spec: spec.value, target: target.value, severity: "blocker" };
|
|
176
|
+
}
|
|
177
|
+
if (spec.value && target.value && spec.value !== target.value) {
|
|
178
|
+
return { field, kind: "mismatch", spec: spec.value, target: target.value, severity: "blocker" };
|
|
179
|
+
}
|
|
180
|
+
if (!spec.value && target.value) {
|
|
181
|
+
return { field, kind: "target_only", spec: spec.value, target: target.value, severity: "warning" };
|
|
182
|
+
}
|
|
183
|
+
if (spec.value && target.value) {
|
|
184
|
+
return { field, kind: "match", spec: spec.value, target: target.value, severity: "clean" };
|
|
185
|
+
}
|
|
186
|
+
return { field, kind: "both_empty", spec: spec.value, target: target.value, severity: "clean" };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function unavailableMatrix(specCampaign, required) {
|
|
190
|
+
return PAGE_KIT_STORE_PROFILE_FIELDS.map((field) => {
|
|
191
|
+
const spec = normalizeFieldValue(specCampaign?.[field]);
|
|
192
|
+
return {
|
|
193
|
+
field,
|
|
194
|
+
kind: "target_unavailable",
|
|
195
|
+
spec: spec.value,
|
|
196
|
+
target: null,
|
|
197
|
+
severity: required ? "blocker" : "info",
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function emptyWaiverAssessment() {
|
|
203
|
+
return {
|
|
204
|
+
active: null,
|
|
205
|
+
inert_counts: { stale: 0, foreign: 0, malformed: 0, expired: 0 },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function evaluatePageKitStoreProfile({
|
|
210
|
+
specCampaign,
|
|
211
|
+
specStatus = "ok",
|
|
212
|
+
targetLoad,
|
|
213
|
+
waivers = [],
|
|
214
|
+
required = true,
|
|
215
|
+
now = new Date().toISOString(),
|
|
216
|
+
} = {}) {
|
|
217
|
+
const subject = {
|
|
218
|
+
public_route_slug: normalizePublicRouteSlug(targetLoad?.public_route_slug),
|
|
219
|
+
target_path: targetLoad?.target_path || PAGE_KIT_CAMPAIGNS_REL_PATH,
|
|
220
|
+
};
|
|
221
|
+
const loadStatus = targetLoad?.status || "target_repo_missing";
|
|
222
|
+
|
|
223
|
+
if (specStatus !== "ok") {
|
|
224
|
+
return {
|
|
225
|
+
id: PAGE_KIT_STORE_PROFILE_SCOPE,
|
|
226
|
+
scope: PAGE_KIT_STORE_PROFILE_SCOPE,
|
|
227
|
+
status: "blocked",
|
|
228
|
+
code: "page_kit.store_profile.spec_unavailable",
|
|
229
|
+
reason: `Local CampaignSpec Store Profile is unavailable or malformed (${specStatus}); packet QA never fetches around missing local parity evidence.`,
|
|
230
|
+
waivable: false,
|
|
231
|
+
subject,
|
|
232
|
+
state: { spec_status: specStatus, discrepancies: [] },
|
|
233
|
+
state_fingerprint: null,
|
|
234
|
+
matrix: PAGE_KIT_STORE_PROFILE_FIELDS.map((field) => ({
|
|
235
|
+
field,
|
|
236
|
+
kind: "spec_unavailable",
|
|
237
|
+
spec: null,
|
|
238
|
+
target: loadStatus === "ok" ? normalizeFieldValue(targetLoad.entry?.[field]).value : null,
|
|
239
|
+
severity: "blocker",
|
|
240
|
+
})),
|
|
241
|
+
blocker_fields: [...PAGE_KIT_STORE_PROFILE_FIELDS],
|
|
242
|
+
warning_fields: [],
|
|
243
|
+
waiver: null,
|
|
244
|
+
waiver_assessment: emptyWaiverAssessment(),
|
|
245
|
+
required_actions: [{
|
|
246
|
+
id: "repair_spec",
|
|
247
|
+
kind: "edit",
|
|
248
|
+
command: null,
|
|
249
|
+
description: "Restore a valid packet-local CampaignSpec export before QA.",
|
|
250
|
+
}],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (loadStatus !== "ok") {
|
|
255
|
+
const optionalMissing = !required && MISSING_TARGET_STATUSES.has(loadStatus);
|
|
256
|
+
return {
|
|
257
|
+
id: PAGE_KIT_STORE_PROFILE_SCOPE,
|
|
258
|
+
scope: PAGE_KIT_STORE_PROFILE_SCOPE,
|
|
259
|
+
status: optionalMissing ? "not_applicable" : "blocked",
|
|
260
|
+
code: optionalMissing ? "page_kit.store_profile.not_applicable" : "page_kit.store_profile.target_unavailable",
|
|
261
|
+
reason: optionalMissing
|
|
262
|
+
? `Target ${subject.target_path} entry is not present before scaffold; Store Profile parity will become required once the target exists.`
|
|
263
|
+
: `Target ${subject.target_path} entry is unavailable or malformed (${loadStatus}); packet build/QA requires an exact Store Profile target.`,
|
|
264
|
+
waivable: false,
|
|
265
|
+
subject,
|
|
266
|
+
state: { target_status: loadStatus, discrepancies: [] },
|
|
267
|
+
state_fingerprint: null,
|
|
268
|
+
matrix: unavailableMatrix(specCampaign, !optionalMissing),
|
|
269
|
+
blocker_fields: optionalMissing ? [] : [...PAGE_KIT_STORE_PROFILE_FIELDS],
|
|
270
|
+
warning_fields: [],
|
|
271
|
+
waiver: null,
|
|
272
|
+
waiver_assessment: emptyWaiverAssessment(),
|
|
273
|
+
required_actions: optionalMissing ? [] : [{
|
|
274
|
+
id: "repair_target",
|
|
275
|
+
kind: "edit",
|
|
276
|
+
command: null,
|
|
277
|
+
description: `Create or repair ${subject.target_path}[${subject.public_route_slug || "<public-route-slug>"}] before build or QA.`,
|
|
278
|
+
}],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const matrix = PAGE_KIT_STORE_PROFILE_FIELDS.map((field) => matrixRow(field, specCampaign?.[field], targetLoad.entry?.[field]));
|
|
283
|
+
const discrepancies = matrix
|
|
284
|
+
.filter((row) => row.severity === "blocker" || row.severity === "warning")
|
|
285
|
+
.map(({ field, kind, spec, target }) => ({ field, kind, spec, target }));
|
|
286
|
+
const state = { discrepancies };
|
|
287
|
+
const state_fingerprint = checkpointStateFingerprint({
|
|
288
|
+
scope: PAGE_KIT_STORE_PROFILE_SCOPE,
|
|
289
|
+
subject,
|
|
290
|
+
state,
|
|
291
|
+
});
|
|
292
|
+
const blocker_fields = matrix.filter((row) => row.severity === "blocker").map((row) => row.field);
|
|
293
|
+
const warning_fields = matrix.filter((row) => row.severity === "warning").map((row) => row.field);
|
|
294
|
+
const blockerRows = matrix.filter((row) => row.severity === "blocker");
|
|
295
|
+
const waivable = blockerRows.length > 0
|
|
296
|
+
&& blockerRows.every((row) => isStoreProfileDiscrepancyWaivable(row.kind));
|
|
297
|
+
// Waiver history matters only while this exact checkpoint is blocked. Once
|
|
298
|
+
// correction removes every blocker (including a target-only warning state),
|
|
299
|
+
// historical decisions stay in the report but must not become stale-warning
|
|
300
|
+
// evidence on an otherwise clean current gate.
|
|
301
|
+
const checkpoint = {
|
|
302
|
+
scope: PAGE_KIT_STORE_PROFILE_SCOPE,
|
|
303
|
+
subject,
|
|
304
|
+
state_fingerprint,
|
|
305
|
+
};
|
|
306
|
+
const waiver_assessment = blocker_fields.length
|
|
307
|
+
? projectCheckpointWaiverAssessment(
|
|
308
|
+
assessCheckpointWaivers(waivers, checkpoint, { now }),
|
|
309
|
+
checkpoint,
|
|
310
|
+
)
|
|
311
|
+
: emptyWaiverAssessment();
|
|
312
|
+
const waiver = blocker_fields.length && waivable ? waiver_assessment.active : null;
|
|
313
|
+
const status = blocker_fields.length ? (waiver ? "waived" : "blocked") : "pass";
|
|
314
|
+
const code = status === "waived"
|
|
315
|
+
? "page_kit.store_profile.waived"
|
|
316
|
+
: status === "blocked"
|
|
317
|
+
? PAGE_KIT_STORE_PROFILE_SCOPE
|
|
318
|
+
: warning_fields.length
|
|
319
|
+
? "page_kit.store_profile.target_only"
|
|
320
|
+
: "page_kit.store_profile.pass";
|
|
321
|
+
const demoResidueFields = blockerRows.filter((row) => row.kind === "demo_residue").map((row) => row.field);
|
|
322
|
+
const reason = status === "blocked"
|
|
323
|
+
? `Target Store Profile differs from the CampaignSpec in blocking field(s): ${blocker_fields.join(", ")}.`
|
|
324
|
+
+ (demoResidueFields.length
|
|
325
|
+
? ` Starter demo residue in ${demoResidueFields.join(", ")} is not waivable; replace the demo value(s).`
|
|
326
|
+
: "")
|
|
327
|
+
: status === "waived"
|
|
328
|
+
? `Target Store Profile has an active named-human waiver for blocking field(s): ${blocker_fields.join(", ")}.`
|
|
329
|
+
: warning_fields.length
|
|
330
|
+
? `Target Store Profile has target-only value(s): ${warning_fields.join(", ")}.`
|
|
331
|
+
: "Target Store Profile exactly matches the CampaignSpec for all nine governed fields.";
|
|
332
|
+
return {
|
|
333
|
+
id: PAGE_KIT_STORE_PROFILE_SCOPE,
|
|
334
|
+
scope: PAGE_KIT_STORE_PROFILE_SCOPE,
|
|
335
|
+
status,
|
|
336
|
+
code,
|
|
337
|
+
reason,
|
|
338
|
+
waivable,
|
|
339
|
+
subject,
|
|
340
|
+
state,
|
|
341
|
+
state_fingerprint,
|
|
342
|
+
matrix,
|
|
343
|
+
blocker_fields,
|
|
344
|
+
warning_fields,
|
|
345
|
+
waiver,
|
|
346
|
+
waiver_assessment,
|
|
347
|
+
required_actions: status === "blocked" ? [
|
|
348
|
+
blockerRows.every(syncRepairsRow)
|
|
349
|
+
? {
|
|
350
|
+
id: "repair_target",
|
|
351
|
+
kind: "command",
|
|
352
|
+
command: PAGE_KIT_SYNC_COMMAND,
|
|
353
|
+
description: `Write the CampaignSpec Store Profile values into ${subject.target_path}[${subject.public_route_slug}] (${blocker_fields.join(", ")}), then re-run doctor.`,
|
|
354
|
+
}
|
|
355
|
+
: {
|
|
356
|
+
id: "repair_target",
|
|
357
|
+
kind: "edit",
|
|
358
|
+
command: null,
|
|
359
|
+
description: repairDescriptionForUnsyncableRows(blockerRows.filter((row) => !syncRepairsRow(row)), subject),
|
|
360
|
+
},
|
|
361
|
+
...(waivable ? [{
|
|
362
|
+
id: "waive_checkpoint",
|
|
363
|
+
kind: "command",
|
|
364
|
+
command: "campaigns-os checkpoint waive --packet <packet> --gate page_kit.store_profile --reason \"<reason>\" --waived-by \"<named human>\" --review-condition \"<re-evaluation trigger>\"",
|
|
365
|
+
description: "Record an explicit bounded named-human I-16 exception for this exact state when correction is intentionally deferred.",
|
|
366
|
+
}] : []),
|
|
367
|
+
] : [],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// `campaigns-os page-kit sync`: reconcile the target's `_data/campaigns.json`
|
|
2
|
+
// entry from the CampaignSpec.
|
|
3
|
+
//
|
|
4
|
+
// A fresh page-kit scaffold (`campaign-init`) seeds the route's entry with the
|
|
5
|
+
// starter family's demo store profile and the family's SDK pin, so doctor
|
|
6
|
+
// blocks on `page_kit.store_profile` (demo residue, unwaivable by design) and
|
|
7
|
+
// `page_kit.sdk_version` on every first run. The spec already holds every
|
|
8
|
+
// value those gates compare against and the gate knows the exact mismatch, so
|
|
9
|
+
// the repair is a command rather than a hand edit.
|
|
10
|
+
//
|
|
11
|
+
// The CampaignSpec is the authority. Exactly the fields the two gates govern
|
|
12
|
+
// are written — the nine Store Profile fields and `sdk_version` — and only
|
|
13
|
+
// those the spec carries. Everything else in the entry, every other entry, and
|
|
14
|
+
// every other file are left as they are. This module is pure: the CLI does
|
|
15
|
+
// the reading, the writing, and the printing.
|
|
16
|
+
import {
|
|
17
|
+
isDemoResidue,
|
|
18
|
+
normalizeStoreProfileValue,
|
|
19
|
+
PAGE_KIT_STORE_PROFILE_FIELDS,
|
|
20
|
+
storeProfileSpecValueProblem,
|
|
21
|
+
} from "./page-kit-store-profile.mjs";
|
|
22
|
+
import { resolveSpecSdkPin, sdkPinWriteDecision } from "./page-kit-sdk-version.mjs";
|
|
23
|
+
|
|
24
|
+
export const PAGE_KIT_SYNC_FIELDS = Object.freeze([...PAGE_KIT_STORE_PROFILE_FIELDS, "sdk_version"]);
|
|
25
|
+
|
|
26
|
+
// The field-by-field plan: what the entry holds, what the spec says, and
|
|
27
|
+
// whether a write is owed. `changes` are the fields whose value will move,
|
|
28
|
+
// `unchanged` already match, `not_in_spec` are governed fields the spec does
|
|
29
|
+
// not carry (left as they are; doctor's `target_only` warning still applies),
|
|
30
|
+
// and `not_synced` are fields the target cannot be made authoritative for: an
|
|
31
|
+
// invalid or conflicting spec SDK pin, a spec value of the wrong type or
|
|
32
|
+
// shape (or the demo value itself), or starter demo residue in a field the
|
|
33
|
+
// spec does not carry. Absent, null and blank spec values are "not carried".
|
|
34
|
+
export function planPageKitSync({ spec, entry, waivedGates = [] } = {}) {
|
|
35
|
+
const target = entry && typeof entry === "object" && !Array.isArray(entry) ? entry : {};
|
|
36
|
+
const changes = [];
|
|
37
|
+
const unchanged = [];
|
|
38
|
+
const notInSpec = [];
|
|
39
|
+
const notSynced = [];
|
|
40
|
+
// A gate under an ACTIVE named-human waiver recorded a human accepting the
|
|
41
|
+
// target's current values; sync must not silently reverse that decision.
|
|
42
|
+
// Its fields are reported as not synced (naming who waived) and left as
|
|
43
|
+
// they are; the waiver stays in force.
|
|
44
|
+
const waivedBy = new Map(
|
|
45
|
+
(Array.isArray(waivedGates) ? waivedGates : [])
|
|
46
|
+
.filter((gate) => gate && typeof gate === "object" && typeof gate.scope === "string")
|
|
47
|
+
.map((gate) => [gate.scope, gate.waived_by || "a named human"]),
|
|
48
|
+
);
|
|
49
|
+
const storeProfileWaiver = waivedBy.get("page_kit.store_profile") || null;
|
|
50
|
+
const sdkWaiver = waivedBy.get("page_kit.sdk_version") || null;
|
|
51
|
+
const waivedDetail = (field, who, scope) => `${field} is covered by an active ${scope} waiver recorded by ${who}; page-kit sync leaves the target as the waiver accepted it. Withdraw the waiver on the Assembly Report (waivers[]) to let sync write the spec value.`;
|
|
52
|
+
|
|
53
|
+
const NOT_SYNCED_DETAIL = {
|
|
54
|
+
spec_invalid_type: (field, raw) => `CampaignSpec campaign.${field} is ${Array.isArray(raw) ? "an array" : `a ${raw === null ? "null" : typeof raw}`}, not a string; doctor blocks on it as spec_invalid_type. Fix the spec, then sync again.`,
|
|
55
|
+
control_characters: (field) => `CampaignSpec campaign.${field} contains control characters; remove them, then sync again.`,
|
|
56
|
+
demo_residue: (field, raw) => `CampaignSpec campaign.${field} is itself the starter demo value ${JSON.stringify(raw)}; doctor blocks on demo residue without a waiver. Replace it in the spec, then sync again.`,
|
|
57
|
+
not_http_url: (field, raw) => `CampaignSpec campaign.${field} ${JSON.stringify(raw)} is not an http(s) URL${field === "store_contact" ? " or mailto: address" : ""}; templates place it in an href, so no other scheme is written. Fix the spec, then sync again.`,
|
|
58
|
+
not_tel_uri: (field, raw) => `CampaignSpec campaign.${field} ${JSON.stringify(raw)} is not a tel: URI of digits, spaces, dashes, parens and dots (e.g. "tel:+18005551234"). Fix the spec, then sync again.`,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
for (const field of PAGE_KIT_STORE_PROFILE_FIELDS) {
|
|
62
|
+
const raw = spec?.campaign?.[field];
|
|
63
|
+
const current = Object.hasOwn(target, field) ? target[field] : undefined;
|
|
64
|
+
const carried = raw !== undefined && raw !== null && !(typeof raw === "string" && !raw.trim());
|
|
65
|
+
if (!carried) {
|
|
66
|
+
// Starter demo residue in a field the spec does not carry is the one
|
|
67
|
+
// state sync cannot end: doctor blocks on it without a waiver and there
|
|
68
|
+
// is no spec value to write over it. Say so instead of reporting a
|
|
69
|
+
// clean "left as it is".
|
|
70
|
+
if (typeof current === "string" && isDemoResidue(field, normalizeStoreProfileValue(current))) {
|
|
71
|
+
notSynced.push({
|
|
72
|
+
field,
|
|
73
|
+
reason: "demo_residue_not_in_spec",
|
|
74
|
+
detail: `the target still holds the starter demo value ${JSON.stringify(current)} and the CampaignSpec does not carry campaign.${field}; add it to the spec (doctor blocks on demo residue without a waiver), then sync again.`,
|
|
75
|
+
});
|
|
76
|
+
} else {
|
|
77
|
+
notInSpec.push(field);
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// A carried value the target cannot be made authoritative for: the wrong
|
|
82
|
+
// type (doctor's spec_invalid_type), the demo value itself, or a shape a
|
|
83
|
+
// template would put into an href unescaped.
|
|
84
|
+
const problem = typeof raw !== "string" ? "spec_invalid_type" : storeProfileSpecValueProblem(field, raw);
|
|
85
|
+
if (problem) {
|
|
86
|
+
notSynced.push({ field, reason: problem, detail: NOT_SYNCED_DETAIL[problem](field, raw) });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const after = normalizeStoreProfileValue(raw);
|
|
90
|
+
const before = current;
|
|
91
|
+
const row = { field, before, after, source: `campaign.${field}` };
|
|
92
|
+
// The gate compares normalized forms, so a target that differs only in
|
|
93
|
+
// surrounding whitespace or Unicode normalization already passes; a
|
|
94
|
+
// rewrite would report a change doctor never saw.
|
|
95
|
+
const alreadyMatches = before === after
|
|
96
|
+
|| (typeof before === "string" && normalizeStoreProfileValue(before) === after);
|
|
97
|
+
if (alreadyMatches) unchanged.push(row);
|
|
98
|
+
else if (storeProfileWaiver) notSynced.push({ field, reason: "waived", detail: waivedDetail(field, storeProfileWaiver, "page_kit.store_profile") });
|
|
99
|
+
else changes.push(row);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// A conflicting canonical/alias pair or a non-released pin is a spec defect
|
|
103
|
+
// the gate blocks on without a waiver; the target cannot be made
|
|
104
|
+
// authoritative for it, so it is reported as not synced, never written.
|
|
105
|
+
const sdk = resolveSpecSdkPin(spec);
|
|
106
|
+
if (sdk.status === "ok") {
|
|
107
|
+
const before = Object.hasOwn(target, "sdk_version") ? target.sdk_version : undefined;
|
|
108
|
+
const row = { field: "sdk_version", before, after: sdk.value, source: sdk.source };
|
|
109
|
+
if (before === sdk.value) unchanged.push(row);
|
|
110
|
+
else if (sdkWaiver) notSynced.push({ field: "sdk_version", reason: "waived", detail: waivedDetail("sdk_version", sdkWaiver, "page_kit.sdk_version") });
|
|
111
|
+
else if (sdkPinWriteDecision({ expected: sdk.value, observed: before, entry: target }) === "target_newer") {
|
|
112
|
+
// A configured campaign whose repo pin is ahead of the spec: the bump
|
|
113
|
+
// happened in the repo and the Map/spec is stale. Writing the spec's
|
|
114
|
+
// pin would undo it silently.
|
|
115
|
+
notSynced.push({
|
|
116
|
+
field: "sdk_version",
|
|
117
|
+
reason: "target_newer",
|
|
118
|
+
detail: `the target pin ${before} is newer than the CampaignSpec pin ${sdk.value} and the entry is no longer in scaffold state; the repo pin moved and the Map/spec is stale. Doctor treats the repo pin as what ships and reports this as a warning, not a blocker; re-save the Map (or edit the spec) to ${before} to clear it. page-kit sync only seeds the pin after a scaffold and never moves a configured campaign's pin backwards.`,
|
|
119
|
+
});
|
|
120
|
+
} else changes.push(row);
|
|
121
|
+
} else if (sdk.status === "spec_missing") {
|
|
122
|
+
notInSpec.push("sdk_version");
|
|
123
|
+
} else {
|
|
124
|
+
notSynced.push({
|
|
125
|
+
field: "sdk_version",
|
|
126
|
+
reason: sdk.status,
|
|
127
|
+
detail: sdk.status === "spec_conflict"
|
|
128
|
+
? "CampaignSpec global_config.sdk_version and runtime.sdk_version disagree; resolve the spec (global_config is canonical), then sync again."
|
|
129
|
+
: "CampaignSpec SDK pin is not a released MAJOR.MINOR.PATCH version; correct the spec, then sync again.",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { changes, unchanged, not_in_spec: notInSpec, not_synced: notSynced };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Apply a plan to the parsed campaigns.json document. Mutates ONLY the
|
|
137
|
+
// governed fields on the named entry, in place, so the entry's key order and
|
|
138
|
+
// every other key survive; a field the entry lacks is appended. Returns the
|
|
139
|
+
// same document object.
|
|
140
|
+
export function applyPageKitSync(campaigns, publicRouteSlug, plan) {
|
|
141
|
+
if (!campaigns || typeof campaigns !== "object" || Array.isArray(campaigns)) {
|
|
142
|
+
throw new Error("campaigns.json root must be an object keyed by public route slug.");
|
|
143
|
+
}
|
|
144
|
+
const entry = campaigns[publicRouteSlug];
|
|
145
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
146
|
+
throw new Error(`campaigns.json has no object entry for "${publicRouteSlug}".`);
|
|
147
|
+
}
|
|
148
|
+
for (const change of plan.changes) {
|
|
149
|
+
if (!PAGE_KIT_SYNC_FIELDS.includes(change.field)) {
|
|
150
|
+
throw new Error(`Refusing to write "${change.field}": page-kit sync governs only ${PAGE_KIT_SYNC_FIELDS.join(", ")}.`);
|
|
151
|
+
}
|
|
152
|
+
entry[change.field] = change.after;
|
|
153
|
+
}
|
|
154
|
+
return campaigns;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// One line per field for the printed diff. `undefined` (the entry had no such
|
|
158
|
+
// key) prints as (absent); everything else is JSON so a string that merely
|
|
159
|
+
// looks empty is visibly quoted.
|
|
160
|
+
export function formatSyncValue(value) {
|
|
161
|
+
return value === undefined ? "(absent)" : JSON.stringify(value);
|
|
162
|
+
}
|