@nextcommerce/campaigns-os 1.43.2 → 1.46.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 (72) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +648 -5103
  3. package/README.md +32 -11
  4. package/agents/claude/CLAUDE.md +1 -1
  5. package/agents/codex/AGENTS.md +1 -1
  6. package/agents/copilot/copilot-instructions.md +1 -1
  7. package/agents/cursor/campaigns-os.mdc +1 -1
  8. package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +2 -2
  9. package/campaign-spec/dist/rules/analytics-contract-shape.js +2 -2
  10. package/campaign-spec/dist/rules/store-profile-shape.d.ts +5 -1
  11. package/campaign-spec/dist/rules/store-profile-shape.js +8 -10
  12. package/campaign-spec/dist/types.d.ts +2 -2
  13. package/contracts/archive/CHANGELOG.2026-09-30.md +5111 -0
  14. package/contracts/archive/release-ledger.2026-09-30.json +5068 -0
  15. package/contracts/effects.v1.json +1176 -113
  16. package/contracts/orientation-reason-codes.v1.json +7 -0
  17. package/contracts/release-ledger.json +2345 -6087
  18. package/contracts/supported-surface.json +7 -4
  19. package/contracts/template-slot-manifest.shared-content-core.v0.json +403 -0
  20. package/docs/brand-theme-bridge.md +81 -0
  21. package/docs/build-packet.md +158 -21
  22. package/docs/campaigns-os-build-flow.md +3 -3
  23. package/docs/design-source-package.md +73 -0
  24. package/docs/effects.md +50 -8
  25. package/docs/gateway-login.md +3 -0
  26. package/docs/local-setup.md +1 -1
  27. package/docs/orientation-contract-reference.md +42 -2
  28. package/docs/polish-evidence.md +74 -0
  29. package/docs/qa-and-test-orders.md +99 -13
  30. package/docs/release-ledger-authoring-guide.md +64 -4
  31. package/docs/runtime-readiness.md +1 -1
  32. package/docs/sdk-storage-compatibility.md +1 -1
  33. package/docs/skills-revision.md +10 -10
  34. package/docs/supported-surface.md +2 -2
  35. package/docs/versioning.md +4 -1
  36. package/package.json +1 -1
  37. package/schemas/campaigns-os-release-ledger.v1.schema.json +32 -2
  38. package/schemas/campaigns-os-tooling-orientation.v1.schema.json +1 -0
  39. package/skills/campaign-lifecycle-orientation/SKILL.md +16 -5
  40. package/skills/campaign-readback-classification/SKILL.md +3 -3
  41. package/skills/campaign-run-evidence/SKILL.md +7 -6
  42. package/skills/contribution-intake/SKILL.md +3 -3
  43. package/skills/next-campaigns-build/SKILL.md +7 -6
  44. package/skills/next-campaigns-os/SKILL.md +7 -7
  45. package/skills/next-campaigns-os/references/session-intake.md +9 -3
  46. package/skills/next-campaigns-os-setup/SKILL.md +5 -5
  47. package/skills/next-campaigns-polish/SKILL.md +28 -9
  48. package/skills/next-campaigns-qa/SKILL.md +7 -4
  49. package/skills.json +10 -10
  50. package/src/brand-theme.mjs +320 -20
  51. package/src/built-site-scope.mjs +16 -4
  52. package/src/cli.mjs +280 -46
  53. package/src/commercial-parity.mjs +48 -2
  54. package/src/deviation.mjs +13 -1
  55. package/src/diagnostic.mjs +5 -2
  56. package/src/doctor/checks.mjs +320 -81
  57. package/src/doctor/inspect.mjs +55 -13
  58. package/src/doctor/source-provenance.mjs +184 -0
  59. package/src/invocation.mjs +4 -0
  60. package/src/live-campaign-refs.mjs +466 -0
  61. package/src/login.mjs +2 -2
  62. package/src/page-kit-store-profile.mjs +69 -12
  63. package/src/page-kit-sync.mjs +31 -12
  64. package/src/progress-node.mjs +3 -1
  65. package/src/qa-browser.mjs +538 -28
  66. package/src/qa-commercial-parity.mjs +48 -5
  67. package/src/qa-node.mjs +122 -7
  68. package/src/qa-test-order-topology.mjs +148 -0
  69. package/src/sdk-markup.mjs +72 -8
  70. package/src/source-html-intake.mjs +116 -0
  71. package/src/stage-record.mjs +551 -0
  72. package/src/upsell-selector-scope.mjs +112 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_note": "The downstream contract manifest. Everything listed here is SUPPORTED SURFACE: consumers (campaigns-agent, campaign-builder, the private ops repo, page-kit campaign repos) may depend on it, and changing it is a deliberate act — hashed entries require a surface_version bump in the same change (check-supported-surface.mjs --base, mirroring the skills.json bump gate), named entries must keep existing at their path, cli_commands must keep resolving in the CLI dispatch, package_exports must stay exported, and every entry must ship in the npm pack (files[] coverage). Anything NOT listed here — src/** internals, scripts/** checkers, examples/**, prompts/**, contracts/** other than this file and the entries named[] below (the orientation contract, the release ledger, and the consumer-facing orientation fixtures) — is implementation: consumers may read it for context but must not build on it, and it can change without notice. Rationale and the compatibility promise: docs/supported-surface.md.",
3
- "surface_version": "1.43.2",
3
+ "surface_version": "1.46.0",
4
4
  "package_exports": [
5
5
  "./commercial-journey",
6
6
  "./commercial-parity",
@@ -22,6 +22,7 @@
22
22
  "prepare-build",
23
23
  "build",
24
24
  "polish",
25
+ "record",
25
26
  "checkpoint",
26
27
  "page-kit",
27
28
  "spec",
@@ -92,7 +93,7 @@
92
93
  "sha256": "22dc5b48f12337163bd36307510e619f49e77331c974f45292ecd6e72175570d"
93
94
  },
94
95
  "schemas/campaigns-os-release-ledger.v1.schema.json": {
95
- "sha256": "08ca132134b9e4610370e88082a5e888d09ddec0d5ee4512d84cc57181da722e"
96
+ "sha256": "d7521fe0ab4deb62485f92286cb890ee548ea290793a6ce7444ecc42ba336d49"
96
97
  },
97
98
  "schemas/campaigns-os-run-record.v0.schema.json": {
98
99
  "sha256": "00dc9bcec79c12ea22a27f636188bcc33c6811df2d646ac7b49d4cc076e97355"
@@ -101,7 +102,7 @@
101
102
  "sha256": "f8a2ab5eadbb71fb6d5653b670ee71e4471c3df74dd04000d3dbd3d9ae9e71fc"
102
103
  },
103
104
  "schemas/campaigns-os-tooling-orientation.v1.schema.json": {
104
- "sha256": "897b192c7b3965c84909616023879b0a269a2e727e10efdec1b34e4c33ec499f"
105
+ "sha256": "8a2431b564041e8d2cbfaaf9d77ec272249268e54f5fc8952681537e0ad06e7c"
105
106
  },
106
107
  "schemas/campaigns-os-workflow-finding.v0.schema.json": {
107
108
  "sha256": "41ff370de4395149781d7e8c4ad6d7d4e2f896eb2030944399d36f1237726a20"
@@ -119,7 +120,7 @@
119
120
  "sha256": "dfc9abed38d456969e47a21f606d308a03bdf036f3466f7d6e47a602747dacf4"
120
121
  },
121
122
  "contracts/effects.v1.json": {
122
- "sha256": "1dae66e70bb5d167ca407d4ad03e70a1e2e761407b8ad7b85c8a7a51456df858"
123
+ "sha256": "794fe51952685e5afe6eee1988ce0230aae7a15a64fa1efdb350624158796f9c"
123
124
  },
124
125
  "schemas/campaigns-os-effects.v1.schema.json": {
125
126
  "sha256": "3eadd22169ab98bc7c2f682af2751267605170581182158d96be035b0cfe44dc"
@@ -148,6 +149,8 @@
148
149
  "AGENTS.md",
149
150
  "CHANGELOG.md",
150
151
  "contracts/release-ledger.json",
152
+ "contracts/archive/release-ledger.2026-09-30.json",
153
+ "contracts/archive/CHANGELOG.2026-09-30.md",
151
154
  "contracts/agent-relevant-change-policy.v1.json",
152
155
  "contracts/orientation-limits.v1.json",
153
156
  "contracts/orientation-reason-codes.v1.json",
@@ -3932,6 +3932,36 @@
3932
3932
  "description": "Page-kit page metadata \u2014 template-owned, assembly never touches.",
3933
3933
  "source_policy": "template_static"
3934
3934
  },
3935
+ {
3936
+ "key": "announcement_text",
3937
+ "kind": "copy",
3938
+ "role": "micro",
3939
+ "description": "Top announcement bar on the upsell page, e.g. 'Special Offer Unlocked - Do Not Close This Page'.",
3940
+ "source_policy": "generated",
3941
+ "length_band_chars": {
3942
+ "min": 20,
3943
+ "max": 90
3944
+ }
3945
+ },
3946
+ {
3947
+ "key": "upsell_hero",
3948
+ "kind": "copy",
3949
+ "role": "headline",
3950
+ "description": "Structured upsell hero: title plus offer/suboffer/heading/text lines and a presentation variant. Hint: {price}, {package_name}, {discount} and {first_name} are SDK-resolved tokens, so keeping them keeps the copy in sync with live pricing. The length band applies to each copy string, not the whole block.",
3951
+ "source_policy": "generated",
3952
+ "length_band_chars": {
3953
+ "min": 15,
3954
+ "max": 140
3955
+ }
3956
+ },
3957
+ {
3958
+ "key": "upsell_layout",
3959
+ "kind": "meta",
3960
+ "role": "page_meta",
3961
+ "owner": "template",
3962
+ "description": "Structured layout knobs for the shared upsell offer shell (style, boxed, media, media_position, media_align, show_* toggles, decline_position). Template-owned presentation, assembly never touches.",
3963
+ "source_policy": "template_static"
3964
+ },
3935
3965
  {
3936
3966
  "key": "upsell_offer",
3937
3967
  "kind": "commerce",
@@ -4015,6 +4045,36 @@
4015
4045
  "description": "Page-kit page metadata \u2014 template-owned, assembly never touches.",
4016
4046
  "source_policy": "template_static"
4017
4047
  },
4048
+ {
4049
+ "key": "announcement_text",
4050
+ "kind": "copy",
4051
+ "role": "micro",
4052
+ "description": "Top announcement bar on the upsell page, e.g. 'Special Offer Unlocked - Do Not Close This Page'.",
4053
+ "source_policy": "generated",
4054
+ "length_band_chars": {
4055
+ "min": 20,
4056
+ "max": 90
4057
+ }
4058
+ },
4059
+ {
4060
+ "key": "upsell_hero",
4061
+ "kind": "copy",
4062
+ "role": "headline",
4063
+ "description": "Structured upsell hero: title plus offer/suboffer/heading/text lines and a presentation variant. Hint: {price}, {package_name}, {discount} and {first_name} are SDK-resolved tokens, so keeping them keeps the copy in sync with live pricing. The length band applies to each copy string, not the whole block.",
4064
+ "source_policy": "generated",
4065
+ "length_band_chars": {
4066
+ "min": 15,
4067
+ "max": 140
4068
+ }
4069
+ },
4070
+ {
4071
+ "key": "upsell_layout",
4072
+ "kind": "meta",
4073
+ "role": "page_meta",
4074
+ "owner": "template",
4075
+ "description": "Structured layout knobs for the shared upsell offer shell (style, boxed, media, media_position, media_align, show_* toggles, decline_position). Template-owned presentation, assembly never touches.",
4076
+ "source_policy": "template_static"
4077
+ },
4018
4078
  {
4019
4079
  "key": "upsell_offer",
4020
4080
  "kind": "commerce",
@@ -4106,6 +4166,36 @@
4106
4166
  "description": "Page-kit page metadata \u2014 template-owned, assembly never touches.",
4107
4167
  "source_policy": "template_static"
4108
4168
  },
4169
+ {
4170
+ "key": "announcement_text",
4171
+ "kind": "copy",
4172
+ "role": "micro",
4173
+ "description": "Top announcement bar on the upsell page, e.g. 'Special Offer Unlocked - Do Not Close This Page'.",
4174
+ "source_policy": "generated",
4175
+ "length_band_chars": {
4176
+ "min": 20,
4177
+ "max": 90
4178
+ }
4179
+ },
4180
+ {
4181
+ "key": "upsell_hero",
4182
+ "kind": "copy",
4183
+ "role": "headline",
4184
+ "description": "Structured upsell hero: title plus offer/suboffer/heading/text lines and a presentation variant. Hint: {price}, {package_name}, {discount} and {first_name} are SDK-resolved tokens, so keeping them keeps the copy in sync with live pricing. The length band applies to each copy string, not the whole block.",
4185
+ "source_policy": "generated",
4186
+ "length_band_chars": {
4187
+ "min": 15,
4188
+ "max": 140
4189
+ }
4190
+ },
4191
+ {
4192
+ "key": "upsell_layout",
4193
+ "kind": "meta",
4194
+ "role": "page_meta",
4195
+ "owner": "template",
4196
+ "description": "Structured layout knobs for the shared upsell offer shell (style, boxed, media, media_position, media_align, show_* toggles, decline_position). Template-owned presentation, assembly never touches.",
4197
+ "source_policy": "template_static"
4198
+ },
4109
4199
  {
4110
4200
  "key": "upsell_offer",
4111
4201
  "kind": "commerce",
@@ -4142,6 +4232,319 @@
4142
4232
  }
4143
4233
  ]
4144
4234
  },
4235
+ "upsell-single": {
4236
+ "sections": [
4237
+ {
4238
+ "section": "main",
4239
+ "slots": [
4240
+ {
4241
+ "key": "title",
4242
+ "kind": "copy",
4243
+ "role": "micro",
4244
+ "description": "Short copy slot.",
4245
+ "source_policy": "generated",
4246
+ "length_band_chars": {
4247
+ "min": 15,
4248
+ "max": 67
4249
+ },
4250
+ "demo_value": "Apollo \u2014 Upsell (single offer)"
4251
+ },
4252
+ {
4253
+ "key": "page_type",
4254
+ "kind": "meta",
4255
+ "role": "page_meta",
4256
+ "owner": "template",
4257
+ "description": "Page-kit page metadata \u2014 template-owned, assembly never touches.",
4258
+ "source_policy": "template_static"
4259
+ },
4260
+ {
4261
+ "key": "next_url",
4262
+ "kind": "link",
4263
+ "role": "routing",
4264
+ "description": "Route/link target.",
4265
+ "source_policy": "spec_truth"
4266
+ },
4267
+ {
4268
+ "key": "decline_url",
4269
+ "kind": "link",
4270
+ "role": "routing",
4271
+ "description": "Route/link target.",
4272
+ "source_policy": "spec_truth"
4273
+ },
4274
+ {
4275
+ "key": "scripts",
4276
+ "kind": "meta",
4277
+ "role": "page_meta",
4278
+ "owner": "template",
4279
+ "description": "Page-kit page metadata \u2014 template-owned, assembly never touches.",
4280
+ "source_policy": "template_static"
4281
+ },
4282
+ {
4283
+ "key": "announcement_text",
4284
+ "kind": "copy",
4285
+ "role": "micro",
4286
+ "description": "Top announcement bar on the upsell page, e.g. 'Special Offer Unlocked - Do Not Close This Page'.",
4287
+ "source_policy": "generated",
4288
+ "length_band_chars": {
4289
+ "min": 20,
4290
+ "max": 90
4291
+ }
4292
+ },
4293
+ {
4294
+ "key": "upsell_hero",
4295
+ "kind": "copy",
4296
+ "role": "headline",
4297
+ "description": "Structured upsell hero: title plus offer/suboffer/heading/text lines and a presentation variant. Hint: {price}, {package_name}, {discount} and {first_name} are SDK-resolved tokens, so keeping them keeps the copy in sync with live pricing. The length band applies to each copy string, not the whole block.",
4298
+ "source_policy": "generated",
4299
+ "length_band_chars": {
4300
+ "min": 15,
4301
+ "max": 140
4302
+ }
4303
+ },
4304
+ {
4305
+ "key": "countdown_timer",
4306
+ "kind": "copy",
4307
+ "role": "micro",
4308
+ "description": "Structured SDK countdown (text, highlight, duration, persistence_id). The length band applies to each copy string (text, highlight).",
4309
+ "source_policy": "generated",
4310
+ "length_band_chars": {
4311
+ "min": 6,
4312
+ "max": 40
4313
+ }
4314
+ },
4315
+ {
4316
+ "key": "upsell_layout",
4317
+ "kind": "meta",
4318
+ "role": "page_meta",
4319
+ "owner": "template",
4320
+ "description": "Structured layout knobs for the shared upsell offer shell (style, boxed, media, media_position, media_align, show_* toggles, decline_position). Template-owned presentation, assembly never touches.",
4321
+ "source_policy": "template_static"
4322
+ },
4323
+ {
4324
+ "key": "upsell_offer",
4325
+ "kind": "commerce",
4326
+ "role": "commerce_structure",
4327
+ "owner": "commerce",
4328
+ "description": "Structured commerce configuration \u2014 owned by the commerce-surface catalog and the family brand contract, not the content-slot layer.",
4329
+ "reference": "integrated_commerce_surface_ref"
4330
+ },
4331
+ {
4332
+ "key": "upsell_body",
4333
+ "kind": "copy",
4334
+ "role": "body",
4335
+ "description": "Structured upsell body sections (features, comparison, steps, reviews). The length band applies to each copy string, not the whole block.",
4336
+ "source_policy": "generated",
4337
+ "length_band_chars": {
4338
+ "min": 10,
4339
+ "max": 240
4340
+ }
4341
+ },
4342
+ {
4343
+ "key": "closing_cta",
4344
+ "kind": "copy",
4345
+ "role": "cta",
4346
+ "description": "Structured closing accept/decline block (variant, title, note, show_guarantees). The length band applies to each copy string (title, note).",
4347
+ "source_policy": "generated",
4348
+ "length_band_chars": {
4349
+ "min": 10,
4350
+ "max": 140
4351
+ }
4352
+ }
4353
+ ]
4354
+ }
4355
+ ]
4356
+ },
4357
+ "upsell-vsl": {
4358
+ "sections": [
4359
+ {
4360
+ "section": "main",
4361
+ "slots": [
4362
+ {
4363
+ "key": "title",
4364
+ "kind": "copy",
4365
+ "role": "micro",
4366
+ "description": "Short copy slot.",
4367
+ "source_policy": "generated",
4368
+ "length_band_chars": {
4369
+ "min": 15,
4370
+ "max": 67
4371
+ },
4372
+ "demo_value": "Apollo \u2014 Upsell (VSL)"
4373
+ },
4374
+ {
4375
+ "key": "page_type",
4376
+ "kind": "meta",
4377
+ "role": "page_meta",
4378
+ "owner": "template",
4379
+ "description": "Page-kit page metadata \u2014 template-owned, assembly never touches.",
4380
+ "source_policy": "template_static"
4381
+ },
4382
+ {
4383
+ "key": "next_url",
4384
+ "kind": "link",
4385
+ "role": "routing",
4386
+ "description": "Route/link target.",
4387
+ "source_policy": "spec_truth"
4388
+ },
4389
+ {
4390
+ "key": "decline_url",
4391
+ "kind": "link",
4392
+ "role": "routing",
4393
+ "description": "Route/link target.",
4394
+ "source_policy": "spec_truth"
4395
+ },
4396
+ {
4397
+ "key": "styles",
4398
+ "kind": "meta",
4399
+ "role": "page_meta",
4400
+ "owner": "template",
4401
+ "description": "Page-kit page metadata \u2014 template-owned, assembly never touches.",
4402
+ "source_policy": "template_static"
4403
+ },
4404
+ {
4405
+ "key": "scripts",
4406
+ "kind": "meta",
4407
+ "role": "page_meta",
4408
+ "owner": "template",
4409
+ "description": "Page-kit page metadata \u2014 template-owned, assembly never touches.",
4410
+ "source_policy": "template_static"
4411
+ },
4412
+ {
4413
+ "key": "announcement_text",
4414
+ "kind": "copy",
4415
+ "role": "micro",
4416
+ "description": "Top announcement bar on the upsell page, e.g. 'Special Offer Unlocked - Do Not Close This Page'.",
4417
+ "source_policy": "generated",
4418
+ "length_band_chars": {
4419
+ "min": 20,
4420
+ "max": 90
4421
+ }
4422
+ },
4423
+ {
4424
+ "key": "announcement_variant",
4425
+ "kind": "meta",
4426
+ "role": "page_meta",
4427
+ "owner": "template",
4428
+ "description": "Announcement bar style knob (e.g. danger). Template-owned presentation, assembly never touches.",
4429
+ "source_policy": "template_static"
4430
+ },
4431
+ {
4432
+ "key": "header_bar",
4433
+ "kind": "meta",
4434
+ "role": "page_meta",
4435
+ "owner": "template",
4436
+ "description": "Header style knob for the upsell page (e.g. logo-only). Template-owned presentation, assembly never touches.",
4437
+ "source_policy": "template_static"
4438
+ },
4439
+ {
4440
+ "key": "upsell_hero",
4441
+ "kind": "copy",
4442
+ "role": "headline",
4443
+ "description": "Structured upsell hero: title plus offer/suboffer/heading/text lines and a presentation variant. Hint: {price}, {package_name}, {discount} and {first_name} are SDK-resolved tokens, so keeping them keeps the copy in sync with live pricing. The length band applies to each copy string, not the whole block.",
4444
+ "source_policy": "generated",
4445
+ "length_band_chars": {
4446
+ "min": 15,
4447
+ "max": 140
4448
+ }
4449
+ },
4450
+ {
4451
+ "key": "countdown_timer",
4452
+ "kind": "copy",
4453
+ "role": "micro",
4454
+ "description": "Structured SDK countdown (text, highlight, duration, persistence_id). The length band applies to each copy string (text, highlight).",
4455
+ "source_policy": "generated",
4456
+ "length_band_chars": {
4457
+ "min": 6,
4458
+ "max": 40
4459
+ }
4460
+ },
4461
+ {
4462
+ "key": "upsell_layout",
4463
+ "kind": "meta",
4464
+ "role": "page_meta",
4465
+ "owner": "template",
4466
+ "description": "Structured layout knobs for the shared upsell offer shell (style, boxed, media, media_position, media_align, show_* toggles, decline_position). Template-owned presentation, assembly never touches.",
4467
+ "source_policy": "template_static"
4468
+ },
4469
+ {
4470
+ "key": "upsell_offer",
4471
+ "kind": "commerce",
4472
+ "role": "commerce_structure",
4473
+ "owner": "commerce",
4474
+ "description": "Structured commerce configuration \u2014 owned by the commerce-surface catalog and the family brand contract, not the content-slot layer.",
4475
+ "reference": "integrated_commerce_surface_ref"
4476
+ },
4477
+ {
4478
+ "key": "upsell_video",
4479
+ "kind": "image",
4480
+ "role": "image",
4481
+ "description": "Structured upsell video (src, poster, title, autoplay, loop).",
4482
+ "source_policy": "merchant_asset_required"
4483
+ },
4484
+ {
4485
+ "key": "swiper_slides",
4486
+ "kind": "commerce",
4487
+ "role": "commerce_structure",
4488
+ "owner": "commerce",
4489
+ "description": "Structured commerce configuration \u2014 owned by the commerce-surface catalog and the family brand contract, not the content-slot layer.",
4490
+ "reference": "integrated_commerce_surface_ref"
4491
+ },
4492
+ {
4493
+ "key": "swiper_thumbs",
4494
+ "kind": "commerce",
4495
+ "role": "commerce_structure",
4496
+ "owner": "commerce",
4497
+ "description": "Structured commerce configuration \u2014 owned by the commerce-surface catalog and the family brand contract, not the content-slot layer.",
4498
+ "reference": "integrated_commerce_surface_ref"
4499
+ },
4500
+ {
4501
+ "key": "offer_bullets",
4502
+ "kind": "copy",
4503
+ "role": "body",
4504
+ "description": "Bullet list beside the upsell offer. The length band applies to each bullet.",
4505
+ "source_policy": "generated",
4506
+ "length_band_chars": {
4507
+ "min": 15,
4508
+ "max": 80
4509
+ }
4510
+ },
4511
+ {
4512
+ "key": "upsell_guarantees",
4513
+ "kind": "copy",
4514
+ "role": "body",
4515
+ "description": "Structured guarantee/trust cards (e.g. secure checkout, money-back terms). The length band applies to each copy string (title, text).",
4516
+ "source_policy": "spec_truth",
4517
+ "length_band_chars": {
4518
+ "min": 10,
4519
+ "max": 200
4520
+ }
4521
+ },
4522
+ {
4523
+ "key": "upsell_body",
4524
+ "kind": "copy",
4525
+ "role": "body",
4526
+ "description": "Structured upsell body sections (features, comparison, steps, reviews). The length band applies to each copy string, not the whole block.",
4527
+ "source_policy": "generated",
4528
+ "length_band_chars": {
4529
+ "min": 10,
4530
+ "max": 240
4531
+ }
4532
+ },
4533
+ {
4534
+ "key": "closing_cta",
4535
+ "kind": "copy",
4536
+ "role": "cta",
4537
+ "description": "Structured closing accept/decline block (variant, title, note, show_guarantees). The length band applies to each copy string (title, note).",
4538
+ "source_policy": "generated",
4539
+ "length_band_chars": {
4540
+ "min": 10,
4541
+ "max": 140
4542
+ }
4543
+ }
4544
+ ]
4545
+ }
4546
+ ]
4547
+ },
4145
4548
  "receipt": {
4146
4549
  "sections": [
4147
4550
  {
@@ -54,6 +54,65 @@ that still falls below the contract's `min_contrast_ratio` is emitted with a
54
54
  confirmed. Derived-foreground confidence scales with the achieved contrast
55
55
  (`>= 7:1` high, `>= 4.5:1` medium, otherwise low).
56
56
 
57
+ One exception keeps the design's own CTA label. The declared CTA foreground is
58
+ read from the selected source in this order:
59
+
60
+ 1. the `color:` of the button rules whose `background` or `background-color`
61
+ is the CTA background (the design's own pairing), only when they all agree
62
+ on one colour; when they disagree, this step declares nothing and the next
63
+ step applies;
64
+ 2. a `:root` inverse/on-colour text token: the name needs a `text` or
65
+ `foreground` part plus `inverse`, or `on` followed by `primary`, `cta`,
66
+ `brand`, `accent` or `dark` (`--text-inverse` first, then for example
67
+ `--text-color-inverse`, `--text-on-primary`, `--on-primary-text` or
68
+ `--foreground-on-dark`). `--border-on-primary`, `--overlay-on-dark` and a
69
+ bare `--on-primary` are not text and never qualify;
70
+ 3. the `color:` of the other button rules that declare no background, only when
71
+ they all agree on one colour.
72
+
73
+ A button rule is one whose every selector ends in a compound selector that is
74
+ the `button` element, `input[type=submit]`, or a class starting with
75
+ `btn`, `button` or `cta` or having a `cta` part (`.btn-primary`, `.button`,
76
+ `.cta`, `.hero-cta`). An attribute alone does not make a button:
77
+ `[type=submit]`, `div[type="submit"]` and `.order-summary[type=submit]` do not
78
+ qualify. Only the compound's own element, class and attribute
79
+ selectors count: the value inside an attribute selector and the arguments of
80
+ `:not()`, `:is()`, `:where()` and `:has()` are not read, so
81
+ `.btn-primary[data-x]` and `button:not(.order-summary)` qualify while
82
+ `.order-summary[data-target=".btn"]`, `.order-summary:not(.btn)` and
83
+ `.cart:has(.button)` do not. Any other pseudo-class or pseudo-element
84
+ (`:hover`, `:disabled`, `::before`) disqualifies the selector. Selectors such
85
+ as `.order-summary` or `.cart-count`, and `.btn .icon`, never qualify. When that
86
+ colour reaches at least 3:1 on the CTA background (WCAG AA for large text),
87
+ `--brand--color--text-inverse` and `--brand--color--cta-foreground` use it
88
+ (`derivation.method: declared-cta-foreground`). White on `#dd4249` is 4.24:1,
89
+ so a design that declares white CTA text keeps white, although black scores
90
+ higher there. A declared colour under 3:1 is ignored and the luminance pick
91
+ applies, so a white scaffold default on a yellow CTA still resolves dark. A
92
+ declared colour between 3:1 and the contract's `min_contrast_ratio` (4.5:1) is
93
+ used and reported with `theme.foreground.low_contrast`. With no declared CTA
94
+ foreground, the output is unchanged. Declarations inside CSS comments are not
95
+ read, so a commented-out token or rule never supplies a CTA or body text colour.
96
+
97
+ ### Body text prefers the darkest declared text token
98
+
99
+ A declared text token is a `:root` custom property in the selected source whose
100
+ name has a `text` part and whose value is a solid colour. Names that carry
101
+ another job are not counted: inverse/on-colour labels (`text` or `foreground`
102
+ with `inverse` in any order, or `on` followed by `primary`, `cta`, `brand`,
103
+ `accent` or `dark`; `--text-on-light` is ordinary copy), `secondary`, `muted` or
104
+ `subtle` copy, link, status and state colours (`link`, `error`, `danger`,
105
+ `success`, `warning`, `info`, `highlight`, `accent`, `placeholder`, `disabled`,
106
+ `selection`), `cta`/`button`/`btn` labels, and text `shadow`, `border`,
107
+ `outline`, `stroke`, `bg` or `background` values. When the source has a solid
108
+ body background (`--surface-bg`), `--brand--color--text-primary` and
109
+ `--brand--color--foreground` take the darkest declared text token that is
110
+ darker than that background and reaches 4.5:1 on it
111
+ (`derivation.method: darkest-declared-text-token`, with the replaced value
112
+ recorded, or `null` when the source yielded no `--text-primary`). This applies
113
+ whether or not the source yields a `--text-primary` of its own. If no token
114
+ qualifies, the existing pick (or its absence) stands.
115
+
57
116
  > **Contract change (PR #117):** `--text-inverse` and the three `*-foreground`
58
117
  > targets are no longer entries under `source_mappings` — they moved to
59
118
  > `foreground_derivations`. A source `--text-inverse` token (or anything that
@@ -141,6 +200,28 @@ If a fresh `brand-theme.css` exists:
141
200
  4. Record `report.theme.status`, `css_path`, `commerce_pages`, `load_order`,
142
201
  and evidence.
143
202
 
203
+ ### Where next-core.css belongs
204
+
205
+ `next-core.css` and the brand layer are needed only on pages where the
206
+ starter-template family's components render, because those components read
207
+ the `--brand--*` tokens. `next-core.css` also carries element resets (`li`,
208
+ `a`, headings, body letter-spacing) that restyle any markup on the page. On a
209
+ page whose upsell, downsell or receipt markup comes from the design rather
210
+ than from family components, loading it breaks that markup. Leave both
211
+ stylesheets off those pages and list them only in the frontmatter styles of
212
+ the pages that render family components, often just checkout.
213
+
214
+ `report.theme.commerce_pages` is the list of pages where the brand layer was
215
+ applied, recorded by the build. Record the pages you actually scoped, for
216
+ example `commerce_pages: ["checkout"]`. The theme gate does not compare this
217
+ list with the funnel. It passes on `report.theme.status: applied` with
218
+ `load_order: after-next-core`. The gate's own `commerce_pages` output is a
219
+ different field: every checkout, upsell, downsell, receipt or thank-you page
220
+ the campaign builds or declares. It decides whether the gate applies, not
221
+ which pages must load the brand layer. No check reads each built page's
222
+ stylesheet order, so the recorded list and the evidence are what a reviewer
223
+ sees.
224
+
144
225
  Polish should verify token parity, load order after next-core, starter-logo
145
226
  replacement when source assets expose a real brand mark, and SDK safety. If the
146
227
  brand layer is repairable, record the first repair-loop defect.