@nextcommerce/campaigns-os 1.54.0 → 1.57.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 (111) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/CONTEXT.md +19 -5
  3. package/README.md +26 -12
  4. package/campaign-spec/README.md +15 -1
  5. package/campaign-spec/dist/checkout-flow.d.ts +90 -0
  6. package/campaign-spec/dist/checkout-flow.js +229 -0
  7. package/campaign-spec/dist/index.d.ts +1 -0
  8. package/campaign-spec/dist/index.js +6 -1
  9. package/campaign-spec/dist/normalize.d.ts +3 -2
  10. package/campaign-spec/dist/normalize.js +7 -5
  11. package/campaign-spec/dist/routing.d.ts +6 -3
  12. package/campaign-spec/dist/routing.js +6 -3
  13. package/campaign-spec/dist/rules/checkout-forward-target.d.ts +17 -0
  14. package/campaign-spec/dist/rules/checkout-forward-target.js +48 -0
  15. package/campaign-spec/dist/rules/checkout-step-reaches-checkout.d.ts +12 -0
  16. package/campaign-spec/dist/rules/checkout-step-reaches-checkout.js +37 -0
  17. package/campaign-spec/dist/rules/exit-intent-validation.js +2 -1
  18. package/campaign-spec/dist/rules/index.d.ts +4 -0
  19. package/campaign-spec/dist/rules/index.js +12 -0
  20. package/campaign-spec/dist/rules/one-checkout-per-path.d.ts +25 -0
  21. package/campaign-spec/dist/rules/one-checkout-per-path.js +84 -0
  22. package/campaign-spec/dist/rules/pre-payment-forward-target.d.ts +12 -0
  23. package/campaign-spec/dist/rules/pre-payment-forward-target.js +42 -0
  24. package/campaign-spec/dist/rules/promo-code-input-validation.js +2 -1
  25. package/campaign-spec/dist/rules/schema-version.d.ts +9 -4
  26. package/campaign-spec/dist/rules/schema-version.js +10 -5
  27. package/campaign-spec/dist/types.d.ts +20 -3
  28. package/campaign-spec/dist/types.js +3 -2
  29. package/compatibility.json +2 -2
  30. package/contracts/fixtures/runtime-recipe/accept/current.json +6 -1
  31. package/contracts/fixtures/runtime-recipe/accept/minimal.json +6 -1
  32. package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
  33. package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +6 -1
  34. package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +6 -1
  35. package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +6 -1
  36. package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +6 -1
  37. package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +6 -1
  38. package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +1 -1
  39. package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +6 -1
  40. package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +6 -1
  41. package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +6 -1
  42. package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +5 -0
  43. package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +6 -1
  44. package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +6 -1
  45. package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +6 -1
  46. package/contracts/release-ledger.json +1146 -0
  47. package/contracts/runtime-recipe.campaigns-os-node-v1.json +6 -1
  48. package/contracts/supported-surface.json +9 -5
  49. package/docs/brand-theme-bridge.md +10 -0
  50. package/docs/build-packet.md +86 -26
  51. package/docs/campaigns-os-build-flow.md +1 -1
  52. package/docs/effects.md +2 -5
  53. package/docs/local-setup.md +1 -1
  54. package/docs/orientation-contract-reference.md +1 -1
  55. package/docs/qa-and-test-orders.md +133 -29
  56. package/docs/runtime-readiness.md +8 -3
  57. package/docs/skills-revision.md +10 -10
  58. package/docs/supported-surface.md +1 -1
  59. package/docs/versioning.md +4 -1
  60. package/docs/workflow-findings-sidecar.md +57 -2
  61. package/package.json +4 -1
  62. package/schemas/campaign-runtime-build-context.v0.schema.json +3 -1
  63. package/schemas/campaign-runtime-build-packet.v0.schema.json +3 -1
  64. package/schemas/campaign-spec.v5.schema.json +586 -0
  65. package/schemas/campaigns-os-run-record.v0.schema.json +20 -15
  66. package/skills/campaign-lifecycle-orientation/SKILL.md +10 -8
  67. package/skills/campaign-readback-classification/SKILL.md +3 -3
  68. package/skills/campaign-run-evidence/SKILL.md +3 -3
  69. package/skills/contribution-intake/SKILL.md +3 -3
  70. package/skills/next-campaigns-build/SKILL.md +6 -4
  71. package/skills/next-campaigns-os/SKILL.md +3 -3
  72. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  73. package/skills/next-campaigns-polish/SKILL.md +21 -6
  74. package/skills/next-campaigns-qa/SKILL.md +5 -4
  75. package/skills.json +10 -10
  76. package/src/adapter-decision-contract.mjs +27 -13
  77. package/src/brand-theme.mjs +44 -8
  78. package/src/build-brief.mjs +14 -5
  79. package/src/campaign-identity.mjs +33 -4
  80. package/src/cli-helpers.mjs +129 -0
  81. package/src/cli.mjs +156 -111
  82. package/src/commercial-journey.mjs +25 -2
  83. package/src/consent.mjs +3 -2
  84. package/src/design-source-package.mjs +1 -1
  85. package/src/design-source-publication.mjs +8 -8
  86. package/src/deviation.mjs +2 -1
  87. package/src/doctor/checks.mjs +212 -19
  88. package/src/doctor/inspect.mjs +4 -2
  89. package/src/doctor/next-step.mjs +46 -16
  90. package/src/doctor-sidecar.mjs +5 -3
  91. package/src/gate-actions.mjs +21 -1
  92. package/src/input-refresh.mjs +6 -6
  93. package/src/invocation.mjs +4 -3
  94. package/src/lifecycle.mjs +88 -6
  95. package/src/local-preview-policy.mjs +116 -12
  96. package/src/login.mjs +2 -1
  97. package/src/polish-browser.mjs +8 -2
  98. package/src/polish-node.mjs +9 -4
  99. package/src/polish-readability.mjs +2 -2
  100. package/src/qa-browser.mjs +741 -120
  101. package/src/qa-cart-entry.mjs +107 -20
  102. package/src/qa-node.mjs +179 -72
  103. package/src/qa-test-order-topology.mjs +118 -1
  104. package/src/qa-verdict-discovery.mjs +21 -4
  105. package/src/qa-verdict-publish.mjs +2 -1
  106. package/src/run-record.mjs +46 -34
  107. package/src/runtime-state-ignore.mjs +7 -5
  108. package/src/sdk-markup.mjs +41 -1
  109. package/src/source-html-intake.mjs +46 -4
  110. package/src/stage-ledger.mjs +18 -4
  111. package/src/stage-record.mjs +53 -16
