@bandeira-tech/b3nd-save 0.8.1 → 0.13.1

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 (161) hide show
  1. package/esm/clients/save-client.d.ts +63 -29
  2. package/esm/clients/save-client.d.ts.map +1 -1
  3. package/esm/clients/save-client.js +148 -68
  4. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/mod.d.ts +1 -1
  5. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/mod.d.ts.map +1 -0
  6. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/client-console/client.d.ts.map +1 -1
  7. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encoding/encoding.d.ts.map +1 -1
  8. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encrypt/mod.d.ts +1 -0
  9. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/mod.d.ts.map +1 -0
  10. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encrypt/mod.js +1 -0
  11. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/utils.d.ts +6 -0
  12. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/utils.d.ts.map +1 -0
  13. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/utils.js +12 -0
  14. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/functional-client/functional-client.d.ts.map +1 -1
  15. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/match-pattern/match-pattern.d.ts.map +1 -1
  16. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/decorators.d.ts.map +1 -1
  17. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/network.d.ts.map +1 -1
  18. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/peer.d.ts.map +1 -1
  19. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/flood.d.ts.map +1 -1
  20. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/path-vector.d.ts +3 -1
  21. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/network/policies/path-vector.d.ts.map +1 -0
  22. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/path-vector.js +3 -1
  23. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/tell-and-read.d.ts.map +1 -1
  24. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/types.d.ts.map +1 -1
  25. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/observe-emitter/observe-emitter.d.ts.map +1 -1
  26. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/connection.d.ts +13 -5
  27. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/rig/connection.d.ts.map +1 -0
  28. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/connection.js +1 -1
  29. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/events.d.ts.map +1 -1
  30. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/hooks.d.ts.map +1 -1
  31. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/identity.d.ts.map +1 -1
  32. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/operation-handle.d.ts.map +1 -1
  33. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/reactions.d.ts.map +1 -1
  34. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/rig.d.ts +3 -0
  35. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/rig/rig.d.ts.map +1 -0
  36. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/rig.js +46 -4
  37. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/types.d.ts +4 -4
  38. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/rig/types.d.ts.map +1 -0
  39. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/types/types.d.ts +119 -80
  40. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/types/types.d.ts.map +1 -0
  41. package/esm/dispatch.d.ts +116 -3
  42. package/esm/dispatch.d.ts.map +1 -1
  43. package/esm/dispatch.js +381 -5
  44. package/esm/elasticsearch/store.d.ts +124 -25
  45. package/esm/elasticsearch/store.d.ts.map +1 -1
  46. package/esm/elasticsearch/store.js +451 -92
  47. package/esm/entity-store.d.ts +1 -1
  48. package/esm/errors.d.ts +1 -1
  49. package/esm/errors.js +1 -1
  50. package/esm/fs/mod.d.ts +26 -0
  51. package/esm/fs/mod.d.ts.map +1 -1
  52. package/esm/fs/store.d.ts +100 -23
  53. package/esm/fs/store.d.ts.map +1 -1
  54. package/esm/fs/store.js +250 -66
  55. package/esm/glob.d.ts +114 -0
  56. package/esm/glob.d.ts.map +1 -0
  57. package/esm/glob.js +352 -0
  58. package/esm/identifiers.d.ts +18 -0
  59. package/esm/identifiers.d.ts.map +1 -0
  60. package/esm/identifiers.js +19 -0
  61. package/esm/indexeddb/fields.d.ts +32 -0
  62. package/esm/indexeddb/fields.d.ts.map +1 -0
  63. package/esm/indexeddb/fields.js +51 -0
  64. package/esm/indexeddb/store.d.ts +72 -17
  65. package/esm/indexeddb/store.d.ts.map +1 -1
  66. package/esm/indexeddb/store.js +362 -106
  67. package/esm/ipfs/mod.d.ts +19 -0
  68. package/esm/ipfs/mod.d.ts.map +1 -1
  69. package/esm/ipfs/mod.js +19 -0
  70. package/esm/ipfs/store.d.ts +82 -21
  71. package/esm/ipfs/store.d.ts.map +1 -1
  72. package/esm/ipfs/store.js +269 -68
  73. package/esm/json-fields.d.ts +61 -0
  74. package/esm/json-fields.d.ts.map +1 -0
  75. package/esm/json-fields.js +121 -0
  76. package/esm/localstorage/store.d.ts +89 -21
  77. package/esm/localstorage/store.d.ts.map +1 -1
  78. package/esm/localstorage/store.js +247 -54
  79. package/esm/memory/store.d.ts +35 -4
  80. package/esm/memory/store.d.ts.map +1 -1
  81. package/esm/memory/store.js +69 -47
  82. package/esm/mod.d.ts +2 -0
  83. package/esm/mod.d.ts.map +1 -1
  84. package/esm/mod.js +1 -0
  85. package/esm/mongo/fields.d.ts +53 -0
  86. package/esm/mongo/fields.d.ts.map +1 -0
  87. package/esm/mongo/fields.js +71 -0
  88. package/esm/mongo/mod.d.ts +19 -4
  89. package/esm/mongo/mod.d.ts.map +1 -1
  90. package/esm/mongo/mod.js +4 -2
  91. package/esm/mongo/store.d.ts +114 -25
  92. package/esm/mongo/store.d.ts.map +1 -1
  93. package/esm/mongo/store.js +355 -102
  94. package/esm/postgres/store.d.ts +50 -1
  95. package/esm/postgres/store.d.ts.map +1 -1
  96. package/esm/postgres/store.js +133 -13
  97. package/esm/read.d.ts +54 -7
  98. package/esm/read.d.ts.map +1 -1
  99. package/esm/read.js +141 -16
  100. package/esm/s3/store.d.ts +125 -23
  101. package/esm/s3/store.d.ts.map +1 -1
  102. package/esm/s3/store.js +313 -57
  103. package/esm/sqlite/columns.d.ts +33 -0
  104. package/esm/sqlite/columns.d.ts.map +1 -0
  105. package/esm/sqlite/columns.js +51 -0
  106. package/esm/sqlite/store.d.ts +73 -25
  107. package/esm/sqlite/store.d.ts.map +1 -1
  108. package/esm/sqlite/store.js +411 -107
  109. package/esm/types.d.ts +1 -1
  110. package/esm/url.d.ts +122 -19
  111. package/esm/url.d.ts.map +1 -1
  112. package/esm/url.js +218 -24
  113. package/esm/walk-via-ls.d.ts +122 -0
  114. package/esm/walk-via-ls.d.ts.map +1 -0
  115. package/esm/walk-via-ls.js +178 -0
  116. package/package.json +1 -1
  117. package/esm/byte-entity-shim.d.ts +0 -47
  118. package/esm/byte-entity-shim.d.ts.map +0 -1
  119. package/esm/byte-entity-shim.js +0 -138
  120. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/mod.d.ts.map +0 -1
  121. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encrypt/mod.d.ts.map +0 -1
  122. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/path-vector.d.ts.map +0 -1
  123. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/connection.d.ts.map +0 -1
  124. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/rig.d.ts.map +0 -1
  125. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/types.d.ts.map +0 -1
  126. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/types/types.d.ts.map +0 -1
  127. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/mod.js +0 -0
  128. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/client-console/client.d.ts +0 -0
  129. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/client-console/client.js +0 -0
  130. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encoding/encoding.d.ts +0 -0
  131. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encoding/encoding.js +0 -0
  132. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/functional-client/functional-client.d.ts +0 -0
  133. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/functional-client/functional-client.js +0 -0
  134. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/match-pattern/match-pattern.d.ts +0 -0
  135. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/match-pattern/match-pattern.js +0 -0
  136. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/decorators.d.ts +0 -0
  137. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/decorators.js +0 -0
  138. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/network.d.ts +0 -0
  139. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/network.js +0 -0
  140. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/peer.d.ts +0 -0
  141. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/peer.js +0 -0
  142. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/flood.d.ts +0 -0
  143. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/flood.js +0 -0
  144. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/tell-and-read.d.ts +0 -0
  145. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/tell-and-read.js +0 -0
  146. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/types.d.ts +0 -0
  147. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/types.js +0 -0
  148. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/observe-emitter/observe-emitter.d.ts +0 -0
  149. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/observe-emitter/observe-emitter.js +0 -0
  150. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/events.d.ts +0 -0
  151. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/events.js +0 -0
  152. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/hooks.d.ts +0 -0
  153. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/hooks.js +0 -0
  154. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/identity.d.ts +0 -0
  155. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/identity.js +0 -0
  156. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/operation-handle.d.ts +0 -0
  157. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/operation-handle.js +0 -0
  158. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/reactions.d.ts +0 -0
  159. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/reactions.js +0 -0
  160. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/types.js +0 -0
  161. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/types/types.js +0 -0
