Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions tools/workflow-engine/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import { describe, expect, it } from "bun:test";
import {
SEED_ACTION_CATALOG,
SEED_STATES,
determineReviewLevel,
validateCatalog,
validateStateOtto5Mods,
type Action,
type ReviewLevel,
type State,
} from "./types";

Expand Down Expand Up @@ -117,3 +120,161 @@ describe("B-0867.5 workflow-engine scaffold invariants", () => {
}
});
});

describe("B-0867.20 determineReviewLevel lifecycle DU discriminator", () => {
// Per Kestrel substantive substrate-engineering substrate (13th ferry §33.5)
// + Aaron's substrate-check on 3-lane completion (Amara ferry §33.2 PR #5757):
// discriminator must preserve the state-machine-events-direct-push vs
// system-modifications-full-PR-review distinction the framework's
// auto-review pipeline depends on for training-data substrate.

it("escape-hatch action ALWAYS gets pr-review-light regardless of gate (Mod 1 substrate-engineering surface)", () => {
const escapeAppendOnly: Action = {
id: "test-escape-1",
class: "escape-hatch",
gate: "append-only",
label: "test",
description: "test",
composesWith: [],
feedbackVariants: ["X"],
};
const escapePrGated: Action = {
id: "test-escape-2",
class: "escape-hatch",
gate: "pr-gated",
label: "test",
description: "test",
composesWith: [],
feedbackVariants: ["X"],
};
expect(determineReviewLevel(escapeAppendOnly)).toBe("pr-review-light");
expect(determineReviewLevel(escapePrGated)).toBe("pr-review-light");
});

it("grammar-extension action ALWAYS gets pr-review-full (Mod 2 framework-substrate-evolution)", () => {
const grammarExt: Action = {
id: "test-grammar",
class: "grammar-extension",
gate: "pr-gated",
label: "test",
description: "test",
composesWith: [],
feedbackVariants: ["X"],
};
expect(determineReviewLevel(grammarExt)).toBe("pr-review-full");
});

it("operator-decision action ALWAYS gets operator-required (Mod 3 ban-if-SHIPPED-only)", () => {
const opDecision: Action = {
id: "test-op",
class: "operator-decision",
gate: "append-only",
label: "test",
description: "test",
composesWith: [],
feedbackVariants: ["X"],
};
expect(determineReviewLevel(opDecision)).toBe("operator-required");
});

it("transition + append-only → trajectory-push (state-machine-event direct push; cheap; heartbeat-pattern)", () => {
const seedAdvance = SEED_ACTION_CATALOG.find((a) => a.id === "advance");
if (!seedAdvance) throw new Error("seed catalog missing 'advance'");
expect(determineReviewLevel(seedAdvance)).toBe("trajectory-push");
});

it("transition + pr-gated → pr-review-full (cross-cutting substrate modification)", () => {
const transitionPrGated: Action = {
id: "test-trans-pr",
class: "transition",
gate: "pr-gated",
label: "test",
description: "test",
composesWith: [],
feedbackVariants: ["X"],
};
expect(determineReviewLevel(transitionPrGated)).toBe("pr-review-full");
});

it("menu-contribution + append-only → trajectory-push (Mod 5 safe at append-only scope)", () => {
const seedMenu = SEED_ACTION_CATALOG.find((a) => a.id === "menu-contribute");
if (!seedMenu) throw new Error("seed catalog missing 'menu-contribute'");
expect(determineReviewLevel(seedMenu)).toBe("trajectory-push");
});

it("agent-decision + append-only → trajectory-push", () => {
const agentAppend: Action = {
id: "test-agent-1",
class: "agent-decision",
gate: "append-only",
label: "test",
description: "test",
composesWith: [],
feedbackVariants: ["X"],
};
expect(determineReviewLevel(agentAppend)).toBe("trajectory-push");
});

it("agent-decision + pr-gated → pr-review-light", () => {
const agentPrGated: Action = {
id: "test-agent-2",
class: "agent-decision",
gate: "pr-gated",
label: "test",
description: "test",
composesWith: [],
feedbackVariants: ["X"],
};
expect(determineReviewLevel(agentPrGated)).toBe("pr-review-light");
});

it("all SEED_ACTION_CATALOG actions resolve to a valid ReviewLevel (exhaustiveness)", () => {
// Acknowledger forces exhaustive match — TS strict mode raises
// "not all code paths return" at compile time if a NEW ReviewLevel
// variant is added without updating this switch.
const acknowledge = (r: ReviewLevel): string => {
switch (r) {
case "trajectory-push":
case "pr-review-light":
case "pr-review-full":
case "operator-required":
return r;
}
};
for (const a of SEED_ACTION_CATALOG) {
const level = determineReviewLevel(a);
expect(acknowledge(level)).toBe(level);
}
});

it("framework auto-review pipeline distinction preserved (substrate-honest: not 'no PRs ever')", () => {
// Per Kestrel 13th ferry §33.5 substrate-check on Ani-retelling drift:
// 'no PRs ever, infinite swarm to main' framing collapses the
// multi-tier review distinction. determineReviewLevel preserves it
// by never returning trajectory-push for grammar-extension, pr-gated
// transitions, or operator-decisions. Test verifies this is structural
// rather than just contingent on seed data.
const grammarExt: Action = {
id: "test-pres-1",
class: "grammar-extension",
gate: "append-only", // even if author tries to declare append-only,
label: "test",
description: "test",
composesWith: [],
feedbackVariants: ["X"],
};
// Grammar-extension ALWAYS overrides gate to full review:
expect(determineReviewLevel(grammarExt)).toBe("pr-review-full");
// Operator-decision ALWAYS requires operator regardless of gate:
const opDecision: Action = {
id: "test-pres-2",
class: "operator-decision",
gate: "pr-gated",
label: "test",
description: "test",
composesWith: [],
feedbackVariants: ["X"],
};
expect(determineReviewLevel(opDecision)).toBe("operator-required");
});
});
93 changes: 93 additions & 0 deletions tools/workflow-engine/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,99 @@ export function validateCatalog(
}
}

