@orkestrel/reason 0.0.9 → 0.0.11
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 +15 -19
- package/dist/src/core/index.cjs +289 -264
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +610 -281
- package/dist/src/core/index.d.ts +610 -281
- package/dist/src/core/index.js +290 -265
- package/dist/src/core/index.js.map +1 -1
- package/package.json +14 -15
package/dist/src/core/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GUARD_DEPTH_LIMIT, arrayOf, holds, isArray, isBoolean, isFiniteNumber, isNumber, isObject, isRecord, isString, lazyOf, literalOf, notOf, orOf, parseJSONAs, parseNumber, parseNumberField, readArrayEntries, recordOf, resolveField, unionOf, whereOf } from "@orkestrel/contract";
|
|
1
|
+
import { GUARD_DEPTH_LIMIT, arrayOf, holds, isArray, isBoolean, isError, isFiniteNumber, isFunction, isInstance, isNumber, isObject, isRecord, isString, lazyOf, literalOf, notOf, orOf, parseJSONAs, parseNumber, parseNumberField, readArrayEntries, recordOf, resolveField, unionOf, whereOf } from "@orkestrel/contract";
|
|
2
2
|
import { Emitter } from "@orkestrel/emitter";
|
|
3
3
|
//#region src/core/constants.ts
|
|
4
4
|
/**
|
|
@@ -47,7 +47,7 @@ var DEFAULT_PRIORITY = 0;
|
|
|
47
47
|
* inferential chaining.
|
|
48
48
|
*
|
|
49
49
|
* @remarks
|
|
50
|
-
* Fixed — unlike {@link DEFAULT_PRECISION} it is
|
|
50
|
+
* Fixed — unlike {@link DEFAULT_PRECISION} it is not overridable per
|
|
51
51
|
* definition, so confidence products stay comparable across derivations.
|
|
52
52
|
*/
|
|
53
53
|
var CONFIDENCE_PRECISION = 4;
|
|
@@ -93,8 +93,8 @@ var DEFINITION_BUILDER_BRAND = Symbol("reasons.definitionBuilder");
|
|
|
93
93
|
*
|
|
94
94
|
* @remarks
|
|
95
95
|
* Only `isSubjectBuilder` (`validators.ts`) reads this key, through `Reflect.get`.
|
|
96
|
-
* Distinct from {@link DEFINITION_BUILDER_BRAND}, so
|
|
97
|
-
* match
|
|
96
|
+
* Distinct from {@link DEFINITION_BUILDER_BRAND}, so neither entity can ever
|
|
97
|
+
* match the other's guard.
|
|
98
98
|
*/
|
|
99
99
|
var SUBJECT_BUILDER_BRAND = Symbol("reasons.subjectBuilder");
|
|
100
100
|
//#endregion
|
|
@@ -143,7 +143,7 @@ var ReasonError = class extends Error {
|
|
|
143
143
|
* ```
|
|
144
144
|
*/
|
|
145
145
|
function isReasonError(value) {
|
|
146
|
-
return value
|
|
146
|
+
return isInstance(value, ReasonError);
|
|
147
147
|
}
|
|
148
148
|
//#endregion
|
|
149
149
|
//#region src/core/validators.ts
|
|
@@ -151,7 +151,7 @@ function isReasonError(value) {
|
|
|
151
151
|
* Determines whether a value is a {@link Reasoning} literal.
|
|
152
152
|
*
|
|
153
153
|
* @param value - The value to test
|
|
154
|
-
* @returns True if `value`
|
|
154
|
+
* @returns True if `value` names a reasoning strategy; false otherwise
|
|
155
155
|
*
|
|
156
156
|
* @example
|
|
157
157
|
* ```ts
|
|
@@ -196,7 +196,7 @@ var isMathOperation = literalOf("add", "subtract", "multiply", "divide", "percen
|
|
|
196
196
|
* Determines whether a value is an {@link Aggregation} literal.
|
|
197
197
|
*
|
|
198
198
|
* @param value - The value to test
|
|
199
|
-
* @returns True if `value`
|
|
199
|
+
* @returns True if `value` names an aggregation; false otherwise
|
|
200
200
|
*
|
|
201
201
|
* @example
|
|
202
202
|
* ```ts
|
|
@@ -211,7 +211,7 @@ var isAggregation = literalOf("sum", "product", "average", "minimum", "maximum")
|
|
|
211
211
|
* Determines whether a value is a {@link Comparison} literal.
|
|
212
212
|
*
|
|
213
213
|
* @param value - The value to test
|
|
214
|
-
* @returns True if `value`
|
|
214
|
+
* @returns True if `value` names a comparison operator; false otherwise
|
|
215
215
|
*
|
|
216
216
|
* @example
|
|
217
217
|
* ```ts
|
|
@@ -226,7 +226,7 @@ var isComparison = literalOf("equals", "not", "above", "below", "from", "to", "a
|
|
|
226
226
|
* Determines whether a value is a {@link LogicalOperator} literal.
|
|
227
227
|
*
|
|
228
228
|
* @param value - The value to test
|
|
229
|
-
* @returns True if `value`
|
|
229
|
+
* @returns True if `value` names a logical connective; false otherwise
|
|
230
230
|
*
|
|
231
231
|
* @example
|
|
232
232
|
* ```ts
|
|
@@ -248,7 +248,7 @@ var isLogicalOperator = literalOf("and", "or", "not", "implies", "xor");
|
|
|
248
248
|
* ```ts
|
|
249
249
|
* import { isFieldPath } from '@src/core'
|
|
250
250
|
*
|
|
251
|
-
* isFieldPath('age') // true —
|
|
251
|
+
* isFieldPath('age') // true — a single key (never dot-split)
|
|
252
252
|
* isFieldPath(['address', 'city']) // true — descends
|
|
253
253
|
* isFieldPath(42) // false
|
|
254
254
|
* ```
|
|
@@ -275,8 +275,8 @@ var isNumberRecord = whereOf(isRecord, (record) => Object.values(record).every(i
|
|
|
275
275
|
* predicate.
|
|
276
276
|
*
|
|
277
277
|
* @remarks
|
|
278
|
-
* `value` may be
|
|
279
|
-
*
|
|
278
|
+
* `value` may be anything (including `null` / `undefined`) but the key must be
|
|
279
|
+
* present — exact-record semantics reject a check that lost its `value` key.
|
|
280
280
|
*
|
|
281
281
|
* @param value - The value to test
|
|
282
282
|
* @returns True if `value` is a well-formed check; false otherwise
|
|
@@ -360,8 +360,8 @@ function isFactorRange(value) {
|
|
|
360
360
|
}, ["bounds"])(value);
|
|
361
361
|
}
|
|
362
362
|
/**
|
|
363
|
-
* Determines whether a value is a {@link Source} —
|
|
364
|
-
*
|
|
363
|
+
* Determines whether a value is a {@link Source} — a static, field, lookup, or
|
|
364
|
+
* range factor source, discriminated by `origin`.
|
|
365
365
|
*
|
|
366
366
|
* @param value - The value to test
|
|
367
367
|
* @returns True if `value` is a well-formed static / field / lookup / range source; false otherwise
|
|
@@ -472,9 +472,9 @@ function isFactorGroup(value) {
|
|
|
472
472
|
* tree of atoms and compounds, discriminated by `form`.
|
|
473
473
|
*
|
|
474
474
|
* @remarks
|
|
475
|
-
* Recursive through `lazyOf` — recursion is
|
|
475
|
+
* Recursive through `lazyOf` — recursion is bounded by the stack, not
|
|
476
476
|
* unbounded: nesting beyond the engine's stack budget (roughly 1000 levels)
|
|
477
|
-
* and cyclic input are
|
|
477
|
+
* and cyclic input are contained as `false`, never a throw. Input past that
|
|
478
478
|
* bound is rejected, not validated.
|
|
479
479
|
*
|
|
480
480
|
* @param value - The value to test
|
|
@@ -534,9 +534,9 @@ function isRule(value) {
|
|
|
534
534
|
* `form`.
|
|
535
535
|
*
|
|
536
536
|
* @remarks
|
|
537
|
-
* Recursive through `lazyOf` — recursion is
|
|
537
|
+
* Recursive through `lazyOf` — recursion is bounded by the stack, not
|
|
538
538
|
* unbounded: nesting beyond the engine's stack budget (roughly 1000 levels)
|
|
539
|
-
* and cyclic input are
|
|
539
|
+
* and cyclic input are contained as `false`, never a throw. Input past that
|
|
540
540
|
* bound is rejected, not validated.
|
|
541
541
|
*
|
|
542
542
|
* @param value - The value to test
|
|
@@ -755,8 +755,8 @@ function isInferentialDefinition(value) {
|
|
|
755
755
|
}, ["description", "depth"])(value);
|
|
756
756
|
}
|
|
757
757
|
/**
|
|
758
|
-
* Determines whether a value is a {@link Definition} —
|
|
759
|
-
* definition
|
|
758
|
+
* Determines whether a value is a {@link Definition} — a quantitative, logical,
|
|
759
|
+
* symbolic, or inferential definition shape, discriminated by `reasoning`.
|
|
760
760
|
*
|
|
761
761
|
* @param value - The value to test
|
|
762
762
|
* @returns True if `value` is a well-formed definition of any reasoning; false otherwise
|
|
@@ -864,6 +864,12 @@ function isResultFact(value) {
|
|
|
864
864
|
/**
|
|
865
865
|
* Determines whether a value is a {@link CheckResult}.
|
|
866
866
|
*
|
|
867
|
+
* @remarks
|
|
868
|
+
* `actual` must be present and may hold anything, including `undefined` — the
|
|
869
|
+
* guard reads its presence, never its value. A result whose `actual` is
|
|
870
|
+
* `undefined` therefore does not survive a JSON round trip: `JSON.stringify`
|
|
871
|
+
* drops the key, and the parsed record is refused here.
|
|
872
|
+
*
|
|
867
873
|
* @param value - The value to test
|
|
868
874
|
* @returns True if every published check-result member is valid; false otherwise
|
|
869
875
|
*
|
|
@@ -885,6 +891,10 @@ function isCheckResult(value) {
|
|
|
885
891
|
/**
|
|
886
892
|
* Determines whether a value is a {@link FactorResult}.
|
|
887
893
|
*
|
|
894
|
+
* @remarks
|
|
895
|
+
* The optional `raw` and `checks` members are absent-or-valid: an absent key
|
|
896
|
+
* passes, a present key is checked in full.
|
|
897
|
+
*
|
|
888
898
|
* @param value - The value to test
|
|
889
899
|
* @returns True if every published factor-result member is valid; false otherwise
|
|
890
900
|
*
|
|
@@ -1056,7 +1066,7 @@ function isLogicalResult(value) {
|
|
|
1056
1066
|
* Determines whether a value is a {@link SymbolicResult}.
|
|
1057
1067
|
*
|
|
1058
1068
|
* @remarks
|
|
1059
|
-
* `solutions` is checked across its
|
|
1069
|
+
* `solutions` is checked across its own string-named members, enumerable or
|
|
1060
1070
|
* not. Inherited members are deliberately unread: they belong to the
|
|
1061
1071
|
* accepted-prototype property, not to the record this guard certifies.
|
|
1062
1072
|
*
|
|
@@ -1134,13 +1144,13 @@ function isReasonValidationResult(value) {
|
|
|
1134
1144
|
})(value);
|
|
1135
1145
|
}
|
|
1136
1146
|
/**
|
|
1137
|
-
* Determines whether a value is a `DefinitionBuilder`
|
|
1147
|
+
* Determines whether a value is a `DefinitionBuilder` entity — the brand-guarded
|
|
1138
1148
|
* stateful workspace, not the plain {@link Definition} data union.
|
|
1139
1149
|
*
|
|
1140
1150
|
* @remarks
|
|
1141
1151
|
* A `unique symbol` brand check (`Reflect.get`): a plain subject
|
|
1142
1152
|
* is an open record whose values may legally be functions, so a
|
|
1143
|
-
* method-presence check (`typeof value.build === 'function'`) is
|
|
1153
|
+
* method-presence check (`typeof value.build === 'function'`) is forgeable —
|
|
1144
1154
|
* this guard is not. A module-owned `unique symbol` cannot be produced by
|
|
1145
1155
|
* `JSON.parse` or written by any consumer that does not import
|
|
1146
1156
|
* `DEFINITION_BUILDER_BRAND`, so plain data can never forge it. Total: a
|
|
@@ -1164,12 +1174,12 @@ function isDefinitionBuilder(value) {
|
|
|
1164
1174
|
return isObject(value) && Reflect.get(value, DEFINITION_BUILDER_BRAND) === true;
|
|
1165
1175
|
}
|
|
1166
1176
|
/**
|
|
1167
|
-
* Determines whether a value is a `SubjectBuilder`
|
|
1177
|
+
* Determines whether a value is a `SubjectBuilder` entity — the brand-guarded
|
|
1168
1178
|
* stateful workspace, not the plain {@link Subject} data record.
|
|
1169
1179
|
*
|
|
1170
1180
|
* @remarks
|
|
1171
1181
|
* A `unique symbol` brand check (`Reflect.get`), distinct from
|
|
1172
|
-
* {@link isDefinitionBuilder} —
|
|
1182
|
+
* {@link isDefinitionBuilder} — neither entity can ever match the other's
|
|
1173
1183
|
* guard. Total: a non-object, a missing brand, or a hostile prototype all
|
|
1174
1184
|
* return `false`, never throw.
|
|
1175
1185
|
*
|
|
@@ -1197,7 +1207,7 @@ function isSubjectBuilder(value) {
|
|
|
1197
1207
|
*
|
|
1198
1208
|
* @remarks
|
|
1199
1209
|
* Display-only: the joined form is how a field appears in traces and derived
|
|
1200
|
-
* overlays; it is
|
|
1210
|
+
* overlays; it is never re-parsed into a path (a string stays a single key).
|
|
1201
1211
|
*
|
|
1202
1212
|
* @param field - The field path to format
|
|
1203
1213
|
* @returns The display string
|
|
@@ -1242,7 +1252,7 @@ function clamp(value, limit) {
|
|
|
1242
1252
|
*
|
|
1243
1253
|
* @remarks
|
|
1244
1254
|
* The range test behind the `between` and `outside` `Comparison` operators: only
|
|
1245
|
-
* the
|
|
1255
|
+
* the first two elements of `range` are read, each end is inclusive, and a
|
|
1246
1256
|
* non-numeric `value`, a non-array `range`, a `range` shorter than two
|
|
1247
1257
|
* elements, or a non-numeric bound all report `false`. `outside` is the pure
|
|
1248
1258
|
* negation of this predicate, so a malformed range reads as outside.
|
|
@@ -1306,9 +1316,9 @@ function emptyAggregate(aggregation) {
|
|
|
1306
1316
|
* source passes its value through; `field` and `range` coerce through the
|
|
1307
1317
|
* contracts `parseNumberField`, so a non-finite subject number or a
|
|
1308
1318
|
* non-numeric string is unresolvable and takes `fallback`; `lookup` reads only
|
|
1309
|
-
*
|
|
1319
|
+
* the table's own keys, and a missing or `null` field takes `fallback` directly
|
|
1310
1320
|
* rather than letting a `''` key intercept absent data. A `range` scans its
|
|
1311
|
-
* bands in order and the
|
|
1321
|
+
* bands in order and the first match wins — a band without `bounds` is a
|
|
1312
1322
|
* catch-all and an absent bound side is open. A malformed factor carrying no
|
|
1313
1323
|
* source at all takes `fallback` rather than crashing.
|
|
1314
1324
|
*
|
|
@@ -1340,7 +1350,7 @@ function resolveSource(source, subject, fallback) {
|
|
|
1340
1350
|
const value = parseNumberField(subject, source.field);
|
|
1341
1351
|
if (value === void 0) return fallback;
|
|
1342
1352
|
for (const range of source.ranges) {
|
|
1343
|
-
if (
|
|
1353
|
+
if (!isObject(range)) continue;
|
|
1344
1354
|
const limit = range.bounds;
|
|
1345
1355
|
if (!limit) return range.value;
|
|
1346
1356
|
const aboveMinimum = limit.minimum === void 0 || value >= limit.minimum;
|
|
@@ -1358,9 +1368,9 @@ function resolveSource(source, subject, fallback) {
|
|
|
1358
1368
|
* @remarks
|
|
1359
1369
|
* `Math.round` semantics — halves round toward `+∞` (`2.5` → `3`, `-2.5` → `-2`).
|
|
1360
1370
|
* A negative precision rounds at whole-number scales (`-1` → tens, `-2` →
|
|
1361
|
-
* hundreds).
|
|
1371
|
+
* hundreds). A precision whose scale factor overflows the double range
|
|
1362
1372
|
* (`10^p` → `Infinity` at roughly `p > 308`, `0` at roughly `p < -323`) returns
|
|
1363
|
-
* the value
|
|
1373
|
+
* the value unchanged — passthrough, never `NaN`.
|
|
1364
1374
|
*
|
|
1365
1375
|
* @param value - The number to round
|
|
1366
1376
|
* @param precision - Decimal places to keep (defaults to `0`)
|
|
@@ -1378,7 +1388,7 @@ function resolveSource(source, subject, fallback) {
|
|
|
1378
1388
|
*/
|
|
1379
1389
|
function roundTo(value, precision = 0) {
|
|
1380
1390
|
const factor = Math.pow(10, precision);
|
|
1381
|
-
if (!
|
|
1391
|
+
if (!isFiniteNumber(factor) || factor === 0) return value;
|
|
1382
1392
|
return Math.round(value * factor) / factor;
|
|
1383
1393
|
}
|
|
1384
1394
|
/**
|
|
@@ -1388,7 +1398,7 @@ function roundTo(value, precision = 0) {
|
|
|
1388
1398
|
* @remarks
|
|
1389
1399
|
* This is the derivation-bookkeeping equality of the chaining reasoners: the
|
|
1390
1400
|
* logical overlay and the inferential fact-dedupe compare with it so a
|
|
1391
|
-
* NaN-valued conclusion or fact term derives exactly
|
|
1401
|
+
* NaN-valued conclusion or fact term derives exactly once and the fixpoint
|
|
1392
1402
|
* converges (raw `===` would re-derive it every iteration, never converging).
|
|
1393
1403
|
* It matches `Array.prototype.includes` semantics — the same membership test
|
|
1394
1404
|
* the `any` / `none` comparisons use.
|
|
@@ -1415,7 +1425,7 @@ function equalValues(left, right) {
|
|
|
1415
1425
|
* @remarks
|
|
1416
1426
|
* The shared evaluation-order helper of the quantitative (factors) and logical
|
|
1417
1427
|
* (rules) reasoners: lower priorities run first, an absent `priority` defaults
|
|
1418
|
-
* to `0`, equal priorities keep
|
|
1428
|
+
* to `0`, equal priorities keep declaration order (stable), and the input array
|
|
1419
1429
|
* is never mutated. An array hole, `null`, or other non-record entry is dropped
|
|
1420
1430
|
* rather than sorted — the output may be shorter than the input.
|
|
1421
1431
|
*
|
|
@@ -1433,18 +1443,18 @@ function equalValues(left, right) {
|
|
|
1433
1443
|
function sortByPriority(items) {
|
|
1434
1444
|
const usable = [];
|
|
1435
1445
|
for (const item of items) {
|
|
1436
|
-
if (
|
|
1446
|
+
if (!isObject(item)) continue;
|
|
1437
1447
|
usable.push(item);
|
|
1438
1448
|
}
|
|
1439
1449
|
return usable.sort((left, right) => (left.priority ?? 0) - (right.priority ?? 0));
|
|
1440
1450
|
}
|
|
1441
1451
|
/**
|
|
1442
|
-
* Collects the ids that appear
|
|
1452
|
+
* Collects the ids that appear more than once in an id-carrying list — each
|
|
1443
1453
|
* duplicated id reported once, in first-occurrence order.
|
|
1444
1454
|
*
|
|
1445
1455
|
* @remarks
|
|
1446
1456
|
* The shared uniqueness scan behind every reasoner's `validate()` duplicate-id
|
|
1447
|
-
*
|
|
1457
|
+
* warnings (rules, groups, factors, equations, inferences). Runtime stays
|
|
1448
1458
|
* permissive about duplicates (first/last-wins artifacts) — this helper only
|
|
1449
1459
|
* surfaces them.
|
|
1450
1460
|
*
|
|
@@ -1470,7 +1480,7 @@ function findDuplicates(items) {
|
|
|
1470
1480
|
* @remarks
|
|
1471
1481
|
* Keys both stored facts and premise patterns (both are `Fact`-shaped) for
|
|
1472
1482
|
* {@link indexByArity}: `matchFacts` already rejects an arity mismatch, so
|
|
1473
|
-
* narrowing a same-predicate bucket to same
|
|
1483
|
+
* narrowing a same-predicate bucket to one of the same predicate and arity only
|
|
1474
1484
|
* excludes candidates that could never unify anyway. Only `predicate` — the
|
|
1475
1485
|
* one free-form, adversary-controlled part — is length-prefixed
|
|
1476
1486
|
* (`length + ':' + predicate`), mirroring {@link factToKey}'s framing so a
|
|
@@ -1499,7 +1509,7 @@ function factToArityKey(fact) {
|
|
|
1499
1509
|
* @remarks
|
|
1500
1510
|
* The index behind the inferential reasoner's same-predicate-and-arity join
|
|
1501
1511
|
* scans (`#findAllBindings` / `#calculatePremiseConfidence`): `matchFacts`
|
|
1502
|
-
* already rejects a predicate
|
|
1512
|
+
* already rejects a predicate or an arity mismatch, so restricting a premise's
|
|
1503
1513
|
* search to its own predicate+arity bucket changes nothing but the cost — the
|
|
1504
1514
|
* surviving matches and their append order are identical to a predicate-only
|
|
1505
1515
|
* index. Append order within a bucket is preserved, so a "first match wins"
|
|
@@ -1535,9 +1545,9 @@ function indexByArity(facts) {
|
|
|
1535
1545
|
* forward-chaining dedupe. Primitives (and `null`) key by value, typeof-prefixed
|
|
1536
1546
|
* so `1` (`number:1`) never collides with `'1'` (`string:1`); `-0` folds to `+0`
|
|
1537
1547
|
* (both `number:0`) and `NaN` is self-consistent (`number:NaN`), matching
|
|
1538
|
-
* SameValueZero. Objects and functions key by
|
|
1548
|
+
* SameValueZero. Objects and functions key by reference through `identities` — a
|
|
1539
1549
|
* first sighting is assigned the map's current size as its id, so distinct
|
|
1540
|
-
* objects never collide and the
|
|
1550
|
+
* objects never collide and the same reference always reproduces its key.
|
|
1541
1551
|
*
|
|
1542
1552
|
* @param term - The term to key
|
|
1543
1553
|
* @param identities - The caller-created reference-identity ledger, threaded across a dedupe pass (a newly seen object/function is registered in it)
|
|
@@ -1553,7 +1563,7 @@ function indexByArity(facts) {
|
|
|
1553
1563
|
* ```
|
|
1554
1564
|
*/
|
|
1555
1565
|
function termToKey(term, identities) {
|
|
1556
|
-
if (
|
|
1566
|
+
if (isObject(term) || isFunction(term)) {
|
|
1557
1567
|
const existing = identities.get(term);
|
|
1558
1568
|
if (existing !== void 0) return `${typeof term}:#${existing}`;
|
|
1559
1569
|
const id = identities.size;
|
|
@@ -1564,14 +1574,14 @@ function termToKey(term, identities) {
|
|
|
1564
1574
|
}
|
|
1565
1575
|
/**
|
|
1566
1576
|
* Derives a fact's canonical dedup key — predicate + arity + per-term
|
|
1567
|
-
* SameValueZero identity
|
|
1577
|
+
* SameValueZero identity, with confidence excluded from it.
|
|
1568
1578
|
*
|
|
1569
1579
|
* @remarks
|
|
1570
1580
|
* The dedup key of the inferential reasoner's forward fixpoint: two facts with
|
|
1571
1581
|
* the same predicate, arity, and SameValueZero-equal terms share a key (so a
|
|
1572
1582
|
* NaN-term fact derives once and ±0 collapse keeping the first), while
|
|
1573
1583
|
* confidence never enters the key. Each part — the predicate, the stringified
|
|
1574
|
-
* arity, and every {@link termToKey} — is
|
|
1584
|
+
* arity, and every {@link termToKey} — is length-prefixed (`length + ':' + part`)
|
|
1575
1585
|
* before joining, so the delimiter can never be forged by an adversarial string
|
|
1576
1586
|
* term embedding it: two distinct facts always produce distinct keys, even when
|
|
1577
1587
|
* a term string contains the delimiter (an injective framing raw joining lacked).
|
|
@@ -1602,10 +1612,10 @@ function factToKey(fact, identities) {
|
|
|
1602
1612
|
*
|
|
1603
1613
|
* @remarks
|
|
1604
1614
|
* The bidirectional unification of the inferential reasoner: a `'?'`-prefixed
|
|
1605
|
-
* string term on
|
|
1615
|
+
* string term on either side (pattern or candidate) is a variable that binds to
|
|
1606
1616
|
* the opposite term (the `'?'` prefix is kept in the binding key), while
|
|
1607
1617
|
* consistency is enforced within the match — a variable seen twice must bind the
|
|
1608
|
-
*
|
|
1618
|
+
* same value (raw `!==`) or the whole match fails. A predicate mismatch or an
|
|
1609
1619
|
* arity (term-count) mismatch fails immediately; non-variable terms must be
|
|
1610
1620
|
* strictly (`===`) equal.
|
|
1611
1621
|
*
|
|
@@ -1628,11 +1638,11 @@ function matchFacts(pattern, candidate) {
|
|
|
1628
1638
|
for (let index = 0; index < pattern.terms.length; index++) {
|
|
1629
1639
|
const patternTerm = pattern.terms[index];
|
|
1630
1640
|
const factTerm = candidate.terms[index];
|
|
1631
|
-
if (
|
|
1641
|
+
if (isString(patternTerm) && patternTerm.startsWith("?")) {
|
|
1632
1642
|
if (patternTerm in bindings) {
|
|
1633
1643
|
if (bindings[patternTerm] !== factTerm) return void 0;
|
|
1634
1644
|
} else bindings[patternTerm] = factTerm;
|
|
1635
|
-
} else if (
|
|
1645
|
+
} else if (isString(factTerm) && factTerm.startsWith("?")) {
|
|
1636
1646
|
if (factTerm in bindings) {
|
|
1637
1647
|
if (bindings[factTerm] !== patternTerm) return void 0;
|
|
1638
1648
|
} else bindings[factTerm] = patternTerm;
|
|
@@ -1647,7 +1657,7 @@ function matchFacts(pattern, candidate) {
|
|
|
1647
1657
|
* @remarks
|
|
1648
1658
|
* The pattern-instantiation step of the inferential reasoner: a `'?'`-prefixed
|
|
1649
1659
|
* string term that is present in `bindings` is replaced by its bound value;
|
|
1650
|
-
* every other term (constants and
|
|
1660
|
+
* every other term (constants and unbound variables alike) is kept verbatim. The
|
|
1651
1661
|
* returned fact is a fresh copy (`{ ...fact, terms }`) — the input is never
|
|
1652
1662
|
* mutated.
|
|
1653
1663
|
*
|
|
@@ -1664,7 +1674,7 @@ function matchFacts(pattern, candidate) {
|
|
|
1664
1674
|
*/
|
|
1665
1675
|
function instantiateFact(fact, bindings) {
|
|
1666
1676
|
const terms = fact.terms.map((term) => {
|
|
1667
|
-
if (
|
|
1677
|
+
if (isString(term) && term.startsWith("?") && term in bindings) return bindings[term];
|
|
1668
1678
|
return term;
|
|
1669
1679
|
});
|
|
1670
1680
|
return {
|
|
@@ -1674,7 +1684,7 @@ function instantiateFact(fact, bindings) {
|
|
|
1674
1684
|
}
|
|
1675
1685
|
/**
|
|
1676
1686
|
* Computes the confidence a set of matched premises contributes to a derived
|
|
1677
|
-
* fact — the product of each premise's
|
|
1687
|
+
* fact — the product of each premise's first matching fact's confidence.
|
|
1678
1688
|
*
|
|
1679
1689
|
* @remarks
|
|
1680
1690
|
* The confidence half of the inferential reasoner's forward derivation: each
|
|
@@ -1715,7 +1725,7 @@ function computePremiseConfidence(premises, index, bindings) {
|
|
|
1715
1725
|
* inferential reasoner's subject-injection step.
|
|
1716
1726
|
*
|
|
1717
1727
|
* @remarks
|
|
1718
|
-
* Every own subject field
|
|
1728
|
+
* Every own subject field except `id` becomes a `has(key, value)` fact at full
|
|
1719
1729
|
* `DEFAULT_CONFIDENCE`; `null` / `undefined` and any `object` (including arrays)
|
|
1720
1730
|
* value is skipped. The returned `trace` carries one line per injection plus a
|
|
1721
1731
|
* final count when at least one fact was produced, so no caller-owned
|
|
@@ -1738,7 +1748,7 @@ function subjectToFacts(subject) {
|
|
|
1738
1748
|
if (key === "id") continue;
|
|
1739
1749
|
const value = subject[key];
|
|
1740
1750
|
if (value === void 0 || value === null) continue;
|
|
1741
|
-
if (
|
|
1751
|
+
if (isObject(value)) continue;
|
|
1742
1752
|
facts.push({
|
|
1743
1753
|
id: `subject:${key}`,
|
|
1744
1754
|
predicate: "has",
|
|
@@ -1793,21 +1803,21 @@ function findUnboundVariables(inference) {
|
|
|
1793
1803
|
return unbound;
|
|
1794
1804
|
}
|
|
1795
1805
|
/**
|
|
1796
|
-
* Determines whether a symbolic expression contains an
|
|
1806
|
+
* Determines whether a symbolic expression contains an unbound occurrence of a
|
|
1797
1807
|
* target variable.
|
|
1798
1808
|
*
|
|
1799
1809
|
* @remarks
|
|
1800
1810
|
* The variable-presence probe of the symbolic reasoner's isolation: a `variable`
|
|
1801
|
-
* node matches only when its name is `target`
|
|
1811
|
+
* node matches only when its name is `target` and `target` is not already in
|
|
1802
1812
|
* `bindings` (a pre-bound target is a known value, not an unknown to isolate); a
|
|
1803
1813
|
* `constant` never matches; an `operation` recurses into both operands (the
|
|
1804
|
-
* `right` operand may be absent on a unary node). The walk is an
|
|
1814
|
+
* `right` operand may be absent on a unary node). The walk is an iterative
|
|
1805
1815
|
* worklist (never recursive) with short-circuit `true` on the first hit, so it
|
|
1806
1816
|
* stays total on pathologically deep expression trees.
|
|
1807
1817
|
*
|
|
1808
1818
|
* @param expression - The expression to probe
|
|
1809
1819
|
* @param target - The variable name being sought
|
|
1810
|
-
* @param bindings - The known bindings (a bound target does
|
|
1820
|
+
* @param bindings - The known bindings (a bound target does not count as present)
|
|
1811
1821
|
* @returns True if an unbound `target` occurs in the expression; false otherwise
|
|
1812
1822
|
*
|
|
1813
1823
|
* @example
|
|
@@ -1833,7 +1843,7 @@ function containsVariable(expression, target, bindings) {
|
|
|
1833
1843
|
return false;
|
|
1834
1844
|
}
|
|
1835
1845
|
/**
|
|
1836
|
-
* Inverts a `x op right = value` step, solving for the
|
|
1846
|
+
* Inverts a `x op right = value` step, solving for the left operand `x`.
|
|
1837
1847
|
*
|
|
1838
1848
|
* @remarks
|
|
1839
1849
|
* The left-operand inverse of the symbolic reasoner's isolation: `add` inverts
|
|
@@ -1868,7 +1878,7 @@ function invertLeft(operator, value, rightValue) {
|
|
|
1868
1878
|
}
|
|
1869
1879
|
}
|
|
1870
1880
|
/**
|
|
1871
|
-
* Inverts a `left op x = value` step, solving for the
|
|
1881
|
+
* Inverts a `left op x = value` step, solving for the right operand `x`.
|
|
1872
1882
|
*
|
|
1873
1883
|
* @remarks
|
|
1874
1884
|
* The right-operand inverse of the symbolic reasoner's isolation: `add` inverts
|
|
@@ -1910,7 +1920,7 @@ function invertRight(operator, value, leftValue) {
|
|
|
1910
1920
|
* by zero is `NaN` (never a throw), the unary operations (`round` / `ceil` /
|
|
1911
1921
|
* `floor` / `abs`) ignore `right`, and `percentage` is `left * (right / 100)`.
|
|
1912
1922
|
* `operator` is typed `string` because untrusted definitions reach here
|
|
1913
|
-
* unchecked; the
|
|
1923
|
+
* unchecked; the only throwing path is the unknown-operator default, which
|
|
1914
1924
|
* throws `ReasonError('OPERATOR', …, { operator })` and is caught per equation
|
|
1915
1925
|
* upstream.
|
|
1916
1926
|
*
|
|
@@ -1981,7 +1991,7 @@ function resolveOperand(operation, operand) {
|
|
|
1981
1991
|
* The shared atom-walk behind both {@link extractConclusions} and the raters'
|
|
1982
1992
|
* conclusion merge: an `atom` yields itself; a compound flattens its operands in
|
|
1983
1993
|
* authored order, so a later operand's atoms follow an earlier one's. The walk is
|
|
1984
|
-
* an
|
|
1994
|
+
* an iterative explicit-stack traversal (never recursive), so it stays total on
|
|
1985
1995
|
* pathologically deep expression trees; a hole in an `operands` array is skipped,
|
|
1986
1996
|
* matching `flatMap`'s hole-skipping behavior.
|
|
1987
1997
|
*
|
|
@@ -2016,13 +2026,13 @@ function extractAtoms(expression) {
|
|
|
2016
2026
|
}
|
|
2017
2027
|
/**
|
|
2018
2028
|
* Flattens a logical conclusion expression into its asserted `field = value`
|
|
2019
|
-
* pairs
|
|
2029
|
+
* pairs, ignoring the connectives.
|
|
2020
2030
|
*
|
|
2021
2031
|
* @remarks
|
|
2022
2032
|
* The conclusion-extraction step of the logical reasoner's chaining: every
|
|
2023
2033
|
* `atom` inside the expression asserts its `formatField(check.field) =
|
|
2024
2034
|
* check.value` pair, and compounds are walked without regard to the connective
|
|
2025
|
-
* (an atom under `not` / `or` is asserted the same way).
|
|
2035
|
+
* (an atom under `not` / `or` is asserted the same way). A later operand wins on
|
|
2026
2036
|
* a key clash (`Object.assign` order). Recursion runs through this exported
|
|
2027
2037
|
* function itself; the derived-overlay keys are `formatField` strings (an array
|
|
2028
2038
|
* field path flattens to its dot-joined form).
|
|
@@ -2049,13 +2059,13 @@ function extractConclusions(expression) {
|
|
|
2049
2059
|
*
|
|
2050
2060
|
* @remarks
|
|
2051
2061
|
* The reported keys are the `formatField`-flattened overlay keys an array-path
|
|
2052
|
-
* conclusion atom writes
|
|
2053
|
-
* also reads
|
|
2062
|
+
* conclusion atom writes anywhere among `rules` that an array-path premise atom
|
|
2063
|
+
* also reads anywhere among `rules`. The cross-rule authoring-time footgun
|
|
2054
2064
|
* probe behind `LogicalReasoner.validate`'s overlay-key-mismatch warning: a
|
|
2055
|
-
* logical conclusion's derived overlay is a
|
|
2065
|
+
* logical conclusion's derived overlay is a flat record keyed by
|
|
2056
2066
|
* `formatField(check.field)` — an array `FieldPath` dot-joins into one string
|
|
2057
|
-
* key. A premise that reads the same field through a
|
|
2058
|
-
* that flat key correctly, but a premise that reads it through an
|
|
2067
|
+
* key. A premise that reads the same field through a dotted-string path resolves
|
|
2068
|
+
* that flat key correctly, but a premise that reads it through an array path
|
|
2059
2069
|
* calls `resolveField`, which descends key-by-key into nesting the flat
|
|
2060
2070
|
* overlay never created, so the chain silently fails to connect. Collects the
|
|
2061
2071
|
* flattened keys of every array-path conclusion atom across `rules` (once
|
|
@@ -2081,7 +2091,7 @@ function findOverlayMismatches(rules) {
|
|
|
2081
2091
|
const writeKeys = [];
|
|
2082
2092
|
const seenWrites = /* @__PURE__ */ new Set();
|
|
2083
2093
|
for (const candidate of rules) for (const atomLeaf of extractAtoms(candidate.conclusion)) {
|
|
2084
|
-
if (!
|
|
2094
|
+
if (!isArray(atomLeaf.check.field)) continue;
|
|
2085
2095
|
const key = formatField(atomLeaf.check.field);
|
|
2086
2096
|
if (seenWrites.has(key)) continue;
|
|
2087
2097
|
seenWrites.add(key);
|
|
@@ -2089,7 +2099,7 @@ function findOverlayMismatches(rules) {
|
|
|
2089
2099
|
}
|
|
2090
2100
|
const readKeys = /* @__PURE__ */ new Set();
|
|
2091
2101
|
for (const candidate of rules) for (const premise of candidate.premises) for (const atomLeaf of extractAtoms(premise)) {
|
|
2092
|
-
if (!
|
|
2102
|
+
if (!isArray(atomLeaf.check.field)) continue;
|
|
2093
2103
|
readKeys.add(formatField(atomLeaf.check.field));
|
|
2094
2104
|
}
|
|
2095
2105
|
return writeKeys.filter((key) => readKeys.has(key));
|
|
@@ -2197,15 +2207,15 @@ function definitionToEnvelope(definition) {
|
|
|
2197
2207
|
}
|
|
2198
2208
|
/**
|
|
2199
2209
|
* Inserts `item` into an id-keyed collection, deduping any existing element
|
|
2200
|
-
* sharing its id, then placing it at the
|
|
2210
|
+
* sharing its id, then placing it at the end, or immediately after `target`.
|
|
2201
2211
|
*
|
|
2202
2212
|
* @remarks
|
|
2203
|
-
* Insertion
|
|
2213
|
+
* Insertion dedupes on the id first: `filtered` is `items` with every `item.id`
|
|
2204
2214
|
* twin removed, because an input array may already carry same-id twins.
|
|
2205
|
-
* Re-appending an existing id therefore
|
|
2215
|
+
* Re-appending an existing id therefore repositions it rather than updating it
|
|
2206
2216
|
* in place — {@link replaceById} is the position-preserving alternative. With
|
|
2207
2217
|
* no `target`, `item` lands at the end; with a `target`, it lands immediately
|
|
2208
|
-
* after the element whose `id === target` (searched in the
|
|
2218
|
+
* after the element whose `id === target` (searched in the deduped array). A
|
|
2209
2219
|
* `target` naming no element throws {@link ReasonError} (`'TARGET'`).
|
|
2210
2220
|
*
|
|
2211
2221
|
* @typeParam T - An id-carrying element type
|
|
@@ -2240,7 +2250,7 @@ function appendById(items, item, target) {
|
|
|
2240
2250
|
}
|
|
2241
2251
|
/**
|
|
2242
2252
|
* Inserts `item` into an id-keyed collection, deduping any existing element
|
|
2243
|
-
* sharing its id, then placing it at the
|
|
2253
|
+
* sharing its id, then placing it at the start, or immediately before `target`.
|
|
2244
2254
|
*
|
|
2245
2255
|
* @remarks
|
|
2246
2256
|
* Mirrors {@link appendById}'s dedup-then-insert semantics exactly, only the
|
|
@@ -2280,7 +2290,7 @@ function prependById(items, item, target) {
|
|
|
2280
2290
|
];
|
|
2281
2291
|
}
|
|
2282
2292
|
/**
|
|
2283
|
-
* Swaps the element sharing `item.id`
|
|
2293
|
+
* Swaps the element sharing `item.id` in place, preserving its position.
|
|
2284
2294
|
*
|
|
2285
2295
|
* @remarks
|
|
2286
2296
|
* The position-preserving update primitive — unlike {@link appendById}, which
|
|
@@ -2337,11 +2347,11 @@ function removeById(items, id) {
|
|
|
2337
2347
|
*
|
|
2338
2348
|
* @remarks
|
|
2339
2349
|
* A Strategic-Merge-Patch-style id-keyed upsert:
|
|
2340
|
-
* the result is ordered by `incoming`'s id order
|
|
2350
|
+
* the result is ordered by `incoming`'s id order first (each element resolved
|
|
2341
2351
|
* through `resolve` when its id also exists in `base`, defaulting to
|
|
2342
|
-
* incoming-wins-wholesale),
|
|
2352
|
+
* incoming-wins-wholesale), then the `base`-only survivors in `base`'s own
|
|
2343
2353
|
* order (retained, never deleted — merge is additive). Same-id twins within
|
|
2344
|
-
*
|
|
2354
|
+
* either input are deduped to their first occurrence.
|
|
2345
2355
|
*
|
|
2346
2356
|
* @typeParam T - An id-carrying element type
|
|
2347
2357
|
* @param base - The base collection
|
|
@@ -2380,7 +2390,7 @@ function mergeById(base, incoming, resolve) {
|
|
|
2380
2390
|
* insert at the end, or immediately after `target`.
|
|
2381
2391
|
*
|
|
2382
2392
|
* @remarks
|
|
2383
|
-
* Group order is
|
|
2393
|
+
* Group order is cosmetic (group aggregation is order-independent) but honored
|
|
2384
2394
|
* uniformly, same as every `append*` helper. Composes with {@link appendFactor}:
|
|
2385
2395
|
* `appendGroup(def, appendFactor(group, factor))`.
|
|
2386
2396
|
*
|
|
@@ -2427,8 +2437,8 @@ function prependGroup(definition, group, target) {
|
|
|
2427
2437
|
};
|
|
2428
2438
|
}
|
|
2429
2439
|
/**
|
|
2430
|
-
* Swaps the group sharing `group.id` in a {@link QuantitativeDefinition}
|
|
2431
|
-
*
|
|
2440
|
+
* Swaps the group sharing `group.id` in a {@link QuantitativeDefinition} in
|
|
2441
|
+
* place, preserving its position (appended when absent).
|
|
2432
2442
|
*
|
|
2433
2443
|
* @param definition - The definition to update
|
|
2434
2444
|
* @param group - The replacement group
|
|
@@ -2475,9 +2485,9 @@ function removeGroup(definition, id) {
|
|
|
2475
2485
|
* at the end, or immediately after `target`.
|
|
2476
2486
|
*
|
|
2477
2487
|
* @remarks
|
|
2478
|
-
* Factor order is
|
|
2488
|
+
* Factor order is load-bearing: the same-priority tiebreak is declaration
|
|
2479
2489
|
* order ({@link sortByPriority} is a stable ascending sort). Operates on the
|
|
2480
|
-
* factor's
|
|
2490
|
+
* factor's direct container — compose into a definition through
|
|
2481
2491
|
* `appendGroup(def, appendFactor(group, factor))`.
|
|
2482
2492
|
*
|
|
2483
2493
|
* @param group - The group to insert into
|
|
@@ -2523,8 +2533,8 @@ function prependFactor(group, factor, target) {
|
|
|
2523
2533
|
};
|
|
2524
2534
|
}
|
|
2525
2535
|
/**
|
|
2526
|
-
* Swaps the factor sharing `factor.id` in a {@link FactorGroup}
|
|
2527
|
-
* preserving its position (
|
|
2536
|
+
* Swaps the factor sharing `factor.id` in a {@link FactorGroup} in place,
|
|
2537
|
+
* preserving its position (appended when absent).
|
|
2528
2538
|
*
|
|
2529
2539
|
* @param group - The group to update
|
|
2530
2540
|
* @param factor - The replacement factor
|
|
@@ -2570,7 +2580,7 @@ function removeFactor(group, id) {
|
|
|
2570
2580
|
* at the end, or immediately after `target`.
|
|
2571
2581
|
*
|
|
2572
2582
|
* @remarks
|
|
2573
|
-
* Order is
|
|
2583
|
+
* Order is load-bearing: the forward conclusion is the last declared
|
|
2574
2584
|
* non-disabled rule, so `appendRule` without a `target` makes the new rule the
|
|
2575
2585
|
* conclusion.
|
|
2576
2586
|
*
|
|
@@ -2617,8 +2627,8 @@ function prependRule(definition, rule, target) {
|
|
|
2617
2627
|
};
|
|
2618
2628
|
}
|
|
2619
2629
|
/**
|
|
2620
|
-
* Swaps the rule sharing `rule.id` in a {@link LogicalDefinition}
|
|
2621
|
-
* preserving its position (
|
|
2630
|
+
* Swaps the rule sharing `rule.id` in a {@link LogicalDefinition} in place,
|
|
2631
|
+
* preserving its position (appended when absent).
|
|
2622
2632
|
*
|
|
2623
2633
|
* @param definition - The definition to update
|
|
2624
2634
|
* @param rule - The replacement rule
|
|
@@ -2665,8 +2675,8 @@ function removeRule(definition, id) {
|
|
|
2665
2675
|
* then-insert at the end, or immediately after `target`.
|
|
2666
2676
|
*
|
|
2667
2677
|
* @remarks
|
|
2668
|
-
* Order is
|
|
2669
|
-
*
|
|
2678
|
+
* Order is load-bearing: equations solve strictly in order and each rounded
|
|
2679
|
+
* solution feeds forward.
|
|
2670
2680
|
*
|
|
2671
2681
|
* @param definition - The definition to insert into
|
|
2672
2682
|
* @param equation - The equation to insert
|
|
@@ -2711,8 +2721,8 @@ function prependEquation(definition, equation, target) {
|
|
|
2711
2721
|
};
|
|
2712
2722
|
}
|
|
2713
2723
|
/**
|
|
2714
|
-
* Swaps the equation sharing `equation.id` in a {@link SymbolicDefinition}
|
|
2715
|
-
*
|
|
2724
|
+
* Swaps the equation sharing `equation.id` in a {@link SymbolicDefinition} in
|
|
2725
|
+
* place, preserving its position (appended when absent).
|
|
2716
2726
|
*
|
|
2717
2727
|
* @param definition - The definition to update
|
|
2718
2728
|
* @param equation - The replacement equation
|
|
@@ -2786,7 +2796,7 @@ function addVariable(definition, name, value) {
|
|
|
2786
2796
|
* Removes one entry of a {@link SymbolicDefinition}'s `variables`.
|
|
2787
2797
|
*
|
|
2788
2798
|
* @remarks
|
|
2789
|
-
* The destructure-rest form
|
|
2799
|
+
* The destructure-rest form omits the key entirely (never sets it to
|
|
2790
2800
|
* `undefined`), keeping the result exact-record valid. A no-op (fresh copy)
|
|
2791
2801
|
* when `name` is absent.
|
|
2792
2802
|
*
|
|
@@ -2813,7 +2823,7 @@ function removeVariable(definition, name) {
|
|
|
2813
2823
|
* insert at the end, or immediately after `target`.
|
|
2814
2824
|
*
|
|
2815
2825
|
* @remarks
|
|
2816
|
-
* `Fact.id` is an
|
|
2826
|
+
* `Fact.id` is an authoring label — the runtime content-dedups facts by
|
|
2817
2827
|
* predicate+arity+terms ({@link factToKey}), independently of this helper's
|
|
2818
2828
|
* id-keyed dedup.
|
|
2819
2829
|
*
|
|
@@ -2860,8 +2870,8 @@ function prependFact(definition, fact, target) {
|
|
|
2860
2870
|
};
|
|
2861
2871
|
}
|
|
2862
2872
|
/**
|
|
2863
|
-
* Swaps the fact sharing `fact.id` in an {@link InferentialDefinition}
|
|
2864
|
-
*
|
|
2873
|
+
* Swaps the fact sharing `fact.id` in an {@link InferentialDefinition} in
|
|
2874
|
+
* place, preserving its position (appended when absent).
|
|
2865
2875
|
*
|
|
2866
2876
|
* @param definition - The definition to update
|
|
2867
2877
|
* @param fact - The replacement fact
|
|
@@ -2908,7 +2918,7 @@ function removeFact(definition, id) {
|
|
|
2908
2918
|
* dedup-then-insert at the end, or immediately after `target`.
|
|
2909
2919
|
*
|
|
2910
2920
|
* @remarks
|
|
2911
|
-
* Order is
|
|
2921
|
+
* Order is load-bearing: backward proving iterates in declaration order and
|
|
2912
2922
|
* returns on first success.
|
|
2913
2923
|
*
|
|
2914
2924
|
* @param definition - The definition to insert into
|
|
@@ -2961,7 +2971,7 @@ function prependInference(definition, inference, target) {
|
|
|
2961
2971
|
}
|
|
2962
2972
|
/**
|
|
2963
2973
|
* Swaps the inference sharing `inference.id` in an {@link InferentialDefinition}
|
|
2964
|
-
*
|
|
2974
|
+
* in place, preserving its position (appended when absent).
|
|
2965
2975
|
*
|
|
2966
2976
|
* @param definition - The definition to update
|
|
2967
2977
|
* @param inference - The replacement inference
|
|
@@ -3008,10 +3018,10 @@ function removeInference(definition, id) {
|
|
|
3008
3018
|
* Reconciles two {@link QuantitativeDefinition}s onto `base`'s id.
|
|
3009
3019
|
*
|
|
3010
3020
|
* @remarks
|
|
3011
|
-
* The merge is
|
|
3021
|
+
* The merge is additive: a base-only group survives into the result and is
|
|
3012
3022
|
* never deleted, and a scalar absent from `incoming` keeps its base value —
|
|
3013
3023
|
* merge never clears a field, which is what {@link clearQuantitativeDefinition}
|
|
3014
|
-
* is for. `groups` merges through {@link mergeById}; a matched (same-id)
|
|
3024
|
+
* is for. `groups` merges through {@link mergeById}; a matched (same-id) pair of
|
|
3015
3025
|
* groups recurses one level deeper — their `factors` also merge through
|
|
3016
3026
|
* `mergeById` — the one exception to incoming-wins-wholesale. Every other
|
|
3017
3027
|
* scalar / value-object field is incoming-wins-when-present, else base kept.
|
|
@@ -3049,7 +3059,7 @@ function mergeQuantitativeDefinition(base, incoming) {
|
|
|
3049
3059
|
* Reconciles two {@link LogicalDefinition}s onto `base`'s id.
|
|
3050
3060
|
*
|
|
3051
3061
|
* @remarks
|
|
3052
|
-
* The merge is
|
|
3062
|
+
* The merge is additive: a base-only rule survives into the result and is never
|
|
3053
3063
|
* deleted, and a scalar absent from `incoming` keeps its base value — merge
|
|
3054
3064
|
* never clears a field, which is what {@link clearLogicalDefinition} is for.
|
|
3055
3065
|
* `rules` merges through {@link mergeById} (incoming-wins-wholesale on a matched
|
|
@@ -3082,7 +3092,7 @@ function mergeLogicalDefinition(base, incoming) {
|
|
|
3082
3092
|
* Reconciles two {@link SymbolicDefinition}s onto `base`'s id.
|
|
3083
3093
|
*
|
|
3084
3094
|
* @remarks
|
|
3085
|
-
* The merge is
|
|
3095
|
+
* The merge is additive: a base-only equation or variable survives into the
|
|
3086
3096
|
* result and is never deleted, and a scalar absent from `incoming` keeps its
|
|
3087
3097
|
* base value — merge never clears a field, which is what
|
|
3088
3098
|
* {@link clearSymbolicDefinition} is for. `equations` merges through
|
|
@@ -3122,7 +3132,7 @@ function mergeSymbolicDefinition(base, incoming) {
|
|
|
3122
3132
|
* Reconciles two {@link InferentialDefinition}s onto `base`'s id.
|
|
3123
3133
|
*
|
|
3124
3134
|
* @remarks
|
|
3125
|
-
* The merge is
|
|
3135
|
+
* The merge is additive: a base-only inference or fact survives into the result
|
|
3126
3136
|
* and is never deleted, and a scalar absent from `incoming` keeps its base
|
|
3127
3137
|
* value — merge never clears a field, which is what
|
|
3128
3138
|
* {@link clearInferentialDefinition} is for. `inferences` and `facts` each
|
|
@@ -3233,7 +3243,7 @@ function clearInferentialDefinition(definition, key) {
|
|
|
3233
3243
|
* Upserts one field of a {@link Subject} — copy-on-write spread.
|
|
3234
3244
|
*
|
|
3235
3245
|
* @remarks
|
|
3236
|
-
* Named `assignField` rather than `setField` because it
|
|
3246
|
+
* Named `assignField` rather than `setField` because it returns a fresh subject
|
|
3237
3247
|
* instead of writing into the one it is given — `set*` reads as an in-place
|
|
3238
3248
|
* write. Id-agnostic: overwrites an `id` key like any other field — id
|
|
3239
3249
|
* protection is an entity's job, not this helper's.
|
|
@@ -3260,7 +3270,7 @@ function assignField(subject, key, value) {
|
|
|
3260
3270
|
* Deletes one field of a {@link Subject} — destructure-rest omit.
|
|
3261
3271
|
*
|
|
3262
3272
|
* @remarks
|
|
3263
|
-
* The key is
|
|
3273
|
+
* The key is deleted entirely (never set to `undefined`), keeping the result
|
|
3264
3274
|
* exact-record valid. A no-op (fresh copy) when `key` is absent.
|
|
3265
3275
|
*
|
|
3266
3276
|
* @param subject - The subject to update
|
|
@@ -3315,8 +3325,8 @@ function mergeSubjects(base, incoming) {
|
|
|
3315
3325
|
* When `subject.id` is a string, each clone's id is minted
|
|
3316
3326
|
* `` `${baseId}-${index}` `` (index from `0`); with no string `id`, the clones
|
|
3317
3327
|
* pass through unchanged (still fresh copies). Pure and deterministic — the
|
|
3318
|
-
* same input always produces the same output (run-twice equality) — and
|
|
3319
|
-
*
|
|
3328
|
+
* same input always produces the same output (run-twice equality) — and emits
|
|
3329
|
+
* nothing. `count <= 0` yields an empty array.
|
|
3320
3330
|
*
|
|
3321
3331
|
* @param subject - The subject to clone
|
|
3322
3332
|
* @param count - How many clones to produce
|
|
@@ -3333,7 +3343,7 @@ function mergeSubjects(base, incoming) {
|
|
|
3333
3343
|
function repeatSubject(subject, count) {
|
|
3334
3344
|
const baseId = subject.id;
|
|
3335
3345
|
const clones = [];
|
|
3336
|
-
for (let index = 0; index < count; index += 1) clones.push(
|
|
3346
|
+
for (let index = 0; index < count; index += 1) clones.push(isString(baseId) ? {
|
|
3337
3347
|
...subject,
|
|
3338
3348
|
id: `${baseId}-${index}`
|
|
3339
3349
|
} : { ...subject });
|
|
@@ -3346,8 +3356,8 @@ function repeatSubject(subject, count) {
|
|
|
3346
3356
|
*
|
|
3347
3357
|
* @remarks
|
|
3348
3358
|
* The safe inverse of the builders: `parseJSONAs` composed with the data guard
|
|
3349
|
-
* {@link isDefinition}. A built definition body
|
|
3350
|
-
* `JSON.stringify(definition)` round-trips through `parseDefinition`.
|
|
3359
|
+
* {@link isDefinition}. A built definition body is the durable JSON payload —
|
|
3360
|
+
* `JSON.stringify(definition)` round-trips through `parseDefinition`. The
|
|
3351
3361
|
* authoring hazards: a required `Check.value: undefined` drops its key on
|
|
3352
3362
|
* `JSON.stringify` (author `null` instead), and a `Fact.terms` element of
|
|
3353
3363
|
* `undefined` serializes to `null` (terms must be JSON-safe scalars/strings).
|
|
@@ -3374,16 +3384,16 @@ function parseDefinition(json) {
|
|
|
3374
3384
|
* {@link ReasonerInterface}s.
|
|
3375
3385
|
*
|
|
3376
3386
|
* @remarks
|
|
3377
|
-
* Holds
|
|
3387
|
+
* Holds no strategy-specific logic: dispatch is purely a registry lookup by
|
|
3378
3388
|
* `definition.reasoning` (one reasoner per reasoning; re-registration
|
|
3379
3389
|
* replaces). A missing reasoner throws `MISSING` and a pre-run validation
|
|
3380
|
-
* failure (when the `validate` option is on) throws `INVALID` —
|
|
3381
|
-
* `bail` and
|
|
3390
|
+
* failure (when the `validate` option is on) throws `INVALID` — each bypasses
|
|
3391
|
+
* `bail` and emits nothing. A reasoner throw emits `error` with the raw thrown
|
|
3382
3392
|
* value, then rethrows under `bail: true` (the default) or converts to a
|
|
3383
|
-
* type-shaped failure result under `bail: false`; only
|
|
3393
|
+
* type-shaped failure result under `bail: false`; only a successful result emits
|
|
3384
3394
|
* `reason` (synchronously, before returning). The batch overload maps subjects
|
|
3385
3395
|
* in order (validation, when on, repeats per subject). `destroy()` clears the
|
|
3386
|
-
* registry, emits `destroy`, then destroys the emitter
|
|
3396
|
+
* registry, emits `destroy`, then destroys the emitter last and is
|
|
3387
3397
|
* idempotent; every other method afterwards throws `DESTROYED` — only the
|
|
3388
3398
|
* {@link emitter} getter keeps working.
|
|
3389
3399
|
*
|
|
@@ -3488,7 +3498,7 @@ var Reason = class {
|
|
|
3488
3498
|
} catch (error) {
|
|
3489
3499
|
this.#emitter.emit("error", error);
|
|
3490
3500
|
if (this.#bail) throw error;
|
|
3491
|
-
return buildErrorResult(definition, error
|
|
3501
|
+
return buildErrorResult(definition, isError(error) ? error.message : String(error));
|
|
3492
3502
|
}
|
|
3493
3503
|
}
|
|
3494
3504
|
#ensureAlive() {
|
|
@@ -3502,14 +3512,14 @@ var Reason = class {
|
|
|
3502
3512
|
* quantitative and logical reasoners.
|
|
3503
3513
|
*
|
|
3504
3514
|
* @remarks
|
|
3505
|
-
*
|
|
3515
|
+
* Total and strict: `evaluate` never throws (an unknown operator is caught and
|
|
3506
3516
|
* surfaced as `CheckResult.error` with `met: false`) and never coerces —
|
|
3507
3517
|
* `equals` / `not` are raw `===` / `!==`, the ordering operators demand numbers
|
|
3508
|
-
* on
|
|
3509
|
-
* not met for either — `none` is
|
|
3510
|
-
* input), while `outside`
|
|
3518
|
+
* on each side, `any` / `none` demand an array expected value (a non-array is
|
|
3519
|
+
* not met for either — `none` is not the raw complement of `any` on malformed
|
|
3520
|
+
* input), while `outside` is the pure negation of `between` (so a malformed
|
|
3511
3521
|
* range is `outside`). Fields resolve through the core `resolveField` — a string
|
|
3512
|
-
* is
|
|
3522
|
+
* is a single key, an array descends. Stateless and deterministic.
|
|
3513
3523
|
*
|
|
3514
3524
|
* @example
|
|
3515
3525
|
* ```ts
|
|
@@ -3538,7 +3548,7 @@ var Evaluator = class {
|
|
|
3538
3548
|
actual
|
|
3539
3549
|
};
|
|
3540
3550
|
} catch (error) {
|
|
3541
|
-
const message = error
|
|
3551
|
+
const message = isError(error) ? error.message : String(error);
|
|
3542
3552
|
return {
|
|
3543
3553
|
field: check.field,
|
|
3544
3554
|
met: false,
|
|
@@ -3573,7 +3583,7 @@ var Evaluator = class {
|
|
|
3573
3583
|
* per-factor pipeline stage.
|
|
3574
3584
|
*
|
|
3575
3585
|
* @remarks
|
|
3576
|
-
*
|
|
3586
|
+
* Total: never throws. Absent-`operand` defaults are operation-specific —
|
|
3577
3587
|
* identity-preserving `1` for `multiply` / `divide` / `power`, `0` for every
|
|
3578
3588
|
* other binary operation; `round` / `ceil` / `floor` / `abs` are unary and
|
|
3579
3589
|
* ignore the operand. `divide` by zero yields `NaN` (deliberately not JS's
|
|
@@ -3613,11 +3623,11 @@ var Transformer = class {
|
|
|
3613
3623
|
* reasoner's group and definition combiner.
|
|
3614
3624
|
*
|
|
3615
3625
|
* @remarks
|
|
3616
|
-
*
|
|
3626
|
+
* Total: never throws. Empty-input identities: `sum` / `average` → `0`,
|
|
3617
3627
|
* `product` → `1`, `minimum` / `maximum` → `NaN` (the deliberate "no data"
|
|
3618
|
-
* signal). Weights are honored
|
|
3628
|
+
* signal). Weights are honored only when their length matches `values` exactly
|
|
3619
3629
|
* (otherwise silently unweighted): a weight multiplies into a `sum`, acts as an
|
|
3620
|
-
*
|
|
3630
|
+
* exponent for a `product`, is the weight of a weighted mean for `average`
|
|
3621
3631
|
* (a zero total weight yields `0`, never a division blow-up), and is ignored by
|
|
3622
3632
|
* `minimum` / `maximum`. An unknown aggregation yields `0`. Stateless and
|
|
3623
3633
|
* deterministic.
|
|
@@ -3663,21 +3673,21 @@ var Aggregator = class {
|
|
|
3663
3673
|
* Performs factor-based numeric scoring.
|
|
3664
3674
|
*
|
|
3665
3675
|
* @remarks
|
|
3666
|
-
* Each factor runs a fixed pipeline: checks gate (
|
|
3667
|
-
* (`fallback` when unresolvable) → finite check → transforms chain →
|
|
3668
|
-
* clamp → finite recheck. Factors evaluate in stable ascending `priority`
|
|
3676
|
+
* Each factor runs a fixed pipeline: checks gate (every check met) → source
|
|
3677
|
+
* resolve (`fallback` when unresolvable) → finite check → transforms chain →
|
|
3678
|
+
* bounds clamp → finite recheck. Factors evaluate in stable ascending `priority`
|
|
3669
3679
|
* order; a `strict` group is all-or-nothing; a group's value is its `base` plus
|
|
3670
|
-
* the weighted aggregation of its
|
|
3680
|
+
* the weighted aggregation of its applied factors, clamped (never rounded); the
|
|
3671
3681
|
* definition's value is its `base` plus the unweighted aggregation of the
|
|
3672
|
-
*
|
|
3682
|
+
* applied groups' values, clamped, then rounded to `precision`. A
|
|
3673
3683
|
* required-factor failure or non-finite value appends an error (`success:
|
|
3674
3684
|
* false`) without aborting — the numeric `value` is always computed. The
|
|
3675
|
-
* definition-level value is finite-checked
|
|
3685
|
+
* definition-level value is finite-checked after rounding: a non-finite
|
|
3676
3686
|
* aggregate (a `minimum` / `maximum` over zero applied groups) appends an error
|
|
3677
3687
|
* while the `NaN` stays visible in `value`. Field sources coerce through the
|
|
3678
3688
|
* contracts `parseNumberField`: a non-finite subject number or a non-numeric
|
|
3679
3689
|
* string is unresolvable and takes the fallback path. Lookup sources read only
|
|
3680
|
-
*
|
|
3690
|
+
* the table's own keys, and a missing / `null` field falls back directly. Nothing
|
|
3681
3691
|
* mutates its inputs; fully deterministic.
|
|
3682
3692
|
*
|
|
3683
3693
|
* @example
|
|
@@ -3754,7 +3764,7 @@ var QuantitativeReasoner = class {
|
|
|
3754
3764
|
definition: definition.id,
|
|
3755
3765
|
reasoning: this.reasoning
|
|
3756
3766
|
});
|
|
3757
|
-
if (!
|
|
3767
|
+
if (!isArray(definition.groups)) return {
|
|
3758
3768
|
reasoning: "quantitative",
|
|
3759
3769
|
value: 0,
|
|
3760
3770
|
groups: [],
|
|
@@ -3768,7 +3778,7 @@ var QuantitativeReasoner = class {
|
|
|
3768
3778
|
let count = 0;
|
|
3769
3779
|
const groupResults = [];
|
|
3770
3780
|
for (const group of definition.groups) {
|
|
3771
|
-
if (
|
|
3781
|
+
if (!isObject(group)) continue;
|
|
3772
3782
|
if (group.enabled === false) {
|
|
3773
3783
|
trace.push(`Skipped group "${group.id}" (disabled)`);
|
|
3774
3784
|
continue;
|
|
@@ -3783,7 +3793,7 @@ var QuantitativeReasoner = class {
|
|
|
3783
3793
|
trace.push(`Aggregated ${groupValues.length} groups with "${definition.aggregation}": base=${base}, raw=${value}`);
|
|
3784
3794
|
value = clamp(value, definition.bounds);
|
|
3785
3795
|
value = roundTo(value, definition.precision ?? 4);
|
|
3786
|
-
if (!
|
|
3796
|
+
if (!isFiniteNumber(value)) {
|
|
3787
3797
|
const description = Number.isNaN(value) ? "NaN" : String(value);
|
|
3788
3798
|
trace.push(`Definition "${definition.id}": produced non-finite value (${description})`);
|
|
3789
3799
|
errors.push(`Definition "${definition.id}" produced non-finite value: ${description}`);
|
|
@@ -3829,7 +3839,7 @@ var QuantitativeReasoner = class {
|
|
|
3829
3839
|
const factorValues = appliedFactors.map((factor) => factor.value);
|
|
3830
3840
|
const weightById = /* @__PURE__ */ new Map();
|
|
3831
3841
|
for (const original of group.factors) {
|
|
3832
|
-
if (
|
|
3842
|
+
if (!isObject(original)) continue;
|
|
3833
3843
|
if (!weightById.has(original.id)) weightById.set(original.id, original.weight ?? 1);
|
|
3834
3844
|
}
|
|
3835
3845
|
const weights = appliedFactors.map((factor) => weightById.get(factor.id) ?? 1);
|
|
@@ -3867,7 +3877,7 @@ var QuantitativeReasoner = class {
|
|
|
3867
3877
|
value: 0
|
|
3868
3878
|
};
|
|
3869
3879
|
}
|
|
3870
|
-
if (!
|
|
3880
|
+
if (!isFiniteNumber(raw)) {
|
|
3871
3881
|
const description = Number.isNaN(raw) ? "NaN" : String(raw);
|
|
3872
3882
|
trace.push(`Factor "${factor.id}": source produced non-finite value (${description})`);
|
|
3873
3883
|
errors.push(`Factor "${factor.id}" produced non-finite value: ${description}`);
|
|
@@ -3881,7 +3891,7 @@ var QuantitativeReasoner = class {
|
|
|
3881
3891
|
let value = raw;
|
|
3882
3892
|
if (factor.transforms && factor.transforms.length > 0) value = this.#transformer.chain(value, factor.transforms);
|
|
3883
3893
|
value = clamp(value, factor.bounds);
|
|
3884
|
-
if (!
|
|
3894
|
+
if (!isFiniteNumber(value)) {
|
|
3885
3895
|
const description = Number.isNaN(value) ? "NaN" : String(value);
|
|
3886
3896
|
trace.push(`Factor "${factor.id}": produced non-finite value (${description})`);
|
|
3887
3897
|
errors.push(`Factor "${factor.id}" produced non-finite value: ${description}`);
|
|
@@ -3909,18 +3919,18 @@ var QuantitativeReasoner = class {
|
|
|
3909
3919
|
* @remarks
|
|
3910
3920
|
* Forward chaining is a naive fixpoint capped at `depth` iterations: rules run
|
|
3911
3921
|
* in ascending `priority` order each pass, and a firing rule's conclusion atoms
|
|
3912
|
-
* become a derived overlay that
|
|
3922
|
+
* become a derived overlay that shadows same-named subject fields on later
|
|
3913
3923
|
* passes (derived keys are `formatField(check.field)` strings). Convergence —
|
|
3914
3924
|
* one full pass with no new derivation — appends a trace containing
|
|
3915
3925
|
* `converged`. Final rule results re-evaluate against the settled overlay in
|
|
3916
|
-
*
|
|
3917
|
-
* overall `conclusion` is whether the
|
|
3918
|
-
* proves
|
|
3926
|
+
* original rule order (forward) / priority-sorted order (backward), and the
|
|
3927
|
+
* overall `conclusion` is whether the last result applied. Backward chaining
|
|
3928
|
+
* proves every rule goal-first: an unmet premise triggers sub-goal search
|
|
3919
3929
|
* through rules whose conclusion atoms assert the needed `field = value` pair,
|
|
3920
3930
|
* guarded by a visited-rule set (cycle-safe) plus the depth cap; `not` succeeds
|
|
3921
3931
|
* when its operand cannot be established (negation-as-failure) and `implies` is
|
|
3922
|
-
* vacuously true on an unprovable antecedent. Expression evaluation is
|
|
3923
|
-
* (no short-circuit); conclusion extraction
|
|
3932
|
+
* vacuously true on an unprovable antecedent. Expression evaluation is eager
|
|
3933
|
+
* (no short-circuit); conclusion extraction ignores connectives — every atom
|
|
3924
3934
|
* inside a conclusion is asserted, even under `not` / `or`. Overlay bookkeeping
|
|
3925
3935
|
* compares with SameValueZero (`equalValues`), so a NaN-valued conclusion
|
|
3926
3936
|
* derives once and the fixpoint converges. Nothing mutates its inputs; fully
|
|
@@ -3993,7 +4003,7 @@ var LogicalReasoner = class {
|
|
|
3993
4003
|
definition: definition.id,
|
|
3994
4004
|
reasoning: this.reasoning
|
|
3995
4005
|
});
|
|
3996
|
-
if (!
|
|
4006
|
+
if (!isArray(definition.rules)) return {
|
|
3997
4007
|
reasoning: "logical",
|
|
3998
4008
|
conclusion: false,
|
|
3999
4009
|
rules: [],
|
|
@@ -4022,7 +4032,7 @@ var LogicalReasoner = class {
|
|
|
4022
4032
|
const sortedRules = sortByPriority(definition.rules);
|
|
4023
4033
|
const reportedErrors = /* @__PURE__ */ new Set();
|
|
4024
4034
|
for (const rule of sortedRules) {
|
|
4025
|
-
if (
|
|
4035
|
+
if (!isObject(rule)) continue;
|
|
4026
4036
|
if (rule.enabled === false) continue;
|
|
4027
4037
|
if (!rule.premises || rule.premises.length === 0) {
|
|
4028
4038
|
errors.push(`Rule "${rule.id}" has no premises — skipped`);
|
|
@@ -4064,7 +4074,7 @@ var LogicalReasoner = class {
|
|
|
4064
4074
|
};
|
|
4065
4075
|
const finalResults = [];
|
|
4066
4076
|
for (const rule of definition.rules) {
|
|
4067
|
-
if (
|
|
4077
|
+
if (!isObject(rule)) continue;
|
|
4068
4078
|
if (rule.enabled === false) continue;
|
|
4069
4079
|
if (reportedErrors.has(rule.id)) continue;
|
|
4070
4080
|
finalResults.push(this.#evaluateRule(rule, finalSubject));
|
|
@@ -4080,9 +4090,9 @@ var LogicalReasoner = class {
|
|
|
4080
4090
|
const ruleResults = /* @__PURE__ */ new Map();
|
|
4081
4091
|
if (definition.rules.length === 0) trace.push("No rules defined");
|
|
4082
4092
|
const sortedRules = sortByPriority(definition.rules.filter((rule) => {
|
|
4083
|
-
if (
|
|
4093
|
+
if (!isObject(rule)) return false;
|
|
4084
4094
|
if (rule.enabled === false) return false;
|
|
4085
|
-
if (!
|
|
4095
|
+
if (!isArray(rule.premises)) {
|
|
4086
4096
|
errors.push(`Rule "${rule.id}" has no premises — skipped`);
|
|
4087
4097
|
return false;
|
|
4088
4098
|
}
|
|
@@ -4093,7 +4103,7 @@ var LogicalReasoner = class {
|
|
|
4093
4103
|
return true;
|
|
4094
4104
|
}));
|
|
4095
4105
|
for (const rule of definition.rules) {
|
|
4096
|
-
if (
|
|
4106
|
+
if (!isObject(rule)) continue;
|
|
4097
4107
|
if (rule.enabled === false) trace.push(`Skipped rule "${rule.id}" (disabled)`);
|
|
4098
4108
|
}
|
|
4099
4109
|
for (const rule of sortedRules) this.#proveRule(rule, 0, /* @__PURE__ */ new Set(), sortedRules, derived, subject, trace, ruleResults, maxDepth);
|
|
@@ -4144,7 +4154,7 @@ var LogicalReasoner = class {
|
|
|
4144
4154
|
#proveExpression(expression, rules, depth, visited, derived, subject, trace, ruleResults, maxDepth) {
|
|
4145
4155
|
if (depth > maxDepth) return false;
|
|
4146
4156
|
if (expression.form === "atom") for (const rule of rules) {
|
|
4147
|
-
if (
|
|
4157
|
+
if (!isObject(rule)) continue;
|
|
4148
4158
|
if (visited.has(rule.id)) continue;
|
|
4149
4159
|
const conclusionFacts = extractConclusions(rule.conclusion);
|
|
4150
4160
|
const field = formatField(expression.check.field);
|
|
@@ -4253,16 +4263,16 @@ var LogicalReasoner = class {
|
|
|
4253
4263
|
* @remarks
|
|
4254
4264
|
* Bindings seed from `definition.variables`, then numeric subject fields (a
|
|
4255
4265
|
* finite number or a numeric string, coerced through the contracts
|
|
4256
|
-
* `parseNumber`)
|
|
4266
|
+
* `parseNumber`) override same-named variables — the `id` field is skipped.
|
|
4257
4267
|
* Equations solve strictly in order: when the `target` is unbound and appears
|
|
4258
|
-
* on exactly
|
|
4268
|
+
* on exactly one side, it is isolated by peeling invertible operations (`add` /
|
|
4259
4269
|
* `subtract` / `multiply` / `divide`); a non-invertible operation, a target on
|
|
4260
4270
|
* both sides of an operation, or an unbound variable throws internally — the
|
|
4261
|
-
* throw is caught
|
|
4271
|
+
* throw is caught per equation and surfaced as a result error (`Equation
|
|
4262
4272
|
* "<id>": <message>` plus a `FAILED` trace) while later equations still run.
|
|
4263
4273
|
* Inversion or division by zero yields `NaN`, caught by the non-finite check.
|
|
4264
|
-
* A solved value is rounded to `precision`
|
|
4265
|
-
* see the rounded value; `solutions` reads
|
|
4274
|
+
* A solved value is rounded to `precision` before binding, so later equations
|
|
4275
|
+
* see the rounded value; `solutions` reads the settled bindings keyed by each
|
|
4266
4276
|
* equation's target (a failed equation's target still appears when bound
|
|
4267
4277
|
* elsewhere). Nothing mutates its inputs; fully deterministic.
|
|
4268
4278
|
*
|
|
@@ -4330,7 +4340,7 @@ var SymbolicReasoner = class {
|
|
|
4330
4340
|
definition: definition.id,
|
|
4331
4341
|
reasoning: this.reasoning
|
|
4332
4342
|
});
|
|
4333
|
-
if (!
|
|
4343
|
+
if (!isArray(definition.equations)) return {
|
|
4334
4344
|
reasoning: "symbolic",
|
|
4335
4345
|
solutions: {},
|
|
4336
4346
|
success: false,
|
|
@@ -4363,12 +4373,12 @@ var SymbolicReasoner = class {
|
|
|
4363
4373
|
};
|
|
4364
4374
|
}
|
|
4365
4375
|
for (const equation of definition.equations) {
|
|
4366
|
-
if (
|
|
4376
|
+
if (!isObject(equation)) continue;
|
|
4367
4377
|
try {
|
|
4368
4378
|
const value = this.#solve(equation, bindings);
|
|
4369
4379
|
const rounded = roundTo(value, precision);
|
|
4370
|
-
if (!
|
|
4371
|
-
const offender =
|
|
4380
|
+
if (!isFiniteNumber(value) || !isFiniteNumber(rounded)) {
|
|
4381
|
+
const offender = isFiniteNumber(value) ? rounded : value;
|
|
4372
4382
|
const description = Number.isNaN(offender) ? "NaN" : `${offender}`;
|
|
4373
4383
|
errors.push(`Equation "${equation.id}": produced non-finite value (${description})`);
|
|
4374
4384
|
trace.push(`Equation "${equation.id}": FAILED — produced non-finite value (${description})`);
|
|
@@ -4377,14 +4387,14 @@ var SymbolicReasoner = class {
|
|
|
4377
4387
|
bindings[equation.target] = rounded;
|
|
4378
4388
|
trace.push(`Equation "${equation.id}": ${equation.target} = ${rounded}`);
|
|
4379
4389
|
} catch (error) {
|
|
4380
|
-
const message = error
|
|
4390
|
+
const message = isError(error) ? error.message : String(error);
|
|
4381
4391
|
errors.push(`Equation "${equation.id}": ${message}`);
|
|
4382
4392
|
trace.push(`Equation "${equation.id}": FAILED — ${message}`);
|
|
4383
4393
|
}
|
|
4384
4394
|
}
|
|
4385
4395
|
const solutions = {};
|
|
4386
4396
|
for (const equation of definition.equations) {
|
|
4387
|
-
if (
|
|
4397
|
+
if (!isObject(equation)) continue;
|
|
4388
4398
|
const value = bindings[equation.target];
|
|
4389
4399
|
if (value !== void 0) solutions[equation.target] = value;
|
|
4390
4400
|
}
|
|
@@ -4455,7 +4465,7 @@ var SymbolicReasoner = class {
|
|
|
4455
4465
|
*
|
|
4456
4466
|
* @remarks
|
|
4457
4467
|
* A string term starting with `?` is a unification variable — matching is
|
|
4458
|
-
* positional and
|
|
4468
|
+
* positional and bidirectional (variables may sit in facts as well as
|
|
4459
4469
|
* patterns), with binding consistency enforced within one match and across
|
|
4460
4470
|
* premises through pre-instantiation. Scalar subject fields (except `id`;
|
|
4461
4471
|
* `null` / `undefined` / objects / arrays skipped) inject as
|
|
@@ -4466,8 +4476,8 @@ var SymbolicReasoner = class {
|
|
|
4466
4476
|
* duplicate facts (same predicate, arity, and SameValueZero-equal terms — a
|
|
4467
4477
|
* NaN term derives once and converges) are never re-derived. Backward chaining
|
|
4468
4478
|
* proves each inference's conclusion in
|
|
4469
|
-
* declaration order and
|
|
4470
|
-
* (confidence = the inference's own — premise confidences are
|
|
4479
|
+
* declaration order and returns on the first success with one derived fact
|
|
4480
|
+
* (confidence = the inference's own — premise confidences are not propagated)
|
|
4471
4481
|
* plus its {@link ProofNode} tree; recursion is guarded only by the depth cap.
|
|
4472
4482
|
* Deriving nothing is still success. Nothing mutates its inputs; fully
|
|
4473
4483
|
* deterministic.
|
|
@@ -4530,11 +4540,11 @@ var InferentialReasoner = class {
|
|
|
4530
4540
|
if (!definition.inferences || definition.inferences.length === 0) warnings.push("Definition has no inference rules");
|
|
4531
4541
|
for (const id of findDuplicates(definition.inferences ?? [])) warnings.push(`Duplicate inference id "${id}"`);
|
|
4532
4542
|
for (const fact of definition.facts ?? []) {
|
|
4533
|
-
if (
|
|
4543
|
+
if (!isObject(fact)) continue;
|
|
4534
4544
|
if (fact.confidence !== void 0 && !(fact.confidence >= 0 && fact.confidence <= 1)) warnings.push(`Fact "${fact.id}" confidence outside [0, 1]`);
|
|
4535
4545
|
}
|
|
4536
4546
|
for (const inference of definition.inferences ?? []) {
|
|
4537
|
-
if (
|
|
4547
|
+
if (!isObject(inference)) continue;
|
|
4538
4548
|
if (!inference.id) errors.push("Inference must have an id");
|
|
4539
4549
|
if (!inference.premises || inference.premises.length === 0) warnings.push(`Inference "${inference.id}" has no premises`);
|
|
4540
4550
|
if (!inference.conclusion) errors.push(`Inference "${inference.id}" must have a conclusion`);
|
|
@@ -4552,7 +4562,7 @@ var InferentialReasoner = class {
|
|
|
4552
4562
|
definition: definition.id,
|
|
4553
4563
|
reasoning: this.reasoning
|
|
4554
4564
|
});
|
|
4555
|
-
if (!definition.facts || !
|
|
4565
|
+
if (!definition.facts || !isArray(definition.facts) || !definition.inferences || !isArray(definition.inferences)) return {
|
|
4556
4566
|
reasoning: "inferential",
|
|
4557
4567
|
derived: [],
|
|
4558
4568
|
success: false,
|
|
@@ -4577,7 +4587,7 @@ var InferentialReasoner = class {
|
|
|
4577
4587
|
const maxDepth = definition.depth ?? 10;
|
|
4578
4588
|
const knownFacts = [];
|
|
4579
4589
|
for (const known of [...definition.facts, ...subjectFacts]) {
|
|
4580
|
-
if (
|
|
4590
|
+
if (!isObject(known)) continue;
|
|
4581
4591
|
knownFacts.push(known);
|
|
4582
4592
|
}
|
|
4583
4593
|
const derived = [];
|
|
@@ -4592,7 +4602,7 @@ var InferentialReasoner = class {
|
|
|
4592
4602
|
}
|
|
4593
4603
|
const validInferences = [];
|
|
4594
4604
|
for (const inference of definition.inferences) {
|
|
4595
|
-
if (
|
|
4605
|
+
if (!isObject(inference)) continue;
|
|
4596
4606
|
if (inference.enabled === false) continue;
|
|
4597
4607
|
if (!inference.premises || inference.premises.length === 0) {
|
|
4598
4608
|
errors.push(`Inference "${inference.id}" has no premises — skipped`);
|
|
@@ -4641,7 +4651,7 @@ var InferentialReasoner = class {
|
|
|
4641
4651
|
const derived = [];
|
|
4642
4652
|
const allBaseFacts = [];
|
|
4643
4653
|
for (const known of [...definition.facts, ...subjectFacts]) {
|
|
4644
|
-
if (
|
|
4654
|
+
if (!isObject(known)) continue;
|
|
4645
4655
|
allBaseFacts.push(known);
|
|
4646
4656
|
}
|
|
4647
4657
|
if (definition.inferences.length === 0) {
|
|
@@ -4649,7 +4659,7 @@ var InferentialReasoner = class {
|
|
|
4649
4659
|
return { derived };
|
|
4650
4660
|
}
|
|
4651
4661
|
for (const inference of definition.inferences) {
|
|
4652
|
-
if (
|
|
4662
|
+
if (!isObject(inference)) continue;
|
|
4653
4663
|
if (inference.enabled === false) continue;
|
|
4654
4664
|
if (!inference.conclusion) {
|
|
4655
4665
|
errors.push(`Inference "${inference.id}" has no conclusion — skipped`);
|
|
@@ -4678,9 +4688,9 @@ var InferentialReasoner = class {
|
|
|
4678
4688
|
depth
|
|
4679
4689
|
};
|
|
4680
4690
|
for (const inference of inferences) {
|
|
4681
|
-
if (
|
|
4691
|
+
if (!isObject(inference)) continue;
|
|
4682
4692
|
if (inference.enabled === false) continue;
|
|
4683
|
-
if (!
|
|
4693
|
+
if (!isArray(inference.premises)) continue;
|
|
4684
4694
|
if (!inference.conclusion) continue;
|
|
4685
4695
|
const conclusionBindings = matchFacts(goal, inference.conclusion);
|
|
4686
4696
|
if (!conclusionBindings) continue;
|
|
@@ -4799,17 +4809,17 @@ var Collection = class {
|
|
|
4799
4809
|
* kind-free manager over a symbolic definition's `equations`.
|
|
4800
4810
|
*
|
|
4801
4811
|
* @remarks
|
|
4802
|
-
*
|
|
4812
|
+
* Owns its `equations` as a private {@link Collection} — copy-on-write state
|
|
4803
4813
|
* shared by composition with the other list managers — plus its own
|
|
4804
4814
|
* {@link Emitter} over {@link EquationManagerEventMap}. Equation order is
|
|
4805
|
-
*
|
|
4806
|
-
*
|
|
4807
|
-
*
|
|
4815
|
+
* load-bearing — equations solve strictly in order and each rounded solution
|
|
4816
|
+
* feeds forward. `remove` is the batch family (array form declared
|
|
4817
|
+
* first): no argument removes every equation, one id removes that equation, an
|
|
4808
4818
|
* id list removes those equations and returns true only when every named id
|
|
4809
4819
|
* existed. It emits one `remove` per equation actually removed.
|
|
4810
4820
|
* `seat` is the owning builder's silent bulk re-seat
|
|
4811
4821
|
* channel (used by `merge`). `destroy()` is idempotent and tears the emitter
|
|
4812
|
-
* down
|
|
4822
|
+
* down last; any other call after it throws `ReasonError('DESTROYED', …)`.
|
|
4813
4823
|
*
|
|
4814
4824
|
* @example
|
|
4815
4825
|
* ```ts
|
|
@@ -4892,17 +4902,17 @@ var EquationManager = class {
|
|
|
4892
4902
|
* manager over an inferential definition's `facts`.
|
|
4893
4903
|
*
|
|
4894
4904
|
* @remarks
|
|
4895
|
-
*
|
|
4905
|
+
* Owns its `facts` as a private {@link Collection} — copy-on-write state shared
|
|
4896
4906
|
* by composition with the other list managers — plus its own {@link Emitter}
|
|
4897
|
-
* over {@link FactManagerEventMap}. `Fact.id` is an
|
|
4907
|
+
* over {@link FactManagerEventMap}. `Fact.id` is an authoring label — the
|
|
4898
4908
|
* runtime content-dedups facts by predicate+arity+terms, independently of this
|
|
4899
4909
|
* manager's id-keyed dedup. `remove` is the batch family (array form declared
|
|
4900
|
-
*
|
|
4910
|
+
* first): no argument removes every fact, one id removes that fact, an id list
|
|
4901
4911
|
* removes those facts and returns true only when every named id existed. It
|
|
4902
4912
|
* emits one `remove` per fact actually removed.
|
|
4903
4913
|
* `seat` is the owning builder's silent bulk re-seat
|
|
4904
4914
|
* channel (used by `merge`). `destroy()` is idempotent and tears the emitter
|
|
4905
|
-
* down
|
|
4915
|
+
* down last; any other call after it throws `ReasonError('DESTROYED', …)`.
|
|
4906
4916
|
*
|
|
4907
4917
|
* @example
|
|
4908
4918
|
* ```ts
|
|
@@ -4976,24 +4986,25 @@ var FactManager = class {
|
|
|
4976
4986
|
//#endregion
|
|
4977
4987
|
//#region src/core/builders/managers/FactorManager.ts
|
|
4978
4988
|
/**
|
|
4979
|
-
* Implements the {@link FactorManagerInterface} — the
|
|
4980
|
-
* manager: factors nest inside groups, so it holds
|
|
4989
|
+
* Implements the {@link FactorManagerInterface} — the one divergent
|
|
4990
|
+
* manager: factors nest inside groups, so it holds no collection state of its
|
|
4981
4991
|
* own and threads a required `groupId` locator.
|
|
4982
4992
|
*
|
|
4983
4993
|
* @remarks
|
|
4984
4994
|
* Constructor-injected with the sibling {@link GroupManagerInterface}: each
|
|
4985
4995
|
* write verb reads the located group (`groups.group(groupId)`), applies the
|
|
4986
|
-
* factor-level pure helper ({@link appendFactor}
|
|
4996
|
+
* factor-level pure helper ({@link appendFactor}, {@link prependFactor},
|
|
4997
|
+
* {@link replaceFactor}, or {@link removeFactor}), and writes the updated
|
|
4987
4998
|
* group back through `groups.replace(…)`. A `groupId` naming no existing group
|
|
4988
|
-
* throws `ReasonError('TARGET', …, { groupId })`. It still owns its
|
|
4999
|
+
* throws `ReasonError('TARGET', …, { groupId })`. It still owns its own
|
|
4989
5000
|
* {@link Emitter} over {@link FactorManagerEventMap} (factor-id payloads).
|
|
4990
5001
|
* `remove` is the batch family behind the leading `groupId` locator (array form
|
|
4991
|
-
* declared
|
|
5002
|
+
* declared first): the locator alone removes every factor of that group, a
|
|
4992
5003
|
* further id removes that factor, a further id list removes those factors and
|
|
4993
5004
|
* returns true only when every named id existed. It emits one `remove` per
|
|
4994
5005
|
* factor actually removed, each paired with a `replace` on the sibling
|
|
4995
5006
|
* `GroupManagerInterface` for the containing group.
|
|
4996
|
-
* `destroy()` is idempotent and tears the emitter down
|
|
5007
|
+
* `destroy()` is idempotent and tears the emitter down last; any other call
|
|
4997
5008
|
* after it throws `ReasonError('DESTROYED', …)`.
|
|
4998
5009
|
*
|
|
4999
5010
|
* @example
|
|
@@ -5092,16 +5103,16 @@ var FactorManager = class {
|
|
|
5092
5103
|
* manager over a quantitative definition's `groups`.
|
|
5093
5104
|
*
|
|
5094
5105
|
* @remarks
|
|
5095
|
-
*
|
|
5106
|
+
* Owns its `groups` as a private {@link Collection} — copy-on-write state
|
|
5096
5107
|
* shared by composition with the other list managers — plus its own
|
|
5097
5108
|
* {@link Emitter} over {@link GroupManagerEventMap}. Every write verb delegates
|
|
5098
|
-
* to the collection, then emits (the affected group id)
|
|
5099
|
-
* `remove` is the batch family (array form declared
|
|
5109
|
+
* to the collection, then emits (the affected group id) after the mutation.
|
|
5110
|
+
* `remove` is the batch family (array form declared first): no argument removes
|
|
5100
5111
|
* every group, one id removes that group, an id list removes those groups and
|
|
5101
5112
|
* returns true only when every named id existed. It emits one `remove` per
|
|
5102
5113
|
* group actually removed.
|
|
5103
5114
|
* `seat` is the owning builder's silent bulk re-seat channel (used by `merge`).
|
|
5104
|
-
* `destroy()` is idempotent and tears the emitter down
|
|
5115
|
+
* `destroy()` is idempotent and tears the emitter down last; any other call
|
|
5105
5116
|
* after it throws `ReasonError('DESTROYED', …)`.
|
|
5106
5117
|
*
|
|
5107
5118
|
* @example
|
|
@@ -5180,16 +5191,16 @@ var GroupManager = class {
|
|
|
5180
5191
|
* kind-free manager over an inferential definition's `inferences`.
|
|
5181
5192
|
*
|
|
5182
5193
|
* @remarks
|
|
5183
|
-
*
|
|
5194
|
+
* Owns its `inferences` as a private {@link Collection} — copy-on-write state
|
|
5184
5195
|
* shared by composition with the other list managers — plus its own
|
|
5185
5196
|
* {@link Emitter} over {@link InferenceManagerEventMap}. Inference order is
|
|
5186
|
-
*
|
|
5187
|
-
* first success. `remove` is the batch family (array form declared
|
|
5197
|
+
* load-bearing — backward proving iterates in declaration order and returns on
|
|
5198
|
+
* first success. `remove` is the batch family (array form declared first): no
|
|
5188
5199
|
* argument removes every inference, one id removes that inference, an id list
|
|
5189
5200
|
* removes those inferences and returns true only when every named id existed.
|
|
5190
5201
|
* It emits one `remove` per inference actually removed.
|
|
5191
5202
|
* `seat` is the owning builder's silent bulk re-seat channel
|
|
5192
|
-
* (used by `merge`). `destroy()` is idempotent and tears the emitter down
|
|
5203
|
+
* (used by `merge`). `destroy()` is idempotent and tears the emitter down last;
|
|
5193
5204
|
* any other call after it throws `ReasonError('DESTROYED', …)`.
|
|
5194
5205
|
*
|
|
5195
5206
|
* @example
|
|
@@ -5278,17 +5289,17 @@ var InferenceManager = class {
|
|
|
5278
5289
|
* manager over a logical definition's `rules`.
|
|
5279
5290
|
*
|
|
5280
5291
|
* @remarks
|
|
5281
|
-
*
|
|
5292
|
+
* Owns its `rules` as a private {@link Collection} — copy-on-write state shared
|
|
5282
5293
|
* by composition with the other list managers — plus its own {@link Emitter}
|
|
5283
|
-
* over {@link RuleManagerEventMap}. Rule order is
|
|
5284
|
-
* conclusion is the
|
|
5294
|
+
* over {@link RuleManagerEventMap}. Rule order is load-bearing — the forward
|
|
5295
|
+
* conclusion is the last declared non-disabled rule, so `append` without a
|
|
5285
5296
|
* `target` makes the new rule the conclusion. `remove` is the batch family
|
|
5286
|
-
* (array form declared
|
|
5297
|
+
* (array form declared first): no argument removes every rule, one id removes
|
|
5287
5298
|
* that rule, an id list removes those rules and returns true only when every
|
|
5288
5299
|
* named id existed. It emits one `remove` per rule actually removed. `seat` is
|
|
5289
5300
|
* the owning builder's
|
|
5290
5301
|
* silent bulk re-seat channel (used by `merge`). `destroy()` is idempotent and
|
|
5291
|
-
* tears the emitter down
|
|
5302
|
+
* tears the emitter down last; any other call after it throws
|
|
5292
5303
|
* `ReasonError('DESTROYED', …)`.
|
|
5293
5304
|
*
|
|
5294
5305
|
* @example
|
|
@@ -5370,17 +5381,17 @@ var RuleManager = class {
|
|
|
5370
5381
|
* unordered record.
|
|
5371
5382
|
*
|
|
5372
5383
|
* @remarks
|
|
5373
|
-
*
|
|
5384
|
+
* Owns its `#variables` record as private copy-on-write state and its own
|
|
5374
5385
|
* {@link Emitter} over {@link VariableManagerEventMap}. The record has no
|
|
5375
5386
|
* placement, so only `add` / `remove` exist (no `append` / `prepend`): `add`
|
|
5376
5387
|
* upserts and emits `add(name)`, `remove` omits the key entirely (never sets
|
|
5377
5388
|
* `undefined`) and emits `remove(name)`. `remove` is the batch family over
|
|
5378
|
-
*
|
|
5389
|
+
* names (array form declared first): no argument removes every variable, one
|
|
5379
5390
|
* name removes that variable, a name list removes those variables and returns
|
|
5380
5391
|
* true only when every named variable existed. It emits one `remove` per
|
|
5381
5392
|
* variable actually removed. `seat` is the owning builder's silent
|
|
5382
5393
|
* bulk re-seat channel (used by `merge`). `destroy()` is idempotent and tears
|
|
5383
|
-
* the emitter down
|
|
5394
|
+
* the emitter down last; any other call after it throws
|
|
5384
5395
|
* `ReasonError('DESTROYED', …)`.
|
|
5385
5396
|
*
|
|
5386
5397
|
* @example
|
|
@@ -5461,22 +5472,22 @@ var VariableManager = class {
|
|
|
5461
5472
|
//#region src/core/builders/DefinitionBuilder.ts
|
|
5462
5473
|
/**
|
|
5463
5474
|
* Implements a stateful workspace builder accumulating a {@link Definition}
|
|
5464
|
-
* through always-present self-owning manager properties: a private
|
|
5465
|
-
*
|
|
5475
|
+
* through always-present self-owning manager properties: a private scalar
|
|
5476
|
+
* envelope (reasoning / id / name plus the kind's scalars) composed with each
|
|
5466
5477
|
* collection read from its manager.
|
|
5467
5478
|
*
|
|
5468
5479
|
* @remarks
|
|
5469
|
-
* Each manager is
|
|
5470
|
-
*
|
|
5471
|
-
*
|
|
5472
|
-
*
|
|
5473
|
-
*
|
|
5480
|
+
* Each manager is the one the caller supplies, or a fresh one seeded from the
|
|
5481
|
+
* seed's matching collection (empty for off-kind collections). Managers are
|
|
5482
|
+
* kind-free — `build()` ignores an off-kind manager, never a `MISMATCH`.
|
|
5483
|
+
* `build()` is total, deterministic, and returns a fresh plain
|
|
5484
|
+
* {@link Definition} each call. `merge(incoming)` requires the same
|
|
5474
5485
|
* `reasoning` (else `MISMATCH`) and distributes incoming scalars into the
|
|
5475
5486
|
* envelope and collections into the managers through the matching `merge*`
|
|
5476
5487
|
* helper. `clear(key)` deletes one optional field of the envelope for the
|
|
5477
5488
|
* instance's `reasoning` (a non-clearable key throws `MISMATCH`). `destroy()`
|
|
5478
5489
|
* cascades to every manager, emits `destroy`, then tears the builder emitter
|
|
5479
|
-
* down
|
|
5490
|
+
* down last; it is idempotent, and post-destroy mutation / build throws
|
|
5480
5491
|
* `ReasonError('DESTROYED', …)`.
|
|
5481
5492
|
*
|
|
5482
5493
|
* @example
|
|
@@ -5658,27 +5669,26 @@ var DefinitionBuilder = class {
|
|
|
5658
5669
|
* flat key-value collection, no managers.
|
|
5659
5670
|
*
|
|
5660
5671
|
* @remarks
|
|
5661
|
-
* `id` is
|
|
5672
|
+
* `id` is optional (`options?.id ?? seed.id`). When present, the builder is
|
|
5662
5673
|
* id-ful: `build()` carries that `id` and `clear()` restores it. When absent,
|
|
5663
|
-
* the builder is
|
|
5664
|
-
*
|
|
5665
|
-
*
|
|
5666
|
-
*
|
|
5667
|
-
*
|
|
5668
|
-
*
|
|
5669
|
-
*
|
|
5670
|
-
*
|
|
5671
|
-
*
|
|
5672
|
-
* `
|
|
5673
|
-
*
|
|
5674
|
-
* `
|
|
5675
|
-
*
|
|
5676
|
-
*
|
|
5677
|
-
*
|
|
5678
|
-
* durable payload each call. Post-destroy mutation throws
|
|
5674
|
+
* the builder is anonymous — `.id` is `undefined` and the accumulated subject
|
|
5675
|
+
* carries no `id` key. `field(key)` / `fields()` are the singular / plural
|
|
5676
|
+
* accessor pair over top-level keys only. `set(key, value)` delegates to
|
|
5677
|
+
* `assignField`; setting `id` throws `ReasonError('MISMATCH', …)`, because the
|
|
5678
|
+
* id is immutable through the entity, id-ful or anonymous alike. `remove` is
|
|
5679
|
+
* the batch family (array form declared first): no argument removes every
|
|
5680
|
+
* non-id field and emits one `remove` per key in key order, one key removes
|
|
5681
|
+
* that field, and a key list removes those fields. Removing `'id'` through a
|
|
5682
|
+
* keyed form throws the same `MISMATCH` for the same reason. `merge(incoming)`
|
|
5683
|
+
* delegates to `mergeSubjects` (incoming-wins, base `id` preserved — plain
|
|
5684
|
+
* {@link Subject} data only). `clear()` removes every non-id field, restoring
|
|
5685
|
+
* `{ id }` when id-ful or an empty record when anonymous. `repeat(count)`
|
|
5686
|
+
* returns `count` deterministic minted-id clones as plain payloads — a pure
|
|
5687
|
+
* read that emits nothing. `build(): Subject` is total, deterministic, and
|
|
5688
|
+
* returns a fresh durable payload each call. Post-destroy mutation throws
|
|
5679
5689
|
* `ReasonError('DESTROYED', …)` — only the `emitter` getter and `destroy`
|
|
5680
5690
|
* itself keep working, mirroring `Reason`. `destroy()` is idempotent and
|
|
5681
|
-
* tears the emitter down
|
|
5691
|
+
* tears the emitter down last.
|
|
5682
5692
|
*
|
|
5683
5693
|
* @example
|
|
5684
5694
|
* ```ts
|
|
@@ -5687,7 +5697,7 @@ var DefinitionBuilder = class {
|
|
|
5687
5697
|
* const applicant = createSubjectBuilder({ id: 'alice', age: 25 })
|
|
5688
5698
|
* applicant.set('region', 'CA')
|
|
5689
5699
|
* applicant.merge({ licensed: true, accidents: 0 }) // incoming wins, id kept
|
|
5690
|
-
* applicant.remove(['accidents']) // batch form first — true when
|
|
5700
|
+
* applicant.remove(['accidents']) // batch form first — true when every key existed
|
|
5691
5701
|
* applicant.field('region') // 'CA'
|
|
5692
5702
|
* applicant.fields() // the live record
|
|
5693
5703
|
* applicant.repeat(2) // ids 'alice-0', 'alice-1'
|
|
@@ -5704,8 +5714,8 @@ var SubjectBuilder = class {
|
|
|
5704
5714
|
#destroyed = false;
|
|
5705
5715
|
constructor(seed, options) {
|
|
5706
5716
|
const id = options?.id ?? seed.id;
|
|
5707
|
-
this.#id =
|
|
5708
|
-
if (
|
|
5717
|
+
this.#id = isString(id) ? id : void 0;
|
|
5718
|
+
if (isString(this.#id)) this.#subject = {
|
|
5709
5719
|
...seed,
|
|
5710
5720
|
id: this.#id
|
|
5711
5721
|
};
|
|
@@ -5751,7 +5761,7 @@ var SubjectBuilder = class {
|
|
|
5751
5761
|
merge(incoming) {
|
|
5752
5762
|
this.#ensureAlive();
|
|
5753
5763
|
const merged = mergeSubjects(this.#subject, incoming);
|
|
5754
|
-
if (
|
|
5764
|
+
if (!isString(this.#id) && Object.hasOwn(merged, "id")) {
|
|
5755
5765
|
const { id: _id, ...rest } = merged;
|
|
5756
5766
|
this.#subject = rest;
|
|
5757
5767
|
} else this.#subject = merged;
|
|
@@ -5759,7 +5769,7 @@ var SubjectBuilder = class {
|
|
|
5759
5769
|
}
|
|
5760
5770
|
clear() {
|
|
5761
5771
|
this.#ensureAlive();
|
|
5762
|
-
this.#subject =
|
|
5772
|
+
this.#subject = isString(this.#id) ? { id: this.#id } : {};
|
|
5763
5773
|
this.#emitter.emit("clear");
|
|
5764
5774
|
}
|
|
5765
5775
|
repeat(count) {
|
|
@@ -5956,7 +5966,7 @@ function createInferentialReasoner(options) {
|
|
|
5956
5966
|
*
|
|
5957
5967
|
* @remarks
|
|
5958
5968
|
* `reasoners` — the initial registry (a later entry of the same reasoning
|
|
5959
|
-
* replaces an earlier one; the orchestrator ships with
|
|
5969
|
+
* replaces an earlier one; the orchestrator ships with no defaults). `bail` —
|
|
5960
5970
|
* `true` (the default) rethrows a reasoner throw after the `error` emit;
|
|
5961
5971
|
* `false` converts it to a failure result. `validate` — validate every
|
|
5962
5972
|
* definition before running it, throwing `INVALID` on failure (default
|
|
@@ -5966,16 +5976,32 @@ function createInferentialReasoner(options) {
|
|
|
5966
5976
|
* @param options - Optional registry, policies, and emitter hooks
|
|
5967
5977
|
* @returns A {@link ReasonInterface}
|
|
5968
5978
|
*
|
|
5969
|
-
* @example
|
|
5979
|
+
* @example Create an orchestrator and score a subject
|
|
5970
5980
|
* ```ts
|
|
5971
|
-
* import {
|
|
5981
|
+
* import {
|
|
5982
|
+
* createFactorGroup,
|
|
5983
|
+
* createFieldFactor,
|
|
5984
|
+
* createQuantitativeDefinition,
|
|
5985
|
+
* createQuantitativeReasoner,
|
|
5986
|
+
* createReason,
|
|
5987
|
+
* createStaticFactor,
|
|
5988
|
+
* } from '@orkestrel/reason'
|
|
5972
5989
|
*
|
|
5973
|
-
* const reason = createReason({
|
|
5974
|
-
*
|
|
5975
|
-
*
|
|
5976
|
-
*
|
|
5977
|
-
*
|
|
5978
|
-
*
|
|
5990
|
+
* const reason = createReason({ reasoners: [createQuantitativeReasoner()] })
|
|
5991
|
+
*
|
|
5992
|
+
* const definition = createQuantitativeDefinition('risk', 'Risk score', [
|
|
5993
|
+
* createFactorGroup('drivers', 'sum', [
|
|
5994
|
+
* createFieldFactor('age', 'age'), // reads subject.age, parseNumber-coerced
|
|
5995
|
+
* createStaticFactor('floor', 10), // a fixed contribution
|
|
5996
|
+
* ]),
|
|
5997
|
+
* ])
|
|
5998
|
+
*
|
|
5999
|
+
* const result = reason.reason({ age: 25 }, definition) // one subject → one result
|
|
6000
|
+
* if (result.reasoning === 'quantitative') result.value // 35 — narrow by the discriminant
|
|
6001
|
+
* result.trace // the step-by-step account of how the value came to be
|
|
6002
|
+
*
|
|
6003
|
+
* reason.supports('quantitative') // true — a reasoner is registered for this reasoning
|
|
6004
|
+
* reason.reasoner('quantitative')?.supports(definition) // the reasoner's own guard, same check
|
|
5979
6005
|
* ```
|
|
5980
6006
|
*/
|
|
5981
6007
|
function createReason(options) {
|
|
@@ -5987,15 +6013,14 @@ function createReason(options) {
|
|
|
5987
6013
|
* `variables`, `facts`, and `inferences` self-owning manager properties.
|
|
5988
6014
|
*
|
|
5989
6015
|
* @remarks
|
|
5990
|
-
* `id`. Default: `seed.id`. Each manager slot
|
|
5991
|
-
*
|
|
5992
|
-
*
|
|
5993
|
-
*
|
|
5994
|
-
*
|
|
5995
|
-
*
|
|
5996
|
-
*
|
|
5997
|
-
*
|
|
5998
|
-
* @param seed - The starting definition (any of the four reasoning kinds)
|
|
6016
|
+
* `id`. Default: `seed.id`. Each manager slot takes the manager the caller
|
|
6017
|
+
* supplies, or a fresh one seeded from the seed's matching collection. `on` —
|
|
6018
|
+
* initial event listeners. `error` — the emitter's listener-error handler.
|
|
6019
|
+
* Mutate through the manager properties (`groups` / `factors` / `rules` /
|
|
6020
|
+
* `equations` / `variables` / `facts` / `inferences`) and `merge` / `clear`,
|
|
6021
|
+
* then call `build()` to produce a fresh, plain {@link Definition} snapshot.
|
|
6022
|
+
*
|
|
6023
|
+
* @param seed - The starting definition, of any reasoning
|
|
5999
6024
|
* @param options - Optional `id` override, manager injections, and emitter hooks
|
|
6000
6025
|
* @returns A {@link DefinitionBuilderInterface}
|
|
6001
6026
|
*
|
|
@@ -6017,8 +6042,8 @@ function createDefinitionBuilder(seed, options) {
|
|
|
6017
6042
|
* {@link Subject}.
|
|
6018
6043
|
*
|
|
6019
6044
|
* @remarks
|
|
6020
|
-
* `id`. Default: `seed.id`;
|
|
6021
|
-
* a string `seed.id` is present the builder is
|
|
6045
|
+
* `id`. Default: `seed.id`; it is optional — when neither `options.id` nor
|
|
6046
|
+
* a string `seed.id` is present the builder is anonymous (`.id` is
|
|
6022
6047
|
* `undefined`, `build()` emits no `id` key). `on` — initial event listeners.
|
|
6023
6048
|
* `error` — the emitter's listener-error handler.
|
|
6024
6049
|
* Mutate through `set` / `remove` / `merge` / `clear`, then call `build()` to
|
|
@@ -6202,7 +6227,7 @@ function createInferenceManager(options) {
|
|
|
6202
6227
|
/**
|
|
6203
6228
|
* Creates a {@link Check} — one field predicate.
|
|
6204
6229
|
*
|
|
6205
|
-
* @param field - The subject field to resolve (a string is
|
|
6230
|
+
* @param field - The subject field to resolve (a string is a single key; an array descends)
|
|
6206
6231
|
* @param operator - The comparison to apply
|
|
6207
6232
|
* @param value - The expected value (any type — the operator decides what is meaningful)
|
|
6208
6233
|
* @returns A fresh check
|
|
@@ -6272,7 +6297,7 @@ function createCompound(operator, operands) {
|
|
|
6272
6297
|
* `enabled` through `overrides`.
|
|
6273
6298
|
*
|
|
6274
6299
|
* @param id - The rule id
|
|
6275
|
-
* @param premises - The expressions that must
|
|
6300
|
+
* @param premises - The expressions that must all hold
|
|
6276
6301
|
* @param conclusion - The expression whose atoms are asserted when they do
|
|
6277
6302
|
* @param overrides - Optional {@link Rule} fields merged over the defaults
|
|
6278
6303
|
* @returns A fresh rule
|
|
@@ -6297,7 +6322,7 @@ function createRule(id, premises, conclusion, overrides) {
|
|
|
6297
6322
|
* Creates a {@link Transform} — one math step.
|
|
6298
6323
|
*
|
|
6299
6324
|
* @remarks
|
|
6300
|
-
* The `operand` key is
|
|
6325
|
+
* The `operand` key is omitted when absent (never set to `undefined`), so the
|
|
6301
6326
|
* transform stays exact-record valid; the transformer then applies its
|
|
6302
6327
|
* per-operation default (`1` for `multiply` / `divide` / `power`, `0` otherwise).
|
|
6303
6328
|
*
|
|
@@ -6323,7 +6348,7 @@ function createTransform(operator, operand) {
|
|
|
6323
6348
|
* Creates a {@link Bounds} — an inclusive numeric clamp.
|
|
6324
6349
|
*
|
|
6325
6350
|
* @remarks
|
|
6326
|
-
* Absent sides are
|
|
6351
|
+
* Absent sides are omitted (never set to `undefined`) — an absent bound is
|
|
6327
6352
|
* unbounded on that side.
|
|
6328
6353
|
*
|
|
6329
6354
|
* @param minimum - The inclusive lower bound
|
|
@@ -6386,7 +6411,7 @@ function createConstant(value) {
|
|
|
6386
6411
|
* Creates an operation {@link SymbolicExpression} node.
|
|
6387
6412
|
*
|
|
6388
6413
|
* @remarks
|
|
6389
|
-
* The `right` key is
|
|
6414
|
+
* The `right` key is omitted when absent — correct for the unary operations
|
|
6390
6415
|
* (`round` / `ceil` / `floor` / `abs`); a binary operation with no `right`
|
|
6391
6416
|
* treats it as the constant `0`.
|
|
6392
6417
|
*
|
|
@@ -6483,7 +6508,7 @@ function createFact(id, predicate, terms, confidence) {
|
|
|
6483
6508
|
* `enabled` through `overrides`.
|
|
6484
6509
|
*
|
|
6485
6510
|
* @param id - The inference id
|
|
6486
|
-
* @param premises - The fact patterns that must
|
|
6511
|
+
* @param premises - The fact patterns that must all unify
|
|
6487
6512
|
* @param conclusion - The fact pattern derived when they do
|
|
6488
6513
|
* @param overrides - Optional {@link Inference} fields merged over the defaults
|
|
6489
6514
|
* @returns A fresh inference
|