mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-03 13:17: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>
587 lines
20 KiB
TypeScript
587 lines
20 KiB
TypeScript
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 };
|