@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,107 @@
|
|
|
1
|
+
// Agent deviation telemetry: makes "the agent ignored Campaigns OS and
|
|
2
|
+
// wandered" measurable instead of anecdotal.
|
|
3
|
+
//
|
|
4
|
+
// `campaigns-os next` records its recommendation (stage + the commands it
|
|
5
|
+
// expects next) on the active run session. When a later pipeline-advancing
|
|
6
|
+
// command does not match that recommendation, a deviation entry is appended to
|
|
7
|
+
// a sidecar journal. Deviations are TELEMETRY, not blocks — hard gates live in
|
|
8
|
+
// `next`/doctor/qa. An agent can declare intent with --deviation-reason so an
|
|
9
|
+
// intentional detour is distinguishable from drift.
|
|
10
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
11
|
+
import { dirname, resolve } from "node:path";
|
|
12
|
+
|
|
13
|
+
export const DEVIATION_SCHEMA = "campaigns-os-agent-deviation/v0";
|
|
14
|
+
export const DEVIATION_JOURNAL_REL_PATH = ".campaign-runtime/agent-deviations.jsonl";
|
|
15
|
+
|
|
16
|
+
// Commands that advance the pipeline. Read-only / bookkeeping commands
|
|
17
|
+
// (doctor, next, findings, telemetry, run, validate-*) never deviate.
|
|
18
|
+
export const TRACKED_STAGE_COMMANDS = Object.freeze(new Set(["start", "prepare-build", "theme", "polish", "qa", "run-record"]));
|
|
19
|
+
|
|
20
|
+
// Commands every recommendation implicitly allows for its stage. Stage work is
|
|
21
|
+
// agent/skill work, so the expected command set is small and explicit.
|
|
22
|
+
const EXPECTED_COMMANDS_BY_STAGE = Object.freeze({
|
|
23
|
+
"prepare-build": ["prepare-build", "start"],
|
|
24
|
+
setup: ["theme"],
|
|
25
|
+
build: ["theme"],
|
|
26
|
+
polish: ["polish", "theme"],
|
|
27
|
+
deploy: [],
|
|
28
|
+
qa: ["qa", "theme"],
|
|
29
|
+
done: ["run-record"],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// The command word of a produced command line, whichever install prefix it
|
|
33
|
+
// was spelled with (bare `campaigns-os`, `npx campaigns-os`, `npm run
|
|
34
|
+
// campaigns-os --`, or `npx --yes <git-spec>` from an npx cache).
|
|
35
|
+
export function commandWord(command) {
|
|
36
|
+
if (typeof command !== "string") return null;
|
|
37
|
+
const stripped = command
|
|
38
|
+
.replace(/^npx\s+--yes\s+\S+\s+/, "campaigns-os ")
|
|
39
|
+
.replace(/^npx\s+campaigns-os\s+/, "campaigns-os ")
|
|
40
|
+
.replace(/^npm\s+run\s+campaigns-os\s+--\s+/, "campaigns-os ");
|
|
41
|
+
return stripped.match(/^campaigns-os\s+([a-z-]+)/)?.[1] || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function expectedCommandsForStage(stage, requiredActions = []) {
|
|
45
|
+
const base = EXPECTED_COMMANDS_BY_STAGE[stage] || [];
|
|
46
|
+
// Gate required_actions name exact commands ("campaigns-os theme generate
|
|
47
|
+
// ..."); their command words are expected too.
|
|
48
|
+
const fromActions = requiredActions
|
|
49
|
+
.map((action) => commandWord(action?.command))
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
return [...new Set([...base, ...fromActions])];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildRecommendation({ stage, status, expectedCommands, now = new Date() }) {
|
|
55
|
+
return {
|
|
56
|
+
stage,
|
|
57
|
+
status,
|
|
58
|
+
expected_commands: expectedCommands,
|
|
59
|
+
issued_at: now.toISOString(),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Compare a pipeline-advancing command against the session's last
|
|
65
|
+
* recommendation. Returns a deviation entry or null.
|
|
66
|
+
*/
|
|
67
|
+
export function detectDeviation({ lastRecommendation, command, argvShape = [], runId = null, deviationReason = null, now = new Date() }) {
|
|
68
|
+
if (!TRACKED_STAGE_COMMANDS.has(command)) return null;
|
|
69
|
+
if (!lastRecommendation || !Array.isArray(lastRecommendation.expected_commands)) return null;
|
|
70
|
+
if (lastRecommendation.expected_commands.includes(command)) return null;
|
|
71
|
+
return {
|
|
72
|
+
schema_version: DEVIATION_SCHEMA,
|
|
73
|
+
observed_at: now.toISOString(),
|
|
74
|
+
run_id: runId,
|
|
75
|
+
recommended_stage: lastRecommendation.stage || null,
|
|
76
|
+
recommended_status: lastRecommendation.status || null,
|
|
77
|
+
recommended_commands: lastRecommendation.expected_commands,
|
|
78
|
+
recommendation_issued_at: lastRecommendation.issued_at || null,
|
|
79
|
+
actual_command: command,
|
|
80
|
+
actual_argv_shape: argvShape,
|
|
81
|
+
deviation_reason: typeof deviationReason === "string" && deviationReason.trim() ? deviationReason.trim() : null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function appendDeviation(journalPath, entry) {
|
|
86
|
+
const path = resolve(journalPath);
|
|
87
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
88
|
+
appendFileSync(path, `${JSON.stringify(entry)}\n`);
|
|
89
|
+
return entry;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Best-effort read; malformed lines are skipped, a missing journal is empty. */
|
|
93
|
+
export function readDeviations(journalPath) {
|
|
94
|
+
const path = resolve(journalPath);
|
|
95
|
+
if (!existsSync(path)) return [];
|
|
96
|
+
const entries = [];
|
|
97
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
98
|
+
if (!line.trim()) continue;
|
|
99
|
+
try {
|
|
100
|
+
const entry = JSON.parse(line);
|
|
101
|
+
if (entry && typeof entry === "object" && entry.schema_version === DEVIATION_SCHEMA) entries.push(entry);
|
|
102
|
+
} catch {
|
|
103
|
+
// tolerate a torn line; telemetry must never block reads
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return entries;
|
|
107
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function defineDoctorCheck({ id, phase = "doctor", run, when = null }, { registryId = "doctor", index = "unknown" } = {}) {
|
|
2
|
+
const location = `Doctor check registry "${registryId}" check at index ${index}`;
|
|
3
|
+
if (!isNonEmptyString(id)) throw new Error(`${location} needs a non-empty id.`);
|
|
4
|
+
const normalizedId = id.trim();
|
|
5
|
+
const checkLabel = `Doctor check registry "${registryId}" check "${normalizedId}"`;
|
|
6
|
+
if (!isNonEmptyString(phase)) throw new Error(`${checkLabel} needs a phase.`);
|
|
7
|
+
if (typeof run !== "function") throw new Error(`${checkLabel} needs a run function.`);
|
|
8
|
+
if (when != null && typeof when !== "function") throw new Error(`${checkLabel} has a non-function when predicate.`);
|
|
9
|
+
|
|
10
|
+
return Object.freeze({
|
|
11
|
+
id: normalizedId,
|
|
12
|
+
phase: phase.trim(),
|
|
13
|
+
run,
|
|
14
|
+
when,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createDoctorCheckRegistry(checks, { registryId = "doctor" } = {}) {
|
|
19
|
+
if (!Array.isArray(checks)) throw new Error(`Doctor check registry "${registryId}" must be an array.`);
|
|
20
|
+
const seen = new Map();
|
|
21
|
+
const normalized = checks.map((check, index) => {
|
|
22
|
+
if (!check || typeof check !== "object") {
|
|
23
|
+
throw new Error(`Doctor check registry "${registryId}" has a non-object check at index ${index}.`);
|
|
24
|
+
}
|
|
25
|
+
const doctorCheck = defineDoctorCheck(check, { registryId, index });
|
|
26
|
+
if (seen.has(doctorCheck.id)) {
|
|
27
|
+
throw new Error(`Doctor check registry "${registryId}" has duplicate check id "${doctorCheck.id}".`);
|
|
28
|
+
}
|
|
29
|
+
seen.set(doctorCheck.id, doctorCheck);
|
|
30
|
+
return doctorCheck;
|
|
31
|
+
});
|
|
32
|
+
return Object.freeze(normalized);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function runDoctorCheckRegistry(checks, context, { phase = null } = {}) {
|
|
36
|
+
const phaseFilter = isNonEmptyString(phase) ? phase.trim() : null;
|
|
37
|
+
const executed = [];
|
|
38
|
+
for (const check of checks) {
|
|
39
|
+
if (phaseFilter && check.phase !== phaseFilter) continue;
|
|
40
|
+
if (check.when && !check.when(context)) continue;
|
|
41
|
+
check.run(context);
|
|
42
|
+
executed.push(check.id);
|
|
43
|
+
}
|
|
44
|
+
return executed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isNonEmptyString(value) {
|
|
48
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
49
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
export const DOCTOR_SIDECAR_REL_PATH = ".campaign-runtime/doctor-output.json";
|
|
6
|
+
export const DOCTOR_SIDECAR_SCHEMA = "campaigns-os-doctor-output/v0";
|
|
7
|
+
|
|
8
|
+
export function doctorSidecarPath(targetBaseDir) {
|
|
9
|
+
return join(targetBaseDir, DOCTOR_SIDECAR_REL_PATH);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Atomic JSON write (tmp + rename) for the artifacts other commands may read
|
|
13
|
+
// concurrently: the assembly report, the doctor sidecar, a run session. A
|
|
14
|
+
// torn report would defeat the gate decision it records, and a torn sidecar
|
|
15
|
+
// would itself break the freshness contract the stale stamp implements.
|
|
16
|
+
export function writeJsonAtomic(path, value) {
|
|
17
|
+
const resolved = resolve(path);
|
|
18
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
19
|
+
const tmp = `${resolved}.${randomUUID()}.tmp`;
|
|
20
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
|
|
21
|
+
renameSync(tmp, resolved);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// #312: every retained sidecar names the command that persisted it, the way
|
|
25
|
+
// a stale stamp already names the command that made it (`stale_marked_by`).
|
|
26
|
+
// `generated_at` alone cannot tell a fresh `doctor --write` from a two-day-old
|
|
27
|
+
// gitignored copy carried along by `cp -R` — which is exactly what got a
|
|
28
|
+
// read-only command accused of writing this file. The name is the producer's
|
|
29
|
+
// own: each producer function states it where it is defined (`doctor`,
|
|
30
|
+
// `next`, `qa run`) or receives it from the dispatch that selected it (the
|
|
31
|
+
// intake body serving `start` / `build`); it is never re-read from argv at
|
|
32
|
+
// the write. A producer that forgets to say who it is fails loudly rather
|
|
33
|
+
// than writing an anonymous artifact, and a name that is not a plain command
|
|
34
|
+
// word (a control character, a stray newline) is refused here, at the seam,
|
|
35
|
+
// so a consumer that greps or re-prints the sidecar reads one producer per
|
|
36
|
+
// line. Placed beside `generated_at` so the two read together.
|
|
37
|
+
const PRODUCER_NAME = /^[a-z0-9][a-z0-9 _-]*$/;
|
|
38
|
+
|
|
39
|
+
export function stampDoctorProducer(doctor, command) {
|
|
40
|
+
if (typeof command !== "string" || !command.trim()) {
|
|
41
|
+
throw new TypeError("stampDoctorProducer requires command: the retained doctor sidecar names the command that produced it (generated_by).");
|
|
42
|
+
}
|
|
43
|
+
if (!PRODUCER_NAME.test(command.trim())) {
|
|
44
|
+
throw new TypeError(`stampDoctorProducer refuses producer name ${JSON.stringify(command)}: generated_by is a plain command word (${PRODUCER_NAME}).`);
|
|
45
|
+
}
|
|
46
|
+
if (!doctor || typeof doctor !== "object" || Array.isArray(doctor)) {
|
|
47
|
+
throw new TypeError("stampDoctorProducer requires a doctor result object.");
|
|
48
|
+
}
|
|
49
|
+
const { schema_version, generated_at, generated_by: _previous, ...rest } = doctor;
|
|
50
|
+
return {
|
|
51
|
+
...(schema_version !== undefined ? { schema_version } : {}),
|
|
52
|
+
...(generated_at !== undefined ? { generated_at } : {}),
|
|
53
|
+
generated_by: command.trim(),
|
|
54
|
+
...rest,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The one writer of the retained doctor sidecar: a wholesale, atomic rewrite
|
|
59
|
+
// of a doctorPacket result stamped with its producer. Every producer —
|
|
60
|
+
// `doctor --write`, `next`, `start`/`build`, the QA stage refresh — goes
|
|
61
|
+
// through this (or through `stampDoctorProducer` when its own transactional
|
|
62
|
+
// writer must do the rename), so `generated_by` cannot be skipped by one of
|
|
63
|
+
// them the way #327's cause labels once were.
|
|
64
|
+
export function writeDoctorSidecar(path, doctor, { command } = {}) {
|
|
65
|
+
writeJsonAtomic(path, stampDoctorProducer(doctor, command));
|
|
66
|
+
return path;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// #171 v1 freshness contract for the retained doctor sidecar: commands that
|
|
70
|
+
// mutate doctor inputs WITHOUT recomputing doctor state (theme waive/generate,
|
|
71
|
+
// qa policy set) stamp the retained snapshot stale instead of leaving a green
|
|
72
|
+
// lie on disk, while commands that DO recompute (doctor, prepare-build/start,
|
|
73
|
+
// next) rewrite the sidecar wholesale — which clears any stale stamp.
|
|
74
|
+
export function markDoctorSidecarStale(targetBaseDir, { command = null, reason = null } = {}) {
|
|
75
|
+
const path = doctorSidecarPath(targetBaseDir);
|
|
76
|
+
if (!existsSync(path)) return null;
|
|
77
|
+
let sidecar;
|
|
78
|
+
try {
|
|
79
|
+
sidecar = JSON.parse(readFileSync(path, "utf8"));
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (!sidecar || typeof sidecar !== "object" || Array.isArray(sidecar)) return null;
|
|
84
|
+
const stamped = {
|
|
85
|
+
...sidecar,
|
|
86
|
+
stale: true,
|
|
87
|
+
stale_marked_by: command,
|
|
88
|
+
stale_marked_at: new Date().toISOString(),
|
|
89
|
+
stale_reason: reason
|
|
90
|
+
|| "A later command changed doctor inputs after this snapshot was written. Re-run campaigns-os doctor (or campaigns-os next) for current state.",
|
|
91
|
+
};
|
|
92
|
+
writeJsonAtomic(path, stamped);
|
|
93
|
+
return path;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// The retained doctor sidecar records its own verdict twice: `ok` (boolean)
|
|
97
|
+
// and `status` ("ready", "ready_with_warnings", "ready_with_waivers",
|
|
98
|
+
// "blocked"). A bundle consumer must read that verdict rather than treat the
|
|
99
|
+
// sidecar's presence, schema validity, or freshness as readiness: a blocked
|
|
100
|
+
// doctor run is a perfectly well-formed artifact whose content says the
|
|
101
|
+
// campaign cannot proceed. Either signal blocks; a sidecar that is not an
|
|
102
|
+
// object reports nothing (its shape is the schema check's job, not this one's).
|
|
103
|
+
export function doctorSidecarBlocked(sidecar) {
|
|
104
|
+
if (!sidecar || typeof sidecar !== "object" || Array.isArray(sidecar)) return false;
|
|
105
|
+
return sidecar.status === "blocked" || sidecar.ok === false;
|
|
106
|
+
}
|