@@ -134,6 +134,11 @@
134
134
  "enum": ["skipped", "ok", "failed"],
135
135
  "description": "skipped: publish was off for the run (no send). ok: the receiver's answer counts as stored. failed: refused or never answered."
136
136
  },
137
+ "reason": {
138
+ "type": ["string", "null"],
139
+ "enum": ["loopback_base_url", "portal_managed_default", "consent_off", "flag_opt_out", "flag_opt_in", "default", "local_spec", null],
140
+ "description": "The QA publish decision's reason. null when no decision was recorded, such as a later qa publish of a stored verdict."
141
+ },
137
142
  "result": {
138
143
  "type": ["string", "null"],
139
144
  "enum": ["stored", "already_stored", "ok_unparsed_ack", "refused", "transport_error", null],
@@ -226,6 +231,9 @@
226
231
  "additionalProperties": false,
227
232
  "properties": {
228
233
  "disposition": { "type": ["string", "null"] },
234
+ "verdict_run_id": { "type": ["string", "null"], "minLength": 1, "description": "Run ID of the verdict supplying disposition." },
235
+ "build_fingerprint": { "type": ["string", "null"], "minLength": 1, "description": "QA build fingerprint from the Assembly Report stage bound to that verdict." },
236
+ "stale": { "type": ["boolean", "null"], "description": "Whether that QA fingerprint differs from the current recorded build; null when either fingerprint is unknown." },
229
237
  "gap_classes": {
230
238
  "type": "array",
231
239
  "items": { "type": "string" },
@@ -286,19 +294,10 @@
286
294
  "type": ["string", "null"],
287
295
  "description": "Optional free-form confidence hint for primary_surface, e.g. \"low\"."
288
296
  },
289
- "agent_usage": {
290
- "type": "object",
291
- "additionalProperties": false,
292
- "description": "Optional host-supplied agent runtime usage. Campaigns OS does not infer these values; outer runners provide them when available.",
293
- "properties": {
294
- "input_tokens": { "type": "integer", "minimum": 0 },
295
- "output_tokens": { "type": "integer", "minimum": 0 },
296
- "tool_output_tokens": { "type": "integer", "minimum": 0 },
297
- "total_tokens": { "type": "integer", "minimum": 0 },
298
- "elapsed_ms": { "type": "integer", "minimum": 0 },
299
- "model": { "type": "string" },
300
- "source": { "type": "string" }
301
- }
297
+ "closed_by": {
298
+ "type": "string",
299
+ "enum": ["run_end", "qa_auto_end", "stale_sweep", "manual"],
300
+ "description": "The command path that closed this Run Record. Absent on records written before this field existed."
302
301
  },
303
302
  "lifecycle": {
304
303
  "type": ["object", "null"],
@@ -327,17 +326,23 @@
327
326
  "type": "object",
328
327
  "additionalProperties": false,
329
328
  "required": ["name"],
329
+ "if": { "required": ["finding_codes"], "properties": { "finding_codes": {} } },
330
+ "then": { "required": ["exit_status"], "properties": { "exit_status": { "type": "integer", "not": { "const": 0 } } } },
330
331
  "properties": {
331
332
  "name": { "type": "string" },
332
333
  "duration_ms": { "type": ["number", "null"] },
333
- "exit_status": { "type": ["integer", "null"] }
334
+ "exit_status": { "type": ["integer", "null"] },
335
+ "wait_ms": { "type": ["integer", "null"], "minimum": 0, "description": "Whole milliseconds blocked on operator input. Absent or null means unknown for legacy entries; duration_ms includes it." },
336
+ "finding_codes": { "type": "array", "maxItems": 5, "uniqueItems": true, "items": { "type": "string" }, "description": "First five distinct doctor or QA finding codes, only on a non-zero exit." }
334
337
  }
335
338
  }
336
339
  },
337
340
  "repair_loop_count": {
338
341
  "type": ["integer", "null"],
339
342
  "description": "Re-runs of any command within the run (a re-run is a repair loop: doctor -> fix -> doctor). 0 when each command ran once."
340
- }
343
+ },
344
+ "needs_input_count": { "type": "integer", "minimum": 0, "description": "Number of command invocations with exit_status 2." },
345
+ "failure_count": { "type": "integer", "minimum": 0, "description": "Number of command invocations with a non-zero exit_status other than 2." }
341
346
  }
342
347
  }
