@littlebigbrain/create 0.5.0 → 0.6.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.
@@ -10,9 +10,12 @@ others to a person: one review item per instance.
10
10
 
11
11
  The LBB-run version is the starter workflow `review@v1`
12
12
  (`docs/reference/starter-workflows.md`, "Review"; design
13
- `docs/architecture/document-output-mapping.md`). This template is the same
14
- decision in TypeScript: the workflow in `workflow.ts`, the rules and the
15
- facts in `decision.ts`. It calls no model.
13
+ `docs/architecture/document-output-mapping.md`). This template takes its
14
+ rules, notes, counters and review items: the workflow in `workflow.ts`, the
15
+ rules and the facts in `decision.ts`. It calls no model. Its eval runs both
16
+ on the samples and on a fixture of every rule, and finds the same
17
+ decisions, notes, facts and items
18
+ ([`../_evals/README.md`](../_evals/README.md)).
16
19
 
17
20
  ## How it runs
18
21
 
@@ -32,29 +35,36 @@ proposal is decided once: one that has a `fact_status` already is skipped.
32
35
  `rdfs:label`, a relation between two instances (the entry's `DENOTES`
33
36
  included), or a property value in its type. The ends of a relation must fit
34
37
  its domain and range, superclasses included.
35
- 3. **The decision** (`decide`), in this order:
38
+ 3. **The decision** (`decide`), in this order, with LBB's notes:
36
39
  - **refused** when the graph cannot hold the statement: a predicate that
37
40
  is no relation or property of the records ontology (nor `rdf:type` or
38
- `rdfs:label`), a value of another type, an object that is no instance,
39
- an instance without a class, an end its relation does not take, a second
40
- class, or a statement about a proposal;
41
+ `rdfs:label`), a value that does not take its property's type (LBB's
42
+ words: `` `vierzehn` is not a number``), an object that is no
43
+ instance, an instance without a class, an end its relation does not
44
+ take, a second class, or a statement about a proposal;
41
45
  - **to a person** when a rule of `alwaysReview` names it (`new_instance`:
42
46
  a statement whose subject or object the graph does not have yet; or a
43
- predicate IRI), when it would replace another writer's value (a records
44
- graph holds one value per property) or name, or when its confidence is
45
- below the gate. The proposal goes into the **review item of its
46
- instance** (`groupOf`): its subject; the object of a link whose subject
47
- the graph has and whose object is new; and always the object of the
48
- trace (`DENOTES`). One item holds every proposal of its instance that
49
- the turn sends to a person, in order: the type, the label, values,
50
- links, the trace (`fileItem`). Its title names the instance (`New
47
+ predicate IRI), when it would replace another writer's value or name, or
48
+ when its gate value is below the gate. The gate value is the
49
+ proposal's confidence, or the typing answer's probability `p` of a
50
+ source whose link is in the graph already (`gateValue`). The proposal
51
+ goes into the **review item of its instance** (`groupOf`): its subject;
52
+ the object of a link whose subject the graph has and whose object is
53
+ new; and always the object of the trace (`DENOTES`). One item holds every
54
+ proposal of its instance that the turn sends to a person, in order: the
55
+ type, the label, values, links, the trace (`fileItem`). The item is
56
+ LBB's: its key (`starter:<workflow>:instance:<hash of the
57
+ instance>:<hash of its proposals>`, `itemKey`), its title (`New
51
58
  Equipment M 7 Medienwagen`, or `Room Raum 0.12 Klassenraum: 1 fact to
52
- check`), its rationale says why, and it has one evidence sample per
53
- fact (what it states, its confidence, its line). Its facts are each
54
- statement and its proposal's `fact_status: accepted`, and each sample
55
- names the rows and triplets of its fact (`row`, `rows`, `triplets`), so
56
- a person can leave facts out of the accept. The type of a new instance
57
- is a required row: a person keeps it or dismisses the item;
59
+ check`), its rationale, its summary (`is a Equipment · label · M 7
60
+ Medienwagen · 1 link`), one status row per fact (the proposal's
61
+ `fact_status: accepted`) and one evidence sample per fact (what it
62
+ states, its gate value, its line, the other new instance a link waits
63
+ for). The type of a new instance is a required row: a person keeps it or
64
+ dismisses the item. After the status rows the item holds each
65
+ statement, so its accept commits the facts (design G8); each sample names
66
+ the rows and triplets of its fact (`rows`, `triplets`), so a person can
67
+ leave facts out of the accept;
58
68
  - **written** otherwise: a relation as an edge whose evidence names the
