Skip to content

Develop - #405

Merged
namedgraph merged 23 commits into
masterfrom
develop
Oct 4, 2026
Merged

namedgraph merged 23 commits into
masterfrom
develop

Conversation

@namedgraph

Copy link
Copy Markdown
Member

No description provided.

namedgraph and others added 23 commits September 30, 2026 00:48
The CLI resolves the library by ${project.version} from a remote repository,
which made every version transition wait on a publish landing first. The
post-release bump broke develop immediately: it asks for 6.0.2-SNAPSHOT, which
nothing deploys, and the release tag asks for 6.0.1, which Central has not seen.
Both HTTP-tests and UI-tests failed on dependency resolution while Load-tests,
which builds no CLI, stayed green.

The library is in the same checkout at the same commit, so the checkout is the
source of truth and the remote round-trip bought nothing but a failure mode. The
three workflows that build the CLI now install rdf/ first, which is what `make
cli` has always done locally - the gap was that CI does not use make, so
verifying through make left it invisible.

cli/pom.xml's snapshot repository goes with it. It is not merely unnecessary now
but harmful: with a remote fallback present, a build that lost the rdf step would
quietly resolve an older published snapshot instead of failing, and pass against
stale code. Verified both ways against throwaway local repositories - install
then package succeeds at 40 and 86 tests, package alone fails on the missing
artifact. Deploying rdf/ is now a REST-VKG concern only; nothing here resolves
the library remotely.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One multi-platform build on an amd64 runner emulated the arm64 leg through
QEMU, so javac, the Saxon SEF compile and npm all ran at a fraction of native
speed, serialized behind the amd64 leg. Split into one job per architecture on
a runner of that architecture: each pushes an untagged manifest by digest, and
a merge job assembles them into the tagged manifest lists with imagetools. The
tags only move once both architectures are known good, so a failed release can
no longer leave a partial multi-architecture manifest behind latest.

Add the GitHub Actions layer cache, one scope per image and architecture. The
SEF compiler build reads the platform scope as well, which is where the maven
stage the two share is exported; a single shared scope would have them
overwrite each other's export on every run.

Resolve the dependency tree in a layer keyed on the POM alone, so a source
change no longer invalidates it - it did before, because COPY src preceded
COPY pom.xml and nothing between them depended on the POM by itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The last SPARQLBuilder calls in the client stylesheets were
`SelectBuilder.fromString(s).build()` and
`SelectBuilder.fromQuery(json).toString()`, which are SPARQL.js's `Parser`
and `Generator` with nothing added: `fromString` constructs a bare Parser,
`build()` returns the parse tree untouched and `toString()` is
`Generator.stringify()`. The 22 sites now call the `$sparql-parser` and
`$sparql-generator` params `client.xsl` already declares, as
`ldh:parse-query` and `ldh:build-update` already did.

`SPARQLBuilder.js` goes, and with it the second copy of SPARQL.js it bundled
(330 KB off every page load). The flag that gated the two scripts in
`layout.xsl` now gates the one it loads, and the comments that blamed the
GRAPH-merging round-trip on SPARQLBuilder name the SPARQL.js round-trip the
behaviour belongs to.

Verified by compiling the client SEF (`make sef`); the browser specs were
not run, the stack was down.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…r comes back as a 500

Jersey parses the header lazily, on every selectVariant() and getAcceptableLanguages()
call, and every exception mapper builds its response through selectVariant(). A value
such as "en-IN,en;q=0.9,en;q=0.9;q=0.8" therefore threw from inside the mapper, past
Jersey's own 400 fallback, and the container answered 500 in place of the mapper's 403.
The same header already gave an authorized request a bare 400 through the three direct
readers (ResponseHeadersFilter, the graph store, the XSLT writer).

The header is advisory, so the request proceeds as if the client stated no preference.
The filter runs pre-matching, before anything that can fail into a mapper, and catches
the JAX-RS ProcessingException rather than Jersey's internal HeaderValueException. Since
the header is re-read from the live map each time, this one filter covers the LDH
mappers, the Web-Client and Core mappers, and the direct readers alike.

The test drives a real ContainerRequest, so it is Jersey's own parser that fails and the
very call which used to throw that is asserted to succeed afterwards.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… ETag, so it no longer fails 412

Restore writes the chosen version back to the live document with If-Match, and quoted
whatever tag LinkedDataHub.contents held under the document's URI. Loading a ?version=
view stored the memento's own tag there - its commit SHA - which never matches the live
document, so a restore pressed after Compare, or from any version link, was refused 412
Precondition Failed ("This version could not be restored"). Pressed from the live document
it worked, which is why it went unnoticed since #392 made the write conditional.

A memento's tag is no longer stored as the document's, and the restore asks for its
validator through ldh:with-document-etag, which HEADs the live document when the browser
holds none. From the live document it still quotes the tag the page loaded with, so a
write made since the history was opened still refuses the restore.

