@pipelex/mthds-ui 0.24.0 → 0.26.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 +42 -11
  2. package/dist/{chunk-FD4PKRGZ.js → chunk-K3BWHQVX.js} +38 -20
  3. package/dist/chunk-K3BWHQVX.js.map +1 -0
  4. package/dist/{chunk-RJVEDP7M.js → chunk-VCO4GZDZ.js} +12 -2
  5. package/dist/chunk-VCO4GZDZ.js.map +1 -0
  6. package/dist/{chunk-HASMGCVK.js → chunk-XBHDA3PQ.js} +2 -2
  7. package/dist/form/react/RunPanel.css +5 -5
  8. package/dist/form/react/index.d.ts +1 -1
  9. package/dist/form/react/index.js +2 -3
  10. package/dist/form/react/index.js.map +1 -1
  11. package/dist/graph/index.d.ts +6 -4
  12. package/dist/graph/index.js +7 -3
  13. package/dist/graph/react/detail/DetailPanel.css +5 -0
  14. package/dist/graph/react/graph-core.css +4 -1
  15. package/dist/graph/react/index.css +6 -1
  16. package/dist/graph/react/index.css.map +1 -1
  17. package/dist/graph/react/index.d.ts +11 -14
  18. package/dist/graph/react/index.js +35 -8
  19. package/dist/graph/react/index.js.map +1 -1
  20. package/dist/graph/react/viewer/GraphToolbar.css +5 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +7 -3
  23. package/dist/standalone/graph-standalone.html +155 -86
  24. package/dist/standalone/graph-viewer.css +15 -3
  25. package/dist/standalone/graph-viewer.js +140 -83
  26. package/dist/static-graph/index.d.ts +80 -2
  27. package/dist/static-graph/index.js +100 -15
  28. package/dist/static-graph/index.js.map +1 -1
  29. package/dist/styles/form-kernel.css +38 -38
  30. package/dist/styles/tailwind.css +51 -0
  31. package/dist/{types-Cn2FttBC.d.ts → types-DgqW3Jg4.d.ts} +87 -56
  32. package/package.json +7 -5
  33. package/dist/chunk-FD4PKRGZ.js.map +0 -1
  34. package/dist/chunk-RJVEDP7M.js.map +0 -1
  35. /package/dist/{chunk-HASMGCVK.js.map → chunk-XBHDA3PQ.js.map} +0 -0