59
69
  proposal (`source_id` is the proposal's IRI, the text its evidence); a
60
70
  property as its value; a new instance's `rdf:type` as the instance of
@@ -64,11 +74,17 @@ proposal is decided once: one that has a `fact_status` already is skipped.
64
74
  name does not depend on which statement is written first. A label is a
65
75
  text: an IRI or an empty label is refused. A statement the graph holds
66
76
  already is not written again. Nothing is taken back, a name included: a
67
- records instance keeps one.
77
+ records instance keeps one. When the graph refuses the commit, the
78
+ proposal is `refused` with `The graph refused the statement: …`.
68
79
  4. **Write** (`writeOwned`): the proposal's `fact_status` (`written`,
69
80
  `in_review` or `refused`) and `review_note`, owned by this workflow, and in
70
81
  the same commit the facts of a written statement.
71
82
 
83
+ The state counts what LBB's version counts: `proposals`, `written` (a
84
+ statement the graph held already included), `held_already`, `in_review`,
85
+ `refused`, `new_instance` (proposals for a person that name a new instance)
86
+ and `items`.
87
+
72
88
  ## Settings
73
89
 
74
90
  `defaults` in `workflow.ts`, those of LBB's `review`:
@@ -94,7 +110,8 @@ beside an LBB-run `review` on the same graph: they own the same properties.
94
110
  only; its sweep writes each kept fact, the type first, and dismisses the
95
111
  facts a person left out. A link to another new instance waits until that
96
112
  instance's item is accepted, and is dismissed with it. The template's item
97
- commits its facts on accept, so such a link makes the other instance too.
113
+ commits its facts on accept (design G8), so such a link makes the other
114
+ instance too.
98
115
  - **One item per instance over time.** Its items follow the open proposals
99
116
  of an instance across messages: when they change, it supersedes the open
100
117
  item with a new one, and an accept of the old item fails (409). The
@@ -122,16 +139,26 @@ beside an LBB-run `review` on the same graph: they own the same properties.
122
139
  graph kinds: the records ontology's relations (the records `DENOTES`
123
140
  included), properties and classes, and `rdfs:label`. An instance is new
124
141
  when it has no class of the records ontology. It refuses any other
125
- predicate with a note that says so (`owl:sameAs`, `lbb:denotes`).
142
+ predicate with LBB's note (`owl:sameAs`, `lbb:denotes`).
126
143
  - **Ownership by statement.** Its facts count as owned by statement, so it
127
144
  conflicts with no other workflow on the predicates it writes. The template
128
145
  owns `fact_status` and `review_note` only and writes the facts without
129
146
  owning them.
130
- - **The graph refuses.** It lets the commit refuse a statement (a domain, a
131
- range) and marks the proposal `refused`. The template checks domain and
132
- range before it writes; another refusal stops the turn.
133
- - **Status and preview.** Counters per outcome, the open items it follows,
134
- and a preview of what it would decide.
147
+ - **Status and preview.** The open items it follows in its status, and a
148
+ preview of what it would decide.
149
+
150
+ The template differs from it in two places, and its eval names both:
151
+
152
+ - **Domain and range.** LBB's review leaves them to the commit, and a
153
+ records graph commits such an edge, so it writes p11 of the samples (a
154
+ room as the equipment of a room). The template checks a relation's ends
155
+ against its domain and range, superclasses included, and refuses it, as
156
+ the answer key says.
157
+ - **What it reads.** The template reads the graph with the commits of its
158
+ own turn; LBB's review reads the published generation. A statement written
159
+ earlier in the same turn (the label of a new instance written with its
160
+ type) counts as `held_already` in the template, and LBB's review writes it
161
+ again. The facts are the same.
135
162
 
136
163
  ## Samples and test
137
164
 
@@ -1,7 +1,14 @@
1
1
  // The parts of review that need no I/O: what a proposal's statement is in the graph's records