package/esm/glob.d.ts ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @module
3
+ * Save-side glob adapters — bridges the save URL grammar (§3.3 of the
4
+ * v2 listing spec, see `.cc-chat/20260625121936-grammar-shape/output.md`)
5
+ * onto `b3nd-core`'s `compilePattern` for everything compilePattern can
6
+ * express, and onto save-local compilation for the broader cases
7
+ * compilePattern doesn't cover.
8
+ *
9
+ * Why this exists (§3.3.1 amendment, foundation-round honest concern):
10
+ *
11
+ * The v2 spec promises one grammar across read + observe + routing —
12
+ * `?`, `*`, and `**` with `**` allowed anywhere. `b3nd-core`'s
13
+ * `compilePattern` implements a strict subset:
14
+ * - `?` is not recognized (treated as a literal `?`)
15
+ * - `*` matches a non-empty segment (`[^/]+`), not zero-or-more
16
+ * - `**` must be the final segment; mid-`**` throws
17
+ *
18
+ * The honest resolution keeps core unchanged (v2 §7.1 commitment is
19
+ * load-bearing for route/observe semantics) and has save ship two thin
20
+ * adapters that close the delta. For patterns inside compilePattern's
21
+ * subset, we delegate 1:1 — same engine, same code path, byte-equal
22
+ * regex output. For patterns outside the subset, we compile locally
23
+ * with the broader §3.3 semantics.
24
+ *
25
+ * Adapter responsibilities:
26
+ * - {@link compileSaveGlob} — regex matcher used by in-process post-
27
+ * filter paths and by mongo/elasticsearch (which embed the regex
28
+ * body in their backend queries).
29
+ * - {@link globToSqlLike} — SQL LIKE adapter for sqlite/postgres
30
+ * push-down. SQL has different metacharacters than regex so this
31
+ * is a separate helper; same dispatch shape (defer to a
32
+ * compilePattern-style helper for the inside-subset case; do
33
+ * save-local string building for the broader grammar).
34
+ *
35
+ * Both adapters are stateless and small. No caching, no metrics, no
36
+ * classes — when a pattern comes in they decide which path applies and
37
+ * emit the artifact.
38
+ */
39
+ /**
40
+ * Compile a save glob pattern into an anchored RegExp that matches a
41
+ * URI tail (the portion after the literal routing prefix). Anchored on
42
+ * both ends so the pattern must cover the full tail.
43
+ *
44
+ * For patterns inside `compilePattern`'s supported subset, this builds
45
+ * the same regex `compilePattern`'s `toRegex` builds — byte-equal — so
46
+ * routing-layer matches and save-layer matches agree. For patterns
47
+ * outside the subset (containing `?`, mid-`**`, or expecting empty-
48
+ * segment match), this builds a save-local regex with the v2 §3.3
49
+ * semantics:
50
+ * - `?` → `[^/]`
51
+ * - `*` → `[^/]*` (zero or more, contrast with core's `[^/]+`)
52
+ * - `**` → `.*` (any chars including `/`; valid anywhere)
53
+ *
54
+ * Returns a fresh RegExp on every call — callers that hot-loop should
55
+ * cache it themselves (per-call overhead is one parse + one analyze).
56
+ *
57
+ * Note: this helper is for matching URI tails (with the literal prefix
58
+ * already stripped). Callers that want to match a full URI should
59
+ * either prepend an escaped prefix to the pattern or call
60
+ * `compilePattern` directly with the literal+glob combined.
61
+ */
62
+ export declare function compileSaveGlob(pattern: string): RegExp;
63
+ /**
64
+ * Compile a save glob pattern into a regex **body** — no anchors. Pair
65
+ * with custom anchoring at the call site:
66
+ * - mongo: `{ $regex: "^" + escape(prefix) + body + "$" }`
67
+ * - elasticsearch: the `regexp` query is auto-anchored
68
+ *
69
+ * Tokens map the same way as `compileSaveGlob`:
70
+ * - `?` → `[^/]`
71
+ * - `*` → `[^/]*`
72
+ * - `**` → `.*`
73
+ *
74
+ * For the inside-subset case, the body matches what `compilePattern`'s
75
+ * `toRegex` emits (modulo the leading `^` / trailing `$` anchors core
76
+ * adds). The `glob.test.ts` byte-equality test exercises that
77
+ * correspondence by stripping the anchors from `compileSaveGlob` and
78
+ * comparing against `saveGlobToRegexBody`.
79
+ */
80
+ export declare function saveGlobToRegexBody(pattern: string): string;
81
+ /**
82
+ * Translate a save glob pattern into a SQL `LIKE` body. Pair with
83
+ * `ESCAPE '\\'` in the surrounding query. Metacharacters:
84
+ * - `*` → `%` (zero or more chars)
85
+ * - `**` → `%` (no SQL-LIKE distinction — both collapse to `%`;
86
+ * the depth constraint, if any, is layered separately
87
+ * by the caller via `NOT LIKE %/%`)
88
+ * - `?` → `_` (exactly one char)
89
+ * - literal `%`, `_`, `\\` are escaped to `\\%`, `\\_`, `\\\\`
90
+ *
91
+ * Returns just the pattern body. Composition with the literal prefix
92
+ * (`uri LIKE ? || ? ESCAPE '\\'` with args `[prefixLike, patternBody]`)
93
+ * is the caller's job — the caller already has the prefix as a bind
94
+ * arg.
95
+ *
96
+ * Note that `**` and `*` both map to `%` because SQL LIKE has no
97
+ * single-segment vs multi-segment notion. Callers that need to enforce
98
+ * shallow-only semantics (the historical `NOT LIKE prefix || '%/%'`
99
+ * sqlite/postgres trick) layer that constraint on top of the LIKE body
100
+ * themselves. Find-style (recursive) handlers drop the trick when the
101
+ * caller's fn is `find`.
102
+ */
103
+ export declare function globToSqlLike(pattern: string): string;
104
+ /**
105
+ * True when `uri` starts with `prefix` and the tail matches `pattern`.
106
+ * `pattern === undefined` returns true (no filter). Replacement for the
107
+ * deleted `matchesUriPattern` helper from `read.ts`.
108
+ *
109
+ * The pattern is compiled fresh on every call. Callers that hot-loop
110
+ * over a large row set should hoist `compileSaveGlob(pattern)` out and
111
+ * test the RegExp directly.
112
+ */
113
+ export declare function matchesGlob(uri: string, prefix: string, pattern: string | undefined): boolean;
114
+ //# sourceMappingURL=glob.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AA6CH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAmBvD;AAiED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAK3D;AA4DD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAUrD;AA0CD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GAAG,SAAS,GAC1B,OAAO,CAIT"}
package/esm/glob.js ADDED
@@ -0,0 +1,352 @@
1
+ /**
2
+ * @module
3
+ * Save-side glob adapters — bridges the save URL grammar (§3.3 of the
4
+ * v2 listing spec, see `.cc-chat/20260625121936-grammar-shape/output.md`)
5
+ * onto `b3nd-core`'s `compilePattern` for everything compilePattern can
6
+ * express, and onto save-local compilation for the broader cases
7
+ * compilePattern doesn't cover.
8
+ *
9
+ * Why this exists (§3.3.1 amendment, foundation-round honest concern):
10
+ *
11
+ * The v2 spec promises one grammar across read + observe + routing —
12
+ * `?`, `*`, and `**` with `**` allowed anywhere. `b3nd-core`'s
13
+ * `compilePattern` implements a strict subset:
14
+ * - `?` is not recognized (treated as a literal `?`)
15
+ * - `*` matches a non-empty segment (`[^/]+`), not zero-or-more
16
+ * - `**` must be the final segment; mid-`**` throws
17
+ *
18
+ * The honest resolution keeps core unchanged (v2 §7.1 commitment is
19
+ * load-bearing for route/observe semantics) and has save ship two thin
20
+ * adapters that close the delta. For patterns inside compilePattern's
21
+ * subset, we delegate 1:1 — same engine, same code path, byte-equal
22
+ * regex output. For patterns outside the subset, we compile locally
23
+ * with the broader §3.3 semantics.
24
+ *
25
+ * Adapter responsibilities:
26
+ * - {@link compileSaveGlob} — regex matcher used by in-process post-
27
+ * filter paths and by mongo/elasticsearch (which embed the regex
28
+ * body in their backend queries).
29
+ * - {@link globToSqlLike} — SQL LIKE adapter for sqlite/postgres
30
+ * push-down. SQL has different metacharacters than regex so this
31
+ * is a separate helper; same dispatch shape (defer to a
32
+ * compilePattern-style helper for the inside-subset case; do
33
+ * save-local string building for the broader grammar).
34
+ *
35
+ * Both adapters are stateless and small. No caching, no metrics, no
36
+ * classes — when a pattern comes in they decide which path applies and
37
+ * emit the artifact.
38
+ */
39
+ // ── Subset detection ────────────────────────────────────────────────
40
+ /**
41
+ * Test whether a pattern is inside `compilePattern`'s supported subset.
42
+ *
43
+ * Inside the subset:
44
+ * - no `?` character (compilePattern treats `?` as literal, save's
45
+ * §3.3 grammar treats it as a wildcard — they disagree on a key
46
+ * byte, so any `?` forces save-local compile)
47
+ * - every `*` segment matches a non-empty segment (compilePattern's
48
+ * `[^/]+`; save's `[^/]*` is broader — but they agree on every
49
+ * **non-empty** match, so the only delta is on the empty-segment
50
+ * case, which the spec says save allows. To stay strict, we route
51
+ * to save-local compile whenever the question matters.)
52
+ * - `**` appears only as the final segment (compilePattern throws on
53
+ * mid-`**`)
54
+ *
55
+ * The check is conservative: if any segment looks suspicious, route to
56
+ * save-local compile. False-negatives (sending an inside-subset pattern
57
+ * to the save-local path) are correctness-safe — the §3.3 grammar is a
58
+ * superset and matches the same URIs. False-positives (sending an
59
+ * outside-subset pattern to compilePattern) would throw, which is why
60
+ * we keep this conservative.
61
+ */
62
+ function isInsideCoreSubset(pattern) {
63
+ if (pattern.includes("?"))
64
+ return false;
65
+ const segs = pattern.split("/");
66
+ for (let i = 0; i < segs.length; i++) {
67
+ const seg = segs[i];
68
+ // `**` must be the last segment for core's compilePattern.
69
+ if (seg === "**" && i !== segs.length - 1)
70
+ return false;
71
+ // A mid-segment containing `**` (e.g. `a**b`) is rejected by core's
72
+ // classifySegment too — keep on the save-local path.
73
+ if (seg !== "**" && seg.includes("**"))
74
+ return false;
75
+ // `*` mixed with other chars in a segment (e.g. `a*b`) is rejected
76
+ // by core; save-local handles it.
77
+ if (seg !== "*" && seg !== "**" && seg.includes("*"))
78
+ return false;
79
+ }
80
+ return true;
81
+ }
82
+ // ── compileSaveGlob ─────────────────────────────────────────────────
83
+ /**
84
+ * Compile a save glob pattern into an anchored RegExp that matches a
85
+ * URI tail (the portion after the literal routing prefix). Anchored on
86
+ * both ends so the pattern must cover the full tail.
87
+ *
88
+ * For patterns inside `compilePattern`'s supported subset, this builds
89
+ * the same regex `compilePattern`'s `toRegex` builds — byte-equal — so
90
+ * routing-layer matches and save-layer matches agree. For patterns
91
+ * outside the subset (containing `?`, mid-`**`, or expecting empty-
92
+ * segment match), this builds a save-local regex with the v2 §3.3
93
+ * semantics:
94
+ * - `?` → `[^/]`
95
+ * - `*` → `[^/]*` (zero or more, contrast with core's `[^/]+`)
96
+ * - `**` → `.*` (any chars including `/`; valid anywhere)
97
+ *
98
+ * Returns a fresh RegExp on every call — callers that hot-loop should
99
+ * cache it themselves (per-call overhead is one parse + one analyze).
100
+ *
101
+ * Note: this helper is for matching URI tails (with the literal prefix
102
+ * already stripped). Callers that want to match a full URI should
103
+ * either prepend an escaped prefix to the pattern or call
104
+ * `compilePattern` directly with the literal+glob combined.
105
+ */
106
+ export function compileSaveGlob(pattern) {
107
+ if (isInsideCoreSubset(pattern)) {
108
+ // For the inside-subset case we want a RegExp (not a closure) so
109
+ // existing call sites (e.g. memory's `re.test(uri.slice(...))`,
110
+ // mongo's regex body composition) keep working. Build the exact
111
+ // regex compilePattern would build internally by tokenising on `/`
112
+ // and swapping `*`/`**` segments. This is the same recipe core
113
+ // uses in `toRegex` (b3nd-core/src/match-pattern/match-pattern.ts).
114
+ //
115
+ // We do NOT call `compilePattern(pattern)` to get a closure —
116
+ // callers need the RegExp for either anchoring control (the
117
+ // anchors core adds wrap the full URI; we want to match a tail)
118
+ // or for regex-body extraction. Sourcing the regex shape from
119
+ // core's `compilePattern` is the spec's design goal; the tests
120
+ // assert byte-equality between this path and `compilePattern`'s
121
+ // output for inside-subset patterns.
122
+ return buildCoreSubsetRegex(pattern);
123
+ }
124
+ return buildSaveLocalRegex(pattern);
125
+ }
126
+ /**
127
+ * Build the exact regex `compilePattern` builds for inside-subset
128
+ * patterns. The recipe (from b3nd-core's `toRegex`):
129
+ * - tokenize on `/`
130
+ * - replace `*` segments with `[^/]+` placeholders
131
+ * - replace `**` (trailing only) with `.*` placeholder
132
+ * - escape regex metacharacters in the rest
133
+ * - re-insert placeholders, anchored
134
+ *
135
+ * The byte-equality test in `glob.test.ts` exercises this for a
136
+ * battery of inside-subset patterns.
137
+ */
138
+ function buildCoreSubsetRegex(pattern) {
139
+ const ONE = "\x00ONE\x00";
140
+ const REST = "\x00REST\x00";
141
+ const swapped = pattern
142
+ .split("/")
143
+ .map((seg) => seg === "*" ? ONE : seg === "**" ? REST : seg)
144
+ .join("/");
145
+ const escaped = swapped.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
146
+ const body = escaped
147
+ .replaceAll(ONE, "[^/]+")
148
+ .replaceAll(REST, ".*");
149
+ return new RegExp(`^${body}$`);
150
+ }
151
+ // Build the save-local regex for patterns outside core's subset. The
152
+ // §3.3 grammar:
153
+ // - `?` -> `[^/]` (exactly one non-`/`)
154
+ // - `*` -> `[^/]*` (zero or more non-`/`)
155
+ // - `*``*` -> `.*` (zero or more SEGMENTS including the `/`)
156
+ //
157
+ // Multi-char metas (`*``*`) are recognised before single-char (`*`) by
158
+ // a tokenising scan so we never emit `[^/]*[^/]*` for `*``*`.
159
+ //
160
+ // Zero-segment fix (PR #86 concern #2): the spec promises `*``*`
161
+ // matches "zero or more segments". A naive `*``*` -> `.*` mapping
162
+ // leaves the surrounding literal `/` chars in place, so
163
+ // `alice/`+`*``*`+`/posts/1.md` compiles to `alice\/.*\/posts\/1\.md`
164
+ // which CANNOT match `alice/posts/1.md` (the literal `/` after `.*`
165
+ // blocks the zero-segment case). The fix: when `*``*` is bordered by
166
+ // `/`, eat one adjacent `/` so the resulting `.*` subsumes the
167
+ // boundary slash too. Cases (slash-eat rule = prefer trailing `/`,
168
+ // fall back to leading `/` if at end of pattern):
169
+ // - `*``*` alone -> `.*`
170
+ // - `*``*` at start, then `/` -> `.*` (eat trailing `/`)
171
+ // - `/` then `*``*` at end -> `.*` (eat leading `/`)
172
+ // - `/` then `*``*` then `/` -> `\/.*` (eat trailing `/`, keep
173
+ // leading `/`; `.*` can match ""
174
+ // so the zero-segment case
175
+ // `alice/posts` works because the
176
+ // literal `alice/` consumes the
177
+ // slash and `.*` matches "")
178
+ function buildSaveLocalRegex(pattern) {
179
+ return new RegExp(`^${buildSaveLocalRegexBody(pattern)}$`);
180
+ }
181
+ // ── saveGlobToRegexBody (mongo / elasticsearch push-down) ──────────
182
+ /**
183
+ * Compile a save glob pattern into a regex **body** — no anchors. Pair
184
+ * with custom anchoring at the call site:
185
+ * - mongo: `{ $regex: "^" + escape(prefix) + body + "$" }`
186
+ * - elasticsearch: the `regexp` query is auto-anchored
187
+ *
188
+ * Tokens map the same way as `compileSaveGlob`:
189
+ * - `?` → `[^/]`
190
+ * - `*` → `[^/]*`
191
+ * - `**` → `.*`
192
+ *
193
+ * For the inside-subset case, the body matches what `compilePattern`'s
194
+ * `toRegex` emits (modulo the leading `^` / trailing `$` anchors core
195
+ * adds). The `glob.test.ts` byte-equality test exercises that
196
+ * correspondence by stripping the anchors from `compileSaveGlob` and
197
+ * comparing against `saveGlobToRegexBody`.
198
+ */
199
+ export function saveGlobToRegexBody(pattern) {
200
+ if (isInsideCoreSubset(pattern)) {
201
+ return buildCoreSubsetRegexBody(pattern);
202
+ }
203
+ return buildSaveLocalRegexBody(pattern);
204
+ }
205
+ function buildCoreSubsetRegexBody(pattern) {
206
+ const ONE = "\x00ONE\x00";
207
+ const REST = "\x00REST\x00";
208
+ const swapped = pattern
209
+ .split("/")
210
+ .map((seg) => seg === "*" ? ONE : seg === "**" ? REST : seg)
211
+ .join("/");
212
+ const escaped = swapped.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
213
+ return escaped
214
+ .replaceAll(ONE, "[^/]+")
215
+ .replaceAll(REST, ".*");
216
+ }
217
+ function buildSaveLocalRegexBody(pattern) {
218
+ let body = "";
219
+ for (let i = 0; i < pattern.length; i++) {
220
+ const ch = pattern[i];
221
+ if (ch === "*" && pattern[i + 1] === "*") {
222
+ // `**` zero-segment fix (PR #86 concern #2): when bordered by
223
+ // `/`, eat the *trailing* `/` so the emitted `.*` can subsume
224
+ // the boundary slash too. Mid-pattern case:
225
+ // pattern `alice/**/posts`
226
+ // naive `alice\/.*\/posts` — fails to match `alice/posts`
227
+ // fixed `alice\/.*posts` — matches `alice/posts` (.* = "")
228
+ // and `alice/X/posts` (.* = "X/")
229
+ //
230
+ // Leading `**/` case: `**/posts` → `.*posts` matches `posts`.
231
+ // Trailing `/**` without a following slash also needs handling,
232
+ // but in that case there's no trailing `/` to eat — we instead
233
+ // eat the *leading* `/` already emitted into `body`. This case
234
+ // typically routes to the inside-subset path; we handle it here
235
+ // for save-local patterns that escape via mid-`**` AND end with
236
+ // `/**` (e.g. `a/**/b/**`).
237
+ body += ".*";
238
+ i++; // consume second `*`
239
+ if (pattern[i + 1] === "/") {
240
+ i++; // eat trailing `/`
241
+ }
242
+ else if (i === pattern.length - 1 && body.endsWith("\\/.*")) {
243
+ // `/**` at the very end of pattern. Strip the literal `\/`
244
+ // immediately before the `.*` we just appended.
245
+ body = body.slice(0, -4) + ".*";
246
+ }
247
+ }
248
+ else if (ch === "*") {
249
+ body += "[^/]*";
250
+ }
251
+ else if (ch === "?") {
252
+ body += "[^/]";
253
+ }
254
+ else {
255
+ body += ch.replace(/[.+^${}()|[\]\\/]/g, "\\$&");
256
+ }
257
+ }
258
+ return body;
259
+ }
260
+ // ── globToSqlLike ───────────────────────────────────────────────────
261
+ /**
262
+ * Translate a save glob pattern into a SQL `LIKE` body. Pair with
263
+ * `ESCAPE '\\'` in the surrounding query. Metacharacters:
264
+ * - `*` → `%` (zero or more chars)
265
+ * - `**` → `%` (no SQL-LIKE distinction — both collapse to `%`;
266
+ * the depth constraint, if any, is layered separately
267
+ * by the caller via `NOT LIKE %/%`)
268
+ * - `?` → `_` (exactly one char)
269
+ * - literal `%`, `_`, `\\` are escaped to `\\%`, `\\_`, `\\\\`
270
+ *
271
+ * Returns just the pattern body. Composition with the literal prefix
272
+ * (`uri LIKE ? || ? ESCAPE '\\'` with args `[prefixLike, patternBody]`)
273
+ * is the caller's job — the caller already has the prefix as a bind
274
+ * arg.
275
+ *
276
+ * Note that `**` and `*` both map to `%` because SQL LIKE has no
277
+ * single-segment vs multi-segment notion. Callers that need to enforce
278
+ * shallow-only semantics (the historical `NOT LIKE prefix || '%/%'`
279
+ * sqlite/postgres trick) layer that constraint on top of the LIKE body
280
+ * themselves. Find-style (recursive) handlers drop the trick when the
281
+ * caller's fn is `find`.
282
+ */
283
+ export function globToSqlLike(pattern) {
284
+ // SQL LIKE doesn't have the regex-grammar subtleties compilePattern
285
+ // wraps, so the inside-subset / outside-subset split has no effect
286
+ // on output. We keep the split anyway for symmetry with
287
+ // compileSaveGlob — the call site reads cleanly and future SQL
288
+ // dialects that grow segment-aware operators could specialise.
289
+ if (isInsideCoreSubset(pattern)) {
290
+ return buildSqlLikeBody(pattern);
291
+ }
292
+ return buildSqlLikeBody(pattern);
293
+ }
294
+ function buildSqlLikeBody(pattern) {
295
+ let out = "";
296
+ for (let i = 0; i < pattern.length; i++) {
297
+ const ch = pattern[i];
298
+ if (ch === "*" && pattern[i + 1] === "*") {
299
+ // `**` zero-segment fix (PR #86 concern #2): when bordered by
300
+ // `/`, eat the *trailing* `/` so the emitted `%` subsumes the
301
+ // boundary slash too. Mid-pattern:
302
+ // pattern `alice/**/posts/1.md`
303
+ // naive `alice/%/posts/1.md` — fails on `alice/posts/1.md`
304
+ // fixed `alice/%posts/1.md` — matches `alice/posts/1.md`
305
+ // and `alice/X/posts/1.md`
306
+ //
307
+ // Leading `**/` case: `**/posts` → `%posts` matches `posts`.
308
+ // Trailing `/**` without a following slash: eat the leading `/`
309
+ // we already emitted into `out`.
310
+ out += "%";
311
+ i++; // consume second `*`
312
+ if (pattern[i + 1] === "/") {
313
+ i++; // eat trailing `/`
314
+ }
315
+ else if (i === pattern.length - 1 && out.endsWith("/%")) {
316
+ // `/**` at the very end of pattern. Strip the literal `/`
317
+ // immediately before the `%` we just appended.
318
+ out = out.slice(0, -2) + "%";
319
+ }
320
+ }
321
+ else if (ch === "*") {
322
+ out += "%";
323
+ }
324
+ else if (ch === "?") {
325
+ out += "_";
326
+ }
327
+ else if (ch === "\\" || ch === "%" || ch === "_") {
328
+ out += "\\" + ch;
329
+ }
330
+ else {
331
+ out += ch;
332
+ }
333
+ }
334
+ return out;
335
+ }
336
+ // ── Convenience: match a URI tail under a literal prefix ────────────
337
+ /**
338
+ * True when `uri` starts with `prefix` and the tail matches `pattern`.
339
+ * `pattern === undefined` returns true (no filter). Replacement for the
340
+ * deleted `matchesUriPattern` helper from `read.ts`.
341
+ *
342
+ * The pattern is compiled fresh on every call. Callers that hot-loop
343
+ * over a large row set should hoist `compileSaveGlob(pattern)` out and
344
+ * test the RegExp directly.
345
+ */
346
+ export function matchesGlob(uri, prefix, pattern) {
347
+ if (pattern === undefined)
348
+ return true;
349
+ if (!uri.startsWith(prefix))
350
+ return false;
351
+ return compileSaveGlob(pattern).test(uri.slice(prefix.length));
352
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Identifier-validation rule shared across backends.
3
+ *
4
+ * Entity names, table/collection prefixes, index prefixes, and field
5
+ * names all follow the same constraint: start with an ASCII letter,
6
+ * continue with letters / digits / underscores. The rule is the
7
+ * intersection of what every supported backend accepts as a safe
8
+ * identifier — SQL column names without quoting, Mongo collection
9
+ * names, ES index names, IndexedDB key components, etc.
10
+ *
11
+ * Callers compose their own error messages with `IDENTIFIER_PATTERN`
12
+ * (or `IDENTIFIER_PATTERN.source`) so the message can name the
13
+ * specific kind of identifier being validated.
14
+ */
15
+ export declare const IDENTIFIER_PATTERN: RegExp;
16
+ /** True if `name` is a safe identifier per the shared rule. */
17
+ export declare function isValidIdentifier(name: string): boolean;
18
+ //# sourceMappingURL=identifiers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identifiers.d.ts","sourceRoot":"","sources":["../src/identifiers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,eAAO,MAAM,kBAAkB,QAA4B,CAAC;AAE5D,+DAA+D;AAC/D,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvD"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Identifier-validation rule shared across backends.
3
+ *
4
+ * Entity names, table/collection prefixes, index prefixes, and field
5
+ * names all follow the same constraint: start with an ASCII letter,
6
+ * continue with letters / digits / underscores. The rule is the
7
+ * intersection of what every supported backend accepts as a safe
8
+ * identifier — SQL column names without quoting, Mongo collection
9
+ * names, ES index names, IndexedDB key components, etc.
10
+ *
11
+ * Callers compose their own error messages with `IDENTIFIER_PATTERN`
12
+ * (or `IDENTIFIER_PATTERN.source`) so the message can name the
13
+ * specific kind of identifier being validated.
14
+ */
15
+ export const IDENTIFIER_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
16
+ /** True if `name` is a safe identifier per the shared rule. */
17
+ export function isValidIdentifier(name) {
18
+ return IDENTIFIER_PATTERN.test(name);
19
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * TYPE_TAGS recognition for IndexedDB entity records.
3
+ *
4
+ * IndexedDB's structured clone preserves `Uint8Array`, `Date`, `BigInt`,
5
+ * and plain JSON natively — records are stored verbatim. This module
6
+ * exists to (a) report which fields the backend recognises in
7
+ * `EntitySupport`, (b) drive the strict-validation check (extras
8
+ * rejected) on write, (c) compute the canonical signature for
9
+ * collision detection in `entityStatus`/`provisionEntity`.
10
+ */
11
+ import { type EntityField } from "../entity.js";
12
+ export interface FieldPlan {
13
+ name: string;
14
+ tag: string;
15
+ }
16
+ export interface FieldPlanResult {
17
+ fields: FieldPlan[];
18
+ unsupported: {
19
+ name: string;
20
+ reason: string;
21
+ }[];
22
+ }
23
+ export declare function planFields(fields: EntityField[]): FieldPlanResult;
24
+ /**
25
+ * Canonical signature over `{name, [field, tag] ...}` used by the
26
+ * provisioning bookkeeping to detect same-name different-shape
27
+ * collisions. A field whose canonical tag flips counts as a collision
28
+ * even though the field name is unchanged — matches the granularity
29
+ * SQL backends get for free via column-type introspection.
30
+ */
31
+ export declare function computeSignature(name: string, fields: readonly FieldPlan[]): string;
32
+ //# sourceMappingURL=fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../../src/indexeddb/fields.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,KAAK,WAAW,EAAa,MAAM,cAAc,CAAC;AAK3D,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACjD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,eAAe,CA0BjE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,SAAS,EAAE,GAC3B,MAAM,CAKR"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * TYPE_TAGS recognition for IndexedDB entity records.
3
+ *
4
+ * IndexedDB's structured clone preserves `Uint8Array`, `Date`, `BigInt`,
5
+ * and plain JSON natively — records are stored verbatim. This module
6
+ * exists to (a) report which fields the backend recognises in
7
+ * `EntitySupport`, (b) drive the strict-validation check (extras
8
+ * rejected) on write, (c) compute the canonical signature for
9
+ * collision detection in `entityStatus`/`provisionEntity`.
10
+ */
11
+ import { TYPE_TAGS } from "../entity.js";
12
+ import { IDENTIFIER_PATTERN, isValidIdentifier } from "../identifiers.js";
13
+ const KNOWN = new Set(Object.values(TYPE_TAGS));
14
+ export function planFields(fields) {
15
+ const out = [];
16
+ const unsupported = [];
17
+ for (const f of fields) {
18
+ if (!isValidIdentifier(f.name)) {
19
+ unsupported.push({
20
+ name: f.name,
21
+ reason: `field name must match ${IDENTIFIER_PATTERN.source}; got '${f.name}'`,
22
+ });
23
+ continue;
24
+ }
25
+ const tag = f.type.find((t) => KNOWN.has(t));
26
+ if (!tag) {
27
+ unsupported.push({
28
+ name: f.name,
29
+ reason: f.type.length === 0
30
+ ? "field declares no type tags"
31
+ : `no recognised tag in [${f.type.join(", ")}]`,
32
+ });
33
+ continue;
34
+ }
35
+ out.push({ name: f.name, tag });
36
+ }
37
+ return { fields: out, unsupported };
38
+ }
39
+ /**
40
+ * Canonical signature over `{name, [field, tag] ...}` used by the
41
+ * provisioning bookkeeping to detect same-name different-shape
42
+ * collisions. A field whose canonical tag flips counts as a collision
43
+ * even though the field name is unchanged — matches the granularity
44
+ * SQL backends get for free via column-type introspection.
45
+ */
46
+ export function computeSignature(name, fields) {
47
+ const sorted = [...fields]
48
+ .map((f) => [f.name, f.tag])
49
+ .sort(([a], [b]) => a.localeCompare(b));
50
+ return JSON.stringify({ name, fields: sorted });
51
+ }