@pipelex/mthds-ui 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +38 -4
  2. package/dist/chunk-2N65P32H.js +17 -0
  3. package/dist/chunk-2N65P32H.js.map +1 -0
  4. package/dist/{chunk-UDA2XA23.js → chunk-BFQHQQ3E.js} +95 -189
  5. package/dist/chunk-BFQHQQ3E.js.map +1 -0
  6. package/dist/chunk-HASMGCVK.js +195 -0
  7. package/dist/chunk-HASMGCVK.js.map +1 -0
  8. package/dist/{chunk-HURB2U53.js → chunk-RJVEDP7M.js} +2 -16
  9. package/dist/chunk-RJVEDP7M.js.map +1 -0
  10. package/dist/form/react/RunPanel.css +131 -0
  11. package/dist/form/react/index.d.ts +188 -0
  12. package/dist/form/react/index.js +265 -0
  13. package/dist/form/react/index.js.map +1 -0
  14. package/dist/graph/index.d.ts +2 -2
  15. package/dist/graph/index.js +13 -9
  16. package/dist/graph/react/detail/DetailPanel.css +65 -0
  17. package/dist/graph/react/index.css +46 -0
  18. package/dist/graph/react/index.css.map +1 -1
  19. package/dist/graph/react/index.d.ts +2 -2
  20. package/dist/graph/react/index.js +608 -373
  21. package/dist/graph/react/index.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +13 -9
  24. package/dist/shiki/index.js +8 -2
  25. package/dist/shiki/index.js.map +1 -1
  26. package/dist/standalone/graph-standalone.html +75 -10
  27. package/dist/standalone/graph-viewer.css +65 -0
  28. package/dist/standalone/graph-viewer.js +10 -10
  29. package/dist/static-graph/index.d.ts +4 -2
  30. package/dist/static-graph/index.js +64 -21
  31. package/dist/static-graph/index.js.map +1 -1
  32. package/dist/{types-DM_Ws3oW.d.ts → types-Cv5Elty8.d.ts} +101 -1
  33. package/package.json +12 -1
  34. package/dist/chunk-HURB2U53.js.map +0 -1
  35. package/dist/chunk-UDA2XA23.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/graph/types.ts"],"sourcesContent":["// ─── Pipe type taxonomy ─────────────────────────────────────────────────────\n// Operators perform work; controllers orchestrate other pipes.\n\nexport type PipeOperatorType =\n | \"PipeLLM\"\n | \"PipeExtract\"\n | \"PipeCompose\"\n | \"PipeImgGen\"\n | \"PipeSearch\"\n | \"PipeFunc\"\n | \"PipeStructure\" // turns Text into a structured concept via an LLM\n | \"PipeSignature\"; // contract-only stub — emitted under `--allow-signatures`\n\nexport type PipeControllerType = \"PipeSequence\" | \"PipeParallel\" | \"PipeCondition\" | \"PipeBatch\";\n\nexport type PipeType = PipeOperatorType | PipeControllerType;\n\n// Presence map over the full `PipeType` union — typed as `Record<PipeType, true>`\n// so adding a pipe class to the union without listing it here is a compile\n// error. `KNOWN_PIPE_TYPES` derives its keys from this map, keeping the runtime\n// check the validator performs in sync with the type.\nconst PIPE_TYPE_PRESENCE: Record<PipeType, true> = {\n PipeLLM: true,\n PipeExtract: true,\n PipeCompose: true,\n PipeImgGen: true,\n PipeSearch: true,\n PipeFunc: true,\n PipeStructure: true,\n PipeSignature: true,\n PipeSequence: true,\n PipeParallel: true,\n PipeCondition: true,\n PipeBatch: true,\n};\n\n/** Every pipe class name pipelex emits as `pipe_type`. Used by validateGraphSpec. */\nexport const KNOWN_PIPE_TYPES: ReadonlySet<string> = new Set(Object.keys(PIPE_TYPE_PRESENCE));\n\nexport type PipeStatus = \"succeeded\" | \"failed\" | \"running\" | \"scheduled\" | \"skipped\" | \"canceled\";\n\n// ─── GraphSpec mode contract ────────────────────────────────────────────────\n\nexport const GRAPH_SPEC_MODE = {\n DRY: \"dry\",\n LIVE: \"live\",\n STATIC: \"static\",\n} as const;\n\nexport type GraphSpecMode = (typeof GRAPH_SPEC_MODE)[keyof typeof GRAPH_SPEC_MODE];\n\nexport interface GraphSpecMeta {\n format: \"mthds\";\n /**\n * Missing mode is a legacy runtime graph. Static UI behavior must key only on\n * `mode === \"static\"` and never infer it from absent runtime fields.\n */\n mode?: GraphSpecMode;\n [key: string]: unknown;\n}\n\n// ─── Node type constants ────────────────────────────────────────────────────\n// Used by graphBuilders and consumed by ReactFlow custom node registration.\n\nexport const NODE_TYPE_PIPE_CARD = \"pipeCard\" as const;\nexport const NODE_TYPE_STUFF = \"default\" as const;\nexport const NODE_TYPE_CONTROLLER = \"controllerGroup\" as const;\n\n// ─── Stuff node ID helpers ──────────────────────────────────────────────────\n// Stuff (data) nodes use a \"stuff_<digest>\" convention throughout the graph.\n\nexport const STUFF_ID_PREFIX = \"stuff_\";\n\nexport function stuffNodeId(digest: string): string {\n return STUFF_ID_PREFIX + digest;\n}\n\nexport function isStuffNodeId(id: string): boolean {\n return id.startsWith(STUFF_ID_PREFIX);\n}\n\nexport function stuffDigestFromId(id: string): string {\n return id.slice(STUFF_ID_PREFIX.length);\n}\n\n// ─── GraphSpec types (from pipelex-agent --view output) ─────────────────────\n\nexport interface GraphSpecNodeIoItem {\n name: string;\n digest?: string;\n concept?: string;\n content_type?: string;\n preview?: string;\n size?: number;\n data?: unknown;\n data_text?: string;\n data_html?: string;\n extra?: Record<string, unknown>;\n}\n\nexport interface GraphSpecNodeIo {\n inputs: GraphSpecNodeIoItem[];\n outputs: GraphSpecNodeIoItem[];\n}\n\nexport type NodeKind =\n | \"pipe_call\"\n | \"controller\"\n | \"operator\"\n | \"input\"\n | \"output\"\n | \"artifact\"\n | \"error\";\n\nexport interface GraphSpecNodeTiming {\n started_at: string;\n ended_at: string;\n duration: number;\n}\n\nexport interface GraphSpecNodeError {\n error_type: string;\n message: string;\n stack?: string;\n}\n\n/**\n * Inference usage attributed to one graph node — the mirror of pipelex's\n * `NodeUsageSpec`. Read the invariants before rendering any of these numbers;\n * getting them wrong means showing a wrong dollar figure, which is worse than\n * showing none.\n *\n * 1. `node.usage === undefined` is ALL-OR-NOTHING across a spec: it means the\n * run reported no usage at all (collection off, or zero inference calls).\n * As soon as one call was reported, EVERY node carries a usage object —\n * zeroed for controllers, PipeFunc, PipeCompose, skipped and lifted pipes.\n * So it never means \"this particular node was not measured\".\n * 2. `cost === null` ⟺ `rated_inference_calls === 0`. \"Made no call\" and \"made\n * only unrated calls\" both land here; `inference_calls` tells them apart.\n * Never render `null` as `$0.00` — a dry run is entirely unrated.\n * 3. `inference_calls > rated_inference_calls > 0` means `cost` is a LOWER\n * BOUND, not a total. It must be marked (a leading \"≥\").\n * 4. `total_tokens` is input_joined + output. Do NOT sum\n * `nb_tokens_by_category`: `input_cached` is a subset of `input`, not\n * additive, so summing double-counts.\n *\n * The `subtree_*` half covers the node plus every descendant, rolled up by\n * pipelex so no consumer re-derives it. Controllers have only a subtree.\n */\n/**\n * What one inference model actually did for a node — the graph's only record of\n * the model that RAN.\n *\n * Everything else in a GraphSpec that names a model records what was *asked for*:\n * `execution_data.resolved_model` is the handle the pipe resolved to (often still\n * an alias like `@default-premium`, since aliases resolve at inference time), and\n * the blueprint holds the authored choice (`$writing-factual`). Only this survives\n * alias resolution, deck defaults, and any fallback or retry.\n *\n * `cost` follows the same rule as the node's: `null` iff no call to this model was\n * priced.\n */\nexport interface GraphSpecModelUsage {\n inference_model_name: string;\n inference_model_id: string;\n /**\n * Kind of inference: `\"llm\"`, `\"img_gen\"`, `\"extract\"`, `\"search\"`. Open set.\n *\n * The discriminator to check before displaying token counts: everything except\n * `\"llm\"` is billed PER REQUEST, and pipelex encodes that price by putting\n * `1_000_000` in each token category (rates are per-million), so those \"tokens\"\n * are a scaled request counter rather than a measurement.\n */\n model_type: string;\n inference_calls: number;\n rated_inference_calls: number;\n cost: number | null;\n}\n\nexport interface GraphSpecNodeUsage {\n inference_calls: number;\n rated_inference_calls: number;\n nb_tokens_by_category: Record<string, number>;\n total_tokens: number;\n cost: number | null;\n /** Components of `cost`; `input` is the joined input cost (non-cached + cached). */\n cost_input: number | null;\n cost_output: number | null;\n /**\n * The models that actually ran, most-used first. A list because one node\n * routinely uses more than one — a PipeLLM's text pass and its object pass\n * resolve separately — so `by_model[0]` is the dominant model, not \"the\" model.\n */\n by_model: GraphSpecModelUsage[];\n subtree_inference_calls: number;\n subtree_rated_inference_calls: number;\n subtree_nb_tokens_by_category: Record<string, number>;\n subtree_total_tokens: number;\n subtree_cost: number | null;\n subtree_cost_input: number | null;\n subtree_cost_output: number | null;\n /** Every model this node's whole branch used. */\n subtree_by_model: GraphSpecModelUsage[];\n}\n\n/**\n * Run-level usage for a whole spec — the mirror of pipelex's `GraphUsageSpec`.\n *\n * `total` covers every call the run reported, attributed or not. `unattributed`\n * is the part that named no live node; on a healthy run it is zeroed, and a\n * non-zero one is worth surfacing rather than hiding. Neither has a subtree\n * distinct from itself, so their `subtree_*` fields repeat their own.\n */\nexport interface GraphSpecUsage {\n total: GraphSpecNodeUsage;\n unattributed: GraphSpecNodeUsage;\n}\n\nexport interface GraphSpecNode {\n id: string;\n kind: NodeKind;\n pipe_code?: string;\n pipe_type: PipeType;\n description?: string;\n domain_code?: string;\n status: PipeStatus;\n timing?: GraphSpecNodeTiming;\n io: GraphSpecNodeIo;\n error?: GraphSpecNodeError;\n tags?: Record<string, string>;\n metrics?: Record<string, number>;\n usage?: GraphSpecNodeUsage;\n execution_data?: Record<string, unknown>;\n}\n\n/**\n * A pipe-call node — the only node kind a real pipelex run emits. Narrowed\n * from `GraphSpecNode` for code paths that only ever render pipe-call nodes,\n * where `pipe_code` is guaranteed (validateGraphSpec enforces this).\n */\nexport type PipeCallNode = GraphSpecNode & {\n kind: \"controller\" | \"operator\";\n pipe_code: string;\n pipe_type: PipeType;\n};\n\nexport type GraphSpecEdgeKind =\n | \"contains\"\n | \"data\"\n | \"control\"\n | \"selected_outcome\"\n | \"batch_item\"\n | \"batch_aggregate\"\n | \"parallel_combine\";\n\nexport interface GraphSpecEdge {\n id: string;\n source: string;\n target: string;\n kind: GraphSpecEdgeKind;\n label?: string;\n source_stuff_digest?: string;\n target_stuff_digest?: string;\n meta?: Record<string, unknown>;\n}\n\n// ─── Concept and Pipe registry types ───────────────────────────────────────\n// Serialized from Python Concept and PipeAbstract instances via model_dump().\n\nexport interface ConceptInfo {\n code: string;\n domain_code: string;\n description: string;\n structure_class_name: string;\n refines: string | null;\n json_schema?: Record<string, unknown>;\n}\n\n/**\n * Presence of a value in a declared slot, mirroring the runtime's\n * `PresenceMarker`: `plain` (always present), `optional` (the `?` suffix — the\n * slot may legitimately hold no value), `force` (the `!` suffix — a use-site\n * assertion that the value must be present). The runtime defaults the field to\n * `plain`, so an absent `presence` reads as plain.\n */\nexport type PresenceMarker = \"plain\" | \"optional\" | \"force\";\n\nexport interface StuffSpecInfo {\n concept: ConceptInfo;\n multiplicity: number | boolean | null;\n presence?: PresenceMarker;\n}\n\n// ─── Template blueprint (shared by LLM prompts, Search, Compose, ImgGen) ───\n\nexport interface TemplateBlueprint {\n template: string;\n templating_style: string | null;\n category: string;\n extra_context: Record<string, unknown> | null;\n}\n\n// ─── Sub-pipe (used by Sequence, Parallel, Batch) ──────────────────────\n\nexport interface SubPipeSpec {\n pipe_code: string;\n output_name: string | null;\n output_multiplicity: string | number | boolean | null;\n batch_params: { input_list_stuff_name: string; input_item_stuff_name: string } | null;\n}\n\n// ─── PipeAbstract base (common to all pipe types) ──────────────────────\n\nexport interface PipeBlueprintBase {\n type: PipeType;\n pipe_category: \"PipeOperator\" | \"PipeController\";\n code: string;\n domain_code: string;\n description: string;\n inputs: Record<string, StuffSpecInfo>;\n output: StuffSpecInfo;\n}\n\n// ─── Operator blueprints ───────────────────────────────────────────────\n\nexport interface PipeLLMBlueprint extends PipeBlueprintBase {\n type: \"PipeLLM\";\n llm_prompt_spec: {\n system_prompt_blueprint: TemplateBlueprint | null;\n prompt_blueprint: TemplateBlueprint | null;\n user_image_references: unknown[] | null;\n user_document_references: unknown[] | null;\n system_image_references: unknown[] | null;\n system_document_references: unknown[] | null;\n };\n llm_choices: { for_text: string | null; for_object: string | null } | null;\n structuring_method: string | null;\n output_multiplicity: string | number | null;\n}\n\nexport interface PipeImgGenBlueprint extends PipeBlueprintBase {\n type: \"PipeImgGen\";\n img_gen_prompt_blueprint: {\n prompt_blueprint: TemplateBlueprint | null;\n negative_prompt_blueprint: TemplateBlueprint | null;\n image_references: unknown[] | null;\n };\n img_gen_choice: string | null;\n aspect_ratio: string | null;\n is_raw: boolean | null;\n seed: number | string | null;\n background: string | null;\n output_format: string | null;\n output_multiplicity: number;\n}\n\n/**\n * A single field in a PipeCompose construct blueprint. Mirrors the\n * `ConstructFieldBlueprint` Pydantic model in pipelex. Exactly one of\n * `fixed_value` / `from_path` / `template` / `nested` is populated, matching\n * the `method` discriminator.\n *\n * - `fixed` → `fixed_value` holds a literal (string, number, bool, list)\n * - `from_var` → `from_path` holds a dotted path into working memory,\n * optionally with a `list_to_dict_keyed_by` modifier\n * - `template` → `template` holds a Jinja2 template string (per-field)\n * - `nested` → `nested` holds a recursive construct blueprint for building\n * nested structured content\n */\nexport interface PipeComposeConstructField {\n method: \"from_var\" | \"fixed\" | \"template\" | \"nested\";\n fixed_value?: unknown;\n from_path?: string | null;\n template?: string | null;\n nested?: PipeComposeConstructBlueprint | null;\n list_to_dict_keyed_by?: string | null;\n}\n\n/**\n * A PipeCompose construct blueprint, parsed from `[pipe.X.construct]` in MTHDS.\n * Mirrors the `ConstructBlueprint` Pydantic model in pipelex.\n */\nexport interface PipeComposeConstructBlueprint {\n fields: Record<string, PipeComposeConstructField>;\n}\n\n/**\n * Per-field record of how each field was built at runtime, emitted by\n * `PipeCompose._run_construct_mode` in pipelex via `execution_data.fields`.\n *\n * - `method` → which composition method was used (mirrors `ConstructFieldMethod`)\n * - `rendered` → present only for `template` fields, holds the Jinja2 output\n *\n * Nested fields record only their method; their sub-fields are not surfaced.\n */\nexport interface FieldResolution {\n method: \"from_var\" | \"fixed\" | \"template\" | \"nested\";\n rendered?: string;\n}\n\nexport interface PipeComposeBlueprint extends PipeBlueprintBase {\n type: \"PipeCompose\";\n /** Legacy monolithic template. Null when construct_blueprint is used instead. */\n template: string | null;\n templating_style: string | null;\n category: string;\n extra_context: Record<string, unknown> | null;\n /** Field-level construct form (e.g. `[pipe.X.construct]` in MTHDS). */\n construct_blueprint: PipeComposeConstructBlueprint | null;\n}\n\nexport interface PipeExtractBlueprint extends PipeBlueprintBase {\n type: \"PipeExtract\";\n extract_choice: string | null;\n should_caption_images: boolean;\n max_page_images: number | null;\n should_include_page_views: boolean;\n page_views_dpi: number | null;\n render_js: boolean | null;\n include_raw_html: boolean | null;\n image_stuff_name: string | null;\n document_stuff_name: string | null;\n}\n\nexport interface PipeSearchBlueprint extends PipeBlueprintBase {\n type: \"PipeSearch\";\n search_choice: string | null;\n prompt_blueprint: TemplateBlueprint;\n include_images_override: boolean | null;\n max_results_override: number | null;\n from_date: string | null;\n to_date: string | null;\n include_domains: string[] | null;\n exclude_domains: string[] | null;\n is_structured_output: boolean;\n}\n\nexport interface PipeFuncBlueprint extends PipeBlueprintBase {\n type: \"PipeFunc\";\n}\n\n/**\n * A PipeStructure: an LLM-backed operator that turns a single Text-compatible\n * input into a structured concept. Serialized from the runtime `PipeStructure`,\n * so the registry entry carries the runtime fields below alongside the base.\n */\nexport interface PipeStructureBlueprint extends PipeBlueprintBase {\n type: \"PipeStructure\";\n /**\n * LLM used to structure the text. A string model handle, an inline LLM\n * setting object, or null (→ resolved from the model deck's `for_object`\n * default at run time).\n */\n llm_choice: string | Record<string, unknown> | null;\n /** The single Text-compatible input variable the structuring reads. */\n text_input_name: string;\n /** `true` → let the LLM decide the count, a number → fixed count, null → single object. */\n output_multiplicity: boolean | number | null;\n}\n\n/**\n * A contract-only pipe: declares inputs + output but has no implementation.\n * Emitted under `--allow-signatures` so an in-progress bundle still validates\n * (dry-run mocks the declared output) before every referenced pipe is built.\n */\nexport interface PipeSignatureBlueprint extends Omit<PipeBlueprintBase, \"pipe_category\"> {\n type: \"PipeSignature\";\n /**\n * Signatures sit outside the executable taxonomy, so pipelex serializes\n * `pipe_category: null` (present, not omitted) — unlike operator/controller\n * blueprints which carry \"PipeOperator\" / \"PipeController\".\n */\n pipe_category: null;\n /** Intended downstream pipe type once implemented — an optional hint. */\n signature_for?: PipeType | null;\n}\n\n// ─── Controller blueprints ─────────────────────────────────────────────\n\nexport interface PipeSequenceBlueprint extends PipeBlueprintBase {\n type: \"PipeSequence\";\n sequential_sub_pipes: SubPipeSpec[];\n}\n\nexport interface PipeParallelBlueprint extends PipeBlueprintBase {\n type: \"PipeParallel\";\n parallel_sub_pipes: SubPipeSpec[];\n add_each_output: boolean;\n /**\n * Removed from the language in pipelex 0.41 (a parallel always combines now),\n * so 0.41 registry dumps omit the key entirely. Optional rather than deleted\n * because the static builder still honors it as a legacy authoring key when\n * naming the combined stuff — see `wip/pr-63-review-notes.md`.\n */\n combined_output?: string | null;\n}\n\nexport interface PipeConditionBlueprint extends PipeBlueprintBase {\n type: \"PipeCondition\";\n expression: string;\n outcome_map: Record<string, string>;\n default_outcome: string;\n add_alias_from_expression_to: string | null;\n}\n\nexport interface PipeBatchBlueprint extends PipeBlueprintBase {\n type: \"PipeBatch\";\n branch_pipe_code: string;\n batch_params: { input_list_stuff_name: string; input_item_stuff_name: string };\n}\n\nexport type PipeBlueprintUnion =\n | PipeLLMBlueprint\n | PipeImgGenBlueprint\n | PipeComposeBlueprint\n | PipeExtractBlueprint\n | PipeSearchBlueprint\n | PipeFuncBlueprint\n | PipeStructureBlueprint\n | PipeSignatureBlueprint\n | PipeSequenceBlueprint\n | PipeParallelBlueprint\n | PipeConditionBlueprint\n | PipeBatchBlueprint;\n\n// ─── GraphSpec top-level ───────────────────────────────────────────────────\n\nexport interface GraphSpec {\n graph_id?: string;\n created_at?: string;\n pipeline_ref?: { domain?: string; main_pipe?: string; entrypoint?: string };\n nodes: GraphSpecNode[];\n edges: GraphSpecEdge[];\n usage?: GraphSpecUsage;\n meta?: GraphSpecMeta;\n pipe_registry?: Record<string, PipeBlueprintUnion>;\n concept_registry?: Record<string, ConceptInfo>;\n}\n\nexport function graphSpecMode(\n spec: Pick<GraphSpec, \"meta\"> | null | undefined,\n): GraphSpecMode | undefined {\n return spec?.meta?.mode;\n}\n\nexport function isStaticGraphSpec(spec: Pick<GraphSpec, \"meta\"> | null | undefined): boolean {\n return graphSpecMode(spec) === GRAPH_SPEC_MODE.STATIC;\n}\n\nexport function isDryGraphSpec(spec: Pick<GraphSpec, \"meta\"> | null | undefined): boolean {\n return graphSpecMode(spec) === GRAPH_SPEC_MODE.DRY;\n}\n\n// ─── Dataflow analysis result ───────────────────────────────────────────────\n\nexport interface DataflowAnalysis {\n readonly stuffRegistry: Readonly<\n Record<string, { name: string; concept?: string; contentType?: string }>\n >;\n readonly stuffProducers: Readonly<Record<string, string>>;\n readonly stuffConsumers: Readonly<Record<string, readonly string[]>>;\n readonly controllerNodeIds: ReadonlySet<string>;\n readonly childNodeIds: ReadonlySet<string>;\n readonly containmentTree: Readonly<Record<string, readonly string[]>>;\n}\n\n// ─── Graph configuration ────────────────────────────────────────────────────\n\nexport const GRAPH_DIRECTION = {\n TB: \"TB\",\n BT: \"BT\",\n LR: \"LR\",\n RL: \"RL\",\n} as const;\n\nexport type GraphDirection = (typeof GRAPH_DIRECTION)[keyof typeof GRAPH_DIRECTION];\n\nexport const EDGE_TYPE = {\n /** Bezier curve — ReactFlow v12 renamed this type from \"bezier\" to \"default\". */\n DEFAULT: \"default\",\n STEP: \"step\",\n STRAIGHT: \"straight\",\n SMOOTH_STEP: \"smoothstep\",\n} as const;\n\nexport type EdgeType = (typeof EDGE_TYPE)[keyof typeof EDGE_TYPE];\n\nexport const FOLD_MODE = {\n /** Every pipe controller folded into a single pipe card. */\n FOLDED: \"folded\",\n /** Every pipe controller expanded as a group wrapper. */\n EXPANDED: \"expanded\",\n /** Renderer decides — reserved for future heuristics; currently behaves like EXPANDED. */\n AUTO: \"auto\",\n} as const;\n\nexport type FoldMode = (typeof FOLD_MODE)[keyof typeof FOLD_MODE];\n\n/**\n * The *resolved* binary theme — the value that actually drives the palette and\n * the container class. `getPaletteForTheme` takes this. `system` is never a\n * resolved theme; it resolves to one of these via the environment.\n */\nexport const GRAPH_THEME = {\n DARK: \"dark\",\n LIGHT: \"light\",\n} as const;\n\nexport type GraphTheme = (typeof GRAPH_THEME)[keyof typeof GRAPH_THEME];\n\n/**\n * The user's theme *selection* — what the toolbar cycles, what gets persisted\n * and reported. `system` follows the host environment (browser `prefers-color-scheme`\n * or an injected `systemTheme`) and resolves to a binary `GraphTheme` at render\n * time. The `dark`/`light` overlap with `GraphTheme` is intentional: a resolved\n * theme is also a valid mode.\n */\nexport const GRAPH_THEME_MODE = {\n DARK: \"dark\",\n LIGHT: \"light\",\n SYSTEM: \"system\",\n} as const;\n\nexport type GraphThemeMode = (typeof GRAPH_THEME_MODE)[keyof typeof GRAPH_THEME_MODE];\n\n/**\n * Anchor for the built-in floating toolbar. The values match ReactFlow's\n * `PanelPosition` union exactly, so they pass straight to `<Panel position=…>`\n * (the React layer asserts that subset relationship at compile time). Orientation\n * is *derived* from the position, never configured independently — see\n * `toolbarOrientation`: only the two edge-center anchors are vertical, everything\n * else (corners + top/bottom-center) is a horizontal bar.\n *\n * `types.ts` stays React-free, so `PanelPosition` is intentionally NOT imported\n * here — the compatibility assertion lives in `GraphToolbar.tsx`.\n */\nexport const TOOLBAR_POSITION = {\n TOP_LEFT: \"top-left\",\n TOP_CENTER: \"top-center\",\n TOP_RIGHT: \"top-right\",\n BOTTOM_LEFT: \"bottom-left\",\n BOTTOM_CENTER: \"bottom-center\",\n BOTTOM_RIGHT: \"bottom-right\",\n CENTER_LEFT: \"center-left\",\n CENTER_RIGHT: \"center-right\",\n} as const;\n\nexport type ToolbarPosition = (typeof TOOLBAR_POSITION)[keyof typeof TOOLBAR_POSITION];\n\nexport type ToolbarOrientation = \"horizontal\" | \"vertical\";\n\n/**\n * Derive the bar orientation from its anchor. \"Corners are horizontal, edges\n * decide the rest\": only `center-left` / `center-right` produce a vertical bar;\n * every other position (the four corners plus `top-center` / `bottom-center`) is\n * horizontal. Pure + React-free so it is unit-testable without rendering.\n */\nexport function toolbarOrientation(position: ToolbarPosition): ToolbarOrientation {\n return position === TOOLBAR_POSITION.CENTER_LEFT || position === TOOLBAR_POSITION.CENTER_RIGHT\n ? \"vertical\"\n : \"horizontal\";\n}\n\nexport type ToolbarSide = \"left\" | \"center\" | \"right\";\n\n/**\n * Derive which edge the anchor hugs. The built-in `DetailPanel` overlays the\n * right edge, so only right-side anchors (`*-right`) need to dodge it — the\n * toolbar reads this to decide whether to shift left while the panel is open.\n * Pure + React-free so it is unit-testable without rendering, and exhaustively\n * switched so adding a `TOOLBAR_POSITION` value without a side fails to compile\n * — exactly like {@link toolbarOrientation}.\n */\nexport function toolbarSide(position: ToolbarPosition): ToolbarSide {\n switch (position) {\n case TOOLBAR_POSITION.TOP_LEFT:\n case TOOLBAR_POSITION.CENTER_LEFT:\n case TOOLBAR_POSITION.BOTTOM_LEFT:\n return \"left\";\n case TOOLBAR_POSITION.TOP_RIGHT:\n case TOOLBAR_POSITION.CENTER_RIGHT:\n case TOOLBAR_POSITION.BOTTOM_RIGHT:\n return \"right\";\n case TOOLBAR_POSITION.TOP_CENTER:\n case TOOLBAR_POSITION.BOTTOM_CENTER:\n return \"center\";\n default: {\n const _exhaustive: never = position;\n return _exhaustive;\n }\n }\n}\n\n// ─── Validation status (toolbar widget) ──────────────────────────────────────\n\n/**\n * State of the toolbar's validation widget. The widget itself is opt-in: it\n * renders only when the host passes a `validationState` to `GraphViewer` —\n * `undefined` (the default) keeps it hidden entirely. The states describe the\n * host's validation lifecycle, not the static analyzer's: `validating` while a\n * verdict is being produced, `valid`/`invalid` once one exists, and `error`\n * when no verdict could be produced at all (validator unavailable, timeout…).\n */\nexport const VALIDATION_STATE = {\n VALIDATING: \"validating\",\n VALID: \"valid\",\n INVALID: \"invalid\",\n ERROR: \"error\",\n} as const;\n\nexport type ValidationState = (typeof VALIDATION_STATE)[keyof typeof VALIDATION_STATE];\n\n/**\n * One issue row in the validation panel. Mostly presentation: the host decides\n * which issues to show for each state (its validator's errors, the static\n * analyzer's diagnostics, or a mix) and handles navigation on row click. The\n * only fields the viewer interprets are the optional targeting fields\n * (`pipeRef` / `nodeId`), which drive node decorations on the graph.\n */\nexport interface ValidationIssue {\n severity: \"error\" | \"warning\";\n message: string;\n /** Short locator chip, e.g. `pipe.analyze_candidate` or a TOML path. */\n context?: string;\n /** Owning-file basename, when the issue lives in a specific file. */\n file?: string;\n /** Human-readable suggested fix, when the validator derived one. */\n suggestedFix?: string;\n /** Which analyzer produced the issue: the host's validator or the static parser. */\n origin?: \"validator\" | \"static\";\n /**\n * Fully-qualified pipe ref (`domain_code.pipe_code`, see `makePipeRef`) this\n * issue targets — decorates every rendered node invoking that pipe (a pipe\n * can be invoked from several places in the graph). Always qualified, never a\n * bare code: two domains may declare the same pipe code, and a bare match\n * would ring every same-code node. An emitter that cannot qualify must leave\n * the issue untargeted (panel-only) rather than decorate by guess.\n */\n pipeRef?: string;\n /**\n * Precise invocation this issue targets — a GraphSpec node id (e.g.\n * `demo.main_flow/step_2`). Takes precedence over `pipeRef` when both are\n * set. Issues with neither targeting field stay panel-only.\n */\n nodeId?: string;\n}\n\n/**\n * Aggregated validation decoration for one rendered node — derived from the\n * targeted `ValidationIssue`s by `buildValidationDecorations` (worst severity\n * wins, folded controllers roll up their hidden descendants' issues). Rendered\n * as a severity ring + count badge on the node, with `lines` as the tooltip.\n */\nexport interface NodeValidationSummary {\n /** Worst severity among the node's issues (`error` wins over `warning`). */\n severity: \"error\" | \"warning\";\n /** Number of issue hits on the node (a multi-instance pipe counts per invocation). */\n count: number;\n /** Tooltip lines: each issue's message, followed by a `Fix: …` line when present. */\n lines: string[];\n}\n\nexport interface GraphConfig {\n direction?: GraphDirection;\n showControllers?: boolean;\n foldMode?: FoldMode;\n /**\n * Theme *mode*: `dark | light | system`. `system` follows the host environment.\n * Defaults to `system` (see `DEFAULT_GRAPH_CONFIG`). Pass `dark`/`light` to pin\n * a fixed appearance.\n */\n theme?: GraphThemeMode;\n /**\n * Anchor for the built-in floating toolbar (default `top-right`). Orientation\n * is derived from the position — see {@link toolbarOrientation}. The\n * `toolbarPosition` *prop* on `GraphViewer` takes precedence over this; both\n * are reactive and persistence is the host's responsibility.\n */\n toolbarPosition?: ToolbarPosition;\n nodesep?: number;\n ranksep?: number;\n edgeType?: EdgeType;\n initialZoom?: number | null;\n panToTop?: boolean;\n paletteColors?: Record<string, string>;\n}\n\n// ─── Label descriptors ──────────────────────────────────────────────────────\n// Plain objects, no React dependency. GraphViewer maps these to React elements.\n\nexport type LabelDescriptor =\n | { kind: \"pipe\"; label: string; isFailed: boolean }\n | { kind: \"stuff\"; label: string; concept: string };\n\n// ─── Fold toggle options ────────────────────────────────────────────────────\n// Passed by UI click handlers so the orchestrator can decide whether the\n// toggle should propagate to \"cousin\" controllers (other instances of the\n// same pipe) or affect only the clicked one.\n\nexport interface FoldToggleOptions {\n /**\n * When `true`, the toggle applies only to the clicked controller — its\n * cousins (other controller nodes sharing the same `pipe_code`) are left\n * untouched. Wired to the alt/option modifier key in the click handlers.\n */\n soloMode?: boolean;\n}\n\n// ─── Pipe card payload ──────────────────────────────────────────────────────\n// Built by graphBuilders, consumed by PipeCardNode in the React layer.\n\nexport interface PipeCardPayload {\n pipeCode: string;\n pipeType: PipeType;\n description?: string;\n status: PipeStatus;\n graphMode?: GraphSpecMode;\n inputs: { name: string; concept: string }[];\n outputs: { name: string; concept: string }[];\n /** Authored/static annotations and runtime tags carried by the GraphSpec node. */\n tags?: Record<string, string>;\n /** Layout direction — injected by the layout engine */\n direction?: \"LR\" | \"TB\";\n /** When set, the card renders an unfold button that invokes this callback. */\n onExpand?: (options?: FoldToggleOptions) => void;\n /** Validation decoration (severity ring + count badge), stamped by GraphViewer. */\n validation?: NodeValidationSummary;\n /** Badge click handler (opens the validation panel), stamped alongside `validation`. */\n onValidationBadgeClick?: () => void;\n}\n\n// ─── Graph node data ────────────────────────────────────────────────────────\n// Extends Record<string, unknown> for ReactFlow's Node<T> generic parameter.\n\nexport type StuffRole = \"input\" | \"output\" | \"combined\";\n\nexport interface GraphNodeData extends Record<string, unknown> {\n labelDescriptor?: LabelDescriptor;\n label?: unknown;\n nodeData?: GraphSpecNode;\n isPipe: boolean;\n isStuff: boolean;\n isController?: boolean;\n labelText: string;\n pipeCode?: string;\n pipeType?: PipeType;\n graphMode?: GraphSpecMode;\n pipeCardData?: PipeCardPayload;\n /** For stuff nodes: \"input\" (no producer), \"output\" (no consumer), or undefined (intermediate). */\n stuffRole?: StuffRole;\n /** For stuff nodes: the digest used to build the node ID. */\n stuffDigest?: string;\n /** Validation decoration (severity ring + count badge), stamped by GraphViewer. */\n validation?: NodeValidationSummary;\n /** Badge click handler (opens the validation panel), stamped alongside `validation`. */\n onValidationBadgeClick?: () => void;\n}\n\n// ─── Graph node / edge / data ───────────────────────────────────────────────\n\nexport interface GraphNode {\n id: string;\n type: string;\n data: GraphNodeData;\n position: { x: number; y: number };\n style?: Record<string, string | number>;\n sourcePosition?: \"top\" | \"bottom\" | \"left\" | \"right\";\n targetPosition?: \"top\" | \"bottom\" | \"left\" | \"right\";\n parentId?: string;\n extent?: \"parent\";\n selected?: boolean;\n}\n\nexport interface GraphEdge {\n id: string;\n source: string;\n target: string;\n type: string;\n animated?: boolean;\n label?: string;\n labelStyle?: Record<string, string | number>;\n labelBgStyle?: Record<string, string | number>;\n labelBgPadding?: [number, number];\n labelBgBorderRadius?: number;\n style?: Record<string, string | number>;\n markerEnd?: { type: string; color: string };\n _batchEdge?: boolean;\n _crossGroup?: boolean;\n}\n\nexport interface GraphData {\n nodes: GraphNode[];\n edges: GraphEdge[];\n}\n\n// ─── Layout ─────────────────────────────────────────────────────────────────\n\nexport interface LayoutConfig {\n nodesep?: number;\n ranksep?: number;\n}\n\n// Controller padding constants (shared between layout and controller modules)\nexport const CONTROLLER_PADDING_X = 40;\nexport const CONTROLLER_PADDING_TOP = 48;\nexport const CONTROLLER_PADDING_BOTTOM = 20;\n\n// Default marker type string (avoids ReactFlow dependency in pure modules)\nexport const ARROW_CLOSED_MARKER = \"arrowclosed\";\n\n// ─── Node dimension helpers ─────────────────────────────────────────────────\n// Extract dimensions from style. Used by buildControllerNodes.\n// NOT used by getLayoutedElements, which estimates dimensions before styles exist.\n\nexport function nodeWidth(n: GraphNode): number {\n const raw = n.style?.width;\n if (raw == null) return 200;\n const w = typeof raw === \"number\" ? raw : parseFloat(raw);\n return isNaN(w) || w <= 0 ? 200 : w;\n}\n\nexport function nodeHeight(n: GraphNode): number {\n const raw = n.style?.height;\n if (raw != null) {\n const h = typeof raw === \"number\" ? raw : parseFloat(raw);\n if (!isNaN(h) && h > 0) return h;\n }\n return n.data?.isStuff ? 60 : 70;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,131 @@
