Develop - #358
Draft
namedgraph wants to merge 19 commits into
Draft
Conversation
* Client-side constructor instantiation: one prototype per instance An edited resource is one rdf:Description with N rdf:types, so its constructor template must be one rdf:Description with those N types and the union of their constructor properties - the template mirrors the instance. /ns?forClass= minted a fresh bnode prototype per class, and every consumer downstream assumed the single-prototype shape: document-order-dependent property picks (the intermittently vanishing app-settings Description field), parent-type match misses, ldh:SetResourceID orphaning N-1 prototypes in EDIT flows, and xs:anyURI? cardinality errors on multi-range predicates. Instead of asking the server to construct, the client fetches the constructor queries with one SPARQL SELECT (rdfs:subClassOf* closure, DISTINCT - cycle-safe, shared constructors dedup) and expands the CONSTRUCT templates onto a single instance: - ldh:constructor-query builds the SELECT; ldh:construct-instance parses each sp:text with SPARQL.js (no execution - constructors must have an empty WHERE clause; others are skipped with a warning), substitutes ?this with one shared instance label, prefixes marker bnodes per constructor, collapses same-(predicate, range) duplicates, and serializes through ldh:triples-to-descriptions - ldh:load-constructed-doc/set-constructed-doc rewired to the SELECT + instantiation; all EDIT/violation/CREATE chains covered unchanged - ldh:construct-forClass split by use-when: SAXON keeps /ns?forClass= (SSR), SaxonJS instantiates client-side - covers the tunnel-less bs2:FormControl default (SSR EditMode, ldh:DocumentForm, ldh:AppSettingsForm) - add-value flow tokenizes multi-type @typeof (was a latent 500) - forClass cardinalities relaxed to xs:anyURI* where a predicate can have several ranges across the type set (spin:query - sp:Query vs sp:Select); bs2:FormControlTypeLabel iterates all ranges like its marker-bnode sibling - GET-ns-constructors.sh pins the constructor SELECT server contract Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Remove /ns?forClass= - constructor instantiation is client-side only The client-side instantiation left server-side rendering as the last consumer of /ns?forClass=. SSR's constructor controls were transient paint - the client re-render replaces the form on load - so the SAXON ldh:construct-forClass becomes an empty-document stub (the ac:construct stub pattern): server-rendered edit forms show data properties, the client supplies the constructor controls. - BREAKING: Namespace no longer serves ?forClass= constructed instances; it serves SPARQL queries and the raw ontology graph only. Removes the Constructor usage and the unmapped-IRIException 500 on malformed class URIs - GET-namespace-forClass-rdfs.sh removed with the feature; the constructor SELECT contract is covered by GET-ns-constructors.sh - CacheInvalidationFilter's forClass ban stays: it serves the typeahead ?forClass= filtering on document URLs, not the removed construct - $ac:forClass in client.xsl stays: the shadowed Web-Client templates compiled into the SEF still reference it Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Read SPARQL.js parse output via JSON round-trip SaxonJS does not marshal plain JS arrays for ixsl:get() access - the 'where' array came back as an empty sequence and the next ixsl:get() failed with a cardinality error. Serialize the parse tree with JSON.stringify + json-to-xml (the form.xsl SELECT-builder idiom) and read the CONSTRUCT template as json:map elements instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Keep violation machinery types out of the row-form instance type set ldh:row-form-submit-violation swept rdf:types from every non-document Description in the violation response, including the spin:ConstraintViolation / http:Response ones. With per-class prototypes that was harmless (junk classes just yielded no prototype), but the union-typed single prototype inherits those types and then fails bs2:FormControl's every-type-satisfies subset test against the edited resource - constructor controls vanished from the violation re-render. Filter the swept Descriptions with the same type list the bs2:RowForm/bs2:Form suppression templates use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Single-source the violation machinery type list as $system-types One global drives the bs2:RowForm/bs2:Form suppression matches and the row-form violation type sweep, so the list cannot silently diverge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Drop base-uri override from the modal violation re-render ldh:render-modal-form-violation forced base-uri to the violation response's base URI, a carry-over from before the render dispatchers gained the about-fallback (both from 197ab9c). In the creation/edit flows the two values coincide, but in the app-settings flow the subject is a urn: outside the response's HTTP space, so the override flipped bs2:FormControl's $show-subject default and exposed the URI control on constraint violations while the initial render kept it hidden. The submit-handler context carries no base-uri, so the dispatchers now fall back to $ctx('about') on the violation path exactly as on the initial render. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#359) Remaining leftovers of the 197ab9c shared-chain refactor, sibling fixes to the base-uri override drop: - property-uris harvested from everything except the edited resource (inverted predicate, contradicting the function's own comment and ldh:load-edited-resource) - property labels degraded to the local-name fallback on modal violation re-renders while metadata was fetched for spin:/sh: machinery terms instead. Flip to @Rdf:about = $about. - object-uris (modal + row-form chains) and the row-form property-uris swept the whole response body including the violation machinery Descriptions, causing junk label lookups. Exclude $system-types-typed Descriptions, mirroring the instance type harvest. - the shared violation render-ctx stamped the Container/Item required function for all flows while the app-settings initial chain passes always-true; masked today by ldh:AppSettingsForm's hardcoded required=true(). Stamp 'required' per flow in the response handlers alongside 'render-fn', and correct the stale 'only used for Container/Item instances' comment that hid this. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Self-federation: origin validators through the proxy, foreign write targets, federation suite The federation test (First Principles ch. 18): one instance's client browses, queries and writes against another, the wire carrying only spec-terms. Most of the machinery already existed - the proxy forwards arbitrary methods with bodies and delegated agent identity, panes carry data-base/data-endpoint from forwarded Link headers so ldt:base() and sd:endpoint() (and with them the ns-bound constructor SELECT and metadata fetches) already resolve against the remote app on remote panes. What was missing: - the proxy's Model branch stamped re-serialization validators, so a client editing a proxied document sent an If-Match the origin had never issued - every cross-instance write 412ed. The origin's ETag/Last-Modified now forward (replacing locally stamped values); the ResultSet branch keeps builder validators. - the Add-data flow rejected foreign targets with a guard written when the remote end refused proxied writes; with delegation the target instance's ACL is the arbiter, so the guard is gone and a remote 403 surfaces as the form error. Import-ontology keeps its local-target guard for a different reason, now stated: the constructor derivation is scoped to the local /sparql endpoint. - nothing proved any of it. New http-tests/federation/ suite runs the browse/query/write triad between the root and test dataspaces at two origins: Link-header endpoint discovery, SPARQL against the discovered endpoint, constructor SELECT against the remote ns, ETag parity between proxied and direct reads, a graph-scoped SPARQL Update PATCH under the origin's If-Match (stale precondition 412s), and the unauthenticated negative. The dataspaces share a triplestore below the HTTP surface in the test config; the meeting surface exercised is the full HTTP stack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Forward conditional request headers through the proxy; fix federation negative CI caught two gaps in the cross-instance write path: - the proxy set only Accept and User-Agent on the outbound request, so If-Match never reached the origin - a proxied preconditioned write ran unconditionally (stale If-Match got 204 instead of 412, losing optimistic concurrency). Forward the conditional request headers (If-Match, If-None-Match, If-Modified-Since, If-Unmodified-Since); identity stays delegated (not via Authorization/Cookie) and Range is excluded (byte offsets don't survive Model re-serialization). - the negative test used an anonymous request, which is not actually anonymous at the origin: a proxied request with no user certificate rides the server's own credential on the proxy->origin leg. Replace it with a delegated write by the signed-up agent, which is a known federation identity not authorized on B - so B's ACL refuses it, which is the property the test means to assert. Both PATCH tests now echo the actual status codes (debug-first). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Relay proxied 4xx/5xx error responses verbatim The header-forwarding fix made the origin actually evaluate proxied write preconditions - and exposed that the proxy could not relay the resulting error responses. A 4xx/5xx from the origin carries a diagnostic body; getResponse routed it through the Model branch, where readEntity(Model.class) throws on the non-RDF/error body, and the catch turned it into 502. So a proxied stale If-Match (origin 412) and an unauthorized delegated write (origin 403) both surfaced as 502. Relay CLIENT_ERROR/SERVER_ERROR responses raw - status, Content-Type and origin validators forwarded, body piped - skipping the re-serialization branches that only make sense for negotiable success representations. Existing proxy error tests assert status only and are unaffected; the acl#agent leak checks are on direct (non-proxied) requests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Match Accept on the conditional PATCH to the read's variant The valid If-Match returned 412 because LDH ETags are variant-specific (getVariantEntityTag folds the negotiated media type into the tag): the test read the ETag with Accept: application/rdf+xml but the conditional PATCH sent none, so the origin computed the precondition tag for a different variant. The local edit flow already sends Accept: application/rdf+xml on its PATCH for this reason. Align the test's conditional PATCHes with the read. Not a proxy defect - the proxy forwards Accept and If-Match faithfully (stale correctly 412s). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Declarative packages: ldh:import in dataspace settings replaces install endpoints An application imports a package with a single <app> ldh:import <package-uri> triple - in config/dataspaces.trig (permanent, applied on restart) or live via PATCH /settings. Package components are discovered from the package's Linked Data description; the stylesheet is loaded from its source URL and composed into the application stylesheet in memory at compile time, per dataspace, with no restart and no webapp file mutation. - XsltExecutableFilter composes the app stylesheet document with package xsl:imports (SecureXML-parsed, import-set-derived cache key, fallback to the plain stylesheet on compile failure) - LocalStylesheetResolver serves app-origin /static/ xsl:import URLs from the webapp instead of HTTPS round-trips through nginx - packages/install and packages/uninstall endpoints, the admin packages/ container and ACL entries, the package Actions UI, CLI scripts and their tests removed; XSLTMasterUpdater and Package.getStylesheetPath() deleted - Package catalog is data at https://packages.linkeddatahub.com/ (bundled one-entry copy served via the proxy's mapped-URI resolution) - Application settings modal lists the packages with Installed checkboxes; Save applies the change set as one DELETE/INSERT/WHERE PATCH on /settings - ac:stylesheet values in dataspaces.trig are absolute per-origin URLs - Known limitation: package ontologies are not yet auto-imported from ldh:import (manual owl:imports for now) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Package checkboxes join the settings form as RDF/POST inputs - one Save The packages fieldset moves inside the application settings form: each row's Installed checkbox is a pu/ou input pair that serializes an ldh:import triple, so the form's single Save submits settings and package imports as one PATCH. The separate packages Save button, the change-set computation and the SPARQL-generating handlers are removed. - ldh:parse-rdf-post skips unchecked checkboxes (successful-controls rule of HTML form submission, which client-side DOM serialization bypasses) - the ldh:import hidden-input round-trip is suppressed in the settings form - the checkboxes are the sole representation, so unchecking uninstalls - the settings form response reloads the page on success: the composed stylesheet only manifests through a full server-side render Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Keep package checkboxes across the settings violation re-render The modal violation chain had no package-catalog load pair, so the re-rendered settings form lost the package table - and since the checkboxes are the sole ldh:import representation (the hidden-input round-trip is suppressed), a Save after fixing a validation error would submit no ldh:import triples and uninstall every package. ldh:settings-form-response stamps the package-catalog load pair as 'load-pairs' alongside 'render-fn'/'required', and ldh:modal-form-submit-violation joins stamped pairs onto its shared list. Every pair must bake a request, so optional fetches ride per flow rather than sitting in the shared list. The re-rendered checkboxes reflect the submitted state: PackageList's installed set comes from the echoed ldh:import triples in the violation body. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Package ontologies join the imports closure, derived from ldh:import The ontology half of declarative packages, mirroring the stylesheet composition: when OntologyFilter assembles an application's ontology closure, each imported package's ontology (from ldt:ontology in the package description) is assembled as its own owl:imports closure and added as a union member. Nothing is materialized - no owl:imports triple is written anywhere, and the raw ontology graph served by /ns stays untouched. A package ontology that fails to load is skipped so a broken package cannot take the application ontology down. Invalidation stays on the plain ontology-URI key that ClearOntology already removes and rebuilds (now package-aware): a /settings PATCH - the only way the ldh:import set changes live - evicts the assembled closure, and the next request re-derives it. The trig-file route implies a restart, which starts from empty caches anyway. getPackage moves from XsltExecutableFilter to the system Application (the filter delegates) so both composition halves share the package description resolution. New http-test asserts the skos:Concept constructor appears on /ns after the ldh:import PATCH and disappears after removal - closure derivation and eviction in one round trip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Instrument package-ontology removal path (temporary diagnostics) PATCH-settings-package-ontology.sh fails only on removal: the package ontology's skos:Concept constructor stays in the closure after the ldh:import is deleted (adding works). Eviction fires and updateApp persists to the live context dataset, so static reading can't explain it. Log what getPackageOntologies actually sees after the DELETE, and have the test report whether /settings still carries the ldh:import triple - one CI run then tells us persist-vs-cache. To be reverted once the cause is fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Settings PATCH evicts the ontology from the repository too, not just the closure Root cause of PATCH-settings-package-ontology.sh failing only on removal, confirmed by the CI diagnostics: after the DELETE, /settings carries 0 ldh:import triples and getPackageOntologies returns [] - the derivation input is correct - yet the skos:Concept constructor stays on /ns. The OWL2 imports build materializes the package's terms into the cached base graph in the OntologyRepository; Settings evicted only the closure union (getOntologyGraphs), leaving that polluted graph cached, so the remove-phase rebuild read skos straight back. Adding worked because the first build ran over a clean cache. Clear both caches, matching ClearOntology (repository graph + fragment- stripped document alias + closure union), minus the proxy purge since settings responses are not cached. Revert the temporary getPackageOntologies logging. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Settings delegates ontology invalidation to ClearOntology via ResourceContext Replaces the inline in-memory eviction with a call-through to the shared ClearOntology logic (the pattern the old InstallPackage used): getResourceContext().getResource(ClearOntology.class).post(uri, null). This gets the full invalidation - repository graph + closure union + varnish BAN/XKEY-PURGE + eager reload - so a settings-driven package add/remove no longer leaves stale /ns SPARQL responses in the proxy caches (the in-memory-only eviction fixed the closure but not varnish; the http-test masked it with a manual purge). ClearOntology becomes context-agnostic: it resolved the end-user app by assuming the current app is admin (getApplication().as(AdminApplication)), which threw for the end-user-scoped /settings. It now resolves both apps from whichever the request matched and derives the admin backend from the resolved admin app rather than the current one, so both backends are purged either way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Import ontology persists annotations only - vocabulary via scratch document (#362) The flow's output becomes the artifact packages ship statically: an annotation ontology holding the derived class constructors plus owl:imports of the canonical vocabulary URI. The fetched vocabulary is scaffolding: PUT into a scratch document (UUID-slugged, under the app root) that scopes the construct-constructors CONSTRUCT, then deleted - on the error paths too (client: error wrapper armed once the scratch exists; CLI: EXIT trap). The vocabulary resolves live through the graph repository (bundled mapping or HTTP), identically to package ontologies. This also fixes constructor derivation for bundled vocabularies: previously the shipped file shadowed the local copy that held the constructors, so they never reached the closure (the old test's TO-DO documented it); now the annotation document enters the closure under its own URI and imports the vocabulary. Wiring changes accordingly: add-ontology-import.sh --import <annotation-document>, not the source. ontologyQuery in web.xml is untouched - the describing-document branch keeps resolving own-origin ontologies; this flow simply stops minting shadowing copies. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The bs2:Row GraphMode branch applied bs2:Graph without the required canvas-id param, crashing the block render with a cardinality error. Pass it, and initialize the 3D force graph after the row is rendered by reusing ldh:InitDocumentGraph3D, fed the whole loaded document to match the bs2:Graph rendering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Since #362 constructors live in the annotation ontology document, but onConstructorUpdate still cleared the ontology derived from the class' rdfs:isDefinedBy - the canonical vocabulary URI, where constructors lived only while the import flow minted shadowing copies. The annotation graph stayed cached in the OntologyRepository, so the namespace ontology reload rebuilt its closure from the stale graph and constructor edits never surfaced in instance forms. The callback now clears ac:document-uri($constructor-uri) - the very document its PATCH just updated - before proceeding to the namespace ontology clear. The rdfs:isDefinedBy DESCRIBE round-trip, the namespace-guessing fallback and the now-unused $type wiring are removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… edit Saving a constructor now updates the instance forms already open on the page instead of leaving them stale until a reload. Once the namespace ontology clear completes (the point where the constructor closure is fresh again), every fieldset with property control groups re-runs the constructor instantiation chain (ldh:load-constructed-doc / ldh:set-constructed-doc) for its type set and diffs the prototype against its controls: - properties the form is missing get bs2:FormControl groups appended at the end, before the re-appended property picker - mirroring the add-value flow, ordering deferred to the next full render - value-less control groups whose property the constructor no longer asserts are removed; controls holding entered values and rdf:type controls are never touched, so user input survives the sync - a failed constructor fetch leaves the form as it was - removals only act on status-200 data The ns SPARQL response carries a model-derived ETag and no freshness lifetime, so the re-fetch always revalidates and cannot be served stale from the browser cache. ldh:ClearNamespace's bogus ldh:NoOp callback is replaced by the sync fan-out and the NoOp template is deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Modal document save re-renders with the active layout mode ldh:modal-form-response dropped the URL's ?mode= on the post-save DocumentNavigate, so saving the btn-edit modal re-rendered the document in the default mode instead of the active one (e.g. ac:GraphMode). Pass the current mode through query-params, guarded to same-document reloads so a PUT overwrite landing in the same branch navigates clean. Snapshot params (?version/?timemap) deliberately don't survive the save. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Remove vestigial forClass URL param usages No LDH flow puts forClass in a URL anymore: the add-constructor onclick reads @data-for-class (the button @href was inert HTML), chart save PATCHes the document URI instead of POSTing to charts/?forClass=, and the ns?forClass= constructor endpoint was replaced by ns?query=. Drop the button @hrefs, the ldh:build-query forClass arity, and CacheInvalidationFilter's unreachable forClass ban branch. The chart form loses @action entirely rather than being repointed: no charts/ container exists in the app structure, the form is never submitted (btn-save-chart is type=button and PATCHes the current document), and bs2:Chart cannot derive the host document anyway -- its context node is the data document, whose base URI is the SPARQL request URL for CSR-fetched results. An absent @action targets the current page URL, which is where the save actually writes. $type and $doc-type go with it, being unused once the action is gone. $ac:forClass in client.xsl stays: the imported Web-Client resource.xsl and document.xsl reference it and the declaring Web-Client layout.xsl is not part of the CSR import tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#364) * Quote the Link type parameter per RFC 8288 com.atomgraph.core.util.Link emits `type=` unquoted. A media type containing a solidus (application/link-format) is not a tchar token, so RFC 8288 requires it as a quoted-string. Subclass rather than patching Core so the fix lands without a Core release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Describe the TimeMap with PROV-O instead of the Memento namespace http://mementoweb.org/ns# has no published vocabulary — it does not dereference, and RFC 7089 defines only HTTP relation tokens, headers and the link-format serialization, no RDF. PROV-O is a REC and covers the same ground: the TimeMap is a prov:Collection of prov:Entity mementos, each a prov:specializationOf the Original Resource, generatedAtTime its commit datetime, and wasRevisionOf its predecessor. The revision chain follows the path-filtered commit list, so adjacent entries are adjacent revisions of that graph — git parents are repository-wide and usually never touched the file. A TimeMap with no mementos now 404s rather than serving an empty document: it describes no history, and the Original Resource would not be derivable from the model since it is reached via prov:specializationOf. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Serve TimeMaps as application/link-format RFC 7089 permits a TimeMap to be serialized in various ways but requires the link-value format to be supported. TimeMapWriter derives it from the PROV description: the prov:Collection is the TimeMap, its members the Mementos, and their shared prov:specializationOf target the Original Resource. Datetimes convert to RFC 1123 at the boundary. The variant is scoped to the ?timemap response rather than added to the global writable Model types, so ordinary documents keep negotiating RDF and HTML only and answer 406 for link-format. It leads the TimeMap's list so Accept: */* gets the RFC serialization; browsers still resolve to HTML because they send text/html at a higher q-value. The formatter is not DateTimeFormatter.RFC_1123_DATE_TIME, which leaves the day of month unpadded where the RFC 7089 grammar wants 2DIGIT. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Serve Memento hypermedia with the registered relation types RFC 7089 requires different links per response type, so the filter now branches on the query parameter that distinguishes them: the Original Resource advertises rel=timemap and MUST NOT carry rel=original; a Memento MUST link back to its Original Resource; the TimeMap identifies itself with rel=self. All three use the IANA-registered tokens instead of Memento-namespace URIs, which nothing in the ecosystem follows, and carry type="application/link-format" so a client knows what it will get. Memento-Datetime moves to the same zero-padded formatter as the TimeMap body, matching the RFC 7089 grammar. Both Link parsers follow: default.xsl for the server-rendered History link, client.xsl for the Saxon-JS pass. MEM.java has no consumers left. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Page through commit history so TimeMaps are comprehensive RFC 7089 defines a TimeMap as a comprehensive list of a resource's Mementos, but listCommits stopped at the first 100 — so on any longer history the oldest entry was merely where the page ended, and rel="first memento" would have been a false claim. The walk is bounded at MAX_COMMIT_PAGES to cap the API calls one TimeMap request can make; beyond that the result is truncated and logged. Paging the TimeMap itself (RFC 7089 section 5) is the real answer for histories that long. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Cover the RFC 7089 contract in the versioning HTTP tests Asserts the PROV description of the TimeMap, its link-format serialization, that link-format stays scoped to the TimeMap (406 on an ordinary document), that the Original Resource does not carry rel=original while a Memento does, and that Memento-Datetime is a zero-padded RFC 1123 value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Order mementos by parsed instant, not datetime string Lexicographic ordering of xsd:dateTime literals holds only while every value shares one format; parsing removes the assumption. Also corrects a comment that named only the Memento case while the branch covers the TimeMap too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Add a Memento TimeGate for Accept-Datetime negotiation Datetime negotiation is the operation Memento exists for, and without a TimeGate a client can only enumerate versions, never ask for the document as it stood at a given time. ?timegate selects the Memento closest to Accept-Datetime and answers 302 with its URI in Location and Vary: accept-datetime, carrying no Memento-Datetime of its own. RFC 7089 leaves the selection algorithm to the server as long as it is consistent: smallest absolute distance, ties towards the more recent Memento, most recent when no datetime is asked for. Parsing accepts any RFC 1123 spelling while responses emit the one the RFC grammar specifies. The Original Resource and Mementos now advertise rel=timegate, and the TimeMap lists it. Being deployment hypermedia rather than version history, the TimeGate URI comes from the request: PROV has no term for it, and inventing one in someone else's namespace is what got the Memento vocabulary retired in the first place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Keep TimeGate redirects out of caches Varnish defines no vcl_hash or Vary handling of its own, so it honours Vary: accept-datetime and stores a variant per distinct value — an unbounded key space. Worse, the write-time ban matches the document URL and never the ?timegate URL, so a redirect naming the most recent Memento would survive the commit that displaced it, for the full 24h default TTL. Negotiation has to see the current history, so the redirect is no-store. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…based on RFC 7089 conformance) (#365) * Version diffs in the history modal Add From/To version selection to the Memento history modal and render a client-side triple diff between the two selected versions as Turtle-style hunks: subject header, then -/+ predicate-object lines (red/green, matching the From/To column headers), changed values of the same property paired up. The diff is the set difference over canonical triple keys, computed with the new shared ldh:triples-map() extracted from the origin-vs-local comparison in layout.xsl. The shared key gains an rdf:parseType="Literal" branch (XMLLiteral objects previously keyed on empty text) and an opt-in numeric normalization: layout.xsl keeps it for cross-serializer comparison, the version diff compares exact lexical forms. Both ?version= representations are fetched with a per-branch-context ixsl:http-request fan-out joined by ixsl:all; no server changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Render version diffs on the document page instead of Turtle text Compare in the history modal now navigates to ?version=<to>&diff=<from> - the diff is display state like ?mode=, read from the URL at render time and never sent to the server, so back/forward re-render it and a plain reload degrades to the ?version= snapshot. ldh:RDFDocumentLoad fetches the compared version through a self-guarding promise step; ldh:rdf-document-response renders the union of both versions (ldh:merge-metadata reused, plus a prune pass so a changed typed block keeps only its current rdf:value) with the one-sided triple-key sets tunneled down the shared template chain. Blocks get diff-added/diff-removed/diff-changed borders via ldh:diff-class() in the bs2:Row emitters, individual property values get marked <dd>s, and a changed XHTML block stacks its old content (red) above the new (green). A legend alert next to the historical-version notice explains the colors. ldh:triple-key() is extracted from ldh:triples-map() so the dd/block decorators and the map builder share one triple identity. The Turtle-hunk rendering from the previous commit is removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Give the diff legend its third color bootstrap.css defines three block borders — diff-added, diff-removed and diff-changed — but the legend named only the first two, leaving an amber-bordered block with nothing explaining it. .text-warning resolves to #ff7518 in this theme, the same value as the diff-changed border, so the swatch matches without new CSS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The GitHub Contents API takes an optimistic lock on the branch head, not on the file, so concurrent runs committing to main lose the race against each other. Versioning is best-effort and asynchronous, so the dropped commits never surface as an error — the document PUT still returns 201 and an unrelated test times out 30 seconds later waiting for a commit that was discarded. Six overlapping runs on 2026-08-25 failed this way, each on a different test. A per-run path prefix isolates the files but not the head, which is the contended resource. The run now branches from main, points the dataspace at that branch, and deletes it afterwards; the suite polls whichever branch it was given. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Restore a document to an earlier version from the history modal Rollback was the last gap in the versioning feature: history was readable but there was no way to act on it. Restore is a per-row action rather than a button beside Compare, because the From/To radios bound a comparison range while restoring acts on a single version — reusing them would leave it ambiguous which endpoint was being restored. Nothing new is needed server-side. The client reads the memento back and writes it to the live document, which the versioning filter records as a new commit, so the restore rolls forward rather than rewriting history and the versions rolled past stay in the TimeMap. The memento itself is never written to, so it stays immutable as RFC 7089 requires and checkSnapshotReadOnly keeps rejecting writes to ?version=. Guards: the button appears only where the agent holds acl:Write, never on the version already being viewed (which would commit identical content), and asks for confirmation first. A failed read or write reports into the modal rather than dropping the agent out of it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Read restore permission from the live document, not the snapshot view The restore buttons never appeared when History was opened from a ?version= view — which is exactly where they are most wanted, having just looked at an old version and decided to bring it back. acl:mode() reflects the current response, and ResponseHeadersFilter deliberately caps snapshot views at acl:Read so the UI disables edit affordances there. The TimeMap response is capped for the same reason, so neither response can answer whether the live document is writable. Opening the modal now also HEADs the live document and reads its acl:mode links, using the same Link parse client.xsl uses to seed acl:mode(). One extra request per modal open, and the answer is about the resource the restore actually writes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Documents accumulated a dct:created value per container recreate — the root document of a development instance had 201 of them going back to June. The dataset load appends rather than replaces, so re-running it stamps the install metadata again. Only dct:created accumulates: dct:creator and acl:owner are identical on every run and the store deduplicates them, while the timestamp differs each time. The guard against re-loading is keyed on the based-datasets directory, which lives in the container's writable layer — a plain restart keeps it and correctly skips, but every rebuild loses it and reloads onto a store that still holds the data. Install metadata now comes from one SPARQL update per app after the uploads, with the check in the WHERE clause, instead of being appended to each .nq file beforehand. Documents that already carry dct:created keep the timestamp they have. This replaces enrich_document_metadata, which wrote the triples blind. Doing the same check per document would have meant an ASK per graph — 21 requests for the admin dataset alone, and a wall of log output to match. Skipping the whole load when the app's documents already exist would also have stopped the accumulation, but a newer dataset's documents would then never arrive. Re-appending content on top of itself is a no-op anyway, since RDF is a set. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Give the owner and secretary authorizations stable slugs The entrypoint minted a fresh UUID for these two documents on every run that loads datasets, so each container recreate left another copy of the same grant behind. A development instance had 60 of them — 30 identical "Public owner's WebID" authorizations and 30 for the secretary — against 24 real ones. Every authorization bundled in admin.trig is named by what it is: public/, sign-up/, full-control/, oauth2-login/. These two now follow that convention, which makes re-running the load a no-op by construction: same document URI, same triples, and RDF is a set. No check needed, which is why public/ and sign-up/ never accumulated. A slug is guessable where a UUID is not, but that is already true of the bundled authorizations, and ACL enforcement rather than URI obscurity is what protects them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Point the test owner fixture at the same authorization slug The fixture named its authorization after the owner's *key* UUID, with a standing TO-DO saying it should use the authorization UUID instead. That UUID no longer exists, and the document it names is a fixed grant, so it takes the same slug the entrypoint now uses. The two paths converge as a result: the fixture and the generated authorization describe the same document with the same triples, where before they were two documents granting the same access under different URIs. The key document keeps its UUID — that is a per-install identity, not a role. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add ldh CLI: Java port of the bin/ HTTP API scripts Standalone Maven project under cli/ (picocli + Jena 6 + Core's GraphStoreClient). One command per script with the same options; bin/ subdirectories become nested subcommand groups. PKCS12 WebID client-cert auth, env-var defaults, shaded executable jar. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Retarget import-ontology at the scratch-document flow, drop the package commands The CLI was ported against develop at 0e8c831; three of its commands aimed at endpoints that have since been removed. import-ontology POSTed a transform argument to {base}transform, which #347 deleted when the transform moved client-side. It now mirrors the rewritten script (#362): fetch the vocabulary through the Linked Data proxy, PUT it into a UUID-slugged scratch document, run construct-constructors over that graph via the SPARQL Protocol dataset specification, append the derived constructors plus an owl:imports header to the target document, and delete the scratch document on every exit path - the failure ones included. The query text is read by dereferencing the query's own document instead of through a SELECT on /sparql; Jena parses the RDF where bash needed a SPARQL round trip and xmllint. install-package/uninstall-package POSTed to packages/install and packages/uninstall, which #361 deleted along with their shell scripts. Packages are declarative now - an ldh:import triple in the dataspace settings - so ldh patch on /settings is the whole interface and the command group goes away. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Cover the CLI surface the http-tests swap will lean on Three gaps had no test anywhere. Exit code 2 and the proxy URI rewriting were already covered; these were not. - HttpException.check: the >=400 throw carrying status, reason phrase and URI, the body excerpt, and its truncation at 1024 characters. Driven against a real inbound response - an outbound one built in-process cannot be read back, so the excerpt branch was unreachable from a plain unit test. - The stdout contract the shell pipelines depend on: a created document's URL is the only line on stdout, diagnostics go to stderr, and a failed command prints nothing on stdout. `container=$(ldh create-container ...)` breaks the moment anything else reaches stdout, and the suite consumes that substitution in dozens of places. - stdin: put parses it and resolves relative URIs against the target, patch sends the update verbatim, and a malformed update never reaches the server. StubServer is a JDK com.sun.net.httpserver wrapper - no new dependency - so the tests exercise the real Jersey/Apache/PKCS12 stack without an instance. Also fills in the seven model builders that had none: AddGenericService, AddResultSetChart, AddClass, AddConstructor, AddPropertyConstraint, AddRestriction, AddObjectBlock. 41 tests to 63. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Drive http-tests fixtures with ldh, deprecating the bin/ HTTP scripts The suite is the CLI's end-to-end coverage: 260 invocations of 19 commands against a live instance through nginx, Varnish and Tomcat, on every CI run. Nothing standalone comes close, and with bin/ on the way out its own coverage is no longer worth preserving. Only the arrange phase moves. Every assertion in the suite is a curl call checking a status code or a response body, and not one of them ever asserted on a bin/ script - the scripts only ever built fixtures, so ldh's exit-1-on-error behaviour fits where they sat and `set -euo pipefail` still aborts the test. Authentication splits by consumer: ldh reads the PKCS12 keystore the platform generates, curl keeps the PEM derived beside it. run.sh derives the keystore paths from the certificate paths it is given and fails fast if either is missing, so its four-argument interface is unchanged. signup.sh stops deleting the keystore it downloads - that file was always the p12, converted to PEM only because the scripts needed one. CI builds cli/ (running its unit tests on the way) and puts the launcher on PATH; the certificate permission fix now covers the keystores too, which the PEM-only chmod would have left unreadable on the host. bin/ keeps the certificate and WebID tooling - webid-keygen.sh, webid-uri.sh, webid-modulus.sh, server-cert-gen.sh - which talk to no API and are not deprecated. The HTTP scripts themselves are left in place for now; deleting them is a separate move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Drop the Maven cache that broke the job at setup-java setup-java runs before the checkout step in this workflow, so cache: 'maven' had an empty workspace to hash and failed the job outright: No file in /home/runner/work/... matched to [**/pom.xml, ...], make sure you have checked out the target repository The cache was an unforced optimization on top of building cli/ on the runner - removing it restores the previous behaviour. Enabling it would mean moving Checkout above Set up Java, which is a separate change and not needed here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Capture add-file's upload URI instead of leaking it onto create-file.sh's stdout The only test the ldh swap actually broke. bin/add-file.sh ended in `curl ... -o /dev/null` and printed nothing; `ldh add-file` prints the content-addressed upload URI, per the convention every create/append command follows. create-file.sh called it unredirected and then echoed the URL it recomputed itself, so its stdout carried two lines - and GET-file-304.sh consumes that stdout as `file=$(./create-file.sh)`, which left it with a mangled URL and a failing conditional request. create-file.sh is the only test script whose stdout another test captures, so this was the only instance of the pattern; the other 259 rewritten invocations are either captured with $(...) or land in a per-test log run.sh discards. Rather than redirect the output away, take the URI from ldh and keep the independent digest as an assertion. That drops the duplicated sha1 computation and additionally checks the CLI's content-addressing, which nothing covered before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Start the CLI JVM tuned for startup rather than throughput Every command is a short-lived process that exits long before C2 could pay for itself, and spends most of its life waiting on HTTP. -XX:TieredStopAtLevel=1 -XX:+UseSerialGC measures 0.231s to 0.201s per invocation through the launcher locally; the http-tests suite pays that cost 260 times per CI run. Sets the floor for what tuning can do here - the structural cost is one JVM per invocation, which only batching or a native image would remove. Measured at 0.78s per invocation on the CI runner, roughly 3.6x this machine. LDH_JAVA_OPTS replaces the defaults rather than appending, so a different collector cannot collide with the one set here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Covers the 17 commits since 45f4d65: the ldh CLI and the http-tests swap onto it, RFC 7089 conformance (PROV-O TimeMaps, link-format, TimeGate), version diffs and restore, the entrypoint metadata and authorization-slug fixes, and the CI versioning branch. The existing Unreleased entries follow the same rule, which moves several bug-shaped items out of Changed and into Fixed - the constructor-instantiation and proxy lines each carried more than one. Details of a single change stay on its line; only distinct changes are split apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Written in 2021 and never registered - the call site in registerContainerResponseFilters has been commented out for as long as the class has existed, and its own Javadoc says "Currently unused". It is not a head start on the PROV-O provenance sidecar (P2.3) either: it records an agent WebID and a timestamp into a fresh urn:uuid: graph per request, with no HTTP method, no per-dataspace provenance graph and no link to a version - and the git SHA is the part that would make such a record worth querying. The 5.9.2 PROV-O work is read-side only, a serialization vocabulary for the ?timemap response. PROV.java stays: TimeMapWriter, GraphVersioningService, ImportExecutor and InvalidWebIDPublicKeyExceptionMapper all use it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.