@littlebigbrain/create 0.4.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 +259 -5
- 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/rows/README.md +8 -1
- package/templates/rows/workflow.test.ts +56 -0
- package/templates/rows/workflow.ts +182 -148
- package/templates/sections/README.md +8 -1
- package/templates/sections/workflow.test.ts +51 -1
- package/templates/sections/workflow.ts +137 -98
- 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
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
// `fact_status: accepted`; a person can leave facts out of the accept, except the type of a new
|
|
19
19
|
// instance. The proposal gets its `fact_status` (`written`, `in_review`, `refused`) and
|
|
20
20
|
// `review_note`, owned by this workflow.
|
|
21
|
-
import { createHash } from "node:crypto";
|
|
22
21
|
import {
|
|
23
22
|
entityRef,
|
|
23
|
+
LbbError,
|
|
24
24
|
triggeredWorkflow,
|
|
25
25
|
type LbbActivities,
|
|
26
26
|
type Schemas,
|
|
@@ -69,15 +69,30 @@ export function ontology(settings: Settings = defaults): Schemas["OntologyEvolve
|
|
|
69
69
|
];
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/** What the workflow decided, over all its turns. */
|
|
72
|
+
/** What the workflow decided, over all its turns: the counters of LBB's review. */
|
|
73
73
|
export interface ReviewState {
|
|
74
74
|
proposals: number;
|
|
75
|
+
/** Written, or held by the graph already (`held_already`). */
|
|
75
76
|
written: number;
|
|
76
|
-
|
|
77
|
+
held_already: number;
|
|
77
78
|
in_review: number;
|
|
78
79
|
refused: number;
|
|
80
|
+
/** Proposals for a person that name an instance the graph does not have. */
|
|
81
|
+
new_instance: number;
|
|
82
|
+
/** Review items filed. */
|
|
83
|
+
items: number;
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
const ZERO: ReviewState = {
|
|
87
|
+
proposals: 0,
|
|
88
|
+
written: 0,
|
|
89
|
+
held_already: 0,
|
|
90
|
+
in_review: 0,
|
|
91
|
+
refused: 0,
|
|
92
|
+
new_instance: 0,
|
|
93
|
+
items: 0,
|
|
94
|
+
};
|
|
95
|
+
|
|
81
96
|
/** Proposals one turn decides: two steps each, and at most one review item each, in a turn of 64 steps. */
|
|
82
97
|
const TURN_PROPOSALS = 20;
|
|
83
98
|
const PLAIN_IRI = /^[^\s<>"{}|\\^`]+$/;
|
|
@@ -106,6 +121,10 @@ interface Outcome {
|
|
|
106
121
|
note?: string;
|
|
107
122
|
facts?: review.Facts;
|
|
108
123
|
holds?: boolean;
|
|
124
|
+
/** The confidence the decision gated on (`gateValue`). */
|
|
125
|
+
gate?: number;
|
|
126
|
+
/** For a person, and it names an instance the graph does not have. */
|
|
127
|
+
newInstance?: boolean;
|
|
109
128
|
/** For a person: the item's instance, its view, and what the item shows of the fact. */
|
|
110
129
|
item?: {
|
|
111
130
|
instance: string;
|
|
@@ -121,27 +140,46 @@ export function createWorkflow(lbb: LbbActivities, settings: Settings = defaults
|
|
|
121
140
|
return triggeredWorkflow<ReviewState>({
|
|
122
141
|
name: workflowType.workflow_type,
|
|
123
142
|
version: workflowType.version,
|
|
124
|
-
initialState:
|
|
143
|
+
initialState: ZERO,
|
|
125
144
|
async onInstances(ctx, message, state) {
|
|
126
145
|
const batch = message.instances.slice(0, TURN_PROPOSALS);
|
|
127
146
|
let cache: Promise<Read> | undefined;
|
|
128
147
|
const read = () => (cache ??= reader(lbb, batch, settings));
|
|
129
|
-
const next = { ...state };
|
|
148
|
+
const next = { ...ZERO, ...state };
|
|
130
149
|
const forPeople: { iri: string; outcome: Outcome }[] = [];
|
|
131
150
|
for (const [i, iri] of batch.entries()) {
|
|
132
|
-
|
|
151
|
+
let outcome = await ctx.step(`decide-${i}`, async (s) => {
|
|
133
152
|
s.progress("deciding proposals", { done: i, total: batch.length, unit: "proposals" });
|
|
134
153
|
return decideOne(lbb, await read(), iri, settings);
|
|
135
154
|
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
155
|
+
try {
|
|
156
|
+
await lbb.writeOwned(ctx, `write-${i}`, async (s) => {
|
|
157
|
+
s.progress("writing facts", { done: i, total: batch.length, unit: "proposals" });
|
|
158
|
+
return statusWrite(await read(), iri, outcome, settings);
|
|
159
|
+
});
|
|
160
|
+
} catch (error) {
|
|
161
|
+
// The graph refused the statement (a value it cannot hold): the proposal says why.
|
|
162
|
+
if (!(error instanceof LbbError && error.status === 400) || outcome.status !== "written")
|
|
163
|
+
throw error;
|
|
164
|
+
outcome = {
|
|
165
|
+
status: "refused",
|
|
166
|
+
note: `The graph refused the statement: ${error.message}`,
|
|
167
|
+
};
|
|
168
|
+
const refused = outcome;
|
|
169
|
+
await lbb.writeOwned(ctx, `refuse-${i}`, async () =>
|
|
170
|
+
statusWrite(await read(), iri, refused, settings),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
140
173
|
if (outcome.status === "decided") continue;
|
|
141
174
|
next.proposals += 1;
|
|
142
|
-
if (outcome.status === "written")
|
|
143
|
-
|
|
144
|
-
|
|
175
|
+
if (outcome.status === "written") {
|
|
176
|
+
next.written += 1;
|
|
177
|
+
if (outcome.holds) next.held_already += 1;
|
|
178
|
+
} else next[outcome.status] += 1;
|
|
179
|
+
if (outcome.status === "in_review") {
|
|
180
|
+
if (outcome.newInstance) next.new_instance += 1;
|
|
181
|
+
forPeople.push({ iri, outcome });
|
|
182
|
+
}
|
|
145
183
|
}
|
|
146
184
|
// One review item per instance of this turn.
|
|
147
185
|
const groups = new Map<string, { iri: string; outcome: Outcome }[]>();
|
|
@@ -149,12 +187,14 @@ export function createWorkflow(lbb: LbbActivities, settings: Settings = defaults
|
|
|
149
187
|
const instance = one.outcome.item!.instance;
|
|
150
188
|
groups.set(instance, [...(groups.get(instance) ?? []), one]);
|
|
151
189
|
}
|
|
152
|
-
for (const [n, members] of [...groups.values()].entries())
|
|
190
|
+
for (const [n, members] of [...groups.values()].entries()) {
|
|
153
191
|
await ctx.step(`review-${n}`, async (s) => {
|
|
154
192
|
s.progress("filing review items", { done: n, total: groups.size, unit: "instances" });
|
|
155
193
|
await fileItem(lbb, await read(), members, message, settings);
|
|
156
194
|
return true;
|
|
157
195
|
});
|
|
196
|
+
next.items += 1;
|
|
197
|
+
}
|
|
158
198
|
// A proposal its proposer withdrew (message.removed) keeps the fact written for it: see the README.
|
|
159
199
|
const rest = message.instances.slice(TURN_PROPOSALS);
|
|
160
200
|
return rest.length
|
|
@@ -295,10 +335,11 @@ async function decideOne(
|
|
|
295
335
|
);
|
|
296
336
|
edgeHolds = rows.length > 0;
|
|
297
337
|
}
|
|
298
|
-
const
|
|
338
|
+
const gate = review.gateValue(proposal.confidence, proposal.evidence);
|
|
339
|
+
const decision = review.decide(terms, s.predicate, gate, settings, edgeHolds);
|
|
299
340
|
if (decision.status === "refused" || terms.kind === "refused")
|
|
300
341
|
return { status: "refused", note: decision.status === "refused" ? decision.why : "" };
|
|
301
|
-
const facts = review.factsOf(terms, iri,
|
|
342
|
+
const facts = review.factsOf(terms, iri, gate, proposal.evidence);
|
|
302
343
|
if (decision.status === "written")
|
|
303
344
|
return decision.holds
|
|
304
345
|
? { status: "written", holds: true }
|
|
@@ -313,6 +354,8 @@ async function decideOne(
|
|
|
313
354
|
status: "in_review",
|
|
314
355
|
note: decision.why,
|
|
315
356
|
facts,
|
|
357
|
+
gate,
|
|
358
|
+
newInstance: review.namesNewInstance(terms),
|
|
316
359
|
item: {
|
|
317
360
|
instance,
|
|
318
361
|
...(known.class || terms.kind === "type"
|
|
@@ -329,10 +372,13 @@ const proposalRef = (proposal: Proposal, settings: Settings) =>
|
|
|
329
372
|
entityRef(proposal.iri, settings.className, proposal.label);
|
|
330
373
|
|
|
331
374
|
/**
|
|
332
|
-
* File the review item of one instance
|
|
333
|
-
* `fact_status: accepted
|
|
334
|
-
*
|
|
335
|
-
*
|
|
375
|
+
* File the review item of one instance, as LBB's review files it: one status row per proposal
|
|
376
|
+
* (`fact_status: accepted`) in the order a person reads them (the type, the label, values,
|
|
377
|
+
* links, the trace), the type of a new instance and the trace required, one evidence sample per
|
|
378
|
+
* fact, and LBB's key, title, rationale and summary. As the design of the workflows on the
|
|
379
|
+
* fleet asks (G8), the item also holds each statement after the status rows, so its accept
|
|
380
|
+
* commits the facts: each sample names the rows and triplets of its fact (`rows`, `triplets`),
|
|
381
|
+
* and a person can leave facts out of the accept.
|
|
336
382
|
*/
|
|
337
383
|
async function fileItem(
|
|
338
384
|
lbb: LbbActivities,
|
|
@@ -341,97 +387,98 @@ async function fileItem(
|
|
|
341
387
|
message: WorkflowTriggerMessage,
|
|
342
388
|
settings: Settings,
|
|
343
389
|
) {
|
|
344
|
-
const
|
|
345
|
-
const instance = first.instance;
|
|
390
|
+
const instance = members[0]!.outcome.item!.instance;
|
|
346
391
|
const kind = (m: { outcome: Outcome }) => m.outcome.item!.shown.kind;
|
|
392
|
+
const text = (m: { outcome: Outcome }) => m.outcome.item!.shown.text;
|
|
347
393
|
const ordered = [...members].sort(
|
|
348
394
|
(a, b) =>
|
|
349
395
|
review.KIND_ORDER.indexOf(kind(a)) - review.KIND_ORDER.indexOf(kind(b)) ||
|
|
350
|
-
a
|
|
396
|
+
(text(a) < text(b) ? -1 : text(a) > text(b) ? 1 : 0) ||
|
|
397
|
+
(a.iri < b.iri ? -1 : a.iri > b.iri ? 1 : 0),
|
|
351
398
|
);
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
399
|
+
const isNew = members.some((m) => m.outcome.item!.isNew);
|
|
400
|
+
const creates = isNew && ordered.some((m) => kind(m) === "type");
|
|
401
|
+
const className = ordered.map((m) => m.outcome.item!.class).find(Boolean);
|
|
402
|
+
const label = ordered.map((m) => m.outcome.item!.label).find(Boolean);
|
|
403
|
+
// The status rows first, one per fact, as LBB's item holds them; the statements after them.
|
|
404
|
+
const rows: Schemas["EntityPropertiesInput"][] = ordered.map(({ iri }) => ({
|
|
405
|
+
...proposalRef(read.proposals.get(iri)!, settings),
|
|
406
|
+
properties: { fact_status: "accepted" },
|
|
407
|
+
}));
|
|
357
408
|
const triplets: Schemas["TripletInput"][] = [];
|
|
358
|
-
const rows: Schemas["EntityPropertiesInput"][] = [];
|
|
359
|
-
// The rows an accept must keep (`required`, from client 0.24; an older client sends it as is).
|
|
360
409
|
const required: { row: number; reason: string }[] = [];
|
|
410
|
+
const statementRequired: { row: number; reason: string }[] = [];
|
|
361
411
|
const samples: Record<string, unknown>[] = [];
|
|
362
|
-
const whys =
|
|
363
|
-
|
|
412
|
+
const whys: string[] = [];
|
|
413
|
+
const proposers = new Set<string>();
|
|
414
|
+
const parts: string[] = [];
|
|
415
|
+
let links = 0;
|
|
416
|
+
for (const [row, { iri, outcome }] of ordered.entries()) {
|
|
364
417
|
const proposal = read.proposals.get(iri)!;
|
|
365
418
|
const shown = outcome.item!.shown;
|
|
366
419
|
const needed = shown.kind === "source" || (creates && shown.kind === "type");
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (outcome.note) whys.add(outcome.note);
|
|
420
|
+
const reason =
|
|
421
|
+
shown.kind === "type"
|
|
422
|
+
? "a new instance needs its type"
|
|
423
|
+
: "the source of an instance goes with it";
|
|
424
|
+
const factRows = (outcome.facts?.entity_properties ?? []).map((r) => rows.push(r) - 1);
|
|
425
|
+
const factTriplets = (outcome.facts?.triplets ?? []).map((t) => triplets.push(t) - 1);
|
|
426
|
+
if (needed) {
|
|
427
|
+
required.push({ row, reason });
|
|
428
|
+
for (const at of factRows) statementRequired.push({ row: at, reason });
|
|
429
|
+
}
|
|
430
|
+
if (outcome.note && !whys.includes(outcome.note)) whys.push(outcome.note);
|
|
431
|
+
if (proposal.proposedBy) proposers.add(proposal.proposedBy);
|
|
432
|
+
if (shown.kind === "link") links += 1;
|
|
433
|
+
else if (shown.kind !== "source") parts.push(shown.text);
|
|
382
434
|
samples.push({
|
|
383
435
|
proposal: iri,
|
|
384
436
|
row,
|
|
385
|
-
...(writeRows.length ? { rows: writeRows } : {}),
|
|
386
|
-
...(writeTriplets.length ? { triplets: writeTriplets } : {}),
|
|
387
437
|
kind: shown.kind,
|
|
388
438
|
fact: shown.text,
|
|
389
|
-
confidence: Math.round(proposal.confidence * 1000) / 1000,
|
|
439
|
+
confidence: Math.round((outcome.gate ?? proposal.confidence) * 1000) / 1000,
|
|
390
440
|
...(review.place(proposal.evidence) ?? {}),
|
|
391
441
|
...(needed ? { required: true } : {}),
|
|
442
|
+
...(shown.waitsFor !== undefined ? { waits_for: shown.waitsFor } : {}),
|
|
443
|
+
...(factRows.length ? { rows: factRows } : {}),
|
|
444
|
+
...(factTriplets.length ? { triplets: factTriplets } : {}),
|
|
392
445
|
});
|
|
393
446
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
ordered
|
|
397
|
-
.map((m) => m.iri)
|
|
398
|
-
.sort()
|
|
399
|
-
.join("\n"),
|
|
400
|
-
)
|
|
401
|
-
.digest("hex")
|
|
402
|
-
.slice(0, 12);
|
|
447
|
+
if (links) parts.push(links === 1 ? "1 link" : `${links} links`);
|
|
448
|
+
const tail = instance.slice(instance.lastIndexOf("/") + 1);
|
|
403
449
|
const title = creates
|
|
404
|
-
? `New ${className} ${label}`
|
|
405
|
-
: `${className} ${label}: ${
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
);
|
|
450
|
+
? `New ${className ?? "Instance"} ${label ?? tail}`
|
|
451
|
+
: `${className ?? "Instance"} ${label ?? tail}: ${ordered.length === 1 ? "1 fact" : `${ordered.length} facts`} to check`;
|
|
452
|
+
const by = [...proposers].sort();
|
|
453
|
+
const rationale = `${whys.slice(0, 3).join(" ")}${by.length ? ` Proposed by ${by.join(", ")}.` : ""}`;
|
|
409
454
|
await lbb.review({
|
|
410
455
|
workflow: message.workflow,
|
|
411
|
-
key:
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
500,
|
|
456
|
+
key: review.itemKey(
|
|
457
|
+
message.workflow,
|
|
458
|
+
instance,
|
|
459
|
+
ordered.map((m) => m.iri),
|
|
416
460
|
),
|
|
461
|
+
title: review.clip(title, 200),
|
|
462
|
+
rationale: review.clip(rationale.trim(), 500),
|
|
463
|
+
origin: { kind: "agent", id: `starter:${message.workflow}`, label: "review" },
|
|
417
464
|
facts: {
|
|
418
465
|
triplets,
|
|
419
466
|
entity_properties: rows,
|
|
420
|
-
required,
|
|
421
|
-
summary: review.clip(
|
|
467
|
+
required: [...required, ...statementRequired],
|
|
468
|
+
summary: review.clip(parts.join(" · "), 500),
|
|
422
469
|
} as Schemas["SuggestedFacts"],
|
|
423
470
|
mapping: {
|
|
424
471
|
review: {
|
|
425
472
|
instance,
|
|
426
|
-
class: className,
|
|
427
|
-
label,
|
|
473
|
+
class: className ?? null,
|
|
474
|
+
label: label ?? null,
|
|
428
475
|
new: creates,
|
|
429
|
-
facts:
|
|
430
|
-
shown:
|
|
476
|
+
facts: ordered.length,
|
|
477
|
+
shown: ordered.length,
|
|
431
478
|
truncated: false,
|
|
432
479
|
},
|
|
433
480
|
},
|
|
434
|
-
evidence: { records:
|
|
481
|
+
evidence: { records: ordered.length, samples },
|
|
435
482
|
});
|
|
436
483
|
}
|
|
437
484
|
|
package/templates/rows/README.md
CHANGED
|
@@ -62,7 +62,14 @@ same offset. A cursor without `byte`, from an earlier version of the
|
|
|
62
62
|
template, reads the file again from its start. The last turn
|
|
63
63
|
records the derivation with what it wrote: the rows, the instances, the
|
|
64
64
|
columns it mapped and left out, the instance the file represents, and the
|
|
65
|
-
commits of its writes.
|
|
65
|
+
commits of its writes. The record names the upload the check
|
|
66
|
+
answered (`content_uploaded_at`). The cursor carries it from turn to turn.
|
|
67
|
+
When the file was deleted while the run read it, the record writes nothing
|
|
68
|
+
(`409 derivation_upload_changed`). The run ends and counts the file as
|
|
69
|
+
skipped. The run of a new upload reads the file again. Say the file was
|
|
70
|
+
deleted and not uploaded again. The next read of its bytes answers
|
|
71
|
+
`404 file_not_found`. The run ends there too, and counts the file as
|
|
72
|
+
skipped.
|
|
66
73
|
|
|
67
74
|
The parser follows RFC 4180: fields in double quotes can hold the
|
|
68
75
|
delimiter, line breaks and quotes (`""`). A row whose quoted field holds a
|
|
@@ -526,6 +526,62 @@ test("a turn that continues starts at the byte offset where the last one stopped
|
|
|
526
526
|
assert.equal(restarted.turn.keys[0], "check");
|
|
527
527
|
});
|
|
528
528
|
|
|
529
|
+
test("a file deleted and uploaded again while it is read records nothing; the new upload's run reads it", async () => {
|
|
530
|
+
const csv = customerList(10);
|
|
531
|
+
const settings = { ...EVERY_FILE, rowsPerTurn: 4 };
|
|
532
|
+
const { lbb, content, definition } = setup(settings, []);
|
|
533
|
+
const file = content.upload("customers-ten.csv", csv, "text/csv");
|
|
534
|
+
const first = content.uploadedAt(file.sha256);
|
|
535
|
+
|
|
536
|
+
// Turn 1 checks the file; the turns after it carry the upload the check named.
|
|
537
|
+
const one = await runTurn<RowsState>(definition, {}, contentMessage("rows", file));
|
|
538
|
+
type Continued = { cursor: { plan: { uploadedAt?: string } } };
|
|
539
|
+
const carried = one.result.continuation!.message as unknown as Continued;
|
|
540
|
+
assert.equal(carried.cursor.plan.uploadedAt, first);
|
|
541
|
+
|
|
542
|
+
// The file is deleted and uploaded again; the old run reads on to its end.
|
|
543
|
+
content.remove(file.sha256);
|
|
544
|
+
const again = content.upload("customers-ten.csv", csv, "text/csv");
|
|
545
|
+
assert.notEqual(content.uploadedAt(again.sha256), first);
|
|
546
|
+
const old = await runMessage<RowsState>(
|
|
547
|
+
definition,
|
|
548
|
+
one.result.continuation!.message,
|
|
549
|
+
one.result.state,
|
|
550
|
+
);
|
|
551
|
+
assert.deepEqual(old.state, { contents: 0, skipped: 1, rows: 10, instances: 10, represents: 0 });
|
|
552
|
+
assert.equal(content.records.at(-1)?.content_uploaded_at, first);
|
|
553
|
+
assert.equal(content.recorded.size, 0, "nothing is recorded for the deleted upload");
|
|
554
|
+
|
|
555
|
+
// The run of the new upload checks, reads the file and records its derivation.
|
|
556
|
+
const fresh = await runMessage<RowsState>(definition, contentMessage("rows", again));
|
|
557
|
+
assert.equal(fresh.state.contents, 1);
|
|
558
|
+
assert.equal(fresh.state.skipped, 0);
|
|
559
|
+
assert.equal(content.records.at(-1)?.content_uploaded_at, content.uploadedAt(again.sha256));
|
|
560
|
+
assert.equal(content.recorded.size, 1);
|
|
561
|
+
assertEveryRow(lbb.graph, again, csv);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("a file deleted while it is read, and not uploaded again, ends the run as skipped", async () => {
|
|
565
|
+
const csv = customerList(10);
|
|
566
|
+
const settings = { ...EVERY_FILE, rowsPerTurn: 4 };
|
|
567
|
+
const { content, definition } = setup(settings, []);
|
|
568
|
+
const file = content.upload("customers-ten.csv", csv, "text/csv");
|
|
569
|
+
const one = await runTurn<RowsState>(definition, {}, contentMessage("rows", file));
|
|
570
|
+
assert.equal(one.result.state.rows, 4);
|
|
571
|
+
|
|
572
|
+
// The next read of the bytes answers 404: the run ends without an error.
|
|
573
|
+
content.remove(file.sha256);
|
|
574
|
+
const old = await runMessage<RowsState>(
|
|
575
|
+
definition,
|
|
576
|
+
one.result.continuation!.message,
|
|
577
|
+
one.result.state,
|
|
578
|
+
);
|
|
579
|
+
assert.equal(old.turns.length, 1, "the run ends at the turn that read nothing");
|
|
580
|
+
assert.deepEqual(old.state, { contents: 0, skipped: 1, rows: 4, instances: 4, represents: 0 });
|
|
581
|
+
assert.equal(content.records.length, 0, "no derivation is recorded");
|
|
582
|
+
assert.equal(content.recorded.size, 0);
|
|
583
|
+
});
|
|
584
|
+
|
|
529
585
|
test("a row longer than the longest row stops the read: a quote that never closes", async () => {
|
|
530
586
|
const bytes = new TextEncoder().encode(`a,b\n1,"open${"x".repeat(500)}`);
|
|
531
587
|
await assert.rejects(
|