@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.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
- constructor(message, salvage = []) {
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
- collected.push(...parse(items));
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
- if (verifyYield) {
1435
- await assertYieldNotCollapsed(client, piece, response.items, entityTypesDescription, logger);
1436
- }
1437
- return { items: response.items, ...response.usage ? { usage: response.usage } : {} };
1438
- }, logger);
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
- collected.push({
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 dedupeAnnotations(annotations) {
1826
+ function makeSpanDeduper() {
1807
1827
  const seen = /* @__PURE__ */ new Set();
1808
- const out = [];
1809
- for (const ann of annotations) {
1810
- const key = annotationDedupeKey(ann);
1811
- if (seen.has(key)) continue;
1812
- seen.add(key);
1813
- out.push(ann);
1814
- }
1815
- return out;
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 highlights = await AnnotationDetection.detectHighlights(
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(60, { code: "creating-annotations", count: highlights.length }, echo);
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
- annotations,
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 comments = await AnnotationDetection.detectComments(
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(60, { code: "creating-annotations", count: comments.length }, echo);
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
- annotations,
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 assessments = await AnnotationDetection.detectAssessments(
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(60, { code: "creating-annotations", count: assessments.length }, echo);
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
- annotations,
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 extractedEntities = await extractEntities(
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
- const unresolvedBody = [
1962
- { type: "TextualBody", value: entityTypeName, purpose: "tagging", format: "text/plain", language: bodyLanguage }
1963
- ];
1964
- const built = [];
1965
- for (const entity of extractedEntities) {
1966
- const reconciled = reconcileSelector(content, {
1967
- exact: entity.exact,
1968
- ...entity.prefix !== void 0 ? { prefix: entity.prefix } : {},
1969
- ...entity.suffix !== void 0 ? { suffix: entity.suffix } : {}
1970
- });
1971
- if (!reconciled) {
1972
- logger.error("Entity dropped \u2014 text not found in source", {
1973
- text: entity.exact,
1974
- entityType: entity.entityType
1975
- });
1976
- errors++;
1977
- continue;
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
- noteAnchor("reference", entity.exact, reconciled.anchorMethod, logger);
1980
- const ann = buildAnnotation("linking", toMatch(reconciled), unresolvedBody);
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: extractedEntities.length,
1990
- persistedCount: unitAnnotations.length
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: { kind: "reference-annotation", totalFound, totalEmitted, errors }
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 allTags = [];
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
- const categoryTags = await AnnotationDetection.detectTags(
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
- completedItems.push({ value: category, foundCount: categoryTags.length });
2036
- allTags.push(...categoryTags);
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: annotations.length, kind: "tag" });
2134
+ onProgress(100, { code: "complete-created", count: created, kind: "tag" });
2055
2135
  return {
2056
- annotations,
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