Skip to content

Add per-subject RDF export endpoint#1090

Merged
JeroenDeDauw merged 6 commits into
masterfrom
feature/subject-rdf-export
Jul 19, 2026
Merged

Add per-subject RDF export endpoint#1090
JeroenDeDauw merged 6 commits into
masterfrom
feature/subject-rdf-export

Conversation

@JeroenDeDauw

@JeroenDeDauw JeroenDeDauw commented Jul 19, 2026

Copy link
Copy Markdown
Member

Fixes #1087

Adds GET /neowiki/v0/subject/{subjectId}/rdf, serving one Subject's outbound
bounded description as RDF: exactly the triples the page export emits for that
Subject, including the native relation reification, with none of the page-metadata
triples, in the hosting page's per-projection named graph. It mirrors the page
endpoint's format (trig/turtle) and projection (native/EDM/...) parameters and
their negotiation.

Mechanism:

  • PageProjector gains projectSubject( Page, SubjectId ): QuadList, implemented by
    both the native and ontology-mapping projectors by reusing their existing
    per-Subject internals (the former private projectSubject helpers, renamed to
    subjectQuads). An absent Subject, or one whose projection yields nothing (native:
    Schema unavailable; ontology: the Subject's Schema has no entry in the target's
    Mapping page), yields an empty list — a readable Subject that projects to zero quads
    is a 200 empty graph, never a 404.
  • RdfSubjectExporter resolves the Subject to its hosting page via the Neo4j
    PageIdentifiersLookup (the same dependency the subject JSON endpoint already
    carries), authorizes the page, loads it, and confirms the Subject is still in the
    current revision before projecting. A Subject that is unknown, has no readable or
    loadable hosting page, or is gone from the slot all return one byte-identical 404
    (anti-oracle parity, cf. Enforce per-page read permission on Subject, Layout, and RDF read endpoints #1046).
  • ExportSubjectRdfApi validates the Subject ID shape first (400 on malformed, never
    a 500) and rejects an unknown projection (400) — read-filtering the listed projection
    names by the caller's permission — before resolving the Subject, so neither can be
    used to probe for restricted Subjects.
  • The format negotiation shared with the page endpoint moves into a new
    RdfFormatNegotiation trait, keeping the two endpoints byte-for-byte consistent.

Docs: the rest-api.md endpoint table and the rdf-export.md endpoint section.

Integrates master's #1086 (one Mapping page per ontology): the per-subject projection
is re-fitted onto the single-page Mapping model (Mapping::forSchema()), the handler
adopts filterReadableProjectionNames(), and projection names are now case-sensitive
Mapping page titles (e.g. EDM).

AI-authored — Claude Code, Opus 4.8 (max); detailed spec from a triage session (@JeroenDeDauw), built end-to-end with no mid-task human steering, then integrated a conflicting master merge (#1086); not yet human-reviewed; TDD unit + integration tests with a mutation pass (every mutation caught), touched PHPUnit suites + phpcs/phpstan green locally, live-verified against the dev wiki after the merge (native, TriG named graph, EDM via the new single-page mapping, and 404/400 paths), CI green post-merge; final docs-wording tweak by the reviewing session (Fable 5 (max)).

In-session AI review

Reviewed in-session (pre-merge) by three independent read-only agents (correctness, security, design-optimality). No blocking findings: the anti-oracle 404 byte-parity and the hosting-page read gate were verified, and the design verdict was optimal (every element of structure warranted; the two rejected simpler alternatives — whole-page-projection-then-filter, and a fake single-subject Page — were verified to genuinely fail the goal). Two minor findings were applied: an integration test for the readable-but-unmapped-Subject 200 empty-graph contract, and a docstring/doc clarification. The subsequent #1086 merge re-fit preserves these; no human has reviewed the diff.

JeroenDeDauw and others added 5 commits July 19, 2026 15:31
Fixes #1087

Adds GET /neowiki/v0/subject/{subjectId}/rdf, serving one Subject's outbound
bounded description as RDF: exactly the triples the page export emits for that
Subject, including the native relation reification, with none of the page-metadata
triples, in the hosting page's per-projection named graph. It mirrors the page
endpoint's `format` (trig/turtle) and `projection` (native/edm/...) parameters and
their negotiation.

Mechanism:

- PageProjector gains projectSubject( Page, SubjectId ): QuadList, implemented by
  both the native and ontology-mapping projectors by reusing their existing
  per-Subject internals (the former private projectSubject helpers, renamed to
  subjectQuads). An absent Subject, or one whose projection yields nothing (native:
  Schema unavailable; ontology: no Mapping for the target), yields an empty list.
- RdfSubjectExporter resolves the Subject to its hosting page via the Neo4j
  PageIdentifiersLookup (the same dependency the subject JSON endpoint already
  carries), authorizes the page, loads it, and confirms the Subject is still in the
  current revision before projecting. A Subject that is unknown, has no readable or
  loadable hosting page, or is gone from the slot all return one byte-identical 404
  (anti-oracle parity, cf. #1046).
- ExportSubjectRdfApi validates the Subject ID shape first (400 on malformed, never
  a 500) and rejects an unknown projection (400) before resolving the Subject, so
  neither can be used to probe for restricted Subjects.
- The format negotiation shared with the page endpoint moves into a new
  RdfFormatNegotiation trait, keeping the two endpoints byte-for-byte consistent.

Docs: the rest-api.md endpoint table and the rdf-export.md endpoint section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refinements from the PR review of the per-subject RDF export endpoint:

- Add an integration test proving a readable Subject whose Schema has no mapping
  for the requested ontology target returns a 200 empty graph, not a 404 — a
  readable Subject is never hidden. Documents that contract in rdf-export.md.
- Reword the RdfSubjectExporter docstring: the page endpoint keeps its read gate
  in the REST handler, not in RdfPageExporter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Integrates #1086 (one Mapping page per ontology). Re-fits the per-subject
RDF export onto the restructured mapping model:

- OntologyMappingProjector now takes one page-level Mapping (name = target,
  holding a per-Schema `schemas` map); re-applied the public
  projectSubject( Page, SubjectId ) + private subjectQuads onto that shape via
  Mapping::forSchema().
- ExportSubjectRdfApi adopts filterReadableProjectionNames() for the unknown-
  projection 400, matching ExportPageRdfApi's #1046 read-filtering.
- OntologyMappingProjectorTest re-fitted to SchemaMapping/Mapping; the handler
  test's mapping fixture uses the new stored format (page title EDM, schemas map)
  and the case-sensitive "EDM" projection name.
- rdf-export.md: kept the per-page/per-Subject endpoint intro, adopting #1086's
  "name of a Mapping page" wording.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The subject RDF endpoint adopted #1086's filterReadableProjectionNames() for the
unknown-projection 400 during the master merge, but no subject-endpoint test
covered it. Add one mirroring the page endpoint: a read-restricted Mapping page
name must not leak into the 400 list (#1046), while "native" stays listed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The graph-lagging-the-slot case reads as plain absence from outside; naming it
required internal knowledge and diluted the absent-vs-denied contract point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…overy (#1097)

* Surface data access in the UI: Data tab export links and RDF autodiscovery

Fixes #1095

Stacked on #1090, whose per-subject RDF endpoint these menus link to; based on
feature/subject-rdf-export so the diff is only this work. GitHub retargets to
master once #1090 merges.

The REST API served a page's RDF, its Subjects as JSON, a Subject as JSON, and
(via #1090) a Subject's RDF, in native or ontology-mapped projections — none of
it reachable from the UI. This adds three things:

- A per-subject export menu in each expanded Data tab row, beside the ID: JSON,
  then Turtle and TriG for each readable projection.
- A page-level export menu in the Data tab header: page-subjects JSON, then
  per-projection Turtle/TriG of the page RDF. The two menus share one labelling
  scheme so they read as one system.
- `<link rel="alternate">` autodiscovery tags (Turtle + TriG, native projection)
  in the head of pages that carry NeoWiki data.

The menus are driven by the projections the viewing user may read. SubjectsAction
exposes that list as the `wgNeoWikiRdfProjections` JS config var, filtered
server-side by filterReadableProjectionNames() so a restricted Mapping page title
never reaches a reader who cannot see it. A later per-wiki choice to hide a
projection then becomes a server-side filter with no UI change.

Menu item URLs carry explicit `format=` parameters and percent-encode the
projection name; each opens in a new tab so the Data tab state is not lost (Codex
1.14 menu-item anchors cannot set a target, so navigation happens on selection).
Autodiscovery links are emitted only when the page has Subjects, so the advertised
endpoint never 404s.

Tests: Vitest covers the URL/label derivation (ordering, native-vs-mapping labels,
projection encoding). PHP integration covers the config var and its permission
filtering, and the head tags being present on a page with Subjects and absent on
one without.

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

* Assert RDF autodiscovery hrefs are absolute

The head-tag test matched only the path substring, so a regression dropping the
PROTO_CANONICAL expansion to a relative href would have shipped silently. Assert
the hrefs are absolute so the canonical-URL behavior is locked.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@JeroenDeDauw

Copy link
Copy Markdown
Member Author
image image

@JeroenDeDauw
JeroenDeDauw merged commit ae98bfe into master Jul 19, 2026
12 checks passed
@JeroenDeDauw
JeroenDeDauw deleted the feature/subject-rdf-export branch July 19, 2026 22:34
@JeroenDeDauw

Copy link
Copy Markdown
Member Author

@malberts can you deploy this to demo wiki?

@malberts @alistair3149 Thoughts on UI enhancements? Follow-ups welcome.

Doing some expedited merging here since getting this live on the demo wiki allows for feedback, and it's part of the entire RDF/Ontology/QLever flow which I've self-merged and is known not-fully-reviewed yet and very much a best-guess spike.

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.

Add subject-level RDF export (per-subject RDF API)

1 participant