@semiont/make-meaning 0.5.29 → 0.5.31

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  import { JobQueue } from '@semiont/jobs';
2
2
  import { SemiontProject, SemiontState } from '@semiont/core/node';
3
- import { GraphServiceConfig, VectorsServiceConfig, EmbeddingServiceConfig, ArchivistServiceConfig, EnvironmentConfig, StateUnit, EventBus, Logger, ResourceId, ResourceDescriptor, components, AnnotationId, GatheredContext, ResourceAnnotations, Annotation, AnnotationCategory, ITransport, BaseUrl, ConnectionState, SemiontError, UserDID, EventMap, IContentTransport, PutBinaryRequest, PutBinaryOptions, ExtractionOutcome, AccessToken, BusRequestPrimitive, ChunkingConfig, UserId } from '@semiont/core';
3
+ import { GraphServiceConfig, VectorsServiceConfig, EmbeddingServiceConfig, ArchivistServiceConfig, EnvironmentConfig, StateUnit, EventBus, Logger, ResourceId, ResourceDescriptor, components, AnnotationId, GatheredContext, ResourceAnnotations, Annotation, AnnotationCategory, ITransport, BaseUrl, ConnectionState, SemiontError, UserDID, EventMap, IContentTransport, PutBinaryRequest, PutBinaryOptions, AccessToken, BusRequestPrimitive, ChunkingConfig, UserId } from '@semiont/core';
4
4
  import { EventReadStorage, ViewMaterializer, EventStore, ViewStorage, EventLog } from '@semiont/event-sourcing';
5
5
  import { WorkingTreeStore, AnchoredTextStore, ContentReads } from '@semiont/content';
6
6
  import { GraphDatabase } from '@semiont/graph';
