@aperant/framework 0.17.0 → 0.20.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/CHANGELOG.md +261 -0
- package/agents/apt-executor.md +6 -0
- package/agents/apt-planner.md +3 -2
- package/bin/features-reconcile-check.mjs +77 -0
- package/dist/cli/artifacts/self-stage.d.mts.map +1 -1
- package/dist/cli/artifacts/self-stage.mjs +2 -1
- package/dist/cli/artifacts/self-stage.mjs.map +1 -1
- package/dist/cli/cli-wrappers/features-reconcile.d.mts +2 -0
- package/dist/cli/cli-wrappers/features-reconcile.d.mts.map +1 -0
- package/dist/cli/cli-wrappers/features-reconcile.mjs +9 -0
- package/dist/cli/cli-wrappers/features-reconcile.mjs.map +1 -0
- package/dist/cli/commands/ci-watch.mjs +49 -2
- package/dist/cli/commands/ci-watch.mjs.map +1 -1
- package/dist/cli/commands/commit.mjs +3 -3
- package/dist/cli/commands/commit.mjs.map +1 -1
- package/dist/cli/commands/event.mjs +16 -16
- package/dist/cli/commands/event.mjs.map +1 -1
- package/dist/cli/commands/features-reconcile.d.mts +2 -0
- package/dist/cli/commands/features-reconcile.d.mts.map +1 -0
- package/dist/cli/commands/features-reconcile.mjs +143 -0
- package/dist/cli/commands/features-reconcile.mjs.map +1 -0
- package/dist/cli/commands/health-check.d.mts +2 -2
- package/dist/cli/commands/health-check.mjs +2 -2
- package/dist/cli/commands/merge-integrate.mjs +3 -3
- package/dist/cli/commands/merge-integrate.mjs.map +1 -1
- package/dist/cli/commands/produce.d.mts +9 -0
- package/dist/cli/commands/produce.d.mts.map +1 -0
- package/dist/cli/commands/produce.mjs +1345 -0
- package/dist/cli/commands/produce.mjs.map +1 -0
- package/dist/cli/commands/task.d.mts +16 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +434 -266
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -1
- package/dist/cli/coordination/event-schema.d.mts +5 -3
- package/dist/cli/coordination/event-schema.d.mts.map +1 -1
- package/dist/cli/coordination/event-schema.mjs +245 -21
- package/dist/cli/coordination/event-schema.mjs.map +1 -1
- package/dist/cli/coordination/store.d.mts +2 -2
- package/dist/cli/coordination/store.mjs +4 -4
- package/dist/cli/coordination/store.mjs.map +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +4 -2
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/features/area-shape.d.mts +11 -0
- package/dist/cli/features/area-shape.d.mts.map +1 -0
- package/dist/cli/features/area-shape.mjs +141 -0
- package/dist/cli/features/area-shape.mjs.map +1 -0
- package/dist/cli/features/delta.d.mts +78 -0
- package/dist/cli/features/delta.d.mts.map +1 -0
- package/dist/cli/features/delta.mjs +591 -0
- package/dist/cli/features/delta.mjs.map +1 -0
- package/dist/cli/features/reconcile.d.mts +236 -0
- package/dist/cli/features/reconcile.d.mts.map +1 -0
- package/dist/cli/features/reconcile.mjs +1126 -0
- package/dist/cli/features/reconcile.mjs.map +1 -0
- package/dist/cli/features/surface-map.d.mts +63 -0
- package/dist/cli/features/surface-map.d.mts.map +1 -0
- package/dist/cli/features/surface-map.mjs +191 -0
- package/dist/cli/features/surface-map.mjs.map +1 -0
- package/dist/cli/{commands/features-audit.d.mts → features/write-root.d.mts} +4 -3
- package/dist/cli/features/write-root.d.mts.map +1 -0
- package/dist/cli/features/write-root.mjs +137 -0
- package/dist/cli/features/write-root.mjs.map +1 -0
- package/dist/cli/gate/gates/features-reconciled.d.mts +36 -0
- package/dist/cli/gate/gates/features-reconciled.d.mts.map +1 -0
- package/dist/cli/gate/gates/features-reconciled.mjs +67 -0
- package/dist/cli/gate/gates/features-reconciled.mjs.map +1 -0
- package/dist/cli/gate/gates/index.mjs +1 -0
- package/dist/cli/gate/gates/index.mjs.map +1 -1
- package/dist/cli/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +31 -1
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/install/legacy-paths.d.mts.map +1 -1
- package/dist/cli/install/legacy-paths.mjs +2 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -1
- package/dist/cli/produce/blind-probe.d.mts +85 -0
- package/dist/cli/produce/blind-probe.d.mts.map +1 -0
- package/dist/cli/produce/blind-probe.mjs +217 -0
- package/dist/cli/produce/blind-probe.mjs.map +1 -0
- package/dist/cli/produce/claim.d.mts +188 -0
- package/dist/cli/produce/claim.d.mts.map +1 -0
- package/dist/cli/produce/claim.mjs +518 -0
- package/dist/cli/produce/claim.mjs.map +1 -0
- package/dist/cli/produce/done-gate.d.mts +87 -0
- package/dist/cli/produce/done-gate.d.mts.map +1 -0
- package/dist/cli/produce/done-gate.mjs +200 -0
- package/dist/cli/produce/done-gate.mjs.map +1 -0
- package/dist/cli/produce/events.d.mts +77 -0
- package/dist/cli/produce/events.d.mts.map +1 -0
- package/dist/cli/produce/events.mjs +126 -0
- package/dist/cli/produce/events.mjs.map +1 -0
- package/dist/cli/produce/evidence-oracle.d.mts +63 -0
- package/dist/cli/produce/evidence-oracle.d.mts.map +1 -0
- package/dist/cli/produce/evidence-oracle.mjs +122 -0
- package/dist/cli/produce/evidence-oracle.mjs.map +1 -0
- package/dist/cli/produce/ledger.d.mts +140 -0
- package/dist/cli/produce/ledger.d.mts.map +1 -0
- package/dist/cli/produce/ledger.mjs +272 -0
- package/dist/cli/produce/ledger.mjs.map +1 -0
- package/dist/cli/produce/probe-family.d.mts +53 -0
- package/dist/cli/produce/probe-family.d.mts.map +1 -0
- package/dist/cli/produce/probe-family.mjs +160 -0
- package/dist/cli/produce/probe-family.mjs.map +1 -0
- package/dist/cli/produce/projection.d.mts +55 -0
- package/dist/cli/produce/projection.d.mts.map +1 -0
- package/dist/cli/produce/projection.mjs +97 -0
- package/dist/cli/produce/projection.mjs.map +1 -0
- package/dist/cli/produce/run-id.d.mts +42 -0
- package/dist/cli/produce/run-id.d.mts.map +1 -0
- package/dist/cli/produce/run-id.mjs +79 -0
- package/dist/cli/produce/run-id.mjs.map +1 -0
- package/dist/cli/produce/saga.d.mts +180 -0
- package/dist/cli/produce/saga.d.mts.map +1 -0
- package/dist/cli/produce/saga.mjs +290 -0
- package/dist/cli/produce/saga.mjs.map +1 -0
- package/dist/cli/produce/scheduler.d.mts +165 -0
- package/dist/cli/produce/scheduler.d.mts.map +1 -0
- package/dist/cli/produce/scheduler.mjs +399 -0
- package/dist/cli/produce/scheduler.mjs.map +1 -0
- package/dist/cli/produce/setpoint.d.mts +52 -0
- package/dist/cli/produce/setpoint.d.mts.map +1 -0
- package/dist/cli/produce/setpoint.mjs +113 -0
- package/dist/cli/produce/setpoint.mjs.map +1 -0
- package/dist/cli/produce/verification-ttl.d.mts +75 -0
- package/dist/cli/produce/verification-ttl.d.mts.map +1 -0
- package/dist/cli/produce/verification-ttl.mjs +169 -0
- package/dist/cli/produce/verification-ttl.mjs.map +1 -0
- package/dist/cli/release-notes/ship-autodraft.mjs +2 -2
- package/dist/cli/roadmap/{conductor-view.d.mts → showrunner-view.d.mts} +3 -3
- package/dist/cli/roadmap/showrunner-view.d.mts.map +1 -0
- package/dist/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +7 -7
- package/dist/cli/roadmap/showrunner-view.mjs.map +1 -0
- package/dist/cli/variant/gallery.d.mts.map +1 -1
- package/dist/cli/variant/gallery.mjs +7 -3
- package/dist/cli/variant/gallery.mjs.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +2 -1
- package/dist/plugin/agents/apt-executor.md +6 -0
- package/dist/plugin/agents/apt-planner.md +3 -2
- package/dist/plugin/skills/apt/SKILL.md +112 -38
- package/dist/plugin/skills/apt-close-task/SKILL.md +2 -2
- package/dist/plugin/skills/apt-debug/SKILL.md +22 -24
- package/dist/plugin/skills/apt-execute/SKILL.md +35 -26
- package/dist/plugin/skills/apt-fan-out/SKILL.md +4 -4
- package/dist/plugin/skills/apt-handoff/SKILL.md +1 -1
- package/dist/plugin/skills/apt-mockup/SKILL.md +1 -1
- package/dist/plugin/skills/apt-plan/SKILL.md +5 -5
- package/dist/plugin/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
- package/dist/plugin/skills/apt-produce/SKILL.md +606 -0
- package/dist/plugin/skills/apt-quick/SKILL.md +27 -33
- package/dist/plugin/skills/apt-review/SKILL.md +14 -11
- package/dist/plugin/skills/apt-run/SKILL.md +132 -3
- package/dist/plugin/skills/apt-ship/SKILL.md +29 -5
- package/dist/plugin/skills/apt-spar/SKILL.md +5 -3
- package/dist/plugin/skills/apt-watch-ci/SKILL.md +6 -4
- package/dist/schemas/feature-registry.d.ts.map +1 -1
- package/dist/schemas/feature-registry.js +34 -3
- package/dist/schemas/feature-registry.js.map +1 -1
- package/dist/types/config.d.ts +8 -5
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +4 -4
- package/prompts/{conductor-framework-context.md → showrunner-framework-context.md} +1 -1
- package/prompts/{conductor-status-check.md → showrunner-status-check.md} +1 -1
- package/prompts/{conductor-sub-agent.md → showrunner-sub-agent.md} +6 -6
- package/prompts/{conductor-system.md → showrunner-system.md} +8 -8
- package/skills/apt/SKILL.md +112 -38
- package/skills/apt-close-task/SKILL.md +2 -2
- package/skills/apt-debug/SKILL.md +22 -24
- package/skills/apt-execute/SKILL.md +35 -26
- package/skills/apt-fan-out/SKILL.md +4 -4
- package/skills/apt-handoff/SKILL.md +1 -1
- package/skills/apt-mockup/SKILL.md +1 -1
- package/skills/apt-plan/SKILL.md +5 -5
- package/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
- package/skills/apt-produce/SKILL.md +606 -0
- package/skills/apt-quick/SKILL.md +27 -33
- package/skills/apt-review/SKILL.md +14 -11
- package/skills/apt-run/SKILL.md +132 -3
- package/skills/apt-ship/SKILL.md +29 -5
- package/skills/apt-spar/SKILL.md +5 -3
- package/skills/apt-watch-ci/SKILL.md +6 -4
- package/src/cli/artifacts/self-stage.mjs +2 -1
- package/src/cli/cli-wrappers/features-reconcile.mjs +9 -0
- package/src/cli/commands/ci-watch.mjs +51 -2
- package/src/cli/commands/commit.mjs +3 -3
- package/src/cli/commands/event.mjs +16 -16
- package/src/cli/commands/features-reconcile.mjs +157 -0
- package/src/cli/commands/health-check.mjs +2 -2
- package/src/cli/commands/merge-integrate.mjs +3 -3
- package/src/cli/commands/produce.mjs +1466 -0
- package/src/cli/commands/task.mjs +482 -285
- package/src/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
- package/src/cli/coordination/event-schema.d.ts +4 -2
- package/src/cli/coordination/event-schema.mjs +276 -21
- package/src/cli/coordination/store.mjs +4 -4
- package/src/cli/dispatch.mjs +4 -2
- package/src/cli/features/area-shape.mjs +140 -0
- package/src/cli/features/delta.mjs +625 -0
- package/src/cli/features/reconcile.mjs +1169 -0
- package/src/cli/features/surface-map.mjs +192 -0
- package/src/cli/features/write-root.mjs +140 -0
- package/src/cli/gate/gates/features-reconciled.mjs +70 -0
- package/src/cli/gate/gates/index.mjs +1 -0
- package/src/cli/help.mjs +31 -1
- package/src/cli/install/legacy-paths.mjs +2 -0
- package/src/cli/produce/blind-probe.mjs +245 -0
- package/src/cli/produce/claim.mjs +543 -0
- package/src/cli/produce/done-gate.mjs +238 -0
- package/src/cli/produce/events.mjs +131 -0
- package/src/cli/produce/evidence-oracle.mjs +133 -0
- package/src/cli/produce/ledger.mjs +284 -0
- package/src/cli/produce/probe-family.mjs +168 -0
- package/src/cli/produce/projection.mjs +105 -0
- package/src/cli/produce/run-id.mjs +84 -0
- package/src/cli/produce/saga.mjs +303 -0
- package/src/cli/produce/scheduler.mjs +423 -0
- package/src/cli/produce/setpoint.mjs +122 -0
- package/src/cli/produce/verification-ttl.mjs +191 -0
- package/src/cli/release-notes/ship-autodraft.mjs +2 -2
- package/src/cli/roadmap/showrunner-view.d.ts +10 -0
- package/src/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +6 -6
- package/src/cli/variant/gallery.mjs +7 -3
- package/templates/aperant-claude-md-appendix.md +2 -2
- package/workflows/scan-features.md +17 -11
- package/dist/cli/cli-wrappers/features-audit.d.mts +0 -2
- package/dist/cli/cli-wrappers/features-audit.d.mts.map +0 -1
- package/dist/cli/cli-wrappers/features-audit.mjs +0 -8
- package/dist/cli/cli-wrappers/features-audit.mjs.map +0 -1
- package/dist/cli/commands/features-audit.d.mts.map +0 -1
- package/dist/cli/commands/features-audit.mjs +0 -293
- package/dist/cli/commands/features-audit.mjs.map +0 -1
- package/dist/cli/features/registry-audit.d.mts +0 -56
- package/dist/cli/features/registry-audit.d.mts.map +0 -1
- package/dist/cli/features/registry-audit.mjs +0 -264
- package/dist/cli/features/registry-audit.mjs.map +0 -1
- package/dist/cli/roadmap/conductor-view.d.mts.map +0 -1
- package/dist/cli/roadmap/conductor-view.mjs.map +0 -1
- package/src/cli/cli-wrappers/features-audit.mjs +0 -8
- package/src/cli/commands/features-audit.mjs +0 -302
- package/src/cli/features/registry-audit.mjs +0 -254
- package/src/cli/roadmap/conductor-view.d.ts +0 -10
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* auto-emit-artifact-ready.mjs — code-enforced plan-ready signal.
|
|
4
4
|
*
|
|
5
5
|
* Promotes the `artifact.ready{kind:'plan'}` emit from a markdown contract
|
|
6
|
-
* in `skills/apt-plan/adapters/
|
|
7
|
-
* postcondition of `apt-tools commit`. This closes the
|
|
6
|
+
* in `skills/apt-plan/adapters/showrunner.md` to a framework-level
|
|
7
|
+
* postcondition of `apt-tools commit`. This closes the Showrunner v2 gap
|
|
8
8
|
* exposed during live verification 2026-05-15: the planner's QUICK /
|
|
9
9
|
* SIMPLE track is hard-exempt from "new interaction discipline"
|
|
10
10
|
* (Aperant Fast Path Guarantee ID-05), so the markdown emit step was
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* OpenCode, Codex) traverses via `apt-tools commit`
|
|
20
20
|
*
|
|
21
21
|
* Conditions for emit (ALL must hold — otherwise no-op):
|
|
22
|
-
* 1. `APERANT_TERMINAL_ID` is set in process.env (
|
|
22
|
+
* 1. `APERANT_TERMINAL_ID` is set in process.env (Showrunner PTY context).
|
|
23
23
|
* 2. The committed files include a path matching
|
|
24
24
|
* `.aperant/tasks/{task-id}/implementation_plan.json`.
|
|
25
25
|
*
|
|
@@ -32,7 +32,7 @@ import { createHash } from 'node:crypto'
|
|
|
32
32
|
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs'
|
|
33
33
|
import { hostname } from 'node:os'
|
|
34
34
|
import { join } from 'node:path'
|
|
35
|
-
import {
|
|
35
|
+
import { validateShowrunnerEvent } from './event-schema.mjs'
|
|
36
36
|
|
|
37
37
|
const PLAN_PATH_RE = /^(?:\.\/)?\.aperant\/tasks\/([^/]+)\/implementation_plan\.json$/
|
|
38
38
|
|
|
@@ -70,7 +70,7 @@ export function maybeAutoEmitPlanReady({ files, projectRoot }) {
|
|
|
70
70
|
terminal_id: terminalId,
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const validation =
|
|
73
|
+
const validation = validateShowrunnerEvent('artifact.ready', data)
|
|
74
74
|
if (!validation.ok) {
|
|
75
75
|
return { emitted: false, reason: `schema violation: ${validation.error}` }
|
|
76
76
|
}
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
export type ValidateResult = { ok: true } | { ok: false; error: string }
|
|
7
7
|
|
|
8
|
-
export function
|
|
8
|
+
export function validateShowrunnerEvent(
|
|
9
9
|
op: string,
|
|
10
10
|
data: Record<string, unknown> | undefined | null,
|
|
11
11
|
): ValidateResult
|
|
12
12
|
|
|
13
|
-
export const
|
|
13
|
+
export const SHOWRUNNER_VALIDATED_OPS: ReadonlySet<string>
|
|
14
|
+
|
|
15
|
+
export const PRODUCE_VALIDATED_OPS: ReadonlySet<string>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @internal
|
|
3
|
-
* event-schema.mjs — single source of truth for
|
|
3
|
+
* event-schema.mjs — single source of truth for showrunner event payload shapes.
|
|
4
4
|
*
|
|
5
5
|
* Why this lives in `framework`, not `core`:
|
|
6
6
|
* `packages/core` depends on `@aperant/framework`. The schema is shared by
|
|
7
7
|
* the CLI write path (`commands/event.mjs`), the framework lifecycle write
|
|
8
8
|
* seam (`coordination/store.mjs:appendEvent`), and the core IPC write seam
|
|
9
|
-
* (`packages/core/src/runtime/orchestration/
|
|
9
|
+
* (`packages/core/src/runtime/orchestration/showrunner/event-log.ts`).
|
|
10
10
|
* Putting it in the lower layer keeps a single rule table that every
|
|
11
11
|
* writer can import.
|
|
12
12
|
*
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
* - `awaiting-input` / `input-rescinded` MUST carry both terminal_id and
|
|
23
23
|
* request_id so the drawer can pair cards strictly.
|
|
24
24
|
* - `artifact.ready{kind:'plan'}` is the canonical plan-completion signal
|
|
25
|
-
* consumed by the
|
|
25
|
+
* consumed by the showrunner's reviewer queue (Step 5's reviewPlan tool).
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
const
|
|
28
|
+
/** Showrunner ops the schema enforces. Other ops pass through unchanged. */
|
|
29
|
+
const SHOWRUNNER_OPS = new Set([
|
|
30
30
|
'phase.changed',
|
|
31
31
|
'artifact.ready',
|
|
32
32
|
'awaiting-input',
|
|
@@ -35,34 +35,120 @@ const CONDUCTOR_OPS = new Set([
|
|
|
35
35
|
'budget.exceeded',
|
|
36
36
|
'terminal.enrolled',
|
|
37
37
|
'terminal.unenrolled',
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
38
|
+
'showrunner.intent',
|
|
39
|
+
'showrunner.cross_check',
|
|
40
|
+
'showrunner.realignment',
|
|
41
41
|
// c-4 (action-capable) — gate verdict + boot-replay audit trail
|
|
42
|
-
'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
42
|
+
'showrunner.action.executed',
|
|
43
|
+
'showrunner.action.gate_blocked',
|
|
44
|
+
'showrunner.boot_reconciled',
|
|
45
45
|
// AC21 — MCP-server consent decisions audited to .aperant/events/*.jsonl
|
|
46
46
|
'mcp-consent.approved',
|
|
47
47
|
'mcp-consent.denied',
|
|
48
48
|
'mcp-consent.tofu-reprompt',
|
|
49
49
|
])
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Produce outer-loop ops (M1) — the `/apt:produce` roadmap-mutation vocabulary.
|
|
53
|
+
* Kept as a PARALLEL set (not merged into SHOWRUNNER_OPS) so the two surfaces
|
|
54
|
+
* stay conceptually distinct; both are consulted by the same
|
|
55
|
+
* `validateShowrunnerEvent` entry point that `store.appendEvent` calls, so a
|
|
56
|
+
* malformed produce event is rejected fail-closed on the canonical event log
|
|
57
|
+
* (ID-04 / AC4). Every produce op carries a `run_id` tagging its run.
|
|
58
|
+
*/
|
|
59
|
+
const PRODUCE_OPS = new Set([
|
|
60
|
+
'produce.run.started',
|
|
61
|
+
'produce.setpoint.frozen',
|
|
62
|
+
'produce.roadmap.milestone_added',
|
|
63
|
+
'produce.roadmap.milestone_status_changed',
|
|
64
|
+
'produce.tick',
|
|
65
|
+
// M2 — the oracle + held-out cross-family blind-probe forensic trail. Every
|
|
66
|
+
// oracle/probe decision lands on the same append-only log, fail-closed
|
|
67
|
+
// against malformed entries (US-1 / AC1 / AC12). The probe ops are
|
|
68
|
+
// STRUCTURALLY blind: they carry only a hash/result/family, NEVER the probe
|
|
69
|
+
// content (ID-M2-03). The projection still folds `produce.roadmap.*` ONLY,
|
|
70
|
+
// so these never enter the roadmap the Showrunner re-reads each tick.
|
|
71
|
+
'produce.milestone.implemented',
|
|
72
|
+
'produce.probe.frozen',
|
|
73
|
+
'produce.probe.run',
|
|
74
|
+
'produce.oracle.judged',
|
|
75
|
+
'produce.oracle.verdict',
|
|
76
|
+
'produce.oracle.probe_gap',
|
|
77
|
+
// M3 — the Lyapunov acceptance-criteria ledger + scope-change forensic trail.
|
|
78
|
+
// Every criteria-open / criteria-close / scope-change decision lands on the
|
|
79
|
+
// same append-only log, fail-closed against malformed entries (US-4 / AC1). V
|
|
80
|
+
// (open acceptance criteria) = declared − closed, folded by ledger.mjs. The
|
|
81
|
+
// projection still folds `produce.roadmap.*` ONLY — criteria/scope ops are a
|
|
82
|
+
// SEPARATE fold, so the M1/M2 roadmap-blindness invariant is preserved.
|
|
83
|
+
'produce.criteria.declared',
|
|
84
|
+
'produce.criteria.closed',
|
|
85
|
+
'produce.scope.changed',
|
|
86
|
+
// M4 — the saga + verification-TTL forensic trail. Each plan→execute→verify→
|
|
87
|
+
// review→ship phase transition, every FORWARD compensation, every diverse
|
|
88
|
+
// review verdict, and every TTL re-verify / regression lands on the same
|
|
89
|
+
// append-only log, fail-closed against malformed entries (US-2 / AC1). The
|
|
90
|
+
// saga fold is SEPARATE from the roadmap projection (which still folds
|
|
91
|
+
// `produce.roadmap.*` ONLY), so the M1/M2/M3 blindness invariant holds. The
|
|
92
|
+
// saga-phase ops enforce a closed `phase` vocabulary (ALLOWED_SAGA_PHASES),
|
|
93
|
+
// mirroring the phase.changed.to / artifact.ready.kind allowed-value pattern.
|
|
94
|
+
'produce.saga.phase_entered',
|
|
95
|
+
'produce.saga.phase_completed',
|
|
96
|
+
'produce.saga.compensated',
|
|
97
|
+
'produce.saga.reviewed',
|
|
98
|
+
'produce.ttl.reverified',
|
|
99
|
+
'produce.ttl.regression_detected',
|
|
100
|
+
// M5 — the WSJF/WIP scheduler's audited decision. The wake `tick` emits ONE
|
|
101
|
+
// `produce.schedule.computed` alongside its `produce.tick` so the scheduler's
|
|
102
|
+
// pick is a recorded fact on the append-only log (the event-sourced ethos). It
|
|
103
|
+
// uses the `produce.schedule.*` prefix — the roadmap projection folds
|
|
104
|
+
// `produce.roadmap.*` ONLY, so this op NEVER enters the roadmap the Showrunner
|
|
105
|
+
// re-reads (the M1/M2 blindness invariant holds automatically — ID-M5-06).
|
|
106
|
+
'produce.schedule.computed',
|
|
107
|
+
// M6 — the Claim Envelope lifecycle forensic trail. A claim's CONTENT lives in
|
|
108
|
+
// the frozen `claims/<claim-id>.json` file, referenced by `content_hash` — it
|
|
109
|
+
// NEVER touches the log (ID-M6-07, mirrors produce.probe.frozen /
|
|
110
|
+
// produce.setpoint.frozen). `declared` seeds the claim ledger, `expired` drops
|
|
111
|
+
// it from the active set, `consumed` records a bounded down-projected delta
|
|
112
|
+
// (target_kind value-checked against the closed set). The claim ledger is a
|
|
113
|
+
// SEPARATE fold (projectClaims) — the roadmap projection still folds
|
|
114
|
+
// `produce.roadmap.*` ONLY, so these ops NEVER enter the roadmap the Showrunner
|
|
115
|
+
// re-reads (the M1/M2 blindness invariant holds automatically).
|
|
116
|
+
'produce.claim.declared',
|
|
117
|
+
'produce.claim.expired',
|
|
118
|
+
'produce.claim.consumed',
|
|
119
|
+
])
|
|
120
|
+
|
|
51
121
|
/** Allowed values for `phase.changed.to` — PTY busy/idle only. */
|
|
52
122
|
const ALLOWED_PHASE_VALUES = new Set(['busy', 'idle'])
|
|
53
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Allowed values for the saga `phase` on `produce.saga.phase_entered` /
|
|
126
|
+
* `produce.saga.phase_completed` / `produce.saga.compensated` (M4). The saga
|
|
127
|
+
* models plan→execute→verify→review→ship; an out-of-vocabulary phase is rejected
|
|
128
|
+
* fail-closed, mirroring the ALLOWED_PHASE_VALUES value check for phase.changed.
|
|
129
|
+
*/
|
|
130
|
+
const ALLOWED_SAGA_PHASES = new Set(['plan', 'execute', 'verify', 'review', 'ship'])
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Allowed values for the claim `target_kind` on `produce.claim.consumed` (M6).
|
|
134
|
+
* Consumption is a DELTA down-projection onto ONE acceptance-criterion / subtask /
|
|
135
|
+
* proof-obligation (ID-M6-06); an out-of-vocabulary kind is rejected fail-closed,
|
|
136
|
+
* mirroring the ALLOWED_SAGA_PHASES value check for the saga-phase ops.
|
|
137
|
+
*/
|
|
138
|
+
const ALLOWED_CLAIM_TARGET_KINDS = new Set(['acceptance_criterion', 'subtask', 'proof_obligation'])
|
|
139
|
+
|
|
54
140
|
/** Allowed values for `artifact.ready.kind`. */
|
|
55
141
|
const ALLOWED_ARTIFACT_KINDS = new Set(['plan', 'spec', 'build', 'review'])
|
|
56
142
|
|
|
57
143
|
/**
|
|
58
|
-
* Validate the data payload of a
|
|
144
|
+
* Validate the data payload of a showrunner event against the op's contract.
|
|
59
145
|
*
|
|
60
146
|
* @param {string} op
|
|
61
147
|
* @param {Record<string, unknown> | undefined | null} data
|
|
62
148
|
* @returns {{ ok: true } | { ok: false, error: string }}
|
|
63
149
|
*/
|
|
64
|
-
export function
|
|
65
|
-
if (!
|
|
150
|
+
export function validateShowrunnerEvent(op, data) {
|
|
151
|
+
if (!SHOWRUNNER_OPS.has(op) && !PRODUCE_OPS.has(op)) return { ok: true }
|
|
66
152
|
const d = data && typeof data === 'object' ? data : {}
|
|
67
153
|
|
|
68
154
|
switch (op) {
|
|
@@ -105,13 +191,13 @@ export function validateConductorEvent(op, data) {
|
|
|
105
191
|
return { ok: true }
|
|
106
192
|
}
|
|
107
193
|
|
|
108
|
-
case '
|
|
194
|
+
case 'showrunner.intent':
|
|
109
195
|
return requireFields(op, d, ['terminal_id', 'request_id', 'action', 'reasoning'])
|
|
110
196
|
|
|
111
|
-
case '
|
|
197
|
+
case 'showrunner.cross_check':
|
|
112
198
|
return requireFields(op, d, ['terminal_id', 'request_id'])
|
|
113
199
|
|
|
114
|
-
case '
|
|
200
|
+
case 'showrunner.realignment': {
|
|
115
201
|
const r = requireFields(op, d, ['terminal_id', 'request_id_orig'])
|
|
116
202
|
if (!r.ok) return r
|
|
117
203
|
if (d.attempt_n === undefined || d.attempt_n === null)
|
|
@@ -125,10 +211,10 @@ export function validateConductorEvent(op, data) {
|
|
|
125
211
|
case 'budget.exceeded':
|
|
126
212
|
return requireFields(op, d, ['terminal_id'])
|
|
127
213
|
|
|
128
|
-
case '
|
|
214
|
+
case 'showrunner.action.executed':
|
|
129
215
|
return requireFields(op, d, ['terminal_id', 'request_id', 'action_kind', 'idempotency_key'])
|
|
130
216
|
|
|
131
|
-
case '
|
|
217
|
+
case 'showrunner.action.gate_blocked':
|
|
132
218
|
return requireFields(op, d, [
|
|
133
219
|
'terminal_id',
|
|
134
220
|
'request_id',
|
|
@@ -152,7 +238,130 @@ export function validateConductorEvent(op, data) {
|
|
|
152
238
|
'session_id',
|
|
153
239
|
])
|
|
154
240
|
|
|
155
|
-
|
|
241
|
+
// Produce outer-loop (M1) — roadmap-mutation + tick vocabulary. Every
|
|
242
|
+
// op requires its `run_id`; roadmap ops additionally carry the
|
|
243
|
+
// milestone identity / status the projection folds (ID-04 / AC4).
|
|
244
|
+
case 'produce.run.started':
|
|
245
|
+
return requireFields(op, d, ['run_id', 'setpoint_hash'])
|
|
246
|
+
|
|
247
|
+
case 'produce.setpoint.frozen':
|
|
248
|
+
return requireFields(op, d, ['run_id', 'setpoint_hash', 'setpoint_path'])
|
|
249
|
+
|
|
250
|
+
case 'produce.roadmap.milestone_added':
|
|
251
|
+
return requireFields(op, d, ['run_id', 'milestone_id', 'title'])
|
|
252
|
+
|
|
253
|
+
case 'produce.roadmap.milestone_status_changed':
|
|
254
|
+
return requireFields(op, d, ['run_id', 'milestone_id', 'status'])
|
|
255
|
+
|
|
256
|
+
case 'produce.tick':
|
|
257
|
+
return requireFields(op, d, ['run_id'])
|
|
258
|
+
|
|
259
|
+
// Produce M2 — oracle + held-out cross-family blind-probe vocabulary.
|
|
260
|
+
// The implementer family is the recorded disjointness anchor (ID-M2-02);
|
|
261
|
+
// the probe ops are content-blind (only hash/result/family — ID-M2-03).
|
|
262
|
+
|
|
263
|
+
// The Showrunner records who implemented a milestone once per milestone;
|
|
264
|
+
// the cross-family rule is enforced against THIS recorded fact.
|
|
265
|
+
case 'produce.milestone.implemented':
|
|
266
|
+
return requireFields(op, d, ['run_id', 'milestone_id', 'implementer_family'])
|
|
267
|
+
|
|
268
|
+
// The frozen blind-probe SET is announced by hash + author family ONLY —
|
|
269
|
+
// NEVER the probe content (structural probe-blindness, AC5).
|
|
270
|
+
case 'produce.probe.frozen':
|
|
271
|
+
return requireFields(op, d, ['run_id', 'milestone_id', 'probe_set_hash', 'author_family'])
|
|
272
|
+
|
|
273
|
+
// A blind-probe RUN carries only its pass/fail result, the running family,
|
|
274
|
+
// and the HEAD sha it ran against — no probe content.
|
|
275
|
+
case 'produce.probe.run':
|
|
276
|
+
return requireFields(op, d, ['run_id', 'milestone_id', 'result', 'probe_family', 'head_sha'])
|
|
277
|
+
|
|
278
|
+
// The independent evidence-quality oracle's verdict over verify-proof's
|
|
279
|
+
// captured evidence, tagged with the (cross-family) oracle family.
|
|
280
|
+
case 'produce.oracle.judged':
|
|
281
|
+
return requireFields(op, d, ['run_id', 'milestone_id', 'verdict', 'oracle_family'])
|
|
282
|
+
|
|
283
|
+
// The two-gate done verdict (visible AND blind agree) — the ONLY path to
|
|
284
|
+
// `done` (ID-03: the Showrunner never self-attests).
|
|
285
|
+
case 'produce.oracle.verdict':
|
|
286
|
+
return requireFields(op, d, ['run_id', 'milestone_id', 'verdict'])
|
|
287
|
+
|
|
288
|
+
// The reward-hacking canary near-miss: visible gate passed, blind gate
|
|
289
|
+
// failed → the milestone stays open (ID-M2-06 / AC9).
|
|
290
|
+
case 'produce.oracle.probe_gap':
|
|
291
|
+
return requireFields(op, d, ['run_id', 'milestone_id'])
|
|
292
|
+
|
|
293
|
+
// Produce M3 — the Lyapunov acceptance-criteria ledger (V = declared −
|
|
294
|
+
// closed) + the scope-change escape hatch (ID-M3-01 / ID-M3-04 / AC1). A
|
|
295
|
+
// criterion binds to a milestone; it opens on `declared` (V += 1), closes
|
|
296
|
+
// on `closed` (V −= 1). `produce.scope.changed` is the ONLY sanctioned
|
|
297
|
+
// post-descent V increase — `milestone_id` is optional (a scope change may
|
|
298
|
+
// not target one milestone), so only `run_id` + `reason` are required.
|
|
299
|
+
case 'produce.criteria.declared':
|
|
300
|
+
return requireFields(op, d, ['run_id', 'milestone_id', 'criterion_id', 'text'])
|
|
301
|
+
|
|
302
|
+
case 'produce.criteria.closed':
|
|
303
|
+
return requireFields(op, d, ['run_id', 'milestone_id', 'criterion_id'])
|
|
304
|
+
|
|
305
|
+
case 'produce.scope.changed':
|
|
306
|
+
return requireFields(op, d, ['run_id', 'reason'])
|
|
307
|
+
|
|
308
|
+
// Produce M4 — the saga phase model + FORWARD compensators + verification-TTL.
|
|
309
|
+
// The saga-phase ops (entered/completed/compensated) enforce the closed
|
|
310
|
+
// `phase` vocabulary (ALLOWED_SAGA_PHASES), mirroring the phase.changed.to
|
|
311
|
+
// value check. No op names a probe/review CONTENT field — the roadmap
|
|
312
|
+
// projection still folds `produce.roadmap.*` ONLY (blindness preserved).
|
|
313
|
+
case 'produce.saga.phase_entered':
|
|
314
|
+
case 'produce.saga.phase_completed': {
|
|
315
|
+
const r = requireFields(op, d, ['run_id', 'milestone_id', 'phase'])
|
|
316
|
+
if (!r.ok) return r
|
|
317
|
+
return checkSagaPhase(op, d)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
case 'produce.saga.compensated': {
|
|
321
|
+
const r = requireFields(op, d, ['run_id', 'milestone_id', 'phase', 'compensator'])
|
|
322
|
+
if (!r.ok) return r
|
|
323
|
+
return checkSagaPhase(op, d)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// A diverse-review verdict over the milestone's diff, tagged with the
|
|
327
|
+
// (cross-family) reviewer family. Content-free — verdict + family only.
|
|
328
|
+
case 'produce.saga.reviewed':
|
|
329
|
+
return requireFields(op, d, ['run_id', 'milestone_id', 'reviewer_family', 'verdict'])
|
|
330
|
+
|
|
331
|
+
// A scheduled post-`done` TTL re-verify RUN against HEAD — result/head_sha
|
|
332
|
+
// only, never probe content. A regression fires the forward compensator.
|
|
333
|
+
case 'produce.ttl.reverified':
|
|
334
|
+
return requireFields(op, d, ['run_id', 'milestone_id', 'head_sha', 'result'])
|
|
335
|
+
|
|
336
|
+
case 'produce.ttl.regression_detected':
|
|
337
|
+
return requireFields(op, d, ['run_id', 'milestone_id', 'head_sha'])
|
|
338
|
+
|
|
339
|
+
// Produce M5 — the WSJF/WIP scheduler's recorded pick. Only `run_id` is
|
|
340
|
+
// required (the `picked` ids + `wip_limit` are optional context); the
|
|
341
|
+
// roadmap projection folds `produce.roadmap.*` ONLY, so it never sees this
|
|
342
|
+
// op (blindness preserved — ID-M5-06).
|
|
343
|
+
case 'produce.schedule.computed':
|
|
344
|
+
return requireFields(op, d, ['run_id'])
|
|
345
|
+
|
|
346
|
+
// Produce M6 — the Claim Envelope lifecycle. `declared` carries a
|
|
347
|
+
// `content_hash` ONLY (the full envelope lives in the frozen
|
|
348
|
+
// claims/<claim-id>.json file — ID-M6-07); `consumed.target_kind` is
|
|
349
|
+
// value-checked against the closed set (mirrors ALLOWED_SAGA_PHASES /
|
|
350
|
+
// checkSagaPhase). No op names a claim-content field — the roadmap
|
|
351
|
+
// projection still folds `produce.roadmap.*` ONLY (blindness preserved).
|
|
352
|
+
case 'produce.claim.declared':
|
|
353
|
+
return requireFields(op, d, ['run_id', 'profile', 'claim_id', 'content_hash'])
|
|
354
|
+
|
|
355
|
+
case 'produce.claim.expired':
|
|
356
|
+
return requireFields(op, d, ['run_id', 'claim_id', 'reason'])
|
|
357
|
+
|
|
358
|
+
case 'produce.claim.consumed': {
|
|
359
|
+
const r = requireFields(op, d, ['run_id', 'claim_id', 'target_kind'])
|
|
360
|
+
if (!r.ok) return r
|
|
361
|
+
return checkClaimTargetKind(op, d)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
case 'showrunner.boot_reconciled': {
|
|
156
365
|
// Fleet-wide event — numeric fields only. All optional but if
|
|
157
366
|
// present must be finite numbers (zero allowed).
|
|
158
367
|
const numFields = ['cleaned_leases', 'replayed', 'start_offset']
|
|
@@ -174,7 +383,10 @@ export function validateConductorEvent(op, data) {
|
|
|
174
383
|
}
|
|
175
384
|
|
|
176
385
|
/** Set of ops the schema validates — exported for test enumeration. */
|
|
177
|
-
export const
|
|
386
|
+
export const SHOWRUNNER_VALIDATED_OPS = SHOWRUNNER_OPS
|
|
387
|
+
|
|
388
|
+
/** Produce outer-loop ops the schema validates — exported for test enumeration. */
|
|
389
|
+
export const PRODUCE_VALIDATED_OPS = PRODUCE_OPS
|
|
178
390
|
|
|
179
391
|
/**
|
|
180
392
|
* @param {string} op
|
|
@@ -191,3 +403,46 @@ function requireFields(op, d, fields) {
|
|
|
191
403
|
if (missing.length === 0) return { ok: true }
|
|
192
404
|
return { ok: false, error: `${op} missing required field(s): ${missing.join(', ')}` }
|
|
193
405
|
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Validate the saga `phase` value against ALLOWED_SAGA_PHASES (M4). Mirrors the
|
|
409
|
+
* inline phase.changed.to value check — fail-closed on an out-of-vocabulary
|
|
410
|
+
* phase so a saga event can never carry an unknown phase onto the log.
|
|
411
|
+
*
|
|
412
|
+
* @param {string} op
|
|
413
|
+
* @param {Record<string, unknown>} d
|
|
414
|
+
* @returns {{ ok: true } | { ok: false, error: string }}
|
|
415
|
+
*/
|
|
416
|
+
function checkSagaPhase(op, d) {
|
|
417
|
+
if (!ALLOWED_SAGA_PHASES.has(String(d.phase))) {
|
|
418
|
+
return {
|
|
419
|
+
ok: false,
|
|
420
|
+
error: `${op}.phase must be one of [${[...ALLOWED_SAGA_PHASES].join(', ')}], got: ${String(
|
|
421
|
+
d.phase,
|
|
422
|
+
)}`,
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return { ok: true }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Validate the claim `target_kind` value against ALLOWED_CLAIM_TARGET_KINDS (M6).
|
|
430
|
+
* Mirrors checkSagaPhase — fail-closed on an out-of-vocabulary target kind so a
|
|
431
|
+
* produce.claim.consumed event can never carry an unknown delta target onto the
|
|
432
|
+
* log (ID-M6-06).
|
|
433
|
+
*
|
|
434
|
+
* @param {string} op
|
|
435
|
+
* @param {Record<string, unknown>} d
|
|
436
|
+
* @returns {{ ok: true } | { ok: false, error: string }}
|
|
437
|
+
*/
|
|
438
|
+
function checkClaimTargetKind(op, d) {
|
|
439
|
+
if (!ALLOWED_CLAIM_TARGET_KINDS.has(String(d.target_kind))) {
|
|
440
|
+
return {
|
|
441
|
+
ok: false,
|
|
442
|
+
error: `${op}.target_kind must be one of [${[...ALLOWED_CLAIM_TARGET_KINDS].join(
|
|
443
|
+
', ',
|
|
444
|
+
)}], got: ${String(d.target_kind)}`,
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return { ok: true }
|
|
448
|
+
}
|
|
@@ -32,7 +32,7 @@ import { withFileLock } from '../util/fs-lock.mjs'
|
|
|
32
32
|
import { atomicWriteJson } from '../util/io.mjs'
|
|
33
33
|
import { getAgentIdentity, resolveSharedRoot } from './agent-identity.mjs'
|
|
34
34
|
import { appendLifecycleEvent } from './event-log.mjs'
|
|
35
|
-
import {
|
|
35
|
+
import { validateShowrunnerEvent } from './event-schema.mjs'
|
|
36
36
|
import { advanceLastSeen as lastSeenAdvance, readLastSeen as lastSeenRead } from './last-seen.mjs'
|
|
37
37
|
import {
|
|
38
38
|
listActive as teamStatusListActive,
|
|
@@ -131,16 +131,16 @@ export function createCoordinationStore(projectRoot) {
|
|
|
131
131
|
* call via appendLifecycleEvent. Best-effort: filesystem errors are
|
|
132
132
|
* swallowed so a missing events/ dir never breaks task lifecycle.
|
|
133
133
|
*
|
|
134
|
-
* Step 1 — schema gate:
|
|
134
|
+
* Step 1 — schema gate: showrunner ops written via the store seam are
|
|
135
135
|
* validated against `event-schema.mjs` before they reach disk.
|
|
136
136
|
* Schema violations throw (caught here and surfaced via console.warn)
|
|
137
|
-
* because a malformed
|
|
137
|
+
* because a malformed showrunner event would silently break the
|
|
138
138
|
* drawer's pairing logic — we want it loud, not best-effort.
|
|
139
139
|
*/
|
|
140
140
|
appendEvent(evt) {
|
|
141
141
|
try {
|
|
142
142
|
if (evt && typeof evt.op === 'string') {
|
|
143
|
-
const validation =
|
|
143
|
+
const validation = validateShowrunnerEvent(evt.op, evt.data)
|
|
144
144
|
if (!validation.ok) {
|
|
145
145
|
console.warn(`[store.appendEvent] schema violation: ${validation.error}`)
|
|
146
146
|
return null
|
package/src/cli/dispatch.mjs
CHANGED
|
@@ -62,7 +62,7 @@ import { cmdEditCheck } from './commands/edit-check.mjs'
|
|
|
62
62
|
import { cmdEffects } from './commands/effects.mjs'
|
|
63
63
|
import { cmdEvent } from './commands/event.mjs'
|
|
64
64
|
import { cmdFanOut } from './commands/fan-out.mjs'
|
|
65
|
-
import {
|
|
65
|
+
import { cmdFeaturesReconcile } from './commands/features-reconcile.mjs'
|
|
66
66
|
import { cmdFollowups } from './commands/followups.mjs'
|
|
67
67
|
import { cmdGate } from './commands/gate.mjs'
|
|
68
68
|
import { cmdHealthCheck } from './commands/health-check.mjs'
|
|
@@ -78,6 +78,7 @@ import { cmdPersonas } from './commands/personas.mjs'
|
|
|
78
78
|
import { cmdPlanApproval } from './commands/plan-approval.mjs'
|
|
79
79
|
import { cmdPrReview } from './commands/pr-review.mjs'
|
|
80
80
|
import { cmdPrecompactHook } from './commands/precompact-hook.mjs'
|
|
81
|
+
import { cmdProduce } from './commands/produce.mjs'
|
|
81
82
|
import { cmdProductContext } from './commands/product-context.mjs'
|
|
82
83
|
import { cmdProfile } from './commands/profile.mjs'
|
|
83
84
|
// cmdRecipeDiff — loaded lazily in the dispatch handler (Fast Path PRF-001)
|
|
@@ -260,7 +261,7 @@ export const DISPATCH = {
|
|
|
260
261
|
exitCode: 1,
|
|
261
262
|
}))()
|
|
262
263
|
},
|
|
263
|
-
'features-
|
|
264
|
+
'features-reconcile': (args) => wrap(cmdFeaturesReconcile)(args[1], args.slice(2)),
|
|
264
265
|
'init-state': (args) => wrap(cmdInitState)(args[1], args.slice(2)),
|
|
265
266
|
'risk-check': (args) => wrap(cmdRiskCheck)(args[1], args.slice(2)),
|
|
266
267
|
'edit-check': (args) => wrap(cmdEditCheck)(args.slice(1)),
|
|
@@ -326,6 +327,7 @@ export const DISPATCH = {
|
|
|
326
327
|
team: (args) => wrap(cmdTeam)(args[1], args[2], args.slice(3)),
|
|
327
328
|
active: (args) => wrap(cmdActive)(args[1], args[2], args.slice(3)),
|
|
328
329
|
roadmap: (args) => wrap(cmdRoadmap)(args[1], args[2], args.slice(3)),
|
|
330
|
+
produce: (args) => wrap(cmdProduce)(args[1], args[2], args.slice(3)),
|
|
329
331
|
seed: (args) => wrap(cmdSeed)(args[1], args[2], args.slice(3)),
|
|
330
332
|
workflow: (args) => wrap(cmdWorkflow)(args[1], args[2], args.slice(3)),
|
|
331
333
|
'pr-review': (args) => wrap(cmdPrReview)(args[1], args[2], args.slice(3)),
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* area-shape.mjs — hand-rolled structural validator for feature-registry
|
|
3
|
+
* area files, mirroring `src/schemas/feature-registry.ts` (FeatureRegistrySchema)
|
|
4
|
+
* PLUS the uniqueness refinement (feature ids unique per area; sub_feature ids
|
|
5
|
+
* unique per feature group).
|
|
6
|
+
*
|
|
7
|
+
* WHY A TWIN VALIDATOR EXISTS (ID-07): the bare-node CI check
|
|
8
|
+
* (`bin/features-reconcile-check.mjs`) runs with NO node_modules, so it cannot
|
|
9
|
+
* import zod. This module is node:-builtins only (in fact, zero imports).
|
|
10
|
+
* `feature-registry-schema-parity.test.ts` validates shared fixtures against
|
|
11
|
+
* BOTH this validator and the zod schema so they cannot drift.
|
|
12
|
+
*
|
|
13
|
+
* Semantics mirror zod defaults:
|
|
14
|
+
* - unknown keys are ALLOWED (zod objects are non-strict);
|
|
15
|
+
* - `components` and `sub_features` may be absent (zod `.default([])`);
|
|
16
|
+
* - `route` stays required non-empty — non-routed areas use the literal
|
|
17
|
+
* "(cross-cutting)" (chrome.json precedent), never null.
|
|
18
|
+
*
|
|
19
|
+
* Returns { ok, errors: [] } — never throws.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const ID_RE = /^[a-z0-9-]+$/
|
|
23
|
+
const LAST_SCANNED_RE = /^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]+)?$/
|
|
24
|
+
const TESTABLE_VALUES = new Set(['testable', 'observable', 'blocked'])
|
|
25
|
+
const WIRED_VALUES = new Set([true, false, 'stubbed', 'partial'])
|
|
26
|
+
|
|
27
|
+
function isNonEmptyString(v) {
|
|
28
|
+
return typeof v === 'string' && v.length > 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function checkOptionalString(errors, at, v) {
|
|
32
|
+
if (v !== undefined && !isNonEmptyString(v)) {
|
|
33
|
+
errors.push(`${at}: must be a non-empty string when present`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function validateSubFeature(errors, at, sub) {
|
|
38
|
+
if (!sub || typeof sub !== 'object' || Array.isArray(sub)) {
|
|
39
|
+
errors.push(`${at}: must be an object`)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
if (!isNonEmptyString(sub.id) || !ID_RE.test(sub.id)) {
|
|
43
|
+
errors.push(`${at}.id: must match ${ID_RE}`)
|
|
44
|
+
}
|
|
45
|
+
if (!isNonEmptyString(sub.name)) errors.push(`${at}.name: required non-empty string`)
|
|
46
|
+
checkOptionalString(errors, `${at}.component`, sub.component)
|
|
47
|
+
if (!isNonEmptyString(sub.type)) errors.push(`${at}.type: required non-empty string`)
|
|
48
|
+
if (!WIRED_VALUES.has(sub.wired)) {
|
|
49
|
+
errors.push(`${at}.wired: must be true|false|"stubbed"|"partial"`)
|
|
50
|
+
}
|
|
51
|
+
if (sub.handler !== undefined && typeof sub.handler !== 'string') {
|
|
52
|
+
errors.push(`${at}.handler: must be a string when present`)
|
|
53
|
+
}
|
|
54
|
+
if (sub.shortcut !== undefined && typeof sub.shortcut !== 'string') {
|
|
55
|
+
errors.push(`${at}.shortcut: must be a string when present`)
|
|
56
|
+
}
|
|
57
|
+
if (sub.testable !== undefined && !TESTABLE_VALUES.has(sub.testable)) {
|
|
58
|
+
errors.push(`${at}.testable: must be "testable"|"observable"|"blocked" when present`)
|
|
59
|
+
}
|
|
60
|
+
if (sub.test_hint !== undefined && typeof sub.test_hint !== 'string') {
|
|
61
|
+
errors.push(`${at}.test_hint: must be a string when present`)
|
|
62
|
+
}
|
|
63
|
+
// default / range / options are `unknown` in the zod schema — anything goes.
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validateFeatureGroup(errors, at, group) {
|
|
67
|
+
if (!group || typeof group !== 'object' || Array.isArray(group)) {
|
|
68
|
+
errors.push(`${at}: must be an object`)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
if (!isNonEmptyString(group.id) || !ID_RE.test(group.id)) {
|
|
72
|
+
errors.push(`${at}.id: must match ${ID_RE}`)
|
|
73
|
+
}
|
|
74
|
+
if (!isNonEmptyString(group.name)) errors.push(`${at}.name: required non-empty string`)
|
|
75
|
+
checkOptionalString(errors, `${at}.component`, group.component)
|
|
76
|
+
if (group.sub_features !== undefined) {
|
|
77
|
+
if (!Array.isArray(group.sub_features)) {
|
|
78
|
+
errors.push(`${at}.sub_features: must be an array when present`)
|
|
79
|
+
} else {
|
|
80
|
+
const seen = new Set()
|
|
81
|
+
for (const [i, sub] of group.sub_features.entries()) {
|
|
82
|
+
validateSubFeature(errors, `${at}.sub_features[${i}]`, sub)
|
|
83
|
+
const id = sub?.id
|
|
84
|
+
if (isNonEmptyString(id)) {
|
|
85
|
+
if (seen.has(id)) {
|
|
86
|
+
errors.push(`${at}.sub_features[${i}].id: duplicate sub_feature id "${id}" in group`)
|
|
87
|
+
}
|
|
88
|
+
seen.add(id)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Structural + uniqueness validation of an area-file value.
|
|
97
|
+
*
|
|
98
|
+
* @param {unknown} data — parsed JSON of a `.aperant/features/<area>.json`
|
|
99
|
+
* @returns {{ ok: boolean, errors: string[] }}
|
|
100
|
+
*/
|
|
101
|
+
export function validateAreaShape(data) {
|
|
102
|
+
const errors = []
|
|
103
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
104
|
+
return { ok: false, errors: ['area file must be a JSON object'] }
|
|
105
|
+
}
|
|
106
|
+
if (!isNonEmptyString(data.area)) errors.push('area: required non-empty string')
|
|
107
|
+
if (!isNonEmptyString(data.route)) {
|
|
108
|
+
errors.push(
|
|
109
|
+
'route: required non-empty string — use "(cross-cutting)" for non-routed areas, never null',
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
if (data.components !== undefined) {
|
|
113
|
+
if (!Array.isArray(data.components)) {
|
|
114
|
+
errors.push('components: must be an array when present')
|
|
115
|
+
} else {
|
|
116
|
+
for (const [i, c] of data.components.entries()) {
|
|
117
|
+
if (!isNonEmptyString(c)) errors.push(`components[${i}]: must be a non-empty string`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!isNonEmptyString(data.last_scanned) || !LAST_SCANNED_RE.test(data.last_scanned)) {
|
|
122
|
+
errors.push('last_scanned: must be a YYYY-MM-DD or full ISO 8601 date string')
|
|
123
|
+
}
|
|
124
|
+
if (!Array.isArray(data.features)) {
|
|
125
|
+
errors.push('features: required array')
|
|
126
|
+
} else {
|
|
127
|
+
const seen = new Set()
|
|
128
|
+
for (const [i, group] of data.features.entries()) {
|
|
129
|
+
validateFeatureGroup(errors, `features[${i}]`, group)
|
|
130
|
+
const id = group?.id
|
|
131
|
+
if (isNonEmptyString(id)) {
|
|
132
|
+
if (seen.has(id)) {
|
|
133
|
+
errors.push(`features[${i}].id: duplicate feature id "${id}" in area`)
|
|
134
|
+
}
|
|
135
|
+
seen.add(id)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { ok: errors.length === 0, errors }
|
|
140
|
+
}
|