Skip to content

perf: typed RDF literal storage + resolveLiteral API — indexed WHERE, boot migrations, envelope-free writes#842

Open
HexaField wants to merge 53 commits into
devfrom
refactor/typed-rdf-literals-and-fn-cleanup
Open

perf: typed RDF literal storage + resolveLiteral API — indexed WHERE, boot migrations, envelope-free writes#842
HexaField wants to merge 53 commits into
devfrom
refactor/typed-rdf-literals-and-fn-cleanup

Conversation

@HexaField

@HexaField HexaField commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Important

Description updated after merging #865. The original description (preserved below, struck through) framed this as resolveLanguage being replaced by resolveLiteral. That is no longer the design: resolveLanguage is retained as a first-class option and behaves exactly as before. resolveLiteral is an additive per-property optimization. Corrected summary follows.

Summary

Replaces the NamedNode-IRI storage model for property values with native typed RDF literals, so Oxigraph's POS index can serve equality/comparison WHERE filters directly instead of scanning through a custom SPARQL function. Adds resolveLiteral as a literal-storage optimization alongside the existing resolveLanguage selector.

Typed RDF literal storage (Rust executor)

Wire-format literal:string:X / :number:N / :boolean:B / :json:J targets are now stored as typed RDF terms ("X"^^xsd:string, "42"^^xsd:integer, "true"^^xsd:boolean, "…"^^ad4m://json) rather than as NamedNode IRIs. This is the core change — it means the SPARQL engine can evaluate ?source <pred> "active"^^xsd:string . as an index lookup instead of binding every row and filtering with fn/parse_literal.

Key files: sparql_store.rstarget_to_storage_term() / storage_term_to_target_string() handle the wire↔storage translation; make_direct_triple() returns a Term instead of a NamedNode.

Index-friendly WHERE builders

Both the root model query builder (sparql_builder.rs) and the projection WHERE builder (projection.rs) now emit typed-literal terms directly:

  • String equality: VALUES ?x { "val"^^xsd:string } (with UNION <val> fallback when the value is IRI-shaped)
  • Boolean: ?source <pred> "true"^^xsd:boolean .
  • Number: "42"^^xsd:integer . or "3.14"^^xsd:decimal .
  • StringArray / NumberArray: VALUES ?x { "a"^^xsd:string "b"^^xsd:string }
  • Ops (gt/lt/gte/lte/between/not/contains): native xsd datatype comparison, no intermediate BIND

Deterministic properties match against typed literals (indexed); envelope / custom-resolveLanguage properties are matched on the decoded value via fn/parse_literal (correct, not index-served). The build-time is_deterministic_literal() decides which.

resolveLanguage (unchanged) + resolveLiteral (additive, opt-in) — TS + Rust

Neither option is defaulted at the decorator / json-schema layer. The effective storage mode is derived from what a property explicitly sets (shared TS effectiveLiteralStorage / Rust is_deterministic_literal):

explicitly set storage mode
neither deterministic literal — POS-index friendly (the perf default)
resolveLiteral: true deterministic literal (explicit opt-in)
resolveLiteral: false signed literal envelope
resolveLanguage: "literal" signed literal envelope — provenance in the value
resolveLanguage: <custom> expression_create on that language

resolveLiteral (when set) wins over the language-implied default; it is ignored for a custom resolveLanguage. resolveLanguage keeps its dev meaning — naming the literal language produces a signed expression ({author, timestamp, data, proof}). The only behavior change vs dev: a property with no resolve options now stores a deterministic literal (perf) instead of an envelope.

SHACL emits only what was explicitly set; readers no longer derive resolveLiteral from resolveLanguage. No breaking change to existing resolveLanguage user code. Flux opts in per-property: channels omit resolveLanguage (deterministic, indexed; provenance on the reifier), messages keep resolveLanguage:"literal" (signed-expression provenance in the value).

A property's transform is applied on read to expression-resolved values (custom language, or resolveLanguage:"literal" / resolveLiteral:false) and to any property that declares a transform.

Write path

  • literal_decode in Rust no longer wraps primitives in synthetic {author, timestamp, data, proof} envelopes — returns the decoded value directly.
  • Ad4mModel.setProperty / Rust resolve_property_value: deterministic literal: IRIs for the deterministic mode (booleans/objects encode as literal:boolean: / literal:json:); expression_create("literal", …) for the envelope mode; expression_create(lang, …) for a custom language.
  • Mention detection unwraps the envelope via fn/parse_literal before substring matching, so author/proof DIDs inside an envelope don't produce false mentions.

Boot-time migrations

Two versioned migrations run at perspective initialization:

  • v3 — migrate_signed_envelopes_to_plain_literals(): rewrites literal:json:<envelope> NamedNode targets to the plain literal:<kind>:<data> form of their inner .data value, landing directly on typed-literal storage in a single pass. Shape-aware: predicates whose property is envelope-mode (resolveLiteral:false, explicit resolveLanguage:"literal", or a custom language) are skipped, so signed-expression provenance is preserved
  • v4 — migrate_iri_literals_to_typed_literals(): rewrites any remaining literal:*: NamedNode IRIs to their corresponding typed RDF literals

Both are idempotent (version-gated) and preserve reifier metadata. v3 failure blocks perspective init to avoid mixing legacy and migrated targets.

Instance ID scheme change

Auto-generated baseExpression values now use ad4m://obj/<id> instead of Literal.from(randomId).toUrl(), since the typed-literal storage layer would strip literal:string:<id> targets to "<id>"^^xsd:string — which can't be a triple subject. The ad4m://obj/ scheme keeps instance IDs unambiguously as NamedNode IRIs.

Bool/JSON write encoding + transform-on-read fixes (#865)

  • resolve_property_value (createSubject / MCP write path) encodes Bool as literal:boolean: and Object/Array as literal:json:, matching the TS valueToLiteralIri encoding (previously these fell through to a raw value.to_string() target).
  • resolve_language_transforms applies a property's transform to deterministic-literal properties that declare one, not only to expression-resolved properties.

Test plan

  • All TS core tests pass
  • Rust unit tests (CircleCI)
  • Integration tests (mcp-http, prolog-and-literals, model) — green locally and in CI
  • Backward compat: existing SHACL data with only resolveLanguage triples loads correctly
  • resolveLanguage (literal + custom) coexists with resolveLiteral; empty-string resolveLanguage preserved

🤖 Generated with Claude Code


⚠️ Original description — superseded by #865 (kept for history; inaccurate claims struck through)

Summary

Replaces the NamedNode-IRI storage model for property values with native typed RDF literals, and consolidates the decorator API from resolveLanguage: string to resolveLiteral: boolean. Together these changes let Oxigraph's POS index serve equality/comparison WHERE filters directly instead of scanning through a custom SPARQL function.

Typed RDF literal storage (Rust executor)

Wire-format literal:string:X / :number:N / :boolean:B / :json:J targets are now stored as typed RDF terms ("X"^^xsd:string, "42"^^xsd:integer, "true"^^xsd:boolean, "…"^^ad4m://json) rather than as NamedNode IRIs. This is the core change — it means the SPARQL engine can evaluate ?source <pred> "active"^^xsd:string . as an index lookup instead of binding every row and filtering with fn/parse_literal.

Key files: sparql_store.rstarget_to_storage_term() / storage_term_to_target_string() handle the wire↔storage translation; make_direct_triple() returns a Term instead of a NamedNode.

Index-friendly WHERE builders

Both the root model query builder (sparql_builder.rs) and the projection WHERE builder (projection.rs) now emit typed-literal terms directly:

  • String equality: VALUES ?x { "val"^^xsd:string } (with UNION <val> fallback when the value is IRI-shaped)
  • Boolean: ?source <pred> "true"^^xsd:boolean .
  • Number: "42"^^xsd:integer . or "3.14"^^xsd:decimal .
  • StringArray / NumberArray: VALUES ?x { "a"^^xsd:string "b"^^xsd:string }
  • Ops (gt/lt/gte/lte/between/not/contains): native xsd datatype comparison, no intermediate BIND

The fn/parse_literal custom function is preserved as a back-compat shim for legacy SDNA queries and Flux mention detection, but the model query engine no longer depends on it.

Signed-envelope removal from write path

  • literal_decode in Rust no longer wraps primitives in synthetic {author, timestamp, data, proof} envelopes — returns the decoded value directly
  • Ad4mModel.setProperty uses valueToLiteralIri() (deterministic) for resolveLiteral: true (default), or createExpression("literal", ...) for resolveLiteral: false
  • Subscription mention matching uses STR(?target) directly instead of fn/parse_literal

Boot-time migrations

Two versioned migrations run at perspective initialization:

  • v3 — migrate_signed_envelopes_to_plain_literals(): rewrites literal:json:<envelope> NamedNode targets to the plain literal:<kind>:<data> form of their inner .data value, landing directly on typed-literal storage in a single pass
  • v4 — migrate_iri_literals_to_typed_literals(): rewrites any remaining literal:*: NamedNode IRIs to their corresponding typed RDF literals

Both are idempotent (version-gated) and preserve reifier metadata. v3 failure blocks perspective init to avoid mixing legacy and migrated targets.

resolveLiteral replaces resolveLanguage (TS + Rust) — SUPERSEDED: see "resolveLanguage (unchanged) + resolveLiteral (new, additive)" above

  • PropertyOptions.resolveLanguage?: stringPropertyOptions.resolveLiteral?: boolean (both options exist; resolveLanguage retained)
  • resolveLiteral: true (default) — deterministic literal:*: IRIs, no expression language involved
  • resolveLiteral: false — routes through expression_create("literal", ...) for signed-envelope storage
  • SHACL emission: ad4m://resolveLiteral → literal:true|false (new) with backward compat reading ad4m://resolveLanguage → literal:string:literal as resolveLiteral: true in both TS (SHACLShape.fromLinks) and Rust (shape.rs, shacl.rs, perspective_instance.rs)

Instance ID scheme change

Auto-generated baseExpression values now use ad4m://obj/<id> instead of Literal.from(randomId).toUrl(), since the typed-literal storage layer would strip literal:string:<id> targets to "<id>"^^xsd:string — which can't be a triple subject. The ad4m://obj/ scheme keeps instance IDs unambiguously as NamedNode IRIs.

Other changes

  • Literal.ts: added boolean: prefix parsing
  • parseLit (SparqlBindings.ts): simplified — JSON objects are JSON-stringified directly instead of extracting .data (envelope unwrapping removed)
  • validate_iri (utils.rs): now rejects control and whitespace characters
  • json-schema.ts: simplified — removed cascading resolveLanguage defaulting logic, replaced with single resolveLiteral ?? true (json-schema now defaults both resolveLanguage: "literal" and resolveLiteral: true)
  • sdna.ts: resolveLiteral !== false emits property_resolve_language(uuid, prop, "literal") for Prolog compat
  • Integration tests: instance IDs changed from Literal.from("text").toUrl() to ad4m://test/<name> scheme; assertions updated from literal.data to literal (no envelope unwrapping)

Files changed (34 files, +1918/-563)

TypeScript core (16 files):
Literal.ts, Ad4mModel.ts, decorators.ts, types.ts, SHACLShape.ts, shacl-gen.ts, query-sparql.ts, query-utils.ts, json-schema.ts, sdna.ts, PerspectiveProxy.ts, SparqlBindings.ts, shacl/index.ts, Ad4mModel.test.ts, query-sparql.test.ts, shacl-roundtrip.test.ts

Rust executor (15 files):
sparql_store.rs (typed-literal storage + migrations), sparql_builder.rs + projection.rs (indexed WHERE), perspective_instance.rs (resolveLiteral), shape.rs + types.rs (resolve_literal), query.rs, utils.rs, shacl_parser.rs, mcp/shacl.rs, mcp/tools/mod.rs + subscriptions.rs, languages/literal.rs, perspectives/mod.rs + migration.rs, integration_tests.rs

Integration tests (2 files):
mcp-http.test.ts, prolog-and-literals.test.ts

Test plan

  • All 422 TS core tests pass
  • Rust unit tests via CircleCI (can't run locally — OOMs on macbook)
  • Integration tests (mcp-http, prolog-and-literals) via CircleCI
  • Verify backward compat: existing SHACL data with old resolveLanguage triples loads correctly
  • Verify boot-time migrations on a perspective with legacy envelope-form data

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for boolean literals via literal:boolean: URLs.
    • Introduced deterministic typed RDF literal storage, replacing legacy envelope formats.
  • Breaking Changes

    • Property metadata now uses resolveLiteral: boolean instead of resolveLanguage: string. (superseded — both options exist; resolveLanguage retained) Database migrations automatically convert existing literal storage to the new typed format.
  • Improvements

    • Enhanced SPARQL query filtering for literal-stored properties with more accurate type handling.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@lucksus, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 53 minutes and 7 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4e782f9e-8362-443b-90c1-ebf318bb4dbe

📥 Commits

Reviewing files that changed from the base of the PR and between d21501b and 3c828c5.

📒 Files selected for processing (26)
  • core/src/model/Ad4mModel.ts
  • core/src/model/decorators.ts
  • core/src/model/json-schema.ts
  • core/src/model/query-sparql.ts
  • core/src/model/query-utils.ts
  • core/src/model/shacl-gen.ts
  • core/src/model/types.ts
  • core/src/perspectives/PerspectiveProxy.ts
  • core/src/shacl/SHACLShape.test.ts
  • core/src/shacl/SHACLShape.ts
  • rust-executor/src/mcp/shacl.rs
  • rust-executor/src/mcp/tools/mod.rs
  • rust-executor/src/mcp/tools/subscriptions.rs
  • rust-executor/src/perspectives/model_query/integration_tests.rs
  • rust-executor/src/perspectives/model_query/projection.rs
  • rust-executor/src/perspectives/model_query/query.rs
  • rust-executor/src/perspectives/model_query/round_trip_tests.rs
  • rust-executor/src/perspectives/model_query/shape.rs
  • rust-executor/src/perspectives/model_query/sparql_builder.rs
  • rust-executor/src/perspectives/model_query/test_helpers.rs
  • rust-executor/src/perspectives/model_query/types.rs
  • rust-executor/src/perspectives/perspective_instance.rs
  • rust-executor/src/perspectives/shacl_parser.rs
  • rust-executor/src/perspectives/sparql_store.rs
  • tests/js/tests/mcp-http.test.ts
  • tests/js/tests/prolog-and-literals.test.ts
📝 Walkthrough

Walkthrough

Replaces the string-based resolveLanguage property-resolution option with a boolean resolveLiteral across the TypeScript model layer, SHACL serialization, Rust query builders, and the SPARQL store. The store gains typed RDF literal storage with two new migration passes (v3 signed-envelope unwrapping and v4 IRI-to-typed-literal conversion), and literal_decode now returns primitives directly without envelope wrapping.

Changes

Resolve-literal model and storage convergence

Layer / File(s) Summary
Core type contracts and option surfaces
core/src/model/types.ts, core/src/model/decorators.ts, core/src/model/json-schema.ts, core/src/shacl/SHACLShape.ts, rust-executor/src/mcp/shacl.rs, rust-executor/src/perspectives/model_query/types.rs, rust-executor/src/perspectives/shacl_parser.rs
PropertyMetadata, PropertyOptions, JSONSchemaProperty, SHACLPropertyShape, ShapeProperty, ShaclProperty, and PropertyShape all replace resolveLanguage?: string with resolveLiteral?: boolean; docs and examples updated throughout.
Literal decode and binding value semantics
core/src/Literal.ts, core/src/perspectives/SparqlBindings.ts, rust-executor/src/languages/literal.rs
Literal.get() adds a boolean: branch; literal_decode removes the signed-envelope wrapper and returns primitives directly; parseLit always JSON.stringifys decoded objects instead of extracting .data.
SHACL serialization, generation, and backward-compat parsing
core/src/shacl/SHACLShape.ts, core/src/model/shacl-gen.ts, core/src/perspectives/PerspectiveProxy.ts, core/src/shacl/index.ts
toLinks/fromLinks/toJSON/fromJSON emit and read ad4m://resolveLiteral with legacy resolveLanguage fallback mapping; datatype inference uses resolveLiteral; PerspectiveProxy metadata types updated.
TypeScript model runtime and query helpers
core/src/model/Ad4mModel.ts, core/src/model/query-sparql.ts, core/src/model/query-utils.ts, core/src/model/sdna.ts, core/src/model/json-schema.ts
Ad4mModel constructor generates ad4m://obj/<id> IRIs; setProperty/save branch on resolveLiteral !== false; valueToLiteralIri exported; isLiteralStoredProperty and buildWhereCondition use resolveLiteral; JSON-schema builder simplifies to single flag.
Rust resolve_literal ingestion and property resolution
rust-executor/src/mcp/shacl.rs, rust-executor/src/perspectives/perspective_instance.rs, rust-executor/src/perspectives/shacl_parser.rs, rust-executor/src/perspectives/model_query/shape.rs, rust-executor/src/perspectives/model_query/query.rs
get_resolve_literal_from_shacl added; resolve_property_value routes Some(false) through expression_create; MCP API replaces resolve_property_resolve_language with resolve_property_resolve_literal; shape loading adds OPTIONAL ?resolveLiteral binding.
SPARQL store typed literal term conversion
rust-executor/src/perspectives/sparql_store.rs
target_to_storage_term and storage_term_to_target_string convert literal:*: wire format to/from typed RDF Literal terms; insert_link_triples, remove_link, for_each_matched_link, and link_from_solution updated; parse_literal custom function rewritten; query() serializes target/t variables as wire strings.
Rust SPARQL/projection builders and literal matching
rust-executor/src/perspectives/model_query/sparql_builder.rs, rust-executor/src/perspectives/model_query/projection.rs, rust-executor/src/perspectives/model_query/utils.rs, rust-executor/src/mcp/tools/subscriptions.rs
Builders emit VALUES with typed literals (xsd:string/xsd:boolean/xsd:integer/xsd:decimal); projection branches per is_literal_prop; contains uses STR(); format_literal_number and looks_like_absolute_iri helpers added; mention filter uses STR(?target) directly.
SPARQL store migration chain
rust-executor/src/perspectives/sparql_store.rs, rust-executor/src/perspectives/mod.rs, rust-executor/src/perspectives/migration.rs
migrate_signed_envelopes_to_plain_literals (v3) unwraps literal:json: envelope targets; migrate_iri_literals_to_typed_literals (v4) converts IRI-shaped literal:*: NamedNodes to typed Literals; initialize_from_db runs both in sequence with error-abort.
TypeScript model and SHACL tests
core/src/model/Ad4mModel.test.ts, core/src/model/query-sparql.test.ts, core/src/model/shacl-roundtrip.test.ts, core/src/shacl/SHACLShape.test.ts
All fixtures and assertions updated from resolveLanguage to resolveLiteral; round-trip toLinks/fromJSON backward-compat paths validated.
Rust store migration and integration tests
rust-executor/src/perspectives/sparql_store.rs, rust-executor/src/perspectives/model_query/integration_tests.rs, rust-executor/src/perspectives/perspective_instance.rs
New tests for v3→v4 migration chain, typed-literal query/filter correctness, plain-literal pagination; fixture IDs updated to ad4m:// for subject stability; SPARQL filter tests updated from parse_literal to STR().
JavaScript end-to-end tests
tests/js/tests/mcp-http.test.ts, tests/js/tests/prolog-and-literals.test.ts
MCP and Prolog tests assert deterministic literal:boolean:/literal:string: targets; remove legacy envelope expectations; replace Literal.from(...).toUrl() IDs with ad4m://test/... where conformance filtering needs NamedNode subjects; subscription polling ceiling raised to 60 s.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Ad4mModel
  participant sparql_store
  participant oxigraph

  rect rgba(70, 130, 180, 0.5)
    note over Client,oxigraph: Write path (resolveLiteral === true, default)
    Client->>Ad4mModel: setProperty("name", "Alice")
    Ad4mModel->>Ad4mModel: valueToLiteralIri("Alice") → "literal:string:Alice"
    Ad4mModel->>sparql_store: addLink(target="literal:string:Alice")
    sparql_store->>sparql_store: target_to_storage_term → "Alice"^^xsd:string
    sparql_store->>oxigraph: store typed Literal triple
  end

  rect rgba(180, 100, 50, 0.5)
    note over Client,oxigraph: Query path
    Client->>Ad4mModel: query(where={name: "Alice"})
    Ad4mModel->>sparql_store: SPARQL VALUES ?name { "Alice"^^xsd:string }
    sparql_store->>oxigraph: evaluate pattern
    oxigraph-->>sparql_store: Literal("Alice", xsd:string)
    sparql_store->>sparql_store: storage_term_to_target_string → "literal:string:Alice"
    sparql_store-->>Ad4mModel: result rows
    Ad4mModel-->>Client: model instances
  end

  rect rgba(60, 160, 60, 0.5)
    note over Client,oxigraph: Migration path (startup)
    sparql_store->>sparql_store: migrate_signed_envelopes_to_plain_literals (v3)
    sparql_store->>sparql_store: migrate_iri_literals_to_typed_literals (v4)
    sparql_store->>oxigraph: rewrite legacy NamedNode targets → typed Literals
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • coasys/ad4m#632: Introduced PropertyMetadata.resolveLanguage?: string in Ad4mModel.ts, which this PR refactors to resolveLiteral?: boolean along with related literal-resolution logic.
  • coasys/ad4m#654: Directly touched SHACLPropertyShape structures that this PR migrates from resolveLanguage to resolveLiteral.
  • coasys/ad4m#831: Introduced parseLit in SparqlBindings.ts, whose JSON/object decoding behavior is then changed in this PR.

Suggested reviewers

  • jhweir

🐇 From string to bool, the literals align,
resolveLiteral: true — a cleaner design!
Typed triples stored in oxigraph's keep,
Old envelopes migrated from their sleep.
No more parse_literal in every query's art,
Just STR(?target) — straight to the heart! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately captures the main technical change: migration to typed RDF literal storage with a simplified resolveLiteral boolean API, indexed WHERE optimization, and boot migrations.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/typed-rdf-literals-and-fn-cleanup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@HexaField HexaField changed the title refactor: typed RDF literals + remove fn/parse_literal (stacked on #837) refactor: typed RDF literals on the wire + remove fn/parse_literal Jun 4, 2026
@HexaField HexaField changed the title refactor: typed RDF literals on the wire + remove fn/parse_literal refactor: store property values as typed RDF literals, drop fn/parse_literal Jun 4, 2026
@HexaField HexaField marked this pull request as ready for review June 8, 2026 11:42
@HexaField HexaField force-pushed the refactor/typed-rdf-literals-and-fn-cleanup branch 2 times, most recently from a9ed90b to d82cc9a Compare June 11, 2026 01:06
HexaField added a commit that referenced this pull request Jun 11, 2026
Hypothesis fix for the model-test failures on #842 caused by the
typed-literal storage layer eating relation targets. When a model
instance is constructed without an explicit baseExpression, we used to
generate `Literal.from(makeRandomId(24)).toUrl()` → `literal:string:<id>`.
That format is fine pre-typed-literal because the storage layer treats
it as a NamedNode IRI both ways.

After #842's `target_to_storage_term`, however, any `literal:string:X`
*target* gets stripped down to a typed `"X"^^xsd:string` literal — which
is correct for property values but wrong for relation targets that need
to remain NamedNode IRIs pointing at sibling instances. A `<post>
--has_comment--> <literal:string:abc>` link would land as
`<post> --has_comment--> "abc"^^xsd:string`, and the include sub-query
that later does `VALUES ?source { <literal:string:abc> }` would no
longer find the comment as a NamedNode source.

Switch to a dedicated `ad4m://obj/<id>` IRI scheme so auto-generated
baseExpressions can never be mistaken for property string literals.
Production callers that pass an explicit `baseExpression` are unaffected.
`Literal.fromUrl` is only used in tests and runtime code against
`link.data.target` (property values), never against instance IDs, so
this scheme change doesn't break any existing decoder.
HexaField added a commit that referenced this pull request Jun 11, 2026
Hypothesis fix for the model-test failures on #842 caused by the
typed-literal storage layer eating relation targets. When a model
instance is constructed without an explicit baseExpression, we used to
generate `Literal.from(makeRandomId(24)).toUrl()` → `literal:string:<id>`.
That format is fine pre-typed-literal because the storage layer treats
it as a NamedNode IRI both ways.

After #842's `target_to_storage_term`, however, any `literal:string:X`
*target* gets stripped down to a typed `"X"^^xsd:string` literal — which
is correct for property values but wrong for relation targets that need
to remain NamedNode IRIs pointing at sibling instances. A `<post>
--has_comment--> <literal:string:abc>` link would land as
`<post> --has_comment--> "abc"^^xsd:string`, and the include sub-query
that later does `VALUES ?source { <literal:string:abc> }` would no
longer find the comment as a NamedNode source.

Switch to a dedicated `ad4m://obj/<id>` IRI scheme so auto-generated
baseExpressions can never be mistaken for property string literals.
Production callers that pass an explicit `baseExpression` are unaffected.
`Literal.fromUrl` is only used in tests and runtime code against
`link.data.target` (property values), never against instance IDs, so
this scheme change doesn't break any existing decoder.
@HexaField HexaField force-pushed the refactor/typed-rdf-literals-and-fn-cleanup branch from 3a8c2ea to 5b27d38 Compare June 11, 2026 02:05
HexaField added a commit that referenced this pull request Jun 11, 2026
…st IDs

The five type-filtered relation tests on this PR's branch reused
`Literal.from(label).toUrl()` to mint deterministic baseExpressions
for the Article/Comment/Message instances they create AND for the
`target` of the relation links between them. The typed-literal storage
layer (#842) strips `literal:string:X` link targets down to
`"X"^^xsd:string` typed literals, which then can't be subjects under
the `?target <type-pred> <type-value>` conformance probe that drives
the type-filter — so every test came back with 0 matches and failed.

Switching the test IDs to a dedicated `ad4m://test/<slug>` scheme keeps
them as NamedNode IRIs end-to-end: as link targets they stay routable,
and the type-filter conformance probe finds the corresponding subject
triples. Production behaviour is unchanged — the auto-generated
baseExpressions in Ad4mModel already use the matching `ad4m://obj/<id>`
scheme; this is a pure test fixture adjustment.

Updated:
- can constrain collection entries through 'where' clause
- can constrain relation entries through SPARQL getter
- should filter collection by type with class reference
- should filter collection by type with string class name
- should filter results in findAll() by type
@HexaField HexaField force-pushed the refactor/typed-rdf-literals-and-fn-cleanup branch from 28389fd to 526942c Compare June 11, 2026 03:19
HexaField added a commit that referenced this pull request Jun 11, 2026
Hypothesis fix for the model-test failures on #842 caused by the
typed-literal storage layer eating relation targets. When a model
instance is constructed without an explicit baseExpression, we used to
generate `Literal.from(makeRandomId(24)).toUrl()` → `literal:string:<id>`.
That format is fine pre-typed-literal because the storage layer treats
it as a NamedNode IRI both ways.

After #842's `target_to_storage_term`, however, any `literal:string:X`
*target* gets stripped down to a typed `"X"^^xsd:string` literal — which
is correct for property values but wrong for relation targets that need
to remain NamedNode IRIs pointing at sibling instances. A `<post>
--has_comment--> <literal:string:abc>` link would land as
`<post> --has_comment--> "abc"^^xsd:string`, and the include sub-query
that later does `VALUES ?source { <literal:string:abc> }` would no
longer find the comment as a NamedNode source.

Switch to a dedicated `ad4m://obj/<id>` IRI scheme so auto-generated
baseExpressions can never be mistaken for property string literals.
Production callers that pass an explicit `baseExpression` are unaffected.
`Literal.fromUrl` is only used in tests and runtime code against
`link.data.target` (property values), never against instance IDs, so
this scheme change doesn't break any existing decoder.
HexaField added a commit that referenced this pull request Jun 11, 2026
…st IDs

The five type-filtered relation tests on this PR's branch reused
`Literal.from(label).toUrl()` to mint deterministic baseExpressions
for the Article/Comment/Message instances they create AND for the
`target` of the relation links between them. The typed-literal storage
layer (#842) strips `literal:string:X` link targets down to
`"X"^^xsd:string` typed literals, which then can't be subjects under
the `?target <type-pred> <type-value>` conformance probe that drives
the type-filter — so every test came back with 0 matches and failed.

Switching the test IDs to a dedicated `ad4m://test/<slug>` scheme keeps
them as NamedNode IRIs end-to-end: as link targets they stay routable,
and the type-filter conformance probe finds the corresponding subject
triples. Production behaviour is unchanged — the auto-generated
baseExpressions in Ad4mModel already use the matching `ad4m://obj/<id>`
scheme; this is a pure test fixture adjustment.

Updated:
- can constrain collection entries through 'where' clause
- can constrain relation entries through SPARQL getter
- should filter collection by type with class reference
- should filter collection by type with string class name
- should filter results in findAll() by type
@HexaField HexaField force-pushed the refactor/typed-rdf-literals-and-fn-cleanup branch from 526942c to 94da674 Compare June 11, 2026 09:30
@HexaField HexaField marked this pull request as draft June 11, 2026 09:58
@HexaField HexaField marked this pull request as ready for review June 12, 2026 02:45
@HexaField HexaField force-pushed the refactor/typed-rdf-literals-and-fn-cleanup branch from 94da674 to 7bbbf2e Compare June 16, 2026 14:31
HexaField added a commit that referenced this pull request Jun 16, 2026
Hypothesis fix for the model-test failures on #842 caused by the
typed-literal storage layer eating relation targets. When a model
instance is constructed without an explicit baseExpression, we used to
generate `Literal.from(makeRandomId(24)).toUrl()` → `literal:string:<id>`.
That format is fine pre-typed-literal because the storage layer treats
it as a NamedNode IRI both ways.

After #842's `target_to_storage_term`, however, any `literal:string:X`
*target* gets stripped down to a typed `"X"^^xsd:string` literal — which
is correct for property values but wrong for relation targets that need
to remain NamedNode IRIs pointing at sibling instances. A `<post>
--has_comment--> <literal:string:abc>` link would land as
`<post> --has_comment--> "abc"^^xsd:string`, and the include sub-query
that later does `VALUES ?source { <literal:string:abc> }` would no
longer find the comment as a NamedNode source.

Switch to a dedicated `ad4m://obj/<id>` IRI scheme so auto-generated
baseExpressions can never be mistaken for property string literals.
Production callers that pass an explicit `baseExpression` are unaffected.
`Literal.fromUrl` is only used in tests and runtime code against
`link.data.target` (property values), never against instance IDs, so
this scheme change doesn't break any existing decoder.
HexaField added a commit that referenced this pull request Jun 16, 2026
…st IDs

The five type-filtered relation tests on this PR's branch reused
`Literal.from(label).toUrl()` to mint deterministic baseExpressions
for the Article/Comment/Message instances they create AND for the
`target` of the relation links between them. The typed-literal storage
layer (#842) strips `literal:string:X` link targets down to
`"X"^^xsd:string` typed literals, which then can't be subjects under
the `?target <type-pred> <type-value>` conformance probe that drives
the type-filter — so every test came back with 0 matches and failed.

Switching the test IDs to a dedicated `ad4m://test/<slug>` scheme keeps
them as NamedNode IRIs end-to-end: as link targets they stay routable,
and the type-filter conformance probe finds the corresponding subject
triples. Production behaviour is unchanged — the auto-generated
baseExpressions in Ad4mModel already use the matching `ad4m://obj/<id>`
scheme; this is a pure test fixture adjustment.

Updated:
- can constrain collection entries through 'where' clause
- can constrain relation entries through SPARQL getter
- should filter collection by type with class reference
- should filter collection by type with string class name
- should filter results in findAll() by type
@HexaField HexaField changed the title refactor: store property values as typed RDF literals, drop fn/parse_literal refactor: resolveLiteral flag — deterministic literal storage with expression contract separation Jun 16, 2026
@HexaField HexaField changed the base branch from refactor/literal-channel-v-separation to dev June 16, 2026 14:33
@HexaField HexaField changed the title refactor: resolveLiteral flag — deterministic literal storage with expression contract separation perf: typed RDF literal storage + resolveLiteral API — indexed WHERE, boot migrations, envelope-free writes Jun 16, 2026
HexaField and others added 7 commits June 18, 2026 18:26
Hypothesis fix for the model-test failures on #842 caused by the
typed-literal storage layer eating relation targets. When a model
instance is constructed without an explicit baseExpression, we used to
generate `Literal.from(makeRandomId(24)).toUrl()` → `literal:string:<id>`.
That format is fine pre-typed-literal because the storage layer treats
it as a NamedNode IRI both ways.

After #842's `target_to_storage_term`, however, any `literal:string:X`
*target* gets stripped down to a typed `"X"^^xsd:string` literal — which
is correct for property values but wrong for relation targets that need
to remain NamedNode IRIs pointing at sibling instances. A `<post>
--has_comment--> <literal:string:abc>` link would land as
`<post> --has_comment--> "abc"^^xsd:string`, and the include sub-query
that later does `VALUES ?source { <literal:string:abc> }` would no
longer find the comment as a NamedNode source.

Switch to a dedicated `ad4m://obj/<id>` IRI scheme so auto-generated
baseExpressions can never be mistaken for property string literals.
Production callers that pass an explicit `baseExpression` are unaffected.
`Literal.fromUrl` is only used in tests and runtime code against
`link.data.target` (property values), never against instance IDs, so
this scheme change doesn't break any existing decoder.
…or typed-literal storage

Legacy notification triggers and Flux-style mention-detection SPARQL
queries call `<ad4m://fn/parse_literal>(?target)` to unwrap the inner
`data` field of signed-expression envelopes — without it, the trigger
filter ends up matching the envelope's `author` field too (which is
always the local agent's DID), producing false positives on every
incoming message.

The typed-literal storage layer dropped the registration along with
`<ad4m://fn/parse_literal>`'s only previous role (NamedNode→string
unwrap), but the envelope-unwrap use-case has no equivalent in plain
SPARQL once values land as `"{...}"^^ad4m:json` typed literals.

Re-register `parse_literal` as a back-compat shim covering both the new
storage and any pre-migration NamedNode form:
- Typed `ad4m:json` literal → extract `.data` field (envelope unwrap)
- Typed `xsd:*` literal → return lexical form (same as STR)
- NamedNode `literal:string|number|boolean|json:…` → legacy decode
  (kept for any feeds still emitting that wire format)
- Anything else → return unchanged

Restores parity with the runtime test "can detect mentions in
notifications (Flux example)" without forcing every callsite to
migrate to a new function name in this PR.
…st IDs

The five type-filtered relation tests on this PR's branch reused
`Literal.from(label).toUrl()` to mint deterministic baseExpressions
for the Article/Comment/Message instances they create AND for the
`target` of the relation links between them. The typed-literal storage
layer (#842) strips `literal:string:X` link targets down to
`"X"^^xsd:string` typed literals, which then can't be subjects under
the `?target <type-pred> <type-value>` conformance probe that drives
the type-filter — so every test came back with 0 matches and failed.

Switching the test IDs to a dedicated `ad4m://test/<slug>` scheme keeps
them as NamedNode IRIs end-to-end: as link targets they stay routable,
and the type-filter conformance probe finds the corresponding subject
triples. Production behaviour is unchanged — the auto-generated
baseExpressions in Ad4mModel already use the matching `ad4m://obj/<id>`
scheme; this is a pure test fixture adjustment.

Updated:
- can constrain collection entries through 'where' clause
- can constrain relation entries through SPARQL getter
- should filter collection by type with class reference
- should filter collection by type with string class name
- should filter results in findAll() by type
Separates the literal expression language contract from deterministic
indexed storage. `resolveLiteral: true` (the default) stores values as
deterministic `literal:string:X` IRIs for efficient Oxigraph POS-index
lookups. `resolveLiteral: false` routes through `expression_create` on
the literal language, producing signed-envelope URIs with
author/timestamp/proof.

This resolves the design tension identified in PR review: the AD4M
expression language contract (expressionCreate → address →
expressionGet → {author,timestamp,data,proof}) is now preserved for
properties that opt into it, while the default path remains optimised
for indexed storage. Backward compat with old SHACL data
(ad4m://resolveLanguage → "literal") is handled in both TS and Rust.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests still referenced the removed resolveLanguage property,
causing TS compilation failures in CI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Ad4mModel.ts: defer all resolveLiteral:false props during creation,
   not just objects — primitives need expression path too
2. shacl-gen.ts: infer SHACL datatype from JS field type, not from
   resolveLiteral flag — numbers/booleans keep their proper xsd types
3. SHACLShape.ts: fall back to legacy resolve_language/resolveLanguage
   keys when loading SHACL JSON, mapping "literal" → resolveLiteral:true
4. mod.rs: v4 migration failure now blocks perspective init (matching v3)
   to prevent mixed IRI/typed-literal state
5. shape.rs: non-literal legacy resolveLanguage values map to Some(false)
   instead of being silently dropped (both SHACL-link and JSON paths)
6. perspective_instance.rs: propagate expression_create failure instead of
   falling back to raw JSON storage

Skipped: sdna.ts resolveLiteral !== false → === true suggestion.
The default is true (per PropertyOptions docs), so !== false correctly
treats undefined as "use default". Changing to === true would break
properties that don't explicitly set resolveLiteral.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/src/model/Ad4mModel.ts (1)

423-450: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the constructor docs to stop recommending literal-backed IDs.

The implementation now avoids literal:string:<id> because relation targets with that shape can be stored as typed literals instead of IRIs, but the JSDoc still says the default is a “random Literal URL” and shows "literal:..." as the explicit-ID example. Please update the public docs/example to use ad4m://obj/<id> or another non-literal:* IRI.

Proposed doc fix
-   * `@param` baseExpression - Optional expression URI for this instance.
-   *             If omitted, a random Literal URL is generated.
+   * `@param` baseExpression - Optional expression URI for this instance.
+   *             If omitted, a random `ad4m://obj/<id>` IRI is generated.
@@
-   * // Create with specific base expression
-   * const recipe = new Recipe(perspective, "literal:...");
+   * // Create with specific base expression
+   * const recipe = new Recipe(perspective, "ad4m://obj/custom-id");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/model/Ad4mModel.ts` around lines 423 - 450, Update the JSDoc comment
for the constructor method to reflect the actual implementation. Change the
description from saying a "random Literal URL is generated" to clarify that it
uses the `ad4m://obj/<id>` scheme. Update the example that shows `"literal:..."`
as the explicit baseExpression to instead show `"ad4m://obj/..."` format to be
consistent with the implementation and stop recommending literal-backed IDs for
relation targets.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/src/model/query-utils.ts`:
- Line 63: The default value of resolveLiteral at line 63 (in the isLiteral
assignment) is currently routing queries through legacy literal-string encoding
paths, which conflicts with the typed-literal query contract. Change the default
behavior so that resolveLiteral does not default to true, ensuring that queries
use typed-literal handling instead of legacy compatibility behavior. Also review
how resolveLiteral is forwarded at line 200 to ensure consistency with this new
default and that typed comparisons are not inadvertently routed through the
legacy literal-string encoding paths.

---

Outside diff comments:
In `@core/src/model/Ad4mModel.ts`:
- Around line 423-450: Update the JSDoc comment for the constructor method to
reflect the actual implementation. Change the description from saying a "random
Literal URL is generated" to clarify that it uses the `ad4m://obj/<id>` scheme.
Update the example that shows `"literal:..."` as the explicit baseExpression to
instead show `"ad4m://obj/..."` format to be consistent with the implementation
and stop recommending literal-backed IDs for relation targets.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fc720108-2dfb-4a9e-8f07-d992b59d5348

📥 Commits

Reviewing files that changed from the base of the PR and between fad952e and 3729e1f.

📒 Files selected for processing (35)
  • core/src/Literal.ts
  • core/src/model/Ad4mModel.test.ts
  • core/src/model/Ad4mModel.ts
  • core/src/model/decorators.ts
  • core/src/model/json-schema.ts
  • core/src/model/query-sparql.test.ts
  • core/src/model/query-sparql.ts
  • core/src/model/query-utils.ts
  • core/src/model/sdna.ts
  • core/src/model/shacl-gen.ts
  • core/src/model/shacl-roundtrip.test.ts
  • core/src/model/types.ts
  • core/src/perspectives/PerspectiveProxy.ts
  • core/src/perspectives/SparqlBindings.ts
  • core/src/shacl/SHACLShape.test.ts
  • core/src/shacl/SHACLShape.ts
  • core/src/shacl/index.ts
  • rust-executor/src/languages/literal.rs
  • rust-executor/src/mcp/shacl.rs
  • rust-executor/src/mcp/tools/mod.rs
  • rust-executor/src/mcp/tools/subscriptions.rs
  • rust-executor/src/perspectives/migration.rs
  • rust-executor/src/perspectives/mod.rs
  • rust-executor/src/perspectives/model_query/integration_tests.rs
  • rust-executor/src/perspectives/model_query/projection.rs
  • rust-executor/src/perspectives/model_query/query.rs
  • rust-executor/src/perspectives/model_query/shape.rs
  • rust-executor/src/perspectives/model_query/sparql_builder.rs
  • rust-executor/src/perspectives/model_query/types.rs
  • rust-executor/src/perspectives/model_query/utils.rs
  • rust-executor/src/perspectives/perspective_instance.rs
  • rust-executor/src/perspectives/shacl_parser.rs
  • rust-executor/src/perspectives/sparql_store.rs
  • tests/js/tests/mcp-http.test.ts
  • tests/js/tests/prolog-and-literals.test.ts
✅ Files skipped from review due to trivial changes (2)
  • core/src/shacl/index.ts
  • rust-executor/src/mcp/tools/mod.rs
🚧 Files skipped from review as they are similar to previous changes (25)
  • core/src/Literal.ts
  • core/src/model/shacl-roundtrip.test.ts
  • rust-executor/src/mcp/tools/subscriptions.rs
  • rust-executor/src/perspectives/migration.rs
  • rust-executor/src/perspectives/shacl_parser.rs
  • core/src/model/shacl-gen.ts
  • core/src/perspectives/SparqlBindings.ts
  • rust-executor/src/perspectives/model_query/types.rs
  • core/src/model/sdna.ts
  • core/src/model/query-sparql.ts
  • rust-executor/src/mcp/shacl.rs
  • core/src/model/query-sparql.test.ts
  • tests/js/tests/mcp-http.test.ts
  • rust-executor/src/languages/literal.rs
  • rust-executor/src/perspectives/perspective_instance.rs
  • rust-executor/src/perspectives/model_query/projection.rs
  • rust-executor/src/perspectives/model_query/query.rs
  • rust-executor/src/perspectives/model_query/sparql_builder.rs
  • rust-executor/src/perspectives/model_query/utils.rs
  • core/src/model/Ad4mModel.test.ts
  • core/src/perspectives/PerspectiveProxy.ts
  • core/src/model/decorators.ts
  • core/src/model/json-schema.ts
  • rust-executor/src/perspectives/model_query/shape.rs
  • rust-executor/src/perspectives/sparql_store.rs

Comment thread core/src/model/query-utils.ts
lucksus and others added 6 commits June 18, 2026 21:29
…data model

Restores resolve_language (general expression-language selector) as a
first-class field on ShapeProperty and PropertyShape, kept beside the
resolve_literal optimization flag added on this branch.

- types.rs / shacl_parser.rs: resolve_language: Option<String> re-added.
- parse_shacl_to_links: emits both ad4m://resolveLanguage and
  ad4m://resolveLiteral links when set.
- shape.rs (load_shape + load_shape_from_meta): populates resolve_language
  from RDF/meta; resolve_literal still derived from resolveLanguage for
  backward compat when the explicit flag is absent.

resolve_language is the dev mechanism (route values through any language);
resolve_literal is the literal-only deterministic-storage optimization.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restore get_resolve_language_from_shacl (string selector) beside
get_resolve_literal_from_shacl. resolve_property_value now applies the
unified precedence:

  1. custom resolve_language (!= "literal") -> expression_create(lang)
     -> signed-envelope URI (the dev behavior, lenient fallback on error)
  2. literal language + resolveLiteral: false -> expression_create("literal")
  3. otherwise -> deterministic literal: IRI (optimized default)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lve_literal

Add ShapeProperty::is_deterministic_literal() — true only for the literal
language (explicit or default) with resolve_literal != Some(false). Custom
languages always produce signed envelopes, so they are never deterministic.

- sparql_builder / projection: WHERE matching uses is_deterministic_literal()
  to decide typed-literal VALUES vs FILTER-on-decoded probing.
- query.rs resolve_language_transforms: now fetches+transforms any
  non-deterministic-literal property (custom language OR resolveLiteral:false),
  restoring dev's general expression-resolution read path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ShaclProperty carries resolve_language beside resolve_literal again, read
from ad4m://resolveLanguage in load_class_properties_with_uri. Add
resolve_property_resolve_language helper beside resolve_property_resolve_literal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… layer

resolveLanguage (general expression-language selector, default "literal") is
a first-class @Property option again, kept alongside the resolveLiteral
literal-storage optimization added on this branch.

- decorators / types: resolveLanguage?: string re-added; Property() defaults
  it to "literal", resolveLiteral to true.
- sdna: emits property_resolve_language with the actual language again.
- shacl-gen / SHACLShape / json-schema: carry + round-trip both fields
  (ad4m://resolveLanguage and ad4m://resolveLiteral links).
- Ad4mModel write path + query builders (query-sparql, query-utils,
  PerspectiveProxy): unified precedence — custom language -> createExpression
  on that language; literal + resolveLiteral:false -> createExpression on
  literal; else deterministic literal: IRI. Custom languages are never
  treated as deterministic-literal storage in WHERE building.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…empty-string

- Rust: struct-literal test fixtures carry both fields; shape.rs preserves an
  explicit empty resolve_language verbatim (matching dev) rather than coercing
  to None. New round_trip tests for resolve_literal true/false and a custom
  resolve_language, asserting is_deterministic_literal() classification.
- integration test exercises the non-literal FILTER path via a custom
  resolveLanguage on the target shape.
- TS: SHACLShape round-trip tests for resolveLanguage (toJSON/fromJSON and
  toLinks/fromLinks) and the resolveLanguage+resolveLiteral pairing.

Full Rust suite (640) and core TS suite (260) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HexaField and others added 12 commits June 19, 2026 12:30
The rebase onto dev brought in test code that references
ShapeProperty.resolve_language, which was removed by this branch
(replaced with resolve_literal). Removed all field references from
integration_tests.rs, test_helpers.rs, and round_trip_tests.rs.

Updated the round-trip test to assert resolve_literal: Some(true)
instead of resolve_language: Some("literal"), matching this branch's
backward-compat mapping.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… deterministic literals

Two read/write-path gaps surfaced by the resolveLanguage+resolveLiteral
integration tests:

1. resolve_property_value (write, createSubject / MCP path) only encoded
   String and Number for deterministic-literal properties; Bool, Object and
   Array fell through to `value.to_string()` and were stored as opaque raw
   targets (`false`, `{...}`) instead of `literal:boolean:` / `literal:json:`
   IRIs. The typed-literal storage layer then kept them as NamedNodes, so
   reads returned strings and indexed WHERE filters never matched. Encode
   Bool as `literal:boolean:{b}` and Object/Array via Literal::from_json,
   matching the TS `valueToLiteralIri` encoding.

2. resolve_language_transforms skipped deterministic-literal properties, so a
   property with `resolveLanguage: "literal"` plus a transform never had its
   transform applied on read. Include properties that carry a transform, and
   use the already-decoded hydrated value as the transform focus (never
   re-interpreting it as an expression URL).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…th-resolve-literal' into feat/restore-resolve-language-with-resolve-literal
…h-resolve-literal

Restore resolveLanguage alongside resolveLiteral
…s first-class

After restoring resolveLanguage alongside resolveLiteral, two comments still
framed behavior as resolveLiteral-only:

- SHACLShape transform doc said transforms are "only used for properties with
  resolveLiteral set". Transforms are a resolveLanguage feature — applied to
  values resolved through a custom language or the literal language with
  resolveLiteral: false, plus any property that declares a transform.
- mcp resolve_property_value doc only mentioned the resolveLiteral setting;
  it respects both resolveLanguage and resolveLiteral.

Comment-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l examples

The class-level example now demonstrates resolveLanguage (the value-resolution
selector: "literal" or a custom language address) alongside resolveLiteral (the
literal-language storage optimization), including a custom-language property
where resolveLiteral does not apply. Reflects that resolveLanguage is
first-class and resolveLiteral is additive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eral:false props

migrate_signed_envelopes_to_plain_literals flattened every literal:json signed
envelope to a plain literal, regardless of the property's resolution settings.
That destroys the expression-level signature for properties that explicitly
keep signed-expression literals (resolveLiteral: false, or a custom
resolveLanguage) — provenance that is distinct from and not recoverable via the
link-level reifier proof.

Build a "keep-envelope" predicate set from the SHACL property shapes
(resolveLiteral: false or a non-"literal" resolveLanguage) and skip those
predicates during flattening. Deterministic-literal (default) properties are
still normalized to typed literals so indexed WHERE equality keeps working on
upgraded perspectives. If the SHACL lookup yields nothing, every envelope is
flattened as before — no regression.

Adds test_migration_v3_preserves_envelopes_for_resolve_literal_false.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nto signed envelopes

Reworks the storage-mode semantics so resolveLiteral is a purely additive
performance optimization and resolveLanguage keeps its dev meaning, instead of
the default silently flipping literal properties from signed envelopes to
deterministic literals.

Effective storage mode (shared TS effectiveLiteralStorage / Rust
is_deterministic_literal), from the property's explicitly-set options:
  - neither set                  → deterministic literal (the perf default)
  - resolveLiteral: true         → deterministic literal (explicit opt-in)
  - resolveLiteral: false        → signed literal envelope
  - resolveLanguage: "literal"   → signed literal envelope (provenance in value)
  - resolveLanguage: <custom>    → expression on that language
resolveLiteral (when set) wins over the language-implied default.

Neither option is defaulted at the decorator / json-schema layer anymore, so
"explicitly literal" (envelope) is distinguishable from "unspecified"
(deterministic). SHACL emits only what was set; readers no longer derive
resolveLiteral from resolveLanguage.

Flux opts in per-property: channels omit resolveLanguage (deterministic,
index-friendly), messages keep resolveLanguage:"literal" for signed-expression
provenance distinct from the link-level reifier proof.

Query path:
  - WHERE/projection builders use parse_literal to compare on the decoded value
    for non-deterministic (envelope/custom) properties, while deterministic
    properties keep indexed typed-literal matching. Fixes value-WHERE on
    envelope properties and removes an invalid relative-IRI match for
    non-absolute string values.
  - mention detection unwraps the envelope (parse_literal) before substring
    matching, so author/proof DIDs in an envelope no longer produce false
    mentions.
  - v3 envelope-flattening migration keeps explicit resolveLanguage:"literal"
    envelopes (alongside resolveLiteral:false and custom languages).

Tests: prolog 75, model 149, mcp-http 67 green; new coverage — a Flux message
envelope round-trip (prolog), message-body provenance (mcp-http), and
is_deterministic_literal round-trip cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eterministic default)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants