Skip to content

rdf/turtle: accept JSON-LD id/type aliases on nested objects (#415)#416

Merged
melvincarvalho merged 2 commits into
gh-pagesfrom
issue-415-turtle-nested-vm
May 10, 2026
Merged

rdf/turtle: accept JSON-LD id/type aliases on nested objects (#415)#416
melvincarvalho merged 2 commits into
gh-pagesfrom
issue-415-turtle-nested-vm

Conversation

@melvincarvalho
Copy link
Copy Markdown
Contributor

Closes #415. Sibling of #389 (array-context) and #390 (@type: '@json').

Symptom

A nested CID v1 verificationMethod authored with the JSON-LD 1.1 id/type aliases (instead of @id/@type) was silently dropped from the Turtle output:

JSON-LD input:

"verificationMethod": [{
  "id": ".../card.jsonld#nostr-key-1",
  "type": "Multikey",
  "controller": ".../card.jsonld#me",
  "publicKeyMultibase": "fe70102de7ec..."
}]

Turtle output (excerpt):

<.../card.jsonld#me>
    cid:authentication <.../card.jsonld#nostr-key-1>.

Missing: the cid:verificationMethod predicate AND the entire #nostr-key-1 resource block (no a Multikey, no controller, no publicKeyMultibase).

Root cause

src/rdf/turtle.js checked for the explicit @-prefixed names everywhere. Two breakage points:

  • valueToTerm() returned null for {id: ..., ...} because it only inspected value['@id']. So the parent predicate quad was never emitted.
  • The BFS enqueue at the bottom of jsonLdToQuads checked v['@id'], so the nested object's own triples weren't enqueued either.

Fix

  • Two helpers: getNodeId(n)n['@id'] ?? n.id, getNodeType(n)n['@type'] ?? n.type.
  • jsonLdToQuads uses them for top-level docs, nested-object enqueue, and rdf:type emission.
  • valueToTerm accepts id as the IRI-reference key.
  • Predicate loop skips id/type keys (handled as aliases above; emitting as predicates produces malformed <id> / <type> triples).
  • Nested-claim probe ignores both @id and id when deciding "has own claims."

Why this matters

JSS's own backend uses JSON-LD throughout, so the LWS10-CID verifier path is unaffected. But anyone consuming the profile as Turtle (RDF stores, SPARQL endpoints, generic Solid clients defaulting to text/turtle) saw the cid:authentication link with zero description of the target — couldn't find the key material. The "JSON-LD and Turtle profiles match" property #386/#388 implicitly relies on did NOT hold for any pod with a Nostr/Schnorr verificationMethod.

Test plan

  • New unit test reproduces the test.solid.social shape (nested VM with id/type aliases) and asserts the Turtle output contains:
    • cid:verificationMethod predicate
    • The VM resource (#k)
    • Multikey type
    • cid:controller predicate on the VM
    • publicKeyMultibase literal
  • Existing turtle.test.js tests still pass (cycle safety, duplicate @id, prefix-as-object, cyclical nested refs)
  • Full suite: 770 → 771 pass
  • Manual on solid.social after deploy: curl -H 'Accept: text/turtle' https://test.solid.social/profile/card.jsonld should now include the full VM block

Solid profiles in the wild use the JSON-LD 1.1 standard alias
form for nested resources — `id` and `type`, not `@id`/`@type`.
The converter only checked for the explicit `@`-prefixed names,
so a nested CID v1 verificationMethod authored as:

    {
      "id": ".../card.jsonld#nostr-key-1",
      "type": "Multikey",
      "controller": ".../card.jsonld#me",
      "publicKeyMultibase": "fe70102de7ec..."
    }

was silently dropped from the Turtle output. The two breakage
points:

  - `valueToTerm()` checks `value['@id']`; for `{id:...}` it
    returns null, and the parent `cid:verificationMethod` quad
    is never emitted.
  - The BFS enqueue at the bottom of `jsonLdToQuads` tests
    `v['@id']`, so the nested object's own triples (its type,
    controller, publicKeyMultibase) are never emitted either.

Net effect on test.solid.social's profile: the JSON-LD form had
the full Multikey VM, the conneg-converted Turtle dropped both
the `cid:verificationMethod` predicate AND the entire
`#nostr-key-1` resource block. JSS's own backend is unaffected
(LWS10-CID verifier requests JSON-LD), but third-party Turtle
consumers — RDF stores, SPARQL endpoints, generic Solid clients
that default to text/turtle — saw `cid:authentication
<#nostr-key-1>` with zero triples about `#nostr-key-1`.

Fix:

  - Two helper functions: `getNodeId(n)` returns `n['@id'] ??
    n.id`, `getNodeType(n)` returns `n['@type'] ?? n.type`.
    Used wherever the converter consults the identifier or
    type of a node.
  - `jsonLdToQuads` accepts `id`/`type` for top-level docs,
    nested-object enqueue, and emits the rdf:type triple from
    either form.
  - `valueToTerm` accepts `id` as the IRI-reference key (so
    the predicate-→-IRI quad gets emitted for nested objects
    using the alias).
  - Predicate loop now also skips `id`/`type` keys (they were
    handled above as @id/@type aliases — emitting them as
    predicates would produce malformed triples like `<id>` /
    `<type>` since the names don't expand via context).
  - Nested-claim probe in the enqueue check ignores both
    `@id` and `id` when deciding whether the object has any
    own claims worth emitting.

Sibling of #389 (array-context lost) and #390 (`@type: '@JSON'`
literals not emitted) — same family of CID-v1 conversion gaps.

New test: writes a profile with the exact pattern test.solid.social
ships (nested VM with `id`/`type` aliases) and asserts the
Turtle output includes `cid:verificationMethod`, the Multikey
type, the controller predicate, and the publicKeyMultibase
literal.

Test count: 770 → 771 in full suite.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes the JSON-LD → Turtle conversion in src/rdf/turtle.js so that nested JSON-LD nodes using the JSON-LD 1.1 id/type aliases (instead of @id/@type) are no longer silently dropped from Turtle output (e.g., CID v1 verificationMethod entries).

Changes:

  • Add getNodeId() / getNodeType() helpers and use them throughout jsonLdToQuads() (node detection, nested enqueue, rdf:type emission).
  • Update valueToTerm() to treat object { id: ... } as an IRI reference (like { '@id': ... }).
  • Add a unit test covering nested verificationMethod with id/type aliases.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/rdf/turtle.js Accept id/type aliases when emitting quads and when enqueuing nested nodes so inline resources are serialized into Turtle.
test/turtle.test.js Adds regression test ensuring nested alias-authored nodes (CID verificationMethod) survive Turtle conversion.
Comments suppressed due to low confidence (1)

src/rdf/turtle.js:395

  • valueToTerm() now treats any non-undefined @id/id as an IRI reference, but resolveUri() assumes the id is a string. If a stored JSON-LD doc contains id: null (or a non-string), conneg to Turtle will throw. Please require objId to be a string (allowing empty string if you want it to resolve to base) before calling resolveUri().
    const objId = value['@id'] !== undefined ? value['@id'] : value.id;
    if (objId !== undefined) {
      const uri = resolveUri(objId, baseUri);
      return uri.startsWith('_:')
        ? blankNode(uri.slice(2))
        : namedNode(uri);
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/rdf/turtle.js Outdated
*/
function getNodeId(n) {
if (!n || typeof n !== 'object') return undefined;
return n['@id'] !== undefined ? n['@id'] : n.id;
Comment thread src/rdf/turtle.js Outdated
}
function getNodeType(n) {
if (!n || typeof n !== 'object') return undefined;
return n['@type'] !== undefined ? n['@type'] : n.type;
Two findings, both real.

`getNodeId` and `getNodeType` returned whatever they found at
`@id`/`id` and `@type`/`type` — including `null`, numbers, or
nested objects from malformed user-authored profiles. Downstream
they were passed to `resolveUri` and `expandUri`, which call
`.startsWith` and `.includes` respectively — a TypeError on the
request path for any malformed profile.

`valueToTerm` had the same gap on its added `id` alias branch.

Fix:

  - getNodeId returns `undefined` unless the value is a string.
    Treats `id: 42` or `id: null` as "no identifier."
  - getNodeType accepts string OR array. Arrays are filtered to
    string entries; if no strings remain, treated as absent.
    This preserves the "string entries still emit" property
    when a user writes `type: ['Multikey', 42]` — the valid
    Multikey emits, the 42 is dropped.
  - valueToTerm's id-alias branch checks `typeof rawObjId ===
    'string'` before calling resolveUri.

New test asserts:
  - nested `id: 42` doesn't crash conneg (returns valid string)
  - nested `type: null` doesn't crash
  - mixed `type: ['Multikey', 42, null]` — string entries survive

Test count: 771 → 772 in full suite.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@melvincarvalho melvincarvalho merged commit fd9420f into gh-pages May 10, 2026
4 checks passed
@melvincarvalho melvincarvalho deleted the issue-415-turtle-nested-vm branch May 10, 2026 09:03
melvincarvalho added a commit that referenced this pull request May 10, 2026
…418)

The default profile generator's `@context` mapped CID v1 *predicate*
terms (`verificationMethod`, `controller`, etc.) but NOT the *class*
names. So a verificationMethod authored with the spec-example shape
`{type: "Multikey", ...}` left the bare term unexpanded:
  - in JSON-LD: a JSON-LD processor with no external context
    fetcher couldn't resolve "Multikey" to a class IRI
  - in Turtle (conneg): `expandUri("Multikey")` returned the bare
    string, N3 wrote `<Multikey>`, RFC-3986 resolved that against
    the document base — yielding `<pod>/profile/Multikey`, a
    fictional class on the pod's own host

Net: the same key on `alice.example.com` and `bob.example.com`
emitted DIFFERENT class IRIs. SPARQL queries on `?s a cid:Multikey`
missed every JSS-issued VM. VC validators couldn't recognize the
type. Same trap for `JsonWebKey`.

Fix: two flat aliases in the profile @context:

    "Multikey":   "cid:Multikey",
    "JsonWebKey": "cid:JsonWebKey"

Why flat aliases (not pre-prefixed CURIE in the JSON-LD payload):
  - Naive JSON readers comparing `type === "Multikey"` keep
    working — readable, unchanged shape
  - JSON-LD processors expand via @context — get the canonical
    `https://www.w3.org/ns/cid/v1#Multikey`
  - Our Turtle conneg layer (which DOES respect @context per #389
    and now #416) expands and N3 writes `cid:Multikey` with the
    `cid:` prefix declared at the top of the document — any
    Turtle parser resolves it correctly
  - Matches the "JSON-LD with flat context aliases" pattern that
    consumers like LION/LOSOS rely on

Tests:
  - Profile Document: `@context` declares both `Multikey` and
    `JsonWebKey` as flat aliases mapping to the CID v1 namespace.
  - Turtle conneg: combines the production profile generator's
    @context with a synthetic VM `{type: "Multikey", ...}` and
    asserts:
      a. Turtle contains `cid:Multikey` (or full IRI form)
      b. Turtle does NOT contain bare `<Multikey>` (which would
         resolve to `<pod>/profile/Multikey`)
      c. The publicKeyMultibase value still appears (#416 didn't
         regress)

Test count: 772 → 774 in full suite.
melvincarvalho added a commit that referenced this pull request May 10, 2026
* rdf/turtle: emit a space before `;` and `.` terminators (#419)

Match the de-facto Turtle style used in W3C 1.1 spec examples,
Apache Jena's RIOT writer, and most hand-authored Turtle in the
Solid / linked-data ecosystem: a space between the previous
token and the statement terminator.

Before:
    <s> foaf:name "Alice";
        foaf:age 30.

After:
    <s> foaf:name "Alice" ;
        foaf:age 30 .

n3.js's writer hardcodes the no-space form (`;\n    next`) and
exposes no config knob. Approach: a literal-aware post-pass on
the writer's output.

The naïve `\S;\n` → `\S ;\n` regex is unsafe — string literals
(especially triple-quoted) can contain `;\n` or `.\n` internally,
and inserting a space inside a literal would silently CHANGE the
literal's value. So:

  1. Stash every string literal AND every <IRI> into placeholders
     using a multi-character non-token-boundary sentinel
     bracketed by NULs (n3.js escapes raw NUL inside literals
     and never emits one in real Turtle output, so the sentinel
     can't collide with real content).
  2. Apply the spacing regex to the redacted output. Now `;`/`.`
     only appear as actual statement terminators because all
     literal/IRI internals are hidden.
  3. Restore placeholders.

Stash order matters — triple-quoted before single-quoted, else
`"""` looks like an empty `""` followed by `"` to the
single-quoted regex. Same for triple-vs-single apostrophe.

Tests:
  - "emits a space before ; and . terminators": positive
  - "does NOT add a space inside literals containing ; or .":
    safety regression — the post-pass MUST NOT corrupt literal
    values like `"foo;bar"`, `"has.dot"`, `"a;b.c"`.
  - "does NOT add a space inside an IRI containing ;": safety
    regression for IRIs like `<https://example.test/path;with;semis>`.

Test count: 770 → 777 in full suite (+7: 3 #419 tests + 4 #416/#417/#415 follow-ups since main).

* Address copilot pass 1 on #420 — assert by value, not by lexical form

The two safety regression tests for #419 hardcoded that N3
serializes string literals with double quotes (`"foo;bar"`)
and IRIs as `<...>`. That made them sensitive to N3 writer
formatting choices — single quotes, long-string
`"""..."""`, IRI escaping, etc. — even when the underlying
literal value would still be preserved correctly.

Fix: parse the emitted Turtle back to quads and assert the
literal/IRI VALUES, not their lexical form. The properties
the tests actually want to enforce are:
  - "foo;bar" round-trips as a literal whose value is foo;bar
    (no inserted space)
  - <https://example.test/path;with;semis> round-trips as a
    NamedNode with that exact IRI

Both are now assertion-by-value. Resilient against any future
N3 upgrade that changes quote style, IRI escaping, or the long-
string threshold.

Test count: same 777.

* Address copilot pass 2 on #420 — pin terminator presence

The "emits a space before ; and ." test asserted no offending
terminators (without preceding whitespace) but didn't assert
the terminators were actually present. If a future N3 upgrade
ever switched to the "one triple per statement, no ;
continuations" output style, the negative assertions would
pass vacuously and the test would stop exercising the spacing
behavior at all.

Fix: pin the presence of at least one ` ;` and one ` .` (with
preceding whitespace) before checking for offending forms. Now
the test fails loudly if either disappears.

Test count: same 777.

* Address copilot pass 3 on #420 — require literal space, not any whitespace

The pass-2 assertions used `\s` / `[\s]` patterns, which would
treat `\n;`, `\t;`, etc. as acceptable too. The #419 intent is
specifically a single SPACE before the terminator (matching
W3C Turtle 1.1 examples and Apache Jena's RIOT writer):
  `value ;` / `value .`

If a future N3 upgrade ever emitted `\n;` or `\t;`, the pass-2
test would have passed despite the visually-different output.

Fix: tighten the assertions to require a literal ` ;` / ` .`
(a single space). Both presence-checks and the offending-form
negative checks use ` ` (space) explicitly instead of `\s`.

Test count: same 777.
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.

rdf/turtle: nested verificationMethod objects dropped from Turtle output

2 participants