@clementine-solutions/jane-io 1.0.1 → 1.0.3

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/package.json +28 -3
  204. package/dist/test.d.ts +0 -1
@@ -0,0 +1,88 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Analysis | Diff
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Structural diff utilities used to compare safe and
7
+ * normalized values.
8
+ * @see https://jane-io.com
9
+ * ----------------------------------------------------------------------------
10
+ */
11
+ import { isObject, isPlainObject } from '../common';
12
+ import { appendSegment, rootPath, setIndex, setKey } from '../field-path';
13
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
14
+ |* Diff *|
15
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
16
+ /**
17
+ * Computes a structural diff between two JSON‑compatible values.
18
+ *
19
+ * Delegates all traversal to `walkDiff`, collecting every change into a flat
20
+ * list of diff entries rooted at the top‑level path. Always returns a stable
21
+ * `{ entries }` object with no filtering or interpretation.
22
+ */
23
+ export const diff = (before, after) => {
24
+ const entries = [];
25
+ walkDiff(before, after, rootPath(), entries);
26
+ return { entries };
27
+ };
28
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
29
+ |* Walk Diff *|
30
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
31
+ /**
32
+ * Recursively walks two JSON‑compatible values and records structural changes.
33
+ *
34
+ * Compares primitives, arrays, and plain objects by shape and content, emitting
35
+ * diff entries at each divergent path. Keys and indices are traversed in a
36
+ * stable order, and non‑object mismatches are treated as direct value changes.
37
+ * All entries are appended to `out`; no filtering or interpretation occurs here.
38
+ */
39
+ export function walkDiff(before, after, path, out) {
40
+ if (before === after)
41
+ return;
42
+ if (!isObject(before) || !isObject(after)) {
43
+ out.push({
44
+ path,
45
+ kind: before === undefined ? 'added' : after === undefined ? 'removed' : 'changed',
46
+ before,
47
+ after,
48
+ });
49
+ return;
50
+ }
51
+ if (Array.isArray(before) && Array.isArray(after)) {
52
+ const max = Math.max(before.length, after.length);
53
+ for (let i = 0; i < max; i++) {
54
+ const childPath = appendSegment(path, setIndex(i));
55
+ walkDiff(before[i], after[i], childPath, out);
56
+ }
57
+ return;
58
+ }
59
+ if (isPlainObject(before) && isPlainObject(after)) {
60
+ const beforeKeys = Object.keys(before);
61
+ const afterKeys = Object.keys(after);
62
+ const allKeys = new Set([...beforeKeys, ...afterKeys]);
63
+ for (const key of allKeys) {
64
+ const childPath = appendSegment(path, setKey(key));
65
+ walkDiff(before[key], after[key], childPath, out);
66
+ }
67
+ return;
68
+ }
69
+ out.push({
70
+ path,
71
+ kind: 'changed',
72
+ before,
73
+ after,
74
+ });
75
+ }
76
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
77
+ |* Run Diff *|
78
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
79
+ /**
80
+ * Runs the pipeline and returns its computed diff.
81
+ *
82
+ * Delegates execution to the provided builder and exposes only the diff
83
+ * portion of the result. No additional processing or filtering occurs here.
84
+ */
85
+ export async function runDiff(builder) {
86
+ const result = await builder.run();
87
+ return result.diff;
88
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Analysis | Explain
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Builds a human‑readable narrative of all pipeline events in
7
+ * order.
8
+ * @see https://jane-io.com
9
+ * ----------------------------------------------------------------------------
10
+ */
11
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
12
+ |* Explain *|
13
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
14
+ /**
15
+ * Builds a linear explanation of all pipeline events.
16
+ *
17
+ * Emits scan hazards, normalization events, diff entries, parse changes, and
18
+ * validation errors in pipeline order. Each event is mapped to a stable
19
+ * ExplainStep with its path, stage, kind, code, and message. No filtering or
20
+ * interpretation occurs; this is a direct narrative of what the pipeline saw.
21
+ */
22
+ export const explain = ({ scanEvents, normalizationEvents, parseEvents, validationEvents, diff, }) => {
23
+ const steps = [];
24
+ for (const ev of scanEvents) {
25
+ steps.push({
26
+ path: ev.path,
27
+ stage: 'scan',
28
+ kind: 'hazard',
29
+ code: ev.code,
30
+ message: ev.message,
31
+ });
32
+ }
33
+ for (const ev of normalizationEvents) {
34
+ steps.push({
35
+ path: ev.path,
36
+ stage: 'normalize',
37
+ kind: ev.kind === 'fatal' ? 'hazard' : 'change',
38
+ code: ev.code,
39
+ message: ev.message,
40
+ });
41
+ }
42
+ if (diff) {
43
+ for (const entry of diff.entries) {
44
+ const message = entry.kind === 'added'
45
+ ? `Added value: ${format(entry.after)}`
46
+ : entry.kind === 'removed'
47
+ ? `Removed value: ${format(entry.before)}`
48
+ : `Changed from ${format(entry.before)} to ${format(entry.after)}`;
49
+ steps.push({
50
+ path: entry.path,
51
+ stage: 'normalize',
52
+ kind: 'change',
53
+ code: `normalize.${entry.kind}`,
54
+ message,
55
+ });
56
+ }
57
+ }
58
+ for (const ev of parseEvents) {
59
+ steps.push({
60
+ path: ev.path,
61
+ stage: 'parse',
62
+ kind: 'change',
63
+ code: ev.code,
64
+ message: ev.message,
65
+ });
66
+ }
67
+ for (const ev of validationEvents) {
68
+ steps.push({
69
+ path: ev.path,
70
+ stage: 'validate',
71
+ kind: 'error',
72
+ code: ev.code,
73
+ message: ev.message,
74
+ });
75
+ }
76
+ return { steps };
77
+ };
78
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
79
+ |* Format *|
80
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
81
+ /**
82
+ * Formats a value for human‑readable explain output.
83
+ *
84
+ * Produces stable string representations for primitives and falls back to
85
+ * compact JSON for objects. Never throws; errors during formatting degrade
86
+ * gracefully to `String(value)`.
87
+ */
88
+ export function format(value) {
89
+ try {
90
+ if (value === undefined)
91
+ return 'undefined';
92
+ if (value === null)
93
+ return 'null';
94
+ if (typeof value === 'string')
95
+ return JSON.stringify(value);
96
+ if (typeof value === 'number' || typeof value === 'boolean') {
97
+ return String(value);
98
+ }
99
+ return JSON.stringify(value, null, 0);
100
+ }
101
+ catch {
102
+ return String(value);
103
+ }
104
+ }
105
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
106
+ |* Run Explain *|
107
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
108
+ /**
109
+ * Runs the pipeline and returns its explain output.
110
+ *
111
+ * Delegates execution to the provided builder and exposes only the explain
112
+ * portion of the result. No additional processing or reshaping occurs here.
113
+ */
114
+ export async function runExplain(builder) {
115
+ const result = await builder.run();
116
+ return result.explain;
117
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Analysis | Barrel File
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Re‑exports the public Analysis modules (Diff, Explain,
7
+ * Replay, Telemetry).
8
+ * @see https://jane-io.com
9
+ * ----------------------------------------------------------------------------
10
+ */
11
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
12
+ |* Diff *|
13
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
14
+ export { diff, walkDiff, runDiff } from './diff';
15
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
16
+ |* Explain *|
17
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
18
+ export { explain, format, runExplain } from './explain';
19
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
20
+ |* Replay *|
21
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
22
+ export { applyEntry, replay, runReplay } from './replay';
23
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
24
+ |* Telemetry *|
25
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
26
+ export { runTelemetry, telemetry } from './telemetry';
@@ -0,0 +1,68 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Analysis | Replay
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Reconstructs intermediate states by applying diff entries
7
+ * in order.
8
+ * @see https://jane-io.com
9
+ * ----------------------------------------------------------------------------
10
+ */
11
+ import { setAtPath } from '../field-path';
12
+ import { deepClone } from '../pipeline';
13
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
14
+ |* Apply Entry *|
15
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
16
+ /**
17
+ * Applies a single diff entry to an internal JSON value.
18
+ *
19
+ * Clones the current state, then sets or clears the value at the entry’s path
20
+ * according to its kind. Used by Replay to advance state one change at a time.
21
+ * Never mutates the original state.
22
+ */
23
+ export function applyEntry(state, entry) {
24
+ const clone = deepClone(state);
25
+ switch (entry.kind) {
26
+ case 'added':
27
+ case 'changed':
28
+ return setAtPath(clone, entry.path, entry.after);
29
+ case 'removed':
30
+ return setAtPath(clone, entry.path, undefined);
31
+ }
32
+ }
33
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
34
+ |* Replay *|
35
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
36
+ /**
37
+ * Replays a diff step‑by‑step to reconstruct intermediate states.
38
+ *
39
+ * Clones the initial value, applies each diff entry in order, and records a
40
+ * snapshot after every application. Produces a stable sequence of ReplayStep
41
+ * objects with index, entry, and resulting state. Never mutates inputs.
42
+ */
43
+ export const replay = ({ before, diff }) => {
44
+ const steps = [];
45
+ let current = deepClone(before);
46
+ diff.forEach((entry, index) => {
47
+ current = applyEntry(current, entry);
48
+ steps.push({
49
+ index,
50
+ entry,
51
+ state: deepClone(current),
52
+ });
53
+ });
54
+ return { steps };
55
+ };
56
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
57
+ |* Run Replay *|
58
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
59
+ /**
60
+ * Runs the pipeline and returns its replay output.
61
+ *
62
+ * Delegates execution to the provided builder and exposes only the replay
63
+ * portion of the result. No additional processing or transformation occurs.
64
+ */
65
+ export async function runReplay(builder) {
66
+ const result = await builder.run();
67
+ return result.replay;
68
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Analysis | Telemetry
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Emits structured records for each pipeline stage for
7
+ * auditing and observability.
8
+ * @see https://jane-io.com
9
+ * ----------------------------------------------------------------------------
10
+ */
11
+ import { defaultPolicy } from '../common';
12
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
13
+ |* Telemetry *|
14
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
15
+ /**
16
+ * Collects flat telemetry records for each pipeline stage.
17
+ *
18
+ * Emits one record per stage with boundary, pipeline, timestamp, and the
19
+ * stage‑specific payload (events, diff, explain, replay). Stages are reported
20
+ * in pipeline order, and a final `decide` record captures the policy outcome.
21
+ * No aggregation, filtering, or interpretation occurs here.
22
+ */
23
+ export function telemetry(ctx) {
24
+ const { boundaryName, pipelineName, result } = ctx;
25
+ const timestamp = new Date().toISOString();
26
+ const records = [];
27
+ if (result.scanEvents?.length) {
28
+ records.push({
29
+ boundary: boundaryName,
30
+ pipeline: pipelineName,
31
+ timestamp,
32
+ stage: 'scan',
33
+ events: result.scanEvents,
34
+ });
35
+ }
36
+ if (result.normalizeEvents?.length) {
37
+ records.push({
38
+ boundary: boundaryName,
39
+ pipeline: pipelineName,
40
+ timestamp,
41
+ stage: 'normalize',
42
+ events: result.normalizeEvents,
43
+ });
44
+ }
45
+ if (result.parseEvents?.length) {
46
+ records.push({
47
+ boundary: boundaryName,
48
+ pipeline: pipelineName,
49
+ timestamp,
50
+ stage: 'parse',
51
+ events: result.parseEvents,
52
+ });
53
+ }
54
+ if (result.validateEvents?.length) {
55
+ records.push({
56
+ boundary: boundaryName,
57
+ pipeline: pipelineName,
58
+ timestamp,
59
+ stage: 'validate',
60
+ events: result.validateEvents,
61
+ });
62
+ }
63
+ if (result.diff) {
64
+ records.push({
65
+ boundary: boundaryName,
66
+ pipeline: pipelineName,
67
+ timestamp,
68
+ stage: 'diff',
69
+ diff: result.diff,
70
+ });
71
+ }
72
+ if (result.explain) {
73
+ records.push({
74
+ boundary: boundaryName,
75
+ pipeline: pipelineName,
76
+ timestamp,
77
+ stage: 'explain',
78
+ explain: result.explain,
79
+ });
80
+ }
81
+ if (result.replay) {
82
+ records.push({
83
+ boundary: boundaryName,
84
+ pipeline: pipelineName,
85
+ timestamp,
86
+ stage: 'replay',
87
+ replay: result.replay,
88
+ });
89
+ }
90
+ records.push({
91
+ boundary: boundaryName,
92
+ pipeline: pipelineName,
93
+ timestamp,
94
+ stage: 'decide',
95
+ decision: result.decision.code,
96
+ mode: result.policy.mode,
97
+ eventCount: result.events.length,
98
+ });
99
+ return { records };
100
+ }
101
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
102
+ |* Run With Telemetry *|
103
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
104
+ /**
105
+ * Runs the pipeline and emits telemetry if enabled by policy.
106
+ *
107
+ * Delegates execution to the builder, collects stage records through the
108
+ * telemetry helper, and forwards them to the provided sink. Returns the full
109
+ * pipeline result unchanged.
110
+ */
111
+ export async function runTelemetry(builder, sink) {
112
+ const result = await builder.run();
113
+ const policy = builder.policy ?? defaultPolicy;
114
+ if (policy.analysis?.telemetry) {
115
+ const tel = telemetry({
116
+ boundaryName: policy.boundaryName ?? 'unknown-boundary',
117
+ pipelineName: policy.pipelineName ?? 'unknown-pipeline',
118
+ result,
119
+ });
120
+ sink(tel.records);
121
+ }
122
+ return result;
123
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Boundary Rules | At Most One
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Ensures that no more than one of the specified fields is
7
+ * accepted. If multiple fields are present, a boundary error
8
+ * is emitted.
9
+ * @see https://jane-io.com
10
+ * ----------------------------------------------------------------------------
11
+ */
12
+ import { rootPath } from '../field-path';
13
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
14
+ |* Implementation *|
15
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
16
+ /**
17
+ * Ensures that at most one of the specified fields is accepted. If more than
18
+ * one field is present, a boundary‑level error is emitted.
19
+ */
20
+ export function atMostOne(...keys) {
21
+ return ({ fields }) => {
22
+ const events = [];
23
+ const issues = [];
24
+ const present = keys.filter((k) => fields[k]?.decision?.code === 'accept');
25
+ if (present.length > 1) {
26
+ const ev = {
27
+ phase: 'decide',
28
+ kind: 'error',
29
+ code: 'boundary.does.at-most-one',
30
+ message: `At most one of [${keys.join(', ')}] may be present`,
31
+ path: rootPath(),
32
+ };
33
+ events.push(ev);
34
+ issues.push(ev);
35
+ }
36
+ return { events, issues };
37
+ };
38
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Boundary Rules | Conditionally Required
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Requires a set of fields when a controlling field matches a
7
+ * specific value. Missing or rejected fields trigger
8
+ * boundary‑level errors.
9
+ * @see https://jane-io.com
10
+ * ----------------------------------------------------------------------------
11
+ */
12
+ import { rootPath } from '../field-path';
13
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
14
+ |* Implementation *|
15
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
16
+ /**
17
+ * Requires a set of fields when a controlling field matches a specific value.
18
+ * If the condition is met and any required field is missing or rejected, an
19
+ * error is emitted for each missing field.
20
+ */
21
+ export function conditionallyRequired(typeKey, typeValue, ...required) {
22
+ return ({ fields }) => {
23
+ const events = [];
24
+ const issues = [];
25
+ const type = fields[typeKey]?.final;
26
+ if (type === typeValue) {
27
+ for (const key of required) {
28
+ const r = fields[key];
29
+ if (!r || r.decision?.code !== 'accept') {
30
+ const ev = {
31
+ phase: 'decide',
32
+ kind: 'error',
33
+ code: 'boundary.does.require-field',
34
+ message: `Field "${key}" is required when "${typeKey}" = "${typeValue}"`,
35
+ path: rootPath(key),
36
+ };
37
+ events.push(ev);
38
+ issues.push(ev);
39
+ }
40
+ }
41
+ }
42
+ return { events, issues };
43
+ };
44
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Boundary Rules | Date Range
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Validates that a start and end date form a proper
7
+ * chronological range. Emits an error when the start occurs
8
+ * after the end.
9
+ * @see https://jane-io.com
10
+ * ----------------------------------------------------------------------------
11
+ */
12
+ import { rootPath } from '../field-path';
13
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
14
+ |* Implementation *|
15
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
16
+ /**
17
+ * Validates that a start/end date pair forms a proper chronological range.
18
+ * Emits an error when both fields are dates and the start occurs after the end.
19
+ */
20
+ export function dateRange(startKey, endKey) {
21
+ return ({ fields }) => {
22
+ const events = [];
23
+ const issues = [];
24
+ const start = fields[startKey]?.final;
25
+ const end = fields[endKey]?.final;
26
+ if (start instanceof Date && end instanceof Date && start > end) {
27
+ const ev = {
28
+ phase: 'decide',
29
+ kind: 'error',
30
+ code: 'boundary.does.require-date-range',
31
+ message: `"${startKey}" must be before "${endKey}"`,
32
+ path: rootPath(),
33
+ };
34
+ events.push(ev);
35
+ issues.push(ev);
36
+ }
37
+ return { events, issues };
38
+ };
39
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Boundary Rules | Barrel File
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Re‑exports shared boundary rules to provide a stable,
7
+ * minimal entry point for internal consumers. This file
8
+ * exposes no logic of its own.
9
+ * @see https://jane-io.com
10
+ * ----------------------------------------------------------------------------
11
+ */
12
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
13
+ |* Boundary Rules *|
14
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
15
+ export { atMostOne } from './at-most-one';
16
+ export { conditionallyRequired } from './conditionally-required';
17
+ export { dateRange } from './date-range';
18
+ export { mutuallyExclusive } from './mutually-exclusive';
19
+ export { noUnknownFields } from './no-unknown-fields';
20
+ export { requireAll } from './require-all';
21
+ export { requireOne } from './require-one';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Boundary Rules | Mutually Exclusive
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Enforces that two fields cannot both be accepted. If both
7
+ * appear, a mutual‑exclusion boundary error is produced.
8
+ * @see https://jane-io.com
9
+ * ----------------------------------------------------------------------------
10
+ */
11
+ import { rootPath } from '../field-path';
12
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
13
+ |* Implementation *|
14
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
15
+ /**
16
+ * Enforces mutual exclusion between two fields. If both fields are accepted,
17
+ * a boundary‑level error is emitted indicating the conflict.
18
+ */
19
+ export function mutuallyExclusive(a, b) {
20
+ return ({ fields }) => {
21
+ const events = [];
22
+ const issues = [];
23
+ const ra = fields[a];
24
+ const rb = fields[b];
25
+ if (ra?.decision?.code === 'accept' && rb?.decision?.code === 'accept') {
26
+ const ev = {
27
+ phase: 'decide',
28
+ kind: 'error',
29
+ code: 'boundary.does.mutual-exclusion',
30
+ message: `Fields "${a}" and "${b}" cannot both be present`,
31
+ path: rootPath(),
32
+ };
33
+ events.push(ev);
34
+ issues.push(ev);
35
+ }
36
+ return { events, issues };
37
+ };
38
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ----------------------------------------------------------------------------
3
+ * Boundary Rules | No Unknown Fields
4
+ * ----------------------------------------------------------------------------
5
+ * @package @clementine-solutions/jane
6
+ * @description Ensures that only explicitly allowed fields appear in the
7
+ * boundary input. Any unknown key results in a boundary error.
8
+ * @see https://jane-io.com
9
+ * ----------------------------------------------------------------------------
10
+ */
11
+ import { rootPath } from '../field-path';
12
+ /* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— *\
13
+ |* Implementation *|
14
+ \* ——— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ————— * ——— */
15
+ /**
16
+ * Ensures that only the explicitly allowed fields appear in the boundary
17
+ * input. Any field not listed is treated as an unknown key and produces a
18
+ * boundary‑level error.
19
+ */
20
+ export function noUnknownFields(...allowed) {
21
+ return ({ fields }) => {
22
+ const events = [];
23
+ const issues = [];
24
+ for (const key of Object.keys(fields)) {
25
+ if (!allowed.includes(key)) {
26
+ const ev = {
27
+ phase: 'decide',
28
+ kind: 'error',
29
+ code: 'boundary.cannot.allow-unknown',
30
+ message: `Unknown field "${key}"`,
31
+ path: rootPath(key),
32
+ };
33
+ events.push(ev);
34
+ issues.push(ev);
35
+ }
36
+ }
37
+ return { events, issues };
38
+ };
39
+ }