@githonllc/avouch 0.1.6 → 0.1.7
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/SPEC.md +12 -8
- package/dist/src/evaluate.js +3 -1
- package/dist/src/reconcile.d.ts +9 -2
- package/dist/src/reconcile.js +30 -9
- package/package.json +1 -1
package/SPEC.md
CHANGED
|
@@ -203,7 +203,7 @@ Host language comparisons (`null != "A"` is true in JavaScript) are not used. `i
|
|
|
203
203
|
|
|
204
204
|
Quantifiers are two-valued. `exists` is T when some element gives T; `all` is T when every element gives T (an element with F or U makes it F); `none = not exists`. An empty collection gives `exists` F, `all` T, `none` T: navigation results are a closed world, not U. Their bodies should be provably two-valued (R12 quantifier_absorbs_unknown; see Static UNKNOWN analysis).
|
|
205
205
|
|
|
206
|
-
Navigation: a foreign-key link (no `table`, or `table` is an object's `datasource`) follows `via`; a pure link (`table` is no object's `datasource`) reads `snapshot.links[link.id]` rows `{from, to}`. A null `via`, a dangling foreign key (missing or out of scope) and a null optional ref give the empty set; orphan data is a data-integrity question, not U.
|
|
206
|
+
Navigation: a foreign-key link (no `table`, or `table` is an object's `datasource`) follows `via`; a pure link (`table` is no object's `datasource`) reads `snapshot.links[link.id]` rows `{from, to}`. A pure link that is navigated from a row and is not a key of `snapshot.links` is an incomplete snapshot: `decide` throws `snapshot lacks link <id>`. An empty link table is passed as `[]`. A null `via`, a dangling foreign key (missing or out of scope) and a null optional ref give the empty set (navigation from a null optional ref does not read the link); orphan data is a data-integrity question, not U.
|
|
207
207
|
|
|
208
208
|
Known risk (vacuous truth): from an optional ref that is null, `all` and `none` are always T. This is not an UNKNOWN risk and is not reported. Guard first: `and(isNotNull({ref: x}), all(...))`.
|
|
209
209
|
|
|
@@ -296,8 +296,9 @@ Types:
|
|
|
296
296
|
|
|
297
297
|
- `Reported = {result, reason?}`: a result as the runtime or a scenario states it. `reason` is set only for `INVALID_BINDING`. There is no `param`: runtimes do not report it.
|
|
298
298
|
- `RuntimeCase = {id, action, snapshot, inputs: {bindings, actor, scope, now}, reported, expected}`: `id` is generated by the harness; `snapshot` is the typed snapshot taken before the invocation, or `null`; `reported` is what the runtime reported, or `null` (not reported); `expected` is the scenario expectation, or `null` (none).
|
|
299
|
-
- `ReconcileRow`: `{id, action, status: match, result}`, `{id, action, status: mismatch, evaluator, reported, expected}`, or `{id, action, status: cannot_reconcile, reason, detail?}`.
|
|
299
|
+
- `ReconcileRow`: `{id, action, status: match, result, row?}`, `{id, action, status: mismatch, evaluator, reported, expected, row?}`, or `{id, action, status: cannot_reconcile, reason, detail?}`. `row` is set only when the evaluator result is a domain result (step 9).
|
|
300
300
|
- `Decode = (raw, type) => Value | undefined` and `TypeDefect = {object, prop, row, value}`.
|
|
301
|
+
- `LinkColumns = {<link id>: {from, to}}`: for each pure link, the columns of its table that hold the `from` and `to` ids.
|
|
301
302
|
|
|
302
303
|
`reconcile(ont, cases)` gives exactly one row per case, in input order. For each case the first step that applies gives the row:
|
|
303
304
|
|
|
@@ -309,8 +310,9 @@ Types:
|
|
|
309
310
|
| 4 | `snapshot` is `null` | `cannot_reconcile`, `snapshot_missing` |
|
|
310
311
|
| 5 | `decide(ont, action, snapshot, scope, bindings, actor, now)` throws | `cannot_reconcile`, `evaluator_error`; `detail` is the error message |
|
|
311
312
|
| 6 | `reported.result` is `SCOPE_DENIED`, or is neither an invocation result (`INVOCATION_RESULTS`) nor the `result` of a decision row or of `otherwise` of this action | `cannot_reconcile`, `runtime_unmapped`; `detail` is `reported.result` |
|
|
312
|
-
| 7 | the result
|
|
313
|
-
| 8 |
|
|
313
|
+
| 7 | `expected.result` is `SCOPE_DENIED`, or is neither an invocation result (`INVOCATION_RESULTS`) nor the `result` of a decision row or of `otherwise` of this action | `cannot_reconcile`, `expectation_unmapped`; `detail` is `expected.result` |
|
|
314
|
+
| 8 | the result is INDETERMINATE | `cannot_reconcile`, `indeterminate`; `detail` is its reason |
|
|
315
|
+
| 9 | otherwise: `evaluator` is `externalResult` of the result without `param` (a domain result is `{result}`; `INVALID_BINDING` keeps `reason`) | `match` with `result = evaluator` when the three results are pairwise equal; else `mismatch`. When the result is a domain result, the row also has `row`: the `row` that `decide` gave (the index of the decision row, from 0, or `otherwise`). The runtime does not report a row, so `row` is always the row that `decide` chose |
|
|
314
316
|
|
|
315
317
|
Two `Reported` values are equal when `result` is equal and `reason` is equal (both absent counts as equal).
|
|
316
318
|
|
|
@@ -318,11 +320,13 @@ A missing runtime result is never filled in from the decision table; neither is
|
|
|
318
320
|
|
|
319
321
|
Once an invocation is inside the domain path of an action with a decision table, the runtime must report the domain result explicitly. A coarse signal, such as the status of a receipt, must not stand in for it: a coarse value can share a name with the expected result and give a false match. `runtime_missing` (nothing reported) and `runtime_unmapped` (a value the action cannot give) record the two ways this evidence fails.
|
|
320
322
|
|
|
321
|
-
`
|
|
323
|
+
`expectation_unmapped` (an expected result the action cannot give) is a defect of the scenario, not of the runtime or the ontology. Defects of the input (steps 6 and 7) come before the evaluator's own gap (INDETERMINATE, step 8).
|
|
322
324
|
|
|
323
|
-
|
|
325
|
+
`projectSnapshot(ont, stores, decode, linkColumns?)` turns stored rows (keyed by table name) into a typed `Snapshot` (objects keyed by object type, pure links keyed by link id) and returns `{snapshot, defects}`:
|
|
326
|
+
|
|
327
|
+
1. For each link type with a `table` that is not the `datasource` of any object type (a pure link), in array order: when `linkColumns` has no entry for its id, the function throws `pure link <id> is not projected`; a missing store throws `snapshot lacks store <table>`. Otherwise each row is copied with all its columns except `from` and `to`; then `from` gets the value of the column `linkColumns[id].from` and `to` gets the value of the column `linkColumns[id].to`. A mapped column absent from the row is not projected (`decide` throws when it reads it). Link columns are not decoded and give no defects. An entry of `linkColumns` that is not a pure link id is ignored.
|
|
324
328
|
2. For each object type, in key order: a missing store throws `snapshot lacks store <datasource>`. Otherwise each row is copied with all its columns, and then each property `p` with a declared `type` is projected: a column absent from the row is not projected and is not a defect (`decide` throws when it reads it, as for any incomplete snapshot); a null value stays null and is never passed to `decode`; any other value goes through `decode(raw, type)`, and a result that is `undefined` or does not fit the type (`fitsType`) is a defect `{object, prop: p, row: String(row.id), value: raw}`, and the column keeps its raw value; else the column gets the decoded value. Columns without a declared type are kept as they are.
|
|
325
|
-
3. The result is `{snapshot: {objects}, defects}
|
|
329
|
+
3. The result is `{snapshot: {objects, links}, defects}`, with one `links` entry per pure link; when the ontology has no pure link, `snapshot.links` is not set. When `defects` is not empty the snapshot holds raw values for those columns; do not reconcile with it.
|
|
326
330
|
|
|
327
331
|
The physical type mapping is checked here: the profile supplies `decode`, which turns a stored value (for example the integers 0 and 1 of SQLite for a boolean) into a value of the declared type, and `fitsType` checks the result.
|
|
328
332
|
|
|
@@ -330,7 +334,7 @@ The physical type mapping is checked here: the profile supplies `decode`, which
|
|
|
330
334
|
|
|
331
335
|
### Existence is not revealed
|
|
332
336
|
|
|
333
|
-
`SCOPE_DENIED` is internal. `externalResult` maps it and `INVALID_BINDING` `not_found` to one value, `{result: INVALID_BINDING, param, reason: not_found}`, so a caller cannot learn that an id exists in another tenant. Other reasons, such as `type`, are reported as they are. A runtime must not report `SCOPE_DENIED`; `reconcile` gives `runtime_unmapped` for it.
|
|
337
|
+
`SCOPE_DENIED` is internal. `externalResult` maps it and `INVALID_BINDING` `not_found` to one value, `{result: INVALID_BINDING, param, reason: not_found}`, so a caller cannot learn that an id exists in another tenant. Other reasons, such as `type`, are reported as they are. A runtime must not report `SCOPE_DENIED`; `reconcile` gives `runtime_unmapped` for it. A scenario must not expect it either; `reconcile` gives `expectation_unmapped` for it (step 7).
|
|
334
338
|
|
|
335
339
|
### Known limit
|
|
336
340
|
|
package/dist/src/evaluate.js
CHANGED
|
@@ -77,7 +77,9 @@ class Session {
|
|
|
77
77
|
linkRows(link) {
|
|
78
78
|
let rows = this.links.get(link.id);
|
|
79
79
|
if (rows === undefined) {
|
|
80
|
-
const all = own(this.env.snapshot.links, link.id)
|
|
80
|
+
const all = own(this.env.snapshot.links, link.id);
|
|
81
|
+
if (all === undefined)
|
|
82
|
+
throw new Error(`snapshot lacks link ${link.id}`);
|
|
81
83
|
const by = this.ont.scope?.by;
|
|
82
84
|
const tenantEnd = this.mode(link.from) === "tenant" || this.mode(link.to) === "tenant";
|
|
83
85
|
rows = by !== undefined && tenantEnd ? all.filter((r) => field(r, by, `${link.id} link row`) === this.env.scope) : all;
|
package/dist/src/reconcile.d.ts
CHANGED
|
@@ -18,12 +18,13 @@ export interface RuntimeCase {
|
|
|
18
18
|
reported: Reported | null;
|
|
19
19
|
expected: Reported | null;
|
|
20
20
|
}
|
|
21
|
-
export type CannotReconcile = "envelope" | "runtime_missing" | "expectation_missing" | "snapshot_missing" | "evaluator_error" | "runtime_unmapped" | "indeterminate";
|
|
21
|
+
export type CannotReconcile = "envelope" | "runtime_missing" | "expectation_missing" | "snapshot_missing" | "evaluator_error" | "runtime_unmapped" | "indeterminate" | "expectation_unmapped";
|
|
22
22
|
export type ReconcileRow = {
|
|
23
23
|
id: string;
|
|
24
24
|
action: string;
|
|
25
25
|
status: "match";
|
|
26
26
|
result: Reported;
|
|
27
|
+
row?: number | "otherwise";
|
|
27
28
|
} | {
|
|
28
29
|
id: string;
|
|
29
30
|
action: string;
|
|
@@ -31,6 +32,7 @@ export type ReconcileRow = {
|
|
|
31
32
|
evaluator: Reported;
|
|
32
33
|
reported: Reported;
|
|
33
34
|
expected: Reported;
|
|
35
|
+
row?: number | "otherwise";
|
|
34
36
|
} | {
|
|
35
37
|
id: string;
|
|
36
38
|
action: string;
|
|
@@ -46,7 +48,12 @@ export type TypeDefect = {
|
|
|
46
48
|
row: string;
|
|
47
49
|
value: unknown;
|
|
48
50
|
};
|
|
49
|
-
|
|
51
|
+
/** For each pure link id: the columns of its table that hold the `from` and `to` ids. */
|
|
52
|
+
export type LinkColumns = Readonly<Record<string, {
|
|
53
|
+
readonly from: string;
|
|
54
|
+
readonly to: string;
|
|
55
|
+
}>>;
|
|
56
|
+
export declare function projectSnapshot(ont: Stage1Ontology, stores: Readonly<Record<string, readonly Readonly<Record<string, unknown>>[]>>, decode: Decode, linkColumns?: LinkColumns): {
|
|
50
57
|
snapshot: Snapshot;
|
|
51
58
|
defects: TypeDefect[];
|
|
52
59
|
};
|
package/dist/src/reconcile.js
CHANGED
|
@@ -3,12 +3,27 @@ import { decide, externalResult } from "./evaluate.js";
|
|
|
3
3
|
import { fitsType, INVOCATION_RESULTS, own } from "./expr.js";
|
|
4
4
|
// Stored rows (physical values) -> a typed snapshot. Each property with a declared type is decoded and then checked with
|
|
5
5
|
// fitsType; a value that cannot be decoded or does not fit is a defect and keeps its raw value. Null is never decoded.
|
|
6
|
-
// An absent column is not projected (decide throws when it reads it: an incomplete snapshot).
|
|
7
|
-
export function projectSnapshot(ont, stores, decode) {
|
|
6
|
+
// An absent column is not projected (decide throws when it reads it: an incomplete snapshot). A pure link is projected only with a linkColumns entry; without one it fails closed.
|
|
7
|
+
export function projectSnapshot(ont, stores, decode, linkColumns) {
|
|
8
8
|
const datasources = new Set(Object.values(ont.objectTypes).map((o) => o.datasource));
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const links = {};
|
|
10
|
+
for (const l of ont.linkTypes) {
|
|
11
|
+
if (l.table === undefined || datasources.has(l.table))
|
|
12
|
+
continue;
|
|
13
|
+
const cols = own(linkColumns, l.id);
|
|
14
|
+
if (cols === undefined)
|
|
11
15
|
throw new Error(`pure link ${l.id} is not projected`);
|
|
16
|
+
if (!Object.hasOwn(stores, l.table))
|
|
17
|
+
throw new Error(`snapshot lacks store ${l.table}`);
|
|
18
|
+
links[l.id] = stores[l.table].map((stored) => {
|
|
19
|
+
const { from: _from, to: _to, ...row } = stored;
|
|
20
|
+
if (Object.hasOwn(stored, cols.from) && stored[cols.from] !== undefined)
|
|
21
|
+
row.from = stored[cols.from];
|
|
22
|
+
if (Object.hasOwn(stored, cols.to) && stored[cols.to] !== undefined)
|
|
23
|
+
row.to = stored[cols.to];
|
|
24
|
+
return row;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
12
27
|
const objects = {};
|
|
13
28
|
const defects = [];
|
|
14
29
|
for (const [object, o] of Object.entries(ont.objectTypes)) {
|
|
@@ -29,7 +44,7 @@ export function projectSnapshot(ont, stores, decode) {
|
|
|
29
44
|
return row;
|
|
30
45
|
});
|
|
31
46
|
}
|
|
32
|
-
return { snapshot: { objects }, defects };
|
|
47
|
+
return { snapshot: Object.keys(links).length > 0 ? { objects, links } : { objects }, defects };
|
|
33
48
|
}
|
|
34
49
|
// Strict on reason: both absent is equal; INVALID_BINDING with different reasons is not.
|
|
35
50
|
const same = (a, b) => a.result === b.result && a.reason === b.reason;
|
|
@@ -55,6 +70,7 @@ export function domainResults(ont, actionId) {
|
|
|
55
70
|
// expectation is reported as such, never filled in from the decision table. Envelope results are not decisions (the
|
|
56
71
|
// evaluator does not implement the envelope; a replay does not recompute domain conditions), so decide is not called.
|
|
57
72
|
// A reported result that the action cannot give (runtime_unmapped) means the runtime did not report its domain result.
|
|
73
|
+
// An expected result that the action cannot give (expectation_unmapped) is a defect of the scenario.
|
|
58
74
|
export function reconcile(ont, cases) {
|
|
59
75
|
return cases.map((c) => {
|
|
60
76
|
const head = { id: c.id, action: c.action };
|
|
@@ -76,14 +92,19 @@ export function reconcile(ont, cases) {
|
|
|
76
92
|
catch (e) {
|
|
77
93
|
return cannot("evaluator_error", e instanceof Error ? e.message : String(e));
|
|
78
94
|
}
|
|
79
|
-
// SCOPE_DENIED
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
// A result this action can give: not SCOPE_DENIED (internal: SPEC section 9, Existence is not revealed), and an
|
|
96
|
+
// invocation result or one of its domain results. Defects of the input (runtime, then scenario) come before INDETERMINATE.
|
|
97
|
+
const results = domainResults(ont, c.action);
|
|
98
|
+
const mapped = (r) => r !== "SCOPE_DENIED" && (INVOCATION.includes(r) || results.includes(r));
|
|
99
|
+
if (!mapped(reported.result))
|
|
82
100
|
return cannot("runtime_unmapped", reported.result);
|
|
101
|
+
if (!mapped(expected.result))
|
|
102
|
+
return cannot("expectation_unmapped", expected.result);
|
|
83
103
|
if (d.layer === "indeterminate")
|
|
84
104
|
return cannot("indeterminate", d.reason);
|
|
85
105
|
const { result, reason } = externalResult(d);
|
|
86
106
|
const evaluator = reason !== undefined ? { result, reason } : { result };
|
|
87
|
-
|
|
107
|
+
const row = d.layer === "domain" ? { row: d.row } : {};
|
|
108
|
+
return same(evaluator, reported) && same(evaluator, expected) ? { ...head, status: "match", result: evaluator, ...row } : { ...head, status: "mismatch", evaluator, reported, expected, ...row };
|
|
88
109
|
});
|
|
89
110
|
}
|