2
- // ontology, the decision (write, a person, or refused), and the facts that write it. Adapted
3
- // from the LBB-run `review@v1` (database/crates/lbb-graph-query/src/review.rs).
4
- import { ENTITY_ID_KEY_SENTINEL, ENTITY_IRI_NAMESPACE, type Schemas } from "@littlebigbrain/client";
2
+ // ontology, the decision (write, a person, or refused), and the facts that write it. The rules,
3
+ // notes and item shapes are those of the LBB-run `review@v1`
4
+ // (database/crates/lbb-graph-query/src/review.rs); its eval (sdk/templates/_evals) runs both on
5
+ // the samples.
6
+ import {
7
+ ENTITY_ID_KEY_SENTINEL,
8
+ ENTITY_IRI_NAMESPACE,
9
+ stableHashHex,
10
+ type Schemas,
11
+ } from "@littlebigbrain/client";
5
12
 
6
13
  export const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
7
14
  export const RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label";
@@ -125,25 +132,76 @@ const ENTITY = /^https:\/\/littlebigbrain\.com\/e\/[0-9a-f]{32}$/;
125
132
  /** Whether an IRI names an instance of a records graph. */
126
133
  export const isEntity = (iri: string) => ENTITY.test(iri.trim());
127
134
 
128
- /** A value in its property's type, or null when it does not take the type. */
129
- export function coerce(raw: string, type: string): string | number | boolean | null {
135
+ const NUMBER = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;
136
+ const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
137
+
138
+ /** A value in its property's type, or why it does not take the type (LBB's `coerce_value`). */
139
+ export function coerceValue(
140
+ raw: string,
141
+ type: string,
142
+ ): { value: string | number | boolean } | { error: string } {
130
143
  const text = raw.trim();
131
- if (!text) return null;
132
- const number = Number(text);
144
+ if (!text) return { error: "the value is empty" };
145
+ const cleaned = text.replaceAll(",", ".").replace(/\s+/g, "");
146
+ const number = NUMBER.test(cleaned) ? Number(cleaned) : NaN;
133
147
  switch (type) {
134
148
  case "i64":
135
- return Number.isInteger(number) ? number : null;
149
+ if (!Number.isFinite(number)) return { error: `\`${text}\` is not a number` };
150
+ return Number.isInteger(number)
151
+ ? { value: number }
152
+ : { error: `\`${text}\` is not a whole number` };
136
153
  case "f64":
137
- return Number.isFinite(number) ? number : null;
138
- case "bool":
139
- return text === "true" ? true : text === "false" ? false : null;
140
- case "date_time":
141
- return Number.isNaN(Date.parse(text)) ? null : text;
154
+ return Number.isFinite(number) ? { value: number } : { error: `\`${text}\` is not a number` };
155
+ case "bool": {
156
+ const lower = text.toLowerCase();
157
+ if (["true", "yes", "ja"].includes(lower)) return { value: true };
158
+ if (["false", "no", "nein"].includes(lower)) return { value: false };
159
+ return { error: `\`${text}\` is not true or false` };
160
+ }
161
+ case "datetime":
162
+ case "date_time": {
163
+ const value = new TextEncoder().encode(text).length === 10 ? `${text}T00:00:00Z` : text;
164
+ return RFC3339.test(value) && !Number.isNaN(Date.parse(value))
165
+ ? { value }
166
+ : { error: `\`${text}\` is not a date` };
167
+ }
168
+ case "keyword":
142
169
  default:
143
- return text;
170
+ return { value: text };
144
171
  }
145
172
  }
146
173
 
174
+ /** A value in its property's type, or null when it does not take the type. */
175
+ export function coerce(raw: string, type: string): string | number | boolean | null {
176
+ const coerced = coerceValue(raw, type);
177
+ return "value" in coerced ? coerced.value : null;
178
+ }
179
+
180
+ /**
181
+ * The confidence the review gates a proposal on (LBB's `gate_value`): its `fact_confidence`,
182
+ * unless a source of its evidence is a link the graph holds already (`documents.map` writes
183
+ * `"link": {"written": true}` beside the typing answer's probability `p`); then that `p` when it
184
+ * is higher. A value that is not a finite number reads as 0.
185
+ */
186
+ export function gateValue(confidence: number, evidence: string | undefined): number {
187
+ let value = Number.isFinite(confidence) ? Math.min(1, Math.max(0, confidence)) : 0;
188
+ const text = evidence?.trim();
189
+ if (!text?.startsWith("{")) return value;
190
+ let parsed: unknown;
191
+ try {
192
+ parsed = JSON.parse(text);
193
+ } catch {
194
+ return value;
195
+ }
196
+ const sources = (parsed as { sources?: unknown }).sources;
197
+ for (const source of Array.isArray(sources) ? sources : []) {
198
+ const s = source as { link?: { written?: unknown }; p?: unknown };
199
+ if (s?.link?.written === true && typeof s.p === "number" && Number.isFinite(s.p))
200
+ value = Math.max(value, Math.min(1, Math.max(0, Math.fround(s.p))));
201
+ }
202
+ return value;
203
+ }
204
+
147
205
  /** Everything `classify` reads about a statement's entities and values. */
148
206
  export interface Seen {
149
207
  /** The entities the statement names, by IRI. */
@@ -152,11 +210,16 @@ export interface Seen {
152
210
  current?: string[];
153
211
  }
154
212
 
213
+ /** Why an entity cannot be written: the graph has no instance of it and no proposal types it. */
214
+ export const untyped = (e: Known) =>
215
+ `<${e.iri}> is not an instance of the graph, and no proposal of its rdf:type gives its class`;
216
+
155
217
  /**
156
- * The terms of a statement in a records graph, or why it cannot hold it:
157
- * `rdf:type` of a class, `rdfs:label`, a relation between two instances, a
158
- * property value. `proposalClass` is the class of the proposals: the review
159
- * writes no statement about a proposal.
218
+ * The terms of a statement in a records graph, or why it cannot hold it, in the order and words
219
+ * of LBB's review: `rdf:type` of a class, `rdfs:label`, a relation between two instances, a
220
+ * property value. `proposalClass` is the class of the proposals: the review writes no statement
221
+ * about a proposal. A relation's ends must fit its domain and range, superclasses included (LBB's
222
+ * review leaves that check to the commit).
160
223
  */
161
224
  export function classify(
162
225
  s: Statement,
@@ -169,26 +232,25 @@ export function classify(
169
232
  return refused(`The subject <${s.subject}> is not an instance IRI of this graph.`);
170
233
  const known = (iri: string): Known => seen.entities.get(iri) ?? { iri, exists: false };
171
234
  const subject = known(s.subject);
172
- if (subject.class === proposalClass)
173
- return refused("The review writes no statement about a proposal.");
174
- const untyped = (e: Known) =>
175
- `<${e.iri}> is not an instance of the graph, and no proposal of its rdf:type gives its class.`;
235
+ const aboutProposal = refused("The review writes no statement about a proposal.");
176
236
  const fits = (cls: string | undefined, allowed: string[]) =>
177
237
  !allowed.length || allowed.some((a) => ontology.ancestors.get(cls ?? "")?.has(a));
178
238
  if (s.predicate === RDF_TYPE) {
179
239
  const cls = ontology.classes.get(s.object.trim());
180
240
  if (!cls) return refused(`<${s.object}> is not a class of the graph's ontology.`);
241
+ if (subject.class === proposalClass) return aboutProposal;
181
242
  if (!subject.exists) return { kind: "type", class: cls, subject, holds: false };
182
243
  const holds = ontology.ancestors.get(subject.class ?? "")?.has(cls) ?? false;
183
244
  if (!holds)
184
245
  return refused(
185
- `<${s.subject}> is an instance of ${subject.class} already; a records graph gives an instance one class.`,
246
+ `<${s.subject}> is an instance of ${subject.class ?? ""} already; a records graph gives an instance one class.`,
186
247
  );
187
248
  return { kind: "type", class: cls, subject, holds };
188
249
  }
189
250
  if (s.predicate === RDFS_LABEL) {
190
251
  if (!s.datatype || !s.object.trim())
191
252
  return refused("A label is a text; the object is an IRI or empty.");
253
+ if (subject.class === proposalClass) return aboutProposal;
192
254
  if (!subject.class) return refused(untyped(subject));
193
255
  return { kind: "label", label: s.object.trim(), subject };
194
256
  }
@@ -198,6 +260,7 @@ export function classify(
198
260
  return refused(
199
261
  `The relation ${relation.name} links two instances; the object is not an instance IRI.`,
200
262
  );
263
+ if (subject.class === proposalClass) return aboutProposal;
201
264
  const object = known(s.object.trim());
202
265
  for (const end of [subject, object]) if (!end.class) return refused(untyped(end));
203
266
  if (!fits(subject.class, relation.domain))
@@ -212,21 +275,21 @@ export function classify(
212
275
  }
213
276
  const property = ontology.properties.get(s.predicate);
214
277
  if (property) {
278
+ if (subject.class === proposalClass) return aboutProposal;
279
+ const coerced = coerceValue(s.object, property.type);
280
+ if ("error" in coerced) return refused(coerced.error);
215
281
  if (!subject.class) return refused(untyped(subject));
216
- const value = coerce(s.object, property.type);
217
- if (value === null)
218
- return refused(`"${s.object}" is not a value of ${property.name} (${property.type}).`);
219
282
  return {
220
283
  kind: "property",
221
284
  field: property.name,
222
285
  type: property.type,
223
286
  subject,
224
- value,
225
- current: seen.current ?? [],
287
+ value: coerced.value,
288
+ current: subject.exists ? (seen.current ?? []) : [],
226
289
  };
227
290
  }
228
291
  return refused(
229
- `<${s.predicate}> is not a relation or a property of the graph's records ontology (nor rdf:type or rdfs:label). The template writes with POST /v1/graph/commit, which takes those only.`,
292
+ `<${s.predicate}> is not a relation or a property of the graph's ontology (nor rdf:type or rdfs:label).`,
230
293
  );
231
294
  }
232
295
 
@@ -305,7 +368,7 @@ export function decide(
305
368
  if (t.kind === "label" && t.subject.exists && t.subject.label && t.subject.label !== t.label)
306
369
  return {
307
370
  status: "in_review",
308
- why: `The subject is named ${t.subject.label} already: writing ${t.label} would rename it (a records entity has one name).`,
371
+ why: `The subject is named ${t.subject.label} already: writing ${t.label} would rename it.`,
309
372
  };
310
373
  if (!Number.isFinite(confidence) || confidence < rules.gate)
311
374
  return {
@@ -416,6 +479,8 @@ export interface Shown {
416
479
  kind: string;
417
480
  /** The fact in words, seen from its instance: `is a Room`, `HAS_EQUIPMENT → T 1`. */
418
481
  text: string;
482
+ /** A link to an instance the graph does not have: its name. The link waits for that instance. */
483
+ waitsFor?: string;
419
484
  }
420
485
 
421
486
  /** What an item shows of a fact about the instance `group`. */
@@ -425,21 +490,37 @@ export function shownOf(
425
490
  group: string,
426
491
  ): Shown {
427
492
  const name = (e: Known) => e.label?.trim() || e.iri.slice(e.iri.lastIndexOf("/") + 1);
493
+ const shown = (kind: string, text: string, waitsFor?: string): Shown => ({
494
+ kind,
495
+ text: clip(text, 200),
496
+ ...(waitsFor !== undefined ? { waitsFor } : {}),
497
+ });
428
498
  switch (t.kind) {
429
499
  case "type":
430
- return { kind: "type", text: `is a ${t.class}` };
500
+ return shown("type", `is a ${t.class}`);
431
501
  case "label":
432
- return { kind: "label", text: `label · ${t.label}` };
502
+ return shown("label", `label · ${t.label}`);
433
503
  case "property":
434
- return { kind: "value", text: `${t.field} · ${t.value}` };
504
+ return shown("value", `${t.field} · ${t.value}`);
435
505
  case "relation":
436
- if (isTrace(s.predicate)) return { kind: "source", text: name(t.subject) };
437
- return t.object.iri === group && t.subject.iri !== group
438
- ? { kind: "link", text: `${t.relation} ← ${name(t.subject)}` }
439
- : { kind: "link", text: `${t.relation} → ${name(t.object)}` };
506
+ if (isTrace(s.predicate)) return shown("source", name(t.subject));
507
+ if (t.object.iri === group && t.subject.iri !== group)
508
+ return shown("link", `${t.relation} ← ${name(t.subject)}`);
509
+ return shown(
510
+ "link",
511
+ `${t.relation} → ${name(t.object)}`,
512
+ !t.object.exists && t.object.iri !== t.subject.iri ? name(t.object) : undefined,
513
+ );
440
514
  }
441
515
  }
442
516
 
517
+ /** The key of an instance's review item: LBB's, from the instance and the proposals it holds. */
518
+ export function itemKey(workflow: string, instance: string, proposals: string[]): string {
519
+ const group = stableHashHex(["review_group_v1", instance]).slice(0, 24);
520
+ const held = stableHashHex(["review_item_v1", ...[...proposals].sort()]).slice(0, 12);
521
+ return `starter:${workflow}:instance:${group}:${held}`;
522
+ }
523
+
443
524
  /** The order of the kinds in an item: the type, the label, values, links, the trace. */
444
525
  export const KIND_ORDER = ["type", "label", "value", "link", "source"];
445
526
 
@@ -188,7 +188,16 @@ test("the samples get the statuses and the facts of the answer key", async () =>
188
188
  assert.deepEqual(edges, want);
189
189
  assert.equal(lbb.graph.entities.get(iris.get("room-012")!)!.properties.area_m2, 62.4);
190
190
  assert.equal(lbb.reviews.size, EXPECTED.review_items);
191
- assert.deepEqual(state, { proposals: 14, written: 4, held: 1, in_review: 5, refused: 4 });
191
+ // LBB's counters: written counts a statement the graph held already too.
192
+ assert.deepEqual(state, {
193
+ proposals: 14,
194
+ written: 5,
195
+ held_already: 1,
196
+ in_review: 5,
197
+ refused: 4,
198
+ new_instance: 3,
199
+ items: 3,
200
+ });
192
201
  assert.equal(turns.length, 1);
193
202
  assert.ok(turns[0]!.keys.length <= 64);
194
203
  // Each step reports its phase as the LBB-run review words it.
@@ -252,12 +261,17 @@ test("one item per instance: a new instance's facts in order, its type required"
252
261
  ["p7", "p8", "p9"].map((id) => proposals.get(id)),
253
262
  );
254
263
  assert.equal(samples[0]!.line, "M 7 Medienwagen, 1 Stück");
255
- // The type's write and its status are required; the link is not.
264
+ // The type's status and its write are required; the link is not. The status rows come
265
+ // first, one per fact, as in LBB's item; the statements after them.
256
266
  const required = (m7.facts as { required?: { row: number }[] }).required!.map((r) => r.row);
257
- assert.deepEqual(required, [samples[0]!.rows, samples[0]!.row].flat());
267
+ assert.deepEqual(required, [samples[0]!.row, samples[0]!.rows].flat());
268
+ assert.deepEqual(
269
+ samples.map((sample) => sample.row),
270
+ [0, 1, 2],
271
+ );
258
272
  assert.equal(samples[2]!.required, undefined);
259
273
  assert.deepEqual(samples[2]!.triplets, [0]);
260
- assert.match(m7.key!, /^starter:review:instance:[0-9a-f]{32}:[0-9a-f]{12}$/);
274
+ assert.match(m7.key!, /^starter:review:instance:[0-9a-f]{24}:[0-9a-f]{12}$/);
261
275
  assert.deepEqual((m7.mapping as { review: { new: boolean } }).review.new, true);
262
276
  });
263
277
 
@@ -294,11 +308,9 @@ test("accepting a review item writes its statement and the proposal's accepted",
294
308
  const rename = [...lbb.reviews.values()].find((r) =>
295
309
  r.title.startsWith("Equipment S 2.1 Spiegel"),
296
310
  )!;
297
- assert.deepEqual(rename.facts!.entity_properties![0]!.name, "S 2.1 Spiegel, Kristallglas");
298
- assert.equal(
299
- rename.facts!.entity_properties![0]!.key,
300
- `\u0000lbb-eid:${iris.get("s21")!.slice(-32)}`,
301
- );
311
+ const renamed = rename.facts!.entity_properties!.find((row) => row.type === "Equipment")!;
312
+ assert.deepEqual(renamed.name, "S 2.1 Spiegel, Kristallglas");
313
+ assert.equal(renamed.key, `\u0000lbb-eid:${iris.get("s21")!.slice(-32)}`);
302
314
  });
303
315
 
304
316
  test("by default the gate decides a new instance: written at the gate, an item below it", async () => {
@@ -389,7 +401,15 @@ test("a message of more proposals than a turn decides continues in a new turn",
389
401
  assert.equal(turns.length, 2);
390
402
  assert.ok(turns.every((t) => t.keys.length <= 64));
391
403
  // The first value of each room is written; each later one would replace it, so it goes to a person.
392
- assert.deepEqual(state, { proposals: 40, written: 2, held: 0, in_review: 38, refused: 0 });
404
+ assert.deepEqual(state, {
405
+ proposals: 40,
406
+ written: 2,
407
+ held_already: 0,
408
+ in_review: 38,
409
+ refused: 0,
410
+ new_instance: 0,
411
+ items: 4,
412
+ });
393
413
  });
394
414
 
395
415
  test("the decision reads the statement's terms as the LBB review does", () => {