@semiont/make-meaning 0.5.24 → 0.5.26
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 +159 -20
- package/dist/index.js +357 -93
- package/dist/index.js.map +1 -1
- package/dist/smelter-main.js +236 -44
- package/dist/smelter-main.js.map +1 -1
- package/package.json +13 -13
package/dist/index.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
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, getPrimaryRepresentation, decodeRepresentation, getBodySource, getTargetSelector, deriveViews, getTextPositionSelector, annotationId, errField, userId, generateUuid, getExactText, busRequest, cloneToken, baseMediaType, isSupportedMediaType, capabilitiesOf, didToAgent, assembleAnnotation, jobId, entityType, baseUrl, busLog, BRIDGED_CHANNELS, burstBuffer, textExtractionOf, getPrimaryMediaType, extensionForMediaType, applyBodyOperations, softwareToAgent } from '@semiont/core';
|
|
3
|
+
import { getResourceEntityTypes, getResourceId, getTargetSource, resourceId, getPrimaryRepresentation, decodeRepresentation, getBodySource, getTargetSelector, deriveViews, getTextPositionSelector, annotationId, errField, userId, generateUuid, getExactText, busRequest, cloneToken, baseMediaType, isSupportedMediaType, capabilitiesOf, didToAgent, assembleAnnotation, jobId, entityType, baseUrl, busLog, BRIDGED_CHANNELS, burstBuffer, textExtractionOf, chunkText, getPrimaryMediaType, extensionForMediaType, applyBodyOperations, 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
|
-
import { getGraphDatabase } from '@semiont/graph';
|
|
7
|
-
import { WorkingTreeStore, deriveStorageUri, calculateChecksum } from '@semiont/content';
|
|
6
|
+
import { compareByRecencyThenId, getGraphDatabase } from '@semiont/graph';
|
|
7
|
+
import { WorkingTreeStore, createAnchoredTextStore, deriveStorageUri, anchoredTextStoreOverTransport, EXTRACTORS, calculateChecksum } from '@semiont/content';
|
|
8
8
|
import { getEntityTypes, DEFAULT_ENTITY_TYPES } from '@semiont/ontology';
|
|
9
9
|
import { promises } from 'fs';
|
|
10
10
|
import * as path3 from 'path';
|
|
11
|
-
import { chunkText } from '@semiont/vectors';
|
|
12
11
|
import { createGzip, createGunzip } from 'zlib';
|
|
13
12
|
import { pipeline, Readable } from 'stream';
|
|
14
13
|
import { promisify } from 'util';
|
|
@@ -9889,6 +9888,10 @@ async function createKnowledgeBase(eventStore, project, graphDb, eventBus, logge
|
|
|
9889
9888
|
project,
|
|
9890
9889
|
logger.child({ component: "working-tree-store" })
|
|
9891
9890
|
);
|
|
9891
|
+
const anchoredText = createAnchoredTextStore(
|
|
9892
|
+
project.anchoredTextDir,
|
|
9893
|
+
logger.child({ component: "anchored-text-store" })
|
|
9894
|
+
);
|
|
9892
9895
|
const weaveProgress = createWeaveProgress(eventBus);
|
|
9893
9896
|
const smeltProgress = createSmeltProgress(eventBus);
|
|
9894
9897
|
if (!options?.skipRebuild) {
|
|
@@ -9898,6 +9901,7 @@ async function createKnowledgeBase(eventStore, project, graphDb, eventBus, logge
|
|
|
9898
9901
|
eventStore,
|
|
9899
9902
|
views,
|
|
9900
9903
|
content,
|
|
9904
|
+
anchoredText,
|
|
9901
9905
|
graph: graphDb,
|
|
9902
9906
|
weaveProgress,
|
|
9903
9907
|
smeltProgress,
|
|
@@ -9933,13 +9937,6 @@ var GraphContext = class {
|
|
|
9933
9937
|
static async getResourceConnections(resourceId10, kb) {
|
|
9934
9938
|
return kb.graph.getResourceConnections(resourceId10);
|
|
9935
9939
|
}
|
|
9936
|
-
/**
|
|
9937
|
-
* Search resources by name (cross-resource query)
|
|
9938
|
-
* Requires full-text search - must use graph database
|
|
9939
|
-
*/
|
|
9940
|
-
static async searchResources(query, kb, limit) {
|
|
9941
|
-
return kb.graph.searchResources(query, limit);
|
|
9942
|
-
}
|
|
9943
9940
|
/**
|
|
9944
9941
|
* Build the unified knowledge graph for a resource's neighborhood:
|
|
9945
9942
|
* resources AND annotations as typed nodes, typed/directional edges.
|
|
@@ -10060,7 +10057,7 @@ Format as a simple list, one suggestion per line.`;
|
|
|
10060
10057
|
}
|
|
10061
10058
|
return response.split("\n").map((line) => line.replace(/^[-*•]\s*/, "").trim()).filter((line) => line.length > 0).slice(0, 3);
|
|
10062
10059
|
}
|
|
10063
|
-
var ResourceContext = class
|
|
10060
|
+
var ResourceContext = class {
|
|
10064
10061
|
/**
|
|
10065
10062
|
* Get resource metadata from view storage
|
|
10066
10063
|
*/
|
|
@@ -10072,38 +10069,34 @@ var ResourceContext = class _ResourceContext {
|
|
|
10072
10069
|
return view.resource;
|
|
10073
10070
|
}
|
|
10074
10071
|
/**
|
|
10075
|
-
* List resources, optionally filtered
|
|
10072
|
+
* List resources, optionally filtered, as one page plus the size of the whole
|
|
10073
|
+
* match set. Every filter is applied before pagination on both paths — a
|
|
10074
|
+
* filter applied afterwards narrows the page rather than the match set, which
|
|
10075
|
+
* is how a search scoped to an entity type can come back empty while hundreds
|
|
10076
|
+
* of resources match.
|
|
10076
10077
|
*
|
|
10077
|
-
* When `search` is set,
|
|
10078
|
-
*
|
|
10079
|
-
* The graph result is then narrowed by `archived` if requested.
|
|
10078
|
+
* When `search` is set, the entire query — filtering, ordering and
|
|
10079
|
+
* pagination — runs inside the graph engine.
|
|
10080
10080
|
*
|
|
10081
|
-
* When `search` is unset,
|
|
10082
|
-
*
|
|
10081
|
+
* When `search` is unset, the materialized views answer instead. They are the
|
|
10082
|
+
* barrier-stamped projection, so an unsearched listing is read-your-writes
|
|
10083
|
+
* where the graph is only eventually consistent.
|
|
10083
10084
|
*/
|
|
10084
10085
|
static async listResources(filters, kb) {
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
return
|
|
10086
|
+
const { search: rawSearch, archived, entityType: entityType2, offset = 0, limit = 50 } = filters ?? {};
|
|
10087
|
+
const search = rawSearch?.trim() || void 0;
|
|
10088
|
+
if (search) {
|
|
10089
|
+
return kb.graph.listResources({
|
|
10090
|
+
search,
|
|
10091
|
+
archived,
|
|
10092
|
+
entityTypes: entityType2 ? [entityType2] : void 0,
|
|
10093
|
+
offset,
|
|
10094
|
+
limit
|
|
10095
|
+
});
|
|
10089
10096
|
}
|
|
10090
10097
|
const allViews = await kb.views.getAll();
|
|
10091
|
-
const
|
|
10092
|
-
|
|
10093
|
-
const doc = view.resource;
|
|
10094
|
-
if (filters?.archived !== void 0 && doc.archived !== filters.archived) {
|
|
10095
|
-
continue;
|
|
10096
|
-
}
|
|
10097
|
-
resources.push(doc);
|
|
10098
|
-
}
|
|
10099
|
-
return _ResourceContext.sortByDateDesc(resources);
|
|
10100
|
-
}
|
|
10101
|
-
static sortByDateDesc(resources) {
|
|
10102
|
-
return [...resources].sort((a, b) => {
|
|
10103
|
-
const aTime = a.dateCreated ? new Date(a.dateCreated).getTime() : 0;
|
|
10104
|
-
const bTime = b.dateCreated ? new Date(b.dateCreated).getTime() : 0;
|
|
10105
|
-
return bTime - aTime;
|
|
10106
|
-
});
|
|
10098
|
+
const matches = allViews.map((view) => view.resource).filter((doc) => archived === void 0 || doc.archived === archived).filter((doc) => !entityType2 || getResourceEntityTypes(doc).includes(entityType2)).sort(compareByRecencyThenId);
|
|
10099
|
+
return { resources: matches.slice(offset, offset + limit), total: matches.length };
|
|
10107
10100
|
}
|
|
10108
10101
|
/**
|
|
10109
10102
|
* Add content previews to resources (for search results)
|
|
@@ -10300,7 +10293,8 @@ Summary:`;
|
|
|
10300
10293
|
resourceId: r.resourceId,
|
|
10301
10294
|
annotationId: r.annotationId,
|
|
10302
10295
|
score: r.score,
|
|
10303
|
-
entityTypes: r.entityTypes
|
|
10296
|
+
entityTypes: r.entityTypes,
|
|
10297
|
+
...r.machineRead ? { machineRead: true } : {}
|
|
10304
10298
|
}))
|
|
10305
10299
|
};
|
|
10306
10300
|
logger?.debug("Semantic context found", { matches: results.length });
|
|
@@ -10642,7 +10636,8 @@ var LLMContext = class {
|
|
|
10642
10636
|
resourceId: m.resourceId,
|
|
10643
10637
|
...m.annotationId ? { annotationId: m.annotationId } : {},
|
|
10644
10638
|
score: m.score,
|
|
10645
|
-
...m.entityTypes ? { entityTypes: m.entityTypes } : {}
|
|
10639
|
+
...m.entityTypes ? { entityTypes: m.entityTypes } : {},
|
|
10640
|
+
...m.machineRead ? { machineRead: true } : {}
|
|
10646
10641
|
})),
|
|
10647
10642
|
...excludeEntityTypes.length ? { excludedEntityTypes: excludeEntityTypes } : {}
|
|
10648
10643
|
};
|
|
@@ -10879,7 +10874,7 @@ var Matcher = class {
|
|
|
10879
10874
|
* Context-driven search: multi-source retrieval + composite scoring
|
|
10880
10875
|
*
|
|
10881
10876
|
* Retrieval sources:
|
|
10882
|
-
* 1. Name match — graph.
|
|
10877
|
+
* 1. Name match — graph.listResources({ search: searchTerm })
|
|
10883
10878
|
* 2. Entity type match — graph.listResources({ entityTypes })
|
|
10884
10879
|
* 3. Graph neighborhood — connections from GatheredContext
|
|
10885
10880
|
*
|
|
@@ -10895,7 +10890,7 @@ var Matcher = class {
|
|
|
10895
10890
|
const views = deriveViews(context.graph, mainResourceId);
|
|
10896
10891
|
const connections = views.connections;
|
|
10897
10892
|
const [nameMatches, entityTypeMatches, semanticMatches] = await Promise.all([
|
|
10898
|
-
this.kb.graph.
|
|
10893
|
+
this.kb.graph.listResources({ search: searchTerm, limit: 20 }).then((r) => r.resources),
|
|
10899
10894
|
annotationEntityTypes.length > 0 ? this.kb.graph.listResources({ entityTypes: annotationEntityTypes, limit: 50 }).then((r) => r.resources) : Promise.resolve([]),
|
|
10900
10895
|
// 4. Semantic match — vector similarity search (if vectors configured)
|
|
10901
10896
|
this.searchVectors(searchTerm)
|
|
@@ -11632,6 +11627,21 @@ var Stower = class {
|
|
|
11632
11627
|
// src/browser.ts
|
|
11633
11628
|
var import_rxjs4 = __toESM(require_cjs());
|
|
11634
11629
|
var import_operators4 = __toESM(require_operators());
|
|
11630
|
+
var ANCHORED_TEXT_SETTLE_TIMEOUT_MS = 15e3;
|
|
11631
|
+
async function readAnchoredText(kb, resourceId10) {
|
|
11632
|
+
const view = await kb.views.get(resourceId(resourceId10));
|
|
11633
|
+
const checksum = getPrimaryRepresentation(view?.resource)?.checksum;
|
|
11634
|
+
if (!checksum) return null;
|
|
11635
|
+
const hit = await kb.anchoredText.read(checksum);
|
|
11636
|
+
if (hit) return hit;
|
|
11637
|
+
try {
|
|
11638
|
+
const outcome = await kb.smeltProgress.whenSettled(resourceId10, checksum, ANCHORED_TEXT_SETTLE_TIMEOUT_MS);
|
|
11639
|
+
return outcome === "indexed" ? kb.anchoredText.read(checksum) : null;
|
|
11640
|
+
} catch (error) {
|
|
11641
|
+
if (error instanceof SmeltProgressTimeout) return null;
|
|
11642
|
+
throw error;
|
|
11643
|
+
}
|
|
11644
|
+
}
|
|
11635
11645
|
async function readTagSchemasProjection(project) {
|
|
11636
11646
|
const tagSchemasPath = path3.join(
|
|
11637
11647
|
project.stateDir,
|
|
@@ -11746,6 +11756,7 @@ var Browser = class {
|
|
|
11746
11756
|
);
|
|
11747
11757
|
this.subscriptions.push(
|
|
11748
11758
|
pipe("browse:resource-requested", (e) => this.handleBrowseResource(e)).subscribe({ error: errorHandler }),
|
|
11759
|
+
pipe("browse:anchored-text-requested", (e) => this.handleAnchoredText(e)).subscribe({ error: errorHandler }),
|
|
11749
11760
|
pipe("browse:resources-requested", (e) => this.handleBrowseResources(e)).subscribe({ error: errorHandler }),
|
|
11750
11761
|
pipe("browse:annotations-requested", (e) => this.handleBrowseAnnotations(e)).subscribe({ error: errorHandler }),
|
|
11751
11762
|
pipe("browse:annotation-requested", (e) => this.handleBrowseAnnotation(e)).subscribe({ error: errorHandler }),
|
|
@@ -11761,6 +11772,34 @@ var Browser = class {
|
|
|
11761
11772
|
// ========================================================================
|
|
11762
11773
|
// KB read handlers
|
|
11763
11774
|
// ========================================================================
|
|
11775
|
+
/**
|
|
11776
|
+
* Serve a resource's derived coordinate map (ANCHORED-TEXT-CACHE Lane 5).
|
|
11777
|
+
*
|
|
11778
|
+
* Read-your-writes on the same barrier `llm-context` uses for vectors: a
|
|
11779
|
+
* caller may arrive before the Smelter has finished the resource it just
|
|
11780
|
+
* uploaded, so a miss waits for that content generation to settle rather than
|
|
11781
|
+
* reporting "no map" for a document that is merely still being read.
|
|
11782
|
+
*
|
|
11783
|
+
* **This path never invokes the engine.** The Smelter is the sole producer.
|
|
11784
|
+
* A miss that survives the barrier answers `null`, and the caller degrades —
|
|
11785
|
+
* for a PDF annotation that means geometry with no quoted text, which is what
|
|
11786
|
+
* shipped before any of this existed. OCR in a request path is precisely what
|
|
11787
|
+
* this design exists to avoid.
|
|
11788
|
+
*/
|
|
11789
|
+
async handleAnchoredText(event) {
|
|
11790
|
+
try {
|
|
11791
|
+
this.eventBus.get("browse:anchored-text-result").next({
|
|
11792
|
+
correlationId: event.correlationId,
|
|
11793
|
+
response: await readAnchoredText(this.kb, event.resourceId)
|
|
11794
|
+
});
|
|
11795
|
+
} catch (error) {
|
|
11796
|
+
this.logger.error("Browse anchored text failed", { resourceId: event.resourceId, error: errField(error) });
|
|
11797
|
+
this.eventBus.get("browse:anchored-text-failed").next({
|
|
11798
|
+
correlationId: event.correlationId,
|
|
11799
|
+
message: error instanceof Error ? error.message : String(error)
|
|
11800
|
+
});
|
|
11801
|
+
}
|
|
11802
|
+
}
|
|
11764
11803
|
async handleBrowseResource(event) {
|
|
11765
11804
|
try {
|
|
11766
11805
|
const response = await assembleResourceGraph(this.kb, resourceId(event.resourceId));
|
|
@@ -11785,22 +11824,21 @@ var Browser = class {
|
|
|
11785
11824
|
}
|
|
11786
11825
|
async handleBrowseResources(event) {
|
|
11787
11826
|
try {
|
|
11788
|
-
let filteredDocs = await ResourceContext.listResources({
|
|
11789
|
-
search: event.search,
|
|
11790
|
-
archived: event.archived
|
|
11791
|
-
}, this.kb);
|
|
11792
|
-
if (event.entityType) {
|
|
11793
|
-
filteredDocs = filteredDocs.filter((doc) => getResourceEntityTypes(doc).includes(event.entityType));
|
|
11794
|
-
}
|
|
11795
11827
|
const offset = event.offset ?? 0;
|
|
11796
11828
|
const limit = event.limit ?? 50;
|
|
11797
|
-
const
|
|
11798
|
-
|
|
11829
|
+
const { resources, total } = await ResourceContext.listResources({
|
|
11830
|
+
search: event.search,
|
|
11831
|
+
archived: event.archived,
|
|
11832
|
+
entityType: event.entityType,
|
|
11833
|
+
offset,
|
|
11834
|
+
limit
|
|
11835
|
+
}, this.kb);
|
|
11836
|
+
const formattedDocs = event.search ? await ResourceContext.addContentPreviews(resources, this.kb) : resources;
|
|
11799
11837
|
this.eventBus.get("browse:resources-result").next({
|
|
11800
11838
|
correlationId: event.correlationId,
|
|
11801
11839
|
response: {
|
|
11802
11840
|
resources: formattedDocs,
|
|
11803
|
-
total
|
|
11841
|
+
total,
|
|
11804
11842
|
offset,
|
|
11805
11843
|
limit
|
|
11806
11844
|
}
|
|
@@ -12916,6 +12954,7 @@ async function startMakeMeaning(project, config, eventBus, logger, options) {
|
|
|
12916
12954
|
return {
|
|
12917
12955
|
knowledgeSystem,
|
|
12918
12956
|
jobQueue,
|
|
12957
|
+
project,
|
|
12919
12958
|
stop: async () => {
|
|
12920
12959
|
logger.info("Stopping Make-Meaning service");
|
|
12921
12960
|
jobStatusSubscription.unsubscribe();
|
|
@@ -13029,6 +13068,40 @@ var LocalContentTransport = class {
|
|
|
13029
13068
|
"LocalContentTransport does not support putBinary() \u2014 create resources via bus emits (mark/yield namespaces) in local mode"
|
|
13030
13069
|
);
|
|
13031
13070
|
}
|
|
13071
|
+
/**
|
|
13072
|
+
* Store a derived coordinate map under the content checksum the producer
|
|
13073
|
+
* read (PERSIST-ANCHORS decision A — see the interface doc for why the
|
|
13074
|
+
* producer supplies the key). In local mode this is the same store the
|
|
13075
|
+
* HTTP route writes to — one storage authority, reached the same way from
|
|
13076
|
+
* every process (ANCHORED-TEXT-CACHE Lane 5).
|
|
13077
|
+
*/
|
|
13078
|
+
async putAnchoredText(checksum, outcome, _options) {
|
|
13079
|
+
busLog("PUT", "anchored-text", { checksum });
|
|
13080
|
+
await this.ks.kb.anchoredText.write(checksum, outcome);
|
|
13081
|
+
}
|
|
13082
|
+
/** The stored outcome, or null when nothing has derived one — the common case. */
|
|
13083
|
+
async getAnchoredText(resourceId10, _options) {
|
|
13084
|
+
busLog("GET", "anchored-text", { resourceId: resourceId10 });
|
|
13085
|
+
return readAnchoredText(this.ks.kb, resourceId10);
|
|
13086
|
+
}
|
|
13087
|
+
/**
|
|
13088
|
+
* The cache-consult read (PERSIST-ANCHORS P2c), straight from the store —
|
|
13089
|
+
* checksum-addressed, so no view resolution and no settle barrier: the
|
|
13090
|
+
* caller holds the content identity already.
|
|
13091
|
+
*/
|
|
13092
|
+
async getAnchoredTextByChecksum(checksum, _options) {
|
|
13093
|
+
busLog("GET", "anchored-text-by-checksum", { checksum });
|
|
13094
|
+
return this.ks.kb.anchoredText.read(checksum);
|
|
13095
|
+
}
|
|
13096
|
+
/**
|
|
13097
|
+
* The store's would-hit keys, straight from the store — planning data for
|
|
13098
|
+
* the reconcile diff (PERSIST-ANCHORS P0), so no settle barrier applies:
|
|
13099
|
+
* presence is being asked, not content at a moment.
|
|
13100
|
+
*/
|
|
13101
|
+
async listAnchoredTextKeys(_options) {
|
|
13102
|
+
busLog("GET", "anchored-text-keys", {});
|
|
13103
|
+
return this.ks.kb.anchoredText.list();
|
|
13104
|
+
}
|
|
13032
13105
|
async getBinary(resourceId10, _options) {
|
|
13033
13106
|
busLog("GET", "content", { resourceId: resourceId10 });
|
|
13034
13107
|
return withSpan(
|
|
@@ -13111,6 +13184,7 @@ function sameStringSet(a, b) {
|
|
|
13111
13184
|
var WORK_ITEM_TYPES = /* @__PURE__ */ new Set([
|
|
13112
13185
|
"smelt:embed",
|
|
13113
13186
|
"smelt:restamp",
|
|
13187
|
+
"smelt:reanchor",
|
|
13114
13188
|
"smelt:purge",
|
|
13115
13189
|
"smelt:embed-annotation",
|
|
13116
13190
|
"smelt:purge-annotation"
|
|
@@ -13119,8 +13193,9 @@ function isWorkItem(input) {
|
|
|
13119
13193
|
return WORK_ITEM_TYPES.has(input.type);
|
|
13120
13194
|
}
|
|
13121
13195
|
var Smelter = class _Smelter {
|
|
13122
|
-
constructor(events$, vectorStore, embeddingProvider, content, bus, chunkingConfig, timing, logger) {
|
|
13196
|
+
constructor(events$, rebuildAnchors$, vectorStore, embeddingProvider, content, bus, chunkingConfig, timing, logger) {
|
|
13123
13197
|
this.events$ = events$;
|
|
13198
|
+
this.rebuildAnchors$ = rebuildAnchors$;
|
|
13124
13199
|
this.vectorStore = vectorStore;
|
|
13125
13200
|
this.embeddingProvider = embeddingProvider;
|
|
13126
13201
|
this.content = content;
|
|
@@ -13128,8 +13203,10 @@ var Smelter = class _Smelter {
|
|
|
13128
13203
|
this.chunkingConfig = chunkingConfig;
|
|
13129
13204
|
this.timing = timing;
|
|
13130
13205
|
this.logger = logger;
|
|
13206
|
+
this.anchoredStore = anchoredTextStoreOverTransport(content, logger.child({ component: "anchored-text-cache" }));
|
|
13131
13207
|
}
|
|
13132
13208
|
events$;
|
|
13209
|
+
rebuildAnchors$;
|
|
13133
13210
|
vectorStore;
|
|
13134
13211
|
embeddingProvider;
|
|
13135
13212
|
content;
|
|
@@ -13142,11 +13219,21 @@ var Smelter = class _Smelter {
|
|
|
13142
13219
|
static RECONCILE_WAVE = 8;
|
|
13143
13220
|
eventSubject = new import_rxjs9.Subject();
|
|
13144
13221
|
sourceSubscription = null;
|
|
13222
|
+
commandSubscription = null;
|
|
13145
13223
|
pipelineSubscription = null;
|
|
13146
13224
|
_eventsProcessed = 0;
|
|
13147
13225
|
_reconcileState = { phase: "pending" };
|
|
13148
13226
|
workDone = 0;
|
|
13227
|
+
workFailed = 0;
|
|
13149
13228
|
workWaiter = null;
|
|
13229
|
+
/**
|
|
13230
|
+
* Serializes every planner drain (reconcile, anchored-text rebuilds):
|
|
13231
|
+
* there is one waiter slot, and the weave:rebuild rule — rebuilds never
|
|
13232
|
+
* interleave — applies to every unit here being a potential multi-second
|
|
13233
|
+
* OCR pass.
|
|
13234
|
+
*/
|
|
13235
|
+
drainChain = Promise.resolve();
|
|
13236
|
+
anchoredStore;
|
|
13150
13237
|
get eventsProcessed() {
|
|
13151
13238
|
return this._eventsProcessed;
|
|
13152
13239
|
}
|
|
@@ -13174,7 +13261,7 @@ var Smelter = class _Smelter {
|
|
|
13174
13261
|
return (0, import_rxjs9.from)(
|
|
13175
13262
|
withActorSpan("smelter", inputOrBatch.type, async () => {
|
|
13176
13263
|
const ok = await this.safeProcessEvent(inputOrBatch);
|
|
13177
|
-
if (isWorkItem(inputOrBatch)) this.noteWorkDone(1);
|
|
13264
|
+
if (isWorkItem(inputOrBatch)) this.noteWorkDone(1, ok ? 0 : 1);
|
|
13178
13265
|
else if (ok) this._eventsProcessed++;
|
|
13179
13266
|
})
|
|
13180
13267
|
);
|
|
@@ -13188,18 +13275,26 @@ var Smelter = class _Smelter {
|
|
|
13188
13275
|
this.logger.debug("Bus event received", { type: event.type, resourceId: event.resourceId });
|
|
13189
13276
|
this.eventSubject.next(event);
|
|
13190
13277
|
});
|
|
13278
|
+
this.commandSubscription = this.rebuildAnchors$.pipe(
|
|
13279
|
+
(0, import_operators7.concatMap)((command) => (0, import_rxjs9.from)(this.rebuildAnchors(command)))
|
|
13280
|
+
).subscribe({
|
|
13281
|
+
error: (err) => this.logger.error("Smelter command pipeline error", { error: errField(err) })
|
|
13282
|
+
});
|
|
13191
13283
|
this.logger.info("Smelter pipeline initialized");
|
|
13192
13284
|
}
|
|
13193
13285
|
stop() {
|
|
13194
13286
|
this.sourceSubscription?.unsubscribe();
|
|
13195
13287
|
this.sourceSubscription = null;
|
|
13288
|
+
this.commandSubscription?.unsubscribe();
|
|
13289
|
+
this.commandSubscription = null;
|
|
13196
13290
|
this.pipelineSubscription?.unsubscribe();
|
|
13197
13291
|
this.pipelineSubscription = null;
|
|
13198
13292
|
this.eventSubject.complete();
|
|
13199
13293
|
this.logger.info("Smelter stopped");
|
|
13200
13294
|
}
|
|
13201
|
-
noteWorkDone(count) {
|
|
13295
|
+
noteWorkDone(count, failed) {
|
|
13202
13296
|
this.workDone += count;
|
|
13297
|
+
this.workFailed += failed;
|
|
13203
13298
|
if (this.workWaiter && this.workDone >= this.workWaiter.target) {
|
|
13204
13299
|
this.workWaiter.resolve();
|
|
13205
13300
|
this.workWaiter = null;
|
|
@@ -13213,12 +13308,15 @@ var Smelter = class _Smelter {
|
|
|
13213
13308
|
let wireProcessed = 0;
|
|
13214
13309
|
for (const run of partitionByType(events)) {
|
|
13215
13310
|
const workRun = isWorkItem(run[0]);
|
|
13311
|
+
let succeeded = 0;
|
|
13216
13312
|
try {
|
|
13217
13313
|
if (run.length === 1) {
|
|
13218
13314
|
const ok = await this.safeProcessEvent(run[0]);
|
|
13315
|
+
if (ok) succeeded = 1;
|
|
13219
13316
|
if (ok && !workRun) wireProcessed++;
|
|
13220
13317
|
} else {
|
|
13221
13318
|
const processed = await this.applyBatchByType(run);
|
|
13319
|
+
succeeded = processed;
|
|
13222
13320
|
if (!workRun) wireProcessed += processed;
|
|
13223
13321
|
}
|
|
13224
13322
|
} catch (error) {
|
|
@@ -13228,7 +13326,7 @@ var Smelter = class _Smelter {
|
|
|
13228
13326
|
error: errField(error)
|
|
13229
13327
|
});
|
|
13230
13328
|
} finally {
|
|
13231
|
-
if (workRun) this.noteWorkDone(run.length);
|
|
13329
|
+
if (workRun) this.noteWorkDone(run.length, run.length - succeeded);
|
|
13232
13330
|
}
|
|
13233
13331
|
}
|
|
13234
13332
|
return wireProcessed;
|
|
@@ -13300,6 +13398,9 @@ var Smelter = class _Smelter {
|
|
|
13300
13398
|
case "smelt:restamp":
|
|
13301
13399
|
await this.restampResource(event);
|
|
13302
13400
|
break;
|
|
13401
|
+
case "smelt:reanchor":
|
|
13402
|
+
await this.reanchorResource(event);
|
|
13403
|
+
break;
|
|
13303
13404
|
case "smelt:purge":
|
|
13304
13405
|
await this.handleResourcePurge(event);
|
|
13305
13406
|
break;
|
|
@@ -13326,6 +13427,45 @@ var Smelter = class _Smelter {
|
|
|
13326
13427
|
await this.vectorStore.updateResourceEntityTypes(resourceId(rid), entityTypes);
|
|
13327
13428
|
this.logger.info("Restamped resource entity types", { resourceId: rid, entityTypes });
|
|
13328
13429
|
}
|
|
13430
|
+
/**
|
|
13431
|
+
* Re-derive a lost anchored-text artifact from the resource's current
|
|
13432
|
+
* bytes (PERSIST-ANCHORS P0, the third drift class). Extraction is the
|
|
13433
|
+
* cost here — the vectors are already correct, so this NEVER calls the
|
|
13434
|
+
* embedding provider, the vector store, or the settled signal: the index
|
|
13435
|
+
* decision was already made and announced at its checksum; only the map
|
|
13436
|
+
* is missing. Name the work for what it does (the S13 discipline).
|
|
13437
|
+
*
|
|
13438
|
+
* The publish is STRICT, unlike the embed path's best-effort side
|
|
13439
|
+
* publish: here the artifact IS the job, so a store failure must throw —
|
|
13440
|
+
* the pipeline logs and counts it, and the rebuild command's partial-
|
|
13441
|
+
* failure accounting depends on that throw.
|
|
13442
|
+
*/
|
|
13443
|
+
async reanchorResource(event) {
|
|
13444
|
+
const rid = event.resourceId;
|
|
13445
|
+
if (!rid) return;
|
|
13446
|
+
const { data, contentType } = await this.content.getBinary(resourceId(rid));
|
|
13447
|
+
const bytes = Buffer.from(data);
|
|
13448
|
+
const extractor = EXTRACTORS[textExtractionOf(contentType)];
|
|
13449
|
+
if (!extractor?.yieldsGeometry) {
|
|
13450
|
+
this.logger.info("Re-anchor found no geometry-capable extractor", { resourceId: rid, contentType });
|
|
13451
|
+
return;
|
|
13452
|
+
}
|
|
13453
|
+
const checksum = calculateChecksum(bytes);
|
|
13454
|
+
const extracted = await extractor.extract(bytes, contentType, {
|
|
13455
|
+
key: checksum,
|
|
13456
|
+
store: this.anchoredStore
|
|
13457
|
+
});
|
|
13458
|
+
if ("declined" in extracted || !extracted.items?.length) {
|
|
13459
|
+
this.logger.info("Re-anchor extraction yielded no geometry", {
|
|
13460
|
+
resourceId: rid,
|
|
13461
|
+
contentType,
|
|
13462
|
+
..."declined" in extracted ? { declined: extracted.declined } : {}
|
|
13463
|
+
});
|
|
13464
|
+
return;
|
|
13465
|
+
}
|
|
13466
|
+
await this.content.putAnchoredText(checksum, { ...extracted, items: extracted.items });
|
|
13467
|
+
this.logger.info("Re-anchored resource", { resourceId: rid, checksum, items: extracted.items.length });
|
|
13468
|
+
}
|
|
13329
13469
|
async handleResourcePurge(event) {
|
|
13330
13470
|
const rid = event.resourceId;
|
|
13331
13471
|
if (!rid) return;
|
|
@@ -13345,12 +13485,34 @@ var Smelter = class _Smelter {
|
|
|
13345
13485
|
const { data, contentType } = await this.content.getBinary(resourceId(resourceId10));
|
|
13346
13486
|
const bytes = Buffer.from(data);
|
|
13347
13487
|
const checksum = calculateChecksum(bytes);
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13488
|
+
const extractor = EXTRACTORS[textExtractionOf(contentType)];
|
|
13489
|
+
if (!extractor) {
|
|
13490
|
+
this.logger.debug("Skipping resource with no extractor for its media type", { resourceId: resourceId10, contentType });
|
|
13491
|
+
return { kind: "skipped", checksum, reason: "no-extractor" };
|
|
13492
|
+
}
|
|
13493
|
+
const extracted = await extractor.extract(bytes, contentType, {
|
|
13494
|
+
key: checksum,
|
|
13495
|
+
store: this.anchoredStore
|
|
13496
|
+
});
|
|
13497
|
+
if ("declined" in extracted) {
|
|
13498
|
+
this.logger.debug("Extractor declined", { resourceId: resourceId10, contentType, reason: extracted.declined });
|
|
13499
|
+
return { kind: "skipped", checksum, reason: extracted.declined };
|
|
13351
13500
|
}
|
|
13352
|
-
|
|
13353
|
-
|
|
13501
|
+
if (extracted.ocrConfidence && extracted.ocrConfidence.lowConfidenceWords > 0) {
|
|
13502
|
+
this.logger.info("OCR read words it was unsure of", {
|
|
13503
|
+
resourceId: resourceId10,
|
|
13504
|
+
contentType,
|
|
13505
|
+
...extracted.ocrConfidence
|
|
13506
|
+
});
|
|
13507
|
+
}
|
|
13508
|
+
if (extracted.unreadPages?.length) {
|
|
13509
|
+
this.logger.info("Partial extraction coverage", {
|
|
13510
|
+
resourceId: resourceId10,
|
|
13511
|
+
contentType,
|
|
13512
|
+
unreadPages: extracted.unreadPages
|
|
13513
|
+
});
|
|
13514
|
+
}
|
|
13515
|
+
return extracted.text.trim() ? { kind: "text", text: extracted.text, checksum, machineRead: extracted.method === "ocr" } : { kind: "skipped", checksum, reason: "empty" };
|
|
13354
13516
|
} catch (error) {
|
|
13355
13517
|
this.logger.warn("Content unavailable for embedding", { resourceId: resourceId10, error: errField(error) });
|
|
13356
13518
|
return { kind: "unavailable" };
|
|
@@ -13362,9 +13524,9 @@ var Smelter = class _Smelter {
|
|
|
13362
13524
|
* fold. Best-effort — waiters degrade to their bounded timeout; a signal
|
|
13363
13525
|
* failure must never fail the embed.
|
|
13364
13526
|
*/
|
|
13365
|
-
async emitSettled(resourceId10, contentChecksum, outcome) {
|
|
13527
|
+
async emitSettled(resourceId10, contentChecksum, outcome, reason) {
|
|
13366
13528
|
try {
|
|
13367
|
-
await this.bus.emit("smelt:settled", { resourceId: resourceId10, contentChecksum, outcome });
|
|
13529
|
+
await this.bus.emit("smelt:settled", { resourceId: resourceId10, contentChecksum, outcome, ...reason ? { reason } : {} });
|
|
13368
13530
|
} catch (error) {
|
|
13369
13531
|
this.logger.warn("Failed to emit smelt:settled", { resourceId: resourceId10, outcome, error: errField(error) });
|
|
13370
13532
|
}
|
|
@@ -13392,12 +13554,14 @@ var Smelter = class _Smelter {
|
|
|
13392
13554
|
const fetched = await this.fetchEmbeddableText(rid);
|
|
13393
13555
|
if (fetched.kind === "unavailable") return;
|
|
13394
13556
|
if (fetched.kind === "skipped") {
|
|
13395
|
-
await this.
|
|
13557
|
+
await this.vectorStore.deleteResourceVectors(resourceId(rid));
|
|
13558
|
+
await this.emitSettled(rid, fetched.checksum, "skipped", fetched.reason);
|
|
13396
13559
|
return;
|
|
13397
13560
|
}
|
|
13398
13561
|
const chunks = chunkText(fetched.text, this.chunkingConfig);
|
|
13399
13562
|
if (chunks.length === 0) {
|
|
13400
|
-
await this.
|
|
13563
|
+
await this.vectorStore.deleteResourceVectors(resourceId(rid));
|
|
13564
|
+
await this.emitSettled(rid, fetched.checksum, "skipped", "empty");
|
|
13401
13565
|
return;
|
|
13402
13566
|
}
|
|
13403
13567
|
const entityTypes = await this.resolveEntityTypes(rid);
|
|
@@ -13407,7 +13571,7 @@ var Smelter = class _Smelter {
|
|
|
13407
13571
|
text: t,
|
|
13408
13572
|
embedding: embeddings[i]
|
|
13409
13573
|
}));
|
|
13410
|
-
await this.vectorStore.upsertResourceVectors(resourceId(rid), embeddingChunks, fetched.checksum, entityTypes);
|
|
13574
|
+
await this.vectorStore.upsertResourceVectors(resourceId(rid), embeddingChunks, fetched.checksum, entityTypes, fetched.machineRead);
|
|
13411
13575
|
await this.emitSettled(rid, fetched.checksum, "indexed");
|
|
13412
13576
|
this.logger.info(logMessage, { resourceId: rid, chunks: chunks.length });
|
|
13413
13577
|
}
|
|
@@ -13451,12 +13615,14 @@ var Smelter = class _Smelter {
|
|
|
13451
13615
|
if (!exactText?.trim()) return;
|
|
13452
13616
|
const aid = annotationId(annotation.id);
|
|
13453
13617
|
const embedding = await this.embeddingProvider.embed(exactText);
|
|
13618
|
+
const stamp = await this.vectorStore.getResourceStamp(resourceId(rid));
|
|
13454
13619
|
const payload = {
|
|
13455
13620
|
annotationId: aid,
|
|
13456
13621
|
resourceId: resourceId(rid),
|
|
13457
13622
|
motivation: annotation.motivation ?? "",
|
|
13458
13623
|
entityTypes: annotation.entityTypes ?? [],
|
|
13459
|
-
exactText
|
|
13624
|
+
exactText,
|
|
13625
|
+
...stamp?.machineRead ? { machineRead: true } : {}
|
|
13460
13626
|
};
|
|
13461
13627
|
await this.vectorStore.upsertAnnotationVector(aid, embedding, payload);
|
|
13462
13628
|
this.logger.info("Indexed annotation", { annotationId: String(aid) });
|
|
@@ -13481,28 +13647,30 @@ var Smelter = class _Smelter {
|
|
|
13481
13647
|
const fetched = await this.fetchEmbeddableText(rid);
|
|
13482
13648
|
if (fetched.kind === "unavailable") continue;
|
|
13483
13649
|
if (fetched.kind === "skipped") {
|
|
13484
|
-
await this.
|
|
13650
|
+
await this.vectorStore.deleteResourceVectors(resourceId(rid));
|
|
13651
|
+
await this.emitSettled(rid, fetched.checksum, "skipped", fetched.reason);
|
|
13485
13652
|
continue;
|
|
13486
13653
|
}
|
|
13487
13654
|
const chunks = chunkText(fetched.text, this.chunkingConfig);
|
|
13488
13655
|
if (chunks.length === 0) {
|
|
13489
|
-
await this.
|
|
13656
|
+
await this.vectorStore.deleteResourceVectors(resourceId(rid));
|
|
13657
|
+
await this.emitSettled(rid, fetched.checksum, "skipped", "empty");
|
|
13490
13658
|
continue;
|
|
13491
13659
|
}
|
|
13492
13660
|
const entityTypes = await this.resolveEntityTypes(rid);
|
|
13493
|
-
resourceData.push({ rid: resourceId(rid), chunks, checksum: fetched.checksum, entityTypes });
|
|
13661
|
+
resourceData.push({ rid: resourceId(rid), chunks, checksum: fetched.checksum, entityTypes, machineRead: fetched.machineRead });
|
|
13494
13662
|
allChunks.push(...chunks);
|
|
13495
13663
|
}
|
|
13496
13664
|
if (allChunks.length === 0) return events.length;
|
|
13497
13665
|
const allEmbeddings = await this.embeddingProvider.embedBatch(allChunks);
|
|
13498
13666
|
let offset = 0;
|
|
13499
|
-
for (const { rid, chunks, checksum, entityTypes } of resourceData) {
|
|
13667
|
+
for (const { rid, chunks, checksum, entityTypes, machineRead } of resourceData) {
|
|
13500
13668
|
const embeddingChunks = chunks.map((t, i) => ({
|
|
13501
13669
|
chunkIndex: i,
|
|
13502
13670
|
text: t,
|
|
13503
13671
|
embedding: allEmbeddings[offset + i]
|
|
13504
13672
|
}));
|
|
13505
|
-
await this.vectorStore.upsertResourceVectors(rid, embeddingChunks, checksum, entityTypes);
|
|
13673
|
+
await this.vectorStore.upsertResourceVectors(rid, embeddingChunks, checksum, entityTypes, machineRead);
|
|
13506
13674
|
await this.emitSettled(String(rid), checksum, "indexed");
|
|
13507
13675
|
this.logger.info("Batch-indexed resource", { resourceId: String(rid), chunks: chunks.length });
|
|
13508
13676
|
offset += chunks.length;
|
|
@@ -13575,26 +13743,23 @@ var Smelter = class _Smelter {
|
|
|
13575
13743
|
}
|
|
13576
13744
|
this._reconcileState = { phase: "running" };
|
|
13577
13745
|
try {
|
|
13578
|
-
const [indexedResources, indexedAnnotations] = await Promise.all([
|
|
13746
|
+
const [indexedResources, indexedAnnotations, anchoredKeys] = await Promise.all([
|
|
13579
13747
|
this.vectorStore.listResourceStamps(),
|
|
13580
|
-
this.vectorStore.listAnnotationIds()
|
|
13748
|
+
this.vectorStore.listAnnotationIds(),
|
|
13749
|
+
// The artifact store's would-hit keys — one bulk read, never a probe
|
|
13750
|
+
// per resource (PERSIST-ANCHORS P0). Keys are resource ids today;
|
|
13751
|
+
// P1 rekeys the store by content checksum and this lookup moves
|
|
13752
|
+
// with it.
|
|
13753
|
+
this.content.listAnchoredTextKeys().then((keys) => new Set(keys))
|
|
13581
13754
|
]);
|
|
13582
13755
|
const resources = await this.listAllResources();
|
|
13583
13756
|
this.logger.info("Reconcile started", {
|
|
13584
13757
|
indexedResources: indexedResources.size,
|
|
13585
13758
|
indexedAnnotations: indexedAnnotations.size,
|
|
13759
|
+
anchoredArtifacts: anchoredKeys.size,
|
|
13586
13760
|
liveResources: resources.length
|
|
13587
13761
|
});
|
|
13588
|
-
const embeddable =
|
|
13589
|
-
for (const resource of resources) {
|
|
13590
|
-
const mediaType = getPrimaryMediaType(resource);
|
|
13591
|
-
if (resource["@id"] && mediaType && textExtractionOf(mediaType) === "decode") {
|
|
13592
|
-
embeddable.set(resource["@id"], {
|
|
13593
|
-
checksum: getPrimaryRepresentation(resource)?.checksum,
|
|
13594
|
-
entityTypes: getResourceEntityTypes(resource)
|
|
13595
|
-
});
|
|
13596
|
-
}
|
|
13597
|
-
}
|
|
13762
|
+
const embeddable = this.classifyEmbeddable(resources);
|
|
13598
13763
|
const work = [];
|
|
13599
13764
|
for (const rid of indexedResources.keys()) {
|
|
13600
13765
|
if (!embeddable.has(rid)) work.push({ type: "smelt:purge", resourceId: rid, payload: {} });
|
|
@@ -13608,6 +13773,9 @@ var Smelter = class _Smelter {
|
|
|
13608
13773
|
} else if (!sameStringSet(indexed.entityTypes, catalog.entityTypes)) {
|
|
13609
13774
|
work.push({ type: "smelt:restamp", resourceId: rid, payload: {} });
|
|
13610
13775
|
}
|
|
13776
|
+
if (indexed && catalog.checksum !== void 0 && indexed.contentChecksum === catalog.checksum && catalog.yieldsGeometry && !anchoredKeys.has(catalog.checksum)) {
|
|
13777
|
+
work.push({ type: "smelt:reanchor", resourceId: rid, payload: {} });
|
|
13778
|
+
}
|
|
13611
13779
|
}
|
|
13612
13780
|
const liveAnnotationIds = /* @__PURE__ */ new Set();
|
|
13613
13781
|
for (const resource of resources) {
|
|
@@ -13636,9 +13804,12 @@ var Smelter = class _Smelter {
|
|
|
13636
13804
|
const summary = {
|
|
13637
13805
|
resourcesEmbedded: work.filter((w) => w.type === "smelt:embed").length,
|
|
13638
13806
|
resourcesRestamped: work.filter((w) => w.type === "smelt:restamp").length,
|
|
13807
|
+
resourcesReanchored: work.filter((w) => w.type === "smelt:reanchor").length,
|
|
13639
13808
|
resourceVectorsDeleted: work.filter((w) => w.type === "smelt:purge").length,
|
|
13640
13809
|
annotationsEmbedded: work.filter((w) => w.type === "smelt:embed-annotation").length,
|
|
13641
|
-
annotationVectorsDeleted: work.filter((w) => w.type === "smelt:purge-annotation").length
|
|
13810
|
+
annotationVectorsDeleted: work.filter((w) => w.type === "smelt:purge-annotation").length,
|
|
13811
|
+
resourcesEligible: embeddable.size,
|
|
13812
|
+
resourcesIndexed: (await this.vectorStore.listResourceStamps()).size
|
|
13642
13813
|
};
|
|
13643
13814
|
this._reconcileState = { phase: "done", summary };
|
|
13644
13815
|
this.logger.info("Reconcile complete", { ...summary });
|
|
@@ -13657,16 +13828,106 @@ var Smelter = class _Smelter {
|
|
|
13657
13828
|
* completion. The pipeline ticks `noteWorkDone` for every consumed work
|
|
13658
13829
|
* item (success or failure — failures are logged like any live event), so
|
|
13659
13830
|
* each wave's waiter resolves exactly when its items have been processed.
|
|
13831
|
+
*
|
|
13832
|
+
* Serialized through `drainChain`: there is ONE waiter slot, and the
|
|
13833
|
+
* planners that drain (reconcile, `smelt:rebuild-anchors`) must not
|
|
13834
|
+
* interleave — a rebuild command arriving mid-reconcile waits its turn.
|
|
13835
|
+
*
|
|
13836
|
+
* @returns how many of THESE items failed — the rebuild command's
|
|
13837
|
+
* partial-failure accounting (failure detail is in the logs).
|
|
13660
13838
|
*/
|
|
13661
13839
|
async drain(work) {
|
|
13662
|
-
|
|
13663
|
-
|
|
13664
|
-
const
|
|
13665
|
-
|
|
13666
|
-
|
|
13667
|
-
|
|
13668
|
-
|
|
13840
|
+
let failures = 0;
|
|
13841
|
+
const run = this.drainChain.then(async () => {
|
|
13842
|
+
const failedBefore = this.workFailed;
|
|
13843
|
+
for (let i = 0; i < work.length; i += _Smelter.RECONCILE_WAVE) {
|
|
13844
|
+
const wave = work.slice(i, i + _Smelter.RECONCILE_WAVE);
|
|
13845
|
+
const done = new Promise((resolve2) => {
|
|
13846
|
+
this.workWaiter = { target: this.workDone + wave.length, resolve: resolve2 };
|
|
13847
|
+
});
|
|
13848
|
+
for (const item of wave) this.eventSubject.next(item);
|
|
13849
|
+
await done;
|
|
13850
|
+
}
|
|
13851
|
+
failures = this.workFailed - failedBefore;
|
|
13852
|
+
});
|
|
13853
|
+
this.drainChain = run.then(() => void 0, () => void 0);
|
|
13854
|
+
await run;
|
|
13855
|
+
return failures;
|
|
13856
|
+
}
|
|
13857
|
+
/**
|
|
13858
|
+
* `smelt:rebuild-anchors` — the operator's explicit re-derivation of
|
|
13859
|
+
* anchored-text artifacts (PERSIST-ANCHORS P0), shaped after
|
|
13860
|
+
* `weave:rebuild`: optionally scoped, strictly serialized (concatMap on
|
|
13861
|
+
* the command stream + the drain chain), correlated ok/failed replies,
|
|
13862
|
+
* and partial completion FAILS — a rebuild that quietly skipped resources
|
|
13863
|
+
* would present exactly like a document with no text, which is the #845
|
|
13864
|
+
* failure mode wearing different clothes.
|
|
13865
|
+
*
|
|
13866
|
+
* Never destructive: nothing is deleted first, stale entries are simply
|
|
13867
|
+
* overwritten (the W5-frames lesson — a rebuild that clears before it
|
|
13868
|
+
* re-derives turns a partial failure into a loss). Re-anchoring makes
|
|
13869
|
+
* zero embedding calls; work items ride the normal per-resource lanes,
|
|
13870
|
+
* so a rebuild can never interleave with live processing of the same
|
|
13871
|
+
* resource (S1/S2).
|
|
13872
|
+
*/
|
|
13873
|
+
async rebuildAnchors(command) {
|
|
13874
|
+
const { correlationId, resourceId: resourceId10 } = command;
|
|
13875
|
+
try {
|
|
13876
|
+
let work;
|
|
13877
|
+
if (resourceId10) {
|
|
13878
|
+
work = [{ type: "smelt:reanchor", resourceId: resourceId10, payload: {} }];
|
|
13879
|
+
} else {
|
|
13880
|
+
const resources = await this.listAllResources();
|
|
13881
|
+
work = [...this.classifyEmbeddable(resources)].filter(([, catalog]) => catalog.yieldsGeometry).map(([rid]) => ({ type: "smelt:reanchor", resourceId: rid, payload: {} }));
|
|
13882
|
+
}
|
|
13883
|
+
this.logger.info("Anchored-text rebuild started", { scoped: resourceId10 ?? null, resources: work.length });
|
|
13884
|
+
const failed = await this.drain(work);
|
|
13885
|
+
if (failed > 0) {
|
|
13886
|
+
await this.bus.emit("smelt:rebuild-anchors-failed", {
|
|
13887
|
+
...correlationId ? { correlationId } : {},
|
|
13888
|
+
message: `${failed} of ${work.length} resources failed to re-anchor \u2014 see smelter logs`
|
|
13889
|
+
});
|
|
13890
|
+
return;
|
|
13891
|
+
}
|
|
13892
|
+
await this.bus.emit("smelt:rebuild-anchors-ok", correlationId ? { correlationId } : {});
|
|
13893
|
+
this.logger.info("Anchored-text rebuild complete", { scoped: resourceId10 ?? null, resources: work.length });
|
|
13894
|
+
} catch (error) {
|
|
13895
|
+
this.logger.error("Anchored-text rebuild failed", { error: errField(error) });
|
|
13896
|
+
try {
|
|
13897
|
+
await this.bus.emit("smelt:rebuild-anchors-failed", {
|
|
13898
|
+
...correlationId ? { correlationId } : {},
|
|
13899
|
+
message: error instanceof Error ? error.message : String(error)
|
|
13900
|
+
});
|
|
13901
|
+
} catch (emitError) {
|
|
13902
|
+
this.logger.warn("Failed to emit smelt:rebuild-anchors-failed", { error: errField(emitError) });
|
|
13903
|
+
}
|
|
13904
|
+
}
|
|
13905
|
+
}
|
|
13906
|
+
/**
|
|
13907
|
+
* Embeddable live resources, each with the catalog's claims: the primary
|
|
13908
|
+
* representation's checksum (the bytes the smelter would read), the
|
|
13909
|
+
* current entity-type set (the discriminator the stamps must carry), and
|
|
13910
|
+
* whether the media type's extractor derives geometry (whether an
|
|
13911
|
+
* anchored-text artifact should exist). Embeddable ⇔ an extractor exists
|
|
13912
|
+
* for the media type's strategy — the same registry the live fetch
|
|
13913
|
+
* resolves, and `yieldsGeometry` is declared on the extractor itself, so
|
|
13914
|
+
* every gate here and the live fetch's behavior are twins by construction.
|
|
13915
|
+
* Shared by `reconcile()` and the `smelt:rebuild-anchors` planner.
|
|
13916
|
+
*/
|
|
13917
|
+
classifyEmbeddable(resources) {
|
|
13918
|
+
const embeddable = /* @__PURE__ */ new Map();
|
|
13919
|
+
for (const resource of resources) {
|
|
13920
|
+
const mediaType = getPrimaryMediaType(resource);
|
|
13921
|
+
const extractor = mediaType ? EXTRACTORS[textExtractionOf(mediaType)] : null;
|
|
13922
|
+
if (resource["@id"] && extractor) {
|
|
13923
|
+
embeddable.set(resource["@id"], {
|
|
13924
|
+
checksum: getPrimaryRepresentation(resource)?.checksum,
|
|
13925
|
+
entityTypes: getResourceEntityTypes(resource),
|
|
13926
|
+
yieldsGeometry: extractor.yieldsGeometry
|
|
13927
|
+
});
|
|
13928
|
+
}
|
|
13669
13929
|
}
|
|
13930
|
+
return embeddable;
|
|
13670
13931
|
}
|
|
13671
13932
|
/** Page through `browse:resources-requested` until the catalog is exhausted. */
|
|
13672
13933
|
async listAllResources() {
|
|
@@ -13697,6 +13958,7 @@ var SMELTER_CHANNELS = [
|
|
|
13697
13958
|
"mark:entity-tag-added",
|
|
13698
13959
|
"mark:entity-tag-removed"
|
|
13699
13960
|
];
|
|
13961
|
+
var SMELTER_COMMAND_CHANNELS = ["smelt:rebuild-anchors"];
|
|
13700
13962
|
function createSmelterActorStateUnit(options) {
|
|
13701
13963
|
const { bus } = options;
|
|
13702
13964
|
let started = false;
|
|
@@ -13711,12 +13973,14 @@ function createSmelterActorStateUnit(options) {
|
|
|
13711
13973
|
)
|
|
13712
13974
|
)
|
|
13713
13975
|
);
|
|
13976
|
+
const rebuildAnchors$ = bus.on$("smelt:rebuild-anchors");
|
|
13714
13977
|
return {
|
|
13715
13978
|
events$,
|
|
13979
|
+
rebuildAnchors$,
|
|
13716
13980
|
start: () => {
|
|
13717
13981
|
if (started) return;
|
|
13718
13982
|
started = true;
|
|
13719
|
-
bus.addChannels?.([...SMELTER_CHANNELS]);
|
|
13983
|
+
bus.addChannels?.([...SMELTER_CHANNELS, ...SMELTER_COMMAND_CHANNELS]);
|
|
13720
13984
|
},
|
|
13721
13985
|
dispose: () => {
|
|
13722
13986
|
started = false;
|