Skip to content

GitHub-backed graph versioning with Memento history UI (P2.2 MVP) - #350

Merged
namedgraph merged 11 commits into
developfrom
ft-graph-versioning
Aug 17, 2026
Merged

GitHub-backed graph versioning with Memento history UI (P2.2 MVP)#350
namedgraph merged 11 commits into
developfrom
ft-graph-versioning

Conversation

@namedgraph

Copy link
Copy Markdown
Member

Closes the audit roadmap's history/undo/audit-trail gap (P2.2) per the wiki implementation plan v1.3.

Capture

Every document write (POST/PUT/PATCH/DELETE) on a versioning-enabled dataspace schedules an async reconcile task: re-read the graph from the store, commit it as a sorted N-Triples file to a GitHub repository (graph gone → file deleted). Commits are chained per file path so they never race the Contents API's SHA optimistic locking, and are authored with the agent's WebIDgit log is the audit trail. Best-effort by design: failures are logged, responses are never delayed or failed.

Configured per dataspace via lapp:versioningRepositorydoap:GitRepository in config/system.trig; the fine-grained token lives in secrets/credentials.trig (a:authToken), merged at entrypoint like SPARQL service credentials. The GitHub client runs on a new hostname-verified HTTP client factory.

Retrieval

  • GET <doc>?version=<sha> serves the historical graph through the normal content-negotiation pipeline with Memento-Datetime, a SHA ETag, and immutable Cache-Control; restricted to hex commit SHAs (movable refs must not be cached as immutable). Inherits the live document's ACL.
  • GET <doc>?timemap serves an RFC 7089 TimeMap as RDF (Memento vocabulary), built from the repository's commit history; advertised via Link rel=memento:timemap, consumed like acl:mode in both pipelines.

History UI

The document's modified-datetime in the action bar becomes a link when the document is versioned; clicking it opens a modal (Request-access-style table, translated strings) listing versions newest-first with the current one marked. Version links navigate in-app: version/timemap are now representation-selecting params (ldh:snapshot-params()) threaded through the CSR fetch and every URL rebuild, so snapshot pages render fully — content blocks included — from the historical graph. A banner marks historical views.

Incidental fixes

  • Entrypoint: parse the credentials secret from a .trig-suffixed copy (extensionless secret mount made riot fail silently and killed startup)
  • Date literals no longer render Saxon's [Language: en] fallback marker (TO-DO: upstream to Web-Client)

Testing

  • 116 unit tests green, including new coverage: GitHubClient against a JDK-built-in fake server, TimeMap model shape, sorted-serialization determinism, VersioningFilter fire/skip conditions
  • New http-tests/versioning/ suite (gated on VERSIONING_TEST_REPO + GITHUB_TOKEN): commit creation with WebID author, ?version= round-trip with Memento-Datetime, TimeMap contents, file deletion
  • Verified end-to-end against a live repository: capture, modal, snapshot navigation, immutable caching

Versioning is off by default — without lapp:versioningRepository config, behavior is unchanged.

🤖 Generated with Claude Code

namedgraph and others added 11 commits August 17, 2026 20:26
Mirror named graphs of versioning-enabled dataspaces into GitHub
repositories. Each document write (POST/PUT/PATCH/DELETE) schedules an
async reconcile task that re-reads the graph from the store and commits
it as a sorted N-Triples file, authored with the agent's WebID; a gone
graph deletes the file. Commits are chained per file path so they never
race the Contents API's SHA-based optimistic locking. Best-effort by
design: failures are logged, responses are never delayed or failed.

Historical versions are retrievable via GET ?version=<commit-sha>, with
Memento-Datetime, a git ETag, and immutable Cache-Control; they inherit
the live document's access control.

Configured per dataspace via lapp:versioningRepository ->
doap:GitRepository in config/system.trig, with the fine-grained access
token supplied as a:authToken in secrets/credentials.trig. The GitHub
client runs on a new hostname-verified HTTP client factory (the existing
clients disable hostname verification against a truststore that includes
public CAs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Docker secret mounts as extensionless /run/secrets/credentials, and
riot picks the parser by file extension - the merge silently exited 1 and
killed the container on startup. Never surfaced before because the
credentials secret had not been enabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The document's modified-datetime in the action bar becomes a link when
the document is versioned (advertised hypermedia-style via a Link
rel=mem:timemap response header, consumed like acl:mode in both
pipelines). Clicking it opens a modal listing the versions - datetime and
agent per entry, fetched client-side as RDF from GET ?timemap, which
serves a Memento (RFC 7089) TimeMap built from the repository's commit
history. Version links open in a new tab (the CSR navigation drops query
params) and render server-side with a historical-version banner driven
by the Memento-Datetime header.

Also overrides the Web-Client date templates to drop the language
argument: Saxon only ships English date names and prepends a
'[Language: en]' fallback marker otherwise (TO-DO: upstream).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The HTML writer perturbs ETags per-agent by parsing them as hex numbers,
so the 'git-' prefix 500'd browser requests. The hex-only restriction
also stops movable refs (?version=main) from being served with immutable
caching.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The initial client boot re-fetched the bare document URI, replacing the
historical snapshot with the live document and rewriting the URL to
mode-only params. Snapshot views skip the boot-time re-fetch - the SSR
content is the content.

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

version/timemap are representation-selecting params (ldh:snapshot-params)
that now ride the second-pass RDF fetch and survive both PushState URL
rebuilds, so snapshot pages render fully - content blocks included - from
the historical graph, and the URL keeps the param. Version links navigate
in-app again (no target=_blank).

History modal: modal-constructor width, table like the Request access
dialog, current version row highlighted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…a row highlight

LDH restyles Bootstrap's .info table row to purple, which makes the links
in it illegible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the Request access dialog: legend + text-info description from
translations.rdf (en-US/es-ES), Version/Agent table headers likewise.

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ResponseHeadersFilter advertises at most acl:Read on ?version=/?timemap
  responses, so both rendering pipelines disable edit affordances through
  the existing acl:mode machinery
- All six write methods reject snapshot URLs with 405 (a PATCH to
  ?version= would have written the live document)
- The banner says read-only; its strings moved to translations.rdf

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ppend is not)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@namedgraph
namedgraph merged commit 04ae32e into develop Aug 17, 2026
1 check passed
@namedgraph
namedgraph deleted the ft-graph-versioning branch August 17, 2026 22:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant