@pipelex/mthds-form 0.27.0 → 0.29.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.
@@ -0,0 +1,42 @@
1
+ // src/core/humanize.ts
2
+ function humanizeFieldName(name) {
3
+ const words = name.replace(/[_-]+/g, " ").trim();
4
+ if (words === "") return name;
5
+ return words.charAt(0).toUpperCase() + words.slice(1);
6
+ }
7
+ function humanizeEnumValue(code) {
8
+ if (!ENUM_CODE.test(code)) return code;
9
+ const allCaps = code === code.toUpperCase() && code !== code.toLowerCase();
10
+ if (!allCaps) return humanizeFieldName(code);
11
+ return CODE_SEPARATOR.test(code) ? humanizeFieldName(code.toLowerCase()) : code;
12
+ }
13
+ var ENUM_CODE = /^\p{L}[\p{L}\p{N}_-]*$/u;
14
+ var CODE_SEPARATOR = /[_-]/;
15
+ function enumValueLabeler(options) {
16
+ let labeler = LABELERS.get(options);
17
+ if (labeler === void 0) {
18
+ labeler = wordsOrCodes(options);
19
+ LABELERS.set(options, labeler);
20
+ }
21
+ return labeler;
22
+ }
23
+ function asWritten(value) {
24
+ return value;
25
+ }
26
+ function wordsOrCodes(options) {
27
+ const labels = /* @__PURE__ */ new Map();
28
+ const owners = /* @__PURE__ */ new Map();
29
+ for (const option of options) {
30
+ const label = humanizeEnumValue(option);
31
+ const owner = owners.get(label);
32
+ if (owner !== void 0 && owner !== option) return asWritten;
33
+ owners.set(label, option);
34
+ labels.set(option, label);
35
+ }
36
+ return (value) => labels.get(value) ?? value;
37
+ }
38
+ var LABELERS = /* @__PURE__ */ new WeakMap();
39
+
40
+ export { enumValueLabeler, humanizeEnumValue, humanizeFieldName };
41
+ //# sourceMappingURL=chunk-GHVVULHY.js.map
42
+ //# sourceMappingURL=chunk-GHVVULHY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/humanize.ts"],"names":[],"mappings":";AAoBO,SAAS,kBAAkB,IAAA,EAAsB;AACtD,EAAA,MAAM,QAAQ,IAAA,CAAK,OAAA,CAAQ,QAAA,EAAU,GAAG,EAAE,IAAA,EAAK;AAC/C,EAAA,IAAI,KAAA,KAAU,IAAI,OAAO,IAAA;AACzB,EAAA,OAAO,KAAA,CAAM,OAAO,CAAC,CAAA,CAAE,aAAY,GAAI,KAAA,CAAM,MAAM,CAAC,CAAA;AACtD;AAkCO,SAAS,kBAAkB,IAAA,EAAsB;AACtD,EAAA,IAAI,CAAC,SAAA,CAAU,IAAA,CAAK,IAAI,GAAG,OAAO,IAAA;AAClC,EAAA,MAAM,UAAU,IAAA,KAAS,IAAA,CAAK,aAAY,IAAK,IAAA,KAAS,KAAK,WAAA,EAAY;AACzE,EAAA,IAAI,CAAC,OAAA,EAAS,OAAO,iBAAA,CAAkB,IAAI,CAAA;AAC3C,EAAA,OAAO,cAAA,CAAe,KAAK,IAAI,CAAA,GAAI,kBAAkB,IAAA,CAAK,WAAA,EAAa,CAAA,GAAI,IAAA;AAC7E;AAGA,IAAM,SAAA,GAAY,yBAAA;AAGlB,IAAM,cAAA,GAAiB,MAAA;AAuBhB,SAAS,iBAAiB,OAAA,EAAuD;AACtF,EAAA,IAAI,OAAA,GAAU,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA;AAClC,EAAA,IAAI,YAAY,MAAA,EAAW;AACzB,IAAA,OAAA,GAAU,aAAa,OAAO,CAAA;AAC9B,IAAA,QAAA,CAAS,GAAA,CAAI,SAAS,OAAO,CAAA;AAAA,EAC/B;AACA,EAAA,OAAO,OAAA;AACT;AAEA,SAAS,UAAU,KAAA,EAAuB;AACxC,EAAA,OAAO,KAAA;AACT;AAOA,SAAS,aAAa,OAAA,EAAuD;AAC3E,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAoB;AACvC,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAoB;AACvC,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,MAAM,KAAA,GAAQ,kBAAkB,MAAM,CAAA;AACtC,IAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA;AAC9B,IAAA,IAAI,KAAA,KAAU,MAAA,IAAa,KAAA,KAAU,MAAA,EAAQ,OAAO,SAAA;AACpD,IAAA,MAAA,CAAO,GAAA,CAAI,OAAO,MAAM,CAAA;AACxB,IAAA,MAAA,CAAO,GAAA,CAAI,QAAQ,KAAK,CAAA;AAAA,EAC1B;AACA,EAAA,OAAO,CAAC,KAAA,KAAU,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA,IAAK,KAAA;AACzC;AAOA,IAAM,QAAA,uBAAe,OAAA,EAAsD","file":"chunk-GHVVULHY.js","sourcesContent":["/**\n * What a person reads in place of an identifier: a field's name, and the value\n * of a choice. Headless, so a host that draws its own choice control, a server\n * that writes a summary and the package's own controls all word a value the\n * same way.\n *\n * The descriptor carries an enum's values and nothing else: the standard has no\n * display label for an option, and none is wanted. Its label is derived from\n * the value here, by rule, and only what a person reads changes: the value a\n * form stores, the JSON view, the copy control and the download all keep the\n * code.\n */\n\n/**\n * `full_name` → `Full name`. Only the first word is capitalised: these are\n * labels, not titles, and Title Case on every word reads as a form from 2009.\n * Takes IDENTIFIERS only - snake_case or kebab-case names, where a separator\n * is structure. An authored `title` never goes through here: it is already\n * human-facing, and its hyphens are spelling (`E-mail address`), not structure.\n */\nexport function humanizeFieldName(name: string): string {\n const words = name.replace(/[_-]+/g, ' ').trim();\n if (words === '') return name;\n return words.charAt(0).toUpperCase() + words.slice(1);\n}\n\n/**\n * An enum CODE as words: `keynote` → `Keynote`, `unit_price_differs_from_po`\n * → `Unit price differs from po`, `HIGH_RISK` → `High risk`.\n *\n * It is `humanizeFieldName`, applied after an all-caps code of several words is\n * lowercased. The capitals of `HIGH_RISK` are a convention of the code, not an\n * emphasis its author meant, and \"HIGH RISK\" on a sentence-case page reads as\n * an alarm.\n *\n * A single all-caps token is left exactly as written: `USD`, `EUR`, `OK` and\n * `HIGH` hold no separator, and a lone capitalised word is far more often an\n * acronym, a unit or a currency than a shouted word, so lowercasing it would\n * misspell it (\"Usd\"). Only a code that is several words joined by `_` or `-`\n * is taken to be capitalised by convention.\n *\n * Acronyms inside a longer code are not guessed: `po` stays `po` where a reader\n * expects `PO`, just as a field name does. That is the price of deriving the\n * label from the value rather than carrying one on the wire, and it is paid\n * knowingly: a method's author who wants `PO` read writes the code so that it\n * reads.\n *\n * Only a code is humanised. A value that is not an identifier is authored\n * words already, and its separators are spelling rather than structure, so it\n * is shown verbatim: `18-24`, `N/A` and `Very satisfied` do not start with a\n * letter or hold something other than letters, digits, `_` and `-`. That is\n * the line `humanizeFieldName` draws between an identifier and an authored\n * title.\n *\n * This words ONE code, with no knowledge of the enum it belongs to. A choice\n * control goes one step further: when two options of one enum would read the\n * same, it shows that enum's codes instead (see `enumValueLabeler`).\n */\nexport function humanizeEnumValue(code: string): string {\n if (!ENUM_CODE.test(code)) return code;\n const allCaps = code === code.toUpperCase() && code !== code.toLowerCase();\n if (!allCaps) return humanizeFieldName(code);\n return CODE_SEPARATOR.test(code) ? humanizeFieldName(code.toLowerCase()) : code;\n}\n\n/** An identifier: a letter, then letters, digits and `_` or `-` separators. */\nconst ENUM_CODE = /^\\p{L}[\\p{L}\\p{N}_-]*$/u;\n\n/** What joins the words of a code. A space never reaches it: no code holds one. */\nconst CODE_SEPARATOR = /[_-]/;\n\n/**\n * How ONE enum's values read, as words: the labeler every choice control and\n * every enum result goes through, so the options a form offers, the value a\n * result shows and a host's own chips read alike. Built from the enum's\n * options, it maps a value to what a person reads.\n *\n * A declared option reads as words (`humanizeEnumValue`), except in two cases,\n * where the code is shown as written:\n *\n * - **Every option, when two of them would read the same.** `high_risk` and\n * `HIGH_RISK`, or `foo-bar` and `foo_bar`, are distinct codes that humanise\n * to one label, and a person offered two identical choices is choosing\n * blind. Wording the others while showing those two as codes would make one\n * enum read in two registers, so the whole enum falls back.\n * - **A value the enum does not declare.** It is the payload disagreeing with\n * its descriptor, and worded it could pass for an option it is not.\n *\n * This is the wording, not the decision to word: a builder-facing surface that\n * must show the code the bundle spells (the control set's `studio`\n * presentation) does not call it.\n */\nexport function enumValueLabeler(options: readonly string[]): (value: string) => string {\n let labeler = LABELERS.get(options);\n if (labeler === undefined) {\n labeler = wordsOrCodes(options);\n LABELERS.set(options, labeler);\n }\n return labeler;\n}\n\nfunction asWritten(value: string): string {\n return value;\n}\n\n/**\n * One enum's options in words, or its codes as written when two DISTINCT\n * options humanise to the same label. A repeated option is not a collision: it\n * is one code, and reads as one.\n */\nfunction wordsOrCodes(options: readonly string[]): (value: string) => string {\n const labels = new Map<string, string>();\n const owners = new Map<string, string>();\n for (const option of options) {\n const label = humanizeEnumValue(option);\n const owner = owners.get(label);\n if (owner !== undefined && owner !== option) return asWritten;\n owners.set(label, option);\n labels.set(option, label);\n }\n return (value) => labels.get(value) ?? value;\n}\n\n/**\n * One labeler per options array, built on first use and kept: the result view\n * asks once for every cell of an enum column, and the answer depends on the\n * options alone.\n */\nconst LABELERS = new WeakMap<readonly string[], (value: string) => string>();\n"]}
@@ -1,7 +1,7 @@
1
1
  import { pruneEmptyOptionals, healStringWrappers } from './chunk-E6BIXJ6C.js';