tests/ui gains the memento spec: a document of its own, written three times, restored from
the live document and from a historical version, asserting the If-Match against the live
ETag. It skips where graph versioning is not configured.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… a dataspace changed at runtime

The settings overlay held a copy of every dataspace changed at runtime, and that copy
replaced the configured graph on load. config/dataspaces.trig and config/system.trig both
write into the dataspace's graph, so the copy froze system.trig's wiring too: installing a
package (PATCH /settings) and then enabling graph versioning left lds:versioningRepository
dropped on every boot - no commits, ?timemap 404, no History link - with only "the
configuration for it is not in effect" in the log. The same held for lds:service, the
ontology, the stylesheet and anything else configured after the first runtime change.

The overlay is now settings/dataspaces.ru, a SPARQL Update of DELETE DATA and INSERT DATA:
the statements a runtime change removed from the configuration and the ones it added. Load
applies it to the configuration as it is, so a removal stays removed - an uninstalled
package does not come back - and a statement added to the configuration later takes
effect. The graphs are copied before it is applied: the copy used to link them, and
deleting from the live dataset would have deleted from the configured one.

A copy-style dataspaces.trig is converted once and left in place. It cannot tell a
statement removed at runtime from one added to the configuration since, so the latter
carries over as removed. Blank nodes cannot be matched across a restart; removing a
statement that has one is logged and not persisted.

The tests had encoded the replacement - an edit reaching an untouched dataspace was the
only case. New ones cover an edit reaching a changed dataspace, a removal staying removed,
the configuration left alone by a load, and the legacy conversion.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ss that keeps an empty fragment

The platform's own Link wrote the relation type unquoted - rel=http://www.w3.org/ns/auth/acl#mode -
which RFC 8288 does not allow: ':', '/' and '#' are not token characters, so a URI relation type has
to be quoted. It could carry no parameter beyond rel and type, and Core's Link.valueOf, which
XSLTWriterBase parsed with, kept the quotes in the value it returned.

Jersey's implementation of jakarta.ws.rs.core.Link cannot stand in for it as it is: it holds the target
in a UriBuilder, which drops an empty fragment whether the link is built or parsed, so
https://localhost:4443/ns# - every dataspace's lds:ontology - came out as https://localhost:4443/ns,
a different URI. server.util.Link now extends jakarta's Link and stores the URI as given, with a
builder shaped like Jersey's. Serialization is Jersey's header delegate, which writes from getUri()
and so writes the fragment; valueOf takes the target from between the angle brackets and lets Jersey
parse only the parameters.

ResponseHeadersFilter, ExceptionMapperBase and XSLTWriterBase use it. Every rel is now quoted, so the
readers follow: the TimeMap marker in server.xsl and client.xsl is rel="timemap", and the http tests
grep the quoted forms. valueOf still reads an unquoted rel, which an older peer behind the proxy may
send. ResponseHeadersFilterTest's negative assertions moved to the quoted form too - left unquoted,
"does not contain rel=timemap" would have passed whatever the header said.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tory link no longer leads to a 404

Every document of a versioned dataspace advertises rel=timemap, but the TimeMap answered 404 "has no
version history" until a write had made a commit for it. Commits only follow writes, so every document
that predated versioning being enabled - imported data, a dataspace switched on later - showed a
History link whose dialog said "The version history could not be loaded".

GraphVersioningService returns the TimeMap without Mementos instead, and the History dialog says the
document has no versions yet and that its next change will be the first. In link-format it lists the
Original Resource, which the Mementos named until now and the request names when there are none, the
TimeMap itself without a from/until range, and the TimeGate. The TimeGate still answers 404: with no
Memento there is nothing to redirect to.

TimeMapWriter builds its links with the Link builder rather than by concatenation, and serializes them
as RFC 6690 link-values, which allow no whitespace - unlike the header form Jersey writes.

GET-timemap-empty.sh puts a document's graph straight into the store, so no write reaches the platform
and no commit is made, and asserts the empty TimeMap end to end. The UI spec serves that TimeMap to
the History dialog, since every write the suite can make is a commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…howed as the fragment of their WebID.

The bootstrap preloads the signed-in agent's own WebID document, so ac:object-label found that one label and
no other. Agents live in the admin dataspace, out of reach of the end-user endpoint the label lookups query,
so the dialog now dereferences the authors' documents through the ?uri= proxy before rendering, and tunnels
them to ac:object-label as object-metadata. all-settled: an author whose document cannot be loaded keeps the
URI fragment, and the history still opens.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…URI, with "(me)" after it.

The option is now Web-Client's xhtml:Option over the agent's description in $foaf:Agent, the WebID document
the page has already loaded, so it reads as the agent's name; xsl:on-empty keeps the bare URI for an agent
the document does not describe. The facet header's fallback to the predicate's local name takes the same
instruction in place of an xsl:choose that tested for the very description it then returned.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ive from ORCID_ISSUER

