@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.
Files changed (310) hide show
  1. package/AGENTS.md +204 -0
  2. package/CHANGELOG.md +5002 -0
  3. package/CONTEXT.md +685 -0
  4. package/LICENSE +202 -0
  5. package/NOTICE +4 -0
  6. package/README.md +368 -0
  7. package/agents/claude/CLAUDE.md +32 -0
  8. package/agents/codex/AGENTS.md +27 -0
  9. package/agents/copilot/copilot-instructions.md +14 -0
  10. package/agents/cursor/campaigns-os.mdc +13 -0
  11. package/bin/campaigns-os.mjs +38 -0
  12. package/campaign-spec/README.md +138 -0
  13. package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
  14. package/campaign-spec/dist/analytics-vocabulary.js +74 -0
  15. package/campaign-spec/dist/index.d.ts +40 -0
  16. package/campaign-spec/dist/index.js +77 -0
  17. package/campaign-spec/dist/normalize.d.ts +22 -0
  18. package/campaign-spec/dist/normalize.js +41 -0
  19. package/campaign-spec/dist/routing.d.ts +190 -0
  20. package/campaign-spec/dist/routing.js +263 -0
  21. package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
  22. package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
  23. package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
  24. package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
  25. package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
  26. package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
  27. package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
  28. package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
  29. package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
  30. package/campaign-spec/dist/rules/cycle-detection.js +141 -0
  31. package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
  32. package/campaign-spec/dist/rules/design-source-shape.js +142 -0
  33. package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
  34. package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
  35. package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
  36. package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
  37. package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
  38. package/campaign-spec/dist/rules/funnel-count.js +37 -0
  39. package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
  40. package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
  41. package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
  42. package/campaign-spec/dist/rules/funnel-identity.js +57 -0
  43. package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
  44. package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
  45. package/campaign-spec/dist/rules/index.d.ts +69 -0
  46. package/campaign-spec/dist/rules/index.js +131 -0
  47. package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
  48. package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
  49. package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
  50. package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
  51. package/campaign-spec/dist/rules/page-count.d.ts +11 -0
  52. package/campaign-spec/dist/rules/page-count.js +31 -0
  53. package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
  54. package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
  55. package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
  56. package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
  57. package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
  58. package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
  59. package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
  60. package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
  61. package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
  62. package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
  63. package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
  64. package/campaign-spec/dist/rules/schema-version.js +53 -0
  65. package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
  66. package/campaign-spec/dist/rules/sdk-version.js +96 -0
  67. package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
  68. package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
  69. package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
  70. package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
  71. package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
  72. package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
  73. package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
  74. package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
  75. package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
  76. package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
  77. package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
  78. package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
  79. package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
  80. package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
  81. package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
  82. package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
  83. package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
  84. package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
  85. package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
  86. package/campaign-spec/dist/sdk-version-parse.js +62 -0
  87. package/campaign-spec/dist/types.d.ts +674 -0
  88. package/campaign-spec/dist/types.js +40 -0
  89. package/campaign-spec/package.json +12 -0
  90. package/compatibility.json +25 -0
  91. package/contracts/agent-relevant-change-policy.v1.json +111 -0
  92. package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
  93. package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
  94. package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
  95. package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
  96. package/contracts/commerce-surface-catalog.json +2452 -0
  97. package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
  98. package/contracts/fixtures/orientation/envelope/current.json +95 -0
  99. package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
  100. package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
  101. package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
  102. package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
  103. package/contracts/fixtures/orientation/envelope/refused.json +100 -0
  104. package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
  105. package/contracts/fixtures/orientation/envelope/updated.json +135 -0
  106. package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
  107. package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
  108. package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
  109. package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
  110. package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
  111. package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
  112. package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
  113. package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
  114. package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
  115. package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
  116. package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
  117. package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
  118. package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
  119. package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
  120. package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
  121. package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
  122. package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
  123. package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
  124. package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
  125. package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
  126. package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
  127. package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
  128. package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
  129. package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
  130. package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
  131. package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
  132. package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
  133. package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
  134. package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
  135. package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
  136. package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
  137. package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
  138. package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
  139. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
  140. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
  141. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
  142. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
  143. package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
  144. package/contracts/migration-sidecar-bundle.v0.json +149 -0
  145. package/contracts/orientation-limits.v1.json +41 -0
  146. package/contracts/orientation-reason-codes.v1.json +196 -0
  147. package/contracts/private-template-sources.json +8 -0
  148. package/contracts/release-ledger.json +4598 -0
  149. package/contracts/reserved-skill-names.json +13 -0
  150. package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
  151. package/contracts/supported-surface.json +180 -0
  152. package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
  153. package/contracts/template-brand-contract.apollo.v0.json +27 -0
  154. package/contracts/template-brand-contract.demeter.v0.json +27 -0
  155. package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
  156. package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
  157. package/contracts/template-brand-contract.olympus.v0.json +27 -0
  158. package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
  159. package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
  160. package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
  161. package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
  162. package/contracts/template-slot-manifest.apollo.v0.json +12 -0
  163. package/contracts/template-slot-manifest.demeter.v0.json +30 -0
  164. package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
  165. package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
  166. package/contracts/template-slot-manifest.olympus.v0.json +12 -0
  167. package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
  168. package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
  169. package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
  170. package/docs/brand-theme-bridge.md +159 -0
  171. package/docs/build-packet.md +1300 -0
  172. package/docs/campaign-build-brief.md +145 -0
  173. package/docs/campaign-standardization-report.md +329 -0
  174. package/docs/campaigns-os-build-flow.md +117 -0
  175. package/docs/design-source-package.md +784 -0
  176. package/docs/legacy-migration.md +58 -0
  177. package/docs/migration-sidecar-bundle.md +139 -0
  178. package/docs/orientation-contract-reference.md +1220 -0
  179. package/docs/polish-evidence.md +502 -0
  180. package/docs/qa-and-test-orders.md +1691 -0
  181. package/docs/release-ledger-authoring-guide.md +274 -0
  182. package/docs/runtime-readiness.md +211 -0
  183. package/docs/supported-surface.md +83 -0
  184. package/docs/versioning.md +55 -0
  185. package/docs/workflow-findings-sidecar.md +588 -0
  186. package/package.json +135 -0
  187. package/prompts/first-build.md +27 -0
  188. package/prompts/friction-log.md +30 -0
  189. package/schemas/campaign-build-brief.v1.schema.json +149 -0
  190. package/schemas/campaign-design-source-package.v0.schema.json +697 -0
  191. package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
  192. package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
  193. package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
  194. package/schemas/campaign-spec.v4.schema.json +582 -0
  195. package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
  196. package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
  197. package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
  198. package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
  199. package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
  200. package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
  201. package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
  202. package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
  203. package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
  204. package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
  205. package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
  206. package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
  207. package/schemas/source-html-manifest.v0.schema.json +252 -0
  208. package/skills/next-campaigns-build/SKILL.md +73 -0
  209. package/skills/next-campaigns-os/SKILL.md +101 -0
  210. package/skills/next-campaigns-os/references/session-intake.md +160 -0
  211. package/skills/next-campaigns-os-setup/SKILL.md +22 -0
  212. package/skills/next-campaigns-polish/SKILL.md +145 -0
  213. package/skills/next-campaigns-qa/SKILL.md +92 -0
  214. package/skills.json +56 -0
  215. package/skills.sh +64 -0
  216. package/src/adapter-decision-contract.mjs +333 -0
  217. package/src/brand-theme.mjs +1151 -0
  218. package/src/browser-launch.mjs +79 -0
  219. package/src/build-brief.mjs +781 -0
  220. package/src/built-site-scope.mjs +312 -0
  221. package/src/campaign-ecosystem.mjs +734 -0
  222. package/src/campaign-identity.mjs +405 -0
  223. package/src/campaign-workspace.mjs +127 -0
  224. package/src/checkpoint-waiver.mjs +302 -0
  225. package/src/cli.mjs +13442 -0
  226. package/src/commercial-journey.mjs +1119 -0
  227. package/src/commercial-parity.mjs +965 -0
  228. package/src/consent.mjs +347 -0
  229. package/src/content-residue.mjs +322 -0
  230. package/src/deadline.mjs +81 -0
  231. package/src/design-source-package.mjs +2604 -0
  232. package/src/deviation.mjs +107 -0
  233. package/src/doctor-check-registry.mjs +49 -0
  234. package/src/doctor-sidecar.mjs +106 -0
  235. package/src/finding-cause.mjs +557 -0
  236. package/src/findings.mjs +326 -0
  237. package/src/fs-identity.mjs +68 -0
  238. package/src/gate-actions.mjs +105 -0
  239. package/src/html-scan.mjs +53 -0
  240. package/src/install-mode.mjs +272 -0
  241. package/src/legacy-migration.d.ts +128 -0
  242. package/src/legacy-migration.mjs +510 -0
  243. package/src/lifecycle.mjs +338 -0
  244. package/src/local-proof.mjs +401 -0
  245. package/src/map-pin-writeback.mjs +210 -0
  246. package/src/orchestration-stage-contract.mjs +81 -0
  247. package/src/package-install-fixture.mjs +33 -0
  248. package/src/page-kit-build-summary.mjs +175 -0
  249. package/src/page-kit-campaign-config.mjs +57 -0
  250. package/src/page-kit-sdk-version.mjs +392 -0
  251. package/src/page-kit-store-profile.mjs +369 -0
  252. package/src/page-kit-sync.mjs +162 -0
  253. package/src/polish-browser.mjs +867 -0
  254. package/src/polish-capture.mjs +1094 -0
  255. package/src/polish-deadline.mjs +51 -0
  256. package/src/polish-gate.mjs +739 -0
  257. package/src/polish-node.mjs +639 -0
  258. package/src/polish-page-load.mjs +1100 -0
  259. package/src/private-template-source.mjs +237 -0
  260. package/src/proof-policy.mjs +82 -0
  261. package/src/qa-analytics-correctness.mjs +307 -0
  262. package/src/qa-analytics-errors.mjs +38 -0
  263. package/src/qa-analytics-parity.mjs +699 -0
  264. package/src/qa-binding-evidence.mjs +140 -0
  265. package/src/qa-browser.mjs +6608 -0
  266. package/src/qa-cart-entry.mjs +406 -0
  267. package/src/qa-commercial-parity.mjs +641 -0
  268. package/src/qa-node.mjs +3620 -0
  269. package/src/qa-order-bump.mjs +381 -0
  270. package/src/qa-parity-capture.mjs +428 -0
  271. package/src/qa-parity-fixture.mjs +359 -0
  272. package/src/qa-publish.mjs +362 -0
  273. package/src/qa-purchase-data-layer.mjs +263 -0
  274. package/src/qa-route-probe.mjs +272 -0
  275. package/src/qa-sidecar.mjs +188 -0
  276. package/src/qa-test-order-topology.mjs +207 -0
  277. package/src/qa-url-privacy.mjs +13 -0
  278. package/src/qa-verdict-discovery.mjs +192 -0
  279. package/src/qa-verdict-publish.mjs +105 -0
  280. package/src/qa-verdict.mjs +287 -0
  281. package/src/remit.mjs +388 -0
  282. package/src/repo-scan.mjs +83 -0
  283. package/src/route-identity.mjs +133 -0
  284. package/src/run-record-closeout.mjs +229 -0
  285. package/src/run-record.mjs +839 -0
  286. package/src/run-session.mjs +226 -0
  287. package/src/runtime-state-ignore.mjs +113 -0
  288. package/src/sdk-attribute-index.mjs +212 -0
  289. package/src/sdk-markup.mjs +358 -0
  290. package/src/sdk-meta-tags.mjs +52 -0
  291. package/src/shell-token.mjs +7 -0
  292. package/src/sidecar-bundle.mjs +399 -0
  293. package/src/source-asset-crawl.mjs +469 -0
  294. package/src/source-html-intake.mjs +627 -0
  295. package/src/source-html-manifest.mjs +276 -0
  296. package/src/source-prep.mjs +284 -0
  297. package/src/spec-derive-store.mjs +431 -0
  298. package/src/spec-derive.mjs +514 -0
  299. package/src/spec-fetch.mjs +66 -0
  300. package/src/spec-hash.mjs +48 -0
  301. package/src/spec-identity.mjs +27 -0
  302. package/src/stage-ledger.mjs +509 -0
  303. package/src/standardization-report.mjs +1297 -0
  304. package/src/template-brand-contract.mjs +473 -0
  305. package/src/template-freshness.mjs +196 -0
  306. package/src/template-reference.mjs +81 -0
  307. package/src/template-slot-manifest.mjs +150 -0
  308. package/src/text-safety.mjs +62 -0
  309. package/src/theme-gate.mjs +185 -0
  310. package/src/upsell-selector-scope.mjs +299 -0