@@ -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// ─── Stuff multiplicity ─────────────────────────────────────────────────────\n// How many values a stuff holds, in the encoding the pipe registry already uses\n// for `StuffSpecInfo.multiplicity`: `true` for a variable-length list (`Code[]`),\n// an integer for a fixed count (`Code[N]`), `null`/`false`/absent for a single\n// value. A count of exactly one is single throughout the MTHDS standard —\n// `Code[1]` is a way of writing `Code`, never a one-item list — so it reads as\n// single here too.\n\nexport type StuffMultiplicity = number | boolean | null;\n\n/** Whether a multiplicity declares a list: variable-length, or a fixed count of at least two. */\nexport function isPluralMultiplicity(multiplicity: StuffMultiplicity | undefined): boolean {\n return multiplicity === true || (Number.isInteger(multiplicity) && (multiplicity as number) > 1);\n}\n\n/** The authored marker for a multiplicity — `\"[]\"`, `\"[N]\"`, or `\"\"` for a single value. */\nexport function multiplicitySuffix(multiplicity: StuffMultiplicity | undefined): string {\n if (!isPluralMultiplicity(multiplicity)) return \"\";\n return multiplicity === true ? \"[]\" : `[${multiplicity as number}]`;\n}\n\n// ─── GraphSpec types (from pipelex-agent --view output) ─────────────────────\n\nexport interface GraphSpecNodeIoItem {\n name: string;\n digest?: string;\n /** The bare concept code or ref, never carrying a multiplicity marker — that is `multiplicity`. */\n concept?: string;\n /**\n * How many values the stuff holds (see `StuffMultiplicity`). Emitted only on a\n * plural stuff; absent means single. The static builder writes it, and a\n * runtime graph carries it once its producer emits it, so a renderer must read\n * absence as single rather than as unknown.\n */\n multiplicity?: StuffMultiplicity;\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//\n// Every field pipelex writes as `T | null` is declared `field?: T | null` here,\n// optional as well as nullable. Unlike `usage`, the registries cross the boundary\n// unvalidated, and a host that drops `null` values from the JSON it relays (ChatGPT\n// does, on the way to an MCP App view) delivers such a field with the key absent.\n// The optional marker is what makes the compiler refuse a strict `!== null` test\n// followed by a dereference.\n\n/**\n * A concept as the registry serializes it. A field that can be `null` is also\n * declared optional, because a host that drops `null` values delivers it absent:\n * test it with `!= null`, optional chaining or truthiness.\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?: StuffMultiplicity;\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\". Optional all the\n * same, since a host that drops `null` values delivers it absent.\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\n/**\n * A pipe as the registry serializes it, discriminated on `type`. A field that can\n * be `null` is also declared optional, because a host that drops `null` values\n * delivers it absent: test it with `!= null`, optional chaining or truthiness.\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\n/** One stuff as the dataflow analysis registers it, from the first io item naming its digest. */\nexport interface StuffRegistryEntry {\n name: string;\n concept?: string;\n multiplicity?: StuffMultiplicity;\n contentType?: string;\n}\n\nexport interface DataflowAnalysis {\n readonly stuffRegistry: Readonly<Record<string, StuffRegistryEntry>>;\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 * `unvalidated` is for a host with no validator at all, such as a page drawing\n * a method from its source: no verdict exists or is coming, and the issues are\n * what reading the source turned up.\n */\nexport const VALIDATION_STATE = {\n VALIDATING: \"validating\",\n VALID: \"valid\",\n INVALID: \"invalid\",\n ERROR: \"error\",\n UNVALIDATED: \"unvalidated\",\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":[]}
@@ -4,7 +4,7 @@ import {
4
4
  GRAPH_THEME,
5
5
  GRAPH_THEME_MODE,
6
6
  TOOLBAR_POSITION
7
- } from "./chunk-RJVEDP7M.js";
7
+ } from "./chunk-VCO4GZDZ.js";
8
8
  import {
9
9
  __spreadProps,
10
10
  __spreadValues
@@ -192,4 +192,4 @@ export {
192
192
  DEFAULT_GRAPH_CONFIG,
193
193
  getPaletteColors
194
194
  };
195
- //# sourceMappingURL=chunk-HASMGCVK.js.map
195
+ //# sourceMappingURL=chunk-XBHDA3PQ.js.map
@@ -4,11 +4,11 @@
4
4
  /* */
5
5
  /* This file styles the PANEL and nothing inside a field. The controls are */
6
6
  /* the kernel's, styled with Tailwind classes over shadcn semantic tokens, */
7
- /* and this package brings those in ITSELF — both React entries import */
8
- /* `../../styles/form-kernel.css`, which pulls the kernel's prebuilt sheet in */
9
- /* under a cascade layer so it can never outrank a host's own Tailwind. The */
10
- /* host adds nothing; saying otherwise was true only before v0.20.0. See */
11
- /* `docs/run-form-panel.md`. This stylesheet imports no kernel CSS itself. */
7
+ /* and the HOST styles them: a host with Tailwind 4 compiles their classes */
8
+ /* through `@pipelex/mthds-ui/tailwind.css`, a host without Tailwind imports */
9
+ /* `@pipelex/mthds-ui/form-kernel.css` once. No React entry imports either, */
10
+ /* and neither does this stylesheet. See "Styling the form controls" in the */
11
+ /* README and `docs/run-form-panel.md`. */
12
12
 
13
13
  .mthds-run-panel {
14
14
  display: flex;
@@ -2,7 +2,7 @@ import { FieldEnv } from '@pipelex/mthds-form/react';
2
2
  export * from '@pipelex/mthds-form/react';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import { ValidationMessageKey, PipeIOContract, RunField, RunInputError, PipeInputFormDescriptor } from '@pipelex/mthds-form';
5
- import { G as GraphTheme } from '../../types-Cn2FttBC.js';
5
+ import { G as GraphTheme } from '../../types-DgqW3Jg4.js';
6
6
 
7
7
  /**
8
8
  * The panel's submit path, kept React-free.
@@ -1,17 +1,16 @@
1
1
  "use client";
2
2
  import {
3
3
  getPaletteForTheme
4
- } from "../../chunk-HASMGCVK.js";
4
+ } from "../../chunk-XBHDA3PQ.js";
5
5
  import {
6
6
  GRAPH_THEME
7
- } from "../../chunk-RJVEDP7M.js";
7
+ } from "../../chunk-VCO4GZDZ.js";
8
8
  import {
9
9
  __spreadProps,
10
10
  __spreadValues
11
11
  } from "../../chunk-2NMEKWO5.js";
12
12
 
13
13
  // src/form/react/index.ts
14
- import "../../styles/form-kernel.css";
15
14
  export * from "@pipelex/mthds-form/react";
16
15
 
17
16
  // src/form/react/RunPanel.tsx
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/form/react/index.ts","../../../src/form/react/RunPanel.tsx","../../../src/form/runGate.ts"],"sourcesContent":["\"use client\";\n\n/**\n * The `./form/react` entry — the run-form panel over `@pipelex/mthds-form`.\n *\n * The kernel is a REQUIRED peer of this package now, so this entry is no longer\n * the only one allowed to import it. It stayed optional while it powered only\n * this run form; it stopped being optional the moment the graph's detail panel\n * began rendering results through it, because a viewer whose detail panel\n * cannot show data is not a viewer. See `docs/stuff-result-panel.md`.\n */\n\n/**\n * The kernel's React surface, re-exported. A host imports its controls from\n * here rather than naming `@pipelex/mthds-form` itself — see `../index.ts` for\n * why that indirection is the point rather than ceremony, and why importing the\n * kernel directly beside this package is the one thing that can reintroduce the\n * two-context-identities bug.\n */\n/**\n * The kernel's own utilities, shipped beside its code.\n *\n * A Tailwind host is meant to generate these by scanning the kernel, and does\n * not: content globs stop at the host's own source and node_modules is off the\n * sweep, so the host gets exactly the classes it happens to use elsewhere and\n * silently misses the rest. The result grid's arbitrary column template is the\n * one that shows - without it a record renders as a stack of labels each above\n * its own value instead of two aligned columns - but the gap is not limited to\n * that class, and nothing reports it.\n *\n * Imported HERE rather than left to the host for the same reason the graph\n * imports its own: a stylesheet a host must remember to add is one that will be\n * missing somewhere.\n *\n * It goes through `../../styles/form-kernel.css`, which pulls the kernel's\n * sheet into a cascade LAYER, and that indirection is the whole point: the\n * kernel ships a complete unscoped Tailwind build, so injected raw it wins\n * every tie against a host's own utilities purely by loading later. Read that\n * file for what it broke and why the layer settles it.\n */\nimport \"../../styles/form-kernel.css\";\n\nexport * from \"@pipelex/mthds-form/react\";\n\nexport { RunPanel } from \"./RunPanel\";\nexport type { RunPanelProps, UploadedFile } from \"./RunPanel\";\n\n// The submit path, React-free and re-exported for hosts that run the gate\n// outside a panel (a toolbar button, a keyboard shortcut).\nexport { runSubmitGate, summarizeVerdict, defaultValidationTranslate } from \"@form/runGate\";\nexport type { RunGateOutcome, RunPanelMessageKey, RunPanelTranslate } from \"@form/runGate\";\n","\"use client\";\n\nimport * as React from \"react\";\nimport {\n computeReadiness,\n fieldsForContract,\n isFilled,\n setValueAtPath,\n type PipeInputFormDescriptor,\n type PipeIOContract,\n type RunField,\n} from \"@pipelex/mthds-form\";\nimport {\n FieldDomIdProvider,\n FieldRenderer,\n OptionalToggle,\n type FieldEnv,\n} from \"@pipelex/mthds-form/react\";\nimport { getPaletteForTheme } from \"@graph/graphConfig\";\nimport { GRAPH_THEME, type GraphTheme } from \"@graph/types\";\nimport { runSubmitGate, type RunPanelTranslate } from \"@form/runGate\";\nimport \"./RunPanel.css\";\n\n/** What a host's upload returns: the stored URL, and optionally a display name. */\nexport interface UploadedFile {\n url: string;\n filename?: string;\n}\n\nexport interface RunPanelProps {\n /**\n * The pipe's IO contract — the kernel's type, supplied by the host.\n *\n * Hosts that can actually run a method already hold `pipe_io_contracts` from\n * the same `/validate` call that produced their graph spec; look an entry up\n * with the kernel's `getPipeIOContract(contracts, domain, pipeCode)`. The\n * panel deliberately does NOT accept a `GraphSpec` and derive a contract from\n * it — deriving field meaning locally is precisely what this component exists\n * not to do (design Decision A).\n */\n contract: PipeIOContract;\n /**\n * The pipe's input-form descriptor — the contract's sibling artifact, and\n * what now decides how every field renders.\n *\n * Since kernel `0.5.0` the descriptor IS the derivation: `buildRunFields`\n * maps this tree structurally, and the concept-name and schema-shape\n * heuristics that used to guess a field's kind are deleted rather than left\n * unreached. The contract is still required beside it — the two are\n * co-walked for the facts the descriptor deliberately omits (a scalar's\n * wrapper property, a nested array's bounds) — so neither artifact renders a\n * form on its own.\n *\n * It arrives the same way the contract does: the `/validate` call that\n * produced the graph spec returns `input_form` beside `pipe_io_contracts`\n * when asked for both views (`views: [\"pipe_io_contracts\", \"input_form\"]`),\n * and the kernel's `getPipeInputForm(inputForm, domain, pipeCode)` is\n * `getPipeIOContract`'s twin for looking an entry up.\n *\n * Required rather than optional, because the alternative is silent: the\n * kernel's `fieldsForContract` returns `[]` unless both artifacts are\n * present, so a host that omitted this would render a panel with no fields\n * and no error — a working form and an empty one look identical from here.\n */\n descriptor: PipeInputFormDescriptor;\n /** Fully controlled field values, host-owned — the kernel's philosophy. */\n values: Record<string, unknown>;\n onValuesChange: (values: Record<string, unknown>) => void;\n /** Fires only after the kernel's run gate passes, with the wire-ready payload. */\n onRun: (apiInputs: Record<string, unknown>) => void;\n /**\n * A run is in flight: the fields and the Run button go inert.\n *\n * **Set it synchronously inside `onRun`, before any `await`.** Across anything\n * longer than one task it is the only thing standing between a second submit\n * and a duplicate execution, and the panel cannot supply that itself: it never\n * learns that a run finished, so a lock held for a run's lifetime would have\n * no release. A host that flips this only once the server answers leaves the\n * button live for the whole round trip, and a second click there starts a\n * second run that nothing undoes.\n *\n * Synchronous re-entry within a single task is the panel's own problem and is\n * handled — see `justSubmittedRef` beside `handleSubmit`. That guard does not\n * reduce what this prop is for; it closes a window this prop cannot reach,\n * because React has not re-rendered inside it.\n */\n running?: boolean;\n /**\n * Ambient field state, passed to every control.\n *\n * For `disabled`, `onDropFile` and `resolveUrl` the host's value wins PER\n * KEY: the panel fills in only what the host left undefined — `disabled`\n * from `running`, `onDropFile` from its own upload tracking when\n * `uploadFile` is supplied.\n *\n * `uploadingIds` is the exception, and deliberately so: it is the UNION of\n * your set and the panel's, never a replacement. Because the keys default\n * independently, a host can supply `uploadFile` — leaving the panel to own\n * the drop and mark the field in its own set — while also passing a tracker\n * of its own; letting yours win outright there would hide an upload the\n * panel itself started and leave Run live through it. A host that owns the\n * whole loop supplies `onDropFile` too, so the panel's set is empty and the\n * union is exactly your set.\n */\n env?: FieldEnv;\n /**\n * Stores a dropped file and returns its URL. The host performs the upload —\n * this library never touches an API client — while the panel owns the\n * bookkeeping around it: marking the field busy and writing the result back\n * at the field's dotted path.\n */\n uploadFile?: (file: File, fieldId: string) => Promise<UploadedFile>;\n /** Panel header. The host names the pipe; there is no default heading. */\n title?: string;\n /**\n * The resolved theme, which drives the kernel's `.dark` class on the panel\n * container alongside this library's own palette variables. Defaults to\n * `light`. Pair it with `GraphViewer`'s `onThemeChange`, whose second\n * argument is exactly this resolved value.\n */\n theme?: GraphTheme;\n /**\n * Renders the whole error summary in the host's language. English by default.\n *\n * It covers the panel's own lines (`runPanel.*`) as well as the kernel's\n * per-error ones, because which of them a rejected run produces is the\n * kernel's decision and not something a host can predict.\n */\n translate?: RunPanelTranslate;\n /**\n * Makes this panel's control DOM ids predictable: a control at value path\n * `cv` gets `id=\"<idPrefix>-cv\"`, and `\"\"` writes the path unprefixed.\n *\n * Ids are namespaced whether or not you pass this. A control's `id` must be\n * unique in the DOCUMENT while a value path is unique only within one form,\n * so two panels each declaring an input named `text` would otherwise emit two\n * `id=\"text\"` — and a `<label for>` binds the first match in tree order,\n * which hands the second panel's label to the first panel's control and\n * leaves the second input with no accessible name. Left unset, the prefix\n * comes from `useId`: unique per panel and hydration-stable, but opaque, so\n * nothing outside can predict it.\n *\n * Pass one when something must ADDRESS a control — `getElementById`, a\n * deep-link that focuses a field, an end-to-end selector. Doing so moves the\n * uniqueness obligation to you: the prefix scopes this whole panel, so it\n * must be unique in the document, and `\"\"` restores the ids this panel emitted\n * before the kernel's fix along with the collision they had.\n */\n idPrefix?: string;\n /** Appended to the panel container's class list. */\n className?: string;\n}\n\nconst EMPTY_IDS: ReadonlySet<string> = new Set<string>();\nconst NOTHING_REVEALED: ReadonlySet<string> = new Set<string>();\n\n/**\n * A pipe's input form: fields, readiness, and the run gate, composed.\n *\n * Every question of meaning is the form kernel's — `fieldsForContract` decides\n * what each input IS, `computeReadiness` decides whether the form MAY run, and\n * `runSubmitGate` (over the kernel's four steps) decides what goes on the wire.\n * What lives here is layout: which fields are visible, where the toggle and the\n * button sit, and how a rejected verdict is shown. The panel never reads\n * `json_schema` to make a rendering decision and never sniffs a value's shape\n * (design Decision C) — which is what keeps a change to the kernel's derivation\n * invisible to this repo.\n */\nexport function RunPanel({\n contract,\n descriptor,\n values,\n onValuesChange,\n onRun,\n running = false,\n env,\n uploadFile,\n title,\n theme = GRAPH_THEME.LIGHT,\n translate,\n idPrefix,\n className,\n}: RunPanelProps) {\n const [showOptional, setShowOptional] = React.useState(false);\n const [uploadingIds, setUploadingIds] = React.useState<ReadonlySet<string>>(EMPTY_IDS);\n const [submitError, setSubmitError] = React.useState<string | null>(null);\n const readinessHintId = React.useId();\n const fallbackPrefix = React.useId();\n\n const fields = React.useMemo(\n () => fieldsForContract(contract, descriptor),\n [contract, descriptor],\n );\n const readiness = React.useMemo(() => computeReadiness(fields, values), [fields, values]);\n\n const commitValues = React.useCallback(\n (next: Record<string, unknown>) => {\n // A summary describes the values that produced it; the moment they move\n // it is stale, and a stale complaint about a field just corrected is\n // worse than none.\n setSubmitError(null);\n onValuesChange(next);\n },\n [onValuesChange],\n );\n\n // An upload resolves long after the render that started it, so the write-back\n // must not target the values THAT render captured — the user may well have\n // typed elsewhere while waiting. This mirror holds the latest ones. Only the\n // async continuation reads it; every synchronous path stays on the prop,\n // which is correct there because it runs inside the render that captured it.\n // Layout, not passive, for the reason spelled out on the contract marker\n // below: a mirror that lags the commit is read by continuations landing in\n // exactly that lag, and here the cost is the host's newer edits overwritten.\n const valuesRef = React.useRef(values);\n React.useLayoutEffect(() => {\n valuesRef.current = values;\n }, [values]);\n\n // Switching pipes makes an upload started under the old one irrelevant — and\n // worse than irrelevant when both pipes declare the same input name, which\n // happens within a single method: `recruitment.cv_screening` and\n // `recruitment.extract_cv` both take a required `cv` document. A file chosen\n // for one would land in the other looking like a deliberate answer, gating\n // satisfied, ready to run. So a drop remembers the generation it happened\n // under, and a result arriving under a later one is dropped. This makes\n // `contract` referentially significant — a host that rebuilds it every render\n // loses its uploads (and is already rebuilding every field, since `fields`\n // memoizes on it).\n //\n // The marker COUNTS rather than naming the contract, because identity answers\n // a subtly different question. A host can leave pipe A and come back to the\n // very same contract object while A's upload is still in flight, and an\n // identity check then says \"same pipe, accept it\" — ABA, and it is not the\n // same form: switching away emptied `uploadingIds`, so that upload stopped\n // gating Run and its dropzone re-opened. Accepting it lands a file in a form\n // that never marked it — Run was live throughout, and for a non-gating input\n // the run may already have gone out without it — and, if the user re-dropped\n // on the way back, overwrites the fresh result while un-marking an upload\n // still in progress. Counting makes the predicate \"has the form moved on\n // since this drop\", which is what the guard always meant.\n //\n // A LAYOUT effect, deliberately. The marker answers \"which form is on\n // screen\", and it has to answer that as of the last COMMIT. A passive effect\n // answers later: React schedules those on a task, while an upload settling is\n // a promise continuation — a microtask, which runs first. In that window the\n // switched form is already rendered and the marker still counts the pipe the\n // user left, so the guard compares the departed generation against itself and\n // lets through precisely the write it exists to reject. Bumping during render\n // would close the window too, and is worse: a concurrent render React\n // abandons would advance past a contract that never commits, and an upload\n // belonging to the pipe still on screen would be thrown away instead. Layout\n // effects run inside the commit, and only for renders that commit.\n //\n // The bump lives in the CLEANUP, which is the one place BOTH ways of leaving a\n // form pass through. Changing `contract` is only one of them: a host that\n // writes `<RunPanel key={pipeRef} …>` — the ordinary React idiom for \"reset\n // this child when the entity changes\" — never changes the prop at all, it\n // unmounts this instance and mounts another. The effect body would then never\n // re-run, the marker would still equal the departed upload's `startedAt`, and\n // that upload's continuation would call `onValuesChange`, which is the HOST's\n // setter and very much still alive. The abandoned file would land in the\n // replacement panel's `cv` exactly as if it had been chosen there. Bumping on\n // cleanup makes the predicate mean what it says — this form instance has\n // stopped being the one on screen — however it stopped.\n const generationRef = React.useRef(0);\n React.useLayoutEffect(() => {\n // Whatever was in flight belonged to the previous pipe; it must not go on\n // gating this form's Run button until it happens to settle.\n setUploadingIds(EMPTY_IDS);\n // Nor may a complaint about the previous pipe's inputs stand over this\n // pipe's form: it names fields that are no longer on screen.\n setSubmitError(null);\n return () => {\n generationRef.current += 1;\n };\n }, [contract]);\n\n const handleDropFile = React.useCallback(\n (id: string, file: File) => {\n if (!uploadFile) return;\n const startedAt = generationRef.current;\n setUploadingIds((previous) => new Set(previous).add(id));\n // Called through an async wrapper rather than directly. `uploadFile` is\n // the HOST's function and is typed as a plain function returning a\n // promise — nothing obliges it to be `async`. One that validates before\n // it starts the request (no API key configured, a file over the size\n // limit, a mime type it will not take) throws SYNCHRONOUSLY, and a\n // synchronous throw happens before there is a promise to hang `.catch()`\n // and `.finally()` on. The id marked one line above would then stay\n // marked for good: `uploading` disables the dropzone, so the field cannot\n // even be retried, and Run stays gated until the user leaves the form\n // entirely. The wrapper turns that throw into the rejection this chain\n // already cleans up, which makes the SAME host logic behave the same way\n // whether it was written `async` or not — the only difference between\n // those two spellings being the keyword.\n void (async () => await uploadFile(file, id))()\n .then(\n (uploaded) => {\n if (generationRef.current !== startedAt) return;\n const next = setValueAtPath(valuesRef.current, id.split(\".\"), {\n url: uploaded.url,\n filename: uploaded.filename ?? file.name,\n });\n // Advance the mirror here rather than waiting for the effect. Two\n // uploads finishing in the same React batch both run before any\n // re-render, so both would otherwise read the same snapshot and the\n // second write would drop the first — a pipe with two file inputs is\n // ordinary (`candidate_screening.screen_candidate` takes a `cv` and a\n // `job_offer`), and the loss is silent: both fields look settled\n // while one value is gone. The effect still owns syncing FROM the\n // host, so the mirror converges on whatever the host actually kept.\n valuesRef.current = next;\n commitValues(next);\n },\n () => {\n // The host owns how a failed upload is announced (it owns the\n // transport). Swallowing it here keeps a rejected promise from\n // becoming an unhandled rejection; the field simply stays empty.\n //\n // This is `.then`'s SECOND ARGUMENT rather than a `.catch()` link,\n // and the difference is the whole point: a `.catch()` chained after\n // the handler above also catches whatever that handler throws, and\n // the handler dereferences a value the host produced. `uploadFile`\n // is typed `Promise<UploadedFile>`, but a type is not a runtime\n // guarantee — a host branch that forgets its `return`, or unwraps a\n // JSON response one level too few, resolves to `undefined`, and\n // `uploaded.url` then throws. Swallowed, that is the one failure in\n // this whole lifecycle with NO diagnostic surface anywhere: the\n // spinner clears, the field reads empty, nothing is logged, and\n // re-dropping repeats it forever. As the second argument this\n // handler covers only the upload's own rejection, which is all it\n // ever claimed to; a write-back bug stays an unhandled rejection,\n // where the host's error reporting can see it.\n },\n )\n .finally(() => {\n // The same generation check as the write-back, for a sharper reason.\n // Switching contracts already emptied this set, and the very same id\n // may since have been re-added by a NEW upload under the new\n // generation — nothing exotic, since that is what sharing the `cv`\n // input name means, and a host returning to the pipe it left reaches\n // it with no rename at all. Deleting it here would un-mark an upload\n // that is still running: its dropzone re-enables mid-flight, its\n // progress indicator vanishes, and the Run gate lets go of it.\n if (generationRef.current !== startedAt) return;\n setUploadingIds((previous) => {\n const next = new Set(previous);\n next.delete(id);\n return next;\n });\n });\n },\n // `contract` is no longer a dependency: the generation is read off the ref\n // at drop time, so this callback does not need rebuilding per pipe.\n [uploadFile, commitValues],\n );\n\n const fieldEnv = React.useMemo<FieldEnv>(\n () => ({\n ...env,\n disabled: env?.disabled ?? running,\n onDropFile: env?.onDropFile ?? (uploadFile ? handleDropFile : undefined),\n // The UNION, not the host's set winning outright: `onDropFile` and\n // `uploadingIds` default independently per key, so a host may hand us\n // `uploadFile` (leaving the panel to own the drop, and to mark the field\n // in its own set) while also passing a tracker of its own. Letting the\n // host's set replace ours would hide an upload the panel itself started,\n // and Run would stay live through it. A host that owns the whole loop\n // supplies `onDropFile` too, so our set is empty and the union is exactly\n // its set — \"yours win\" still holds where that clause applies.\n uploadingIds: new Set([...(env?.uploadingIds ?? EMPTY_IDS), ...uploadingIds]),\n }),\n [env, running, uploadFile, handleDropFile, uploadingIds],\n );\n\n // Readiness alone would let a run go out while a file is still uploading: a\n // non-gating file input (optional, or plural — `mustBeFilled` excludes lists)\n // never counts toward readiness, so Run stays live through its upload and the\n // method would run with the file simply absent. Read off `fieldEnv` rather\n // than the raw state, so a host driving its own loop through\n // `env.uploadingIds` gets the same gate.\n const uploading = (fieldEnv.uploadingIds?.size ?? 0) > 0;\n const notReady = readiness.missing.length > 0;\n\n // ONE expression, read by the button and by the submit path, because they are\n // the same question asked twice and any gap between them is reachable: the\n // panel puts a real `<form>` in the host's DOM under a class name the docs\n // offer as a hook, and `form.requestSubmit()` ignores the submitter entirely,\n // so a disabled button stops nothing there. (The keyboard it does stop: Run is\n // this form's only submit button — every control the kernel renders carries\n // `type=\"button\"` — hence the default button, and implicit submission on a\n // disabled default button does nothing. Measured, not assumed.)\n //\n // Each term has to be here on its own merit, and none is redundant with the\n // kernel gate that runs afterwards. `uploading`: a non-gating file input\n // (optional, or plural — `mustBeFilled` excludes lists) never counts toward\n // readiness, so without this the method runs with the file simply absent.\n // `notReady`: an empty REQUIRED text input reaches ajv as `{ text: \"\" }`, a\n // perfectly valid string, so the gate passes it and only readiness notices —\n // that is pinned in `runGate.test.ts` and is exactly why the button gates on\n // readiness at all. `running`: a second run started over the first is a\n // duplicate execution, which nothing downstream would undo — and this term is\n // only ever as timely as the host makes it, which is why the prop's own doc\n // asks for it synchronously. `env.disabled`: the panel already treats the\n // host's flag and `running` as ONE concept where the fields are concerned\n // (`fieldEnv.disabled` falls back from one to the other), so gating the\n // button on only one of the two sources was the same asymmetry rounds 4 and 9\n // turned up — a host freezing the form got every input inert and a live Run\n // button beside them, and a click there starts a run from a form the user\n // cannot edit. It is read off the prop rather than off `fieldEnv.disabled`\n // because those differ exactly when the host passes `disabled: false` while\n // `running` is true, and there the button must still be held.\n const blocked = running || notReady || uploading || (env?.disabled ?? false);\n\n // Optional inputs that are still empty stay folded, so the form opens at its\n // simplest shape and grows on demand. Required or already-filled always show.\n //\n // Which optionals are revealed is LATCHED rather than re-derived from the\n // current value, and that is the whole point. Deriving it live means an\n // optional the user empties disappears from under the cursor: clearing the\n // last character flips `isFilled`, the field drops out of `visibleFields`,\n // and React unmounts the very input being typed into — focus lost, every\n // keystroke after it going nowhere. It needs no host pre-fill to reach:\n // expand the fold, fill an optional, collapse it (it stays on screen because\n // it is filled), then clear it.\n //\n // So between fold transitions the form's shape only ever GROWS. The latch is\n // cleared by the toggle's own handler and by a contract change, which are the\n // two moments nobody is mid-edit; each is followed by the grow pass below\n // re-adding every optional that still holds a value, so an emptied one folds\n // back. Nothing about readiness or the wire depends on this — an emptied\n // optional is omitted from the payload whether or not its input is on screen.\n //\n // The latch is STATE adjusted during render, not a ref mutated during render,\n // and in a library component that distinction is load-bearing. A ref written\n // during render survives a render that never commits, so a host rendering the\n // panel inside a transition — which App Router navigation does by default —\n // can have an abandoned render for contract B wipe the set that the committed\n // contract A is still displaying from, and an optional the user revealed,\n // collapsed and emptied vanishes on A's next render. That is the very defect\n // this latch exists to prevent, re-entering by another door. React discards a\n // state update from an abandoned render, so the two-`useState` form below is\n // the one that actually holds. It is React's documented shape for adjusting\n // state when a prop changes, and it converges: after the update lands, the\n // reset branch and `newlyRevealed` are both empty on the following pass.\n const [revealed, setRevealed] = React.useState<ReadonlySet<string>>(NOTHING_REVEALED);\n const [latchedFor, setLatchedFor] = React.useState(contract);\n // Read this render off the local value, never off `revealed`: a state update\n // made during render is not visible until the re-render it schedules, and the\n // output computed below has to already reflect it.\n let revealedNow = revealed;\n if (latchedFor !== contract) {\n revealedNow = NOTHING_REVEALED;\n setLatchedFor(contract);\n setRevealed(revealedNow);\n }\n const newlyRevealed = fields.filter(\n (field) => !field.required && isFilled(values[field.name]) && !revealedNow.has(field.name),\n );\n if (newlyRevealed.length > 0) {\n revealedNow = new Set([...revealedNow, ...newlyRevealed.map((field) => field.name)]);\n setRevealed(revealedNow);\n }\n const isFolded = React.useCallback(\n (field: RunField) => !field.required && !revealedNow.has(field.name),\n [revealedNow],\n );\n // What a collapse would hide — every EMPTY optional — rather than what is\n // hidden right now. The two differ only for an optional that was revealed and\n // then emptied, and that difference is the whole affordance: counting the\n // latched-visible ones leaves the count at zero there, the toggle unrendered,\n // and the empty input stuck on screen with no control that would fold it. The\n // latch keeps deciding what renders; it just stops deciding whether the user\n // is offered a way out of it.\n const foldableCount = fields.filter(\n (field) => !field.required && !isFilled(values[field.name]),\n ).length;\n const visibleFields = showOptional ? fields : fields.filter((field) => !isFolded(field));\n\n // A submit that has passed the gate latches until the end of the current task.\n //\n // `blocked` is computed during render, so two `requestSubmit()` calls made in\n // ONE synchronous host callback both close over the same render and both pass\n // it — even when the host sets `running` synchronously inside `onRun`, exactly\n // as that prop's doc demands, because React has not re-rendered in between.\n // Two real clicks are two separate tasks with a commit between them and were\n // never affected; this closes the programmatic path, which is a surface this\n // component documents and pins a story against.\n //\n // The release is a microtask, and that is the whole reason this is safe rather\n // than the lock this component deliberately does NOT take. A lock held for the\n // lifetime of a run would have no release — the panel is told when a run\n // starts and never that one finished — so a host that never passes `running`\n // would be wedged after its first run. This latch outlives nothing: the\n // microtask is queued unconditionally and always runs, so it cannot wedge, and\n // it holds no run state a later render could disagree with.\n const justSubmittedRef = React.useRef(false);\n\n const handleSubmit = (event: React.FormEvent) => {\n event.preventDefault();\n // The submit PATH owns the gate, not the button's `disabled` attribute —\n // see `blocked` above for why each term is there and why none is covered by\n // the kernel gate that runs next.\n if (blocked || justSubmittedRef.current) return;\n const outcome = runSubmitGate(contract, fields, values, translate);\n if (!outcome.ok) {\n setSubmitError(outcome.summary);\n return;\n }\n setSubmitError(null);\n // Latched BEFORE `onRun`, so a host that re-enters synchronously from inside\n // its own handler is held by the same guard.\n justSubmittedRef.current = true;\n queueMicrotask(() => {\n justSubmittedRef.current = false;\n });\n onRun(outcome.apiInputs);\n };\n\n const paletteStyle = React.useMemo(\n () => getPaletteForTheme(theme) as React.CSSProperties,\n [theme],\n );\n\n // One provider around the whole panel, which is exactly one form — the scope\n // the kernel's contract asks for. `fallbackPrefix` is this panel's own\n // `useId`, so an unset `idPrefix` keeps the kernel's default behaviour (unique\n // per instance) rather than turning the namespacing off.\n return (\n <FieldDomIdProvider prefix={idPrefix ?? fallbackPrefix}>\n <form\n className={[\"mthds-run-panel\", theme === GRAPH_THEME.DARK && \"dark\", className]\n .filter(Boolean)\n .join(\" \")}\n style={paletteStyle}\n // The panel's gate is the authority on whether a run may go out, and the\n // browser's is not merely redundant beside it — it is WRONG. The kernel's\n // number control carries `step={integer ? 1 : 0.1}`, so any float with\n // more than one decimal is a `stepMismatch` even though `native.Number`\n // accepts it and the kernel's own ajv pass does too. Interactive\n // validation then aborts submission before `onSubmit` runs at all: no\n // gate, no `onRun`, no error from the panel, and a native message that is\n // wrong about the domain (\"nearest valid values are 87.2 and 87.3\").\n // Turning it off is what makes the documented `requestSubmit()` promise\n // true, and costs nothing — every constraint that should hold is checked\n // by `runSubmitGate`.\n noValidate\n onSubmit={handleSubmit}\n >\n {title && <h2 className=\"mthds-run-panel-title\">{title}</h2>}\n\n {fields.length > 0 ? (\n <div className=\"mthds-run-panel-fields\">\n {visibleFields.map((field) => (\n <FieldRenderer\n key={field.name}\n field={field}\n value={values[field.name]}\n onChange={(value) => commitValues({ ...values, [field.name]: value })}\n id={field.name}\n env={fieldEnv}\n />\n ))}\n {foldableCount > 0 && (\n <OptionalToggle\n count={foldableCount}\n expanded={showOptional}\n onToggle={() => {\n setShowOptional((expanded) => !expanded);\n // Clearing the latch here is what makes \"the shape only grows\"\n // a temporary state rather than a permanent one. A fold\n // transition is a click on this button, so nobody is mid-edit\n // in a field, and the render below re-adds every optional that\n // still holds a value — leaving exactly the emptied ones to\n // fold away, which is what the user just asked for.\n setRevealed(NOTHING_REVEALED);\n }}\n noun=\"input\"\n />\n )}\n </div>\n ) : (\n <p className=\"mthds-run-panel-empty\">This pipe takes no inputs.</p>\n )}\n\n {submitError && (\n <p className=\"mthds-run-panel-error\" role=\"alert\">\n {submitError}\n </p>\n )}\n\n <div className=\"mthds-run-panel-footer\">\n {/*\n The readiness line is the only thing on screen that says WHY Run is\n disabled, so it has to be reachable by the people who cannot see it\n sitting next to the button — and a disabled button is out of the tab\n order, so they will not arrive at it by walking the controls either.\n `aria-describedby` is the association that makes a screen reader read\n the reason with the button instead of announcing a bare dimmed \"Run\".\n The id comes from `useId` because a host may put two panels on one\n page, and a hardcoded one would make the second panel describe the\n first panel's button.\n */}\n <button\n type=\"submit\"\n className=\"mthds-run-panel-run\"\n disabled={blocked}\n aria-describedby={notReady ? readinessHintId : undefined}\n >\n {running ? \"Running…\" : \"Run\"}\n </button>\n {notReady && (\n <span id={readinessHintId} className=\"mthds-run-panel-readiness\">\n {`${readiness.ready} of ${readiness.total} ready — still needed: ${readiness.missing.join(\", \")}`}\n </span>\n )}\n </div>\n </form>\n </FieldDomIdProvider>\n );\n}\n","/**\n * The panel's submit path, kept React-free.\n *\n * Everything between \"the user pressed Run\" and \"the payload goes on the wire\"\n * is the kernel's (`@pipelex/mthds-form`, `core/gate.ts`); this module bridges\n * the panel's values into the shape the gate takes, calls it, and turns the\n * verdict into one line a panel can show. Splitting it out of `RunPanel.tsx` is\n * what lets the node vitest project test that seam without mounting React — the\n * gate's own behavior is covered by the kernel's tests and is deliberately not\n * re-tested here.\n */\nimport {\n describeValidationError,\n gateRunInputs,\n rjsfDataFromRunValues,\n type PipeIOContract,\n type RunField,\n type RunInputError,\n type ValidationMessageKey,\n} from \"@pipelex/mthds-form\";\n\n/**\n * How many ajv errors the summary quotes when the missing-input scan comes up\n * empty. A form with a deep mismatch can produce dozens; past a handful the\n * line stops being readable and the user acts on the first one anyway.\n */\nconst MAX_QUOTED_ERRORS = 3;\n\n/** The outcome of one submit attempt. */\nexport type RunGateOutcome =\n | { ok: true; apiInputs: Record<string, unknown> }\n | { ok: false; missingInputs: string[]; summary: string };\n\n/**\n * Every message key the error summary can render — the kernel's, plus the two\n * this panel owns.\n *\n * The kernel's keys describe one ajv error each and are rendered by\n * `describeValidationError`. The `runPanel.` ones are lines the panel writes\n * itself, around those errors, and they carry the prefix precisely so the\n * ownership is legible at the call site and in a host's translation file.\n *\n * Nesting the kernel's union inside ours is deliberate: a key the kernel ADDS\n * lands here automatically and then fails `VALIDATION_STRINGS`'s exhaustiveness\n * until it is given a string, which is how a kernel bump is stopped from\n * silently rendering `undefined` at a user.\n */\nexport type RunPanelMessageKey =\n | ValidationMessageKey\n | \"runPanel.missingInputs\"\n | \"runPanel.fillRequired\";\n\n/**\n * The panel's i18n seam — the kernel's `Translate` widened to the panel's own\n * keys. It accepts a superset of what the kernel asks for, so it is still a\n * valid `Translate` wherever the kernel wants one.\n */\nexport type RunPanelTranslate = (\n key: RunPanelMessageKey,\n values?: Record<string, string>,\n) => string;\n\n/**\n * English defaults for every key above. The kernel deliberately ships no\n * defaults for its own (its `Translate` contract is host-supplied), so a panel\n * that wants to say anything at all has to name them — the wording matches the\n * canonical consumer's `en.json` so the two surfaces read alike.\n */\nconst VALIDATION_STRINGS: Record<RunPanelMessageKey, string> = {\n \"inputPanel.aDateField\": \"A date field\",\n \"inputPanel.pickValidDate\": \"{label}: “{value}” is not a valid date\",\n \"inputPanel.pickValidDateEmpty\": \"{label}: pick a valid date\",\n \"inputPanel.dateCarriesTime\":\n \"{label}: “{value}” carries a time, but a date field takes the day alone — use “{day}” and put the time in the “time” field\",\n \"inputPanel.invalidValue\": \"Invalid value\",\n \"inputPanel.invalidValueWithData\": \"{stack} (value: “{value}”)\",\n \"runPanel.missingInputs\": \"Missing required fields in: {inputs}\",\n \"runPanel.fillRequired\": \"Please fill in all required fields before running.\",\n};\n\n/**\n * The default translator: the English strings above with `{placeholder}`\n * interpolation. A host that localizes passes its own translator instead.\n */\nexport const defaultValidationTranslate: RunPanelTranslate = (key, values) => {\n const template = VALIDATION_STRINGS[key];\n if (!values) return template;\n return template.replace(/\\{(\\w+)\\}/g, (whole, name: string) => values[name] ?? whole);\n};\n\n/**\n * One human line for an invalid verdict.\n *\n * `missingInputs` is the good case: it names the VARIABLES at fault, which is\n * what the user clicks on. The scan can legitimately come up empty on an\n * invalid form (a wrong value shape, a nested mismatch), and that is exactly\n * when falling back to the raw ajv errors keeps the failure diagnosable rather\n * than silent — see `validateRunInputs` in the kernel.\n *\n * All three routes go through `t`. Which one runs is decided by the kernel's\n * verdict, so a host that localizes cannot know in advance which line it will\n * be shown — leaving any of them in hardcoded English would surface as the\n * summary silently reverting to English on some inputs and not others.\n */\nexport function summarizeVerdict(\n missingInputs: string[],\n errors: RunInputError[],\n preparedData: unknown,\n t: RunPanelTranslate = defaultValidationTranslate,\n): string {\n if (missingInputs.length > 0) {\n return t(\"runPanel.missingInputs\", { inputs: missingInputs.join(\", \") });\n }\n const described = errors\n .slice(0, MAX_QUOTED_ERRORS)\n .map((error) => describeValidationError(error, t, preparedData))\n .join(\"; \");\n return described || t(\"runPanel.fillRequired\");\n}\n\n/**\n * Run the kernel's gate over the panel's current values.\n *\n * `gateRunInputs` IS the gate — the whole chain in one call — so all this\n * module does either side of it is bridge and phrase: the panel holds values\n * keyed by variable, the gate takes the schema-shaped data those become, and a\n * rejected verdict has to come back as one line rather than as a discriminated\n * union the panel would have to read.\n *\n * Assembling those steps here instead was the earlier shape and it was wrong in\n * a way nothing local could see. The chain's schema validation is not the whole\n * rule: ajv's `required` only asserts a key is PRESENT, and a content model\n * carries no `minLength`, so a required input that arrived empty — `{text: \"\"}`\n * from an untouched text field, `{url: \"\"}` from an untouched file field —\n * satisfied the schema and the run went out. The Run button refuses exactly\n * those, through `computeReadiness`. The gate now re-applies that same\n * emptiness rule with `computeReadiness`'s own functions over the same derived\n * fields, which makes the button's verdict and this one a single invariant\n * rather than two things that resemble each other.\n */\nexport function runSubmitGate(\n contract: PipeIOContract,\n fields: RunField[],\n values: Record<string, unknown>,\n t: RunPanelTranslate = defaultValidationTranslate,\n): RunGateOutcome {\n const verdict = gateRunInputs(contract, rjsfDataFromRunValues(values, fields));\n if (!verdict.ok) {\n return {\n ok: false,\n missingInputs: verdict.missingInputs,\n summary: summarizeVerdict(verdict.missingInputs, verdict.errors, verdict.preparedData, t),\n };\n }\n return { ok: true, apiInputs: verdict.inputs };\n}\n"],"mappings":"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","names":["_a","_b","_c"]}
1
+ {"version":3,"sources":["../../../src/form/react/index.ts","../../../src/form/react/RunPanel.tsx","../../../src/form/runGate.ts"],"sourcesContent":["\"use client\";\n\n/**\n * The `./form/react` entry — the run-form panel over `@pipelex/mthds-form`.\n *\n * The kernel is a DEPENDENCY of this package, so this entry is no longer the\n * only one allowed to import it. It stayed an optional peer while it powered\n * only this run form; it stopped being optional the moment the graph's detail\n * panel began rendering results through it, because a viewer whose detail\n * panel cannot show data is not a viewer. See `docs/stuff-result-panel.md`.\n */\n\n/**\n * No form kernel stylesheet is imported here, on purpose: the host styles the\n * kernel's controls in the way its kind of host needs. A host with Tailwind 4\n * compiles the kernel's classes itself, with\n * `@import \"@pipelex/mthds-ui/tailwind.css\"` in place of the `@source` line of\n * the kernel's documented setup; a host without Tailwind imports\n * `@pipelex/mthds-ui/form-kernel.css` once. The README's \"Styling the form\n * controls\" section is the host's side of it.\n *\n * Do not put an import back. v0.20.0 made the kernel's sheet this entry's job,\n * because Tailwind 3 hosts kept forgetting a content glob into `node_modules`\n * and silently lost the classes only the kernel uses (the result grid's column\n * template was the one that showed). v0.21.0 wrapped the sheet in a cascade\n * layer after the raw copy beat the host's own responsive variants. Neither\n * arrangement works, because the sheet is a complete Tailwind build whose two\n * halves need different places in a Tailwind 4 host's cascade: appended, the\n * layer outranked every variant the host wrote; named first, the host's\n * preflight stripped the kernel's utilities. In a host without Tailwind, the\n * preflight reset every browser default on the page the moment a panel\n * mounted. `docs/run-form-panel.md` has the full account.\n */\n\n/**\n * The kernel's React surface, re-exported. A host imports its controls from\n * here rather than naming `@pipelex/mthds-form` itself — see `../index.ts` for\n * why that indirection is the point rather than ceremony, and why importing the\n * kernel directly beside this package is the one thing that can reintroduce the\n * two-context-identities bug.\n */\nexport * from \"@pipelex/mthds-form/react\";\n\nexport { RunPanel } from \"./RunPanel\";\nexport type { RunPanelProps, UploadedFile } from \"./RunPanel\";\n\n// The submit path, React-free and re-exported for hosts that run the gate\n// outside a panel (a toolbar button, a keyboard shortcut).\nexport { runSubmitGate, summarizeVerdict, defaultValidationTranslate } from \"@form/runGate\";\nexport type { RunGateOutcome, RunPanelMessageKey, RunPanelTranslate } from \"@form/runGate\";\n","\"use client\";\n\nimport * as React from \"react\";\nimport {\n computeReadiness,\n fieldsForContract,\n isFilled,\n setValueAtPath,\n type PipeInputFormDescriptor,\n type PipeIOContract,\n type RunField,\n} from \"@pipelex/mthds-form\";\nimport {\n FieldDomIdProvider,\n FieldRenderer,\n OptionalToggle,\n type FieldEnv,\n} from \"@pipelex/mthds-form/react\";\nimport { getPaletteForTheme } from \"@graph/graphConfig\";\nimport { GRAPH_THEME, type GraphTheme } from \"@graph/types\";\nimport { runSubmitGate, type RunPanelTranslate } from \"@form/runGate\";\nimport \"./RunPanel.css\";\n\n/** What a host's upload returns: the stored URL, and optionally a display name. */\nexport interface UploadedFile {\n url: string;\n filename?: string;\n}\n\nexport interface RunPanelProps {\n /**\n * The pipe's IO contract — the kernel's type, supplied by the host.\n *\n * Hosts that can actually run a method already hold `pipe_io_contracts` from\n * the same `/validate` call that produced their graph spec; look an entry up\n * with the kernel's `getPipeIOContract(contracts, domain, pipeCode)`. The\n * panel deliberately does NOT accept a `GraphSpec` and derive a contract from\n * it — deriving field meaning locally is precisely what this component exists\n * not to do (design Decision A).\n */\n contract: PipeIOContract;\n /**\n * The pipe's input-form descriptor — the contract's sibling artifact, and\n * what now decides how every field renders.\n *\n * Since kernel `0.5.0` the descriptor IS the derivation: `buildRunFields`\n * maps this tree structurally, and the concept-name and schema-shape\n * heuristics that used to guess a field's kind are deleted rather than left\n * unreached. The contract is still required beside it — the two are\n * co-walked for the facts the descriptor deliberately omits (a scalar's\n * wrapper property, a nested array's bounds) — so neither artifact renders a\n * form on its own.\n *\n * It arrives the same way the contract does: the `/validate` call that\n * produced the graph spec returns `input_form` beside `pipe_io_contracts`\n * when asked for both views (`views: [\"pipe_io_contracts\", \"input_form\"]`),\n * and the kernel's `getPipeInputForm(inputForm, domain, pipeCode)` is\n * `getPipeIOContract`'s twin for looking an entry up.\n *\n * Required rather than optional, because the alternative is silent: the\n * kernel's `fieldsForContract` returns `[]` unless both artifacts are\n * present, so a host that omitted this would render a panel with no fields\n * and no error — a working form and an empty one look identical from here.\n */\n descriptor: PipeInputFormDescriptor;\n /** Fully controlled field values, host-owned — the kernel's philosophy. */\n values: Record<string, unknown>;\n onValuesChange: (values: Record<string, unknown>) => void;\n /** Fires only after the kernel's run gate passes, with the wire-ready payload. */\n onRun: (apiInputs: Record<string, unknown>) => void;\n /**\n * A run is in flight: the fields and the Run button go inert.\n *\n * **Set it synchronously inside `onRun`, before any `await`.** Across anything\n * longer than one task it is the only thing standing between a second submit\n * and a duplicate execution, and the panel cannot supply that itself: it never\n * learns that a run finished, so a lock held for a run's lifetime would have\n * no release. A host that flips this only once the server answers leaves the\n * button live for the whole round trip, and a second click there starts a\n * second run that nothing undoes.\n *\n * Synchronous re-entry within a single task is the panel's own problem and is\n * handled — see `justSubmittedRef` beside `handleSubmit`. That guard does not\n * reduce what this prop is for; it closes a window this prop cannot reach,\n * because React has not re-rendered inside it.\n */\n running?: boolean;\n /**\n * Ambient field state, passed to every control.\n *\n * For `disabled`, `onDropFile` and `resolveUrl` the host's value wins PER\n * KEY: the panel fills in only what the host left undefined — `disabled`\n * from `running`, `onDropFile` from its own upload tracking when\n * `uploadFile` is supplied.\n *\n * `uploadingIds` is the exception, and deliberately so: it is the UNION of\n * your set and the panel's, never a replacement. Because the keys default\n * independently, a host can supply `uploadFile` — leaving the panel to own\n * the drop and mark the field in its own set — while also passing a tracker\n * of its own; letting yours win outright there would hide an upload the\n * panel itself started and leave Run live through it. A host that owns the\n * whole loop supplies `onDropFile` too, so the panel's set is empty and the\n * union is exactly your set.\n */\n env?: FieldEnv;\n /**\n * Stores a dropped file and returns its URL. The host performs the upload —\n * this library never touches an API client — while the panel owns the\n * bookkeeping around it: marking the field busy and writing the result back\n * at the field's dotted path.\n */\n uploadFile?: (file: File, fieldId: string) => Promise<UploadedFile>;\n /** Panel header. The host names the pipe; there is no default heading. */\n title?: string;\n /**\n * The resolved theme, which drives the kernel's `.dark` class on the panel\n * container alongside this library's own palette variables. Defaults to\n * `light`. Pair it with `GraphViewer`'s `onThemeChange`, whose second\n * argument is exactly this resolved value.\n */\n theme?: GraphTheme;\n /**\n * Renders the whole error summary in the host's language. English by default.\n *\n * It covers the panel's own lines (`runPanel.*`) as well as the kernel's\n * per-error ones, because which of them a rejected run produces is the\n * kernel's decision and not something a host can predict.\n */\n translate?: RunPanelTranslate;\n /**\n * Makes this panel's control DOM ids predictable: a control at value path\n * `cv` gets `id=\"<idPrefix>-cv\"`, and `\"\"` writes the path unprefixed.\n *\n * Ids are namespaced whether or not you pass this. A control's `id` must be\n * unique in the DOCUMENT while a value path is unique only within one form,\n * so two panels each declaring an input named `text` would otherwise emit two\n * `id=\"text\"` — and a `<label for>` binds the first match in tree order,\n * which hands the second panel's label to the first panel's control and\n * leaves the second input with no accessible name. Left unset, the prefix\n * comes from `useId`: unique per panel and hydration-stable, but opaque, so\n * nothing outside can predict it.\n *\n * Pass one when something must ADDRESS a control — `getElementById`, a\n * deep-link that focuses a field, an end-to-end selector. Doing so moves the\n * uniqueness obligation to you: the prefix scopes this whole panel, so it\n * must be unique in the document, and `\"\"` restores the ids this panel emitted\n * before the kernel's fix along with the collision they had.\n */\n idPrefix?: string;\n /** Appended to the panel container's class list. */\n className?: string;\n}\n\nconst EMPTY_IDS: ReadonlySet<string> = new Set<string>();\nconst NOTHING_REVEALED: ReadonlySet<string> = new Set<string>();\n\n/**\n * A pipe's input form: fields, readiness, and the run gate, composed.\n *\n * Every question of meaning is the form kernel's — `fieldsForContract` decides\n * what each input IS, `computeReadiness` decides whether the form MAY run, and\n * `runSubmitGate` (over the kernel's four steps) decides what goes on the wire.\n * What lives here is layout: which fields are visible, where the toggle and the\n * button sit, and how a rejected verdict is shown. The panel never reads\n * `json_schema` to make a rendering decision and never sniffs a value's shape\n * (design Decision C) — which is what keeps a change to the kernel's derivation\n * invisible to this repo.\n */\nexport function RunPanel({\n contract,\n descriptor,\n values,\n onValuesChange,\n onRun,\n running = false,\n env,\n uploadFile,\n title,\n theme = GRAPH_THEME.LIGHT,\n translate,\n idPrefix,\n className,\n}: RunPanelProps) {\n const [showOptional, setShowOptional] = React.useState(false);\n const [uploadingIds, setUploadingIds] = React.useState<ReadonlySet<string>>(EMPTY_IDS);\n const [submitError, setSubmitError] = React.useState<string | null>(null);\n const readinessHintId = React.useId();\n const fallbackPrefix = React.useId();\n\n const fields = React.useMemo(\n () => fieldsForContract(contract, descriptor),\n [contract, descriptor],\n );\n const readiness = React.useMemo(() => computeReadiness(fields, values), [fields, values]);\n\n const commitValues = React.useCallback(\n (next: Record<string, unknown>) => {\n // A summary describes the values that produced it; the moment they move\n // it is stale, and a stale complaint about a field just corrected is\n // worse than none.\n setSubmitError(null);\n onValuesChange(next);\n },\n [onValuesChange],\n );\n\n // An upload resolves long after the render that started it, so the write-back\n // must not target the values THAT render captured — the user may well have\n // typed elsewhere while waiting. This mirror holds the latest ones. Only the\n // async continuation reads it; every synchronous path stays on the prop,\n // which is correct there because it runs inside the render that captured it.\n // Layout, not passive, for the reason spelled out on the contract marker\n // below: a mirror that lags the commit is read by continuations landing in\n // exactly that lag, and here the cost is the host's newer edits overwritten.\n const valuesRef = React.useRef(values);\n React.useLayoutEffect(() => {\n valuesRef.current = values;\n }, [values]);\n\n // Switching pipes makes an upload started under the old one irrelevant — and\n // worse than irrelevant when both pipes declare the same input name, which\n // happens within a single method: `recruitment.cv_screening` and\n // `recruitment.extract_cv` both take a required `cv` document. A file chosen\n // for one would land in the other looking like a deliberate answer, gating\n // satisfied, ready to run. So a drop remembers the generation it happened\n // under, and a result arriving under a later one is dropped. This makes\n // `contract` referentially significant — a host that rebuilds it every render\n // loses its uploads (and is already rebuilding every field, since `fields`\n // memoizes on it).\n //\n // The marker COUNTS rather than naming the contract, because identity answers\n // a subtly different question. A host can leave pipe A and come back to the\n // very same contract object while A's upload is still in flight, and an\n // identity check then says \"same pipe, accept it\" — ABA, and it is not the\n // same form: switching away emptied `uploadingIds`, so that upload stopped\n // gating Run and its dropzone re-opened. Accepting it lands a file in a form\n // that never marked it — Run was live throughout, and for a non-gating input\n // the run may already have gone out without it — and, if the user re-dropped\n // on the way back, overwrites the fresh result while un-marking an upload\n // still in progress. Counting makes the predicate \"has the form moved on\n // since this drop\", which is what the guard always meant.\n //\n // A LAYOUT effect, deliberately. The marker answers \"which form is on\n // screen\", and it has to answer that as of the last COMMIT. A passive effect\n // answers later: React schedules those on a task, while an upload settling is\n // a promise continuation — a microtask, which runs first. In that window the\n // switched form is already rendered and the marker still counts the pipe the\n // user left, so the guard compares the departed generation against itself and\n // lets through precisely the write it exists to reject. Bumping during render\n // would close the window too, and is worse: a concurrent render React\n // abandons would advance past a contract that never commits, and an upload\n // belonging to the pipe still on screen would be thrown away instead. Layout\n // effects run inside the commit, and only for renders that commit.\n //\n // The bump lives in the CLEANUP, which is the one place BOTH ways of leaving a\n // form pass through. Changing `contract` is only one of them: a host that\n // writes `<RunPanel key={pipeRef} …>` — the ordinary React idiom for \"reset\n // this child when the entity changes\" — never changes the prop at all, it\n // unmounts this instance and mounts another. The effect body would then never\n // re-run, the marker would still equal the departed upload's `startedAt`, and\n // that upload's continuation would call `onValuesChange`, which is the HOST's\n // setter and very much still alive. The abandoned file would land in the\n // replacement panel's `cv` exactly as if it had been chosen there. Bumping on\n // cleanup makes the predicate mean what it says — this form instance has\n // stopped being the one on screen — however it stopped.\n const generationRef = React.useRef(0);\n React.useLayoutEffect(() => {\n // Whatever was in flight belonged to the previous pipe; it must not go on\n // gating this form's Run button until it happens to settle.\n setUploadingIds(EMPTY_IDS);\n // Nor may a complaint about the previous pipe's inputs stand over this\n // pipe's form: it names fields that are no longer on screen.\n setSubmitError(null);\n return () => {\n generationRef.current += 1;\n };\n }, [contract]);\n\n const handleDropFile = React.useCallback(\n (id: string, file: File) => {\n if (!uploadFile) return;\n const startedAt = generationRef.current;\n setUploadingIds((previous) => new Set(previous).add(id));\n // Called through an async wrapper rather than directly. `uploadFile` is\n // the HOST's function and is typed as a plain function returning a\n // promise — nothing obliges it to be `async`. One that validates before\n // it starts the request (no API key configured, a file over the size\n // limit, a mime type it will not take) throws SYNCHRONOUSLY, and a\n // synchronous throw happens before there is a promise to hang `.catch()`\n // and `.finally()` on. The id marked one line above would then stay\n // marked for good: `uploading` disables the dropzone, so the field cannot\n // even be retried, and Run stays gated until the user leaves the form\n // entirely. The wrapper turns that throw into the rejection this chain\n // already cleans up, which makes the SAME host logic behave the same way\n // whether it was written `async` or not — the only difference between\n // those two spellings being the keyword.\n void (async () => await uploadFile(file, id))()\n .then(\n (uploaded) => {\n if (generationRef.current !== startedAt) return;\n const next = setValueAtPath(valuesRef.current, id.split(\".\"), {\n url: uploaded.url,\n filename: uploaded.filename ?? file.name,\n });\n // Advance the mirror here rather than waiting for the effect. Two\n // uploads finishing in the same React batch both run before any\n // re-render, so both would otherwise read the same snapshot and the\n // second write would drop the first — a pipe with two file inputs is\n // ordinary (`candidate_screening.screen_candidate` takes a `cv` and a\n // `job_offer`), and the loss is silent: both fields look settled\n // while one value is gone. The effect still owns syncing FROM the\n // host, so the mirror converges on whatever the host actually kept.\n valuesRef.current = next;\n commitValues(next);\n },\n () => {\n // The host owns how a failed upload is announced (it owns the\n // transport). Swallowing it here keeps a rejected promise from\n // becoming an unhandled rejection; the field simply stays empty.\n //\n // This is `.then`'s SECOND ARGUMENT rather than a `.catch()` link,\n // and the difference is the whole point: a `.catch()` chained after\n // the handler above also catches whatever that handler throws, and\n // the handler dereferences a value the host produced. `uploadFile`\n // is typed `Promise<UploadedFile>`, but a type is not a runtime\n // guarantee — a host branch that forgets its `return`, or unwraps a\n // JSON response one level too few, resolves to `undefined`, and\n // `uploaded.url` then throws. Swallowed, that is the one failure in\n // this whole lifecycle with NO diagnostic surface anywhere: the\n // spinner clears, the field reads empty, nothing is logged, and\n // re-dropping repeats it forever. As the second argument this\n // handler covers only the upload's own rejection, which is all it\n // ever claimed to; a write-back bug stays an unhandled rejection,\n // where the host's error reporting can see it.\n },\n )\n .finally(() => {\n // The same generation check as the write-back, for a sharper reason.\n // Switching contracts already emptied this set, and the very same id\n // may since have been re-added by a NEW upload under the new\n // generation — nothing exotic, since that is what sharing the `cv`\n // input name means, and a host returning to the pipe it left reaches\n // it with no rename at all. Deleting it here would un-mark an upload\n // that is still running: its dropzone re-enables mid-flight, its\n // progress indicator vanishes, and the Run gate lets go of it.\n if (generationRef.current !== startedAt) return;\n setUploadingIds((previous) => {\n const next = new Set(previous);\n next.delete(id);\n return next;\n });\n });\n },\n // `contract` is no longer a dependency: the generation is read off the ref\n // at drop time, so this callback does not need rebuilding per pipe.\n [uploadFile, commitValues],\n );\n\n const fieldEnv = React.useMemo<FieldEnv>(\n () => ({\n ...env,\n disabled: env?.disabled ?? running,\n onDropFile: env?.onDropFile ?? (uploadFile ? handleDropFile : undefined),\n // The UNION, not the host's set winning outright: `onDropFile` and\n // `uploadingIds` default independently per key, so a host may hand us\n // `uploadFile` (leaving the panel to own the drop, and to mark the field\n // in its own set) while also passing a tracker of its own. Letting the\n // host's set replace ours would hide an upload the panel itself started,\n // and Run would stay live through it. A host that owns the whole loop\n // supplies `onDropFile` too, so our set is empty and the union is exactly\n // its set — \"yours win\" still holds where that clause applies.\n uploadingIds: new Set([...(env?.uploadingIds ?? EMPTY_IDS), ...uploadingIds]),\n }),\n [env, running, uploadFile, handleDropFile, uploadingIds],\n );\n\n // Readiness alone would let a run go out while a file is still uploading: a\n // non-gating file input (optional, or plural — `mustBeFilled` excludes lists)\n // never counts toward readiness, so Run stays live through its upload and the\n // method would run with the file simply absent. Read off `fieldEnv` rather\n // than the raw state, so a host driving its own loop through\n // `env.uploadingIds` gets the same gate.\n const uploading = (fieldEnv.uploadingIds?.size ?? 0) > 0;\n const notReady = readiness.missing.length > 0;\n\n // ONE expression, read by the button and by the submit path, because they are\n // the same question asked twice and any gap between them is reachable: the\n // panel puts a real `<form>` in the host's DOM under a class name the docs\n // offer as a hook, and `form.requestSubmit()` ignores the submitter entirely,\n // so a disabled button stops nothing there. (The keyboard it does stop: Run is\n // this form's only submit button — every control the kernel renders carries\n // `type=\"button\"` — hence the default button, and implicit submission on a\n // disabled default button does nothing. Measured, not assumed.)\n //\n // Each term has to be here on its own merit, and none is redundant with the\n // kernel gate that runs afterwards. `uploading`: a non-gating file input\n // (optional, or plural — `mustBeFilled` excludes lists) never counts toward\n // readiness, so without this the method runs with the file simply absent.\n // `notReady`: an empty REQUIRED text input reaches ajv as `{ text: \"\" }`, a\n // perfectly valid string, so the gate passes it and only readiness notices —\n // that is pinned in `runGate.test.ts` and is exactly why the button gates on\n // readiness at all. `running`: a second run started over the first is a\n // duplicate execution, which nothing downstream would undo — and this term is\n // only ever as timely as the host makes it, which is why the prop's own doc\n // asks for it synchronously. `env.disabled`: the panel already treats the\n // host's flag and `running` as ONE concept where the fields are concerned\n // (`fieldEnv.disabled` falls back from one to the other), so gating the\n // button on only one of the two sources was the same asymmetry rounds 4 and 9\n // turned up — a host freezing the form got every input inert and a live Run\n // button beside them, and a click there starts a run from a form the user\n // cannot edit. It is read off the prop rather than off `fieldEnv.disabled`\n // because those differ exactly when the host passes `disabled: false` while\n // `running` is true, and there the button must still be held.\n const blocked = running || notReady || uploading || (env?.disabled ?? false);\n\n // Optional inputs that are still empty stay folded, so the form opens at its\n // simplest shape and grows on demand. Required or already-filled always show.\n //\n // Which optionals are revealed is LATCHED rather than re-derived from the\n // current value, and that is the whole point. Deriving it live means an\n // optional the user empties disappears from under the cursor: clearing the\n // last character flips `isFilled`, the field drops out of `visibleFields`,\n // and React unmounts the very input being typed into — focus lost, every\n // keystroke after it going nowhere. It needs no host pre-fill to reach:\n // expand the fold, fill an optional, collapse it (it stays on screen because\n // it is filled), then clear it.\n //\n // So between fold transitions the form's shape only ever GROWS. The latch is\n // cleared by the toggle's own handler and by a contract change, which are the\n // two moments nobody is mid-edit; each is followed by the grow pass below\n // re-adding every optional that still holds a value, so an emptied one folds\n // back. Nothing about readiness or the wire depends on this — an emptied\n // optional is omitted from the payload whether or not its input is on screen.\n //\n // The latch is STATE adjusted during render, not a ref mutated during render,\n // and in a library component that distinction is load-bearing. A ref written\n // during render survives a render that never commits, so a host rendering the\n // panel inside a transition — which App Router navigation does by default —\n // can have an abandoned render for contract B wipe the set that the committed\n // contract A is still displaying from, and an optional the user revealed,\n // collapsed and emptied vanishes on A's next render. That is the very defect\n // this latch exists to prevent, re-entering by another door. React discards a\n // state update from an abandoned render, so the two-`useState` form below is\n // the one that actually holds. It is React's documented shape for adjusting\n // state when a prop changes, and it converges: after the update lands, the\n // reset branch and `newlyRevealed` are both empty on the following pass.\n const [revealed, setRevealed] = React.useState<ReadonlySet<string>>(NOTHING_REVEALED);\n const [latchedFor, setLatchedFor] = React.useState(contract);\n // Read this render off the local value, never off `revealed`: a state update\n // made during render is not visible until the re-render it schedules, and the\n // output computed below has to already reflect it.\n let revealedNow = revealed;\n if (latchedFor !== contract) {\n revealedNow = NOTHING_REVEALED;\n setLatchedFor(contract);\n setRevealed(revealedNow);\n }\n const newlyRevealed = fields.filter(\n (field) => !field.required && isFilled(values[field.name]) && !revealedNow.has(field.name),\n );\n if (newlyRevealed.length > 0) {\n revealedNow = new Set([...revealedNow, ...newlyRevealed.map((field) => field.name)]);\n setRevealed(revealedNow);\n }\n const isFolded = React.useCallback(\n (field: RunField) => !field.required && !revealedNow.has(field.name),\n [revealedNow],\n );\n // What a collapse would hide — every EMPTY optional — rather than what is\n // hidden right now. The two differ only for an optional that was revealed and\n // then emptied, and that difference is the whole affordance: counting the\n // latched-visible ones leaves the count at zero there, the toggle unrendered,\n // and the empty input stuck on screen with no control that would fold it. The\n // latch keeps deciding what renders; it just stops deciding whether the user\n // is offered a way out of it.\n const foldableCount = fields.filter(\n (field) => !field.required && !isFilled(values[field.name]),\n ).length;\n const visibleFields = showOptional ? fields : fields.filter((field) => !isFolded(field));\n\n // A submit that has passed the gate latches until the end of the current task.\n //\n // `blocked` is computed during render, so two `requestSubmit()` calls made in\n // ONE synchronous host callback both close over the same render and both pass\n // it — even when the host sets `running` synchronously inside `onRun`, exactly\n // as that prop's doc demands, because React has not re-rendered in between.\n // Two real clicks are two separate tasks with a commit between them and were\n // never affected; this closes the programmatic path, which is a surface this\n // component documents and pins a story against.\n //\n // The release is a microtask, and that is the whole reason this is safe rather\n // than the lock this component deliberately does NOT take. A lock held for the\n // lifetime of a run would have no release — the panel is told when a run\n // starts and never that one finished — so a host that never passes `running`\n // would be wedged after its first run. This latch outlives nothing: the\n // microtask is queued unconditionally and always runs, so it cannot wedge, and\n // it holds no run state a later render could disagree with.\n const justSubmittedRef = React.useRef(false);\n\n const handleSubmit = (event: React.FormEvent) => {\n event.preventDefault();\n // The submit PATH owns the gate, not the button's `disabled` attribute —\n // see `blocked` above for why each term is there and why none is covered by\n // the kernel gate that runs next.\n if (blocked || justSubmittedRef.current) return;\n const outcome = runSubmitGate(contract, fields, values, translate);\n if (!outcome.ok) {\n setSubmitError(outcome.summary);\n return;\n }\n setSubmitError(null);\n // Latched BEFORE `onRun`, so a host that re-enters synchronously from inside\n // its own handler is held by the same guard.\n justSubmittedRef.current = true;\n queueMicrotask(() => {\n justSubmittedRef.current = false;\n });\n onRun(outcome.apiInputs);\n };\n\n const paletteStyle = React.useMemo(\n () => getPaletteForTheme(theme) as React.CSSProperties,\n [theme],\n );\n\n // One provider around the whole panel, which is exactly one form — the scope\n // the kernel's contract asks for. `fallbackPrefix` is this panel's own\n // `useId`, so an unset `idPrefix` keeps the kernel's default behaviour (unique\n // per instance) rather than turning the namespacing off.\n return (\n <FieldDomIdProvider prefix={idPrefix ?? fallbackPrefix}>\n <form\n className={[\"mthds-run-panel\", theme === GRAPH_THEME.DARK && \"dark\", className]\n .filter(Boolean)\n .join(\" \")}\n style={paletteStyle}\n // The panel's gate is the authority on whether a run may go out, and the\n // browser's is not merely redundant beside it — it is WRONG. The kernel's\n // number control carries `step={integer ? 1 : 0.1}`, so any float with\n // more than one decimal is a `stepMismatch` even though `native.Number`\n // accepts it and the kernel's own ajv pass does too. Interactive\n // validation then aborts submission before `onSubmit` runs at all: no\n // gate, no `onRun`, no error from the panel, and a native message that is\n // wrong about the domain (\"nearest valid values are 87.2 and 87.3\").\n // Turning it off is what makes the documented `requestSubmit()` promise\n // true, and costs nothing — every constraint that should hold is checked\n // by `runSubmitGate`.\n noValidate\n onSubmit={handleSubmit}\n >\n {title && <h2 className=\"mthds-run-panel-title\">{title}</h2>}\n\n {fields.length > 0 ? (\n <div className=\"mthds-run-panel-fields\">\n {visibleFields.map((field) => (\n <FieldRenderer\n key={field.name}\n field={field}\n value={values[field.name]}\n onChange={(value) => commitValues({ ...values, [field.name]: value })}\n id={field.name}\n env={fieldEnv}\n />\n ))}\n {foldableCount > 0 && (\n <OptionalToggle\n count={foldableCount}\n expanded={showOptional}\n onToggle={() => {\n setShowOptional((expanded) => !expanded);\n // Clearing the latch here is what makes \"the shape only grows\"\n // a temporary state rather than a permanent one. A fold\n // transition is a click on this button, so nobody is mid-edit\n // in a field, and the render below re-adds every optional that\n // still holds a value — leaving exactly the emptied ones to\n // fold away, which is what the user just asked for.\n setRevealed(NOTHING_REVEALED);\n }}\n noun=\"input\"\n />\n )}\n </div>\n ) : (\n <p className=\"mthds-run-panel-empty\">This pipe takes no inputs.</p>\n )}\n\n {submitError && (\n <p className=\"mthds-run-panel-error\" role=\"alert\">\n {submitError}\n </p>\n )}\n\n <div className=\"mthds-run-panel-footer\">\n {/*\n The readiness line is the only thing on screen that says WHY Run is\n disabled, so it has to be reachable by the people who cannot see it\n sitting next to the button — and a disabled button is out of the tab\n order, so they will not arrive at it by walking the controls either.\n `aria-describedby` is the association that makes a screen reader read\n the reason with the button instead of announcing a bare dimmed \"Run\".\n The id comes from `useId` because a host may put two panels on one\n page, and a hardcoded one would make the second panel describe the\n first panel's button.\n */}\n <button\n type=\"submit\"\n className=\"mthds-run-panel-run\"\n disabled={blocked}\n aria-describedby={notReady ? readinessHintId : undefined}\n >\n {running ? \"Running…\" : \"Run\"}\n </button>\n {notReady && (\n <span id={readinessHintId} className=\"mthds-run-panel-readiness\">\n {`${readiness.ready} of ${readiness.total} ready — still needed: ${readiness.missing.join(\", \")}`}\n </span>\n )}\n </div>\n </form>\n </FieldDomIdProvider>\n );\n}\n","/**\n * The panel's submit path, kept React-free.\n *\n * Everything between \"the user pressed Run\" and \"the payload goes on the wire\"\n * is the kernel's (`@pipelex/mthds-form`, `core/gate.ts`); this module bridges\n * the panel's values into the shape the gate takes, calls it, and turns the\n * verdict into one line a panel can show. Splitting it out of `RunPanel.tsx` is\n * what lets the node vitest project test that seam without mounting React — the\n * gate's own behavior is covered by the kernel's tests and is deliberately not\n * re-tested here.\n */\nimport {\n describeValidationError,\n gateRunInputs,\n rjsfDataFromRunValues,\n type PipeIOContract,\n type RunField,\n type RunInputError,\n type ValidationMessageKey,\n} from \"@pipelex/mthds-form\";\n\n/**\n * How many ajv errors the summary quotes when the missing-input scan comes up\n * empty. A form with a deep mismatch can produce dozens; past a handful the\n * line stops being readable and the user acts on the first one anyway.\n */\nconst MAX_QUOTED_ERRORS = 3;\n\n/** The outcome of one submit attempt. */\nexport type RunGateOutcome =\n | { ok: true; apiInputs: Record<string, unknown> }\n | { ok: false; missingInputs: string[]; summary: string };\n\n/**\n * Every message key the error summary can render — the kernel's, plus the two\n * this panel owns.\n *\n * The kernel's keys describe one ajv error each and are rendered by\n * `describeValidationError`. The `runPanel.` ones are lines the panel writes\n * itself, around those errors, and they carry the prefix precisely so the\n * ownership is legible at the call site and in a host's translation file.\n *\n * Nesting the kernel's union inside ours is deliberate: a key the kernel ADDS\n * lands here automatically and then fails `VALIDATION_STRINGS`'s exhaustiveness\n * until it is given a string, which is how a kernel bump is stopped from\n * silently rendering `undefined` at a user.\n */\nexport type RunPanelMessageKey =\n | ValidationMessageKey\n | \"runPanel.missingInputs\"\n | \"runPanel.fillRequired\";\n\n/**\n * The panel's i18n seam — the kernel's `Translate` widened to the panel's own\n * keys. It accepts a superset of what the kernel asks for, so it is still a\n * valid `Translate` wherever the kernel wants one.\n */\nexport type RunPanelTranslate = (\n key: RunPanelMessageKey,\n values?: Record<string, string>,\n) => string;\n\n/**\n * English defaults for every key above. The kernel deliberately ships no\n * defaults for its own (its `Translate` contract is host-supplied), so a panel\n * that wants to say anything at all has to name them — the wording matches the\n * canonical consumer's `en.json` so the two surfaces read alike.\n */\nconst VALIDATION_STRINGS: Record<RunPanelMessageKey, string> = {\n \"inputPanel.aDateField\": \"A date field\",\n \"inputPanel.pickValidDate\": \"{label}: “{value}” is not a valid date\",\n \"inputPanel.pickValidDateEmpty\": \"{label}: pick a valid date\",\n \"inputPanel.dateCarriesTime\":\n \"{label}: “{value}” carries a time, but a date field takes the day alone — use “{day}” and put the time in the “time” field\",\n \"inputPanel.invalidValue\": \"Invalid value\",\n \"inputPanel.invalidValueWithData\": \"{stack} (value: “{value}”)\",\n \"runPanel.missingInputs\": \"Missing required fields in: {inputs}\",\n \"runPanel.fillRequired\": \"Please fill in all required fields before running.\",\n};\n\n/**\n * The default translator: the English strings above with `{placeholder}`\n * interpolation. A host that localizes passes its own translator instead.\n */\nexport const defaultValidationTranslate: RunPanelTranslate = (key, values) => {\n const template = VALIDATION_STRINGS[key];\n if (!values) return template;\n return template.replace(/\\{(\\w+)\\}/g, (whole, name: string) => values[name] ?? whole);\n};\n\n/**\n * One human line for an invalid verdict.\n *\n * `missingInputs` is the good case: it names the VARIABLES at fault, which is\n * what the user clicks on. The scan can legitimately come up empty on an\n * invalid form (a wrong value shape, a nested mismatch), and that is exactly\n * when falling back to the raw ajv errors keeps the failure diagnosable rather\n * than silent — see `validateRunInputs` in the kernel.\n *\n * All three routes go through `t`. Which one runs is decided by the kernel's\n * verdict, so a host that localizes cannot know in advance which line it will\n * be shown — leaving any of them in hardcoded English would surface as the\n * summary silently reverting to English on some inputs and not others.\n */\nexport function summarizeVerdict(\n missingInputs: string[],\n errors: RunInputError[],\n preparedData: unknown,\n t: RunPanelTranslate = defaultValidationTranslate,\n): string {\n if (missingInputs.length > 0) {\n return t(\"runPanel.missingInputs\", { inputs: missingInputs.join(\", \") });\n }\n const described = errors\n .slice(0, MAX_QUOTED_ERRORS)\n .map((error) => describeValidationError(error, t, preparedData))\n .join(\"; \");\n return described || t(\"runPanel.fillRequired\");\n}\n\n/**\n * Run the kernel's gate over the panel's current values.\n *\n * `gateRunInputs` IS the gate — the whole chain in one call — so all this\n * module does either side of it is bridge and phrase: the panel holds values\n * keyed by variable, the gate takes the schema-shaped data those become, and a\n * rejected verdict has to come back as one line rather than as a discriminated\n * union the panel would have to read.\n *\n * Assembling those steps here instead was the earlier shape and it was wrong in\n * a way nothing local could see. The chain's schema validation is not the whole\n * rule: ajv's `required` only asserts a key is PRESENT, and a content model\n * carries no `minLength`, so a required input that arrived empty — `{text: \"\"}`\n * from an untouched text field, `{url: \"\"}` from an untouched file field —\n * satisfied the schema and the run went out. The Run button refuses exactly\n * those, through `computeReadiness`. The gate now re-applies that same\n * emptiness rule with `computeReadiness`'s own functions over the same derived\n * fields, which makes the button's verdict and this one a single invariant\n * rather than two things that resemble each other.\n */\nexport function runSubmitGate(\n contract: PipeIOContract,\n fields: RunField[],\n values: Record<string, unknown>,\n t: RunPanelTranslate = defaultValidationTranslate,\n): RunGateOutcome {\n const verdict = gateRunInputs(contract, rjsfDataFromRunValues(values, fields));\n if (!verdict.ok) {\n return {\n ok: false,\n missingInputs: verdict.missingInputs,\n summary: summarizeVerdict(verdict.missingInputs, verdict.errors, verdict.preparedData, t),\n };\n }\n return { ok: true, apiInputs: verdict.inputs };\n}\n"],"mappings":"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","names":["_a","_b","_c"]}
@@ -1,5 +1,5 @@
1
- import { p as GraphSpecNode, r as PipeCallNode, b as GraphSpec, D as DataflowAnalysis, C as ConceptInfo, P as PipeBlueprintUnion, s as GraphData, o as GraphSpecMode, t as PipeCardPayload, m as FoldToggleOptions, e as GraphNode, d as GraphEdge, c as GraphNodeData, N as NodeValidationSummary, V as ValidationIssue, j as GraphSpecNodeIoItem, g as GraphDirection, u as LayoutConfig, f as GraphConfig, T as ToolbarPosition, G as GraphTheme } from '../types-Cn2FttBC.js';
2
- export { A as ARROW_CLOSED_MARKER, v as CONTROLLER_PADDING_BOTTOM, w as CONTROLLER_PADDING_TOP, x as CONTROLLER_PADDING_X, E as EDGE_TYPE, y as EdgeType, z as FOLD_MODE, B as FieldResolution, F as FoldMode, H as GRAPH_DIRECTION, I as GRAPH_SPEC_MODE, J as GRAPH_THEME, K as GRAPH_THEME_MODE, M as GraphSpecEdge, O as GraphSpecEdgeKind, Q as GraphSpecMeta, R as GraphSpecModelUsage, S as GraphSpecNodeError, U as GraphSpecNodeIo, W as GraphSpecNodeTiming, X as GraphSpecNodeUsage, Y as GraphSpecUsage, h as GraphThemeMode, Z as KNOWN_PIPE_TYPES, L as LabelDescriptor, _ as NODE_TYPE_CONTROLLER, $ as NODE_TYPE_PIPE_CARD, a0 as NODE_TYPE_STUFF, a1 as NodeKind, a2 as PipeBatchBlueprint, a3 as PipeBlueprintBase, a4 as PipeComposeBlueprint, a5 as PipeComposeConstructBlueprint, a6 as PipeComposeConstructField, a7 as PipeConditionBlueprint, l as PipeControllerType, a8 as PipeExtractBlueprint, a9 as PipeFuncBlueprint, aa as PipeImgGenBlueprint, ab as PipeLLMBlueprint, q as PipeOperatorType, ac as PipeParallelBlueprint, ad as PipeSearchBlueprint, ae as PipeSequenceBlueprint, af as PipeSignatureBlueprint, i as PipeStatus, ag as PipeStructureBlueprint, n as PipeType, a as PresenceMarker, ah as STUFF_ID_PREFIX, ai as StuffRole, aj as StuffSpecInfo, ak as SubPipeSpec, al as TOOLBAR_POSITION, am as TemplateBlueprint, an as ToolbarOrientation, ao as ToolbarSide, ap as VALIDATION_STATE, k as ValidationState, aq as graphSpecMode, ar as isDryGraphSpec, as as isStaticGraphSpec, at as isStuffNodeId, au as nodeHeight, av as nodeWidth, aw as stuffDigestFromId, ax as stuffNodeId, ay as toolbarOrientation, az as toolbarSide } from '../types-Cn2FttBC.js';
1
+ import { p as GraphSpecNode, r as PipeCallNode, b as GraphSpec, D as DataflowAnalysis, C as ConceptInfo, P as PipeBlueprintUnion, s as GraphData, o as GraphSpecMode, t as PipeCardPayload, m as FoldToggleOptions, e as GraphNode, d as GraphEdge, c as GraphNodeData, N as NodeValidationSummary, V as ValidationIssue, j as GraphSpecNodeIoItem, g as GraphDirection, u as LayoutConfig, f as GraphConfig, T as ToolbarPosition, G as GraphTheme } from '../types-DgqW3Jg4.js';
2
+ export { A as ARROW_CLOSED_MARKER, v as CONTROLLER_PADDING_BOTTOM, w as CONTROLLER_PADDING_TOP, x as CONTROLLER_PADDING_X, E as EDGE_TYPE, y as EdgeType, z as FOLD_MODE, B as FieldResolution, F as FoldMode, H as GRAPH_DIRECTION, I as GRAPH_SPEC_MODE, J as GRAPH_THEME, K as GRAPH_THEME_MODE, M as GraphSpecEdge, O as GraphSpecEdgeKind, Q as GraphSpecMeta, R as GraphSpecModelUsage, S as GraphSpecNodeError, U as GraphSpecNodeIo, W as GraphSpecNodeTiming, X as GraphSpecNodeUsage, Y as GraphSpecUsage, h as GraphThemeMode, Z as KNOWN_PIPE_TYPES, L as LabelDescriptor, _ as NODE_TYPE_CONTROLLER, $ as NODE_TYPE_PIPE_CARD, a0 as NODE_TYPE_STUFF, a1 as NodeKind, a2 as PipeBatchBlueprint, a3 as PipeBlueprintBase, a4 as PipeComposeBlueprint, a5 as PipeComposeConstructBlueprint, a6 as PipeComposeConstructField, a7 as PipeConditionBlueprint, l as PipeControllerType, a8 as PipeExtractBlueprint, a9 as PipeFuncBlueprint, aa as PipeImgGenBlueprint, ab as PipeLLMBlueprint, q as PipeOperatorType, ac as PipeParallelBlueprint, ad as PipeSearchBlueprint, ae as PipeSequenceBlueprint, af as PipeSignatureBlueprint, i as PipeStatus, ag as PipeStructureBlueprint, n as PipeType, a as PresenceMarker, ah as STUFF_ID_PREFIX, ai as StuffMultiplicity, aj as StuffRegistryEntry, ak as StuffRole, al as StuffSpecInfo, am as SubPipeSpec, an as TOOLBAR_POSITION, ao as TemplateBlueprint, ap as ToolbarOrientation, aq as ToolbarSide, ar as VALIDATION_STATE, k as ValidationState, as as graphSpecMode, at as isDryGraphSpec, au as isPluralMultiplicity, av as isStaticGraphSpec, aw as isStuffNodeId, ax as multiplicitySuffix, ay as nodeHeight, az as nodeWidth, aA as stuffDigestFromId, aB as stuffNodeId, aC as toolbarOrientation, aD as toolbarSide } from '../types-DgqW3Jg4.js';
3
3
  export { P as ParsedPipeRef, m as makePipeRef, p as parsePipeRef } from '../pipeRefs-BQJO5nh5.js';
