@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.
- package/README.md +42 -6
- package/lib/create.mjs +228 -14
- package/package.json +2 -1
- package/project/python/values_extract/tests/fake_lbb.py +226 -0
- package/project/python/values_extract/tests/test_workflow.py +150 -0
- package/python/values_extract/README.md +52 -0
- package/python/values_extract/__init__.py +3 -0
- package/python/values_extract/__main__.py +97 -0
- package/python/values_extract/samples/expected.json +149 -0
- package/python/values_extract/samples/records.json +26 -0
- package/python/values_extract/samples.py +37 -0
- package/python/values_extract/workflow.py +1044 -0
- package/python/versions.json +3 -0
- package/templates/_shared/testing.ts +197 -2
- package/templates/ontology-fit/README.md +41 -32
- package/templates/ontology-fit/workflow.test.ts +56 -5
- package/templates/ontology-fit/workflow.ts +708 -207
- package/templates/review/README.md +56 -29
- package/templates/review/decision.ts +118 -37
- package/templates/review/workflow.test.ts +30 -10
- package/templates/review/workflow.ts +123 -76
- package/templates/values-extract/README.md +92 -23
- package/templates/values-extract/samples/expected.json +1 -0
- package/templates/values-extract/test-examples.ts +155 -0
- package/templates/values-extract/workflow.test.ts +532 -27
- package/templates/values-extract/workflow.ts +719 -179
- package/templates/versions.json +1 -1
|
@@ -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
|
|
14
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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.
|
|
3
|
-
//
|
|
4
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|
132
|
-
const
|
|
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
|
-
|
|
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 :
|
|
138
|
-
case "bool":
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
500
|
+
return shown("type", `is a ${t.class}`);
|
|
431
501
|
case "label":
|
|
432
|
-
return
|
|
502
|
+
return shown("label", `label · ${t.label}`);
|
|
433
503
|
case "property":
|
|
434
|
-
return
|
|
504
|
+
return shown("value", `${t.field} · ${t.value}`);
|
|
435
505
|
case "relation":
|
|
436
|
-
if (isTrace(s.predicate)) return
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
|
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]!.
|
|
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]{
|
|
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
|
-
|
|
298
|
-
assert.
|
|
299
|
-
|
|
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, {
|
|
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", () => {
|