@@ -0,0 +1,226 @@
1
+ // Run Telemetry — ambient run session for Campaigns OS.
2
+ // See docs/workflow-findings-sidecar.md (Run Telemetry).
3
+ //
4
+ // A run session makes telemetry AMBIENT: `run start` writes one small file to
5
+ // the project's .campaign-runtime/, and every subsequent command auto-discovers
6
+ // it (walking up from cwd) to share one run_id and one lifecycle journal —
7
+ // WITHOUT the operator threading --run-id / --lifecycle-journal on each call.
8
+ // `run end` assembles the aggregated Run Record and clears the session. This is
9
+ // the experience for "talk to your agent and build": the agent runs `run start`
10
+ // once, builds, then `run end`. No flag bookkeeping.
11
+ //
12
+ // The session file is transient, machine-local, and lives under the
13
+ // scrubber-ignored .campaign-runtime/. No network, no credentials.
14
+
15
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
16
+ import { randomBytes } from "node:crypto";
17
+ import { homedir } from "node:os";
18
+ import { dirname, join, parse, resolve, sep } from "node:path";
19
+
20
+ import { LIFECYCLE_JOURNAL_REL_PATH } from "./lifecycle.mjs";
21
+ import { sameFile } from "./fs-identity.mjs";
22
+
23
+ export const RUN_SESSION_SCHEMA = "campaigns-os-run-session/v0";
24
+ export const RUN_SESSION_REL_PATH = ".campaign-runtime/run-session.json";
25
+ export const RUN_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
26
+
27
+ // Files that mark a project root. Session discovery never climbs ABOVE the
28
+ // nearest project root, so a stray session in an unrelated ancestor (e.g.
29
+ // $HOME or a shared /tmp) can't hijack commands run inside a real project.
30
+ const PROJECT_ROOT_MARKERS = [".git", "package.json"];
31
+
32
+ function isNonEmptyString(value) {
33
+ return typeof value === "string" && value.trim().length > 0;
34
+ }
35
+
36
+ function isProjectRoot(dir) {
37
+ return PROJECT_ROOT_MARKERS.some((marker) => existsSync(join(dir, marker)));
38
+ }
39
+
40
+ /** Canonical session path for a project root: <root>/.campaign-runtime/run-session.json */
41
+ export function resolveRunSessionPath(rootDir = process.cwd()) {
42
+ return join(resolve(rootDir), RUN_SESSION_REL_PATH);
43
+ }
44
+
45
+ /**
46
+ * Walk up from `cwd` to find an active run session, so commands run from a
47
+ * subdirectory still share the project's session. Best-effort: never throws; a
48
+ * missing or malformed session file simply means "no active session".
49
+ * Returns `{ session, path, dir }` or null.
50
+ *
51
+ * Scope is bounded to the project to prevent a stray ancestor session from
52
+ * hijacking unrelated commands:
53
+ * - the walk STOPS at the nearest project root (a dir with .git/package.json),
54
+ * so a session in an ancestor ABOVE the project is never adopted;
55
+ * - a session located at $HOME, ANY ancestor of $HOME (e.g. /Users), or the
56
+ * filesystem root is never honored — those are shared dirs, not project roots.
57
+ *
58
+ * `home` is injectable for tests.
59
+ */
60
+ export function isRunSessionStale(session, { now = new Date(), ttlMs = RUN_SESSION_TTL_MS } = {}) {
61
+ if (!session || typeof session !== "object" || Array.isArray(session)) return false;
62
+ if (!Number.isFinite(ttlMs) || ttlMs < 0) return false;
63
+ const activity = isNonEmptyString(session.updated_at) ? session.updated_at : session.started_at;
64
+ const timestamp = Date.parse(activity || "");
65
+ if (!Number.isFinite(timestamp)) return false;
66
+ return now.getTime() - timestamp > ttlMs;
67
+ }
68
+
69
+ export function isRunSessionTerminal(session) {
70
+ if (!session || typeof session !== "object" || Array.isArray(session)) return false;
71
+ return session.terminal === true
72
+ || session.status === "terminal";
73
+ }
74
+
75
+ export function findRunSession(cwd = process.cwd(), { home = homedir(), now = new Date(), ttlMs = RUN_SESSION_TTL_MS } = {}) {
76
+ let dir = resolve(cwd);
77
+ const { root } = parse(dir);
78
+ const resolvedHome = resolve(home);
79
+ // True when `dir` is $HOME itself or a strict ancestor of it (e.g. /Users).
80
+ const isHomeOrAncestor = (d) => d === resolvedHome || resolvedHome.startsWith(d + sep);
81
+ for (let depth = 0; depth < 64; depth += 1) {
82
+ const candidate = join(dir, RUN_SESSION_REL_PATH);
83
+ if (existsSync(candidate)) {
84
+ // A session at the filesystem root, $HOME, or an ancestor of $HOME is not
85
+ // a project session — honoring it would capture every command beneath it.
86
+ if (dir === root || isHomeOrAncestor(dir)) return null;
87
+ try {
88
+ const session = JSON.parse(readFileSync(candidate, "utf8"));
89
+ if (session && typeof session === "object" && !Array.isArray(session) && isNonEmptyString(session.run_id) && !isRunSessionStale(session, { now, ttlMs })) {
90
+ return { session, path: candidate, dir };
91
+ }
92
+ } catch {
93
+ // malformed session file => treat as no active session
94
+ }
95
+ return null;
96
+ }
97
+ // Don't climb past the project boundary: the session must live within the
98
+ // project being built, not in an unrelated ancestor directory.
99
+ if (isProjectRoot(dir)) break;
100
+ if (dir === root) break;
101
+ dir = dirname(dir);
102
+ }
103
+ return null;
104
+ }
105
+
106
+ /**
107
+ * The session file at `rootDir` when it exists but is STALE (idle past the
108
+ * TTL). findRunSession deliberately ignores such a file so a new work session
109
+ * never inherits an old run_id — but ignoring it also meant abandoning it: the
110
+ * file lingered, its lifecycle journal was never assembled into a Run Record,
111
+ * and nothing was ever remitted for that run. Callers that are about to open a
112
+ * new session (start / prepare-build / run start) use this to close the old
113
+ * one out first. Exact-path only, no upward walk: sweeping is a write, and it
114
+ * must never reach into an ancestor project. Never throws.
115
+ */
116
+ export function findStaleRunSession(rootDir = process.cwd(), { home = homedir(), now = new Date(), ttlMs = RUN_SESSION_TTL_MS } = {}) {
117
+ const dir = resolve(rootDir);
118
+ // Same boundary findRunSession enforces: the filesystem root, $HOME, or an
119
+ // ancestor of $HOME is a shared directory, never a project — and this
120
+ // caller goes on to assemble, remit, and delete.
121
+ const { root } = parse(dir);
122
+ const resolvedHome = resolve(home);
123
+ if (dir === root || dir === resolvedHome || resolvedHome.startsWith(dir + sep)) return null;
124
+ const path = resolveRunSessionPath(dir);
125
+ if (!existsSync(path)) return null;
126
+ try {
127
+ const session = JSON.parse(readFileSync(path, "utf8"));
128
+ if (!session || typeof session !== "object" || Array.isArray(session) || !isNonEmptyString(session.run_id)) return null;
129
+ if (!isRunSessionStale(session, { now, ttlMs })) return null;
130
+ return { session, path, dir: resolve(rootDir) };
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ /** Mint the session's canonical run_id (same shape as the run-record minter). */
137
+ export function mintSessionRunId(now = new Date()) {
138
+ return `run_${now.getTime()}_${randomBytes(4).toString("hex")}`;
139
+ }
140
+
141
+ /** Build the session object (its own schema_version, the run_id, the journal it owns, an optional packet, and a timestamp). */
142
+ export function buildRunSession({ runId, lifecycleJournal, packet = null, now = new Date() }) {
143
+ const createdAt = now.toISOString();
144
+ return {
145
+ schema_version: RUN_SESSION_SCHEMA,
146
+ run_id: runId,
147
+ lifecycle_journal: lifecycleJournal,
148
+ packet: isNonEmptyString(packet) ? packet : null,
149
+ started_at: createdAt,
150
+ updated_at: createdAt,
151
+ };
152
+ }
153
+
154
+ /** Atomically write the session file under <rootDir>/.campaign-runtime/. */
155
+ export function writeRunSession(rootDir, session) {
156
+ const path = resolveRunSessionPath(rootDir);
157
+ mkdirSync(dirname(path), { recursive: true });
158
+ const tmp = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
159
+ writeFileSync(tmp, `${JSON.stringify(session, null, 2)}\n`);
160
+ renameSync(tmp, path);
161
+ return path;
162
+ }
163
+
164
+ /** Remove the active session file so the next `run start` is clean. Non-fatal. */
165
+ export function clearRunSession(sessionPath) {
166
+ try {
167
+ rmSync(sessionPath, { force: true });
168
+ return true;
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ // Is this session `packetPath`'s? A session records the packet it was opened
175
+ // for; one opened with none is every packet's. The comparison is on the real
176
+ // path, so a packet reached through a symlinked checkout and the same file
177
+ // reached directly are one packet. `boundPacket` is returned for the caller's
178
+ // message when the answer is no.
179
+ export function sessionBoundTo(session, packetPath) {
180
+ const boundPacket = isNonEmptyString(session?.packet) ? session.packet : null;
181
+ if (!boundPacket) return { same: true, boundPacket };
182
+ if (!isNonEmptyString(packetPath)) return { same: false, boundPacket };
183
+ return { same: sameFile(boundPacket, packetPath), boundPacket };
184
+ }
185
+
186
+
187
+ /**
188
+ * Open a run session at `rootDir`, or join the one already there. The two
189
+ * openers — `run start` and the auto-start behind `start`/`prepare-build` —
190
+ * write the same session and differ only in what an already-open one means:
191
+ * `run start` refuses it unless `force` replaces it; the auto-start joins it
192
+ * when it is bound to this packet (`join`) and stands off when it is bound to
193
+ * another. Returns `{ found, joined, existing, binding }`: `found` is the open
194
+ * session as findRunSession reports it ({ session, path, dir }) or null when
195
+ * the existing one could not be joined, in which case `existing` is that
196
+ * session and `binding` says which packet holds it. Nothing is written on a
197
+ * join or a refusal.
198
+ */
199
+ export function openRunSession(rootDir, {
200
+ runId = null,
201
+ lifecycleJournal = null,
202
+ packet = null,
203
+ lastRecommendation = null,
204
+ join: joinExisting = false,
205
+ force = false,
206
+ now = new Date(),
207
+ } = {}) {
208
+ const dir = resolve(rootDir);
209
+ const existing = force ? null : findRunSession(dir, { now });
210
+ if (existing) {
211
+ const binding = sessionBoundTo(existing.session, packet);
212
+ if (joinExisting && binding.same) return { found: existing, joined: true, existing: null, binding };
213
+ return { found: null, joined: false, existing, binding };
214
+ }
215
+ const session = {
216
+ ...buildRunSession({
217
+ runId: runId || mintSessionRunId(now),
218
+ lifecycleJournal: lifecycleJournal || join(dir, LIFECYCLE_JOURNAL_REL_PATH),
219
+ packet: isNonEmptyString(packet) ? resolve(packet) : null,
220
+ now,
221
+ }),
222
+ ...(lastRecommendation ? { last_recommendation: lastRecommendation } : {}),
223
+ };
224
+ const path = writeRunSession(dir, session);
225
+ return { found: { session, path, dir }, joined: false, existing: null, binding: null };
226
+ }
@@ -0,0 +1,113 @@
1
+ // Which .campaign-runtime/ entries are meant for the campaign repository's git
2
+ // history, and which are not. Three kinds share the directory:
3
+ //
4
+ // 1. The readback bundle (build-context, assembly-report, doctor-output,
5
+ // qa-verdict) — current readback truth; docs/migration-sidecar-bundle.md
6
+ // names it and `bundle check` validates it. Committed by design.
7
+ // 2. Handoff inputs and agent context (input/, theme/, agent-context/,
8
+ // setup-handoff.json) — durable build handoff. Committed by design.
9
+ // 3. Telemetry, sessions, caches, and evidence — per-machine, append-only,
10
+ // or carrying live URLs and absolute paths. Never meant for the repo, and
11
+ // nothing wrote a rule for them: three campaign repositories were found
12
+ // carrying another operator's run-session.json (absolute home path and
13
+ // all), lifecycle journals, and deviation logs.
14
+ //
15
+ // This module writes the rule for kind 3, once, into the target's .gitignore.
16
+ // A deny-list by name rather than "ignore the directory, re-include the
17
+ // bundle": git cannot re-include a file beneath an ignored directory, and a
18
+ // new sidecar should default to visible so someone has to decide about it.
19
+
20
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
21
+ import { join, resolve } from "node:path";
22
+
23
+ export const RUNTIME_STATE_IGNORE_MARKER = "# campaigns-os: machine-local runtime state (managed block; edit the list, keep the marker)";
24
+
25
+ export const RUNTIME_STATE_IGNORED_PATHS = [
26
+ ".campaign-runtime/run-session.json",
27
+ ".campaign-runtime/command-lifecycle.jsonl",
28
+ ".campaign-runtime/agent-deviations.jsonl",
29
+ ".campaign-runtime/workflow-findings.jsonl",
30
+ ".campaign-runtime/run-records/",
31
+ ".campaign-runtime/fetched-specs/",
32
+ ".campaign-runtime/polish-evidence/",
33
+ ".campaign-runtime/evidence/",
34
+ // Full QA verdicts carry live storefront URLs and order references; the
35
+ // committed form is the .campaign-runtime/qa-verdict.json projection.
36
+ "qa-output/",
37
+ ".campaign-runtime/*.log",
38
+ ".campaign-runtime/*.tmp",
39
+ ".campaign-runtime/**/*.tmp",
40
+ ];
41
+
42
+ // Entries the toolkit adds to an existing managed block when they are
43
+ // missing from the file. The block is otherwise the operator's to edit, but
44
+ // these name directories the toolkit itself fills with live URLs and order
45
+ // references, so a target set up before the entry existed must gain it on the
46
+ // next setup command rather than on the next accidental commit. An entry an
47
+ // operator has placed anywhere else in the file counts as present.
48
+ export const RUNTIME_STATE_REQUIRED_IGNORED_PATHS = ["qa-output/"];
49
+
50
+ export const RUNTIME_STATE_IGNORE_BLOCK = [
51
+ RUNTIME_STATE_IGNORE_MARKER,
52
+ "# Sessions, journals, Run Records, spec caches, capture evidence, and full",
53
+ "# QA verdicts are per-machine. The readback bundle (build-context, assembly-report,",
54
+ "# doctor-output, qa-verdict), input/, theme/, and agent-context/ are meant",
55
+ "# to be committed and are deliberately not listed here.",
56
+ ...RUNTIME_STATE_IGNORED_PATHS,
57
+ ].join("\n");
58
+
59
+ function ignoreRuleLines(text) {
60
+ return new Set(text.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")));
61
+ }
62
+
63
+ // Append the missing required entries at the end of the managed block: after
64
+ // the marker, up to the first blank line or the end of the file.
65
+ function withRequiredEntries(existing, missing) {
66
+ const lines = existing.split("\n");
67
+ const markerIndex = lines.findIndex((line) => line.trim() === RUNTIME_STATE_IGNORE_MARKER);
68
+ let end = markerIndex + 1;
69
+ while (end < lines.length && lines[end].trim() !== "") end += 1;
70
+ lines.splice(end, 0, ...missing);
71
+ return lines.join("\n");
72
+ }
73
+
74
+ /**
75
+ * Ensure the target's .gitignore carries the runtime-state block. Idempotent
76
+ * on the marker line: an existing block, however edited beneath it, is left
77
+ * alone — except for RUNTIME_STATE_REQUIRED_IGNORED_PATHS, which are appended
78
+ * to an existing block when the file carries them nowhere. Never throws for
79
+ * an unwritable target — the build must not fail on housekeeping — but
80
+ * reports what happened so callers can surface it.
81
+ * Returns `{ path, action: "added" | "updated" | "present" | "skipped", reason?, added_entries? }`.
82
+ */
83
+ export function ensureRuntimeStateIgnored(targetRepo, { dryRun = false } = {}) {
84
+ const path = join(resolve(targetRepo), ".gitignore");
85
+ let existing = "";
86
+ if (existsSync(path)) {
87
+ try {
88
+ existing = readFileSync(path, "utf8");
89
+ } catch (error) {
90
+ return { path, action: "skipped", reason: `unreadable: ${error.message}` };
91
+ }
92
+ if (existing.includes(RUNTIME_STATE_IGNORE_MARKER)) {
93
+ const present = ignoreRuleLines(existing);
94
+ const missing = RUNTIME_STATE_REQUIRED_IGNORED_PATHS.filter((entry) => !present.has(entry));
95
+ if (missing.length === 0) return { path, action: "present" };
96
+ if (dryRun) return { path, action: "updated", dry_run: true, added_entries: missing };
97
+ try {
98
+ writeFileSync(path, withRequiredEntries(existing, missing));
99
+ } catch (error) {
100
+ return { path, action: "skipped", reason: `unwritable: ${error.message}` };
101
+ }
102
+ return { path, action: "updated", added_entries: missing };
103
+ }
104
+ }
105
+ if (dryRun) return { path, action: "added", dry_run: true };
106
+ const separator = existing.length === 0 || existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
107
+ try {
108
+ writeFileSync(path, `${existing}${separator}${RUNTIME_STATE_IGNORE_BLOCK}\n`);
109
+ } catch (error) {
110
+ return { path, action: "skipped", reason: `unwritable: ${error.message}` };
111
+ }
112
+ return { path, action: "added" };
113
+ }
@@ -0,0 +1,212 @@
1
+ // The Campaign Cart SDK attribute index, vendored for the static SDK markup
2
+ // checks (#303).
3
+ //
4
+ // Source: docs/attribute-index.md in NextCommerceCo/campaign-cart at tag
5
+ // v0.4.38 (the SDK pin every certified starter family ships today), which is
6
+ // generated from the feature manifests. Only the data-next-* names are kept:
7
+ // the checks that read this list ask "does the SDK read this data-next-*
8
+ // attribute at all", and the SDK's other attribute families (data-item-*,
9
+ // data-package-*, plain data-*) are not what an agent kit misspells.
10
+ //
11
+ // This is a hardcoded copy and says so, rather than restating the SDK in
12
+ // prose that then drifts (the partner kit's mistake): the list is regenerated
13
+ // whole from the index at a named tag, never edited by hand.
14
+ //
15
+ // git -C ../campaign-cart show v0.4.38:docs/attribute-index.md \
16
+ // | grep -oE '`data-[a-z0-9-]+(\s*/\s*data-[a-z0-9-]+)*`' \
17
+ // | tr -d '`' | tr '/' '\n' | tr -d ' ' | grep '^data-next-' | sort -u
18
+ //
19
+ // A name ending in "-" (data-next-class-) is a prefix the SDK reads with any
20
+ // suffix.
21
+
22
+ export const SDK_ATTRIBUTE_INDEX_VERSION = "0.4.38";
23
+
24
+ export const SDK_DATA_NEXT_ATTRIBUTES = Object.freeze([
25
+ "data-next-accordion",
26
+ "data-next-accordion-panel",
27
+ "data-next-accordion-text",
28
+ "data-next-accordion-trigger",
29
+ "data-next-action",
30
+ "data-next-active",
31
+ "data-next-await",
32
+ "data-next-bump",
33
+ "data-next-bundle-card",
34
+ "data-next-bundle-display",
35
+ "data-next-bundle-id",
36
+ "data-next-bundle-items",
37
+ "data-next-bundle-name",
38
+ "data-next-bundle-price",
39
+ "data-next-bundle-qty-for",
40
+ "data-next-bundle-selector",
41
+ "data-next-bundle-selector-id",
42
+ "data-next-bundle-slot-template",
43
+ "data-next-bundle-slot-template-id",
44
+ "data-next-bundle-slots",
45
+ "data-next-bundle-slots-for",
46
+ "data-next-bundle-template",
47
+ "data-next-bundle-template-id",
48
+ "data-next-bundle-vouchers",
49
+ "data-next-bundles",
50
+ "data-next-cart-item-id",
51
+ "data-next-cart-items",
52
+ "data-next-cart-selector",
53
+ "data-next-cart-summary",
54
+ "data-next-checkout",
55
+ "data-next-checkout-field",
56
+ "data-next-checkout-payment",
57
+ "data-next-checkout-review",
58
+ "data-next-checkout-step",
59
+ "data-next-checkout-submit",
60
+ "data-next-class-",
61
+ "data-next-clear-cart",
62
+ "data-next-component",
63
+ "data-next-component-location",
64
+ "data-next-confirm",
65
+ "data-next-confirm-message",
66
+ "data-next-coupon",
67
+ "data-next-default-property",
68
+ "data-next-discounts",
69
+ "data-next-display",
70
+ "data-next-enhancer",
71
+ "data-next-error-for",
72
+ "data-next-exclude-property",
73
+ "data-next-express-checkout",
74
+ "data-next-fallback",
75
+ "data-next-format",
76
+ "data-next-hide",
77
+ "data-next-id",
78
+ "data-next-in-cart",
79
+ "data-next-include-shipping",
80
+ "data-next-is-upsell",
81
+ "data-next-item-properties",
82
+ "data-next-loading",
83
+ "data-next-max-quantity",
84
+ "data-next-min-quantity",
85
+ "data-next-multiply-quantity",
86
+ "data-next-next-url",
87
+ "data-next-order-items",
88
+ "data-next-package",
89
+ "data-next-package-id",
90
+ "data-next-package-price",
91
+ "data-next-package-selector",
92
+ "data-next-package-selector-id",
93
+ "data-next-package-sync",
94
+ "data-next-package-template",
95
+ "data-next-package-template-id",
96
+ "data-next-package-toggle",
97
+ "data-next-packages",
98
+ "data-next-payment-form",
99
+ "data-next-payment-method",
100
+ "data-next-payment-state",
101
+ "data-next-product-sync",
102
+ "data-next-property",
103
+ "data-next-property-container",
104
+ "data-next-quantity",
105
+ "data-next-quantity-decrease",
106
+ "data-next-quantity-display",
107
+ "data-next-quantity-increase",
108
+ "data-next-quantity-selector-id",
109
+ "data-next-quantity-text",
110
+ "data-next-remove-item",
111
+ "data-next-required",
112
+ "data-next-scroll-target",
113
+ "data-next-scroll-threshold",
114
+ "data-next-sdk-loading",
115
+ "data-next-selected",
116
+ "data-next-selection-mode",
117
+ "data-next-selector-card",
118
+ "data-next-selector-id",
119
+ "data-next-shipping-id",
120
+ "data-next-show",
121
+ "data-next-slot-index",
122
+ "data-next-step-number",
123
+ "data-next-timer",
124
+ "data-next-timer-display",
125
+ "data-next-timer-expired",
126
+ "data-next-toggle",
127
+ "data-next-toggle-card",
128
+ "data-next-toggle-container",
129
+ "data-next-toggle-display",
130
+ "data-next-toggle-image",
131
+ "data-next-toggle-price",
132
+ "data-next-toggle-template",
133
+ "data-next-toggle-template-id",
134
+ "data-next-tooltip",
135
+ "data-next-tooltip-class",
136
+ "data-next-tooltip-delay",
137
+ "data-next-tooltip-max-width",
138
+ "data-next-tooltip-offset",
139
+ "data-next-tooltip-placement",
140
+ "data-next-tracking-tag",
141
+ "data-next-upsell",
142
+ "data-next-upsell-action",
143
+ "data-next-upsell-action-for",
144
+ "data-next-upsell-context",
145
+ "data-next-upsell-item",
146
+ "data-next-upsell-option",
147
+ "data-next-upsell-quantity",
148
+ "data-next-upsell-quantity-toggle",
149
+ "data-next-upsell-section",
150
+ "data-next-upsell-select",
151
+ "data-next-upsell-selector",
152
+ "data-next-url",
153
+ "data-next-validate",
154
+ "data-next-variant-code",
155
+ "data-next-variant-option",
156
+ "data-next-variant-option-template-id",
157
+ "data-next-variant-options",
158
+ "data-next-variant-selector-template-id",
159
+ "data-next-variant-selectors",
160
+ ]);
161
+
162
+ // The fixed data-next-checkout-field names, from the same tag. The SDK maps a
163
+ // field to the order by this value, so an unlisted name is a silent no-op: the
164
+ // input renders, the shopper fills it, nothing reaches the order. Sources at
165
+ // v0.4.38: src/features/checkout/README.md ("Standard field names"), the
166
+ // checkout-form enhancer and its validation/persistence tables, the hosted
167
+ // payment field slots (cc-number, cvv, cc-month/cc-year and their exp-*
168
+ // spellings), payment-method, and accepts_marketing. Billing fields are the
169
+ // same names behind a "billing-" prefix and the SDK passes any billing-*
170
+ // suffix through (billing-field-routing.ts), so the prefix is accepted with
171
+ // any suffix rather than enumerated.
172
+ export const SDK_CHECKOUT_FIELD_NAMES = Object.freeze([
173
+ "email",
174
+ "fname",
175
+ "lname",
176
+ "phone",
177
+ "address1",
178
+ "address2",
179
+ "city",
180
+ "province",
181
+ "postal",
182
+ "country",
183
+ "payment-method",
184
+ "accepts_marketing",
185
+ "cc-number",
186
+ "cc-month",
187
+ "cc-year",
188
+ "exp-month",
189
+ "exp-year",
190
+ "cvv",
191
+ // Legacy spellings the README at v0.4.38 still lists.
192
+ "card-number",
193
+ "card-expiry",
194
+ "card-cvv",
195
+ "card-name",
196
+ ]);
197
+
198
+ export const SDK_CHECKOUT_FIELD_PREFIXES = Object.freeze(["billing-"]);
199
+
200
+ const exact = new Set(SDK_DATA_NEXT_ATTRIBUTES.filter((name) => !name.endsWith("-")));
201
+ const prefixes = SDK_DATA_NEXT_ATTRIBUTES.filter((name) => name.endsWith("-"));
202
+
203
+ export function isIndexedSdkAttribute(name) {
204
+ const value = String(name || "").toLowerCase();
205
+ return exact.has(value) || prefixes.some((prefix) => value.startsWith(prefix) && value.length > prefix.length);
206
+ }
207
+
208
+ export function isKnownCheckoutFieldName(value) {
209
+ const name = String(value ?? "");
210
+ return SDK_CHECKOUT_FIELD_NAMES.includes(name)
211
+ || SDK_CHECKOUT_FIELD_PREFIXES.some((prefix) => name.startsWith(prefix) && name.length > prefix.length);
212
+ }