2
+ import { isFilled, mustBeFilled, fieldFilled } from './chunk-XO5OENNF.js';
2
3
  import { gatingFieldsFromInputs } from './chunk-Y2ND6W4W.js';
3
4
  import { prepareSchemaForRjsf } from './chunk-LBFZXYCY.js';
4
- import { isFilled, mustBeFilled, fieldFilled } from './chunk-XO5OENNF.js';
5
5
  import { inputMustBeFilled, isOptionalInput, isPluralInput } from './chunk-WYH56SOZ.js';
6
6
  import { ownProp } from './chunk-V3OV2XQT.js';
7
7
  import { validateRunInputsSchema } from './chunk-VTPUDBSJ.js';
@@ -95,5 +95,5 @@ function gateRunInputs(contract, data) {
95
95
  }
96
96
 
97
97
  export { apiInputsFromSchemaData, buildRunInputsSchema, gateRunInputs, prepareRunInputs, validateRunInputs };
98
- //# sourceMappingURL=chunk-4DPYGNJ3.js.map
99
- //# sourceMappingURL=chunk-4DPYGNJ3.js.map
98
+ //# sourceMappingURL=chunk-IBBAJXIO.js.map
99
+ //# sourceMappingURL=chunk-IBBAJXIO.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/gate.ts"],"names":[],"mappings":"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","file":"chunk-4DPYGNJ3.js","sourcesContent":["/**\n * The gate every run passes through, whichever surface starts it.\n *\n * A host may offer more than one place to run a method - a schema-driven\n * inputs panel (an RJSF form) and a standalone run page (a bespoke field\n * renderer), say. **Only the rendering of inputs differs.** Everything between\n * \"the user pressed Run\" and \"the payload goes on the wire\" lives here, so\n * those surfaces cannot drift:\n *\n * 1. `buildRunInputsSchema` - the combined JSON Schema, whose `required` list\n * is `inputMustBeFilled` (optional `?` and variable-plural `[]` never\n * block; a fixed-count `[N]` list does).\n * 2. `prepareRunInputs` - heal legacy wrappers, then prune empty optionals.\n * 3. `validateRunInputs` - ajv, plus the missing-input scan that names the\n * VARIABLE at fault.\n * 4. `apiInputsFromSchemaData` - the `{concept, content}` payload, built from\n * the PREPARED data so the pruning reaches the wire too.\n *\n * **`gateRunInputs` is that whole chain as one call**, and it is what a server\n * should use. The four steps stay exported because a host that renders its own\n * panel needs the schema and the verdict separately, but assembling them is not\n * a host's job: the step between validation and the payload is an emptiness\n * check with four look-alike predicates to choose from, only two of which are\n * the ones the Run button reads, and choosing the near-miss pair produces a\n * server that is more permissive than the button in front of it.\n *\n * This module is pure: no React, no toasts, no i18n. Each caller renders the\n * verdict in its own idiom.\n */\nimport { inputMustBeFilled, isOptionalInput, isPluralInput } from './contracts';\nimport { gatingFieldsFromInputs } from './gating-fields';\nimport { fieldFilled, isFilled, mustBeFilled } from './readiness';\nimport { validateRunInputsSchema, type RunInputError } from './gate-validator';\nimport { healStringWrappers, pruneEmptyOptionals } from './wire-format';\nimport { prepareSchemaForRjsf } from './normalize-schema';\nimport { ownProp } from './own-property';\nimport type { PipeIOContract, PipeInputContract } from './contracts';\n\ntype Dict = Record<string, unknown>;\n\n/**\n * Combine the per-input schemas into one object schema.\n *\n * Each input variable becomes a property, titled with the variable name and\n * described with the concept code. Only the inputs the method actually demands\n * land in `required` - an optional (`?`) or variable-plural (`[]`) input must\n * not block the form (see `inputMustBeFilled`).\n *\n * The per-input schema travels verbatim, which is how a fixed-count (`[N]`)\n * list's declared count reaches ajv: pipelex states it as `minItems`/`maxItems`\n * on the array wrapper, so the count is enforced here without the kernel\n * restating it.\n */\nexport function buildRunInputsSchema(inputs: Record<string, PipeInputContract>): Dict {\n const properties: Dict = {};\n const required: string[] = [];\n\n for (const [varName, input] of Object.entries(inputs)) {\n const inputSchema = { ...input.json_schema } as Dict;\n inputSchema.title = varName;\n if (input.concept_ref && input.concept_ref !== varName) {\n inputSchema.description = input.concept_ref;\n }\n properties[varName] = inputSchema;\n if (inputMustBeFilled(input)) required.push(varName);\n }\n\n return prepareSchemaForRjsf({ type: 'object', properties, required });\n}\n\n/**\n * Last-gate repair, in two passes against the exact schema ajv validates.\n *\n * 1. **HEAL** - persisted `inputData` may still carry the legacy double-wrapped\n * `{ text: { text } }` shape (or a form's state may lag a store fix).\n * 2. **PRUNE** - RJSF fills every unset string with `\"\"` on mount, which an\n * optional CONSTRAINED field rejects (`DateContent.time` is `format: \"time\"`)\n * - blocking a run on a field the user never opened. An absent optional field\n * is what pipelex means by `None` anyway, so dropping it is the semantically\n * correct repair, not a validation dodge.\n */\nexport function prepareRunInputs(data: Dict, schema: Dict): Dict {\n return pruneEmptyOptionals(healStringWrappers(data, schema), schema) as Dict;\n}\n\nexport interface RunInputsVerdict {\n isValid: boolean;\n /** Variable names the scan could pin a missing required field on. */\n missingInputs: string[];\n /** Raw ajv errors, for when the scan cannot name anything. */\n errors: RunInputError[];\n}\n\n/**\n * Validate PREPARED data against the combined schema.\n *\n * `missingInputs` is reported by VARIABLE NAME - the name on the node the user\n * clicks - never by `concept_ref`, which used to produce \"Missing required\n * fields in: native.Text\" and named neither the input at fault nor, when several\n * inputs share a concept, which of them. Inputs the method declared omittable\n * are skipped: an empty `?` input is not a missing one.\n *\n * The scan can come up empty even on an invalid form (a wrong value shape, a\n * nested mismatch); `errors` is what the caller falls back to then, so a failure\n * is never undiagnosable.\n *\n * `preparedData` is `unknown` for the same reason `gateRunInputs`' `data` is: on\n * a public endpoint the body is whatever was sent, and ajv must be shown that\n * rather than a repair of it, or `must be object` is a verdict no caller can\n * ever receive. The name-the-variable scan below needs something indexable, so\n * a body that is not a record scans against nothing and names every input the\n * method demands - which is the honest answer for a body that carried none.\n */\nexport function validateRunInputs(\n preparedData: unknown,\n inputs: Record<string, PipeInputContract>,\n schema: Dict,\n): RunInputsVerdict {\n const errors = validateRunInputsSchema(preparedData, schema);\n if (errors.length === 0) return { isValid: true, missingInputs: [], errors: [] };\n\n const record0 =\n typeof preparedData === 'object' && preparedData !== null && !Array.isArray(preparedData)\n ? (preparedData as Dict)\n : undefined;\n const missingInputs: string[] = [];\n for (const [varName, input] of Object.entries(inputs)) {\n if (!inputMustBeFilled(input)) continue;\n const varData = ownProp(record0, varName);\n // A demanded input that is not there at all is missing whatever its concept\n // declares inside. The check has to come BEFORE the required-children one:\n // a struct whose concept demands no child used to fall out of the scan\n // here, so the run was refused by the combined schema's `required` list\n // while the scan named nothing and the caller could only quote ajv.\n if (varData == null) {\n missingInputs.push(varName);\n continue;\n }\n const inputSchema = input.json_schema as Dict;\n const requiredFields = (inputSchema.required as string[]) || [];\n if (requiredFields.length === 0) continue;\n if (typeof varData !== 'object') {\n missingInputs.push(varName);\n continue;\n }\n const record = varData as Dict;\n if (\n requiredFields.some((f) => {\n const child = ownProp(record, f);\n return child === undefined || child === '';\n })\n ) {\n missingInputs.push(varName);\n }\n }\n\n return { isValid: false, missingInputs, errors };\n}\n\n/**\n * Build the `{ concept, content }` map a run expects, from PREPARED data.\n *\n * The two ways a method says \"this may be nothing\" land differently on the wire:\n * - an **optional (`?`)** input left blank is OMITTED, so the runtime records a\n * real absence the method can branch on - sending `{ text: \"\" }` would look\n * like the caller supplied an empty string;\n * - a **plural (`[]`)** input keeps its key, because a plural slot is never\n * absent - its empty form IS the empty list, and the runtime requires the key.\n * When that list is EMPTY it is sent BARE, without the envelope: the envelope\n * is the runtime's \"explicit form\", which bypasses the top-down shaper and\n * hands the value to the bottom-up factory, and that factory types a list from\n * its first item - so an empty list there raises \"Cannot create Stuff from\n * empty list in content\". The bare form keeps the shaper, which reads the\n * DECLARED concept and builds the empty list correctly. (pipelex/#1096 makes\n * the envelope agree; the bare form runs today and stays correct after.)\n *\n * A fixed-count (`[N]`) list takes that same empty-list path, and an empty one\n * is not a payload the method can accept. It is unreachable through the gate,\n * which demands the input and rejects a short list on `minItems`; building the\n * payload straight from unvalidated data is what would reach it, and inventing\n * a different absence here would only hide that.\n */\nexport function apiInputsFromSchemaData(\n preparedData: Dict,\n inputs: Record<string, PipeInputContract>,\n): Dict {\n const out: Dict = {};\n for (const [varName, input] of Object.entries(inputs)) {\n const raw = ownProp(preparedData, varName);\n if (isOptionalInput(input) && !isFilled(raw)) continue;\n if (isPluralInput(input) && !isFilled(raw)) {\n out[varName] = [];\n continue;\n }\n const content = isPluralInput(input) && !Array.isArray(raw) ? [] : raw;\n out[varName] = { concept: input.concept_ref, content };\n }\n return out;\n}\n\n// ─── The whole chain, as one call ────────────────────────────────────────────\n\n/** The gate's verdict: the payload to send, or why the run may not start. */\nexport type RunInputsGateResult =\n | { ok: true; inputs: Dict }\n | {\n ok: false;\n /** Variable names the caller left empty or short. */\n missingInputs: string[];\n /** Raw ajv errors, for when the scan cannot name anything. */\n errors: RunInputError[];\n /** The repaired data the verdict was reached on - what\n * `describeValidationError` needs to quote the value it received. */\n preparedData: Dict;\n };\n\n/**\n * One schema object per contract, for the lifetime of the process.\n *\n * `buildRunInputsSchema` is a pure function of the contract, so rebuilding it is\n * *semantically* free - but validation runs through a module-level ajv whose\n * compiled-schema cache is keyed on **schema object identity** and is never\n * evicted. A fresh object per call therefore misses every time and retains\n * another compiled validator. On a publicly callable server endpoint that is\n * unbounded growth driven by the cheapest request there is: an empty body,\n * rejected in a fraction of a millisecond, costing nothing to send.\n *\n * Weakly keyed so the map never pins a contract that goes out of scope. The\n * schema a run has been gated against is pinned for the process lifetime anyway\n * by ajv's own cache; the point is that the NUMBER of them is bounded by the\n * number of distinct contracts rather than by traffic.\n */\nconst SCHEMA_CACHE = new WeakMap<PipeIOContract, Dict>();\n\nfunction schemaForContract(contract: PipeIOContract): Dict {\n const cached = SCHEMA_CACHE.get(contract);\n if (cached) return cached;\n const schema = buildRunInputsSchema(contract.inputs);\n SCHEMA_CACHE.set(contract, schema);\n return schema;\n}\n\n/**\n * Gate a run: repair the caller's data, validate it against the contract,\n * refuse anything the Run button would have refused, and build the payload.\n *\n * Returns a verdict rather than throwing. A server endpoint must not throw\n * across the boundary to its client - frameworks routinely strip the message to\n * an opaque digest in a production build - and a rejection here is an ordinary\n * outcome, not an exceptional one.\n *\n * **`data` is `unknown` because that is what it is.** A run endpoint is public\n * and no framework enforces a declared parameter type, so the argument is\n * whatever the caller put in the body. The chain indexes the payload by\n * variable name without first checking it is indexable, so a `null` body would\n * throw *after* ajv had already reported `must be object` and before any\n * verdict came back.\n *\n * So the two uses are separated rather than collapsed into one repair. **ajv\n * judges the value the caller actually sent**, which is what makes `must be\n * object` the verdict for a body that is not one; the indexing that follows\n * uses a safe `{}` stand-in, which is what keeps the gate from throwing. Repair\n * FIRST - coercing a non-object to `{}` before validating - is the shape that\n * looks equivalent and is not: a contract whose inputs are all optional has an\n * empty `required` list, so `{}` validates, and `gateRunInputs(\"garbage\", …)`\n * came back `ok: true` on a body the caller never meant as a run. Fail-closed\n * costs a malformed caller an error it earned; the other direction bills a run.\n *\n * **The schema alone is not the rule.** ajv's `required` asserts only that a\n * key is PRESENT, and a content model carries no `minLength` - so a required\n * input that arrived empty (`{document: {url: \"\"}}`, `{text: {text: \"\"}}`)\n * satisfies the schema. That is the natural payload, not a contrived one:\n * `rjsfDataFromRunValues({}, fields)` emits exactly that when nothing is\n * selected. So the gate re-applies the emptiness rule using\n * `computeReadiness`'s OWN two functions over the same derived fields, which is\n * the only way to be sure the two sides cannot disagree.\n *\n * Picking a pair that merely looks equivalent is not enough, and this is the\n * whole reason the assembly lives here rather than in each host: the obvious\n * choice, `inputMustBeFilled` + `isFilled`, agrees on every leaf kind and\n * diverges on a structured concept in BOTH directions - `isFilled` on an object\n * is `some(child filled)` where `fieldFilled` is `every(required child\n * filled)`. That accepts a half-filled struct the browser refuses (a paid run\n * past a disabled button) and rejects a filled all-optional struct the browser\n * accepts. A host whose methods happen to use only native concepts has no test\n * that can catch either.\n */\nexport function gateRunInputs(contract: PipeIOContract, data: unknown): RunInputsGateResult {\n const isRecord = typeof data === 'object' && data !== null && !Array.isArray(data);\n const payload = isRecord ? (data as Dict) : {};\n const schema = schemaForContract(contract);\n const preparedData = prepareRunInputs(payload, schema);\n\n // The value ajv is shown is the caller's, not the stand-in - see above.\n const verdict = validateRunInputs(isRecord ? preparedData : data, contract.inputs, schema);\n if (!verdict.isValid) {\n return {\n ok: false,\n missingInputs: verdict.missingInputs,\n errors: verdict.errors,\n preparedData,\n };\n }\n\n // The SELECTION is half the rule, and leaving it out is what made the two\n // halves disagree. `computeReadiness` reckons with an input the method\n // demands AND one it left optional that the user has touched - because a\n // structure with something in it owes its concept every field the concept\n // declares. Filtering on `mustBeFilled` alone dropped the second kind, so a\n // half-filled optional struct was refused by the button and accepted here.\n //\n // ajv cannot cover for it: a required child that is a plain string arrives as\n // `''`, which satisfies `required`. And the payload builder below then SENDS\n // that input, because `isFilled` is exactly what it tests. Which is the rule\n // stated properly: the gate holds to its concept precisely the inputs it is\n // about to put on the wire, and nothing it will omit.\n //\n // The tree the predicates run over is the gate's own, walked structurally\n // from the same `json_schema` ajv just validated - NOT the render tree, which\n // is built from the input-form descriptor. A server validating a payload must\n // never need that presentation artifact; see `gating-fields.ts`.\n const missingInputs = gatingFieldsFromInputs(contract.inputs)\n .filter((field) => mustBeFilled(field) || isFilled(ownProp(preparedData, field.name)))\n .filter((field) => !fieldFilled(field, ownProp(preparedData, field.name)))\n .map((field) => field.name);\n if (missingInputs.length) return { ok: false, missingInputs, errors: [], preparedData };\n\n return { ok: true, inputs: apiInputsFromSchemaData(preparedData, contract.inputs) };\n}\n"]}
1
+ {"version":3,"sources":["../src/core/gate.ts"],"names":[],"mappings":"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","file":"chunk-IBBAJXIO.js","sourcesContent":["/**\n * The gate every run passes through, whichever surface starts it.\n *\n * A host may offer more than one place to run a method - a schema-driven\n * inputs panel (an RJSF form) and a standalone run page (a bespoke field\n * renderer), say. **Only the rendering of inputs differs.** Everything between\n * \"the user pressed Run\" and \"the payload goes on the wire\" lives here, so\n * those surfaces cannot drift:\n *\n * 1. `buildRunInputsSchema` - the combined JSON Schema, whose `required` list\n * is `inputMustBeFilled` (optional `?` and variable-plural `[]` never\n * block; a fixed-count `[N]` list does).\n * 2. `prepareRunInputs` - heal legacy wrappers, then prune empty optionals.\n * 3. `validateRunInputs` - ajv, plus the missing-input scan that names the\n * VARIABLE at fault.\n * 4. `apiInputsFromSchemaData` - the `{concept, content}` payload, built from\n * the PREPARED data so the pruning reaches the wire too.\n *\n * **`gateRunInputs` is that whole chain as one call**, and it is what a server\n * should use. The four steps stay exported because a host that renders its own\n * panel needs the schema and the verdict separately, but assembling them is not\n * a host's job: the step between validation and the payload is an emptiness\n * check with four look-alike predicates to choose from, only two of which are\n * the ones the Run button reads, and choosing the near-miss pair produces a\n * server that is more permissive than the button in front of it.\n *\n * This module is pure: no React, no toasts, no i18n. Each caller renders the\n * verdict in its own idiom.\n */\nimport { inputMustBeFilled, isOptionalInput, isPluralInput } from './contracts';\nimport { gatingFieldsFromInputs } from './gating-fields';\nimport { fieldFilled, isFilled, mustBeFilled } from './readiness';\nimport { validateRunInputsSchema, type RunInputError } from './gate-validator';\nimport { healStringWrappers, pruneEmptyOptionals } from './wire-format';\nimport { prepareSchemaForRjsf } from './normalize-schema';\nimport { ownProp } from './own-property';\nimport type { PipeIOContract, PipeInputContract } from './contracts';\n\ntype Dict = Record<string, unknown>;\n\n/**\n * Combine the per-input schemas into one object schema.\n *\n * Each input variable becomes a property, titled with the variable name and\n * described with the concept code. Only the inputs the method actually demands\n * land in `required` - an optional (`?`) or variable-plural (`[]`) input must\n * not block the form (see `inputMustBeFilled`).\n *\n * The per-input schema travels verbatim, which is how a fixed-count (`[N]`)\n * list's declared count reaches ajv: pipelex states it as `minItems`/`maxItems`\n * on the array wrapper, so the count is enforced here without the kernel\n * restating it.\n */\nexport function buildRunInputsSchema(inputs: Record<string, PipeInputContract>): Dict {\n const properties: Dict = {};\n const required: string[] = [];\n\n for (const [varName, input] of Object.entries(inputs)) {\n const inputSchema = { ...input.json_schema } as Dict;\n inputSchema.title = varName;\n if (input.concept_ref && input.concept_ref !== varName) {\n inputSchema.description = input.concept_ref;\n }\n properties[varName] = inputSchema;\n if (inputMustBeFilled(input)) required.push(varName);\n }\n\n return prepareSchemaForRjsf({ type: 'object', properties, required });\n}\n\n/**\n * Last-gate repair, in two passes against the exact schema ajv validates.\n *\n * 1. **HEAL** - persisted `inputData` may still carry the legacy double-wrapped\n * `{ text: { text } }` shape (or a form's state may lag a store fix).\n * 2. **PRUNE** - RJSF fills every unset string with `\"\"` on mount, which an\n * optional CONSTRAINED field rejects (`DateContent.time` is `format: \"time\"`)\n * - blocking a run on a field the user never opened. An absent optional field\n * is what pipelex means by `None` anyway, so dropping it is the semantically\n * correct repair, not a validation dodge.\n */\nexport function prepareRunInputs(data: Dict, schema: Dict): Dict {\n return pruneEmptyOptionals(healStringWrappers(data, schema), schema) as Dict;\n}\n\nexport interface RunInputsVerdict {\n isValid: boolean;\n /** Variable names the scan could pin a missing required field on. */\n missingInputs: string[];\n /** Raw ajv errors, for when the scan cannot name anything. */\n errors: RunInputError[];\n}\n\n/**\n * Validate PREPARED data against the combined schema.\n *\n * `missingInputs` is reported by VARIABLE NAME - the name on the node the user\n * clicks - never by `concept_ref`, which used to produce \"Missing required\n * fields in: native.Text\" and named neither the input at fault nor, when several\n * inputs share a concept, which of them. Inputs the method declared omittable\n * are skipped: an empty `?` input is not a missing one.\n *\n * The scan can come up empty even on an invalid form (a wrong value shape, a\n * nested mismatch); `errors` is what the caller falls back to then, so a failure\n * is never undiagnosable.\n *\n * `preparedData` is `unknown` for the same reason `gateRunInputs`' `data` is: on\n * a public endpoint the body is whatever was sent, and ajv must be shown that\n * rather than a repair of it, or `must be object` is a verdict no caller can\n * ever receive. The name-the-variable scan below needs something indexable, so\n * a body that is not a record scans against nothing and names every input the\n * method demands - which is the honest answer for a body that carried none.\n */\nexport function validateRunInputs(\n preparedData: unknown,\n inputs: Record<string, PipeInputContract>,\n schema: Dict,\n): RunInputsVerdict {\n const errors = validateRunInputsSchema(preparedData, schema);\n if (errors.length === 0) return { isValid: true, missingInputs: [], errors: [] };\n\n const record0 =\n typeof preparedData === 'object' && preparedData !== null && !Array.isArray(preparedData)\n ? (preparedData as Dict)\n : undefined;\n const missingInputs: string[] = [];\n for (const [varName, input] of Object.entries(inputs)) {\n if (!inputMustBeFilled(input)) continue;\n const varData = ownProp(record0, varName);\n // A demanded input that is not there at all is missing whatever its concept\n // declares inside. The check has to come BEFORE the required-children one:\n // a struct whose concept demands no child used to fall out of the scan\n // here, so the run was refused by the combined schema's `required` list\n // while the scan named nothing and the caller could only quote ajv.\n if (varData == null) {\n missingInputs.push(varName);\n continue;\n }\n const inputSchema = input.json_schema as Dict;\n const requiredFields = (inputSchema.required as string[]) || [];\n if (requiredFields.length === 0) continue;\n if (typeof varData !== 'object') {\n missingInputs.push(varName);\n continue;\n }\n const record = varData as Dict;\n if (\n requiredFields.some((f) => {\n const child = ownProp(record, f);\n return child === undefined || child === '';\n })\n ) {\n missingInputs.push(varName);\n }\n }\n\n return { isValid: false, missingInputs, errors };\n}\n\n/**\n * Build the `{ concept, content }` map a run expects, from PREPARED data.\n *\n * The two ways a method says \"this may be nothing\" land differently on the wire:\n * - an **optional (`?`)** input left blank is OMITTED, so the runtime records a\n * real absence the method can branch on - sending `{ text: \"\" }` would look\n * like the caller supplied an empty string;\n * - a **plural (`[]`)** input keeps its key, because a plural slot is never\n * absent - its empty form IS the empty list, and the runtime requires the key.\n * When that list is EMPTY it is sent BARE, without the envelope: the envelope\n * is the runtime's \"explicit form\", which bypasses the top-down shaper and\n * hands the value to the bottom-up factory, and that factory types a list from\n * its first item - so an empty list there raises \"Cannot create Stuff from\n * empty list in content\". The bare form keeps the shaper, which reads the\n * DECLARED concept and builds the empty list correctly. (pipelex/#1096 makes\n * the envelope agree; the bare form runs today and stays correct after.)\n *\n * A fixed-count (`[N]`) list takes that same empty-list path, and an empty one\n * is not a payload the method can accept. It is unreachable through the gate,\n * which demands the input and rejects a short list on `minItems`; building the\n * payload straight from unvalidated data is what would reach it, and inventing\n * a different absence here would only hide that.\n */\nexport function apiInputsFromSchemaData(\n preparedData: Dict,\n inputs: Record<string, PipeInputContract>,\n): Dict {\n const out: Dict = {};\n for (const [varName, input] of Object.entries(inputs)) {\n const raw = ownProp(preparedData, varName);\n if (isOptionalInput(input) && !isFilled(raw)) continue;\n if (isPluralInput(input) && !isFilled(raw)) {\n out[varName] = [];\n continue;\n }\n const content = isPluralInput(input) && !Array.isArray(raw) ? [] : raw;\n out[varName] = { concept: input.concept_ref, content };\n }\n return out;\n}\n\n// ─── The whole chain, as one call ────────────────────────────────────────────\n\n/** The gate's verdict: the payload to send, or why the run may not start. */\nexport type RunInputsGateResult =\n | { ok: true; inputs: Dict }\n | {\n ok: false;\n /** Variable names the caller left empty or short. */\n missingInputs: string[];\n /** Raw ajv errors, for when the scan cannot name anything. */\n errors: RunInputError[];\n /** The repaired data the verdict was reached on - what\n * `describeValidationError` needs to quote the value it received. */\n preparedData: Dict;\n };\n\n/**\n * One schema object per contract, for the lifetime of the process.\n *\n * `buildRunInputsSchema` is a pure function of the contract, so rebuilding it is\n * *semantically* free - but validation runs through a module-level ajv whose\n * compiled-schema cache is keyed on **schema object identity** and is never\n * evicted. A fresh object per call therefore misses every time and retains\n * another compiled validator. On a publicly callable server endpoint that is\n * unbounded growth driven by the cheapest request there is: an empty body,\n * rejected in a fraction of a millisecond, costing nothing to send.\n *\n * Weakly keyed so the map never pins a contract that goes out of scope. The\n * schema a run has been gated against is pinned for the process lifetime anyway\n * by ajv's own cache; the point is that the NUMBER of them is bounded by the\n * number of distinct contracts rather than by traffic.\n */\nconst SCHEMA_CACHE = new WeakMap<PipeIOContract, Dict>();\n\nfunction schemaForContract(contract: PipeIOContract): Dict {\n const cached = SCHEMA_CACHE.get(contract);\n if (cached) return cached;\n const schema = buildRunInputsSchema(contract.inputs);\n SCHEMA_CACHE.set(contract, schema);\n return schema;\n}\n\n/**\n * Gate a run: repair the caller's data, validate it against the contract,\n * refuse anything the Run button would have refused, and build the payload.\n *\n * Returns a verdict rather than throwing. A server endpoint must not throw\n * across the boundary to its client - frameworks routinely strip the message to\n * an opaque digest in a production build - and a rejection here is an ordinary\n * outcome, not an exceptional one.\n *\n * **`data` is `unknown` because that is what it is.** A run endpoint is public\n * and no framework enforces a declared parameter type, so the argument is\n * whatever the caller put in the body. The chain indexes the payload by\n * variable name without first checking it is indexable, so a `null` body would\n * throw *after* ajv had already reported `must be object` and before any\n * verdict came back.\n *\n * So the two uses are separated rather than collapsed into one repair. **ajv\n * judges the value the caller actually sent**, which is what makes `must be\n * object` the verdict for a body that is not one; the indexing that follows\n * uses a safe `{}` stand-in, which is what keeps the gate from throwing. Repair\n * FIRST - coercing a non-object to `{}` before validating - is the shape that\n * looks equivalent and is not: a contract whose inputs are all optional has an\n * empty `required` list, so `{}` validates, and `gateRunInputs(\"garbage\", …)`\n * came back `ok: true` on a body the caller never meant as a run. Fail-closed\n * costs a malformed caller an error it earned; the other direction bills a run.\n *\n * **The schema alone is not the rule.** ajv's `required` asserts only that a\n * key is PRESENT, and a content model carries no `minLength` - so a required\n * input that arrived empty (`{document: {url: \"\"}}`, `{text: {text: \"\"}}`)\n * satisfies the schema. That is the natural payload, not a contrived one:\n * `rjsfDataFromRunValues({}, fields)` emits exactly that when nothing is\n * selected. So the gate re-applies the emptiness rule using\n * `computeReadiness`'s OWN two functions over the same derived fields, which is\n * the only way to be sure the two sides cannot disagree.\n *\n * Picking a pair that merely looks equivalent is not enough, and this is the\n * whole reason the assembly lives here rather than in each host: the obvious\n * choice, `inputMustBeFilled` + `isFilled`, agrees on every leaf kind and\n * diverges on a structured concept in BOTH directions - `isFilled` on an object\n * is `some(child filled)` where `fieldFilled` is `every(required child\n * filled)`. That accepts a half-filled struct the browser refuses (a paid run\n * past a disabled button) and rejects a filled all-optional struct the browser\n * accepts. A host whose methods happen to use only native concepts has no test\n * that can catch either.\n */\nexport function gateRunInputs(contract: PipeIOContract, data: unknown): RunInputsGateResult {\n const isRecord = typeof data === 'object' && data !== null && !Array.isArray(data);\n const payload = isRecord ? (data as Dict) : {};\n const schema = schemaForContract(contract);\n const preparedData = prepareRunInputs(payload, schema);\n\n // The value ajv is shown is the caller's, not the stand-in - see above.\n const verdict = validateRunInputs(isRecord ? preparedData : data, contract.inputs, schema);\n if (!verdict.isValid) {\n return {\n ok: false,\n missingInputs: verdict.missingInputs,\n errors: verdict.errors,\n preparedData,\n };\n }\n\n // The SELECTION is half the rule, and leaving it out is what made the two\n // halves disagree. `computeReadiness` reckons with an input the method\n // demands AND one it left optional that the user has touched - because a\n // structure with something in it owes its concept every field the concept\n // declares. Filtering on `mustBeFilled` alone dropped the second kind, so a\n // half-filled optional struct was refused by the button and accepted here.\n //\n // ajv cannot cover for it: a required child that is a plain string arrives as\n // `''`, which satisfies `required`. And the payload builder below then SENDS\n // that input, because `isFilled` is exactly what it tests. Which is the rule\n // stated properly: the gate holds to its concept precisely the inputs it is\n // about to put on the wire, and nothing it will omit.\n //\n // The tree the predicates run over is the gate's own, walked structurally\n // from the same `json_schema` ajv just validated - NOT the render tree, which\n // is built from the input-form descriptor. A server validating a payload must\n // never need that presentation artifact; see `gating-fields.ts`.\n const missingInputs = gatingFieldsFromInputs(contract.inputs)\n .filter((field) => mustBeFilled(field) || isFilled(ownProp(preparedData, field.name)))\n .filter((field) => !fieldFilled(field, ownProp(preparedData, field.name)))\n .map((field) => field.name);\n if (missingInputs.length) return { ok: false, missingInputs, errors: [], preparedData };\n\n return { ok: true, inputs: apiInputsFromSchemaData(preparedData, contract.inputs) };\n}\n"]}
@@ -60,5 +60,5 @@ function roomForChosenFiles(held, maxItemCount) {
60
60
  }
