/** * Centralized NODE_REGISTRY — single source of truth for all node metadata. * * When adding a new node type, add ONE entry here. The dashboard derives * all per-node maps (colors, URLs, Redux keys, palette, delete, etc.) * from this registry. No more 18+ touch points. */ import type { NodeType } from "./types.js"; export type NodeGroup = "Sources" | "Flow Control" | "Actions" | "Annotations"; export interface NodeRegistryEntry { /** Canonical type (e.g., "httpAction") */ type: NodeType; /** Canvas ID prefix (e.g., "ha") */ prefix: string; /** Display label (e.g., "HTTP Action") */ label: string; /** Group in palette */ group: NodeGroup; /** Detail page path (e.g., "/dashboard/http-actions") */ detailPath: string; /** API endpoint base (e.g., "/http-actions") */ apiPath: string; /** Redux state key for filtered array (e.g., "httpActions") */ stateKey: string; /** Redux state key for all array (e.g., "allHttpActions") */ allStateKey: string; /** MiniMap hex color */ color: string; /** Whether this node supports test execution */ testable: boolean; /** * Optional per-instance predicate marking a node as "tool-only". * When this returns true, the canvas should hide every connectable * handle so the user cannot draw pipeline edges to/from this node — * it lives instead inside an upstream AI Node's tools[] list. * * Lets new toolkit-style nodes opt-in without per-component hardcoding * in the dashboard. Currently only emailAction(aiEnabled=true) uses * this; trigger or future Slack/Discord toolkits could too. * * Predicate must be pure (no side effects) — called on every render. */ isToolOnly?: (entity: Record) => boolean; /** * The versions this node type ships, when it ships more than one. * * Absent means the type does not version, which is true of every type * today. A node type only declares this the day its provider deprecates * something and a second version has to live alongside the first. * * See `docs/ADR-0002-node-versioning.md` in the api repo. */ versions?: NodeVersionSpec[]; } /** * One version of a node type. * * Deliberately NOT a schema. The api's DTO stays the validator; all this * needs to know is WHICH config fields a version understands, because that * is what answers the only question the UI has to ask: switching from here * to there, what does the user lose? */ export interface NodeVersionSpec { /** The number the user sees. Unique within a type. */ v: number; /** Shown next to the number in the picker, e.g. "Discord API v10". */ label?: string; /** * `current` is what a new node is born on — exactly one per type. * `legacy` still runs but is on its way out. `beta` is opt-in. */ status: "current" | "legacy" | "beta"; /** * The config fields this version understands. * * Only fields the user configures. Identity and topology — name, * inputNodes, outputNodes, isActive, canvas position — never belong * here: they survive a version change untouched, and listing them * would put them at risk of being unset. */ fields: string[]; /** * Entering this version keeps nothing. For the case where "v2" is * really a different integration wearing the same node's name. */ cleanBreak?: boolean; /** Why it was retired, or what it brings. Shown in picker and dialog. */ note?: string; } export declare const NODE_REGISTRY: Record; /** Get registry entry by canonical type */ export declare function getNodeRegistryEntry(type: string): NodeRegistryEntry | undefined; /** type → detail page path */ export declare const NODE_DETAIL_PATHS: Record; /** type → minimap color */ export declare const NODE_COLORS: Record; /** type → [stateKey, allStateKey] */ export declare const NODE_STATE_KEYS: Record; /** prefix → type */ export declare const PREFIX_TO_NODE_TYPE: Record; /** type → prefix */ export declare const NODE_TYPE_TO_PREFIX: Record; /** * The catalog these helpers read. Every one of them takes it as an * optional last argument defaulting to the real registry: consumers never * pass it, and the tests can exercise a versioned type without waiting * for a real one to exist. */ export type VersionCatalog = Record>; /** True when this type ships more than one version. False for every type today. */ export declare function isVersioned(type: string, catalog?: VersionCatalog): boolean; /** * Every declared version, newest first — the order a picker wants. * Empty for a type that does not version. */ export declare function versionsOf(type: string, catalog?: VersionCatalog): NodeVersionSpec[]; /** One version's spec, or undefined when the type or the number is unknown. */ export declare function versionSpec(type: string, v: number, catalog?: VersionCatalog): NodeVersionSpec | undefined; /** * The version a new node of this type is born on. * * Returns 1 for a type that does not version, which is also how an entity * with no `nodeVersion` is read. A node created before its type ever * versioned was calling the original endpoints, so calling it version 1 * is not a convention — it is the truth. */ export declare function currentVersion(type: string, catalog?: VersionCatalog): number; /** * Config fields present in `from` that `to` does not declare — the exact * list a confirmation dialog should name, and the exact list the server * should unset. * * Returns `null` when the question cannot be answered: the type does not * version, or one of the two versions is not declared. `null` is not the * same as "nothing is lost", and callers must not treat it that way — a * dialog should refuse to offer the switch, and the api should reject the * request. Returning `null` rather than `[]` is what stops "I don't know" * from being read as "nothing". * * This lives here rather than in each consumer because the dialog that * ANNOUNCES the loss and the server that PERFORMS it have to give the * same answer. Two implementations would agree right until the day they * did not, and that day the dialog lies. */ export declare function fieldsLostBetween(type: string, from: number, to: number, catalog?: VersionCatalog): string[] | null; /** * The same answer for two specs already in hand — what `fieldsLostBetween` * delegates to once it has resolved both, and what a caller that already * holds them from `versionsOf()` can use without a second lookup. */ export declare function fieldsLost(from: NodeVersionSpec, to: NodeVersionSpec): string[]; /** * Declaration mistakes in the version catalogs, as readable lines. * * A guard rather than a line in a document: what this codebase learned * from fifteen nodes skipping a checklist point everyone had read is that * the written rule prevented nothing and the guard did. Empty means every * catalog is well-formed. */ export declare function versionCatalogErrors(catalog?: VersionCatalog): string[];