4
4
  import { ElkNode } from 'elkjs/lib/elk-api';
5
5
 
@@ -34,9 +34,11 @@ declare class GraphSpecValidationError extends Error {
34
34
  * `GraphSpecValidationError` on the first violation.
35
35
  *
36
36
  * **Mutates the input in place:** on success it returns the *same* object
37
- * (typed as `GraphSpec`), and where a node has no `io` key it writes
37
+ * (typed as `GraphSpec`), with its normalizations written directly onto the
38
+ * passed-in object: where a node has no `io` key it writes
38
39
  * `io = { inputs: [], outputs: [] }` (and fills absent `io.inputs` /
39
- * `io.outputs` arrays) directly onto the passed-in object. Pass a fresh
40
+ * `io.outputs` arrays), and an absent unrated cost is written as `null`, so
41
+ * code downstream never meets `undefined` where the types promise `null`. Pass a fresh
40
42
  * parse result (e.g. straight from `JSON.parse`) — do not pass a frozen or
41
43
  * externally shared object.
42
44
  */
@@ -29,7 +29,7 @@ import {
29
29
  resolveConceptRef,
30
30
  resolveIssueTargetNodeId,
31
31
  validateGraphSpec
32
- } from "../chunk-FD4PKRGZ.js";
32
+ } from "../chunk-K3BWHQVX.js";
33
33
  import {
34
34
  makePipeRef,
35
35
  parsePipeRef
@@ -40,7 +40,7 @@ import {
40
40
  LIGHT_PALETTE_COLORS,
41
41
  getPaletteColors,
42
42
  getPaletteForTheme
43
- } from "../chunk-HASMGCVK.js";
43
+ } from "../chunk-XBHDA3PQ.js";
44
44
  import {
45
45
  ARROW_CLOSED_MARKER,
46
46
  CONTROLLER_PADDING_BOTTOM,
@@ -61,15 +61,17 @@ import {
61
61
  VALIDATION_STATE,
62
62
  graphSpecMode,
63
63
  isDryGraphSpec,
64
+ isPluralMultiplicity,
64
65
  isStaticGraphSpec,
65
66
  isStuffNodeId,
67
+ multiplicitySuffix,
66
68
  nodeHeight,
67
69
  nodeWidth,
68
70
  stuffDigestFromId,
69
71
  stuffNodeId,
70
72
  toolbarOrientation,
71
73
  toolbarSide
72
- } from "../chunk-RJVEDP7M.js";
74
+ } from "../chunk-VCO4GZDZ.js";
73
75
  import "../chunk-2NMEKWO5.js";