61
61
 
62
62
  export { addChosenFiles, chosenFilesAdded, placeDroppedFiles, roomForChosenFiles, roomForDroppedFiles, sourceFileValue };
63
- //# sourceMappingURL=chunk-NSHGLM6A.js.map
64
- //# sourceMappingURL=chunk-NSHGLM6A.js.map
63
+ //# sourceMappingURL=chunk-L23R3JNQ.js.map
64
+ //# sourceMappingURL=chunk-L23R3JNQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/sources.ts"],"names":[],"mappings":"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","file":"chunk-L23R3JNQ.js","sourcesContent":["/**\n * The two optional host seams beside the upload: a **file source**, which lets\n * a file field take a file the host already stores, and an **example source**,\n * which lets a whole form take a set of values someone saved for the method.\n *\n * The kernel states the shapes and applies the answers; the host answers. It\n * knows nothing of any platform's routes, records or permissions: a host maps\n * its own list onto these shapes, and a host that gives neither seam gets the\n * form it had before. The controls that use them are `./react`'s\n * (`FilePicker`, `FillWithExample`), and docs/sources.md is the contract.\n *\n * Shapes and pure functions only, so a host's server can import the types it\n * answers with, and the rules that write an answer into a value are tested in\n * node. No React, no fetch. What a row SAYS is data here (`SourceFile`'s\n * `typeLabel` and `origin` are the host's words); how a row DRAWS is a\n * rendering, so the glyph a host draws on a file's row and the line it draws\n * under an example's name are `./react`'s (`FilePickerSource.renderGlyph`,\n * `ExamplePickerSource.renderDetail`), never a field of these shapes.\n */\n\n/** Whose files a file source lists: the person's own, or the whole team's. */\nexport type FileSourceScope = 'mine' | 'everyone';\n\n/** One page of a file source's list, as the picker asks for it. */\nexport interface FileSourceQuery {\n /** What the person searched for, trimmed; empty when they searched for nothing. */\n search: string;\n /** Whose files to list. */\n scope: FileSourceScope;\n /**\n * The MIME types the field accepts, after any narrowing a host applied\n * (`narrowFileFormats`), in the field's own order. The list must hold only\n * files of these types: the picker writes what is chosen straight into the\n * field, and a field that refuses a dropped file of another type must not\n * be handed one this way.\n */\n mediaTypes: readonly string[];\n /** Where to continue: absent for the first page, else the previous page's `nextCursor`. */\n cursor?: string;\n /** Aborted when the answer is no longer wanted: a newer search, a closed picker. */\n signal: AbortSignal;\n}\n\n/** One row of a file source's list: a file the host already stores. */\nexport interface SourceFile {\n /** The row's identity in the host's list, which a choice is kept by. */\n id: string;\n /**\n * The stored file's address, written into the field as its `url`, exactly as\n * an upload's result is. Several rows may share one address.\n */\n url: string;\n /** The file's name, shown on the row and written into the field as its `filename`. */\n name: string;\n /**\n * The file's MIME type, which picks the field's format the row's file is:\n * the row names that format's label (\"PNG\") unless `typeLabel` words the\n * type, and draws an image's glyph or a document's from it unless the\n * host draws its own.\n */\n mediaType?: string;\n /**\n * The file's type in the host's own words (\"Picture\", \"PDF document\"), said\n * on the row's second line in place of the format's label, so a row reads\n * as the host's own list of files does. An empty string counts as none.\n */\n typeLabel?: string;\n /** The file's size in bytes. */\n size?: number;\n /** How the file came to be. A `test` file is marked \"Test file\" at the head of its row's second line. */\n kind?: 'uploaded' | 'generated' | 'test';\n /** When the file was stored, as an ISO 8601 timestamp; the row says it as a date. */\n createdAt?: string;\n /**\n * Where the file came from, in the host's own words (\"Uploaded by Ana\",\n * \"Made by Invoice extraction\"), shown on the row's second line as it is.\n */\n origin?: string;\n}\n\n/** One page of a file source's answer. */\nexport interface FileSourcePage {\n /** The page's files, newest first. A page may be empty and still carry a cursor. */\n files: readonly SourceFile[];\n /** Where the next page starts, or nothing when this page is the last. */\n nextCursor?: string | null;\n}\n\n/**\n * Where a file field can take a file the host already stores. When a form has\n * one, every file field offers \"Choose from your team's files\", and a list of\n * files offers it for several at once.\n */\nexport interface FileSource {\n /** Lists one page of files for a query. Rejecting shows the failure with Try again. */\n list: (query: FileSourceQuery) => Promise<FileSourcePage>;\n /** Which of Mine and Everyone the picker opens on. */\n initialScope: FileSourceScope;\n}\n\n/** One example of a method: a name and the inputs a run of it sends. */\nexport interface FormExample {\n /** The example's identity in the host's list. */\n id: string;\n /** The example's name, shown on its row. */\n name: string;\n /**\n * The inputs as a run takes them: each input `{ concept, content }`, an\n * optional input left out, an empty plural input a bare `[]`. It is the\n * shape `apiInputsFromRunValues` produces, so a host saves an example from\n * `form.inputs` and hands it back as it is.\n */\n inputs: Readonly<Record<string, unknown>>;\n}\n\n/** One page of an example source's list. */\nexport interface ExampleSourceQuery {\n /** Where to continue: absent for the first page, else the previous page's `nextCursor`. */\n cursor?: string;\n /** Aborted when the answer is no longer wanted. */\n signal: AbortSignal;\n}\n\n/** One page of an example source's answer. */\nexport interface ExampleSourcePage {\n /** The page's examples, in the order the list shows them. */\n examples: readonly FormExample[];\n /** Where the next page starts, or nothing when this page is the last. */\n nextCursor?: string | null;\n}\n\n/**\n * Where a form can take a whole set of values. When a form has one, it offers\n * \"Fill with an example\". `open`, `rename` and `delete` are capabilities: each\n * is offered on a row's menu only when given, Open first, so a host gives each\n * only to a person it is for, and `rename` and `delete` only to a person\n * allowed to change examples. A source with any one of them has a row menu.\n *\n * The three are declared as methods, as `./react`'s `renderDetail` is,\n * because TypeScript compares a method's parameter in both directions: a\n * source whose handlers take the host's own example type, the type its `list`\n * answers with, is accepted. Declared as function-typed properties they would\n * refuse it under `strictFunctionTypes`, which is why they must stay methods.\n * The control calls each on its source, so a class's methods keep `this`.\n */\nexport interface ExampleSource {\n /** Lists one page of the method's examples. Rejecting shows the failure with Try again. */\n list: (query: ExampleSourceQuery) => Promise<ExampleSourcePage>;\n /**\n * Opens an example in the host's own surface, to show what it holds and\n * what it produced. Choosing Open closes the list and gives focus back to\n * \"Fill with an example\", then calls `open` once, so a surface the host\n * opens over the form from there returns focus there as it closes.\n *\n * `fill` is the row's own choice of that example, made when it is called:\n * the example's inputs read back against the form's fields as they are at\n * that moment, \"Replace what you entered?\" asked first when the form holds\n * the person's own values, and nothing at all while the form is held shut\n * or once the control is gone, `hideWhenEmpty` having taken it away included.\n * It fills through the same path a row's choice does, so a host's \"Fill the\n * form\" calls it rather than setting the values itself. It may be called\n * any number of times, or never.\n */\n open?(example: FormExample, fill: () => void): void;\n /**\n * Renames an example. Resolving to the example as stored replaces the row;\n * resolving to nothing keeps the row under the new name.\n */\n rename?(example: FormExample, name: string): Promise<FormExample | void>;\n /** Deletes an example. Asked for only after the person confirmed it. */\n delete?(example: FormExample): Promise<void>;\n /**\n * Whether \"Fill with an example\" is left out while the method has no\n * example. `false` by default, when the empty list says how to make one.\n * A host sets it for a person who cannot save an example, to whom an empty\n * list has nothing to offer; the form then asks for the first page as it\n * mounts, to know whether to offer the control.\n */\n hideWhenEmpty?: boolean;\n}\n\n/** What a chosen file writes into its field: the same `{ url, filename }` an upload writes. */\nexport function sourceFileValue(file: SourceFile): { url: string; filename: string } {\n return { url: file.url, filename: file.name };\n}\n\n/** The `url` a list item holds, when it is a file value holding one. */\nfunction heldUrl(item: unknown): string | undefined {\n if (!item || typeof item !== 'object') return undefined;\n const url = (item as { url?: unknown }).url;\n return typeof url === 'string' && url !== '' ? url : undefined;\n}\n\n/** The addresses a list's items hold, each once. */\nfunction heldUrls(items: readonly unknown[]): Set<string> {\n return new Set(items.map(heldUrl).filter((url): url is string => url !== undefined));\n}\n\n/**\n * A list of files with the chosen files written into it, in the order they\n * were chosen: first into the rows that hold no file yet (a row the person\n * added and left empty), then as new rows after the last. A file whose address\n * the list already holds is not written again, and neither is a second choice\n * of one address: two rows of a source may name the same stored file. Returns\n * a new array and leaves `held` as it was.\n */\nexport function addChosenFiles(\n held: readonly unknown[] | undefined,\n chosen: readonly SourceFile[],\n): unknown[] {\n const items = [...(held ?? [])];\n const urls = heldUrls(items);\n let free = 0;\n for (const file of chosen) {\n if (urls.has(file.url)) continue;\n urls.add(file.url);\n while (free < items.length && heldUrl(items[free]) !== undefined) free += 1;\n if (free < items.length) items[free] = sourceFileValue(file);\n else items.push(sourceFileValue(file));\n free += 1;\n }\n return items;\n}\n\n/** Where `placeDroppedFiles` puts a batch of files, and what it leaves out. */\nexport interface DroppedFilesPlacement {\n /**\n * The list once the rows the batch needs are appended: `held` as it was,\n * then one empty row (`undefined`) per file that did not go into a free row.\n * A new array whenever rows are appended, and a copy of `held` otherwise.\n */\n items: unknown[];\n /** How many rows were appended, so a caller writes the list only when it grew. */\n appended: number;\n /** The row each placed file goes to, in the order the files were given. */\n targets: number[];\n /** How many files, from the end of the batch, found no room. */\n leftOut: number;\n}\n\n/**\n * Where a batch of `count` files dropped on a list of files goes: first into\n * the rows that are free, in index order, then into new rows after the last,\n * as far as `maxItemCount` allows. The files keep the order they were given,\n * and those beyond the room are left out from the end of the batch.\n *\n * A row is free when it holds no file (its value carries no `url`) and\n * `isFree` says so too. The caller's predicate is where it adds what only it\n * knows of a row: that a file is still arriving there, whose write-back the row\n * must be kept for, or that the host reports a failure there, the only record\n * that one of the person's files did not arrive. Without one, every row holding\n * no file is free.\n *\n * It mirrors `addChosenFiles`, so the two ways several files enter a list fill\n * it alike, and it knows nothing of `File`: the caller judges each file and\n * hands over how many it accepted.\n */\nexport function placeDroppedFiles(\n held: readonly unknown[] | undefined,\n count: number,\n maxItemCount: number | undefined,\n isFree: (index: number) => boolean = () => true,\n): DroppedFilesPlacement {\n const items = [...(held ?? [])];\n const targets: number[] = [];\n for (let index = 0; index < items.length && targets.length < count; index += 1) {\n if (heldUrl(items[index]) === undefined && isFree(index)) targets.push(index);\n }\n let appended = 0;\n while (targets.length < count && (maxItemCount === undefined || items.length < maxItemCount)) {\n targets.push(items.length);\n items.push(undefined);\n appended += 1;\n }\n return { items, appended, targets, leftOut: count - targets.length };\n}\n\n/**\n * How many files `placeDroppedFiles` can place in `held`: its free rows, by the\n * same test, and the rows its bound still allows after the last. `undefined`\n * when the list has no bound, since appending then never runs out.\n */\nexport function roomForDroppedFiles(\n held: readonly unknown[] | undefined,\n maxItemCount: number | undefined,\n isFree: (index: number) => boolean = () => true,\n): number | undefined {\n if (maxItemCount === undefined) return undefined;\n const items = held ?? [];\n let free = 0;\n for (let index = 0; index < items.length; index += 1) {\n if (heldUrl(items[index]) === undefined && isFree(index)) free += 1;\n }\n return free + Math.max(0, maxItemCount - items.length);\n}\n\n/**\n * How many files `addChosenFiles` writes for `chosen` into `held`: the choices\n * whose address the list does not hold yet, each address once. This is what a\n * choice spends of `roomForChosenFiles`, so a held file chosen again, or a\n * second row naming a chosen file, takes no room.\n */\nexport function chosenFilesAdded(\n held: readonly unknown[] | undefined,\n chosen: readonly SourceFile[],\n): number {\n const urls = heldUrls(held ?? []);\n const before = urls.size;\n for (const file of chosen) urls.add(file.url);\n return urls.size - before;\n}\n\n/**\n * How many files a list may still take from a source: its bound less the files\n * it holds, since `addChosenFiles` writes into its empty rows before adding\n * any. `undefined` when the list has no bound.\n */\nexport function roomForChosenFiles(\n held: readonly unknown[] | undefined,\n maxItemCount: number | undefined,\n): number | undefined {\n if (maxItemCount === undefined) return undefined;\n const files = (held ?? []).filter((item) => heldUrl(item) !== undefined).length;\n return Math.max(0, maxItemCount - files);\n}\n"]}
@@ -1,8 +1,9 @@
1
1
  import { planFileSave, planStuffSave, splitFileName, typedFileStem } from './chunk-4VAO6BDE.js';
