@equationalapplications/core-llm-wiki 7.1.1 → 7.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1016,6 +1016,33 @@ const { nodesWritten, edgesWritten, superseded } = await wikiMemory.upsertGraph(
1016
1016
 
1017
1017
  `upsertGraph` is "the tail of `ingestDocument` with the middle (LLM extraction) step removed" — it accepts caller-supplied nodes (`{ id, type, title, body? }`) and edges (`{ type, sourceId, targetId, id? }`) and writes them under the same `(sourceRef, sourceHash)` semantics. If a *different* live `sourceRef` already holds the same `sourceHash`, it throws `WikiSourceRefHashCollision`; re-writing the identical `(sourceRef, sourceHash)` is a no-op returning zero counts. The adapter parameter is required so writes participate in the caller's transaction.
1018
1018
 
1019
+ Entry IDs occupy one globally shared namespace within a database. An ID belongs
1020
+ to its existing entity while its row exists, including after soft deletion.
1021
+ Same-entity updates and resurrection retain their existing behavior; physically
1022
+ absent IDs may be inserted. Hosts remain responsible for authenticating callers
1023
+ and selecting the authorized entity ID. Entity-qualified deterministic IDs can
1024
+ avoid accidental collisions, but are not required by the API.
1025
+
1026
+ A foreign node ID rejects the entire non-no-op graph write with
1027
+ `WikiGraphNodeOwnershipConflict` (code `WIKI_GRAPH_NODE_OWNERSHIP_CONFLICT`).
1028
+ The error does not identify the owner, conflicting node, or stored details.
1029
+ A collision visible to node preflight performs no persistent writes, including
1030
+ seed-manifest writes. The live source-hash probe still runs first: an identical
1031
+ mapping is a zero-count no-op; a different live reference retains its hash
1032
+ collision error. Deleted source-reference mappings do not trigger that probe.
1033
+
1034
+ Call `upsertGraph` inside the host transaction and pass that callback's `tx`.
1035
+ It neither starts nor ends a transaction. Propagate later failures so the host
1036
+ transaction rolls back partial writes; catching a late error and committing is
1037
+ not atomic. Foreign edge-ID collisions retain their existing late bare `Error`
1038
+ and require host rollback, even though the foreign edge is never overwritten.
1039
+
1040
+ `ingestDocument` shares ownership enforcement on both its full and partial paths
1041
+ and may also throw `WikiGraphNodeOwnershipConflict`. It normally generates new
1042
+ IDs, so collisions are unexpected. Unlike direct graph preflight, ingestion may
1043
+ already have persisted ontology or other state when rejection occurs; its
1044
+ transaction rollback, not a zero-write preflight guarantee, protects atomicity.
1045
+
1019
1046
  ## Duplicate Hash Detection
1020
1047
 
1021
1048
  Control behavior when a different live `sourceRef` already holds the same `sourceHash`. The option is passed as a third argument to `ingestDocument`, not inside the params object:
@@ -1115,11 +1142,17 @@ export interface SQLiteAdapter {
1115
1142
  runAsync(sql: string, params?: unknown[]): Promise<{ changes: number; lastInsertRowId: number }>;
1116
1143
  getAllAsync<T>(sql: string, params?: unknown[]): Promise<T[]>;
1117
1144
  getFirstAsync<T>(sql: string, params?: unknown[]): Promise<T | null>;
1118
- withTransactionAsync<T>(fn: () => Promise<T>): Promise<T>;
1145
+ withTransactionAsync<T>(fn: (tx: SQLiteAdapter) => Promise<T>): Promise<T>;
1119
1146
  closeAsync(): Promise<void>;
1120
1147
  }
1121
1148
  ```
1122
1149
 
1150
+ `runAsync().changes` must accurately count written rows: a conflict-suppressed
1151
+ write reports 0, while a permitted value-identical update still counts its row.
1152
+ Host adapters are responsible for this contract; see the full affected-row
1153
+ requirements on [`SQLiteAdapter.runAsync`](src/types.ts). Ownership checks,
1154
+ edge counts, metadata compare-and-set, and maintenance depend on accurate counts.
1155
+
1123
1156
  `@equationalapplications/expo-llm-wiki` provides a pre-built adapter for Expo/React Native. For web and Node.js, implement the interface yourself — examples below.
1124
1157
 
1125
1158
  **Browser ([sql.js](https://github.com/sql-js/sql.js)):**
@@ -1155,7 +1188,7 @@ const adapter: SQLiteAdapter = {
1155
1188
  },
1156
1189
  async withTransactionAsync(fn) {
1157
1190
  sqlDb.run('BEGIN');
1158
- try { const r = await fn(); sqlDb.run('COMMIT'); return r; }
1191
+ try { const r = await fn(this); sqlDb.run('COMMIT'); return r; }
1159
1192
  catch (e) { sqlDb.run('ROLLBACK'); throw e; }
1160
1193
  },
1161
1194
  async closeAsync() { sqlDb.close(); },
@@ -1184,7 +1217,7 @@ const adapter: SQLiteAdapter = {
1184
1217
  },
1185
1218
  async withTransactionAsync(fn) {
1186
1219
  db.exec('BEGIN');
1187
- try { const r = await fn(); db.exec('COMMIT'); return r; }
1220
+ try { const r = await fn(this); db.exec('COMMIT'); return r; }
1188
1221
  catch (e) { db.exec('ROLLBACK'); throw e; }
1189
1222
  },
1190
1223
  async closeAsync() { db.close(); },
@@ -105,6 +105,21 @@ var WikiSourceRefHashCollision = class extends Error {
105
105
  this.name = "WikiSourceRefHashCollision";
106
106
  }
107
107
  };
108
+ var WikiGraphNodeOwnershipConflict = class extends Error {
109
+ /**
110
+ * Privacy-preserving constructor: accepts no arguments, so neither the foreign
111
+ * owner, the conflicting node ID, the stored content, nor any provenance can
112
+ * leak through this error. The fixed message and `code` are the entire signal.
113
+ *
114
+ * @returns a contextless `WikiGraphNodeOwnershipConflict` instance. See the
115
+ * class-level JSDoc for the privacy rationale.
116
+ */
117
+ constructor() {
118
+ super("Graph write rejected because a node ID is unavailable for this entity.");
119
+ this.code = "WIKI_GRAPH_NODE_OWNERSHIP_CONFLICT";
120
+ this.name = "WikiGraphNodeOwnershipConflict";
121
+ }
122
+ };
108
123
  var WikiTransactionError = class extends Error {
109
124
  constructor(message, options) {
110
125
  super(message, options);
@@ -1942,6 +1957,36 @@ var IngestionService = class {
1942
1957
  releaseIngestLocks();
1943
1958
  }
1944
1959
  }
1960
+ /**
1961
+ * Reject the whole write when any incoming node ID is already owned by a
1962
+ * different entity. Runs entirely inside the supplied `tx`, through the
1963
+ * repository abstraction, and — being the first thing `upsertGraphCore`
1964
+ * does — before ontology resolution, which can itself persist a configured
1965
+ * seed manifest.
1966
+ *
1967
+ * The lookup has no deleted-row filter, so a soft-deleted foreign row keeps
1968
+ * its ID reserved. Only the lookup IDs are deduplicated; `params.nodes` and
1969
+ * the later write loops keep their ordering and counts.
1970
+ *
1971
+ * The error is deliberately contextless — see WikiGraphNodeOwnershipConflict.
1972
+ *
1973
+ * @param entityId - the entity the caller is writing on behalf of. Foreign
1974
+ * rows under a different entity trigger the rejection.
1975
+ * @param nodes - incoming node set; only the IDs are inspected, so callers
1976
+ * may pass the public `params.nodes` shape directly without slicing.
1977
+ * @param tx - the open transaction. Reuses the same handle every later
1978
+ * statement in `upsertGraphCore` runs against, so atomicity is enforced
1979
+ * by the host transaction's commit/rollback, not by this method.
1980
+ */
1981
+ async assertGraphNodeOwnership(entityId, nodes, tx) {
1982
+ const ids = [...new Set(nodes.map((node) => node.id))];
1983
+ const existing = await this.entryRepo.findExistingMetadataByIds(ids, tx);
1984
+ for (const row of existing) {
1985
+ if (row.entity_id !== entityId) {
1986
+ throw new WikiGraphNodeOwnershipConflict();
1987
+ }
1988
+ }
1989
+ }
1945
1990
  /**
1946
1991
  * Implements spec §2 data flow steps a–j with the host-facing
1947
1992
  * `{sourceRef, sourceHash, nodes, edges}` parameter shape. Shared between
@@ -1960,8 +2005,19 @@ var IngestionService = class {
1960
2005
  * @returns Counts: nodesWritten (validated nodes persisted), edgesWritten
1961
2006
  * (manifest-valid edges persisted), superseded (prior facts soft-deleted
1962
2007
  * plus prior source-ref edges hard-deleted).
2008
+ *
2009
+ * Node-ownership pre-flight (spec §3, REQ-GRAPH-01): the first statement
2010
+ * is `assertGraphNodeOwnership(entityId, params.nodes, tx)`, which runs
2011
+ * before ontology resolution, seed-manifest persistence, and every other
2012
+ * write. A foreign-owned ID throws `WikiGraphNodeOwnershipConflict` with
2013
+ * no disclosed owner, node ID, or content; the caller's transaction
2014
+ * remains open and must be rolled back by the host. Soft-deleted foreign
2015
+ * rows keep their IDs reserved. Duplicate incoming IDs are deduplicated
2016
+ * only for the lookup; `params.nodes` and the write loops keep their
2017
+ * ordering and counts so success counts match the pre-change contract.
1963
2018
  */
1964
2019
  async upsertGraphCore(entityId, params, tx, opts) {
2020
+ await this.assertGraphNodeOwnership(entityId, params.nodes, tx);
1965
2021
  const now = Date.now();
1966
2022
  const ontologyState = await this.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
1967
2023
  let { mode, manifest } = ontologyState;
@@ -4913,6 +4969,6 @@ var WriteService = class {
4913
4969
  }
4914
4970
  };
4915
4971
 
4916
- export { BaseRepository, DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, EMBED_CHARS_CEILING, EmbeddingService, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MaintenanceService, MetadataRepository, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, RetrievalService, SearchService, WikiBusyError, WikiDuplicateHashError, WikiIngestEmptyError, WikiInvalidReadOptions, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, emptyManifest, entitySummaryMetaKey, extractSqliteCode, generateId, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinitions, resolveNodeType, safeSlice, typeSatisfies, validateInlineEdges, validateManifest };
4917
- //# sourceMappingURL=chunk-ZU7K6NAU.mjs.map
4918
- //# sourceMappingURL=chunk-ZU7K6NAU.mjs.map
4972
+ export { BaseRepository, DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, EMBED_CHARS_CEILING, EmbeddingService, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MaintenanceService, MetadataRepository, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, RetrievalService, SearchService, WikiBusyError, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, emptyManifest, entitySummaryMetaKey, extractSqliteCode, generateId, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinitions, resolveNodeType, safeSlice, typeSatisfies, validateInlineEdges, validateManifest };
4973
+ //# sourceMappingURL=chunk-TTECJ3S3.mjs.map
4974
+ //# sourceMappingURL=chunk-TTECJ3S3.mjs.map