@clementine-solutions/jane-io 1.0.1 → 1.0.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 (204) hide show
  1. package/dist/core/analysis/diff.js +88 -0
  2. package/dist/core/analysis/explain.js +117 -0
  3. package/dist/core/analysis/index.js +26 -0
  4. package/dist/core/analysis/replay.js +68 -0
  5. package/dist/core/analysis/telemetry.js +123 -0
  6. package/dist/core/boundary-rules/at-most-one.js +38 -0
  7. package/dist/core/boundary-rules/conditionally-required.js +44 -0
  8. package/dist/core/boundary-rules/date-range.js +39 -0
  9. package/dist/core/boundary-rules/index.js +21 -0
  10. package/dist/core/boundary-rules/mutually-exclusive.js +38 -0
  11. package/dist/core/boundary-rules/no-unknown-fields.js +39 -0
  12. package/dist/core/boundary-rules/require-all.js +39 -0
  13. package/dist/core/boundary-rules/require-one.js +41 -0
  14. package/dist/core/common/events.js +82 -0
  15. package/dist/core/common/fluent.js +429 -0
  16. package/dist/core/common/index.js +31 -0
  17. package/dist/core/common/policy.js +550 -0
  18. package/dist/core/common/utilities.js +177 -0
  19. package/dist/core/common/wildcard.js +63 -0
  20. package/dist/core/field-path/construct.js +189 -0
  21. package/dist/core/field-path/format.js +154 -0
  22. package/dist/core/field-path/index.js +26 -0
  23. package/dist/core/field-path/utilities.js +138 -0
  24. package/dist/core/field-path/walk.js +80 -0
  25. package/dist/core/fluent-registry.js +151 -0
  26. package/dist/core/normalizers/array/compact-sparse-array.js +40 -0
  27. package/dist/core/normalizers/array/flatten-one-level.js +45 -0
  28. package/dist/core/normalizers/array/remove-empty-string-items.js +34 -0
  29. package/dist/core/normalizers/array/remove-null-items.js +34 -0
  30. package/dist/core/normalizers/array/remove-undefined-items.js +34 -0
  31. package/dist/core/normalizers/date/invalid-date-to-undefined.js +35 -0
  32. package/dist/core/normalizers/index.js +46 -0
  33. package/dist/core/normalizers/normalizer-register.js +41 -0
  34. package/dist/core/normalizers/number/infinity-to-undefined.js +35 -0
  35. package/dist/core/normalizers/number/nan-to-undefined.js +34 -0
  36. package/dist/core/normalizers/number/normalize-negative-zero.js +33 -0
  37. package/dist/core/normalizers/object/remove-empty-array-keys.js +38 -0
  38. package/dist/core/normalizers/object/remove-empty-object-keys.js +42 -0
  39. package/dist/core/normalizers/object/remove-empty-string-keys.js +37 -0
  40. package/dist/core/normalizers/object/remove-null-keys.js +37 -0
  41. package/dist/core/normalizers/object/remove-undefined-keys.js +37 -0
  42. package/dist/core/normalizers/string/collapse-whitespace.js +35 -0
  43. package/dist/core/normalizers/string/empty-to-undefined.js +33 -0
  44. package/dist/core/normalizers/string/trim.js +34 -0
  45. package/dist/core/parsers/index.js +43 -0
  46. package/dist/core/parsers/parse-array-string.js +36 -0
  47. package/dist/core/parsers/parse-bigint-string.js +33 -0
  48. package/dist/core/parsers/parse-binary-string.js +33 -0
  49. package/dist/core/parsers/parse-boolean-string.js +27 -0
  50. package/dist/core/parsers/parse-date-string.js +30 -0
  51. package/dist/core/parsers/parse-duration-string.js +37 -0
  52. package/dist/core/parsers/parse-hex-string.js +29 -0
  53. package/dist/core/parsers/parse-integer-string.js +30 -0
  54. package/dist/core/parsers/parse-json-string.js +35 -0
  55. package/dist/core/parsers/parse-numeric-string.js +29 -0
  56. package/dist/core/parsers/parse-object-string.js +36 -0
  57. package/dist/core/parsers/parse-octal-string.js +33 -0
  58. package/dist/core/parsers/parse-scientific-notation-string.js +30 -0
  59. package/dist/core/parsers/parse-url-string.js +36 -0
  60. package/dist/core/pipeline/boundary.js +256 -0
  61. package/dist/core/pipeline/contain.js +339 -0
  62. package/dist/core/pipeline/index.js +37 -0
  63. package/dist/core/pipeline/normalize.js +76 -0
  64. package/dist/core/pipeline/parse.js +91 -0
  65. package/dist/core/pipeline/pipeline.js +418 -0
  66. package/dist/core/pipeline/scan.js +115 -0
  67. package/dist/core/pipeline/validate.js +74 -0
  68. package/dist/core/scanners/any/scan-for-sentinels.js +35 -0
  69. package/dist/core/scanners/array/array-is-deep.js +38 -0
  70. package/dist/core/scanners/array/array-is-heterogenous.js +39 -0
  71. package/dist/core/scanners/array/array-is-large.js +32 -0
  72. package/dist/core/scanners/bigint/bigint-is-large.js +34 -0
  73. package/dist/core/scanners/bigint/bigint-not-safe.js +34 -0
  74. package/dist/core/scanners/date/date-is-before-epoch.js +32 -0
  75. package/dist/core/scanners/date/date-is-far-future.js +32 -0
  76. package/dist/core/scanners/date/date-is-invalid.js +31 -0
  77. package/dist/core/scanners/index.js +58 -0
  78. package/dist/core/scanners/number/number-is-infinite.js +31 -0
  79. package/dist/core/scanners/number/number-is-nan.js +31 -0
  80. package/dist/core/scanners/number/number-is-too-large.js +33 -0
  81. package/dist/core/scanners/number/number-is-unsafe-integer.js +31 -0
  82. package/dist/core/scanners/object/object-has-circular-references.js +43 -0
  83. package/dist/core/scanners/object/object-has-many-keys.js +33 -0
  84. package/dist/core/scanners/object/object-is-deep.js +38 -0
  85. package/dist/core/scanners/scanner-registry.js +36 -0
  86. package/dist/core/scanners/string/string-has-unsafe-unicode.js +32 -0
  87. package/dist/core/scanners/string/string-has-whitespace-edges.js +31 -0
  88. package/dist/core/scanners/string/string-is-long.js +32 -0
  89. package/dist/core/scanners/unknown/unknown-not-scannable.js +34 -0
  90. package/dist/core/shapes/analysis.js +11 -0
  91. package/dist/core/shapes/boundary.js +11 -0
  92. package/dist/core/shapes/events.js +10 -0
  93. package/dist/core/shapes/field-path.js +10 -0
  94. package/dist/core/shapes/index.js +11 -0
  95. package/dist/core/shapes/normalize.js +11 -0
  96. package/dist/core/shapes/parse.js +11 -0
  97. package/dist/core/shapes/pipeline.js +11 -0
  98. package/dist/core/shapes/policy.js +11 -0
  99. package/dist/core/shapes/public.js +10 -0
  100. package/dist/core/shapes/scan.js +11 -0
  101. package/dist/core/shapes/validate.js +11 -0
  102. package/dist/core/validators/array/array-max-items.js +42 -0
  103. package/dist/core/validators/array/array-min-items.js +42 -0
  104. package/dist/core/validators/array/array.js +34 -0
  105. package/dist/core/validators/array/excludes.js +47 -0
  106. package/dist/core/validators/array/has-unique-items.js +46 -0
  107. package/dist/core/validators/array/includes.js +46 -0
  108. package/dist/core/validators/array/items-equal.js +42 -0
  109. package/dist/core/validators/array/no-empty-string-items.js +46 -0
  110. package/dist/core/validators/array/no-null-items.js +46 -0
  111. package/dist/core/validators/array/no-undefined-items.js +45 -0
  112. package/dist/core/validators/array/non-empty-array.js +45 -0
  113. package/dist/core/validators/array/not-sparse.js +44 -0
  114. package/dist/core/validators/bigint/bigint-equals.js +57 -0
  115. package/dist/core/validators/bigint/bigint-max.js +63 -0
  116. package/dist/core/validators/bigint/bigint-min.js +87 -0
  117. package/dist/core/validators/bigint/bigint-negative.js +73 -0
  118. package/dist/core/validators/bigint/bigint-non-negative.js +72 -0
  119. package/dist/core/validators/bigint/bigint-non-positive.js +72 -0
  120. package/dist/core/validators/bigint/bigint-positive.js +72 -0
  121. package/dist/core/validators/bigint/bigint-safe.js +75 -0
  122. package/dist/core/validators/bigint/bigint.js +38 -0
  123. package/dist/core/validators/boolean/boolean.js +39 -0
  124. package/dist/core/validators/boolean/is-false.js +48 -0
  125. package/dist/core/validators/boolean/is-true.js +48 -0
  126. package/dist/core/validators/common/is-country-code.js +36 -0
  127. package/dist/core/validators/common/is-currency-code.js +36 -0
  128. package/dist/core/validators/common/is-email-strict.js +36 -0
  129. package/dist/core/validators/common/is-email.js +36 -0
  130. package/dist/core/validators/common/is-ip.js +37 -0
  131. package/dist/core/validators/common/is-phone-strict.js +36 -0
  132. package/dist/core/validators/common/is-phone.js +36 -0
  133. package/dist/core/validators/common/is-port.js +35 -0
  134. package/dist/core/validators/common/is-postal-code.js +36 -0
  135. package/dist/core/validators/common/is-url.js +38 -0
  136. package/dist/core/validators/common/is-uuid.js +36 -0
  137. package/dist/core/validators/date/before-epoch.js +56 -0
  138. package/dist/core/validators/date/date-now-required.js +48 -0
  139. package/dist/core/validators/date/is-date.js +47 -0
  140. package/dist/core/validators/date/is-far-future.js +46 -0
  141. package/dist/core/validators/date/is-future.js +59 -0
  142. package/dist/core/validators/date/is-past.js +59 -0
  143. package/dist/core/validators/date/not-after.js +66 -0
  144. package/dist/core/validators/date/not-before.js +66 -0
  145. package/dist/core/validators/date/same-day.js +60 -0
  146. package/dist/core/validators/date/same-month.js +59 -0
  147. package/dist/core/validators/date/same-year.js +56 -0
  148. package/dist/core/validators/date/too-early.js +57 -0
  149. package/dist/core/validators/date/too-late.js +57 -0
  150. package/dist/core/validators/date/weekday.js +65 -0
  151. package/dist/core/validators/date/weekend.js +56 -0
  152. package/dist/core/validators/index.js +139 -0
  153. package/dist/core/validators/nullish/is-null-or-undefined.js +40 -0
  154. package/dist/core/validators/nullish/is-null.js +39 -0
  155. package/dist/core/validators/nullish/is-undefined.js +39 -0
  156. package/dist/core/validators/number/finite.js +40 -0
  157. package/dist/core/validators/number/integer.js +40 -0
  158. package/dist/core/validators/number/less-than.js +39 -0
  159. package/dist/core/validators/number/max.js +39 -0
  160. package/dist/core/validators/number/min.js +39 -0
  161. package/dist/core/validators/number/more-than.js +39 -0
  162. package/dist/core/validators/number/negative.js +38 -0
  163. package/dist/core/validators/number/non-negative.js +37 -0
  164. package/dist/core/validators/number/non-positive.js +37 -0
  165. package/dist/core/validators/number/number.js +31 -0
  166. package/dist/core/validators/number/positive.js +38 -0
  167. package/dist/core/validators/number/safe-integer.js +42 -0
  168. package/dist/core/validators/object/deep-equals.js +47 -0
  169. package/dist/core/validators/object/has-key.js +42 -0
  170. package/dist/core/validators/object/has-value.js +59 -0
  171. package/dist/core/validators/object/keys-equal.js +47 -0
  172. package/dist/core/validators/object/max-keys.js +43 -0
  173. package/dist/core/validators/object/min-keys.js +43 -0
  174. package/dist/core/validators/object/missing-key.js +42 -0
  175. package/dist/core/validators/object/no-empty-array-values.js +44 -0
  176. package/dist/core/validators/object/no-empty-object-values.js +44 -0
  177. package/dist/core/validators/object/no-null-values.js +44 -0
  178. package/dist/core/validators/object/no-undefined-values.js +44 -0
  179. package/dist/core/validators/object/non-empty-object.js +40 -0
  180. package/dist/core/validators/object/only-keys.js +43 -0
  181. package/dist/core/validators/object/plain-object.js +35 -0
  182. package/dist/core/validators/string/alpha-num.js +50 -0
  183. package/dist/core/validators/string/alpha.js +51 -0
  184. package/dist/core/validators/string/chars-equal.js +49 -0
  185. package/dist/core/validators/string/ends-with.js +50 -0
  186. package/dist/core/validators/string/is-ascii.js +53 -0
  187. package/dist/core/validators/string/is-printable.js +53 -0
  188. package/dist/core/validators/string/matches.js +50 -0
  189. package/dist/core/validators/string/max-length.js +50 -0
  190. package/dist/core/validators/string/min-length.js +50 -0
  191. package/dist/core/validators/string/no-lead-space.js +50 -0
  192. package/dist/core/validators/string/no-repeat-space.js +52 -0
  193. package/dist/core/validators/string/no-space.js +51 -0
  194. package/dist/core/validators/string/no-trail-space.js +50 -0
  195. package/dist/core/validators/string/non-empty.js +48 -0
  196. package/dist/core/validators/string/not-one-of.js +51 -0
  197. package/dist/core/validators/string/num-string.js +50 -0
  198. package/dist/core/validators/string/one-of.js +50 -0
  199. package/dist/core/validators/string/starts-with.js +50 -0
  200. package/dist/core/validators/string/string.js +39 -0
  201. package/dist/core/validators/string/trimmed.js +51 -0
  202. package/dist/index.js +26 -0
  203. package/dist/test.js +12 -0
  204. package/package.json +1 -1
