Skip to content

EXTENDS validation with feasibility-pruned partition search - #105

Open
ericprud wants to merge 13 commits into
linkml:masterfrom
ericprud:extends-validation
Open

EXTENDS validation with feasibility-pruned partition search#105
ericprud wants to merge 13 commits into
linkml:masterfrom
ericprud:extends-validation

Conversation

@ericprud

@ericprud ericprud commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

What

Implements ShEx 2.1 EXTENDS / ABSTRACT / RESTRICTS evaluation, plus a feasibility layer that prunes the partition search.

  • satisfiesExtendedShape (p5_5): neighbourhood triples are allocated to the triple constraints of the extension targets (reached through references, AND/OR branches and extends chains). A constraint reached through several chains — diamond inheritance — allocates its triples to every one of them, so a triple cannot evade one chain's NOT by hiding in another. Extension targets are evaluated through ordinary satisfies() against their allocated part, so ShapeOr / ShapeAnd / ShapeNot parents work uniformly.
  • Node-scoped restriction stack: extension evaluation sees only its allocated part while value-expression recursion on other nodes sees the whole graph; the (node, shape) evaluation cache is bypassed under a restriction (the same pair can genuinely differ between parts — cf. shex.js's identical rule under subGraph).
  • satisfiesShapeDecl (p5_3): restricts are conjuncts; an abstract declaration is satisfied only through a non-abstract descendant (except when evaluated as an EXTENDS target); labeled shapes with registered extensions get the same descendant fallback. Handles both ShExJ 2.1 ShapeDecl and legacy inline-id flavours, and threads the declaration's id to ShapeExternal (2.1 moved labels onto the decl).
  • pyshex/utils/feasibility.py: a sound refutation test over partial bags of TripleConstraint matches — refutes structurally hopeless neighbourhoods (with Missing property reasons) before any partitioning, and pre-filters the 2^n EXTRA permutation loop. Ported from the same analysis implemented and proved for Apache Jena (jena-shex/docs/matching-search-optimization.md), rudof and shex.js.

shexSpec/shexTest validation manifest

schemas tests passed failed skipped (traits)
ShExJ 1182 1093 0 89
ShExC 1184 1091 0 93

Baseline could not run the current manifest at all (NotImplementedError on the first ShapeDecl).

Notes for review

🤖 Generated with Claude Code

@codecov-commenter

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

@ericprud
ericprud force-pushed the extends-validation branch 3 times, most recently from 97beb1a to e6dae0d Compare July 28, 2026 00:13
Eric Prud'hommeaux and others added 4 commits July 28, 2026 03:08
Implements ShEx 2.1 EXTENDS/ABSTRACT/RESTRICTS evaluation:

- satisfiesExtendedShape: neighbourhood triples are allocated to the triple
  constraints of the extension targets (through references, AND/OR branches and
  extends chains); a constraint reached through several chains (diamond
  inheritance) allocates its triples to every one of them, so a triple cannot
  evade one chain's constraints by hiding in another. Extension targets are
  evaluated through ordinary satisfies() against their allocated part, so
  ShapeOr / ShapeAnd / ShapeNot parents work uniformly.
- Node-scoped restriction stack: extension evaluation sees only its allocated
  part while value-expression recursion on other nodes sees the whole graph;
  the (node, shape) evaluation cache is bypassed under a restriction (the same
  pair can genuinely differ between parts).
- satisfiesShapeDecl: restricts are conjuncts; an abstract declaration is
  satisfied only through a non-abstract descendant (except as an EXTENDS
  target); labeled shapes with registered extensions get the same descendant
  fallback. Handles both ShExJ 2.1 ShapeDecl and legacy inline-id flavours.
- pyshex/utils/feasibility.py: a sound refutation test over partial bags of
  TripleConstraint matches, used to prune hopeless neighbourhoods before
  partition enumeration and to pre-filter EXTRA permutations. Ported from the
  same analysis implemented for Apache Jena, rudof and shex.js.

shexSpec/shexTest validation manifest:
  ShExJ:  1182 tests = 1093 passed, 0 failed, 89 trait-skips
  ShExC:  1184 tests = 1091 passed, 0 failed, 93 trait-skips
  (baseline could not run: NotImplementedError on the first ShapeDecl)

pyshexc is temporarily sourced from the EXTENDS-grammar PR branch
(linkml/grammar-python-antlr-linkml#4); repoint to PyPI when released.
Requires tests/data/shexTest (git submodule) for the manifest tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tive IRIs

- SchemaLoader: IMPORT resolution (recursive merge, cycle-guarded, extensionless
  imports completed with .shex before redirect); relative IRIs parse against the
  schema's canonical location; ShExJ (JSON) input gets base absolutization
  (ObjectLiteral lexical values excluded).
- ShapeAssociation accepts blank node selectors.
- Test harness: preserve told bnode labels in rdflib parsing; sht:map JSON shape
  maps; canonical-space shape/focus/data-base alignment; empty skip_traits.
- shexTest data fix pushed to shexSpec/shexTest#69: stale start2RefS2.json.

shexTest validation manifest, zero trait-skips:
  ShExJ: 1182 = 1182 passed, 0 failed, 0 skips
  ShExC: 1184 = 1180 passed, 0 failed, 4 skips (schemas pyshexc cannot parse)
was: 1093/0/89 and 1091/0/93.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assertions were anonymous BNode()s with random UUID labels, and the
turtle serializer orders bnode stanzas by label, so every regeneration
shuffled the whole report.  Zero-padded sequential labels preserve
manifest order through serialization, and a single per-report dc:date
(instead of per-result utcnow) keeps regeneration diffs line-for-line
comparable.  Regenerated against the pinned shexTest (0f45c51).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Windows / cross-version portability:
- the schema loader handed raw filesystem paths to the ShExC parser as
  the document base; Windows paths (drive letter, backslashes) fail
  ShExJ IRIREF validation -- convert to file:/// URIs (POSIX untouched)
- golden .results files read/written with the platform default encoding
  garble U+2260 on Windows (cp1252); pin encoding='utf-8'
- "2016-07"^^xsd:date renders differently across Python versions
  (isodate on py<3.11 normalizes reduced-precision dates; fromisoformat
  on 3.11+ rejects them, preserving the lexical form) -- use a
  fully-specified date

Also, to get the matrix running at all (red since the pytest migration):
- workflow: checkout the shexTest submodule (https URL in .gitmodules),
  push trigger main -> master, SKIP_EXTERNAL_URLS (coerced to bool --
  a string in pytest.mark.skipif is eval'ed as Python), guard the
  unmarked external-URL tests, never collect from tests/data
- rdflib 7: serialize() returns str (drop .decode()), parse into
  bind_namespaces="core" graphs in PrefixLibrary.add_rdf, replace
  removed Graph.load(), regenerate goldens (dc1 renaming, bnode labels)
- latent bugs: missing @ on shape refs in test_schemaorg DS_SHEX,
  CFGraph.open() loads nothing (validated an empty graph; parse()
  instead), manifest_tester failed the suite on unloadable-schema skips

Full suite with SKIP_EXTERNAL_URLS: 129 passed, 21 skipped, 1 xfailed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ericprud
ericprud force-pushed the extends-validation branch from 8755309 to 816c2fa Compare July 28, 2026 01:08
Eric Prud'hommeaux and others added 9 commits August 1, 2026 19:37
…outcomes

- Search ../shexTest and ../../shexSpec/shexTest for the suite before
  falling back to the tests/data/shexTest submodule, so runs track a
  current checkout without a pin bump.
- Contain per-entry exceptions: a load failure becomes a counted skip
  (the latest suite's annotations/semActs-on-NodeConstraint schemas
  exceed ShExJSG's dialect), an engine exception becomes a failure,
  and neither aborts the whole aggregate.
- Compare outcomes honestly: rslt[0] == should_pass -- the previous
  `or not should_pass` meant a ValidationFailure entry could never
  fail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restriction-stack entries carry whether the allocation was made under
a CLOSED shape, inherited transitively: every triple allocated under a
CLOSED extended shape must be consumed by the target it was allocated
to, so a triple matching only an unselected OR branch cannot hide
there (cf. jena-shex's selection-dependent matchables and rudof's
selection-local CLOSED alphabet).  satisfiesShape and
satisfiesExtendedShape treat an inherited-closed restriction like
their own `closed`.

Fixes extends-closed-or-each-each_fail-cross-each: <Child> EXTENDS
@<Base> CLOSED {} with <Base> = { <p>.; <q>. } OR { <p>.; <r>. } must
reject a node carrying p, q and r -- whichever branch is selected
leaves one triple unaccounted for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EachOfEvaluator buckets T's triples by predicate against the
subexpressions; a triple whose predicate none of them mention landed
in no bucket and was silently ignored, so an expression could "match"
a bag it cannot partition.  One guard restores the definition --
tolerating leftovers is the caller's business (satisfiesShape's
remainder, or the EXTRA retry loop in _expression_matches).

Fixes extends-abstract-multi-empty_fail-Ref1ExtraOR (an extra OneOf
branch's triple hid inside the other branch's recursive EachOf,
letting an abstract descendant's CLOSED shape conform) and four
partition-corner entries: 1dotOne2dot-oneOf_fail_p1p2p3,
1dotOne2dot-someOf_fail_p1p2p3, 1dotOne2dotExtra-someOf_pass_p1p2p3
and open1dotOneopen2dotcloseclose_fail_p1p2p3.

Both shexTest validation manifests (1189 entries): 0 Extends failures;
1108/79/2 and 1106/79/4 passed/failed/skipped (ShExC / ShExJ).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Inherited restriction closedness (5a3391a) holds a shape expression
responsible for consuming its whole EXTENDS allocation.  That is right
for an OR branch -- the selected branch must account for every
allocated triple (extends-closed-or-each-each_fail-cross-each) -- but
wrong below an AND or NOT: an AND distributes the allocation among its
conjuncts, so no single conjunct covers it, and a negated shape is a
filter over the allocation, never its consumer.  Contact =
AND(OR(...), rolls-checker EXTRA rolls, NOT robot) saw its EXTRA
conjunct treated as CLOSED and rejected the id/name/mbox triples the
OR conjunct consumes.

satisfiesShapeAnd and satisfiesShapeNot now suspend the flag on the
innermost allocation for the focus node.  Allocation coverage is still
guaranteed by the allocator (a triple matching no extension target is
rejected before parts are formed) and by any conjunct's own CLOSED.

Restores the three tests/test_extends/test_person_extends.py cases
5a3391a broke; both shexTest validation manifests still report zero
Extends failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gex escapes

Clears 71 of the 79 manifest entries surfaced by b7fe380's honest
outcomes:

- nodeSatisfiesDataType rejects a literal whose lexical form is
  outside its datatype's lexical space.  rdflib leaves unparseable
  literals ill-typed but validates neither lexical spaces (Decimal
  accepts "1E0") nor the integer-derived value ranges (257 is a happy
  xsd:byte), so datatype_utils gains the XSD lexical regexes and
  ranges.
- rdflib.NORMALIZE_LITERALS is turned off at package import: a
  validator must see literals as written, and normalization rewrites
  "TRUE"^^xsd:boolean to "true" and "INF"^^xsd:double to "inf" at
  parse time, hiding invalid forms from the check above (and merging
  "0.0e0" with "0E0" in value sets).  rdflib's Turtle tokenizer still
  normalizes bare numerics (00 -> "0"); nothing downstream can recover
  those.
- Language stems match as RFC 4647 basic language ranges ("fr" matches
  "fr-BE" but not "frc") and stem-range exclusions handle typed
  LanguageStem/LiteralStem exclusions, which were compared as plain
  strings and so never excluded anything; literal stems compare
  lexical forms rather than values.
- totaldigits/fractiondigits apply only to xsd:decimal and its
  derivatives; constraints on float/double fail per spec.
- reencode_escapes keeps the escape on every regex metacharacter: a
  \| stripped to | turned 1literalPattern_with_all_punctuation's whole
  pattern into an alternation.

Golden updates: fraction_digits(float/double) is now None, and a
dateTime's "Z" zone designator survives serialization.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in bump

- expected_failures records the eight remaining engine gaps with
  reasons: four semAct aborts (ShExtension abort semantics
  unimplemented), two EXTRA corners (the retry loop does not enforce
  valid_remainder's rule that absorbed triples match no
  TripleConstraint), and two rdflib bare-numeric tokenizer
  normalizations.  Each is counted as a skip with its reason in the
  run summary, so the aggregate assertion is honest and green.
- SHEXTEST=<path> short-circuits the sibling-checkout search, e.g. to
  run against the exact commit CI will see.
- tests/data/shexTest advances 0f45c51 -> 8955d83 (upstream main) so
  CI exercises the extends-closed-or-each-each / or-each-each entries
  the recent EXTENDS commits target.
- EARL report regenerated.

Full suite: 129 passed / 0 failed with SKIP_EXTERNAL_URLS; validation
manifests report zero failures at both the old and new pins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
semActsSatisfied was a stub returning True.  It now evaluates the
shexTest Test extension (http://shex.io/extensions/Test/): print(arg)
succeeds and fail(arg) fails, each recording its argument -- a string
literal or one of s/p/o naming a component of the matched triples --
on cntxt.semact_prints (mf:prints comparison is future harness work).
A failing action aborts the list, so actions after it never run.
Actions of unknown extensions succeed silently, as before.

isValid evaluates the schema's startActs before any node evaluation
(a stage at which there is no current parse node to attach a failure
reason to), and matches() passes the matched triples through for
s/p/o resolution.

Fixes 1dotCode3fail_abort, startCode1fail_abort,
startCode1startReffail_abort and startCode3fail_abort.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ession

Two absorption paths let a triple hide in EXTRA even though it matched
a TripleConstraint, violating valid_remainder's rule that no remainder
triple matches a constraint in the expression:

- matchesCardinality dropped every extras-predicate triple from
  must_match, so <S> EXTRA <p1> { <p1> . } accepted two p1 triples
  against a {1,1} constraint.
- _expression_matches' retry loop permuted all extras-predicate
  triples; it now only absorbs triples that match no TripleConstraint
  (any constraint, selected branch or not).

Fixes 1dotExtra1_fail-iri2 and 1val2IRIREFExtra1_fail-iri2.

The type-samples/pred-samples CLI goldens are regenerated: a biolink
association carrying two rdf:type arcs that both satisfy the type
constraint's @<OntologyClass>? now honestly fails, and the new failure
traces name anonymous shapes by their per-parse bnode label, which the
comparison filter now neutralizes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…es ledger

rdflib.NORMALIZE_LITERALS = False keeps quoted literals as written,
but the N3/Turtle tokenizer separately round-trips bare numerics
through int()/Decimal() before normalise() rebuilds the Literal from
str(), so <p> 00 reached the graph as "0"^^xsd:integer and its
non-canonical (validation-relevant) lexical form was unrecoverable.
long_type and Decimal are notation3 module globals used only to build
and type-check those tokens; pyshex now installs lexical-carrying
subclasses at import, restoring fidelity without touching any parser
method.

Fixes 1val1INTEGER_00 and 1val1DECIMAL_00 -- the last entries in the
expected-failures ledger, which is now empty: both shexTest validation
manifests report zero failures and zero known gaps (1185/0/4 ShExC,
1187/0/2 ShExJ at shexTest 8955d83; remaining skips are schemas
beyond ShExJSG's dialect).  EARL report regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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