@es-joy/jsoe 0.28.0 → 0.28.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/CHANGES.md +8 -0
  2. package/badges/coverage-badge.svg +1 -1
  3. package/badges/tests-badge.svg +1 -1
  4. package/dist/deepEqual.d.ts +3 -0
  5. package/dist/deepEqual.d.ts.map +1 -0
  6. package/dist/formats/schema.d.ts.map +1 -1
  7. package/dist/fundamentalTypes/arrayType.d.ts.map +1 -1
  8. package/dist/fundamentalTypes/stringType.d.ts.map +1 -1
  9. package/dist/index.js +2 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/search/queryTree.d.ts +180 -0
  12. package/dist/search/queryTree.d.ts.map +1 -0
  13. package/dist/search/queryTreeBuilders.d.ts +156 -0
  14. package/dist/search/queryTreeBuilders.d.ts.map +1 -0
  15. package/dist/search/searchDispatch.d.ts +77 -0
  16. package/dist/search/searchDispatch.d.ts.map +1 -0
  17. package/dist/typeChoices.d.ts.map +1 -1
  18. package/dist/types.d.ts.map +1 -1
  19. package/dist/utils/rawTypesonEditor.d.ts.map +1 -1
  20. package/docs/proposals/search-plan.md +166 -0
  21. package/package.json +22 -9
  22. package/pnpm-workspace.yaml +31 -30
  23. package/src/formats/schema.js +8 -14
  24. package/src/fundamentalTypes/arrayType.js +14 -22
  25. package/src/fundamentalTypes/blobType.js +16 -16
  26. package/src/fundamentalTypes/fileType.js +13 -13
  27. package/src/fundamentalTypes/regexpType.js +1 -1
  28. package/src/fundamentalTypes/stringType.js +5 -0
  29. package/src/subTypes/blobHTMLType.js +1 -1
  30. package/src/superTypes/dommatrixType.js +1 -1
  31. package/src/superTypes/errorsSpecialType.js +1 -1
  32. package/src/typeChoices.js +1 -3
  33. package/src/types.js +0 -6
  34. package/src/utils/media.js +3 -3
  35. package/src/utils/rawTypesonEditor.js +12 -2
  36. package/tsconfig.json +1 -1
  37. package/vendor/jamilih/dist/jml.mjs +898 -25
  38. package/vendor/zod/classic/checks.d.ts +1 -1
  39. package/vendor/zod/classic/checks.js +1 -1
  40. package/vendor/zod/classic/schemas.d.ts +2 -6
  41. package/vendor/zod/classic/schemas.js +9 -13
  42. package/vendor/zod/core/api.d.ts +8 -4
  43. package/vendor/zod/core/api.js +11 -23
  44. package/vendor/zod/core/checks.d.ts +16 -0
  45. package/vendor/zod/core/checks.js +31 -0
  46. package/vendor/zod/core/compile.js +20 -24
  47. package/vendor/zod/core/core.js +1 -1
  48. package/vendor/zod/core/json-schema-processors.d.ts +0 -1
  49. package/vendor/zod/core/json-schema-processors.js +8 -43
  50. package/vendor/zod/core/memoizer.js +2 -5
  51. package/vendor/zod/core/regexes.d.ts +1 -0
  52. package/vendor/zod/core/regexes.js +2 -0
  53. package/vendor/zod/core/schemas.d.ts +8 -23
  54. package/vendor/zod/core/schemas.js +39 -80
  55. package/vendor/zod/core/to-json-schema.d.ts +0 -3
  56. package/vendor/zod/core/to-json-schema.js +1 -1
  57. package/vendor/zod/core/util.js +0 -4
  58. package/vendor/zod/core/versions.js +1 -1
  59. package/vendor/zod/core/visit.js +0 -12
  60. package/vendor/zod/locales/ar.js +1 -0
  61. package/vendor/zod/locales/az.js +1 -0
  62. package/vendor/zod/locales/be.js +1 -0
  63. package/vendor/zod/locales/bg.js +1 -0
  64. package/vendor/zod/locales/bn.js +1 -0
  65. package/vendor/zod/locales/ca.js +1 -0
  66. package/vendor/zod/locales/ckb.js +1 -0
  67. package/vendor/zod/locales/cs.js +1 -0
  68. package/vendor/zod/locales/da.js +1 -0
  69. package/vendor/zod/locales/de.js +1 -0
  70. package/vendor/zod/locales/el.js +1 -0
  71. package/vendor/zod/locales/en.js +1 -0
  72. package/vendor/zod/locales/eo.js +1 -0
  73. package/vendor/zod/locales/es.js +1 -0
  74. package/vendor/zod/locales/fa.js +1 -0
  75. package/vendor/zod/locales/fi.js +1 -0
  76. package/vendor/zod/locales/fr-CA.js +1 -0
  77. package/vendor/zod/locales/fr.js +1 -0
  78. package/vendor/zod/locales/gu.js +1 -0
  79. package/vendor/zod/locales/he.js +1 -0
  80. package/vendor/zod/locales/hi.js +1 -0
  81. package/vendor/zod/locales/hr.js +1 -0
  82. package/vendor/zod/locales/hu.js +1 -0
  83. package/vendor/zod/locales/hy.js +1 -0
  84. package/vendor/zod/locales/id.js +1 -0
  85. package/vendor/zod/locales/is.js +1 -0
  86. package/vendor/zod/locales/it.js +1 -0
  87. package/vendor/zod/locales/ja.js +1 -0
  88. package/vendor/zod/locales/ka.js +1 -0
  89. package/vendor/zod/locales/km.js +1 -0
  90. package/vendor/zod/locales/kn.js +1 -0
  91. package/vendor/zod/locales/ko.js +1 -0
  92. package/vendor/zod/locales/lt.js +1 -0
  93. package/vendor/zod/locales/mk.js +1 -0
  94. package/vendor/zod/locales/ms.js +1 -0
  95. package/vendor/zod/locales/ne.js +1 -0
  96. package/vendor/zod/locales/nl.js +1 -0
  97. package/vendor/zod/locales/nn.js +1 -0
  98. package/vendor/zod/locales/no.js +1 -0
  99. package/vendor/zod/locales/ota.js +1 -0
  100. package/vendor/zod/locales/pl.js +1 -0
  101. package/vendor/zod/locales/ps.js +1 -0
  102. package/vendor/zod/locales/pt-BR.js +1 -0
  103. package/vendor/zod/locales/pt.js +1 -0
  104. package/vendor/zod/locales/ro.js +1 -0
  105. package/vendor/zod/locales/ru.js +1 -0
  106. package/vendor/zod/locales/sk.js +1 -0
  107. package/vendor/zod/locales/sl.js +1 -0
  108. package/vendor/zod/locales/sv.js +1 -0
  109. package/vendor/zod/locales/ta.js +1 -0
  110. package/vendor/zod/locales/tg.js +1 -0
  111. package/vendor/zod/locales/th.js +1 -0
  112. package/vendor/zod/locales/tk.js +1 -0
  113. package/vendor/zod/locales/tr.js +1 -0
  114. package/vendor/zod/locales/uk.js +1 -0
  115. package/vendor/zod/locales/ur.js +1 -0
  116. package/vendor/zod/locales/uz.js +1 -0
  117. package/vendor/zod/locales/vi.js +1 -0
  118. package/vendor/zod/locales/yo.js +1 -0
  119. package/vendor/zod/locales/zh-CN.js +1 -0
  120. package/vendor/zod/locales/zh-TW.js +1 -0
  121. package/vendor/zod/mini/checks.d.ts +1 -1
  122. package/vendor/zod/mini/checks.js +1 -1
  123. package/vendor/zod/mini/schemas.d.ts +2 -5
  124. package/vendor/zod/mini/schemas.js +9 -11
  125. package/vendor/zodexy/dist/esm/index.js +4 -33
  126. package/vendor/zodexy/dist/schema.zodexy.json +1 -17
