@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,1297 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { shellToken } from "./shell-token.mjs";
|
|
5
|
+
import {
|
|
6
|
+
compareVersions,
|
|
7
|
+
escapeRegExp,
|
|
8
|
+
extractVersion,
|
|
9
|
+
listFiles as scanFiles,
|
|
10
|
+
normalizeString,
|
|
11
|
+
relPath,
|
|
12
|
+
rootId,
|
|
13
|
+
safeReadDir,
|
|
14
|
+
shouldSkipDir as skipDir,
|
|
15
|
+
unique,
|
|
16
|
+
} from "./repo-scan.mjs";
|
|
17
|
+
import { resolveBuiltSiteScope } from "./built-site-scope.mjs";
|
|
18
|
+
import {
|
|
19
|
+
detectFrameworks,
|
|
20
|
+
discoverCampaignCartAppRoots,
|
|
21
|
+
evaluateVersionPolicy,
|
|
22
|
+
inspectCheckoutFields,
|
|
23
|
+
loadCheckoutFieldContract,
|
|
24
|
+
loadSdkSupportPolicy,
|
|
25
|
+
scanCampaignCartAppRoot,
|
|
26
|
+
} from "./campaign-ecosystem.mjs";
|
|
27
|
+
import { resolveCommerceCatalog } from "./private-template-source.mjs";
|
|
28
|
+
import {
|
|
29
|
+
assessTemplateFreshness,
|
|
30
|
+
defaultSdkSupportPolicy,
|
|
31
|
+
renderTemplateFreshness,
|
|
32
|
+
} from "./template-freshness.mjs";
|
|
33
|
+
|
|
34
|
+
export const STANDARDIZATION_REPORT_SCHEMA = "campaign-standardization-report/v0";
|
|
35
|
+
|
|
36
|
+
const PAGE_KIT_PACKAGE_NAMES = [
|
|
37
|
+
"next-campaign-page-kit",
|
|
38
|
+
"@nextcommerce/campaign-page-kit",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// The Campaign Cart SDK cutoffs come from the SDK support policy contract
|
|
42
|
+
// (bundled contracts/campaign-cart-sdk-support-policy.v0.json, or the
|
|
43
|
+
// --sdk-support-policy override) — the same policy a campaign_cart_app root is
|
|
44
|
+
// judged by. Only the Page Kit dependency cutoff is a scanner constant: the
|
|
45
|
+
// policy contract has no Page Kit field.
|
|
46
|
+
const PREFERRED_PAGE_KIT_MIN = "0.1.1";
|
|
47
|
+
const MAX_SAMPLE_COUNT = 8;
|
|
48
|
+
const SKIP_DIRS = new Set([
|
|
49
|
+
".git",
|
|
50
|
+
".campaign-runtime",
|
|
51
|
+
"_site",
|
|
52
|
+
"node_modules",
|
|
53
|
+
"qa-output",
|
|
54
|
+
"dist",
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const listFiles = (root, options = {}) => scanFiles(root, { ...options, skipDirs: SKIP_DIRS });
|
|
58
|
+
const shouldSkipDir = (name) => skipDir(name, SKIP_DIRS);
|
|
59
|
+
const STRUCTURE_EXTENSIONS = new Set([".html", ".liquid"]);
|
|
60
|
+
const TEXT_EXTENSIONS = new Set([".html", ".liquid", ".js", ".css"]);
|
|
61
|
+
const RAW_BLOCK_PATTERN = /{%-?\s*raw\s*-?%}/g;
|
|
62
|
+
const HARDCODED_ROOT_ASSET_ATTR_PATTERN = /\b(?:src|href)\s*=\s*(["'])\/assets\/[^"']*\1/g;
|
|
63
|
+
const HARDCODED_ROOT_ASSET_URL_PATTERN = /\burl\(\s*(["']?)\/assets\/[^)"']*\1\s*\)/g;
|
|
64
|
+
|
|
65
|
+
export function createStandardizationReport({
|
|
66
|
+
targetRepo,
|
|
67
|
+
slug = null,
|
|
68
|
+
templateFamily = null,
|
|
69
|
+
generatedAt = new Date().toISOString(),
|
|
70
|
+
fieldContract = null,
|
|
71
|
+
sdkSupportPolicy = null,
|
|
72
|
+
} = {}) {
|
|
73
|
+
if (!targetRepo) throw new Error("createStandardizationReport requires targetRepo");
|
|
74
|
+
// One-shot per report run: a catalog-resolution failure warns once, however
|
|
75
|
+
// many Page Kit roots the target repository contains.
|
|
76
|
+
resetFreshnessSuppressionWarning();
|
|
77
|
+
const target = resolve(targetRepo);
|
|
78
|
+
const pageKitRootPaths = discoverPageKitRoots(target);
|
|
79
|
+
const roots = pageKitRootPaths.map((rootPath) => scanPageKitRoot({
|
|
80
|
+
targetRepo: target,
|
|
81
|
+
rootPath,
|
|
82
|
+
requestedSlug: normalizeString(slug),
|
|
83
|
+
explicitTemplateFamily: normalizeString(templateFamily),
|
|
84
|
+
fieldContract,
|
|
85
|
+
sdkSupportPolicy,
|
|
86
|
+
}));
|
|
87
|
+
const appRoots = discoverCampaignCartAppRoots(target, { excludeRoots: pageKitRootPaths });
|
|
88
|
+
for (const discovered of appRoots) {
|
|
89
|
+
const root = scanCampaignCartAppRoot({
|
|
90
|
+
targetRepo: target,
|
|
91
|
+
rootPath: discovered.rootPath,
|
|
92
|
+
evidence: discovered.evidence,
|
|
93
|
+
fieldContract,
|
|
94
|
+
sdkSupportPolicy,
|
|
95
|
+
excludeRoots: [
|
|
96
|
+
...pageKitRootPaths,
|
|
97
|
+
...appRoots.map((entry) => entry.rootPath),
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
finalizeRoot(root);
|
|
101
|
+
roots.push(root);
|
|
102
|
+
}
|
|
103
|
+
const report = {
|
|
104
|
+
schema_version: STANDARDIZATION_REPORT_SCHEMA,
|
|
105
|
+
generated_at: generatedAt,
|
|
106
|
+
target_repo: target,
|
|
107
|
+
status: "unknown",
|
|
108
|
+
ok: false,
|
|
109
|
+
summary: null,
|
|
110
|
+
roots,
|
|
111
|
+
errors: [],
|
|
112
|
+
recommendation: {
|
|
113
|
+
home: "staged_split",
|
|
114
|
+
summary: "Keep the read-only source/runtime scanner in public campaigns-os first; layer private repo discovery, issue creation, and merchant ops context in an internal campaign-ops wrapper.",
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (!existsSync(target) || !statSync(target).isDirectory()) {
|
|
119
|
+
report.errors.push({
|
|
120
|
+
code: "target.not_found",
|
|
121
|
+
message: `Target repo does not exist or is not a directory: ${target}`,
|
|
122
|
+
});
|
|
123
|
+
} else if (roots.length === 0) {
|
|
124
|
+
report.errors.push({
|
|
125
|
+
code: "campaign.root_not_found",
|
|
126
|
+
message: "No campaign root was detected. Expected a Page Kit root (package.json with next-campaign-page-kit or _data/campaigns.json) or portable Campaign Cart evidence (loader script, next-campaign-id meta, window.nextConfig, or data-next-* anchors).",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
finalizeReport(report);
|
|
131
|
+
return report;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function attachBuiltOutputDoctor(report, rootId, doctorResult) {
|
|
135
|
+
const root = (report?.roots || []).find((entry) => entry.id === rootId);
|
|
136
|
+
if (!root?.built_output) return report;
|
|
137
|
+
root.built_output.doctor = summarizeDoctorResult(doctorResult);
|
|
138
|
+
// capabilities[] lists the inspections that ran: the doctor is on it only
|
|
139
|
+
// once a result is attached, never as a standing promise.
|
|
140
|
+
if (Array.isArray(root.capabilities) && !root.capabilities.includes("built_output_doctor")) {
|
|
141
|
+
root.capabilities.push("built_output_doctor");
|
|
142
|
+
}
|
|
143
|
+
if (doctorResult?.errors?.length) {
|
|
144
|
+
for (const [index, issue] of doctorResult.errors.entries()) {
|
|
145
|
+
root.findings.push(finding({
|
|
146
|
+
severity: "blocker",
|
|
147
|
+
category: "standardization_blocker",
|
|
148
|
+
code: builtDoctorFindingCode(issue, "error", index),
|
|
149
|
+
message: issue.message || String(issue),
|
|
150
|
+
evidence: issue.detail || null,
|
|
151
|
+
next_action: "Resolve built-output doctor blockers, then rerun the standardization report.",
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (doctorResult?.warnings?.length) {
|
|
156
|
+
for (const [index, issue] of doctorResult.warnings.entries()) {
|
|
157
|
+
root.findings.push(finding({
|
|
158
|
+
severity: "warning",
|
|
159
|
+
category: "standardization_warning",
|
|
160
|
+
code: builtDoctorFindingCode(issue, "warning", index),
|
|
161
|
+
message: issue.message || String(issue),
|
|
162
|
+
evidence: issue.detail || null,
|
|
163
|
+
next_action: "Use the built-output doctor finding as the concrete repair target.",
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
finalizeRoot(root);
|
|
168
|
+
finalizeReport(report);
|
|
169
|
+
return report;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function formatStandardizationReportMarkdown(report) {
|
|
173
|
+
const lines = [];
|
|
174
|
+
lines.push("# Campaign Standardization Report");
|
|
175
|
+
lines.push("");
|
|
176
|
+
lines.push(`Status: ${String(report.status || "unknown").toUpperCase()}`);
|
|
177
|
+
lines.push(`Target: ${report.target_repo || "(unknown)"}`);
|
|
178
|
+
lines.push(`Generated: ${report.generated_at || "(unknown)"}`);
|
|
179
|
+
lines.push("");
|
|
180
|
+
lines.push("## Summary");
|
|
181
|
+
const summary = report.summary || {};
|
|
182
|
+
lines.push(`- Campaign roots: ${summary.root_count ?? 0}`);
|
|
183
|
+
lines.push(`- Findings: ${summary.blockers ?? 0} blocker(s), ${summary.warnings ?? 0} warning(s), ${summary.operator_readiness ?? 0} operator-readiness item(s)`);
|
|
184
|
+
lines.push(`- Home recommendation: ${report.recommendation?.home || "unknown"} - ${report.recommendation?.summary || ""}`);
|
|
185
|
+
if (report.errors?.length) {
|
|
186
|
+
lines.push("");
|
|
187
|
+
lines.push("## Errors");
|
|
188
|
+
for (const error of report.errors) lines.push(`- [${error.code}] ${error.message}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const root of report.roots || []) {
|
|
192
|
+
if (root.implementation?.kind === "campaign_cart_app") {
|
|
193
|
+
appendCampaignCartAppMarkdown(lines, root);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const rootLabel = root.identity.page_kit_root_relative === "."
|
|
197
|
+
? `${root.identity.repo} (repo root)`
|
|
198
|
+
: root.identity.page_kit_root_relative;
|
|
199
|
+
lines.push("");
|
|
200
|
+
lines.push(`## ${rootLabel || root.identity.page_kit_root}`);
|
|
201
|
+
lines.push("");
|
|
202
|
+
lines.push("### Identity");
|
|
203
|
+
lines.push(`- Status: ${String(root.status || "unknown").toUpperCase()}`);
|
|
204
|
+
lines.push(`- Implementation: ${root.implementation?.kind || "page_kit"}`);
|
|
205
|
+
lines.push(`- Slug(s): ${root.identity.campaign_slugs.map((entry) => entry.slug).join(", ") || "(none)"}`);
|
|
206
|
+
lines.push(`- SDK: ${root.identity.sdk_versions.join(", ") || "(unknown)"}`);
|
|
207
|
+
if (root.version_policy) {
|
|
208
|
+
lines.push(`- Version policy: min ${root.version_policy.minimum_supported || "(none)"}, preferred ${root.version_policy.preferred_minimum || "(none)"} (${root.version_policy.source})`);
|
|
209
|
+
}
|
|
210
|
+
lines.push(`- Page Kit: ${root.identity.page_kit_dependency?.name || "(unknown)"} ${root.identity.page_kit_dependency?.version || "(unknown)"}`);
|
|
211
|
+
lines.push(`- Template family: ${root.identity.template_family.value || "(unknown)"} (${root.identity.template_family.source || "unknown"})`);
|
|
212
|
+
if (root.identity.template_certification_freshness) {
|
|
213
|
+
lines.push(`- Certification freshness: ${root.identity.template_certification_freshness.summary}`);
|
|
214
|
+
}
|
|
215
|
+
lines.push(`- Campaigns OS artifacts: ${root.identity.has_campaign_runtime ? "yes" : "no"}`);
|
|
216
|
+
lines.push(`- Built _site: ${formatBuiltSiteState(root)}`);
|
|
217
|
+
lines.push("");
|
|
218
|
+
lines.push("### Source Structure");
|
|
219
|
+
lines.push(`- HTML files: ${root.source_structure.html_file_count}; pages: ${root.source_structure.page_file_count}; includes: ${root.source_structure.include_file_count}; layouts: ${root.source_structure.layout_file_count}`);
|
|
220
|
+
lines.push(`- Helpers: campaign_asset=${root.source_structure.helper_counts.campaign_asset}, campaign_include=${root.source_structure.helper_counts.campaign_include}, campaign_link=${root.source_structure.helper_counts.campaign_link}`);
|
|
221
|
+
lines.push(`- Raw blocks: ${root.source_structure.raw_blocks.count}; document wrappers in pages: ${root.source_structure.document_wrappers.count}; hardcoded /assets refs: ${root.source_structure.hardcoded_root_assets.count}; unreadable files: ${root.source_structure.unreadable_files.count}`);
|
|
222
|
+
lines.push(`- Payment methods include: ${root.source_structure.payment_methods_include.detected ? "detected" : "not detected"}`);
|
|
223
|
+
lines.push("");
|
|
224
|
+
lines.push("### Runtime Contract");
|
|
225
|
+
lines.push(`- data-next anchors: ${root.runtime_contract.data_next.total_occurrences} occurrence(s), ${root.runtime_contract.data_next.unique_attributes.length} unique attribute(s)`);
|
|
226
|
+
lines.push(`- Package refs: ${root.runtime_contract.package_refs.count}; shipping refs: ${root.runtime_contract.shipping_refs.count}`);
|
|
227
|
+
lines.push(`- Source manifest: ${root.runtime_contract.source_html_manifest.present ? "present" : "missing"}`);
|
|
228
|
+
lines.push("");
|
|
229
|
+
lines.push("### Built Output");
|
|
230
|
+
lines.push(`- Built slug: ${formatBuiltSlug(root)}`);
|
|
231
|
+
lines.push(`- Built pages: ${root.built_output.html_count || 0}`);
|
|
232
|
+
lines.push(`- Doctor: ${root.built_output.doctor.status || "not_run"}${root.built_output.doctor.reason ? ` (${root.built_output.doctor.reason})` : ""}`);
|
|
233
|
+
if (root.findings.length) {
|
|
234
|
+
lines.push("");
|
|
235
|
+
lines.push("### Findings");
|
|
236
|
+
for (const item of root.findings) {
|
|
237
|
+
lines.push(`- [${item.severity}] ${item.code}: ${item.message}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
lines.push("");
|
|
241
|
+
lines.push("### Remediation");
|
|
242
|
+
appendList(lines, "Safe agent repairs", root.remediation.safe_agent_repairs);
|
|
243
|
+
appendList(lines, "Clarification needed", root.remediation.clarification_needed);
|
|
244
|
+
appendList(lines, "Product or merchant risks", root.remediation.product_or_merchant_risks);
|
|
245
|
+
appendList(lines, "Proof commands", root.remediation.proof_commands);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return `${lines.join("\n")}\n`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function appendCampaignCartAppMarkdown(lines, root) {
|
|
252
|
+
const rootLabel = root.identity.campaign_root_relative === "."
|
|
253
|
+
? `${root.identity.repo} (repo root)`
|
|
254
|
+
: root.identity.campaign_root_relative;
|
|
255
|
+
lines.push("");
|
|
256
|
+
lines.push(`## ${rootLabel || root.identity.campaign_root}`);
|
|
257
|
+
lines.push("");
|
|
258
|
+
lines.push("### Identity");
|
|
259
|
+
lines.push(`- Status: ${String(root.status || "unknown").toUpperCase()}`);
|
|
260
|
+
lines.push(`- Implementation: ${root.implementation.kind}`);
|
|
261
|
+
lines.push(`- Frameworks: ${root.implementation.frameworks.join(", ") || "(none detected)"}`);
|
|
262
|
+
lines.push(`- Campaign ID(s): ${root.identity.campaign_ids.join(", ") || "(unknown)"}`);
|
|
263
|
+
lines.push(`- SDK loader version(s): ${root.identity.sdk_versions.join(", ") || "(undiscovered)"}`);
|
|
264
|
+
lines.push(`- Version policy: min ${root.version_policy.minimum_supported || "(none)"}, preferred ${root.version_policy.preferred_minimum || "(none)"} (${root.version_policy.source})`);
|
|
265
|
+
lines.push(`- Campaigns OS artifacts: ${root.identity.has_campaign_runtime ? "yes" : "no"}`);
|
|
266
|
+
lines.push("");
|
|
267
|
+
lines.push("### Checkout Fields");
|
|
268
|
+
lines.push(`- Bindings: ${root.checkout_fields.bindings.length}; unsupported stale aliases: ${root.checkout_fields.unsupported.length}; unknown: ${root.checkout_fields.unknown.length}`);
|
|
269
|
+
for (const entry of root.checkout_fields.unsupported.slice(0, 8)) {
|
|
270
|
+
lines.push(`- Stale alias \`${entry.value}\` -> \`${entry.canonical}\` (${entry.path}:${entry.line})`);
|
|
271
|
+
}
|
|
272
|
+
lines.push("");
|
|
273
|
+
lines.push("### Payment Interaction");
|
|
274
|
+
lines.push(`- SDK payment_method radios: ${root.payment.sdk_method_radios.detected ? "detected" : "not detected"}; hidden radios: ${root.payment.hidden_radios.detected ? "yes" : "no"}; custom triggers: ${root.payment.custom_triggers.detected ? "yes" : "no"}`);
|
|
275
|
+
lines.push(`- Synchronization script: ${root.payment.synchronization_script.detected ? "detected" : "not detected"}; proof state: ${root.payment.proof_state}`);
|
|
276
|
+
lines.push("");
|
|
277
|
+
lines.push("### Runtime Contract");
|
|
278
|
+
lines.push(`- data-next anchors: ${root.runtime_contract.data_next.total_occurrences} occurrence(s), ${root.runtime_contract.data_next.unique_attributes.length} unique attribute(s)`);
|
|
279
|
+
if (root.findings.length) {
|
|
280
|
+
lines.push("");
|
|
281
|
+
lines.push("### Findings");
|
|
282
|
+
for (const item of root.findings) {
|
|
283
|
+
const confidence = item.confidence ? ` (confidence: ${item.confidence})` : "";
|
|
284
|
+
lines.push(`- [${item.severity}] ${item.code}: ${item.message}${confidence}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
lines.push("");
|
|
288
|
+
lines.push("### Remediation");
|
|
289
|
+
appendList(lines, "Safe agent repairs", root.remediation.safe_agent_repairs);
|
|
290
|
+
appendList(lines, "Clarification needed", root.remediation.clarification_needed);
|
|
291
|
+
appendList(lines, "Product or merchant risks", root.remediation.product_or_merchant_risks);
|
|
292
|
+
appendList(lines, "Proof commands", root.remediation.proof_commands);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function discoverPageKitRoots(targetRepo) {
|
|
296
|
+
const target = resolve(targetRepo);
|
|
297
|
+
if (!existsSync(target) || !statSync(target).isDirectory()) return [];
|
|
298
|
+
const roots = [];
|
|
299
|
+
const seen = new Set();
|
|
300
|
+
|
|
301
|
+
function addRoot(dir) {
|
|
302
|
+
const resolved = resolve(dir);
|
|
303
|
+
if (seen.has(resolved)) return;
|
|
304
|
+
seen.add(resolved);
|
|
305
|
+
roots.push(resolved);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (isPageKitRoot(target)) {
|
|
309
|
+
addRoot(target);
|
|
310
|
+
return roots;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function walk(dir, depth) {
|
|
314
|
+
if (depth > 4) return;
|
|
315
|
+
for (const entry of safeReadDir(dir)) {
|
|
316
|
+
if (!entry.isDirectory()) continue;
|
|
317
|
+
if (shouldSkipDir(entry.name)) continue;
|
|
318
|
+
const child = join(dir, entry.name);
|
|
319
|
+
if (isPageKitRoot(child)) {
|
|
320
|
+
addRoot(child);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
walk(child, depth + 1);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
walk(target, 1);
|
|
328
|
+
return roots.sort();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function scanPageKitRoot({
|
|
332
|
+
targetRepo,
|
|
333
|
+
rootPath,
|
|
334
|
+
requestedSlug = null,
|
|
335
|
+
explicitTemplateFamily = null,
|
|
336
|
+
fieldContract = null,
|
|
337
|
+
sdkSupportPolicy = null,
|
|
338
|
+
}) {
|
|
339
|
+
const files = listFiles(rootPath);
|
|
340
|
+
const structureFiles = files.filter((file) => STRUCTURE_EXTENSIONS.has(extname(file).toLowerCase()));
|
|
341
|
+
const sourceFiles = files.filter((file) => TEXT_EXTENSIONS.has(extname(file).toLowerCase()));
|
|
342
|
+
const campaigns = readCampaigns(rootPath);
|
|
343
|
+
const packageInfo = readPackageInfo(rootPath);
|
|
344
|
+
const runtime = readRuntimeArtifacts(rootPath, targetRepo, files);
|
|
345
|
+
// The checkout field contract applies wherever inline checkout bindings
|
|
346
|
+
// exist; a Page Kit root that inlines checkout markup is inspected exactly
|
|
347
|
+
// like an application root, and the capability is listed only when it ran.
|
|
348
|
+
// The binding attributes come from the effective contract (override
|
|
349
|
+
// included), so a contract that names other attributes still finds them;
|
|
350
|
+
// the prefilter runs inside the one read scanSourceFiles already makes.
|
|
351
|
+
const contract = loadCheckoutFieldContract(fieldContract);
|
|
352
|
+
const bindingPattern = new RegExp(`\\b(?:${(contract?.binding_attributes || ["data-next-checkout-field", "os-checkout-field"]).map(escapeRegExp).join("|")})\\s*=`);
|
|
353
|
+
const sourceScan = scanSourceFiles(rootPath, structureFiles, sourceFiles, campaigns.slugs, bindingPattern);
|
|
354
|
+
const templateFamily = inferTemplateFamily({
|
|
355
|
+
explicitTemplateFamily,
|
|
356
|
+
runtime,
|
|
357
|
+
files,
|
|
358
|
+
rootPath,
|
|
359
|
+
});
|
|
360
|
+
const policy = loadSdkSupportPolicy(sdkSupportPolicy);
|
|
361
|
+
const campaignSlug = resolveCampaignSlug({ requestedSlug, campaigns, runtime, rootPath });
|
|
362
|
+
const builtOutput = inspectBuiltOutput(rootPath, campaignSlug);
|
|
363
|
+
const capabilities = [
|
|
364
|
+
"page_kit_source_contract",
|
|
365
|
+
"sdk_version_policy",
|
|
366
|
+
"campaign_cart_runtime_inventory",
|
|
367
|
+
];
|
|
368
|
+
const contractFindings = [];
|
|
369
|
+
const versionPolicy = evaluateVersionPolicy(
|
|
370
|
+
unique(campaigns.slugs.map((entry) => entry.sdk_version).filter(Boolean))
|
|
371
|
+
.map((version) => ({ version, source: "campaigns_json" })),
|
|
372
|
+
policy,
|
|
373
|
+
contractFindings,
|
|
374
|
+
);
|
|
375
|
+
const bindingFiles = sourceScan.binding_files;
|
|
376
|
+
let checkoutFields = null;
|
|
377
|
+
if (bindingFiles.length) {
|
|
378
|
+
checkoutFields = inspectCheckoutFields(rootPath, bindingFiles, contract, contractFindings);
|
|
379
|
+
capabilities.push("checkout_field_contract");
|
|
380
|
+
}
|
|
381
|
+
const root = {
|
|
382
|
+
id: rootId(targetRepo, rootPath),
|
|
383
|
+
status: "unknown",
|
|
384
|
+
ok: false,
|
|
385
|
+
implementation: {
|
|
386
|
+
kind: "page_kit",
|
|
387
|
+
evidence: pageKitImplementationEvidence(rootPath, packageInfo),
|
|
388
|
+
frameworks: detectFrameworks(rootPath),
|
|
389
|
+
},
|
|
390
|
+
capabilities,
|
|
391
|
+
identity: {
|
|
392
|
+
repo: basename(targetRepo),
|
|
393
|
+
target_repo: targetRepo,
|
|
394
|
+
page_kit_root: rootPath,
|
|
395
|
+
page_kit_root_relative: relPath(targetRepo, rootPath),
|
|
396
|
+
campaign_slug: campaignSlug.slug,
|
|
397
|
+
campaign_slug_source: campaignSlug.source,
|
|
398
|
+
campaign_slugs: campaigns.slugs,
|
|
399
|
+
sdk_versions: unique(campaigns.slugs.map((entry) => entry.sdk_version).filter(Boolean)),
|
|
400
|
+
page_kit_dependency: packageInfo.page_kit_dependency,
|
|
401
|
+
template_family: templateFamily,
|
|
402
|
+
template_certification_freshness: assessRootCertificationFreshness(templateFamily.value, sdkSupportPolicy),
|
|
403
|
+
has_campaign_runtime: runtime.present,
|
|
404
|
+
has_built_site: builtOutput.present,
|
|
405
|
+
},
|
|
406
|
+
version_policy: versionPolicy,
|
|
407
|
+
...(checkoutFields ? { checkout_fields: checkoutFields } : {}),
|
|
408
|
+
source_structure: sourceScan.source_structure,
|
|
409
|
+
runtime_contract: {
|
|
410
|
+
data_next: sourceScan.runtime_contract.data_next,
|
|
411
|
+
checkout_surface: sourceScan.runtime_contract.checkout_surface,
|
|
412
|
+
upsell_surface: sourceScan.runtime_contract.upsell_surface,
|
|
413
|
+
receipt_surface: sourceScan.runtime_contract.receipt_surface,
|
|
414
|
+
package_refs: sourceScan.runtime_contract.package_refs,
|
|
415
|
+
shipping_refs: sourceScan.runtime_contract.shipping_refs,
|
|
416
|
+
campaign_runtime: runtime.summary,
|
|
417
|
+
source_html_manifest: runtime.source_html_manifest,
|
|
418
|
+
},
|
|
419
|
+
built_output: builtOutput,
|
|
420
|
+
findings: [
|
|
421
|
+
...campaigns.findings,
|
|
422
|
+
...packageInfo.findings,
|
|
423
|
+
...contractFindings,
|
|
424
|
+
...sourceScan.findings,
|
|
425
|
+
...runtime.findings,
|
|
426
|
+
...builtOutput.findings,
|
|
427
|
+
],
|
|
428
|
+
remediation: {
|
|
429
|
+
safe_agent_repairs: [],
|
|
430
|
+
clarification_needed: [],
|
|
431
|
+
product_or_merchant_risks: [],
|
|
432
|
+
proof_commands: [],
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
addVersionFindings(root);
|
|
436
|
+
addTemplateFamilyFindings(root);
|
|
437
|
+
finalizeRoot(root);
|
|
438
|
+
return root;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function finalizeReport(report) {
|
|
442
|
+
const rootCounts = { blockers: 0, warnings: 0, operator_readiness: 0 };
|
|
443
|
+
for (const root of report.roots || []) {
|
|
444
|
+
for (const item of root.findings || []) {
|
|
445
|
+
if (item.severity === "blocker") rootCounts.blockers += 1;
|
|
446
|
+
else if (item.severity === "warning") rootCounts.warnings += 1;
|
|
447
|
+
else if (item.severity === "operator_readiness") rootCounts.operator_readiness += 1;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const hasBlockingError = Boolean(report.errors?.length);
|
|
451
|
+
report.summary = {
|
|
452
|
+
root_count: report.roots?.length || 0,
|
|
453
|
+
blockers: rootCounts.blockers + (hasBlockingError ? report.errors.length : 0),
|
|
454
|
+
warnings: rootCounts.warnings,
|
|
455
|
+
operator_readiness: rootCounts.operator_readiness,
|
|
456
|
+
blocked_roots: (report.roots || []).filter((root) => root.status === "blocked").length,
|
|
457
|
+
warning_roots: (report.roots || []).filter((root) => root.status === "ready_with_warnings").length,
|
|
458
|
+
ready_roots: (report.roots || []).filter((root) => root.status === "ready").length,
|
|
459
|
+
};
|
|
460
|
+
report.status = report.summary.blockers > 0
|
|
461
|
+
? "blocked"
|
|
462
|
+
: report.summary.warnings || report.summary.operator_readiness
|
|
463
|
+
? "ready_with_warnings"
|
|
464
|
+
: "ready";
|
|
465
|
+
report.ok = report.status !== "blocked";
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function finalizeRoot(root) {
|
|
469
|
+
dedupeFindings(root);
|
|
470
|
+
if (root.implementation?.kind !== "campaign_cart_app") {
|
|
471
|
+
root.remediation = buildRemediation(root);
|
|
472
|
+
}
|
|
473
|
+
if (root.findings.some((item) => item.severity === "blocker")) {
|
|
474
|
+
root.status = "blocked";
|
|
475
|
+
} else if (root.findings.some((item) => item.severity === "warning" || item.severity === "operator_readiness")) {
|
|
476
|
+
root.status = "ready_with_warnings";
|
|
477
|
+
} else {
|
|
478
|
+
root.status = "ready";
|
|
479
|
+
}
|
|
480
|
+
root.ok = root.status !== "blocked";
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function pageKitImplementationEvidence(rootPath, packageInfo) {
|
|
484
|
+
const evidence = [];
|
|
485
|
+
if (existsSync(join(rootPath, "_data", "campaigns.json"))) {
|
|
486
|
+
evidence.push({ signal: "campaigns_json", strength: "strong", path: "_data/campaigns.json", detail: null });
|
|
487
|
+
}
|
|
488
|
+
if (packageInfo.page_kit_dependency) {
|
|
489
|
+
evidence.push({
|
|
490
|
+
signal: "page_kit_dependency",
|
|
491
|
+
strength: "strong",
|
|
492
|
+
path: "package.json",
|
|
493
|
+
detail: `${packageInfo.page_kit_dependency.name}@${packageInfo.page_kit_dependency.version}`,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
return evidence;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function isPageKitRoot(dir) {
|
|
500
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) return false;
|
|
501
|
+
if (existsSync(join(dir, "_data", "campaigns.json"))) return true;
|
|
502
|
+
const pkg = readJsonFile(join(dir, "package.json"));
|
|
503
|
+
if (!pkg.ok) return false;
|
|
504
|
+
return Boolean(findPageKitDependency(pkg.value));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function readCampaigns(rootPath) {
|
|
508
|
+
const campaignsPath = join(rootPath, "_data", "campaigns.json");
|
|
509
|
+
if (!existsSync(campaignsPath)) {
|
|
510
|
+
return {
|
|
511
|
+
slugs: [],
|
|
512
|
+
findings: [finding({
|
|
513
|
+
severity: "blocker",
|
|
514
|
+
category: "standardization_blocker",
|
|
515
|
+
code: "page_kit.campaigns_json_missing",
|
|
516
|
+
message: "Missing _data/campaigns.json; campaign slug and SDK version cannot be confirmed.",
|
|
517
|
+
next_action: "Add or restore _data/campaigns.json before treating this as a modern CPK root.",
|
|
518
|
+
})],
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
const parsed = readJsonFile(campaignsPath);
|
|
522
|
+
if (!parsed.ok || !isObject(parsed.value)) {
|
|
523
|
+
return {
|
|
524
|
+
slugs: [],
|
|
525
|
+
findings: [finding({
|
|
526
|
+
severity: "blocker",
|
|
527
|
+
category: "standardization_blocker",
|
|
528
|
+
code: "page_kit.campaigns_json_invalid",
|
|
529
|
+
message: `Could not parse _data/campaigns.json: ${parsed.error}`,
|
|
530
|
+
next_action: "Fix campaigns.json before running version or slug checks.",
|
|
531
|
+
})],
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
const slugs = Object.entries(parsed.value).map(([slug, entry]) => ({
|
|
535
|
+
slug,
|
|
536
|
+
name: normalizeString(entry?.name),
|
|
537
|
+
sdk_version: normalizeString(entry?.sdk_version),
|
|
538
|
+
store_url: normalizeString(entry?.store_url),
|
|
539
|
+
}));
|
|
540
|
+
return { slugs, findings: [] };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function readPackageInfo(rootPath) {
|
|
544
|
+
const packagePath = join(rootPath, "package.json");
|
|
545
|
+
if (!existsSync(packagePath)) {
|
|
546
|
+
return {
|
|
547
|
+
page_kit_dependency: null,
|
|
548
|
+
findings: [finding({
|
|
549
|
+
severity: "warning",
|
|
550
|
+
category: "standardization_warning",
|
|
551
|
+
code: "page_kit.package_json_missing",
|
|
552
|
+
message: "Missing package.json; Page Kit dependency version cannot be confirmed.",
|
|
553
|
+
next_action: "Restore package.json or identify the Page Kit build command source.",
|
|
554
|
+
})],
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
const parsed = readJsonFile(packagePath);
|
|
558
|
+
if (!parsed.ok) {
|
|
559
|
+
return {
|
|
560
|
+
page_kit_dependency: null,
|
|
561
|
+
findings: [finding({
|
|
562
|
+
severity: "warning",
|
|
563
|
+
category: "standardization_warning",
|
|
564
|
+
code: "page_kit.package_json_invalid",
|
|
565
|
+
message: `Could not parse package.json: ${parsed.error}`,
|
|
566
|
+
next_action: "Fix package.json before checking Page Kit dependency drift.",
|
|
567
|
+
})],
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const dep = findPageKitDependency(parsed.value);
|
|
571
|
+
return {
|
|
572
|
+
page_kit_dependency: dep,
|
|
573
|
+
findings: dep ? [] : [finding({
|
|
574
|
+
severity: "warning",
|
|
575
|
+
category: "standardization_warning",
|
|
576
|
+
code: "page_kit.dependency_missing",
|
|
577
|
+
message: "No next-campaign-page-kit dependency was found in package.json.",
|
|
578
|
+
next_action: "Confirm whether this root is built by Page Kit or another wrapper.",
|
|
579
|
+
})],
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function findPageKitDependency(pkg) {
|
|
584
|
+
const deps = {
|
|
585
|
+
...(pkg?.dependencies || {}),
|
|
586
|
+
...(pkg?.devDependencies || {}),
|
|
587
|
+
};
|
|
588
|
+
for (const name of PAGE_KIT_PACKAGE_NAMES) {
|
|
589
|
+
if (typeof deps[name] === "string") return { name, version: deps[name] };
|
|
590
|
+
}
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function readRuntimeArtifacts(rootPath, targetRepo, rootFiles = []) {
|
|
595
|
+
const runtimeDirs = unique([join(rootPath, ".campaign-runtime"), join(targetRepo, ".campaign-runtime")]);
|
|
596
|
+
const artifactFiles = [];
|
|
597
|
+
for (const dir of runtimeDirs) {
|
|
598
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) continue;
|
|
599
|
+
artifactFiles.push(...listFiles(dir, { includeRuntime: true }).map((file) => ({
|
|
600
|
+
path: file,
|
|
601
|
+
relative_path: relPath(rootPath, file),
|
|
602
|
+
})));
|
|
603
|
+
}
|
|
604
|
+
const sourceManifestFiles = [
|
|
605
|
+
...rootFiles.filter((file) => file.endsWith(".campaigns-os/source-html-manifest.json")),
|
|
606
|
+
...runtimeDirs.flatMap((dir) => listFiles(dir, { includeRuntime: true })
|
|
607
|
+
.filter((file) => file.endsWith("source-html-manifest.json"))),
|
|
608
|
+
];
|
|
609
|
+
const present = artifactFiles.length > 0;
|
|
610
|
+
const findings = [];
|
|
611
|
+
if (!present) {
|
|
612
|
+
findings.push(finding({
|
|
613
|
+
severity: "warning",
|
|
614
|
+
category: "standardization_warning",
|
|
615
|
+
code: "campaigns_os.artifacts_missing",
|
|
616
|
+
message: "No .campaign-runtime artifacts were found for this root.",
|
|
617
|
+
next_action: "Generate or attach Campaigns OS build context before treating runtime provenance as confirmed.",
|
|
618
|
+
}));
|
|
619
|
+
}
|
|
620
|
+
if (!sourceManifestFiles.length) {
|
|
621
|
+
findings.push(finding({
|
|
622
|
+
severity: "operator_readiness",
|
|
623
|
+
category: "operator_readiness",
|
|
624
|
+
code: "campaigns_os.source_manifest_missing",
|
|
625
|
+
message: "No source-html manifest was found; Figma/source producer provenance is not confirmed.",
|
|
626
|
+
next_action: "Attach .campaigns-os/source-html-manifest.json when source producer proof matters.",
|
|
627
|
+
}));
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
present,
|
|
631
|
+
artifactFiles,
|
|
632
|
+
source_html_manifest: {
|
|
633
|
+
present: sourceManifestFiles.length > 0,
|
|
634
|
+
paths: sourceManifestFiles.map((file) => relPath(rootPath, file)),
|
|
635
|
+
},
|
|
636
|
+
summary: {
|
|
637
|
+
present,
|
|
638
|
+
artifact_count: artifactFiles.length,
|
|
639
|
+
artifacts: artifactFiles.slice(0, 40).map((file) => file.relative_path),
|
|
640
|
+
},
|
|
641
|
+
findings,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function scanSourceFiles(rootPath, structureFiles, sourceFiles, slugs, bindingPattern = null) {
|
|
646
|
+
const findings = [];
|
|
647
|
+
const helperCounts = {
|
|
648
|
+
campaign_asset: 0,
|
|
649
|
+
campaign_include: 0,
|
|
650
|
+
campaign_link: 0,
|
|
651
|
+
};
|
|
652
|
+
const rawBlocks = [];
|
|
653
|
+
const hardcodedAssets = [];
|
|
654
|
+
const unreadableFiles = [];
|
|
655
|
+
const documentWrappers = [];
|
|
656
|
+
const paymentMethodFiles = [];
|
|
657
|
+
const dataNextCounts = new Map();
|
|
658
|
+
const checkoutFiles = new Set();
|
|
659
|
+
const upsellFiles = new Set();
|
|
660
|
+
const receiptFiles = new Set();
|
|
661
|
+
const packageRefs = [];
|
|
662
|
+
const shippingRefs = [];
|
|
663
|
+
const bindingFiles = [];
|
|
664
|
+
|
|
665
|
+
const includeFiles = structureFiles.filter((file) => relPath(rootPath, file).includes("/_includes/"));
|
|
666
|
+
const layoutFiles = structureFiles.filter((file) => relPath(rootPath, file).includes("/_layouts/"));
|
|
667
|
+
const pageFiles = structureFiles.filter((file) => {
|
|
668
|
+
const rel = relPath(rootPath, file);
|
|
669
|
+
return !rel.includes("/_includes/") && !rel.includes("/_layouts/") && !rel.includes("/assets/");
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
for (const file of sourceFiles) {
|
|
673
|
+
const rel = relPath(rootPath, file);
|
|
674
|
+
const readResult = safeReadText(file);
|
|
675
|
+
if (!readResult.ok) {
|
|
676
|
+
unreadableFiles.push({
|
|
677
|
+
path: rel,
|
|
678
|
+
line: 1,
|
|
679
|
+
match: readResult.error,
|
|
680
|
+
});
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
const content = readResult.value;
|
|
684
|
+
if (STRUCTURE_EXTENSIONS.has(extname(file).toLowerCase())) {
|
|
685
|
+
helperCounts.campaign_asset += countLiteral(content, "campaign_asset");
|
|
686
|
+
helperCounts.campaign_include += countLiteral(content, "campaign_include");
|
|
687
|
+
helperCounts.campaign_link += countLiteral(content, "campaign_link");
|
|
688
|
+
collectPatternSamples(rawBlocks, rootPath, file, content, RAW_BLOCK_PATTERN);
|
|
689
|
+
collectHardcodedAssetSamples(hardcodedAssets, rootPath, file, content, slugs);
|
|
690
|
+
if (bindingPattern && bindingPattern.test(content)) bindingFiles.push(file);
|
|
691
|
+
if (isPaymentMethodsInclude(file, content)) {
|
|
692
|
+
paymentMethodFiles.push(rel);
|
|
693
|
+
}
|
|
694
|
+
const documentWrapperIndex = pageFiles.includes(file) ? documentWrapperMatchIndex(content) : -1;
|
|
695
|
+
if (documentWrapperIndex >= 0) {
|
|
696
|
+
documentWrappers.push(sample(rootPath, file, content, documentWrapperIndex, "document wrapper"));
|
|
697
|
+
}
|
|
698
|
+
if (/checkout/i.test(rel) || /data-next-checkout|data-next-action=["'](?:create-order|add-to-cart)/i.test(content)) {
|
|
699
|
+
checkoutFiles.add(rel);
|
|
700
|
+
}
|
|
701
|
+
if (/upsell/i.test(rel) || /data-next-upsell/i.test(content)) {
|
|
702
|
+
upsellFiles.add(rel);
|
|
703
|
+
}
|
|
704
|
+
if (/receipt|thank/i.test(rel) || /\bdata-next-(?:order|receipt)[a-zA-Z0-9_-]*/i.test(content)) {
|
|
705
|
+
receiptFiles.add(rel);
|
|
706
|
+
}
|
|
707
|
+
collectDataNextAttributes(dataNextCounts, content);
|
|
708
|
+
collectPatternSamples(packageRefs, rootPath, file, content, /(?:\bpackageId\b|\bpackage_id\b|\bdata-next-package-id\b|\bdata-next-display=["']package\.[^"']+["'])/g);
|
|
709
|
+
collectPatternSamples(shippingRefs, rootPath, file, content, /(?:shippingId|shipping_id|data-next-shipping-id|shipping\.)/g);
|
|
710
|
+
} else {
|
|
711
|
+
collectDataNextAttributes(dataNextCounts, content);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (unreadableFiles.length) {
|
|
716
|
+
findings.push(finding({
|
|
717
|
+
severity: "warning",
|
|
718
|
+
category: "standardization_warning",
|
|
719
|
+
code: "source.file_unreadable",
|
|
720
|
+
message: `${unreadableFiles.length} source file(s) could not be read; skipped files may hide standardization issues.`,
|
|
721
|
+
evidence: unreadableFiles.slice(0, MAX_SAMPLE_COUNT),
|
|
722
|
+
next_action: "Fix file permissions or remove unreadable source artifacts, then rerun the standardization report.",
|
|
723
|
+
}));
|
|
724
|
+
}
|
|
725
|
+
if (rawBlocks.length) {
|
|
726
|
+
findings.push(finding({
|
|
727
|
+
severity: "blocker",
|
|
728
|
+
category: "standardization_blocker",
|
|
729
|
+
code: "source.raw_block",
|
|
730
|
+
message: `${rawBlocks.length} Liquid raw block(s) found; raw blocks can suppress Page Kit helpers and runtime Liquid.`,
|
|
731
|
+
evidence: rawBlocks.slice(0, MAX_SAMPLE_COUNT),
|
|
732
|
+
next_action: "Remove raw blocks and preserve only the page-owned HTML that should bypass Liquid.",
|
|
733
|
+
}));
|
|
734
|
+
}
|
|
735
|
+
if (hardcodedAssets.length) {
|
|
736
|
+
findings.push(finding({
|
|
737
|
+
severity: "warning",
|
|
738
|
+
category: "standardization_warning",
|
|
739
|
+
code: "source.hardcoded_root_assets",
|
|
740
|
+
message: `${hardcodedAssets.length} hardcoded /assets or /<slug>/assets reference(s) found in source.`,
|
|
741
|
+
evidence: hardcodedAssets.slice(0, MAX_SAMPLE_COUNT),
|
|
742
|
+
next_action: "Rewrite portable local assets through the campaign_asset helper.",
|
|
743
|
+
}));
|
|
744
|
+
}
|
|
745
|
+
if (documentWrappers.length) {
|
|
746
|
+
findings.push(finding({
|
|
747
|
+
severity: "warning",
|
|
748
|
+
category: "standardization_warning",
|
|
749
|
+
code: "source.document_wrappers",
|
|
750
|
+
message: `${documentWrappers.length} page file(s) still contain document wrappers.`,
|
|
751
|
+
evidence: documentWrappers.slice(0, MAX_SAMPLE_COUNT),
|
|
752
|
+
next_action: "Move document-level markup into layouts and keep page files body-owned.",
|
|
753
|
+
}));
|
|
754
|
+
}
|
|
755
|
+
if (!paymentMethodFiles.length) {
|
|
756
|
+
findings.push(finding({
|
|
757
|
+
severity: "warning",
|
|
758
|
+
category: "standardization_warning",
|
|
759
|
+
code: "source.payment_methods_include_not_detected",
|
|
760
|
+
message: "No payment-methods include was detected; this is tentative because some template families inline or rename the surface.",
|
|
761
|
+
next_action: "Confirm the template family before treating this as a repair task.",
|
|
762
|
+
}));
|
|
763
|
+
}
|
|
764
|
+
if (!helperCounts.campaign_asset) {
|
|
765
|
+
findings.push(finding({
|
|
766
|
+
severity: "warning",
|
|
767
|
+
category: "standardization_warning",
|
|
768
|
+
code: "source.campaign_asset_missing",
|
|
769
|
+
message: "No campaign_asset helper usage was detected.",
|
|
770
|
+
next_action: "Confirm asset references are portable through Page Kit before standardizing this repo.",
|
|
771
|
+
}));
|
|
772
|
+
}
|
|
773
|
+
if (sumMap(dataNextCounts) === 0) {
|
|
774
|
+
findings.push(finding({
|
|
775
|
+
severity: "warning",
|
|
776
|
+
category: "standardization_warning",
|
|
777
|
+
code: "runtime.data_next_missing",
|
|
778
|
+
message: "No data-next-* runtime anchors were detected in source.",
|
|
779
|
+
next_action: "Confirm whether this campaign contains SDK-owned checkout, upsell, or receipt surfaces.",
|
|
780
|
+
}));
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return {
|
|
784
|
+
source_structure: {
|
|
785
|
+
html_file_count: structureFiles.length,
|
|
786
|
+
page_file_count: pageFiles.length,
|
|
787
|
+
include_file_count: includeFiles.length,
|
|
788
|
+
layout_file_count: layoutFiles.length,
|
|
789
|
+
helper_counts: helperCounts,
|
|
790
|
+
raw_blocks: { count: rawBlocks.length, samples: rawBlocks.slice(0, MAX_SAMPLE_COUNT) },
|
|
791
|
+
hardcoded_root_assets: { count: hardcodedAssets.length, samples: hardcodedAssets.slice(0, MAX_SAMPLE_COUNT) },
|
|
792
|
+
unreadable_files: { count: unreadableFiles.length, samples: unreadableFiles.slice(0, MAX_SAMPLE_COUNT) },
|
|
793
|
+
document_wrappers: { count: documentWrappers.length, samples: documentWrappers.slice(0, MAX_SAMPLE_COUNT) },
|
|
794
|
+
payment_methods_include: {
|
|
795
|
+
detected: paymentMethodFiles.length > 0,
|
|
796
|
+
paths: unique(paymentMethodFiles).slice(0, MAX_SAMPLE_COUNT),
|
|
797
|
+
},
|
|
798
|
+
},
|
|
799
|
+
runtime_contract: {
|
|
800
|
+
data_next: summarizeDataNext(dataNextCounts),
|
|
801
|
+
checkout_surface: { detected: checkoutFiles.size > 0, files: [...checkoutFiles].slice(0, MAX_SAMPLE_COUNT) },
|
|
802
|
+
upsell_surface: { detected: upsellFiles.size > 0, files: [...upsellFiles].slice(0, MAX_SAMPLE_COUNT) },
|
|
803
|
+
receipt_surface: { detected: receiptFiles.size > 0, files: [...receiptFiles].slice(0, MAX_SAMPLE_COUNT) },
|
|
804
|
+
package_refs: { count: packageRefs.length, samples: packageRefs.slice(0, MAX_SAMPLE_COUNT) },
|
|
805
|
+
shipping_refs: { count: shippingRefs.length, samples: shippingRefs.slice(0, MAX_SAMPLE_COUNT) },
|
|
806
|
+
},
|
|
807
|
+
binding_files: bindingFiles,
|
|
808
|
+
findings,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// The slug the built output is scoped by, in precedence order: the operator's
|
|
813
|
+
// --slug, the single slug _data/campaigns.json declares, the public_route_slug
|
|
814
|
+
// the .campaign-runtime packets name — only when every packet that names one
|
|
815
|
+
// agrees, since a multi-campaign repository can carry several — and finally
|
|
816
|
+
// the _site/ layout itself.
|
|
817
|
+
function resolveCampaignSlug({ requestedSlug, campaigns, runtime, rootPath }) {
|
|
818
|
+
if (requestedSlug) return { slug: requestedSlug, source: "operator_flag" };
|
|
819
|
+
if (campaigns.slugs.length === 1) return { slug: campaigns.slugs[0].slug, source: "campaigns_json" };
|
|
820
|
+
const candidates = new Map();
|
|
821
|
+
for (const artifact of runtime.artifactFiles || []) {
|
|
822
|
+
if (!artifact.path.endsWith(".json")) continue;
|
|
823
|
+
const parsed = readJsonFile(artifact.path);
|
|
824
|
+
if (!parsed.ok) continue;
|
|
825
|
+
const value = firstStringAt(parsed.value, [
|
|
826
|
+
["campaign", "public_route_slug"],
|
|
827
|
+
["public_route_slug"],
|
|
828
|
+
["campaign_slug"],
|
|
829
|
+
]);
|
|
830
|
+
if (value && !candidates.has(value)) candidates.set(value, relPath(rootPath, artifact.path));
|
|
831
|
+
}
|
|
832
|
+
if (candidates.size === 1) {
|
|
833
|
+
const [[slug, source]] = candidates.entries();
|
|
834
|
+
return { slug, source };
|
|
835
|
+
}
|
|
836
|
+
return { slug: null, source: null };
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function inspectBuiltOutput(rootPath, campaignSlug) {
|
|
840
|
+
const requestedSlug = campaignSlug.slug;
|
|
841
|
+
const siteRoot = join(rootPath, "_site");
|
|
842
|
+
if (!existsSync(siteRoot) || !statSync(siteRoot).isDirectory()) {
|
|
843
|
+
return {
|
|
844
|
+
present: false,
|
|
845
|
+
scope_resolved: false,
|
|
846
|
+
site_root: siteRoot,
|
|
847
|
+
slug: requestedSlug || null,
|
|
848
|
+
slug_source: requestedSlug ? campaignSlug.source : null,
|
|
849
|
+
html_count: 0,
|
|
850
|
+
pages: [],
|
|
851
|
+
doctor: { status: "skipped", reason: "no built _site found" },
|
|
852
|
+
findings: [finding({
|
|
853
|
+
severity: "operator_readiness",
|
|
854
|
+
category: "operator_readiness",
|
|
855
|
+
code: "built_output.site_missing",
|
|
856
|
+
message: "No built _site directory was found; built-output doctor was skipped.",
|
|
857
|
+
next_action: "Run the Page Kit build in an isolated worktree when deeper proof is needed.",
|
|
858
|
+
})],
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
const scope = resolveBuiltSiteScope(rootPath, { slug: requestedSlug || null });
|
|
862
|
+
if (!scope.ok) {
|
|
863
|
+
// A slug the scanner derived (not one the operator passed) with no matching
|
|
864
|
+
// built directory is a mismatch, not an ambiguity: the built output belongs
|
|
865
|
+
// to some other campaign, so it is named rather than silently inspected.
|
|
866
|
+
const derivedMismatch = requestedSlug && campaignSlug.source !== "operator_flag";
|
|
867
|
+
// The scope resolver sets campaign_dir when _site/<slug>/ exists but holds
|
|
868
|
+
// no HTML pages; that is an empty build, not a missing one, and is named
|
|
869
|
+
// as such so the remediation cue is right.
|
|
870
|
+
const directoryPresent = Boolean(scope.campaign_dir);
|
|
871
|
+
const candidates = derivedMismatch ? builtSlugCandidates(siteRoot) : (scope.slug_candidates || []);
|
|
872
|
+
const builtDirectories = `built directories: ${candidates.join(", ") || "(none)"}`;
|
|
873
|
+
const mismatch = directoryPresent
|
|
874
|
+
? `built _site/${requestedSlug}/ exists but holds no HTML pages for campaign slug ${requestedSlug} (from ${campaignSlug.source})`
|
|
875
|
+
: `built _site has no ${requestedSlug}/ directory for campaign slug ${requestedSlug} (from ${campaignSlug.source})`;
|
|
876
|
+
const reason = derivedMismatch
|
|
877
|
+
? `${mismatch}; ${builtDirectories}`
|
|
878
|
+
: (scope.error || "built scope could not be resolved");
|
|
879
|
+
return {
|
|
880
|
+
present: true,
|
|
881
|
+
scope_resolved: false,
|
|
882
|
+
site_root: siteRoot,
|
|
883
|
+
slug: requestedSlug || null,
|
|
884
|
+
slug_source: requestedSlug ? campaignSlug.source : null,
|
|
885
|
+
html_count: 0,
|
|
886
|
+
pages: [],
|
|
887
|
+
slug_candidates: candidates,
|
|
888
|
+
...(derivedMismatch ? { slug_directory_present: directoryPresent } : {}),
|
|
889
|
+
doctor: { status: "skipped", reason },
|
|
890
|
+
findings: [derivedMismatch
|
|
891
|
+
? finding({
|
|
892
|
+
severity: "operator_readiness",
|
|
893
|
+
category: "operator_readiness",
|
|
894
|
+
code: "built_output.slug_mismatch",
|
|
895
|
+
message: `${mismatch.charAt(0).toUpperCase()}${mismatch.slice(1)}; ${builtDirectories}. The built-output doctor was skipped rather than run against ${directoryPresent ? "an empty build" : "another campaign's pages"}.`,
|
|
896
|
+
evidence: { expected_slug: requestedSlug, slug_source: campaignSlug.source, slug_directory_present: directoryPresent, slug_candidates: candidates },
|
|
897
|
+
next_action: directoryPresent
|
|
898
|
+
? "Rebuild the campaign so _site/<slug>/ contains its HTML pages, or pass --slug to inspect a different built directory on purpose."
|
|
899
|
+
: "Rebuild the campaign so _site/<slug>/ exists, or pass --slug to inspect a different built directory on purpose.",
|
|
900
|
+
})
|
|
901
|
+
: finding({
|
|
902
|
+
severity: "operator_readiness",
|
|
903
|
+
category: "operator_readiness",
|
|
904
|
+
code: "built_output.scope_unresolved",
|
|
905
|
+
message: scope.error || "Built output exists but scope could not be resolved.",
|
|
906
|
+
evidence: scope.slug_candidates?.length ? { slug_candidates: scope.slug_candidates } : null,
|
|
907
|
+
next_action: "Pass --slug when a repo has multiple built campaign outputs and neither _data/campaigns.json nor a .campaign-runtime packet names one.",
|
|
908
|
+
})],
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
present: true,
|
|
913
|
+
scope_resolved: true,
|
|
914
|
+
site_root: scope.site_root,
|
|
915
|
+
slug: scope.slug || null,
|
|
916
|
+
slug_source: scope.slug ? campaignSlug.source || "site_layout" : "site_layout",
|
|
917
|
+
html_count: scope.html_count,
|
|
918
|
+
pages: scope.pages.map((page) => ({ page_id: page.page_id, type: page.page_type, route: page.route })),
|
|
919
|
+
doctor: { status: "not_run", reason: "doctor not attached yet" },
|
|
920
|
+
findings: [],
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// SDK versions are judged by the support policy in scanPageKitRoot; only the
|
|
925
|
+
// Page Kit dependency keeps a scanner-side cutoff.
|
|
926
|
+
function addVersionFindings(root) {
|
|
927
|
+
const depVersion = root.identity.page_kit_dependency?.version;
|
|
928
|
+
if (extractVersion(depVersion) && compareVersions(depVersion, PREFERRED_PAGE_KIT_MIN) < 0) {
|
|
929
|
+
root.findings.push(finding({
|
|
930
|
+
severity: "warning",
|
|
931
|
+
category: "standardization_warning",
|
|
932
|
+
code: "version.page_kit_below_preferred_cutoff",
|
|
933
|
+
message: `Page Kit dependency ${root.identity.page_kit_dependency.version} is below the preferred ${PREFERRED_PAGE_KIT_MIN}+ sample cutoff.`,
|
|
934
|
+
next_action: "Check migration notes before bumping next-campaign-page-kit.",
|
|
935
|
+
}));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function addTemplateFamilyFindings(root) {
|
|
940
|
+
if (!root.identity.template_family.value) {
|
|
941
|
+
root.findings.push(finding({
|
|
942
|
+
severity: "operator_readiness",
|
|
943
|
+
category: "operator_readiness",
|
|
944
|
+
code: "template_family.unknown",
|
|
945
|
+
message: "Template family is unknown; family-specific source and built-output checks remain tentative.",
|
|
946
|
+
next_action: "Rerun with --family <template-family> or attach Campaigns OS assembly artifacts.",
|
|
947
|
+
}));
|
|
948
|
+
} else if (root.identity.template_family.confidence === "tentative") {
|
|
949
|
+
root.findings.push(finding({
|
|
950
|
+
severity: "operator_readiness",
|
|
951
|
+
category: "operator_readiness",
|
|
952
|
+
code: "template_family.tentative",
|
|
953
|
+
message: `Template family inferred as ${root.identity.template_family.value}, but only from source hints.`,
|
|
954
|
+
next_action: "Confirm the family against CampaignSpec or .campaign-runtime before treating family-specific findings as blockers.",
|
|
955
|
+
}));
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function buildRemediation(root) {
|
|
960
|
+
const safe = [];
|
|
961
|
+
const clarification = [];
|
|
962
|
+
const risks = [];
|
|
963
|
+
for (const item of root.findings) {
|
|
964
|
+
if (item.code === "source.hardcoded_root_assets") safe.push("Rewrite hardcoded /assets refs through campaign_asset.");
|
|
965
|
+
if (item.code === "source.document_wrappers") safe.push("Move page-level document wrappers into layouts.");
|
|
966
|
+
if (item.code === "source.raw_block") safe.push("Remove Liquid raw blocks while preserving intended page-owned markup.");
|
|
967
|
+
if (item.code === "built_doctor.template_contract.literal_residue") safe.push("Remove generic starter/template residue from built source assets and pages.");
|
|
968
|
+
if (item.code === "template_family.unknown" || item.code === "template_family.tentative") clarification.push("Confirm the template family from CampaignSpec or Campaigns OS artifacts.");
|
|
969
|
+
if (item.code === "source.payment_methods_include_not_detected") clarification.push("Confirm whether the template family expects payment-methods.html or an equivalent include.");
|
|
970
|
+
if (item.code === "campaigns_os.source_manifest_missing") clarification.push("Find CampaignSpec/Map ID or source-html manifest before relying on provenance.");
|
|
971
|
+
if (item.code.startsWith("version.")) risks.push("Version bumps can affect SDK/Page Kit runtime behavior; confirm against campaign QA scope before changing.");
|
|
972
|
+
}
|
|
973
|
+
if (!root.identity.campaign_slugs.some((entry) => entry.store_url)) {
|
|
974
|
+
risks.push("Production storefront/deploy URL is unknown.");
|
|
975
|
+
}
|
|
976
|
+
const proof = [
|
|
977
|
+
`campaigns-os standardize --target ${shellToken(root.identity.page_kit_root)} --json`,
|
|
978
|
+
];
|
|
979
|
+
const family = root.identity.template_family.value;
|
|
980
|
+
const familyConfirmed = family && root.identity.template_family.confidence !== "tentative";
|
|
981
|
+
// The doctor proof command carries --slug whenever scope needed one: the
|
|
982
|
+
// resolved slug when there is one, a placeholder when the scope is still
|
|
983
|
+
// unresolved among several built directories.
|
|
984
|
+
const slugFlag = root.built_output.slug
|
|
985
|
+
? ` --slug ${shellToken(root.built_output.slug)}`
|
|
986
|
+
: root.built_output.present && root.built_output.scope_resolved === false && root.built_output.slug_candidates?.length
|
|
987
|
+
? " --slug <slug>"
|
|
988
|
+
: "";
|
|
989
|
+
if (root.built_output.present && familyConfirmed) {
|
|
990
|
+
proof.push(`campaigns-os doctor --built ${shellToken(root.identity.page_kit_root)} --family ${shellToken(family)}${slugFlag} --json`);
|
|
991
|
+
} else if (root.built_output.present) {
|
|
992
|
+
proof.push(`campaigns-os doctor --built ${shellToken(root.identity.page_kit_root)} --family <template-family>${slugFlag} --json`);
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
safe_agent_repairs: unique(safe),
|
|
996
|
+
clarification_needed: unique(clarification),
|
|
997
|
+
product_or_merchant_risks: unique(risks),
|
|
998
|
+
proof_commands: proof,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Certification freshness (#263) for the report's identity block, read from
|
|
1003
|
+
// the vendored catalog snapshot only — the report never fetches verification
|
|
1004
|
+
// data live. Report generation must never crash on a malformed local catalog,
|
|
1005
|
+
// so resolution failures degrade to null (freshness simply not reported) —
|
|
1006
|
+
// but not silently: the operator staring at a missing freshness field gets
|
|
1007
|
+
// one warn per run saying why (one-shot, not repeated per root).
|
|
1008
|
+
let freshnessSuppressionWarned = false;
|
|
1009
|
+
|
|
1010
|
+
export function resetFreshnessSuppressionWarning() {
|
|
1011
|
+
freshnessSuppressionWarned = false;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function assessRootCertificationFreshness(family, sdkSupportPolicy = null) {
|
|
1015
|
+
if (!family) return null;
|
|
1016
|
+
let catalog;
|
|
1017
|
+
try {
|
|
1018
|
+
catalog = resolveCommerceCatalog();
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
if (!freshnessSuppressionWarned) {
|
|
1021
|
+
freshnessSuppressionWarned = true;
|
|
1022
|
+
console.warn(`[standardize] freshness suppressed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1023
|
+
}
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
const assessment = assessTemplateFreshness({
|
|
1027
|
+
family,
|
|
1028
|
+
catalog,
|
|
1029
|
+
sdkSupportPolicy: sdkSupportPolicy && typeof sdkSupportPolicy === "object" ? sdkSupportPolicy : defaultSdkSupportPolicy(),
|
|
1030
|
+
});
|
|
1031
|
+
return { ...assessment, summary: renderTemplateFreshness(assessment) };
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function inferTemplateFamily({ explicitTemplateFamily, runtime, files, rootPath }) {
|
|
1035
|
+
if (explicitTemplateFamily) {
|
|
1036
|
+
return { value: explicitTemplateFamily, source: "operator_flag", confidence: "explicit" };
|
|
1037
|
+
}
|
|
1038
|
+
const runtimeCandidates = [];
|
|
1039
|
+
for (const artifact of runtime.artifactFiles || []) {
|
|
1040
|
+
if (!artifact.path.endsWith(".json")) continue;
|
|
1041
|
+
const parsed = readJsonFile(artifact.path);
|
|
1042
|
+
if (!parsed.ok) continue;
|
|
1043
|
+
const value = firstStringAt(parsed.value, [
|
|
1044
|
+
["template", "value"],
|
|
1045
|
+
["template_family", "value"],
|
|
1046
|
+
["assembly", "template_family"],
|
|
1047
|
+
["template_family"],
|
|
1048
|
+
["campaign", "template_family"],
|
|
1049
|
+
["spec", "preferred_template_family"],
|
|
1050
|
+
]);
|
|
1051
|
+
if (value) runtimeCandidates.push({ value, path: relPath(rootPath, artifact.path) });
|
|
1052
|
+
}
|
|
1053
|
+
if (runtimeCandidates.length) {
|
|
1054
|
+
return { value: runtimeCandidates[0].value, source: runtimeCandidates[0].path, confidence: "artifact" };
|
|
1055
|
+
}
|
|
1056
|
+
const sourceHint = inferTemplateFamilyFromPaths(files, rootPath);
|
|
1057
|
+
if (sourceHint) {
|
|
1058
|
+
return { value: sourceHint, source: "source_path_hint", confidence: "tentative" };
|
|
1059
|
+
}
|
|
1060
|
+
return { value: null, source: null, confidence: "unknown" };
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function inferTemplateFamilyFromPaths(files, rootPath) {
|
|
1064
|
+
const tokenSets = files
|
|
1065
|
+
.map((file) => relPath(rootPath, file).toLowerCase())
|
|
1066
|
+
.filter((rel) => !rel.includes("/assets/"))
|
|
1067
|
+
.map((rel) => rel.split("/").flatMap(componentTokens));
|
|
1068
|
+
if (tokenSets.some((tokens) => hasTokenSequence(tokens, ["olympus", "mv"]) || hasTokenSequence(tokens, ["upsell", "mv"]))) return "olympus-mv-single-step";
|
|
1069
|
+
if (tokenSets.some((tokens) => hasTokenSequence(tokens, ["shop", "three", "step"]))) return "shop-three-step";
|
|
1070
|
+
if (tokenSets.some((tokens) => hasTokenSequence(tokens, ["shop", "single", "step"]))) return "shop-single-step";
|
|
1071
|
+
if (tokenSets.some((tokens) => tokens.includes("olympus"))) return "olympus";
|
|
1072
|
+
if (tokenSets.some((tokens) => tokens.includes("apollo"))) return "apollo";
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function summarizeDoctorResult(result) {
|
|
1077
|
+
if (!result) return { status: "not_run", reason: "doctor did not return a result" };
|
|
1078
|
+
return {
|
|
1079
|
+
status: result.status || (result.ok ? "ready" : "blocked"),
|
|
1080
|
+
ok: Boolean(result.ok),
|
|
1081
|
+
mode: result.mode || null,
|
|
1082
|
+
error_count: result.errors?.length || 0,
|
|
1083
|
+
warning_count: result.warnings?.length || 0,
|
|
1084
|
+
ready_count: result.ready?.length || 0,
|
|
1085
|
+
errors: (result.errors || []).map(summarizeIssue),
|
|
1086
|
+
warnings: (result.warnings || []).map(summarizeIssue),
|
|
1087
|
+
ready: (result.ready || []).slice(0, 12),
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function summarizeIssue(issue) {
|
|
1092
|
+
if (typeof issue === "string") return { message: issue };
|
|
1093
|
+
return {
|
|
1094
|
+
code: issue?.code || null,
|
|
1095
|
+
message: issue?.message || String(issue),
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function collectDataNextAttributes(counts, content) {
|
|
1100
|
+
for (const match of content.matchAll(/\bdata-next-[a-zA-Z0-9_-]+/g)) {
|
|
1101
|
+
const attr = match[0];
|
|
1102
|
+
counts.set(attr, (counts.get(attr) || 0) + 1);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function summarizeDataNext(counts) {
|
|
1107
|
+
const attrs = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
1108
|
+
return {
|
|
1109
|
+
total_occurrences: sumMap(counts),
|
|
1110
|
+
unique_attributes: attrs.map(([name]) => name),
|
|
1111
|
+
top_attributes: attrs.slice(0, 12).map(([name, count]) => ({ name, count })),
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function formatBuiltSiteState(root) {
|
|
1116
|
+
if (!root.identity.has_built_site) return "no";
|
|
1117
|
+
return root.built_output?.scope_resolved === false ? "unresolved" : "yes";
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function formatBuiltSlug(root) {
|
|
1121
|
+
const built = root.built_output || {};
|
|
1122
|
+
if (!built.present) return "(no built _site)";
|
|
1123
|
+
if (built.scope_resolved === false) {
|
|
1124
|
+
const candidates = built.slug_candidates?.length ? ` (built directories: ${built.slug_candidates.join(", ")})` : "";
|
|
1125
|
+
if (!built.slug) return `unresolved${candidates}`;
|
|
1126
|
+
return built.slug_directory_present ? `${built.slug} has no HTML pages${candidates}` : `${built.slug} not found${candidates}`;
|
|
1127
|
+
}
|
|
1128
|
+
return built.slug ? `${built.slug} (${built.slug_source})` : `site root (${built.slug_source || "site_layout"})`;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Html-bearing directories directly under _site/, the same set the scope
|
|
1132
|
+
// resolver offers as candidates when no slug is known.
|
|
1133
|
+
function builtSlugCandidates(siteRoot) {
|
|
1134
|
+
return safeReadDir(siteRoot)
|
|
1135
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("_") && !entry.name.startsWith(".") && entry.name !== "node_modules")
|
|
1136
|
+
.map((entry) => entry.name)
|
|
1137
|
+
.filter((name) => listFiles(join(siteRoot, name), { includeRuntime: true }).some((file) => file.toLowerCase().endsWith(".html")))
|
|
1138
|
+
.sort();
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function isPaymentMethodsInclude(file, content) {
|
|
1142
|
+
if (basename(file) === "payment-methods.html") return true;
|
|
1143
|
+
return /{%\s*(?:campaign_)?include\s+["'][^"']*payment-methods(?:\.html)?["']/i.test(content);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function documentWrapperMatchIndex(content) {
|
|
1147
|
+
const masked = maskHtmlAndLiquidComments(content);
|
|
1148
|
+
const head = masked.slice(0, 4096);
|
|
1149
|
+
const htmlMatch = head.search(/<\s*html\b/i);
|
|
1150
|
+
if (htmlMatch >= 0) return htmlMatch;
|
|
1151
|
+
const bodyMatch = head.search(/<\s*body\b/i);
|
|
1152
|
+
if (bodyMatch >= 0 && /<\/\s*(?:body|html)\s*>/i.test(masked)) return bodyMatch;
|
|
1153
|
+
return -1;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function maskHtmlAndLiquidComments(content) {
|
|
1157
|
+
return String(content || "")
|
|
1158
|
+
.replace(/<!--[\s\S]*?-->/g, maskIgnoredRegion)
|
|
1159
|
+
.replace(/{%-?\s*comment\s*-?%}[\s\S]*?{%-?\s*endcomment\s*-?%}/gi, maskIgnoredRegion);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function maskScriptsAndComments(content) {
|
|
1163
|
+
return maskHtmlAndLiquidComments(content)
|
|
1164
|
+
.replace(/(<script\b[^>]*>)([\s\S]*?)(<\/script\s*>)/gi, (_match, open, body, close) =>
|
|
1165
|
+
`${open}${maskIgnoredRegion(body)}${close}`
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function maskIgnoredRegion(value) {
|
|
1170
|
+
return String(value).replace(/[^\r\n]/g, " ");
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function componentTokens(component) {
|
|
1174
|
+
return component
|
|
1175
|
+
.replace(/\.[a-z0-9]+$/i, "")
|
|
1176
|
+
.split(/[^a-z0-9]+/i)
|
|
1177
|
+
.filter(Boolean);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function hasTokenSequence(tokens, sequence) {
|
|
1181
|
+
for (let index = 0; index <= tokens.length - sequence.length; index += 1) {
|
|
1182
|
+
if (sequence.every((token, offset) => tokens[index + offset] === token)) return true;
|
|
1183
|
+
}
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function collectHardcodedAssetSamples(out, rootPath, file, content, slugs) {
|
|
1188
|
+
const searchable = maskScriptsAndComments(content);
|
|
1189
|
+
collectPatternSamples(out, rootPath, file, content, HARDCODED_ROOT_ASSET_ATTR_PATTERN, searchable);
|
|
1190
|
+
collectPatternSamples(out, rootPath, file, content, HARDCODED_ROOT_ASSET_URL_PATTERN, searchable);
|
|
1191
|
+
for (const entry of slugs || []) {
|
|
1192
|
+
const slug = escapeRegExp(entry.slug);
|
|
1193
|
+
if (!slug) continue;
|
|
1194
|
+
collectPatternSamples(out, rootPath, file, content, new RegExp(`\\b(?:src|href)\\s*=\\s*(["'])/${slug}/assets/[^"']*\\1`, "g"), searchable);
|
|
1195
|
+
collectPatternSamples(out, rootPath, file, content, new RegExp(`\\burl\\(\\s*(["']?)/${slug}/assets/[^)"']*\\1\\s*\\)`, "g"), searchable);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function collectPatternSamples(out, rootPath, file, content, pattern, searchableContent = content) {
|
|
1200
|
+
const globalPattern = withGlobalFlag(pattern);
|
|
1201
|
+
globalPattern.lastIndex = 0;
|
|
1202
|
+
for (const match of String(searchableContent).matchAll(globalPattern)) {
|
|
1203
|
+
const index = match.index || 0;
|
|
1204
|
+
out.push(sample(rootPath, file, content, index, String(content).slice(index, index + match[0].length)));
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function sample(rootPath, file, content, index, match) {
|
|
1209
|
+
const before = content.slice(0, Math.max(0, index));
|
|
1210
|
+
return {
|
|
1211
|
+
path: relPath(rootPath, file),
|
|
1212
|
+
line: before.split(/\r?\n/).length,
|
|
1213
|
+
match,
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function safeReadText(path) {
|
|
1218
|
+
try {
|
|
1219
|
+
return { ok: true, value: readFileSync(path, "utf8") };
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
return { ok: false, error: error.message || String(error) };
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function builtDoctorFindingCode(issue, fallback, index) {
|
|
1226
|
+
const code = normalizeString(issue?.code);
|
|
1227
|
+
return `built_doctor.${code || `${fallback}.${index + 1}`}`;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function withGlobalFlag(pattern) {
|
|
1231
|
+
if (!(pattern instanceof RegExp)) return new RegExp(String(pattern), "g");
|
|
1232
|
+
if (pattern.flags.includes("g")) return pattern;
|
|
1233
|
+
return new RegExp(pattern.source, `${pattern.flags}g`);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function readJsonFile(path) {
|
|
1237
|
+
try {
|
|
1238
|
+
return { ok: true, value: JSON.parse(readFileSync(path, "utf8")) };
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
return { ok: false, error: error.message };
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function countLiteral(content, needle) {
|
|
1245
|
+
return content.split(needle).length - 1;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function finding({ severity, category, code, message, evidence = null, next_action = null }) {
|
|
1249
|
+
return {
|
|
1250
|
+
severity,
|
|
1251
|
+
category,
|
|
1252
|
+
code,
|
|
1253
|
+
message,
|
|
1254
|
+
evidence,
|
|
1255
|
+
next_action,
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function dedupeFindings(root) {
|
|
1260
|
+
const seen = new Set();
|
|
1261
|
+
root.findings = root.findings.filter((item) => {
|
|
1262
|
+
const key = `${item.severity}:${item.code}:${item.message}`;
|
|
1263
|
+
if (seen.has(key)) return false;
|
|
1264
|
+
seen.add(key);
|
|
1265
|
+
return true;
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function firstStringAt(object, paths) {
|
|
1270
|
+
for (const path of paths) {
|
|
1271
|
+
let cursor = object;
|
|
1272
|
+
for (const part of path) cursor = cursor?.[part];
|
|
1273
|
+
const value = normalizeString(cursor);
|
|
1274
|
+
if (value) return value;
|
|
1275
|
+
}
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function sumMap(map) {
|
|
1280
|
+
let total = 0;
|
|
1281
|
+
for (const value of map.values()) total += value;
|
|
1282
|
+
return total;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function isObject(value) {
|
|
1286
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function appendList(lines, label, items) {
|
|
1290
|
+
lines.push(`${label}:`);
|
|
1291
|
+
if (!items?.length) {
|
|
1292
|
+
lines.push("- none");
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
for (const item of items) lines.push(`- ${item}`);
|
|
1296
|
+
}
|
|
1297
|
+
|