A client registered on production ORCID was unknown to the sandbox at authorization, and its
ID tokens failed verification against the sandbox JWKS. ORCID_ISSUER (default https://orcid.org)
reaches the platform as the orcid:issuer context parameter; the authorize, token, userinfo and
JWKS endpoints are built from it, and only tokens from that issuer are accepted.

To make room in the ROOT.xml transform, which sits at xsltproc's 32-parameter limit,
MAX_CONN_PER_ROUTE, MAX_TOTAL_CONN and MAX_REQUEST_RETRIES become system properties like the
other HTTP client settings, still falling back to the ldhc: context parameters.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Temporary. The spec failed on c4cf624 with the /ns property-label query sent
twice under refusal against once unrefused; the log shows each send and refusal
of that shape in both loads, with a short id per exact request, so the run says
whether the second send is a duplicate request or the retry of the first.

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

The browser moves its current drag target onto a freshly mounted overlay only
at the next drag tick. A file dragged in and back out of the window before
then fires dragleave on the element underneath, so the overlay-scoped rule
never ran and the overlay stayed over the page; an external drag has no
dragend to clear it either. The dragleave rule now matches every element and
unmounts the overlay whenever relatedTarget is outside it.

The file drop spec drives a real external drag through Chromium's
Input.dispatchDragEvent, leaving the window both before and after the overlay
takes over.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tch cannot pass (#403)

A write to a document is a read-modify-write over separate calls to the store: the graph is
read, If-Match is checked against that copy, the change is applied in memory and validated,
and the whole graph is written back. Nothing spanned those calls, so two writers quoting the
same entity tag that both read before either wrote both passed the check, and the second
write silently undid the first - the very lost update the precondition exists to refuse.

GraphLocks holds a striped set of reentrant locks keyed by graph URI; writeLocked() wraps
the window in POST, PUT, PATCH, DELETE and multipart POST. The writer behind reads the graph
as the first one left it, quotes a tag that no longer matches and is answered 412. Reentrant
because a PATCH that empties a graph completes as a DELETE of it.

A unit test races eight writers quoting one tag against a slow in-memory store and expects
one 204, seven 412 and a single write; without the lock all eight are accepted. An HTTP test
does the same against the root document.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…inds no resources

SPARQL.js gives an aliased projection, (?s AS ?resource), as a map rather than the string "?s",
and reading only the string left the view's focus variable empty and its whole render failing.
ldh:first-var-name reads both shapes.

An alias is bound only in the projection, so the rewrites that project something else or append
patterns to the WHERE (the result count, the container lookup, the parallax step) first wrap the
query as a subquery (ldh:wrap-subquery). Without it the container lookup joined on an unbound
variable, matched every triple in the store and exhausted Fuseki's memory. The count's projection
and the parallax step's WHERE now match the outer query only, not a subquery's.

A first variable that binds values, (YEAR(NOW()) AS ?year), gives rows the DESCRIBE finds nothing
for. The count tells the two apart, and the block says the query lists no resources rather than
that nothing matched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… their positional (#404)

* Drop the CLI's --base option: document commands never needed it, dataspace commands take the base URI as their positional

Every command that addresses a document derived nothing from -b; the scripts it mirrors
required the option and never read it either. add file, import csv, import rdf and push
now resolve the upload URI against the target document's root, so they print the same
URL without being told the base.

The commands that act on the dataspace itself - packages, admin clear ontology, admin
create ontology/group/authorization, admin import ontology, admin make-public - take
its base URI as the positional argument, where every other command puts its target,
with LDH_BASE as its default.

The HTTP and UI suites move with it; the one test still calling the deprecated
create-item.sh uses ldh create item.

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

* Restore the executable bit on the test scripts the rewrite had dropped

The scripts were rewritten through a temporary file, which came back as 0644; the suite
runs each test with bash, so only GET-file-304.sh, which executes ./create-file.sh
directly, noticed: "Permission denied".

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The next release removes the CLI's --base option and quotes every Link relation type,
both breaking for clients, so it is a minor release rather than 6.0.2. The platform, the
RDF library and the CLI move together, as cli/pom.xml resolves the library by
${project.version}.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Unreleased section named three of the eighteen commits. It now opens with the
migration notes - the CLI's --base option gone, Link relation types quoted, runtime
settings kept as a change set - and lists the CLI upload URIs, the Link builder and the
native image builds under Changed, and the lost update under concurrent writes, runtime
settings overriding the configuration, Restore failing 412 from a historical view, the
empty TimeMap, the author names in the History and request access dialogs, the malformed
Accept-Language, aliased first variables in views, the file drop overlay and the CI
library build under Fixed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@namedgraph
namedgraph merged commit 93fffb8 into master Oct 4, 2026
3 checks passed
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