343
348
  }
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-lifecycle-orientation
3
- version: 1.0.30
3
+ version: 1.0.35
4
4
  description: Orient a reader to the Campaigns OS lifecycle artifacts a run has already emitted, without advancing any stage or changing any state.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.57.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.57.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -53,9 +53,9 @@ from Git HEAD, from the word "latest", or from prose in a checkout.
53
53
 
54
54
  `CONTEXT.md` is the glossary kept reconciled against the code, and it separates
55
55
  public lifecycle language from internal implementation language. A
56
- **CampaignSpec** is the JSON campaign contract; current packets use CampaignSpec
57
- 4.2, whose funnel structure lives in `funnels[]`
58
- (`schemas/campaign-spec.v4.schema.json`). A **Build Packet** is the assembly
56
+ **CampaignSpec** is the JSON campaign contract; current specs use CampaignSpec
57
+ 5.0, whose funnel structure lives in `funnels[]`
58
+ (`schemas/campaign-spec.v5.schema.json`; 4.2 and 4.3 specs are upgraded on read). A **Build Packet** is the assembly
59
59
  handoff that wraps the spec without replacing it (`docs/build-packet.md`). An
60
60
  **Assembly Report** is the machine-readable record of lifecycle progress.
61
61
 
@@ -88,8 +88,10 @@ route cannot make evidence from another local spec belong to this campaign
88
88
  (`docs/build-packet.md`, "Local-spec entry").
89
89
 
90
90
  Campaign pages are typed. The page-type vocabulary is `presell`, `landing`,
91
- `select`, `checkout`, `upsell`, `downsell` and `thankyou`; `select` is where a
92
- shopper chooses a package before checkout. Use the spec's own term for a page
91
+ `select`, `checkout_step`, `checkout`, `upsell`, `downsell` and `thankyou`;
92
+ `select` is where a shopper chooses a package before checkout, a
93
+ `checkout_step` collects checkout details without placing an order, and
94
+ `checkout` is the page that takes payment. Use the spec's own term for a page
93
95
  rather than describing it.
94
96
 
95
97
  ## Read doctor as a gate
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-readback-classification
3
- version: 1.0.30
3
+ version: 1.0.35
4
4
  description: Classify a selected campaign from the readback projection's v2 fields and write a read-only handoff without turning diagnosis into permission.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.57.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.57.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-run-evidence
3
- version: 1.0.30
3
+ version: 1.0.35
4
4
  description: Interpret existing Campaigns OS doctor, QA and proof-depth evidence without claiming more proof than the artifacts contain.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.57.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.57.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: contribution-intake
3
- version: 1.0.30
3
+ version: 1.0.35
4
4
  description: Turn a suggestion about the agent surface into a classified, evidence-checked proposal and, only with attended approval, one issue on this repository's tracker.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.57.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.57.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-build
3
- version: 1.0.35
3
+ version: 1.0.40
4
4
  description: Assemble a NEXT campaign from a doctor-cleared Build Packet, CampaignSpec/API values, prepared HTML/assets, page-kit, and starter-template contracts.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.57.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.57.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -100,7 +100,9 @@ Build rules:
100
100
  - If setup/build needs starter-template files, copy the template family atomically with its dependent pages, `_includes/`, `_layouts/`, `assets/css/`, and `assets/js/`; copying only checkout/receipt pages is incomplete.
101
101
  - Resolve SDK routing meta tags to deployed campaign-root paths, not spec literals. For example, `next-success-url: upsell/` in the spec should become `/<public_route_slug>/upsell/` in built HTML.
102
102
  - If an order bump package comes from `packages.prepurchase_*` and is not one of the main `bundles[]`, default `package_sync=false` and `show_line_total_price=false` unless the CampaignSpec explicitly says the add-on quantity must sync with the main bundle.
