// Module catalog for the Workflows editor — TS port of FLOW_MODULES / // FLOW_CATS from the design handoff's `screens_flow.jsx`. The shapes here // drive the palette tree, the per-node ports, and the inspector form. export type FlowKind = "trigger" | "scanner" | "ai" | "logic" | "action"; export const KIND_COLOR: Record = { trigger: "var(--accent)", scanner: "var(--ink-2)", ai: "var(--ink-2)", logic: "var(--warn)", action: "var(--ok)", }; export type FlowCat = { id: string; label: string; kind: FlowKind; }; export const FLOW_CATS: FlowCat[] = [ { id: "triggers", label: "Triggers", kind: "trigger" }, { id: "scanner", label: "Compliance Scanner", kind: "scanner" }, { id: "certifai", label: "CERTifAI", kind: "ai" }, { id: "logic", label: "Logic", kind: "logic" }, { id: "actions", label: "Actions", kind: "action" }, ]; export type FlowSetting = | { k: string; label: string; type: "select"; opts: string[]; def: string; ph?: string } | { k: string; label: string; type: "text"; def: string; ph?: string } | { k: string; label: string; type: "num"; def: number; ph?: string } | { k: string; label: string; type: "area"; def: string; ph?: string } | { k: string; label: string; type: "toggle"; def: boolean; ph?: string }; export type FlowModule = { name: string; cat: string; kind: FlowKind; mono: string; in: string[]; out: string[]; desc: string; settings?: FlowSetting[]; }; export const FLOW_MODULES: Record = { // ---- triggers ---- schedule: { name: "On schedule", cat: "triggers", kind: "trigger", mono: "⏱", in: [], out: ["out"], desc: "daily · 02:00", settings: [ { k: "cadence", label: "Cadence", type: "select", opts: ["Every hour", "Daily", "Weekly", "Monthly"], def: "Daily", }, { k: "time", label: "At time (UTC)", type: "text", def: "02:00" }, ], }, "scan-complete": { name: "On scan complete", cat: "triggers", kind: "trigger", mono: "◆", in: [], out: ["out"], desc: "compliance-scanner", settings: [ { k: "product", label: "Product", type: "select", opts: ["compliance-scanner", "certifai", "any"], def: "compliance-scanner", }, ], }, "new-finding": { name: "On new finding", cat: "triggers", kind: "trigger", mono: "▲", in: [], out: ["out"], desc: "severity ≥ Medium", settings: [ { k: "minSev", label: "Min severity", type: "select", opts: ["Low", "Medium", "High", "Critical"], def: "Medium", }, ], }, webhook: { name: "Webhook", cat: "triggers", kind: "trigger", mono: "↯", in: [], out: ["out"], desc: "POST /hooks/…", settings: [ { k: "path", label: "Path", type: "text", def: "/hooks/ingest" }, { k: "secret", label: "Signing secret", type: "text", def: "whsec_••••" }, ], }, // ---- scanner ---- "run-scan": { name: "Run scan", cat: "scanner", kind: "scanner", mono: "CS", in: ["in"], out: ["out"], desc: "full · cloud + code", settings: [ { k: "scope", label: "Scope", type: "select", opts: ["Full", "Cloud only", "Code only", "Delta"], def: "Full", }, { k: "frameworks", label: "Frameworks", type: "text", def: "ISO 27001, BSI C5" }, ], }, filter: { name: "Filter findings", cat: "scanner", kind: "scanner", mono: "≡", in: ["in"], out: ["out"], desc: "status = open", settings: [ { k: "status", label: "Status", type: "select", opts: ["open", "resolved", "any"], def: "open", }, { k: "control", label: "Control matches", type: "text", def: "", ph: "e.g. ISO 27001 A.8*" }, ], }, "sev-gate": { name: "Severity gate", cat: "scanner", kind: "scanner", mono: "⊟", in: ["in"], out: ["pass", "fail"], desc: "≥ High", settings: [ { k: "threshold", label: "Threshold", type: "select", opts: ["Low", "Medium", "High", "Critical"], def: "High", }, ], }, "map-control": { name: "Map to control", cat: "scanner", kind: "scanner", mono: "⌖", in: ["in"], out: ["out"], desc: "framework: ISO 27001", settings: [ { k: "framework", label: "Framework", type: "select", opts: ["ISO 27001", "BSI C5", "NIS2", "TISAX"], def: "ISO 27001", }, ], }, // ---- certifai ---- "gen-annex": { name: "Generate Annex IV", cat: "certifai", kind: "ai", mono: "Ai", in: ["in"], out: ["out"], desc: "EU AI Act dossier", settings: [ { k: "model", label: "Model system", type: "select", opts: ["risk-scorer-v3", "doc-classifier", "all"], def: "risk-scorer-v3", }, { k: "sign", label: "Sign dossier", type: "toggle", def: true }, ], }, "bias-check": { name: "Bias evaluation", cat: "certifai", kind: "ai", mono: "Ai", in: ["in"], out: ["pass", "fail"], desc: "fairness ≥ 0.8", settings: [ { k: "metric", label: "Metric", type: "select", opts: ["Demographic parity", "Equalised odds"], def: "Demographic parity", }, { k: "min", label: "Min score", type: "num", def: 0.8 }, ], }, "model-audit": { name: "Model card audit", cat: "certifai", kind: "ai", mono: "Ai", in: ["in"], out: ["out"], desc: "Annex IV §2", settings: [{ k: "strict", label: "Strict mode", type: "toggle", def: false }], }, // ---- logic ---- branch: { name: "Branch", cat: "logic", kind: "logic", mono: "⑂", in: ["in"], out: ["true", "false"], desc: "if condition", settings: [ { k: "expr", label: "Condition", type: "text", def: "count > 0", ph: "expression" }, ], }, merge: { name: "Merge", cat: "logic", kind: "logic", mono: "⑃", in: ["a", "b"], out: ["out"], desc: "wait all", settings: [ { k: "mode", label: "Mode", type: "select", opts: ["Wait all", "First wins"], def: "Wait all", }, ], }, delay: { name: "Delay", cat: "logic", kind: "logic", mono: "◴", in: ["in"], out: ["out"], desc: "1 h", settings: [{ k: "amount", label: "Duration", type: "text", def: "1 h" }], }, // ---- actions ---- "create-evidence": { name: "Create evidence", cat: "actions", kind: "action", mono: "▤", in: ["in"], out: ["out"], desc: "PDF · signed", settings: [ { k: "name", label: "Bundle name", type: "text", def: "Auto-evidence" }, { k: "format", label: "Format", type: "select", opts: ["PDF", "JSON", "PDF + JSON"], def: "PDF", }, { k: "sign", label: "Hash-chain sign", type: "toggle", def: true }, ], }, notify: { name: "Notify", cat: "actions", kind: "action", mono: "✉", in: ["in"], out: [], desc: "Slack · #compliance", settings: [ { k: "channel", label: "Channel", type: "select", opts: ["Slack", "Email", "Microsoft Teams"], def: "Slack", }, { k: "target", label: "Target", type: "text", def: "#compliance" }, { k: "msg", label: "Message", type: "area", def: "{{count}} findings need review" }, ], }, "open-ticket": { name: "Open ticket", cat: "actions", kind: "action", mono: "⊞", in: ["in"], out: [], desc: "Jira · COMP", settings: [ { k: "system", label: "System", type: "select", opts: ["Jira", "ServiceNow", "Linear"], def: "Jira", }, { k: "project", label: "Project key", type: "text", def: "COMP" }, ], }, "export-bundle": { name: "Export bundle", cat: "actions", kind: "action", mono: "⇪", in: ["in"], out: [], desc: "S3 · eu-central", settings: [ { k: "dest", label: "Destination", type: "select", opts: ["S3 (eu-central)", "SFTP", "Download"], def: "S3 (eu-central)", }, ], }, }; export const modsByCat = (cat: string) => Object.entries(FLOW_MODULES) .filter(([, m]) => m.cat === cat) .map(([id, m]) => ({ id, ...m })); export const NODE_W = 202; export function nodeH(m: FlowModule): number { return Math.max(58, 36 + Math.max(m.in.length, m.out.length, 1) * 22); } export function defConfig(modId: string): Record { const m = FLOW_MODULES[modId]; if (!m) return {}; const c: Record = {}; (m.settings || []).forEach((s) => { c[s.k] = s.def; }); return c; } export type FlowNode = { id: string; mod: string; x: number; y: number; config: Record; }; export type FlowEdge = { id: string; from: [string, number]; to: [string, number]; }; export function seedFlow(): { nodes: FlowNode[]; edges: FlowEdge[] } { const mk = (id: string, mod: string, x: number, y: number): FlowNode => ({ id, mod, x, y, config: defConfig(mod), }); const nodes: FlowNode[] = [ mk("n1", "scan-complete", 24, 70), mk("n2", "sev-gate", 270, 86), mk("n3", "create-evidence", 516, 24), mk("n4", "notify", 516, 188), mk("n6", "schedule", 24, 320), mk("n7", "gen-annex", 270, 320), mk("n5", "map-control", 516, 320), ]; const edges: FlowEdge[] = [ { id: "e1", from: ["n1", 0], to: ["n2", 0] }, { id: "e2", from: ["n2", 0], to: ["n3", 0] }, { id: "e3", from: ["n2", 1], to: ["n4", 0] }, { id: "e4", from: ["n6", 0], to: ["n7", 0] }, { id: "e5", from: ["n7", 0], to: ["n5", 0] }, ]; return { nodes, edges }; } export function portY(node: FlowNode, side: "in" | "out"): number[] { const m = FLOW_MODULES[node.mod]; if (!m) return []; const ports = side === "in" ? m.in : m.out; const H = nodeH(m); if (ports.length === 0) return []; return ports.map((_, i) => node.y + (H * (i + 1)) / (ports.length + 1)); } export function portX(node: FlowNode, side: "in" | "out"): number { return side === "in" ? node.x : node.x + NODE_W; } export function wirePath(x1: number, y1: number, x2: number, y2: number): string { const dx = Math.max(36, Math.abs(x2 - x1) * 0.45); return `M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`; }