@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,7 +18,11 @@
|
|
|
18
18
|
// header is row 1);
|
|
19
19
|
// 4. with `represents`, writes `Document REPRESENTS <instance>` when the
|
|
20
20
|
// file's name names exactly one instance of that class;
|
|
21
|
-
// 5. records the derivation.
|
|
21
|
+
// 5. records the derivation, for the upload its check named. When the file
|
|
22
|
+
// was deleted (or deleted and uploaded again) since, nothing is recorded,
|
|
23
|
+
// and the run of the new upload reads the file again. A file deleted and
|
|
24
|
+
// not uploaded again answers 404 to the next read of its bytes: the run
|
|
25
|
+
// ends there and counts the file as skipped.
|
|
22
26
|
//
|
|
23
27
|
// A turn reads at most `rowsPerTurn` rows; the rest goes to a new turn
|
|
24
28
|
// (`ctx.continue`) that starts where the last one stopped. The file is read
|
|
@@ -29,6 +33,7 @@ import { createHash } from "node:crypto";
|
|
|
29
33
|
import {
|
|
30
34
|
LbbError,
|
|
31
35
|
entityRef,
|
|
36
|
+
isDerivationUploadChanged,
|
|
32
37
|
triggeredWorkflow,
|
|
33
38
|
type LbbActivities,
|
|
34
39
|
type Schemas,
|
|
@@ -138,7 +143,7 @@ export interface ContentPorts {
|
|
|
138
143
|
export interface RowsState {
|
|
139
144
|
/** Files read to their end. */
|
|
140
145
|
contents: number;
|
|
141
|
-
/** Files skipped: read before with these settings, another name, or
|
|
146
|
+
/** Files skipped: read before with these settings, another name, no file, or deleted while read. */
|
|
142
147
|
skipped: number;
|
|
143
148
|
rows: number;
|
|
144
149
|
instances: number;
|
|
@@ -165,6 +170,8 @@ interface Plan {
|
|
|
165
170
|
key: number | null;
|
|
166
171
|
label: number | null;
|
|
167
172
|
represents: string | null;
|
|
173
|
+
/** The upload of the file the check named: the record names it. */
|
|
174
|
+
uploadedAt?: string;
|
|
168
175
|
}
|
|
169
176
|
|
|
170
177
|
/** Where a file's next turn starts, and what its turns did so far. */
|
|
@@ -196,9 +203,11 @@ interface FileInfo {
|
|
|
196
203
|
mediaType: string;
|
|
197
204
|
}
|
|
198
205
|
|
|
199
|
-
/** What the check of a file found: the file to read, or why it is skipped. */
|
|
206
|
+
/** What the check of a file found: the file to read and its upload, or why it is skipped. */
|
|
200
207
|
interface Checked {
|
|
201
208
|
file?: FileInfo;
|
|
209
|
+
/** The `content_uploaded_at` of the check: absent without a file record. */
|
|
210
|
+
uploadedAt?: string;
|
|
202
211
|
skip?: string;
|
|
203
212
|
}
|
|
204
213
|
|
|
@@ -227,6 +236,10 @@ const SCALAR: Record<string, ValueType> = {
|
|
|
227
236
|
/** The media type without parameters, in lower case. */
|
|
228
237
|
const baseType = (mediaType: string) => mediaType.split(";")[0]!.trim().toLowerCase();
|
|
229
238
|
|
|
239
|
+
/** Whether an error is the 404 of a file's bytes: the file was deleted and not uploaded again. */
|
|
240
|
+
export const fileGone = (error: unknown) =>
|
|
241
|
+
error instanceof LbbError && error.status === 404 && error.code === "file_not_found";
|
|
242
|
+
|
|
230
243
|
/** A file name without its last extension. */
|
|
231
244
|
export const stem = (name: string) => name.replace(/\.[^./\\]*$/, "");
|
|
232
245
|
|
|
@@ -381,158 +394,178 @@ export function createWorkflow(
|
|
|
381
394
|
delete plain.cursor;
|
|
382
395
|
return ctx.continue(next, plain);
|
|
383
396
|
};
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
397
|
+
try {
|
|
398
|
+
const document = message.instances[0];
|
|
399
|
+
if (!document) return { state };
|
|
400
|
+
// A cursor without a byte offset (a message of an earlier version of
|
|
401
|
+
// this template) reads the file again from its start.
|
|
402
|
+
let cursor = typeof message.cursor?.byte === "number" ? message.cursor : undefined;
|
|
403
|
+
// The bytes the header read holds after the header, for the rows of
|
|
404
|
+
// the same turn: they are not read again.
|
|
405
|
+
let held: HeldBytes | undefined;
|
|
406
|
+
|
|
407
|
+
if (!cursor) {
|
|
408
|
+
const checked = await ctx.step("check", async (s): Promise<Checked> => {
|
|
409
|
+
s.progress("checking the file", { done: 0, total: 1, unit: "documents" });
|
|
410
|
+
const file = await readDocument(message, document);
|
|
411
|
+
if (!file) return { skip: "no file" };
|
|
412
|
+
const named = /\.(csv|tsv)$/i.test(file.name);
|
|
413
|
+
if (!MEDIA_TYPES.includes(file.mediaType) && !named) return { skip: "media type" };
|
|
414
|
+
if (match && !match.test(file.name)) return { skip: "name" };
|
|
415
|
+
const found = await ports.workflows.derivations.check(
|
|
416
|
+
derivationKey(file.sha256, settings),
|
|
417
|
+
);
|
|
418
|
+
if (found.exists) return { skip: "derived" };
|
|
419
|
+
const uploadedAt = found.content_uploaded_at ?? undefined;
|
|
420
|
+
return uploadedAt ? { file, uploadedAt } : { file };
|
|
421
|
+
});
|
|
422
|
+
if (!checked.file) {
|
|
423
|
+
const next = { ...state, skipped: state.skipped + 1 };
|
|
424
|
+
return rest(next);
|
|
425
|
+
}
|
|
426
|
+
const file = checked.file;
|
|
427
|
+
const planned = await ctx.step("plan", async (s) => {
|
|
428
|
+
s.progress("reading the header", { done: 0, total: 1, unit: "documents" });
|
|
429
|
+
const delimiter = delimiterOf(file.mediaType, file.name);
|
|
430
|
+
const header = await readRecords(rangeOf(file.sha256), delimiter, 0, 1, 1, windows);
|
|
431
|
+
held = header.held;
|
|
432
|
+
return plan(file, delimiter, header, checked.uploadedAt);
|
|
433
|
+
});
|
|
434
|
+
const first: number[] = [];
|
|
435
|
+
if (planned.plan.represents) {
|
|
436
|
+
const written = await lbb.writeOwned(ctx, "represents", (s) => {
|
|
437
|
+
s.progress("writing facts", { done: 0, total: 1, unit: "documents" });
|
|
438
|
+
const p = planned.plan;
|
|
439
|
+
return {
|
|
440
|
+
subjects: [entityRef(p.document, "Document", p.name)],
|
|
441
|
+
relations: [REPRESENTS],
|
|
442
|
+
triplets: [
|
|
443
|
+
{
|
|
444
|
+
source: entityRef(p.document, "Document", p.name),
|
|
445
|
+
relation: REPRESENTS,
|
|
446
|
+
target: entityRef(
|
|
447
|
+
p.represents!,
|
|
448
|
+
settings.represents!.class,
|
|
449
|
+
planned.label ?? undefined,
|
|
450
|
+
),
|
|
451
|
+
confidence: 1,
|
|
452
|
+
evidence: { text: p.name, source_id: p.document },
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
};
|
|
456
|
+
});
|
|
457
|
+
if (written.commit_seq !== null) first.push(written.commit_seq);
|
|
458
|
+
}
|
|
459
|
+
cursor = {
|
|
460
|
+
byte: planned.at,
|
|
461
|
+
row: 2,
|
|
462
|
+
plan: planned.plan,
|
|
463
|
+
done: { rows: 0, instances: 0, keyless: 0, rejected: 0, commits: first },
|
|
464
|
+
};
|
|
409
465
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
466
|
+
|
|
467
|
+
// The rows of this turn, read from the byte offset where the last turn
|
|
468
|
+
// stopped, then their writes in parts. A turn run again after a crash
|
|
469
|
+
// reads its rows again from the same offset.
|
|
470
|
+
const from = cursor;
|
|
471
|
+
const filePlan = from.plan;
|
|
472
|
+
let rows: Promise<RecordsRead> | undefined;
|
|
473
|
+
const turnRows = () =>
|
|
474
|
+
(rows ??= readRecords(
|
|
475
|
+
rangeOf(filePlan.sha256),
|
|
476
|
+
filePlan.delimiter,
|
|
477
|
+
from.byte,
|
|
478
|
+
from.row,
|
|
479
|
+
settings.rowsPerTurn,
|
|
480
|
+
windows,
|
|
481
|
+
held,
|
|
482
|
+
));
|
|
483
|
+
const windowRecords = async (): Promise<CsvRecord[]> => (await turnRows()).records;
|
|
484
|
+
const window = await ctx.step("rows", async (s) => {
|
|
485
|
+
s.progress("reading rows", { done: 0, unit: "rows" });
|
|
486
|
+
const read = await turnRows();
|
|
487
|
+
const taken = read.records;
|
|
488
|
+
s.progress("reading rows", { done: taken.length, unit: "rows" });
|
|
489
|
+
const counts = { instances: 0, keyless: 0, rejected: 0 };
|
|
490
|
+
for (const record of taken) {
|
|
491
|
+
const built = rowFacts(filePlan, record);
|
|
492
|
+
if (built === "blank") continue;
|
|
493
|
+
if (built === "keyless") counts.keyless += 1;
|
|
494
|
+
else {
|
|
495
|
+
counts.instances += 1;
|
|
496
|
+
counts.rejected += built.rejected;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return { count: taken.length, byte: read.next, row: read.row, end: read.end, ...counts };
|
|
417
500
|
});
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
const written = await lbb.writeOwned(ctx,
|
|
421
|
-
s.progress("writing facts", { done:
|
|
422
|
-
const
|
|
423
|
-
return
|
|
424
|
-
subjects: [entityRef(p.document, "Document", p.name)],
|
|
425
|
-
relations: [REPRESENTS],
|
|
426
|
-
triplets: [
|
|
427
|
-
{
|
|
428
|
-
source: entityRef(p.document, "Document", p.name),
|
|
429
|
-
relation: REPRESENTS,
|
|
430
|
-
target: entityRef(
|
|
431
|
-
p.represents!,
|
|
432
|
-
settings.represents!.class,
|
|
433
|
-
planned.label ?? undefined,
|
|
434
|
-
),
|
|
435
|
-
confidence: 1,
|
|
436
|
-
evidence: { text: p.name, source_id: p.document },
|
|
437
|
-
},
|
|
438
|
-
],
|
|
439
|
-
};
|
|
501
|
+
const commits: number[] = [];
|
|
502
|
+
for (let n = 0; n * PART < window.count; n++) {
|
|
503
|
+
const written = await lbb.writeOwned(ctx, `write-${n}`, async (s) => {
|
|
504
|
+
s.progress("writing facts", { done: n * PART, total: window.count, unit: "rows" });
|
|
505
|
+
const part = (await windowRecords()).slice(n * PART, (n + 1) * PART);
|
|
506
|
+
return partWrite(filePlan, part);
|
|
440
507
|
});
|
|
441
|
-
if (written.commit_seq !== null)
|
|
508
|
+
if (written.commit_seq !== null) commits.push(written.commit_seq);
|
|
442
509
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
510
|
+
const done: Done = {
|
|
511
|
+
rows: from.done.rows + window.count,
|
|
512
|
+
instances: from.done.instances + window.instances,
|
|
513
|
+
keyless: from.done.keyless + window.keyless,
|
|
514
|
+
rejected: from.done.rejected + window.rejected,
|
|
515
|
+
commits: [...from.done.commits, ...commits].slice(0, 256),
|
|
448
516
|
};
|
|
449
|
-
|
|
517
|
+
const counted = {
|
|
518
|
+
...state,
|
|
519
|
+
rows: state.rows + window.count,
|
|
520
|
+
instances: state.instances + window.instances,
|
|
521
|
+
};
|
|
522
|
+
if (!window.end)
|
|
523
|
+
return ctx.continue(counted, {
|
|
524
|
+
...message,
|
|
525
|
+
removed: [],
|
|
526
|
+
cursor: { byte: window.byte, row: window.row, plan: filePlan, done },
|
|
527
|
+
});
|
|
450
528
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const built = rowFacts(filePlan, record);
|
|
476
|
-
if (built === "blank") continue;
|
|
477
|
-
if (built === "keyless") counts.keyless += 1;
|
|
478
|
-
else {
|
|
479
|
-
counts.instances += 1;
|
|
480
|
-
counts.rejected += built.rejected;
|
|
529
|
+
const recorded = await ctx.step("record", async (s) => {
|
|
530
|
+
s.progress("recording the derivation", { done: 0, total: 1, unit: "documents" });
|
|
531
|
+
try {
|
|
532
|
+
const answer = await ports.workflows.derivations.record({
|
|
533
|
+
...derivationKey(filePlan.sha256, settings),
|
|
534
|
+
run_id: ctx.messageId,
|
|
535
|
+
outputs: {
|
|
536
|
+
rows: done.rows,
|
|
537
|
+
instances: done.instances,
|
|
538
|
+
rows_without_key: done.keyless,
|
|
539
|
+
values_left_out: done.rejected,
|
|
540
|
+
columns: filePlan.columns.slice(0, 100).map((c) => `${c.header} → ${c.property}`),
|
|
541
|
+
columns_left_out: filePlan.left,
|
|
542
|
+
represents: filePlan.represents,
|
|
543
|
+
commits: done.commits,
|
|
544
|
+
},
|
|
545
|
+
...(filePlan.uploadedAt ? { content_uploaded_at: filePlan.uploadedAt } : {}),
|
|
546
|
+
});
|
|
547
|
+
return answer.recorded;
|
|
548
|
+
} catch (error) {
|
|
549
|
+
// The file was deleted while this run read it: nothing is
|
|
550
|
+
// recorded, and the run of a new upload reads it again.
|
|
551
|
+
if (isDerivationUploadChanged(error)) return "upload changed";
|
|
552
|
+
throw error;
|
|
481
553
|
}
|
|
482
|
-
}
|
|
483
|
-
return { count: taken.length, byte: read.next, row: read.row, end: read.end, ...counts };
|
|
484
|
-
});
|
|
485
|
-
const commits: number[] = [];
|
|
486
|
-
for (let n = 0; n * PART < window.count; n++) {
|
|
487
|
-
const written = await lbb.writeOwned(ctx, `write-${n}`, async (s) => {
|
|
488
|
-
s.progress("writing facts", { done: n * PART, total: window.count, unit: "rows" });
|
|
489
|
-
const part = (await windowRecords()).slice(n * PART, (n + 1) * PART);
|
|
490
|
-
return partWrite(filePlan, part);
|
|
491
|
-
});
|
|
492
|
-
if (written.commit_seq !== null) commits.push(written.commit_seq);
|
|
493
|
-
}
|
|
494
|
-
const done: Done = {
|
|
495
|
-
rows: from.done.rows + window.count,
|
|
496
|
-
instances: from.done.instances + window.instances,
|
|
497
|
-
keyless: from.done.keyless + window.keyless,
|
|
498
|
-
rejected: from.done.rejected + window.rejected,
|
|
499
|
-
commits: [...from.done.commits, ...commits].slice(0, 256),
|
|
500
|
-
};
|
|
501
|
-
const counted = {
|
|
502
|
-
...state,
|
|
503
|
-
rows: state.rows + window.count,
|
|
504
|
-
instances: state.instances + window.instances,
|
|
505
|
-
};
|
|
506
|
-
if (!window.end)
|
|
507
|
-
return ctx.continue(counted, {
|
|
508
|
-
...message,
|
|
509
|
-
removed: [],
|
|
510
|
-
cursor: { byte: window.byte, row: window.row, plan: filePlan, done },
|
|
511
554
|
});
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
outputs: {
|
|
519
|
-
rows: done.rows,
|
|
520
|
-
instances: done.instances,
|
|
521
|
-
rows_without_key: done.keyless,
|
|
522
|
-
values_left_out: done.rejected,
|
|
523
|
-
columns: filePlan.columns.slice(0, 100).map((c) => `${c.header} → ${c.property}`),
|
|
524
|
-
columns_left_out: filePlan.left,
|
|
525
|
-
represents: filePlan.represents,
|
|
526
|
-
commits: done.commits,
|
|
527
|
-
},
|
|
555
|
+
if (recorded === "upload changed")
|
|
556
|
+
return rest({ ...counted, skipped: counted.skipped + 1 });
|
|
557
|
+
return rest({
|
|
558
|
+
...counted,
|
|
559
|
+
contents: counted.contents + 1,
|
|
560
|
+
represents: counted.represents + (filePlan.represents ? 1 : 0),
|
|
528
561
|
});
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
562
|
+
} catch (error) {
|
|
563
|
+
// The file was deleted while this run read it, and not uploaded
|
|
564
|
+
// again: its bytes answer 404. Nothing is recorded, and the file
|
|
565
|
+
// counts as skipped.
|
|
566
|
+
if (!fileGone(error)) throw error;
|
|
567
|
+
return rest({ ...state, skipped: state.skipped + 1 });
|
|
568
|
+
}
|
|
536
569
|
},
|
|
537
570
|
});
|
|
538
571
|
|
|
@@ -557,8 +590,8 @@ export function createWorkflow(
|
|
|
557
590
|
};
|
|
558
591
|
}
|
|
559
592
|
|
|
560
|
-
/** The plan of a file: its delimiter, the mapped columns, the key and label columns,
|
|
561
|
-
async function plan(file: FileInfo, delimiter: string, read: RecordsRead) {
|
|
593
|
+
/** The plan of a file: its delimiter, the mapped columns, the key and label columns, what it represents, and its upload. */
|
|
594
|
+
async function plan(file: FileInfo, delimiter: string, read: RecordsRead, uploadedAt?: string) {
|
|
562
595
|
const first = read.records[0];
|
|
563
596
|
const header = first ? first.fields.map((h) => h.trim()) : [];
|
|
564
597
|
const registry = header.length ? ((await lbb.ontology()).property_defs ?? []) : [];
|
|
@@ -580,6 +613,7 @@ export function createWorkflow(
|
|
|
580
613
|
key: key >= 0 ? key : null,
|
|
581
614
|
label,
|
|
582
615
|
represents: found?.iri ?? null,
|
|
616
|
+
...(uploadedAt ? { uploadedAt } : {}),
|
|
583
617
|
} satisfies Plan,
|
|
584
618
|
};
|
|
585
619
|
}
|
|
@@ -54,7 +54,14 @@ The text before the first heading, when it is not blank, is the section at
|
|
|
54
54
|
position 0, of level 0, titled with the file's name without its extension.
|
|
55
55
|
A section's lines run from its heading to its last line that is not
|
|
56
56
|
blank. The last turn records the derivation: the sections, their count per
|
|
57
|
-
level, the instance the file represents, and the commits of the writes.
|
|
57
|
+
level, the instance the file represents, and the commits of the writes. The record names the upload the check
|
|
58
|
+
answered (`content_uploaded_at`). The cursor carries it from turn to turn.
|
|
59
|
+
When the file was deleted while the run read it, the record writes nothing
|
|
60
|
+
(`409 derivation_upload_changed`). The run ends and counts the file as
|
|
61
|
+
skipped. The run of a new upload reads the file again. Say the file was
|
|
62
|
+
deleted and not uploaded again. The next read of its bytes answers
|
|
63
|
+
`404 file_not_found`. The run ends there too, and counts the file as
|
|
64
|
+
skipped.
|
|
58
65
|
|
|
59
66
|
## Settings
|
|
60
67
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
phases,
|
|
13
13
|
readJson,
|
|
14
14
|
runMessage,
|
|
15
|
+
runTurn,
|
|
15
16
|
samplesDir,
|
|
16
17
|
type MemoryGraph,
|
|
17
18
|
type MockFile,
|
|
@@ -74,7 +75,7 @@ function setup(settings: Settings = WITH_GUIDES, guides = GUIDES.map((g) => g.na
|
|
|
74
75
|
const definition = createWorkflow(lbb, content, settings);
|
|
75
76
|
const run = (file: MockFile, state: unknown = {}) =>
|
|
76
77
|
runMessage<SectionsState>(definition, contentMessage("sections", file), state);
|
|
77
|
-
return { lbb, content, run };
|
|
78
|
+
return { lbb, content, run, definition };
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/** The written sections by position. */
|
|
@@ -207,6 +208,55 @@ test("a content with a derivation for these settings is skipped, under any name"
|
|
|
207
208
|
assert.deepEqual(again.turns[0]!.keys, ["check"]);
|
|
208
209
|
});
|
|
209
210
|
|
|
211
|
+
test("a file deleted and uploaded again while it is read records nothing; the new upload's run reads it", async () => {
|
|
212
|
+
const { lbb, content, definition } = setup({ ...WITH_GUIDES, sectionsPerTurn: 3 });
|
|
213
|
+
const file = content.upload("support-handbook.md", HANDBOOK, "text/markdown");
|
|
214
|
+
const first = content.uploadedAt(file.sha256);
|
|
215
|
+
|
|
216
|
+
// Turn 1 checks the file; the turns after it carry the upload the check named.
|
|
217
|
+
const one = await runTurn<SectionsState>(definition, {}, contentMessage("sections", file));
|
|
218
|
+
type Continued = { cursor: { plan: { uploadedAt?: string } } };
|
|
219
|
+
const carried = one.result.continuation!.message as unknown as Continued;
|
|
220
|
+
assert.equal(carried.cursor.plan.uploadedAt, first);
|
|
221
|
+
|
|
222
|
+
// The file is deleted and uploaded again; the old run writes on to its end.
|
|
223
|
+
content.remove(file.sha256);
|
|
224
|
+
const again = content.upload("support-handbook.md", HANDBOOK, "text/markdown");
|
|
225
|
+
const old = await runMessage<SectionsState>(
|
|
226
|
+
definition,
|
|
227
|
+
one.result.continuation!.message,
|
|
228
|
+
one.result.state,
|
|
229
|
+
);
|
|
230
|
+
assert.deepEqual(old.state, { contents: 0, skipped: 1, sections: 8, represents: 0 });
|
|
231
|
+
assert.equal(content.records.at(-1)?.content_uploaded_at, first);
|
|
232
|
+
assert.equal(content.recorded.size, 0, "nothing is recorded for the deleted upload");
|
|
233
|
+
|
|
234
|
+
// The run of the new upload checks, writes the sections and records.
|
|
235
|
+
const fresh = await runMessage<SectionsState>(definition, contentMessage("sections", again));
|
|
236
|
+
assert.deepEqual(fresh.state, { contents: 1, skipped: 0, sections: 8, represents: 1 });
|
|
237
|
+
assert.equal(content.records.at(-1)?.content_uploaded_at, content.uploadedAt(again.sha256));
|
|
238
|
+
assert.equal(content.recorded.size, 1);
|
|
239
|
+
assert.equal(written(lbb.graph).length, EXPECTED.sections.length);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("a file deleted while it is read, and not uploaded again, ends the run as skipped", async () => {
|
|
243
|
+
const { content, definition } = setup({ ...WITH_GUIDES, sectionsPerTurn: 3 });
|
|
244
|
+
const file = content.upload("support-handbook.md", HANDBOOK, "text/markdown");
|
|
245
|
+
const one = await runTurn<SectionsState>(definition, {}, contentMessage("sections", file));
|
|
246
|
+
assert.equal(one.result.state.sections, 3);
|
|
247
|
+
|
|
248
|
+
// The next read of the bytes answers 404: the run ends without an error.
|
|
249
|
+
content.remove(file.sha256);
|
|
250
|
+
const old = await runMessage<SectionsState>(
|
|
251
|
+
definition,
|
|
252
|
+
one.result.continuation!.message,
|
|
253
|
+
one.result.state,
|
|
254
|
+
);
|
|
255
|
+
assert.equal(old.turns.length, 1, "the run ends at the turn that read nothing");
|
|
256
|
+
assert.deepEqual(old.state, { contents: 0, skipped: 1, sections: 3, represents: 0 });
|
|
257
|
+
assert.equal(content.records.length, 0, "no derivation is recorded");
|
|
258
|
+
});
|
|
259
|
+
|
|
210
260
|
test("REPRESENTS needs exactly one instance whose label is the file's name", async () => {
|
|
211
261
|
const counts: number[] = [];
|
|
212
262
|
for (const guides of [
|