mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-14 06:47:28 +00:00
260 lines
11 KiB
TypeScript
260 lines
11 KiB
TypeScript
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterAll, describe, expect, it } from "vitest";
|
|
import { buildOntologyPackage, loadOntologyPackage, verifyPackageDigest } from "./package.js";
|
|
import { initOntologyPackage } from "./scaffold.js";
|
|
import { renderReport, validateOntologyPackage } from "./validate.js";
|
|
import { OPENONTOLOGY_VERSION } from "./types.js";
|
|
import type { Claim, LoadedPackage } from "./types.js";
|
|
|
|
const NOW = "2026-07-26T00:00:00Z";
|
|
const dirs: string[] = [];
|
|
|
|
function scaffold(): { dir: string; pkg: LoadedPackage } {
|
|
const dir = mkdtempSync(join(tmpdir(), "openontology-"));
|
|
dirs.push(dir);
|
|
initOntologyPackage(dir, { id: "test-ecosystem", now: NOW });
|
|
return { dir, pkg: loadOntologyPackage(dir) };
|
|
}
|
|
|
|
afterAll(() => {
|
|
for (const dir of dirs) rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
/** Clone the scaffold and mutate one claim, to isolate a single failure mode. */
|
|
function withClaims(pkg: LoadedPackage, mutate: (claims: Claim[]) => void): LoadedPackage {
|
|
const copy = structuredClone(pkg);
|
|
mutate(copy.data.claims);
|
|
return copy;
|
|
}
|
|
|
|
describe("init + validate", () => {
|
|
it("scaffolds a package that passes strict validation with no edits (R139)", () => {
|
|
const { pkg } = scaffold();
|
|
const report = validateOntologyPackage(pkg, { strict: true });
|
|
const errors = report.findings.filter((f) => f.severity === "error");
|
|
expect(errors).toEqual([]);
|
|
expect(report.ok).toBe(true);
|
|
expect(report.checked.entities).toBe(8);
|
|
expect(report.checked.claims).toBe(14);
|
|
});
|
|
|
|
it("loads YAML manifest, YAML schema files, and NDJSON data together", () => {
|
|
const { pkg } = scaffold();
|
|
expect(pkg.manifest.kind).toBe("OntologyPackage");
|
|
expect(pkg.schema.entityTypes.map((t) => t.id)).toEqual(["Person", "Organization", "Project"]);
|
|
expect(pkg.data.sources).toHaveLength(2);
|
|
expect(pkg.files.map((f) => f.path)).toContain("data/claims.ndjson");
|
|
});
|
|
|
|
it("loads a package from an explicit manifest path", () => {
|
|
const { dir } = scaffold();
|
|
const pkg = loadOntologyPackage(join(dir, "openontology.yaml"));
|
|
expect(pkg.dir).toBe(dir);
|
|
expect(pkg.manifest.id).toBe("test-ecosystem");
|
|
});
|
|
|
|
it("builds a deterministic digest that survives a rebuild", () => {
|
|
const { pkg } = scaffold();
|
|
const first = buildOntologyPackage(pkg);
|
|
const second = buildOntologyPackage(loadOntologyPackage(structuredClone(pkg)));
|
|
expect(first.digest).toBe(second.digest);
|
|
expect(verifyPackageDigest(first).ok).toBe(true);
|
|
});
|
|
|
|
it("detects a tampered built package", () => {
|
|
const { pkg } = scaffold();
|
|
const built = buildOntologyPackage(pkg);
|
|
built.files[0] = { ...built.files[0], digest: `sha256:${"0".repeat(64)}` };
|
|
expect(verifyPackageDigest(built).ok).toBe(false);
|
|
});
|
|
|
|
it("reports a missing declared file instead of loading a partial package", () => {
|
|
const { dir } = scaffold();
|
|
rmSync(join(dir, "data/claims.ndjson"));
|
|
expect(() => loadOntologyPackage(dir)).toThrow(/data.claims.*missing/);
|
|
});
|
|
});
|
|
|
|
describe("graph validation", () => {
|
|
it("rejects a claim whose subject type is outside the relationship domain", () => {
|
|
const { pkg } = scaffold();
|
|
const broken = withClaims(pkg, (claims) => {
|
|
const claim = claims.find((c) => c.predicate === "worksOn") as Claim;
|
|
claim.subject = "test:org:northwind"; // an Organization cannot worksOn
|
|
});
|
|
const report = validateOntologyPackage(broken);
|
|
expect(report.ok).toBe(false);
|
|
expect(report.findings.map((f) => f.code)).toContain("OO-G-DOMAIN");
|
|
});
|
|
|
|
it("rejects a claim whose object type is outside the relationship range", () => {
|
|
const { pkg } = scaffold();
|
|
const broken = withClaims(pkg, (claims) => {
|
|
const claim = claims.find((c) => c.predicate === "worksOn") as Claim;
|
|
claim.object = { entity: "test:org:northwind" };
|
|
});
|
|
expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-G-RANGE");
|
|
});
|
|
|
|
it("rejects a value object on a relationship predicate", () => {
|
|
const { pkg } = scaffold();
|
|
const broken = withClaims(pkg, (claims) => {
|
|
const claim = claims.find((c) => c.predicate === "worksOn") as Claim;
|
|
claim.object = { value: "ZK Prover" };
|
|
});
|
|
expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-G-OBJECT-KIND");
|
|
});
|
|
|
|
it("rejects a value that does not match its declared datatype", () => {
|
|
const { pkg } = scaffold();
|
|
const broken = withClaims(pkg, (claims) => {
|
|
const claim = claims.find((c) => c.predicate === "homepage") as Claim;
|
|
claim.object = { value: 42 };
|
|
});
|
|
expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-G-DATATYPE");
|
|
});
|
|
|
|
it("rejects dangling entity, source, and claim references", () => {
|
|
const { pkg } = scaffold();
|
|
const broken = withClaims(pkg, (claims) => {
|
|
(claims[0] as Claim).subject = "test:person:nobody";
|
|
(claims[1] as Claim).sources = ["test:source:missing"];
|
|
(claims[2] as Claim).supersedes = "test:claim:9999";
|
|
});
|
|
const codes = validateOntologyPackage(broken).findings.map((f) => f.code);
|
|
expect(codes.filter((c) => c === "OO-G-DANGLING-REF").length).toBeGreaterThanOrEqual(3);
|
|
});
|
|
|
|
it("rejects a claim with no source and no firstParty declaration (R54)", () => {
|
|
const { pkg } = scaffold();
|
|
const broken = withClaims(pkg, (claims) => {
|
|
const claim = claims[0] as Claim;
|
|
delete claim.sources;
|
|
delete claim.firstParty;
|
|
});
|
|
expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-P-NO-SOURCE");
|
|
});
|
|
|
|
it("requires a runId on agent-authored claims (R55)", () => {
|
|
const { pkg } = scaffold();
|
|
const broken = withClaims(pkg, (claims) => {
|
|
(claims[0] as Claim).assertedBy = "agent:research-mapper";
|
|
});
|
|
expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-P-NO-RUN");
|
|
});
|
|
|
|
it("requires derivedFrom on derived claims (R56)", () => {
|
|
const { pkg } = scaffold();
|
|
const broken = withClaims(pkg, (claims) => {
|
|
(claims[0] as Claim).status = "derived";
|
|
});
|
|
expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-P-NO-DERIVATION");
|
|
});
|
|
|
|
it("rejects validTime.to before validTime.from", () => {
|
|
const { pkg } = scaffold();
|
|
const broken = withClaims(pkg, (claims) => {
|
|
(claims[0] as Claim).validTime = { from: "2026-05-01T00:00:00Z", to: "2026-01-01T00:00:00Z" };
|
|
});
|
|
expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-G-TEMPORAL-ORDER");
|
|
});
|
|
|
|
it("catches duplicate ids", () => {
|
|
const { pkg } = scaffold();
|
|
const broken = structuredClone(pkg);
|
|
broken.data.entities.push(structuredClone(broken.data.entities[0]));
|
|
expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-G-DUPLICATE-ID");
|
|
});
|
|
|
|
it("treats unknown predicates as warnings by default and errors in strict mode (R75)", () => {
|
|
const { pkg } = scaffold();
|
|
const broken = withClaims(pkg, (claims) => {
|
|
(claims[0] as Claim).predicate = "notDeclared";
|
|
});
|
|
const lenient = validateOntologyPackage(broken);
|
|
const strict = validateOntologyPackage(broken, { strict: true });
|
|
expect(lenient.findings.find((f) => f.code === "OO-G-UNKNOWN-PREDICATE")?.severity).toBe("warning");
|
|
expect(strict.findings.find((f) => f.code === "OO-G-UNKNOWN-PREDICATE")?.severity).toBe("error");
|
|
expect(lenient.ok).toBe(true);
|
|
expect(strict.ok).toBe(false);
|
|
});
|
|
|
|
it("flags a schema-invalid object with its path", () => {
|
|
const { pkg } = scaffold();
|
|
const broken = structuredClone(pkg);
|
|
delete (broken.data.entities[0] as Partial<{ canonicalName: string }>).canonicalName;
|
|
const report = validateOntologyPackage(broken);
|
|
const finding = report.findings.find((f) => f.code === "OO-S-OBJECT");
|
|
expect(finding).toBeDefined();
|
|
expect(finding?.path).toMatch(/^\/0/);
|
|
});
|
|
});
|
|
|
|
describe("provenance policy", () => {
|
|
it("raises a policy finding for an over-long excerpt", () => {
|
|
const { pkg } = scaffold();
|
|
const withEvidence = structuredClone(pkg);
|
|
withEvidence.data.evidence.push({
|
|
openontology: OPENONTOLOGY_VERSION,
|
|
kind: "Evidence",
|
|
id: "test:evidence:1",
|
|
source: "test:source:team-page",
|
|
selector: { type: "line-range", start: 1, end: 2 },
|
|
excerpt: "x".repeat(600)
|
|
});
|
|
const report = validateOntologyPackage(withEvidence, { maxExcerptLength: 500 });
|
|
const finding = report.findings.find((f) => f.code === "OO-P-EXCERPT-LENGTH");
|
|
expect(finding?.severity).toBe("policy");
|
|
// A policy finding is not an error: the package still validates.
|
|
expect(report.ok).toBe(true);
|
|
});
|
|
|
|
it("rejects public evidence drawn from a private source", () => {
|
|
const { pkg } = scaffold();
|
|
const copy = structuredClone(pkg);
|
|
copy.data.sources[0].visibility = "private";
|
|
copy.data.evidence.push({
|
|
openontology: OPENONTOLOGY_VERSION,
|
|
kind: "Evidence",
|
|
id: "test:evidence:2",
|
|
source: copy.data.sources[0].id,
|
|
selector: { type: "whole-document" },
|
|
visibility: "public"
|
|
});
|
|
expect(validateOntologyPackage(copy).findings.map((f) => f.code)).toContain("OO-P-VISIBILITY");
|
|
});
|
|
});
|
|
|
|
describe("constraints", () => {
|
|
it("reports a violation of a declared required-predicate constraint", () => {
|
|
const { pkg } = scaffold();
|
|
const broken = structuredClone(pkg);
|
|
broken.data.claims = broken.data.claims.filter((c) => c.predicate !== "homepage");
|
|
const report = validateOntologyPackage(broken);
|
|
const violations = report.findings.filter((f) => f.code === "OO-C-REQUIRED-PREDICATE");
|
|
expect(violations).toHaveLength(3);
|
|
expect(violations[0].severity).toBe("warning");
|
|
});
|
|
});
|
|
|
|
describe("report rendering", () => {
|
|
it("renders text, json, yaml, and markdown (R71)", () => {
|
|
const { pkg } = scaffold();
|
|
const report = validateOntologyPackage(pkg);
|
|
expect(renderReport(report, "text")).toContain("OpenOntology package is valid.");
|
|
expect(JSON.parse(renderReport(report, "json")).ok).toBe(true);
|
|
expect(renderReport(report, "yaml")).toContain("ok: true");
|
|
expect(renderReport(report, "markdown")).toContain("# Validation passed");
|
|
});
|
|
|
|
it("gives every finding a stable code and a remediation hint where known", () => {
|
|
const { pkg } = scaffold();
|
|
const broken = withClaims(pkg, (claims) => {
|
|
delete (claims[0] as Claim).sources;
|
|
});
|
|
const finding = validateOntologyPackage(broken).findings.find((f) => f.code === "OO-P-NO-SOURCE");
|
|
expect(finding?.hint).toMatch(/firstParty/);
|
|
});
|
|
});
|