@@ -0,0 +1,166 @@
1
+ # Schema-driven search widget subsystem
2
+
3
+ ## Context
4
+
5
+ jsoe today has two things: per-type modules under `src/fundamentalTypes/`, `src/subTypes/`, `src/superTypes/` that render `viewUI`/`editUI` controls for *editing a value* against a zodexy/Zod schema, and a `README.md` to-do item ("**Schema-driven search**", `README.md:162-185`) that already specs, type-by-type, a second capability that has never been built: iterating a *schema alone* (no value) to build a hierarchical search widget — e.g. a `Date` property gets two date pickers for a range query, an `Object` schema gets additive "has property X" controls, a `Tuple` schema gets distinct per-position controls unlike a plain `Array`. The goal of this work is to build that second capability as a new, parallel subsystem, reusing jsoe's existing schema-shape recognition and rendering conventions rather than reinventing them.
6
+
7
+ Decisions the user has already made (not open for re-litigation during implementation):
8
+ 1. **Structure**: a parallel module tree, not new methods bolted onto the existing `viewUI`/`editUI` type objects — because schema-only iteration needs different per-type behavior than value iteration (tuple/record/discriminatedUnion in particular need their own dedicated logic, where the value-editing side currently delegates them to `arrayType`/`objectType` via a runtime `specificSchemaObject.type` check). The new modules should still *call into* existing type files to reuse concrete UI-building pieces where that makes sense (e.g. a date-range widget reusing the date `<input>` construction from `dateType.js`).
9
+ 2. **Output**: each widget produces a structured, serializable **query object** (an AND/OR tree of typed leaf constraints), not an in-memory predicate function, so a host app can translate it into an IndexedDB query, an HTTP query string, etc. IndexedDB specifically should be more than "translatable with effort" — the query language's shape is deliberately chosen so its `range` leaves map directly onto `IDBKeyRange` with no translation logic beyond picking the right constructor (see §2's new "IndexedDB executability" note).
10
+ 3. **Scope**: full breadth in this pass — all ~25 type variants in the README to-do, not a small slice first.
11
+ 4. **Query vocabulary**: the tree borrows MongoDB's own operator names (`$and`/`$or`, `$gt`/`$gte`/`$lt`/`$lte`, `$in`/`$nin`, `$regex`/`$options`, `$exists`) wherever a leaf kind has a clean Mongo equivalent, so a host can adapt the common cases to `sift()` (or real MongoDB) almost for free. This is *not* a claim of full drop-in Mongo query-document compatibility — several jsoe-specific leaf kinds (`blobHTML`, `domShape`, `keyValueEnum`, `mapRecordJoint`, `passThrough`) have no Mongo equivalent and stay custom, and the tree keeps its own `kind`-discriminated, path-carrying leaf shape rather than Mongo's field-keyed document shape, since jsoe needs things Mongo's shape can't express (e.g. multiple OR'd alternative constraints on the same path, per the README's "OR range/Is Not Range" pattern).
12
+ 5. **DOM primitives**: search controls are implemented as custom elements, registered via `jml`'s own `$define` attribute (confirmed in `~/jamilih/src/jml.js` — it already wraps `customElements.define()` for exactly this; no separate registration mechanism needed). Every search element — leaf and AND/OR container alike — implements the same small **polymorphic instance-method interface** (chiefly `getQuery()`), so reading and composing query state happens through method calls on element references rather than through fragile `querySelector`/class-name string matching into another control's implementation details. No shadow root: not because state-reading needs to reach through it (the method interface makes that a non-issue either way) but because Cypress/E2E tests still need to simulate real user interaction — typing into an actual `<input>`, clicking an actual checkbox — and a shadow boundary would get in the way of that, independent of how application state is read back. See §8. Retrofitting the *existing* value-editing modules (`src/fundamentalTypes/*.js` etc.) to the same custom-element-plus-method-interface pattern is a deliberate future to-do — also §8 — not part of this pass.
13
+
14
+ Confirmed mid-design clarifications from the user:
15
+ - jsoe represents several runtime types via zodexy's "checked" mechanism — `{type: 'any', checks: [{name: 'blob'}]}` for `Blob`, and similarly for `regexp`, `error`, `domrect`, etc. — read by `getCheckedType` (`src/formats/schema.js:117-121`, confirmed at lines 117-166). The new dispatcher must go through the *same* recognizer, not reimplement it.
16
+ - `type: 'instanceof'` is **not** a general-purpose case to handle broadly. It occurs in exactly one place: the `File` element schema nested inside a `FileList`'s `codec` definition (confirmed at `src/formats/schema.js:963-969`, resolving `parentSchema.output.element` when `typesonType === 'file'`). No standalone/top-level `instanceof` schema exists elsewhere in this codebase's usage; a bare `instanceof` schema outside that FileList nesting should fall through to the non-editable/escape-hatch case, not to a generic "instanceof" search widget.
17
+
18
+ ## Architecture
19
+
20
+ ### 1. New file layout — `src/search/`
21
+
22
+ A fourth top-level tree, sibling to `fundamentalTypes/`, `subTypes/`, `superTypes/`, mirroring their names so a contributor can always find `src/search/fundamentalTypes/dateSearchType.js` next to `src/fundamentalTypes/dateType.js`:
23
+
24
+ ```
25
+ src/search/
26
+ index.js # exports buildSearchChoices (top-level entry point)
27
+ searchDispatch.js # getSearchSchemaType(), availableSearchTypes registry, getSearchTypeObject()
28
+ queryTree.js # JSDoc typedefs only: QueryAnd/QueryOr/QueryLeaf and all leaf shapes
29
+ queryTreeBuilders.js # runtime helpers: makeAndNode, makeOrNode, makeXLeaf(...) constructors
30
+ searchUtils.js # shared UI helpers: buildPathLabel, buildRangeInputsPair, buildMultiSelect, buildHasPropertyToggle
31
+ fundamentalTypes/ # one *SearchType.js per src/fundamentalTypes/*Type.js: date, number, bigint,
32
+ # string, regexp, boolean, symbol, undefined, null, nan, array, object, map,
33
+ # set, filelist, file, blob, error, domexception, promise, function, enum
34
+ subTypes/ # tupleSearchType.js, recordSearchType.js, blobHTMLSearchType.js
35
+ superTypes/ # domrectSearchType.js, dompointSearchType.js, dommatrixSearchType.js,
36
+ # errorsSpecialSearchType.js, specialNumberSearchType.js,
37
+ # specialRealNumberSearchType.js, buffersourceSearchType.js
38
+ unions/ # unionSearchType.js, xorSearchType.js, discriminatedUnionSearchType.js
39
+ noneditableSearchType.js # instanceof-outside-FileList + not-yet-supported types: no widget, escape hatch only
40
+ ```
41
+
42
+ Modules that get their **own** dedicated file instead of delegating (unlike the value-editing side, which delegates tuple/record to `arrayType.js` and has no dedicated union module at all):
43
+ - `tupleSearchType.js` — each position needs its own control from `.items[i]`/`.rest`, not one control repeated per element.
44
+ - `recordSearchType.js` — record search is "key-schema search AND/OR value-schema search," structurally unlike object's additive "has property."
45
+ - `objectSearchType.js` — "has property `<X>`" is an additive pulldown-driven affordance (README: "avoid listing required"), fundamentally different from rendering every property, which is what value-editing's `objectType.js` (thin wrapper over `arrayType.js`) does.
46
+ - `discriminatedUnionSearchType.js` (plus sibling `unionSearchType.js`/`xorSearchType.js`) — the discriminator field drives a typed pulldown (README explicitly calls out "discriminator of discriminated union" as its own case), which has no value-editing analogue to delegate to.
47
+ - `mapSearchType.js` — key-search × value-search composite, closer to a bespoke type than a delegate.
48
+ - `enumSearchType.js` — the value-editing side has *no* dedicated `enumType.js` (`getSchemaType` resolves `enum` to whatever its underlying value type is, e.g. `'string'`); search needs a genuinely different `multiSelect` control (a list of the enum's actual allowed values) rather than a generic literal/regex box, so the search dispatcher intercepts `enum` explicitly (§3) instead of falling through.
49
+
50
+ ### 2. Query object shape (`src/search/queryTree.js`)
51
+
52
+ JSDoc-only typedefs; leaf `path` values reuse the existing JSON-Pointer convention from `src/utils/jsonPointer.js` (`makeJSONPointer`, `getJSONPointerParts`) rather than inventing a new path format.
53
+
54
+ ```js
55
+ /**
56
+ * @typedef {{$and: QueryNode[]}} QueryAnd
57
+ * @typedef {{$or: QueryNode[]}} QueryOr
58
+ * @typedef {QueryAnd|QueryOr|QueryLeaf} QueryNode
59
+ */
60
+ ```
61
+
62
+ Leaf kinds, discriminated by `kind`, each carrying a JSON-Pointer `path` (jsoe's own path convention, not Mongo's dot-notation — trivial for a host to convert, and keeping it means the search tree uses the same path format as the rest of jsoe, e.g. `arrayType.js`'s `currentPath`):
63
+ - `hasProperty` (object, via `$exists: true/false`); `lengthSize` (array/set/tuple-with-rest/filelist, via `$size`, +`sparseCheck` for array sparse/not-sparse) — if the schema has a minimum or maximum on length, enforce that in input box and show no box if the length is exactly fixed.
64
+ - `range` (number/NumberObject/bigint/date/buffersource, with `valueType` distinguishing them; bounds are Mongo-style `$gt`/`$gte`/`$lt`/`$lte` rather than a `min`/`max` pair plus an inclusive boolean — inclusivity is simply which operator is present. The README's "Is Not Range" variant wraps the same leaf in `$not` rather than being a separate kind), `integerCheck`
65
+ - `literalSet` (via `$in`/`$nin`), `regex` (via `$regex`/`$options`, matching Mongo's own field names), `notContains` (string/StringObject/Blob/File/regexp-source/symbol-description)
66
+ - `multiSelect` (enum, SpecialNumber's Infinity/-Infinity/NaN/-0; via `$in`/`$nin`), `keyValueEnum` (native enum key-vs-value; no Mongo equivalent, stays custom)
67
+ - `typeOf` (union/xor/discriminatedUnion "has type", carrying `discriminatorValue` when applicable, including when nested under a Map/Record key or value; no Mongo equivalent, stays custom)
68
+ - `blobHTML` (XPath/CSS-selector/full-text/raw-HTML-regex; no Mongo equivalent), `domShape` (per-dimension ranges for DOMRect/Point/Matrix, each dimension itself a `range` leaf) + `readonlyCheck`/`dimensionCheck` (is/is-not readonly, is/is-not 3d; no Mongo equivalent)
69
+ - `mapRecordJoint` (paired key+value leaves with a joint-match flag; no Mongo equivalent)
70
+ - `passThrough` (promise/literal/catch/function: forwards to a nested `QueryNode` for the child schema so the tree stays uniform even where a type adds no constraint of its own; purely structural, no Mongo equivalent)
71
+ - `presence` (undefined/void/null, via `$exists`), `booleanEquals` (boolean/BooleanObject — Mongo would normally express this as a bare `{field: true}` shorthand, which doesn't fit our path-carrying leaf shape, so this stays a custom kind)
72
+
73
+ `src/search/queryTreeBuilders.js` exposes the only runtime pieces — `makeAndNode`, `makeOrNode`, and one small factory per leaf kind — so every search module builds nodes through one place and tests can import the same constructors to build expected-value fixtures.
74
+
75
+ **IndexedDB executability.** IndexedDB has no native compound query document — its only real primitive is `IDBKeyRange` (`.only`/`.lowerBound`/`.upperBound`/`.bound`) evaluated against a single index via a cursor or `getAll(range)`, plus whatever a caller filters in JS as it iterates. The leaf shapes above are chosen so a host doesn't have to reinvent this mapping:
76
+ - A single-path `range` leaf translates directly: `$gte`+`$lte` present → `IDBKeyRange.bound(gte, lte, false, false)`; swap in `$gt`/`$lt` for the open-boundary form (`bound`'s 3rd/4th args); only a lower or only an upper bound present → `.lowerBound`/`.upperBound`; a bare equality (`$gte === $lte`, or a future `$eq`) → `.only`. No other leaf kind in this tree needs its own IndexedDB mapping rule beyond this one.
77
+ - A multi-path `$and` can become a *compound* `IDBKeyRange` only if the host already has a compound index (`createIndex(name, [pathA, pathB, ...])`) matching those exact paths in that exact order — jsoe never creates indexes itself, so this is opportunistic, not guaranteed. The normal, expected execution model is two-tier: pick whichever single leaf (or compound-indexed group of leaves) is most selective and IDB-native, open a cursor/`getAll` over just that `IDBKeyRange`, then evaluate the *rest* of the `$and` as an ordinary in-memory predicate per row — which is exactly the kind of walk `sift()` (decision 4, above) already does over this same tree, so the in-memory fallback and the Mongo-flavored vocabulary reinforce each other rather than needing separate code paths.
78
+ - `$or` has no native IndexedDB equivalent either; the standard pattern (multiple cursor queries, one per branch, merged and deduplicated by primary key) applies unchanged and needs nothing special from this tree's shape.
79
+ - Leaf kinds that can *never* be pushed to a native `IDBKeyRange` — `regex`, `blobHTML`, `keyValueEnum`, `mapRecordJoint`, `typeOf`, `passThrough`, and `domShape` as a whole (though each of its per-dimension `range` children individually is IDB-range-shaped) — always require the in-memory fallback pass. This should be documented plainly wherever `buildSearchChoices`/the query shape is documented for consumers, so it's an expected limitation rather than a surprise hit mid-integration. Also worth flagging for whoever implements the IndexedDB adapter: `IDBKeyRange` only accepts IndexedDB's own valid key types (number, string, `Date`, binary, or an `Array` of valid keys) — a `range` leaf whose `valueType` is `bigint` has no valid IndexedDB key representation at all and falls back to in-memory filtering same as the never-pushable kinds above.
80
+
81
+ ### 3. Schema-only dispatcher (`src/search/searchDispatch.js`)
82
+
83
+ Must not reimplement `getSchemaType`'s special-casing (stringbool pipe, `codec`/filelist, `instanceof`, `literal`, `enum`, `templateLiteral`, `getCheckedType` fallback — all at `src/formats/schema.js:117-166`) — it imports and calls `getSchemaType` directly, then adds only the extra shape distinctions that function's own `AvailableArbitraryType` output collapses (tuple, record/looseRecord, union/xor/discriminatedUnion all currently reduce to `'array'`/`'object'`/nothing, per `zodexToStructuredCloningTypeMap`, `schema.js:30-56`):
84
+
85
+ ```js
86
+ export function getSearchSchemaType (schemaObject) {
87
+ if (schemaObject.type === 'tuple') return 'tuple';
88
+ if (schemaObject.type === 'record' || schemaObject.type === 'looseRecord') {
89
+ return schemaObject.type;
90
+ }
91
+ if (['union', 'xor', 'discriminatedUnion'].includes(schemaObject.type)) {
92
+ return schemaObject.type;
93
+ }
94
+
95
+ // An `enum` control can show a multiple-select list and template literal parts might justify their own search controls, so need to detect these schema types.
96
+ if (['templateLiteral', 'enum'].includes(schemaObject.type)) {
97
+ return schemaObject.type;
98
+ }
99
+
100
+ return getSchemaType(schemaObject); // inherits stringbool/codec/instanceof/checks handling verbatim
101
+ }
102
+ ```
103
+
104
+ The `enum`/`templateLiteral` branch resolves as follows: `enum` routes to the new `enumSearchType.js` (its own `multiSelect` control, listing the schema's actual allowed values — falling through to `getSchemaType` here would collapse it into whatever its underlying value type is, landing it in a generic string/number search box instead). `templateLiteral` routes to `stringSearchType.js` for this pass — a template literal is still fundamentally a string-shape constraint, so treating it as one `regex`-style leaf is honestly sufficient for now; giving it dedicated per-part controls (one control per literal/interpolation segment) is a plausible future enhancement but not decided or scoped here, so `availableSearchTypes.templateLiteral` simply points at the same module as `string`.
105
+
106
+ Intersection schemas (`schemaObject.type === 'intersection'`) are **not** a case inside `getSearchSchemaType` at all — they're resolved *before* dispatch. jsoe already has real machinery for this on the value-editing side: `getTypesForSchema`'s `case 'intersection'` (`schema.js:643-652`) recursively flattens both `schemaObject.left` and `schemaObject.right`, then `flattenIntersection`/`mergeSchema` (`schema.js:401-524`) cross-merge same-type branches into concrete synthesized schemas (tighter of the two `min`/`max`/`minLength`/`maxLength` bounds, merged `properties`/`meta`/`description`, throwing on mismatched types or conflicting properties). The search-widget walker should call `getTypesForSchema` for exactly this one node type, hand each resulting merged schema to `getSearchSchemaType`/`getSearchTypeObject` as normal, and never register `'intersection'` itself in `availableSearchTypes`. (`getTypesForSchema` takes an `originalJSON` second argument used elsewhere for `$ref` resolution; the search side will need to supply *something* there — likely the root schema, mirroring existing callers — when this is actually implemented.) Unlike unions, intersection branches never need to stay distinguishable: an intersection is a single AND-of-constraints on one value, so collapsing it to its merged leaf(s) is exactly correct rather than a loss of information. The one edge case worth a passing note: if either side of the intersection is itself a union, flattening can yield more than one merged result — treat that set the same way `unionSearchType.js` treats branches, rather than assuming exactly one.
107
+
108
+ No dispatcher-level `instanceof` branch is added. Because the walker only ever reaches `getSchemaType`'s `instanceof` case when recursing into a FileList's `.output.element` (the one real occurrence in this codebase, per `schema.js:963-969`), that recursion naturally resolves to `'file'` and routes to `fileSearchType.js` — enforcing the "FileList-only" invariant by construction. A bare top-level `instanceof` schema (which doesn't occur via any current codec) falls through to `noneditableSearchType.js`'s escape hatch, same as any other unrecognized/not-yet-supported shape.
109
+
110
+ `availableSearchTypes` is a `Record<string, SearchTypeObject>` registry (analogous to `Types.prototype.availableTypes`, `src/types.js:473-581`) mapping every key `getSearchSchemaType` can return to its module; `getSearchTypeObject(schemaObject)` is the one exported call site every recursive widget uses (analogous to `types.getTypeObject(type)`, `src/types.js:1106-1108`, but schema-in rather than type-string-in, since there's no value to key off).
111
+
112
+ `SearchTypeObject` (JSDoc typedef alongside `queryTree.js`) is the search-side analogue of `TypeObject` (`src/types.js:308-408`): two methods, `buildUI({schemaObject, path, typeNamespace, topRoot, types})` returning a `jml` array, and `getQuery({root, path})` returning a `QueryNode|undefined` (`undefined` = "no constraint entered here").
113
+
114
+ ### 4. Reuse of existing value-editing modules
115
+
116
+ - **Date** (`src/fundamentalTypes/dateType.js`): extract the `<input type="datetime-local">` construction (currently inlined in `editUI`, lines 174-193, including the `min`/`max` wiring from `dateSchemaObject?.min`/`.max` and the ISO-slice formatting) into a new named export `buildDateInputControl`, added alongside the existing default export. `dateSearchType.js` calls it twice (range start/end). This is the **only** existing file this feature needs to modify, and it's purely additive — the default export and all existing behavior/tests are untouched.
117
+ - **Number/string/regexp** (`numberType.js`, `stringType.js`, `regexpType.js`): no extraction — these types' reusable unit is a single native `<input>`, too trivial to be worth a function boundary, and the value-editing versions carry string round-tripping logic (`stringRegex`/`toValue`) the search UI doesn't need. Search modules build their own inputs directly via `jml`. Exception: `regexpType.js`'s `allowedFlags` list (a plain property, already exported on the object) is imported as-is by `regexpSearchType.js` for its flags multi-select — no extraction needed, it's already accessible.
118
+ - **`getChildSchema`** (closure inside `arrayType.js`'s `editUI`, ~lines 1614-1690): not extracted (entangled with `arrayType.js`'s DOM-diffing state). `tupleSearchType.js`/`recordSearchType.js` write their own 1-3 line equivalents (`schemaObject.items[i] ?? schemaObject.rest`; `schemaObject.value`/`.key`) — below the threshold where duplication is a real risk.
119
+ - **`schemaLabel`** (`src/utils/schemaMeta.js`) and **`isUnionLike`** (`src/utils/types.js:9-25`): imported as-is by every relevant search module, same as the value-editing side already does.
120
+ - **`getXorBranchMatchInfo`** (`src/formats/schema.js:262-279`) is explicitly *not* reused by `xorSearchType.js` — it requires a concrete value to test branch match, which the search side never has.
121
+
122
+ ### 5. Public API entry point
123
+
124
+ New `src/search/index.js` exports `buildSearchChoices({schemaContent, typeNamespace, topRoot, types})`, re-exported from `src/index.js` alongside the existing `Types`/`Formats`/`typeChoices`/`formatAndTypeChoices`/`getTypesForSchema` exports (`src/index.js:13-27`). Mirrors `buildTypeChoices`'s (`src/typeChoices.js:395`) `whenReady`/pull-based convention rather than inventing a push/callback API: returns `{container, $getQuery, whenReady}`, where `$getQuery()` reads the live DOM into a `QueryAnd` on demand (same shape as `typeChoices.js`'s `$getValue`), and a host that wants live updates wraps it in its own `container.addEventListener('input', ...)` since `container` is a plain `HTMLDivElement`.
125
+
126
+ `buildSearchChoices` recurses via `getSearchTypeObject(...).buildUI(...)`, starting at `path = '#/'` — it does **not** route through `getTypesForSchema` (`src/formats/schema.js:581-864`), because that function flattens union members into one flat Set of leaf types for a type-choice dropdown, whereas search needs union branches to stay distinguishable nested sub-widgets for the "has type" affordance. Intersections could be mergeable if of the same type, however (though their constraints should still apply to the search control--e.g., a minlength on a string) — see §3 for the concrete resolution, which reuses jsoe's existing intersection-merging machinery rather than inventing a new one.
127
+
128
+ ### 6. Build order
129
+
130
+ 1. Query-tree contract + `queryTreeBuilders.js` + `searchDispatch.js` skeleton, with every registry entry pointing at a temporary stub (`{buildUI: () => ['span', ['TODO']], getQuery: () => undefined}`) so the full shape typechecks end-to-end immediately.
131
+ 2. `searchUtils.js` shared UI helpers + the additive `buildDateInputControl` export on `dateType.js`.
132
+ 3. Primitive leaves: date, number, bigint, string, regexp, boolean, enum, symbol, undefined, null, nan.
133
+ 4. Object / Array / Set (first types recursing into child schemas via `getSearchTypeObject`).
134
+ 5. Tuple / Record (isolated in their own step since per-position/per-key-vs-value logic is qualitatively different from step 4's recursion).
135
+ 6. Map / FileList / File / Blob — this is where the FileList→instanceof→File routing gets exercised for the first time; add the regression test described below here.
136
+ 7. Union family (union/xor/discriminatedUnion) — lands after composites so a union-of-composites round-trips through already-working recursion.
137
+ 8. Remaining independent leaves/composites: Error family, DOMException, DOMRect/Point/Matrix, BlobHTML, promise/function/literal/catch pass-through, SpecialNumber/SpecialRealNumber, buffersource, noneditable. `buffersource` and `function` are flagged as fuzzy in the README itself ("OR Range/Is Not Range" over raw bytes; args/return-type pass-through) — ship a minimal, honest stub for these two (byte-length range only; pass-through only if there's a searchable child) rather than over-building past what the spec actually defines.
138
+ 9. `buildSearchChoices` + `src/index.js` export (composes everything above).
139
+ 10. Demo page + Cypress suite.
140
+
141
+ Resolved watch item: `tsconfig.json` used to exclude `src/formats/schema.js` from type-checking, presumably because zodexy's generic unions were once thought to defeat strict narrowing there. That exclusion (and a stale, unused `./src/index.ts` entry) has since been removed — all three tsc scripts (`tsc`, `tsc:ts7`, `tsc-cypress`) pass cleanly against the whole repo without it. `src/search/searchDispatch.js`'s narrowing over the same `ZodexSchema` union therefore needs no special dispensation; if it somehow does hit a wall `schema.js` didn't, that would be a new, surprising finding worth its own investigation rather than an expected outcome.
142
+
143
+ ### 7. Test plan
144
+
145
+ Follows the existing Cypress-e2e-against-a-demo-page convention (this repo has no unit-test runner; per prior verified project knowledge, a change also isn't considered typechecked unless `tsc`, `tsc:ts7`, and `tsc-cypress` all pass, not just the default `tsc`):
146
+ - New `demo/index-search.html`/`-instrumented.html` + `demo/index-search.js`, structured like `demo/index-schema.html`/`.js`, reusing existing fixtures from `demo/schema-data.js` where they already cover a shape (e.g. its date schema with real `min`/`max` for the range-widget test), and additively exporting a few new fixtures it lacks (tuple-with-rest, record/looseRecord, a discriminatedUnion with a date branch).
147
+ - New `cypress/e2e/search/` directory mirroring the existing `fundamentalTypes/`/`subTypes/`/`superTypes/` structure, each with an `all.cy.js` aggregator per that convention.
148
+ - A representative spec (`cypress/e2e/search/fundamentalTypes/date.cy.js`): visit the search demo page, type into both range inputs, trigger the demo's "get query" button, assert the logged/parsed JSON matches the expected `QueryAnd` shape (start/end values, `valueType: 'date'`), and separately assert the two `datetime-local` inputs' `min`/`max` HTML attributes match the schema's constraints — proving `dateSearchType.js` is actually calling `buildDateInputControl` rather than a drifted re-implementation.
149
+ - A dedicated regression spec for the FileList/instanceof invariant: build a FileList search widget and assert its element control is `fileSearchType`'s UI (not the generic `noneditableSearchType` stub), and separately confirm a bare `instanceof` schema outside a FileList (if constructible via the demo fixtures) renders `noneditableSearchType`'s escape hatch instead — this is the one test directly protecting the FileList-only `instanceof` invariant from regressing.
150
+
151
+ ### 8. DOM primitives: custom elements with a polymorphic method interface, no shadow DOM
152
+
153
+ Search containers and leaves are built as custom elements rather than generic `div`/`fieldset` soup, for semantic clarity (e.g. `<jsoe-search-and>`, `<jsoe-search-or>`, `<jsoe-search-date>`, `<jsoe-search-object>` instead of a stack of same-tag `div`s distinguished only by class). This needs no new tooling: `jml`'s own `$define` attribute (confirmed in `~/jamilih/src/jml.js`) already wraps `window.customElements.define()`, supporting both autonomous hyphenated tag names and "customized built-ins" via `is=`.
154
+
155
+ This also replaces the `SearchTypeObject` contract from §3 with something more robust than a free function threaded through `{root, path}`: every search element class (leaf and container alike) implements the same small **polymorphic method interface** — chiefly `getQuery(): QueryNode|undefined` — so reading state, composing recursively, and any future host integration go through method calls on element references rather than `querySelector`/class-name matching into another control's private markup (the same brittleness class as the `.jsoe-raw-editor .cm-content` selector chain the raw-editor dialog already leans on, which is exactly what we want to avoid repeating here). `<jsoe-search-and>`/`<jsoe-search-or>` implement `getQuery()` by walking `this.children` and calling `.getQuery()` on each polymorphically — they don't need to know or care what concrete leaf/container type each child is, only that it answers to `.getQuery()` — combining the results per `$and`/`$or` without a separate schema re-dispatch pass at read time. `buildUI` (§3) becomes whatever constructs/populates the element (its constructor or a render method), still calling into existing value-editing helpers like `dateType.js`'s extracted `buildDateInputControl` (§4) internally to build its actual form controls.
156
+
157
+ Given the method interface handles state-reading, the *only* remaining reason to skip `attachShadow()` is that Cypress/E2E tests need to simulate real user interaction against the actual rendered form controls (type into a real `<input>`, click a real checkbox) — a shadow boundary would get in the way of that regardless of how well-designed the JS-facing API is. jsoe's existing `$e`/`$$e` helpers (`src/utils/templateUtils.js`) and the whole Cypress suite's plain-`querySelector` conventions incidentally also keep working unmodified this way, but that's a side effect of the testability call, not the primary reason for it.
158
+
159
+ **Future to-do, explicitly out of scope for this pass**: retrofit the *existing* value-editing modules (`src/fundamentalTypes/*.js`, `src/subTypes/*.js`, `src/superTypes/*.js`) to render via the same custom-element pattern instead of today's generic `div`s. This is a much larger, separate-blast-radius change — it touches every existing type file and every existing Cypress spec's plain-class/tag selectors across the whole suite — with no dependency relationship to the search subsystem, so it's noted here as a marker for a future, standalone initiative rather than scheduled in the build order above.
160
+
161
+ ## Verification
162
+
163
+ - `npm run tsc && npm run tsc:ts7 && npm run tsc-cypress` clean after every step (per existing project convention: all three scripts, not just the default, gate a change).
164
+ - `npm run eslint` clean (`eslint-config-ash-nazg(['sauron','browser'])`).
165
+ - `npm run cypress` (or `npm test`, which runs `eslint && rollup && cypress`) green, including the new `cypress/e2e/search/**` specs.
166
+ - Manually open `demo/index-search.html` in a browser and exercise a Date range, an Object "has property," and a Tuple's per-position controls to confirm the rendered UI and the printed query JSON match expectations before considering any given step done.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@es-joy/jsoe",
3
- "version": "0.28.0",
3
+ "version": "0.28.2",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "main": "./src/index.js",
@@ -28,25 +28,38 @@
28
28
  ]
29
29
  },
30
30
  "dependencies": {
31
+ "@codemirror/autocomplete": "^6.20.3",
32
+ "@codemirror/commands": "^6.11.0",
31
33
  "@codemirror/lang-javascript": "^6.2.5",
34
+ "@codemirror/language": "^6.12.4",
35
+ "@codemirror/lint": "^6.9.7",
36
+ "@codemirror/search": "^6.7.2",
32
37
  "@codemirror/state": "^6.7.4",
38
+ "@codemirror/view": "^6.43.11",
39
+ "@lezer/common": "^1.5.2",
40
+ "@lezer/highlight": "^1.2.3",
41
+ "@lezer/javascript": "^1.5.4",
42
+ "@lezer/lr": "^1.4.10",
33
43
  "@marijn/find-cluster-break": "^1.0.4",
34
44
  "@node-static/node-static": "^0.9.2",
35
45
  "acorn": "^8.18.0",
36
46
  "codemirror": "^6.0.2",
37
- "jamilih": "0.69.1",
47
+ "crelt": "^1.0.7",
48
+ "jamilih": "0.70.0",
38
49
  "json-6": "^1.1.4",
39
50
  "mime": "^4.1.0",
40
51
  "sceditor": "3.2.1",
52
+ "style-mod": "^4.1.3",
41
53
  "typeson-registry": "14.6.1",
42
- "zod": "^4.6.1",
43
- "zodexy": "^0.31.0"
54
+ "w3c-keyname": "^2.2.8",
55
+ "zod": "^4.6.4",
56
+ "zodexy": "^0.32.0"
44
57
  },
45
58
  "devDependencies": {
46
- "@babel/core": "^8.0.1",
59
+ "@babel/core": "^8.0.5",
47
60
  "@babel/plugin-transform-modules-commonjs": "^8.0.1",
48
- "@babel/preset-env": "^8.0.2",
49
- "@babel/register": "^8.0.1",
61
+ "@babel/preset-env": "^8.0.5",
62
+ "@babel/register": "^8.0.5",
50
63
  "@cypress/code-coverage": "^4.0.3",
51
64
  "@rollup/plugin-babel": "^7.1.0",
52
65
  "@rollup/plugin-commonjs": "^29.0.3",
@@ -61,7 +74,7 @@
61
74
  "cypress-axe": "^1.7.0",
62
75
  "cypress-terminal-report": "^7.3.3",
63
76
  "eslint": "^10.10.0",
64
- "eslint-config-ash-nazg": "^43.1.8",
77
+ "eslint-config-ash-nazg": "^43.1.10",
65
78
  "fast-deep-equal": "^3.1.3",
66
79
  "jsdom": "^30.0.1",
67
80
  "license-badger": "^0.23.0",
@@ -71,7 +84,7 @@
71
84
  "nyc": "^18.0.0",
72
85
  "open-cli": "^9.0.0",
73
86
  "rimraf": "^6.1.3",
74
- "rollup": "4.63.1",
87
+ "rollup": "4.63.2",
75
88
  "rollup-plugin-istanbul": "^5.0.0",
76
89
  "rollup-plugin-node-builtins": "^2.1.2",
77
90
  "typescript": "^6.0.3",
@@ -10,40 +10,40 @@ minimumReleaseAgeExclude:
10
10
  - cypress@15.20.1 || 15.21.0 || 16.0.0
11
11
  - '@es-joy/jsdoccomment@0.95.0 || 0.95.1'
12
12
  - '@typescript-eslint/types@8.67.0 || 8.69.0'
13
- - eslint-config-ash-nazg@42.3.0 || 42.5.0 || 43.0.1 || 43.1.0 || 43.1.2 || 43.1.5 || 43.1.6
14
- - eslint-plugin-jsdoc@64.1.0 || 64.2.1 || 64.3.0 || 64.3.2 || 64.3.3
13
+ - eslint-config-ash-nazg@42.3.0 || 42.5.0 || 43.0.1 || 43.1.0 || 43.1.2 || 43.1.5 || 43.1.6 || 43.1.10
14
+ - eslint-plugin-jsdoc@64.1.0 || 64.2.1 || 64.3.0 || 64.3.2 || 64.3.3 || 64.3.10
15
15
  - jsdoc-type-pratt-parser@9.1.1 || 9.1.2
16
- - zodexy@0.21.1 || 0.21.2 || 0.22.0 || 0.23.0 || 0.24.0 || 0.24.1 || 0.25.0 || 0.26.0 || 0.26.1 || 0.27.0 || 0.30.0 || 0.30.1 || 0.31.0
16
+ - zodexy@0.21.1 || 0.21.2 || 0.22.0 || 0.23.0 || 0.24.0 || 0.24.1 || 0.25.0 || 0.26.0 || 0.26.1 || 0.27.0 || 0.30.0 || 0.30.1 || 0.31.0 || 0.31.1 || 0.32.0
17
17
  - typeson-registry@13.0.1 || 13.1.0 || 14.4.0 || 14.5.0
18
18
  - typeson@10.1.1 || 10.2.0 || 10.3.0
19
- - '@rollup/rollup-android-arm-eabi@4.63.1'
20
- - '@rollup/rollup-android-arm64@4.63.1'
21
- - '@rollup/rollup-darwin-arm64@4.63.1'
22
- - '@rollup/rollup-darwin-x64@4.63.1'
23
- - '@rollup/rollup-freebsd-arm64@4.63.1'
24
- - '@rollup/rollup-freebsd-x64@4.63.1'
25
- - '@rollup/rollup-linux-arm-gnueabihf@4.63.1'
26
- - '@rollup/rollup-linux-arm-musleabihf@4.63.1'
27
- - '@rollup/rollup-linux-arm64-gnu@4.63.1'
28
- - '@rollup/rollup-linux-arm64-musl@4.63.1'
29
- - '@rollup/rollup-linux-loong64-gnu@4.63.1'
30
- - '@rollup/rollup-linux-loong64-musl@4.63.1'
31
- - '@rollup/rollup-linux-ppc64-gnu@4.63.1'
32
- - '@rollup/rollup-linux-ppc64-musl@4.63.1'
33
- - '@rollup/rollup-linux-riscv64-gnu@4.63.1'
34
- - '@rollup/rollup-linux-riscv64-musl@4.63.1'
35
- - '@rollup/rollup-linux-s390x-gnu@4.63.1'
36
- - '@rollup/rollup-linux-x64-gnu@4.63.1'
37
- - '@rollup/rollup-linux-x64-musl@4.63.1'
38
- - '@rollup/rollup-openbsd-x64@4.63.1'
39
- - '@rollup/rollup-openharmony-arm64@4.63.1'
40
- - '@rollup/rollup-win32-arm64-msvc@4.63.1'
41
- - '@rollup/rollup-win32-ia32-msvc@4.63.1'
42
- - '@rollup/rollup-win32-x64-gnu@4.63.1'
43
- - '@rollup/rollup-win32-x64-msvc@4.63.1'
19
+ - '@rollup/rollup-android-arm-eabi@4.63.1 || 4.63.2'
20
+ - '@rollup/rollup-android-arm64@4.63.1 || 4.63.2'
21
+ - '@rollup/rollup-darwin-arm64@4.63.1 || 4.63.2'
22
+ - '@rollup/rollup-darwin-x64@4.63.1 || 4.63.2'
23
+ - '@rollup/rollup-freebsd-arm64@4.63.1 || 4.63.2'
24
+ - '@rollup/rollup-freebsd-x64@4.63.1 || 4.63.2'
25
+ - '@rollup/rollup-linux-arm-gnueabihf@4.63.1 || 4.63.2'
26
+ - '@rollup/rollup-linux-arm-musleabihf@4.63.1 || 4.63.2'
27
+ - '@rollup/rollup-linux-arm64-gnu@4.63.1 || 4.63.2'
28
+ - '@rollup/rollup-linux-arm64-musl@4.63.1 || 4.63.2'
29
+ - '@rollup/rollup-linux-loong64-gnu@4.63.1 || 4.63.2'
30
+ - '@rollup/rollup-linux-loong64-musl@4.63.1 || 4.63.2'
31
+ - '@rollup/rollup-linux-ppc64-gnu@4.63.1 || 4.63.2'
32
+ - '@rollup/rollup-linux-ppc64-musl@4.63.1 || 4.63.2'
33
+ - '@rollup/rollup-linux-riscv64-gnu@4.63.1 || 4.63.2'
34
+ - '@rollup/rollup-linux-riscv64-musl@4.63.1 || 4.63.2'
35
+ - '@rollup/rollup-linux-s390x-gnu@4.63.1 || 4.63.2'
36
+ - '@rollup/rollup-linux-x64-gnu@4.63.1 || 4.63.2'
37
+ - '@rollup/rollup-linux-x64-musl@4.63.1 || 4.63.2'
38
+ - '@rollup/rollup-openbsd-x64@4.63.1 || 4.63.2'
39
+ - '@rollup/rollup-openharmony-arm64@4.63.1 || 4.63.2'
40
+ - '@rollup/rollup-win32-arm64-msvc@4.63.1 || 4.63.2'
41
+ - '@rollup/rollup-win32-ia32-msvc@4.63.1 || 4.63.2'
42
+ - '@rollup/rollup-win32-x64-gnu@4.63.1 || 4.63.2'
43
+ - '@rollup/rollup-win32-x64-msvc@4.63.1 || 4.63.2'
44
44
  - eslint-plugin-escompat@3.11.6 || 3.12.0
45
- - rollup@4.63.1
46
- - zod@4.5.2 || 4.5.4 || 4.6.1
45
+ - rollup@4.63.1 || 4.63.2
46
+ - zod@4.5.2 || 4.5.4 || 4.6.1 || 4.6.3 || 4.6.4
47
47
  - '@typescript-eslint/eslint-plugin@8.69.0'
48
48
  - '@typescript-eslint/parser@8.69.0'
49
49
  - '@typescript-eslint/project-service@8.69.0'
@@ -57,3 +57,4 @@ minimumReleaseAgeExclude:
57
57
  - '@types/node@26.4.1'
58
58
  - '@node-static/node-static@0.9.2'
59
59
  - mocha-multi-reporters@1.5.2
60
+ - '@eslint-community/eslint-plugin-eslint-comments@4.8.1'
@@ -35,6 +35,7 @@ const zodexToStructuredCloningTypeMap = new Map([
35
35
  ['string', 'string'],
36
36
  ['date', 'date'],
37
37
  ['file', 'file'],
38
+ ['symbol', 'symbol'],
38
39
  ['undefined', 'undef'],
39
40
  ['void', 'undef'],
40
41
  ['null', 'null'],
@@ -43,10 +44,6 @@ const zodexToStructuredCloningTypeMap = new Map([
43
44
  ['array', 'arrayNonindexKeys'],
44
45
 
45
46
  ['object', 'object'],
46
- // `z.properties()` (Zod 4.5+) is a bare named-property shape: like `object`
47
- // but with no `catchall`/`symbols` and, in Zod, no unknown-key stripping.
48
- // jsoe edits it through the same `object` UI, keyed off the schema `type`.
49
- ['properties', 'object'],
50
47
 
51
48
  ['tuple', 'array'],
52
49
  ['record', 'object'],
@@ -388,13 +385,12 @@ function mergeMeta (target, source) {
388
385
 
389
386
  /**
390
387
  * Whether a schema node carries a fixed bag of named property schemas under
391
- * `properties` - `object` and `z.properties()`. Both are merged the same way
392
- * across an intersection and both drive the `object` editor UI.
388
+ * `properties` - i.e., `object`.
393
389
  * @param {ZodexSchema['type']|undefined} type
394
390
  * @returns {boolean}
395
391
  */
396
392
  function isPropertyBagType (type) {
397
- return type === 'object' || type === 'properties';
393
+ return type === 'object';
398
394
  }
399
395
 
400
396
  /**
@@ -613,7 +609,6 @@ export function getTypesForSchema (schemaObject, originalJSON) {
613
609
  originalJSON
614
610
  );
615
611
  }
616
- case 'properties':
617
612
  case 'object': {
618
613
  const set = new Set();
619
614
  // const {properties} = schemaObject;
@@ -936,13 +931,12 @@ const schema = {
936
931
 
937
932
  switch (parentSchema?.type) {
938
933
  case 'object':
939
- case 'properties':
940
934
  // A key outside the declared shape: an `object` pins it to its
941
- // `catchall` if it has one; otherwise (and always for `properties`,
942
- // which has no `catchall`) its control is offered the unconstrained
943
- // type choice - as a `looseRecord`'s non-conforming entry is - rather
944
- // than the bare "unschema'd" fallback. A strict `object` will still
945
- // strip the key on parse; this only governs how it is edited.
935
+ // `catchall` if it has one; otherwise its control is offered the
936
+ // unconstrained type choice - as a `looseRecord`'s non-conforming
937
+ // entry is - rather than the bare "unschema'd" fallback. A strict
938
+ // `object` will still strip the key on parse; this only governs how
939
+ // it is edited.
946
940
  currentSchema = /** @type {import('zodexy').SzObject} */ (
947
941
  parentSchema
948
942
  ).properties[
@@ -267,7 +267,7 @@ const arrayType = {
267
267
  return {value: retObj, remnant: stringVal};
268
268
  },
269
269
  getValue ({root, stateObj, currentPath = ''}) {
270
- /* istanbul ignore if */
270
+ /* istanbul ignore if -- TS guard */
271
271
  if (!stateObj) {
272
272
  throw new Error('TS guard'); // TS guard
273
273
  }
@@ -1420,7 +1420,7 @@ const arrayType = {
1420
1420
  const method = latest ? 'after' : 'before';
1421
1421
  // Ensure move *after* splice that will occur after this
1422
1422
  setTimeout(() => {
1423
- /* istanbul ignore if */
1423
+ /* istanbul ignore if -- Should be present */
1424
1424
  if (!nearest) {
1425
1425
  return;
1426
1426
  }
@@ -1671,21 +1671,19 @@ const arrayType = {
1671
1671
  const objectSchema = /** @type {import('zodexy').SzObject} */ (
1672
1672
  specificSchemaObject
1673
1673
  );
1674
- const schemaType = specificSchemaObject?.type;
1675
1674
  const propSchema = objectSchema?.properties?.[/** @type {string} */ (
1676
1675
  propName
1677
1676
  )];
1678
1677
  if (
1679
1678
  !propSchema &&
1680
- (schemaType === 'properties' ||
1681
- (schemaType === 'object' && !objectSchema.catchall))
1679
+ specificSchemaObject?.type === 'object' && !objectSchema.catchall
1682
1680
  ) {
1683
- // A key outside the declared shape of a `z.properties()` (which keeps
1684
- // it) or a `catchall`-less `z.object()` (which strips it on parse but
1685
- // still shows it while editing): offer the unconstrained type choice
1686
- // rather than an unschema'd control, as a `looseRecord`'s
1687
- // non-conforming entry gets. An `object` `catchall` takes precedence
1688
- // (handled where the per-property schema is chosen elsewhere).
1681
+ // A key outside the declared shape of a `catchall`-less `z.object()`
1682
+ // (which strips it on parse but still shows it while editing):
1683
+ // offer the unconstrained type choice rather than an unschema'd
1684
+ // control, as a `looseRecord`'s non-conforming entry gets. An
1685
+ // `object` `catchall` takes precedence (handled where the
1686
+ // per-property schema is chosen elsewhere).
1689
1687
  return /** @type {import('zodexy').SzType} */ ({type: 'unknown'});
1690
1688
  }
1691
1689
  return propSchema;
@@ -2733,16 +2731,10 @@ const arrayType = {
2733
2731
  }
2734
2732
  }, /** @type {import('jamilih').JamilihChildren} */ ([
2735
2733
  [specificSchemaObject ? 'span' : 'b', {
2736
- title: schemaLabel(specificSchemaObject) ?? (
2737
- // `z.properties()` renders through the `object` UI; without a
2738
- // `meta` label of its own, name it "Properties" rather than an
2739
- // indistinguishable "Object".
2740
- specificSchemaObject?.type === 'properties'
2741
- ? 'Properties'
2742
- : DOM.initialCaps(
2743
- /** @type {import('../types.js').AvailableType} */
2744
- (itemType)
2745
- ).replace(/s$/u, ''))
2734
+ title: schemaLabel(specificSchemaObject) ?? DOM.initialCaps(
2735
+ /** @type {import('../types.js').AvailableType} */
2736
+ (itemType)
2737
+ ).replace(/s$/u, '')
2746
2738
  }, [
2747
2739
  specificSchemaObject
2748
2740
  ? '—'
@@ -2762,7 +2754,7 @@ const arrayType = {
2762
2754
  * @this {HTMLInputElement}
2763
2755
  */
2764
2756
  change () {
2765
- /* istanbul ignore if */
2757
+ /* istanbul ignore if -- TS */
2766
2758
  if (!this.files) {
2767
2759
  return;
2768
2760
  }
@@ -117,7 +117,7 @@ function binaryButton (value, editable) {
117
117
  ['textarea', {
118
118
  class: 'view-binary'
119
119
  }, [
120
- /* istanbul ignore next */
120
+ /* istanbul ignore next -- Guard */
121
121
  /** @type {string|null} */ (reader.result) ?? ''
122
122
  ]]
123
123
  ]]
@@ -127,11 +127,11 @@ function binaryButton (value, editable) {
127
127
  // Seems not feasible to accurately simulate
128
128
  reader.addEventListener(
129
129
  'error',
130
- /* istanbul ignore next */
130
+ /* istanbul ignore next -- See comment above */
131
131
  async function () {
132
- /* istanbul ignore next */
132
+ /* istanbul ignore nex -- See comment abovet */
133
133
  console.error(reader.error);
134
- /* istanbul ignore next */
134
+ /* istanbul ignore next -- See comment above */
135
135
  await dialogs.alert(/** @type {string} */ (
136
136
  /** @type {DOMException} */ (reader.error).message
137
137
  ));
@@ -205,7 +205,7 @@ const blobType = {
205
205
  'Text source',
206
206
  ['br'],
207
207
  ['textarea', {class: 'view-text'}, [
208
- /* istanbul ignore next */
208
+ /* istanbul ignore next -- Guard */
209
209
  /** @type {string|null} */ (reader.result) ?? ''
210
210
  ]]
211
211
  ]));
@@ -213,11 +213,11 @@ const blobType = {
213
213
  // Seems not feasible to accurately simulate
214
214
  reader.addEventListener(
215
215
  'error',
216
- /* istanbul ignore next */
216
+ /* istanbul ignore next -- See comment above */
217
217
  function () {
218
- /* istanbul ignore next */
218
+ /* istanbul ignore next -- See comment above */
219
219
  console.error(reader.error);
220
- /* istanbul ignore next */
220
+ /* istanbul ignore next -- See comment above */
221
221
  div.append(
222
222
  /** @type {DOMException} */ (reader.error).message
223
223
  );
@@ -400,7 +400,7 @@ const blobType = {
400
400
  blob
401
401
  ).slice(8, -1) === 'Blob'
402
402
  ? /** @type {Blob} */ (blob)
403
- /* istanbul ignore next */
403
+ /* istanbul ignore next -- Guard */
404
404
  : undefined;
405
405
  }
406
406
  }
@@ -619,7 +619,7 @@ const blobType = {
619
619
 
620
620
  if (constraints) {
621
621
  const mediaStream = await getUserMedia(constraints);
622
- /* istanbul ignore if */
622
+ /* istanbul ignore if -- Should be present */
623
623
  if (!mediaStream) {
624
624
  await dialogs.alert('Error getting user media');
625
625
  return;
@@ -667,7 +667,7 @@ const blobType = {
667
667
  const mediaStream = await startScreenCapture(
668
668
  screenShareConstraints
669
669
  );
670
- /* istanbul ignore if */
670
+ /* istanbul ignore if -- Should be present */
671
671
  if (!mediaStream) {
672
672
  await dialogs.alert('Error getting user media');
673
673
  return;
@@ -747,7 +747,7 @@ const blobType = {
747
747
  'video.previewMedia'
748
748
  ));
749
749
 
750
- /* istanbul ignore if */
750
+ /* istanbul ignore if -- Should be present */
751
751
  if (!previewMedia.srcObject) {
752
752
  dialogs.alert('No stream found to record');
753
753
  return;
@@ -782,11 +782,11 @@ const blobType = {
782
782
 
783
783
  try {
784
784
  mediaRecorder.start();
785
- /* istanbul ignore next */
785
+ /* istanbul ignore next -- Should not err */
786
786
  } catch {
787
- /* istanbul ignore next */
787
+ /* istanbul ignore next -- Should not err */
788
788
  dialogs.alert('Error starting media recorder');
789
- /* istanbul ignore next */
789
+ /* istanbul ignore next -- Should not err */
790
790
  return;
791
791
  }
792
792
  mediaRecorder.addEventListener('stop', () => {
@@ -923,7 +923,7 @@ const blobType = {
923
923
  ($e(videoContainer, 'img.photo'));
924
924
 
925
925
  canvas.toBlob((blob) => {
926
- /* istanbul ignore if */
926
+ /* istanbul ignore if -- Should not err */
927
927
  if (!blob) {
928
928
  dialogs.alert('Error converting canvas to Blob');
929
929
  return;