2
2
  import { seedInputs, closeOptionalStructures, openOptionalStructures } from './chunk-VH5CDQDR.js';
3
- import { sourceFileValue, roomForChosenFiles, roomForDroppedFiles, addChosenFiles, placeDroppedFiles, chosenFilesAdded } from './chunk-NSHGLM6A.js';
3
+ import { sourceFileValue, roomForChosenFiles, roomForDroppedFiles, addChosenFiles, placeDroppedFiles, chosenFilesAdded } from './chunk-L23R3JNQ.js';
4
4
  import { readStuffFile } from './chunk-56WECFBD.js';
5
5
  import { apiInputsFromRunValues, sameRunValues, runValuesFromInputs, setValueAtPath } from './chunk-ZX52CQJ4.js';
6
+ import { enumValueLabeler, humanizeFieldName, humanizeEnumValue } from './chunk-GHVVULHY.js';
6
7
  import { TYPED_SCALAR_MARKERS, viewableUrl, splitCitations, webHref, isNativeDateNode, isNativeHtmlNode, readHtmlContent, isNativeCompositeNode, readCompositeContent, isNativePageNode, nativeVerdictOf, isNativeSearchResultNode, readSearchResultContent, isNativeValueNode, readDateContent, formatDateContent, readPageContent, readYesNoContent, readRatingContent, readChoiceContent, readDocumentContent, readImageContent, readDataUrl, citedSource } from './chunk-QQK6Y55E.js';
