@jarenjs/core 0.83.3 → 0.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -130,7 +130,7 @@ Three rules, kept by every function so that no caller has to check them again:
130
130
  - **A malformed comparison scores 0 and never throws.** Mismatched lengths, an empty vector, a null or a non-finite component answer 0: one bad vector among ten thousand loses the comparison, it does not kill the sweep, and it never poisons a ranking with `NaN`.
131
131
  - **Refuse, never fix.** `packVector` and `l2Normalize` answer `null` for anything `isVector` refuses, the way a bounding box refuses a position it cannot bound; nothing truncates, pads or zero-fills a vector into the shape it was supposed to have.
132
132
 
133
- The packed form is `4·d` bytes of little-endian binary32 — the value a database column stores; components round to `Math.fround` and come back exactly. Unpacking aligned bytes on a little-endian host is a *view*, not a copy, which is what a sweep over ten thousand fetched rows is paid for by; misaligned bytes (a pooled `Buffer`, an odd offset into a record) and big-endian hosts take the copy path to the same values. The client that produces embeddings and a deterministic reference embedder for tests lives in [`@jarenjs/ai`](../ai/README.md#embeddings).
133
+ The packed form is `4·d` bytes of little-endian binary32 — the value a database column stores; components round to `Math.fround` and come back exactly. Unpacking aligned bytes on a little-endian host is a *view*, not a copy, which is what a sweep over ten thousand fetched rows is paid for by; misaligned bytes (a pooled `Buffer`, an odd offset into a record) and big-endian hosts take the copy path to the same values. Embedding services belong to the host; this package owns finite-vector validation, packing and similarity kernels.
134
134
 
135
135
  ## Intervals and series
136
136
 
@@ -303,6 +303,8 @@ Every subpath a consumer can import, derived from the manifest by
303
303
  | `@jarenjs/core/range` | JavaScript | declared |
304
304
  | `@jarenjs/core/retry` | JavaScript | declared |
305
305
  | `@jarenjs/core/schedule` | JavaScript | declared |
306
+ | `@jarenjs/core/check` | JavaScript | declared |
307
+ | `@jarenjs/core/guarded` | JavaScript | declared |
306
308
  <!--/fact-->
307
309
 
308
310
  ## Development
@@ -316,3 +318,13 @@ See [lexical search](docs/SEARCH.md) for the opt-in resident ranker and bounded
316
318
  [Bounded scheduling and retry](docs/SCHEDULING.md) describes `core/schedule` and
317
319
  `core/retry`: fair per-scope admission, drained shutdown, shared attempt budgets
318
320
  and explicit strict/AI/contract compatibility policies.
321
+
322
+ ## Checks and guarded editing
323
+
324
+ `@jarenjs/core/check` exports `checkOutcome` and `composeChecks`. Only `true`
325
+ or `{ valid: true }` passes. Composition stops at the first failure and
326
+ preserves its errors. `@jarenjs/core/guarded` exports `createGuardedRefiner`: it
327
+ copies JSON before validating/applying/planning, serializes its own commits,
328
+ and pairs optional snapshot/restore hooks. A failed commit attempts restoration
329
+ once and retains both original and restoration failures. External writers must
330
+ share the host's serialization policy.
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The shared reading of a compiled check's return value. Both the tool
3
+ * boundary and structured generation accept an injected `check`, and an
4
+ * injected one must answer strict true: the default
5
+ * `JarenValidator` check reports `{ valid, errors }`, a hand-written one
6
+ * may answer a bare boolean. Normalizing here keeps both call sites from
7
+ * having to know which kind they were handed.
8
+ */
9
+ /**
10
+ * @param {any} outcome - a compiled check's return value
11
+ * @returns {{ valid: boolean, errors: any[] }}
12
+ */
13
+ export declare function checkOutcome(outcome: any): {
14
+ valid: boolean;
15
+ errors: any[];
16
+ };
17
+ /**
18
+ * Compose several checks into one, run in order: the FIRST invalid
19
+ * outcome wins and its errors are returned; a value that passes every
20
+ * check is valid with no errors. This is how "the reply validates
21
+ * against the schema AND compiles as a program" becomes a single
22
+ * injected `validator` — the schema check first (cheap, structural),
23
+ * the engine's compile gate second (semantic). It is deliberately
24
+ * engine-agnostic: a Jaren query, JSLT, app or flow compile gate all
25
+ * compose the same way, because each reports the same `{ valid, errors }`
26
+ * shape through {@link checkOutcome} and each error carries a `code`
27
+ * and a `docPath` the repair loop can act on.
28
+ *
29
+ * A compile gate is the two-line adapter `(doc) => { try { compile(doc);
30
+ * return true; } catch (e) { return { valid: false, errors: [{ code:
31
+ * e.code, docPath: e.docPath, message: e.reason ?? e.message }] }; } }`
32
+ * — `reason` is the bare text of a coded error; falling back to
33
+ * `message` keeps the adapter total over non-coded throws.
34
+ *
35
+ * @param {...(value: any) => any} checks - each returns a boolean or a
36
+ * `{ valid, errors }` outcome (mixed freely)
37
+ * @returns {(value: any) => { valid: boolean, errors: any[] }}
38
+ */
39
+ export declare function composeChecks(...checks: ((value: any) => any)[]): (value: any) => {
40
+ valid: boolean;
41
+ errors: any[];
42
+ };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Guard edits to any JSON document using injected, synchronous validation and
3
+ * planning. Neither apply nor a validator receives the reader's original object.
4
+ * Commit owns atomic persistence (or snapshot/restore on a single writer).
5
+ * A failed restore is attempted once and retains both diagnostic causes.
6
+ * @param {{ read: () => Promise<any>, validateProposal: (proposal: any) => any,
7
+ * apply: (document: any, proposal: any) => any,
8
+ * validateCandidate: (next: any, previous: any) => any,
9
+ * planCommit: (next: any, previous: any) => any,
10
+ * commit: (plan: any, context: { previous: any, next: any, snapshot: any }) => Promise<any>,
11
+ * snapshot?: () => Promise<any>, restore?: (token: any) => Promise<any>,
12
+ * applyFailure?: (error: any) => any }} options
13
+ */
14
+ export declare function createGuardedRefiner(options: {
15
+ read: () => Promise<any>;
16
+ validateProposal: (proposal: any) => any;
17
+ apply: (document: any, proposal: any) => any;
18
+ validateCandidate: (next: any, previous: any) => any;
19
+ planCommit: (next: any, previous: any) => any;
20
+ commit: (plan: any, context: {
21
+ previous: any;
22
+ next: any;
23
+ snapshot: any;
24
+ }) => Promise<any>;
25
+ snapshot?: () => Promise<any>;
26
+ restore?: (token: any) => Promise<any>;
27
+ applyFailure?: (error: any) => any;
28
+ }): {
29
+ prepare: (document: any, proposal: any) => {
30
+ valid: boolean;
31
+ errors: any;
32
+ next?: undefined;
33
+ plan?: undefined;
34
+ } | {
35
+ valid: boolean;
36
+ errors: never[];
37
+ next: any;
38
+ plan: any;
39
+ };
40
+ commitPrepared: (previous: any, prepared: any) => Promise<{
41
+ ok: boolean;
42
+ stage: string;
43
+ errors: any;
44
+ cause?: undefined;
45
+ snapshot?: undefined;
46
+ value?: undefined;
47
+ } | {
48
+ ok: boolean;
49
+ stage: string;
50
+ cause: unknown;
51
+ errors: {
52
+ code: any;
53
+ docPath: any;
54
+ message: any;
55
+ stage: any;
56
+ }[];
57
+ snapshot?: undefined;
58
+ value?: undefined;
59
+ } | {
60
+ stage?: undefined;
61
+ cause?: undefined;
62
+ errors?: undefined;
63
+ ok: boolean;
64
+ value: any;
65
+ snapshot: any;
66
+ } | {
67
+ ok: boolean;
68
+ stage: string;
69
+ cause: unknown;
70
+ snapshot: any;
71
+ restoreError?: {} | null | undefined;
72
+ errors: {
73
+ code: any;
74
+ docPath: any;
75
+ message: any;
76
+ stage: any;
77
+ }[];
78
+ value?: undefined;
79
+ }>;
80
+ commit: (proposal: any) => Promise<{
81
+ ok: boolean;
82
+ stage: string;
83
+ errors: any;
84
+ cause?: undefined;
85
+ snapshot?: undefined;
86
+ value?: undefined;
87
+ } | {
88
+ ok: boolean;
89
+ stage: string;
90
+ cause: unknown;
91
+ errors: {
92
+ code: any;
93
+ docPath: any;
94
+ message: any;
95
+ stage: any;
96
+ }[];
97
+ snapshot?: undefined;
98
+ value?: undefined;
99
+ } | {
100
+ stage?: undefined;
101
+ cause?: undefined;
102
+ errors?: undefined;
103
+ ok: boolean;
104
+ value: any;
105
+ snapshot: any;
106
+ } | {
107
+ ok: boolean;
108
+ stage: string;
109
+ cause: unknown;
110
+ snapshot: any;
111
+ restoreError?: {} | null | undefined;
112
+ errors: {
113
+ code: any;
114
+ docPath: any;
115
+ message: any;
116
+ stage: any;
117
+ }[];
118
+ value?: undefined;
119
+ }>;
120
+ };
@@ -117,3 +117,5 @@ export declare function getArrayClass(obj: any, def?: undefined): any;
117
117
  * @returns {Array} An array containing the inclusive and exclusive bounds.
118
118
  */
119
119
  export declare function getInclusiveExclusiveBounds(getType: Function, inclusive: number | string | undefined, exclusive: number | string | boolean | undefined): any[];
120
+ export { checkOutcome, composeChecks } from './check.js';
121
+ export { createGuardedRefiner } from './guarded.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jarenjs/core",
3
3
  "private": false,
4
- "version": "0.83.3",
4
+ "version": "0.85.0",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./dist/types/index.d.ts",
@@ -192,6 +192,14 @@
192
192
  "./schedule": {
193
193
  "types": "./dist/types/schedule.d.ts",
194
194
  "default": "./src/schedule.js"
195
+ },
196
+ "./check": {
197
+ "types": "./dist/types/check.d.ts",
198
+ "default": "./src/check.js"
199
+ },
200
+ "./guarded": {
201
+ "types": "./dist/types/guarded.d.ts",
202
+ "default": "./src/guarded.js"
195
203
  }
196
204
  },