103
- - For two-step package-selection-before-checkout flows, use the selector page as the pre-checkout step, encode the selected cart with `forcePackageId`, preserve attribution/tracking params, and strip `forcePackageId` from the visible checkout URL after SDK initialization.
103
+ - Multi-step checkout paths run `select` → `checkout_step`… → `checkout`; only the `checkout` page takes payment. On a `select` page the swap-mode bundle selector (`data-next-bundle-selector` with `data-next-selection-mode="swap"`) writes the cart and the SDK persists it between pages; a checkout button or link (`href` from the page's `next_url`) goes to the next page. Use `forcePackageId` only on a page with no selector, such as a landing call-to-action into a checkout that renders none.
104
+ - Each `checkout_step` page's checkout form (`<form data-next-checkout="form">`) carries `data-next-checkout-step="<its next_url>"` and `data-next-step-number="<step_number>"` (frontmatter `step_number`, 1 for the first step on its path). The `checkout` page's form carries neither.
105
+ - On every page that feeds a Checkout (`select`, `checkout_step`, or a landing page with express checkout buttons), the `next-success-url` meta tag is that Checkout's post-payment destination (frontmatter `success_url`), not the next step: the SDK reads it for express orders placed on that page. Forward navigation uses the step form attribute or a plain link, never that meta tag.
104
106
  - Record intentional drops from source HTML in the assembly report, especially payment/provider changes such as "PayPal removed because CampaignSpec available_payment_methods excludes it." Polish must inherit these decisions.
105
107
  - Preserve any existing Build Context `theme` inspection state and Assembly Report `theme` application state. If build applies, skips, or invalidates generated theme CSS, update `report.theme` rather than leaving stale evidence.
106
108
  - After page-kit build, inspect rendered `_site` output: body exists, Campaign Cart runtime markers exist, `sdk_hints.meta_tags` rendered, route meta points at the campaign root, and copied funnel attribution/runtime baggage is gone.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-os
3
- version: 1.0.50
3
+ version: 1.0.55
4
4
  description: Coordinate Campaigns OS lifecycle workflows from CampaignSpec, Build Packet, starter-template contracts, stage reports, deploy evidence, and QA proof depth.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.57.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.57.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-os-setup
3
- version: 2.0.33
3
+ version: 2.0.38
4
4
  description: Bootstrap or prepare a target page-kit campaign repo from a doctor-cleared Campaigns OS Build Packet before full build wiring. Formerly installed as next-campaigns-setup; renamed 2026-08 to stop colliding with the published NextCommerceCo/skills scaffolder of that name.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.57.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.57.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-polish
3
- version: 1.1.34
3
+ version: 1.1.40
4
4
  description: Run the visual/runtime polish pass after build and before QA for a Campaigns OS campaign.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.57.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.57.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -71,14 +71,29 @@ Responsibilities:
71
71
  - For bundle selectors and order bumps, verify active/inactive visual state after interaction, mobile label wrapping, badge placement, and selected/unchecked state. A native hidden input state that disagrees with the SDK class state is a polish blocker. So is a selected/active visual driven by a *non-SDK static attribute the SDK never clears* — e.g. a hardcoded `data-selected="true"` left on the initial card. (The native `selected` attribute on `<option>`/custom selects *is* SDK/browser-managed, so do not flag that.) The selected style must follow the SDK-managed class (e.g. `.next-selected` — see the family's bundle-selector selectors in `contracts/template-brand-contract.<family>.v0.json`) so a single-select swap clears the previous selection. Verify by interaction that the previously-selected item deselects after another is picked — a selector that only ever paints "selected" on click is a blocker.
72
72
  - For exit-intent pops and promo-code inputs, polish the wrapper/copy states without breaking SDK coupon/voucher apply hooks or `cart.hasCoupon("CODE")` conditional labels.
73
73
  - If `report.theme` or `context.theme` exists, verify brand-theme load order after `next-core.css`, source-token parity for primary color/CTA/surface/text/font/radius when present, and SDK safety. When the brand layer is missing, stale, low-confidence, or unsafe to apply, record the first repair-loop defect or an explicit skipped reason.
74
- - Before recording a terminal Polish status, install the package-owned browser
75
- once with `npx --no-install campaigns-os qa install-browser`, serve the
76
- current build, and run `campaigns-os polish capture --packet <packet> --base-url <served-build-url>`
74
+ - When at least one mapped page has a design route, before recording a terminal
75
+ Polish status, install the package-owned browser once with
76
+ `npx --no-install campaigns-os qa install-browser`, serve the current build,
77
+ and run `campaigns-os polish capture --packet <packet> --base-url <served-build-url>`
77
78
  (tier `A`: it writes the polish evidence and assembly report under the target
78
79
  and fetches the served build at `--base-url`).
79
80
  The package captures every mapped route at fixed desktop/mobile viewports and
80
81
  attaches `stages.polish.evidence.visual_review.page_load`. Never hand-author,
81
82
  copy, or repair that object directly.
83
+ - If every mapped page is template stock (`skip_reason`), there is no design
84
+ route for that capture. On a local loopback preview, or a hosted preview at
85
+ the packet's non-loopback HTTP(S) `deploy.preview_url` that differs from
86
+ `deploy.production_url` by origin or route path, doctor carries the missing
87
+ evidence forward as a warning. Query or fragment text on the production
88
+ page does not make it a distinct preview. If the hosted preview URL is not
89
+ yet recorded, run
90
+ `campaigns-os qa policy set --packet <packet> --preview-url <url>` first,
91
+ then `campaigns-os next --packet <packet>`. Run
92
+ `campaigns-os next qa --packet <packet>`, then
93
+ `campaigns-os qa run --packet <packet> --base-url <preview-url> --browser
94
+ --test-order common`. The hosted preview URL satisfies the deploy handoff;
95
+ after QA records its verdict, run `campaigns-os next --packet <packet>` for
96
+ closeout. QA against the packet's production URL keeps the checkpoint blocked.
82
97
  - When a readability warning appears, report it with its colours, ratio, requirement and pages. Offer the operator options inside the brand palette (a darker or lighter shade of the same hue, the palette's text colour, or a larger, bold label where the design allows). Do not recolour the merchant's design until the operator chooses. Accept a warning only with the refs, reason and name the operator gives you at the QC handoff.
83
98
  - Record Polish as `completed`, `skipped`, or `blocked` with
84
99
  `campaigns-os record polish --packet <packet> --evidence <polish-evidence.json>`
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-qa
3
- version: 1.3.34
3
+ version: 1.3.39
4
4
  description: Run spec-aware QA from a saved Map or local-spec Build Packet and tested campaign URL after build, polish, and deploy/local evidence exist, including Playwright typed-card test-order proof.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.57.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.57.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -80,7 +80,7 @@ Rules:
80
80
  - Parity capture blocking proof is the voucher-adjusted persisted line from typed-card order readback. Browser totals and client state do not replace the persisted-line voucher guard.
81
81
  - Read client purchase values per event. A whole-cart `dl_purchase` must not mask or supply an offer-level upsell purchase expectation.
82
82
  - Live `qa parity` runs publish to the QA portal by default like other QA runs. Pass `--no-post-verdict` for dev, replay, negative-control, and other local proof runs.
83
- - Theme gate: `qa run` refuses to run when a generatable brand theme is not applied to commerce pages and no waiver exists. Apply the brand layer or record a waiver (`campaigns-os theme waive`, tier `C`: it overwrites the assembly report and doctor output / `qa run --theme-waive "<reason>"`); do not bypass the gate another way. A waived run still reports template-residue findings at warn severity.
83
+ - Theme gate: `qa run` refuses to run when a generatable brand theme is not applied to commerce pages and no waiver exists. Apply the brand layer or use the recorded waiver `campaigns-os theme waive --packet <p> --reason "<why>" --waived-by "<named human>"` (tier `C`: it overwrites the assembly report and doctor output). `qa run --theme-waive "<reason>"` is the one-run ephemeral form. Do not bypass the gate another way. A waived run still reports template-residue findings at warn severity.
84
84
  - Template residue is a QA dimension, not advice: promoted starter families must have a brand/residue/pricing contract (`contracts/template-brand-contract.<family>.v0.json`). Browser QA inspects computed styles on commerce surfaces and fails pages that still render starter defaults (`#3c7dff`/`#0a265c`, starter `next-logo.png`, paypal/klarna chrome absent from the spec).
85
85
  - Pricing visibility is a blocker: an upsell/downsell offer with zero visible price rows fails QA. Pricing surfaces render via template pricing modes (`full_price`, `compare_at_current`, `unit_price_plus_total`, `savings_badge_amount`, `code_discounted_post_checkout`), never via campaign CSS `display:none` on price wrappers.
86
86
  - The upsell and checkout bundle price checks count the SDK's `data-next-bundle-display` price as a price row when it is visible and not empty.
@@ -110,6 +110,7 @@ Rules:
110
110
  - After the base checkout test order redirects to upsell, click the rendered SDK upsell accept/decline controls to prove the live upsell path. Do not fabricate upsell lines with a direct API call.
111
111
  - Valid test-order modes are `common`, `checkout`, `accept`, `decline`, `both`, `full`, `off`, and explicit accept/decline paths such as `accept-decline-accept`.
112
112
  - Browser test orders default to `--max-test-orders 6` (an accidental-flood guard, not a permission gate). Planning happens before browser launch; when `full` exceeds the cap, use explicit sample paths or rerun with the exact larger cap printed by the command (a linear three-offer graph plans nine orders, so use `--max-test-orders 9`). That cap bounds planned paths; `--max-order-creations` bounds real order creations, defaults to the planned path count, and is reserved before each submit click. A run that hits it stops that path with an explicit budget assertion — read it as a safety stop, not as a checkout defect, and account for the orders already created before raising it.
113
+ - The checkout a test order drives is the Checkout on the tested path: the page typed `checkout`, which takes payment, never the first page typed checkout. On a multi-step path (`select` → `checkout_step`… → `checkout`) the runner enters the cart on the select page (bundle cards plus a checkout button; `--select-package` clicks the matching `data-next-bundle-card`) or on the first step, submits each `checkout_step` form and asserts it lands on the step's declared next page (`checkout_step_submitted`), and submits the order only on the Checkout. A campaign may hold several Checkouts, one per split-test path.
113
114
  - Resolve paths from the selected checkout's `expected_next_url`, then follow reachable offer `expected_accept_url` / `expected_decline_url` edges. Treat only declared receipt/thank-you pages and genuine cross-origin handoffs as terminals; an absent same-origin route is unresolved, not an external handoff.
114
115
  - `--test-order common` is the default depth. When every actual terminal path fits under the flood cap (`--max-test-orders`, 6 by default), it runs them all and records effective depth `full`, reason `under_cap`. Above the cap it runs checkout, first-offer accept/decline and the shortest real receipt path, then adds the shortest path that clicks the decline on each offer or downsell page no planned path declines yet, until the cap is reached, and names any page left out. It never trims the checkout/accept/decline/receipt sample to fit a lower cap, and it must not synthesize a receipt path from offer count.
115
116
  - A page counts as exercised only when an order of its own funnel clicked its decline control. Reaching the page, or clicking only its accept, does not count. `browser-test-order:upsell-action-coverage` reports this for every page with upsell actions in every funnel of the run, and is `pass` or `warn` only when coverage is certain (orders placed, every offer page listed with its own unshared URL, every plan matched to one funnel, every click on a declared page of the order's funnel): `warn` names each page whose decline no order clicked and each funnel no order ran through, `pass` means every page's decline was clicked. Anything uncertain is `manual_review` naming the pages and why (`evidence.reason`, `evidence.not_assessable`, `evidence.uncertainty`), including a `--test-order off` run or attempts that all failed before an order reference. Read `warn` and `manual_review` as missing proof, not as a defect.
package/skills.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "campaigns-os-skills",
3
3
  "description": "Skills bundled with Campaigns OS. `skills.sh` installs these into the shared agent skill directories (~/.claude/skills, ~/.codex/skills), so each one is a versioned package: bump the version whenever a package changes. `bundle_revision` identifies the bundle as a whole — `<package version>+skills.<n>`, where the prefix is this package's version and `<n>` counts the skill-text revisions published against it. It advances whenever ANY bundled skill changes, every SKILL.md states it on its first body line, and `campaigns-os tooling status --skills-revision <value>` compares the value an agent read from a skill against the bundle on disk. See docs/skills-revision.md.",
4
- "bundle_revision": "1.54.0+skills.1",
4
+ "bundle_revision": "1.57.0+skills.1",
5
5
  "homepage": "https://github.com/NextCommerceCo/campaigns-os",
6
6
  "retired_skills": [
7
7
  {
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "id": "next-campaigns-os",
18
18
  "name": "Campaigns OS Lifecycle",
19
- "version": "1.0.50",
19
+ "version": "1.0.55",
20
20
  "path": "skills/next-campaigns-os/SKILL.md",
21
21
  "domain": "campaigns",
22
22
  "description": "Coordinate Campaigns OS lifecycle workflows from CampaignSpec, Build Packet, starter-template contracts, stage reports, deploy evidence, and QA proof depth."
@@ -24,7 +24,7 @@
24
24
  {
25
25
  "id": "next-campaigns-os-setup",
26
26
  "name": "Campaigns OS Setup",
27
- "version": "2.0.33",
27
+ "version": "2.0.38",
28
28
  "path": "skills/next-campaigns-os-setup/SKILL.md",
29
29
  "domain": "campaigns",
30
30
  "description": "Bootstrap or prepare a target page-kit campaign repo from a doctor-cleared Campaigns OS Build Packet before full build wiring. Formerly next-campaigns-setup; renamed to release that name to the published NextCommerceCo/skills scaffolder."
@@ -32,7 +32,7 @@
32
32
  {
33
33
  "id": "next-campaigns-build",
34
34
  "name": "Campaign Build",
35
- "version": "1.0.35",
35
+ "version": "1.0.40",
36
36
  "path": "skills/next-campaigns-build/SKILL.md",
37
37
  "domain": "campaigns",
38
38
  "description": "Assemble a NEXT campaign from a doctor-cleared Build Packet, CampaignSpec/API values, prepared HTML/assets, page-kit, and starter-template contracts."
@@ -40,7 +40,7 @@
40
40
  {
41
41
  "id": "next-campaigns-polish",
42
42
  "name": "Campaign Polish",
43
- "version": "1.1.34",
43
+ "version": "1.1.40",
44
44
  "path": "skills/next-campaigns-polish/SKILL.md",
45
45
  "domain": "campaigns",
46
46
  "description": "Run the visual/runtime polish pass after build and before QA for a Campaigns OS campaign."
@@ -48,7 +48,7 @@
48
48
  {
49
49
  "id": "next-campaigns-qa",
50
50
  "name": "Campaign QA",
51
- "version": "1.3.34",
51
+ "version": "1.3.39",
52
52
  "path": "skills/next-campaigns-qa/SKILL.md",
53
53
  "domain": "campaigns",
54
54
  "description": "Run spec-aware QA from a saved Map or local-spec Build Packet and tested campaign URL after build, polish, and deploy/local evidence exist, including Playwright typed-card test-order proof."
@@ -56,7 +56,7 @@
56
56
  {
57
57
  "id": "campaign-lifecycle-orientation",
58
58
  "name": "Campaign Lifecycle Orientation",
59
- "version": "1.0.30",
59
+ "version": "1.0.35",
60
60
  "path": "skills/campaign-lifecycle-orientation/SKILL.md",
61
61
  "domain": "campaigns",
62
62
  "description": "Orient a reader to the Campaigns OS lifecycle artifacts a run has already emitted, without advancing their state: the shared vocabulary, doctor as a gate, the stage record, and the store-theme/Page Kit two-worlds trap."
@@ -64,7 +64,7 @@
64
64
  {
65
65
  "id": "campaign-run-evidence",
66
66
  "name": "Campaign Run Evidence",
67
- "version": "1.0.30",
67
+ "version": "1.0.35",
68
68
  "path": "skills/campaign-run-evidence/SKILL.md",
69
69
  "domain": "campaigns",
70
70
  "description": "Interpret existing doctor, QA verdict and proof-depth evidence without claiming more proof than the artifacts contain, and keep funnel proof apart from merchant launch readiness."
@@ -72,7 +72,7 @@
72
72
  {
73
73
  "id": "campaign-readback-classification",
74
74
  "name": "Campaign Readback Classification",
75
- "version": "1.0.30",
75
+ "version": "1.0.35",
76
76
  "path": "skills/campaign-readback-classification/SKILL.md",
77
77
  "domain": "campaigns",
78
78
  "description": "Classify a selected campaign from the campaigns-os-readback/v2 fields (artifacts, staleness.stale_keys, clean, doctor, divergences, skip_cascades) and write a read-only handoff without turning diagnosis into permission."
@@ -80,7 +80,7 @@
80
80
  {
81
81
  "id": "contribution-intake",
82
82
  "name": "Contribution Intake",
83
- "version": "1.0.30",
83
+ "version": "1.0.35",
84
84
  "path": "skills/contribution-intake/SKILL.md",
85
85
  "domain": "campaigns",
86
86
  "description": "Turn a suggestion about the agent surface into a classified, evidence-checked and redacted proposal, filed on this repository tracker only with attended approval."
@@ -38,7 +38,7 @@ export const TEMPLATE_SLICE_REQUIRED_GROUPS = Object.freeze([
38
38
  ]);
39
39
 
40
40
  const TEMPLATE_FILES_COPIED_REQUIRED_FIELDS = Object.freeze(["status", "required_groups", "groups", "paths"]);
41
- const RUNTIME_PAGE_TYPES = new Set(["checkout", "select", "upsell", "downsell", "thankyou", "receipt"]);
41
+ const RUNTIME_PAGE_TYPES = new Set(["checkout", "checkout_step", "select", "upsell", "downsell", "thankyou", "receipt"]);
42
42
  const RAW_HTML_CONVERSION_STATUSES = new Set(["pending", "in_progress", "completed", "not_required", "blocked"]);
43
43
  const SOURCE_ASSET_STRATEGIES = new Set(["pagekit_campaign_asset_root", "external_cdn", "raw_passthrough", "not_applicable", "unknown"]);
44
44
  const ROUTE_REWRITE_POLICIES = new Set(["campaignspec_routes_via_campaign_link", "pagekit_public_routes", "raw_passthrough", "not_applicable", "unknown"]);
@@ -66,6 +66,20 @@ const SCRIPT_STYLE_REFERENCE_POLICIES = new Set(["frontmatter_or_campaign_asset"
66
66
  const CTA_REWRITE_POLICIES = ROUTE_REWRITE_POLICIES;
67
67
  const LAYOUT_CHOICES = new Set(["campaign_layout", "page_layout", "raw_passthrough", "not_applicable", "unknown"]);
68
68
 
69
+ // The scalar choices record build can set; the object-valued template slice
70
+ // stays with its existing proof fields and doctor checks.
71
+ export const ADAPTER_DECISION_SCALAR_VALUES = Object.freeze(Object.assign(Object.create(null), {
72
+ raw_html_conversion_status: [...RAW_HTML_CONVERSION_STATUSES],
73
+ source_asset_strategy: [...SOURCE_ASSET_STRATEGIES],
74
+ commerce_shell_adoption: [...COMMERCE_SHELL_ADOPTIONS],
75
+ route_rewrite_policy: [...ROUTE_REWRITE_POLICIES],
76
+ config_script_strategy: [...CONFIG_SCRIPT_STRATEGIES],
77
+ frontmatter_policy: [...FRONTMATTER_POLICIES],
78
+ script_style_reference_policy: [...SCRIPT_STYLE_REFERENCE_POLICIES],
79
+ cta_rewrite_policy: [...CTA_REWRITE_POLICIES],
80
+ layout_choice: [...LAYOUT_CHOICES],
81
+ }));
82
+
69
83
  /**
70
84
  * Seeds the adapter decision record written onto a new Build Packet.
71
85
  *
@@ -170,7 +184,7 @@ export function validateAdapterSourceFiles({ decisions, sourceRoot, pages = [],
170
184
  addIssue(
171
185
  warnings,
172
186
  "source_html.raw_html_wrappers",
173
- `source_html.adapter_contract.raw_html_conversion_status is "${status}", but mapped source HTML still contains document wrapper tags: ${sample}${more}. Strip <!doctype>, <html>, <head>, and <body> before treating the page as page-kit-ready source.`
187
+ `Effective raw_html_conversion_status is "${status}", but mapped source HTML still contains document wrapper tags: ${sample}${more}. Strip <!doctype>, <html>, <head>, and <body> before treating the page as page-kit-ready source.`
174
188
  );
175
189
  }
176
190
 
@@ -184,32 +198,32 @@ export function validateAdapterDecisionGates({ decisions, location, specPages =
184
198
  addIssue(
185
199
  warnings,
186
200
  "adapter.raw_html_conversion_status",
187
- `Assembly is recorded complete, but ${location}.raw_html_conversion_status is "${decisions.raw_html_conversion_status}". Record completed/not_required after wrapper stripping, frontmatter, asset moves, script/style refs, CTA rewrites, route policy, and layout choice are settled.`
201
+ `Assembly is recorded complete, but ${location}.raw_html_conversion_status is "${decisions.raw_html_conversion_status}". Use record build --adapter-decision raw_html_conversion_status=<value> to record completed/not_required after wrapper stripping, frontmatter, asset moves, script/style refs, CTA rewrites, route policy, and layout choice are settled.`
188
202
  );
189
203
  }
190
204
  if (["raw_passthrough", "unknown"].includes(decisions.source_asset_strategy)) {
191
- addIssue(warnings, "adapter.source_asset_strategy", `Assembly is recorded complete with ${location}.source_asset_strategy="${decisions.source_asset_strategy}". Page-kit builds should normally use pagekit_campaign_asset_root so src/<slug>/assets/* publishes at /<slug>/*.`);
205
+ addIssue(warnings, "adapter.source_asset_strategy", `Assembly is recorded complete with ${location}.source_asset_strategy="${decisions.source_asset_strategy}". Use record build --adapter-decision source_asset_strategy=<value> to record pagekit_campaign_asset_root when src/<slug>/assets/* publishes at /<slug>/*.`);
192
206
  }
193
207
  if (["raw_passthrough", "unknown"].includes(decisions.route_rewrite_policy)) {
194
- addIssue(warnings, "adapter.route_rewrite_policy", `Assembly is recorded complete with ${location}.route_rewrite_policy="${decisions.route_rewrite_policy}". Record how CampaignSpec routes and CTA destinations were rewritten before QA.`);
208
+ addIssue(warnings, "adapter.route_rewrite_policy", `Assembly is recorded complete with ${location}.route_rewrite_policy="${decisions.route_rewrite_policy}". Use record build --adapter-decision route_rewrite_policy=<value> to record how CampaignSpec routes and CTA destinations were rewritten before QA.`);
195
209
  }
196
210
  if (decisions.config_script_strategy === "unknown") {
197
- addIssue(warnings, "adapter.config_script_strategy", `Assembly is recorded complete but ${location}.config_script_strategy is unknown. Record whether config scripts load via campaign assets, frontmatter scripts, inline config, or not_required.`);
211
+ addIssue(warnings, "adapter.config_script_strategy", `Assembly is recorded complete but ${location}.config_script_strategy is unknown. Use record build --adapter-decision config_script_strategy=<value> to record whether config scripts load via campaign assets, frontmatter scripts, inline config, or not_required.`);
198
212
  }
199
213
  if (["preserve_document_wrappers", "unknown"].includes(decisions.wrapper_policy)) {
200
- addIssue(warnings, "adapter.wrapper_policy", `Assembly is recorded complete with ${location}.wrapper_policy="${decisions.wrapper_policy}". Page-kit source should normally strip document wrappers unless preserving them is explicitly not_required.`);
214
+ addIssue(warnings, "adapter.wrapper_policy", `Assembly is recorded complete with ${location}.wrapper_policy="${decisions.wrapper_policy}". Select wrapper_policy at intake with prepare-build --wrapper-policy or the source-html manifest's wrapper_policy option; page-kit source should normally strip document wrappers unless preserving them is explicitly not_required.`);
201
215
  }
202
216
  if (["raw_passthrough", "unknown"].includes(decisions.frontmatter_policy)) {
203
- addIssue(warnings, "adapter.frontmatter_policy", `Assembly is recorded complete with ${location}.frontmatter_policy="${decisions.frontmatter_policy}". Record how Page Kit YAML frontmatter was created or why it is not_required.`);
217
+ addIssue(warnings, "adapter.frontmatter_policy", `Assembly is recorded complete with ${location}.frontmatter_policy="${decisions.frontmatter_policy}". Use record build --adapter-decision frontmatter_policy=<value> to record how Page Kit YAML frontmatter was created or why it is not_required.`);
204
218
  }
205
219
  if (["raw_passthrough", "unknown"].includes(decisions.script_style_reference_policy)) {
206
- addIssue(warnings, "adapter.script_style_reference_policy", `Assembly is recorded complete with ${location}.script_style_reference_policy="${decisions.script_style_reference_policy}". Record how scripts/styles load via frontmatter, campaign assets, inline blocks, or not_required.`);
220
+ addIssue(warnings, "adapter.script_style_reference_policy", `Assembly is recorded complete with ${location}.script_style_reference_policy="${decisions.script_style_reference_policy}". Use record build --adapter-decision script_style_reference_policy=<value> to record how scripts/styles load via frontmatter, campaign assets, inline blocks, or not_required.`);
207
221
  }
208
222
  if (["raw_passthrough", "unknown"].includes(decisions.cta_rewrite_policy)) {
209
- addIssue(warnings, "adapter.cta_rewrite_policy", `Assembly is recorded complete with ${location}.cta_rewrite_policy="${decisions.cta_rewrite_policy}". Record how CTA destinations were rewritten from CampaignSpec routes before QA.`);
223
+ addIssue(warnings, "adapter.cta_rewrite_policy", `Assembly is recorded complete with ${location}.cta_rewrite_policy="${decisions.cta_rewrite_policy}". Use record build --adapter-decision cta_rewrite_policy=<value> to record how CTA destinations were rewritten from CampaignSpec routes before QA.`);
210
224
  }
211
225
  if (["raw_passthrough", "unknown"].includes(decisions.layout_choice)) {
212
- addIssue(warnings, "adapter.layout_choice", `Assembly is recorded complete with ${location}.layout_choice="${decisions.layout_choice}". Record the Page Kit layout strategy before handoff.`);
226
+ addIssue(warnings, "adapter.layout_choice", `Assembly is recorded complete with ${location}.layout_choice="${decisions.layout_choice}". Use record build --adapter-decision layout_choice=<value> to record the Page Kit layout strategy before handoff.`);
213
227
  }
214
228
  }
215
229
 
@@ -219,13 +233,13 @@ export function validateAdapterDecisionGates({ decisions, location, specPages =
219
233
  addIssue(
220
234
  errors,
221
235
  "adapter.commerce_shell_adoption",
222
- `Runtime commerce pages (${runtimePages.map((page) => page.id).join(", ")}) are marked custom_html_experimental for template family "${family}". Use template_clone_first_verified or sdk_surfaces_preserved before treating checkout/upsell/downsell/receipt as build-ready.`
236
+ `Runtime commerce pages (${runtimePages.map((page) => page.id).join(", ")}) are marked custom_html_experimental for template family "${family}". Use record build --adapter-decision commerce_shell_adoption=<value> to record template_clone_first_verified or sdk_surfaces_preserved before treating checkout/upsell/downsell/receipt as build-ready.`
223
237
  );
224
238
  } else if (assemblyComplete && !["template_clone_first_verified", "sdk_surfaces_preserved"].includes(adoption)) {
225
239
  addIssue(
226
240
  warnings,
227
241
  "adapter.commerce_shell_adoption",
228
- `Assembly is recorded complete, but ${location}.commerce_shell_adoption is "${adoption || "missing"}" for runtime commerce pages (${runtimePages.map((page) => page.id).join(", ")}). Commerce pages should be template-clone-first, then styled, with SDK-owned surfaces preserved.`
242
+ `Assembly is recorded complete, but ${location}.commerce_shell_adoption is "${adoption || "missing"}" for runtime commerce pages (${runtimePages.map((page) => page.id).join(", ")}). Use record build --adapter-decision commerce_shell_adoption=<value> after commerce pages are template-clone-first, then styled, with SDK-owned surfaces preserved.`
229
243
  );
230
244
  }
231
245
  }