1
+ /* ─── Run panel — chrome around the form kernel's controls ───────────────── */
2
+ /* Token-only: every colour comes from this library's palette, applied to the */
3
+ /* container as CSS variables by RunPanel itself (see `graphConfig.ts`). */
4
+ /* */
5
+ /* This file styles the PANEL and nothing inside a field. The controls are */
6
+ /* the kernel's, styled with Tailwind classes over shadcn semantic tokens, */
7
+ /* and bringing those in is the HOST's lane — either a Tailwind build that */
8
+ /* scans the kernel's dist, or its prebuilt `theme.css` + `styles.css`. See */
9
+ /* `docs/run-form-panel.md`; this stylesheet deliberately imports neither, so */
10
+ /* no host inherits Tailwind's preflight from us. */
11
+
12
+ .mthds-run-panel {
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: 18px;
16
+ padding: 18px 20px 20px;
17
+ background: var(--surface-panel);
18
+ border: 1px solid var(--border-default);
19
+ border-radius: 10px;
20
+ font-family: var(--font-sans);
21
+ font-size: 13px;
22
+ color: var(--text-default);
23
+ }
24
+
25
+ .mthds-run-panel-title {
26
+ margin: 0;
27
+ font-size: 14px;
28
+ font-weight: 600;
29
+ letter-spacing: -0.01em;
30
+ color: var(--text-primary);
31
+ }
32
+
33
+ .mthds-run-panel-fields {
34
+ display: flex;
35
+ flex-direction: column;
36
+ gap: 20px;
37
+ }
38
+
39
+ .mthds-run-panel-empty {
40
+ margin: 0;
41
+ color: var(--text-muted);
42
+ }
43
+
44
+ .mthds-run-panel-error {
45
+ margin: 0;
46
+ padding: 10px 12px;
47
+ border: 1px solid var(--color-error-border);
48
+ border-radius: 6px;
49
+ background: var(--color-error-bg);
50
+ color: var(--text-default);
51
+ line-height: 1.5;
52
+ }
53
+
54
+ /* ─── Footer — the commit action and what still stands in its way ────────── */
55
+
56
+ .mthds-run-panel-footer {
57
+ display: flex;
58
+ flex-wrap: wrap;
59
+ align-items: center;
60
+ gap: 10px 14px;
61
+ padding-top: 4px;
62
+ border-top: 1px solid var(--border-subtle);
63
+ }
64
+
65
+ .mthds-run-panel-run {
66
+ padding: 7px 18px;
67
+ border: 1px solid transparent;
68
+ border-radius: 6px;
69
+ /* Both colours below are deliberately literal, where the rest of this file
70
+ reads the palette through `var()`. That is a trade, and it was made
71
+ knowingly.
72
+
73
+ A 13px/600 label needs 4.5:1 to clear WCAG AA, and white on the palette's
74
+ `--color-accent-strong` does not manage it in EITHER theme: 4.10:1 on the
75
+ light `#0284c7`, 3.68:1 on the dark `#3b82f6`. Reaching for
76
+ `--text-on-accent` does not repair that. It is calibrated against
77
+ `--color-accent`, a different colour, and resolves to `#ffffff` in the
78
+ light palette — so adopting it would flip the dark label to near-black
79
+ while leaving the panel's DEFAULT theme exactly as unreadable, which is
80
+ the half of the defect most hosts actually see.
81
+
82
+ So the background moves per theme instead and the label stays white:
83
+ `#0369a1` gives 5.93:1 here, `#1d4ed8` gives 6.70:1 in the `.dark` rule
84
+ below. What that costs is real — a host overriding
85
+ `--color-accent-strong` no longer re-themes this button along with the
86
+ graph. It is accepted because this is the primary call to action, and
87
+ because the button is the only place in the repo that puts text on that
88
+ token, so nothing else shifts as a consequence.
89
+
90
+ `src/form/__tests__/contrast.test.ts` reads these two literals back and
91
+ holds them to the AA line — including the `var()` they must not become. And
92
+ `wip/adopt-form/deferred-run-button-contrast.md` records the alternatives
93
+ that were weighed. */
94
+ background: #0369a1;
95
+ color: #ffffff;
96
+ font-family: inherit;
97
+ font-size: 13px;
98
+ font-weight: 600;
99
+ cursor: pointer;
100
+ transition:
101
+ opacity 0.15s,
102
+ filter 0.15s;
103
+ }
104
+
105
+ /* `RunPanel` puts `dark` on the panel alongside the inline palette variables,
106
+ so each theme gets its own accessible background without the palette itself
107
+ moving — `--color-accent-strong` is a graph token, and darkening it there
108
+ would restyle the graph to fix a button. */
109
+ .mthds-run-panel.dark .mthds-run-panel-run {
110
+ background: #1d4ed8;
111
+ }
112
+
113
+ .mthds-run-panel-run:hover:not(:disabled) {
114
+ filter: brightness(1.08);
115
+ }
116
+
117
+ .mthds-run-panel-run:focus-visible {
118
+ outline: 2px solid var(--focus-ring);
119
+ outline-offset: 2px;
120
+ }
121
+
122
+ .mthds-run-panel-run:disabled {
123
+ opacity: 0.45;
124
+ cursor: not-allowed;
125
+ }
126
+
127
+ .mthds-run-panel-readiness {
128
+ color: var(--text-muted);
129
+ font-size: 12px;
130
+ line-height: 1.4;
131
+ }
@@ -0,0 +1,188 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ValidationMessageKey, PipeIOContract, RunField, RunInputError } from '@pipelex/mthds-form';
3
+ import { FieldEnv } from '@pipelex/mthds-form/react';
4
+ import { G as GraphTheme } from '../../types-Cv5Elty8.js';
5
+
6
+ /**
7
+ * The panel's submit path, kept React-free.
8
+ *
9
+ * Everything between "the user pressed Run" and "the payload goes on the wire"
10
+ * is the kernel's (`@pipelex/mthds-form`, `core/gate.ts`); this module bridges
11
+ * the panel's values into the shape the gate takes, calls it, and turns the
12
+ * verdict into one line a panel can show. Splitting it out of `RunPanel.tsx` is
13
+ * what lets the node vitest project test that seam without mounting React — the
14
+ * gate's own behavior is covered by the kernel's tests and is deliberately not
15
+ * re-tested here.
16
+ */
17
+
18
+ /** The outcome of one submit attempt. */
19
+ type RunGateOutcome = {
20
+ ok: true;
21
+ apiInputs: Record<string, unknown>;
22
+ } | {
23
+ ok: false;
24
+ missingInputs: string[];
25
+ summary: string;
26
+ };
27
+ /**
28
+ * Every message key the error summary can render — the kernel's, plus the two
29
+ * this panel owns.
30
+ *
31
+ * The kernel's keys describe one ajv error each and are rendered by
32
+ * `describeValidationError`. The `runPanel.` ones are lines the panel writes
33
+ * itself, around those errors, and they carry the prefix precisely so the
34
+ * ownership is legible at the call site and in a host's translation file.
35
+ *
36
+ * Nesting the kernel's union inside ours is deliberate: a key the kernel ADDS
37
+ * lands here automatically and then fails `VALIDATION_STRINGS`'s exhaustiveness
38
+ * until it is given a string, which is how a kernel bump is stopped from
39
+ * silently rendering `undefined` at a user.
40
+ */
41
+ type RunPanelMessageKey = ValidationMessageKey | "runPanel.missingInputs" | "runPanel.fillRequired";
42
+ /**
43
+ * The panel's i18n seam — the kernel's `Translate` widened to the panel's own
44
+ * keys. It accepts a superset of what the kernel asks for, so it is still a
45
+ * valid `Translate` wherever the kernel wants one.
46
+ */
47
+ type RunPanelTranslate = (key: RunPanelMessageKey, values?: Record<string, string>) => string;
48
+ /**
49
+ * The default translator: the English strings above with `{placeholder}`
50
+ * interpolation. A host that localizes passes its own translator instead.
51
+ */
52
+ declare const defaultValidationTranslate: RunPanelTranslate;
53
+ /**
54
+ * One human line for an invalid verdict.
55
+ *
56
+ * `missingInputs` is the good case: it names the VARIABLES at fault, which is
57
+ * what the user clicks on. The scan can legitimately come up empty on an
58
+ * invalid form (a wrong value shape, a nested mismatch), and that is exactly
59
+ * when falling back to the raw ajv errors keeps the failure diagnosable rather
60
+ * than silent — see `validateRunInputs` in the kernel.
61
+ *
62
+ * All three routes go through `t`. Which one runs is decided by the kernel's
63
+ * verdict, so a host that localizes cannot know in advance which line it will
64
+ * be shown — leaving any of them in hardcoded English would surface as the
65
+ * summary silently reverting to English on some inputs and not others.
66
+ */
67
+ declare function summarizeVerdict(missingInputs: string[], errors: RunInputError[], preparedData: unknown, t?: RunPanelTranslate): string;
68
+ /**
69
+ * Run the kernel's gate over the panel's current values.
70
+ *
71
+ * `gateRunInputs` IS the gate — the whole chain in one call — so all this
72
+ * module does either side of it is bridge and phrase: the panel holds values
73
+ * keyed by variable, the gate takes the schema-shaped data those become, and a
74
+ * rejected verdict has to come back as one line rather than as a discriminated
75
+ * union the panel would have to read.
76
+ *
77
+ * Assembling those steps here instead was the earlier shape and it was wrong in
78
+ * a way nothing local could see. The chain's schema validation is not the whole
79
+ * rule: ajv's `required` only asserts a key is PRESENT, and a content model
80
+ * carries no `minLength`, so a required input that arrived empty — `{text: ""}`
81
+ * from an untouched text field, `{url: ""}` from an untouched file field —
82
+ * satisfied the schema and the run went out. The Run button refuses exactly
83
+ * those, through `computeReadiness`. The gate now re-applies that same
84
+ * emptiness rule with `computeReadiness`'s own functions over the same derived
85
+ * fields, which makes the button's verdict and this one a single invariant
86
+ * rather than two things that resemble each other.
87
+ */
88
+ declare function runSubmitGate(contract: PipeIOContract, fields: RunField[], values: Record<string, unknown>, t?: RunPanelTranslate): RunGateOutcome;
89
+
90
+ /** What a host's upload returns: the stored URL, and optionally a display name. */
91
+ interface UploadedFile {
92
+ url: string;
93
+ filename?: string;
94
+ }
95
+ interface RunPanelProps {
96
+ /**
97
+ * The pipe's IO contract — the kernel's type, supplied by the host.
98
+ *
99
+ * Hosts that can actually run a method already hold `pipe_io_contracts` from
100
+ * the same `/validate` call that produced their graph spec; look an entry up
101
+ * with the kernel's `getPipeIOContract(contracts, domain, pipeCode)`. The
102
+ * panel deliberately does NOT accept a `GraphSpec` and derive a contract from
103
+ * it — deriving field meaning locally is precisely what this component exists
104
+ * not to do (design Decision A).
105
+ */
106
+ contract: PipeIOContract;
107
+ /** Fully controlled field values, host-owned — the kernel's philosophy. */
108
+ values: Record<string, unknown>;
109
+ onValuesChange: (values: Record<string, unknown>) => void;
110
+ /** Fires only after the kernel's run gate passes, with the wire-ready payload. */
111
+ onRun: (apiInputs: Record<string, unknown>) => void;
112
+ /**
113
+ * A run is in flight: the fields and the Run button go inert.
114
+ *
115
+ * **Set it synchronously inside `onRun`, before any `await`.** Across anything
116
+ * longer than one task it is the only thing standing between a second submit
117
+ * and a duplicate execution, and the panel cannot supply that itself: it never
118
+ * learns that a run finished, so a lock held for a run's lifetime would have
119
+ * no release. A host that flips this only once the server answers leaves the
120
+ * button live for the whole round trip, and a second click there starts a
121
+ * second run that nothing undoes.
122
+ *
123
+ * Synchronous re-entry within a single task is the panel's own problem and is
124
+ * handled — see `justSubmittedRef` beside `handleSubmit`. That guard does not
125
+ * reduce what this prop is for; it closes a window this prop cannot reach,
126
+ * because React has not re-rendered inside it.
127
+ */
128
+ running?: boolean;
129
+ /**
130
+ * Ambient field state, passed to every control.
131
+ *
132
+ * For `disabled`, `onDropFile` and `resolveUrl` the host's value wins PER
133
+ * KEY: the panel fills in only what the host left undefined — `disabled`
134
+ * from `running`, `onDropFile` from its own upload tracking when
135
+ * `uploadFile` is supplied.
136
+ *
137
+ * `uploadingIds` is the exception, and deliberately so: it is the UNION of
138
+ * your set and the panel's, never a replacement. Because the keys default
139
+ * independently, a host can supply `uploadFile` — leaving the panel to own
140
+ * the drop and mark the field in its own set — while also passing a tracker
141
+ * of its own; letting yours win outright there would hide an upload the
142
+ * panel itself started and leave Run live through it. A host that owns the
143
+ * whole loop supplies `onDropFile` too, so the panel's set is empty and the
144
+ * union is exactly your set.
145
+ */
146
+ env?: FieldEnv;
147
+ /**
148
+ * Stores a dropped file and returns its URL. The host performs the upload —
149
+ * this library never touches an API client — while the panel owns the
150
+ * bookkeeping around it: marking the field busy and writing the result back
151
+ * at the field's dotted path.
152
+ */
153
+ uploadFile?: (file: File, fieldId: string) => Promise<UploadedFile>;
154
+ /** Panel header. The host names the pipe; there is no default heading. */
155
+ title?: string;
156
+ /**
157
+ * The resolved theme, which drives the kernel's `.dark` class on the panel
158
+ * container alongside this library's own palette variables. Defaults to
159
+ * `light`. Pair it with `GraphViewer`'s `onThemeChange`, whose second
160
+ * argument is exactly this resolved value.
161
+ */
162
+ theme?: GraphTheme;
163
+ /**
164
+ * Renders the whole error summary in the host's language. English by default.
165
+ *
166
+ * It covers the panel's own lines (`runPanel.*`) as well as the kernel's
167
+ * per-error ones, because which of them a rejected run produces is the
168
+ * kernel's decision and not something a host can predict.
169
+ */
170
+ translate?: RunPanelTranslate;
171
+ /** Appended to the panel container's class list. */
172
+ className?: string;
173
+ }
174
+ /**
175
+ * A pipe's input form: fields, readiness, and the run gate, composed.
176
+ *
177
+ * Every question of meaning is the form kernel's — `fieldsForContract` decides
178
+ * what each input IS, `computeReadiness` decides whether the form MAY run, and
179
+ * `runSubmitGate` (over the kernel's four steps) decides what goes on the wire.
180
+ * What lives here is layout: which fields are visible, where the toggle and the
181
+ * button sit, and how a rejected verdict is shown. The panel never reads
182
+ * `json_schema` to make a rendering decision and never sniffs a value's shape
183
+ * (design Decision C) — which is what keeps a change to the kernel's derivation
184
+ * invisible to this repo.
185
+ */
186
+ declare function RunPanel({ contract, values, onValuesChange, onRun, running, env, uploadFile, title, theme, translate, className, }: RunPanelProps): react_jsx_runtime.JSX.Element;
187
+
188
+ export { type RunGateOutcome, RunPanel, type RunPanelMessageKey, type RunPanelProps, type RunPanelTranslate, type UploadedFile, defaultValidationTranslate, runSubmitGate, summarizeVerdict };