From 0c7a01c561fe26e8911f6da3f767af827dbbc309 Mon Sep 17 00:00:00 2001 From: Jonathan Vajda <36548200+jonathanvajda@users.noreply.github.com> Date: Mon, 25 May 2026 23:51:30 -0400 Subject: [PATCH 1/7] add coverage and documentation for inconsistent ontologies --- docs/inference-and-inconsistency-coverage.md | 145 +++++ package-lock.json | 2 +- public/app/axiolotl-inconsistency.d.ts | 63 ++ public/app/axiolotl-inconsistency.js | 644 +++++++++++++++++++ public/app/axiolotl-inconsistency.test.js | 137 ++++ 5 files changed, 990 insertions(+), 1 deletion(-) create mode 100644 docs/inference-and-inconsistency-coverage.md create mode 100644 public/app/axiolotl-inconsistency.d.ts create mode 100644 public/app/axiolotl-inconsistency.js create mode 100644 public/app/axiolotl-inconsistency.test.js diff --git a/docs/inference-and-inconsistency-coverage.md b/docs/inference-and-inconsistency-coverage.md new file mode 100644 index 0000000..caf2c04 --- /dev/null +++ b/docs/inference-and-inconsistency-coverage.md @@ -0,0 +1,145 @@ +# Axiolotl Inference and Inconsistency Coverage + +This document describes the current coverage of Axiolotl's browser-side inference and inconsistency tooling. It is intended as a living benchmark map: a future contributor should be able to add a row, add a query, and compare Axiolotl coverage against ELK, Pellet, HermiT, or another OWL reasoner. + +## Scope + +Axiolotl currently provides lightweight materialization and inconsistency checks over RDF/JS stores using JavaScript and SPARQL queries. It is not currently a complete OWL 2 DL reasoner. + +Current implementation sources: + +- `public/app/axiolotl-inference.js`: materialization rules and `applyConstructWithComunica`. +- `public/app/axiolotl-inconsistency.js`: inconsistency query registry, violation CONSTRUCTs, hydration queries, and coverage metadata. + +Reasoner comparison columns are profile-level guidance, not benchmark results. + +- ELK is treated as OWL 2 EL-oriented coverage. +- Pellet is treated as OWL-DL / SROIQ-style tableau reasoner coverage. +- HermiT is treated as OWL 2 DL direct-semantics coverage. + +## Legend + +| Value | Meaning | +| --- | --- | +| Yes | Covered directly enough to rely on for that row's stated pattern. | +| Partial | Some cases are covered, but important semantic cases are missing. | +| Heuristic | Useful signal, but not a sound/complete inconsistency proof under OWL open-world semantics. | +| No | Not currently covered. | +| Profile no | Outside the relevant OWL profile, even if another reasoner may support it. | +| N/A | Not applicable to that tool or row. | + +## Current Materialization Coverage + +These rules are implemented in `axiolotl-inference.js`. + +| Pattern | Axiolotl materializes | Axiolotl approach | ELK | Pellet | HermiT | Notes | +| --- | --- | --- | --- | --- | --- | --- | +| `rdfs:subClassOf+` type propagation | Yes | JS transitive closure over subclass edges, plus SPARQL rule fallback still present | Yes | Yes | Yes | `x rdf:type A`, `A rdfs:subClassOf+ B` yields `x rdf:type B`. Blank-node class expressions are not interpreted as DL restrictions here. | +| `rdfs:subPropertyOf+` property propagation | Yes | JS transitive closure over subproperty edges, plus SPARQL rule fallback still present | Yes | Yes | Yes | `x p y`, `p rdfs:subPropertyOf+ q` yields `x q y`. | +| `owl:inverseOf` | Yes | Bidirectional map in JS and SPARQL CONSTRUCT rule | Profile no | Yes | Yes | OWL 2 EL does not support inverse object property expressions. Axiolotl supports direct named inverse property assertions. | +| `owl:SymmetricProperty` | Yes | JS expansion and SPARQL CONSTRUCT rule | Profile no / limited | Yes | Yes | Axiolotl materializes `y p x` from `x p y`. | +| `owl:TransitiveProperty` | Yes | JS queue expansion and SPARQL CONSTRUCT rule | Yes | Yes | Yes | Axiolotl computes new edges from existing edges in the same graph context. | +| `rdfs:domain` | Yes | JS map and SPARQL CONSTRUCT rule | Yes | Yes | Yes | `x p y`, `p rdfs:domain C` yields `x rdf:type C`. | +| `rdfs:range` | Yes | JS map and SPARQL CONSTRUCT rule | Yes | Yes | Yes | Only materializes object types when the object can be a subject. | +| `owl:equivalentClass` | No | Not currently expanded | Yes, for EL class expressions | Yes | Yes | Could be added by reducing named equivalence to two subclass edges, then relying on subclass closure. | +| `owl:equivalentProperty` | No | Not currently expanded | Yes, for EL property expressions | Yes | Yes | Could be added by reducing to two subproperty edges. | +| `owl:sameAs` equality closure | No | Not currently expanded | Partial | Yes | Yes | Current violation checks can consume explicit `owl:sameAs`, but do not compute equality closure. | +| `owl:differentFrom` closure | No | Not currently expanded | Partial | Yes | Yes | Current functional-property checks consume explicit `owl:differentFrom`. | +| `owl:someValuesFrom` existential witness creation | Partial | New hydration query can create shallow synthetic witnesses | Yes, in OWL 2 EL | Yes | Yes | Axiolotl hydration is opt-in and heuristic; normal inference does not materialize witnesses. | +| `owl:intersectionOf` class-expression reasoning | Partial | Hydration supports shallow intersection parts inside someValuesFrom fillers | Yes, in OWL 2 EL | Yes | Yes | No general recursive class-expression normalization yet. | +| `owl:allValuesFrom` / "only values from" | No for materialization | Not currently materialized | Profile no | Yes | Yes | OWL RDF uses `owl:allValuesFrom`; "OnlyValuesFrom" is common Manchester-style wording. | +| Cardinality restrictions | No | Not currently materialized | Profile no, except limited functional data property support in EL profile syntax | Yes | Yes | Current checks only detect simple conflicts from explicit data/object values. | +| Property chains | No | Not currently materialized | Yes, with OWL 2 EL restrictions | Yes | Yes | Axiolotl has transitive properties but no general chain expansion. | +| Datatype reasoning | No | Literal inequality uses SPARQL `!=`; no datatype satisfiability reasoning | Limited profile datatypes | Yes | Yes | Axiolotl does not normalize lexical forms or detect datatype-range contradictions. | + +## Current Axiom Violation Detection + +These checks are registered in `axiolotl-inconsistency.js`. Each query can be rendered as `SELECT`, `ASK`, or a violation-reporting `CONSTRUCT`. + +| Violation pattern | Axiolotl detects | Query id | ELK | Pellet | HermiT | Notes | +| --- | --- | --- | --- | --- | --- | --- | +| Individual typed as both sides of `owl:disjointWith` | Yes | `disjointWithTypeOverlap` | Yes | Yes | Yes | Depends on available `rdf:type` assertions. Run materialization first for subclass/domain/range-derived types. | +| Individual typed as two members of `owl:AllDisjointClasses` | Yes | `allDisjointClassesTypeOverlap` | Yes | Yes | Yes | Expands RDF lists with `rdf:rest*/rdf:first`. | +| Individual typed as class and complement class | Yes | `complementOfTypeOverlap` | Profile no | Yes | Yes | Detects explicit type overlap only; no general complement satisfiability reasoning. | +| Functional property with two unequal literal values | Yes | `datatypeFunctionalPropertyConflict` | Partial | Yes | Yes | Uses SPARQL literal inequality. Does not perform full datatype canonicalization. | +| Functional object property with values known `owl:differentFrom` | Yes | `objectFunctionalPropertyDifferentFromConflict` | Partial | Yes | Yes | Requires explicit inequality; does not infer inequality or same/different closure. | +| Asserted triple contradicts `owl:NegativePropertyAssertion` | Yes | `negativePropertyAssertionConflict` | Profile no / limited | Yes | Yes | Direct ABox contradiction check. | +| Members of `owl:AllDifferent` also related by `owl:sameAs` | Yes | `allDifferentSameAsConflict` | Partial | Yes | Yes | Requires explicit `owl:sameAs`; no equality closure yet. | +| Same subject/object pair uses two `owl:AllDisjointProperties` members | Yes | `allDisjointPropertiesSharedPair` | Partial / profile-dependent | Yes | Yes | Direct property-disjointness conflict. | +| Instance of `owl:disjointUnionOf` class lacks any member type | Heuristic | `disjointUnionMissingMemberHeuristic` | Profile no | Yes | Yes | Under open-world semantics, missing a known member type is not by itself an inconsistency. This is a modeling smell check. | +| Value of `owl:allValuesFrom` restriction lacks known filler type | Heuristic | `allValuesFromMissingTypeHeuristic` | Profile no | Yes | Yes | Missing a type is not a contradiction in OWL. A stricter check needs proof that the value cannot be in the filler class. | +| Single-property `owl:hasKey` collision plus `owl:differentFrom` | Partial | `singlePropertyHasKeyDifferentFromConflict` | Yes, profile allows keys | Yes | Yes | Single-property key only. Multi-property key joins are not implemented. | + +## Complex Axiom Pattern Coverage + +This table is the main roadmap for expanding beyond low-hanging ABox conflicts. + +| Complex pattern | Axiolotl current support | ELK | Pellet | HermiT | What is missing in Axiolotl | +| --- | --- | --- | --- | --- | --- | +| Named subclass chain: `A subClassOf B subClassOf C` | Yes | Yes | Yes | Yes | Already covered. | +| Named class instance plus named superclass | Yes | Yes | Yes | Yes | Already covered. | +| `A subClassOf [ owl:onProperty p ; owl:someValuesFrom F ]` | Partial hydration | Yes | Yes | Yes | Hydration can create a synthetic `p` witness typed as `F`; normal inference does not derive it automatically. | +| Nested existential in intersection filler | Partial hydration | Yes, if in OWL 2 EL grammar | Yes | Yes | Current hydration handles one nested pattern: filler intersection part that is itself a `someValuesFrom` restriction. Needs recursive traversal and cycle protection. | +| Arbitrary-depth nested `owl:Restriction` | No | Partial, only EL-supported constructs | Yes | Yes | Need recursive class-expression walker and bounded/unbounded strategy for witness generation. | +| `owl:intersectionOf` on named class equivalence | Partial | Yes | Yes | Yes | Current support is mostly inside hydration witness fillers. No full normalization of `EquivalentClasses(A ObjectIntersectionOf(...))`. | +| `owl:unionOf` | No | Profile no | Yes | Yes | Need branching semantics. Materializing all branches would be unsound; inconsistency checks require DL reasoning or careful query patterns. | +| `owl:complementOf` satisfiability | Partial direct conflict | Profile no | Yes | Yes | Only explicit type overlap is checked. No proof that a class is unsatisfiable because it is subclass of its complement or disjoint constraints. | +| `owl:allValuesFrom` / only-values restriction | Heuristic only | Profile no | Yes | Yes | Need sound check: `x type R`, `x p y`, and `y` proven disjoint with or impossible to be `C`, not merely missing type `C`. | +| `owl:hasValue` | No | Yes | Yes | Yes | Could add materialization from `x type restriction` to `x p value`, and violation checks involving disjoint property/class consequences. | +| `owl:minCardinality`, `owl:maxCardinality`, exact cardinality | No | Profile no | Yes | Yes | Need counting plus equality semantics. SPARQL can catch explicit max-cardinality conflicts when fillers are known different. | +| Qualified cardinality restrictions | No | Profile no | Yes | Yes | Similar to cardinality, but scoped to filler class membership and disjointness. | +| `owl:FunctionalProperty` as max-cardinality-one | Partial violation detection | Partial | Yes | Yes | Axiolotl detects obvious literal conflicts and explicit `differentFrom` object conflicts, but does not derive `sameAs`. | +| `owl:InverseFunctionalProperty` | No | Profile no | Yes | Yes | Could detect two subjects with same object and explicit `differentFrom`; could materialize `sameAs` heuristically only with care. | +| `owl:IrreflexiveProperty` | No | Profile no | Yes | Yes | Easy direct check: `p a owl:IrreflexiveProperty` and `x p x`. | +| `owl:AsymmetricProperty` | No | Profile no | Yes | Yes | Easy direct check: `x p y` and `y p x`. | +| `owl:propertyDisjointWith` | No | Profile-dependent | Yes | Yes | Existing `AllDisjointProperties` check can be adapted to binary disjoint properties. | +| Property chains | No | Yes, with EL restrictions | Yes | Yes | Need chain-list expansion and materialization rule generation. | +| Negative class assertions | No | Profile-dependent | Yes | Yes | Need RDF mapping support for negative class assertion patterns, where present. | +| Datatype range contradictions | No | Limited | Yes | Yes | Need datatype parser/canonicalizer or external reasoner integration. | +| OWL 2 DL global restrictions / profile validation | No | N/A | Partial operationally | Partial operationally | Need a profile validator or OWL API/ROBOT-style external validation path. | + +## Hydration Coverage + +Hydration is not proof by itself. It is a strategy for creating synthetic individuals that exercise class axioms so subsequent materialization and violation checks can expose latent contradictions. + +| Hydration query | Current behavior | Intended use | Limitation | +| --- | --- | --- | --- | +| `namedClassInstances` | Creates one synthetic instance for each named `owl:Class`, excluding `owl:Thing` and `owl:Nothing`. | Seed a model with representative class instances. | Does not satisfy restrictions or complex superclass expressions by itself. | +| `subclassAndExistentialWitnesses` | Creates a synthetic instance for each named class, adds named superclass types, creates shallow `someValuesFrom` witnesses, and handles one nested existential inside an intersection filler. | Exercise OWL 2 EL-style existential patterns like CCO/BFO subclass restrictions. | Not recursive, no `allValuesFrom`, no union/complement/cardinality semantics, and deterministic witness IRIs may merge across runs unless run IRIs are managed. | + +## Suggested Expansion Backlog + +| Priority | Addition | Why it helps | Likely implementation path | +| --- | --- | --- | --- | +| 1 | Binary `owl:propertyDisjointWith` violation query | Complements existing `AllDisjointProperties` support | Add SPARQL SELECT/ASK/CONSTRUCT pattern. | +| 1 | `owl:IrreflexiveProperty` and `owl:AsymmetricProperty` checks | Easy direct ABox contradictions | Add two query definitions. | +| 1 | `owl:equivalentClass` and `owl:equivalentProperty` materialization | Common ontology pattern | Reduce named equivalence to subclass/subproperty pairs or add closure maps. | +| 2 | Multi-property `owl:hasKey` conflicts | Current key coverage is intentionally narrow | Generate joins for RDF key lists, initially bounded by key length. | +| 2 | Sounder `allValuesFrom` contradiction query | Replaces missing-type heuristic with real inconsistency evidence | Look for `y rdf:type D` where `D` is disjoint with `C`, or `y` is typed as complement of `C`. | +| 2 | Recursive EL hydration | Better implicit inconsistency exposure for EL ontologies | Build a bounded recursive class-expression expander for `intersectionOf` and `someValuesFrom`. | +| 3 | Property-chain materialization | Important OWL 2 EL/RL feature | Expand RDF lists in `owl:propertyChainAxiom` and construct chain consequences. | +| 3 | Datatype contradiction checks | Needed for data-heavy KGs | Add datatype normalization or delegate to a reasoner/library. | +| 3 | External reasoner benchmark harness | Enables Axiolotl vs ELK/Pellet/HermiT comparison | Define fixtures, expected entailments, expected inconsistencies, and timing metrics. | + +## Benchmark Notes + +For fair benchmark comparisons: + +1. Separate materialization benchmarks from consistency-checking benchmarks. +2. Record graph scope: default graph, named graph, or default-plus-named union. +3. Run Axiolotl in phases: base load, optional hydration, materialization, violation detection. +4. Track both runtime and coverage: + - triples materialized, + - violation rows found, + - false positives from heuristics, + - expected contradictions missed, + - memory usage when available. +5. Keep open-world semantics visible. A missing type is not the same as a contradiction. + +## Sources for Reasoner/Profile Columns + +- W3C OWL 2 Profiles: OWL 2 EL supports `ObjectIntersectionOf` and `ObjectSomeValuesFrom`, disallows inverse object property expressions, and disallows `DisjointUnion` in the EL profile. See https://www.w3.org/TR/owl2-profiles/ +- ELK project page: ELK's stated goal is support for the OWL 2 EL profile. See https://liveontologies.github.io/elk-reasoner/ +- HermiT system description: HermiT is described as fully compliant with OWL 2 Direct Semantics and supports standard OWL 2 reasoning tasks including entailment, class classification, and property classification. See https://ora.ox.ac.uk/objects/uuid:e719ebc8-ff8b-4efa-a14f-7da8478ff0ed +- Pellet system description: Pellet is described as a complete OWL-DL reasoner with consistency checking, individual reasoning, datatype support, and extensions toward OWL 1.1/SROIQ-era features. See https://doi.org/10.1016/j.websem.2007.03.004 + diff --git a/package-lock.json b/package-lock.json index c4bde8a..afdd315 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "ontologies", + "name": "axiolotl", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/public/app/axiolotl-inconsistency.d.ts b/public/app/axiolotl-inconsistency.d.ts new file mode 100644 index 0000000..375f9e0 --- /dev/null +++ b/public/app/axiolotl-inconsistency.d.ts @@ -0,0 +1,63 @@ +export type AxiolotlGraphScope = 'default' | 'named' | 'union'; +export type AxiolotlResultForm = 'select' | 'ask' | 'construct'; + +export interface AxiolotlQueryOptions { + scope?: AxiolotlGraphScope; + graphIri?: string | null; + resultForm?: AxiolotlResultForm; +} + +export interface AxiolotlRuntimeOptions extends AxiolotlQueryOptions { + engine?: unknown; + baseIRI?: string; +} + +export interface AxiolotlConstructOptions extends AxiolotlQueryOptions { + applyConstruct?: (constructQuery: string, rdfjsStore: unknown) => Promise; +} + +export interface AxiolotlHydrationOptions extends AxiolotlConstructOptions { + runIri?: string; + deterministicIris?: boolean; +} + +export interface AxiolotlCoverageItem { + id: string; + label: string; + family: string; + owlProfile: string; + support: string; + notes: string; +} + +export interface AxiolotlQueryDefinition { + id: string; + label: string; + description: string; + family: string; + supported: boolean; + select: (options?: AxiolotlQueryOptions) => string; + ask: (options?: AxiolotlQueryOptions) => string; + construct: (options?: AxiolotlQueryOptions) => string; +} + +export interface AxiolotlHydrationDefinition { + id: string; + label: string; + description: string; +} + +export function normalizeQueryOptions(options?: AxiolotlQueryOptions): Required; +export function scopedWhere(body: string, options?: AxiolotlQueryOptions): string; +export function hasInconsistencyQuery(id: string): boolean; +export function listInconsistencyQueries(): AxiolotlQueryDefinition[]; +export function getInconsistencyQuery(id: string, options?: AxiolotlQueryOptions): string; +export function getAllInconsistencySelectQueries(options?: AxiolotlQueryOptions): Array<{ id: string; label: string; query: string }>; +export function runInconsistencySelect(id: string, rdfjsStore: unknown, options?: AxiolotlRuntimeOptions): Promise<{ id: string; rows: Array> }>; +export function runAllInconsistencySelects(rdfjsStore: unknown, options?: AxiolotlRuntimeOptions): Promise> }>>; +export function constructInconsistencyReport(id: string, rdfjsStore: unknown, options?: AxiolotlConstructOptions): Promise; +export function listInconsistencyCoverage(): AxiolotlCoverageItem[]; +export function listHydrationQueries(): AxiolotlHydrationDefinition[]; +export function getHydrationConstructQuery(id: string, options?: AxiolotlHydrationOptions): string; +export function applyHydrationConstruct(id: string, rdfjsStore: unknown, options?: AxiolotlHydrationOptions): Promise; + diff --git a/public/app/axiolotl-inconsistency.js b/public/app/axiolotl-inconsistency.js new file mode 100644 index 0000000..0bc5460 --- /dev/null +++ b/public/app/axiolotl-inconsistency.js @@ -0,0 +1,644 @@ +// axiolotl-inconsistency.js +// Additive helpers for ontology inconsistency checks and hydration queries. + +const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; +const RDFS = 'http://www.w3.org/2000/01/rdf-schema#'; +const OWL = 'http://www.w3.org/2002/07/owl#'; +const AXI = 'http://example.org/axiolotl/inconsistency#'; +const HYD = 'http://example.org/hydration/'; + +const COMMON_PREFIXES = ` +PREFIX rdf: <${RDF}> +PREFIX rdfs: <${RDFS}> +PREFIX owl: <${OWL}> +PREFIX xsd: +PREFIX axi: <${AXI}> +PREFIX hyd: <${HYD}> +`; + +const DEFAULT_QUERY_OPTIONS = Object.freeze({ + scope: 'union', + graphIri: null, + resultForm: 'select', +}); + +/** + * @typedef {'default'|'named'|'union'} AxiolotlGraphScope + * @typedef {'select'|'ask'|'construct'} AxiolotlResultForm + * @typedef {{ scope?: AxiolotlGraphScope, graphIri?: string|null, resultForm?: AxiolotlResultForm }} AxiolotlQueryOptions + * @typedef {{ id: string, label: string, family: string, owlProfile: string, support: string, notes: string }} AxiolotlCoverageItem + * @typedef {{ id: string, label: string, description: string, family: string, supported: boolean, select: Function, construct?: Function, ask?: Function }} AxiolotlQueryDefinition + */ + +/** + * Return a normalized options object for inconsistency and hydration queries. + * @param {AxiolotlQueryOptions} [options] + * @returns {Required} + */ +export function normalizeQueryOptions(options = {}) { + const scope = options.scope || DEFAULT_QUERY_OPTIONS.scope; + if (!['default', 'named', 'union'].includes(scope)) { + throw new Error(`Unsupported graph scope: ${scope}`); + } + + const resultForm = options.resultForm || DEFAULT_QUERY_OPTIONS.resultForm; + if (!['select', 'ask', 'construct'].includes(resultForm)) { + throw new Error(`Unsupported result form: ${resultForm}`); + } + + if (scope === 'named' && !options.graphIri) { + throw new Error('Named graph scope requires graphIri.'); + } + + return { + scope, + graphIri: options.graphIri || null, + resultForm, + }; +} + +/** + * Wrap a SPARQL group so it evaluates against the requested graph scope. + * @param {string} body + * @param {AxiolotlQueryOptions} [options] + * @returns {string} + */ +export function scopedWhere(body, options = {}) { + const normalized = normalizeQueryOptions(options); + const trimmed = body.trim(); + + if (normalized.scope === 'default') return trimmed; + if (normalized.scope === 'named') return `GRAPH <${escapeIri(normalized.graphIri)}> {\n${indent(trimmed)}\n}`; + + return `{\n${indent(trimmed)}\n}\nUNION\n{\n GRAPH ?axiolotlGraph {\n${indent(trimmed, 4)}\n }\n}`; +} + +/** + * Return true when a value is a registered inconsistency query id. + * @param {string} id + * @returns {boolean} + */ +export function hasInconsistencyQuery(id) { + return Object.prototype.hasOwnProperty.call(INCONSISTENCY_QUERIES, id); +} + +/** + * List the registered inconsistency query definitions. + * @returns {AxiolotlQueryDefinition[]} + */ +export function listInconsistencyQueries() { + return Object.values(INCONSISTENCY_QUERIES); +} + +/** + * Build a SPARQL query for a registered inconsistency check. + * @param {string} id + * @param {AxiolotlQueryOptions} [options] + * @returns {string} + */ +export function getInconsistencyQuery(id, options = {}) { + const definition = INCONSISTENCY_QUERIES[id]; + if (!definition) throw new Error(`Unknown inconsistency query: ${id}`); + + const normalized = normalizeQueryOptions(options); + const builder = definition[normalized.resultForm]; + if (typeof builder !== 'function') { + throw new Error(`Query ${id} does not support ${normalized.resultForm}.`); + } + + return compactSparql(`${COMMON_PREFIXES}\n${builder(normalized)}`); +} + +/** + * Build every SELECT inconsistency query for the requested graph scope. + * @param {AxiolotlQueryOptions} [options] + * @returns {{ id: string, label: string, query: string }[]} + */ +export function getAllInconsistencySelectQueries(options = {}) { + return listInconsistencyQueries() + .filter(query => query.supported) + .map(query => ({ + id: query.id, + label: query.label, + query: getInconsistencyQuery(query.id, { ...options, resultForm: 'select' }), + })); +} + +/** + * Run a SELECT inconsistency query with a Comunica engine. + * @param {string} id + * @param {any} rdfjsStore + * @param {{ engine?: any, baseIRI?: string } & AxiolotlQueryOptions} [options] + * @returns {Promise<{ id: string, rows: Record[] }>} + */ +export async function runInconsistencySelect(id, rdfjsStore, options = {}) { + const queryEngine = options.engine || globalThis.engine; + if (!queryEngine || typeof queryEngine.queryBindings !== 'function') { + throw new Error('runInconsistencySelect requires a Comunica engine with queryBindings.'); + } + + const query = getInconsistencyQuery(id, { ...options, resultForm: 'select' }); + console.info(`[axiolotl-inconsistency] Running ${id}`); + + try { + const bindingsStream = await queryEngine.queryBindings(query, { + sources: [{ type: 'rdfjsSource', value: rdfjsStore }], + baseIRI: options.baseIRI || 'http://example.org/', + }); + const rows = await collectBindings(bindingsStream); + console.info(`[axiolotl-inconsistency] ${id} returned ${rows.length} row(s).`); + return { id, rows }; + } catch (error) { + console.error(`[axiolotl-inconsistency] ${id} failed`, error); + throw error; + } +} + +/** + * Run every supported SELECT inconsistency query. + * @param {any} rdfjsStore + * @param {{ engine?: any, baseIRI?: string } & AxiolotlQueryOptions} [options] + * @returns {Promise<{ id: string, rows: Record[] }[]>} + */ +export async function runAllInconsistencySelects(rdfjsStore, options = {}) { + const results = []; + for (const query of listInconsistencyQueries().filter(item => item.supported)) { + results.push(await runInconsistencySelect(query.id, rdfjsStore, options)); + } + return results; +} + +/** + * Apply a registered CONSTRUCT query with axiolotl-inference.js' applyConstructWithComunica. + * @param {string} id + * @param {any} rdfjsStore + * @param {{ applyConstruct?: Function } & AxiolotlQueryOptions} [options] + * @returns {Promise} + */ +export async function constructInconsistencyReport(id, rdfjsStore, options = {}) { + const applyConstruct = options.applyConstruct || globalThis.applyConstructWithComunica; + if (typeof applyConstruct !== 'function') { + throw new Error('constructInconsistencyReport requires applyConstructWithComunica or options.applyConstruct.'); + } + + const query = getInconsistencyQuery(id, { ...options, resultForm: 'construct' }); + return await applyConstruct(query, rdfjsStore); +} + +/** + * List documented support boundaries for the first inconsistency pass. + * @returns {AxiolotlCoverageItem[]} + */ +export function listInconsistencyCoverage() { + return COVERAGE.map(item => ({ ...item })); +} + +/** + * Return hydration query ids and descriptions. + * @returns {{ id: string, label: string, description: string }[]} + */ +export function listHydrationQueries() { + return Object.values(HYDRATION_QUERIES).map(({ id, label, description }) => ({ id, label, description })); +} + +/** + * Build a registered ontology hydration CONSTRUCT query. + * @param {string} id + * @param {{ runIri?: string, deterministicIris?: boolean } & AxiolotlQueryOptions} [options] + * @returns {string} + */ +export function getHydrationConstructQuery(id, options = {}) { + const definition = HYDRATION_QUERIES[id]; + if (!definition) throw new Error(`Unknown hydration query: ${id}`); + const normalized = normalizeQueryOptions({ ...options, resultForm: 'construct' }); + return compactSparql(`${COMMON_PREFIXES}\n${definition.construct({ ...normalized, ...options })}`); +} + +/** + * Apply a hydration CONSTRUCT query with axiolotl-inference.js' applyConstructWithComunica. + * @param {string} id + * @param {any} rdfjsStore + * @param {{ applyConstruct?: Function, runIri?: string, deterministicIris?: boolean } & AxiolotlQueryOptions} [options] + * @returns {Promise} + */ +export async function applyHydrationConstruct(id, rdfjsStore, options = {}) { + const applyConstruct = options.applyConstruct || globalThis.applyConstructWithComunica; + if (typeof applyConstruct !== 'function') { + throw new Error('applyHydrationConstruct requires applyConstructWithComunica or options.applyConstruct.'); + } + + const query = getHydrationConstructQuery(id, options); + return await applyConstruct(query, rdfjsStore); +} + +const INCONSISTENCY_QUERIES = Object.freeze({ + disjointWithTypeOverlap: makeQueryDefinition({ + id: 'disjointWithTypeOverlap', + label: 'owl:disjointWith type overlap', + family: 'class-disjointness', + description: 'Find individuals typed as both sides of an owl:disjointWith pair.', + variables: ['x', 'A', 'B'], + where: options => ` + ${scopedWhere('?A owl:disjointWith ?B .\n?x rdf:type ?A, ?B .', options)} + `, + }), + + allDisjointClassesTypeOverlap: makeQueryDefinition({ + id: 'allDisjointClassesTypeOverlap', + label: 'owl:AllDisjointClasses type overlap', + family: 'class-disjointness', + description: 'Find individuals typed as multiple members of an owl:AllDisjointClasses list.', + variables: ['x', 'set', 'A', 'B'], + where: options => ` + ${scopedWhere(` + ?set rdf:type owl:AllDisjointClasses . + ?set owl:members/rdf:rest*/rdf:first ?A . + ?set owl:members/rdf:rest*/rdf:first ?B . + ?x rdf:type ?A, ?B . + `, options)} + FILTER(?A != ?B) + `, + }), + + complementOfTypeOverlap: makeQueryDefinition({ + id: 'complementOfTypeOverlap', + label: 'owl:complementOf type overlap', + family: 'class-complement', + description: 'Find individuals typed as a class and its complement.', + variables: ['x', 'A', 'B'], + where: options => ` + { + ${scopedWhere('?A owl:complementOf ?B .\n?x rdf:type ?A, ?B .', options)} + } + UNION + { + ${scopedWhere('?B owl:complementOf ?A .\n?x rdf:type ?A, ?B .', options)} + } + `, + }), + + datatypeFunctionalPropertyConflict: makeQueryDefinition({ + id: 'datatypeFunctionalPropertyConflict', + label: 'Datatype functional property conflict', + family: 'property-cardinality', + description: 'Find literal values that violate an owl:FunctionalProperty assertion.', + variables: ['x', 'p', 'v1', 'v2'], + where: options => ` + ${scopedWhere(` + ?p rdf:type owl:FunctionalProperty . + ?x ?p ?v1 . + ?x ?p ?v2 . + `, options)} + FILTER(isLiteral(?v1) && isLiteral(?v2) && ?v1 != ?v2) + `, + }), + + objectFunctionalPropertyDifferentFromConflict: makeQueryDefinition({ + id: 'objectFunctionalPropertyDifferentFromConflict', + label: 'Object functional property differentFrom conflict', + family: 'property-cardinality', + description: 'Find object values that violate a functional property via owl:differentFrom.', + variables: ['x', 'p', 'y1', 'y2'], + where: options => ` + ${scopedWhere(` + ?p rdf:type owl:FunctionalProperty . + ?x ?p ?y1 . + ?x ?p ?y2 . + ?y1 owl:differentFrom ?y2 . + `, options)} + `, + }), + + negativePropertyAssertionConflict: makeQueryDefinition({ + id: 'negativePropertyAssertionConflict', + label: 'Negative property assertion conflict', + family: 'negative-assertion', + description: 'Find asserted triples that contradict owl:NegativePropertyAssertion nodes.', + variables: ['x', 'p', 'y', 'npa'], + where: options => ` + ${scopedWhere(` + ?x ?p ?y . + ?npa rdf:type owl:NegativePropertyAssertion ; + owl:sourceIndividual ?x ; + owl:assertionProperty ?p ; + owl:targetIndividual ?y . + `, options)} + `, + }), + + allDifferentSameAsConflict: makeQueryDefinition({ + id: 'allDifferentSameAsConflict', + label: 'owl:AllDifferent sameAs conflict', + family: 'individual-identity', + description: 'Find members of owl:AllDifferent that are also linked with owl:sameAs.', + variables: ['set', 'a', 'b'], + where: options => ` + ${scopedWhere(` + ?set rdf:type owl:AllDifferent ; + owl:distinctMembers/rdf:rest*/rdf:first ?a ; + owl:distinctMembers/rdf:rest*/rdf:first ?b . + ?a owl:sameAs ?b . + `, options)} + FILTER(?a != ?b) + `, + }), + + allDisjointPropertiesSharedPair: makeQueryDefinition({ + id: 'allDisjointPropertiesSharedPair', + label: 'owl:AllDisjointProperties shared pair', + family: 'property-disjointness', + description: 'Find subject/object pairs connected by two members of an owl:AllDisjointProperties list.', + variables: ['set', 'p1', 'p2', 's', 'o'], + where: options => ` + ${scopedWhere(` + ?set rdf:type owl:AllDisjointProperties ; + owl:members/rdf:rest*/rdf:first ?p1 ; + owl:members/rdf:rest*/rdf:first ?p2 . + ?s ?p1 ?o . + ?s ?p2 ?o . + `, options)} + FILTER(?p1 != ?p2) + `, + }), + + disjointUnionMissingMemberHeuristic: makeQueryDefinition({ + id: 'disjointUnionMissingMemberHeuristic', + label: 'owl:disjointUnionOf missing member heuristic', + family: 'class-disjointness', + description: 'Heuristic: find instances of a disjoint union class lacking any known union member type.', + variables: ['x', 'A', 'L'], + where: options => ` + ${scopedWhere('?A owl:disjointUnionOf ?L .\n?x rdf:type ?A .', options)} + FILTER NOT EXISTS { + ${scopedWhere('?L rdf:rest*/rdf:first ?Bi .\n?x rdf:type ?Bi .', options)} + } + `, + }), + + allValuesFromMissingTypeHeuristic: makeQueryDefinition({ + id: 'allValuesFromMissingTypeHeuristic', + label: 'owl:allValuesFrom missing type heuristic', + family: 'restriction-heuristic', + description: 'Heuristic: find values of an allValuesFrom restriction with no known filler type.', + variables: ['x', 'p', 'y', 'C', 'R'], + where: options => ` + ${scopedWhere(` + ?R rdf:type owl:Restriction ; + owl:onProperty ?p ; + owl:allValuesFrom ?C . + `, options)} + { + ${scopedWhere('?A rdfs:subClassOf ?R .\n?x rdf:type ?A .', options)} + } + UNION + { + ${scopedWhere('?x rdf:type ?R .', options)} + } + ${scopedWhere('?x ?p ?y .', options)} + FILTER NOT EXISTS { + ${scopedWhere('?y rdf:type ?C .', options)} + } + `, + }), + + singlePropertyHasKeyDifferentFromConflict: makeQueryDefinition({ + id: 'singlePropertyHasKeyDifferentFromConflict', + label: 'Single-property owl:hasKey differentFrom conflict', + family: 'key-identity', + description: 'Find same-key individuals that are explicitly owl:differentFrom. Single-property keys only.', + variables: ['A', 'x1', 'x2', 'k', 'v'], + where: options => ` + ${scopedWhere('?A owl:hasKey ?keyList .\n?keyList rdf:first ?k .\n?keyList rdf:rest rdf:nil .', options)} + ${scopedWhere('?x1 rdf:type ?A .\n?x2 rdf:type ?A .\n?x1 ?k ?v .\n?x2 ?k ?v .\n?x1 owl:differentFrom ?x2 .', options)} + FILTER(?x1 != ?x2) + `, + }), +}); + +const HYDRATION_QUERIES = Object.freeze({ + namedClassInstances: { + id: 'namedClassInstances', + label: 'Hydrate named class instances', + description: 'Create one synthetic instance for each named owl:Class.', + construct: options => { + const nodeExpr = hydrationNodeExpression('?A', options); + const runExpr = hydrationRunExpression(options); + return ` + CONSTRUCT { + ?x rdf:type ?A ; + hyd:hydratedFromClass ?A ; + hyd:hydrationRun ?run . + } + WHERE { + ${scopedWhere(` + ?A rdf:type owl:Class . + FILTER(isIRI(?A)) + FILTER(?A != owl:Thing && ?A != owl:Nothing) + `, options)} + BIND(${nodeExpr} AS ?x) + BIND(${runExpr} AS ?run) + } + `; + }, + }, + + subclassAndExistentialWitnesses: { + id: 'subclassAndExistentialWitnesses', + label: 'Hydrate subclass and someValuesFrom witnesses', + description: 'For each named class, create a synthetic instance, named superclass types, and shallow existential witnesses.', + construct: options => { + const nodeExpr = hydrationNodeExpression('?A', options); + return ` + CONSTRUCT { + ?x rdf:type ?A . + ?x rdf:type ?Super . + ?x ?p ?y . + ?y rdf:type ?Filler . + ?y rdf:type ?Part . + ?y ?p2 ?z . + ?z rdf:type ?Filler2 . + ?x hyd:hydratedFromClass ?A . + ?y hyd:hydrationRole hyd:ExistentialWitness ; + hyd:witnessForRestriction ?R . + ?z hyd:hydrationRole hyd:NestedExistentialWitness ; + hyd:witnessForRestriction ?Part . + } + WHERE { + ${scopedWhere(` + ?A rdf:type owl:Class . + FILTER(isIRI(?A)) + FILTER(?A != owl:Thing && ?A != owl:Nothing) + `, options)} + BIND(${nodeExpr} AS ?x) + + OPTIONAL { + ${scopedWhere('?A rdfs:subClassOf ?Super .\nFILTER(isIRI(?Super))\nFILTER(?Super != owl:Thing && ?Super != owl:Nothing)', options)} + } + + OPTIONAL { + ${scopedWhere(` + ?A rdfs:subClassOf ?R . + ?R rdf:type owl:Restriction ; + owl:onProperty ?p ; + owl:someValuesFrom ?Filler . + `, options)} + BIND(IRI(CONCAT("${HYD}witness/", ENCODE_FOR_URI(STR(?A)), "/", ENCODE_FOR_URI(STR(?p)), "/", ENCODE_FOR_URI(STR(?Filler)))) AS ?y) + + OPTIONAL { + FILTER(isIRI(?Filler)) + } + + OPTIONAL { + ${scopedWhere('?Filler owl:intersectionOf/rdf:rest*/rdf:first ?Part .', options)} + + OPTIONAL { + FILTER(isIRI(?Part)) + } + + OPTIONAL { + ${scopedWhere(` + ?Part rdf:type owl:Restriction ; + owl:onProperty ?p2 ; + owl:someValuesFrom ?Filler2 . + `, options)} + BIND(IRI(CONCAT("${HYD}witness/", ENCODE_FOR_URI(STR(?A)), "/", ENCODE_FOR_URI(STR(?p)), "/", ENCODE_FOR_URI(STR(?Part)), "/", ENCODE_FOR_URI(STR(?p2)), "/", ENCODE_FOR_URI(STR(?Filler2)))) AS ?z) + } + } + } + } + `; + }, + }, +}); + +const COVERAGE = Object.freeze([ + coverage('class-disjointness', 'OWL2 EL-ish', 'supported', 'Direct owl:disjointWith and owl:AllDisjointClasses overlaps are checked after available rdf:type materialization.'), + coverage('class-complement', 'OWL2 DL construct, direct ABox check', 'supported', 'Detects explicit complement type overlap. It does not attempt full class expression satisfiability.'), + coverage('property-disjointness', 'OWL2 EL-ish', 'supported', 'Detects shared subject/object pairs for owl:AllDisjointProperties.'), + coverage('individual-identity', 'OWL2 DL construct, direct ABox check', 'supported', 'Detects owl:AllDifferent conflicts with explicit owl:sameAs. It does not compute complete equality closure by itself.'), + coverage('property-cardinality', 'OWL2 DL construct, direct ABox check', 'partial', 'Detects literal functional conflicts and object functional conflicts when owl:differentFrom is explicit.'), + coverage('negative-assertion', 'OWL2 DL construct, direct ABox check', 'supported', 'Detects explicitly asserted triples that contradict owl:NegativePropertyAssertion nodes.'), + coverage('key-identity', 'OWL2 DL construct, direct ABox check', 'partial', 'Single-property owl:hasKey only. Multi-property keys need additional equality joins.'), + coverage('restriction-heuristic', 'OWL2 DL construct, heuristic', 'partial', 'allValuesFrom missing-type checks are open-world heuristics, not proof of inconsistency.'), + coverage('implicit-model-hydration', 'OWL2 EL-oriented heuristic', 'partial', 'Hydration creates synthetic witnesses for named classes, named superclasses, and shallow someValuesFrom/intersection patterns.'), + coverage('full-owl-dl-satisfiability', 'OWL2 DL', 'unsupported', 'No complete tableau reasoning for constructs such as arbitrary allValuesFrom, cardinality, property chains, or complex negation.'), +]); + +function makeQueryDefinition(input) { + return Object.freeze({ + id: input.id, + label: input.label, + description: input.description, + family: input.family, + supported: input.supported !== false, + select: options => `SELECT DISTINCT ${input.variables.map(name => `?${name}`).join(' ')} WHERE {\n${indent(input.where(options))}\n}`, + ask: options => `ASK WHERE {\n${indent(input.where(options))}\n}`, + construct: options => makeViolationConstruct(input, options), + }); +} + +function makeViolationConstruct(input, options) { + const bindings = input.variables + .map(name => `?violation axi:binding_${name} ?${name} .`) + .join('\n '); + + return ` + CONSTRUCT { + ?violation rdf:type axi:InconsistencyViolation ; + axi:violationKind "${input.id}" ; + rdfs:label "${escapeString(input.label)}" ; + axi:severity "error" . + ${bindings} + } + WHERE { + ${indent(input.where(options))} + BIND(IRI(CONCAT("${AXI}violation/", "${input.id}", "/", STRUUID())) AS ?violation) + } + `; +} + +function coverage(family, owlProfile, support, notes) { + return { id: family, label: family, family, owlProfile, support, notes }; +} + +function hydrationNodeExpression(classVariable, options) { + if (options.deterministicIris === false) return 'IRI(CONCAT("urn:uuid:", STRUUID()))'; + return `IRI(CONCAT("${HYD}node/", ENCODE_FOR_URI(STR(${classVariable}))))`; +} + +function hydrationRunExpression(options) { + if (options.runIri) return `<${escapeIri(options.runIri)}>`; + return 'IRI(CONCAT("urn:uuid:", STRUUID()))'; +} + +async function collectBindings(bindingsStream) { + return await new Promise((resolve, reject) => { + const rows = []; + bindingsStream.on('data', bindings => rows.push(bindingsToObject(bindings))); + bindingsStream.on('end', () => resolve(rows)); + bindingsStream.on('error', reject); + }); +} + +function bindingsToObject(bindings) { + if (bindings && typeof bindings.entries === 'function') { + return Object.fromEntries(Array.from(bindings.entries()).map(([key, value]) => [String(key), value])); + } + + if (bindings && typeof bindings.forEach === 'function') { + const row = {}; + bindings.forEach((value, key) => { + row[String(key).replace(/^\?/, '')] = value; + }); + return row; + } + + return {}; +} + +function compactSparql(query) { + return query + .split('\n') + .map(line => line.replace(/\s+$/u, '')) + .join('\n') + .replace(/\n{3,}/gu, '\n\n') + .trim() + '\n'; +} + +function indent(text, spaces = 2) { + const padding = ' '.repeat(spaces); + return String(text) + .trim() + .split('\n') + .map(line => `${padding}${line}`) + .join('\n'); +} + +function escapeIri(value) { + return String(value).replace(/[<>"{}|^`\\]/gu, encodeURIComponent); +} + +function escapeString(value) { + return String(value).replace(/\\/gu, '\\\\').replace(/"/gu, '\\"'); +} + +const api = Object.freeze({ + normalizeQueryOptions, + scopedWhere, + hasInconsistencyQuery, + listInconsistencyQueries, + getInconsistencyQuery, + getAllInconsistencySelectQueries, + runInconsistencySelect, + runAllInconsistencySelects, + constructInconsistencyReport, + listInconsistencyCoverage, + listHydrationQueries, + getHydrationConstructQuery, + applyHydrationConstruct, +}); + +if (typeof globalThis !== 'undefined') { + globalThis.AxiolotlInconsistency = api; +} + diff --git a/public/app/axiolotl-inconsistency.test.js b/public/app/axiolotl-inconsistency.test.js new file mode 100644 index 0000000..9294fa5 --- /dev/null +++ b/public/app/axiolotl-inconsistency.test.js @@ -0,0 +1,137 @@ +import { + constructInconsistencyReport, + getAllInconsistencySelectQueries, + getHydrationConstructQuery, + getInconsistencyQuery, + hasInconsistencyQuery, + listHydrationQueries, + listInconsistencyCoverage, + listInconsistencyQueries, + normalizeQueryOptions, + scopedWhere, +} from './axiolotl-inconsistency.js'; + +describe('axiolotl-inconsistency query registry', () => { + test('registers the expected low-hanging-fruit checks', () => { + const ids = listInconsistencyQueries().map(query => query.id); + + expect(ids).toEqual(expect.arrayContaining([ + 'disjointWithTypeOverlap', + 'allDisjointClassesTypeOverlap', + 'complementOfTypeOverlap', + 'datatypeFunctionalPropertyConflict', + 'objectFunctionalPropertyDifferentFromConflict', + 'negativePropertyAssertionConflict', + 'allDifferentSameAsConflict', + 'allDisjointPropertiesSharedPair', + 'disjointUnionMissingMemberHeuristic', + 'allValuesFromMissingTypeHeuristic', + 'singlePropertyHasKeyDifferentFromConflict', + ])); + }); + + test('builds SELECT, ASK, and CONSTRUCT forms', () => { + expect(getInconsistencyQuery('disjointWithTypeOverlap')).toContain('SELECT DISTINCT ?x ?A ?B'); + expect(getInconsistencyQuery('disjointWithTypeOverlap', { resultForm: 'ask' })).toContain('ASK WHERE'); + expect(getInconsistencyQuery('disjointWithTypeOverlap', { resultForm: 'construct' })).toContain('axi:InconsistencyViolation'); + }); + + test('builds all supported SELECT queries', () => { + const queries = getAllInconsistencySelectQueries(); + + expect(queries.length).toBe(listInconsistencyQueries().filter(query => query.supported).length); + expect(queries.every(item => item.query.includes('SELECT DISTINCT'))).toBe(true); + }); + + test('identifies query ids without throwing', () => { + expect(hasInconsistencyQuery('disjointWithTypeOverlap')).toBe(true); + expect(hasInconsistencyQuery('missing')).toBe(false); + }); +}); + +describe('graph scoping', () => { + test('normalizes default options', () => { + expect(normalizeQueryOptions()).toEqual({ + scope: 'union', + graphIri: null, + resultForm: 'select', + }); + }); + + test('requires a graph IRI for named scope', () => { + expect(() => normalizeQueryOptions({ scope: 'named' })).toThrow('graphIri'); + }); + + test('wraps union scope with default and named graph alternatives', () => { + const body = scopedWhere('?s ?p ?o .', { scope: 'union' }); + + expect(body).toContain('?s ?p ?o .'); + expect(body).toContain('GRAPH ?axiolotlGraph'); + }); + + test('wraps named scope with a concrete graph IRI', () => { + const query = getInconsistencyQuery('negativePropertyAssertionConflict', { + scope: 'named', + graphIri: 'http://example.org/graph', + }); + + expect(query).toContain('GRAPH '); + expect(query).toContain('owl:NegativePropertyAssertion'); + }); +}); + +describe('hydration queries', () => { + test('lists hydration query metadata', () => { + const ids = listHydrationQueries().map(item => item.id); + + expect(ids).toEqual(expect.arrayContaining([ + 'namedClassInstances', + 'subclassAndExistentialWitnesses', + ])); + }); + + test('builds deterministic named class instance hydration by default', () => { + const query = getHydrationConstructQuery('namedClassInstances', { + runIri: 'http://example.org/run/1', + }); + + expect(query).toContain('CONSTRUCT'); + expect(query).toContain('hyd:hydratedFromClass'); + expect(query).toContain('http://example.org/hydration/node/'); + expect(query).toContain(''); + }); + + test('can build uuid-backed hydration nodes', () => { + const query = getHydrationConstructQuery('namedClassInstances', { + deterministicIris: false, + }); + + expect(query).toContain('STRUUID()'); + }); +}); + +describe('coverage documentation and construct adapter', () => { + test('documents supported, partial, and unsupported areas', () => { + const support = new Set(listInconsistencyCoverage().map(item => item.support)); + + expect(support.has('supported')).toBe(true); + expect(support.has('partial')).toBe(true); + expect(support.has('unsupported')).toBe(true); + }); + + test('constructInconsistencyReport delegates to the supplied construct function', async () => { + const calls = []; + const quads = [{ subject: 's' }]; + const result = await constructInconsistencyReport('disjointWithTypeOverlap', {}, { + applyConstruct: async (query, store) => { + calls.push({ query, store }); + return quads; + }, + }); + + expect(result).toBe(quads); + expect(calls).toHaveLength(1); + expect(calls[0].query).toContain('axi:InconsistencyViolation'); + }); +}); + From 8c0451a9f65174c7f137ffe1471abe83dd4833e4 Mon Sep 17 00:00:00 2001 From: Jonathan Vajda <36548200+jonathanvajda@users.noreply.github.com> Date: Tue, 26 May 2026 00:33:12 -0400 Subject: [PATCH 2/7] hook to ui --- public/app/axiolotl-ui.js | 256 +++++++++++++++++++++++++++++++++++++ public/index.html | 49 +++++-- public/styles/axiolotl.css | 57 ++++++++- 3 files changed, 349 insertions(+), 13 deletions(-) create mode 100644 public/app/axiolotl-ui.js diff --git a/public/app/axiolotl-ui.js b/public/app/axiolotl-ui.js new file mode 100644 index 0000000..1c9113d --- /dev/null +++ b/public/app/axiolotl-ui.js @@ -0,0 +1,256 @@ +import { + listInconsistencyQueries, + runInconsistencySelect, +} from './axiolotl-inconsistency.js'; + +const CONSISTENCY_PROFILES = Object.freeze({ + axiolotl: { + label: 'Axiolotl maximum', + help: 'Runs every implemented Axiolotl check, including heuristic checks that flag modeling smells rather than formal OWL inconsistency proofs.', + editable: true, + checks: 'all', + }, + 'owl2-el': { + label: 'OWL2 EL', + help: 'ELK-like preset. Uses implemented checks that fit common OWL 2 EL consistency coverage, mainly class/property disjointness over available materialized types.', + editable: false, + checks: [ + 'disjointWithTypeOverlap', + 'allDisjointClassesTypeOverlap', + 'allDisjointPropertiesSharedPair', + ], + }, + 'owl2-dl': { + label: 'OWL2 full', + help: 'Pellet/HermiT-like preset. Axiolotl still runs only its implemented direct checks; it is not a complete OWL 2 DL reasoner.', + editable: false, + checks: [ + 'disjointWithTypeOverlap', + 'allDisjointClassesTypeOverlap', + 'complementOfTypeOverlap', + 'datatypeFunctionalPropertyConflict', + 'objectFunctionalPropertyDifferentFromConflict', + 'negativePropertyAssertionConflict', + 'allDifferentSameAsConflict', + 'allDisjointPropertiesSharedPair', + 'singlePropertyHasKeyDifferentFromConflict', + ], + }, + 'direct-abox': { + label: 'Direct ABox only', + help: 'Runs direct asserted-data contradiction checks and skips open-world missing-type heuristics.', + editable: false, + checks: [ + 'disjointWithTypeOverlap', + 'allDisjointClassesTypeOverlap', + 'complementOfTypeOverlap', + 'datatypeFunctionalPropertyConflict', + 'objectFunctionalPropertyDifferentFromConflict', + 'negativePropertyAssertionConflict', + 'allDifferentSameAsConflict', + 'allDisjointPropertiesSharedPair', + ], + }, +}); + +if (typeof document !== 'undefined') { + document.addEventListener('DOMContentLoaded', initAxiolotlInferenceUi); +} + +function initAxiolotlInferenceUi() { + const modeSelect = document.getElementById('inference-task-mode'); + const profileSelect = document.getElementById('consistency-profile'); + const runButton = document.getElementById('run-inference'); + + renderConsistencyOptions(); + syncInferenceTaskUi(); + + modeSelect?.addEventListener('change', syncInferenceTaskUi); + profileSelect?.addEventListener('change', syncInferenceTaskUi); + runButton?.addEventListener('click', handleConsistencyRunClick, true); +} + +function syncInferenceTaskUi() { + const mode = getInferenceTaskMode(); + const isConsistency = mode === 'consistency'; + const profile = getConsistencyProfile(); + const profileConfig = CONSISTENCY_PROFILES[profile] || CONSISTENCY_PROFILES.axiolotl; + + setHidden('materialization-rule-panel', isConsistency); + setHidden('consistency-rule-panel', !isConsistency); + setText('inference-task-heading', isConsistency ? 'Inference Engine: Consistency Checks' : 'Inference Engine: Forward-Chain Reasoning'); + setText('run-inference', isConsistency ? 'Check Consistency' : 'Run Inference'); + setText('inference-output-label', isConsistency ? 'Consistency report:' : 'Output preview:'); + setHelpText(profileConfig.help); + syncConsistencyCheckboxes(profileConfig); +} + +function renderConsistencyOptions() { + const container = document.getElementById('consistency-rule-options'); + if (!container) return; + + const queries = listInconsistencyQueries().filter(query => query.supported); + const items = queries.map(query => ` +
  • + +
  • + `); + + container.innerHTML = ` + +
      + ${items.join('')} +
    + `; +} + +function syncConsistencyCheckboxes(profileConfig) { + const checkboxes = Array.from(document.querySelectorAll('input[name="consistency-rule"]')); + const enabledIds = new Set(profileConfig.checks === 'all' + ? checkboxes.map(input => input.value) + : profileConfig.checks); + + for (const checkbox of checkboxes) { + checkbox.checked = enabledIds.has(checkbox.value); + checkbox.disabled = !profileConfig.editable; + checkbox.closest('label')?.classList.toggle('is-disabled', checkbox.disabled); + } + + setText( + 'consistency-rule-hint', + profileConfig.editable + ? 'Toggle the Axiolotl checks to run. For best results, materialize type-producing rules before checking.' + : 'This profile uses a fixed preset. Switch to Axiolotl maximum to toggle individual checks.' + ); +} + +async function handleConsistencyRunClick(event) { + if (getInferenceTaskMode() !== 'consistency') return; + + event.preventDefault(); + event.stopImmediatePropagation(); + + try { + clearInferenceConsole?.(); + setInferenceBusy?.(true); + appendInferenceConsoleLine?.('[checkConsistency] Starting consistency checks...'); + + if (document.getElementById('reasoner-source-endpoint')?.checked) { + throw new Error('Consistency checks currently run against the Active Workspace only.'); + } + + if (typeof loadGraphFromIndexedDB !== 'function') { + throw new Error('loadGraphFromIndexedDB is not available.'); + } + + const selectedChecks = getSelectedConsistencyChecks(); + if (!selectedChecks.length) { + throw new Error('No consistency checks selected.'); + } + + const rdfjsStore = await loadGraphFromIndexedDB(); + const results = []; + + for (const id of selectedChecks) { + const result = await runInconsistencySelect(id, rdfjsStore); + results.push(result); + appendInferenceConsoleLine?.(`[checkConsistency] ${id}: ${result.rows.length} violation row(s).`); + } + + const report = formatConsistencyReport(results); + const preview = document.getElementById('rdf-preview'); + if (preview) preview.value = report; + + const violationCount = results.reduce((sum, result) => sum + result.rows.length, 0); + const message = violationCount + ? `Consistency checks found ${violationCount} violation row${violationCount === 1 ? '' : 's'}.` + : 'Consistency checks found no violation rows.'; + + showToast?.(message, violationCount ? 'warning' : 'success'); + appendInferenceConsoleLine?.(`[checkConsistency] Complete. ${message}`); + } catch (error) { + console.error('[checkConsistency] failed', error); + appendInferenceConsoleLine?.(`ERROR: ${error.message || error}`); + showToast?.(`Consistency check error: ${error.message || error}`, 'error'); + } finally { + setInferenceBusy?.(false); + } +} + +function getSelectedConsistencyChecks() { + return Array.from(document.querySelectorAll('input[name="consistency-rule"]:checked')) + .map(input => input.value); +} + +function formatConsistencyReport(results) { + const lines = [ + 'Axiolotl consistency report', + `Generated: ${new Date().toISOString()}`, + '', + ]; + + for (const result of results) { + lines.push(`${result.id}: ${result.rows.length} violation row(s)`); + result.rows.slice(0, 25).forEach((row, index) => { + lines.push(` ${index + 1}. ${formatBindingRow(row)}`); + }); + if (result.rows.length > 25) { + lines.push(` ... ${result.rows.length - 25} additional row(s) omitted from preview`); + } + lines.push(''); + } + + return lines.join('\n'); +} + +function formatBindingRow(row) { + const entries = Object.entries(row); + if (!entries.length) return '(no bindings)'; + return entries + .map(([key, value]) => `${key}=${formatTerm(value)}`) + .join(', '); +} + +function formatTerm(term) { + if (!term) return ''; + if (typeof term === 'string') return term; + if (term.termType === 'Literal') return JSON.stringify(term.value); + return term.value || String(term); +} + +function getInferenceTaskMode() { + return document.getElementById('inference-task-mode')?.value || 'materialize'; +} + +function getConsistencyProfile() { + return document.getElementById('consistency-profile')?.value || 'axiolotl'; +} + +function setHidden(id, hidden) { + const element = document.getElementById(id); + if (element) element.hidden = !!hidden; +} + +function setText(id, text) { + const element = document.getElementById(id); + if (element) element.textContent = text; +} + +function setHelpText(text) { + const help = document.getElementById('consistency-profile-help'); + if (!help) return; + help.title = text; + help.setAttribute('aria-label', text); +} + +function escapeHtml(value) { + return String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} diff --git a/public/index.html b/public/index.html index 71213bb..33e4a31 100644 --- a/public/index.html +++ b/public/index.html @@ -1551,24 +1551,48 @@

    Query Library