mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-14 23:07:29 +00:00
feat(openontology): Phase 2 + Phase 3 — storage, REST/SSE, MCP, RDF/SHACL, adapters, TUI, explorer (#101)
Everything the two shipped PRD phases deferred, minus what is called out below.
Storage (Phase 2)
@logicsrc/openontology gains a SQLite/Turso adapter. It hydrates the read
model at open, serves reads synchronously — a query evaluator that awaits per
triple pattern is unusable — and buffers mutations as SQL that flush() writes
in one transaction. Versioned idempotent migrations; indexes over subject,
predicate, entity-valued object, status, both time axes, aliases, and external
ids; FTS5 for label/alias search. The append-only status log is replayed on
open, so retractions, supersessions, and merge redirects survive a reopen.
REST + SSE + OpenAPI (Phase 2)
16 paths under /api/ontologies in logicsrc-web, described at
/api/ontologies/openapi and referencing the published JSON Schemas rather
than restating them. No token is read-only; a curator token can apply; an
agent token can propose and cannot apply. Idempotency-Key on mutations,
revision ETags, 409 on a stale base revision, and an SSE stream that emits
the same event objects as the JSON endpoint.
MCP (Phase 2)
OpenOntology and OpenPRD surfaces on the standards server: spec/manifest/
schema/queries and PRD spec/index as resources, 11 ontology tools and 6 PRD
tools, 7 prompts. Read-only by default; OPENONTOLOGY_MCP_WRITABLE=1 buys
proposals, never applies — the denial is the shared policy layer, not a
second rule that could drift.
Interoperability (Phase 3)
RDF/Turtle export and import of the reified profile, plus the plain triple
for asserted relationships so a consumer wanting only the accepted graph gets
one. SHACL for 5 of 7 constraint kinds; `unique` and `query` are reported as
unmapped in both the return value and the generated Turtle, because a shape
that quietly means something narrower is worse than no shape.
Source adapters (Phase 3)
CSV, JSON, YAML, NDJSON, Markdown, generic JSON HTTP, and GitHub. All produce
PROPOSED change-set operations with source, evidence selector, run id, and
confidence attached; fetch is injected so ingestion is offline and testable.
Each declares its capabilities, so "nothing was deleted upstream" is never
confused with "this adapter cannot see deletions" — none of the seven can.
TUI + explorer
Keyboard-first panels (types, entities, claims, sources, queries, change
sets, validation, audit) as plain strings that survive SSH and 60 columns;
status is a glyph and a word, never colour alone; the key bar wraps rather
than truncating. Wired as `logicsrc ontology tui`. A read-only web explorer
at /openontology/explore with entity and claim views showing status, both
clocks, confidence, sources, evidence, and append-only history — plus an
/openprd page for the companion standard.
Bugs found and fixed while testing
- the API built a new engine per request, so `explain` could never find a
resultId from a prior request; engines are now cached per role
- the TUI status bar called engine.validateOntologyPackage(), appending a
package.validated event on every repaint; it now uses the pure validator
Verification: 76 new tests (527 total across the monorepo, all passing); full
build green; the libSQL adapter is exercised against real files, the API
through its route handlers, and MCP over an in-memory transport.
Not included: PWA review/approval write flows (they need an auth story this
deployment does not have), OWL/RDFS mappings, SPARQL/Cypher/Datalog query
adapters, and Phase 4 governed actions. The compatibility matrix marks those
"planned", not "supported".
Refs: prd/0001-add-logicsrc-openontology-spec.md
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
296775e003
commit
da5f6f8381
53 changed files with 6939 additions and 23 deletions
94
packages/tui/src/openontology.test.ts
Normal file
94
packages/tui/src/openontology.test.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
createOntologyEngine,
|
||||
initOntologyPackage,
|
||||
loadOntologyPackage,
|
||||
localActor
|
||||
} from "@logicsrc/openontology";
|
||||
import { renderOntologyKeyHelp, renderOntologyTui, PANEL_KEYS } from "./openontology.js";
|
||||
|
||||
const dirs: string[] = [];
|
||||
afterAll(() => {
|
||||
for (const dir of dirs) rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function engine() {
|
||||
const dir = mkdtempSync(join(tmpdir(), "tui-ontology-"));
|
||||
dirs.push(dir);
|
||||
initOntologyPackage(dir, { id: "test-ecosystem", now: "2026-07-26T00:00:00Z" });
|
||||
return createOntologyEngine({
|
||||
package: loadOntologyPackage(dir),
|
||||
actor: localActor("curator"),
|
||||
clock: () => "2026-07-26T00:00:00Z"
|
||||
});
|
||||
}
|
||||
|
||||
describe("ontology TUI", () => {
|
||||
it("renders a bordered frame with the key bar and status line", () => {
|
||||
const output = renderOntologyTui(engine());
|
||||
const lines = output.split("\n");
|
||||
expect(lines[0]!.startsWith("┌")).toBe(true);
|
||||
expect(lines.at(-1)!.startsWith("└")).toBe(true);
|
||||
expect(output).toContain("e entities");
|
||||
expect(output).toContain("q quit");
|
||||
expect(output).toContain("asserted claims");
|
||||
});
|
||||
|
||||
it("never exceeds the requested width, and stays usable when narrow", () => {
|
||||
for (const width of [60, 78, 120]) {
|
||||
const lines = renderOntologyTui(engine(), { width }).split("\n");
|
||||
const widths = new Set(lines.map((line) => [...line].length));
|
||||
expect(widths.size).toBe(1);
|
||||
expect([...widths][0]).toBe(Math.max(width, 60));
|
||||
}
|
||||
});
|
||||
|
||||
it("shows every panel", () => {
|
||||
const e = engine();
|
||||
expect(renderOntologyTui(e, { panel: "types" })).toContain("Person");
|
||||
expect(renderOntologyTui(e, { panel: "entities" })).toContain("Alice Reyes");
|
||||
expect(renderOntologyTui(e, { panel: "claims" })).toContain("worksOn");
|
||||
expect(renderOntologyTui(e, { panel: "sources" })).toContain("web-page");
|
||||
expect(renderOntologyTui(e, { panel: "queries" })).toContain("contributors");
|
||||
expect(renderOntologyTui(e, { panel: "changesets" })).toContain("no change sets");
|
||||
expect(renderOntologyTui(e, { panel: "violations" })).toContain("no errors");
|
||||
expect(renderOntologyTui(e, { panel: "audit" })).toContain("no events");
|
||||
// Rendering is read-only: repainting must not append to the audit log.
|
||||
expect(renderOntologyTui(e, { panel: "audit" })).toContain("no events");
|
||||
});
|
||||
|
||||
it("distinguishes claim status by glyph and word, not colour", () => {
|
||||
const claims = renderOntologyTui(engine(), { panel: "claims", rows: 20 });
|
||||
expect(claims).toMatch(/✓ asserted/);
|
||||
// No ANSI escapes: the panel must survive a monochrome terminal.
|
||||
expect(claims.includes("[")).toBe(false);
|
||||
});
|
||||
|
||||
it("surfaces proposed change sets and disputes in the status bar", () => {
|
||||
const e = engine();
|
||||
e.createOntologyChangeSet({
|
||||
title: "pending",
|
||||
operations: [
|
||||
{
|
||||
op: "assert-claim",
|
||||
value: {
|
||||
subject: "test:person:alice",
|
||||
predicate: "worksOn",
|
||||
object: { entity: "test:project:docs-portal" },
|
||||
sources: ["test:source:repo"]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(renderOntologyTui(e)).toContain("1 proposed");
|
||||
expect(renderOntologyTui(e, { panel: "changesets" })).toContain("pending");
|
||||
});
|
||||
|
||||
it("lists the keys it binds", () => {
|
||||
expect(renderOntologyKeyHelp()).toContain("v: validate");
|
||||
expect(PANEL_KEYS.map((entry) => entry.key)).toContain("q");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue