@nextcommerce/campaigns-os 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +204 -0
- package/CHANGELOG.md +5002 -0
- package/CONTEXT.md +685 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +368 -0
- package/agents/claude/CLAUDE.md +32 -0
- package/agents/codex/AGENTS.md +27 -0
- package/agents/copilot/copilot-instructions.md +14 -0
- package/agents/cursor/campaigns-os.mdc +13 -0
- package/bin/campaigns-os.mjs +38 -0
- package/campaign-spec/README.md +138 -0
- package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
- package/campaign-spec/dist/analytics-vocabulary.js +74 -0
- package/campaign-spec/dist/index.d.ts +40 -0
- package/campaign-spec/dist/index.js +77 -0
- package/campaign-spec/dist/normalize.d.ts +22 -0
- package/campaign-spec/dist/normalize.js +41 -0
- package/campaign-spec/dist/routing.d.ts +190 -0
- package/campaign-spec/dist/routing.js +263 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
- package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
- package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
- package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
- package/campaign-spec/dist/rules/cycle-detection.js +141 -0
- package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
- package/campaign-spec/dist/rules/design-source-shape.js +142 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
- package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
- package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
- package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
- package/campaign-spec/dist/rules/funnel-count.js +37 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
- package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
- package/campaign-spec/dist/rules/funnel-identity.js +57 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
- package/campaign-spec/dist/rules/index.d.ts +69 -0
- package/campaign-spec/dist/rules/index.js +131 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
- package/campaign-spec/dist/rules/page-count.d.ts +11 -0
- package/campaign-spec/dist/rules/page-count.js +31 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
- package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
- package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
- package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
- package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
- package/campaign-spec/dist/rules/schema-version.js +53 -0
- package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
- package/campaign-spec/dist/rules/sdk-version.js +96 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
- package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
- package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
- package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
- package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
- package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
- package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
- package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
- package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
- package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
- package/campaign-spec/dist/sdk-version-parse.js +62 -0
- package/campaign-spec/dist/types.d.ts +674 -0
- package/campaign-spec/dist/types.js +40 -0
- package/campaign-spec/package.json +12 -0
- package/compatibility.json +25 -0
- package/contracts/agent-relevant-change-policy.v1.json +111 -0
- package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
- package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
- package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
- package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
- package/contracts/commerce-surface-catalog.json +2452 -0
- package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
- package/contracts/fixtures/orientation/envelope/current.json +95 -0
- package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
- package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
- package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
- package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
- package/contracts/fixtures/orientation/envelope/refused.json +100 -0
- package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
- package/contracts/fixtures/orientation/envelope/updated.json +135 -0
- package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
- package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
- package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
- package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
- package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
- package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
- package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
- package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
- package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
- package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
- package/contracts/migration-sidecar-bundle.v0.json +149 -0
- package/contracts/orientation-limits.v1.json +41 -0
- package/contracts/orientation-reason-codes.v1.json +196 -0
- package/contracts/private-template-sources.json +8 -0
- package/contracts/release-ledger.json +4598 -0
- package/contracts/reserved-skill-names.json +13 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
- package/contracts/supported-surface.json +180 -0
- package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.apollo.v0.json +27 -0
- package/contracts/template-brand-contract.demeter.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
- package/contracts/template-brand-contract.olympus.v0.json +27 -0
- package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
- package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
- package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.apollo.v0.json +12 -0
- package/contracts/template-slot-manifest.demeter.v0.json +30 -0
- package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
- package/contracts/template-slot-manifest.olympus.v0.json +12 -0
- package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
- package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
- package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
- package/docs/brand-theme-bridge.md +159 -0
- package/docs/build-packet.md +1300 -0
- package/docs/campaign-build-brief.md +145 -0
- package/docs/campaign-standardization-report.md +329 -0
- package/docs/campaigns-os-build-flow.md +117 -0
- package/docs/design-source-package.md +784 -0
- package/docs/legacy-migration.md +58 -0
- package/docs/migration-sidecar-bundle.md +139 -0
- package/docs/orientation-contract-reference.md +1220 -0
- package/docs/polish-evidence.md +502 -0
- package/docs/qa-and-test-orders.md +1691 -0
- package/docs/release-ledger-authoring-guide.md +274 -0
- package/docs/runtime-readiness.md +211 -0
- package/docs/supported-surface.md +83 -0
- package/docs/versioning.md +55 -0
- package/docs/workflow-findings-sidecar.md +588 -0
- package/package.json +135 -0
- package/prompts/first-build.md +27 -0
- package/prompts/friction-log.md +30 -0
- package/schemas/campaign-build-brief.v1.schema.json +149 -0
- package/schemas/campaign-design-source-package.v0.schema.json +697 -0
- package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
- package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
- package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
- package/schemas/campaign-spec.v4.schema.json +582 -0
- package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
- package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
- package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
- package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
- package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
- package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
- package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
- package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
- package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
- package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
- package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
- package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
- package/schemas/source-html-manifest.v0.schema.json +252 -0
- package/skills/next-campaigns-build/SKILL.md +73 -0
- package/skills/next-campaigns-os/SKILL.md +101 -0
- package/skills/next-campaigns-os/references/session-intake.md +160 -0
- package/skills/next-campaigns-os-setup/SKILL.md +22 -0
- package/skills/next-campaigns-polish/SKILL.md +145 -0
- package/skills/next-campaigns-qa/SKILL.md +92 -0
- package/skills.json +56 -0
- package/skills.sh +64 -0
- package/src/adapter-decision-contract.mjs +333 -0
- package/src/brand-theme.mjs +1151 -0
- package/src/browser-launch.mjs +79 -0
- package/src/build-brief.mjs +781 -0
- package/src/built-site-scope.mjs +312 -0
- package/src/campaign-ecosystem.mjs +734 -0
- package/src/campaign-identity.mjs +405 -0
- package/src/campaign-workspace.mjs +127 -0
- package/src/checkpoint-waiver.mjs +302 -0
- package/src/cli.mjs +13442 -0
- package/src/commercial-journey.mjs +1119 -0
- package/src/commercial-parity.mjs +965 -0
- package/src/consent.mjs +347 -0
- package/src/content-residue.mjs +322 -0
- package/src/deadline.mjs +81 -0
- package/src/design-source-package.mjs +2604 -0
- package/src/deviation.mjs +107 -0
- package/src/doctor-check-registry.mjs +49 -0
- package/src/doctor-sidecar.mjs +106 -0
- package/src/finding-cause.mjs +557 -0
- package/src/findings.mjs +326 -0
- package/src/fs-identity.mjs +68 -0
- package/src/gate-actions.mjs +105 -0
- package/src/html-scan.mjs +53 -0
- package/src/install-mode.mjs +272 -0
- package/src/legacy-migration.d.ts +128 -0
- package/src/legacy-migration.mjs +510 -0
- package/src/lifecycle.mjs +338 -0
- package/src/local-proof.mjs +401 -0
- package/src/map-pin-writeback.mjs +210 -0
- package/src/orchestration-stage-contract.mjs +81 -0
- package/src/package-install-fixture.mjs +33 -0
- package/src/page-kit-build-summary.mjs +175 -0
- package/src/page-kit-campaign-config.mjs +57 -0
- package/src/page-kit-sdk-version.mjs +392 -0
- package/src/page-kit-store-profile.mjs +369 -0
- package/src/page-kit-sync.mjs +162 -0
- package/src/polish-browser.mjs +867 -0
- package/src/polish-capture.mjs +1094 -0
- package/src/polish-deadline.mjs +51 -0
- package/src/polish-gate.mjs +739 -0
- package/src/polish-node.mjs +639 -0
- package/src/polish-page-load.mjs +1100 -0
- package/src/private-template-source.mjs +237 -0
- package/src/proof-policy.mjs +82 -0
- package/src/qa-analytics-correctness.mjs +307 -0
- package/src/qa-analytics-errors.mjs +38 -0
- package/src/qa-analytics-parity.mjs +699 -0
- package/src/qa-binding-evidence.mjs +140 -0
- package/src/qa-browser.mjs +6608 -0
- package/src/qa-cart-entry.mjs +406 -0
- package/src/qa-commercial-parity.mjs +641 -0
- package/src/qa-node.mjs +3620 -0
- package/src/qa-order-bump.mjs +381 -0
- package/src/qa-parity-capture.mjs +428 -0
- package/src/qa-parity-fixture.mjs +359 -0
- package/src/qa-publish.mjs +362 -0
- package/src/qa-purchase-data-layer.mjs +263 -0
- package/src/qa-route-probe.mjs +272 -0
- package/src/qa-sidecar.mjs +188 -0
- package/src/qa-test-order-topology.mjs +207 -0
- package/src/qa-url-privacy.mjs +13 -0
- package/src/qa-verdict-discovery.mjs +192 -0
- package/src/qa-verdict-publish.mjs +105 -0
- package/src/qa-verdict.mjs +287 -0
- package/src/remit.mjs +388 -0
- package/src/repo-scan.mjs +83 -0
- package/src/route-identity.mjs +133 -0
- package/src/run-record-closeout.mjs +229 -0
- package/src/run-record.mjs +839 -0
- package/src/run-session.mjs +226 -0
- package/src/runtime-state-ignore.mjs +113 -0
- package/src/sdk-attribute-index.mjs +212 -0
- package/src/sdk-markup.mjs +358 -0
- package/src/sdk-meta-tags.mjs +52 -0
- package/src/shell-token.mjs +7 -0
- package/src/sidecar-bundle.mjs +399 -0
- package/src/source-asset-crawl.mjs +469 -0
- package/src/source-html-intake.mjs +627 -0
- package/src/source-html-manifest.mjs +276 -0
- package/src/source-prep.mjs +284 -0
- package/src/spec-derive-store.mjs +431 -0
- package/src/spec-derive.mjs +514 -0
- package/src/spec-fetch.mjs +66 -0
- package/src/spec-hash.mjs +48 -0
- package/src/spec-identity.mjs +27 -0
- package/src/stage-ledger.mjs +509 -0
- package/src/standardization-report.mjs +1297 -0
- package/src/template-brand-contract.mjs +473 -0
- package/src/template-freshness.mjs +196 -0
- package/src/template-reference.mjs +81 -0
- package/src/template-slot-manifest.mjs +150 -0
- package/src/text-safety.mjs +62 -0
- package/src/theme-gate.mjs +185 -0
- package/src/upsell-selector-scope.mjs +299 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Release-ledger authoring guide
|
|
2
|
+
|
|
3
|
+
Every agent-relevant change to this repository gets one entry in
|
|
4
|
+
`contracts/release-ledger.json`. This is how you write one, and what CI will do
|
|
5
|
+
to you if you do not.
|
|
6
|
+
|
|
7
|
+
If you are an agent reading this repository rather than changing it, you want
|
|
8
|
+
[`AGENTS.md`](../AGENTS.md) and
|
|
9
|
+
[`docs/orientation-contract-reference.md`](orientation-contract-reference.md)
|
|
10
|
+
instead.
|
|
11
|
+
|
|
12
|
+
## Why a ledger as well as a changelog
|
|
13
|
+
|
|
14
|
+
`CHANGELOG.md` is keyed to the supported-surface version. That misses a whole
|
|
15
|
+
class of change that a downstream agent very much cares about: a renamed CLI
|
|
16
|
+
flag, a rewritten contract doc, a new or reworded skill, a changed
|
|
17
|
+
generated-runtime input, a widened compatibility statement. None of those need
|
|
18
|
+
touch a hashed file, so none of them move `surface_version`, so an agent reading
|
|
19
|
+
only the changelog concludes nothing happened.
|
|
20
|
+
|
|
21
|
+
The ledger records those. The changelog stays the human narrative, and each
|
|
22
|
+
ledger entry links to exactly one changelog section so the two can never tell
|
|
23
|
+
different stories.
|
|
24
|
+
|
|
25
|
+
## What counts as agent-relevant
|
|
26
|
+
|
|
27
|
+
One definition, one file: `contracts/agent-relevant-change-policy.v1.json`. The
|
|
28
|
+
classifier, the gate, the generated reference, and every test read it. There is
|
|
29
|
+
no second copy to keep in sync, and you should not make one.
|
|
30
|
+
|
|
31
|
+
It defines ten semantic classes — `schema`, `hashed_surface`, `named_surface`,
|
|
32
|
+
`cli_surface`, `skill`, `package_export`, `compatibility_policy`,
|
|
33
|
+
`documentation`, `workflow`, `generated_runtime` — and classifies a changed path
|
|
34
|
+
in a fixed, total order:
|
|
35
|
+
|
|
36
|
+
1. **Self-referential exemptions.** `contracts/release-ledger.json` and
|
|
37
|
+
`CHANGELOG.md`. Recording a change is not itself a recorded change.
|
|
38
|
+
2. **Explicit rules.** Ordered, first match wins.
|
|
39
|
+
3. **Derived from the supported surface.** Every path in `hashed{}` or `named[]`
|
|
40
|
+
is agent-relevant by construction. This pass runs *before* the ignore list on
|
|
41
|
+
purpose: a path you just added to the supported surface must never be
|
|
42
|
+
swallowed by a broad ignore prefix like `docs/` or `contracts/`.
|
|
43
|
+
4. **Ignored, with a stated reason.** "No agent impact" is an assertion someone
|
|
44
|
+
wrote down, not an omission someone forgot.
|
|
45
|
+
5. **Unclassified — an error.** A path matching nothing fails the gate. The
|
|
46
|
+
classifier fails closed, which means adding a new top-level file makes you
|
|
47
|
+
say what it is.
|
|
48
|
+
|
|
49
|
+
## Writing an entry
|
|
50
|
+
|
|
51
|
+
```jsonc
|
|
52
|
+
{
|
|
53
|
+
"id": "RL-0007", // never reused, never renumbered
|
|
54
|
+
"sequence": 7, // exactly one more than the last entry
|
|
55
|
+
"date": "2026-09-01", // not earlier than the last entry
|
|
56
|
+
"kind": "release", // or "amendment"
|
|
57
|
+
"surface_version": "1.15.0", // null for a same-surface change
|
|
58
|
+
"changelog_section": "1.15.0",
|
|
59
|
+
"changelog_sha256": "<sha256 of that section body>",
|
|
60
|
+
"agent_impact": "What a consumer must do differently. 'None.' is fine — but write it.",
|
|
61
|
+
"compatibility": "compatible | additive | breaking",
|
|
62
|
+
"migration": "The exact action, or \"none\". A breaking entry may not say none.",
|
|
63
|
+
"changes": [
|
|
64
|
+
{
|
|
65
|
+
"class": "named_surface",
|
|
66
|
+
"path": "docs/build-packet.md",
|
|
67
|
+
"surface_entry": "docs/build-packet.md",
|
|
68
|
+
"summary": "One sentence, written for a consumer."
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"entry_sha256": "<sha256 of this entry with entry_sha256 removed>"
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`sequence` and `id` are two different things. `sequence` is the entry's
|
|
76
|
+
position in the array and must equal that position: the checker derives the
|
|
77
|
+
expected value from the index, not from the previous entry's claim, so it is the
|
|
78
|
+
authoritative order. `id` is a unique, immutable label (`RL-NNNN`) that is never
|
|
79
|
+
reused and never renumbered; nothing requires its number to match `sequence`.
|
|
80
|
+
The two therefore diverge legitimately, and already do in this ledger: when two
|
|
81
|
+
PRs are open at once, the second to merge restamps its `sequence` to sit after
|
|
82
|
+
the first while keeping the id it was written with. Read order from `sequence`
|
|
83
|
+
and identity from `id`; never renumber an id to close the gap.
|
|
84
|
+
|
|
85
|
+
Two hashes, two jobs. `changelog_sha256` catches a changelog section edited
|
|
86
|
+
after the fact. `entry_sha256` catches a historical entry edited in place, even
|
|
87
|
+
in a squashed history where the diff is gone.
|
|
88
|
+
|
|
89
|
+
Computing them:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
node --input-type=module -e '
|
|
93
|
+
import { readFileSync } from "node:fs";
|
|
94
|
+
import { parseChangelogSections, entryHash } from "./scripts/orientation-contract.mjs";
|
|
95
|
+
const section = parseChangelogSections(readFileSync("CHANGELOG.md", "utf8"))
|
|
96
|
+
.find((s) => s.section_id === "1.15.0");
|
|
97
|
+
console.log("changelog_sha256:", section.body_sha256);
|
|
98
|
+
'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Then add the entry with that `changelog_sha256`, and compute `entry_sha256` the
|
|
102
|
+
same way with `entryHash(entry)`.
|
|
103
|
+
|
|
104
|
+
### Same-surface changes
|
|
105
|
+
|
|
106
|
+
Set `surface_version` to `null` and link a changelog section identified as
|
|
107
|
+
`<current-version>+agent.<n>` — for example `1.14.0+agent.1`. It sorts at the
|
|
108
|
+
same version, so nobody reads it as a release that did not happen, and it gives
|
|
109
|
+
the entry a real section to point at.
|
|
110
|
+
|
|
111
|
+
### Path-less change items
|
|
112
|
+
|
|
113
|
+
A CLI flag has no file of its own. Record it with `"path": null` and name the
|
|
114
|
+
affected `surface_entry` (the command). The gate still requires that some
|
|
115
|
+
classified path of the same class changed in the range, so a flag change cannot
|
|
116
|
+
be recorded without the bytes actually moving somewhere.
|
|
117
|
+
|
|
118
|
+
### Fixes that touch only policy-ignored paths
|
|
119
|
+
|
|
120
|
+
A fix living entirely in paths the policy ignores — `src/` other than
|
|
121
|
+
`src/cli.mjs`, `scripts/`, tests and fixtures — carries a same-surface CHANGELOG
|
|
122
|
+
section (`X.Y.Z+agent.N`) and **no ledger entry**. There is nothing for an entry
|
|
123
|
+
to claim: every change item must map to a classified changed path in the range,
|
|
124
|
+
and an ignored path is never classified, so an entry written for such a PR is
|
|
125
|
+
refused by the backward direction of the gate rather than merely unnecessary. A
|
|
126
|
+
path-less item fails the same way, because no classified change of its class
|
|
127
|
+
exists in the range. The ignore list and its stated reasons are in
|
|
128
|
+
[`contracts/agent-relevant-change-policy.v1.json`](../contracts/agent-relevant-change-policy.v1.json).
|
|
129
|
+
|
|
130
|
+
The dividing line inside `src/` is `src/cli.mjs`: an explicit rule classifies it
|
|
131
|
+
as `cli_surface`, so any change to it is agent-relevant and owes an entry, even
|
|
132
|
+
when the behaviour change originates in a helper module beside it.
|
|
133
|
+
|
|
134
|
+
### Amendments
|
|
135
|
+
|
|
136
|
+
Historical entries are never edited. When an entry turns out to be wrong or
|
|
137
|
+
incomplete, append a correction:
|
|
138
|
+
|
|
139
|
+
```jsonc
|
|
140
|
+
{
|
|
141
|
+
"id": "RL-0008",
|
|
142
|
+
"sequence": 8,
|
|
143
|
+
"kind": "amendment",
|
|
144
|
+
"amends": "RL-0007",
|
|
145
|
+
"amendment_reason": "RL-0007 was recorded as compatible; it removed a documented guarantee.",
|
|
146
|
+
"surface_version": null,
|
|
147
|
+
"changelog_section": "1.15.0+agent.1",
|
|
148
|
+
"compatibility": "breaking",
|
|
149
|
+
"migration": "Stop relying on the removed guarantee; see docs/build-packet.md.",
|
|
150
|
+
"agent_impact": "Treat the 1.15.0 packet doc change as breaking, not compatible.",
|
|
151
|
+
"changes": [ /* … */ ]
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
An amendment is the only entry kind whose change items may map to no changed
|
|
156
|
+
path in its own range, because it corrects meaning rather than moving bytes.
|
|
157
|
+
|
|
158
|
+
### Correcting a section's bytes
|
|
159
|
+
|
|
160
|
+
A changelog section an entry hashes is as frozen as the entry. When the section
|
|
161
|
+
itself has to change — a stray merge-conflict marker committed inside it is the
|
|
162
|
+
case that has happened — the historical entry cannot take the new hash, and a
|
|
163
|
+
plain amendment pointing at some other section leaves the stale hash failing.
|
|
164
|
+
So an amendment may link the **same** `changelog_section` as the entry it
|
|
165
|
+
amends, carrying that section's current `changelog_sha256`. The checker then
|
|
166
|
+
reads the amended entry's hash as superseded, and the amendment's own hash
|
|
167
|
+
keeps the section pinned. Only an amendment, and only one that names the
|
|
168
|
+
entry currently holding the link, may re-link a section; any other second link
|
|
169
|
+
still fails the one-to-one rule. Say in `amendment_reason` what changed in the
|
|
170
|
+
section and why.
|
|
171
|
+
|
|
172
|
+
`scripts/check-changelog-structure.mjs` (part of `npm run check`) refuses the
|
|
173
|
+
marker lines outright, in `CHANGELOG.md` and under `docs/`, and also holds the
|
|
174
|
+
section layout: identifiers unique, `+agent.N` sections in one run directly
|
|
175
|
+
above their release with N descending (newest first), and every ledger
|
|
176
|
+
`changelog_section` naming a section that exists. Insert a new `+agent.N`
|
|
177
|
+
section at the top of its release's run, not directly above the release
|
|
178
|
+
heading.
|
|
179
|
+
|
|
180
|
+
## Running the gate
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
node ./scripts/check-release-ledger.mjs # structure, hashes, limits
|
|
184
|
+
node ./scripts/check-release-ledger.mjs --base origin/main # the two-way gate
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Without `--base` the checker validates the ledger as it stands. The completeness
|
|
188
|
+
gate needs a comparison point, so pass `--base` in CI and before you open a PR.
|
|
189
|
+
`npm run check` runs the structural half.
|
|
190
|
+
|
|
191
|
+
## What passes and what fails
|
|
192
|
+
|
|
193
|
+
The authoritative matrix is data, not prose:
|
|
194
|
+
`contracts/fixtures/orientation/release-gate/cases.json`. Every case there is a
|
|
195
|
+
test in `scripts/check-release-ledger.test.mjs`. Add a case and you have added a
|
|
196
|
+
test.
|
|
197
|
+
|
|
198
|
+
Passes:
|
|
199
|
+
|
|
200
|
+
- A hashed schema changed, `surface_version` advanced, one entry claims it.
|
|
201
|
+
- A named contract doc changed with no surface bump, one entry with
|
|
202
|
+
`surface_version: null` and a `+agent.N` changelog section.
|
|
203
|
+
- A CLI flag, a skill, a workflow, or the compatibility statement changed, same
|
|
204
|
+
shape.
|
|
205
|
+
- Several changes in one entry, each path covered by exactly one change item.
|
|
206
|
+
- An amendment that maps to no changed path.
|
|
207
|
+
|
|
208
|
+
Fails:
|
|
209
|
+
|
|
210
|
+
- An agent-relevant path with no change item — the failure the gate exists for.
|
|
211
|
+
- A change item naming a path that did not change and is not an amendment.
|
|
212
|
+
- A change item claiming an implementation path the policy excludes.
|
|
213
|
+
- A duplicate entry id, or one entry recording the same `(class, path,
|
|
214
|
+
surface_entry)` identity twice. Identity is unique WITHIN an entry, not across
|
|
215
|
+
the ledger: a path is touched by many releases over a repository's life, and
|
|
216
|
+
each of those is a real change that must be recordable. Recording one change
|
|
217
|
+
twice inside a single range is caught by the coverage rule instead — a path
|
|
218
|
+
covered by more than one change item fails.
|
|
219
|
+
- A sequence gap, or a date earlier than the previous entry.
|
|
220
|
+
- A `breaking` entry whose migration is `none`, or a blank `agent_impact`.
|
|
221
|
+
- A missing changelog section, a duplicate section identifier, or a stale
|
|
222
|
+
`changelog_sha256`.
|
|
223
|
+
- A stale `entry_sha256`.
|
|
224
|
+
- Any commit-shaped property on an entry — see below.
|
|
225
|
+
- A rewritten or deleted historical entry.
|
|
226
|
+
- Two entries claiming the same `surface_version`.
|
|
227
|
+
- A changed path the policy has never seen.
|
|
228
|
+
- A supported-surface bump that no new entry claims.
|
|
229
|
+
- A path-less change item with no classified change of its class in the range.
|
|
230
|
+
- An amendment with no `amends`, an `amends` naming no earlier entry, or no
|
|
231
|
+
`amendment_reason`; or a non-amendment carrying either field.
|
|
232
|
+
|
|
233
|
+
Only entries NEW in the comparison range are classified against the current
|
|
234
|
+
policy and supported surface. A historical entry was written under the policy in
|
|
235
|
+
force at the time and the ledger is append-only, so re-judging it under a
|
|
236
|
+
tightened policy would fail a document nobody is permitted to edit. Everything
|
|
237
|
+
else — shape, ordering, sequence, hashes, changelog correspondence — applies to
|
|
238
|
+
every entry.
|
|
239
|
+
|
|
240
|
+
## Entries carry no commit
|
|
241
|
+
|
|
242
|
+
An entry cannot name the commit containing it without being rewritten after that
|
|
243
|
+
commit exists. The schema rejects any commit-shaped property, and the checker
|
|
244
|
+
says so by name rather than reporting a generic "unexpected property".
|
|
245
|
+
|
|
246
|
+
A consumer derives the introducing commit from history at the target OID:
|
|
247
|
+
oldest-first over the commits touching the ledger, crediting each entry id to the
|
|
248
|
+
first commit whose ledger blob contains it. Merge commits are handled by that
|
|
249
|
+
walk without a special case.
|
|
250
|
+
|
|
251
|
+
The walk covers the FULL history ending at the target, with history
|
|
252
|
+
simplification disabled (`git rev-list --full-history --reverse --topo-order
|
|
253
|
+
<oid> -- contracts/release-ledger.json`). A walk that starts at some base loses
|
|
254
|
+
every entry introduced before it; a simplified walk can drop the side-branch
|
|
255
|
+
commit that actually introduced an entry. `AGENTS.md` states both properties for
|
|
256
|
+
consumers.
|
|
257
|
+
|
|
258
|
+
## Size bounds
|
|
259
|
+
|
|
260
|
+
`contracts/orientation-limits.v1.json` bounds what a consumer reads: source
|
|
261
|
+
bytes, section count, section bytes, envelope bytes, ledger entries. Exceeding
|
|
262
|
+
one is a refusal with `orientation_too_large`, never a truncation. So is a
|
|
263
|
+
measurement that is absent or non-finite: a bound nobody measured is a bound
|
|
264
|
+
nobody enforced.
|
|
265
|
+
|
|
266
|
+
Every bound is a whole-artifact guardrail — the complete changelog and the
|
|
267
|
+
complete ledger at the target commit, not a baseline-to-target window. That is
|
|
268
|
+
the conservative direction, since a window is always a subset of the whole. Two
|
|
269
|
+
of them (section count, ledger entries) grow monotonically; when one is reached
|
|
270
|
+
the answer is baseline rotation, described in the contract's `_growth_note`, not
|
|
271
|
+
a quiet raise.
|
|
272
|
+
|
|
273
|
+
Raising a limit is a reviewed policy change: advance `limits_version`, and the
|
|
274
|
+
change owes its own ledger entry like anything else.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
GENERATED FILE — do not edit.
|
|
3
|
+
Source: contracts/runtime-recipe.campaigns-os-node-v1.json
|
|
4
|
+
Regenerate: node ./scripts/generate-runtime-readiness.mjs --write
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# Runtime readiness
|
|
8
|
+
|
|
9
|
+
How a checkout of this repository at one commit becomes a usable installed runtime, and how a consumer decides whether a prepared one is still trustworthy. Everything below is generated from `contracts/runtime-recipe.campaigns-os-node-v1.json`, which is the only authority for these values.
|
|
10
|
+
|
|
11
|
+
Recipe kind `campaigns-os-node-v1`, revision `1.0.1`, validated by `schemas/campaigns-os-runtime-recipe.v1.schema.json` (`Campaigns OS Runtime Recipe v1`). Supported surface at generation time: `1.33.0`.
|
|
12
|
+
|
|
13
|
+
## What this is
|
|
14
|
+
|
|
15
|
+
A recipe is data, not code. A consumer executes exactly the commands enumerated here and never a command assembled from repository data. This repository publishes the recipe; the installed consumer bootstrap executes it, using its own released parser rather than anything loaded from the checkout under evaluation.
|
|
16
|
+
|
|
17
|
+
Enforcement is fail-closed. Every field is normative: `fail_closed` is `true` and a check that cannot be performed counts as `failed`, never as skipped. A refusal carries the stable reason code `runtime_recipe_refused`.
|
|
18
|
+
|
|
19
|
+
## What a prepared runtime can and cannot do
|
|
20
|
+
|
|
21
|
+
What a generation prepared by this recipe can and cannot do. Stated explicitly because 'runtime ready' invites the wrong reading: the same flag that makes the install safe also means the prepared generation has no browser to drive. Preparing a runtime and being able to run browser QA are different readiness questions, and this recipe answers only the first.
|
|
22
|
+
|
|
23
|
+
| Capability | Available |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `type_check` | yes |
|
|
26
|
+
| `build_spec` | yes |
|
|
27
|
+
| `browser_qa` | **no** |
|
|
28
|
+
|
|
29
|
+
## Preconditions
|
|
30
|
+
|
|
31
|
+
All four must already hold before any step runs. They are fixed booleans because a precondition a document could switch off is not a precondition.
|
|
32
|
+
|
|
33
|
+
- `target_oid_resolved`
|
|
34
|
+
- `lockfile_present`
|
|
35
|
+
- `clean_staging_generation`
|
|
36
|
+
- `input_fingerprint_recorded`
|
|
37
|
+
|
|
38
|
+
## Tool versions
|
|
39
|
+
|
|
40
|
+
| Tool | Accepted range | Verified against | Rationale |
|
|
41
|
+
|---|---|---|---|
|
|
42
|
+
| Node | `>=20.19.0 <25` | `22.23.1` | The lower bound is the target's own declared minimum. The upper bound is the recipe's, not the target's: the target declares an open-ended minimum, and an open-ended range is not a bound. Delegating the ceiling to the checkout under evaluation would let that checkout widen the accepted runtime of the consumer evaluating it. Qualifying a new Node major is a one-line revision plus a ledger entry, which is the intended cost. |
|
|
43
|
+
| npm | `10 || 11` | `10.9.8`, `11.19.1` | Both majors were run end to end against this recipe and produced byte-identical output, and together they are exactly the set that supported Node release lines ship by default. Later majors are opt-in installs rather than what the ecosystem is running, so they stay outside the range until a Node line bundles one; that trigger is an external fact rather than a taste call. |
|
|
44
|
+
|
|
45
|
+
The contract declares its own ranges rather than inheriting the target's. It records the target's `engines.node` as `>=20.19.0`; on disagreement the disposition is `refuse`. The exact value this revision was authored against. A consumer compares the target's live value to this string and refuses on any difference, per on_disagreement. That is deliberately strict: a widened engines range in the target is precisely the silent widening this contract exists to catch, and re-agreeing is a one-line revision.
|
|
46
|
+
|
|
47
|
+
## Network
|
|
48
|
+
|
|
49
|
+
Two independent bounds that must both hold. The per-step policy bounds WHERE bytes may come from; the integrity digests recorded in the lockfile bound WHICH bytes are acceptable — that second bound is declared at `target_expectations.lockfile.integrity_pinned`, over the file at `target_expectations.lockfile.path`, which is also listed in `inputs.files`. Neither bound substitutes for the other. WHO ENFORCES THIS, PRECISELY: this object is a declaration the CONSUMER enforces. The argv in `steps[].args` deliberately does not carry it — there is no `--registry` and no offline flag there, and no `env` field on a step. A conforming consumer constructs the process environment for each step so that its package manager resolves only from that step's declared `hosts` (and from nothing at all where the policy is `deny`), owns the cache and both npmrc paths per `cache_ownership`, and refuses inherited npmrc, proxy, and credential configuration per the three `inherit_*` fields above. Carrying the pin in argv instead would change the recipe's commands, which is a new recipe KIND rather than a revision; it is a legitimate future design, not a silent fix. WHAT THAT GUARANTEES: a package-manager configuration bound, not a host-level network sandbox. It cannot stop a process from opening a socket to some other address. What bounds that is the other half of the recipe: `--ignore-scripts` on both steps means no third-party dependency code executes during preparation at all, so the only programs that run are the package manager and the compiler. Read each step's policy as 'the consumer configures this step's process to resolve packages only from that step's declared hosts, which for a `deny` step is none at all, and no third-party code runs that could disregard it' — which is true and checkable. Do not read it as 'the host is prevented from reaching anything else', which would require a sandbox this contract does not specify. A consumer that adds a real network sandbox strengthens this bound without changing a field below, and is encouraged to; a consumer that treats the declared hosts as advisory violates it.
|
|
50
|
+
|
|
51
|
+
| Step | Policy | Hosts | Rationale |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| `install` | `allowlist` | `registry.npmjs.org` | The install step is the only step that needs bytes it does not already have, and it needs them from exactly one place. Measured cold-cache install is a few seconds, so the network window is small. The cache is an optimisation and never a correctness input: an offline-after-warm policy would make a stale or poisoned cache silently change what gets built, with no fetch left to catch it. Note that `args` above carries no registry flag — the consumer is responsible for setting the package manager's registry to this host in the process environment before invoking that argv, and for refusing any inherited npmrc, proxy, or credential configuration that could redirect it. Combined with `--ignore-scripts`, nothing that runs during install is third-party code that could disregard the setting. The integrity digests remain the independent second bound, which is what makes a redirected or substituted byte stream fail even if the first bound were evaded. |
|
|
54
|
+
| `build` | `deny` | none | The build step is a local type-directed compile and needs no network at all. Declaring that turns an assumption into a check. As with install, `args` above carries no offline flag — the consumer puts the package manager into offline mode through the process environment it constructs for this step. `tsc` opens no sockets of its own, and `--ignore-scripts` keeps pre/post hooks from introducing any. |
|
|
55
|
+
|
|
56
|
+
Cache ownership is `consumer_profile`. Inherited proxy configuration: `false`. Inherited credentials: `false`. Inherited npm configuration file: `false`.
|
|
57
|
+
|
|
58
|
+
## Steps
|
|
59
|
+
|
|
60
|
+
### install
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
npm ci --ignore-scripts --no-audit --fund=false
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Working directory `target_root`, stdin `closed`, lifecycle scripts `disabled`, bounded by `install_seconds`.
|
|
67
|
+
|
|
68
|
+
ci rather than install, so the lockfile is authoritative and the tree is reproducible. --ignore-scripts is the load-bearing flag: it suppresses every dependency lifecycle script and the target's own prepare. Exactly one dependency in the resolved tree declares an install script, and it ships a prebuilt binary in its published tarball, so nothing in the tree needs its scripts to function. --no-audit and --fund=false remove two network- and output-side effects that are not part of preparing a runtime.
|
|
69
|
+
|
|
70
|
+
### build
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
npm run --ignore-scripts build:spec
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Working directory `target_root`, stdin `closed`, lifecycle scripts `disabled`, bounded by `build_seconds`.
|
|
77
|
+
|
|
78
|
+
Not redundant with the install step. Because install runs with lifecycle scripts disabled, the target's prepare script does not fire and the output directory is absent afterwards; this step is the only thing that builds the runtime under the recipe's own flags. --ignore-scripts here means the named script runs while its pre and post siblings do not, so the build is exactly one contracted command rather than an open-ended chain the target can extend.
|
|
79
|
+
|
|
80
|
+
## Inputs
|
|
81
|
+
|
|
82
|
+
The complete input set, enumerated explicitly rather than globbed. The compiler's configured include globs are NOT the input set: two root modules enter the compilation transitively through imports from the entry module and are emitted, so a fingerprint derived from the globs would cover 36 of the 38 compiled sources and miss one of the larger emitted surfaces. Test fixtures and the package's own tests are not inputs; nothing under them is emitted. A checker resolves the compiler's actual file list and asserts it equals this enumeration, so this list cannot rot silently.
|
|
83
|
+
|
|
84
|
+
Fingerprint algorithm `sha256`, over 42 enumerated files:
|
|
85
|
+
|
|
86
|
+
- `campaign-spec/analytics-vocabulary.ts`
|
|
87
|
+
- `campaign-spec/index.ts`
|
|
88
|
+
- `campaign-spec/normalize.ts`
|
|
89
|
+
- `campaign-spec/package.json`
|
|
90
|
+
- `campaign-spec/routing.ts`
|
|
91
|
+
- `campaign-spec/rules/analytics-contract-shape.ts`
|
|
92
|
+
- `campaign-spec/rules/assembly-hints-shape.ts`
|
|
93
|
+
- `campaign-spec/rules/campaign-metadata.ts`
|
|
94
|
+
- `campaign-spec/rules/checkout-has-success-url.ts`
|
|
95
|
+
- `campaign-spec/rules/cycle-detection.ts`
|
|
96
|
+
- `campaign-spec/rules/design-source-shape.ts`
|
|
97
|
+
- `campaign-spec/rules/downsell-without-upsell.ts`
|
|
98
|
+
- `campaign-spec/rules/exit-intent-validation.ts`
|
|
99
|
+
- `campaign-spec/rules/funnel-count.ts`
|
|
100
|
+
- `campaign-spec/rules/funnel-hypothesis-length.ts`
|
|
101
|
+
- `campaign-spec/rules/funnel-identity.ts`
|
|
102
|
+
- `campaign-spec/rules/funnel-weight-sum.ts`
|
|
103
|
+
- `campaign-spec/rules/index.ts`
|
|
104
|
+
- `campaign-spec/rules/offer-ref-integrity.ts`
|
|
105
|
+
- `campaign-spec/rules/package-pricing-sanity.ts`
|
|
106
|
+
- `campaign-spec/rules/page-count.ts`
|
|
107
|
+
- `campaign-spec/rules/page-id-uniqueness.ts`
|
|
108
|
+
- `campaign-spec/rules/promo-code-input-validation.ts`
|
|
109
|
+
- `campaign-spec/rules/promo-codes-shape.ts`
|
|
110
|
+
- `campaign-spec/rules/route-field-ignored-for-page-type.ts`
|
|
111
|
+
- `campaign-spec/rules/route-target-resolves.ts`
|
|
112
|
+
- `campaign-spec/rules/schema-version.ts`
|
|
113
|
+
- `campaign-spec/rules/sdk-version.ts`
|
|
114
|
+
- `campaign-spec/rules/shipping-countries-shape.ts`
|
|
115
|
+
- `campaign-spec/rules/shipping-methods-present.ts`
|
|
116
|
+
- `campaign-spec/rules/store-profile-shape.ts`
|
|
117
|
+
- `campaign-spec/rules/thank-you-requirement.ts`
|
|
118
|
+
- `campaign-spec/rules/unknown-top-level-fields.ts`
|
|
119
|
+
- `campaign-spec/rules/upsell-has-packages.ts`
|
|
120
|
+
- `campaign-spec/rules/upsell-routing-complete.ts`
|
|
121
|
+
- `campaign-spec/rules/upsell-without-checkout.ts`
|
|
122
|
+
- `campaign-spec/rules/variant-labels-shape.ts`
|
|
123
|
+
- `campaign-spec/sdk-version-parse.ts`
|
|
124
|
+
- `campaign-spec/tsconfig.build.json`
|
|
125
|
+
- `campaign-spec/types.ts`
|
|
126
|
+
- `package-lock.json`
|
|
127
|
+
- `package.json`
|
|
128
|
+
|
|
129
|
+
## Outputs
|
|
130
|
+
|
|
131
|
+
The output directory is a build product. It is untracked and git-ignored, no committed copy exists, and the copy in a published tarball exists only because packing runs the prepare script. There is therefore no baseline hash for its CONTENTS that this repository could publish, and any acceptance criterion phrased as 'output matches expected hashes' is not implementable as written. Verification is self-consistency instead: the inventory is complete and has nothing extra, the recorded hashes still hold, the entry module imports, the type entry is present, and the inputs that produced the output still match the inputs at the target commit. The build is deterministic — independent clean checkouts at one commit produce byte-identical output, and the compiler config emits neither source maps nor declaration maps, so no absolute paths or timestamps are embedded — which is what makes content hashing a sound strategy rather than a hopeful one.
|
|
132
|
+
|
|
133
|
+
Directory `campaign-spec/dist`. Committed: `false`. Type entry `campaign-spec/dist/index.d.ts`.
|
|
134
|
+
|
|
135
|
+
Expected inventory is derived, never listed twice. For every enumerated input under module_source_root whose path ends in .ts, the build is expected to emit campaign-spec/dist/<path relative to module_source_root, with .ts replaced> once per entry in emitted_extensions. The expected inventory is exactly that set: nothing missing, nothing extra. Emitted extensions: `.js`, `.d.ts`. At this revision that derivation yields 76 files.
|
|
136
|
+
|
|
137
|
+
### Mandatory checks
|
|
138
|
+
|
|
139
|
+
Every check below is mandatory; there is no optional check, because an optional check is an advisory bound under another name.
|
|
140
|
+
|
|
141
|
+
| Check | Kind | Detects | Applies to | Rationale |
|
|
142
|
+
|---|---|---|---|---|
|
|
143
|
+
| `dist_inventory` | `dist_inventory` | `absent`, `extra` | The set of files present under outputs.directory after the build step. | Catches both directions. A missing module is an incomplete emit; an unexpected file is as much a signal as a missing one, because it means something other than the declared build wrote into the output directory. |
|
|
144
|
+
| `content_hash_stability` | `content_hash_stability` | `corrupt` | The bytes of every file under outputs.directory, hashed with the inputs.fingerprint_algorithm digest and recorded in the generation manifest. | Detects any post-build modification of the prepared runtime. Costs single-digit milliseconds against an output measured in hundreds of kilobytes. It is a self-consistency check, not a comparison against a hash published here: no such hash can exist, because the output is not committed. |
|
|
145
|
+
| `module_import_smoke` | `module_import_smoke` (shallow) | `corrupt` | Importing the emitted package entry module once. | A file can hash cleanly and still be unloadable. Shallow for this revision: importing the entry module transitively loads most of the emitted graph for one import's cost. A per-module deep import is a revision, warranted if a partial emit is ever actually observed rather than in anticipation of one. |
|
|
146
|
+
| `type_entry_presence` | `type_entry_presence` | `absent` | outputs.type_entry. | Consumers resolve types for the package export through this file, and the pack check already asserts its presence in the published tarball. Cheap, and it guards a real declared contract rather than an internal detail. |
|
|
147
|
+
| `cli_skill_commit_agreement` | `cli_skill_commit_agreement` | `mismatched_generation` | The executable and the skills tree resolved by the running session. | Asserts that the executable a session runs and the skills tree it loads resolve below the same generation path and the same target OID. Two halves of a session drawn from different generations is the failure this catches, and neither hashes nor imports would notice it. |
|
|
148
|
+
| `tool_versions` | `tool_versions` | `unsupported_tooling` | The Node and npm versions that actually executed the steps, against tooling.node and tooling.npm. | Recorded after the fact as well as checked before, so a generation carries evidence of what built it rather than only of what was permitted to. |
|
|
149
|
+
| `input_fingerprint` | `input_fingerprint` | `stale` | The digest over the enumerated inputs.files recorded at build time, compared against the same digest computed at the target commit. | The one check that cannot be dropped. Stale output is internally consistent — its hashes are correct, it imports, its types are present — so it is invisible to every output-side check. Only comparing the inputs that produced it against the inputs at the target commit catches it. |
|
|
150
|
+
|
|
151
|
+
### Prepared-runtime states
|
|
152
|
+
|
|
153
|
+
Declarative descriptions of the prepared-runtime states an output-check implementation must distinguish. A test builds each state from the accepted recipe rather than from paths written down here, so adding a module to the input set cannot leave a fixture describing an inventory that no longer exists.
|
|
154
|
+
|
|
155
|
+
| State | Expect | Detected as | Why it matters |
|
|
156
|
+
|---|---|---|---|
|
|
157
|
+
| `healthy` | pass | — | The control. Without it the four failure fixtures prove only that the checker fails, not that it discriminates. |
|
|
158
|
+
| `absent` | fail | `absent` | Nothing was built, or the output directory was removed after the build. The inventory check is the only one that can report this cleanly; every other check would report a cascade. |
|
|
159
|
+
| `extra` | fail | `extra` | Something other than the declared build wrote into the output directory. An unexpected file is as much a signal as a missing one. |
|
|
160
|
+
| `corrupt` | fail | `corrupt` | A file was modified after the build recorded its digest. Caught by hash stability; the import smoke is the second line for the case where the alteration also makes the module unloadable. |
|
|
161
|
+
| `stale` | fail | `stale` | The output is internally consistent — complete inventory, correct hashes, imports fine — but was produced from inputs that no longer match the target commit. Only the input fingerprint sees this. |
|
|
162
|
+
|
|
163
|
+
## What the recipe assumes about the target
|
|
164
|
+
|
|
165
|
+
What this revision assumes about the target, stated as values a checker can compare rather than as prose a reader has to trust. Each one is a thing that, if it changed without the recipe changing, would silently alter what preparation does: a widened engine range, a rewritten build script, a lockfile format the install step reads differently, or a new dependency that would execute code the moment the suppressing flag was dropped.
|
|
166
|
+
|
|
167
|
+
| Assumption | Value |
|
|
168
|
+
|---|---|
|
|
169
|
+
| Manifest | `package.json` |
|
|
170
|
+
| Lockfile | `package-lock.json`, version `3`, integrity pinned `true` |
|
|
171
|
+
| Script `build:spec` | `tsc -p campaign-spec/tsconfig.build.json` |
|
|
172
|
+
| Script `prepare` | `npm run build:spec` |
|
|
173
|
+
| Dependencies declaring an install script | `fsevents` |
|
|
174
|
+
|
|
175
|
+
## Bounds
|
|
176
|
+
|
|
177
|
+
| Bound | Value | Measured baseline | Applies to | Rationale |
|
|
178
|
+
|---|---|---|---|---|
|
|
179
|
+
| `install_seconds` | 180 seconds | about 3.2 seconds on a cold cache, about 0.3 seconds warm | Wall-clock duration of the install step. | Roughly 57x the measured cold-cache cost, and deliberately generous. The measurement is a fast local connection, which is the best case rather than the typical one; this bound has to hold on a cold cache, a congested network, a loaded machine, and in CI. A timeout that trips on a slow morning produces a refusal the operator cannot act on. |
|
|
180
|
+
| `build_seconds` | 90 seconds | about 0.8 seconds | Wall-clock duration of the build step and the output checks that follow it. | Roughly 115x the measured cost. The whole mandatory check set adds well under a second on top, so nothing here is deferred for cost. Generous for the same reason as the install bound. |
|
|
181
|
+
| `transaction_seconds` | 450 seconds | about 4 seconds end to end | Wall-clock duration of the whole preparation transaction: preconditions, both steps, and every output check. | Roughly 110x measured. It bounds the transaction as a whole rather than being the sum of its parts, so a phase that stalls short of its own bound still cannot hold a preparation open indefinitely. |
|
|
182
|
+
| `max_output_bytes` | 16777216 bytes | 240,359 bytes | Total bytes of all files under outputs.directory after the build step. | 16 MiB, roughly 70x the measured output. This bound does not exist in the performance budget it otherwise mirrors; it is added so that a build which goes haywire is a typed refusal rather than a filled disk. |
|
|
183
|
+
| `max_output_files` | 4096 files | 76 files | Count of files under outputs.directory after the build step. | Roughly 54x the measured count. Paired with max_output_bytes because the two catch different runaway shapes: many small files, and few enormous ones. |
|
|
184
|
+
|
|
185
|
+
When a bound below is genuinely reached, the answer is to find out why before raising it. A dependency install that exceeds its bound on a warm machine is a supply-chain change, not a slow morning; an output inventory that exceeds its file or byte bound is a build that went wrong, not a package that grew 50x overnight. Raising a bound is the fallback, it advances recipe_revision, and it owes a release-ledger entry. Widening the accepted npm range follows the same path, and its trigger is external and checkable: widen when a Node release line ships that npm major by default, not when a particular machine happens to have it installed.
|
|
186
|
+
|
|
187
|
+
## Changing the recipe
|
|
188
|
+
|
|
189
|
+
The line is consumer comprehension, not semantic significance. A NEW KIND is anything an installed consumer would have to newly understand in order to execute the document correctly: a different command or package manager, a changed network policy shape, a new KIND of output check, or a new required field. An older consumer must fail closed on it, and a consumer release comes first. A REVISION re-parameterises fields the consumer already understands: accepted version bounds, timeout values, the enumerated input set, the expected output inventory. An older consumer executes a revision correctly, with different numbers. Both owe a release-ledger entry; only a new kind gates on a consumer release. The test for which one applies is answerable in a fixture — does a consumer built against this schema parse and execute the document? — rather than by judgement about how big the change feels.
|
|
190
|
+
|
|
191
|
+
The recipe and its schema are both HASHED supported-surface entries, so changing either requires `surface_version` to advance in the same change. The single authority for how a checkout of this repository at one commit becomes a usable installed runtime. No checker, schema, document, or test may carry its own copy of a command, a version bound, a timeout, an input path, or an output rule stated here — every one of them is read from this file. It is data, never code: a consumer executes exactly the argv enumerated in steps[] and never a command assembled from repository data. Registered as a HASHED supported-surface entry rather than a named one, deliberately departing from the policy contracts introduced alongside it: a reason-code vocabulary grows additively and can safely live behind a named entry, but any change to the commands, network policy, accepted tool versions, inputs, or output verification here is an agent-relevant release event by the recipe's own rule. Only a hashed entry makes such a change require surface_version to advance in the same change (scripts/check-supported-surface.mjs --base). A recipe whose commands can change without a version bump is not a contract. Changing this file also owes a release-ledger entry.
|
|
192
|
+
|
|
193
|
+
## Refusals
|
|
194
|
+
|
|
195
|
+
These documents are refused. Each is a single-mutation fixture under `contracts/fixtures/runtime-recipe/reject/`, so a refusal is always attributable to one change.
|
|
196
|
+
|
|
197
|
+
| Fixture | Why it is refused |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `reject/unknown-kind.json` | recipe_kind names a kind this schema version does not define. An installed consumer was not released knowing how to execute it, so it fails closed rather than guessing that a v2 is a v1 with extras. |
|
|
200
|
+
| `reject/unknown-revision.json` | recipe_revision leaves the major line the kind defines. A revision may only re-parameterise fields the consumer already understands; a different major is a shape change wearing a revision's clothes. |
|
|
201
|
+
| `reject/unknown-network-policy.json` | A safety-critical enum: the install step declares a network policy outside the defined set. There is no allow-all value, and an unrecognized one is refused rather than treated as permissive. |
|
|
202
|
+
| `reject/allowlist-without-hosts.json` | An allowlist with no hosts is not a bound, it is an empty declaration that reads like one. The schema requires a non-empty host list whenever the policy is an allowlist. |
|
|
203
|
+
| `reject/unknown-output-check.json` | A safety-critical enum: an output check names a kind the consumer cannot perform. A new KIND of check is a new recipe kind, because an installed consumer cannot perform a check it was not released knowing. |
|
|
204
|
+
| `reject/unknown-step-id.json` | A safety-critical enum: a step identity outside the defined set. Steps are identified rather than positional, so an unrecognized id is a command the consumer has no contract for. |
|
|
205
|
+
| `reject/lifecycle-scripts-enabled.json` | A safety-critical enum: a step that permits lifecycle scripts. Enabling them would let the target run arbitrary code during preparation, which is the single thing the recipe's flags exist to prevent. |
|
|
206
|
+
| `reject/engines-disagreement-warns.json` | A safety-critical enum: disagreement between the contract's tool range and the target's declared engines resolved as a warning. An accept-with-warning path produces a build made under conditions nobody approved. |
|
|
207
|
+
| `reject/advisory-enforcement.json` | fail_closed switched off. Advisory bounds record the right numbers and enforce nothing, so the first time a bound matters you discover it was decorative. |
|
|
208
|
+
| `reject/unperformable-check-skipped.json` | A safety-critical enum: a check that cannot be performed treated as skipped. A skipped check reports success it never established. |
|
|
209
|
+
| `reject/committed-output-claim.json` | The recipe claims its output directory is a committed artifact. It is not: the directory is git-ignored and untracked, so no baseline for its contents can exist here and verification must be self-consistency. |
|
|
210
|
+
| `reject/unpinned-lockfile.json` | The recipe claims its lockfile is not integrity-pinned. The network allowlist bounds where bytes may come from and the lockfile's digests bound which bytes are acceptable; dropping the second leaves the first standing alone, which it was never meant to do. |
|
|
211
|
+
| `reject/missing-required-field.json` | The enumerated input set is absent. Without it there is nothing to fingerprint, and staleness — the one failure mode no output-side check can see — becomes undetectable. |
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Supported surface
|
|
2
|
+
|
|
3
|
+
This repo stopped being an implementation the day other tooling started building
|
|
4
|
+
on it. Campaigns Agent pins schemas, contract docs, and a CLI argv surface;
|
|
5
|
+
the private ops repo vendors the runtime schemas behind a byte-parity gate;
|
|
6
|
+
page-kit campaign repos consume the artifacts the CLI emits. This document — and
|
|
7
|
+
its machine twin, [`contracts/supported-surface.json`](../contracts/supported-surface.json),
|
|
8
|
+
enforced by `scripts/check-supported-surface.mjs` in `npm run check` and CI —
|
|
9
|
+
names exactly what those consumers may depend on. If it is not listed, it is
|
|
10
|
+
implementation detail, however stable it looks.
|
|
11
|
+
|
|
12
|
+
## What is supported
|
|
13
|
+
|
|
14
|
+
| Surface | Contract | Change discipline |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `schemas/*.schema.json` (all of them) | The portable contract catalog: CampaignSpec, Design Source Package, Build Packet, Build Context, Assembly Report, Doctor Output, sidecar-bundle conformance, Run Record, Workflow Finding, Build Brief, Source-HTML Manifest, Tooling Orientation, Release Ledger, QA Verdict, the QA Verdict sidecar projection, Runtime Recipe, and the legacy-migration inventory/plan/receipt trio. | Hashed. Any content change requires updating the recorded hash **and** bumping `surface_version` in the same PR. A shape change that alters meaning gets a new schema-version const — one version identifier must never cover two shapes (the 2026-08 assembly-report drift is the incident this rule encodes). Additions to an open `v0` schema are expected and consumers must tolerate unknown fields; the security-sensitive legacy-migration schemas are closed, so additions there require a new lineage. 1.28.0 (RL entry `surface_version: 1.28.0`, breaking) removed the two required Build Packet booleans `qa.test_orders_allowed` and `qa.sandbox_test_card_confirmed` (nothing read them; test orders run from `--test-order <mode>` alone), added `local-serve` to the `deploy.target` enum, and added the optional `remit_result` / `remit_base_kind` fields to the Run Record. 1.30.0 (additive) added the optional `data_layer` record to the QA Verdict's `test_orders[]` entries — the order's `dl_purchase` reading (#325). 1.33.0 (additive) added the optional `qa_verdict_publish` block to the Run Record — which verdict was posted to the QA portal, by `qa run` or `qa publish`, and what the portal answered, in the `remit_result` vocabulary (#328). |
|
|
17
|
+
| CLI commands: `start`, `prepare-build`, `build`, `polish`, `checkpoint`, `page-kit`, `spec`, `doctor`, `bundle`, `next`, `theme`, `tooling`, `install-skills`, `install-agent-context`, `validate-assembly-report`, `telemetry`, `standardize`, `qa`, `findings`, `run-record`, `run` | Scriptable entry points. The founding list (1.0.0) was the argv surface Campaigns Agent's remit fixture pins; 1.25.0 adds the partner entry path the public guides instruct — `install-skills` (skill install), `tooling status` (preflight), `next --packet` (the runtime cursor) — plus `theme`, `install-agent-context`, `validate-assembly-report`, and `telemetry`, since consent is part of the partner contract. `validate-build-packet`, formerly an undocumented and unsupported alias of `doctor`, was removed in 1.25.0+agent.7 (RL-0036) and now gets the unknown-command error; use `doctor`. `standardization-report`, a supported but redundant second spelling of `standardize` — same flags, same output, same exit codes — was removed in 1.27.0 and now gets the same unknown-command error; use `standardize`. Removing a supported command is a breaking change and is why that release moved the minor. `bundle check` validates the canonical JSON readback set and makes QA required only under `--require-qa`. `polish capture` owns page-load evidence production. `checkpoint waive` currently accepts four registered gates: `page_kit.store_profile`, `page_kit.sdk_version`, `polish.hidden_eager_media`, and `built_output.upsell_selector_scope`. `page-kit sync` (added in 1.29.0) writes the CampaignSpec's Store Profile fields and SDK pin into the target's `_data/campaigns.json` entry — the repair the first two gates name — and is the only `page-kit` subcommand. `spec derive` (added in 1.31.0) is the reverse write for the repo-derived field class (#432): the target's SDK pin, page routes and analytics ids into the packet's local CampaignSpec; it is the only `spec` subcommand, and the `page_kit.sdk_version.repo_newer` advisory names it; its `--write-map` flag (1.33.0+agent.2) also records the derived pin in the saved Map's Build hints field through the proxy Worker, never moving a Map pin backwards. `qa publish` (added in 1.33.0) posts an already-stored verdict to the QA portal without a re-run or an order, refusing a stale `spec_hash` or a verdict its Run Record already records as published (#328). Within Polish, only the broader Source Freshness waiver remains on its existing report lane; theme and QA decisions also retain their existing lanes. | Any change to this list — adding, renaming, or removing a command — bumps `surface_version` in the same PR, and since 1.25.0 `check-supported-surface.mjs --base` enforces that (before, only hashed and named entries owed a bump, so `checkpoint` landed unbumped). Subcommands, registered gates, and flags may grow freely beneath a listed command. Removing a listed command from dispatch fails the gate outright. Do not infer support for an unregistered checkpoint from the top-level command. |
|
|
18
|
+
| `bin/campaigns-os.mjs` (`campaigns-os`) | The CLI entry itself. | Declared in `package.json` `bin`; the gate fails if it disappears. |
|
|
19
|
+
| Package export `./campaign-spec` | The versioned campaign-spec rule registry, consumed as `@nextcommerce/campaigns-os` (pinned by consumers' lockfiles; lockstep policy — ADR-003 in the ops repo). | Behavior-guarded from the consumer side by their contract tests; the export path itself is gated here. |
|
|
20
|
+
| Package exports `./commercial-journey` and `./commercial-parity` | Portable scenario planning, response normalization, contract-governed source extraction, Exact-only parity comparison, and deterministic QA assertion serialization. These modules own no network transport and do not calculate prices locally. | Consumers execute descriptors through a supported calculate transport, then pass captured envelopes into the pure normalizer. Existing export paths are gated and may not be renamed or removed without a breaking surface change. |
|
|
21
|
+
| Package export `./legacy-migration` and its three schema exports | Portable SDK 0.3.x migration inventory, preview-plan, receipt, Offer request/readback, and token-free evidence helpers. Guide: [`docs/legacy-migration.md`](legacy-migration.md). | Pure contract only: no authenticated transport, write executor, audit store, receipt store, sessions, deletes, or rollback. Consumers own execution and must retain the guarded apply protocol. |
|
|
22
|
+
| Package export `./text-safety` | `singleLineField`, `singleLineFragment` and `singleLineDetail`: flatten a value this toolkit did not author to one line with no control characters before it is rendered into a single-line notice. `singleLineField` is for a value printed as its own field (a run id, a target path): every control character becomes U+FFFD and nothing else changes. `singleLineFragment` is for a value folded into a sentence (a gate's repair command or instruction): line breaks and tabs become spaces, runs of whitespace collapse, the ends are trimmed, and every other control character becomes U+FFFD. `singleLineDetail` is `singleLineFragment` plus Markdown escaping, a length cap and a placeholder for an empty value (a quoted loader message). | Pure string functions, no imports, no I/O. Added at surface 1.27.0; `singleLineFragment` added at 1.27.0+agent.2. The escape set may widen; a value that was already safe stays unchanged. |
|
|
23
|
+
| Contract docs: `CONTEXT.md`, `docs/campaigns-os-build-flow.md`, `docs/build-packet.md`, `docs/migration-sidecar-bundle.md`, `docs/design-source-package.md`, `docs/campaign-build-brief.md`, `docs/campaign-standardization-report.md`, `docs/brand-theme-bridge.md`, `docs/qa-and-test-orders.md`, `docs/legacy-migration.md`, `docs/versioning.md`, `docs/workflow-findings-sidecar.md`, this file | Named entry points consumers pin for context. | Content evolves freely; the path must keep existing. |
|
|
24
|
+
| `skills.json` + `skills/` + `skills.sh` | Versioned skill packages and their installer. | Governed by `check-skill-versions.mjs` (parity + bump gate + reserved external names). `skills.json` ships in the npm pack as of surface 1.0.0. |
|
|
25
|
+
| `compatibility.json` | The published compatibility statement. | Named; must keep existing. |
|
|
26
|
+
| Orientation contract: `AGENTS.md`, `CHANGELOG.md`, `contracts/release-ledger.json`, `contracts/agent-relevant-change-policy.v1.json`, `contracts/orientation-limits.v1.json`, `contracts/orientation-reason-codes.v1.json`, `docs/orientation-contract-reference.md`, `docs/release-ledger-authoring-guide.md`, `contracts/fixtures/orientation/canonicalization/v1.json` | The declarative data a consumer reads from Git objects to decide whether a commit is safe to work against, without executing anything from this repository. Schema `campaigns-os-tooling-orientation/v1`; ledger schema `campaigns-os-release-ledger/v1`. The canonicalization fixture gives independent implementations exact ledger and changelog bytes plus their expected SHA-256 digests. Entry point: [`AGENTS.md`](../AGENTS.md). | Named. The ledger is append-only: a correction ships as a new amendment entry, never an edit. `scripts/check-release-ledger.mjs` enforces the two-way gate; `docs/orientation-contract-reference.md` and the canonicalization fixture are generated together, and CI fails if either is stale. |
|
|
27
|
+
| Orientation fixtures: `contracts/fixtures/orientation/envelope/*.json`, `contracts/fixtures/orientation/hostile-target/**` (as named) | The bytes a consumer's parser validates against: one envelope per terminal outcome, plus a hostile target carrying Git hooks, an executable file, and npm lifecycle scripts for proving a reader executes nothing. The hostile target carries a second invariant for the runtime recipe: preparing it must run the recipe's own two steps and no lifecycle script reachable from them. | Named. Regenerate the envelopes with `npm run generate:orientation-docs`. Fixtures under `contracts/fixtures/` that are **not** named here — including the legacy-migration conformance corpus — are this repo's own test data and are not supported. |
|
|
28
|
+
| Runtime recipe: `contracts/runtime-recipe.campaigns-os-node-v1.json` | The declarative description of how a checkout at one commit becomes a usable installed runtime: exact commands, accepted tool ranges, per-step network policy, the enumerated input set, the mandatory output checks, and the enforced bounds. This repository publishes it; the consumer bootstrap executes it. Guide: [`docs/runtime-readiness.md`](runtime-readiness.md). | **Hashed**, deliberately unlike the orientation policy contracts beside it. Any change to commands, network policy, tool versions, inputs, or output verification is an agent-relevant release event, and only a hashed entry makes such a change require `surface_version` to advance in the same PR. A recipe whose commands can change without a version bump is not a contract. |
|
|
29
|
+
| Runtime-recipe fixtures: `contracts/fixtures/runtime-recipe/**` (as named), `docs/runtime-readiness.md` | Accept and single-mutation reject documents a consumer's parser validates against, the prepared-runtime states its output checks must distinguish, and the generated guide. | Named. All of it is generated — regenerate with `npm run generate:runtime-docs`; CI fails on a stale copy. |
|
|
30
|
+
| Migration sidecar bundle: `contracts/migration-sidecar-bundle.v0.json`, `docs/migration-sidecar-bundle.md`, and `contracts/fixtures/sidecar-bundle/production-shaped/**` (as named) | The strict machine contract and production-shaped consumer fixture for the root Build Packet plus Build Context, Assembly Report, Doctor Output, and QA Verdict JSON sidecars. Packet selection uses `generated_at`, never mtime; raw spec integrity is distinct from canonical material identity; safe repository-relative spellings normalize without accepting traversal; markdown may coexist but is never readback truth. | The machine contract and schemas are hashed. The fixture is named byte-for-byte consumer input and must keep passing `campaigns-os bundle check --require-qa`. A required QA verdict with `disposition: blocked` keeps handoff nonconformant and sets `stage_blocked`. A doctor sidecar recording a blocked run emits `bundle.doctor_output.blocked`, and a blocked QA verdict `bundle.qa_verdict.blocked`, as warnings by default and errors under `--require-qa`; `stage_blocked` is unchanged; `status: conformant` never asserts that doctor or QA passed. |
|
|
31
|
+
|
|
32
|
+
The Run Record lifecycle block keeps two duration meanings explicit:
|
|
33
|
+
`duration_ms` is summed active command time, while `wall_clock_duration_ms` is
|
|
34
|
+
the elapsed span between the first command start and last completion. Run
|
|
35
|
+
sessions bind to an explicit packet across working directories, retain blocked
|
|
36
|
+
QA attempts for repair, and close only on a ready verdict or explicit `run end`.
|
|
37
|
+
Doctor and QA producers update only their matching Assembly Report stage with
|
|
38
|
+
their current output paths and timestamps; they do not synthesize historical
|
|
39
|
+
stage completion.
|
|
40
|
+
|
|
41
|
+
Everything on this list must also **ship in the npm tarball** — the gate checks
|
|
42
|
+
`package.json` `files[]` coverage, so "supported" can never mean "absent from
|
|
43
|
+
the package a consumer installs."
|
|
44
|
+
|
|
45
|
+
## What is NOT supported
|
|
46
|
+
|
|
47
|
+
- `src/**` except the files reached through the explicit
|
|
48
|
+
`./commercial-journey`, `./commercial-parity`, and `./legacy-migration` package exports — including
|
|
49
|
+
files downstream context spines currently read
|
|
50
|
+
(`src/cli.mjs`, `src/qa-*.mjs`, `src/doctor-check-registry.mjs`, …). Reading
|
|
51
|
+
them for context is fine; importing or pinning behavior from them is not.
|
|
52
|
+
Doctor issue **codes** are contract-adjacent but currently governed by the
|
|
53
|
+
ops-repo ADR-003 parity baseline, not this manifest.
|
|
54
|
+
- `scripts/**` — repo checkers, including this gate's own implementation.
|
|
55
|
+
- `examples/**`, `prompts/**`, `agents/**` — illustrative, regenerated at will.
|
|
56
|
+
- `contracts/**` other than `supported-surface.json`,
|
|
57
|
+
`reserved-skill-names.json`, and the orientation contract/fixture entries
|
|
58
|
+
named in the manifest — internal build/QA contract data. In particular,
|
|
59
|
+
`contracts/fixtures/orientation/release-gate/cases.json` is this repo's own
|
|
60
|
+
release-gate test matrix, not a consumer contract.
|
|
61
|
+
- CLI output text, log lines, and human-facing handoff strings. Machine-readable
|
|
62
|
+
artifact fields are governed by their schemas, not by prose.
|
|
63
|
+
|
|
64
|
+
## Changing the surface
|
|
65
|
+
|
|
66
|
+
1. Make the change and update `contracts/supported-surface.json` (hash and/or
|
|
67
|
+
entries) in the same PR.
|
|
68
|
+
2. Bump `surface_version` when any hashed file changed (the `--base` gate in CI
|
|
69
|
+
enforces this; parity runs in every `npm run check`). Also bump it when the
|
|
70
|
+
manifest adds a `cli_commands`, `package_exports`, or `bin` entry: those are
|
|
71
|
+
additive public-surface expansions even though the gate cannot yet derive
|
|
72
|
+
the owed bump automatically.
|
|
73
|
+
3. Add a release-ledger entry. Every agent-relevant change — hashed or named
|
|
74
|
+
path, CLI command/subcommand/flag, skill, schema, package export,
|
|
75
|
+
compatibility policy, agent-facing documentation, workflow, or generated
|
|
76
|
+
runtime — owes exactly one entry in `contracts/release-ledger.json`, whether
|
|
77
|
+
or not `surface_version` moved. `scripts/check-release-ledger.mjs --base`
|
|
78
|
+
enforces this in both directions. See
|
|
79
|
+
[the authoring guide](release-ledger-authoring-guide.md).
|
|
80
|
+
4. Breaking a consumer-visible shape? New schema-version const, and say so in
|
|
81
|
+
the PR body — downstream pins (Campaigns Agent context spine, ops-repo
|
|
82
|
+
`public-contracts.manifest.json`) update on their own cadence against a
|
|
83
|
+
version they can see move.
|