@pipelex/mthds-form 0.3.0 → 0.5.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.
- package/README.md +21 -7
- package/dist/chunk-25I6JDIK.js +28 -0
- package/dist/chunk-25I6JDIK.js.map +1 -0
- package/dist/chunk-5Q2W6HBA.js +201 -0
- package/dist/chunk-5Q2W6HBA.js.map +1 -0
- package/dist/chunk-GCXV76WU.js +213 -0
- package/dist/chunk-GCXV76WU.js.map +1 -0
- package/dist/chunk-GKV77RAS.js +46 -0
- package/dist/chunk-GKV77RAS.js.map +1 -0
- package/dist/chunk-IIJRJGZQ.js +128 -0
- package/dist/chunk-IIJRJGZQ.js.map +1 -0
- package/dist/chunk-IM2AHWBR.js +99 -0
- package/dist/chunk-IM2AHWBR.js.map +1 -0
- package/dist/chunk-LBFZXYCY.js +44 -0
- package/dist/chunk-LBFZXYCY.js.map +1 -0
- package/dist/chunk-OD3VYXUY.js +156 -0
- package/dist/chunk-OD3VYXUY.js.map +1 -0
- package/dist/chunk-PM55AHRO.js +52 -0
- package/dist/chunk-PM55AHRO.js.map +1 -0
- package/dist/chunk-RPA7DNSK.js +41 -0
- package/dist/chunk-RPA7DNSK.js.map +1 -0
- package/dist/chunk-V3OV2XQT.js +12 -0
- package/dist/chunk-V3OV2XQT.js.map +1 -0
- package/dist/chunk-VTPUDBSJ.js +61 -0
- package/dist/chunk-VTPUDBSJ.js.map +1 -0
- package/dist/chunk-XYQ7NXQZ.js +31 -0
- package/dist/chunk-XYQ7NXQZ.js.map +1 -0
- package/dist/chunk-YEVVR6FE.js +66 -0
- package/dist/chunk-YEVVR6FE.js.map +1 -0
- package/dist/core/contracts.d.ts +114 -0
- package/dist/core/contracts.js +4 -0
- package/dist/core/contracts.js.map +1 -0
- package/dist/core/date-format.d.ts +87 -0
- package/dist/core/date-format.js +3 -0
- package/dist/core/date-format.js.map +1 -0
- package/dist/core/derive.d.ts +55 -0
- package/dist/core/derive.js +6 -0
- package/dist/core/derive.js.map +1 -0
- package/dist/core/descriptor.d.ts +172 -0
- package/dist/core/descriptor.js +3 -0
- package/dist/core/descriptor.js.map +1 -0
- package/dist/core/gate-validator.d.ts +27 -0
- package/dist/core/gate-validator.js +4 -0
- package/dist/core/gate-validator.js.map +1 -0
- package/dist/core/gate.d.ts +144 -0
- package/dist/core/gate.js +12 -0
- package/dist/core/gate.js.map +1 -0
- package/dist/core/gating-fields.d.ts +38 -0
- package/dist/core/gating-fields.js +6 -0
- package/dist/core/gating-fields.js.map +1 -0
- package/dist/core/index.d.ts +13 -623
- package/dist/core/index.js +14 -1
- package/dist/core/normalize-schema.d.ts +30 -0
- package/dist/core/normalize-schema.js +4 -0
- package/dist/core/normalize-schema.js.map +1 -0
- package/dist/core/own-property.d.ts +46 -0
- package/dist/core/own-property.js +3 -0
- package/dist/core/own-property.js.map +1 -0
- package/dist/core/readiness.d.ts +91 -0
- package/dist/core/readiness.js +4 -0
- package/dist/core/readiness.js.map +1 -0
- package/dist/core/schema-utils.d.ts +86 -0
- package/dist/core/schema-utils.js +3 -0
- package/dist/core/schema-utils.js.map +1 -0
- package/dist/core/validation-message.d.ts +39 -0
- package/dist/core/validation-message.js +4 -0
- package/dist/core/validation-message.js.map +1 -0
- package/dist/core/values.d.ts +98 -0
- package/dist/core/values.js +10 -0
- package/dist/core/values.js.map +1 -0
- package/dist/core/wire-format.d.ts +95 -0
- package/dist/core/wire-format.js +6 -0
- package/dist/core/wire-format.js.map +1 -0
- package/dist/react/index.d.ts +43 -2
- package/dist/react/index.js +166 -64
- package/dist/react/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -1
- package/dist/chunk-S6KOTTGD.js +0 -990
- package/dist/chunk-S6KOTTGD.js.map +0 -1
- package/dist/descriptor-DPrrfAjL.d.ts +0 -101
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { PresenceMarker, PipeIOContracts, PipeIOContract, PipeInputContract } from 'mthds/protocol';
|
|
2
|
+
export { IOMultiplicity, PipeIOContract, PipeIOContracts, PipeInputContract, PipeOutputContract } from 'mthds/protocol';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The `pipe_io_contracts` types and the kernel's gating predicates over them.
|
|
6
|
+
*
|
|
7
|
+
* The types are the STANDARD'S, imported from `mthds/protocol` and re-exported
|
|
8
|
+
* under the names this package has always exported. `pipe_io_contracts` is an
|
|
9
|
+
* MTHDS artifact - the standard specifies it (the "Pipe I/O Contracts" page)
|
|
10
|
+
* and its TypeScript client declares it - so this module states no shape of its
|
|
11
|
+
* own any more. What it keeps is the half that is genuinely the kernel's: the
|
|
12
|
+
* predicates that turn a declared slot into a gating answer, and the lookup
|
|
13
|
+
* that tolerates both key conventions a real payload arrives with.
|
|
14
|
+
*
|
|
15
|
+
* `mthds` is a TYPES-ONLY peer dependency: every import here is `import type`,
|
|
16
|
+
* so nothing survives into `dist/` and no consumer ships the standard's CLI.
|
|
17
|
+
* `scripts/assert-bundle.mjs` holds that on the built graph and lint holds it
|
|
18
|
+
* on the source. See docs/dependency-budget.md.
|
|
19
|
+
*
|
|
20
|
+
* The map itself is keyed by namespaced pipe ref (`<domain>.<pipe_code>`),
|
|
21
|
+
* NOT bare pipe code - use `getPipeIOContract` to look entries up by the bare
|
|
22
|
+
* codes the UI tracks.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The authored presence marker on an input slot, verbatim - the standard's
|
|
27
|
+
* `PresenceMarker` under the name this package exports.
|
|
28
|
+
*
|
|
29
|
+
* - `plain` - no marker: the caller must supply the input.
|
|
30
|
+
* - `optional` (`?`) - the caller may omit it and the pipe handles the absence
|
|
31
|
+
* itself.
|
|
32
|
+
* - `force` (`!`) - a use-site assertion that the value IS present. It must be
|
|
33
|
+
* supplied, exactly like `plain`; the distinction is the authored assertion,
|
|
34
|
+
* which lint and graph surfaces read and a form does not.
|
|
35
|
+
*
|
|
36
|
+
* Kept as an alias rather than renamed because a consumer imports it by this
|
|
37
|
+
* name; a host that would rather read the standard's spelling has `mthds`
|
|
38
|
+
* installed through this package's peer and can import `PresenceMarker`
|
|
39
|
+
* straight from `mthds/protocol`.
|
|
40
|
+
*/
|
|
41
|
+
type InputPresence = PresenceMarker;
|
|
42
|
+
/** Namespaced pipe ref (`<domain>.<pipe_code>`) - the identity convention the
|
|
43
|
+
* validate artifacts key on. */
|
|
44
|
+
declare function buildPipeRef(domain: string, pipeCode: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Look up a pipe's IO contract by pipe code, tolerant of both key conventions.
|
|
47
|
+
*
|
|
48
|
+
* The map may be keyed by namespaced `pipe_ref` (`<domain>.<code>`, the
|
|
49
|
+
* canonical convention) OR by bare pipe code - the current hosted `/validate`
|
|
50
|
+
* keys `pipe_structures` by bare code, and methods persisted before the
|
|
51
|
+
* pipe_ref re-keying also carry bare-code keys. Try the namespaced ref first
|
|
52
|
+
* (when the caller supplies a domain), then fall back to the bare code.
|
|
53
|
+
* Returns `undefined` (never crashes) when neither matches.
|
|
54
|
+
*
|
|
55
|
+
* Both lookups read OWN properties only. A bare index returned the inherited
|
|
56
|
+
* `Object` constructor for a pipe code named `constructor` (or `toString`, …) -
|
|
57
|
+
* a truthy non-contract that sailed straight through the `if (!contract)` guard
|
|
58
|
+
* hosts are shown writing, so a pipe that does not exist rendered as one taking
|
|
59
|
+
* no inputs instead of reaching the host's not-found path. See `ownProp`.
|
|
60
|
+
*/
|
|
61
|
+
declare function getPipeIOContract(pipeIoContracts: PipeIOContracts | null | undefined, domain: string | null | undefined, pipeCode: string | null | undefined): PipeIOContract | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* True when the method declared the input omittable (`?`).
|
|
64
|
+
*
|
|
65
|
+
* The ONE reader of `presence` in the package: everything that asks "may this
|
|
66
|
+
* be absent?" - the descriptor's `required`, the gate's `required` list, the
|
|
67
|
+
* payload builders that omit an unfilled input - goes through here, so a
|
|
68
|
+
* marker the wire adds later cannot mean one thing on one surface and another
|
|
69
|
+
* elsewhere. `force` (`!`) reads as NOT optional, like `plain`.
|
|
70
|
+
*/
|
|
71
|
+
declare function isOptionalInput(input: PipeInputContract): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* True when the input carries a plural concept (`Image[]`, `Image[3]`): its
|
|
74
|
+
* JSON Schema is an array wrapper.
|
|
75
|
+
*
|
|
76
|
+
* Deliberately a SCHEMA test rather than a read of the declared `multiplicity`,
|
|
77
|
+
* even though the wire now states both. The schema is what `buildRunFields`
|
|
78
|
+
* maps and therefore what the user is shown; a predicate reading the other
|
|
79
|
+
* field could disagree with the rendered control, which is the exact failure
|
|
80
|
+
* mode `mustBeFilled` (readiness.ts) exists to prevent. The two agree by
|
|
81
|
+
* construction upstream - the engine wraps the schema in an array exactly when
|
|
82
|
+
* it reports `variable` or `fixed`.
|
|
83
|
+
*/
|
|
84
|
+
declare function isPluralInput(input: PipeInputContract): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* True when the slot declares an EXACT number of items (`Concept[N]`), whose
|
|
87
|
+
* count is `item_count`.
|
|
88
|
+
*
|
|
89
|
+
* The distinction that matters to the gate: a variable list can legitimately be
|
|
90
|
+
* empty, a fixed one cannot.
|
|
91
|
+
*/
|
|
92
|
+
declare function isFixedCountInput(input: PipeInputContract): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* True when the user MUST put a value in before a run can start.
|
|
95
|
+
*
|
|
96
|
+
* Two declarations relieve an input of that duty, for different reasons:
|
|
97
|
+
* - **optional (`?`)** - the method itself states the caller may omit it;
|
|
98
|
+
* - **variable-plural (`[]`)** - the empty list is a legitimate value and the
|
|
99
|
+
* language cannot declare "at least one", so gating on it would invent a
|
|
100
|
+
* constraint the method never made.
|
|
101
|
+
*
|
|
102
|
+
* Everything else gates: `plain`, `force` (`!`), and a **fixed-count** list
|
|
103
|
+
* (`[N]`), which is a list whose empty form the method has explicitly ruled out.
|
|
104
|
+
* This is the derivation the input-form descriptor spec states for `gating`.
|
|
105
|
+
*
|
|
106
|
+
* Emptiness is all this predicate answers for - WHETHER a slot gates, never how
|
|
107
|
+
* much it needs. A fixed-count list that holds fewer items than it declares
|
|
108
|
+
* gates here and is refused by `fieldFilled`, which reads the declared count off
|
|
109
|
+
* the descriptor's `itemCount`, and by the gate's ajv pass on the schema's
|
|
110
|
+
* `minItems`. All three read the one number the method stated.
|
|
111
|
+
*/
|
|
112
|
+
declare function inputMustBeFilled(input: PipeInputContract): boolean;
|
|
113
|
+
|
|
114
|
+
export { type InputPresence, buildPipeRef, getPipeIOContract, inputMustBeFilled, isFixedCountInput, isOptionalInput, isPluralInput };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"contracts.js"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure conversions between a stored date value and what a native `<input
|
|
3
|
+
* type="date">` shows, plus the leniency rules our schema validation uses.
|
|
4
|
+
* Kept React-free so both the runner's `DateField` and the RJSF `DateWidget`
|
|
5
|
+
* share one source of truth (and it's trivially unit-tested).
|
|
6
|
+
*
|
|
7
|
+
* Two schema shapes reach us, and they are NOT interchangeable:
|
|
8
|
+
* - an MTHDS `type = "date"` structure field compiles to `format: date-time`,
|
|
9
|
+
* so its value round-trips as a full RFC 3339 timestamp;
|
|
10
|
+
* - `native.Date`'s own `date` field is a python `datetime.date`, i.e.
|
|
11
|
+
* `format: date` - a bare `YYYY-MM-DD`.
|
|
12
|
+
*
|
|
13
|
+
* ## Why these rules exist
|
|
14
|
+
*
|
|
15
|
+
* The runtime (pydantic) is LENIENT in both directions, and our client used to
|
|
16
|
+
* be stricter than the contract it was enforcing: `ajv`'s stock `format: "date"`
|
|
17
|
+
* accepts a bare `YYYY-MM-DD` and nothing else, so a hand-written
|
|
18
|
+
* `"2026-07-06T00:00:00Z"` in an `inputs.json` was rejected here even though
|
|
19
|
+
* pydantic accepts it happily and coerces it to `date(2026, 7, 6)`. The run
|
|
20
|
+
* never even reached the runner. These predicates mirror pydantic exactly
|
|
21
|
+
* (measured, not assumed):
|
|
22
|
+
*
|
|
23
|
+
* format: date "2026-07-06" -> date(2026, 7, 6)
|
|
24
|
+
* "2026-07-06T00:00:00Z" -> date(2026, 7, 6)
|
|
25
|
+
* "2026-07-06T00:00:00+02:00" -> date(2026, 7, 6) (literal date, no UTC shift)
|
|
26
|
+
* "2026-07-06T15:40:00Z" -> REJECTED "should have zero time"
|
|
27
|
+
* format: date-time "2026-07-06" -> datetime(2026, 7, 6, 0, 0)
|
|
28
|
+
* "2026-07-06T00:00:00Z" -> datetime(2026, 7, 6, 0, 0, tz)
|
|
29
|
+
*
|
|
30
|
+
* A midnight-padded timestamp is unambiguously "just a day", so take it. A real
|
|
31
|
+
* time is data - refusing it is the point (`DateContentError`: "no silent
|
|
32
|
+
* midnight"), because silently dropping 15:40 corrupts the value. And an
|
|
33
|
+
* ambiguous `06/07/2026` is never accepted by anyone: 6 July or June 7 depends
|
|
34
|
+
* on who's reading, and guessing wrong fails silently.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* The calendar day a value denotes, or `null` when it doesn't denote one.
|
|
38
|
+
* Accepts a bare date or a midnight-padded timestamp; the day is taken
|
|
39
|
+
* LITERALLY from the string (never shifted through a timezone), matching
|
|
40
|
+
* pydantic - `2026-07-06T00:00:00+02:00` is 2026-07-06, not the 5th.
|
|
41
|
+
*/
|
|
42
|
+
declare function asCalendarDate(value: string): string | null;
|
|
43
|
+
/**
|
|
44
|
+
* The calendar day inside a value that carries a REAL time - precisely the case
|
|
45
|
+
* `asCalendarDate` refuses. `null` when the value isn't a timestamp at all, or
|
|
46
|
+
* when it IS already an acceptable day (nothing to report).
|
|
47
|
+
*
|
|
48
|
+
* This exists to EXPLAIN, never to coerce. `2026-07-06T14:30:00+02:00` is
|
|
49
|
+
* rejected for a `format: date` field, and the rejection is right (DT3, "no
|
|
50
|
+
* silent midnight" - dropping 14:30 corrupts the value). But telling the user
|
|
51
|
+
* "pick a valid date" while the picker shows them a perfectly good 06/07/2026
|
|
52
|
+
* reads as a lie: `toDateInputValue` renders the day, the stored value carries
|
|
53
|
+
* the time, and only one of those is what gets validated. Naming both the value
|
|
54
|
+
* we hold and the day it would have to become is what makes the error legible.
|
|
55
|
+
*/
|
|
56
|
+
declare function dayWithinDateTime(value: string): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* What `format: "date"` accepts: a bare `YYYY-MM-DD`, and nothing else.
|
|
59
|
+
*
|
|
60
|
+
* DO NOT relax this to take a midnight-padded timestamp. `DateContent` in
|
|
61
|
+
* pipelex carries a `mode="before"` field validator (`_reject_lax_temporal`)
|
|
62
|
+
* that deliberately CLOSES pydantic's lax coercion for exactly this case:
|
|
63
|
+
*
|
|
64
|
+
* "A Date's `date` field takes a calendar date alone; a string carrying a
|
|
65
|
+
* time (e.g. '2026-07-07T00:00:00') would drop it - put the time and offset
|
|
66
|
+
* in `time` instead."
|
|
67
|
+
*
|
|
68
|
+
* That rule (DT3, "no silent midnight") is the concept's whole point, so a
|
|
69
|
+
* lenient client here does not help anyone: it just forwards the value and the
|
|
70
|
+
* runner answers with a 500 instead of the field saying so up front.
|
|
71
|
+
*
|
|
72
|
+
* The leniency belongs in `asCalendarDate` - normalize the pasted timestamp to
|
|
73
|
+
* the day it denotes, then this passes. Accept-and-fix, not accept-and-forward.
|
|
74
|
+
*/
|
|
75
|
+
declare function isAcceptableDate(value: string): boolean;
|
|
76
|
+
/** What `format: "date-time"` should accept - a bare day is midnight. */
|
|
77
|
+
declare function isAcceptableDateTime(value: string): boolean;
|
|
78
|
+
/** The `YYYY-MM-DD` a native date input shows, from any stored date/date-time. */
|
|
79
|
+
declare function toDateInputValue(value: string | undefined): string;
|
|
80
|
+
/**
|
|
81
|
+
* The value to store for a picked day. A `date-time` field becomes midnight-UTC
|
|
82
|
+
* (`2026-07-06T00:00:00Z`); a plain `date` field keeps the bare `YYYY-MM-DD`.
|
|
83
|
+
* An empty day clears the value.
|
|
84
|
+
*/
|
|
85
|
+
declare function toStoredDateValue(day: string, datetime: boolean): string | undefined;
|
|
86
|
+
|
|
87
|
+
export { asCalendarDate, dayWithinDateTime, isAcceptableDate, isAcceptableDateTime, toDateInputValue, toStoredDateValue };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"date-format.js"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PipeInputFormDescriptor, PipeInputContract, InputForm } from 'mthds/protocol';
|
|
2
|
+
import { RunField } from './descriptor.js';
|
|
3
|
+
import './contracts.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The ONE derivation function: wire input-form descriptor → `RunField[]`.
|
|
7
|
+
*
|
|
8
|
+
* `buildRunFields` maps the standard's per-pipe descriptor STRUCTURALLY onto
|
|
9
|
+
* the kernel's `RunField` shape. It decides nothing: what a field IS - its
|
|
10
|
+
* kind, its constraints, its refinement chain, whether Run gates on it - is
|
|
11
|
+
* stated by the wire, derived by the engine from the resolved library. The
|
|
12
|
+
* heuristics that used to live here (native-concept sets, the url-bearing
|
|
13
|
+
* object test, the depth-based prose/label rule) are deleted, not bypassed;
|
|
14
|
+
* docs/derivation-swap.md is the record of that swap and of every behaviour
|
|
15
|
+
* that moved with it.
|
|
16
|
+
*
|
|
17
|
+
* The contract is still consulted, for exactly two facts the wire deliberately
|
|
18
|
+
* does not carry:
|
|
19
|
+
*
|
|
20
|
+
* - **`contentKey`** - the single property a native scalar's value sits inside
|
|
21
|
+
* on the wire (`TextContent {text}`, `NumberContent {number}`). A renderer
|
|
22
|
+
* holds the bare scalar; the value bridge wraps by this name. It is read off
|
|
23
|
+
* the contract's `json_schema` because it is a fact about the PAYLOAD shape,
|
|
24
|
+
* which is the schema's job - the descriptor describes the form, never the
|
|
25
|
+
* payload.
|
|
26
|
+
* - **Nested list bounds** - the wire puts `item_count` only on a top-level
|
|
27
|
+
* fixed `[N]` slot, but the model behind a structured concept may state
|
|
28
|
+
* `minItems`/`maxItems` on an array property of its own, and ajv enforces
|
|
29
|
+
* them. Reading them off the schema is what keeps readiness from being more
|
|
30
|
+
* permissive than the gate.
|
|
31
|
+
*
|
|
32
|
+
* Both are structural reads (a property count, two keywords ajv itself
|
|
33
|
+
* enforces), not judgements - the mapper stays honest about owning no taxonomy.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Look up a pipe's input-form descriptor by pipe code, tolerant of both key
|
|
38
|
+
* conventions - the exact mirror of `getPipeIOContract`, because the standard
|
|
39
|
+
* keys `input_form` by the same `pipe_ref` set as `pipe_io_contracts` and a
|
|
40
|
+
* host tracks the same bare codes either way.
|
|
41
|
+
*/
|
|
42
|
+
declare function getPipeInputForm(inputForm: InputForm | null | undefined, domain: string | null | undefined, pipeCode: string | null | undefined): PipeInputFormDescriptor | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Map a pipe's wire input-form descriptor onto the kernel's field descriptors,
|
|
45
|
+
* with the pipe's input contracts beside it for the two schema-derived facts.
|
|
46
|
+
*
|
|
47
|
+
* The order is the DESCRIPTOR's - authored input order, the fact the contract's
|
|
48
|
+
* `inputs` map deliberately does not carry. `required`, `presence` and `gating`
|
|
49
|
+
* are taken from the wire verbatim: the standard's top-level type ties
|
|
50
|
+
* `required` to the presence marker, and `gating` is the producer-derived
|
|
51
|
+
* answer `mustBeFilled` reads - the kernel re-derives none of them.
|
|
52
|
+
*/
|
|
53
|
+
declare function buildRunFields(descriptor: PipeInputFormDescriptor, inputs: Record<string, PipeInputContract>): RunField[];
|
|
54
|
+
|
|
55
|
+
export { buildRunFields, getPipeInputForm };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"derive.js"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { InputPresence } from './contracts.js';
|
|
2
|
+
import 'mthds/protocol';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The `RunField` descriptor - the consumer-facing currency of the form kernel.
|
|
6
|
+
*
|
|
7
|
+
* A method's inputs arrive from the API as two sibling artifacts: the wire
|
|
8
|
+
* input-form descriptor (`input_form`, the ordered presentation view the MTHDS
|
|
9
|
+
* standard specifies) and the `pipe_io_contracts` entry whose `json_schema`
|
|
10
|
+
* stays the validation contract. `buildRunFields` (in `derive.ts`) maps the
|
|
11
|
+
* wire descriptor structurally onto this `RunField[]` shape - so the
|
|
12
|
+
* presentational field components never touch either wire artifact directly,
|
|
13
|
+
* and stories can describe any input shape declaratively.
|
|
14
|
+
*
|
|
15
|
+
* Pure module: no React, no API calls. Trivially unit-testable.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type RunFieldKind = 'text' | 'prose' | 'date' | 'number' | 'boolean' | 'enum' | 'document' | 'image' | 'object' | 'list' | 'unknown';
|
|
19
|
+
interface RunFieldCommon {
|
|
20
|
+
/** The variable identifier as written in the method (rendered in mono). */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Human label; falls back to `name` when absent. */
|
|
23
|
+
title?: string;
|
|
24
|
+
/** The concept this input carries, e.g. `native.Document`, `demo.Invoice`. */
|
|
25
|
+
conceptRef?: string;
|
|
26
|
+
/**
|
|
27
|
+
* The concept's refinement chain, immediate parent first, walked to its end
|
|
28
|
+
* (`['legal.BaseClause', 'native.Text']`). Straight off the wire descriptor;
|
|
29
|
+
* absent when the concept refines nothing. "Does this refine `native.X`?" is
|
|
30
|
+
* a membership test on this list, never shape sniffing.
|
|
31
|
+
*/
|
|
32
|
+
refines?: string[];
|
|
33
|
+
/** One-line helper text shown under the label. */
|
|
34
|
+
description?: string;
|
|
35
|
+
/** Required inputs always show; optional ones can collapse. */
|
|
36
|
+
required: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The authored presence marker of the pipe's input slot, verbatim - set on
|
|
39
|
+
* TOP-LEVEL fields only, like `gating`. `required` is its two-valued
|
|
40
|
+
* projection (`presence !== 'optional'`); the marker itself is carried so a
|
|
41
|
+
* renderer can distinguish the authored `!` assertion from a plain slot.
|
|
42
|
+
*/
|
|
43
|
+
presence?: InputPresence;
|
|
44
|
+
/**
|
|
45
|
+
* The value applied when the caller omits the field - present only when a
|
|
46
|
+
* default was AUTHORED, never the `null` a schema projection attaches to an
|
|
47
|
+
* optional field. A defaulted field always arrives `required: false`.
|
|
48
|
+
*/
|
|
49
|
+
defaultValue?: unknown;
|
|
50
|
+
/** Example values for the field, when the wire carries any. */
|
|
51
|
+
examples?: unknown[];
|
|
52
|
+
/**
|
|
53
|
+
* The node's effective intent hints - the flat `string → string` merge the
|
|
54
|
+
* language defines, carried verbatim. Non-normative: a renderer that ignores
|
|
55
|
+
* them stays correct, and nothing in the kernel's gating reads them.
|
|
56
|
+
*/
|
|
57
|
+
hints?: Record<string, string>;
|
|
58
|
+
/**
|
|
59
|
+
* Whether Run waits for this input - set on TOP-LEVEL fields only, straight
|
|
60
|
+
* from `inputMustBeFilled` (see `mustBeFilled` in `readiness.ts` for why this
|
|
61
|
+
* is not the same question as `required`). Nested fields leave it undefined;
|
|
62
|
+
* they are gated through their parent's `fieldFilled`.
|
|
63
|
+
*/
|
|
64
|
+
gating?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* The single property this field's value sits inside on the wire, when its
|
|
67
|
+
* concept declares a scalar content model: `native.Text` is `TextContent
|
|
68
|
+
* {text}`, `native.Number` is `NumberContent {number}`, `native.YesNo` is
|
|
69
|
+
* `YesNoContent {yes_no}`. The control still holds the plain scalar; the
|
|
70
|
+
* value bridge wraps into this property on the way out and unwraps on the way
|
|
71
|
+
* back in.
|
|
72
|
+
*
|
|
73
|
+
* A property NAME, never a schema - the descriptor stays the only currency
|
|
74
|
+
* and `buildRunFields` stays the only reader of JSON Schema. Absent whenever
|
|
75
|
+
* the value is already the whole content (a structured concept, a file, a
|
|
76
|
+
* child property of a structure): wrapping those would double-nest them.
|
|
77
|
+
*/
|
|
78
|
+
contentKey?: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The constraint slots the two text kinds share, mapped from the wire's
|
|
82
|
+
* `min_length` / `max_length` / `pattern` / `format`. `format` is the open
|
|
83
|
+
* string set carrying schema formats the `date` kind does not absorb
|
|
84
|
+
* (`'time'`, `'uri'`, …); a renderer may hint on it and ignore it safely.
|
|
85
|
+
*/
|
|
86
|
+
interface TextConstraints {
|
|
87
|
+
minLength?: number;
|
|
88
|
+
maxLength?: number;
|
|
89
|
+
pattern?: string;
|
|
90
|
+
format?: string;
|
|
91
|
+
}
|
|
92
|
+
interface TextRunField extends RunFieldCommon, TextConstraints {
|
|
93
|
+
kind: 'text';
|
|
94
|
+
placeholder?: string;
|
|
95
|
+
}
|
|
96
|
+
interface ProseRunField extends RunFieldCommon, TextConstraints {
|
|
97
|
+
kind: 'prose';
|
|
98
|
+
placeholder?: string;
|
|
99
|
+
}
|
|
100
|
+
interface DateRunField extends RunFieldCommon {
|
|
101
|
+
kind: 'date';
|
|
102
|
+
/**
|
|
103
|
+
* The wire's `datetime` flag - true when the value carries a time of day
|
|
104
|
+
* (the backing schema is `format: date-time`): the stored value is a full
|
|
105
|
+
* RFC 3339 timestamp (`2026-07-06T00:00:00Z`) so it passes the API's
|
|
106
|
+
* `date-time` validation, even though the control only asks for a day. False
|
|
107
|
+
* for a bare calendar date, where the value stays `YYYY-MM-DD`.
|
|
108
|
+
*/
|
|
109
|
+
datetime: boolean;
|
|
110
|
+
}
|
|
111
|
+
interface NumberRunField extends RunFieldCommon {
|
|
112
|
+
kind: 'number';
|
|
113
|
+
integer: boolean;
|
|
114
|
+
min?: number;
|
|
115
|
+
max?: number;
|
|
116
|
+
}
|
|
117
|
+
interface BooleanRunField extends RunFieldCommon {
|
|
118
|
+
kind: 'boolean';
|
|
119
|
+
}
|
|
120
|
+
interface EnumRunField extends RunFieldCommon {
|
|
121
|
+
kind: 'enum';
|
|
122
|
+
options: string[];
|
|
123
|
+
}
|
|
124
|
+
interface FileRunField extends RunFieldCommon {
|
|
125
|
+
kind: 'document' | 'image';
|
|
126
|
+
/** Accept hint shown to the user, e.g. "PDF, PNG, JPG". */
|
|
127
|
+
accept?: string;
|
|
128
|
+
}
|
|
129
|
+
interface ObjectRunField extends RunFieldCommon {
|
|
130
|
+
kind: 'object';
|
|
131
|
+
fields: RunField[];
|
|
132
|
+
}
|
|
133
|
+
interface ListRunField extends RunFieldCommon {
|
|
134
|
+
kind: 'list';
|
|
135
|
+
/** The element descriptor (its `name` is unused; the index labels items). */
|
|
136
|
+
item: RunField;
|
|
137
|
+
/**
|
|
138
|
+
* The FEWEST items the slot accepts. Absent for a variable `Concept[]` list,
|
|
139
|
+
* which demands none.
|
|
140
|
+
*
|
|
141
|
+
* On a top-level slot this is the wire descriptor's `item_count` (present
|
|
142
|
+
* exactly on a fixed `[N]` slot); an engine emits `minItems`/`maxItems` = N
|
|
143
|
+
* on the same slot's schema, so the number `fieldFilled` gates on and the
|
|
144
|
+
* keyword ajv reads agree by construction.
|
|
145
|
+
*
|
|
146
|
+
* The wire never puts `item_count` on a NESTED list, but the model behind a
|
|
147
|
+
* structured concept may state `minItems` on an array property of its own -
|
|
148
|
+
* so on nested nodes this is read off the schema, which is the direction
|
|
149
|
+
* that cannot leave readiness more permissive than the gate.
|
|
150
|
+
*/
|
|
151
|
+
itemCount?: number;
|
|
152
|
+
/**
|
|
153
|
+
* The MOST items the slot accepts, off the schema's `maxItems` (which a
|
|
154
|
+
* fixed `[N]` slot also states as N).
|
|
155
|
+
*
|
|
156
|
+
* A separate field because it is a separate fact, and conflating them is a
|
|
157
|
+
* bug that hides: `itemCount` was read as a minimum by readiness and as a
|
|
158
|
+
* maximum by the control that stops offering `Add`, which agree only while
|
|
159
|
+
* a contract states both bounds equal. A list told "at least two" was then
|
|
160
|
+
* presented as a list of exactly two.
|
|
161
|
+
*/
|
|
162
|
+
maxItemCount?: number;
|
|
163
|
+
}
|
|
164
|
+
interface UnknownRunField extends RunFieldCommon {
|
|
165
|
+
kind: 'unknown';
|
|
166
|
+
}
|
|
167
|
+
type RunField = TextRunField | ProseRunField | DateRunField | NumberRunField | BooleanRunField | EnumRunField | FileRunField | ObjectRunField | ListRunField | UnknownRunField;
|
|
168
|
+
/** The short, friendly category label for a concept_ref (drives the pill dot). */
|
|
169
|
+
type ConceptCategory = 'text' | 'date' | 'document' | 'image' | 'number' | 'boolean' | 'choice' | 'structured' | 'list';
|
|
170
|
+
declare function conceptCategory(field: RunField): ConceptCategory;
|
|
171
|
+
|
|
172
|
+
export { type BooleanRunField, type ConceptCategory, type DateRunField, type EnumRunField, type FileRunField, type ListRunField, type NumberRunField, type ObjectRunField, type ProseRunField, type RunField, type RunFieldCommon, type RunFieldKind, type TextConstraints, type TextRunField, type UnknownRunField, conceptCategory };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"descriptor.js"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One validation error, in the shape hosts read (`property`, `message`,
|
|
3
|
+
* `name`, `stack`, `params.format`). Structurally a superset-compatible twin of
|
|
4
|
+
* RJSF's `RJSFValidationError`, so a host surface still holding RJSF form errors
|
|
5
|
+
* (an RJSF-based inputs panel) can hand them to the same message renderer.
|
|
6
|
+
*/
|
|
7
|
+
interface RunInputError {
|
|
8
|
+
/** The ajv keyword that failed (`required`, `format`, `type`, …). */
|
|
9
|
+
name?: string;
|
|
10
|
+
/** Dotted path into the form data (`.quote_date.date`); for a `required`
|
|
11
|
+
* error the missing property name is appended (top-level: no leading dot). */
|
|
12
|
+
property?: string;
|
|
13
|
+
message?: string;
|
|
14
|
+
params?: Record<string, unknown>;
|
|
15
|
+
/** The human line: `property message`, or `'title' message` when the parent
|
|
16
|
+
* schema titles the field. */
|
|
17
|
+
stack: string;
|
|
18
|
+
schemaPath?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Validate `data` against `schema`, returning mapped errors (empty = valid).
|
|
22
|
+
* MUTATES `data` where ajv coerces (`"20"` → `20`) - deliberate and relied on:
|
|
23
|
+
* the string the user typed is stored as a number going forward.
|
|
24
|
+
*/
|
|
25
|
+
declare function validateRunInputsSchema(data: unknown, schema: object): RunInputError[];
|
|
26
|
+
|
|
27
|
+
export { type RunInputError, validateRunInputsSchema };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"gate-validator.js"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { RunInputError } from './gate-validator.js';
|
|
2
|
+
import { PipeInputContract, PipeIOContract } from 'mthds/protocol';
|
|
3
|
+
|
|
4
|
+
type Dict = Record<string, unknown>;
|
|
5
|
+
/**
|
|
6
|
+
* Combine the per-input schemas into one object schema.
|
|
7
|
+
*
|
|
8
|
+
* Each input variable becomes a property, titled with the variable name and
|
|
9
|
+
* described with the concept code. Only the inputs the method actually demands
|
|
10
|
+
* land in `required` - an optional (`?`) or variable-plural (`[]`) input must
|
|
11
|
+
* not block the form (see `inputMustBeFilled`).
|
|
12
|
+
*
|
|
13
|
+
* The per-input schema travels verbatim, which is how a fixed-count (`[N]`)
|
|
14
|
+
* list's declared count reaches ajv: pipelex states it as `minItems`/`maxItems`
|
|
15
|
+
* on the array wrapper, so the count is enforced here without the kernel
|
|
16
|
+
* restating it.
|
|
17
|
+
*/
|
|
18
|
+
declare function buildRunInputsSchema(inputs: Record<string, PipeInputContract>): Dict;
|
|
19
|
+
/**
|
|
20
|
+
* Last-gate repair, in two passes against the exact schema ajv validates.
|
|
21
|
+
*
|
|
22
|
+
* 1. **HEAL** - persisted `inputData` may still carry the legacy double-wrapped
|
|
23
|
+
* `{ text: { text } }` shape (or a form's state may lag a store fix).
|
|
24
|
+
* 2. **PRUNE** - RJSF fills every unset string with `""` on mount, which an
|
|
25
|
+
* optional CONSTRAINED field rejects (`DateContent.time` is `format: "time"`)
|
|
26
|
+
* - blocking a run on a field the user never opened. An absent optional field
|
|
27
|
+
* is what pipelex means by `None` anyway, so dropping it is the semantically
|
|
28
|
+
* correct repair, not a validation dodge.
|
|
29
|
+
*/
|
|
30
|
+
declare function prepareRunInputs(data: Dict, schema: Dict): Dict;
|
|
31
|
+
interface RunInputsVerdict {
|
|
32
|
+
isValid: boolean;
|
|
33
|
+
/** Variable names the scan could pin a missing required field on. */
|
|
34
|
+
missingInputs: string[];
|
|
35
|
+
/** Raw ajv errors, for when the scan cannot name anything. */
|
|
36
|
+
errors: RunInputError[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validate PREPARED data against the combined schema.
|
|
40
|
+
*
|
|
41
|
+
* `missingInputs` is reported by VARIABLE NAME - the name on the node the user
|
|
42
|
+
* clicks - never by `concept_ref`, which used to produce "Missing required
|
|
43
|
+
* fields in: native.Text" and named neither the input at fault nor, when several
|
|
44
|
+
* inputs share a concept, which of them. Inputs the method declared omittable
|
|
45
|
+
* are skipped: an empty `?` input is not a missing one.
|
|
46
|
+
*
|
|
47
|
+
* The scan can come up empty even on an invalid form (a wrong value shape, a
|
|
48
|
+
* nested mismatch); `errors` is what the caller falls back to then, so a failure
|
|
49
|
+
* is never undiagnosable.
|
|
50
|
+
*
|
|
51
|
+
* `preparedData` is `unknown` for the same reason `gateRunInputs`' `data` is: on
|
|
52
|
+
* a public endpoint the body is whatever was sent, and ajv must be shown that
|
|
53
|
+
* rather than a repair of it, or `must be object` is a verdict no caller can
|
|
54
|
+
* ever receive. The name-the-variable scan below needs something indexable, so
|
|
55
|
+
* a body that is not a record scans against nothing and names every input the
|
|
56
|
+
* method demands - which is the honest answer for a body that carried none.
|
|
57
|
+
*/
|
|
58
|
+
declare function validateRunInputs(preparedData: unknown, inputs: Record<string, PipeInputContract>, schema: Dict): RunInputsVerdict;
|
|
59
|
+
/**
|
|
60
|
+
* Build the `{ concept, content }` map a run expects, from PREPARED data.
|
|
61
|
+
*
|
|
62
|
+
* The two ways a method says "this may be nothing" land differently on the wire:
|
|
63
|
+
* - an **optional (`?`)** input left blank is OMITTED, so the runtime records a
|
|
64
|
+
* real absence the method can branch on - sending `{ text: "" }` would look
|
|
65
|
+
* like the caller supplied an empty string;
|
|
66
|
+
* - a **plural (`[]`)** input keeps its key, because a plural slot is never
|
|
67
|
+
* absent - its empty form IS the empty list, and the runtime requires the key.
|
|
68
|
+
* When that list is EMPTY it is sent BARE, without the envelope: the envelope
|
|
69
|
+
* is the runtime's "explicit form", which bypasses the top-down shaper and
|
|
70
|
+
* hands the value to the bottom-up factory, and that factory types a list from
|
|
71
|
+
* its first item - so an empty list there raises "Cannot create Stuff from
|
|
72
|
+
* empty list in content". The bare form keeps the shaper, which reads the
|
|
73
|
+
* DECLARED concept and builds the empty list correctly. (pipelex/#1096 makes
|
|
74
|
+
* the envelope agree; the bare form runs today and stays correct after.)
|
|
75
|
+
*
|
|
76
|
+
* A fixed-count (`[N]`) list takes that same empty-list path, and an empty one
|
|
77
|
+
* is not a payload the method can accept. It is unreachable through the gate,
|
|
78
|
+
* which demands the input and rejects a short list on `minItems`; building the
|
|
79
|
+
* payload straight from unvalidated data is what would reach it, and inventing
|
|
80
|
+
* a different absence here would only hide that.
|
|
81
|
+
*/
|
|
82
|
+
declare function apiInputsFromSchemaData(preparedData: Dict, inputs: Record<string, PipeInputContract>): Dict;
|
|
83
|
+
/** The gate's verdict: the payload to send, or why the run may not start. */
|
|
84
|
+
type RunInputsGateResult = {
|
|
85
|
+
ok: true;
|
|
86
|
+
inputs: Dict;
|
|
87
|
+
} | {
|
|
88
|
+
ok: false;
|
|
89
|
+
/** Variable names the caller left empty or short. */
|
|
90
|
+
missingInputs: string[];
|
|
91
|
+
/** Raw ajv errors, for when the scan cannot name anything. */
|
|
92
|
+
errors: RunInputError[];
|
|
93
|
+
/** The repaired data the verdict was reached on - what
|
|
94
|
+
* `describeValidationError` needs to quote the value it received. */
|
|
95
|
+
preparedData: Dict;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Gate a run: repair the caller's data, validate it against the contract,
|
|
99
|
+
* refuse anything the Run button would have refused, and build the payload.
|
|
100
|
+
*
|
|
101
|
+
* Returns a verdict rather than throwing. A server endpoint must not throw
|
|
102
|
+
* across the boundary to its client - frameworks routinely strip the message to
|
|
103
|
+
* an opaque digest in a production build - and a rejection here is an ordinary
|
|
104
|
+
* outcome, not an exceptional one.
|
|
105
|
+
*
|
|
106
|
+
* **`data` is `unknown` because that is what it is.** A run endpoint is public
|
|
107
|
+
* and no framework enforces a declared parameter type, so the argument is
|
|
108
|
+
* whatever the caller put in the body. The chain indexes the payload by
|
|
109
|
+
* variable name without first checking it is indexable, so a `null` body would
|
|
110
|
+
* throw *after* ajv had already reported `must be object` and before any
|
|
111
|
+
* verdict came back.
|
|
112
|
+
*
|
|
113
|
+
* So the two uses are separated rather than collapsed into one repair. **ajv
|
|
114
|
+
* judges the value the caller actually sent**, which is what makes `must be
|
|
115
|
+
* object` the verdict for a body that is not one; the indexing that follows
|
|
116
|
+
* uses a safe `{}` stand-in, which is what keeps the gate from throwing. Repair
|
|
117
|
+
* FIRST - coercing a non-object to `{}` before validating - is the shape that
|
|
118
|
+
* looks equivalent and is not: a contract whose inputs are all optional has an
|
|
119
|
+
* empty `required` list, so `{}` validates, and `gateRunInputs("garbage", …)`
|
|
120
|
+
* came back `ok: true` on a body the caller never meant as a run. Fail-closed
|
|
121
|
+
* costs a malformed caller an error it earned; the other direction bills a run.
|
|
122
|
+
*
|
|
123
|
+
* **The schema alone is not the rule.** ajv's `required` asserts only that a
|
|
124
|
+
* key is PRESENT, and a content model carries no `minLength` - so a required
|
|
125
|
+
* input that arrived empty (`{document: {url: ""}}`, `{text: {text: ""}}`)
|
|
126
|
+
* satisfies the schema. That is the natural payload, not a contrived one:
|
|
127
|
+
* `rjsfDataFromRunValues({}, fields)` emits exactly that when nothing is
|
|
128
|
+
* selected. So the gate re-applies the emptiness rule using
|
|
129
|
+
* `computeReadiness`'s OWN two functions over the same derived fields, which is
|
|
130
|
+
* the only way to be sure the two sides cannot disagree.
|
|
131
|
+
*
|
|
132
|
+
* Picking a pair that merely looks equivalent is not enough, and this is the
|
|
133
|
+
* whole reason the assembly lives here rather than in each host: the obvious
|
|
134
|
+
* choice, `inputMustBeFilled` + `isFilled`, agrees on every leaf kind and
|
|
135
|
+
* diverges on a structured concept in BOTH directions - `isFilled` on an object
|
|
136
|
+
* is `some(child filled)` where `fieldFilled` is `every(required child
|
|
137
|
+
* filled)`. That accepts a half-filled struct the browser refuses (a paid run
|
|
138
|
+
* past a disabled button) and rejects a filled all-optional struct the browser
|
|
139
|
+
* accepts. A host whose methods happen to use only native concepts has no test
|
|
140
|
+
* that can catch either.
|
|
141
|
+
*/
|
|
142
|
+
declare function gateRunInputs(contract: PipeIOContract, data: unknown): RunInputsGateResult;
|
|
143
|
+
|
|
144
|
+
export { type RunInputsGateResult, type RunInputsVerdict, apiInputsFromSchemaData, buildRunInputsSchema, gateRunInputs, prepareRunInputs, validateRunInputs };
|