feat(openontology): implement OpenOntology Phase 0 + local engine and CLI (#99)
Some checks failed
CI / build (push) Has been cancelled
test / test (push) Has been cancelled

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>
This commit is contained in:
Anthony Ettinger 2026-07-26 02:10:13 -07:00 • committed by GitHub
parent 0d9dab0447
commit 58c942c67f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
101 changed files with 11934 additions and 10 deletions

View file

@ -0,0 +1,400 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { stringify as toYaml } from "yaml";
import { OPENONTOLOGY_VERSION } from "./types.js";
import type { Claim, Entity, EntityType, RelationshipType, SavedQuery, Source } from "./types.js";
export interface InitOptions {
id: string;
name?: string;
namespace?: string;
maintainer?: string;
license?: string;
/** Pinned so `init` output is byte-identical across runs and runtimes. */
now?: string;
}
export interface InitResult {
dir: string;
files: string[];
counts: { entityTypes: number; relationshipTypes: number; entities: number; claims: number; sources: number };
}
/**
* Create a starter package that passes `validate --strict` with no edits.
*
* The shape is deliberately tiny — three types, three relationships, eight
* entities, fourteen claims — so a newcomer can read the whole thing in a
* minute and still see temporal claims, provenance, and a saved query.
*/
export function initOntologyPackage(dir: string, options: InitOptions): InitResult {
const now = options.now ?? new Date().toISOString();
const id = options.id;
const name = options.name ?? titleize(id);
const namespace = options.namespace ?? `https://logicsrc.com/ontology/${id}/`;
const maintainer = options.maintainer ?? "urn:logicsrc:local";
const prefix = id.split("-")[0] as string;
// Binds the compact prefix to the namespace, so ids survive an IRI round trip.
const namespaces = [
{
openontology: OPENONTOLOGY_VERSION,
kind: "Namespace" as const,
prefix,
uri: namespace,
description: `Compact id prefix for ${name}.`
}
];
const entityTypes: EntityType[] = [
{
openontology: OPENONTOLOGY_VERSION,
kind: "EntityType",
id: "Person",
label: "Person",
description: "A human participant.",
keyProperties: ["canonicalName"],
properties: { role: { type: "string", description: "Current role, if known." } }
},
{
openontology: OPENONTOLOGY_VERSION,
kind: "EntityType",
id: "Organization",
label: "Organization",
description: "A company, foundation, working group, or other collective."
},
{
openontology: OPENONTOLOGY_VERSION,
kind: "EntityType",
id: "Project",
label: "Project",
description: "A named body of work that people and organizations contribute to.",
properties: { homepage: { type: "url", description: "Canonical project URL." } }
}
];
const relationships: RelationshipType[] = [
{
openontology: OPENONTOLOGY_VERSION,
kind: "RelationshipType",
id: "worksAt",
label: "works at",
description: "A person is affiliated with an organization.",
from: ["Person"],
to: ["Organization"],
cardinality: "many-to-many",
temporal: true
},
{
openontology: OPENONTOLOGY_VERSION,
kind: "RelationshipType",
id: "worksOn",
label: "works on",
description: "A person actively contributes work to a project.",
from: ["Person"],
to: ["Project"],
cardinality: "many-to-many",
temporal: true,
inverse: "hasContributor"
},
{
openontology: OPENONTOLOGY_VERSION,
kind: "RelationshipType",
id: "hasContributor",
label: "has contributor",
description: "Inverse of worksOn.",
from: ["Project"],
to: ["Person"],
cardinality: "many-to-many",
inverse: "worksOn"
},
{
openontology: OPENONTOLOGY_VERSION,
kind: "RelationshipType",
id: "maintains",
label: "maintains",
description: "An organization is responsible for a project.",
from: ["Organization"],
to: ["Project"],
cardinality: "one-to-many"
}
];
const properties = [
{
openontology: OPENONTOLOGY_VERSION,
kind: "Property" as const,
id: "homepage",
label: "homepage",
description: "Canonical URL for a project.",
type: "url" as const
},
{
openontology: OPENONTOLOGY_VERSION,
kind: "Property" as const,
id: "role",
label: "role",
description: "A person's stated role.",
type: "string" as const
}
];
const person = (slug: string, canonicalName: string): Entity => ({
openontology: OPENONTOLOGY_VERSION,
kind: "Entity",
id: `${prefix}:person:${slug}`,
type: "Person",
canonicalName,
createdAt: now,
createdBy: maintainer
});
const org = (slug: string, canonicalName: string): Entity => ({
openontology: OPENONTOLOGY_VERSION,
kind: "Entity",
id: `${prefix}:org:${slug}`,
type: "Organization",
canonicalName,
createdAt: now,
createdBy: maintainer
});
const project = (slug: string, canonicalName: string): Entity => ({
openontology: OPENONTOLOGY_VERSION,
kind: "Entity",
id: `${prefix}:project:${slug}`,
type: "Project",
canonicalName,
createdAt: now,
createdBy: maintainer
});
const entities: Entity[] = [
person("alice", "Alice Reyes"),
person("bob", "Bob Nakamura"),
person("carol", "Carol Okonkwo"),
org("northwind", "Northwind Labs"),
org("bluebird", "Bluebird Foundation"),
project("zk-prover", "ZK Prover"),
project("ledger-indexer", "Ledger Indexer"),
project("docs-portal", "Docs Portal")
];
const sources: Source[] = [
{
openontology: OPENONTOLOGY_VERSION,
kind: "Source",
id: `${prefix}:source:team-page`,
sourceType: "web-page",
uri: "https://example.org/team",
title: "Team page",
publisher: "example.org",
retrievedAt: now,
mediaType: "text/html",
license: "CC-BY-4.0"
},
{
openontology: OPENONTOLOGY_VERSION,
kind: "Source",
id: `${prefix}:source:repo`,
sourceType: "git-commit",
uri: "https://example.org/repo/commit/0000000",
title: "Repository commit",
publisher: "example.org",
retrievedAt: now,
mediaType: "text/plain",
license: "MIT"
}
];
let claimSeq = 0;
const nextClaimId = () => `${prefix}:claim:${String(++claimSeq).padStart(4, "0")}`;
const rel = (
subject: string,
predicate: string,
object: string,
extra: Partial<Claim> = {}
): Claim => ({
openontology: OPENONTOLOGY_VERSION,
kind: "Claim",
id: nextClaimId(),
ontology: `${id}@0.1.0`,
subject: `${prefix}:${subject}`,
predicate,
object: { entity: `${prefix}:${object}` },
status: "asserted",
confidence: 0.9,
validTime: { from: "2026-01-01T00:00:00Z", to: null },
assertedAt: now,
assertedBy: maintainer,
sources: [`${prefix}:source:team-page`],
...extra
});
const value = (subject: string, predicate: string, val: unknown): Claim => ({
openontology: OPENONTOLOGY_VERSION,
kind: "Claim",
id: nextClaimId(),
ontology: `${id}@0.1.0`,
subject: `${prefix}:${subject}`,
predicate,
object: { value: val },
status: "asserted",
assertedAt: now,
assertedBy: maintainer,
firstParty: true
});
const claims: Claim[] = [
rel("person:alice", "worksAt", "org:northwind"),
rel("person:bob", "worksAt", "org:northwind"),
rel("person:carol", "worksAt", "org:bluebird"),
rel("person:alice", "worksOn", "project:zk-prover"),
rel("person:bob", "worksOn", "project:zk-prover"),
rel("person:bob", "worksOn", "project:ledger-indexer"),
rel("person:carol", "worksOn", "project:docs-portal"),
rel("org:northwind", "maintains", "project:zk-prover", { validTime: undefined }),
rel("org:northwind", "maintains", "project:ledger-indexer", { validTime: undefined }),
rel("org:bluebird", "maintains", "project:docs-portal", { validTime: undefined }),
value("project:zk-prover", "homepage", "https://example.org/zk-prover"),
value("project:ledger-indexer", "homepage", "https://example.org/ledger-indexer"),
value("project:docs-portal", "homepage", "https://example.org/docs-portal"),
value("person:alice", "role", "Protocol engineer")
];
const queries: SavedQuery[] = [
{
openontology: OPENONTOLOGY_VERSION,
kind: "SavedQuery",
id: "contributors",
label: "Contributors by project",
description: "Every person actively working on a project, with the organization they work at.",
query: {
match: [
{ subject: "?person", predicate: "worksOn", object: "?project" },
{ subject: "?person", predicate: "worksAt", object: "?org" }
],
select: ["?person", "?project", "?org"],
include: { claimStatus: ["asserted"], labels: true },
orderBy: [{ variable: "?person", direction: "asc" }]
}
}
];
const constraints = [
{
openontology: OPENONTOLOGY_VERSION,
kind: "Constraint" as const,
id: "project-has-homepage",
description: "Every project should record a homepage so readers can verify it exists.",
severity: "warning" as const,
remediation: "Add a homepage claim for the project.",
rule: { type: "required-predicate" as const, entityType: "Project", predicate: "homepage" }
}
];
const manifest = {
openontology: OPENONTOLOGY_VERSION,
kind: "OntologyPackage",
id,
name,
version: "0.1.0",
namespace,
description: `${name} — a starter OpenOntology package.`,
license: options.license ?? "CC-BY-4.0",
maintainers: [{ id: maintainer }],
imports: [],
schema: {
namespaces: "schema/namespaces.yaml",
entityTypes: "schema/entity-types.yaml",
properties: "schema/properties.yaml",
relationships: "schema/relationships.yaml",
constraints: "schema/constraints.yaml",
queries: "schema/queries.yaml"
},
data: {
entities: "data/entities.ndjson",
claims: "data/claims.ndjson",
sources: "data/sources.ndjson"
}
};
mkdirSync(join(dir, "schema"), { recursive: true });
mkdirSync(join(dir, "data"), { recursive: true });
const written: string[] = [];
const writeYaml = (relPath: string, value_: unknown) => {
writeFileSync(join(dir, relPath), toYaml(value_, { lineWidth: 100 }), "utf8");
written.push(relPath);
};
const writeNdjson = (relPath: string, rows: unknown[]) => {
writeFileSync(join(dir, relPath), `${rows.map((row) => JSON.stringify(row)).join("\n")}\n`, "utf8");
written.push(relPath);
};
writeYaml("openontology.yaml", manifest);
writeYaml("schema/namespaces.yaml", namespaces);
writeYaml("schema/entity-types.yaml", entityTypes);
writeYaml("schema/properties.yaml", properties);
writeYaml("schema/relationships.yaml", relationships);
writeYaml("schema/constraints.yaml", constraints);
writeYaml("schema/queries.yaml", queries);
writeNdjson("data/entities.ndjson", entities);
writeNdjson("data/claims.ndjson", claims);
writeNdjson("data/sources.ndjson", sources);
writeFileSync(join(dir, "README.md"), readme(id, name), "utf8");
written.push("README.md");
return {
dir,
files: written,
counts: {
entityTypes: entityTypes.length,
relationshipTypes: relationships.length,
entities: entities.length,
claims: claims.length,
sources: sources.length
}
};
}
function titleize(id: string): string {
return id
.split("-")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
}
function readme(id: string, name: string): string {
return `# ${name}
A [LogicSRC OpenOntology](https://logicsrc.com/openontology) package.
\`\`\`bash
logicsrc ontology validate . --strict
logicsrc ontology query run contributors --format table
logicsrc ontology query explain contributors --row 0 --format markdown
\`\`\`
## Layout
| Path | What it holds |
| --- | --- |
| \`openontology.yaml\` | Package identity, namespace, license, and file map |
| \`schema/\` | Entity types, properties, relationship types, constraints, saved queries |
| \`data/\` | Entities, claims, and sources as newline-delimited JSON |
## Model
Five nouns are enough to read everything here:
- **Type** — what kind of thing something is (\`Person\`, \`Organization\`, \`Project\`)
- **Entity** — a specific thing with a stable id (\`${id.split("-")[0]}:person:alice\`)
- **Claim** — a typed statement about an entity, or between two entities
- **Source** — where the claim came from
- **Change set** — a reviewable proposal to add, correct, merge, dispute, or retract
Claims are append-only. Corrections are expressed as a dispute, retraction, or
supersession, so the record of what was believed and when is never overwritten.
`;
}