Skip to content

v1.1.0 - #5

Open
julianrojas87 wants to merge 22 commits into
mainfrom
feat/shacl-targets
Open

julianrojas87 wants to merge 22 commits into
mainfrom
feat/shacl-targets

Conversation

@julianrojas87

Copy link
Copy Markdown
Contributor

No description provided.

julianrojas87 and others added 22 commits July 6, 2026 17:45
- implement capacity to represent a shape as an IQuery
- generalize shape parsing among SHACL and ShEx
- rely on vocabulary factory to handle ontology terms
Adds a query-level result to the containment report and corrects three cases
where the classification did not match the semantics of the input.

Aggregate result

- `solveShapeQueryContainment` and `solveShapeShapeContainment` now expose
  `report.result`, the least favorable result among the star patterns. It is
  `CONTAINED` only when every star pattern is.

Explicit negation (sh:not / ShEx negative triple constraints)

A predicate declared under `sh:not` was stored in the same predicate map as the
positive ones, so a query asking for exactly the forbidden predicate bound
against it and reported `CONTAINED` — the opposite of what the shape states.

- Negative predicates never yield a match.
- A shape that forbids a required predicate is definitive evidence of
  incompatibility, so it no longer counts as the open shape that softens
  `REJECTED` into `WEAKLY_REJECTED`. Such a query is now `REJECTED` even when
  the shape is open.

OPTIONAL patterns

Full binding was computed differently for open and closed shapes: open shapes
required every triple pattern to bind, closed shapes only the non-optional
ones. The same query against the same shape therefore returned `ALIGNED` when
the shape was open and `CONTAINED` once `sh:closed true` was added — closing a
shape made it look more relevant.

- One rule for both: every *required* triple pattern must bind, and at least
  one triple pattern must bind. An unmatched OPTIONAL never blocks containment,
  since a resource can answer the query without it.

MINUS and SERVICE

Neither had an algebra handler, so their triple patterns were collected as
ordinary required patterns. A MINUS block could lower `CONTAINED` to
`UNALIGNED`, and a SERVICE clause could raise `REJECTED` to `UNALIGNED` — the
result moved in both directions on patterns the resource is not expected to
hold.

- Both are discarded during normalization; MINUS keeps its left operand.
- Discarded constructs are listed in `IQuery.unsupported` and surfaced on
  `IResult.unsupported`. Discarding only removes constraints, so the result can
  over-estimate relevance but never classifies a relevant resource lower.

Queries with no triple pattern

- An unrestricted triple pattern (`?s ?p ?o`) asks for every triple of the
  resource. It yields no star pattern, but it is not a discarded construct:
  the input is `CONTAINED` by any shape and every shape is marked visitable,
  flagged by `IQuery.matchesAnyTriple`.
- An input with no triple pattern at all — an empty `WHERE`, a body of only
  `VALUES` or `FILTER`, or one left empty after discarding — never asks the
  resource for data, so relevance is undefined. It previously returned
  `CONTAINED`, ranking every resource maximally relevant on no evidence, and
  now throws `EmptyQueryError`.

Docs

- README: aggregate result documented, examples reorganised around the
  SPARQL-to-SHACL and shape-to-shape entry points, containment results
  illustrated with FILTER and constraint cases.
- roadmap: record SHACL target support (sh:targetClass, sh:targetNode,
  sh:targetSubjectsOf, sh:targetObjectsOf) as future work.

BREAKING CHANGE: `solveShapeQueryContainment` throws `EmptyQueryError` for an
input that normalizes to no triple pattern, where it previously returned
`CONTAINED`. Inputs using OPTIONAL, MINUS, SERVICE or `sh:not` may be assigned
a different result than before. Implementors of `IBindings` must add
`hasNegativeContradiction()`.
- `dependentShapes` was accepted but never passed to
  `groupShapeBydependencies`, so every `sh:node` pointing outside `shapes` was
  treated as satisfied and the root triple bound on an unchecked dependency.
- An unresolved dependency is no longer always a refutation. A closed
  referenced shape that does not declare the nested pattern forbids it and
  still yields `REJECTED`; an open one is merely silent, so the predicate
  counts as a partial match and the pattern is `UNALINGED` or `ALIGNED`.
- A nested star pattern is now classified against the shape that governs it
  through `sh:node`. Previously a fully satisfiable query could come out `REJECTED`.
- Candidate and dependency-only placement of a referenced shape now give the
  same classification.
- Update README
- Shapes outside `decidingShapes` are now passed on as `dependentShapes`: they
  still resolve `sh:node` references from the candidates, but their own
  `sh:node` references no longer count as evidence. Previously that evidence
  was still collected, so excluded shapes alone could raise a result, e.g.
  from `REJECTED` to `UNALINGED`.
