From 518a26fdd98ff000294a7767d404a9a96b531a55 Mon Sep 17 00:00:00 2001 From: artale Date: Mon, 29 Jun 2026 01:14:36 +0200 Subject: [PATCH] feat: add fableflow manifest generator --- COMMANDS.md | 2 + src/fable5/fableflow-manifest.test.ts | 64 ++++++++++++++++ src/fable5/fableflow-manifest.ts | 102 ++++++++++++++++++++++++++ src/fable5/index.ts | 3 + src/index.ts | 13 ++++ 5 files changed, 184 insertions(+) create mode 100644 src/fable5/fableflow-manifest.test.ts create mode 100644 src/fable5/fableflow-manifest.ts diff --git a/COMMANDS.md b/COMMANDS.md index 8c99963..9b767c3 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -360,6 +360,8 @@ fable-agent plinius godmode "improve explanation quality" - `--fallback ` - `--risk ` - `--out ` +- `fable5 fableflow` + - `--out ` - `fable5 spec ` - `--repo ` - `--out ` diff --git a/src/fable5/fableflow-manifest.test.ts b/src/fable5/fableflow-manifest.test.ts new file mode 100644 index 0000000..52fb120 --- /dev/null +++ b/src/fable5/fableflow-manifest.test.ts @@ -0,0 +1,64 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { describe, expect, it } from "vitest"; +import { createFableFlowManifest, writeFableFlowManifest } from "./fableflow-manifest.js"; + +describe("fableflow manifest", () => { + it("includes required Fable topology nodes", () => { + const manifest = createFableFlowManifest(); + const ids = manifest.nodes.map((node) => node.id); + + expect(ids).toEqual(expect.arrayContaining([ + "forgejo", + "forgejo-runner", + "git-proxy-8099", + "deploy-webhook-8098", + "agent-site-8084", + "feedbackpilot-intake", + "work-inbox", + "forgejo-intake", + "forgejo-writeback", + "governance-contract", + "agent-chain-workers", + "zte-gate", + "factory-reconcile", + "model-access", + "rsi-canary", + ])); + }); + + it("marks 8099 canonical and 8098 legacy/stale", () => { + const manifest = createFableFlowManifest(); + const canonical = manifest.nodes.find((node) => node.id === "git-proxy-8099")!; + const legacy = manifest.nodes.find((node) => node.id === "deploy-webhook-8098")!; + + expect(manifest.meta.deployAuthority).toBe("8099/deploy via git-proxy"); + expect(manifest.meta.noDeploy).toBe(true); + expect(canonical.desc).toContain("Canonical guarded deploy route"); + expect(legacy.desc).toContain("Legacy/stale visual-only"); + expect(legacy.line).toBe("legacy"); + }); + + it("wires intake through governance, verification, and canonical deploy", () => { + const manifest = createFableFlowManifest(); + const edges = manifest.edges.map((edge) => `${edge.from}>${edge.to}`); + + expect(edges).toContain("forgejo-intake>governance-contract"); + expect(edges).toContain("governance-contract>agent-chain-workers"); + expect(edges).toContain("agent-chain-workers>zte-gate"); + expect(edges).toContain("factory-reconcile>git-proxy-8099"); + expect(edges).toContain("deploy-webhook-8098>git-proxy-8099"); + }); + + it("writes JSON manifest", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fableflow-")); + const file = path.join(dir, "manifest.json"); + + writeFableFlowManifest(file); + + const manifest = JSON.parse(fs.readFileSync(file, "utf-8")) as ReturnType; + expect(manifest.meta.schema).toBe("fable.fableflow.manifest.v1"); + expect(manifest.nodes.length).toBeGreaterThan(10); + }); +}); diff --git a/src/fable5/fableflow-manifest.ts b/src/fable5/fableflow-manifest.ts new file mode 100644 index 0000000..aa02122 --- /dev/null +++ b/src/fable5/fableflow-manifest.ts @@ -0,0 +1,102 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; + +export interface FableFlowNode { + id: string; + label: string; + kind: string; + line: string; + zone: string; + sub: string; + desc: string; + watched: string; + records: string; + icon: string; + x: number; + y: number; +} + +export interface FableFlowEdge { + id: string; + from: string; + to: string; + line: string; + label: string; +} + +export interface FableFlowManifest { + nodes: FableFlowNode[]; + edges: FableFlowEdge[]; + lines: Record; + meta: { + schema: "fable.fableflow.manifest.v1"; + generatedFrom: "committed-receipts"; + deployAuthority: "8099/deploy via git-proxy"; + legacyDeploy: "8098/deploy-webhook visual-only legacy/stale"; + noDeploy: true; + }; +} + +export function createFableFlowManifest(): FableFlowManifest { + return { + meta: { + schema: "fable.fableflow.manifest.v1", + generatedFrom: "committed-receipts", + deployAuthority: "8099/deploy via git-proxy", + legacyDeploy: "8098/deploy-webhook visual-only legacy/stale", + noDeploy: true, + }, + lines: { + intake: { label: "Intake", color: "#58a6ff" }, + governance: { label: "Governance", color: "#bc8cff" }, + verify: { label: "Verify / receipts", color: "#3fb950" }, + deploy: { label: "Guarded deploy", color: "#f0883e" }, + legacy: { label: "Legacy / stale", color: "#8b949e" }, + }, + nodes: [ + node("forgejo", "Forgejo", "service", "intake", "source", "issues / PRs / wiki", "Forgejo replaces GitHub/Jira as work source", "FORGEJO_URL", ".fable/tasks/forgejo-*.json", "FG", 0, 0), + node("forgejo-runner", "Forgejo Runner", "service", "verify", "ci", "runner", "Runner status must be refreshed live before claims", "live docker/runner receipt", ".fable/cyber-preflight/*.json", "CI", 240, 0), + node("git-proxy-8099", "git-proxy 8099", "service", "deploy", "deploy", "canonical", "Canonical guarded deploy route: 8099/deploy via git-proxy", "http://77.42.112.29:8099/health", ".fable/factory/reconciliation-live.json", "DP", 480, 0), + node("deploy-webhook-8098", "deploy-webhook 8098", "legacy", "legacy", "deploy", "legacy/stale", "Legacy/stale visual-only deploy-webhook; not a trusted decision path", "legacy only", ".fable/cyber-preflight/*8098*", "LG", 720, 0), + node("agent-site-8084", "agent-site 8084", "service", "intake", "ui", "agent site", "Agent/site surface; refresh live before availability claims", "http://77.42.112.29:8084", ".fable/cyber-preflight/*.json", "UI", 960, 0), + node("feedbackpilot-intake", "FeedbackPilot Intake", "receipt", "intake", "wp", "user feedback", "FeedbackPilot JSON to receipt / optional Forgejo issue", "src/fable5/feedbackpilot-intake.ts", "fable.feedbackpilot.intake.v1", "FP", 0, 180), + node("work-inbox", "Work Inbox", "receipt", "intake", "native work", "slack/discord/email/chat", "Where-you-work messages become scanned async receipts", "src/fable5/work-inbox.ts", "fable.work_inbox.receipt.v1", "IN", 240, 180), + node("forgejo-intake", "Forgejo Intake", "receipt", "intake", "work", "issues/PR/wiki", "Forgejo item to scanned task receipt", "src/fable5/forgejo-intake.ts", "fable.tasks/forgejo-*.json", "FI", 480, 180), + node("forgejo-writeback", "Forgejo Writeback", "receipt", "intake", "work", "comments/labels", "Receipt-backed comments/labels only; no deploy authority", "src/fable5/forgejo-writeback.ts", "fable.forgejo.writeback.v1", "FW", 720, 180), + node("governance-contract", "Governance Contract", "receipt", "governance", "control", "quorum", "Proposer/approver/quorum/tie-break/fallback receipt", "src/fable5/governance-contract.ts", "fable.governance.contract.v1", "GV", 240, 360), + node("agent-chain-workers", "Agent Chain Workers", "receipt", "verify", "workers", "scout/plan/build/review", "Receipt-backed local worker stages", "src/fable5/agent-chains.ts", "fable.agent_chain.stage.v1", "CH", 480, 360), + node("zte-gate", "ZTE Gate", "receipt", "verify", "gate", "plan/gate", "Verifier gate before deploy decisions", "src/fable5/zte-protocol.ts", ".fable/zte-receipts.jsonl", "ZT", 720, 360), + node("factory-reconcile", "Factory Reconcile", "receipt", "verify", "factory", "live receipt", "Live reconciliation overrides memory", "src/fable5/factory-reconciliation.ts", ".fable/factory/reconciliation-live.json", "RC", 960, 360), + node("model-access", "Model Access", "receipt", "verify", "models", "access check", "Model access claims require fresh receipt", "src/fable5/model-access-receipt.ts", "fable.model_access.receipt.v1", "MA", 0, 540), + node("rsi-canary", "RSI Canary", "receipt", "verify", "rsi", "canary only", "Canary repair proven; broad autopatch remains unproven without fresh receipt", "fable rsi receipts", ".fable/rsi/canary-live-*.json", "RS", 240, 540), + ], + edges: [ + edge("feedbackpilot-intake", "forgejo-intake", "intake", "opens issue"), + edge("work-inbox", "forgejo-intake", "intake", "captures work"), + edge("forgejo", "forgejo-intake", "intake", "fetches"), + edge("forgejo-intake", "governance-contract", "governance", "routes"), + edge("governance-contract", "agent-chain-workers", "verify", "authorizes work"), + edge("agent-chain-workers", "zte-gate", "verify", "produces evidence"), + edge("zte-gate", "factory-reconcile", "verify", "checks live state"), + edge("factory-reconcile", "git-proxy-8099", "deploy", "canonical gated path"), + edge("deploy-webhook-8098", "git-proxy-8099", "legacy", "superseded by"), + edge("forgejo-writeback", "forgejo", "intake", "comments/labels"), + edge("model-access", "agent-chain-workers", "verify", "provider evidence"), + edge("rsi-canary", "zte-gate", "verify", "canary proof only"), + ], + }; +} + +export function writeFableFlowManifest(file: string, manifest = createFableFlowManifest()): string { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`); + return file; +} + +function node(id: string, label: string, kind: string, line: string, zone: string, sub: string, desc: string, watched: string, records: string, icon: string, x: number, y: number): FableFlowNode { + return { id, label, kind, line, zone, sub, desc, watched, records, icon, x, y }; +} + +function edge(from: string, to: string, line: string, label: string): FableFlowEdge { + return { id: `${from}>${to}`, from, to, line, label }; +} diff --git a/src/fable5/index.ts b/src/fable5/index.ts index 8b10376..86c0cd8 100644 --- a/src/fable5/index.ts +++ b/src/fable5/index.ts @@ -99,6 +99,9 @@ export type { PlanPhase, PlanReceipt, PlanReceiptOptions } from "./plan-receipt. export { createGovernanceContract, writeGovernanceContract } from "./governance-contract.js"; export type { GovernanceContractOptions, GovernanceContractReceipt, GovernanceDecision } from "./governance-contract.js"; +export { createFableFlowManifest, writeFableFlowManifest } from "./fableflow-manifest.js"; +export type { FableFlowEdge, FableFlowManifest, FableFlowNode } from "./fableflow-manifest.js"; + export { createContextWorkspaceReceipt, writeContextWorkspaceReceipt } from "./context-workspace-receipt.js"; export type { ContextLayer, ContextStageContract, ContextWorkspaceReceipt, ContextWorkspaceReceiptOptions } from "./context-workspace-receipt.js"; diff --git a/src/index.ts b/src/index.ts index edf04c4..b9bfee1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2298,6 +2298,19 @@ fable if (receipt.decision === "blocked") process.exit(1); }); +fable + .command("fableflow") + .description("Generate a JamFlow-compatible FableFlow manifest from receipt-backed topology") + .option("--out ", "Manifest output path", path.join("TEMP", "fable-manifest.json")) + .action(async (opts: { out?: string }) => { + const { createFableFlowManifest, writeFableFlowManifest } = await import("./fable5/fableflow-manifest.js"); + const manifest = createFableFlowManifest(); + const out = opts.out ?? path.join("TEMP", "fable-manifest.json"); + writeFableFlowManifest(out, manifest); + console.log(JSON.stringify(manifest, null, 2)); + console.log("\n FableFlow manifest: " + out + "\n"); + }); + fable .command("spec ") .description("Generate a six-section anti-hallucination spec with verifier gates")