@@ -337,6 +337,25 @@ declare class Stower {
337
337
  private handleYieldUpdate;
338
338
  private handleYieldMv;
339
339
  private handleMarkCreate;
340
+ /**
341
+ * Persist a detection unit's annotations as ONE acknowledged batch, then
342
+ * answer (JOB-RESTART-SAFETY P6).
343
+ *
344
+ * The difference from `mark:create` is the reply, and it is the whole point.
345
+ * `mark:create` is fire-and-forget: the worker's emit resolves when the bus
346
+ * accepts it, which says nothing about the event log, so a down Stower loses
347
+ * a unit silently and a flapping one hangs the worker forever. This answers
348
+ * only after every append has returned, so the worker can gate unit
349
+ * completion — and its checkpoint — on durability.
350
+ *
351
+ * Appends are sequential, not concurrent: the event log is the system of
352
+ * record and a batch that half-lands under concurrency is harder to reason
353
+ * about than one that stops at the first failure. A partial batch is
354
+ * reported as a failure and the worker retries the WHOLE unit, which is safe
355
+ * because ids are deterministic (P3) and the annotation fold is idempotent
356
+ * by id — re-appending what already landed changes nothing.
357
+ */
358
+ private handleMarkCommit;
340
359
  private handleMarkDelete;
341
360
  private handleMarkUpdateBody;
342
361
  private handleMarkArchive;
@@ -722,8 +741,6 @@ interface LimitsDiscovery {
722
741
  *
723
742
  * Handles:
724
743
  * - browse:resource-requested — single resource metadata (materialized from events)
725
- * - browse:anchored-text-by-checksum-requested — the detection workers' read-through
726
- * cache consult: a stored extraction outcome by content identity (ANCHORED-TEXT-TO-SMELTER P2)
727
744
  * - browse:resources-requested — list resources
728
745
  * - browse:annotations-requested — all annotations for a resource
729
746
  * - browse:annotation-requested — single annotation with resolved resource
@@ -794,7 +811,6 @@ declare class Browser {
794
811
  * parser nor engine. Read-only over the wire: the Smelter is the sole
795
812
  * writer and never answers here.
796
813
  */
797
- private handleAnchoredTextByChecksum;
798
814
  private handleBrowseResource;
799
815
  private handleBrowseResources;
800
816
  private handleBrowseAnnotations;
@@ -1133,36 +1149,6 @@ declare class LocalContentTransport implements IContentTransport {
1133
1149
  putBinary(_request: PutBinaryRequest, _options?: PutBinaryOptions): Promise<{
1134
1150
  resourceId: ResourceId;
1135
1151
  }>;
1136
- /**
1137
- * Store a derived coordinate map under the content checksum the producer
1138
- * read (PERSIST-ANCHORS decision A — see the interface doc for why the
1139
- * producer supplies the key). In local mode this is the same store the
1140
- * HTTP route writes to — one storage authority, reached the same way from
1141
- * every process (ANCHORED-TEXT-CACHE Lane 5).
1142
- */
1143
- putAnchoredText(checksum: string, outcome: ExtractionOutcome, _options?: {
1144
- auth?: AccessToken;
1145
- }): Promise<void>;
1146
- /** The stored outcome, or null when nothing has derived one — the common case. */
1147
- getAnchoredText(resourceId: ResourceId, _options?: {
1148
- auth?: AccessToken;
1149
- }): Promise<ExtractionOutcome | null>;
1150
- /**
1151
- * The cache-consult read (PERSIST-ANCHORS P2c), straight from the store —
1152
- * checksum-addressed, so no view resolution and no settle barrier: the
1153
- * caller holds the content identity already.
1154
- */
1155
- getAnchoredTextByChecksum(checksum: string, _options?: {
1156
- auth?: AccessToken;
1157
- }): Promise<ExtractionOutcome | null>;
1158
- /**
1159
- * The store's would-hit keys, straight from the store — planning data for
1160
- * the reconcile diff (PERSIST-ANCHORS P0), so no settle barrier applies:
1161
- * presence is being asked, not content at a moment.
1162
- */
1163
- listAnchoredTextKeys(_options?: {
1164
- auth?: AccessToken;
1165
- }): Promise<string[]>;
1166
1152
  getBinary(resourceId: ResourceId, _options?: {
1167
1153
  auth?: AccessToken;
1168
1154
  }): Promise<{
@@ -1654,10 +1640,12 @@ declare class Smelter {
1654
1640
  * representation's checksum (the bytes the smelter would read), the
1655
1641
  * current entity-type set (the discriminator the stamps must carry), and
1656
1642
  * whether the media type's extractor derives geometry (whether an
1657
- * anchored-text artifact should exist). Embeddable an extractor exists
1658
- * for the media type's strategy the same registry the live fetch
1659
- * resolves, and `yieldsGeometry` is declared on the extractor itself, so
1660
- * every gate here and the live fetch's behavior are twins by construction.
1643
+ * anchored-text artifact should exist). Both answers are core's, keyed by the
1644
+ * media type's strategy: embeddable the type has any text-reading strategy
1645
+ * at all, and geometry that strategy derives it. Until READ-VS-EXTRACT P2
1646
+ * embeddability was asked as `EXTRACTORS[strategy] !== null` true, but a
1647
+ * second statement of `strategy !== 'none'`, answered by resolving an
1648
+ * implementation to learn a fact about a media type.
1661
1649
  * Shared by `reconcile()` and the `smelt:rebuild-anchors` planner.
1662
1650
  */
1663
1651
  private classifyEmbeddable;
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { STALL_THRESHOLD_MS, FsJobQueue } from '@semiont/jobs';
2
2
  import { FilesystemViewStorage, resolveStorageUri, EventQuery, createEventStore } from '@semiont/event-sourcing';
3
- import { getResourceEntityTypes, getResourceId, getTargetSource, resourceId, decodeRepresentation, getBodySource, getStorageUri, getPrimaryRepresentation, getTargetSelector, deriveViews, getTextPositionSelector, annotationId, errField, userId, generateUuid, getExactText, busRequest, cloneToken, assembleAnnotation, applyBodyOperations, didToAgent, isGenerationJobParams, jobId, entityType, baseUrl, busLog, BRIDGED_CHANNELS, burstBuffer, textExtractionOf, chunkText, getPrimaryMediaType, isAnnotatable, softwareToAgent } from '@semiont/core';
3
+ import { getResourceEntityTypes, getResourceId, getTargetSource, resourceId, decodeRepresentation, getBodySource, getStorageUri, getPrimaryRepresentation, getTargetSelector, deriveViews, getTextPositionSelector, annotationId, errField, userId, generateUuid, getExactText, busRequest, cloneToken, assembleAnnotation, applyBodyOperations, didToAgent, isGenerationJobParams, jobId, entityType, baseUrl, busLog, BRIDGED_CHANNELS, burstBuffer, textSourceOf, chunkText, getPrimaryMediaType, yieldsGeometryOf, isAnnotatable, softwareToAgent } from '@semiont/core';
4
4
  import { recordGatherDegrade, withActorSpan, recordBusEmit, withSpan, SpanKind, registerJobQueueProvider, registerVectorIndexSizeProvider } from '@semiont/observability';
5
5
  import { createInferenceClient } from '@semiont/inference';
6
6
  import { compareByRecencyThenId, getGraphDatabase } from '@semiont/graph';
7
- import { WorkingTreeStore, createAnchoredTextStore, RepresentationMissing, ChecksumMismatchError, EXTRACTORS, calculateChecksum } from '@semiont/content';
7
+ import { WorkingTreeStore, createAnchoredTextStore, RepresentationMissing, ChecksumMismatchError, derivingExtractorFor, calculateChecksum } from '@semiont/content';
8
8
  import { getEntityTypes, DEFAULT_ENTITY_TYPES } from '@semiont/ontology';
9
9
  import { mergeByResource } from '@semiont/vectors';
10
10
  import { promises } from 'fs';
@@ -11335,6 +11335,7 @@ var Stower = class {
11335
11335
  pipe("yield:update", (e) => this.handleYieldUpdate(e)),
11336
11336
  pipe("yield:mv", (e) => this.handleYieldMv(e)),
11337
11337
  pipe("mark:create", (e) => this.handleMarkCreate(e)),
11338
+ pipe("mark:commit", (e) => this.handleMarkCommit(e)),
11338
11339
  pipe("mark:delete", (e) => this.handleMarkDelete(e)),
11339
11340
  pipe("mark:update-body", (e) => this.handleMarkUpdateBody(e)),
11340
11341
  pipe("frame:add-entity-type", (e) => this.handleAddEntityType(e)),
@@ -11560,6 +11561,61 @@ var Stower = class {
11560
11561
  });
11561
11562
  }
11562
11563
  }
11564
+ /**
11565
+ * Persist a detection unit's annotations as ONE acknowledged batch, then
11566
+ * answer (JOB-RESTART-SAFETY P6).
11567
+ *
11568
+ * The difference from `mark:create` is the reply, and it is the whole point.
11569
+ * `mark:create` is fire-and-forget: the worker's emit resolves when the bus
11570
+ * accepts it, which says nothing about the event log, so a down Stower loses
11571
+ * a unit silently and a flapping one hangs the worker forever. This answers
11572
+ * only after every append has returned, so the worker can gate unit
11573
+ * completion — and its checkpoint — on durability.
11574
+ *
11575
+ * Appends are sequential, not concurrent: the event log is the system of
11576
+ * record and a batch that half-lands under concurrency is harder to reason
11577
+ * about than one that stops at the first failure. A partial batch is
11578
+ * reported as a failure and the worker retries the WHOLE unit, which is safe
11579
+ * because ids are deterministic (P3) and the annotation fold is idempotent
11580
+ * by id — re-appending what already landed changes nothing.
11581
+ */
11582
+ async handleMarkCommit(event) {
11583
+ if (!event._userId) {
11584
+ throw new Error("mark:commit missing _userId (gateway injection)");
11585
+ }
11586
+ const annotations = event.annotations ?? [];
11587
+ try {
11588
+ let persisted = 0;
11589
+ for (const annotation of annotations) {
11590
+ await this.stores.eventStore.appendEvent({
11591
+ type: "mark:added",
11592
+ resourceId: resourceId(event.resourceId),
11593
+ userId: userId(event._userId),
11594
+ version: 1,
11595
+ payload: { annotation }
11596
+ });
11597
+ persisted++;
11598
+ }
11599
+ this.logger.debug("Committed annotation batch", {
11600
+ correlationId: event.correlationId,
11601
+ resourceId: event.resourceId,
11602
+ persisted
11603
+ });
11604
+ this.eventBus.get("mark:commit-ok").next({
11605
+ correlationId: event.correlationId,
11606
+ response: { persisted, annotationIds: annotations.map((a) => String(a.id)) }
11607
+ });
11608
+ } catch (error) {
11609
+ this.logger.error("Failed to commit annotation batch", {
11610
+ correlationId: event.correlationId,
11611
+ error: errField(error)
11612
+ });
11613
+ this.eventBus.get("mark:commit-failed").next({
11614
+ correlationId: event.correlationId,
11615
+ message: error instanceof Error ? error.message : String(error)
11616
+ });
11617
+ }
11618
+ }
11563
11619
  async handleMarkDelete(event) {
11564
11620
  if (!event._userId) {
11565
11621
  throw new Error("mark:delete missing _userId (gateway injection)");
@@ -11805,17 +11861,19 @@ var Stower = class {
11805
11861
  var import_rxjs4 = __toESM(require_cjs());
11806
11862
  var import_operators4 = __toESM(require_operators());
11807
11863
  var ANCHORED_TEXT_SETTLE_TIMEOUT_MS = 15e3;
11808
- async function readAnchoredText(kb, resourceId10) {
11864
+ async function readAnchoredText(kb, resourceId10, settleTimeoutMs = ANCHORED_TEXT_SETTLE_TIMEOUT_MS) {
11809
11865
  const view = await kb.views.get(resourceId(resourceId10));
11810
11866
  const checksum = getPrimaryRepresentation(view?.resource)?.checksum;
11811
- if (!checksum) return null;
11867
+ if (!checksum) return { kind: "unknown" };
11812
11868
  const hit = await kb.anchoredText.read(checksum);
11813
11869
  if (hit) return hit;
11814
11870
  try {
11815
- const outcome = await kb.smeltProgress.whenSettled(resourceId10, checksum, ANCHORED_TEXT_SETTLE_TIMEOUT_MS);
11816
- return outcome === "indexed" ? kb.anchoredText.read(checksum) : null;
11871
+ const outcome = await kb.smeltProgress.whenSettled(resourceId10, checksum, settleTimeoutMs);
11872
+ if (outcome === "skipped") return { kind: "no-map" };
11873
+ if (outcome === "inert") return { kind: "not-yet" };
11874
+ return await kb.anchoredText.read(checksum) ?? { kind: "not-yet" };
11817
11875
  } catch (error) {
11818
- if (error instanceof SmeltProgressTimeout) return null;
11876
+ if (error instanceof SmeltProgressTimeout) return { kind: "not-yet" };
11819
11877
  throw error;
11820
11878
  }
11821
11879
  }
@@ -11947,7 +12005,6 @@ var Browser = class {
11947
12005
  this.subscriptions.push(
11948
12006
  pipe("browse:resource-requested", (e) => this.handleBrowseResource(e)).subscribe({ error: errorHandler }),
11949
12007
  pipe("browse:anchored-text-requested", (e) => this.handleAnchoredText(e)).subscribe({ error: errorHandler }),
11950
- pipe("browse:anchored-text-by-checksum-requested", (e) => this.handleAnchoredTextByChecksum(e)).subscribe({ error: errorHandler }),
11951
12008
  pipe("browse:resources-requested", (e) => this.handleBrowseResources(e)).subscribe({ error: errorHandler }),
11952
12009
  pipe("browse:annotations-requested", (e) => this.handleBrowseAnnotations(e)).subscribe({ error: errorHandler }),
11953
12010
  pipe("browse:annotation-requested", (e) => this.handleBrowseAnnotation(e)).subscribe({ error: errorHandler }),
@@ -12001,20 +12058,6 @@ var Browser = class {
12001
12058
  * parser nor engine. Read-only over the wire: the Smelter is the sole
12002
12059
  * writer and never answers here.
12003
12060
  */
12004
- async handleAnchoredTextByChecksum(event) {
12005
- try {
12006
- this.eventBus.get("browse:anchored-text-by-checksum-result").next({
12007
- correlationId: event.correlationId,
12008
- response: await this.kb.anchoredText.read(event.checksum)
12009
- });
12010
- } catch (error) {
12011
- this.logger.error("Browse anchored text by checksum failed", { checksum: event.checksum, error: errField(error) });
12012
- this.eventBus.get("browse:anchored-text-by-checksum-failed").next({
12013
- correlationId: event.correlationId,
12014
- message: error instanceof Error ? error.message : String(error)
12015
- });
12016
- }
12017
- }
12018
12061
  async handleBrowseResource(event) {
12019
12062
  try {
12020
12063
  const response = await assembleResourceGraph(this.kb, resourceId(event.resourceId));
@@ -13194,7 +13237,7 @@ function registerJobCommandHandlers(eventBus, jobQueue, state, parentLogger) {
13194
13237
  });
13195
13238
  eventBus.get("job:fail").subscribe(async (event) => {
13196
13239
  try {
13197
- const outcome = await jobQueue.failJob(jobId(event.jobId), event.error);
13240
+ const outcome = await jobQueue.failJob(jobId(event.jobId), event.error, event.completedUnits, event.failureClass);
13198
13241
  if (outcome === "retried") {
13199
13242
  logger.info("Job re-queued for retry", { jobId: event.jobId });
13200
13243
  } else if (outcome === null) {
@@ -13220,16 +13263,45 @@ function registerJobCommandHandlers(eventBus, jobQueue, state, parentLogger) {
13220
13263
  });
13221
13264
  }
13222
13265
  });
13266
+ eventBus.get("job:checkpoint").subscribe(async (event) => {
13267
+ try {
13268
+ await jobQueue.checkpointUnits(jobId(event.jobId), event.completedUnits);
13269
+ } catch (error) {
13270
+ logger.error("Failed to checkpoint job units", {
13271
+ jobId: event.jobId,
13272
+ error: error.message
13273
+ });
13274
+ }
13275
+ });
13223
13276
  eventBus.get("job:cancel-requested").subscribe(async (event) => {
13224
13277
  try {
13225
- const cancelled = await jobQueue.cancelPendingJobs(event.jobType);
13226
- logger.info("Cancel requested", { jobType: event.jobType, cancelled });
13278
+ let cancelled;
13279
+ if (event.jobId) {
13280
+ const target = await jobQueue.getJob(jobId(event.jobId));
13281
+ if (!target) {
13282
+ cancelled = 0;
13283
+ } else if (target.status === "pending") {
13284
+ cancelled = await jobQueue.cancelJob(jobId(event.jobId)) ? 1 : 0;
13285
+ } else if (target.status === "running") {
13286
+ logger.info("Cancel of running job delegated to its worker", { jobId: event.jobId });
13287
+ cancelled = 1;
13288
+ } else {
13289
+ cancelled = 0;
13290
+ }
13291
+ logger.info("Cancel requested", { jobId: event.jobId, cancelled });
13292
+ } else if (event.jobType) {
13293
+ cancelled = await jobQueue.cancelPendingJobs(event.jobType);
13294
+ logger.info("Cancel requested", { jobType: event.jobType, cancelled });
13295
+ } else {
13296
+ cancelled = 0;
13297
+ }
13227
13298
  eventBus.get("job:cancel-ok").next({
13228
13299
  correlationId: event.correlationId,
13229
13300
  response: { cancelled }
13230
13301
  });
13231
13302
  } catch (error) {
13232
- logger.error("Failed to cancel pending jobs", {
13303
+ logger.error("Failed to cancel jobs", {
13304
+ jobId: event.jobId,
13233
13305
  jobType: event.jobType,
13234
13306
  error: error.message
13235
13307
  });
@@ -13239,6 +13311,17 @@ function registerJobCommandHandlers(eventBus, jobQueue, state, parentLogger) {
13239
13311
  });
13240
13312
  }
13241
13313
  });
13314
+ eventBus.get("job:cancel").subscribe(async (event) => {
13315
+ try {
13316
+ await jobQueue.cancelJob(jobId(event.jobId));
13317
+ logger.info("Job cancelled by its worker", { jobId: event.jobId });
13318
+ } catch (error) {
13319
+ logger.error("Failed to cancel job", {
13320
+ jobId: event.jobId,
13321
+ error: error.message
13322
+ });
13323
+ }
13324
+ });
13242
13325
  }
13243
13326
 
13244
13327
  // src/handlers/index.ts
@@ -13636,40 +13719,6 @@ var LocalContentTransport = class {
13636
13719
  "LocalContentTransport does not support putBinary() \u2014 create resources via bus emits (mark/yield namespaces) in local mode"
13637
13720
  );
13638
13721
  }
13639
- /**
13640
- * Store a derived coordinate map under the content checksum the producer
13641
- * read (PERSIST-ANCHORS decision A — see the interface doc for why the
13642
- * producer supplies the key). In local mode this is the same store the
13643
- * HTTP route writes to — one storage authority, reached the same way from
13644
- * every process (ANCHORED-TEXT-CACHE Lane 5).
13645
- */
13646
- async putAnchoredText(checksum, outcome, _options) {
13647
- busLog("PUT", "anchored-text", { checksum });
13648
- await this.kb.anchoredText.write(checksum, outcome);
13649
- }
13650
- /** The stored outcome, or null when nothing has derived one — the common case. */
13651
- async getAnchoredText(resourceId10, _options) {
13652
- busLog("GET", "anchored-text", { resourceId: resourceId10 });
13653
- return readAnchoredText(this.kb, resourceId10);
13654
- }
13655
- /**
13656
- * The cache-consult read (PERSIST-ANCHORS P2c), straight from the store —
13657
- * checksum-addressed, so no view resolution and no settle barrier: the
13658
- * caller holds the content identity already.
13659
- */
13660
- async getAnchoredTextByChecksum(checksum, _options) {
13661
- busLog("GET", "anchored-text-by-checksum", { checksum });
13662
- return this.kb.anchoredText.read(checksum);
13663
- }
13664
- /**
13665
- * The store's would-hit keys, straight from the store — planning data for
13666
- * the reconcile diff (PERSIST-ANCHORS P0), so no settle barrier applies:
13667
- * presence is being asked, not content at a moment.
13668
- */
13669
- async listAnchoredTextKeys(_options) {
13670
- busLog("GET", "anchored-text-keys", {});
13671
- return this.kb.anchoredText.list();
13672
- }
13673
13722
  async getBinary(resourceId10, _options) {
13674
13723
  busLog("GET", "content", { resourceId: resourceId10 });
13675
13724
  return withSpan(
@@ -14011,8 +14060,8 @@ var Smelter = class _Smelter {
14011
14060
  if (!rid) return;
14012
14061
  const { data, contentType } = await this.content.getBinary(resourceId(rid));
14013
14062
  const bytes = Buffer.from(data);
14014
- const extractor = EXTRACTORS[textExtractionOf(contentType)];
14015
- if (!extractor?.yieldsGeometry) {
14063
+ const extractor = derivingExtractorFor(contentType);
14064
+ if (!extractor) {
14016
14065
  this.logger.info("Re-anchor found no geometry-capable extractor", { resourceId: rid, contentType });
14017
14066
  return;
14018
14067
  }
@@ -14051,15 +14100,12 @@ var Smelter = class _Smelter {
14051
14100
  const { data, contentType } = await this.content.getBinary(resourceId(resourceId10));
14052
14101
  const bytes = Buffer.from(data);
14053
14102
  const checksum = calculateChecksum(bytes);
14054
- const extractor = EXTRACTORS[textExtractionOf(contentType)];
14055
- if (!extractor) {
14056
- this.logger.debug("Skipping resource with no extractor for its media type", { resourceId: resourceId10, contentType });
14103
+ const extractor = derivingExtractorFor(contentType);
14104
+ if (!extractor && textSourceOf(contentType) === "none") {
14105
+ this.logger.debug("Skipping resource with no way to read its media type", { resourceId: resourceId10, contentType });
14057
14106
  return { kind: "skipped", checksum, reason: "no-extractor" };
14058
14107
  }
14059
- const extracted = await extractor.extract(bytes, contentType, {
14060
- key: checksum,
14061
- store: this.anchoredStore
14062
- });
14108
+ const extracted = extractor ? await extractor.extract(bytes, contentType, { key: checksum, store: this.anchoredStore }) : { kind: "extracted", text: decodeRepresentation(bytes, contentType), method: "text-passthrough" };
14063
14109
  if (extracted.kind === "declined") {
14064
14110
  this.logger.debug("Extractor declined", { resourceId: resourceId10, contentType, reason: extracted.declined });
14065
14111
  return { kind: "skipped", checksum, reason: extracted.declined };
@@ -14474,22 +14520,24 @@ var Smelter = class _Smelter {
14474
14520
  * representation's checksum (the bytes the smelter would read), the
14475
14521
  * current entity-type set (the discriminator the stamps must carry), and
14476
14522
  * whether the media type's extractor derives geometry (whether an
14477
- * anchored-text artifact should exist). Embeddable an extractor exists
14478
- * for the media type's strategy the same registry the live fetch
14479
- * resolves, and `yieldsGeometry` is declared on the extractor itself, so
14480
- * every gate here and the live fetch's behavior are twins by construction.
14523
+ * anchored-text artifact should exist). Both answers are core's, keyed by the
14524
+ * media type's strategy: embeddable the type has any text-reading strategy
14525
+ * at all, and geometry that strategy derives it. Until READ-VS-EXTRACT P2
14526
+ * embeddability was asked as `EXTRACTORS[strategy] !== null` true, but a
14527
+ * second statement of `strategy !== 'none'`, answered by resolving an
14528
+ * implementation to learn a fact about a media type.
14481
14529
  * Shared by `reconcile()` and the `smelt:rebuild-anchors` planner.
14482
14530
  */
14483
14531
  classifyEmbeddable(resources) {
14484
14532
  const embeddable = /* @__PURE__ */ new Map();
14485
14533
  for (const resource of resources) {
14486
14534
  const mediaType = getPrimaryMediaType(resource);
14487
- const extractor = mediaType ? EXTRACTORS[textExtractionOf(mediaType)] : null;
14488
- if (resource["@id"] && extractor) {
14535
+ const readable = mediaType !== void 0 && textSourceOf(mediaType) !== "none";
14536
+ if (resource["@id"] && mediaType && readable) {
14489
14537
  embeddable.set(resource["@id"], {
14490
14538
  checksum: getPrimaryRepresentation(resource)?.checksum,
14491
14539
  entityTypes: getResourceEntityTypes(resource),
14492
- yieldsGeometry: extractor.yieldsGeometry
14540
+ yieldsGeometry: yieldsGeometryOf(mediaType)
14493
14541
  });
14494
14542
  }
14495
14543
  }