@semiont/jobs 0.5.32 → 0.5.34
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/dist/index.d.ts +51 -60
- package/dist/index.js +227 -143
- package/dist/index.js.map +1 -1
- package/dist/worker-main.js +326 -179
- package/dist/worker-main.js.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -557,12 +557,16 @@ var YieldCollapseError = class extends DeterministicJobError {
|
|
|
557
557
|
/** What the flagged extraction DID find — every span write-time-verified,
|
|
558
558
|
* so discarding it at the floor would add loss on top of the under-report.
|
|
559
559
|
* Carried on the error because the flag site cannot know whether descent
|
|
560
|
-
* remains possible; the floor is the subdivider's knowledge.
|
|
561
|
-
|
|
560
|
+
* remains possible; the floor is the subdivider's knowledge. The verdict
|
|
561
|
+
* rides beside it so an acceptance can report evidence, not a message
|
|
562
|
+
* string. */
|
|
563
|
+
constructor(message, salvage, verdict) {
|
|
562
564
|
super(message);
|
|
563
565
|
this.salvage = salvage;
|
|
566
|
+
this.verdict = verdict;
|
|
564
567
|
}
|
|
565
568
|
salvage;
|
|
569
|
+
verdict;
|
|
566
570
|
name = "YieldCollapseError";
|
|
567
571
|
};
|
|
568
572
|
function deriveDetectionBudget(limits, scaffoldTokens, typesPerCall) {
|
|
@@ -616,7 +620,7 @@ function outcomeOf(error) {
|
|
|
616
620
|
if (error instanceof InferenceTimeoutError) return "timeout";
|
|
617
621
|
return "error";
|
|
618
622
|
}
|
|
619
|
-
async function callChunkSubdividing(label, chunk, chunking, call, logger) {
|
|
623
|
+
async function callChunkSubdividing(label, chunk, chunking, call, logger, onUnderReport, onCounted) {
|
|
620
624
|
async function recorded(piece, depth, reroll) {
|
|
621
625
|
const start = performance.now();
|
|
622
626
|
try {
|
|
@@ -631,6 +635,7 @@ async function callChunkSubdividing(label, chunk, chunking, call, logger) {
|
|
|
631
635
|
outcome: "success",
|
|
632
636
|
...result.usage ? { inputTokens: result.usage.inputTokens, outputTokens: result.usage.outputTokens } : {}
|
|
633
637
|
});
|
|
638
|
+
if (result.counted !== void 0) onCounted?.(result.counted);
|
|
634
639
|
return result;
|
|
635
640
|
} catch (error) {
|
|
636
641
|
recordDetectionCall({
|
|
@@ -661,6 +666,8 @@ async function callChunkSubdividing(label, chunk, chunking, call, logger) {
|
|
|
661
666
|
salvaged: error.salvage.length,
|
|
662
667
|
error: error.message
|
|
663
668
|
});
|
|
669
|
+
onUnderReport?.(error.verdict);
|
|
670
|
+
onCounted?.(error.verdict.counted);
|
|
664
671
|
return error.salvage;
|
|
665
672
|
}
|
|
666
673
|
if (!truncation(error)) throw error;
|
|
@@ -1184,7 +1191,7 @@ function logAnchorMethod(motivation, exact, anchorMethod) {
|
|
|
1184
1191
|
}
|
|
1185
1192
|
|
|
1186
1193
|
// src/workers/annotation-detection.ts
|
|
1187
|
-
async function detectInChunks(client, content, buildPrompt, motivation, elementSchema, parse, onActivity) {
|
|
1194
|
+
async function detectInChunks(client, content, buildPrompt, motivation, elementSchema, parse, onActivity, onChunkResults) {
|
|
1188
1195
|
const limits = await client.limits();
|
|
1189
1196
|
const scaffoldTokens = estimateTokens(buildPrompt(""));
|
|
1190
1197
|
const { chunking, outputBudget } = deriveDetectionBudget(limits, scaffoldTokens, 1);
|
|
@@ -1204,7 +1211,9 @@ async function detectInChunks(client, content, buildPrompt, motivation, elementS
|
|
|
1204
1211
|
assertNotTruncated(response, `${motivation} detection`, i + 1, chunks.length, outputBudget);
|
|
1205
1212
|
return { items: response.items, ...response.usage ? { usage: response.usage } : {} };
|
|
1206
1213
|
});
|
|
1207
|
-
|
|
1214
|
+
const fromChunk = parse(items);
|
|
1215
|
+
collected.push(...fromChunk);
|
|
1216
|
+
await onChunkResults?.(fromChunk);
|
|
1208
1217
|
if (i < chunks.length - 1) {
|
|
1209
1218
|
onActivity?.(i + 1, chunks.length);
|
|
1210
1219
|
}
|
|
@@ -1220,7 +1229,7 @@ var AnnotationDetection = class {
|
|
|
1220
1229
|
* (source-resource locale). See `types.ts` "Locale conventions" for the
|
|
1221
1230
|
* full discussion.
|
|
1222
1231
|
*/
|
|
1223
|
-
static async detectComments(content, client, instructions, tone, density, language, sourceLanguage, onActivity) {
|
|
1232
|
+
static async detectComments(content, client, instructions, tone, density, language, sourceLanguage, onActivity, onChunkResults) {
|
|
1224
1233
|
return detectInChunks(
|
|
1225
1234
|
client,
|
|
1226
1235
|
content,
|
|
@@ -1228,7 +1237,8 @@ var AnnotationDetection = class {
|
|
|
1228
1237
|
"comment",
|
|
1229
1238
|
COMMENT_ELEMENT_SCHEMA,
|
|
1230
1239
|
(items) => MotivationParsers.parseComments(items, content),
|
|
1231
|
-
onActivity
|
|
1240
|
+
onActivity,
|
|
1241
|
+
onChunkResults
|
|
1232
1242
|
);
|
|
1233
1243
|
}
|
|
1234
1244
|
/**
|
|
@@ -1238,7 +1248,7 @@ var AnnotationDetection = class {
|
|
|
1238
1248
|
* applies, used in the prompt so the LLM analyzes non-English source
|
|
1239
1249
|
* correctly.
|
|
1240
1250
|
*/
|
|
1241
|
-
static async detectHighlights(content, client, instructions, density, sourceLanguage, onActivity) {
|
|
1251
|
+
static async detectHighlights(content, client, instructions, density, sourceLanguage, onActivity, onChunkResults) {
|
|
1242
1252
|
return detectInChunks(
|
|
1243
1253
|
client,
|
|
1244
1254
|
content,
|
|
@@ -1246,7 +1256,8 @@ var AnnotationDetection = class {
|
|
|
1246
1256
|
"highlight",
|
|
1247
1257
|
HIGHLIGHT_ELEMENT_SCHEMA,
|
|
1248
1258
|
(items) => MotivationParsers.parseHighlights(items, content),
|
|
1249
|
-
onActivity
|
|
1259
|
+
onActivity,
|
|
1260
|
+
onChunkResults
|
|
1250
1261
|
);
|
|
1251
1262
|
}
|
|
1252
1263
|
/**
|
|
@@ -1256,7 +1267,7 @@ var AnnotationDetection = class {
|
|
|
1256
1267
|
* (annotation body locale). `sourceLanguage` is the locale of the content
|
|
1257
1268
|
* being analyzed (source-resource locale).
|
|
1258
1269
|
*/
|
|
1259
|
-
static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage, onActivity) {
|
|
1270
|
+
static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage, onActivity, onChunkResults) {
|
|
1260
1271
|
return detectInChunks(
|
|
1261
1272
|
client,
|
|
1262
1273
|
content,
|
|
@@ -1264,7 +1275,8 @@ var AnnotationDetection = class {
|
|
|
1264
1275
|
"assessment",
|
|
1265
1276
|
ASSESSMENT_ELEMENT_SCHEMA,
|
|
1266
1277
|
(items) => MotivationParsers.parseAssessments(items, content),
|
|
1267
|
-
onActivity
|
|
1278
|
+
onActivity,
|
|
1279
|
+
onChunkResults
|
|
1268
1280
|
);
|
|
1269
1281
|
}
|
|
1270
1282
|
/**
|
|
@@ -1279,7 +1291,7 @@ var AnnotationDetection = class {
|
|
|
1279
1291
|
* identifiers, not LLM-generated text — so it's consumed at the body-stamp
|
|
1280
1292
|
* site, not here.
|
|
1281
1293
|
*/
|
|
1282
|
-
static async detectTags(content, client, schema, category, sourceLanguage, onActivity) {
|
|
1294
|
+
static async detectTags(content, client, schema, category, sourceLanguage, onActivity, onChunkResults) {
|
|
1283
1295
|
const categoryInfo = schema.tags.find((t) => t.name === category);
|
|
1284
1296
|
if (!categoryInfo) {
|
|
1285
1297
|
throw new Error(`Invalid category "${category}" for schema ${schema.id}`);
|
|
@@ -1300,7 +1312,8 @@ var AnnotationDetection = class {
|
|
|
1300
1312
|
"tag",
|
|
1301
1313
|
TAG_ELEMENT_SCHEMA,
|
|
1302
1314
|
(items) => MotivationParsers.parseTags(items),
|
|
1303
|
-
onActivity
|
|
1315
|
+
onActivity,
|
|
1316
|
+
onChunkResults ? async (raw) => onChunkResults(MotivationParsers.validateTagOffsets(raw, content, category)) : void 0
|
|
1304
1317
|
);
|
|
1305
1318
|
return MotivationParsers.validateTagOffsets(parsedTags, content, category);
|
|
1306
1319
|
}
|
|
@@ -1337,20 +1350,22 @@ ${piece}
|
|
|
1337
1350
|
pieceChars: piece.length,
|
|
1338
1351
|
error: err instanceof Error ? err.message : String(err)
|
|
1339
1352
|
});
|
|
1340
|
-
return;
|
|
1353
|
+
return void 0;
|
|
1341
1354
|
}
|
|
1342
1355
|
if (counted === void 0) {
|
|
1343
1356
|
logger.warn("Count-verifier answer carried no number \u2014 yield check skipped for this chunk", { pieceChars: piece.length });
|
|
1344
|
-
return;
|
|
1357
|
+
return void 0;
|
|
1345
1358
|
}
|
|
1346
1359
|
if (items.length * YIELD_COLLAPSE_BAND < counted) {
|
|
1347
1360
|
throw new YieldCollapseError(
|
|
1348
1361
|
`Extraction found ${items.length} entities where a count call reports ~${counted} mentions (band \xD7${YIELD_COLLAPSE_BAND}) on a ${piece.length}-char chunk \u2014 silent yield collapse (F7): deterministic \u2014 a same-size retry returns the identical under-report.`,
|
|
1349
|
-
[...items]
|
|
1362
|
+
[...items],
|
|
1363
|
+
{ found: items.length, counted, pieceChars: piece.length }
|
|
1350
1364
|
);
|
|
1351
1365
|
}
|
|
1366
|
+
return counted;
|
|
1352
1367
|
}
|
|
1353
|
-
async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences, logger, sourceLanguage, onActivity) {
|
|
1368
|
+
async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences, logger, sourceLanguage, onActivity, onUnderReport, onCounted, onChunkResults) {
|
|
1354
1369
|
const entityTypesDescription = entityTypes.map((et) => {
|
|
1355
1370
|
if (typeof et === "string") {
|
|
1356
1371
|
return et;
|
|
@@ -1431,14 +1446,17 @@ Example output:
|
|
|
1431
1446
|
items: response.items.length
|
|
1432
1447
|
});
|
|
1433
1448
|
assertNotTruncated(response, "Entity extraction", i + 1, chunks.length, outputBudget);
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1449
|
+
const counted = verifyYield ? await assertYieldNotCollapsed(client, piece, response.items, entityTypesDescription, logger) : void 0;
|
|
1450
|
+
return {
|
|
1451
|
+
items: response.items,
|
|
1452
|
+
...response.usage ? { usage: response.usage } : {},
|
|
1453
|
+
...counted !== void 0 ? { counted } : {}
|
|
1454
|
+
};
|
|
1455
|
+
}, logger, onUnderReport, onCounted);
|
|
1456
|
+
const fromChunk = [];
|
|
1439
1457
|
for (const e of items) {
|
|
1440
1458
|
if (isObject(e) && isString(e.exact) && isString(e.entityType)) {
|
|
1441
|
-
|
|
1459
|
+
fromChunk.push({
|
|
1442
1460
|
exact: e.exact,
|
|
1443
1461
|
entityType: e.entityType,
|
|
1444
1462
|
...isString(e.prefix) ? { prefix: e.prefix } : {},
|
|
@@ -1448,6 +1466,8 @@ Example output:
|
|
|
1448
1466
|
logger.debug("Dropped malformed LLM entity", { entity: e });
|
|
1449
1467
|
}
|
|
1450
1468
|
}
|
|
1469
|
+
collected.push(...fromChunk);
|
|
1470
|
+
await onChunkResults?.(fromChunk);
|
|
1451
1471
|
if (i < chunks.length - 1) {
|
|
1452
1472
|
onActivity?.(i + 1, chunks.length);
|
|
1453
1473
|
}
|
|
@@ -1803,38 +1823,45 @@ function annotationDedupeKey(ann) {
|
|
|
1803
1823
|
}
|
|
1804
1824
|
return [ann.motivation, anchor, JSON.stringify(ann.body ?? null)].join("|");
|
|
1805
1825
|
}
|
|
1806
|
-
function
|
|
1826
|
+
function makeSpanDeduper() {
|
|
1807
1827
|
const seen = /* @__PURE__ */ new Set();
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
const
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1828
|
+
return (annotations) => {
|
|
1829
|
+
const out = [];
|
|
1830
|
+
for (const ann of annotations) {
|
|
1831
|
+
const key = annotationDedupeKey(ann);
|
|
1832
|
+
if (seen.has(key)) continue;
|
|
1833
|
+
seen.add(key);
|
|
1834
|
+
out.push(ann);
|
|
1835
|
+
}
|
|
1836
|
+
return out;
|
|
1837
|
+
};
|
|
1816
1838
|
}
|
|
1817
|
-
async function processHighlightJob(content, inferenceClient, params, buildAnnotation, onProgress) {
|
|
1839
|
+
async function processHighlightJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete) {
|
|
1818
1840
|
const echo = detectionEcho(params);
|
|
1819
1841
|
onProgress(10, { code: "loading" }, echo);
|
|
1820
1842
|
onProgress(30, { code: "analyzing" }, echo);
|
|
1821
|
-
const
|
|
1843
|
+
const dedupe = makeSpanDeduper();
|
|
1844
|
+
let found = 0;
|
|
1845
|
+
let created = 0;
|
|
1846
|
+
await AnnotationDetection.detectHighlights(
|
|
1822
1847
|
content,
|
|
1823
1848
|
inferenceClient,
|
|
1824
1849
|
params.instructions,
|
|
1825
1850
|
params.density,
|
|
1826
1851
|
params.sourceLanguage,
|
|
1827
1852
|
// Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
|
|
1828
|
-
(completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo)
|
|
1853
|
+
(completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo),
|
|
1854
|
+
async (matches) => {
|
|
1855
|
+
found += matches.length;
|
|
1856
|
+
const fresh = dedupe(matches.map((h) => buildAnnotation("highlighting", h)));
|
|
1857
|
+
created += fresh.length;
|
|
1858
|
+
onProgress(60, { code: "creating-annotations", count: created }, echo);
|
|
1859
|
+
await onChunkComplete(fresh);
|
|
1860
|
+
}
|
|
1829
1861
|
);
|
|
1830
|
-
onProgress(
|
|
1831
|
-
const annotations = dedupeAnnotations(highlights.map(
|
|
1832
|
-
(h) => buildAnnotation("highlighting", h)
|
|
1833
|
-
));
|
|
1834
|
-
onProgress(100, { code: "complete-created", count: annotations.length, kind: "highlight" }, echo);
|
|
1862
|
+
onProgress(100, { code: "complete-created", count: created, kind: "highlight" }, echo);
|
|
1835
1863
|
return {
|
|
1836
|
-
|
|
1837
|
-
result: { kind: "highlight-annotation", highlightsFound: highlights.length, highlightsCreated: annotations.length }
|
|
1864
|
+
result: { kind: "highlight-annotation", highlightsFound: found, highlightsCreated: created }
|
|
1838
1865
|
};
|
|
1839
1866
|
}
|
|
1840
1867
|
function detectionEcho(p) {
|
|
@@ -1844,11 +1871,15 @@ function detectionEcho(p) {
|
|
|
1844
1871
|
if (p.density !== void 0) requestParams.push({ label: "density", value: String(p.density) });
|
|
1845
1872
|
return requestParams.length > 0 ? { requestParams } : {};
|
|
1846
1873
|
}
|
|
1847
|
-
async function processCommentJob(content, inferenceClient, params, buildAnnotation, onProgress) {
|
|
1874
|
+
async function processCommentJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete) {
|
|
1848
1875
|
const echo = detectionEcho(params);
|
|
1849
1876
|
onProgress(10, { code: "loading" }, echo);
|
|
1850
1877
|
onProgress(30, { code: "analyzing" }, echo);
|
|
1851
|
-
const
|
|
1878
|
+
const bodyLanguage = params.language ?? "en";
|
|
1879
|
+
const dedupe = makeSpanDeduper();
|
|
1880
|
+
let found = 0;
|
|
1881
|
+
let created = 0;
|
|
1882
|
+
await AnnotationDetection.detectComments(
|
|
1852
1883
|
content,
|
|
1853
1884
|
inferenceClient,
|
|
1854
1885
|
params.instructions,
|
|
@@ -1857,31 +1888,38 @@ async function processCommentJob(content, inferenceClient, params, buildAnnotati
|
|
|
1857
1888
|
params.language,
|
|
1858
1889
|
params.sourceLanguage,
|
|
1859
1890
|
// Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
|
|
1860
|
-
(completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo)
|
|
1891
|
+
(completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo),
|
|
1892
|
+
async (comments) => {
|
|
1893
|
+
found += comments.length;
|
|
1894
|
+
const fresh = dedupe(comments.map(
|
|
1895
|
+
(c) => (
|
|
1896
|
+
// Match the pre-#651 CommentAnnotationWorker: include format and
|
|
1897
|
+
// language on the body TextualBody. Optional in the schema, but
|
|
1898
|
+
// consumers that do language-aware rendering rely on them.
|
|
1899
|
+
buildAnnotation("commenting", c, [
|
|
1900
|
+
{ type: "TextualBody", value: c.comment, purpose: "commenting", format: "text/plain", language: bodyLanguage }
|
|
1901
|
+
])
|
|
1902
|
+
)
|
|
1903
|
+
));
|
|
1904
|
+
created += fresh.length;
|
|
1905
|
+
onProgress(60, { code: "creating-annotations", count: created }, echo);
|
|
1906
|
+
await onChunkComplete(fresh);
|
|
1907
|
+
}
|
|
1861
1908
|
);
|
|
1862
|
-
onProgress(
|
|
1863
|
-
const bodyLanguage = params.language ?? "en";
|
|
1864
|
-
const annotations = dedupeAnnotations(comments.map(
|
|
1865
|
-
(c) => (
|
|
1866
|
-
// Match the pre-#651 CommentAnnotationWorker: include format and
|
|
1867
|
-
// language on the body TextualBody. Optional in the schema, but
|
|
1868
|
-
// consumers that do language-aware rendering rely on them.
|
|
1869
|
-
buildAnnotation("commenting", c, [
|
|
1870
|
-
{ type: "TextualBody", value: c.comment, purpose: "commenting", format: "text/plain", language: bodyLanguage }
|
|
1871
|
-
])
|
|
1872
|
-
)
|
|
1873
|
-
));
|
|
1874
|
-
onProgress(100, { code: "complete-created", count: annotations.length, kind: "comment" }, echo);
|
|
1909
|
+
onProgress(100, { code: "complete-created", count: created, kind: "comment" }, echo);
|
|
1875
1910
|
return {
|
|
1876
|
-
|
|
1877
|
-
result: { kind: "comment-annotation", commentsFound: comments.length, commentsCreated: annotations.length }
|
|
1911
|
+
result: { kind: "comment-annotation", commentsFound: found, commentsCreated: created }
|
|
1878
1912
|
};
|
|
1879
1913
|
}
|
|
1880
|
-
async function processAssessmentJob(content, inferenceClient, params, buildAnnotation, onProgress) {
|
|
1914
|
+
async function processAssessmentJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete) {
|
|
1881
1915
|
const echo = detectionEcho(params);
|
|
1882
1916
|
onProgress(10, { code: "loading" }, echo);
|
|
1883
1917
|
onProgress(30, { code: "analyzing" }, echo);
|
|
1884
|
-
const
|
|
1918
|
+
const bodyLanguage = params.language ?? "en";
|
|
1919
|
+
const dedupe = makeSpanDeduper();
|
|
1920
|
+
let found = 0;
|
|
1921
|
+
let created = 0;
|
|
1922
|
+
await AnnotationDetection.detectAssessments(
|
|
1885
1923
|
content,
|
|
1886
1924
|
inferenceClient,
|
|
1887
1925
|
params.instructions,
|
|
@@ -1890,40 +1928,45 @@ async function processAssessmentJob(content, inferenceClient, params, buildAnnot
|
|
|
1890
1928
|
params.language,
|
|
1891
1929
|
params.sourceLanguage,
|
|
1892
1930
|
// Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
|
|
1893
|
-
(completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo)
|
|
1931
|
+
(completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo),
|
|
1932
|
+
async (assessments) => {
|
|
1933
|
+
found += assessments.length;
|
|
1934
|
+
const fresh = dedupe(assessments.map(
|
|
1935
|
+
(a) => (
|
|
1936
|
+
// Single-object body with purpose aligned to motivation, matching the
|
|
1937
|
+
// pre-#651 AssessmentAnnotationWorker's shape and the majority of
|
|
1938
|
+
// persisted assessments. Do not switch to an array or to
|
|
1939
|
+
// purpose='describing' — that loses the "this is an assessment, not
|
|
1940
|
+
// a description" signal and breaks existing readers that access
|
|
1941
|
+
// `body.value` directly on the object.
|
|
1942
|
+
buildAnnotation("assessing", a, {
|
|
1943
|
+
type: "TextualBody",
|
|
1944
|
+
value: a.assessment,
|
|
1945
|
+
purpose: "assessing",
|
|
1946
|
+
format: "text/plain",
|
|
1947
|
+
language: bodyLanguage
|
|
1948
|
+
})
|
|
1949
|
+
)
|
|
1950
|
+
));
|
|
1951
|
+
created += fresh.length;
|
|
1952
|
+
onProgress(60, { code: "creating-annotations", count: created }, echo);
|
|
1953
|
+
await onChunkComplete(fresh);
|
|
1954
|
+
}
|
|
1894
1955
|
);
|
|
1895
|
-
onProgress(
|
|
1896
|
-
const bodyLanguage = params.language ?? "en";
|
|
1897
|
-
const annotations = dedupeAnnotations(assessments.map(
|
|
1898
|
-
(a) => (
|
|
1899
|
-
// Single-object body with purpose aligned to motivation, matching the
|
|
1900
|
-
// pre-#651 AssessmentAnnotationWorker's shape and the majority of
|
|
1901
|
-
// persisted assessments. Do not switch to an array or to
|
|
1902
|
-
// purpose='describing' — that loses the "this is an assessment, not
|
|
1903
|
-
// a description" signal and breaks existing readers that access
|
|
1904
|
-
// `body.value` directly on the object.
|
|
1905
|
-
buildAnnotation("assessing", a, {
|
|
1906
|
-
type: "TextualBody",
|
|
1907
|
-
value: a.assessment,
|
|
1908
|
-
purpose: "assessing",
|
|
1909
|
-
format: "text/plain",
|
|
1910
|
-
language: bodyLanguage
|
|
1911
|
-
})
|
|
1912
|
-
)
|
|
1913
|
-
));
|
|
1914
|
-
onProgress(100, { code: "complete-created", count: annotations.length, kind: "assessment" }, echo);
|
|
1956
|
+
onProgress(100, { code: "complete-created", count: created, kind: "assessment" }, echo);
|
|
1915
1957
|
return {
|
|
1916
|
-
|
|
1917
|
-
result: { kind: "assessment-annotation", assessmentsFound: assessments.length, assessmentsCreated: annotations.length }
|
|
1958
|
+
result: { kind: "assessment-annotation", assessmentsFound: found, assessmentsCreated: created }
|
|
1918
1959
|
};
|
|
1919
1960
|
}
|
|
1920
|
-
async function processReferenceJob(content, inferenceClient, params, buildAnnotation, onProgress, logger, onUnitComplete, signal) {
|
|
1961
|
+
async function processReferenceJob(content, inferenceClient, params, buildAnnotation, onProgress, logger, onUnitComplete, signal, onChunkComplete) {
|
|
1921
1962
|
const entityTypeNames = params.entityTypes.map(String);
|
|
1922
1963
|
const requestParams = [{ label: "entity-types", value: entityTypeNames.join(", ") }];
|
|
1923
1964
|
const completedItems = [];
|
|
1924
1965
|
let totalFound = 0;
|
|
1925
1966
|
let totalEmitted = 0;
|
|
1926
1967
|
let errors = 0;
|
|
1968
|
+
let totalUnderReportedPieces = 0;
|
|
1969
|
+
let totalExpected = 0;
|
|
1927
1970
|
onProgress(10, { code: "loading" }, { requestParams });
|
|
1928
1971
|
const bodyLanguage = params.language ?? "en";
|
|
1929
1972
|
let completed = 0;
|
|
@@ -1935,6 +1978,7 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
|
|
|
1935
1978
|
processed: completed,
|
|
1936
1979
|
total,
|
|
1937
1980
|
entitiesFound: totalFound,
|
|
1981
|
+
...totalExpected > 0 ? { entitiesExpected: totalExpected } : {},
|
|
1938
1982
|
entitiesEmitted: totalEmitted,
|
|
1939
1983
|
completedItems: [...completedItems],
|
|
1940
1984
|
requestParams
|
|
@@ -1944,7 +1988,14 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
|
|
|
1944
1988
|
if (!entityTypeName) return;
|
|
1945
1989
|
if (signal?.aborted) return;
|
|
1946
1990
|
emitTypeProgress(entityTypeName);
|
|
1947
|
-
const
|
|
1991
|
+
const unresolvedBody = [
|
|
1992
|
+
{ type: "TextualBody", value: entityTypeName, purpose: "tagging", format: "text/plain", language: bodyLanguage }
|
|
1993
|
+
];
|
|
1994
|
+
const dedupe = makeSpanDeduper();
|
|
1995
|
+
let unitFound = 0;
|
|
1996
|
+
let unitPersisted = 0;
|
|
1997
|
+
let underReported;
|
|
1998
|
+
await extractEntities(
|
|
1948
1999
|
content,
|
|
1949
2000
|
[entityTypeName],
|
|
1950
2001
|
inferenceClient,
|
|
@@ -1956,54 +2007,80 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
|
|
|
1956
2007
|
// not silent. It repeats the current position rather than inventing an
|
|
1957
2008
|
// advance — the stall watchdog, janitor and client timeout need a signal,
|
|
1958
2009
|
// not a monotone.
|
|
1959
|
-
() => emitTypeProgress(entityTypeName)
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2010
|
+
() => emitTypeProgress(entityTypeName),
|
|
2011
|
+
(verdict) => {
|
|
2012
|
+
underReported = {
|
|
2013
|
+
pieces: (underReported?.pieces ?? 0) + 1,
|
|
2014
|
+
found: (underReported?.found ?? 0) + verdict.found,
|
|
2015
|
+
counted: (underReported?.counted ?? 0) + verdict.counted
|
|
2016
|
+
};
|
|
2017
|
+
},
|
|
2018
|
+
(counted) => {
|
|
2019
|
+
totalExpected += counted;
|
|
2020
|
+
emitTypeProgress(entityTypeName);
|
|
2021
|
+
},
|
|
2022
|
+
async (chunkEntities) => {
|
|
2023
|
+
const built = [];
|
|
2024
|
+
for (const entity of chunkEntities) {
|
|
2025
|
+
const reconciled = reconcileSelector(content, {
|
|
2026
|
+
exact: entity.exact,
|
|
2027
|
+
...entity.prefix !== void 0 ? { prefix: entity.prefix } : {},
|
|
2028
|
+
...entity.suffix !== void 0 ? { suffix: entity.suffix } : {}
|
|
2029
|
+
});
|
|
2030
|
+
if (!reconciled) {
|
|
2031
|
+
logger.error("Entity dropped \u2014 text not found in source", {
|
|
2032
|
+
text: entity.exact,
|
|
2033
|
+
entityType: entity.entityType
|
|
2034
|
+
});
|
|
2035
|
+
errors++;
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
noteAnchor("reference", entity.exact, reconciled.anchorMethod, logger);
|
|
2039
|
+
built.push(buildAnnotation("linking", toMatch(reconciled), unresolvedBody));
|
|
2040
|
+
}
|
|
2041
|
+
const fresh = dedupe(built);
|
|
2042
|
+
await onChunkComplete?.(fresh);
|
|
2043
|
+
unitFound += chunkEntities.length;
|
|
2044
|
+
unitPersisted += fresh.length;
|
|
2045
|
+
totalFound += chunkEntities.length;
|
|
2046
|
+
totalEmitted += fresh.length;
|
|
2047
|
+
emitTypeProgress(entityTypeName);
|
|
1978
2048
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
built.push(ann);
|
|
1982
|
-
}
|
|
1983
|
-
const unitAnnotations = dedupeAnnotations(built);
|
|
1984
|
-
await onUnitComplete(entityTypeName, unitAnnotations);
|
|
1985
|
-
totalEmitted += unitAnnotations.length;
|
|
1986
|
-
totalFound += extractedEntities.length;
|
|
2049
|
+
);
|
|
2050
|
+
await onUnitComplete(entityTypeName);
|
|
1987
2051
|
completedItems.push({
|
|
1988
2052
|
value: entityTypeName,
|
|
1989
|
-
foundCount:
|
|
1990
|
-
persistedCount:
|
|
2053
|
+
foundCount: unitFound,
|
|
2054
|
+
persistedCount: unitPersisted,
|
|
2055
|
+
...underReported ? { underReported } : {}
|
|
1991
2056
|
});
|
|
2057
|
+
if (underReported) totalUnderReportedPieces += underReported.pieces;
|
|
1992
2058
|
completed++;
|
|
1993
2059
|
emitTypeProgress(entityTypeName);
|
|
1994
2060
|
});
|
|
1995
2061
|
onProgress(100, { code: "complete-created", count: totalEmitted, kind: "reference" }, {
|
|
2062
|
+
...totalExpected > 0 ? { entitiesExpected: totalExpected } : {},
|
|
1996
2063
|
completedItems: [...completedItems],
|
|
1997
2064
|
requestParams
|
|
1998
2065
|
});
|
|
1999
2066
|
return {
|
|
2000
|
-
result: {
|
|
2067
|
+
result: {
|
|
2068
|
+
kind: "reference-annotation",
|
|
2069
|
+
totalFound,
|
|
2070
|
+
totalEmitted,
|
|
2071
|
+
errors,
|
|
2072
|
+
...totalUnderReportedPieces > 0 ? { underReportedPieces: totalUnderReportedPieces } : {}
|
|
2073
|
+
}
|
|
2001
2074
|
};
|
|
2002
2075
|
}
|
|
2003
|
-
async function processTagJob(content, inferenceClient, params, buildAnnotation, onProgress) {
|
|
2076
|
+
async function processTagJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete) {
|
|
2004
2077
|
onProgress(10, { code: "loading" });
|
|
2005
2078
|
onProgress(30, { code: "analyzing-tags" });
|
|
2006
|
-
const
|
|
2079
|
+
const bodyLanguage = params.language ?? "en";
|
|
2080
|
+
const dedupe = makeSpanDeduper();
|
|
2081
|
+
let found = 0;
|
|
2082
|
+
let created = 0;
|
|
2083
|
+
const byCategory = {};
|
|
2007
2084
|
const completedItems = [];
|
|
2008
2085
|
for (let c = 0; c < params.categories.length; c++) {
|
|
2009
2086
|
const category = params.categories[c];
|
|
@@ -2018,7 +2095,8 @@ async function processTagJob(content, inferenceClient, params, buildAnnotation,
|
|
|
2018
2095
|
{ code: "analyzing-tags" },
|
|
2019
2096
|
position()
|
|
2020
2097
|
);
|
|
2021
|
-
|
|
2098
|
+
let categoryFound = 0;
|
|
2099
|
+
await AnnotationDetection.detectTags(
|
|
2022
2100
|
content,
|
|
2023
2101
|
inferenceClient,
|
|
2024
2102
|
params.schema,
|
|
@@ -2030,31 +2108,32 @@ async function processTagJob(content, inferenceClient, params, buildAnnotation,
|
|
|
2030
2108
|
30 + Math.round((c + completed / total) / params.categories.length * 30),
|
|
2031
2109
|
{ code: "analyzing-tags" },
|
|
2032
2110
|
position()
|
|
2033
|
-
)
|
|
2111
|
+
),
|
|
2112
|
+
async (matches) => {
|
|
2113
|
+
categoryFound += matches.length;
|
|
2114
|
+
const fresh = dedupe(matches.map((t) => {
|
|
2115
|
+
const cat = t.category ?? "unknown";
|
|
2116
|
+
return buildAnnotation("tagging", t, [
|
|
2117
|
+
{ type: "TextualBody", value: cat, purpose: "tagging", format: "text/plain", language: bodyLanguage },
|
|
2118
|
+
{ type: "TextualBody", value: params.schema.id, purpose: "classifying", format: "text/plain" }
|
|
2119
|
+
]);
|
|
2120
|
+
}));
|
|
2121
|
+
created += fresh.length;
|
|
2122
|
+
for (const ann of fresh) {
|
|
2123
|
+
const body = ann.body;
|
|
2124
|
+
const cat = Array.isArray(body) && typeof body[0]?.value === "string" ? body[0].value : "unknown";
|
|
2125
|
+
byCategory[cat] = (byCategory[cat] ?? 0) + 1;
|
|
2126
|
+
}
|
|
2127
|
+
onProgress(60, { code: "creating-tag-annotations", count: created });
|
|
2128
|
+
await onChunkComplete(fresh);
|
|
2129
|
+
}
|
|
2034
2130
|
);
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
}
|
|
2038
|
-
const tags = allTags;
|
|
2039
|
-
onProgress(60, { code: "creating-tag-annotations", count: tags.length });
|
|
2040
|
-
const bodyLanguage = params.language ?? "en";
|
|
2041
|
-
const annotations = dedupeAnnotations(tags.map((t) => {
|
|
2042
|
-
const category = t.category ?? "unknown";
|
|
2043
|
-
return buildAnnotation("tagging", t, [
|
|
2044
|
-
{ type: "TextualBody", value: category, purpose: "tagging", format: "text/plain", language: bodyLanguage },
|
|
2045
|
-
{ type: "TextualBody", value: params.schema.id, purpose: "classifying", format: "text/plain" }
|
|
2046
|
-
]);
|
|
2047
|
-
}));
|
|
2048
|
-
const byCategory = {};
|
|
2049
|
-
for (const ann of annotations) {
|
|
2050
|
-
const body = ann.body;
|
|
2051
|
-
const category = Array.isArray(body) && typeof body[0]?.value === "string" ? body[0].value : "unknown";
|
|
2052
|
-
byCategory[category] = (byCategory[category] ?? 0) + 1;
|
|
2131
|
+
found += categoryFound;
|
|
2132
|
+
completedItems.push({ value: category, foundCount: categoryFound });
|
|
2053
2133
|
}
|
|
2054
|
-
onProgress(100, { code: "complete-created", count:
|
|
2134
|
+
onProgress(100, { code: "complete-created", count: created, kind: "tag" });
|
|
2055
2135
|
return {
|
|
2056
|
-
|
|
2057
|
-
result: { kind: "tag-annotation", tagsFound: tags.length, tagsCreated: annotations.length, byCategory }
|
|
2136
|
+
result: { kind: "tag-annotation", tagsFound: found, tagsCreated: created, byCategory }
|
|
2058
2137
|
};
|
|
2059
2138
|
}
|
|
2060
2139
|
function assertWithinOutputBudget(byteLength) {
|
|
@@ -2214,7 +2293,12 @@ var WORKER_AWAITED_OPERATIONS = [
|
|
|
2214
2293
|
// P6). The worker AWAITS this one — a unit may not advance until its
|
|
2215
2294
|
// annotations are in the event log — so its replies must be in the narrow
|
|
2216
2295
|
// channel set or every commit fails fast with `bus.unsubscribed`.
|
|
2217
|
-
"mark:commit"
|
|
2296
|
+
"mark:commit",
|
|
2297
|
+
// The durability probe for a commit whose acknowledgement never routed
|
|
2298
|
+
// (COMMIT-ACK-FALSE-FAILURE F1). SINGULAR by design: the annotation LIST
|
|
2299
|
+
// channel is the multi-MB fan-out this narrowing exists to keep out, and a
|
|
2300
|
+
// rare error path is no reason to let it back in.
|
|
2301
|
+
"browse:annotation-requested"
|
|
2218
2302
|
];
|
|
2219
2303
|
replyChannelsFor(WORKER_AWAITED_OPERATIONS);
|
|
2220
2304
|
|