@pipelex/mthds-form 0.28.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"]}
@@ -3,6 +3,7 @@ import { seedInputs, closeOptionalStructures, openOptionalStructures } from './c
3
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;
@@ -8508,6 +8477,6 @@ function pdfFileName(field, value, baseName) {
8508
8477
  return name === void 0 ? "" : name.replace(/\.[^.]+$/, "");
8509
8478
  }
8510
8479
 
8511
- 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 };
8512
- //# sourceMappingURL=chunk-II5TOORN.js.map
8513
- //# sourceMappingURL=chunk-II5TOORN.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