mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-03 21:27:07 +00:00
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>
151 lines
6.3 KiB
TypeScript
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);
|
|
});
|
|
});
|