logicsrc/packages/openontology/src/conformance.test.ts
Anthony Ettinger 58c942c67f
Some checks failed
CI / build (push) Has been cancelled
test / test (push) Has been cancelled
feat(openontology): implement OpenOntology Phase 0 + local engine and CLI (#99)
Implements OpenPRD 0001 through Phase 0 (specification, schemas, example,
docs surface) and Phase 1 (local engine, CLI, conformance tests).

Schemas (17 contracts, JSON Schema Draft 2020-12, additionalProperties:false)
  manifest, namespace, entity-type, property, relationship-type, constraint,
  query, action, entity, claim, source, evidence, changeset, review, approval,
  event, package — registered in @logicsrc/validators and exported from
  @logicsrc/schemas under https://logicsrc.com/schemas/openontology/.

@logicsrc/openontology
  - canonical JSON + sha256 package digests; YAML, JSON, NDJSON, and inline
    authoring all compile to the same bytes, so digests are authoring-agnostic
  - id profile: compact / IRI / urn with one canonicalization rule, prefix
    bound by a Namespace object so IRIs reverse unambiguously
  - validation: schema, graph (domain/range, datatypes, dangling refs),
    provenance (source-or-firstParty, agent runId, derivation inputs), policy
    (excerpt limits, licensing, visibility, staleness) and declared
    constraints; four severities, stable codes, text/json/yaml/markdown
  - portable triple-pattern query AST: multi-hop, 14 operators, asOf and
    recordedAsOf, per-status filtering, distinct/order/limit, explanation
    mode, and enforced depth/binding/row limits
  - append-only store: claims are immutable; dispute/retract/supersede append
    status transitions and the effective status is the latest one
  - change sets: 9 operations, atomic pre-flight, conflict detection on stale
    base revisions, semantic diff with duplicate-identity warnings and
    affected-query deltas, per-operation reviewer decisions
  - policy: agents propose but can never apply — the denial keys on actor
    type, so every scope plus high confidence plus --yolo still cannot apply;
    merges need approval, bulk retractions need two, undeclared action side
    effects are denied
  - JSON-LD 1.1 export/import with PROV-O aliases and lossy-field reporting
  - pluggable signature envelope with a jws-ed25519 reference profile and a
    fail-closed trust policy

CLI: logicsrc ontology init|validate|lint|build|inspect, entity, claim, query,
changeset, import, export, audit. Reads take --format, writes default to a
proposal, exit codes are stable for CI.

Example: examples/openontology/ethereum-ecosystem — 12 entity types, 17
relationship types, 63 entities, 169 claims, 25 sources, 31 evidence records,
5 saved queries, every claim lifecycle state, and a pending merge proposal.
All data is fictional; the directory is removable without affecting any core
test.

Docs: docs/openontology{,-governance,-interoperability}.md, a real
/openontology route, homepage + nav + sitemap entries, and a root README
section.

Verification: 112 new tests; full monorepo build and every workspace test
pass; conformance bundle (18 valid + 13 invalid fixtures) runs against the
published schemas alone; Node.js 25 and Bun 1.3 produce byte-identical
digests, revisions, event trails, and query results.

Not included (later PRD phases): MCP resources, REST/SSE, Turso adapter, TUI
and PWA surfaces, RDF/SHACL mappings, source adapters, governed actions.

Refs: prd/0001-add-logicsrc-openontology-spec.md

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 02:10:13 -07:00

151 lines
6.3 KiB
TypeScript

import { existsSync, readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { validate as validateSchema, type SchemaKind } from "@logicsrc/validators";
import { buildOntologyPackage, loadOntologyPackage } from "./package.js";
import { createOntologyEngine } from "./engine.js";
import { exportJsonLd, importJsonLd, packagePrefix } from "./jsonld.js";
import { localActor } from "./policy.js";
const HERE = dirname(fileURLToPath(import.meta.url));
const FIXTURES = resolve(HERE, "../../schemas/fixtures/openontology");
const EXAMPLE = resolve(HERE, "../../../examples/openontology/ethereum-ecosystem");
type Conformance = {
valid: Array<{ fixture: string; kind: SchemaKind }>;
invalid: Array<{ fixture: string; kind: SchemaKind; reason: string }>;
};
const conformance = JSON.parse(readFileSync(join(FIXTURES, "conformance.json"), "utf8")) as Conformance;
const read = (relPath: string) => JSON.parse(readFileSync(join(FIXTURES, relPath), "utf8")) as unknown;
/**
* The conformance bundle is the third-party contract (R27): these assertions
* only touch published schemas and fixture files, so another implementation
* can reproduce them without importing a line of the reference engine.
*/
describe("conformance fixtures", () => {
it("has a fixture for every normative object", () => {
const kinds = new Set(conformance.valid.map((entry) => entry.kind));
expect(kinds).toEqual(
new Set([
"openontology-manifest",
"openontology-namespace",
"openontology-entity-type",
"openontology-property",
"openontology-relationship-type",
"openontology-constraint",
"openontology-query",
"openontology-action",
"openontology-entity",
"openontology-claim",
"openontology-source",
"openontology-evidence",
"openontology-changeset",
"openontology-review",
"openontology-approval",
"openontology-event"
])
);
});
it.each(conformance.valid)("$fixture validates as $kind", ({ fixture, kind }) => {
const result = validateSchema(kind, read(fixture));
if (!result.ok) {
throw new Error(
`${fixture} should validate but did not:\n${result.errors
.map((e) => ` ${e.instancePath || "/"} ${e.message}`)
.join("\n")}`
);
}
expect(result.ok).toBe(true);
});
it.each(conformance.invalid)("$fixture is rejected: $reason", ({ fixture, kind }) => {
const result = validateSchema(kind, read(fixture));
expect(result.ok).toBe(false);
});
});
/**
* The Ethereum example is a demonstration, not a dependency (R195): if it is
* removed, these tests skip rather than fail.
*/
const describeExample = existsSync(join(EXAMPLE, "openontology.yaml")) ? describe : describe.skip;
describeExample("ethereum ecosystem example", () => {
const pkg = () => loadOntologyPackage(EXAMPLE);
it("passes strict validation", () => {
const loaded = pkg();
const report = createOntologyEngine({ package: loaded, actor: localActor() }).validateOntologyPackage({
strict: true,
expectedDigest: buildOntologyPackage(loaded).digest
});
const errors = report.findings.filter((f) => f.severity === "error");
expect(errors).toEqual([]);
});
it("meets the PRD's fixture-coverage bar", () => {
const loaded = pkg();
expect(loaded.schema.entityTypes.length).toBeGreaterThanOrEqual(10);
expect(loaded.schema.relationships.length).toBeGreaterThanOrEqual(12);
expect(loaded.data.entities.length).toBeGreaterThanOrEqual(50);
expect(loaded.data.claims.length).toBeGreaterThanOrEqual(150);
expect(loaded.data.sources.length).toBeGreaterThanOrEqual(25);
expect(loaded.schema.queries.length).toBeGreaterThanOrEqual(5);
});
it("demonstrates every claim lifecycle state", () => {
const byStatus = new Map<string, number>();
for (const claim of pkg().data.claims) {
byStatus.set(claim.status, (byStatus.get(claim.status) ?? 0) + 1);
}
for (const status of ["asserted", "proposed", "disputed", "retracted", "superseded", "derived"]) {
expect(byStatus.get(status), `expected at least one ${status} claim`).toBeGreaterThanOrEqual(1);
}
});
it("ships a pending merge proposal for the duplicate identity", () => {
const loaded = pkg();
const duplicates = loaded.data.entities.filter((e) => e.canonicalName.includes("Haddad"));
expect(duplicates).toHaveLength(2);
expect(existsSync(join(EXAMPLE, "changesets/merge-haddad.yaml"))).toBe(true);
});
it("answers a three-hop question and explains the answer", () => {
const engine = createOntologyEngine({ package: pkg(), actor: localActor() });
const result = engine.queryOntology("orgs-behind-a-network");
expect(result.rows.length).toBeGreaterThan(0);
const explanation = engine.explainOntologyResult(result.id, 0);
// Four patterns matched, so the answer rests on four claims, each sourced.
expect(explanation.claims).toHaveLength(4);
for (const entry of explanation.claims) expect(entry.sources.length).toBeGreaterThan(0);
});
it("hides proposed, disputed, and retracted claims from the default view", () => {
const engine = createOntologyEngine({ package: pkg(), actor: localActor() });
const defaultView = engine.queryOntology({
match: [{ subject: "?p", predicate: "worksOn", object: "?x", bindClaim: "?claim" }]
});
const everything = engine.queryOntology({
match: [{ subject: "?p", predicate: "worksOn", object: "?x", bindClaim: "?claim" }],
include: { claimStatus: ["asserted", "proposed", "disputed", "retracted", "superseded", "derived"] }
});
expect(everything.rows.length).toBeGreaterThan(defaultView.rows.length);
});
it("round-trips through JSON-LD without losing ids", () => {
const loaded = pkg();
const exported = exportJsonLd(loaded);
const back = importJsonLd(exported.document, { ...loaded.manifest, prefix: packagePrefix(loaded) });
expect(back.entities.map((e) => e.id).sort()).toEqual(loaded.data.entities.map((e) => e.id).sort());
expect(back.claims.map((c) => c.id).sort()).toEqual(loaded.data.claims.map((c) => c.id).sort());
});
it("builds the same digest twice (deterministic build)", () => {
expect(buildOntologyPackage(pkg()).digest).toBe(buildOntologyPackage(pkg()).digest);
});
});