mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 22:37:29 +00:00
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>
459 lines
18 KiB
TypeScript
459 lines
18 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { GET as listOntologies } from "../src/app/api/ontologies/route";
|
|
import { GET as getManifest } from "../src/app/api/ontologies/[ontologyId]/manifest/route";
|
|
import { GET as getSchema } from "../src/app/api/ontologies/[ontologyId]/schema/route";
|
|
import { GET as listEntities } from "../src/app/api/ontologies/[ontologyId]/entities/route";
|
|
import { GET as getEntity } from "../src/app/api/ontologies/[ontologyId]/entities/[entityId]/route";
|
|
import { GET as listClaims } from "../src/app/api/ontologies/[ontologyId]/claims/route";
|
|
import { GET as getClaim } from "../src/app/api/ontologies/[ontologyId]/claims/[claimId]/route";
|
|
import { POST as runQuery } from "../src/app/api/ontologies/[ontologyId]/query/route";
|
|
import { POST as explain } from "../src/app/api/ontologies/[ontologyId]/explain/route";
|
|
import { POST as validate } from "../src/app/api/ontologies/[ontologyId]/validate/route";
|
|
import { GET as listChangeSets, POST as createChangeSet } from "../src/app/api/ontologies/[ontologyId]/changesets/route";
|
|
import { POST as approve } from "../src/app/api/ontologies/[ontologyId]/changesets/[changeSetId]/approve/route";
|
|
import { POST as apply } from "../src/app/api/ontologies/[ontologyId]/changesets/[changeSetId]/apply/route";
|
|
import { GET as events } from "../src/app/api/ontologies/[ontologyId]/events/route";
|
|
import { GET as openapi } from "../src/app/api/ontologies/openapi/route";
|
|
import { resetService } from "../src/lib/ontology-service";
|
|
|
|
/**
|
|
* Contract tests for the OpenOntology reference API.
|
|
*
|
|
* They call the route handlers directly, so they exercise the real request →
|
|
* policy → engine → response path without needing a listening server.
|
|
*/
|
|
|
|
const ONTOLOGY = "ethereum-ecosystem";
|
|
const CURATOR = "curator-token";
|
|
const AGENT = "agent-token";
|
|
|
|
const params = <T extends Record<string, string>>(value: T) => ({ params: Promise.resolve(value) });
|
|
|
|
function request(
|
|
path: string,
|
|
init: { method?: string; body?: unknown; token?: string; headers?: Record<string, string> } = {}
|
|
): Request {
|
|
return new Request(`https://logicsrc.com${path}`, {
|
|
method: init.method ?? "GET",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
...(init.token ? { authorization: `Bearer ${init.token}` } : {}),
|
|
...(init.headers ?? {})
|
|
},
|
|
...(init.body === undefined ? {} : { body: JSON.stringify(init.body) })
|
|
});
|
|
}
|
|
|
|
async function body<T>(response: Response): Promise<T> {
|
|
return (await response.json()) as T;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
process.env.OPENONTOLOGY_API_TOKEN = CURATOR;
|
|
process.env.OPENONTOLOGY_AGENT_TOKEN = AGENT;
|
|
resetService();
|
|
});
|
|
|
|
afterEach(() => {
|
|
delete process.env.OPENONTOLOGY_API_TOKEN;
|
|
delete process.env.OPENONTOLOGY_AGENT_TOKEN;
|
|
resetService();
|
|
});
|
|
|
|
describe("reads", () => {
|
|
it("lists the ontologies it serves", async () => {
|
|
const response = await listOntologies();
|
|
expect(response.status).toBe(200);
|
|
const payload = await body<{ ontologies: Array<{ id: string }>; persistence: string }>(response);
|
|
expect(payload.ontologies[0]?.id).toBe(ONTOLOGY);
|
|
expect(["memory", "turso"]).toContain(payload.persistence);
|
|
});
|
|
|
|
it("serves the manifest with a revision ETag", async () => {
|
|
const response = await getManifest(request(`/api/ontologies/${ONTOLOGY}/manifest`), params({ ontologyId: ONTOLOGY }));
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers.get("etag")).toMatch(/data-\d+/);
|
|
expect((await body<{ id: string }>(response)).id).toBe(ONTOLOGY);
|
|
});
|
|
|
|
it("404s an unknown ontology and names one that exists", async () => {
|
|
const response = await getManifest(request("/api/ontologies/nope/manifest"), params({ ontologyId: "nope" }));
|
|
expect(response.status).toBe(404);
|
|
const payload = await body<{ error: { code: string; hint?: string } }>(response);
|
|
expect(payload.error.code).toBe("OO-A-NOT-FOUND");
|
|
expect(payload.error.hint).toContain(ONTOLOGY);
|
|
});
|
|
|
|
it("serves the schema layer", async () => {
|
|
const response = await getSchema(request(`/api/ontologies/${ONTOLOGY}/schema`), params({ ontologyId: ONTOLOGY }));
|
|
const payload = await body<{ entityTypes: unknown[]; relationships: unknown[] }>(response);
|
|
expect(payload.entityTypes.length).toBeGreaterThanOrEqual(10);
|
|
expect(payload.relationships.length).toBeGreaterThanOrEqual(12);
|
|
});
|
|
|
|
it("lists and paginates entities", async () => {
|
|
const response = await listEntities(
|
|
request(`/api/ontologies/${ONTOLOGY}/entities?type=Person&limit=3`),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const payload = await body<{ total: number; entities: Array<{ type: string }> }>(response);
|
|
expect(payload.entities).toHaveLength(3);
|
|
expect(payload.entities.every((entity) => entity.type === "Person")).toBe(true);
|
|
expect(payload.total).toBeGreaterThan(3);
|
|
});
|
|
|
|
it("returns ranked matches with evidence when searching", async () => {
|
|
const response = await listEntities(
|
|
request(`/api/ontologies/${ONTOLOGY}/entities?q=Avery`),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const payload = await body<{ matches: Array<{ id: string; matchedOn: string; evidence: string }> }>(response);
|
|
expect(payload.matches[0]?.id).toContain("avery");
|
|
expect(payload.matches[0]?.matchedOn).toBeTruthy();
|
|
});
|
|
|
|
it("returns an entity with its claims", async () => {
|
|
const response = await getEntity(
|
|
request(`/api/ontologies/${ONTOLOGY}/entities/eth:person:avery-lindqvist`),
|
|
params({ ontologyId: ONTOLOGY, entityId: "eth:person:avery-lindqvist" })
|
|
);
|
|
const payload = await body<{ entity: { canonicalName: string }; claims: unknown[] }>(response);
|
|
expect(payload.entity.canonicalName).toBe("Avery Lindqvist");
|
|
expect(payload.claims.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("filters claims by status", async () => {
|
|
const asserted = await listClaims(
|
|
request(`/api/ontologies/${ONTOLOGY}/claims?status=asserted&limit=500`),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const proposed = await listClaims(
|
|
request(`/api/ontologies/${ONTOLOGY}/claims?status=proposed&limit=500`),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const a = await body<{ total: number }>(asserted);
|
|
const p = await body<{ total: number }>(proposed);
|
|
expect(a.total).toBeGreaterThan(p.total);
|
|
expect(p.total).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it("returns a claim with its history, sources, and evidence", async () => {
|
|
const list = await listClaims(
|
|
request(`/api/ontologies/${ONTOLOGY}/claims?limit=1`),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const { claims } = await body<{ claims: Array<{ id: string }> }>(list);
|
|
const id = claims[0]!.id;
|
|
|
|
const response = await getClaim(
|
|
request(`/api/ontologies/${ONTOLOGY}/claims/${id}`),
|
|
params({ ontologyId: ONTOLOGY, claimId: id })
|
|
);
|
|
const payload = await body<{ claim: { id: string }; history: unknown[]; sources: unknown[] }>(response);
|
|
expect(payload.claim.id).toBe(id);
|
|
expect(payload.history.length).toBeGreaterThanOrEqual(1);
|
|
expect(payload.sources.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
});
|
|
|
|
describe("query and explain", () => {
|
|
it("runs a saved query and returns claim ids per row", async () => {
|
|
const response = await runQuery(
|
|
request(`/api/ontologies/${ONTOLOGY}/query`, {
|
|
method: "POST",
|
|
body: { savedQuery: "orgs-behind-a-network" }
|
|
}),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const payload = await body<{ rows: Array<{ claims: string[] }>; resultId: string }>(response);
|
|
expect(payload.rows.length).toBeGreaterThan(0);
|
|
expect(payload.rows[0]!.claims).toHaveLength(4);
|
|
});
|
|
|
|
it("runs an ad-hoc triple-pattern query", async () => {
|
|
const response = await runQuery(
|
|
request(`/api/ontologies/${ONTOLOGY}/query`, {
|
|
method: "POST",
|
|
body: {
|
|
query: {
|
|
match: [{ subject: "?person", predicate: "worksOn", object: "?project" }],
|
|
select: ["?person", "?project"],
|
|
include: { claimStatus: ["asserted"] },
|
|
limit: 5
|
|
}
|
|
}
|
|
}),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const payload = await body<{ rows: unknown[] }>(response);
|
|
expect(payload.rows).toHaveLength(5);
|
|
});
|
|
|
|
it("rejects a query with neither savedQuery nor query", async () => {
|
|
const response = await runQuery(
|
|
request(`/api/ontologies/${ONTOLOGY}/query`, { method: "POST", body: {} }),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
expect(response.status).toBe(422);
|
|
});
|
|
|
|
it("explains a row down to its sources", async () => {
|
|
const queried = await runQuery(
|
|
request(`/api/ontologies/${ONTOLOGY}/query`, { method: "POST", body: { savedQuery: "funded-work" } }),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const { resultId } = await body<{ resultId: string }>(queried);
|
|
|
|
const response = await explain(
|
|
request(`/api/ontologies/${ONTOLOGY}/explain`, { method: "POST", body: { resultId, row: 0 } }),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const payload = await body<{ claims: Array<{ sources: unknown[] }>; ontology: string }>(response);
|
|
expect(payload.ontology).toContain(ONTOLOGY);
|
|
expect(payload.claims[0]!.sources.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("validates the package", async () => {
|
|
const response = await validate(
|
|
request(`/api/ontologies/${ONTOLOGY}/validate`, { method: "POST", body: { strict: true } }),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const payload = await body<{ ok: boolean; counts: { error: number } }>(response);
|
|
expect(payload.ok).toBe(true);
|
|
expect(payload.counts.error).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("governance and permissions", () => {
|
|
const operations = [
|
|
{
|
|
op: "assert-claim",
|
|
value: {
|
|
subject: "eth:person:avery-lindqvist",
|
|
predicate: "worksOn",
|
|
object: { entity: "eth:project:docs-portal" },
|
|
sources: ["eth:source:roadmap-2026"]
|
|
}
|
|
}
|
|
];
|
|
|
|
it("denies an anonymous proposal and names the missing scope", async () => {
|
|
const response = await createChangeSet(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets`, { method: "POST", body: { title: "x", operations } }),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
expect(response.status).toBe(403);
|
|
const payload = await body<{ error: { code: string; message: string } }>(response);
|
|
expect(payload.error.code).toBe("OO-A-DENIED");
|
|
expect(payload.error.message).toContain("ontology:claim:propose");
|
|
});
|
|
|
|
it("lets an agent token propose but never apply", async () => {
|
|
const created = await createChangeSet(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets`, {
|
|
method: "POST",
|
|
token: AGENT,
|
|
body: { title: "agent proposal", operations, runId: "run_api_1" }
|
|
}),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
expect(created.status).toBe(201);
|
|
const { changeSet } = await body<{ changeSet: { id: string; status: string } }>(created);
|
|
expect(changeSet.status).toBe("proposed");
|
|
|
|
const applied = await apply(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets/${changeSet.id}/apply`, { method: "POST", token: AGENT }),
|
|
params({ ontologyId: ONTOLOGY, changeSetId: changeSet.id })
|
|
);
|
|
expect(applied.status).toBe(403);
|
|
expect((await body<{ error: { message: string } }>(applied)).error.message).toMatch(/never apply directly/);
|
|
});
|
|
|
|
it("returns the semantic diff alongside a proposal", async () => {
|
|
const created = await createChangeSet(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets`, {
|
|
method: "POST",
|
|
token: CURATOR,
|
|
body: { title: "with diff", operations }
|
|
}),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const payload = await body<{ diff: { summary: { claimsAdded: number } } }>(created);
|
|
expect(payload.diff.summary.claimsAdded).toBe(1);
|
|
});
|
|
|
|
it("runs the curator loop: propose → approve → apply", async () => {
|
|
const created = await createChangeSet(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets`, {
|
|
method: "POST",
|
|
token: CURATOR,
|
|
body: { title: "curator loop", operations }
|
|
}),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const { changeSet } = await body<{ changeSet: { id: string } }>(created);
|
|
|
|
const approved = await approve(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets/${changeSet.id}/approve`, { method: "POST", token: CURATOR }),
|
|
params({ ontologyId: ONTOLOGY, changeSetId: changeSet.id })
|
|
);
|
|
expect(approved.status).toBe(201);
|
|
|
|
const applied = await apply(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets/${changeSet.id}/apply`, { method: "POST", token: CURATOR }),
|
|
params({ ontologyId: ONTOLOGY, changeSetId: changeSet.id })
|
|
);
|
|
expect(applied.status).toBe(200);
|
|
const payload = await body<{ revision: string; addedClaims: string[] }>(applied);
|
|
expect(payload.revision).toMatch(/^data-\d+$/);
|
|
expect(payload.addedClaims).toHaveLength(1);
|
|
});
|
|
|
|
it("requires approval for a merge, then allows it", async () => {
|
|
const mergeOps = [
|
|
{ op: "merge-entity", source: "eth:person:s-haddad", target: "eth:person:samir-haddad" }
|
|
];
|
|
const created = await createChangeSet(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets`, {
|
|
method: "POST",
|
|
token: CURATOR,
|
|
body: { title: "merge", operations: mergeOps }
|
|
}),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const { changeSet } = await body<{ changeSet: { id: string } }>(created);
|
|
|
|
const tooSoon = await apply(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets/${changeSet.id}/apply`, { method: "POST", token: CURATOR }),
|
|
params({ ontologyId: ONTOLOGY, changeSetId: changeSet.id })
|
|
);
|
|
expect(tooSoon.status).toBe(409);
|
|
|
|
await approve(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets/${changeSet.id}/approve`, { method: "POST", token: CURATOR }),
|
|
params({ ontologyId: ONTOLOGY, changeSetId: changeSet.id })
|
|
);
|
|
const applied = await apply(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets/${changeSet.id}/apply`, { method: "POST", token: CURATOR }),
|
|
params({ ontologyId: ONTOLOGY, changeSetId: changeSet.id })
|
|
);
|
|
expect(applied.status).toBe(200);
|
|
});
|
|
|
|
it("replays a POST carrying the same Idempotency-Key", async () => {
|
|
const key = "idem-1";
|
|
const first = await createChangeSet(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets`, {
|
|
method: "POST",
|
|
token: CURATOR,
|
|
headers: { "idempotency-key": key },
|
|
body: { title: "idempotent", operations }
|
|
}),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const second = await createChangeSet(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets`, {
|
|
method: "POST",
|
|
token: CURATOR,
|
|
headers: { "idempotency-key": key },
|
|
body: { title: "idempotent", operations }
|
|
}),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
|
|
expect(second.headers.get("idempotency-replayed")).toBe("true");
|
|
const a = await body<{ changeSet: { id: string } }>(first);
|
|
const b = await body<{ changeSet: { id: string } }>(second);
|
|
expect(b.changeSet.id).toBe(a.changeSet.id);
|
|
});
|
|
|
|
it("rejects a change set with no operations", async () => {
|
|
const response = await createChangeSet(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets`, {
|
|
method: "POST",
|
|
token: CURATOR,
|
|
body: { title: "empty", operations: [] }
|
|
}),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
expect(response.status).toBe(422);
|
|
});
|
|
|
|
it("lists change sets with their approval counts", async () => {
|
|
await createChangeSet(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets`, {
|
|
method: "POST",
|
|
token: CURATOR,
|
|
body: { title: "listed", operations }
|
|
}),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const response = await listChangeSets(
|
|
request(`/api/ontologies/${ONTOLOGY}/changesets`),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
const payload = await body<{ changeSets: Array<{ title: string; approvals: number }> }>(response);
|
|
expect(payload.changeSets.some((entry) => entry.title === "listed")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("events", () => {
|
|
it("returns the event log as JSON", async () => {
|
|
const response = await events(
|
|
request(`/api/ontologies/${ONTOLOGY}/events?limit=10`),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers.get("content-type")).toContain("application/json");
|
|
});
|
|
|
|
it("streams Server-Sent Events when asked", async () => {
|
|
const controller = new AbortController();
|
|
const response = await events(
|
|
new Request(`https://logicsrc.com/api/ontologies/${ONTOLOGY}/events`, {
|
|
headers: { accept: "text/event-stream" },
|
|
signal: controller.signal
|
|
}),
|
|
params({ ontologyId: ONTOLOGY })
|
|
);
|
|
|
|
expect(response.headers.get("content-type")).toBe("text/event-stream");
|
|
const reader = response.body!.getReader();
|
|
const { value } = await reader.read();
|
|
const text = new TextDecoder().decode(value);
|
|
expect(text).toContain("event: ");
|
|
controller.abort();
|
|
await reader.cancel();
|
|
});
|
|
});
|
|
|
|
describe("openapi", () => {
|
|
it("describes every implemented path and points at the published schemas", async () => {
|
|
const response = await openapi();
|
|
const document = await body<{ openapi: string; paths: Record<string, unknown> }>(response);
|
|
|
|
expect(document.openapi).toBe("3.1.0");
|
|
for (const path of [
|
|
"/ontologies",
|
|
"/ontologies/{ontologyId}/manifest",
|
|
"/ontologies/{ontologyId}/schema",
|
|
"/ontologies/{ontologyId}/entities",
|
|
"/ontologies/{ontologyId}/entities/{entityId}",
|
|
"/ontologies/{ontologyId}/claims",
|
|
"/ontologies/{ontologyId}/claims/{claimId}",
|
|
"/ontologies/{ontologyId}/query",
|
|
"/ontologies/{ontologyId}/explain",
|
|
"/ontologies/{ontologyId}/validate",
|
|
"/ontologies/{ontologyId}/changesets",
|
|
"/ontologies/{ontologyId}/changesets/{changeSetId}",
|
|
"/ontologies/{ontologyId}/changesets/{changeSetId}/review",
|
|
"/ontologies/{ontologyId}/changesets/{changeSetId}/approve",
|
|
"/ontologies/{ontologyId}/changesets/{changeSetId}/apply",
|
|
"/ontologies/{ontologyId}/events"
|
|
]) {
|
|
expect(document.paths, `missing ${path}`).toHaveProperty([path]);
|
|
}
|
|
|
|
expect(JSON.stringify(document)).toContain("https://logicsrc.com/schemas/openontology/claim.schema.json");
|
|
});
|
|
});
|