@cruba/crubal 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +30 -2
  2. package/dist/bin/crubal.js +70 -4
  3. package/dist/bin/crubal.js.map +1 -1
  4. package/dist/src/access.d.ts +126 -0
  5. package/dist/src/access.js +517 -0
  6. package/dist/src/access.js.map +1 -0
  7. package/dist/src/address.d.ts +29 -0
  8. package/dist/src/address.js +59 -0
  9. package/dist/src/address.js.map +1 -0
  10. package/dist/src/arrival.d.ts +55 -0
  11. package/dist/src/arrival.js +124 -0
  12. package/dist/src/arrival.js.map +1 -0
  13. package/dist/src/bundle.d.ts +66 -0
  14. package/dist/src/bundle.js +753 -0
  15. package/dist/src/bundle.js.map +1 -0
  16. package/dist/src/conditions.d.ts +7 -1
  17. package/dist/src/conditions.js +52 -11
  18. package/dist/src/conditions.js.map +1 -1
  19. package/dist/src/corpus.js +568 -34
  20. package/dist/src/corpus.js.map +1 -1
  21. package/dist/src/defs.d.ts +37 -2
  22. package/dist/src/defs.js +95 -14
  23. package/dist/src/defs.js.map +1 -1
  24. package/dist/src/dictionary.d.ts +15 -0
  25. package/dist/src/dictionary.js +72 -13
  26. package/dist/src/dictionary.js.map +1 -1
  27. package/dist/src/exchange.d.ts +204 -11
  28. package/dist/src/exchange.js +762 -45
  29. package/dist/src/exchange.js.map +1 -1
  30. package/dist/src/forms.d.ts +16 -0
  31. package/dist/src/forms.js +169 -0
  32. package/dist/src/forms.js.map +1 -0
  33. package/dist/src/index.d.ts +19 -6
  34. package/dist/src/index.js +18 -5
  35. package/dist/src/index.js.map +1 -1
  36. package/dist/src/lifecycle.d.ts +84 -0
  37. package/dist/src/lifecycle.js +415 -0
  38. package/dist/src/lifecycle.js.map +1 -0
  39. package/dist/src/log.d.ts +78 -0
  40. package/dist/src/log.js +506 -0
  41. package/dist/src/log.js.map +1 -0
  42. package/dist/src/migrate.d.ts +24 -1
  43. package/dist/src/migrate.js +104 -9
  44. package/dist/src/migrate.js.map +1 -1
  45. package/dist/src/model.d.ts +33 -1
  46. package/dist/src/model.js +158 -2
  47. package/dist/src/model.js.map +1 -1
  48. package/dist/src/packages.d.ts +44 -0
  49. package/dist/src/packages.js +137 -3
  50. package/dist/src/packages.js.map +1 -1
  51. package/dist/src/print-text.js +22 -0
  52. package/dist/src/print-text.js.map +1 -1
  53. package/dist/src/publish.d.ts +27 -0
  54. package/dist/src/publish.js +275 -0
  55. package/dist/src/publish.js.map +1 -0
  56. package/dist/src/read-text.d.ts +2 -0
  57. package/dist/src/read-text.js +72 -4
  58. package/dist/src/read-text.js.map +1 -1
  59. package/dist/src/records.d.ts +32 -0
  60. package/dist/src/records.js +104 -0
  61. package/dist/src/records.js.map +1 -0
  62. package/dist/src/review.d.ts +22 -0
  63. package/dist/src/review.js +198 -0
  64. package/dist/src/review.js.map +1 -0
  65. package/dist/src/rules.d.ts +38 -3
  66. package/dist/src/rules.js +184 -30
  67. package/dist/src/rules.js.map +1 -1
  68. package/dist/src/schedule.d.ts +24 -0
  69. package/dist/src/schedule.js +126 -0
  70. package/dist/src/schedule.js.map +1 -0
  71. package/dist/src/std-packages.d.ts +9 -0
  72. package/dist/src/std-packages.js +268 -0
  73. package/dist/src/std-packages.js.map +1 -0
  74. package/dist/src/store.d.ts +38 -7
  75. package/dist/src/store.js +166 -24
  76. package/dist/src/store.js.map +1 -1
  77. package/dist/src/tree.d.ts +3 -0
  78. package/dist/src/tree.js +36 -0
  79. package/dist/src/tree.js.map +1 -1
  80. package/dist/src/units-table.d.ts +1 -0
  81. package/dist/src/units-table.js +200 -0
  82. package/dist/src/units-table.js.map +1 -0
  83. package/dist/src/units.d.ts +42 -4
  84. package/dist/src/units.js +91 -30
  85. package/dist/src/units.js.map +1 -1
  86. package/dist/src/views.d.ts +116 -6
  87. package/dist/src/views.js +484 -74
  88. package/dist/src/views.js.map +1 -1
  89. package/package.json +5 -3