- Excluded shapes no longer get an entry in `visitShapeBoundedResource`.
- Update README; add regression tests.
- An absent `sh:minCount` is now 0, as in SHACL. A source-shape property
  constraint without it becomes an optional triple pattern and no longer has
  to be matched for `CONTAINED`. Previously it was required.
- An absent `sh:maxCount` is now unbounded (-1) instead of 1. Cardinality
  ranges are not compared during binding, so no result changes. SHACL
  predicates now always carry a `cardinality`.
- `sh:maxCount 0` on its own now yields a negative predicate, like `sh:not`.
  It previously needed an explicit `sh:minCount 0`, and otherwise became a
  required positive predicate.
- ShEx is unchanged: a triple constraint without a cardinality still occurs
  exactly once.
- Update README; add parser and containment tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- FILTER NOT EXISTS, FILTER(!EXISTS) and MINUS over a single triple
  pattern (bound subject, fresh object) become excludedPredicates of the
  star pattern; a shape requiring the predicate (minCount >= 1) is a
  negative contradiction, so the pattern is REJECTED even on open shapes
- translate sh:not of an input shape the same way
- report other EXISTS/NOT EXISTS (kept as undecidable) and constructs
  discarded inside UNION branches in `unsupported`
- count a contradicting shape reached through sh:node as closed
- decide sh:pattern/regex contradictions only for flagless literal
  patterns that no string matches both (^foo vs bar was REJECTED)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Parse sh:targetNode, sh:targetClass, implicit class targets (a shape
  that is also an rdfs:Class), sh:targetSubjectsOf and sh:targetObjectsOf
  into `IShape.targets`, and sh:ignoredProperties into
  `IShape.ignoredProperties`. A shape that only declares targets is no
  longer a parse error.
- Source shapes: shapeToQuery translates the targets of the given shape,
  not those of shapes reached through sh:node, into patterns about its
  focus node: rdf:type for a class target, the predicate for a
  subjects-of target, `subjectValues` for node targets, and an incoming
  star pattern for an objects-of target. Several class and subjects-of
  targets become UNION branches; other combinations are ignored and
  reported as `TARGETS`.
- Candidate shapes: targets imply constraints (rdf:type with a target
  class, matched by IRI; the predicate of a subjects-of target) that match
  like declared ones and that a closed shape allows, as it allows its
  sh:ignoredProperties. They are only required when every focus node
  carries them. A shape with node targets only cannot contain a star
  pattern about another constant subject. Targets are ignored through
  sh:node.
- A shape with targets only constrains its focus nodes, so its closedness
  and negative constraints count only for star patterns whose matching
  nodes are all in a target (`describesStarPattern`). Otherwise it counts
  as open, and only shapes that describe a pattern can make it REJECTED.
  Shapes without targets behave as before.
- A star pattern whose triple patterns are all in UNION branches can now
  be contained, and star patterns without constraints are pruned from a
  translated shape.
- Blank-node subjects of a query are variables (`isVariable: true`).
- Add lib/target.ts and test/target.test.ts; update README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Any other sh:not holds for some nodes that carry p (sh:minCount 2
allows one value), so reading it as forbidding p gave unsound
REJECTED results. It is now ignored with an UNSUPPORTED_NOT
diagnostic. Also keep every sh:or, sh:xone and sh:not value of a
shape; the parser stored only one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both branches of `if (shape.closed === false)` had become the same code, so
the condition only duplicated it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J27pqtwzm8UXeEv14dUbCX
Both parsers built IPredicates, converted them to IPredicateParts and then
back to IPredicates through buildShapeFromRaw, which was the identity on
everything they produce. The parsers now build IPredicates once and create
the shape with tryCreateShape, which only turns the inconsistency error
into a returned value.

IConstraintFacets is shared by IConstraint and by the parser state, so the
SHACL property data feeds buildConstraint directly and ShEx keeps one facet
record per node instead of six parallel maps copied into every call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J27pqtwzm8UXeEv14dUbCX
…dules

query.ts had grown to 1343 lines with three unrelated jobs and
containment.ts to 1158. They are now:

- lib/shapeToQuery.ts: shape to IQuery translation (also exported from the
  package index, as before through query.ts)
- lib/negation.ts: reading MINUS / NOT EXISTS as negated triple patterns
- lib/filter-compatibility.ts: FILTER compatibility with shape constraints