197
205
  "scripts": {
package/src/check.js ADDED
@@ -0,0 +1,51 @@
1
+ //@ts-check
2
+ /**
3
+ * The shared reading of a compiled check's return value. Both the tool
4
+ * boundary and structured generation accept an injected `check`, and an
5
+ * injected one must answer strict true: the default
6
+ * `JarenValidator` check reports `{ valid, errors }`, a hand-written one
7
+ * may answer a bare boolean. Normalizing here keeps both call sites from
8
+ * having to know which kind they were handed.
9
+ */
10
+
11
+ /**
12
+ * @param {any} outcome - a compiled check's return value
13
+ * @returns {{ valid: boolean, errors: any[] }}
14
+ */
15
+ export function checkOutcome(outcome) {
16
+ return typeof outcome === 'object' && outcome !== null
17
+ ? { valid: outcome.valid === true, errors: outcome.errors ?? [] }
18
+ : { valid: outcome === true, errors: [] };
19
+ }
20
+
21
+ /**
22
+ * Compose several checks into one, run in order: the FIRST invalid
23
+ * outcome wins and its errors are returned; a value that passes every
24
+ * check is valid with no errors. This is how "the reply validates
25
+ * against the schema AND compiles as a program" becomes a single
26
+ * injected `validator` — the schema check first (cheap, structural),
27
+ * the engine's compile gate second (semantic). It is deliberately
28
+ * engine-agnostic: a Jaren query, JSLT, app or flow compile gate all
29
+ * compose the same way, because each reports the same `{ valid, errors }`
30
+ * shape through {@link checkOutcome} and each error carries a `code`
31
+ * and a `docPath` the repair loop can act on.
32
+ *
33
+ * A compile gate is the two-line adapter `(doc) => { try { compile(doc);
34
+ * return true; } catch (e) { return { valid: false, errors: [{ code:
35
+ * e.code, docPath: e.docPath, message: e.reason ?? e.message }] }; } }`
36
+ * — `reason` is the bare text of a coded error; falling back to
37
+ * `message` keeps the adapter total over non-coded throws.
38
+ *
39
+ * @param {...(value: any) => any} checks - each returns a boolean or a
40
+ * `{ valid, errors }` outcome (mixed freely)
41
+ * @returns {(value: any) => { valid: boolean, errors: any[] }}
42
+ */
43
+ export function composeChecks(...checks) {
44
+ return (value) => {
45
+ for (const check of checks) {
46
+ const outcome = checkOutcome(check(value));
47
+ if (!outcome.valid) return outcome;
48
+ }
49
+ return { valid: true, errors: [] };
50
+ };
51
+ }
package/src/guarded.js ADDED
@@ -0,0 +1,90 @@
1
+ //@ts-check
2
+ import { checkOutcome } from './check.js';
3
+
4
+ const copy = (value) => value === undefined ? undefined : JSON.parse(JSON.stringify(value));
5
+ const failure = (error, stage) => ({ code: error?.code ?? 'GUARDED',
6
+ docPath: error?.docPath ?? '', message: error?.message ?? String(error), stage });
7
+
8
+ /**
9
+ * Guard edits to any JSON document using injected, synchronous validation and
10
+ * planning. Neither apply nor a validator receives the reader's original object.
11
+ * Commit owns atomic persistence (or snapshot/restore on a single writer).
12
+ * A failed restore is attempted once and retains both diagnostic causes.
13
+ * @param {{ read: () => Promise<any>, validateProposal: (proposal: any) => any,
14
+ * apply: (document: any, proposal: any) => any,
15
+ * validateCandidate: (next: any, previous: any) => any,
16
+ * planCommit: (next: any, previous: any) => any,
17
+ * commit: (plan: any, context: { previous: any, next: any, snapshot: any }) => Promise<any>,
18
+ * snapshot?: () => Promise<any>, restore?: (token: any) => Promise<any>,
19
+ * applyFailure?: (error: any) => any }} options
20
+ */
21
+ export function createGuardedRefiner(options) {
22
+ for (const name of ['read', 'validateProposal', 'apply', 'validateCandidate', 'planCommit', 'commit'])
23
+ if (typeof options[name] !== 'function') throw new TypeError(`guarded refiner needs ${name}`);
24
+ if (Boolean(options.snapshot) !== Boolean(options.restore))
25
+ throw new TypeError('snapshot and restore must be supplied together');
26
+ let pending = Promise.resolve();
27
+
28
+ /** Validate and plan without mutating the source or proposal. */
29
+ function prepare(document, proposal) {
30
+ let stage = 'shape';
31
+ try {
32
+ const patch = copy(proposal);
33
+ const shape = checkOutcome(options.validateProposal(patch));
34
+ if (!shape.valid) return { valid: false, errors: shape.errors };
35
+ const previous = copy(document);
36
+ stage = 'apply';
37
+ const next = options.apply(copy(previous), patch);
38
+ stage = 'candidate';
39
+ const candidate = checkOutcome(options.validateCandidate(copy(next), copy(previous)));
40
+ if (!candidate.valid) return { valid: false, errors: candidate.errors };
41
+ stage = 'plan';
42
+ const planned = options.planCommit(copy(next), copy(previous));
43
+ if (planned?.valid === false) return { valid: false, errors: planned.errors };
44
+ return { valid: true, errors: [], next, plan: planned?.plan ?? planned };
45
+ }
46
+ catch (error) {
47
+ return { valid: false, errors: [stage === 'apply' && options.applyFailure
48
+ ? options.applyFailure(error) : failure(error, stage)] };
49
+ }
50
+ }
51
+
52
+ /** Commit a prepared candidate; callers must serialize any external writers. */
53
+ async function commitPrepared(previous, prepared) {
54
+ if (!prepared.valid) return { ok: false, stage: 'validation', errors: prepared.errors };
55
+ let token;
56
+ try { token = options.snapshot ? await options.snapshot() : null; }
57
+ catch (error) { return { ok: false, stage: 'snapshot', cause: error, errors: [failure(error, 'snapshot')] }; }
58
+ try {
59
+ const value = await options.commit(copy(prepared.plan), {
60
+ previous: copy(previous), next: copy(prepared.next), snapshot: token,
61
+ });
62
+ return { ok: true, value, snapshot: token };
63
+ }
64
+ catch (cause) {
65
+ let restoreError;
66
+ if (options.restore) {
67
+ try {
68
+ const restored = await options.restore(token);
69
+ if (restored?.error !== undefined) throw new Error(restored.error);
70
+ }
71
+ catch (error) { restoreError = error; }
72
+ }
73
+ return { ok: false, stage: 'commit', cause, snapshot: token,
74
+ ...(restoreError === undefined ? {} : { restoreError }),
75
+ errors: [failure(cause, 'commit'), ...(restoreError === undefined ? [] : [failure(restoreError, 'restore')])] };
76
+ }
77
+ }
78
+
79
+ /** Read, prepare and commit one proposal, serialized with this engine's peers. */
80
+ function commit(proposal) {
81
+ const captured = copy(proposal);
82
+ const result = pending.then(async () => {
83
+ const previous = await options.read();
84
+ return commitPrepared(previous, prepare(previous, captured));
85
+ });
86
+ pending = result.then(() => undefined, () => undefined);
87
+ return result;
88
+ }
89
+ return { prepare, commitPrepared, commit };
90
+ }
package/src/index.js CHANGED
@@ -221,3 +221,6 @@ export function getInclusiveExclusiveBounds(getType, inclusive, exclusive) {
221
221
  ? [includes, undefined]
222
222
  : [undefined, excludes];
223
223
  }
224
+
225
+ export { checkOutcome, composeChecks } from './check.js';
226
+ export { createGuardedRefiner } from './guarded.js';