@@ -0,0 +1,198 @@
1
+ // review.ts: review decisions as writes (spec 9.5, 10.4, 11.9, 14.7; docs/design/completion.md A). A
2
+ // decision is one entry: the `review` write that records it, and the writes it causes: a `define` of the
3
+ // facet, contract or units extension it changed, a value made current from a suggestion or a held bag, a
4
+ // suggestion denied. Nothing here replays or runs; a store does that after the entry lands.
5
+ import { contractJson } from "./bundle.js";
6
+ import { facetJson } from "./dictionary.js";
7
+ import { split } from "./lifecycle.js";
8
+ import { CrubalError } from "./tree.js";
9
+ import { extendUnits } from "./units.js";
10
+ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
11
+ /** The units extension as text, for the units table (11.9). */
12
+ export function unitsExtensionText(measures) {
13
+ return measures
14
+ .map((m) => {
15
+ const lines = [`measure: ${m.measure}`];
16
+ for (const k of ["dimension", "factor", "offset", "rounding", "uncefact"])
17
+ if (m[k] !== undefined)
18
+ lines.push(` ${k}: ${m[k]}`);
19
+ if (Array.isArray(m.spellings) && m.spellings.length)
20
+ lines.push(` spellings: ${m.spellings.join(", ")}`);
21
+ return lines.join("\n");
22
+ })
23
+ .join("\n");
24
+ }
25
+ /**
26
+ * Apply a decision to the draft and return the writes the entry carries beyond what the draft shows:
27
+ * the review write and any define. Throws a CrubalError for a decision that can't be made.
28
+ */
29
+ export function decide(d, store, defs) {
30
+ const review = { ...d };
31
+ const key = d.key;
32
+ if (key.startsWith("facet:")) {
33
+ const f = defs.dict.facets.get(key.slice(6));
34
+ if (!f)
35
+ throw new CrubalError(`${key}: no such facet`);
36
+ const next = { ...f, values: f.values ? f.values.map((v) => ({ ...v, aliases: [...v.aliases] })) : f.values, aliases: [...f.aliases] };
37
+ if (d.decision === "add value") {
38
+ if (!next.values)
39
+ throw new CrubalError(`${key} isn't a choice`);
40
+ if (next.values.some((v) => v.value === d.value))
41
+ throw new CrubalError(`${d.value} is already a value of ${f.key}`);
42
+ next.values.push({ value: String(d.value), aliases: [] });
43
+ }
44
+ else if (d.decision === "add alias") {
45
+ if (!next.values)
46
+ throw new CrubalError(`${key} isn't a choice`);
47
+ const v = next.values.find((x) => x.value === d.value);
48
+ if (!v)
49
+ throw new CrubalError(`${d.value} is not a value of ${f.key}`);
50
+ v.aliases.push(String(d.alias));
51
+ }
52
+ else if (d.decision !== "reject")
53
+ throw new CrubalError(`${key}: ${d.decision} isn't a decision`);
54
+ if (d.decision === "reject")
55
+ return [{ review }];
56
+ defs.dict.facets.set(f.key, next);
57
+ return [{ review }, { define: facetJson(next) }];
58
+ }
59
+ if (key.startsWith("contract:")) {
60
+ const c = defs.contracts.get(key.slice(9));
61
+ if (!c)
62
+ throw new CrubalError(`${key}: no such contract`);
63
+ const next = { ...c, reads: c.reads.map((m) => ({ ...m, values: [...m.values] })), ignore: [...c.ignore] };
64
+ const field = String(d.field);
65
+ if (d.decision === "map") {
66
+ if (!defs.dict.facets.has(String(d.to)) && !String(d.to).includes(":"))
67
+ throw new CrubalError(`${d.to} isn't a facet`);
68
+ next.reads.push({ from: field, to: String(d.to), values: [], line: c.line, valueLines: [] });
69
+ }
70
+ else if (d.decision === "map value") {
71
+ const m = next.reads.find((x) => x.from === field);
72
+ if (!m)
73
+ throw new CrubalError(`${c.name} doesn't read ${field}`);
74
+ m.values.push([String(d.theirs), String(d.ours)]);
75
+ }
76
+ else if (d.decision === "ignore")
77
+ next.ignore.push(field);
78
+ else
79
+ throw new CrubalError(`${key}: ${d.decision} isn't a decision`);
80
+ defs.contracts.set(c.name, next);
81
+ return [{ review }, { define: contractJson(next) }];
82
+ }
83
+ if (key.startsWith("unit:")) {
84
+ const spelling = key.slice(5);
85
+ const measures = [...(defs.units ?? [])];
86
+ if (d.decision === "alias") {
87
+ const have = measures.find((m) => m.measure === d.unit);
88
+ if (have)
89
+ have.spellings = [...(have.spellings ?? []), spelling];
90
+ else
91
+ measures.push({ measure: String(d.unit), spellings: [spelling] });
92
+ }
93
+ else if (d.decision === "new unit") {
94
+ const m = { measure: String(d.unit), dimension: String(d.dimension), factor: String(d.factor), spellings: [spelling] };
95
+ if (d.offset !== undefined)
96
+ m.offset = String(d.offset);
97
+ measures.push(m);
98
+ }
99
+ else if (d.decision === "not a unit")
100
+ return [{ review }];
101
+ else
102
+ throw new CrubalError(`${key}: ${d.decision} isn't a decision`);
103
+ defs.units = measures;
104
+ extendUnits(unitsExtensionText(measures));
105
+ const writes = [{ review }, { define: { units: "extension", measures } }];
106
+ // The facets that accept the unit from now on (4.2 `accepts`), each a define of its own.
107
+ for (const key of Array.isArray(d.accepts) ? d.accepts : []) {
108
+ const f = defs.dict.facets.get(key);
109
+ if (!f)
110
+ throw new CrubalError(`${key} isn't a facet`);
111
+ if (!f.unit)
112
+ throw new CrubalError(`${key} has no unit to accept ${d.unit} into`);
113
+ const unit = String(d.unit);
114
+ if (f.accepts.includes(unit))
115
+ continue;
116
+ const next = { ...f, accepts: [...f.accepts, unit] };
117
+ defs.dict.facets.set(key, next);
118
+ writes.push({ define: facetJson(next) });
119
+ }
120
+ return writes;
121
+ }
122
+ if (key === "suggestion") {
123
+ const r = store.records.get(String(d.record));
124
+ if (!r)
125
+ throw new CrubalError(`no record ${d.record}`);
126
+ const address = String(d.address);
127
+ const list = r.suggestions?.[address] ?? [];
128
+ const pick = d.value === undefined ? list[0] : list.find((s) => JSON.stringify(s.value) === JSON.stringify(d.value));
129
+ if (!pick)
130
+ throw new CrubalError(`${r.id} has no suggestion at ${address}${d.value !== undefined ? ` with that value` : ""}`);
131
+ const rest = list.filter((s) => s !== pick);
132
+ const suggestions = { ...(r.suggestions ?? {}) };
133
+ if (rest.length)
134
+ suggestions[address] = rest;
135
+ else
136
+ delete suggestions[address];
137
+ const next = { ...r, suggestions };
138
+ if (!Object.keys(suggestions).length)
139
+ delete next.suggestions;
140
+ if (d.decision === "accept") {
141
+ // The value with its original source (14.7).
142
+ const source = { origin: String(pick.origin ?? "reported"), via: String(pick.via ?? "") };
143
+ if (typeof pick.owner === "string")
144
+ source.owner = pick.owner;
145
+ next.values = { ...r.values, [address]: pick.value };
146
+ next.sources = { ...(r.sources ?? {}), [address]: source };
147
+ }
148
+ else if (d.decision !== "deny")
149
+ throw new CrubalError(`suggestion: ${d.decision} isn't a decision`);
150
+ store.records.set(r.id, next);
151
+ return [{ review: { ...review, value: pick.value, ...(d.decision === "deny" ? { denied: true } : {}) } }];
152
+ }
153
+ if (key === "held") {
154
+ const r = store.records.get(String(d.record));
155
+ if (!r)
156
+ throw new CrubalError(`no record ${d.record}`);
157
+ const address = String(d.address);
158
+ const h = r.held?.[address];
159
+ if (!h)
160
+ throw new CrubalError(`${r.id} holds nothing at ${address}`);
161
+ const held = { ...(r.held ?? {}) };
162
+ delete held[address];
163
+ const next = { ...r, held };
164
+ if (!Object.keys(held).length)
165
+ delete next.held;
166
+ if (d.decision === "value") {
167
+ const value = d.value === undefined ? h.value : d.value;
168
+ next.values = { ...r.values, [address]: value };
169
+ next.sources = { ...(r.sources ?? {}), [address]: (isObj(h.source) ? h.source : { origin: "declared", via: "review" }) };
170
+ }
171
+ else if (d.decision !== "drop")
172
+ throw new CrubalError(`held: ${d.decision} isn't a decision`);
173
+ store.records.set(r.id, next);
174
+ return [{ review }];
175
+ }
176
+ if (key.startsWith("split:")) {
177
+ const record = key.slice(6);
178
+ // The decision closes the item it answers: the same record, address and value (openItems).
179
+ if (review.record === undefined)
180
+ review.record = record;
181
+ if (d.decision === "not a version")
182
+ return [{ review }];
183
+ if (d.decision !== "split")
184
+ throw new CrubalError(`${key}: ${d.decision} isn't a decision`);
185
+ const instances = Array.isArray(d.instances) ? d.instances : [];
186
+ const values = typeof d.address === "string" && d.value !== undefined ? { [d.address]: d.value } : {};
187
+ split(store, record, String(d.into), { children: instances, values }, { origin: "observed", via: String(d.via ?? "review") });
188
+ return [{ review }];
189
+ }
190
+ throw new CrubalError(`${key}: not a review item kind a decision applies to here`);
191
+ }
192
+ /** The review items still open: every item without a decision that no later decision names. */
193
+ export function openItems(items) {
194
+ const identity = (it) => JSON.stringify(["key", "record", "address", "run", "ref", "field", "value", "step"].map((k) => it[k] ?? null));
195
+ const decided = new Set(items.filter((it) => it.decision !== undefined).map(identity));
196
+ return items.filter((it) => it.decision === undefined && !decided.has(identity(it)));
197
+ }
198
+ //# sourceMappingURL=review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../../src/review.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { type Expr } from "./conditions.ts";
2
- import type { Dictionary, Facet } from "./defs.ts";
2
+ import { type Dictionary, type Facet } from "./defs.ts";
3
3
  import type { Json, JsonObject, Record } from "./model.ts";
