From 7d289ad456f39e482e8622bc69d0d181c888ee1d Mon Sep 17 00:00:00 2001 From: pallaoro Date: Mon, 13 Apr 2026 09:29:16 +0200 Subject: [PATCH] Derive NODE_KEYS from interfaces with compile-time guards, bump to 0.9.6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move allowed-key definitions next to their interfaces in types.ts. CheckKeys produces a compile error if the key tuple drifts from the interface — missing or extra keys are caught at build time. validate.ts imports NODE_KEYS instead of maintaining a duplicate list. --- package.json | 2 +- src/core/types.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++ src/core/validate.ts | 52 ++++++++++++++-------------------------- src/index.ts | 2 +- 4 files changed, 75 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index aa034b2..c537a0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@clawnify/clawflow", - "version": "0.9.5", + "version": "0.9.6", "description": "The n8n for agents. A declarative, AI-native workflow format that agents can read, write, and run.", "type": "module", "main": "./dist/index.js", diff --git a/src/core/types.ts b/src/core/types.ts index f0ae339..9b2518e 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -58,6 +58,17 @@ export interface BaseNode { timeout?: string | number; // e.g. "30s", or ms integer } +// Helper: compile-time check that a key tuple matches exactly the own keys of T (minus BaseNode). +// If a key is added to an interface but not the tuple (or vice versa), this produces a type error +// on the corresponding _XCheck variable below. +type OwnKeys = Exclude; +type CheckKeys = + [OwnKeys] extends [K[number]] + ? [K[number]] extends [OwnKeys] + ? true + : "Extra key(s) in tuple not on interface" + : "Missing key(s) from interface in tuple"; + // ---- Node Types ----------------------------------------------------------------- export interface AiNode extends BaseNode { @@ -71,6 +82,8 @@ export interface AiNode extends BaseNode { /** File paths (images, PDFs) to include as multimodal content. Supports templates. */ attachments?: string[]; } +const AI_KEYS = ["prompt", "input", "schema", "model", "temperature", "maxTokens", "attachments"] as const; +const _aiCheck: CheckKeys = true; export interface AgentNode extends BaseNode { do: "agent"; @@ -80,6 +93,8 @@ export interface AgentNode extends BaseNode { /** OpenClaw agent ID to delegate to (e.g. "main", "clawflow"). Uses OpenClaw's default routing if omitted. */ agentId?: string; } +const AGENT_KEYS = ["task", "input", "tools", "agentId"] as const; +const _agentCheck: CheckKeys = true; export interface BranchNode extends BaseNode { do: "branch"; @@ -87,6 +102,8 @@ export interface BranchNode extends BaseNode { paths: Record; // value -> sub-flow to execute default?: FlowNode[]; // sub-flow if no path matches } +const BRANCH_KEYS = ["on", "paths", "default"] as const; +const _branchCheck: CheckKeys = true; export interface LoopNode extends BaseNode { do: "loop"; @@ -94,12 +111,16 @@ export interface LoopNode extends BaseNode { as: string; // variable name for current item nodes: FlowNode[]; } +const LOOP_KEYS = ["over", "as", "nodes"] as const; +const _loopCheck: CheckKeys = true; export interface ParallelNode extends BaseNode { do: "parallel"; nodes: FlowNode[]; mode?: "all" | "race"; // "all" = wait for all, "race" = first wins } +const PARALLEL_KEYS = ["nodes", "mode"] as const; +const _parallelCheck: CheckKeys = true; export interface HttpNode extends BaseNode { do: "http"; @@ -108,6 +129,8 @@ export interface HttpNode extends BaseNode { body?: string | Record; headers?: Record; } +const HTTP_KEYS = ["url", "method", "body", "headers"] as const; +const _httpCheck: CheckKeys = true; export interface MemoryNode extends BaseNode { do: "memory"; @@ -115,6 +138,8 @@ export interface MemoryNode extends BaseNode { key: string; value?: string; // required for write } +const MEMORY_KEYS = ["action", "key", "value"] as const; +const _memoryCheck: CheckKeys = true; /** * wait — pause for human approval or external event. @@ -143,17 +168,23 @@ export interface WaitNode extends BaseNode { event?: string; // event type to match (for: event) timeout?: string; // e.g. "24h", "5m" -- fail if exceeded } +const WAIT_KEYS = ["for", "prompt", "preview", "event"] as const; +const _waitCheck: CheckKeys = true; export interface SleepNode extends BaseNode { do: "sleep"; duration: string; // e.g. "30s", "5m", "2h", "1d" } +const SLEEP_KEYS = ["duration"] as const; +const _sleepCheck: CheckKeys = true; export interface CodeNode extends BaseNode { do: "code"; run: string; input?: string; } +const CODE_KEYS = ["run", "input"] as const; +const _codeCheck: CheckKeys = true; /** * exec — run a shell command deterministically, no AI involved. @@ -171,6 +202,8 @@ export interface ExecNode extends BaseNode { command: string; cwd?: string; // working directory (resolved via templates) } +const EXEC_KEYS = ["command", "cwd"] as const; +const _execCheck: CheckKeys = true; /** * condition — if/else with sub-node blocks that reconverge. @@ -205,6 +238,29 @@ export interface ConditionNode extends BaseNode { then: FlowNode[]; // nodes to run when condition is true else?: FlowNode[]; // nodes to run when condition is false } +const CONDITION_KEYS = ["if", "then", "else"] as const; +const _conditionCheck: CheckKeys = true; + +// ---- Allowed Node Keys (derived from interfaces above) -------------------------- +// Used by the validator to reject unknown fields. The ExactKeys constraint ensures +// a compile error if a key list drifts from its interface. + +const BASE_KEYS: readonly string[] = ["name", "do", "output", "retry", "timeout"]; + +export const NODE_KEYS: Record> = { + ai: new Set([...BASE_KEYS, ...AI_KEYS]), + agent: new Set([...BASE_KEYS, ...AGENT_KEYS]), + branch: new Set([...BASE_KEYS, ...BRANCH_KEYS]), + condition: new Set([...BASE_KEYS, ...CONDITION_KEYS]), + loop: new Set([...BASE_KEYS, ...LOOP_KEYS]), + parallel: new Set([...BASE_KEYS, ...PARALLEL_KEYS]), + http: new Set([...BASE_KEYS, ...HTTP_KEYS]), + memory: new Set([...BASE_KEYS, ...MEMORY_KEYS]), + wait: new Set([...BASE_KEYS, ...WAIT_KEYS]), + sleep: new Set([...BASE_KEYS, ...SLEEP_KEYS]), + code: new Set([...BASE_KEYS, ...CODE_KEYS]), + exec: new Set([...BASE_KEYS, ...EXEC_KEYS]), +}; // ---- Runtime Types -------------------------------------------------------------- diff --git a/src/core/validate.ts b/src/core/validate.ts index b2dd1db..2bc91de 100644 --- a/src/core/validate.ts +++ b/src/core/validate.ts @@ -1,18 +1,19 @@ -import type { - FlowDefinition, - FlowNode, - AiNode, - AgentNode, - BranchNode, - ConditionNode, - LoopNode, - ParallelNode, - HttpNode, - MemoryNode, - WaitNode, - SleepNode, - CodeNode, - ExecNode, +import { + NODE_KEYS, + type FlowDefinition, + type FlowNode, + type AiNode, + type AgentNode, + type BranchNode, + type ConditionNode, + type LoopNode, + type ParallelNode, + type HttpNode, + type MemoryNode, + type WaitNode, + type SleepNode, + type CodeNode, + type ExecNode, } from "./types.js"; // ---- Flow Validator ------------------------------------------------------------- @@ -153,25 +154,6 @@ function validateNodes( } } -// ---- Allowed keys per node type (BaseNode keys are always allowed) ---------------- - -const BASE_KEYS = new Set(["name", "do", "output", "retry", "timeout"]); - -const ALLOWED_KEYS: Record> = { - ai: new Set([...BASE_KEYS, "prompt", "input", "schema", "model", "temperature", "maxTokens", "attachments"]), - agent: new Set([...BASE_KEYS, "task", "input", "tools", "agentId"]), - branch: new Set([...BASE_KEYS, "on", "paths", "default"]), - condition: new Set([...BASE_KEYS, "if", "then", "else"]), - loop: new Set([...BASE_KEYS, "over", "as", "nodes"]), - parallel: new Set([...BASE_KEYS, "nodes", "mode"]), - http: new Set([...BASE_KEYS, "url", "method", "body", "headers"]), - memory: new Set([...BASE_KEYS, "action", "key", "value"]), - wait: new Set([...BASE_KEYS, "for", "prompt", "preview", "event"]), - sleep: new Set([...BASE_KEYS, "duration"]), - code: new Set([...BASE_KEYS, "run", "input"]), - exec: new Set([...BASE_KEYS, "command", "cwd"]), -}; - /** Validate required fields per node type */ function validateNodeFields(node: FlowNode, errors: ValidationError[]): void { const e = (field: string, msg: string) => @@ -184,7 +166,7 @@ function validateNodeFields(node: FlowNode, errors: ValidationError[]): void { } // Check for unknown keys - const allowed = ALLOWED_KEYS[nodeType]; + const allowed = NODE_KEYS[nodeType]; if (allowed) { for (const key of Object.keys(node)) { if (!allowed.has(key)) { diff --git a/src/index.ts b/src/index.ts index 93868a3..60476f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,7 +32,7 @@ export type { CodeNode, ExecNode, } from "./core/types.js"; -export { parseDuration, MODEL_MAP, DEFAULT_MODEL } from "./core/types.js"; +export { parseDuration, MODEL_MAP, DEFAULT_MODEL, NODE_KEYS } from "./core/types.js"; export { startWebhookServer } from "./core/serve.js"; export type { WebhookServerOpts } from "./core/serve.js"; export type { ServeConfig } from "./core/types.js";