Repository navigation
Conversation
|
This also fixes #1991, which the description doesn't reference. All three repro cases from the issue fail on main (
Fixes #1991 Could you also put |
|
Filed #2069: a branch created from a ledger that has no index yet reads none of its source's ledger config. On replay, the source's config lands in a user graph, and the branch's config graph is empty. This PR didn't cause it, but it widens it.
Could you list it with the breaking change and add |
bplatz
left a comment
There was a problem hiding this comment.
Review of fae15f997 (with main merged). Where a comment says "reproduced", I ran it against both main and this branch. #1991 and #2069 are in separate comments above.
Items without a line anchor:
- Upgrade visibility:
- Nothing warns at write time or ledger load when a ledger holds shapes and SHACL is off.
- MCP's ledger-info output (
fluree-db-server/src/mcp/format.rs) doesn't includeconfigDiagnostics, and server-routedfluree infoprints the raw JSON. fluree model entity definestates the rule but doesn't check this ledger's posture.
- GraphQL:
sh:minCount ≥ 1still makes a field non-null (fluree-db-graphql/src/schema/build.rs:430). On a ledger without SHACL config nothing enforces it, so a record written without the field reads back as a non-null error.docs/cli/graphql.mdanddocs/query/graphql.mdstill say SHACL applies unchanged. - Missing from the behavior-change list:
@reversecontext terms now write the opposite direction. That's correct per spec, but data written by older versions stays inverted, so a delete through the same term no longer matches it.- Graph names no longer resolve through
@vocab; they're refused as relative. opts.shapessent with an identity is now a 400.
- Duplicated predicates:
- Three definitions of a config field:
is_config_field, anyf:predicate ingraphs_with_config_fields, andcarries_config_settings. - A graph IRI that can't be resolved is handled two ways:
stored_shacl_policyskips it, whileinline_shapes_policyandresolve_per_graph_unique_sidsfall back to the ledger-wide config. - The
@graphselector block is duplicated inparse_node_mapandparse_nested_node_map.
- Three definitions of a config field:
- Coordination: #2007 adds its own
fluree-db-core/src/dataset_ref.rswith a differentGraphIri/RefError. Whichever lands second has to reconcile them. - Description:
| // the same key does in an insert or delete (`resolve_where_graph`), so | ||
| // one document means one graph by it. (It used to be read as a predicate | ||
| // named `@graph`, so the pattern matched nothing.) | ||
| if let Some((graph_key, raw)) = node_graph_selector(map, context)? { |
There was a problem hiding this comment.
A CONSTRUCT template node with @graph now becomes a Graph pattern here, and parse_construct_items (parse/mod.rs, the filter that keeps only Triple / EdgeAnnotation) drops it. Reproduced: "construct": [{"@id": "?s", "@graph": "ex:g2", "ex:label": "?n"}] returns "@graph": [] with no error, while main returns the triples. Nothing tests a CONSTRUCT template with @graph.
There was a problem hiding this comment.
Thanks, a real regression. A template node's own @graph is now GRAPH sugar in a template too: parse_construct_items keeps the Graph pattern (and the triples and annotations inside it) instead of filtering it out, so your "construct": [{"@id": "?s", "@graph": "ex:g2", "ex:label": "?n"}] writes its triples into ex:g2, exactly as ["graph", "ex:g2", {…}] and SPARQL's CONSTRUCT { GRAPH ex:g2 { … } } do (77592cadd). Test: jsonld_node_level_graph_in_a_template_names_the_output_graph compares both JSON-LD forms with the SPARQL one as N-Quads. The CONSTRUCT page shows the node form, and the compatibility notes list the change from main, where @graph was read as a property and the triples went to the default graph (cca4934bd). The "construct": true shorthand had the same filter, so a graph-scoped where node matched and wrote nothing there too; it now goes through the same function (7125b79dc, jsonld_construct_shorthand_keeps_a_where_nodes_graph).
| // A node-level `@graph` in `where` can name a `fromNamed` alias, which | ||
| // must stay the alias rather than expand as an IRI. | ||
| let ctx = JsonLdParseCtx::new(context.clone(), path_aliases, parse_policy).with_graph_names( | ||
| GraphNameEnv { |
There was a problem hiding this comment.
A ["query", …] subquery in a where is parsed through here with a fresh GraphNameEnv (no ledger, no outer aliases, no ledger_default_graph), so it resolves graph names differently from the enclosing where. Reproduced: in an update with "graph": "ex:g", {"@id": "?s", "@graph": "default", "ex:p": "?o"} reads the default graph at the top level but ex:g inside ["query", {…}]. Passing the parent's environment through would fix it.
There was a problem hiding this comment.
This was fixed after the head you reviewed: a ["query", …] subquery takes the naming environment of the where it sits in, the ledger, the fromNamed aliases and the ledger default graph included (2d5824671). subquery_names_graphs_as_its_where_does is your repro: an update with "graph": "ex:g" whose subquery reads {"@id": "?s", "@graph": "default", "ex:p": "?o"} moves the default graph's data into ex:g (with and without a restricting fromNamed), and the subquery sees the update's aliases.
| // A reverse property's edge points at this node, and a nested | ||
| // value's properties are this node's: an annotation inside either | ||
| // would be lowered against the wrong edge. | ||
| Some(keyword @ ("@reverse" | "@nest")) => { |
There was a problem hiding this comment.
This refuses annotations under the @reverse keyword only. A context term defined with "@reverse" falls through. Reproduced with {"parent": {"@reverse": "ex:child", "@type": "@id"}} and {"@id": "ex:kid", "parent": {"@id": "ex:mom", "@annotation": {"ex:since": 2020}}}: the edge is stored as ex:mom ex:child ex:kid (now correct), but the annotation reifies ex:kid ex:child ex:mom, which doesn't exist. It commits without error in the default graph. On main both used the inverted direction, so they matched.
There was a problem hiding this comment.
Also fixed after that head (4c5e4c050): one edge-key classifier, shared by the insert and delete walkers, tells a keyword, a reverse-defined term and a forward term apart, so an annotation on a value of a reverse term reifies the edge as stored (ex:mom ex:child ex:kid), and the same document deletes it. Your exact context ({"parent": {"@reverse": "ex:child", "@type": "@id"}}) is now a case in annotation_on_a_reverse_property_reifies_the_stored_edge, next to a term without @type and the @reverse keyword.
| /// every graph (ledger-wide and each graph override) and the shapes source | ||
| /// holds at least one `sh:NodeShape` or `sh:PropertyShape`. A config that | ||
| /// enables SHACL for one graph only is configured. | ||
| async fn shapes_without_enforcement( |
There was a problem hiding this comment.
This counts only subjects typed sh:NodeShape / sh:PropertyShape, but the compiler also compiles untyped shapes, which the old rule enforced. Reproduced: ex:PersonShape sh:targetClass ex:Person ; sh:property [sh:path ex:name ; sh:minCount 1] with no config enforces on main. Here it stops enforcing and configDiagnostics is empty.
Separately, it also reports shacl-not-configured when the config explicitly sets f:shaclEnabled false.
There was a problem hiding this comment.
Untyped shapes not being enforced without config is the explicit-only rule itself (the breaking change), but the diagnostic and the load-time warning should still have flagged them, and now do: a subject with a SHACL target predicate (sh:targetClass, sh:targetNode, sh:targetSubjectsOf, sh:targetObjectsOf) counts as a shape, typed or not. The load gate reads the index's property stats and the novelty for those predicates, so it still doesn't read the ledger for a ledger without shapes. And shacl-not-configured is no longer reported when the config sets f:shaclEnabled false (ledger-wide or for a graph): that's a posture, not a gap. (f25bfb626) Tests: diagnostics_report_shapes_without_enforcement (an untyped shape reported; switched off not reported) and the load-warning binary (untyped, in novelty and indexed; switched off silent). fluree info's page and the compatibility notes say what counts (cca4934bd).
| /// `"default"` writes. It contains a space, which no IRI can, so no graph | ||
| /// written under an IRI has it; an update refuses it as a graph name or VALUES | ||
| /// value it writes ([`reserved_graph_name_refusal`]). | ||
| pub const LEDGER_DEFAULT_GRAPH: &str = "@ledger default"; |
There was a problem hiding this comment.
Suggestion: #2005 already makes urn:default name the ledger's default graph in an update's WHERE (stage.rs registers it as a non-enumerable alias for g_id 0). Resolving "default" in an update where to urn:default would remove the need for this constant, its refusals (VALUES, fromNamed, node-level and ["graph", …]), the registered-name collision error, and the Turtle \u follow-up.
There was a problem hiding this comment.
Agreed, and done after the head you reviewed: a where node's "@graph": "default" resolves to urn:default, #2005's name, and the sentinel, its refusals and the registered-name collision are gone (2d5824671).
| &mut view, | ||
| &mut ns_registry, | ||
| &rustc_hash::FxHashMap::default(), | ||
| WriteChecks { |
There was a problem hiding this comment.
The plain-Turtle branch passes only tracker here. The txn_opts that stage_turtle_insert_with_opts receives (inline shapes, unique properties, validation mode) reach the checks only on the TriG fallback. So insert_turtle(..).txn_opts(..) with inline shapes commits unchecked. That predates this PR, but it contradicts "read on every surface".
There was a problem hiding this comment.
Fixed: the plain Turtle lane now passes the caller's inline shapes, requested validation mode and inline uniqueness constraints (and the policy context, so policy-scoped inline constraints are refused as elsewhere) to the shared checks, as TriG does (fdaa4fdd5, 5d27ae6a9). Tests: inline_unique_property_applies_to_turtle_with_and_without_graph_blocks and inline_shapes_apply_to_turtle_with_and_without_graph_blocks (each refuses in reject mode and commits in warn mode, plain and TriG).
| /// Where inline shapes are permitted, a requested warn mode applies to them: | ||
| /// the violation is logged and the write commits. | ||
| #[tokio::test] | ||
| async fn inline_shapes_honor_a_requested_warn_mode() { |
There was a problem hiding this comment.
This only asserts that the write commits, which also holds with the inline pass removed. Asserting the warning, or that the same request fails in reject mode, would pin it.
There was a problem hiding this comment.
Fixed: inline_shapes_honor_a_requested_warn_mode now also sends the same write in the default mode and asserts the SHACL violation, so it fails with the inline pass removed (5d27ae6a9).
| /// returns. | ||
| #[cfg(feature = "shacl")] | ||
| #[tokio::test] | ||
| async fn repair_recipe_restores_a_split_config() { |
There was a problem hiding this comment.
The split group is seeded as the IRI urn:config:shacl. A ledger split the documented way (#1979) has an anonymous group, so a blank node. One case with a blank-node group would cover the ledgers that will actually run this recipe.
There was a problem hiding this comment.
Added: repair_recipe_restores_a_split_config has an anonymous-group case (the group a blank node, its fields in the default graph), and the docs recipe repairs it verbatim (b55c02aa0).
| Err(TransactError::ShaclViolation(report).into()) | ||
| } | ||
| Some(rejection) => { | ||
| let _ = describe; |
There was a problem hiding this comment.
For uniqueness and config-guard refusals describe is dropped, so a rebase that stops on one doesn't say which commit. No rebase test covers either.
There was a problem hiding this comment.
Fixed for config refusals: a rebase stopped by one now says replaying commit t=N was refused: … (409), like a SHACL stop (f89b0a638). A uniqueness violation is returned as its typed error, which has no free-text field to carry the commit, so it doesn't name the commit yet; that's in #2077. Tests: rebase_refused_by_a_config_check_names_the_commit and rebase_rejected_when_replay_violates_uniqueness.
| - Nested objects | ||
| - Arrays | ||
|
|
||
| Fluree extension: a node's own `"@graph": "<graph IRI>"` writes the node, and every node nested in it, to that graph (`"default"` and `"config"` name the default graph and the ledger's config graph). `@index` is accepted and ignored. Other `@`-keywords on a node object are refused. |
There was a problem hiding this comment.
Unknown @ keywords are refused here, where the JSON-LD 1.1 expansion algorithm drops them, so "Fully compliant" above should list this as a deviation. Also, the @json literals bullet below now hangs off this paragraph instead of the list.
There was a problem hiding this comment.
Fixed: the JSON-LD status is now "compliant, with the deviations listed below", and the deviations are listed (unknown @-keywords refused, plus two scoped-context differences a corpus run found, which are follow-ups). The @json bullet is back in the list (68bc66e33).
bplatz
left a comment
There was a problem hiding this comment.
Approving: the shared check step, fast-forward validation and graph scoping are a solid direction, and CI is green with main merged (fae15f997).
Please work through the review comments before merging, in particular:
- the four reproduced regressions: CONSTRUCT dropping
@graphnodes, reverse-term annotations, subquery graph names, and the untyped-shape diagnostic - #2069, which this PR widens
Fixes #1991in the description
The rest can be fixed here or filed and linked as follow-ups.
Times fluree_db_transact::parse_transaction alone (context handling, edge-annotation lowering, expansion, template construction) with no ledger, staging or commit, so parser changes are attributable. Four shapes: a flat insert envelope, depth-3 node trees under a node-level graph selector, an update of ["graph", g, node] items, and annotated edges. Registered in regression-budget.json and the bench categories table.
graph_shape::classify_graph_value decides what a node's @graph value is: a graph selector (a string, a bare {"@id"} object, or a one-element array of either), JSON-LD 1.1 named-graph content (node objects), or invalid (numbers, booleans, null, value objects, nested arrays). doc_shape classifies a top-level object as an envelope, a named graph or a node from the same answer, and is_graph_key recognizes @graph, a context alias of it, and the legacy bare `graph` key only when the context does not define `graph` as a term. The expander adopts both: a context term `graph` is now an ordinary property at every level instead of being hijacked as @graph (the triple was dropped and the node re-routed), and a top-level @graph is an envelope only when it holds node objects and the object has no @id; a top-level graph selector expands as a node.
A JSON-LD node's statements, and those of every node nested in it, now land in the node's scope: its own graph selector if it has one, else the enclosing node's. Before, only a node's own triples followed its "@graph" selector; nested nodes, anonymous nodes and @list items fell back to the default graph (or the update's "graph" key), so the nested config groups of #1979 landed in the default graph, a delete or upsert of a nested node under a selector retracted in the wrong graph, and list members' properties split from their list. - GraphScope (ir.rs) is passed down the recursion by reference; every value/list/node function takes one, so a nested node cannot be emitted without a scope. WriteGraphs::scope is the only way to get a named scope, so every graph a template names is registered as a write target. The ambient TemplateParseCtx::default_graph and its save/restore are gone. - One graph-name resolver for node selectors, ["graph", g, ...] items and the update "graph" key: a variable (update templates only) is the WHERE binding (TemplateGraph::Var, as SPARQL GRAPH ?g), a fromNamed alias its IRI, then the keyword table (dataset_ref::GraphSel, new in fluree-db-core): "default" is the default graph everywhere, "config" the ledger's config graph, "txn-meta" is refused; anything else expands @id-style and must be an absolute IRI. A variable graph used to write a graph literally named "?g"; "default" meant a graph named "default" outside the update key; relative names minted relative-IRI graphs. - Graph insert / sync parse with the request's graph as the fixed root scope instead of re-homing templates afterwards; a selector naming any other graph is refused. - parse_transaction / parse_graph_insert / parse_sync_transaction take the target ledger id (for the "config" keyword).
The #1979 recipe from docs/ledger-config/writing-config.md, verbatim: all four statements land in #config and the configured SHACL refuses the forbidden record. Nested @id, anonymous and @list-item nodes land in the selector's graph (P1, P10'); a node selector beats the update graph key for its subtree (P7c); delete and upsert of a nested node act in its graph (P7e, P9); a variable graph writes the WHERE-bound graph; the "config" keyword writes #config. Each compares against the SPARQL spelling of the same write where one exists. Renames it_config_graph::config_write_json_ld to config_write_trig_iri_groups: its body writes TriG.
A node object with @graph content, {"@id": G, "@graph": [nodes]}, is a JSON-LD 1.1 named graph (section 4.9): the content nodes are written to G and the node's own properties stay in the enclosing scope. Named graphs nest (innermost wins) and may appear as property values. Before, the content was dropped and the first content node's @id was read as the node's graph selector; a property-bearing object @graph lost its data the same way. - A named graph needs an IRI name: a blank-node or variable name, or a graph object without @id below the top level, is refused. - Graph insert / sync refuse named-graph objects (the request names the graph). {"@id": G, "@graph": []} is no longer mistaken for the explicitly empty envelope a sync may clear a graph with. - txn-meta extraction and the reserved-key strip use doc_shape: only an envelope's top-level keys are metadata. A single node carrying a graph selector (P10e) and a named graph's own keys are data; they used to be written as data and also as txn-meta on the commit. - doc_shape and the expander: at the top level, an array @graph without @id is an envelope even when it holds one bare {"@id"} node, as every reader has always treated it; a single property-bearing object @graph is an envelope too (the single-node write form some clients send).
Anonymous nodes are labelled _:b0, _:b1, ... by one issuer for the whole transaction body. The counter used to restart for every expansion call, so the anonymous nodes of different ["graph", g, ...] items (and of the plain items after them) all became _:b0 and merged into one node. A user-written label of the issuer's own form (_:b3) would merge with the anonymous node given that number. The parse notes such labels (templates and VALUES), and when one was also issued it parses the document once more with the issuer skipping those numbers. The positional labels are part of a stored blank node's identity under upsert and sync, so their spelling is unchanged; only documents that were being corrupted get different labels. The edge-annotation firewall now refuses user labels in the lowering's reserved _:fluree_ann_ prefix, which would merge with the lowering's anonymous annotation nodes.
Keywords on node objects no longer leak into the data as predicates named after the keyword: - @reverse (JSON-LD 1.1 section 4.8): each value is the subject of a statement pointing at the node, written in the node's scope. The expander used to write a context term defined with @reverse as a forward property, inverting the statement; it now expands into the node's @reverse map, as the keyword does. - @included (section 4.7): the included nodes are written in the node's scope with no linking statement. - @nest (section 4.6): the expander merges a nested value's properties and types into the enclosing node, recursively; a nested value cannot carry @id or @graph. - @index carries no statement and is dropped. - Any other key of JSON-LD keyword form (@ then letters, e.g. @language on a node) is refused. Keys that merely start with @ (@odata.etag) are ordinary property names, as before. Bulk import reads @reverse and @included the same way (they were dropped). The edge-annotation lowering treats @included nodes as nodes and refuses an annotation inside @reverse or @nest, which it would have lowered against the wrong edge.
…itter The edge-annotation lowering computed "which graph is this edge in" on its own, and disagreed with the parser: it ignored the update's graph key (A1: the annotation was refused as a GraphMismatch), the ["graph", g, ...] item (A2) and the array selector (A3), writing the bundle to the default graph without an anchor while the edge was in g, and never lowered annotations inside named-graph content. - The walkers read @graph with the shared classifier and is_graph_key (aliases included; the root document's bare `graph` routing key is not a selector), recognize ["graph", g, ...] items in update clauses, and descend into named-graph content with the graph's @id as the scope. Siblings are placed with the edge's scope as their graph selector, which the parser resolves exactly as it resolved the edge's. attach_siblings reads the document shape: a top-level named graph is re-wrapped, not appended into its content. - GraphScope::emit is the one template-level writer of f:reifiesGraph: a reifier's f:reifiesSubject emitted in a named scope brings (reifier, f:reifiesGraph, scope graph) with it, for assertions and retractions alike. The walkers' stamps and graph insert's post-hoc anchor loop are deleted. - With #1997 and #2005, an update's `graph` key that names the default graph (the ledger's own address, or `urn:default`) writes the default graph: staging moves the templates of that template default there. An annotated edge under it now moves with its bundle, and staging drops the bundle's anchor, which named that graph: a default-graph bundle has none, so the annotation is found with its edge in the default graph. - A fail-closed cross-check: after an annotated document parses, every reifier bundle must reify an edge asserted in the bundle's own graph. It runs once any template names a graph (a document entirely in the default graph has no scoping to disagree about) and allocates nothing per template, which keeps the annotation parse bench within noise.
A node-level "@graph" selector in a where node-map now scopes that node's patterns (and those of nodes nested in it) to the named graph, exactly as ["graph", <name>, {...}] does, so the same key means the same scope in where, delete and insert. It used to be parsed as a predicate named @graph: a query matched nothing, and an update's WHERE matched nothing, so the update committed zero flakes and its target survived. The name follows the existing where-clause GRAPH rule (an alias or the graph's IRI as written); a context alias of @graph works the same way, and a bare `graph` key stays an ordinary property in a pattern. A node with @graph content (a JSON-LD named graph) is refused in a where.
The bulk-import JSON-LD adapter skipped every @-key but @type, so a node's graph selector was dropped (its statements folded into the default graph) and a JSON-LD named graph's content never reached the sink. The GraphSink contract calls that folding data loss. The adapter now refuses a node-level @graph (selector or content) with a message pointing at `fluree insert` or a TriG / N-Quads import. Writing graph scopes from bulk JSON-LD (ImportSink quads) is left for a follow-up; a top-level envelope is unwrapped by expansion and imports as before.
The config reader took the first binding of a setting-group pointer, so a config with two f:shaclDefaults values (a re-inserted config) could read either group, and a group whose fields were written outside the config graph (the #1979 shape) read as a present-but-empty group. - A group that sets nothing in the config graph reads as absent. That is observably the same as before for every group; it makes the reader deterministic and keeps "empty group" from diverging from "no group". - Several values of one pointer: empty groups are skipped and the rest ordered by decoded IRI, the rule several f:LedgerConfig subjects already follow. A rate-limited warning names the pointer. - config_resolver::diagnose reports degenerate config state: several f:LedgerConfig subjects, ambiguous group pointers, empty groups and the graphs their fields were stranded in, and shapes that no config enables ("shapes present; SHACL enforcement not configured"). - Ledger info carries them as `configDiagnostics` (omitted when there are none), and `fluree info` prints them.
A Turtle insert's programmatic options reached the checks only when the document had a `GRAPH` block, which sends it through the TriG path: a plain Turtle document ignored `TxnOpts::shapes` and `TxnOpts::validation_mode` and committed unchecked. The plain lane now passes them to the shared checks with the inline uniqueness constraints, as the TriG path does; a build without SHACL support refuses inline shapes there too, as on the other lanes. Tests: inline_shapes_apply_to_turtle_with_and_without_graph_blocks (plain and TriG, refused in reject mode, committed in warn mode). inline_shapes_honor_a_requested_warn_mode now also asserts that the same write is refused in the default mode, so it fails with the inline pass removed.
`sh:minCount >= 1` made a GraphQL field non-null. With SHACL enforced only where the ledger config enables it, nothing keeps a record on another ledger from lacking the value, and a non-null field turns such a record into an error on read. The schema now marks a field non-null from `sh:minCount` only where the config enables SHACL for the view's graph (the view's resolved config, or the ledger's own when the view was built without it); elsewhere the field is nullable. The derived schema's cache keys on the view's graph, since the schema now depends on it. The CLI and query GraphQL pages say so, and that SHACL validation of mutations applies where SHACL is enabled. Tests: min_count_makes_a_field_non_null_only_where_shacl_is_enforced, min_count_follows_the_views_graph (a view of a graph with SHACL enabled, derived after a view of the default graph at the same t). The other shape tests seed a SHACL-enabled ledger.
A rebase replays the branch's commits one at a time and names the commit it stopped on, but only for a SHACL violation: a refusal from the config checks (a split group, a second value) came back without it. Branch-operation refusals from the config checks now go through the same description, `replaying commit t=N was refused: ...`, still a 409. A uniqueness violation is still returned as its typed error, which carries no free text for the commit. Tests: rebase_refused_by_a_config_check_names_the_commit, rebase_rejected_when_replay_violates_uniqueness.
…s them Local `fluree info` built the full ledger-info statistics only to read `configDiagnostics`. It now calls `config_resolver::diagnose` on the loaded ledger. The MCP server's ledger-info markdown gains a "Config Diagnostics" section when there are any, so an agent reading the data model sees, for example, shapes that no config enforces. Test: test_format_data_model_config_diagnostics.
…roup annotation_on_a_reverse_property_reifies_the_stored_edge gains a reverse term with `"@type": "@id"`. repair_recipe_restores_a_split_config gains a group that is a blank node with its fields in the default graph, as the documented JSON-LD form wrote one before this branch; the docs recipe repairs it verbatim.
…tions The JSON-LD 1.1 status lists its deviations instead of "fully compliant": unknown `@`-keywords on a node are refused where the expansion algorithm drops them, and two scoped-context differences (a type-scoped context propagates into nested nodes, and a term defined only in a property-scoped context stays a bare key outside it). The `@json` bullet is back in the feature list. The behavior notes add that a graph name no longer expands through `@vocab`, and that a plain Turtle insert applies inline shapes and a requested validation mode.
…t its policy A transaction stages JSON-LD, Turtle and SPARQL operations through different staging lanes. Under a deny-all policy each kind is refused, and the ledger does not move.
…hapes The CONSTRUCT page shows a template node's own `@graph` beside the `["graph", …]` form. The compatibility notes list it, the GraphQL nullability rule and what the `shacl-not-configured` diagnostic counts (typed shapes and subjects with a SHACL target, not a config that sets `f:shaclEnabled false`), which `fluree info`'s page now says too. Two GraphQL paragraphs are rewrapped.
`"construct": true` uses the `where` clause as the template, but it kept only the clause's top-level triples and edge annotations, so a `where` node with its own `@graph` (a graph block) matched and wrote nothing. The shorthand now takes its template through the same `construct_template_pattern` as an explicit template, so the node's triples come out in its graph. SPARQL's `CONSTRUCT WHERE` has no `GRAPH` form, so it is unchanged. Test: jsonld_construct_shorthand_keeps_a_where_nodes_graph (a default graph node and a graph-scoped node, against the explicit SPARQL template, as N-Quads).
fae15f9 to
7125b79
Compare
Performance detailsThe measurements behind the PR body's Performance section. Wall time, median over 6 rounds (load 9-17). Push and loads are on file storage; each push goes into a fresh copy of the base:
Commits 28 and 29. An in-process harness (not committed) counts instructions retired and allocator calls per transaction (
The last row was +0.97% (allocations 3,663 → 3,766) before flake generation cached each re-encoded IRI per transaction; it is now +0.11%. The canonical write is one comparison per written Sid: commit 28 costs +0.38% on the 100-node insert (its ranges do not overlap commit 27's) and at most +0.05% elsewhere. Commit 29's cost is in the last two rows, which run only when an update binds an IRI in a namespace the ledger has not registered. There the Commit cost with
The fast-forward validation cost in full, since it's the one new cost on a user-visible operation: Fast-forward merge (commit 20). A timing harness (not committed; file storage,
The criterion table, the annotation fix, and the per-commit costs of commits 17–27Criterion,
Commits 17-27 on the transaction path, not re-benchmarked with criterion locally (EC2 and in-process counts for
Commit 50 (untyped shapes in the load-time check). A harness (not committed;
|
Deviations from the design, in full
Merging into
|
Tests by area, and the mutation tableNew and changed tests, by area (all in the owning groups): JSON-LD scope, named graphs, blank issuer, keywords and the refusal-prefix table ( Every fix was checked by reverting only the fix (a mutation), watching its tests fail, and restoring:
|
|
While working on this branch since the last push, I decided on several revisions, improvements and fold-ins. They're all reflected in the updated description, but here they are in one place, by area, each with its commit and its tests. The branch is also rebased onto Graph names in an update's
One config vocabulary, and the guard and diagnostics built on it
Multi-operation requests and transactions
Inline uniqueness constraints
Branch operations and the system graphs
Push
Status codes
Fast-forward cost
Enabling SHACL, and a warning at load
Annotations
Follow-ups The follow-ups are filed, grouped under one tracking issue, #2095, and the description's Follow-ups section links each one. Data written by earlier releases No stored format changes, and no migration. The compatibility notes add two behaviors on earlier data:
Happy to talk through any of these. |
Thanks for confirming all three cases. |
Thanks for filing it. It's listed with the breaking change in the description (on a branch created from an unindexed ledger, SHACL is off entirely until #2069 lands, where the shapes-exist fallback used to enforce it), and the description has |
|
On the review's general points:
|
Brings in #2019, #2044, #2048, #2049 and #2052 (main at 58b5323). #2052 keys cross-ledger governance by the model ledger's head commit and opens the model at that commit. Two files conflicted: - tx.rs: this branch moved the stored-shapes pass into its own block (`stored_cache`), which #2052 edited in its old place. The block keeps this branch's shape, and its compiled-SHACL cache key carries #2052's commit id beside the model's t, as main's does. The shapes model opener and the cache key type merged without conflict. - it_policy_cross_ledger.rs: both sides appended tests; both are kept.
A multi-operation request is governed by the state before it: each operation reads the config, the shapes and the uniqueness constraints from the ledger as the request found it. A cross-ledger model ledger was the exception: each operation resolved its own context, so one that ran after the model committed read the model's newer head. #2052 pins a model's head (its `t` and commit) within one resolution context; the request now carries those pins across its operations, so every operation reads a model ledger at the head the request's first lookup of it found, and a model that is the data ledger itself from the state before the request. A `Transaction` staged again from its own base (after a failed operation, or to roll back to a savepoint) keeps them; staged again over a newer head, it is governed as that head is. The SPARQL, Rust API and compatibility pages say so. Test: a_transaction_reads_a_model_ledger_at_one_head (a shape the model activates while a transaction is open, before and after a rollback, and in a transaction begun after it).
|
Merged main in ( One commit on top, |
Brings in #2102 (one HTTP status table), #2068, #2056, #2053, #2046, #2045 and the v4.2.4 release. - api error.rs: main's exhaustive status_code / error_type tables, with ApiError::ConfigUnavailable and TransactError::ConfigUnavailable classified as 500 err:storage/ReadFailure in both, and rows for them and RequestOverrideRefused in every_variant. - server error.rs: main's derivation from ApiError; this branch's ConfigUnavailable arms move to the API tables. - consensus: SubmissionError keeps both ConfigUnavailable and CommitContention. - server routes/transact.rs: both submission arms and test rows. - transaction.rs, tx_builder.rs: main's txn_attempts loops, with this branch's request-governance argument on every stage_plan / stage_all call.
|
Merged main in again ( |
|
Merged |
…s root Both push tests copy a storage root that a dropped instance wrote. The dropped instance can leave its WAL live, and the WAL's flush thread deletes a segment once the files it covers are flushed, so a copy running at the same time could miss the segment (copy file: NotFound on .fluree-wal/00000001.wal). Disconnecting first retires the log before the copy starts.
|
One more small one on top (6f25530, tests only): |
This is deliberately more than #1979 needs. Which graph a write lands in and which checks run on it were decided per JSON-LD keyword and per write lane, and the answers drifted: nested nodes lost their graph (#1979), named-graph content was silently dropped, config groups split across graphs, and a heuristic decided whether SHACL ran. So rather than patch one surface, each answer now has one home, which is easier to reason about and tune: one rule for a node's graph, one check path shared by the write lanes instead of three drifted copies, and SHACL exactly where config turns it on. Fwiw, it's one of five PRs taking this approach: #2006 and #2008 have merged, and #2007 and #2010 are open.
Follows #1979, which #2022 closed. #2022's
7d894b7e9makes a node's own@graphthe default graph for the node objects nested in it, and4a4f7c309names the construct a TriGGRAPHblock refuses. This PR makes the first fix the one rule for every JSON-LD reader, and covers the config half of #1979 (item 2, the config docs, and the checks behind them).Fixes #1991: an annotated edge under
["graph", …]or a top-levelgraphis written, and deleted, with its annotation in that graph.Before #2022, a JSON-LD config written the documented way (
docs/ledger-config/writing-config.md) silently did not work. The config node landed in#config, but its nestedf:shaclDefaultsgroup landed in the default graph, so the reader saw an empty group and SHACL stayed off, while a ledger with shapes and no config enforced them anyway. #2022 fixed where the nested group lands. This PR closes the class around both halves: B1 makes a JSON-LD template's graph its innermost enclosing lexical scope, in every reader, B2 checks config integrity and makes SHACL explicit-only, and B3 gives an IRI an update binds one encoding wherever it binds it.Heads-up for reviewers: B2 is a breaking change (ledgers that hold shapes without
f:shaclEnabled truestop enforcing on upgrade), so the behavior-change list and the enable recipe below are the parts I'd most like eyes on.What changed
B1: JSON-LD graph scoping. A template's graph is its innermost enclosing lexical scope, in every JSON-LD reader:
@listitems, anonymous nodes,@reverse/@included/@nestcontent) are written to their enclosing node's graph. A nested node's own@graphwins for its subtree;"default"names the default graph.{"@id": G, "@graph": [...]}) are supported. Named-graph object content that was silently dropped is now stored.@graphclassifier and one graph-name resolution order (classify_written_graph_name), shared by templates andwhere: a variable, afromNamedalias, the keywords"default","config"(the ledger's own config graph) and"txn-meta", then expansion against the@context(absolute and compact IRIs;@vocabdoes not apply). Relative graph names are refused. A?vargraph name in an update template writes to the graph the WHERE binds (SPARQLGRAPH ?gparity); elsewhere it is refused.["graph", …]items; user labels never collide with minted ones).@graphselector does not. At the top level, a document without@idwhose@graphis a single node object is read exactly like the one-element array: envelope content, a bare reference{"@id": X}included. (Solo MCP's single-node envelope{"@graph": {…}, "f:message": …}keeps its metadata.)f:reifiesGraphemitter (GraphScope::emit). An annotation bundle is placed in its edge subject's@included, so it lands in the edge's graph by construction (no run-time cross-check, and nothing for RDF 1.2 triple terms: rdf:reifies links, triple terms as values, faster annotation queries #2017's reifier links to conflict with). One edge-key classifier, shared by the insert and delete walkers, tells a keyword, a reverse-defined term and a forward term apart, so an annotation on a reverse property reifies the edge as stored. Deleting an annotated edge addresses the edge's graph, not the object's, and the selector form resolves its graph name as a template does. An annotated edge under a top-levelgraphnaming the default graph (the ledger's own address, fix: unresolvable graph references fail closed, and TriG directives apply in document order #1997, orurn:default, fix(query): 4xx for unknown graphs and commit refs; urn:default names the default graph #2005) lands with its annotation in the default graph. An annotation under a variable graph name is refused while parsing.@graphin a JSON-LDwhereis aGRAPHpattern, and its name resolves in the order above, so an update whosewhereanddeleteboth say"@graph": "ex:g"address one graph. In an update,"default"there names the ledger's default graph, as in the templates and asurn:defaultdoes, also when a top-levelgraphorfromgives thewhereanother default graph, so moving data from the default graph into a named graph reads what it deletes; an update'swherenever reads a union default graph. A nested node's own@graphscopes its patterns;"default"inside an enclosing graph pattern is refused (awherepattern cannot leave it). A["query", …]subquery names graphs as thewherearound it does. Each name in an update'sfromNamed(a graph's IRI, its alias, the name it is listed by) must mean one graph across the list, as a query's dataset aliases must, and"default"in afromlist reads the default graph beside the graphs listed (it was dropped). Top-level and nestedwherenodes read their@graphselector through one path.@graphnames the graph the node's triples are written to, as["graph", …]and SPARQL'sGRAPHblock do. The"construct": trueshorthand, which uses thewhereclause as the template, keeps awherenode's own@graphthe same way (SPARQL'sCONSTRUCT WHEREhas noGRAPHform and is unchanged).@reverse,@includedand@nestare implemented;@indexis ignored; other keyword-form (^@[A-Za-z]+$) keys on a node are refused. Non-keyword@keys such as@odata.etagbehave exactly as before.@graphscoping instead of folding it into the default graph."a": {"@context": {"b": {"@id": "ex:b"}}}) expand against the context where the scoped context applies; they stayed compact, so the keybbecame the stringex:b, which a transaction stored as an IRI. This predates the PR.main. That speedup is this change's, independent of the annotation placement: against amainwith the same change, default-graph annotation parse is +10.3% and an end-to-end insert −2.0% (Performance).B2: config integrity and explicit-only SHACL.
f:shaclEnabled true, ledger-wide or per graph). The shapes-exist heuristic is gone; posture is a pure function of the resolved config, on every lane (JSON-LD/SPARQL, Turtle insert, branch merge/rebase/revert, push).check_staged_write): one config read, then SHACL, uniqueness and the staged-config guard (every check on authoring lanes; the config-group re-check on branch operations). It replaces three copies that had drifted: the plain Turtle insert lane now enforces uniqueness like the other lanes and applies the caller's inline shapes, requested validation mode and inline uniqueness constraints, which a plain Turtle document ignored while TriG honored them, and merge, rebase and revert (and their previews) enforce uniqueness as well as SHACL. Push is the one exception: it validates pushed commits through its own SHACL call, with replay semantics (origin_validated_replay; a violation is a 422), and runs neither uniqueness nor the config-group checks. Routing it through the tail would change its statuses and add those checks to replication, so it is a follow-up (Write paths outside the shared check tail: push,validate, andstage+commit_staged#2076). Push takes graph ids from the ledger's graph registry (the registered id, or the id applying the pushed commits assigns), so each named graph a push writes is validated under its own config; a pushed flake in a graph that neither the ledger nor the pushed commits register is refused (422) before anything is written.BranchConflict), naming the target's config group and pointing at the repair recipe; previews report it, and a rebase stopped by one names the commit it was replaying. A merge or rebase that would carry what a branch wrote to its own config or txn-meta graph (urn:fluree:<ledger>:<branch>#config) into another branch is refused the same way: those graphs configure the branch that wrote them, and on the target they would be ordinary graphs. Merging from a branch the target was created from is unaffected. The other direction is refused too: a merge (fast-forward or not) that would change the target's own config or txn-meta graph with what a branch wrote there, where that IRI names an ordinary graph, is a 409. A branch's config is changed on that branch.opts.shapes) validate on their own pass, gated by the SHACL group'sf:overrideControlrather thanf:shaclEnabled; where override control refuses them the transaction fails with a 400 (RequestOverrideRefused). They are not supported in a policy-scoped request (refused with a 400), and neither are inline uniqueness constraints (opts.uniqueProperties); support for both in policy-scoped requests is a follow-up.opts.shapesandopts.uniquePropertiesin a transaction body are read on every surface (HTTP, the embedded API and the CLI's local mode; only the HTTP route read them), malformed values are refused, and a build without SHACL support refuses inline shapes.sh:minCount ≥ 1makes a field non-null only where the ledger config enables SHACL for the view's graph. Elsewhere nothing enforces it, so the field is nullable.err:storage/ReadFailure, in fix(server): one HTTP status table for API errors #2102's one status table) on the write lanes, branch operations, push and queries:TransactError::ConfigUnavailableon every staging lane (transactions, multi-operation updates, branch operations, push), andApiError::ConfigUnavailablefor the transaction policy context,f:policySourceresolution and a query's config. It was aParse error(400) when staging, a 422 on push, andInvalid configuration(400) elsewhere.Fluree::begin_transaction, feat(api): multi-write transactions #2023), a nested multi-operation update or Cypher statement included, resolves SHACL, uniqueness and config from the state before the request, as one transaction does, in both directions: an earlier operation that turns enforcement off, or removes a shape or constraint, does not admit a later operation's data, and one that turns enforcement on, or adds a shape or constraint, does not apply it to the same request. A model ledger the config names (a cross-ledgerf:shapesSource) is read at one head for the whole request, the one its first lookup found (commit 61, on fix(api): key cross-ledger governance by the model ledger's head commit #2052's head pins). So a requestmainrefused can now commit: a shape and data violating it, written in one multi-operation update, both commit. The config checks run once, on the request's result, soINSERT DATA {…} ; DELETE DATA {…}is judged by what it commits, not by operation order. Explicit Bolt transactions are not requests of this kind: they commit each statement as it runs, so each statement is governed by the state before it, as separate transactions are.f:enforceUnique) now makes a write or transaction staged before it stage again when it commits, as a commit that changes the shapes, the class hierarchy or a named graph already did, so the write is checked against the declaration rather than re-based over it.configDiagnostics(shacl-not-configured,empty-group,stranded-fields,ambiguous-pointer,multiple-ledger-configs,duplicate-graph-override);fluree infoprints them, reading them throughconfig_resolver::diagnoserather than the full ledger info, and MCP's ledger-info output lists them. A shape counts forshacl-not-configuredwhether it is typed or only has a SHACL target (sh:targetClass,sh:targetNode,sh:targetSubjectsOf,sh:targetObjectsOf), and a config that setsf:shaclEnabled falseis not reported. The query groupmaingained (f:queryDefaults, feat(query): union default graph, per ledger and per query #2004) is read, diagnosed and guarded like the others, and a test destructuresLedgerConfigfield by field: a setting group added later breaks the test build until it is mapped to its group pointer, and the test then fails until the guard and the diagnostics list that pointer. A second test reads the config vocabulary's source and requires each of its terms to be a guarded field or listed as a class, a value or a data term, so a setting added inside a group fails it too. The config vocabulary is one module (config_vocab.rs): the guard, the diagnostics,graphs_with_config_fields,carries_config_settingsand the repair recipe derive from it, a test records every predicate the reader looks up while it resolves a maximal config and requires the vocabulary to equal it, and another parses the documented recipe and requires it to follow every config edge. Diagnostics walk every node reachable from the config root, so a stranded per-graph override, a partly stranded group and an override without a target are reported, and so are config fields on the recommended config subject when the config graph does not hold it; the reader's two per-resolve warnings are throttled.fluree infoprints the diagnostics for a tracked ledger too. A ledger that holds shapes no config enforces logs that once each time it loads (reading the ledger only when its index stats or novelty show a shape type or a target predicate).Parse error, HTTP 400) a setting group split across graphs (the rootf:LedgerConfigsubject's own fields included), a config typed outside the ledger's own config graph, config fields written outside it on the recommended config subjecturn:fluree:<ledger>:config:ledger(also on a ledger with no config, where that IRI is what marks the write as config), and a second value for a single-valued setting or a secondf:LedgerConfigsubject. The split check holds across transactions too, in either order: config fields (the predicates the config reader reads) written outside the config graph for a group the config graph already points at, and a config edge to a node whose config fields sit in another graph. Otherf:data in a user graph (an edge annotation'sf:reifies*, a property'sf:enforceUnique, a policy document'sf:allow) is not a config field, and writing it reads no config. Moving the fields in with the edge in one transaction (the repair) is accepted.B3: one encoding for an IRI an update binds (commits 28 and 29).
whereconstant in a namespace the ledger has not seen was stored under the encoder's fallback code; once the namespace got its code, a lookup by the IRI missed what that write stored, as a subject and as an object, in JSON-LD and SPARQL updates alike. Facts already stored that way stay as they are: there is no migration.wherereads every IRI in the ledger's encoding (commit 29). An IRI the update's top-levelvaluesbinds, in a namespace the ledger has not seen, reached thewherewith the code the transaction gave that namespace, which thewherecould not decode:STRgave the encoded form ("15:thing"), which an insert then stored, andGRAPH ?gbound tourn:defaultread nothing. It now enters as the same IRI in awhereVALUES does, and a write stores it under its canonical code.Behavior changes (release note)
Suggested label:
breaking-change. The full text is indocs/reference/compatibility.md#behavior-changes.f:shaclEnabled true(including shapes published through GraphQL orfluree model entity define) stop enforcing on upgrade; merge, rebase, revert and push stop validating on them too. Configs without a SHACL group were already unenforced and do not change. Ledger info reports affected ledgers asshapes present; SHACL enforcement not configured, and such a ledger logs it once each time it loads. To keep enforcing, enable SHACL in the config (recipes below): for a ledger that holds shapes and no config, oneINSERT DATA. Ledger info counts untyped shapes (a subject with a SHACL target) too, and does not report a config that setsf:shaclEnabled false. A branch created from a ledger that has no index yet reads none of its source's config (A branch of a never-indexed ledger ignores its source's ledger config: configured policy fails open, uniqueness is off #2069, which predates this PR): the shapes-exist fallback still enforced SHACL on such a branch, and with this change SHACL is off there until A branch of a never-indexed ledger ignores its source's ledger config: configured policy fails open, uniqueness is off #2069 is fixed.fluree branch diff/GET /merge-previewreport itsvalidation. Fast-forwards into other targets are still not validated against shapes or uniqueness.TxnOpts), which it ignored.mainrefused them). Nothing a single operation accepts changes. Explicit Bolt transactions are unchanged: each statement commits on its own, governed by the state before it.err:storage/ReadFailure, on every lane: transactions, multi-operation updates, branch operations, push, the transaction policy context and queries (a tracked query's answer carries only the status, as fix(server): one HTTP status table for API errors #2102 notes for every tracked error). It was a 400Parse errorwhen staging, a 422 on push, and a 400err:system/ConfigErrorfor the policy context and queries.@graphselector are written to that graph (in v4.2.3 they landed in the default graph;mainhas done this for nested node objects since fix: engine fixes found building the Python package #2022, and this PR applies the rule to every reader and node shape). A client that relied on the old placement should give the nested node"@graph": "default".@graphselector, and a named-graph object's own keys, are data only (they were written as data and as metadata). A top-level{"@graph": {…}}without@idstays an envelope, as before.@graphin a JSON-LDwhereis aGRAPHpattern, with the graph name resolved as in a template (it was read as a predicate named@graphand matched nothing). In an update,"default"there names the ledger's default graph, as in the templates and asurn:defaultdoes, even under a top-levelgraphorfrom, and an update'swherenever reads a union default graph. In a query it names the query's default graph, which a union default graph widens;urn:defaultnames the default graph alone. AfromNamedlist that gives one name to two graphs is refused (Parse error, 400), an alias spelledurn:defaultthat names another graph among them;"default"in an update'sfromlist now reads the default graph (it was dropped); a["query", …]subquery names graphs as itswheredoes; in a CONSTRUCT template, the"construct": trueshorthand included, a node's own@graphnames the graph its triples are written to (it was read as a property named@graph, and the triples went to the default graph); andopts.unionDefaultGraphis refused on a JSON-LD transaction, as SPARQL refuses its pragma on an update.@vocab(such a name is refused as relative);"config"names the ledger's config graph.valuesbinds as written, also in a namespace the ledger has not seen:STRgave the encoded form and an insert stored it, andGRAPH ?gread nothing forurn:default. An IRI an update'swherebinds in such a namespace is stored under its canonical code, so it is found by IRI with the ledger's other facts about it; facts already stored under the fallback encoding stay split, with no migration.f:shaclEnabled false, and are refused underf:OverrideNone); they are refused in policy-scoped requests, as are inline uniqueness constraints (a request with an identity is policy-scoped, soopts.shapessent with an identity is a 400).opts.shapesandopts.uniquePropertiesin a transaction body are honored by the embedded API and the CLI's local mode (they were ignored there), and malformed values are refused.@keys on a node are refused;@nestis honored;@indexis ignored. Edge annotations under a variable graph name are refused while parsing (they failed at commit with an unhelpful message).@graphscoping; usefluree insert, or TriG / N-Quads."@reverse"writes its edge in reverse (object → subject), as JSON-LD defines it; it wrote it forward. Data written that way stays as it is (below).@annotationon a value of a reverse property annotates the edge as stored.sh:minCount ≥ 1makes a field non-null only where the ledger config enables SHACL; elsewhere the field is nullable.{"b": {"@id": "ex:b"}}under an outerex) now expand to full IRIs; such keys were written as the compact string (ex:b) and stored as an IRI with the schemeex. Data already stored that way stays as it is.fluree_db_transact::parse_transactiontakes the target ledger's id as a new last argument; graph names resolve against it ("config", and the ledger's own reserved graph IRIs). Solo does not call it.f:defaultAllow falseincluded; diagnostics flag it asambiguous-pointer;"childOf": {"@reverse": "ex:child"}) were stored as if the term were not reversed, so the same document no longer reaches them (a delete with it does nothing);docs/reference/compatibility.mdgives a repair query.Enable SHACL on a ledger that holds shapes and no config (the usual affected ledger):
On a ledger that already has a config subject, attach the group to it instead (a second
f:LedgerConfigsubject is refused; this form does nothing on a ledger without config):Repair a config split by #1979 (policy and SHACL groups it affected: a split group's settings,
f:defaultAllow falseincluded, aren't in effect until it's repaired; merges, rebases and reverts that carry a split config are refused until it is repaired). It starts from the config subject and from every node a config edge in the config graph points at, and follows config edges through the default graph. Run withSELECT ?n ?p ?ofirst:The recipes are exercised by tests:
enable_recipe_enables_shacl_with_or_without_an_existing_config;repair_recipe_restores_a_split_config, which runs the docs' recipe verbatim on four split shapes (a nested group, the root subject's own link, a stranded per-graph override, a partly stranded group); andthe_repair_recipe_follows_every_config_edge, which parses it and checks it follows every config edge the reader reads.Solo lockstep
From an audit of fluree/solo's write paths, updated for the review changes:
<dataset>:<branch>and a person merging, a merge into an idle main is a fast-forward. When main's config enables SHACL or uniqueness, that fast-forward is now validated against main's config and refused on a violation, with the errors a general merge gives; the merge preview reports it asvalidation(mergeablefalse). Solo's review UI should show a fast-forward preview'svalidationtoo. Merge, rebase and revert also refuse uniqueness violations now, and a result that would split one of the target's config groups (a 409 naming the group); a fast-forward whose commits carry config settings is checked that way even into an ungoverned main. A merge that would carry what an agent branch wrote to its own config or txn-meta graph into main, or change main's config or txn-meta graph with what the branch wrote there, is a 409 too, so config changes are made on main itself. Solo writes a dataset's config under its branch-qualified IRI (fluree-lambda-model'sconfig_graph_iri), so neither refusal reaches its writes as they are today.readPosturerenders a ledger with no storedf:validationModeas "enforce" (readPosture,graphs.ts:262;QualityTab.tsx:152, solomainatc1efea7c6). For ungoverned ledgers that hold their own shapes and no config (user imports, ledgers unbound withgovernanceTx(…, null)), that label becomes false: return a third "not enforced" state when no enabled SHACL group exists, or write an explicit posture at import/unbind. The MCP tools'validationMode: "reject"does not turn SHACL on for such a ledger either (the heuristic did). Governed ledgers already setf:shaclEnabled trueand are unaffected.urn:fluree:<ledger>:main:config:ledger a f:LedgerConfig, and the web posture toggle (postureTx,governance.ts:249, sent bygraphs.ts:249fromModelHead.tsxandDatasetSettings.tsx) then inserts its ownurn:cfg:main a f:LedgerConfig. This PR refuses that second config subject (400). Onmainit commits, and the reader silently keepsurn:cfg:mainand drops the fulltext config. Solo should attach the posture to the existing config subject (the docs'INSERT … WHEREform, falling back toINSERT DATAwhen there is none);governanceNodeshas the same shape. This needs to land before or with the pin bump. Latent refusal: a re-run of the model lambda'sensure-fulltext-configwould add a secondf:fullTextDefaultsgroup (make it idempotent). The refusal of config fields on the recommended config subject outside the config graph covers thef:config vocabulary only: theftm:coreLanguagesset solo keeps on that node in the default graph is data and is unaffected.…:knowledge) and semantic-transformmodel_jsonld(urn:fluree:workflow:vocabulary:<id>) now store their content in that named graph instead of dropping it; solo's default-graph readers and cleanup do not see it, so emit an envelope or strip the wrapper@idas pack-ner and apply-schema do. MCPstamp_provenanceon an agent document with a top-level@idand an@graph(a named graph object, or a node with a graph selector) makes the stamped keys data of that node rather than transaction metadata. On a context-free bare reference ({"@id": …}, wrapped as{"@graph": {"@id": …}, …}) it is refused, as before. MCPjsonld_update: awherenode's"@graph"now selects that graph, with compact names expanded against the document's@contextand"default"naming the ledger's default graph (it matched nothing before). Bulk import of JSON-LD with@graphscoping now fails with the engine message instead of folding the data.@odata.*keys behave as before; custom webhooks sending@nestnow merge it.main(2941d470c). Nothing new reaches solo: it namesurn:defaultonly to label the default graph in its graph list (GraphDetail.tsx), usesfromNamedonly in query datasets (the new refusal is for an update'sfromNamedalias), and writes nof:queryDefaultsgroup (code search of solo's default branch, Oct 5). The later review changes reach no solo call either: solo sends noopts.unionDefaultGraph, no updatefromNamedand nofromlist holding"default", and commits 28 and 29 change only what an update reads and stores for an IRI in a namespace the ledger has not seen.Parse error:,JSON-LD error:orUnsupported feature:(all mapped to 400 by solo's router), exceptRequestOverrideRefusedfor inline shapes, which solo does not send, and a named-graph object whose@idis the ledger's#txn-metagraph: staging's existing reserved-graph check refuses it astransaction targets reserved system graph …, which solo's router maps to 502 (map it to 400). A branch operation the config checks refuse is now a 409 (it was 400Parse error). A config read failure is HTTP 500,err:storage/ReadFailure, on every lane. To retry it like other storage failures, solo needs two arms:classify()treats everyApiError::Transact(_)as deterministic before it looks at the status, soTransactError::ConfigUnavailablegoes in its transient arm. Without it, solo behaves as today.SubmissionError::ConfigUnavailablekeeps the code through the consensus flattening. The router's exhaustiveclassify_submission()needs an arm for it, as transient, or the pin bump does not compile.Sequencing with bplatz's engine PRs
This branch is rebased on
mainatc5874d586, which carries #2022–#2025, #2006, #2008, #2013, #2036 and #1451:7d894b7e9(nested nodes take their parent's@graph) fixes the Node-level@graphdoes not reach nested nodes: the documented JSON-LD config disables SHACL #1979 symptom, and this PR'sGraphScopefixes the pattern.jsonld.rstakes this PR's whole version of the function; fix: engine fixes found building the Python package #2022's two tests are kept and pass. Its multi-operation net-change fold is kept, and the config guard runs on its result.Transaction): a transaction is governed as a multi-operation update is.SequentialStager::stage_withhands each operation the request's governance, and every staging entry point takes it.transaction_is_governed_by_the_state_it_began_on(config, a shape removed,f:enforceUniqueremoved and added),transaction_is_guarded_on_its_result, anda_transaction_behind_a_commit_that_declares_a_property_unique_is_checked_against_it.ShaclViolations): branch-operation previews and the rebase/merge refusals carry it.violation_carries_resolved_resultsnow builds its ledger withcreate_shacl_ledger, which enables SHACL in the config.set_full_textattaches to the existing config subject, so the package is consistent with explicit-only SHACL and the config guard. Its error classes follow HTTP status, so a branch-operation config refusal raisesConflictError(409), and a config read failure raisesFlureeError(500).sh:sparqlconstraint that cannot run follows severity and mode; trailingVALUESon every query form): SHACL validation keeps its constraint failures and reports each under the validation mode of the graph it validated, as this PR's per-graph posture gives it. The JSON-LD query parser keepsparse_top_level_valuesbeside this PR's graph-name environment.Transactionreads SHACL, uniqueness and config from the state the request began on, and TriG inserts go through this PR's check tail. Template parsing carries fix(transact): retractions name stored facts; RDF text is parsed once #2008's ill-typed-literal flag beside this PR's document-wide blank-node issuer. Test:every_kind_of_operation_is_checked_against_the_policystages each kind of operation in aTransactionunder a deny-all policy.e6abd54af,mainat58b53238a), as a merge commit so the commits above keep their SHAs. fix(api): key cross-ledger governance by the model ledger's head commit #2052 keys cross-ledger governance by the model ledger's head commit and opens the model at that commit; the stored-shapes pass this PR moved into its own block keys its compile cache on that commit too, and commit 61 carries fix(api): key cross-ledger governance by the model ledger's head commit #2052's per-resolution head pins across a request's operations.cf1d8b586,mainatba4a982ec), also as a merge commit. fix(server): one HTTP status table for API errors #2102 makesApiError's status and@typethe one table; both config-read forms are classified there as 500err:storage/ReadFailure, the status this PR documents, so the server's own arms for them are gone, andRequestOverrideRefusedis a 400. Consensus keepsConfigUnavailablebeside fix(server): one HTTP status table for API errors #2102'sCommitContention, and fix(server): one HTTP status table for API errors #2102's retry loops pass the request governance as before. (perf(query): batched probes skip empty leaflets and gallop subject runs #2050 and feat(indexer): one leaf size for import, rebuild and incremental builds #2051 went tonext.)maingained since the last rebase relied on the shapes-exist fallback, so they now build their ledgers withcreate_shacl_ledger(commit 12), as bplatz's merge ofmaininto the previous push did. Everything else that merge resolved, this rebase resolved in the commits that own each part.main: it changes no stored format, and runs on data written bymainwithout migration.next. RDF 1.2 triple terms: rdf:reifies links, triple terms as values, faster annotation queries #2017 and feat: ledger lifecycle — name bindings, restorable drops, fenced writers #1980 are merged there, so themain→nextmerge after this lands carries a port (main→nextmerge: ports for #2009 and #2010 #2094); the deviations comment below lists what the RDF 1.2 triple terms: rdf:reifies links, triple terms as values, faster annotation queries #2017 hunks need and which files feat: ledger lifecycle — name bindings, restorable drops, fenced writers #1980 adds.Performance
Across two criterion rounds on a shared box, the JSON-LD parse and insert benches move a few percent in both directions (the untouched Turtle bulk-import path moved −10% to +18%, which is the noise floor here), and
jsonld_parse'supdate_sugarcase is 22% faster.jsonld_parseannotations went over the 5% budget twice. It is now faster thanmainonly because of the expander fix (commit 33), which is independent of the annotation placement and would speed upmainby itself: against amainwith the same fix, default-graph annotation parse is +10.3% and an end-to-end insert −2.0% (Annotation parse, below). These criterion numbers predate commits 17–27 and use default features, so they don't exercise the SHACL transaction path this PR restructures; the quiet box below and the per-commit counts in the performance comment do. All of these measurements predate the catch-up withmainat2941d470c, which adds no work to the measured insert and commit paths: commit 27's change is in an update'swhere(see its bullet in the performance comment), the query group is one more entry in the guard's field set (a hash set), and the reader reads that group with the same two lookupsmainmakes. They also predate the rebase ontoc5874d586and commits 48–58, of which only commit 50 adds work on a path every ledger takes: the load-time check also looks for SHACL target predicates, in the index's property statistics and in novelty. Measured on cold loads against the commit before it (8 alternating rounds), that is +0.6% instructions on an indexed ledger without shapes (wall time within noise) and nothing measurable unindexed; an indexed ledger whose only shapes are untyped now pays the warning's lookup, as one with typed shapes already did (the performance comment has the table).Annotation parse. The first overrun (+9.9% / +7.8%) was the reifier cross-check allocating a key per template; it was made to borrow its keys (+2.2%), and later removed when annotation bundles moved into their edge subject's
@included(commit 34). That placement was the second: it nests each bundle two levels deeper, and the JSON-LD expander copied its position in the document (a vector of keys and indexes, one allocation per key) for every key and item it descended into, and cloned the active context for every node. Commit 33 makes descending allocation-free and borrows the context. That fix is independent of the placement: onmainalone it takes 35% off this parse and 11–12% off the insert, so the speedup againstmainin the table is the fix's, not the placement's. This PR's own cost is the last column, against amainwith the same fix: +10.3% on the default-graph parse, −2.0% on the end-to-end insert. A harness (not committed;dev-fastwithshacl, Apple M4) parses 2,000 annotated edges 20 times per round, or inserts them into a fresh memory ledger 5 times, with four builds rotated over 9 rounds on a shared machine (load 9-15). "Fixedmain" ismainwith the same expander change, to separate this PR's own cost from the expander's saving. Median of the per-round means:mainmainmain@graph@graphAgainst
mainthe head was faster in every round on every row. Against fixedmainit was slower in 9 of 9 rounds on the default-graph parse, and in 2, 3 and 2 of 9 on the other three rows. Against fixedmain, the default-graph parse keeps +10%: about half is the placement itself (+4.8% against the commit before it, both with the expander change, in a second run of 10 rounds), and half comes from the commits before it (a profile puts most of it in annotation lowering under the graph scope). End to end, the insert, it is at parity or better. Sibling placement for a subject that sits in the root array would bring that parse row to about +5%, at the cost of a second placement path; it was not built.bplatz's review changes, measured against
main(6b9d5b619, before the last rebase) and this branch before those changes. A harness (not committed;dev-fastwithshacl, Apple M4, data on the internal disk) runs each write lane on a memory ledger with no config and on a governed one (SHACL enabled with a shape, plus a uniqueness constraint): 20 warm-up operations, then 300 timed, three builds rotated per round. Instructions retired per operation, median of 6 processes per build:mainmainOn a single-operation write these changes are within ±0.1% of the branch before them. A multi-operation request reads its governance once, from the state before it, instead of once per operation (−16.7% governed). The gap to
maincomes from rounds 1–3 (the check tail, the config read and the explicit-only posture); it was not split commit by commit here. Re-checked after the rebase onto745736a04(6 alternating processes per build): this head against thatmainis −3.1% to −36.8% on every lane, and a two-operationTransaction(#2023: a JSON-LD insert and a SPARQL update, committed) is −5.3% with no config and −32.2% governed. Part of the governed saving is the one every governed single-operation lane shows againstmain(−15% to −22%, from the check tail and the explicit-only posture); the rest is reading governance once per transaction rather than once per operation.Wall time for push, cold loads and fast-forwards, the instruction counts for commits 28 and 29, the commit cost with
shacl, the fast-forward merge timings and the original criterion table are in the performance comment below. In short: push is unchanged; the load-time warning costs +76 to +95 µs on an indexed ledger that holds shapes without config; a fast-forward into a cached target pays +0 to +2.5 ms, and one on a fresh instance pays the target load (on an unindexed target of 1,000 commits, 166.7 ms against 16.3 ms onmain); a commit does less work than onmain(−11% instructions on a fresh ledger).Later changes. The restage on a uniqueness declaration (commit 43) adds one comparison per staged flake on commit. Instructions per commit through the cached-handle commit path, before and after, 8 alternating processes (
dev-fast, default features): 720,808 → 720,746 for a 1-node insert (−0.01%) and 17,353,578 → 17,365,012 for a 100-node insert (+0.07%, ranges overlapping). The recommended-subject check (commit 41) reads nothing and runs only when a write puts config fields outside the system graphs, and the merge's system-graph check (commit 44) is one pass over the staged flakes of a merge.Quiet box. EC2 c7i.4xlarge, the fat-LTO bench profile with
shacl, base and head in interleaved rounds; a change counts as a win or a loss only when the base and head ranges don't overlap and the median delta exceeds the bench's budget (5% atsmall). No loss by the rule.jsonld_parsewins onupdate_sugar(−14.7%) andflat_envelope(−15.3%; the base there was bimodal, 148 or 170 ms, but the head was at or below it in every round), andannotationswas +4.4%, slower in every round but under the budget (before commits 33 and 34; see Annotation parse).insert_formats(JSON-LD and Turtle) is stable.transact_commitwas inconclusive onpopulated_ledgerin the first session (+23.9%, with the ranges overlapping) and is stable over the second session's 7 rounds (Commit cost withshacl, in the performance comment).Before ready
shacl):insert_formats,jsonld_parseandtransact_commit, no loss by the rule, andtransact_commitcounted in process (see Performance). That run predates the latest changes.jsonld_parse(annotations) andinsert_formatson the quiet machine at the final head, to confirm Annotation parse's shared-machine numbers.Follow-ups
Grouped under one tracking issue, #2095. Several are one issue each, so the lines go by issue:
minCountrule, the config diagnostics in MCP's ledger info and a lighterfluree info, the warn-mode inline-shapes test, the anonymous-group repair case, and a rebase stopped by a config refusal naming its commit. Left there: a notice at write time and a posture check influree model entity define; one rule for a written named graph whose IRI neither the transaction nor the ledger records (stored shapes skip it, inline shapes and uniqueness read the ledger-wide config); a dedicated error variant for config-guard refusals, still a 400; and a rebase stopped by a uniqueness violation naming its commit.compatibility.mdlists the unknown-keyword deviation. Left there: the["graph", …]form (JSON-LDwhere: a compact IRI in["graph", …]is not expanded, so the pattern silently matches nothing #1993), Datalog rule heads reading@graphon their own, the fix: typed dataset and graph references: one grammar, one resolver #2007 seam (itsdataset_ref.rsreplaces this PR's subset, item for item), graph names resolved against the top-level@contextonly, type-scoped context propagation and order, and a property-scoped-only term kept as a bare key.where: a compact IRI in["graph", …]is not expanded, so the pattern silently matches nothing #1993: the["graph", <name>, …]array form in awheretakes its name as written. Resolving it as the node-level form does means telling a dataset alias written as a name ("test/movies") from a relative IRI, which goes with fix: typed dataset and graph references: one grammar, one resolver #2007's dataset-reference parser.validate, andstage+commit_staged#2076: push through the shared check tail, keeping its replay semantics and statuses (uniqueness and the config-group checks on pushed commits);validateuses default config when the config graph cannot be read;stagepluscommit_stagedwrites the config graph without registering it in the commit'sgraph_delta.@foodropped vs refused;@odata.etagdropped vs stored, which predates this PR).BINDorIRI()over a namespace the ledger has not seen, or a graph source's IRI, cannot be an update template subject (it predates the PR, and is why the enable recipe comes in two forms); facts stored under the fallback namespace encoding before commit 28 stay split.FROM, streaming on the view lane, and the remaining readers of dataset text #2079: query-sideurn:defaultreads that find nothing: SPARQLFROM <ledger>withGRAPH <urn:default>, a queryfromNamedalias whose graph isurn:default, and a?gbound insideGRAPH <urn:default>then used inGRAPH ?g.next) #2093 (next): a fast-forward on a fresh instance loads an unindexed target to read its config; parity needs the config readable without a load (a persisted config summary), which changes a stored format.main→nextmerge: ports for #2009 and #2010 #2094: themain→nextmerge ports (the deviations comment lists this PR's).Deviations from the design
The main departures: the config is read once, after staging, so the inline-shapes policy-scope refusal comes after staging; one more cross-ledger brick is fixed outside the listed sites (
build_transact_policy_context); participation is per written graph; a governed fast-forward is validated in place rather than through the general merge; and uniqueness comes to branch operations with the shared check tail. Each of them, and the smaller ones, is written out in the deviations comment below.Commits
62 commits, reviewable in order: 59 on
c5874d586, the merge ofmainat58b53238a(60), one after it (61), and the merge ofmainatba4a982ec(62); every fix commit carries its tests. Commits 17–29 answer earlier review passes, and commits 30–47 answer bplatz's review and a review of those changes. Commits 48–59 answer his review of the previous push (fae15f997), and commit 61 follows the merge of #2052. Againstmainatba4a982ec, the diff is 111 files, +19.2k / −2.9k.The commit list, and the diff by kind
bench: jsonld_parse, a parse-only JSON-LD transaction bench(baseline; no production change).json-ld: one @graph classifier shared by every reader(graph_shape.rs,is_graph_key).transact: one lexical graph scope for JSON-LD templates(GraphScope,WriteGraphs, one resolver overdataset_ref::GraphSel).tests: JSON-LD graph scope end to end, with SPARQL twins.transact: JSON-LD 1.1 named-graph objects; only envelopes carry txn-meta.transact: one blank-node label sequence per JSON-LD document.json-ld/transact: @reverse, @included and @nest; refuse other keywords.transact: annotation lowering shares the scope; one f:reifiesGraph emitter.query: node-level @graph in a JSON-LD where is GRAPH sugar.import: bulk JSON-LD import refuses @graph scoping.api: config reader normalization + diagnostics.tests: SHACL tests enable SHACL explicitly(no behavior change; see the test-helper row of the mutation table in the tests comment).api: explicit-only SHACL posture.api: resolve validation artifacts only for participating graphs.api: staged config guard, merged with the reasoning-mode check.docs: graph scoping, explicit-only SHACL, config checks and recipes.json-ld: a top-level single bare {"@id"} in @graph is envelope content.query/transact: a where node's @graph names its graph as templates do(parse/graph_name.rs, shared by the template resolver and thewheresugar).api: one post-staging check tail for every write lane(check_staged_write,WriteChecks).merge: validate fast-forwards into a target whose config governs writes.api: the config guard refuses a group split across transactions.transact: read opts.shapes and opts.uniqueProperties from the body.api/transact: review nits(annotations under a variable graph, the graph-insert selector message, two diagnostics).docs: compatibility notes for the review changes.merge/rebase/revert: re-check the target's config groups.api: the config guard reads config only for config writes(is_config_field).query/transact: a where's "default" is the ledger's default graph.transact: flake generation stores every IRI under its canonical namespace code.transact: an update's where reads top-level values in the ledger's encoding.api: governance is read once, from the state before the write(multi-operation requests and transactions governed by the state before them and guarded on their result; the fast-forward config test narrowed to config settings;RequestGovernance,ConfigRead; feat(api): multi-write transactions #2023'sTransactionthroughSequentialStager::stage_with).api/server: inline uniqueness follows the inline-shapes policy rule.api: one config vocabulary for the reader, the guard, diagnostics and the recipe(config_vocab.rs; the root subject's fields; every stranded node diagnosed).json-ld: expansion borrows its path and its context.transact: annotation nodes are included in their edge's subject(placement by construction; the edge graph on delete; selector graph names; reverse properties).merge/rebase/revert: refuse carrying another branch's config graph.api/cli: typed config errors and statuses; tracked info prints diagnostics(409 for branch operations; 500err:storage/ReadFailurefor a config read failure).push: graph ids come from the ledger's graph registry.api: a ledger warns at load when it holds shapes no config enforces.docs: enabling SHACL leads with INSERT DATA; compatibility notes(and the persisted-data notes).tests: overflow values read from the default graph under a named base graph(the6a03e83a4seam).api: the recommended config subject's fields belong in the config graph.api: a plain Turtle insert honors inline uniqueness constraints.api: a commit that declares a property unique makes earlier stages stage again.merge: a merge does not change the target's system graphs.docs: fast-forward triggers, branch-operation statuses, latest compatibility notes.merge: a fast-forward's config-settings test keeps only what a refusal needs(thef:type arm, which no refusal needs, is dropped).json-ld: a scoped context's term IRIs expand against the active context(predates the PR; found by a differential run of the expander).query: a CONSTRUCT template node's own @graph names its output graph.query: one @graph selector path for where nodes, top-level and nested(no behavior change).api: untyped shapes count as shapes; SHACL switched off is not reported.api: a plain Turtle insert applies inline shapes and a requested mode.graphql: sh:minCount makes a field non-null only where SHACL is enforced(the schema cache keys on the view's graph).rebase: a config refusal names the commit the rebase stopped on.cli/server: config diagnostics without the full ledger info; MCP lists them.tests: a typed reverse term's annotation; an anonymous split config group.docs: JSON-LD deviations; graph names through @vocab; plain Turtle options.tests: every kind of operation a transaction stages is checked against its policy.docs: a CONSTRUCT template node's @graph; what counts as unenforced shapes.query: the CONSTRUCT shorthand keeps a where node's graph.mainat58b53238a(fix(server): query routes read a request's dataset the way the engine does #2019, fix(index): every xsd:double value is indexable #2044, perf(index): fewer requests for remote leaf reads; whole-leaf fetch by default #2048, perf(index): reuse zstd contexts per thread for column blocks #2049, fix(api): key cross-ledger governance by the model ledger's head commit #2052).api: a request reads each model ledger at one head.mainatba4a982ec(fix(server): one HTTP status table for API errors #2102, perf(indexer): warm-on-write only the leaflets readers had cached, on a bounded pool #2068, fix(indexer): debug index builds accept f:message, f:author and f:reverts in txn-meta #2056, perf: decode only the columns count fast paths read; one cached entry per leaflet #2053, build(deps): move RustSec-flagged dependencies to patched releases #2046, ci(release): pre-releases publish to Homebrew as fluree-beta #2045, v4.2.4).Diff against
ba4a982ec(111 files, +19.2k / −2.9k): Rust under src/ +9.9k / −2.5k (including in-file test modules), integration tests andshacl_tests.rs+8.7k / −0.4k, docs +0.4k / −0.1k, bench +0.2k.Tests and non-vacuity
New and changed tests cover every area this touches: JSON-LD scope and named graphs, txn-meta, annotations, the
wheregraph names, the bulk-import refusal, the config reader and diagnostics, the explicit-only matrix, fast-forward validation and uniqueness on branch operations, inline shapes from options and from the body, the cross-ledger fail-closed pairs, and the config guard on every lane. Every fix was checked by reverting only the fix, watching its tests fail, and restoring. The area list and the mutation table are in the tests comment below.Gates
Rebased onto
mainatc5874d586(#2006, #2008, #2013, #2036 and #1451 merged since the previous round; how each meets this PR is in Sequencing above).tx.rs(fix(transact): retractions name stored facts; RDF text is parsed once #2008's RDF-text lane and the sync parse),jsonld.rs(fix(transact): retractions name stored facts; RDF text is parsed once #2008's ill-typed-literal flag),stage.rs(fix(query): evaluate grouped SELECT expressions once per group #2006's constraint failures),parse/mod.rs(fix(query): evaluate grouped SELECT expressions once per group #2006's top-levelVALUES) and two test files. The SHACL testsmainadded since are migrated to an explicit config in commit 12.shacl_tests) and 30 (80 transaction and RDF-text tests). Commit 20's fast-forward test was changed after that; its 16 fast-forward tests were run at commit 20 on its own, and pass.At commit 58 (
cca4934bd), on a shared Mac under load from other builds, these pass:-D warnings,--all-featuresand default features, on api, transact, query, CLI, server, core and JSON-LD, except core's default-feature run (below); both wasm32 clippy runs from CI;cargo check --workspace --all-features --all-targets, and CI's server check without default features;nextest:
fluree-db-apiwithshacl,graphql;Commit 59 (
7125b79dc) changes the query parser's CONSTRUCT shorthand, a test and two doc lines; for it, fmt, clippy-D warningson query and api (all features and default), 1,723 query tests, 97 CONSTRUCT and graph-scope tests across the api test binaries and the query doc tests pass, and its test fails with the fix reverted.Commit 61's test (
0454f401e) fails with the request's head pins removed, and with a rollback's restage dropping them.At the head (
cf1d8b586, after the merge ofmainatba4a982ec): fmt;cargo check --workspace --all-targets, default and all features; clippy-D warningson api, transact, consensus, server and SHACL, all features and default; nextest 4,531 passed / 21 skipped forfluree-db-apiwithshacl,graphql(it_transaction,it_policy_cross_ledgerandit_shapes_cross_ledgerincluded), 850 for the api lib without SHACL, 3,397 over transact, JSON-LD, query, core and policy, and 779 over consensus and server. The status-table test fails with a config read failure classified 400.Not run locally, or not clean:
fluree-db-corealone fails on an unused test import incommit.rs, which this PR does not touch and which fails the same way onmain. The workspace-level clippy CI runs unifies features and does not see it.--all-featuresnextest that CI runs (api'siceberg,sql,delta,aws,credentialandvectortargets, and the crates not listed).testsuite-shaclandtestsuite-sparql; this PR touches neither.