query.ts (804 lines) now only normalizes SPARQL algebra and no longer
depends on the shape model; containment.ts (470 lines) only classifies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J27pqtwzm8UXeEv14dUbCX
IShape inherited the optional fields of its serialized form, so callers
filled in defaults the Shape constructor had already set (targetsOf and
`?? []`). targets, ignoredProperties and negativePredicates are now
required on IShape; only toObject leaves them out when they are empty.

target.ts classifies the focus nodes of a shape once (focusNodes) and
lists its target alternatives (targetAlternatives). impliedPredicates and
the shape to query translation both use them, instead of each adding up
array lengths and stating the "only alternative is required" rule apart.

Node targets are keyed with nodeTargetKey: an IRI is its value, as before,
and a literal or blank node is its N-Triples form. Plain string literals
keep the key they had; typed and language-tagged literals no longer
collide, and a blank node no longer matches an IRI with the same text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J27pqtwzm8UXeEv14dUbCX
SHACL ignores the targets of a shape reached through sh:node. Instead of a
flag passed to Bindings, UnionBinding, impliedPredicates and
impliedPredicateNames, the nested binding now receives withoutTargets(shape),
a memoized copy without targets, at the one place sh:node is resolved. The
Bindings constructor is back to its signature on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J27pqtwzm8UXeEv14dUbCX
The classification of a star pattern was written twice: once for candidate
shapes in updateContainmentStats and once, inline, for the shapes reached
through sh:node. Both now build an IShapeEvidence record and pass it to
recordEvidence. Bindings.hasRootMatch replaces the two copies of the
"bound triple or undecided dependency" check.

For sh:node evidence, a contained pattern no longer needs to be added to
the root matches separately: a nested binding has no UNION, so it is only
fully bound when it binds a triple, which is already a root match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J27pqtwzm8UXeEv14dUbCX
`result` was defined as a non-enumerable property, so spreading,
JSON.stringify or structuredClone lost it, and the 34 tests comparing whole
reports with toStrictEqual never checked it. Both reports are now built by
buildResult with `result` as an ordinary field, and those tests assert the
aggregate result too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J27pqtwzm8UXeEv14dUbCX
The malformed-list check was copied into three places and the copies had
drifted apart. ShEx returned a parse error under the strict policy; SHACL
only labelled the diagnostic 'error' and went on with the partial list,
although the policy says malformed lists are parse errors. ShEx also used
walkRdfList only for its malformed flag and then walked the list again by
hand, re-reading the rest of a oneOf from each member, and that loop never
ended on a cyclic list under the non-strict policy.

readRdfList (parser-policy.ts) now reads every list a parser uses: sh:or,
sh:xone and sh:ignoredProperties in SHACL, eachOf and oneOf in ShEx. A
malformed list is reported, is a parse error under the strict policy (the
default), and otherwise contributes its members up to the defect. The ShEx
handlers iterate those members once and share a parse context instead of
forwarding the shape IRI and options as unused-looking arguments.

BREAKING CHANGE: under the default policy, a SHACL shape with a malformed
sh:or, sh:xone or sh:ignoredProperties list is now a PoorlyFormatedShapeError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J27pqtwzm8UXeEv14dUbCX
parseShapeFromQuads lived in Shape.ts and used `await import('./shacl')` to
get around the import cycle with the parsers. It is now in
lib/shape-parser.ts (exported from the package index as before) and
imports them statically.

The stream handling was written three times, typing the stream error as
any. forEachQuad (parser-policy.ts) now feeds the quads of an array or a
stream to a callback and returns the stream error, as before. Both parsers
still consume a stream quad by quad.

The 170-line if/return chain of the SHACL parseQuad is now a table of
handlers by predicate IRI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J27pqtwzm8UXeEv14dUbCX
termExpression and operatorExpression hand-built expression objects
without their `type` and cast them through `as unknown as`. Traqula's
AlgebraFactory builds the same expressions as valid algebra.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J27pqtwzm8UXeEv14dUbCX
collectFromAlgebra took eight positional arguments and every handler took
its own subset in its own order. INormalizationState held two of the seven
accumulators, and default values meant a recursive call that left one out
silently dropped what it collected.

The state now holds the triples, VALUES bindings, unions, filters,
discarded constructs and negations. collectFromAlgebra and buildQuery take
it as their one argument, and createNormalizationState makes the branch
built for one alternative of a property path, which shares the VALUES
bindings of its query as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J27pqtwzm8UXeEv14dUbCX
`npx eslint .` reported 14 errors and 2 warnings; it now reports none.
The containment tests also use the real IResult instead of an alias that
left out `result`, which no longer type-checked once the expectations
assert the aggregate result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J27pqtwzm8UXeEv14dUbCX
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants