@equationalapplications/core-llm-wiki 6.0.1 → 6.1.0

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
@@ -582,6 +582,86 @@ In **Strict** and **Emergent** modes, librarian and ingest JSON may include type
582
582
 
583
583
  See the design spec: [`docs/superpowers/specs/2026-06-23-per-entity-seeded-ontology-design.md`](https://github.com/equationalapplications/expo-llm-wiki/blob/main/docs/superpowers/specs/2026-06-23-per-entity-seeded-ontology-design.md).
584
584
 
585
+ ### Ontology type inheritance
586
+
587
+ `OntologyNodeType` accepts an optional `parent_type` field for **one-level type inheritance** — the building block for polymorphic queries like "all CreativeWorks" without deep, multi-level hierarchies.
588
+
589
+ ```ts
590
+ // packages/core/src/types.ts
591
+ export interface OntologyNodeType {
592
+ type: string;
593
+ description: string;
594
+ /** Optional parent type slug. One level only — the parent must exist in the
595
+ * same manifest and must not itself declare a `parent_type`. */
596
+ parent_type?: string;
597
+ }
598
+ ```
599
+
600
+ A concrete type declares `parent_type: '<parent-slug>'`; the parent must be a top-level node in the same manifest. A `design_spec` node with `parent_type: 'creativework'` is treated as both itself *and* a `creativework` during edge matching.
601
+
602
+ #### Inheritance rules (strictly enforced)
603
+
604
+ `validateManifest` enforces the **one-level invariant** at every persist and read (`entity_manifests.manifest_json` is `JSON.parse`d on every read, so validation runs on the read path too). Violations throw:
605
+
606
+ | Condition | Error |
607
+ |-----------|-------|
608
+ | `parent_type` references an unknown slug | `Parent type not found: <slug>` |
609
+ | `parent_type` equals the node's own slug | `Self-parent: <type>` |
610
+ | The referenced parent also declares a `parent_type` | `Parent chain too deep: <a> → <b> → <grandparent>` |
611
+ | `parent_type` is present but blank (`''` / whitespace) or non-string (`number`, `null`, `object`) | `Ontology parent_type must be a non-empty string when present: <type>` |
612
+
613
+ Two consequences worth knowing:
614
+
615
+ - The depth check rejects **2-cycles** (`a → b → a`) which a self-parent check alone would miss.
616
+ - A *present* `parent_type` key is required to be a usable string; an *absent* key (`undefined`) is fine and means "no parent". This is the same rule core applies to blank slugs elsewhere (`Ontology node type slug must be non-empty`).
617
+
618
+ Parent types are **instantiable** — a fact may be classified as bare `creativework`, and `resolveNodeType('creativework')` returns `'creativework'` normally. There is no `abstract` flag; if you want a parent to stay abstract, write the description to steer classification toward the concrete children.
619
+
620
+ #### Edge matching (symmetric, exact-first)
621
+
622
+ Edge matching is parent-aware on **both** sides and routes through a single primitive, `typeSatisfies(declaredType, concreteType, manifest)`:
623
+
624
+ - `declaredType === concreteType` (case-insensitive) → **exact match**, short-circuits before the node lookup. Manifests with no `parent_type` behave bit-for-bit as before.
625
+ - Otherwise: looks up `concreteType`'s definition in the manifest and returns `true` if its `parent_type` equals `declaredType`. One hop only — `typeSatisfies` never recurses.
626
+
627
+ `typeSatisfies` is used at four gates — `validateInlineEdges` (source), `OntologyService.resolveEdges` (source and target, via two-pass), `IngestionService.upsertGraph` (source) — so a parent-satisfied edge that validates will also persist.
628
+
629
+ **Targets resolve exact-first.** When resolving an edge against a target fact, `OntologyService.resolveEdges` runs two passes over candidate defs:
630
+
631
+ ```ts
632
+ // Packages/core/src/services/OntologyService.ts (abridged)
633
+ const targetType = (target.okf_type ?? '').trim().toLowerCase();
634
+ const def = candidates.find(d => d.target_type.trim().toLowerCase() === targetType)
635
+ ?? candidates.find(d => typeSatisfies(d.target_type, targetType, manifest));
636
+ ```
637
+
638
+ The exact pass runs first, so a `design_spec` target with both `about creativework → creativework` and `about creativework → design_spec` declared resolves to the **narrower** row — array order never decides which pass wins. Ties within a single pass are immaterial: only `def.type` is read off the winning def, and `validateManifest` already rejects a manifest whose triples spell one edge name with different casing, so every candidate within a pass yields a byte-identical edge row.
639
+
640
+ > **Accepted cost:** declaring `→ creativework` now admits every child of `creativework`, and there is no way to say "the parent type only." A type that needs an exact-only target must not be given children.
641
+
642
+ #### Emergent prompt schema
643
+
644
+ In `emergent` mode the LLM may propose new types via `ontology_updates`. The prompt schema (`EMERGENT_EXTRA` in `packages/core/src/prompts/ontology.ts`) advertises the optional `parent_type` so emergent proposals can suggest children of an existing type:
645
+
646
+ ```json
647
+ "ontology_updates": {
648
+ "node_types": [{ "type": "slug", "description": "...", "parent_type": "optional existing slug" }],
649
+ "edge_types": [{ "type": "slug", "source_type": "...", "target_type": "...", "description": "..." }]
650
+ }
651
+ ```
652
+
653
+ The rest of the manifest reaches the LLM unchanged — `buildPromptContext` does `JSON.stringify(manifest, null, 2)`, so an established manifest's `parent_type` fields appear verbatim alongside `type` and `description`.
654
+
655
+ Emergent proposals are **untrusted input**: `mergeOntologyUpdates` drops any `parent_type` (rather than throwing) when it is a non-string, blank, unresolvable, self-referential, or whose referenced parent already declares its own parent. The lenient merge contract keeps malformed LLM proposals from aborting an ingest transaction. Changing an established type's parent is a `setOntologyManifest` operation.
656
+
657
+ #### Backwards compatibility
658
+
659
+ `parent_type` is optional and manifests persist as a whole JSON blob in `entity_manifests.manifest_json`. Existing manifests without the field validate identically; no SQLite migration is needed. `setOntologyManifest` rejects a two-level chain at the public API boundary, so callers cannot accidentally introduce an unenforced chain through a typo.
660
+
661
+ `typeSatisfies` is intentionally **not** re-exported from the package's public surface (`packages/core/src/index.ts`). No host needs the primitive to author or validate a manifest; publishing it would freeze an internal matching rule into the package's public surface.
662
+
663
+ See the design spec: [`docs/superpowers/specs/2026-08-28-ontology-parent-field-spec.md`](https://github.com/equationalapplications/expo-llm-wiki/blob/main/docs/superpowers/specs/2026-08-28-ontology-parent-field-spec.md).
664
+
585
665
  ### Ontology backfill
586
666
 
587
667
  Facts that enter the store without passing through the librarian (synced-down
@@ -190,6 +190,17 @@ function resolveEdgeDefinitions(rawEdgeType, manifest) {
190
190
  function edgeTripleKey(type, sourceType, targetType) {
191
191
  return `${type.trim().toLowerCase()}|${sourceType.trim().toLowerCase()}|${targetType.trim().toLowerCase()}`;
192
192
  }
193
+ function typeSatisfies(declaredType, concreteType, manifest) {
194
+ const concrete = concreteType.trim().toLowerCase();
195
+ const declared = declaredType.trim().toLowerCase();
196
+ if (!concrete || !declared) return false;
197
+ if (declared === concrete) return true;
198
+ const def = (manifest.node_types ?? []).find(
199
+ (n) => typeof n?.type === "string" && n.type.trim().toLowerCase() === concrete
200
+ );
201
+ const parent = typeof def?.parent_type === "string" ? def.parent_type.trim().toLowerCase() : "";
202
+ return parent !== "" && parent === declared;
203
+ }
193
204
  function validateManifest(manifest) {
194
205
  const nodeSlugs = /* @__PURE__ */ new Set();
195
206
  for (const node of manifest.node_types ?? []) {
@@ -199,6 +210,28 @@ function validateManifest(manifest) {
199
210
  if (nodeSlugs.has(key)) throw new Error(`Duplicate node type: ${type}`);
200
211
  nodeSlugs.add(key);
201
212
  }
213
+ const parentOf = /* @__PURE__ */ new Map();
214
+ for (const node of manifest.node_types ?? []) {
215
+ const parent = typeof node.parent_type === "string" ? node.parent_type.trim().toLowerCase() : void 0;
216
+ parentOf.set(node.type.trim().toLowerCase(), parent);
217
+ }
218
+ for (const node of manifest.node_types ?? []) {
219
+ if (node.parent_type === void 0) continue;
220
+ if (typeof node.parent_type !== "string" || !node.parent_type.trim()) {
221
+ throw new Error(`Ontology parent_type must be a non-empty string when present: ${node.type}`);
222
+ }
223
+ const parentSlug = node.parent_type.trim().toLowerCase();
224
+ if (parentSlug === node.type.trim().toLowerCase()) {
225
+ throw new Error(`Self-parent: ${node.type}`);
226
+ }
227
+ if (!nodeSlugs.has(parentSlug)) {
228
+ throw new Error(`Parent type not found: ${node.parent_type}`);
229
+ }
230
+ const grandparent = parentOf.get(parentSlug);
231
+ if (grandparent) {
232
+ throw new Error(`Parent chain too deep: ${node.type} \u2192 ${node.parent_type} \u2192 ${grandparent}`);
233
+ }
234
+ }
202
235
  const edgeKeys = /* @__PURE__ */ new Set();
203
236
  const edgeNames = /* @__PURE__ */ new Map();
204
237
  for (const edge of manifest.edge_types ?? []) {
@@ -222,18 +255,38 @@ function validateManifest(manifest) {
222
255
  }
223
256
  }
224
257
  }
258
+ function buildDeclaresParentIndex(nodes) {
259
+ const declaresParent = /* @__PURE__ */ new Map();
260
+ for (const n of nodes) {
261
+ const slug = typeof n?.type === "string" ? n.type.trim().toLowerCase() : "";
262
+ if (!slug || declaresParent.has(slug)) continue;
263
+ declaresParent.set(slug, n?.parent_type !== void 0);
264
+ }
265
+ return declaresParent;
266
+ }
225
267
  function mergeOntologyUpdates(current, updates) {
226
268
  const node_types = [...current.node_types];
227
269
  const edge_types = [...current.edge_types];
228
270
  const nodeSlugs = new Set(node_types.map((n) => n.type.trim().toLowerCase()));
229
271
  const edgeKeys = new Set(edge_types.map((e) => edgeTripleKey(e.type, e.source_type, e.target_type)));
230
272
  const edgeNames = new Map(edge_types.map((e) => [e.type.trim().toLowerCase(), e.type.trim()]));
273
+ const declaresParent = buildDeclaresParentIndex([
274
+ ...current.node_types,
275
+ ...updates.node_types ?? []
276
+ ]);
231
277
  for (const node of updates.node_types ?? []) {
232
278
  const type = node?.type?.trim();
233
279
  if (!type) continue;
234
280
  const key = type.toLowerCase();
235
281
  if (nodeSlugs.has(key)) continue;
236
- node_types.push({ type, description: String(node.description ?? "") });
282
+ const rawParent = typeof node?.parent_type === "string" ? node.parent_type.trim() : "";
283
+ const parentSlug = rawParent.toLowerCase();
284
+ const keepParent = parentSlug !== "" && parentSlug !== key && declaresParent.get(parentSlug) === false;
285
+ node_types.push({
286
+ type,
287
+ description: String(node.description ?? ""),
288
+ ...keepParent ? { parent_type: rawParent } : {}
289
+ });
237
290
  nodeSlugs.add(key);
238
291
  }
239
292
  for (const edge of updates.edge_types ?? []) {
@@ -270,7 +323,7 @@ function validateInlineEdges(sourceType, _targetType, edges, manifest, opts) {
270
323
  continue;
271
324
  }
272
325
  const defs = resolveEdgeDefinitions(edge.edge_type, manifest);
273
- const match = defs.find((d) => d.source_type.toLowerCase() === sourceType.toLowerCase());
326
+ const match = defs.find((d) => typeSatisfies(d.source_type, sourceType, manifest));
274
327
  if (!match) {
275
328
  if (strict) throw new WikiStrictOntologyViolation(entityId, "edge", edge.edge_type);
276
329
  continue;
@@ -1946,7 +1999,7 @@ var IngestionService = class {
1946
1999
  }
1947
2000
  const sourceType = sourceIdToType.get(edge.sourceId);
1948
2001
  const candidates = (manifest.edge_types ?? []).filter(
1949
- (d) => d.type.toLowerCase() === edge.type.toLowerCase() && d.source_type.toLowerCase() === (sourceType ?? "").toLowerCase()
2002
+ (d) => d.type.toLowerCase() === edge.type.toLowerCase() && typeSatisfies(d.source_type, sourceType ?? "", manifest)
1950
2003
  );
1951
2004
  const match = candidates[0];
1952
2005
  if (!match) {
@@ -4566,6 +4619,6 @@ var WriteService = class {
4566
4619
  }
4567
4620
  };
4568
4621
 
4569
- export { BaseRepository, DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, 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, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, emptyManifest, entitySummaryMetaKey, extractSqliteCode, generateId, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinitions, resolveNodeType, safeSlice, validateInlineEdges, validateManifest };
4570
- //# sourceMappingURL=chunk-HREQ6F6Z.mjs.map
4571
- //# sourceMappingURL=chunk-HREQ6F6Z.mjs.map
4622
+ export { BaseRepository, DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, 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, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, emptyManifest, entitySummaryMetaKey, extractSqliteCode, generateId, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinitions, resolveNodeType, safeSlice, typeSatisfies, validateInlineEdges, validateManifest };
4623
+ //# sourceMappingURL=chunk-YEESP6J2.mjs.map
4624
+ //# sourceMappingURL=chunk-YEESP6J2.mjs.map