@@ -0,0 +1,418 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Pipeline | Pipeline
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Orchestrates all pipeline stages into a unified execution
7
+ * lifecycle.
8
+ * @see https://jane-io.com
9
+ * ----------------------------------------------------------------------------
10
+ */
11
+ import { attachFluentParsers, attachFluentValidators, createEvent, defaultPolicy, policyDecision, resolvePolicy, } from '../common';
12
+ import { rootPath } from '../field-path';
13
+ import { deepClone, detectStructuralType, normalizationRunner, scanRunner, selectNormalizationRules, selectScanRules, validationRunner, } from '../pipeline';
14
+ import { janeRegistry } from '../fluent-registry';
15
+ import { diff, explain, replay, telemetry } from '../analysis';
16
+ import { parseRunner } from '.';
17
+ import { isFluentValidator, isValidationFactory } from './validate';
18
+ import { isFluentParser, isParseFactory } from '../pipeline';
19
+ import { normalizePolicy } from '../common/policy';
20
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
21
+ |* Pipeline Runner *|
22
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
23
+ /**
24
+ * Runs the configured pipeline through all enabled stages and assembles the
25
+ * complete result. Coordinates scan, normalize, parse, validate, analysis,
26
+ * and policy decision into a single execution flow.
27
+ */
28
+ export async function pipelineRunner(input) {
29
+ const startedAt = new Date();
30
+ const { raw, path: inputPath, policy, scanRules, parseRules, validationRules, inputName, userMessageOverride, telemetrySink, } = input;
31
+ const path = inputPath ?? rootPath();
32
+ const allEvents = [];
33
+ let scanEvents = [];
34
+ let normalizeEvents = [];
35
+ let parseEvents = [];
36
+ let validateEvents = [];
37
+ // ----------------------------------------
38
+ // 1. Scan (optional, fail-fast on fatal)
39
+ // ----------------------------------------
40
+ let safe = deepClone(raw);
41
+ if (policy.scan) {
42
+ const scanResult = await scanRunner({
43
+ raw,
44
+ path,
45
+ rules: scanRules,
46
+ policy,
47
+ });
48
+ safe = scanResult.safe;
49
+ scanEvents = scanResult.events;
50
+ allEvents.push(...scanResult.events);
51
+ const fatalEvent = scanResult.events.find((e) => e.kind === 'fatal');
52
+ if (fatalEvent) {
53
+ const finishedAt = new Date();
54
+ const metadata = {
55
+ runId: crypto.randomUUID(),
56
+ startedAt: startedAt.toISOString(),
57
+ finishedAt: finishedAt.toISOString(),
58
+ boundaryName: policy.boundaryName,
59
+ pipelineName: policy.pipelineName,
60
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
61
+ inputName,
62
+ analysis: {
63
+ diff: policy.analysis?.diff,
64
+ explain: policy.analysis?.explain,
65
+ replay: policy.analysis?.replay,
66
+ telemetry: policy.analysis?.telemetry,
67
+ },
68
+ };
69
+ const policyResult = policyDecision({
70
+ policy,
71
+ raw,
72
+ safe,
73
+ normalized: undefined,
74
+ final: undefined,
75
+ path,
76
+ rawType: detectStructuralType(raw),
77
+ safeType: detectStructuralType(safe),
78
+ normalizedType: detectStructuralType(undefined),
79
+ finalType: detectStructuralType(undefined),
80
+ events: allEvents,
81
+ issues: scanEvents,
82
+ scanEvents,
83
+ normalizeEvents: [],
84
+ parseEvents: [],
85
+ validateEvents: [],
86
+ diff: undefined,
87
+ explain: undefined,
88
+ replay: undefined,
89
+ }, metadata);
90
+ if (policy.analysis?.telemetry && telemetrySink) {
91
+ const tel = telemetry({
92
+ boundaryName: policy.boundaryName ?? 'unknown-boundary',
93
+ pipelineName: policy.pipelineName ?? 'unknown-pipeline',
94
+ result: policyResult,
95
+ });
96
+ telemetrySink(tel.records);
97
+ }
98
+ return policyResult;
99
+ }
100
+ }
101
+ else {
102
+ safe = deepClone(raw);
103
+ const event = createEvent('scan', 'info', 'scan.is.disabled', path, 'Scan stage was skipped because policy.scan is false.', undefined, { scan: false, mode: policy.mode });
104
+ scanEvents = [event];
105
+ allEvents.push(event);
106
+ }
107
+ // ----------------------------------------
108
+ // 2. Normalize (mode-aware)
109
+ // ----------------------------------------
110
+ let normalized = safe;
111
+ let finalValue = safe;
112
+ if (policy.mode !== 'strict') {
113
+ const normalizationRules = selectNormalizationRules(safe);
114
+ const normalizationResult = await normalizationRunner({
115
+ safe,
116
+ path,
117
+ rules: normalizationRules,
118
+ mode: policy.mode,
119
+ });
120
+ normalized = normalizationResult.normalized;
121
+ finalValue = normalizationResult.normalized;
122
+ normalizeEvents = normalizationResult.events;
123
+ allEvents.push(...normalizationResult.events);
124
+ }
125
+ // ----------------------------------------
126
+ // 3. Parse (explicit rules only)
127
+ // ----------------------------------------
128
+ if (parseRules.length > 0) {
129
+ if (policy.mode === 'strict') {
130
+ const warn = createEvent('parse', 'warn', 'policy.is.strict', path, 'Parse stage was skipped in strict mode.', undefined, { mode: policy.mode, rules: parseRules.length });
131
+ parseEvents = [warn];
132
+ allEvents.push(warn);
133
+ }
134
+ else {
135
+ const parseResult = await parseRunner({
136
+ value: normalized,
137
+ path,
138
+ rules: parseRules,
139
+ });
140
+ finalValue = parseResult.parsed;
141
+ parseEvents = parseResult.events;
142
+ allEvents.push(...parseResult.events);
143
+ }
144
+ }
145
+ // ----------------------------------------
146
+ // 4. Diff (lazy)
147
+ // ----------------------------------------
148
+ const diffResult = policy.analysis?.diff
149
+ ? await diff(safe, normalized)
150
+ : undefined;
151
+ // ----------------------------------------
152
+ // 5. Validate
153
+ // ----------------------------------------
154
+ const validateResult = await validationRunner({
155
+ value: finalValue,
156
+ path,
157
+ rules: validationRules,
158
+ });
159
+ validateEvents = validateResult.events;
160
+ if (userMessageOverride) {
161
+ validateEvents = validateEvents.map((event) => ({
162
+ ...event,
163
+ userMessage: userMessageOverride,
164
+ }));
165
+ }
166
+ allEvents.push(...validateEvents);
167
+ if (inputName) {
168
+ for (let i = 0; i < allEvents.length; i++) {
169
+ allEvents[i] = {
170
+ ...allEvents[i],
171
+ inputName,
172
+ };
173
+ }
174
+ }
175
+ // ----------------------------------------
176
+ // 6. Explain (lazy)
177
+ // ----------------------------------------
178
+ const explanation = policy.analysis?.explain
179
+ ? explain({
180
+ scanEvents,
181
+ normalizationEvents: normalizeEvents,
182
+ parseEvents,
183
+ validationEvents: validateEvents,
184
+ diff: diffResult,
185
+ })
186
+ : undefined;
187
+ // ----------------------------------------
188
+ // 7. Replay (lazy)
189
+ // ----------------------------------------
190
+ const replayResult = policy.analysis?.replay
191
+ ? replay({
192
+ before: safe,
193
+ diff: diffResult?.entries ?? [],
194
+ })
195
+ : undefined;
196
+ // ----------------------------------------
197
+ // 8. Decide (policy reducer)
198
+ // ----------------------------------------
199
+ const finishedAt = new Date();
200
+ const metadata = {
201
+ runId: crypto.randomUUID(),
202
+ startedAt: startedAt.toISOString(),
203
+ finishedAt: finishedAt.toISOString(),
204
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
205
+ inputName,
206
+ };
207
+ const policyResult = policyDecision({
208
+ policy,
209
+ raw,
210
+ safe,
211
+ normalized,
212
+ final: finalValue,
213
+ path,
214
+ rawType: detectStructuralType(raw),
215
+ safeType: detectStructuralType(safe),
216
+ normalizedType: detectStructuralType(normalized),
217
+ finalType: detectStructuralType(finalValue),
218
+ events: allEvents,
219
+ issues: [],
220
+ scanEvents,
221
+ normalizeEvents,
222
+ parseEvents,
223
+ validateEvents,
224
+ diff: diffResult,
225
+ explain: explanation,
226
+ replay: replayResult,
227
+ }, metadata);
228
+ const pipelineResult = {
229
+ decision: policyResult.decision,
230
+ policy,
231
+ raw,
232
+ safe,
233
+ normalized,
234
+ final: finalValue,
235
+ path,
236
+ rawType: detectStructuralType(raw),
237
+ safeType: detectStructuralType(safe),
238
+ normalizedType: detectStructuralType(normalized),
239
+ finalType: detectStructuralType(finalValue),
240
+ events: allEvents,
241
+ issues: policyResult.issues,
242
+ scanEvents,
243
+ normalizeEvents,
244
+ parseEvents,
245
+ validateEvents,
246
+ diff: diffResult,
247
+ explain: explanation,
248
+ replay: replayResult,
249
+ metadata,
250
+ };
251
+ // ----------------------------------------
252
+ // 9. Telemetry (after result is complete)
253
+ // ----------------------------------------
254
+ if (policy.analysis?.telemetry && telemetrySink) {
255
+ const tel = telemetry({
256
+ boundaryName: policy.boundaryName ?? 'unknown-boundary',
257
+ pipelineName: policy.pipelineName ?? 'unknown-pipeline',
258
+ result: pipelineResult,
259
+ });
260
+ telemetrySink(tel.records);
261
+ }
262
+ return pipelineResult;
263
+ }
264
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
265
+ |* Create Pipeline *|
266
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
267
+ /**
268
+ * Creates a new pipeline builder rooted at the given raw value and path.
269
+ * Establishes the initial rule sets, policy state, and override surface,
270
+ * returning a fluent interface for assembling and running a complete pipeline.
271
+ */
272
+ export function createPipeline(raw, path = rootPath(), scanRules = [], parseRules = [], validationRules = [], policy, overrides = {}) {
273
+ const base = {
274
+ raw,
275
+ path,
276
+ scanRules,
277
+ parseRules,
278
+ validationRules,
279
+ policy,
280
+ overrides,
281
+ async run() {
282
+ const effectivePolicy = resolvePolicy(defaultPolicy, this.policy, this.overrides);
283
+ return pipelineRunner({
284
+ raw: this.raw,
285
+ path: this.path,
286
+ policy: effectivePolicy,
287
+ scanRules: [...this.scanRules],
288
+ parseRules: [...this.parseRules],
289
+ validationRules: [...this.validationRules],
290
+ inputName: this.overrides.inputName,
291
+ userMessageOverride: this.overrides.userMessageOverride,
292
+ telemetrySink: this.overrides.telemetrySink,
293
+ });
294
+ },
295
+ };
296
+ const core = {
297
+ ...base,
298
+ scan() {
299
+ return createPipeline(this.raw, this.path, selectScanRules(this.raw), this.parseRules, this.validationRules, this.policy, { ...this.overrides, scan: true });
300
+ },
301
+ parse(ruleOrName, ...args) {
302
+ let rule;
303
+ if (typeof ruleOrName === 'string') {
304
+ const entry = janeRegistry.parsers[ruleOrName];
305
+ if (isParseFactory(entry)) {
306
+ // entry is the factory — NOT ruleOrName
307
+ const factory = entry;
308
+ rule = factory(...args);
309
+ }
310
+ else if (isFluentParser(entry)) {
311
+ rule = entry;
312
+ }
313
+ else {
314
+ throw new Error('Invalid parser entry');
315
+ }
316
+ }
317
+ else {
318
+ rule = ruleOrName;
319
+ }
320
+ return createPipeline(this.raw, this.path, this.scanRules, [...this.parseRules, rule], this.validationRules, this.policy, this.overrides);
321
+ },
322
+ validate(ruleOrName, ...args) {
323
+ let rule;
324
+ if (typeof ruleOrName === 'string') {
325
+ const entry = janeRegistry.validators[ruleOrName];
326
+ if (isValidationFactory(entry)) {
327
+ const factory = entry;
328
+ rule = factory(...args);
329
+ }
330
+ else if (isFluentValidator(entry)) {
331
+ rule = entry;
332
+ }
333
+ else {
334
+ throw new Error('Invalid validator entry');
335
+ }
336
+ }
337
+ else {
338
+ rule = ruleOrName;
339
+ }
340
+ return createPipeline(this.raw, this.path, this.scanRules, this.parseRules, [...this.validationRules, rule], this.policy, this.overrides);
341
+ },
342
+ named(name) {
343
+ return createPipeline(this.raw, this.path, this.scanRules, this.parseRules, this.validationRules, this.policy, {
344
+ ...this.overrides,
345
+ inputName: name,
346
+ });
347
+ },
348
+ userMessage(message) {
349
+ return createPipeline(this.raw, this.path, this.scanRules, this.parseRules, this.validationRules, this.policy, {
350
+ ...this.overrides,
351
+ userMessageOverride: message,
352
+ });
353
+ },
354
+ strict() {
355
+ return createPipeline(this.raw, this.path, this.scanRules, this.parseRules, this.validationRules, this.policy, { ...this.overrides, mode: 'strict' });
356
+ },
357
+ moderate() {
358
+ return createPipeline(this.raw, this.path, this.scanRules, this.parseRules, this.validationRules, this.policy, { ...this.overrides, mode: 'moderate' });
359
+ },
360
+ lax() {
361
+ return createPipeline(this.raw, this.path, this.scanRules, this.parseRules, this.validationRules, this.policy, { ...this.overrides, mode: 'lax' });
362
+ },
363
+ withDiff() {
364
+ return createPipeline(this.raw, this.path, this.scanRules, this.parseRules, this.validationRules, this.policy, {
365
+ ...this.overrides,
366
+ analysis: {
367
+ ...(this.overrides.analysis ?? {}),
368
+ diff: true,
369
+ },
370
+ });
371
+ },
372
+ withExplain() {
373
+ return createPipeline(this.raw, this.path, this.scanRules, this.parseRules, this.validationRules, this.policy, {
374
+ ...this.overrides,
375
+ analysis: {
376
+ ...(this.overrides.analysis ?? {}),
377
+ explain: true,
378
+ },
379
+ });
380
+ },
381
+ withReplay() {
382
+ return createPipeline(this.raw, this.path, this.scanRules, this.parseRules, this.validationRules, this.policy, {
383
+ ...this.overrides,
384
+ analysis: {
385
+ ...(this.overrides.analysis ?? {}),
386
+ replay: true,
387
+ },
388
+ });
389
+ },
390
+ withTelemetry(sink) {
391
+ return createPipeline(this.raw, this.path, this.scanRules, this.parseRules, this.validationRules, this.policy, {
392
+ ...this.overrides,
393
+ telemetrySink: sink,
394
+ analysis: {
395
+ ...(this.overrides.analysis ?? {}),
396
+ telemetry: true,
397
+ },
398
+ });
399
+ },
400
+ withPolicy(input) {
401
+ const merged = normalizePolicy(this.policy ?? defaultPolicy, input);
402
+ return createPipeline(this.raw, this.path, this.scanRules, this.parseRules, this.validationRules, merged, this.overrides);
403
+ },
404
+ allowBigint() {
405
+ return createPipeline(this.raw, this.path, this.scanRules, this.parseRules, this.validationRules, this.policy, {
406
+ ...this.overrides,
407
+ exception: {
408
+ ...this.overrides.exception,
409
+ bigint: true,
410
+ },
411
+ });
412
+ },
413
+ };
414
+ const builder = core;
415
+ attachFluentValidators(builder);
416
+ attachFluentParsers(builder);
417
+ return builder;
418
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Pipeline | Scan
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Inspects the contained value and emits structural
7
+ * observations for later stages.
8
+ * @see https://jane-io.com
9
+ * ----------------------------------------------------------------------------
10
+ */
11
+ import { createEvent } from '../common';
12
+ import { rootPath, walk } from '../field-path';
13
+ import { scanRuleRegistry } from '../scanners';
14
+ import { containValue, defaultContainmentOptions } from '.';
15
+ import { makeFieldPath } from '../field-path/construct';
16
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
17
+ |* Detect Structural Type *|
18
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
19
+ /**
20
+ * Classifies a raw value into one of Jane’s structural types.
21
+ *
22
+ * Used by the scanner to select appropriate rules without relying on
23
+ * user‑defined schemas or runtime shape assumptions.
24
+ */
25
+ export function detectStructuralType(input) {
26
+ if (typeof input === 'string')
27
+ return 'string';
28
+ if (typeof input === 'number')
29
+ return 'number';
30
+ if (typeof input === 'boolean')
31
+ return 'boolean';
32
+ if (typeof input === 'bigint')
33
+ return 'bigint';
34
+ if (input === null)
35
+ return 'unknown';
36
+ if (Array.isArray(input))
37
+ return 'array';
38
+ if (input instanceof Date)
39
+ return 'date';
40
+ if (typeof input === 'object')
41
+ return 'object';
42
+ return 'unknown';
43
+ }
44
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
45
+ |* Measure Depth *|
46
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
47
+ /**
48
+ * Computes the maximum nesting depth of a JSON‑like structure.
49
+ *
50
+ * Traverses objects recursively and returns the deepest level encountered,
51
+ * ignoring non‑object values and null.
52
+ */
53
+ export function measureDepth(value, current = 0) {
54
+ if (typeof value !== 'object' || value === null)
55
+ return current;
56
+ let max = current;
57
+ for (const key of Object.keys(value)) {
58
+ const child = value[key];
59
+ const depth = measureDepth(child, current + 1);
60
+ if (depth > max)
61
+ max = depth;
62
+ }
63
+ return max;
64
+ }
65
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
66
+ |* Scan Event *|
67
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
68
+ /**
69
+ * Creates a scan‑stage event.
70
+ *
71
+ * Wraps createEvent with the 'scan' stage tag, producing structured
72
+ * observations emitted during the scan pipeline phase.
73
+ */
74
+ export const scanEvent = (kind, code, path, message, userMessage, meta) => createEvent('scan', kind, code, path, message, userMessage, meta);
75
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
76
+ |* Scan Runner *|
77
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
78
+ /**
79
+ * Executes the scan stage for a single value.
80
+ *
81
+ * Produces a safe contained value, applies all scan rules, collects
82
+ * emitted events, and performs a structural walk to ensure full coverage.
83
+ */
84
+ export const scanRunner = async (input) => {
85
+ const { raw, path: inputPath, rules, policy } = input;
86
+ const path = inputPath ? makeFieldPath([...inputPath.segments]) : rootPath();
87
+ const events = [];
88
+ const safe = containValue(raw, path, defaultContainmentOptions, policy);
89
+ for (const rule of rules) {
90
+ try {
91
+ const emitted = await rule(raw, path);
92
+ if (emitted && emitted.length > 0) {
93
+ events.push(...emitted);
94
+ }
95
+ }
96
+ catch (err) {
97
+ events.push(createEvent('scan', 'fatal', 'error.while.scanning', path, String(err)));
98
+ }
99
+ }
100
+ await walk(raw, path, async () => { });
101
+ return { events, safe: safe };
102
+ };
103
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
104
+ |* Select Scan Rules *|
105
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
106
+ /**
107
+ * Selects scan rules based on the structural type of the raw value.
108
+ *
109
+ * Returns the union of universal rules and the rules specific to the
110
+ * detected type, ensuring consistent and predictable scan behavior.
111
+ */
112
+ export function selectScanRules(raw) {
113
+ const type = detectStructuralType(raw);
114
+ return [...scanRuleRegistry.all, ...scanRuleRegistry[type]];
115
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Pipeline | Validate
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Evaluates the normalized value and emits rule‑driven
7
+ * correctness events.
8
+ * @see https://jane-io.com
9
+ * ----------------------------------------------------------------------------
10
+ */
11
+ import { createEvent } from '../common';
12
+ import { rootPath } from '../field-path';
13
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
14
+ |* Is Validation Factory *|
15
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
16
+ /**
17
+ * Determines whether a fluent validator entry is a factory.
18
+ *
19
+ * Factories accept arbitrary parameters and return a ValidationRule,
20
+ * distinguishing them from fixed‑signature fluent validators.
21
+ */
22
+ export function isValidationFactory(fn) {
23
+ // A factory returns a ValidationRule when called with ANY args
24
+ return typeof fn === 'function' && fn.length !== 2;
25
+ }
26
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
27
+ |* Is Fluent Validator *|
28
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
29
+ /**
30
+ * Determines whether a fluent validator entry is a direct validation rule.
31
+ *
32
+ * Fluent validators always expose a two‑argument signature—value and path—
33
+ * making them distinguishable from parameterized factories.
34
+ */
35
+ export function isFluentValidator(fn) {
36
+ return typeof fn === 'function' && fn.length === 2;
37
+ }
38
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
39
+ |* Validation Event *|
40
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
41
+ /**
42
+ * Creates a validation‑stage event.
43
+ *
44
+ * Wraps createEvent with the 'validate' phase tag, producing correctness‑
45
+ * focused messages emitted during rule evaluation.
46
+ */
47
+ export const validationEvent = (kind, code, path, message, userMessage, meta) => createEvent('validate', kind, code, path, message, userMessage, meta);
48
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
49
+ |* Validation Runner *|
50
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
51
+ /**
52
+ * Executes the validation stage.
53
+ *
54
+ * Applies each validation rule to the value, collects all emitted events,
55
+ * and records fatal errors when rules throw. Produces the complete set of
56
+ * correctness observations for the stage.
57
+ */
58
+ export const validationRunner = async (input) => {
59
+ const { value, path: inputPath, rules } = input;
60
+ const path = inputPath ?? rootPath();
61
+ const events = [];
62
+ for (const rule of rules) {
63
+ try {
64
+ const emitted = await rule(value, path);
65
+ if (emitted && emitted.length > 0) {
66
+ events.push(...emitted);
67
+ }
68
+ }
69
+ catch (err) {
70
+ events.push(createEvent('validate', 'fatal', 'error.while.validating', path, String(err)));
71
+ }
72
+ }
73
+ return { events };
74
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Scanners | Scan For Sentinels
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Detects sentinel markers in raw input and emits structural‑
7
+ * hazard events.
8
+ * @see https://jane-io.com
9
+ * ----------------------------------------------------------------------------
10
+ */
11
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
12
+ |* Internal Modules *|
13
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
14
+ import { isSentinel, sentinelEvent } from '../../../core/pipeline';
15
+ import { scanEvent } from '../../pipeline';
16
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
17
+ |* Implementation *|
18
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
19
+ /**
20
+ * Detects containment sentinels produced during safe‑clone operations.
21
+ *
22
+ * Flags any sentinel string as a fatal structural hazard, emitting a
23
+ * scan event that identifies the hazard code and the original sentinel
24
+ * value for downstream analysis and policy decisions.
25
+ */
26
+ export const scanForSentinels = (raw, path) => {
27
+ if (typeof raw !== 'string')
28
+ return [];
29
+ if (!isSentinel(raw))
30
+ return [];
31
+ const meta = sentinelEvent(raw);
32
+ return [
33
+ scanEvent('fatal', 'value.was.contained', path, `Structural hazard detected: ${meta.code}.`, 'A structural hazard was detected in the input.', { sentinel: raw, hazard: meta.code }),
34
+ ];
35
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Scanners | Array Is Deep
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Detects arrays nested beyond a safe structural depth and
7
+ * emits a warning event.
8
+ * @see https://jane-io.com
9
+ * ----------------------------------------------------------------------------
10
+ */
11
+ import { scanEvent } from '../../pipeline';
12
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
13
+ |* Implementation *|
14
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
15
+ /**
16
+ * Warns when an array is nested beyond a safe structural depth.
17
+ *
18
+ * Walks down the first element chain to estimate nesting. If the depth
19
+ * exceeds the conservative threshold, emits a warning describing the
20
+ * observed and allowed depth.
21
+ */
22
+ export const arrayIsDeep = (raw, path) => {
23
+ if (!Array.isArray(raw))
24
+ return [];
25
+ const maxDepth = 20;
26
+ let depth = 0;
27
+ let current = raw;
28
+ while (Array.isArray(current)) {
29
+ depth++;
30
+ if (depth > maxDepth) {
31
+ return [
32
+ scanEvent('warn', 'array.is.deep', path, `Array nesting depth exceeds safe threshold ${maxDepth}.`, 'This list is nested too deeply.', { depth, maxDepth }),
33
+ ];
34
+ }
35
+ current = current[0];
36
+ }
37
+ return [];
38
+ };