74
76
  export {
75
77
  ARROW_CLOSED_MARKER,
@@ -119,9 +121,11 @@ export {
119
121
  graphSpecMode,
120
122
  inputPortId,
121
123
  isDryGraphSpec,
124
+ isPluralMultiplicity,
122
125
  isStaticGraphSpec,
123
126
  isStuffNodeId,
124
127
  makePipeRef,
128
+ multiplicitySuffix,
125
129
  nodeHeight,
126
130
  nodeWidth,
127
131
  outermostFoldedAncestor,
@@ -569,6 +569,11 @@
569
569
  color: var(--text-muted);
570
570
  }
571
571
 
572
+ .detail-multiplicity {
573
+ font-size: 12px;
574
+ color: var(--text-muted);
575
+ }
576
+
572
577
  .detail-refines-code {
573
578
  font-family: var(--font-mono);
574
579
  color: var(--color-accent);
@@ -11,7 +11,10 @@
11
11
  * cases still render with valid colors / fonts / shadows:
12
12
  * - first paint before the React inline-palette effect runs
13
13
  * - SSR / hydration mismatches
14
- * - a consumer rendering a graph subcomponent outside `GraphViewer`
14
+ * - a consumer rendering a graph subcomponent outside `GraphViewer`, as
15
+ * long as it wraps that subcomponent in an element carrying the
16
+ * `.react-flow-container` class: the defaults are scoped to that class,
17
+ * never to `:root`, so they do not leak into the host page
15
18
  * - JS disabled / failed
16
19
  * Values mirror `DARK_PALETTE_COLORS` in `graphConfig.ts`. The runtime
17
20
  * palette (dark or light) is applied as higher-specificity inline styles
@@ -448,6 +448,10 @@
448
448
  font-size: 11px;
449
449
  color: var(--text-muted);
450
450
  }
451
+ .detail-multiplicity {
452
+ font-size: 12px;
453
+ color: var(--text-muted);
454
+ }
451
455
  .detail-refines-code {
452
456
  font-family: var(--font-mono);
453
457
  color: var(--color-accent);
@@ -622,7 +626,8 @@
622
626
  pointer-events: none;
623
627
  }
624
628
  .graph-toolbar-validation-btn--valid .graph-toolbar-validation-badge,
625
- .graph-toolbar-validation-btn--error .graph-toolbar-validation-badge {
629
+ .graph-toolbar-validation-btn--error .graph-toolbar-validation-badge,
630
+ .graph-toolbar-validation-btn--unvalidated .graph-toolbar-validation-badge {
626
631
  background: var(--color-warning);
627
632
  }
628
633
  .graph-validation-panel {