mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-03 13:17:07 +00:00
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>
This commit is contained in:
parent
0d9dab0447
commit
58c942c67f
101 changed files with 11934 additions and 10 deletions
587
packages/openontology/src/engine.ts
Normal file
587
packages/openontology/src/engine.ts
Normal file
|
|
@ -0,0 +1,587 @@
|
|||
import { applyChangeSet, diffChangeSet, type ApplyResult, type SemanticDiff } from "./changeset.js";
|
||||
import { canonicalObject } from "./canonical.js";
|
||||
import { createIdFactory } from "./ids.js";
|
||||
import { exportJsonLd, importJsonLd, packagePrefix, type JsonLdExport } from "./jsonld.js";
|
||||
import { buildOntologyPackage, loadOntologyPackage, type LoadInput } from "./package.js";
|
||||
import { evaluatePolicy, localActor, type Actor, type PolicyDecision, type PolicyOptions } from "./policy.js";
|
||||
import { evaluateQuery, type KnowledgeView, type QueryLimits } from "./query.js";
|
||||
import { createMemoryStore, type EntityMatch, type OntologyStore } from "./store.js";
|
||||
import { renderReport, validateOntologyPackage, type ValidateOptions } from "./validate.js";
|
||||
import { OPENONTOLOGY_VERSION } from "./types.js";
|
||||
import type {
|
||||
Approval,
|
||||
BuiltPackage,
|
||||
ChangeOperation,
|
||||
ChangeSet,
|
||||
Claim,
|
||||
Entity,
|
||||
Evidence,
|
||||
LoadedPackage,
|
||||
OntologyEvent,
|
||||
QueryBody,
|
||||
QueryResult,
|
||||
Review,
|
||||
SavedQuery,
|
||||
Source,
|
||||
ValidationReport
|
||||
} from "./types.js";
|
||||
|
||||
export class OntologyPermissionError extends Error {
|
||||
readonly code = "OO-A-DENIED";
|
||||
constructor(message: string, readonly decision: PolicyDecision) {
|
||||
super(message);
|
||||
this.name = "OntologyPermissionError";
|
||||
}
|
||||
}
|
||||
|
||||
export class OntologyApprovalError extends Error {
|
||||
readonly code = "OO-A-APPROVAL-REQUIRED";
|
||||
constructor(message: string, readonly decision: PolicyDecision, readonly have: number) {
|
||||
super(message);
|
||||
this.name = "OntologyApprovalError";
|
||||
}
|
||||
}
|
||||
|
||||
export class OntologyNotFoundError extends Error {
|
||||
readonly code = "OO-A-NOT-FOUND";
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "OntologyNotFoundError";
|
||||
}
|
||||
}
|
||||
|
||||
export interface EngineOptions {
|
||||
/** A loaded package, a directory, or a pre-built store. */
|
||||
package?: LoadedPackage | string | LoadInput;
|
||||
store?: OntologyStore;
|
||||
actor?: Actor;
|
||||
policy?: PolicyOptions;
|
||||
limits?: Partial<QueryLimits>;
|
||||
/** Injected for determinism: tests and conformance runs pin both. */
|
||||
clock?: () => string;
|
||||
idFactory?: (kind: "claim" | "event" | "entity" | "changeset" | "review" | "approval") => string;
|
||||
client?: string;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
export interface ExplainedClaim {
|
||||
claim: Claim;
|
||||
sources: Source[];
|
||||
evidence: Evidence[];
|
||||
history: Array<{ status: string; at: string; by: string; reason?: string }>;
|
||||
}
|
||||
|
||||
export interface Explanation {
|
||||
resultId: string;
|
||||
ontology: string;
|
||||
row: number;
|
||||
bindings: Record<string, unknown>;
|
||||
claims: ExplainedClaim[];
|
||||
filters: QueryResult["explanation"]["filters"];
|
||||
claimStatus: QueryResult["explanation"]["claimStatus"];
|
||||
asOf?: string;
|
||||
}
|
||||
|
||||
export interface OntologyEngine {
|
||||
readonly store: OntologyStore;
|
||||
readonly actor: Actor;
|
||||
|
||||
getOntologyManifest(): LoadedPackage["manifest"];
|
||||
getOntologySchema(): LoadedPackage["schema"];
|
||||
|
||||
getEntity(id: string): Entity;
|
||||
findEntities(input: { text?: string; type?: string; externalId?: Record<string, string>; limit?: number }): EntityMatch[];
|
||||
getClaim(id: string): Claim;
|
||||
claimHistory(id: string): ReturnType<OntologyStore["claimHistory"]>;
|
||||
|
||||
queryOntology(query: QueryBody | SavedQuery | string, params?: Record<string, unknown>): QueryResult & { id: string };
|
||||
explainOntologyResult(resultId: string, row?: number): Explanation;
|
||||
|
||||
createOntologyChangeSet(input: {
|
||||
title: string;
|
||||
rationale?: string;
|
||||
operations: ChangeOperation[];
|
||||
requiredApprovals?: number;
|
||||
runId?: string;
|
||||
}): ChangeSet;
|
||||
validateOntologyChangeSet(id: string): ValidationReport;
|
||||
diffOntologyChangeSet(id: string): SemanticDiff;
|
||||
reviewOntologyChangeSet(id: string, review: { state: Review["state"]; comment?: string; operationDecisions?: Review["operationDecisions"] }): Review;
|
||||
approveOntologyChangeSet(id: string, approval?: { comment?: string }): Approval;
|
||||
rejectOntologyChangeSet(id: string, comment?: string): ChangeSet;
|
||||
applyOntologyChangeSet(id: string, options?: { skipRejectedOperations?: boolean }): ApplyResult;
|
||||
|
||||
validateOntologyPackage(options?: ValidateOptions): ValidationReport;
|
||||
buildOntologyPackage(): BuiltPackage;
|
||||
exportOntology(format: "json" | "jsonld"): { format: string; document: unknown; lossy: JsonLdExport["lossy"] };
|
||||
importOntology(input: { format: "jsonld"; document: Record<string, unknown> }): { entities: number; claims: number; operations: ChangeOperation[] };
|
||||
|
||||
subscribeOntologyEvents(listener: (event: OntologyEvent) => void, filter?: { type?: string[] }): () => void;
|
||||
listEvents(filter?: { type?: string[]; changeSet?: string; limit?: number }): OntologyEvent[];
|
||||
|
||||
view(): KnowledgeView;
|
||||
}
|
||||
|
||||
export function createOntologyEngine(options: EngineOptions = {}): OntologyEngine {
|
||||
const loaded: LoadedPackage | undefined = options.store
|
||||
? undefined
|
||||
: normalizePackage(options.package);
|
||||
|
||||
const store =
|
||||
options.store ??
|
||||
createMemoryStore(
|
||||
loaded ?? {
|
||||
manifest: {
|
||||
openontology: OPENONTOLOGY_VERSION,
|
||||
kind: "OntologyPackage",
|
||||
id: "untitled",
|
||||
name: "Untitled ontology",
|
||||
version: "0.0.0",
|
||||
namespace: "https://logicsrc.com/ontology/untitled/",
|
||||
description: "In-memory ontology",
|
||||
license: "unknown",
|
||||
maintainers: [{ id: "urn:logicsrc:anonymous" }]
|
||||
},
|
||||
schema: {
|
||||
namespaces: [],
|
||||
entityTypes: [],
|
||||
properties: [],
|
||||
relationships: [],
|
||||
constraints: [],
|
||||
queries: [],
|
||||
actions: []
|
||||
},
|
||||
data: { entities: [], claims: [], sources: [], evidence: [] },
|
||||
files: []
|
||||
}
|
||||
);
|
||||
|
||||
const actor = options.actor ?? localActor();
|
||||
const clock = options.clock ?? (() => new Date().toISOString());
|
||||
const counters = new Map<string, () => string>();
|
||||
const idFactory =
|
||||
options.idFactory ??
|
||||
((kind: string) => {
|
||||
if (!counters.has(kind)) counters.set(kind, createIdFactory(kind));
|
||||
return (counters.get(kind) as () => string)();
|
||||
});
|
||||
|
||||
const results = new Map<string, QueryResult>();
|
||||
const listeners = new Set<{ fn: (event: OntologyEvent) => void; types?: string[] }>();
|
||||
|
||||
const baseAppendEvent = store.appendEvent.bind(store);
|
||||
store.appendEvent = (event: OntologyEvent) => {
|
||||
baseAppendEvent(event);
|
||||
for (const listener of listeners) {
|
||||
if (listener.types && !listener.types.includes(event.type)) continue;
|
||||
listener.fn(event);
|
||||
}
|
||||
};
|
||||
|
||||
const requirePolicy = (operation: Parameters<typeof evaluatePolicy>[0]): PolicyDecision => {
|
||||
const decision = evaluatePolicy(operation, actor, options.policy);
|
||||
if (decision.decision === "deny") {
|
||||
throw new OntologyPermissionError(
|
||||
`${decision.reason}${decision.missingScopes.length ? ` (missing: ${decision.missingScopes.join(", ")})` : ""}`,
|
||||
decision
|
||||
);
|
||||
}
|
||||
return decision;
|
||||
};
|
||||
|
||||
const emit = (
|
||||
type: OntologyEvent["type"],
|
||||
subject?: string,
|
||||
data?: Record<string, unknown>,
|
||||
decision?: PolicyDecision,
|
||||
changeSetId?: string
|
||||
) => {
|
||||
const event: OntologyEvent = {
|
||||
openontology: OPENONTOLOGY_VERSION,
|
||||
kind: "Event",
|
||||
id: idFactory("event"),
|
||||
type,
|
||||
ontology: store.getManifest().id,
|
||||
at: clock(),
|
||||
actor: actor.id,
|
||||
actorType: actor.type,
|
||||
client: options.client ?? actor.client,
|
||||
requestId: options.requestId,
|
||||
subject,
|
||||
...(changeSetId ? { changeSet: changeSetId } : {}),
|
||||
revision: store.revision(),
|
||||
...(decision
|
||||
? { policyDecision: { rule: decision.rule, decision: decision.decision, reason: decision.reason } }
|
||||
: {}),
|
||||
...(data ? { data } : {})
|
||||
};
|
||||
store.appendEvent(event);
|
||||
return event;
|
||||
};
|
||||
|
||||
const resolveQuery = (query: QueryBody | SavedQuery | string, params?: Record<string, unknown>): QueryBody => {
|
||||
if (typeof query === "string") {
|
||||
const saved = store.getSchema().queries.find((q) => q.id === query);
|
||||
if (!saved) throw new OntologyNotFoundError(`Unknown saved query ${query}`);
|
||||
return bindParameters(saved, params);
|
||||
}
|
||||
if ("kind" in query && query.kind === "SavedQuery") return bindParameters(query, params);
|
||||
return query as QueryBody;
|
||||
};
|
||||
|
||||
return {
|
||||
store,
|
||||
actor,
|
||||
|
||||
getOntologyManifest: () => store.getManifest(),
|
||||
getOntologySchema: () => store.getSchema(),
|
||||
|
||||
getEntity(id) {
|
||||
requirePolicy({ kind: "read" });
|
||||
const entity = store.getEntity(id);
|
||||
if (!entity) throw new OntologyNotFoundError(`Unknown entity ${id}`);
|
||||
return entity;
|
||||
},
|
||||
|
||||
findEntities(input) {
|
||||
requirePolicy({ kind: "read" });
|
||||
return store.findEntities(input);
|
||||
},
|
||||
|
||||
getClaim(id) {
|
||||
requirePolicy({ kind: "read" });
|
||||
const claim = store.getClaim(id);
|
||||
if (!claim) throw new OntologyNotFoundError(`Unknown claim ${id}`);
|
||||
return claim;
|
||||
},
|
||||
|
||||
claimHistory(id) {
|
||||
requirePolicy({ kind: "read" });
|
||||
return store.claimHistory(id);
|
||||
},
|
||||
|
||||
queryOntology(query, params) {
|
||||
requirePolicy({ kind: "query" });
|
||||
const body = resolveQuery(query, params);
|
||||
const result = evaluateQuery(store.view(), body, options.limits);
|
||||
const id = idFactory("event");
|
||||
results.set(id, result);
|
||||
return { ...result, id };
|
||||
},
|
||||
|
||||
explainOntologyResult(resultId, row = 0) {
|
||||
requirePolicy({ kind: "read" });
|
||||
const result = results.get(resultId);
|
||||
if (!result) throw new OntologyNotFoundError(`Unknown query result ${resultId}`);
|
||||
const target = result.rows[row];
|
||||
if (!target) throw new OntologyNotFoundError(`Result ${resultId} has no row ${row}`);
|
||||
|
||||
const claims: ExplainedClaim[] = target.claims.map((claimId) => {
|
||||
const claim = store.getClaim(claimId);
|
||||
if (!claim) throw new OntologyNotFoundError(`Unknown claim ${claimId}`);
|
||||
return {
|
||||
claim,
|
||||
sources: (claim.sources ?? []).map((s) => store.getSource(s)).filter(Boolean) as Source[],
|
||||
evidence: (claim.evidence ?? []).map((e) => store.getEvidence(e)).filter(Boolean) as Evidence[],
|
||||
history: store.claimHistory(claimId).map((t) => ({
|
||||
status: String(t.status),
|
||||
at: t.at,
|
||||
by: t.by,
|
||||
reason: t.reason
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
resultId,
|
||||
ontology: `${store.getManifest().id}@${store.getManifest().version}`,
|
||||
row,
|
||||
bindings: target.bindings,
|
||||
claims,
|
||||
filters: result.explanation.filters,
|
||||
claimStatus: result.explanation.claimStatus,
|
||||
asOf: result.explanation.asOf
|
||||
};
|
||||
},
|
||||
|
||||
createOntologyChangeSet(input) {
|
||||
const decision = requirePolicy({ kind: "propose" });
|
||||
const changeSet: ChangeSet = {
|
||||
openontology: OPENONTOLOGY_VERSION,
|
||||
kind: "ChangeSet",
|
||||
id: idFactory("changeset"),
|
||||
ontology: `${store.getManifest().id}@${store.getManifest().version}`,
|
||||
title: input.title,
|
||||
rationale: input.rationale,
|
||||
createdAt: clock(),
|
||||
createdBy: actor.id,
|
||||
actorType: actor.type,
|
||||
runId: input.runId,
|
||||
operations: canonicalObject(input.operations),
|
||||
requiredApprovals: input.requiredApprovals ?? (actor.type === "agent" ? 1 : 0),
|
||||
// R92: anything an agent creates starts as a proposal, never applied.
|
||||
status: "proposed",
|
||||
baseRevision: store.revision()
|
||||
};
|
||||
store.putChangeSet(changeSet);
|
||||
emit("changeset.created", changeSet.id, { operations: changeSet.operations.length }, decision, changeSet.id);
|
||||
return changeSet;
|
||||
},
|
||||
|
||||
validateOntologyChangeSet(id) {
|
||||
requirePolicy({ kind: "read" });
|
||||
const changeSet = mustGetChangeSet(store, id);
|
||||
// Validate the package as it would look after the change set applies.
|
||||
const preview = previewPackage(store, changeSet, clock());
|
||||
return validateOntologyPackage(preview);
|
||||
},
|
||||
|
||||
diffOntologyChangeSet(id) {
|
||||
requirePolicy({ kind: "read" });
|
||||
return diffChangeSet(store, mustGetChangeSet(store, id));
|
||||
},
|
||||
|
||||
reviewOntologyChangeSet(id, input) {
|
||||
const decision = requirePolicy({ kind: "review" });
|
||||
const changeSet = mustGetChangeSet(store, id);
|
||||
const review: Review = {
|
||||
openontology: OPENONTOLOGY_VERSION,
|
||||
kind: "Review",
|
||||
id: idFactory("review"),
|
||||
changeSet: changeSet.id,
|
||||
reviewer: actor.id,
|
||||
state: input.state,
|
||||
comment: input.comment,
|
||||
createdAt: clock(),
|
||||
operationDecisions: input.operationDecisions
|
||||
};
|
||||
store.putReview(review);
|
||||
emit("changeset.reviewed", changeSet.id, { state: review.state }, decision, changeSet.id);
|
||||
return review;
|
||||
},
|
||||
|
||||
approveOntologyChangeSet(id, input) {
|
||||
const decision = requirePolicy({ kind: "approve" });
|
||||
const changeSet = mustGetChangeSet(store, id);
|
||||
const approval: Approval = {
|
||||
openontology: OPENONTOLOGY_VERSION,
|
||||
kind: "Approval",
|
||||
id: idFactory("approval"),
|
||||
changeSet: changeSet.id,
|
||||
approver: actor.id,
|
||||
approverType: actor.type,
|
||||
scopes: actor.scopes,
|
||||
createdAt: clock(),
|
||||
comment: input?.comment
|
||||
};
|
||||
store.putApproval(approval);
|
||||
store.putChangeSet({ ...changeSet, status: "approved" });
|
||||
emit("changeset.approved", changeSet.id, undefined, decision, changeSet.id);
|
||||
return approval;
|
||||
},
|
||||
|
||||
rejectOntologyChangeSet(id, comment) {
|
||||
const decision = requirePolicy({ kind: "review" });
|
||||
const changeSet = mustGetChangeSet(store, id);
|
||||
const rejected: ChangeSet = { ...changeSet, status: "rejected" };
|
||||
store.putChangeSet(rejected);
|
||||
store.putReview({
|
||||
openontology: OPENONTOLOGY_VERSION,
|
||||
kind: "Review",
|
||||
id: idFactory("review"),
|
||||
changeSet: changeSet.id,
|
||||
reviewer: actor.id,
|
||||
state: "rejected",
|
||||
comment,
|
||||
createdAt: clock()
|
||||
});
|
||||
emit("changeset.rejected", changeSet.id, { comment }, decision, changeSet.id);
|
||||
return rejected;
|
||||
},
|
||||
|
||||
applyOntologyChangeSet(id, applyOptions) {
|
||||
const changeSet = mustGetChangeSet(store, id);
|
||||
const decision = requirePolicy({ kind: "apply", changeSet });
|
||||
|
||||
if (decision.decision === "require-approval") {
|
||||
const approvals = store.listApprovals(changeSet.id).length;
|
||||
if (approvals < decision.requiredApprovals) {
|
||||
// R107: unattended/--yolo does not reach this branch differently.
|
||||
throw new OntologyApprovalError(
|
||||
`${decision.reason}: ${approvals}/${decision.requiredApprovals} approvals recorded`,
|
||||
decision,
|
||||
approvals
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const rejectedOps = applyOptions?.skipRejectedOperations
|
||||
? store
|
||||
.listReviews(changeSet.id)
|
||||
.flatMap((review) => review.operationDecisions ?? [])
|
||||
.filter((d) => d.decision === "reject")
|
||||
.map((d) => d.index)
|
||||
: [];
|
||||
|
||||
return applyChangeSet(store, changeSet, {
|
||||
actorId: actor.id,
|
||||
actorType: actor.type,
|
||||
now: clock(),
|
||||
nextId: (kind) => idFactory(kind),
|
||||
requestId: options.requestId,
|
||||
client: options.client ?? actor.client,
|
||||
skipOperations: rejectedOps
|
||||
});
|
||||
},
|
||||
|
||||
validateOntologyPackage(validateOptions) {
|
||||
requirePolicy({ kind: "read" });
|
||||
const report = validateOntologyPackage(currentPackage(store), validateOptions);
|
||||
emit("package.validated", store.getManifest().id, {
|
||||
ok: report.ok,
|
||||
errors: report.counts.error,
|
||||
warnings: report.counts.warning
|
||||
});
|
||||
return report;
|
||||
},
|
||||
|
||||
buildOntologyPackage() {
|
||||
requirePolicy({ kind: "read" });
|
||||
return buildOntologyPackage(currentPackage(store));
|
||||
},
|
||||
|
||||
exportOntology(format) {
|
||||
requirePolicy({ kind: "read" });
|
||||
const pkg = currentPackage(store);
|
||||
if (format === "jsonld") {
|
||||
const exported = exportJsonLd(pkg);
|
||||
emit("export.completed", store.getManifest().id, { format, lossy: exported.lossy.length });
|
||||
return { format, document: exported.document, lossy: exported.lossy };
|
||||
}
|
||||
emit("export.completed", store.getManifest().id, { format, lossy: 0 });
|
||||
return { format, document: buildOntologyPackage(pkg), lossy: [] };
|
||||
},
|
||||
|
||||
importOntology(input) {
|
||||
const decision = requirePolicy({ kind: "propose" });
|
||||
const manifest = store.getManifest();
|
||||
const { entities, claims } = importJsonLd(input.document, {
|
||||
...manifest,
|
||||
prefix: packagePrefix(currentPackage(store))
|
||||
});
|
||||
|
||||
// R113: an import proposes; it never silently becomes application state.
|
||||
const operations: ChangeOperation[] = [
|
||||
...entities
|
||||
.filter((entity) => !store.getEntity(entity.id))
|
||||
.map((entity) => ({ op: "add-entity" as const, value: entity as unknown as Record<string, unknown> })),
|
||||
...claims
|
||||
.filter((claim) => !store.getClaim(claim.id))
|
||||
.map((claim) => ({ op: "assert-claim" as const, value: claim as unknown as Record<string, unknown> }))
|
||||
];
|
||||
|
||||
emit("import.completed", manifest.id, { entities: entities.length, claims: claims.length }, decision);
|
||||
return { entities: entities.length, claims: claims.length, operations };
|
||||
},
|
||||
|
||||
subscribeOntologyEvents(listener, filter) {
|
||||
const entry = { fn: listener, types: filter?.type };
|
||||
listeners.add(entry);
|
||||
return () => void listeners.delete(entry);
|
||||
},
|
||||
|
||||
listEvents: (filter) => store.listEvents(filter),
|
||||
view: () => store.view()
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePackage(input: EngineOptions["package"]): LoadedPackage | undefined {
|
||||
if (!input) return undefined;
|
||||
if (typeof input === "string") return loadOntologyPackage(input);
|
||||
if ("manifest" in input && "schema" in input && "data" in input && "files" in input) {
|
||||
return input as LoadedPackage;
|
||||
}
|
||||
return loadOntologyPackage(input as LoadInput);
|
||||
}
|
||||
|
||||
function currentPackage(store: OntologyStore): LoadedPackage {
|
||||
return {
|
||||
manifest: store.getManifest(),
|
||||
schema: store.getSchema(),
|
||||
data: {
|
||||
entities: store.listEntities(),
|
||||
claims: store.listClaims({
|
||||
status: ["asserted", "proposed", "disputed", "retracted", "superseded", "derived"]
|
||||
}),
|
||||
sources: store.listSources(),
|
||||
evidence: store.listEvidence()
|
||||
},
|
||||
files: []
|
||||
};
|
||||
}
|
||||
|
||||
function previewPackage(store: OntologyStore, changeSet: ChangeSet, now: string): LoadedPackage {
|
||||
const pkg = currentPackage(store);
|
||||
const entities = [...pkg.data.entities];
|
||||
const claims = [...pkg.data.claims];
|
||||
let n = 0;
|
||||
|
||||
for (const op of changeSet.operations) {
|
||||
if (op.op === "add-entity") {
|
||||
const input = op.value as unknown as Partial<Entity>;
|
||||
entities.push({
|
||||
...input,
|
||||
openontology: input.openontology ?? OPENONTOLOGY_VERSION,
|
||||
kind: "Entity",
|
||||
id: input.id as string,
|
||||
type: input.type as string,
|
||||
canonicalName: input.canonicalName as string,
|
||||
createdAt: input.createdAt ?? now,
|
||||
createdBy: input.createdBy ?? changeSet.createdBy
|
||||
});
|
||||
}
|
||||
if (op.op === "assert-claim") {
|
||||
const input = op.value as unknown as Partial<Claim>;
|
||||
claims.push({
|
||||
...input,
|
||||
openontology: input.openontology ?? OPENONTOLOGY_VERSION,
|
||||
kind: "Claim",
|
||||
id: input.id ?? `preview:claim:${++n}`,
|
||||
subject: input.subject as string,
|
||||
predicate: input.predicate as string,
|
||||
object: input.object as Claim["object"],
|
||||
status: input.status ?? "asserted",
|
||||
assertedAt: input.assertedAt ?? now,
|
||||
assertedBy: input.assertedBy ?? changeSet.createdBy
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { ...pkg, data: { ...pkg.data, entities, claims } };
|
||||
}
|
||||
|
||||
function mustGetChangeSet(store: OntologyStore, id: string): ChangeSet {
|
||||
const changeSet = store.getChangeSet(id);
|
||||
if (!changeSet) throw new OntologyNotFoundError(`Unknown change set ${id}`);
|
||||
return changeSet;
|
||||
}
|
||||
|
||||
function bindParameters(saved: SavedQuery, params?: Record<string, unknown>): QueryBody {
|
||||
if (!params || Object.keys(params).length === 0) return saved.query;
|
||||
|
||||
const substitute = (value: unknown): unknown => {
|
||||
if (typeof value === "string" && value.startsWith("$")) {
|
||||
const key = value.slice(1);
|
||||
return key in params ? params[key] : value;
|
||||
}
|
||||
if (Array.isArray(value)) return value.map(substitute);
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, substitute(v)]));
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
return substitute(saved.query) as QueryBody;
|
||||
}
|
||||
|
||||
export { renderReport };
|
||||
Loading…
Add table
Add a link
Reference in a new issue