7
8
  import { buildRunFields } from './chunk-ZB4RYUKV.js';
8
9
  import { conceptCategory } from './chunk-FABY3DEY.js';
@@ -73,44 +74,12 @@ function FieldPresentationProvider({
73
74
  function useFieldPresentation() {
74
75
  return use(FieldPresentationContext);
75
76
  }
76
- function humanizeFieldName(name) {
77
- const words = name.replace(/[_-]+/g, " ").trim();
78
- if (words === "") return name;
79
- return words.charAt(0).toUpperCase() + words.slice(1);
80
- }
81
- function humanizeEnumValue(code) {
82
- if (!ENUM_CODE.test(code)) return code;
83
- const allCaps = code === code.toUpperCase() && code !== code.toLowerCase();
84
- if (!allCaps) return humanizeFieldName(code);
85
- return CODE_SEPARATOR.test(code) ? humanizeFieldName(code.toLowerCase()) : code;
86
- }
87
- var ENUM_CODE = /^\p{L}[\p{L}\p{N}_-]*$/u;
88
- var CODE_SEPARATOR = /[_-]/;
89
77
  function enumLabeler(options, presentation) {
90
- if (presentation !== "app") return asWritten;
91
- let labeler = APP_LABELERS.get(options);
92
- if (labeler === void 0) {
93
- labeler = wordsOrCodes(options);
94
- APP_LABELERS.set(options, labeler);
95
- }
96
- return labeler;
78
+ return presentation === "app" ? enumValueLabeler(options) : asWritten;
97
79
  }
98
80
  function asWritten(value) {
99
81
  return value;
100
82
  }
101
- function wordsOrCodes(options) {
102
- const labels = /* @__PURE__ */ new Map();
103
- const owners = /* @__PURE__ */ new Map();
104
- for (const option of options) {
105
- const label = humanizeEnumValue(option);
106
- const owner = owners.get(label);
107
- if (owner !== void 0 && owner !== option) return asWritten;
108
- owners.set(label, option);
109
- labels.set(option, label);
110
- }
111
- return (value) => labels.get(value) ?? value;
112
- }
113
- var APP_LABELERS = /* @__PURE__ */ new WeakMap();
114
83
  function fieldLabel(title, name, presentation) {
115
84
  if (title !== void 0) return title;
116
85
  return presentation === "app" ? humanizeFieldName(name) : name;
@@ -353,6 +322,7 @@ var EN_FIELD_STRINGS = {
353
322
  replaceEnteredDescription: (name) => `Filling the form with \u201C${name}\u201D replaces every value in it.`,
354
323
  replaceEnteredConfirm: "Replace",
355
324
  exampleActions: (name) => `Actions for ${name}`,
325
+ openExample: "Open",
356
326
  renameExample: "Rename",
357
327
  deleteExample: "Delete",
358
328
  renameExampleTitle: "Rename the example",
@@ -537,6 +507,7 @@ var FR_FIELD_STRINGS = {
537
507
  replaceEnteredDescription: (name) => `Remplir le formulaire avec \xAB\xA0${name}\xA0\xBB remplace toutes ses valeurs.`,
538
508
  replaceEnteredConfirm: "Remplacer",
539
509
  exampleActions: (name) => `Actions pour ${name}`,
510
+ openExample: "Ouvrir",
540
511
  renameExample: "Renommer",
541
512
  deleteExample: "Supprimer",
542
513
  renameExampleTitle: "Renommer l\u2019exemple",
@@ -795,6 +766,10 @@ function useSourcePages(load, key) {
795
766
  updateItems
796
767
  };
797
768
  }
769
+ function drawsNothing(drawn) {
770
+ if (Array.isArray(drawn)) return drawn.every((item) => drawsNothing(item));
771
+ return drawn === null || drawn === void 0 || typeof drawn === "boolean" || drawn === "";
772
+ }
798
773
  function DimRows({ count = 4 }) {
799
774
  return /* @__PURE__ */ jsx("div", { "aria-hidden": "true", "data-dim-rows": true, className: "divide-y divide-border", children: Array.from({ length: count }, (_, index) => /* @__PURE__ */ jsxs("div", { className: "flex min-h-16 items-center gap-3 px-4 py-3 opacity-60", children: [
800
775
  /* @__PURE__ */ jsx("span", { className: "size-8 shrink-0 rounded-md bg-muted" }),
@@ -1268,9 +1243,6 @@ function rowMeta(s, file, format) {
1268
1243
  }
1269
1244
  return parts.join(" \xB7 ");
1270
1245
  }
1271
- function drawsNothing(glyph) {
1272
- return glyph === null || glyph === void 0 || typeof glyph === "boolean" || glyph === "";
1273
- }
1274
1246
  function FileRow({
1275
1247
  file,
1276
1248
  formats,
@@ -3026,6 +2998,8 @@ function FillWithExample({
3026
2998
  const afterControl = useRef(null);
3027
2999
  const rows = useRef(/* @__PURE__ */ new Map());
3028
3000
  const menus = useRef(/* @__PURE__ */ new Map());
3001
+ const handingOver = useRef(null);
3002
+ const latestChoice = useRef(() => void 0);
3029
3003
  const afterRowDialog = useRef(null);
3030
3004
  const register = (map, id) => (element) => {
3031
3005
  map.set(id, element);
@@ -3046,7 +3020,12 @@ function FillWithExample({
3046
3020
  setRenaming(null);
3047
3021
  setDeleting(null);
3048
3022
  }
3049
- if (source.hideWhenEmpty && !open && (answer ?? hasExamples) !== true) return null;
3023
+ const hidden = source.hideWhenEmpty === true && !open && (answer ?? hasExamples) !== true;
3024
+ if (hidden && (replacing || renaming || deleting)) {
3025
+ setReplacing(null);
3026
+ setRenaming(null);
3027
+ setDeleting(null);
3028
+ }
3050
3029
  const closeList = () => {
3051
3030
  if (source.hideWhenEmpty && pages.status === "ready") {
3052
3031
  if (pages.items.length > 0) setHasExamples(true);
@@ -3062,13 +3041,27 @@ function FillWithExample({
3062
3041
  if (disabled) return;
3063
3042
  onFill(runValuesFromInputs(example.inputs, fields), example);
3064
3043
  setReplacing(null);
3065
- closeList();
3044
+ if (open) closeList();
3066
3045
  };
3067
3046
  const choose = (example) => {
3047
+ if (disabled) return;
3068
3048
  if (touched) setReplacing(example);
3069
3049
  else fill(example);
3070
3050
  };
3071
- const manages = source.rename !== void 0 || source.delete !== void 0;
3051
+ useEffect(() => {
3052
+ latestChoice.current = hidden ? () => void 0 : choose;
3053
+ return () => {
3054
+ latestChoice.current = () => void 0;
3055
+ };
3056
+ });
3057
+ if (hidden) return null;
3058
+ const manages = source.open !== void 0 || source.rename !== void 0 || source.delete !== void 0;
3059
+ const handOver2 = () => {
3060
+ const example = handingOver.current;
3061
+ handingOver.current = null;
3062
+ if (!example || !source.open) return;
3063
+ queueMicrotask(() => source.open?.(example, () => latestChoice.current(example)));
3064
+ };
3072
3065
  const status = pages.status === "idle" || pages.status === "loading" ? s.examplesLoading : pages.status === "failed" ? s.examplesFailed : pages.more === "failed" ? s.loadMoreFailed : pages.items.length > 0 ? s.examplesShown(pages.items.length) : pages.empty ? s.examplesNone : s.examplesLoading;
3073
3066
  const returnFromRowDialog = () => {
3074
3067
  const next = afterRowDialog.current;
@@ -3111,6 +3104,7 @@ function FillWithExample({
3111
3104
  className: "top-auto max-h-[calc(100dvh-1.5rem)]",
3112
3105
  ref: sheet,
3113
3106
  returnFocus: () => control.current ?? afterControl.current,
3107
+ onCloseAutoFocus: handOver2,
3114
3108
  children: [
3115
3109
  /* @__PURE__ */ jsx("div", { className: "px-4 pt-4 pb-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
3116
3110
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-1 pt-1", children: [
@@ -3143,7 +3137,13 @@ function FillWithExample({
3143
3137
  "flex min-h-16 min-w-0 flex-1 items-center px-4 py-2.5 text-left transition-colors hover:bg-muted/60",
3144
3138
  focusOutlineInsetClass
3145
3139
  ),
3146
- children: /* @__PURE__ */ jsx("span", { className: "truncate text-[16px] font-medium text-foreground", children: example.name })
3140
+ children: /* @__PURE__ */ jsx(
3141
+ ExampleRowText,
3142
+ {
3143
+ name: example.name,
3144
+ detail: source.renderDetail?.(example, fields)
3145
+ }
3146
+ )
3147
3147
  }
3148
3148
  ),
3149
3149
  manages && // Not modal, so the question a menu item opens takes
@@ -3162,6 +3162,16 @@ function FillWithExample({
3162
3162
  }
3163
3163
  ),
3164
3164
  /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
3165
+ source.open && /* @__PURE__ */ jsx(
3166
+ DropdownMenuItem,
3167
+ {
3168
+ onSelect: () => {
3169
+ handingOver.current = example;
3170
+ closeList();
3171
+ },
3172
+ children: s.openExample
3173
+ }
3174
+ ),
3165
3175
  source.rename && /* @__PURE__ */ jsx(DropdownMenuItem, { onSelect: () => setRenaming(example), children: s.renameExample }),
3166
3176
  source.delete && /* @__PURE__ */ jsx(
3167
3177
  DropdownMenuItem,
@@ -3211,7 +3221,7 @@ function FillWithExample({
3211
3221
  RenameExampleDialog,
3212
3222
  {
3213
3223
  example: renaming,
3214
- rename: source.rename,
3224
+ rename: source.rename.bind(source),
3215
3225
  returnFocus: returnFromRowDialog,
3216
3226
  onDone: (renamed) => {
3217
3227
  pages.updateItems(
@@ -3230,7 +3240,7 @@ function FillWithExample({
3230
3240
  DeleteExampleDialog,
3231
3241
  {
3232
3242
  example: deleting,
3233
- remove: source.delete,
3243
+ remove: source.delete.bind(source),
3234
3244
  returnFocus: returnFromRowDialog,
3235
3245
  onDone: (deleted) => {
3236
3246
  const index = pages.items.findIndex((item) => item.id === deleted.id);
@@ -3247,6 +3257,22 @@ function FillWithExample({
3247
3257
  )
3248
3258
  ] });
3249
3259
  }
3260
+ function ExampleRowText({ name, detail }) {
3261
+ return /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
3262
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-[16px] font-medium text-foreground", children: name }),
3263
+ !drawsNothing(detail) && /* @__PURE__ */ jsxs(Fragment, { children: [
3264
+ " ",
3265
+ /* @__PURE__ */ jsx(
3266
+ "span",
3267
+ {
3268
+ "data-slot": "example-row-detail",
3269
+ className: "mt-0.5 block text-[13px] wrap-break-word text-muted-foreground",
3270
+ children: detail
3271
+ }
3272
+ )
3273
+ ] })
3274
+ ] });
3275
+ }
3250
3276
  function RenameExampleDialog({
3251
3277
  example,
3252
3278
  rename,
@@ -8451,6 +8477,6 @@ function pdfFileName(field, value, baseName) {
8451
8477
  return name === void 0 ? "" : name.replace(/\.[^.]+$/, "");
8452
8478
  }
8453
8479
 
8454
- export { BooleanField, ConceptPill, DEFAULT_FIELD_STRINGS, DateField, DocumentField, EN_FIELD_STRINGS, EnumField, FIELD_STRINGS_BY_LOCALE, FR_FIELD_STRINGS, FieldDomIdProvider, FieldPresentationProvider, FieldRenderer, FieldShell, FieldStringsProvider, FilePicker, FilePickerEntry, FillWithExample, HtmlPreview, ImageField, JsonView, ListField, Markdown, MethodForm, NumberField, ObjectField, OptionalToggle, PdfDownloadButton, ProseField, ResultEnvProvider, ResultField, ResultRoot, SUPPORTED_LOCALES, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StuffViewer, Switch, TextField, ToggleGroup, ToggleGroupItem, UnknownField, checkedAccentClass, cn, fieldControlClass, fieldStringsFor, focusOutlineClass, humanizeFieldName, linkTextClass, renderHtmlPdf, resolveHtmlStorageUrls, saveInBrowser, selectedBorderClass, selectedFillClass, selectedFillStrongClass, useFieldDomId, useFieldPresentation, useFieldStrings, useMethodForm, useProseImages, useResolveRendition, useResolveShareUrl, useResolveUrl, useResolvedUrl };
8455
- //# sourceMappingURL=chunk-AZIUFSIQ.js.map
8456
- //# sourceMappingURL=chunk-AZIUFSIQ.js.map
8480
+ export { BooleanField, ConceptPill, DEFAULT_FIELD_STRINGS, DateField, DocumentField, EN_FIELD_STRINGS, EnumField, FIELD_STRINGS_BY_LOCALE, FR_FIELD_STRINGS, FieldDomIdProvider, FieldPresentationProvider, FieldRenderer, FieldShell, FieldStringsProvider, FilePicker, FilePickerEntry, FillWithExample, HtmlPreview, ImageField, JsonView, ListField, Markdown, MethodForm, NumberField, ObjectField, OptionalToggle, PdfDownloadButton, ProseField, ResultEnvProvider, ResultField, ResultRoot, SUPPORTED_LOCALES, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StuffViewer, Switch, TextField, ToggleGroup, ToggleGroupItem, UnknownField, checkedAccentClass, cn, enumLabeler, fieldControlClass, fieldStringsFor, focusOutlineClass, linkTextClass, renderHtmlPdf, resolveHtmlStorageUrls, saveInBrowser, selectedBorderClass, selectedFillClass, selectedFillStrongClass, useFieldDomId, useFieldPresentation, useFieldStrings, useMethodForm, useProseImages, useResolveRendition, useResolveShareUrl, useResolveUrl, useResolvedUrl };
8481
+ //# sourceMappingURL=chunk-RHX7LQRL.js.map
8482
+ //# sourceMappingURL=chunk-RHX7LQRL.js.map