@littlebigbrain/create 0.0.0-stage → 0.2.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 (52) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +73 -2
  3. package/bin/create-littlebigbrain.mjs +9 -0
  4. package/lib/create.mjs +244 -0
  5. package/package.json +46 -4
  6. package/project/setup.ts +18 -0
  7. package/project/try.ts +30 -0
  8. package/project/worker.ts +23 -0
  9. package/templates/_shared/testing.ts +521 -0
  10. package/templates/documents-link/README.md +146 -0
  11. package/templates/documents-link/link.ts +580 -0
  12. package/templates/documents-link/samples/ausstattungsliste.pdf +76 -0
  13. package/templates/documents-link/samples/baubeschreibung.pdf +54 -0
  14. package/templates/documents-link/samples/expected.json +404 -0
  15. package/templates/documents-link/samples/pages/ausstattungsliste.json +1150 -0
  16. package/templates/documents-link/samples/pages/baubeschreibung.json +802 -0
  17. package/templates/documents-link/samples/pages/raumblaetter.json +1580 -0
  18. package/templates/documents-link/samples/raumblaetter.pdf +109 -0
  19. package/templates/documents-link/samples.ts +31 -0
  20. package/templates/documents-link/workflow.test.ts +497 -0
  21. package/templates/documents-link/workflow.ts +467 -0
  22. package/templates/documents-map/README.md +212 -0
  23. package/templates/documents-map/map.ts +1135 -0
  24. package/templates/documents-map/samples/ausstattungsliste.pdf +76 -0
  25. package/templates/documents-map/samples/baubeschreibung.pdf +54 -0
  26. package/templates/documents-map/samples/expected.json +50 -0
  27. package/templates/documents-map/samples/pages/raumblaetter.json +1580 -0
  28. package/templates/documents-map/samples/raumblaetter.pdf +109 -0
  29. package/templates/documents-map/samples/trace.json +613 -0
  30. package/templates/documents-map/samples.ts +182 -0
  31. package/templates/documents-map/workflow.test.ts +770 -0
  32. package/templates/documents-map/workflow.ts +1199 -0
  33. package/templates/ontology-fit/README.md +81 -0
  34. package/templates/ontology-fit/samples/expected.md +88 -0
  35. package/templates/ontology-fit/samples/texts.json +22 -0
  36. package/templates/ontology-fit/samples.ts +40 -0
  37. package/templates/ontology-fit/workflow.test.ts +270 -0
  38. package/templates/ontology-fit/workflow.ts +416 -0
  39. package/templates/review/README.md +158 -0
  40. package/templates/review/decision.ts +483 -0
  41. package/templates/review/samples/expected.json +37 -0
  42. package/templates/review/samples/proposals.json +35 -0
  43. package/templates/review/samples.ts +154 -0
  44. package/templates/review/workflow.test.ts +438 -0
  45. package/templates/review/workflow.ts +459 -0
  46. package/templates/values-extract/README.md +81 -0
  47. package/templates/values-extract/samples/expected.json +148 -0
  48. package/templates/values-extract/samples/records.json +26 -0
  49. package/templates/values-extract/samples.ts +27 -0
  50. package/templates/values-extract/workflow.test.ts +279 -0
  51. package/templates/values-extract/workflow.ts +419 -0
  52. package/templates/versions.json +3 -0