4
+ import type { RecordSet } from "./records.ts";
4
5
  export type Strength = "must" | "should" | "suggest";
5
6
  export type Rule = {
6
7
  name: string;
@@ -12,6 +13,7 @@ export type Rule = {
12
13
  then: Expr | null;
13
14
  strength: Strength;
14
15
  because?: string;
16
+ version?: string;
15
17
  /** A production: `key is <value | template | aggregate>`. */
16
18
  produces?: {
17
19
  key: string;
@@ -37,18 +39,51 @@ export type Result = {
37
39
  export declare class Engine {
38
40
  readonly dict: Dictionary;
39
41
  readonly rules: Rule[];
42
+ /** The moment `now` means in conditions; unset means the clock's. */
43
+ now?: string;
40
44
  private store;
41
45
  private order;
42
- constructor(dict: Dictionary, rules: Rule[]);
46
+ /** The records a global rule reads (6.6), when the engine runs inside a store; else its own cache. */
47
+ private readonly records?;
48
+ constructor(dict: Dictionary, rules: Rule[], records?: () => RecordSet);
43
49
  /** The values a record reads: its own over everything above it (1.7). */
44
50
  effective(rec: Record): JsonObject;
45
51
  private applies;
52
+ /** Whether any rollup here reads a lower level (`count of unit::…`): then a child's write concerns its ancestors. */
53
+ get readsLowerLevels(): boolean;
54
+ /** The chain up from a record along `of`, nearest first. */
55
+ ancestorsOf(id: string): string[];
56
+ private recordOf;
57
+ /**
58
+ * The records at a lower level under a record, along `of`: merged ones left out always; archived ones left
59
+ * out unless asked for (6.4: a rollup reads the active records below; a filter naming `status` reads them all).
60
+ */
61
+ private descendants;
62
+ /**
63
+ * Run the productions again for a record as it stands (6.4: recomputed whenever what it reads changes):
64
+ * what a store calls on the ancestors of a changed record, and on a record a plain write touched.
65
+ */
66
+ reproduce(id: string): Result[];
46
67
  /** Where a produced key may be stored: the top of its chain, or a level where it varies. */
47
68
  private storesAt;
48
69
  private states;
49
70
  /** A path a check tests, for reporting. */
50
71
  private pathOf;
72
+ /** `is unique` compares records of the same level, and never one archived as merged (6.6). */
51
73
  private uniqueClash;
74
+ /** Keep a record as it stands, without running the rules: how a store keeps the engine in step (7.4). */
75
+ put(rec: Record): void;
76
+ /** A record as the engine last left it: its values with what the rules produced, and the keys held. */
77
+ get(id: string): {
78
+ rec: Record;
79
+ produced: Set<string>;
80
+ held: Set<string>;
81
+ } | undefined;
82
+ /**
83
+ * Would this write break a `must` rule on a value it touches (6.2)? Nothing is stored. Intake uses it to
84
+ * park such a value as a contradiction citing the rule (9.3) instead of landing it.
85
+ */
86
+ probe(incoming: Record): Result | null;
52
87
  /** Write a record (a create, or values merged onto one already stored). Returns its results. */
53
88
  write(incoming: Record): Result[];
54
89
  private layered;
@@ -56,7 +91,7 @@ export declare class Engine {
56
91
  private text;
57
92
  /** A template (6.4.1): `{a}`, `{a ?? b}`, `{a?}`, `{{`, `}}`. Nothing when a needed value is empty. */
58
93
  template(right: string, vals: JsonObject): string | undefined;
59
- /** A rollup over parts: `worst of parts[*].condition`. */
94
+ /** A rollup over parts (`worst of parts[*].condition`) or over a lower level (`count of unit::cashierfuId`). */
60
95
  rollup(right: string, rec: Record): Json | undefined;
61
96
  /** Completeness (6.9): asked facets with a value ÷ asked facets, per record and per part. */
62
97
  completeness(rec: Record): Result[];
package/dist/src/rules.js CHANGED
@@ -3,10 +3,11 @@
3
3
  // one spelling); and writes run through the rules in order (6.2, 6.4, 6.7, 6.9): refused, held, filled,
4
4
  // derived, flagged, incomplete, complete and completeness.
5
5
  import { evaluate, parseCondition, reads } from "./conditions.js";
6
+ import { aggregateOf, levelStep, rowStep } from "./address.js";
7
+ import { unquote } from "./defs.js";
6
8
  import { resolve } from "./dictionary.js";
7
9
  import { itemText } from "./print-text.js";
8
10
  import { CrubalError, readTree } from "./tree.js";
9
- const AGG = /^(join|sum|best|narrow|worst|deduct|replace|keep|count|least|most) of (\S+)$/;
10
11
  const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
11
12
  function production(thenText, strength) {
12
13
  const m = thenText.match(/^([A-Za-z_][\w:]*) is (.+)$/);
@@ -16,7 +17,7 @@ function production(thenText, strength) {
16
17
  const right = m[2].trim();
17
18
  if (right.startsWith('"'))
18
19
  return { key, kind: "template", right };
19
- if (AGG.test(right))
20
+ if (aggregateOf(right))
20
21
  return { key, kind: "rollup", right };
21
22
  if (right === "unique" || right === "_" || right === "-")
22
23
  return undefined;
@@ -46,7 +47,9 @@ export function readRules(source) {
46
47
  const thenNode = get("then");
47
48
  if (!thenNode)
48
49
  throw new CrubalError(`the rule ${node.value} needs a then`, node.line);
49
- const because = get("because")?.value;
50
+ // A printed `because` holding a comma or a colon comes back quoted (the printer quotes it); the quotes are not its text.
51
+ const because = get("because") ? unquote(get("because")?.value ?? "") : undefined;
52
+ const version = get("version")?.value.trim();
50
53
  out.push(makeRule({
51
54
  name: node.value.trim(),
52
55
  line: node.line,
@@ -55,6 +58,7 @@ export function readRules(source) {
55
58
  thenText: thenNode.value,
56
59
  strength,
57
60
  ...(because ? { because } : {}),
61
+ ...(version ? { version } : {}),
58
62
  }));
59
63
  }
60
64
  return out;
@@ -90,8 +94,12 @@ function readsOf(r) {
90
94
  }
91
95
  }
92
96
  // A rollup reads its parts' addresses (parts[*].condition), not the key it produces on the whole.
93
- if (r.produces?.kind === "rollup")
94
- keys.push(r.produces.right.match(AGG)?.[2] ?? "");
97
+ if (r.produces?.kind === "rollup") {
98
+ const address = aggregateOf(r.produces.right)?.address ?? "";
99
+ // Rows of the record's own compound: the rollup reads that compound (the key before `[*]`).
100
+ const rows = address.match(/^(\w+)\[\*\](?:\.(\w+))?$/);
101
+ keys.push(rows && rows[1] !== "parts" ? rows[1] : address);
102
+ }
95
103
  return keys.map((k) => k.replace(/^\w+::/, ""));
96
104
  }
97
105
  /** Top-level `and`-terms of a condition that pin a key to a set of values. */
@@ -181,9 +189,14 @@ function emptyOf(v) {
181
189
  export class Engine {
182
190
  dict;
183
191
  rules;
192
+ /** The moment `now` means in conditions; unset means the clock's. */
193
+ now;
184
194
  store = new Map();
185
195
  order = [];
186
- constructor(dict, rules) {
196
+ /** The records a global rule reads (6.6), when the engine runs inside a store; else its own cache. */
197
+ records;
198
+ constructor(dict, rules, records) {
199
+ this.records = records;
187
200
  this.dict = dict;
188
201
  this.rules = [...shortcutRules(dict), ...rules];
189
202
  }
@@ -195,11 +208,69 @@ export class Engine {
195
208
  applies(r, rec) {
196
209
  if (r.for === "everywhere")
197
210
  return true;
198
- if (this.dict.classes.has(r.for))
199
- return (rec.wears ?? []).some((w) => w === r.for || resolveUses(this.dict, w).includes(r.for));
200
- if (this.dict.levels.has(r.for))
201
- return rec.level === r.for;
202
- return rec.kind === r.for;
211
+ // A level is written with its prefix (`unit::`, 6.1), the same spelling conditions use.
212
+ const scope = r.for.replace(/::$/, "");
213
+ if (this.dict.classes.has(scope))
214
+ return (rec.wears ?? []).some((w) => w === scope || resolveUses(this.dict, w).includes(scope));
215
+ if (this.dict.levels.has(scope))
216
+ return rec.level === scope;
217
+ return rec.kind === scope;
218
+ }
219
+ /** Whether any rollup here reads a lower level (`count of unit::…`): then a child's write concerns its ancestors. */
220
+ get readsLowerLevels() {
221
+ return this.rules.some((r) => r.produces?.kind === "rollup" && /\b\w+::/.test(r.produces.right));
222
+ }
223
+ /** The chain up from a record along `of`, nearest first. */
224
+ ancestorsOf(id) {
225
+ const out = [];
226
+ const seen = new Set([id]);
227
+ let cur = this.recordOf(id)?.of;
228
+ while (cur && !seen.has(cur)) {
229
+ out.push(cur);
230
+ seen.add(cur);
231
+ cur = this.recordOf(cur)?.of;
232
+ }
233
+ return out;
234
+ }
235
+ recordOf(id) {
236
+ return this.store.get(id)?.rec ?? this.records?.().get(id);
237
+ }
238
+ /**
239
+ * The records at a lower level under a record, along `of`: merged ones left out always; archived ones left
240
+ * out unless asked for (6.4: a rollup reads the active records below; a filter naming `status` reads them all).
241
+ */
242
+ descendants(rec, level, archivedToo = false) {
243
+ const set = this.records?.();
244
+ const out = [];
245
+ const walk = (id, depth) => {
246
+ if (depth > 8)
247
+ return;
248
+ const kids = set ? set.childrenOf(id, "of") : [...this.store.values()].map((s) => s.rec).filter((r) => r.of === id);
249
+ for (const k of kids) {
250
+ if (k.status?.reason === "merged")
251
+ continue;
252
+ if (k.level === level) {
253
+ if (archivedToo || k.status?.state !== "archived")
254
+ out.push(this.store.get(k.id)?.rec ?? k);
255
+ }
256
+ else
257
+ walk(k.id, depth + 1);
258
+ }
259
+ };
260
+ walk(rec.id, 0);
261
+ return out;
262
+ }
263
+ /**
264
+ * Run the productions again for a record as it stands (6.4: recomputed whenever what it reads changes):
265
+ * what a store calls on the ancestors of a changed record, and on a record a plain write touched.
266
+ */
267
+ reproduce(id) {
268
+ const rec = this.recordOf(id);
269
+ if (!rec)
270
+ return [];
271
+ if (!this.store.has(id))
272
+ this.put(rec);
273
+ return this.write({ ...rec, values: {} });
203
274
  }
204
275
  /** Where a produced key may be stored: the top of its chain, or a level where it varies. */
205
276
  storesAt(key, rec) {
@@ -232,23 +303,76 @@ export class Engine {
232
303
  return this.pathOf(e.a);
233
304
  return this.pathOf(e.a);
234
305
  }
306
+ /** `is unique` compares records of the same level, and never one archived as merged (6.6). */
235
307
  uniqueClash(r, rec) {
236
308
  const key = r.thenText.replace(/ is unique$/, "");
237
309
  const mine = rec.values[key];
238
310
  if (emptyOf(mine) || (isObj(mine) && "$empty" in mine))
239
311
  return false;
312
+ const set = this.records?.();
313
+ if (set && mine !== undefined)
314
+ return set.byKey(key, mine, rec.level).some((r) => r.id !== rec.id && r.status?.reason !== "merged");
240
315
  const want = JSON.stringify(mine);
241
- for (const [id, s] of this.store)
242
- if (id !== rec.id && JSON.stringify(s.rec.values[key]) === want)
316
+ for (const [id, s] of this.store) {
317
+ if (id === rec.id || s.rec.level !== rec.level || s.rec.status?.reason === "merged")
318
+ continue;
319
+ if (JSON.stringify(s.rec.values[key]) === want)
243
320
  return true;
321
+ }
244
322
  return false;
245
323
  }
324
+ /** Keep a record as it stands, without running the rules: how a store keeps the engine in step (7.4). */
325
+ put(rec) {
326
+ const was = this.store.get(rec.id);
327
+ this.store.set(rec.id, { rec: { ...rec, values: { ...rec.values } }, produced: new Set(was?.produced ?? []), held: new Set(was?.held ?? []) });
328
+ if (!this.order.includes(rec.id))
329
+ this.order.push(rec.id);
330
+ }
331
+ /** A record as the engine last left it: its values with what the rules produced, and the keys held. */
332
+ get(id) {
333
+ return this.store.get(id);
334
+ }
335
+ /**
336
+ * Would this write break a `must` rule on a value it touches (6.2)? Nothing is stored. Intake uses it to
337
+ * park such a value as a contradiction citing the rule (9.3) instead of landing it.
338
+ */
339
+ probe(incoming) {
340
+ const before = this.store.get(incoming.id);
341
+ const rec = before ? { ...before.rec, values: { ...before.rec.values, ...incoming.values } } : { ...incoming, values: { ...incoming.values } };
342
+ const touched = new Set(Object.keys(incoming.values));
343
+ const none = new Set();
344
+ for (const r of this.rules) {
345
+ if (r.strength !== "must" || r.produces || !this.applies(r, rec))
346
+ continue;
347
+ const vals = this.effective(rec);
348
+ if (r.when && evaluate(r.when, vals, this.states(rec, none), this.now) !== true)
349
+ continue;
350
+ if (r.thenText.endsWith(" is unique")) {
351
+ const key = r.thenText.replace(/ is unique$/, "");
352
+ if (touched.has(key) && this.uniqueClash(r, rec))
353
+ return { record: rec.id, result: "refused", address: key, rule: r.name };
354
+ continue;
355
+ }
356
+ const path = this.pathOf(r.then);
357
+ if (!touched.has(path) || emptyOf(vals[path]))
358
+ continue;
359
+ if (r.then && evaluate(r.then, vals, this.states(rec, none), this.now) === false)
360
+ return { record: rec.id, result: "refused", address: path, rule: r.name };
361
+ }
362
+ return null;
363
+ }
246
364
  /** Write a record (a create, or values merged onto one already stored). Returns its results. */
247
365
  write(incoming) {
248
366
  const before = this.store.get(incoming.id);
249
367
  const rec = before
250
368
  ? { ...before.rec, values: { ...before.rec.values, ...incoming.values }, parts: incoming.parts ?? before.rec.parts }
251
369
  : { ...incoming, values: { ...incoming.values } };
370
+ // A value a record holds (6.7) is evaluated as if present, so a verdict that no longer holds releases it.
371
+ const heldBag = (incoming.held ?? before?.rec?.held ?? {});
372
+ for (const [k, h] of Object.entries(heldBag))
373
+ if (rec.values[k] === undefined && h.value !== undefined)
374
+ rec.values[k] = h.value;
375
+ const heldBefore = new Set([...(before?.held ?? []), ...Object.keys(heldBag)]);
252
376
  const touched = new Set(Object.keys(incoming.values));
253
377
  const produced = new Set(before?.produced ?? []);
254
378
  const held = new Set();
@@ -258,7 +382,7 @@ export class Engine {
258
382
  if (r.strength !== "must" || r.produces || !this.applies(r, rec))
259
383
  continue;
260
384
  const vals = this.effective(rec);
261
- if (r.when && evaluate(r.when, vals, this.states(rec, held)) !== true)
385
+ if (r.when && evaluate(r.when, vals, this.states(rec, held), this.now) !== true)
262
386
  continue;
263
387
  if (r.thenText.endsWith(" is unique")) {
264
388
  const key = r.thenText.replace(/ is unique$/, "");
@@ -273,7 +397,7 @@ export class Engine {
273
397
  const path = this.pathOf(r.then);
274
398
  if (emptyOf(vals[path]))
275
399
  continue; // an empty value is incompleteness, reported below
276
- if (r.then && evaluate(r.then, vals, this.states(rec, held)) === false) {
400
+ if (r.then && evaluate(r.then, vals, this.states(rec, held), this.now) === false) {
277
401
  if (touched.has(path))
278
402
  return [{ record: rec.id, result: "refused", address: path, rule: r.name }];
279
403
  held.add(path);
@@ -296,7 +420,7 @@ export class Engine {
296
420
  const key = gate[1];
297
421
  if (held.has(key) || rec.values[key] !== undefined || !this.storesAt(key, rec))
298
422
  continue;
299
- if (r.when && evaluate(r.when, visible(), this.states(rec, held)) !== true)
423
+ if (r.when && evaluate(r.when, visible(), this.states(rec, held), this.now) !== true)
300
424
  continue;
301
425
  rec.values[key] = { $empty: "n/a" };
302
426
  produced.add(key);
@@ -310,7 +434,7 @@ export class Engine {
310
434
  if (!this.storesAt(key, rec))
311
435
  continue;
312
436
  const vals = visible();
313
- if (r.when && evaluate(r.when, vals, this.states(rec, held)) !== true)
437
+ if (r.when && evaluate(r.when, vals, this.states(rec, held), this.now) !== true)
314
438
  continue;
315
439
  const current = rec.values[key];
316
440
  const mine = produced.has(key);
@@ -324,8 +448,11 @@ export class Engine {
324
448
  results.push({ record: rec.id, result: "filled", address: key, value: v, rule: r.name });
325
449
  continue;
326
450
  }
327
- if (!emptyOf(current) && !mine)
328
- continue; // a person's value: adopted, not overwritten
451
+ // A value the rules produced keeps following them, whichever engine produced it (its source says so);
452
+ // a person's value is adopted and never overwritten.
453
+ const derivedBefore = rec.sources?.[key]?.origin === "derived" && !rec.sources?.[key]?.adopted;
454
+ if (!emptyOf(current) && !mine && !derivedBefore)
455
+ continue;
329
456
  const v = kind === "template" ? this.template(right, vals) : this.rollup(right, rec);
330
457
  if (v === undefined)
331
458
  continue;
@@ -333,17 +460,23 @@ export class Engine {
333
460
  produced.add(key);
334
461
  results.push({ record: rec.id, result: "derived", address: key, value: v, rule: r.name });
335
462
  }
463
+ // 2b. A value held before whose verdict no longer holds comes back (6.7).
464
+ for (const k of heldBefore) {
465
+ if (held.has(k) || rec.values[k] === undefined)
466
+ continue;
467
+ results.push({ record: rec.id, result: "released", address: k, value: rec.values[k] });
468
+ }
336
469
  // 3. Results: incomplete, flagged, complete, completeness.
337
470
  let complete = held.size === 0;
338
471
  const vals = visible();
339
472
  for (const r of this.rules) {
340
473
  if (r.produces || !this.applies(r, rec))
341
474
  continue;
342
- if (r.when && evaluate(r.when, vals, this.states(rec, held)) !== true)
475
+ if (r.when && evaluate(r.when, vals, this.states(rec, held), this.now) !== true)
343
476
  continue;
344
477
  if (r.thenText.endsWith(" is unique"))
345
478
  continue;
346
- const truth = r.then ? evaluate(r.then, vals, this.states(rec, held)) : true;
479
+ const truth = r.then ? evaluate(r.then, vals, this.states(rec, held), this.now) : true;
347
480
  const path = this.pathOf(r.then);
348
481
  const needsValue = path !== undefined && emptyOf(vals[path]);
349
482
  if (r.strength === "must" && (truth === "undetermined" || (truth === false && needsValue))) {
@@ -422,16 +555,37 @@ export class Engine {
422
555
  }
423
556
  return out;
424
557
  }
425
- /** A rollup over parts: `worst of parts[*].condition`. */
558
+ /** A rollup over parts (`worst of parts[*].condition`) or over a lower level (`count of unit::cashierfuId`). */
426
559
  rollup(right, rec) {
427
- const m = right.match(AGG);
428
- if (!m)
560
+ const agg = aggregateOf(right);
561
+ if (!agg)
429
562
  return undefined;
430
- const how = m[1];
431
- const key = m[2].replace(/^parts\[\*\]\./, "");
432
- const items = (rec.parts ?? [])
433
- .map((p) => (isObj(p) && isObj(p.values) ? p.values[key] : undefined))
434
- .filter((v) => v !== undefined && !(isObj(v) && "$empty" in v));
563
+ const { how, address } = agg;
564
+ const where = agg.where ? parseCondition(agg.where) : null;
565
+ const lower = levelStep(address);
566
+ const rows = lower ? null : rowStep(address, this.dict);
567
+ const key = lower ? lower.rest : rows ? (rows.child ?? "") : address.replace(/^parts\[\*\]\./, "");
568
+ let raw;
569
+ if (lower) {
570
+ // A filter over the lower records reads each with its envelope; one that names `status` sees the archived too.
571
+ const namesStatus = where ? reads(where).some((k) => k === "status" || k === "crubal:status") : false;
572
+ const below = this.descendants(rec, lower.level, namesStatus);
573
+ const kept = where ? below.filter((d) => evaluate(where, { ...d.values, id: d.id, status: d.status?.state ?? "active", "crubal:status": d.status?.state ?? "active" }, undefined, this.now) === true) : below;
574
+ raw = kept.map((d) => d.values[key]);
575
+ }
576
+ else if (rows) {
577
+ // The rows of one of the record's own compounds (6.11): a count needs no child; a filter reads each row.
578
+ const list = rec.values[rows.compound];
579
+ const all = (Array.isArray(list) ? list : list === undefined ? [] : [list]).filter(isObj);
580
+ const kept = where ? all.filter((row) => evaluate(where, row, undefined, this.now) === true) : all;
581
+ raw = kept.map((row) => (key ? row[key] : row));
582
+ }
583
+ else {
584
+ const parts = (rec.parts ?? []).filter((p) => isObj(p) && isObj(p.values));
585
+ const kept = where ? parts.filter((p) => evaluate(where, p.values, undefined, this.now) === true) : parts;
586
+ raw = kept.map((p) => p.values[key]);
587
+ }
588
+ const items = raw.filter((v) => v !== undefined && !(isObj(v) && "$empty" in v));
435
589
  if (how === "count")
436
590
  return String(items.length);
437
591
  if (!items.length)
@@ -457,7 +611,7 @@ export class Engine {
457
611
  completeness(rec) {
458
612
  const out = [];
459
613
  const measure = (worn, values) => {
460
- const asked = resolve(this.dict, worn.filter((w) => this.dict.classes.has(w))).filter((l) => !l.when || evaluate(parseCondition(l.when), values) === true);
614
+ const asked = resolve(this.dict, worn.filter((w) => this.dict.classes.has(w))).filter((l) => !l.when || evaluate(parseCondition(l.when), values, undefined, this.now) === true);
461
615
  if (!asked.length)
462
616
  return null;
463
617
  const had = asked.filter((l) => !emptyOf(values[l.facet])).length;