@jarenjs/core 0.83.3 → 0.84.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.
- package/README.md +13 -1
- package/dist/types/check.d.ts +42 -0
- package/dist/types/guarded.d.ts +120 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +9 -1
- package/src/check.js +51 -0
- package/src/guarded.js +90 -0
- package/src/index.js +3 -0
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.
|
|
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
|
+
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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.
|
|
4
|
+
"version": "0.84.3",
|
|
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';
|