@@ -0,0 +1,81 @@
1
+ # ontology-fit
2
+
3
+ Reads the text of each new or changed instance of a class and proposes the
4
+ ontology changes the text supports: a class `Mangel` for the defects a set
5
+ of site reports follows, its properties `frist` and `status`, a relation
6
+ `zustaendig` to `Firma`. Each change becomes a review item with its
7
+ evolve operations and the quotes that support it; a person accepts it, and
8
+ the ontology changes.
9
+
10
+ The LBB-run version is the starter workflow `ontology.fit@v1`, the fit
11
+ sources of `/v1/ontology/fit-sources` (`docs/reference/fit-sources.md`).
12
+
13
+ ## How it runs
14
+
15
+ The triggered workflow watches the class and its text field. For each
16
+ instance of a trigger message, one step:
17
+
18
+ 1. The reasoning model reads the text with the ontology
19
+ (`lbb.ontology()`, as classes, relations with their domain and range,
20
+ and properties) and the changes the earlier texts of the turn proposed,
21
+ and returns up to 12 proposals: `add_class`, `add_property`,
22
+ `add_relation` or `widen_relation`, each with a sentence of the text as
23
+ its quote. The prompt and schema are the LBB version's.
24
+ 2. Each proposal becomes a change (`shape`): its key, its title and its
25
+ evolve operations. A class the change names that the ontology lacks is
26
+ added first, and a new relation with the name of an existing one widens
27
+ it. The key follows the LBB version:
28
+ `fit:<workflow name>/add_class/mangel`,
29
+ `fit:<workflow name>/add_relation/mangel/zustaendig/firma`,
30
+ `fit:<workflow name>/widen_relation/betrifft-raum/mangel/x`.
31
+ 3. A change is kept when its quote is in the text with at least three
32
+ words, and a dry run (`lbb.evolve(change, { dryRun: true })`) says the
33
+ ontology does not have it yet.
34
+
35
+ A last step files one review item per key, with the quotes of every text of
36
+ the turn that supports it. The same key again updates the item. A turn reads
37
+ at most 48 texts; the rest goes to a new turn.
38
+
39
+ ## Settings
40
+
41
+ `defaults` in `workflow.ts`, which fit the samples:
42
+
43
+ | Setting | Default | Meaning |
44
+ | --- | --- | --- |
45
+ | `className` | `Baustellenbericht` | The class whose instances hold the text |
46
+ | `textField` | `text` | The property that holds the text |
47
+ | `context` | the samples' | One sentence about what the text is |
48
+
49
+ `trigger(settings)` names the triggered workflow `berichte`; the name starts
50
+ every key. The workflow writes no graph facts, so it owns nothing.
51
+
52
+ ## What the LBB-run version adds
53
+
54
+ - **Support scores.** The decision model scores how well each quote
55
+ supports its change; a change below 0.5 is dropped.
56
+ - **Support across instances.** The instances that support each key are
57
+ counted over every run, and a dismissed change opens again when twice its
58
+ support and three more instances support it.
59
+ - **The proposals of the source.** The open suggestions of the source and
60
+ the names of the dismissed ones go into every prompt, so the model reuses
61
+ names and does not propose a rejected change again.
62
+ - **Name style.** A new relation or property name is written in the
63
+ ontology's style (`works_in` or `WORKS_IN`).
64
+ - **Long texts.** A text past 40,000 characters is read in overlapping
65
+ chunks.
66
+ - Its own monthly budget, a preview with the proposals of a sample, and
67
+ counters per outcome in the source's status.
68
+
69
+ ## Samples and test
70
+
71
+ `samples/texts.json` holds five invented site reports and meeting minutes;
72
+ `samples/expected.md` describes the graph before the run, the proposals a
73
+ good run makes with their keys, those it may make, and those it does not
74
+ make. `samples.ts` sets up that graph for `npm run try`.
75
+
76
+ `workflow.test.ts` runs the workflow with a mock reasoning model whose rules
77
+ are fixed (a sentence about a `Mangel` proposes the class, `Frist`, `Status`
78
+ and `Zuständig` its facts, a room widens `betrifft_raum`). The run files
79
+ the five keys of `expected.md` and no other: the relation `anwesend`, which
80
+ the ontology has, is dropped by the dry run, and a proposal whose quote is
81
+ not in its text is dropped by the quote check.
@@ -0,0 +1,88 @@
1
+ # Fit from text samples: what a good run proposes
2
+
3
+ The five texts in `texts.json` are invented site reports and meeting minutes
4
+ of one project, Neubau Grundschule Musterstadt. They follow the defects found
5
+ on site over four weeks: in which room, which company must fix each one, by
6
+ when, and whether it is fixed. The graph has no class for a defect, so a fit
7
+ run over these texts should propose one, with the facts the texts give about
8
+ each defect. Texts are numbered 1 to 5 in the order of `texts.json`.
9
+
10
+ ## The graph before the run
11
+
12
+ Each text is an instance of `Baustellenbericht`: its name is the label, its
13
+ text is the property `text`. The ontology holds:
14
+
15
+ | Class | Properties |
16
+ | --- | --- |
17
+ | `Baustellenbericht` | `text` (text), `datum` (datetime) |
18
+ | `Raum` | `nummer` (keyword), `bezeichnung` (text) |
19
+ | `Firma` | `name` (text), `gewerk` (keyword) |
20
+
21
+ | Relation | From | To |
22
+ | --- | --- | --- |
23
+ | `betrifft_raum` | `Baustellenbericht` | `Raum` |
24
+ | `anwesend` | `Baustellenbericht` | `Firma` |
25
+
26
+ The fit source:
27
+
28
+ ```json
29
+ {
30
+ "class": "Baustellenbericht",
31
+ "name": "berichte",
32
+ "from": ["text"],
33
+ "context": "Site reports and meeting minutes of a school building project, in German"
34
+ }
35
+ ```
36
+
37
+ ## Proposals a good run makes
38
+
39
+ The names follow the ontology: lower snake_case in ASCII (`zustaendig` for
40
+ "zuständig"). A run can spell a name another way with the same meaning. The
41
+ keys hold for the names in this table (a suggestion key keeps lowercase ASCII
42
+ letters and digits, and writes every other run of characters as one dash; a
43
+ missing class in a widen key is `x`).
44
+
45
+ | Change | Key | Texts | Quote (one of them) |
46
+ | --- | --- | --- | --- |
47
+ | Add class `Mangel` | `fit:berichte/add_class/mangel` | 1 to 5 | "Der Mangel wurde der Fliesenbau Muster GmbH gemeldet" |
48
+ | Add property `frist` (datetime) to `Mangel` | `fit:berichte/add_property/mangel/frist` | 1 to 4 | "die Frist zur Beseitigung endet am 16.09.2026" |
49
+ | Add property `status` (keyword: `offen`, `behoben`) to `Mangel` | `fit:berichte/add_property/mangel/status` | 3 to 5 | "Frist 23.09.2026, Status offen" |
50
+ | Add relation `zustaendig` from `Mangel` to `Firma` | `fit:berichte/add_relation/mangel/zustaendig/firma` | 1 to 4 | "Zuständig ist die Trockenbau Probe GmbH" |
51
+ | Let `betrifft_raum` start at `Mangel` | `fit:berichte/widen_relation/betrifft-raum/mangel/x` | 1 to 5 | "In Raum 0.12 fehlt der Spiegel über dem Waschtisch" |
52
+
53
+ The last row tests reuse. Every defect names its room, and the ontology
54
+ already has a relation to `Raum`, so a good run widens `betrifft_raum`. A new
55
+ relation from `Mangel` to `Raum` with another name (`liegt_in`,
56
+ `befindet_sich_in`) is a synonym of it.
57
+
58
+ ## Proposals a good run may make
59
+
60
+ - Add property `einstufung` (keyword: `wesentlich`, `unwesentlich`) to
61
+ `Mangel`. Only text 5 gives it: "die Bauleitung stuft diesen Mangel als
62
+ unwesentlich ein".
63
+ - Add property `beschreibung` (text) to `Mangel`.
64
+ - Add relation `stellt_fest` from `Baustellenbericht` to `Mangel`, for the
65
+ report that records a defect.
66
+ - Add class `Bauteil`, for the part a defect concerns (Bodenfliesen, Spiegel,
67
+ Trennwand).
68
+ - Add class `Abnahme` with a date. Only text 5 names it.
69
+ - Add class `Lieferung` with a `liefertermin` (datetime). Texts 2 and 4 give
70
+ the delivery date of the ceiling lights.
71
+
72
+ ## Proposals a good run does not make
73
+
74
+ - A relation from `Baustellenbericht` to `Firma` for the companies present
75
+ ("Anwesend: …", "Teilnehmer: …"). `anwesend` holds it; the check drops
76
+ such a change as `already_covered`.
77
+ - A property for the date of a report. `datum` holds it.
78
+ - A class for one company, one room or one defect (`Fliesenbau Muster GmbH`,
79
+ `Flur 0.10`, `Trennwand Kabine 2`). These are instances.
80
+ - A second name for a change the run proposes: `Maengel` or `Defect` beside
81
+ `Mangel`, `deadline` beside `frist`, `verantwortlich` beside `zustaendig`.
82
+
83
+ ## How to check a run
84
+
85
+ 1. Every change in "Proposals a good run makes" is filed.
86
+ 2. No change in "Proposals a good run does not make" is filed.
87
+ 3. Every filed quote is in its text, with whitespace runs as one space, and
88
+ holds at least three words.
@@ -0,0 +1,22 @@
1
+ [
2
+ {
3
+ "name": "Baustellenbericht 12",
4
+ "text": "Baustellenbericht 12 vom 02.09.2026. Begehung des Erdgeschosses mit der Bauleitung. Anwesend: Fliesenbau Muster GmbH, Bodenbau Beispiel GmbH. In Raum 0.14 (WC Mädchen) sind zwei Bodenfliesen gerissen. Der Mangel wurde der Fliesenbau Muster GmbH gemeldet, die Frist zur Beseitigung endet am 16.09.2026. In Raum 0.12 ist der Estrich trocken; die Bodenbau Beispiel GmbH verlegt das Linoleum ab dem 07.09.2026."
5
+ },
6
+ {
7
+ "name": "Baubesprechung 05.09.2026",
8
+ "text": "Protokoll der Baubesprechung vom 05.09.2026. Teilnehmer: Bauleitung, Elektro Beispiel GmbH, Haustechnik Exempel KG. Die Elektro Beispiel GmbH meldet, dass die Deckenleuchten erst am 21.09.2026 geliefert werden. Die Haustechnik Exempel KG hat die Waschtische im Erdgeschoss montiert. Mangel: In Raum 0.12 fehlt der Spiegel über dem Waschtisch. Zuständig ist die Haustechnik Exempel KG, Frist 12.09.2026."
9
+ },
10
+ {
11
+ "name": "Baustellenbericht 13",
12
+ "text": "Baustellenbericht 13 vom 09.09.2026. Mängelbegehung in Raum 0.14: Die gerissenen Bodenfliesen sind ersetzt, der Mangel ist behoben. Neu festgestellt: Die Trennwand der WC-Kabine 2 in Raum 0.14 steht schief. Zuständig ist die Trockenbau Probe GmbH, Frist 23.09.2026, Status offen."
13
+ },
14
+ {
15
+ "name": "Baustellenbericht 14",
16
+ "text": "Baustellenbericht 14 vom 16.09.2026. In Raum 0.12 ist der Spiegel montiert, der Mangel ist behoben. Im Flur 0.10 hat das Linoleum an zwei Stellen tiefe Kratzer. Zuständig ist die Bodenbau Beispiel GmbH, Frist 30.09.2026, Status offen. Die Deckenleuchten sind geliefert und werden bis zum 25.09.2026 montiert."
17
+ },
18
+ {
19
+ "name": "Vorbegehung zur Abnahme",
20
+ "text": "Vorbegehung zur Abnahme am 28.09.2026 mit Bauherr und Bauleitung. Die Trennwand der Kabine 2 in Raum 0.14 ist ausgerichtet, der Mangel ist behoben. Offen bleiben die Kratzer im Flur 0.10; die Bauleitung stuft diesen Mangel als unwesentlich ein. Wesentliche Mängel liegen nicht vor. Die Abnahme ist für den 02.10.2026 geplant."
21
+ }
22
+ ]
@@ -0,0 +1,40 @@
1
+ // The samples of ontology-fit, for `npm run try`: the ontology of
2
+ // samples/expected.md and five invented site reports (samples/texts.json),
3
+ // committed as instances of the class the settings name.
4
+ import { readFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import type { LbbClient, Schemas } from "@littlebigbrain/client";
7
+ import { defaults, ontology } from "./workflow.js";
8
+
9
+ /** The graph before the run: the classes, properties and relations of expected.md. */
10
+ export const sampleOntology: Schemas["OntologyEvolveOp"][] = [
11
+ ...ontology(),
12
+ { op: "add_entity_type", name: "Raum" },
13
+ { op: "add_entity_type", name: "Firma" },
14
+ { op: "add_property", name: "datum", value_type: "date_time" },
15
+ { op: "add_property", name: "nummer", value_type: "keyword" },
16
+ { op: "add_property", name: "bezeichnung", value_type: "text" },
17
+ { op: "add_property", name: "gewerk", value_type: "keyword" },
18
+ { op: "add_relation", name: "betrifft_raum", domain: [defaults.className], range: ["Raum"] },
19
+ { op: "add_relation", name: "anwesend", domain: [defaults.className], range: ["Firma"] },
20
+ ];
21
+
22
+ /** Set up the sample ontology and commit the sample texts to the client's graph. */
23
+ export async function loadSamples(client: LbbClient, dir: string): Promise<void> {
24
+ const texts = JSON.parse(await readFile(join(dir, "texts.json"), "utf8")) as {
25
+ name: string;
26
+ text: string;
27
+ }[];
28
+ await client.ontology.evolve({ ops: sampleOntology });
29
+ await client.commit(
30
+ {
31
+ entity_properties: texts.map((t) => ({
32
+ type: defaults.className,
33
+ name: t.name,
34
+ properties: { [defaults.textField]: t.text },
35
+ })),
36
+ },
37
+ { idempotencyKey: "ontology-fit-samples" },
38
+ );
39
+ console.log(`Committed ${texts.length} ${defaults.className} texts`);
40
+ }
@@ -0,0 +1,270 @@
1
+ // ontology-fit on its samples, with a mock reasoning model whose rules are
2
+ // fixed: a sentence about a defect (`Mangel`) proposes the class `Mangel`,
3
+ // `Frist`, `Status` and `Zuständig` propose its facts, a room in the same
4
+ // text widens `betrifft_raum`, a list of who was present proposes the
5
+ // relation `anwesend` the ontology has, and a text about the acceptance
6
+ // (`Abnahme`) gets a quote that is not in the text. The expected keys come
7
+ // from samples/expected.md.
8
+ import { test } from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { ENTITY_IRI_NAMESPACE, type Schemas } from "@littlebigbrain/client";
13
+ import {
14
+ MockActivities,
15
+ mockId,
16
+ phases,
17
+ readJson,
18
+ runMessage,
19
+ samplesDir,
20
+ triggerMessage,
21
+ type MockRules,
22
+ } from "../_shared/testing.js";
23
+ import { createWorkflow, keyPart, quoteInText } from "./workflow.js";
24
+ import { sampleOntology } from "./samples.js";
25
+
26
+ const SAMPLES = samplesDir(import.meta.url, "ontology-fit");
27
+ const TEXTS = readJson<{ name: string; text: string }[]>(join(SAMPLES, "texts.json"));
28
+ const EXPECTED_MD = readFileSync(join(SAMPLES, "expected.md"), "utf8");
29
+ /** The keys of the table "Proposals a good run makes". */
30
+ const EXPECTED_KEYS = [
31
+ ...EXPECTED_MD.split("## Proposals a good run makes")[1]!
32
+ .split("## Proposals a good run may make")[0]!
33
+ .matchAll(/`(fit:[^`]+)`/g),
34
+ ].map((m) => m[1]!);
35
+
36
+ const iriOf = (name: string) => ENTITY_IRI_NAMESPACE + mockId(`Baustellenbericht:${name}`);
37
+
38
+ /** The ontology view of the graph before the run, built from the sample ontology. */
39
+ function view(): Schemas["OntologyView"] {
40
+ const ops = sampleOntology;
41
+ const types = ops.flatMap((op) => (op.op === "add_entity_type" ? [op.name] : []));
42
+ return {
43
+ graph: { tenant_id: "t", graph_id: "samples", branch_id: "main" },
44
+ ontology_version: 3,
45
+ entity_types: [],
46
+ relations: [],
47
+ entity_type_defs: types.map((name) => ({
48
+ name,
49
+ stable_id: `type:${name}`,
50
+ properties: [],
51
+ resolvable: true,
52
+ since_version: 1,
53
+ })),
54
+ relation_defs: ops.flatMap((op) =>
55
+ op.op === "add_relation"
56
+ ? [
57
+ {
58
+ name: op.name,
59
+ src_types: op.domain ?? [],
60
+ dst_types: op.range ?? [],
61
+ cardinality: "many_to_many",
62
+ since_version: 1,
63
+ symmetric: false,
64
+ temporal_semantics: "bitemporal",
65
+ transitive: false,
66
+ },
67
+ ]
68
+ : [],
69
+ ),
70
+ property_defs: ops.flatMap((op) =>
71
+ op.op === "add_property"
72
+ ? [{ name: op.name, value_type: op.value_type ?? "text", required: false }]
73
+ : [],
74
+ ),
75
+ } as Schemas["OntologyView"];
76
+ }
77
+
78
+ /** Whether the ontology has a change already: the dry run's no-op. */
79
+ function covered(op: Schemas["OntologyEvolveOp"], v: Schemas["OntologyView"]): boolean {
80
+ const has = (names: { name: string }[] | undefined, name: string) =>
81
+ (names ?? []).some((n) => n.name.toLowerCase() === name.toLowerCase());
82
+ if (op.op === "add_entity_type") return has(v.entity_type_defs, op.name);
83
+ if (op.op === "add_property") return has(v.property_defs, op.name);
84
+ if (op.op === "add_relation") return has(v.relation_defs, op.name);
85
+ if (op.op === "widen_relation") {
86
+ const r = v.relation_defs?.find((d) => d.name.toLowerCase() === op.relation.toLowerCase());
87
+ return Boolean(
88
+ r &&
89
+ (op.add_domain ?? []).every((d) => r.src_types.includes(d)) &&
90
+ (op.add_range ?? []).every((d) => r.dst_types.includes(d)),
91
+ );
92
+ }
93
+ return false;
94
+ }
95
+
96
+ const proposal = (fields: Partial<Record<string, string>>) => ({
97
+ kind: "",
98
+ name: "",
99
+ parent: "",
100
+ class: "",
101
+ value_type: "",
102
+ domain: "",
103
+ range: "",
104
+ quote: "",
105
+ reason: "",
106
+ ...fields,
107
+ });
108
+
109
+ function propose(text: string) {
110
+ const sentences = text.split(/(?<=\.)\s+/);
111
+ const find = (pattern: RegExp) => sentences.find((s) => pattern.test(s));
112
+ const out = [];
113
+ const defect = find(/Mangel|Mängel/);
114
+ if (defect)
115
+ out.push(
116
+ proposal({
117
+ kind: "add_class",
118
+ name: "Mangel",
119
+ quote: defect,
120
+ reason: "The texts follow defects.",
121
+ }),
122
+ );
123
+ const deadline = find(/Frist/);
124
+ if (defect && deadline)
125
+ out.push(
126
+ proposal({
127
+ kind: "add_property",
128
+ name: "frist",
129
+ class: "Mangel",
130
+ value_type: "date_time",
131
+ quote: deadline,
132
+ }),
133
+ );
134
+ const status = find(/Status/);
135
+ if (defect && status)
136
+ out.push(
137
+ proposal({
138
+ kind: "add_property",
139
+ name: "status",
140
+ class: "Mangel",
141
+ value_type: "keyword",
142
+ quote: status,
143
+ }),
144
+ );
145
+ const owner = find(/Zuständig/);
146
+ if (defect && owner)
147
+ out.push(
148
+ proposal({
149
+ kind: "add_relation",
150
+ name: "zustaendig",
151
+ domain: "Mangel",
152
+ range: "Firma",
153
+ quote: owner,
154
+ }),
155
+ );
156
+ const room = find(/Raum 0\.\d+/);
157
+ if (defect && room)
158
+ out.push(
159
+ proposal({ kind: "widen_relation", name: "betrifft_raum", domain: "Mangel", quote: room }),
160
+ );
161
+ const present = find(/Anwesend:|Teilnehmer:/);
162
+ if (present)
163
+ out.push(
164
+ proposal({
165
+ kind: "add_relation",
166
+ name: "anwesend",
167
+ domain: "Baustellenbericht",
168
+ range: "Firma",
169
+ quote: present,
170
+ }),
171
+ );
172
+ if (find(/Abnahme/))
173
+ out.push(
174
+ proposal({ kind: "add_class", name: "Abnahme", quote: "Die Abnahme findet morgen statt." }),
175
+ );
176
+ return out;
177
+ }
178
+
179
+ function rules(more: MockRules = {}): MockRules {
180
+ const v = view();
181
+ return {
182
+ ontology: v,
183
+ reason: (request) => ({
184
+ proposals: propose((JSON.parse(request.prompt) as { text: string }).text),
185
+ }),
186
+ evolve: (ops) => ({ no_op: ops.every((op) => covered(op, v)) }),
187
+ sparql: (query) =>
188
+ TEXTS.filter((t) => query.includes(iriOf(t.name))).map((t) => ({
189
+ s: iriOf(t.name),
190
+ label: t.name,
191
+ text: t.text,
192
+ })),
193
+ ...more,
194
+ };
195
+ }
196
+
197
+ test("the samples give the changes of expected.md, one review item per key", async () => {
198
+ const lbb = new MockActivities(rules());
199
+ const { state, turns } = await runMessage(
200
+ createWorkflow(lbb),
201
+ triggerMessage(
202
+ "berichte",
203
+ TEXTS.map((t) => iriOf(t.name)),
204
+ ),
205
+ );
206
+
207
+ assert.deepEqual(phases(turns), ["reading texts", "filing review items"]);
208
+ assert.deepEqual(
209
+ turns[0]!.progress.filter((p) => p.phase === "filing review items").map((p) => p.done),
210
+ [0, 1, 2, 3, 4],
211
+ );
212
+ assert.equal(EXPECTED_KEYS.length, 5);
213
+ assert.deepEqual([...lbb.reviews.keys()].sort(), [...EXPECTED_KEYS].sort());
214
+ assert.deepEqual(state, { texts: 5, proposals: 3 + 4 + 5 + 5 + 2, filed: 5 });
215
+ const mangel = lbb.reviews.get("fit:berichte/add_class/mangel")!;
216
+ assert.equal(mangel.workflow, "berichte");
217
+ assert.deepEqual(mangel.change, [{ op: "add_entity_type", name: "Mangel" }]);
218
+ assert.equal(mangel.evidence!.records, 5);
219
+ });
220
+
221
+ test("a change names the classes it needs, and a widening keeps the relation's name", async () => {
222
+ const lbb = new MockActivities(rules());
223
+ await runMessage(
224
+ createWorkflow(lbb),
225
+ triggerMessage(
226
+ "berichte",
227
+ TEXTS.map((t) => iriOf(t.name)),
228
+ ),
229
+ );
230
+
231
+ assert.deepEqual(lbb.reviews.get("fit:berichte/add_relation/mangel/zustaendig/firma")!.change, [
232
+ { op: "add_entity_type", name: "Mangel" },
233
+ { op: "add_relation", name: "zustaendig", domain: ["Mangel"], range: ["Firma"] },
234
+ ]);
235
+ assert.deepEqual(lbb.reviews.get("fit:berichte/widen_relation/betrifft-raum/mangel/x")!.change, [
236
+ { op: "add_entity_type", name: "Mangel" },
237
+ { op: "widen_relation", relation: "betrifft_raum", add_domain: ["Mangel"], add_range: [] },
238
+ ]);
239
+ assert.equal(
240
+ lbb.reviews.get("fit:berichte/add_property/mangel/frist")!.title,
241
+ "Add property frist to Mangel",
242
+ );
243
+ });
244
+
245
+ test("a change the ontology has, and a quote that is not in the text, are not filed", async () => {
246
+ const lbb = new MockActivities(rules());
247
+ await runMessage(
248
+ createWorkflow(lbb),
249
+ triggerMessage(
250
+ "berichte",
251
+ TEXTS.map((t) => iriOf(t.name)),
252
+ ),
253
+ );
254
+ const keys = [...lbb.reviews.keys()];
255
+
256
+ assert.ok(!keys.some((k) => k.includes("anwesend")), "anwesend is already in the ontology");
257
+ assert.ok(!keys.some((k) => k.includes("abnahme")), "its quote is not in the text");
258
+ for (const item of lbb.reviews.values())
259
+ for (const sample of item.evidence!.samples! as { entity: string; quote: string }[]) {
260
+ const text = TEXTS.find((t) => iriOf(t.name) === sample.entity)!.text;
261
+ assert.ok(quoteInText(sample.quote, text), sample.quote);
262
+ }
263
+ });
264
+
265
+ test("keys keep lowercase ASCII letters and digits, other runs as one dash", () => {
266
+ assert.equal(keyPart("Mangel"), "mangel");
267
+ assert.equal(keyPart("betrifft_raum"), "betrifft-raum");
268
+ assert.equal(keyPart("-"), "x");
269
+ assert.equal(keyPart("zuständig"), "zust-ndig");
270
+ });