mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-03 05:07:10 +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>
99 lines
3.9 KiB
TypeScript
99 lines
3.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { parse as parseYaml, stringify as toYaml } from "yaml";
|
|
import { canonicalize, canonicalObject, digest, packageDigest } from "./canonical.js";
|
|
import { idForm, isValidId, toIri, createIdFactory } from "./ids.js";
|
|
|
|
describe("canonical JSON", () => {
|
|
it("sorts object keys so key order never changes the bytes", () => {
|
|
expect(canonicalize({ b: 1, a: 2 })).toBe('{"a":2,"b":1}');
|
|
expect(canonicalize({ a: 2, b: 1 })).toBe(canonicalize({ b: 1, a: 2 }));
|
|
});
|
|
|
|
it("sorts nested keys and preserves array order", () => {
|
|
const value = { z: [{ y: 1, x: 2 }], a: { d: 4, c: 3 } };
|
|
expect(canonicalize(value)).toBe('{"a":{"c":3,"d":4},"z":[{"x":2,"y":1}]}');
|
|
});
|
|
|
|
it("drops undefined members but keeps explicit nulls", () => {
|
|
expect(canonicalize({ a: undefined, b: null })).toBe('{"b":null}');
|
|
});
|
|
|
|
it("normalizes -0 so it cannot produce a second digest", () => {
|
|
expect(digest({ n: -0 })).toBe(digest({ n: 0 }));
|
|
});
|
|
|
|
it("refuses non-finite numbers rather than emitting null", () => {
|
|
expect(() => canonicalize({ n: Number.NaN })).toThrow(/non-finite/);
|
|
});
|
|
|
|
it("produces the same digest for YAML- and JSON-authored input (R16/R17)", () => {
|
|
const object = { kind: "Entity", id: "x:person:a", aliases: ["a", "b"], nested: { k: 1 } };
|
|
const fromJson = JSON.parse(JSON.stringify(object)) as unknown;
|
|
const fromYaml = parseYaml(toYaml(object)) as unknown;
|
|
expect(digest(fromYaml)).toBe(digest(fromJson));
|
|
});
|
|
|
|
it("round-trips through canonicalObject", () => {
|
|
const object = { b: 1, a: { d: [3, 2], c: undefined } };
|
|
expect(canonicalize(canonicalObject(object))).toBe(canonicalize(object));
|
|
});
|
|
|
|
it("changes the package digest when any file digest changes", () => {
|
|
const manifest = { id: "p", version: "0.1.0" };
|
|
const a = packageDigest(manifest, [{ path: "data/claims.ndjson", digest: "sha256:aa" }]);
|
|
const b = packageDigest(manifest, [{ path: "data/claims.ndjson", digest: "sha256:bb" }]);
|
|
expect(a).not.toBe(b);
|
|
});
|
|
|
|
it("is insensitive to the order files are listed in", () => {
|
|
const manifest = { id: "p" };
|
|
const files = [
|
|
{ path: "b.ndjson", digest: "sha256:bb" },
|
|
{ path: "a.ndjson", digest: "sha256:aa" }
|
|
];
|
|
expect(packageDigest(manifest, files)).toBe(packageDigest(manifest, [...files].reverse()));
|
|
});
|
|
});
|
|
|
|
describe("identifier profile", () => {
|
|
it("recognizes the three accepted forms", () => {
|
|
expect(idForm("ethereum:person:alice")).toBe("compact");
|
|
expect(idForm("https://example.org/person/alice")).toBe("iri");
|
|
expect(idForm("urn:logicsrc:ethereum:person:alice")).toBe("urn");
|
|
expect(idForm("not an id")).toBeNull();
|
|
expect(isValidId("Person")).toBe(false);
|
|
});
|
|
|
|
it("canonicalizes compact ids against the package namespace", () => {
|
|
expect(
|
|
toIri("ethereum:person:alice", { defaultNamespace: "https://logicsrc.com/ontology/ethereum/" })
|
|
).toBe("https://logicsrc.com/ontology/ethereum/person/alice");
|
|
});
|
|
|
|
it("adds the trailing slash when a namespace omits it", () => {
|
|
expect(toIri("x:person:a", { defaultNamespace: "https://example.org/ns" })).toBe(
|
|
"https://example.org/ns/person/a"
|
|
);
|
|
});
|
|
|
|
it("leaves IRIs and URNs untouched", () => {
|
|
const iri = "https://example.org/person/alice";
|
|
expect(toIri(iri, { defaultNamespace: "https://other.example/" })).toBe(iri);
|
|
const urn = "urn:logicsrc:person:alice";
|
|
expect(toIri(urn, { defaultNamespace: "https://other.example/" })).toBe(urn);
|
|
});
|
|
|
|
it("honours per-prefix namespaces for imported packages", () => {
|
|
expect(
|
|
toIri("other:person:bob", {
|
|
defaultNamespace: "https://example.org/mine/",
|
|
namespaces: { other: "https://example.org/theirs/" }
|
|
})
|
|
).toBe("https://example.org/theirs/person/bob");
|
|
});
|
|
|
|
it("generates deterministic sequential ids", () => {
|
|
const next = createIdFactory("claim");
|
|
expect([next(), next()]).toEqual(["claim:000001", "claim:000002"]);
|
|
});
|
|
});
|