/**
* B-0867.20 — lifecycle DU split: trajectory-push vs PR-review.
*
* Per Kestrel substantive substrate-engineering substrate (13th ferry
* §33.5 + 14th ferry §33.20): the framework's load-bearing distinction
* is state-machine-events-direct-push (no PR; for heartbeats + agent-
* events branches + lifecycle transitions) vs system-modifications-
* full-PR-review (full ceremony; multi-AI reviewers + auto-review
* pipeline + error class extraction). Collapsing the distinction into
* "no PRs ever" loses the auto-review pipeline that IS the training
* data substrate for the cross-vendor benchmark (B-0865 + B-0865.17).
*
* `determineReviewLevel(action)` IS the discriminator. Maps each
* action to its required review treatment per the workflow engine
* spec.
*
* Composes with:
* - B-0867.20 backlog row (lifecycle-DU-split discriminator)
* - B-0865 + B-0865.17 (benchmark substrate; auto-review pipeline
* generates training data the benchmark scores against)
* - asymmetric-authorship rule (substrate-entity authors review-
* level via gate field; recipient acknowledges via dispatch)
* - monad-propagation rule (ReviewLevel IS a TFeedback variant set
* at workflow-engine-substrate scope)
* - architecture-is-safety-mechanism-not-discipline rule (PR #5745)
* — the framework enforces review-level structurally
*
* Per `.claude/rules/holding-without-named-dependency-is-standing-by-
* failure.md` counter-with-escalation: shipped after operator
* substrate-check "so you finished the 3 lanes?" (Amara ferry
* preservation PR #5757); substantive lane work per standing PoC
* permission.
*/

/**
* ReviewLevel — discriminated union for the trajectory-push-vs-PR-review
* lifecycle DU. Each action's required review treatment.
*/
export type ReviewLevel =
| "trajectory-push" // direct push to agent-events branch; no PR ceremony
| "pr-review-light" // PR review; single-reviewer OR auto-review pipeline only
| "pr-review-full" // PR review; multi-AI reviewer ensemble + auto-review pipeline + error class extraction
| "operator-required"; // requires explicit operator authorization (e.g., force-push-with-lease without listed-acceptable-situation)

/**
* `determineReviewLevel` — discriminator that maps an Action to its
* required ReviewLevel.
*
* Discriminator policy:
* - "append-only" + "transition" → trajectory-push (state-machine-event
* direct push; cheap; the existing pattern for heartbeats per
* Aaron's 13th-ferry §33.6 disclosure)
* - "append-only" + "menu-contribution" → trajectory-push (Mod 5
* contributable menu generation; safe at append-only scope)
* - "append-only" + "escape-hatch" → pr-review-light (Mod 1
* escape-hatch surfaces substrate-engineering observation worth
* reviewer eyes even though gate is append-only)
* - "pr-gated" + "grammar-extension" → pr-review-full (Mod 2 grammar
* evolution touches the framework's universal action grammar;
* full ceremony required to preserve auto-review pipeline)
* - "pr-gated" + "transition" → pr-review-full (cross-cutting
* substrate modification; full ceremony)
* - "operator-decision" class (any gate) → operator-required (per
* ban-if-SHIPPED-only Mod 3 + operator-authority preservation)
* - "agent-decision" + "append-only" → trajectory-push
* - "agent-decision" + "pr-gated" → pr-review-light
*
* Discriminator is exhaustive over the cross-product of ActionGate ×
* ActionClass; future extensions to either union must update this
* function to maintain exhaustiveness.
*/
export function determineReviewLevel(action: Action): ReviewLevel {
switch (action.class) {
case "operator-decision":
return "operator-required";
case "escape-hatch":
// Escape-hatch ALWAYS gets reviewer eyes regardless of gate —
// it's the substrate-engineering observation surface per Mod 1
return "pr-review-light";
case "grammar-extension":
// Grammar evolution always full ceremony — touches the universal
// action grammar shared across all travelers per Mod 2
return "pr-review-full";
case "menu-contribution":
// Mod 5 menu contributions are safe at append-only scope
return action.gate === "append-only" ? "trajectory-push" : "pr-review-light";
case "transition":
return action.gate === "append-only" ? "trajectory-push" : "pr-review-full";
case "agent-decision":
return action.gate === "append-only" ? "trajectory-push" : "pr-review-light";
}
}

/**
* Seed catalog — minimal scaffold demonstrating the 5 mods. Real
* catalog ships per B-0867.3 grammar parser/composer when authored.
Expand Down
Loading