Skip to content

SHACL part 2a: commit-time gating under a shapes binding, incremental validation (v0.1.0-preview.12) - #140

Open
Hafeok wants to merge 31 commits into
mainfrom
ccr-bf4bdc29-n67y35
Open

Hafeok wants to merge 31 commits into
mainfrom
ccr-bf4bdc29-n67y35

Conversation

@Hafeok

@Hafeok Hafeok commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

SHACL part 2a, under milestone 8 (#13). Its issue is #138, and its release is v0.1.0-preview.11. This is the first of the two PRs the plan proposed and the maintainer approved. ADRs 0169–0173 are filed unaccepted, so CI is red only on CS0618. The full report is docs/traceability/2026-10-20-issue-138-shacl-part-2a.md.

What this changes, and why

A shapes binding can now gate a dataset's commits.

  • Where it is declared. A binding is a management declaration (varve:ShapesBinding, ADR 0170). It names a shapes graph of the same dataset, the graphs to validate (in the grant vocabulary's graph-set terms), and a mode.
  • Who enforces it. Every server adds the dataset's gate at the management position a write is authorised under.

How the gate behaves. Varve.Shacl.Store (layer 5, ADR 0169) is a dataset validator.

  • reject refuses a commit whose affected focus nodes have a violation not present at the pinned position. So violations never grow, and a conforming dataset stays conforming. This follows the approved change to R1.
  • report accepts the commit and attaches <<( binding varve:affectedReport "sha256:…" )>> (ADR 0173).
  • off runs nothing.
  • A shapes change is never refused for existing data (R5).
  • Full shapes under reject. Such a binding is refused at apply time unless it declares varve:allowFullEvaluation (R4). If its shapes become full later, it fails closed (R6).
  • Refused functions. NOW(), RAND(), UUID() and STRUUID() are refused, and so is a BNODE() whose value can reach a result.
  • Problem types. Three new ones, each with a page: shapes-violated, binding-not-incremental and shapes-invalid.
  • Describing a binding. GET /manage/datasets/{name}/bindings/{binding} serves a binding with its classification.

Only the focus nodes a commit affects are validated (Varve.Shacl, ADR 0171).

  • How they are found. Footprints are anchored from each changed triple, in the states before and after the commit. They are then propagated over the shapes' dependencies to a least fixpoint.
  • Classification. Every shape is classed incremental, scoped or full, statically.
  • SHACL-SPARQL. Its constraints are analysed by their join graph.

Varve.Derivation (layer 2, ADR 0172, approved with ADR 0003's amendment) holds the dependency graphs and stratification, regular paths as automata, and semi-naive propagation. SHACL uses it now, and Inference will later.

The REC generator's output version 2. The generator's leases now carry rec:leasor.

Departures from the approved plan. The record has all of them. The visible ones, both accepted on this PR and stated in ADRs 0170 and 0155:

  • No varve:validates node. ADR 0166's code validator refuses blank nodes in management graphs, so the graph-set terms sit on the binding node itself, as on a grant.
  • One public entry point. The plan's Validate(…, FocusSelection) and AffectedBy became IncrementalValidation.Compare, because the gate needs the affected set, both reports and the introduced results together.

What was measured

Properties, with no counterexample at the stated counts. Each count is per CI run, then the long run under VARVE_LONG_PROPERTIES:

Property Per CI run Long run
Incremental comparison against full validation, over generated shapes and histories 500 5,000
The same for SHACL-SPARQL's join-graph analysis 300 3,000
The gate's verdicts and digests against a full-validation oracle, with shapes changed alone and with data 500 5,000
Varve.Derivation's paths and stratification 3,000 each —

Three sabotages of the engine each failed the incremental property:

  • no path anchors;
  • an empty transfer;
  • no provenance.

Commit latency on the file backend. BenchmarkDotNet 0.15.8, Xeon @ 2.80 GHz, 4 cores, a cloud container. The setup:

  • REC 4.1.0's shapes, without Brick's three SPARQL constraints (see below);
  • the portfolio's last 220 commits, after the rest was loaded;
  • the REC generator's output version 2, whose leases carry rec:leasor.

No commit was refused. Slower rows first:

REC Binding Mean per commit StdDev Allocated
M (≈5 M quads) reject 11.86 ms 3.00 ms 2,471.3 KB
S (≈203 k quads) reject 7.62 ms 1.69 ms 2,294.6 KB
M none 0.86 ms 0.10 ms 30.5 KB
S none 0.51 ms 0.07 ms 35.4 KB

The number of affected focus nodes per commit is the same at both sizes. The gate's cost is theirs, not the dataset's:

REC Affected focus nodes per commit Time per affected focus node
S 2.6 2.48 ms
M 2.6 3.44 ms

The machine was slower during this run than during the first one, on generator version 1. That run measured 8.24 ms gated at both sizes, and the README has both runs.

The benchmark also led to a fix: the shapes graph's subclass steps are now remembered. That cut the time in the gate from 13.2 ms to 4.5 ms per commit at S.

Test durations on Linux (CI's other operating systems to follow, per #126):

Project Duration
Varve.Derivation.Tests (new) 2.2 s
Varve.Shacl.Store.Tests (new) 16.5 s

The W3C SHACL suites are unchanged: 628 pass, and the same 28 are exempt.

What was found

  • REC 4.1.0 can't be bound as vendored. Three of Brick's SPARQL constraints use sh:prefixes with no sh:declare in any of the three files. Two of them also use a prefix they don't list: owl: and brick:. A strict processor refuses the whole shapes graph. RecStandaloneTests pins this, with no exemption. Part 2b's REC gate will add the declarations and the owl:Class triple, and ADR 0176 will record the exact triples.
  • The upstream draft for REC is a comment on this PR, for the maintainer to file.
  • The rest of REC is maintainable. Without those three constraints, nothing is classed full. A full validation of the S head takes about 74 s; incrementally, a commit takes a few milliseconds.
  • The generator wrote leases without rec:leasor, giving 141 violations at S.
    • It's fixed here: each site has an owner, and every lease names it.
    • The output version is now 2, with new pinned hashes, and the S head now conforms.
    • The reject tests get their violations from an explicit mutation (a lease's leasor retracted), never from the generator.
  • The plan misread REC's SPARQL. It called those three constraints analysable.

What is left

Public API baseline (additive; nothing removed). The lines added to each PublicAPI.Unshipped.txt

Varve.Derivation (new; 69 lines):

Varve.Derivation.DependencyGraph<TLabel>
Varve.Derivation.DependencyGraph<TLabel>.Add(int source, int target, Varve.Derivation.Model.Polarity polarity, TLabel label) -> void
Varve.Derivation.DependencyGraph<TLabel>.DependencyGraph(int vertexCount) -> void
Varve.Derivation.DependencyGraph<TLabel>.Incoming(int vertex) -> System.Collections.Generic.IReadOnlyList<Varve.Derivation.Model.DependencyEdge<TLabel>>!
Varve.Derivation.DependencyGraph<TLabel>.Outgoing(int vertex) -> System.Collections.Generic.IReadOnlyList<Varve.Derivation.Model.DependencyEdge<TLabel>>!
Varve.Derivation.DependencyGraph<TLabel>.Stratify() -> Varve.Derivation.Stratification<TLabel>!
Varve.Derivation.DependencyGraph<TLabel>.VertexCount.get -> int
Varve.Derivation.IPathGraph<TNode, TLabel>
Varve.Derivation.IPathGraph<TNode, TLabel>.Nodes.get -> System.Collections.Generic.IEqualityComparer<TNode>!
Varve.Derivation.IPathGraph<TNode, TLabel>.Objects(TNode subject, TLabel label, System.Collections.Generic.List<TNode>! into) -> void
Varve.Derivation.IPathGraph<TNode, TLabel>.Subjects(TLabel label, TNode obj, System.Collections.Generic.List<TNode>! into) -> void
Varve.Derivation.Model.DependencyEdge<TLabel>  (record struct: Source, Target, Polarity, Label; equality, Deconstruct)
Varve.Derivation.Model.Polarity  (Positive = 0, Negative = 1, Both = 2, Target = 3)
Varve.Derivation.Model.RefusedCycle<TLabel>.Edge.get -> Varve.Derivation.Model.DependencyEdge<TLabel>
Varve.Derivation.Model.RefusedCycle<TLabel>.Edges.get -> System.Collections.Generic.IReadOnlyList<Varve.Derivation.Model.DependencyEdge<TLabel>>!
Varve.Derivation.PathAutomaton<TLabel>.AcceptsEmpty.get -> bool
Varve.Derivation.PathAutomaton<TLabel>.Anchors<TNode>(IPathGraph<TNode, TLabel>! graph, TNode subject, TLabel label, TNode obj, List<TNode>! into, HashSet<TNode>! seen) -> void
Varve.Derivation.PathAutomaton<TLabel>.Evaluate<TNode>(IPathGraph<TNode, TLabel>! graph, TNode start, List<TNode>! into, HashSet<TNode>! seen) -> void
Varve.Derivation.PathAutomaton<TLabel>.IsBounded.get -> bool
Varve.Derivation.PathAutomaton<TLabel>.Labels.get -> System.Collections.Generic.IEnumerable<TLabel>!
Varve.Derivation.PathAutomaton<TLabel>.Reverse<TNode>(IPathGraph<TNode, TLabel>! graph, TNode end, List<TNode>! into, HashSet<TNode>! seen) -> void
Varve.Derivation.Propagation<TNode, TLabel>.Propagation(DependencyGraph<TLabel>! graph, Stratification<TLabel>! strata, IEqualityComparer<TNode>! nodes) -> void
Varve.Derivation.Propagation<TNode, TLabel>.Run(PropagationTransfer<TNode, TLabel>! transfer) -> void
Varve.Derivation.Propagation<TNode, TLabel>.Seed(int vertex, TNode node) -> bool
Varve.Derivation.Propagation<TNode, TLabel>.SetOf(int vertex) -> System.Collections.Generic.IReadOnlySet<TNode>!
virtual Varve.Derivation.PropagationTransfer<TNode, TLabel>.Invoke(DependencyEdge<TLabel> edge, TNode node, List<TNode>! into) -> void
Varve.Derivation.RegularPath<TLabel>.Compile() -> Varve.Derivation.PathAutomaton<TLabel>!
static Varve.Derivation.RegularPath.Edge / Inverse / Sequence / Alternative / ZeroOrMore / OneOrMore / ZeroOrOne<TLabel>(…) -> RegularPath<TLabel>!
Varve.Derivation.Stratification<TLabel>.ComponentCount / ComponentOf(int) / IsCyclic(int) / IsStratifiable / Members(int) / Refused

(abbreviated by the session for reading; the file has every line verbatim)

Varve.Shacl.Store (new; 26 lines):

Varve.Shacl.Store.Model.BindingCheck.Accepted / Classification / Problems / Refusal (.get)
Varve.Shacl.Store.Model.ShapesBinding.ShapesBinding(RdfTerm! binding, RdfTerm! shapesGraph, GraphScope! validates, ValidationMode mode) -> void
Varve.Shacl.Store.Model.ShapesBinding.Binding / ShapesGraph / Validates / Mode (.get); AllowFullEvaluation / SubClassOfInShapesGraph (.get, .init)
Varve.Shacl.Store.Model.ValidationMode  (Off = 0, Report = 1, Reject = 2)
Varve.Shacl.Store.ShapesGate.ShapesGate(IEnumerable<ShapesBinding!>! bindings, ShaclOptions? options = null) -> void
Varve.Shacl.Store.ShapesGate.Bindings.get -> IReadOnlyList<ShapesBinding!>!
Varve.Shacl.Store.ShapesGate.Check(ShapesBinding! binding, IQuadSource! state) -> BindingCheck!
Varve.Shacl.Store.ShapesGate.Validate(IQuadSource! proposed, QuadDelta delta) -> ValidationVerdict
Varve.Shacl.Store.ShapesGate.Validate(IQuadSource! proposed, BulkDelta! delta) -> ValidationVerdict

(abbreviated the same way)

Varve.Shacl (+43):

+Varve.Shacl.FootprintContext
+Varve.Shacl.FootprintContext.Add(Varve.Rdf.TermHandle focus) -> void
+Varve.Shacl.FootprintContext.AddValueNode(Varve.Rdf.TermHandle value) -> void
+Varve.Shacl.FootprintContext.Data.get -> Varve.Rdf.IQuadSource!
+Varve.Shacl.FootprintContext.DataGraph.get -> Varve.Rdf.GraphPattern
+Varve.Shacl.FootprintContext.Object.get -> Varve.Rdf.TermHandle
+Varve.Shacl.FootprintContext.Predicate.get -> Varve.Rdf.TermHandle
+Varve.Shacl.FootprintContext.Subject.get -> Varve.Rdf.TermHandle
+Varve.Shacl.IShaclFootprint
+Varve.Shacl.IShaclFootprint.Anchors(Varve.Shacl.FootprintContext! context) -> void
+Varve.Shacl.IShaclFootprint.Construct.get -> string!
+Varve.Shacl.IShaclFootprint.Maintainability.get -> Varve.Shacl.Model.Maintainability
+Varve.Shacl.IncrementalOutcome
+Varve.Shacl.IncrementalOutcome.Comparison.get -> Varve.Shacl.Model.IncrementalComparison?
+Varve.Shacl.IncrementalOutcome.Problems.get -> System.Collections.Generic.IReadOnlyList<Varve.Shacl.Model.ShaclProblem!>!
+Varve.Shacl.IncrementalOutcome.Succeeded.get -> bool
+Varve.Shacl.IncrementalValidation
+Varve.Shacl.Model.ClassificationReason
+Varve.Shacl.Model.ClassificationReason.Construct.get -> string!
+Varve.Shacl.Model.ClassificationReason.Maintainability.get -> Varve.Shacl.Model.Maintainability
+Varve.Shacl.Model.ClassificationReason.Section.get -> string!
+Varve.Shacl.Model.IncrementalComparison
+Varve.Shacl.Model.IncrementalComparison.AffectedFocusNodes.get -> System.Collections.Generic.IReadOnlyList<Varve.Rdf.RdfTerm!>!
+Varve.Shacl.Model.IncrementalComparison.AffectedPairs.get -> int
+Varve.Shacl.Model.IncrementalComparison.After.get -> Varve.Shacl.Model.ValidationReport!
+Varve.Shacl.Model.IncrementalComparison.Before.get -> Varve.Shacl.Model.ValidationReport!
+Varve.Shacl.Model.IncrementalComparison.Introduced.get -> System.Collections.Generic.IReadOnlyList<Varve.Shacl.Model.ShaclResult!>!
+Varve.Shacl.Model.Maintainability
+Varve.Shacl.Model.Maintainability.Full = 2 -> Varve.Shacl.Model.Maintainability
+Varve.Shacl.Model.Maintainability.Incremental = 0 -> Varve.Shacl.Model.Maintainability
+Varve.Shacl.Model.Maintainability.Scoped = 1 -> Varve.Shacl.Model.Maintainability
+Varve.Shacl.Model.ShapeClassification
+Varve.Shacl.Model.ShapeClassification.Maintainability.get -> Varve.Shacl.Model.Maintainability
+Varve.Shacl.Model.ShapeClassification.Reasons.get -> System.Collections.Generic.IReadOnlyList<Varve.Shacl.Model.ClassificationReason!>!
+Varve.Shacl.Model.ShapeClassification.Shape.get -> Varve.Rdf.RdfTerm!
+Varve.Shacl.Model.ShapesClassification
+Varve.Shacl.Model.ShapesClassification.Shapes.get -> System.Collections.Generic.IReadOnlyList<Varve.Shacl.Model.ShapeClassification!>!
+Varve.Shacl.Model.ShapesClassification.Worst.get -> Varve.Shacl.Model.Maintainability
+Varve.Shacl.ShaclOptions.RefuseNonDeterministicFunctions.get -> bool
+Varve.Shacl.ShaclOptions.RefuseNonDeterministicFunctions.init -> void
+Varve.Shacl.ShapesGraph.Classify() -> Varve.Shacl.Model.ShapesClassification!
+Varve.Shacl.ShapesGraphContext.RefuseNonDeterministicFunctions.get -> bool
+static Varve.Shacl.IncrementalValidation.Compare(Varve.Shacl.ShapesGraph! shapes, Varve.Rdf.IQuadSource! after, Varve.Rdf.GraphPattern dataGraph, Varve.Rdf.QuadDelta delta, Varve.Shacl.ShaclOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Varve.Shacl.IncrementalOutcome!

Varve.Store (+2):

+Varve.Store.Log.ValidationVerdict.Attachments.get -> System.Collections.Generic.IReadOnlyList<Varve.Rdf.RdfTerm!>!
+static Varve.Store.Log.ValidationVerdict.Accept(System.Collections.Generic.IReadOnlyList<Varve.Rdf.RdfTerm!>! attachments) -> Varve.Store.Log.ValidationVerdict

Varve.Management (+12):

+Varve.Management.Model.BindingDeclaration.AllowFullEvaluation.get -> bool
+Varve.Management.Model.BindingDeclaration.AllowFullEvaluation.init -> void
+Varve.Management.Model.BindingDeclaration.Mode.get -> Varve.Management.Model.ShapesMode
+Varve.Management.Model.BindingDeclaration.Mode.init -> void
+Varve.Management.Model.BindingDeclaration.SubClassOfInShapesGraph.get -> bool
+Varve.Management.Model.BindingDeclaration.SubClassOfInShapesGraph.init -> void
+Varve.Management.Model.BindingDeclaration.Validates.get -> Varve.Management.Model.GraphSet?
+Varve.Management.Model.BindingDeclaration.Validates.init -> void
+Varve.Management.Model.ShapesMode
+Varve.Management.Model.ShapesMode.Off = 0 -> Varve.Management.Model.ShapesMode
+Varve.Management.Model.ShapesMode.Reject = 2 -> Varve.Management.Model.ShapesMode
+Varve.Management.Model.ShapesMode.Report = 1 -> Varve.Management.Model.ShapesMode

Varve.Protocol (+23):

+static Varve.Protocol.Model.ProblemType.BindingNotIncremental.get -> Varve.Protocol.Model.ProblemType
+static Varve.Protocol.Model.ProblemType.ShapesInvalid.get -> Varve.Protocol.Model.ProblemType
+static Varve.Protocol.Model.ProblemType.ShapesViolated.get -> Varve.Protocol.Model.ProblemType
+Varve.Protocol.IManagementPlane.CheckBindings(Varve.Management.Model.Declarations! proposed) -> System.Collections.Generic.IReadOnlyList<Varve.Rdf.RdfTerm!>?
+Varve.Protocol.IManagementPlane.DescribeBinding(Varve.Protocol.Model.DatasetName dataset, Varve.Rdf.RdfTerm! binding) -> Varve.Protocol.Model.BindingDescription?
+Varve.Protocol.Model.BindingDescription  (record: Declaration, Position, Classification, Problems, with the record members)

Checklist

  • Issue referenced: every commit carries Refs #138 (issue-refs passes); the descriptor commit carries the closing trailer
  • ADR cited: ADRs 0169–0173, with dated amendments to 0003, 0017, 0058, 0119, 0150, 0152, 0154, 0155 and 0166
  • Specification updated, or n/a: n/a. The log format is unchanged, and the attachment is a validator attachment (ADR 0017); ADR 0173 defines its term
  • API baseline updated: PublicAPI.Unshipped.txt only, as above
  • Conformance baseline updated, or n/a: n/a, unchanged
  • Tests: real store, real server and real HTTP; oracles are full validation
  • A new gate's failure path was proven by running it: the three sabotages above each failed the incremental property
  • Signed and signed off: every commit is SSH-signed and carries Signed-off-by:
  • A traceability record exists
  • dotnet run eng/ci.cs passes locally: everything but release (preview.10's missing tag, above) and CS0618

🤖 Generated with Claude Code

https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA

Hafeok and others added 21 commits October 20, 2026 10:20
Commit-time gating under a shapes binding (0169), the binding vocabulary
(0170), which shapes are incrementally maintainable (0171),
Varve.Derivation at layer 2 (0172), and the validation report on the
commit as a reference and a digest (0173), as decided on the part 2 plan
in #13 with the maintainer's changes: reject refuses new violations only,
and NOW(), RAND(), UUID(), STRUUID() and BNODE() reaching a result are
refused. Dated amendments to 0003, 0017, 0058, 0119, 0150, 0152, 0154
and 0155. Every ruling is filed unaccepted.

Refs #138
Refs #13

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
The dependency graph with polarities, its strongly connected components
numbered dependencies first and the cycles that refuse a stratification;
regular paths compiled by Thompson's construction and searched forwards,
backwards and from a changed edge for its anchors; and propagation of
affected keys to the least fixpoint, semi-naively (ADR 0172). Generic over
the caller's node and label, referencing Varve.Rdf only.

Properties against naive oracles, 3,000 cases each: paths against a
relational oracle over graphs with cycles, the preimage, anchors that
find every start whose image an edge changes, boundedness, components by
reachability, refused cycles, and propagation as the least fixpoint. A
sabotaged anchor search fails the anchor property.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
Stratification builds Varve.Derivation's dependency graph from the
shapes' references and sh:targetWhere, and takes its components and
refused cycles from it (ADR 0150 amended); a SHACL path compiles once to
a regular-path automaton, and the data graph is the graph it walks, with
a backward search for the anchors to come (ADR 0152 amended). The
semantics is unchanged: Varve.Shacl's 21 tests, the path differential
against the evaluator, and the W3C SHACL suites (622 of 650 pass, the 28
failures are the existing exemptions) all hold.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
IncrementalValidation.Compare finds the (shape, focus node) pairs a delta
affects by each changed triple's anchors per ADR 0171's table, propagated
along the shapes' dependency graph to the least fixpoint through
Varve.Derivation, with the nodes the delta may move into or out of each
shape's targets; every search runs before and after the delta. It
validates those pairs before and after, and reports the results it
introduces: an identity new among the affected pairs is checked up the
sh:property chains against the pairs it did not affect (ADR 0169's R1).
ShapesGraph.Classify gives each shape incremental, scoped or full, with
the constructs and sections that decide it. Extension constraints may
state their footprint through IShaclFootprint; one that does not is
full. The report is restricted to affected pairs (ADR 0154 amended), and
a union data graph no longer repeats a triple held in two graphs for
sh:closed.

The property: over generated shapes graphs using every classified
construct (targets of each kind, paths with closures, inverse, sequence
and alternative, the value, cardinality, string, pair, logical,
shape-based, list, closed, root-class and unique-values components,
positive recursion, a property shape shared by several node shapes) and
generated histories of commits with retractions, the whole report before
a commit without the comparison's results before and with its results
after equals a full validation after it, byte for byte, and the results
it introduces are exactly the full reports' new identities. 500
histories on the land gate; 1,686 histories and 7,697 positions in the
long setting, no counterexample. Removing the path anchors, the
propagation's transfer, or the provenance check each fails it.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
A SPARQL-based constraint or component states its footprint (ADR 0171
§4): the join graph of its algebra, its variables the vertices and its
triple patterns and paths the edges, wherever they stand (OPTIONAL,
MINUS, EXISTS, NOT EXISTS, sub-selects, whose unprojected variables are
their own). When every edge is joined to $this, or to $value in an ASK
validator, a changed triple's anchors are found by evaluating the join
path from the patterns it matches back to the root, filters dropped;
otherwise the constraint is full. A GRAPH block, a negated property set
and an unjoined pattern make it full.

ShaclOptions.RefuseNonDeterministicFunctions, which a shapes binding
sets, refuses at load a query calling NOW(), RAND(), UUID() or
STRUUID(), or BNODE() whose value can reach a result (ADR 0169); the
extension sees it on its shapes graph context.

The property: SPARQL-based constraints from nine templates, analysable
and not, an ASK-validated component and a $PATH query, over generated
histories of commits: the whole report before a commit, less the
comparison's results before, plus its results after, has a full
validation's results after it. 300 histories on the land gate, 3,000 in
the long setting, no counterexample once ?value was taken as a root only
where it is pre-bound (the property found that). Emptying the anchors
fails it. The W3C SHACL suites hold: 622 pass, the 28 failures are the
existing exemptions.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
Varve.Shacl.Store, layer 5 (ADR 0169): ShapesGate implements the
pre-commit validator contract over Varve.Shacl and Varve.Shacl.Sparql.
Each binding's shapes graph is a named graph of the dataset, read as of
the overlay and kept between commits until a delta touches it; its data
graph is the union of the bound graphs without the shapes graph and the
varve: graphs. Under reject, a commit is refused when its affected focus
nodes gain a sh:Violation result the pinned position does not have, as
shapes-violated with each finding by focus node and path; a shapes change
is never refused for existing data, and its data is compared under the
new shapes; a reject binding whose shapes are full without
varve:allowFullEvaluation refuses as binding-not-incremental, at apply
time through Check and failing closed at commit time; a shapes graph that
does not load, or calls a non-deterministic function, is shapes-invalid.
Every accepted commit under report or reject records one attachment per
binding, in the order of their IRIs: the binding, varve:affectedReport,
and the SHA-256 of the affected report's canonical N-Quads (ADR 0173).
Bulk commits are gated alike and carry no attachment (ADR 0081).

Varve.Store: ValidationVerdict.Accept takes a list, and Attachments gives
it; the sequencer and the bulk path record each one.

Tests: a reject binding against a full-validation oracle over generated
shapes and commits that replace the shapes, alone or with data — refused
exactly when the full report gains a violation under the commit's shapes
(150 histories on the land gate, 1,500 in the long setting; the sticky
rule the maintainer rejected fails it) — the attachment's digest
reproduced from the log after the commit, with a blank node the commit
creates, and each rule on its own.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
shapes-violated, binding-not-incremental and shapes-invalid, each 422,
catalogued with their members and a page, and validator types: a gate's
two-term report is answered as its own type (ADR 0119 amended, ADR 0169).
The catalogue test triggers all three over HTTP through a gate whose
shapes graph is a writable graph and whose data graph is the default
graph; Varve.Protocol's 108 tests pass.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
The conflicts were additive: ADRs 0017 and 0058 keep the Management
plane's 2026-10-19 amendment and this branch's 2026-10-20 one, in that
order; the validator problem types are the union; the public API
baselines take both sides' lines. The solution builds clean but for the
unaccepted rulings.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
ADR 0170 in Varve.Management, by a dated amendment of ADR 0166: a
varve:ShapesBinding carries its data graphs in the grant vocabulary's
graph-set terms on its own node, a varve:validationMode of varve:Reject,
varve:Report or varve:Off, and varve:allowFullEvaluation and
varve:subClassOfInShapesGraph. The code validator refuses one without a
mode or a graph set, with a graph template, or naming its own shapes
graph. The plan's separate varve:validates node is not a term: the code
validator refuses blank nodes, so the binding node carries the set as a
grant does; ADR 0170 says so. docs/vocabulary.md gains the terms and
varve:affectedReport.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
A data write's validators at its pinned management position now carry
the dataset's shapes gate beside the order of positions (ADRs 0165,
0169): the bindings are the ones declared at the position the write was
authorised under, and the gate is kept per dataset while they stay the
same, so its shapes stay loaded. Before a management commit, the apply
path checks each binding it declares against its dataset where this
instance holds it (R4), and answers binding-not-incremental or
shapes-invalid; a dataset held elsewhere fails closed at its writer
(R6). GET /manage/datasets/{name}/bindings/{binding} serves a binding at
the applied position with its shapes' classification (ADR 0171).

IManagementPlane gains CheckBindings and DescribeBinding with defaults,
and Varve.Protocol references Varve.Shacl for the classification it
serves. Server tests: a reject binding refusing a new violation over
HTTP, accepting a conforming write, refusing a shapes commit that makes
its shapes full, and described; a reject binding over full shapes
refused at apply time and accepted with varve:allowFullEvaluation. The
server's 105 tests pass, 11 needing CI's services skipped.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
ADR 0170 puts the graph-set terms on the binding node.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
Under SHACL 1.2 Core §6.4's parameter every validation run walked
rdfs:subClassOf in the shapes graph again, a scan of an in-memory
dataset per step. The shapes graph does not change once loaded, so its
steps are kept on the source. With REC 4.1.0 bound at S, the gate's
time per commit falls from 13.2 ms to 4.5 ms.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
Part 2a's early signal for the REC gate: Brick's three SPARQL
constraints name prefixes no file declares, so the shapes graph is
refused as it stands; without them nothing is classed full, and the S
portfolio's head violates only rec:leasor's minimum on its leases. The
full validation runs under VARVE_LONG_PROPERTIES.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
REC S or M on the file backend, the history but its last 240 commits
loaded with REC 4.1.0's shapes, then those commits under a reject
binding or none; --shapes-gate-affected gives the gate's time per
commit and per affected focus node.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
…nd part 2a's state

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
README names preview.11 and the two new packages; the roadmap splits
SHACL part 2 into 2a (#138) and 2b (#13), records part 2a's gate, and
marks incremental SHACL resolved by ADR 0171.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
… S and M

The run's log is its console output, the CS0618 warnings of the build
left out; its exported reports were lost to a cleanup and are not
reconstructed.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
5,000 under VARVE_LONG_PROPERTIES. The project runs in 17 s on Linux,
REC's shapes loaded once in it.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
The descriptor names #138, the first half of SHACL's part 2, with
storage-format 1, delta-format 1 and ADRs 0169-0173. #13 is named in the
summary, not the basis: part 2b closes it. CHANGELOG.md is its
projection, and the README's Status names it, with the image tags at
preview.11. preview.10's tag is not yet cut, so release --check counts
from preview.9 and lists #134's ADRs 0163-0166 and 0168 as first shipped
here; they clear when preview.10 is tagged. If another release lands
first, this renumbers at landing (ADR 0102).

Closes #138
Refs #13
Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
The opening message, the brief, the plan and the approval verbatim;
what shipped by ADR, where it departed from the plan, the REC findings,
the properties with their counts, durations, benchmarks and the API.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
Hafeok and others added 2 commits October 20, 2026 19:22
REC requires rec:leasor on a rec:Lease (sh:minCount 1, sh:class
rec:Agent), and the generator never wrote it, so the S head had 141
violations. Each site now gets an owner, a rec:Company made with the
site, and every lease names it as its leasor. No random draw changes,
so the history is the same commits with these quads added.

RecPortfolio.Version names the output, 2, and the pinned hashes move:
S 66218df6…, M fb544c09…, L c004a342…. The S head conforms to REC's
shapes, and the long REC tests now refuse a violation made by an
explicit mutation (a lease's leasor retracted) and accept the
generator's next commit, never relying on a generator defect.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
0155's amendment names the single IncrementalValidation.Compare and
what it returns, in place of the plan's two members, with
FootprintContext and the context's refusal flag; 0170 records that the
graph-set terms on the binding node were accepted on #140.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA

Hafeok commented Oct 20, 2026

Copy link
Copy Markdown
Owner Author

Draft for the maintainer to file with REC upstream (RealEstateCore, rec repository). It is not in this repository.


Title: REC 4.1.0's SHACL release: Brick's SPARQL constraints use prefixes the shapes graph never declares, and implicit class targets need owl:Class rdfs:subClassOf rdfs:Class

Files: the SHACL release files of v4.1.0:

File SHA-256
rec.ttl 3cdda54a…ce74b
Brick+patches.ttl 7bbe0d34…9158b
recimports.ttl 58cebae4…22983

1. Undeclared SPARQL prefixes. SHACL-SPARQL (SHACL 1.0 §5.2.1; SHACL 1.2 SPARQL, "Prefix declarations") resolves a query's prefixes only from the sh:declare triples on the nodes that sh:prefixes names. A processor must not fall back on the file's Turtle @prefix lines.

Three sh:SPARQLConstraints in Brick+patches.ttl name namespace nodes with sh:prefixes. None of the three files has a single sh:declare. So a conforming processor cannot parse any of the three queries, and it refuses the whole shapes graph.

Two of the queries also use a prefix they don't even list:

Shape sh:prefixes Prefixes the query uses Used but not listed
bsh:DeprecationRuleForInstances rdf:, brick: rdf:, owl: owl: ($this rdf:type/owl:deprecated true)
bsh:TimeseriesReferenceOnPointsConstraint rdf:, rdfs:, ref: ref:, rdf:, rdfs:, brick: brick: (brick:Point)
bsh:VirtualMeterRule rdf:, rdfs:, brick: brick:, rdf:, rdfs: none

Suggested fix. Two parts:

  1. Declare each prefix on the node that sh:prefixes already names. For example:

    rdf:   sh:declare [ sh:prefix "rdf" ;   sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] .
    rdfs:  sh:declare [ sh:prefix "rdfs" ;  sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] .
    owl:   sh:declare [ sh:prefix "owl" ;   sh:namespace "http://www.w3.org/2002/07/owl#"^^xsd:anyURI ] .
    brick: sh:declare [ sh:prefix "brick" ; sh:namespace "https://brickschema.org/schema/Brick#"^^xsd:anyURI ] .
    ref:   sh:declare [ sh:prefix "ref" ;   sh:namespace "https://brickschema.org/schema/Brick/ref#"^^xsd:anyURI ] .
  2. Add the missing sh:prefixes: owl: on bsh:DeprecationRuleForInstances, and brick: on bsh:TimeseriesReferenceOnPointsConstraint.

Brick's own build declares these prefixes; the merged Brick+patches.ttl appears to have lost them.

2. Implicit class targets. About 1,445 Brick shapes are typed both owl:Class and sh:NodeShape. SHACL's implicit class target (§2.1.3.3) applies only to a shape that is an rdfs:Class, that is, a SHACL instance of rdfs:Class through rdf:type/rdfs:subClassOf*. None of the files states owl:Class rdfs:subClassOf rdfs:Class, so a processor that doesn't import OWL gives these shapes no targets. Adding that one triple, which is OWL's own vocabulary, to the release would make the targets apply wherever the files are used.

How this was found. Varve's SHACL 1.2 processor loaded the three files as one shapes graph, an RDF merge with each file's blank nodes kept apart. It reported three failures:

ExtensionFailure: UndeclaredPrefix: The prefix 'rdf:' is not declared.
ExtensionFailure: UndeclaredPrefix: The prefix 'ref:' is not declared.
ExtensionFailure: UndeclaredPrefix: The prefix 'brick:' is not declared.

Without those three constraints, and with the owl:Class triple added, the rest of the shapes load and validate.


What happens in Varve meanwhile:

  • Part 2b's REC gate adds both fixes, the sh:declare triples above and the owl:Class triple, in its shapes composition. ADR 0176 will record the exact triples.
  • RecStandaloneTests.BricksSparqlConstraintsDeclareNoPrefixes keeps pinning that REC 4.1.0 as vendored is refused.

Generated by Claude Code

…ion 2

Version 2's head conforms to REC's shapes, so no held commit is
refused; the version-1 run's log goes, and the version-2 run replaces
its numbers.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA

Hafeok commented Oct 20, 2026

Copy link
Copy Markdown
Owner Author

CI on the current head: every job that builds the solution fails at the build step, and only on CS0618.

  • The cause is the unaccepted decisions of ADRs 0169–0173: ADR 0066's design is that a pull request is red only on those warnings.
  • I checked two of the logs, build (ubuntu-latest) (64 × CS0618) and browser storage in Chromium (60 × CS0618). Neither has any other error code.
  • The other red jobs build the same solution: conformance, auth, samples, storage/fleet, container, native aot and decision report.
  • The gates that don't build are green: dco, release pending, rec generator on both architectures, durability and bicep.
  • agent review is waiting on your approval of the head.

Nothing else needs fixing in this pull request. The test jobs, and with them the Windows and macOS durations of the new test projects, run once the decisions are accepted. I'll report those durations here then.


Generated by Claude Code

…sion behind each REC number

No commit refused at S or M. Under the gate 7.62 ms per commit at S and
11.86 ms at M, against 0.51 and 0.86 ms without, on a slower run than
the first; 2.6 affected focus nodes per commit at both sizes. The README
states that the Object storage section's REC numbers (#92) were taken
on version 1, that #110 has none yet, and that this section's are
version 2's. The run's log and exported reports are kept.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
The maintainer's answer verbatim, the generator's version 2, the
mutation test, the benchmarks on version 2 and the upstream draft.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
…SHACL part 2a

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA

Hafeok commented Oct 20, 2026

Copy link
Copy Markdown
Owner Author

v0.1.0-preview.10 is tagged, so I merged main into this branch (1db38aa; the only incoming change is release.yml's id-token: write). #135 hasn't landed, so the descriptor stays v0.1.0-preview.11.

Locally, all six of these pass: release, changelog, status, release-pending, issue-refs and dco. The item in "What is left" about the release gate is resolved.

What remains red is the build's CS0618, until ADRs 0169–0173 are accepted, and agent review on the new head 1db38aa.


Generated by Claude Code

…art 2a

#135 took preview.11, so this part's descriptor is renumbered to
v0.1.0-preview.12 and the changelog re-projected. README Status names
preview.12 first, with preview.11 and preview.10 behind it, and the image
tags at preview.12; AGENTS.md's State keeps part 2a first, then Extent
maintenance. The benchmarks README keeps #110's section before this
part's, and states that #110's REC runs were taken on generator output
version 1 too.

Refs #138
Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
@Hafeok Hafeok changed the title SHACL part 2a: commit-time gating under a shapes binding, incremental validation (v0.1.0-preview.11) SHACL part 2a: commit-time gating under a shapes binding, incremental validation (v0.1.0-preview.12) Oct 21, 2026

Hafeok commented Oct 21, 2026

Copy link
Copy Markdown
Owner Author

Renumbered to v0.1.0-preview.12. #135 (Extent maintenance) landed as preview.11, and I merged main in as cb46087. Where the description says preview.11 for this release, read preview.12.

What the merge changed:

Gates: locally, decision-sets, changelog, status, issue-refs and dco pass. release fails the same way it did before preview.10 was tagged: v0.1.0-preview.11 isn't tagged yet, so with two proposed descriptors the checker swaps the ADRs between them. It should clear once preview.11 is cut, and I'll re-run it then.

What remains red is CS0618, until ADRs 0169–0173 are accepted, and agent review on the new head cb46087.


Generated by Claude Code

@Hafeok

Hafeok commented Oct 21, 2026

Copy link
Copy Markdown
Owner Author

approve cb46087

Hafeok commented Oct 21, 2026

Copy link
Copy Markdown
Owner Author

v0.1.0-preview.11 is tagged. On cb46087, which already contains main, the release gates now pass locally: release, changelog, status and release-pending.

The merged code's tests pass too: Protocol 113, Shacl.Store 13, Server 106 and Store 277, with none failing.

The only thing keeping CI red is CS0618, until ADRs 0169–0173 are accepted.


Generated by Claude Code

Accepts the decision sets of SHACL, part 2a: commit-time gating under a
shapes binding (0169), the shapes binding vocabulary (0170), which shapes
are incrementally maintainable (0171), Varve.Derivation (0172), and the
validation report on the commit (0173).

Refs #138

Signed-off-by: Emil Okkels Klein <emil@okkels-klein.dk>
@Hafeok

Hafeok commented Oct 21, 2026

Copy link
Copy Markdown
Owner Author

approve 0bb0d34

Hafeok and others added 2 commits October 21, 2026 19:11
pack (NU5019) needs a README for every packed project; Varve.Shacl.Store
had none. It states the binding, the three modes, what is validated, and
where bindings come from on a server.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA
… the pack fix

The new test projects' durations from CI's run at 0bb0d34 on Ubuntu and
Windows (CI runs no macOS test job); preview.12 after #135; the
decisions accepted; the README pack required; and the declined request
of a session hook to rewrite the commit identity.

Refs #138

Signed-off-by: Emil Okkels Klein <hannibal.okkels@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtkE4cRmAMRARHDu5Y9ZA

Hafeok commented Oct 21, 2026

Copy link
Copy Markdown
Owner Author

CI after the acceptance (0bb0d34): every test project built and passed on Ubuntu and Windows. The only failure was in dotnet pack: Varve.Shacl.Store had no README.md (NU5019). I've added one in fd94e29, pack passes locally, and it's pushed with the traceability update as 3dbffa7.

Durations of the new test projects, from that run (#126). CI runs these projects on Ubuntu and Windows only; the macOS job runs the durability suite alone, so there's no macOS figure.

Project ubuntu-latest windows-latest
Varve.Derivation.Tests (new) 0.97 s 1.23 s
Varve.Shacl.Store.Tests (new; the two long REC tests skipped) 10.9 s 12.1 s
Varve.Shacl.Tests 4.6 s 4.9 s
Varve.Shacl.Sparql.Tests 5.2 s 6.4 s

The new head 3dbffa7 needs your approve comment for agent review.


Generated by Claude Code

@Hafeok

Hafeok commented Oct 21, 2026

Copy link
Copy Markdown
Owner Author

approve 3dbffa7

Hafeok commented Oct 21, 2026

Copy link
Copy Markdown
Owner Author

pipeline (devcontainer) failed in conformance, and the failure is not this PR's.

The W3C ratchet itself is clean: 6,141 in the baseline, 40 exempt, 0 regressed, 0 newly passing. What failed is a single property, CanonPropertyTests.Without_blank_graph_names_isomorphism_holds_exactly_when_the_canonical_forms_are_equal, with CsCheck seed 1Axba7DbGjr2.

Why it isn't this PR's. The property exercises only Varve.Rdf's RdfCanonicaliser and Varve.Turtle, and this PR changes neither. With CsCheck_Seed=1Axba7DbGjr2 it fails the same way on main at 0b02803. CsCheck draws a new seed on every run, so it shows up only occasionally; both conformance jobs passed in this same run.

What it is. A real counterexample to the claim in docs/spec/rdf-canon.md §6: that when every graph name is an IRI, isomorphic datasets get equal canonical forms.

  • The two datasets are isomorphic. I checked the mapping by hand.
  • Their canonical forms differ only in which of two leaf blank nodes gets c14n3 and which gets c14n4.
  • The two leaves are distinguishable only by which graph their two edges sit in. Each has one edge in <http://example.org/é> and one in the default graph, to the same two already-canonical nodes, c14n1 and c14n2, the other way round.
  • RDFC-1.0 can't see that. Hash Related Blank Node (§4.7.3) hashes the position, the predicate and the related node's identifier, but not the graph of the quad that relates them. So the two leaves get equal N-degree hashes, and the input order breaks the tie.

As far as I can tell, RdfCanonicaliser implements the spec as written. The documented "holds with IRI graph names" is too strong, so the property is testing a claim RDFC-1.0 doesn't make.

No fix exists yet. It belongs in its own change, not this PR:

  • correct §6 of rdf-canon.md;
  • narrow the property, either by keeping the generator from producing a blank node whose relations differ only by the graph of the relating quad, or by counting such pairs and reporting them as the blank-graph-name direction already does;
  • add this pair as a pinned example.

I'll re-run the failed job once, after build (windows-latest) finishes.


Generated by Claude Code

Hafeok commented Oct 21, 2026

Copy link
Copy Markdown
Owner Author

build (windows-latest) passed too, so on 3dbffa7 every check is green except pipeline (devcontainer). That one hit the RDFC property defect described above, which also reproduces on main.

I tried to re-run the failed job, but the session's token isn't allowed to (403 Resource not accessible by integration). It needs a re-run from the Actions page, run 39662117839, "Re-run failed jobs". A new seed will almost certainly pass.


Generated by Claude Code

This branch was successfully deployed

1 active deployment
gates — 3dbffa71 Deployed Oct 21, 2026 by Hafeok via agent review (judge) #336
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.

1 participant