Develop - #373
Draft
namedgraph wants to merge 389 commits into
Draft
Develop#373namedgraph wants to merge 389 commits into
namedgraph wants to merge 389 commits into
Conversation
getEffectiveLanguage was a pure static function parked on the JAX-RS lifecycle class, and both call sites had to spell it com.atomgraph.linkeddatahub.Application.getEffectiveLanguage(...) because each already has a different Application in scope - apps.model.Application in the response filter, and the JAX-RS one in the writer. Needing the fully qualified name is the tell that it was in the wrong place. It is now LanguageNegotiator.negotiate in server/util, beside URLValidator, Skolemizer, SecureXML and WebIDCertGen, which is where focused helpers live. The name also stops claiming to be an accessor: get* on a two-argument static reads like a bean property and it is a computation over its arguments. readBundleLanguages stays on Application. That one is genuinely application configuration - it populates supportedLanguages from the constructor and answers what this deployment can render its chrome in. Per-request negotiation is what did not belong. No behaviour change; the tests are the same assertions against the new home. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
… it a second time. $ac:contentLang and Content-Language were both computed by calling LanguageNegotiator, once in the writer and once in the response filter. Two computations that agree is not the same as one value, and it is the shape this whole change set has been arguing against - it is how $ac:lang and ldhc:supportedLanguages drifted apart in the first place. ResponseHeadersFilter runs before the entity is serialised, so by the time the writer assembles its parameters the header is already on the response. Reading it back makes the document's lang attribute and the Content-Language header the same value by construction rather than by two call sites staying in step. Where no header was set - a response that never reached the filter - the parameter is simply omitted and the stylesheet default applies. LanguageNegotiator now has exactly one caller. This is as far as the unification goes for now. Core also negotiates, for the Vary language dimension and the entity tag, and folding that into the same single computation needs the Core-side work to be consumable here: LinkedDataHub resolves com.atomgraph:core 5.0.2 transitively through client 5.0.3, while the Core changes are on 5.0.3-SNAPSHOT. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
… served with. LinkedDataHub overrides match="/" and emits the root element itself, so the lang attribute never came from Web-Client's template - it came from layout.xsl, and it read $ac:lang, the language the reader asked for. That is how a page written entirely in English went out as lang="de" for a German reader and lang="es" for a Spanish one. It now reads Content-Language out of $ldh:httpHeaders, which the writer already populates from the response headers - the same pattern ldh:memento-datetime uses. Deriving the attribute from the header rather than recomputing the same value beside it means the two agree structurally instead of by two call sites staying in step, which is the property this whole change set has been chasing. The parameter the writer was passing for this is gone with it. Falling back to en, never to the request: with no header to go on the honest answer is the language the chrome ships in, and falling back to $ac:lang would reinstate the defect as the default. Also declares $ac:lang for the client-side compilation. Web-Client 5.0.4-SNAPSHOT moved $ac:langs and $ac:lang out of imports/default.xsl into bootstrap/2.3.2/layout.xsl, which client.xsl does not import, so $ac:lang became unbound and the SEF build failed with XPST0008. client.xsl already redeclares $ac:langs for the browser's list; this is the same treatment for the value derived from it. Verified in a browser across four readers: <html lang> equals Content-Language in every case - en-US for lt, en and de readers, es-ES for the Spanish one - while the values and predicate labels keep their own lang, and Vary carries Accept-Language for all of them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
…stop colliding. The HTML rendering negotiates over the whole accepted-language list, falling back per value, so two requests that select the same language-neutral variant are still different representations. Core hashes the list into the entity tag when it is given one, but nothing gave it one - measured on the running instance, Accept-Language lt and de came back with identical ETag "bf450a1e36535bd0" at 25763 and 25710 bytes, so a conditional request could be answered 304 with the wrong language. Wired on the document path, which is where the collision was observed. Verified after: lt, de, fr and en now yield four distinct tags, and repeating lt returns the same one. de and fr render byte-identical output here and still get different tags, because the hash is over the requested list rather than over what the renderer did with it. That is deliberate - the alternative is Core inspecting the rendering it does not perform - and it costs a little revalidation efficiency in exchange for never merging two representations. The remaining Response construction sites - the SPARQL endpoint, the proxy and the access resource - do not expose HttpHeaders and are left alone; their existing entity tags are unchanged because the parameter is additive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
… it. Content-Language was set on every response carrying an entity, so an RDF/XML document came back as Content-Language: en-US. That representation is byte-identical for every reader - its literals carry their own tags and none is dropped - so it is intended for all language audiences, which RFC 9110 spells as no Content-Language at all. The header also contradicted the response's own Vary, which carries no Accept-Language dimension for exactly the same reason. Scoped with the predicate that already draws this line. HTMLMediaTypePredicate is what decides whether language becomes a variant dimension and, since the entity tag change, whether the accepted-language list is hashed into the validator; labelling the representation is the third thing that should follow from the same answer rather than from a separate rule. It matches both text/html and application/xhtml+xml, so the negotiated HTML flavour is covered either way. Verified per media type: HTML keeps Content-Language: en-US, Vary with Accept-Language and a language-dependent ETag; RDF/XML now has no Content-Language, no Accept-Language in Vary, and one ETag shared across readers. <html lang> is unaffected - it reads the header, and the header is still set for the representation it appears in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
…ther than a copy on each side of the boundary. ldh:lang-rank and ac:lang-rank were the same expression. Web-Client needs it for itself now that table cells order their values instead of dropping all but one, and LDH cannot lend a function to a library it depends on, so the one that survives is the one lower in the stack. The four sort keys here follow it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
The two templates that consumed it in Web-Client have been commented out for long enough that the parameter behind them was never declared - reviving them would have meant adding one first. The vocabulary constant outlived the feature. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
…ls responses with. Two of these guard behaviour that fails quietly. A snapshot request must advertise at most acl:Read however much access the agent holds on the live document, which is what withdraws the edit affordances from an immutable version; and a proxy response must carry none of this instance's endpoint, ontology or stylesheet, because it describes someone else's resource and ProxyRequestFilter forwards that application's own links instead. Both are one boolean away from being wrong and neither shows up as an error. The Memento cases needed a repository that is actually present - the earlier stub returned an empty Optional, which left the whole block unexecuted - and they pin the four RFC 7089 relations against each other: the TimeMap is the only resource that identifies itself, the TimeGate never links to itself, and the Original Resource is the only one that does not carry rel=original. Since most of these assert an absence, and an absence is what a filter that does nothing produces, the two above were checked against a mutated filter: relaxing the snapshot predicate fails the read-only tests, hardcoding isProxyRequest to false fails the suppression test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
…t can disagree. Six scripts covering what a unit test cannot reach: that Content-Language is the first accepted language the bundle has rather than the reader's top preference, that it appears on HTML and not on RDF, that Vary carries Accept-Language and two readers survive each other through the caches, that entity tags separate the two renderings while the RDF tag stays stable across them, that <html lang> agrees with the header, and that each value carries its own language. The document assertions request application/xhtml+xml and query it with xmllint, the way the sparql-protocol and federation suites already do. That is not only tidier than matching text: the build inlines entities through an XML round trip that alphabetizes literal-result-element attributes, while xsl:attribute instructions append in execution order, so a fixed attribute string passes or fails depending on which build produced the webapp. Every probe also asserts a 200. LinkedDataHub renders its errors through the same stylesheet, so an error page carries a well-formed Content-Language and a matching lang attribute, and three of these tests passed against a 400 before the status check went in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
…t happens to be stored under.
Deriving the supported languages from translations.rdf brought its keys out onto
the wire with them: a Spanish reader was answered Content-Language: es-ES, and
since the root lang attribute is read back from that header, the document
claimed es-ES too. BCP 47 asks for a tag no more specific than is justified and
says a region subtag "MAY be omitted, as when it adds no distinguishing value to
the tag" (RFC 5646 section 2.2.4). The region in es-ES records how the bundle is
keyed; it is not a claim that the translations are Castilian, which under RFC
9110 section 8.5 is exactly what publishing it as the intended audience asserts.
It also costs reach. Under RFC 4647 a reader asking for es-MX arrives at a page
tagged es by truncating their own range, and never arrives at one tagged es-ES.
Over-specifying excludes every Spanish reader who is not Spanish.
So the rule is mechanised rather than hardcoded: the region survives only where
the bundle carries more than one variant of that language. Today's en-US and
es-ES publish as en and es; ship pt-BR alongside pt-PT and both keep their
regions, because there the region does distinguish. A blanket getLanguage()
would be right today and quietly wrong then.
The bundle's own xml:lang keys are left alone. XPath's fn:lang() prefix-matches
the other way round - fn:lang('es') matches xml:lang="es-ES" but not the reverse
- so the specific key is the reachable one. Same rule, opposite optimum, which is
why the internal key and the published claim are no longer the same string.
This restores the wire format that HEAD-accept-lang.sh has always asserted; that
test passes again untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
…es no language state of its own. The parameter this replaces was wrong for most readers, and had been since it was written. navigator.languages arrives as a sequence, so iterating it yields each tag - but the value was also the only declaration of $ac:langs in the client-side compilation, which meant the browser's normalisation and the server's lived in different files and did not match. Whichever engine drew a page decided how its languages ranked. This is the SaxonJS half of ac:langs(); Web-Client holds the SAXON half, which reads the writer-supplied parameter. use-when keeps them mutually exclusive, so the client-side compilation never sees the body that reads a parameter it does not declare, and the server-side one never sees ixsl. Both normalise identically - primary subtags, deduped, 'en' when the reader expressed nothing - because there is now one place per engine that does it. $ac:lang is gone the same way, its 36 call sites reading ac:langs()[1] directly. It was never supplied by anything: no vocabulary term, no writer, just a derived alias declared once per engine. Measured in Chromium afterwards, since the shape of what ixsl:get returns is not uniform and cannot be assumed: navigator.languages of lt-LT,lt,en-US,en gives lt en, es-ES,es,en gives es en, de-DE,de gives de. Reaching into it with ?* - correct for DataTransfer.types, which does arrive as an XDM array - fails here with "Required item type is function(*); supplied value is xs:untypedAtomic". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
Brings in the language work: the reader's languages read through ac:langs() with engine-specific bodies, per-literal lang marking that replaces the old suppression, Content-Language and <html lang> derived from one negotiation, and the language/ http-tests category covering them over HTTP. develop already carries the SIGPIPE fix for PATCH-settings-package-import.sh, which rf-design-system predates and which was failing its CI runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
… head and not the file. (#374) Concurrent writes to a versioned application lost commits. The commit chain was keyed by file path, so two documents written at once reconciled on separate threads of the pool, and the GitHub Contents API takes its optimistic lock on the branch head rather than on the blob it was handed: whichever PUT arrived second got a 409 and, versioning being best-effort, was dropped with a log line. CI run 33618323291 lost eight commits that way, and two versioning tests timed out polling for files that were never going to appear. Keying the chain by owner/repo/branch serializes every commit to a branch. Commits to different repositories still run in parallel, and the reconcile design - re-read the graph at execution time and make the file match - already tolerates a task sitting in a queue, since it reads the state it finds when it runs rather than the state that scheduled it. Serialization alone would still lose commits, as nothing stops a writer outside this JVM from moving the head between the SHA probe and the write. A conflicting write is now retried up to MAX_CONFLICT_RETRIES times, re-reading the blob SHA on each attempt: the stale SHA is what the conflict is about, so replaying the same request would only conflict again. Queued tasks for one path are deliberately left uncoalesced, tempting as the single chain makes it. GET-version.sh writes one document twice and asserts two distinct commits; collapsing them would erase a version from the TimeMap, which is history the platform promises to keep. Also builds the PUT body before the invocation rather than inside its supplier. JsonObjectBuilder yields its object only to the first build(), so a commit that hit the rate limit retried with an empty body. Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t describes. Two versioning tests failed in CI run 33626306733 while the platform did exactly what they were asserting: the commits landed in ~1.3s, and 664d147d carried the agent's WebID as its author. What failed was how the tests read GitHub back. Each polled the Contents API until the file appeared, then threw that result away and re-derived it from a fresh read. PUT-creates-commit read commits?path= once, and got an empty listing: GitHub indexes a path's commit history asynchronously, so a file that is already readable can still have no history. DELETE-removes-file re-issued the identical contents request 150ms after it had succeeded and got a 404 back. Both tests finished in under 5s, so neither loop ran to its timeout. The poll is now the assertion. PUT-creates-commit waits for the author it wants to compare, GET-timemap - which passed this run, on the same pattern and the same luck - waits for the listing its TimeMap is built from, and DELETE-removes-file keeps what its loop found rather than asking again. This is what GET-version and GET-timegate already do, and why they pass: they spent 10-12s waiting out the same lag the other three refused to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK1ssk2A67MYseeH3hWnhf
…s already are. Web-Client now folds every value of a property into the one cell its header allots, each value a block of its own inside a container the cell holds. Give that container the treatment the property list's dd's already have: a divider between consecutive values, and a cap on the stack. The cap belongs on the container rather than the cell because a td cannot bound its own height. Past it the surplus scrolls, so an order with fifty line items stays whole and reachable instead of setting the row height for the whole table. The language pill moves behind the ac:lang-tag mode Web-Client applies from wherever values are laid out. It was written inline in the dd and would have had to be written again for the cell; now the property list and the table cell read the same pill from one place, and the mode is what a skin answers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YxhcqMPK1kZR1pxxJqoge
Versioning has served all three RFC 7089 roles from the document URI since 5.9.0, but the CLI could reach only two of them, and only by concatenating the query string itself. ?timegate it could not reach at all: negotiation is carried by an Accept-Datetime request header, and no command could send one. So the suite that covers the contract asserted two thirds of it through ldh and dropped to curl for the rest, converting datetimes with a `date -u -j -f ... || date -u -d ...` pair to paper over BSD and GNU disagreeing about how to parse a flag. get now takes --version, --timemap and --timegate as an exclusive group, each still one request, plus a --datetime that reads RFC 1123 or ISO 8601 and writes the zero-padded RFC 1123 the grammar asks for. The ISO half is what removes the date(1) dance: a prov:generatedAtTime lifted out of a TimeMap, or a commit datetime out of the GitHub API, goes straight back in. --timegate prints the Location as the only line on stdout, which is the same contract create-item has and what lets the negotiated version pipe into another get. It is also the one request that needs no --accept, a 302 having no representation to negotiate, so the option moves from picocli's required to a check in call(). Through --proxy the printed URI is rewritten back to the logical origin, as everywhere else the CLI prints one. The enablers were already in place and unused: Core's get() has taken a header map since 4.0.14, and the client has never followed redirects, on the grounds that the scripts called curl without -L. That decision was about matching the scripts; it is what makes reading Location possible. Nothing here has run against a live instance. The suite is gated on a GitHub token and skips without one, so CI is where these paths first execute. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YxhcqMPK1kZR1pxxJqoge
Seventy-seven echoes across eighteen scripts printed DEBUG lines, most of them on every passing run. They accumulated the way CLAUDE.md prescribes - when a test fails, add output showing expected against actual, then diagnose - but the step after that, taking the scaffolding back out once the cause is found, was never taken. What was left restated in a string what the line under it already asserts in code, and buried the runs that mattered under the ones that didn't. Three kinds went beyond the echo itself. A label ending in a colon introduced a bare dump on the next line, so removing only the label would have left the body printing. Failure branches carried `gh api ... || true` calls, and one carried a five-line curl piped into sed, that no assertion reads and no exit code depends on. PUT-creates-commit had an if whose entire body was diagnostic and which never exited; the equality test after it already fails on an empty author. import_triple_count() had no callers left once its two call sites went. Every assertion stands, and the comments were already saying what the strings were repeating. What is lost is expected-against-actual at the moment of failure: a failing run now names the script and not the values. That is the first thing to add back when one fails, which is where CLAUDE.md starts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YxhcqMPK1kZR1pxxJqoge
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YxhcqMPK1kZR1pxxJqoge
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YxhcqMPK1kZR1pxxJqoge
The pill was written for the property list and scoped to it: .ldh-block dl > div > dd .chip-inline. But ac:lang-tag emits the badge from wherever values are laid out, and since a table cell started holding every value a property has, it emits it there too - where no rule matched it and it fell back to bare inline text glued to the value it annotates, so a Lithuanian label read "Seimaslt". The graph3d node details had the same gap. So the pill belongs to .chip-inline itself rather than to a position inside one container, the way .btn-type already belongs to the type and not to the header list it first appeared in. A property renders every language it carries, ordered by the reader's preference, so the tag is what tells the values apart: it has to survive being laid out somewhere new. Its tag stays on one line - sr-Latn-RS must not break the way the prose beside it does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YxhcqMPK1kZR1pxxJqoge
…ent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YxhcqMPK1kZR1pxxJqoge
These blocks are never stored. ldh:RenderRow queries /ns for the views attached to a resource's types, fetches each one and appends it to the host wrapper, so the block is rebuilt on every load. ac:uuid() gave each rebuild a fresh subject, which left the block's Copy URI value true for exactly as long as the page stayed open. What identifies such a block is the resource it hangs off and the view it renders, and both are already in hand at the insertion point. Deriving the fragment from that pair reproduces the same URI the next time the block is derived. Both halves are needed: ldh:RenderRow visits every wrapper, so a container listing injects the same view under each of its children, and the view URI alone would hand them all one @about - enough to make element-by-about ambiguous and to give ldh:ScrollToFragment more than one element to land on. Combining them means hashing. No digest is reachable from XSLT in either product, and crypto.subtle.digest returns a promise, so it cannot stand where ac:uuid() stands. djb2 over the codepoints needs only multiplication and addition, which XPath 3.1 has, so one definition serves Saxon-HE and Saxon-JS alike. Random UUIDs stay right wherever a block's subject is written down, so ldh:CreateBlock and the form flows keep theirs. The line is whether the URI is stored or re-derived. A stable fragment is still not one you can follow, because the scroll ran too early. ldh:RenderTab defers a fragment scroll until the page's block factories settle, but the view chain fired its per-view promises as detached instructions and returned, so it settled as the /ns query came back and the countdown reached zero while the document held none of the injected blocks. The chains are returned and joined with ixsl:all now, down to the injected block's own hydration: the block's results are what give it its height, and a scroll that lands before they arrive lands on a block that then grows out from under it. The per-view context merge kept the first 'request' it saw, which was the /ns query rather than the view fetch. Nothing read it while the fetch took the request directly, but the fan-out reads it from the map, and ldh:retry-request re-fires whatever it holds when a 429 comes back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YxhcqMPK1kZR1pxxJqoge
A <pre> in an ldh:XHTML block had no skin left once Bootstrap 2 went, so a Turtle listing ran on the bare page background. It now takes the recessed well the inline .ldh-code chip and the error-detail listing already share, which tints with the theme rather than fixing a white that inverts wrong in dark mode. Colour and size come from the inline chip instead of the error listing, whose --fg-muted at --fs-xs demotes text that here is the content; and a long IRI scrolls the block instead of wrapping, so a listing keeps the line structure it was written with. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YxhcqMPK1kZR1pxxJqoge
… dls to their own layout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1UnYpbW5aYLj7LbwtyUo1
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YxhcqMPK1kZR1pxxJqoge
A view block spent the band between its title and its first row on two control bars, and a chart block on a grid of three labelled selects. All three now ship carrying is-collapsed, and a tune button in the card header is the one thing that clears it - the token and the button vocabulary the query block's editor toggle already used over div.ldh-sparql, so "show me this block's controls" is one gesture and one state name across the product. No band is re-anatomised. Each keeps its markup exactly, which is what lets every facet, sort, pivot, mode, pager and chart-select handler go on binding the same elements, and what makes the CSS a single display rule in ldh.css rather than a new component. One handler serves both block types, because the gesture belongs to the block rather than to the view or the chart: it finds the bands by class under the block instead of knowing which kind it is, so a view in ChartMode reveals its toolbar, its pivot row and its chart grid together rather than half of them. The state is read back off the first band, so aria-pressed stays a report of the DOM rather than a second copy that can drift. What the collapsed toolbar used to state moves to a status line under the title, because a row of results cannot say how many of them there are or what was filtered out of them. The slot is emitted empty by ac:BlockHeader and filled by the view, as the create slot is - the header does not know the container id the count is addressed by. The count element itself MOVES there rather than being duplicated: same element, same id, so its three writers are indifferent to which end of the card it sits at. Beside it a second span names the applied facets and pivot steps, read back off the pills the toolbar has just rendered rather than re-derived from the query, which is what keeps the line and the pills incapable of disagreeing. A chart needs none of this: its category is the axis, its series is what the bars measure, and its type is the thing being looked at, so hiding its selects leaves nothing unsaid. The pivot row stops being a disclosure. Closed-by-default was the answer to a full pill row pushing the results below the fold; with the whole control header now off the card until it is asked for, a disclosure inside it only meant asking twice. What is left is the flat bar it always was: no summary, no chevron, no count, and no "Related results" caption on a row of pills that each say what they are - the label survives as the group's accessible name, since the lead glyph is aria-hidden. That glyph now opens the row on the toolbar's own padding, so the funnel and the junction line up down the card's left edge. Two match patterns had to follow it out of the details element, and one of them was the pivot pill's own onclick - left behind, pivoting would have stopped working with nothing to show for it, so the new spec drives a pivot and asserts on the step chip it leaves rather than merely looking at the row. A second defect surfaced the same way: ldh:chart-results-response replaces div.chart-controls wholesale, so every category or series pick re-rendered the grid from a state the toggle had never reached and shut the controls under the hand using them. It now carries the collapsed state across from the element it replaces. Where a view has no card header - a modal-hosted one, from search, geo, latest or class instances - it gets no toggle, and so nothing that could ever clear the token. Those views keep their chrome open and their count in the toolbar, on the same test the create slot already makes; collapsing them would have hidden the controls permanently. ChartMode at document level is exempt for the same reason and states it in the parameter: ldh:Chart defaults collapsed to false and only the two block call sites opt in, because a document is not a block and has no header to hang the affordance on. Verified against the running instance rather than read off the code: five new assertions in block-controls.spec.mjs covering both block types, the one-press reveal, the status line under an applied facet, a pivot actually taken, and the headerless fallback; the owner suite at 63 passed and the anonymous one at 15, with responsive and view-overflow updated because they reached for controls that are now behind the toggle - the change working rather than breaking. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e chrome rules. The hosted kit was ahead of the vendored copy: the constructor modal had gained .ldh-ctor-modal, .ldh-ctor-lede, .ldh-ctor-rmctor, .ldh-ctor-empty and the whole .ctor-pred-* predicate typeahead since the last vendor. Pushing the local file over it would have deleted all of that, so the block-controls rules went the other way - replayed onto the hosted file and pushed from there - and this is that same merged file coming back down, which is what keeps re-vendoring a plain copy rather than a patch. One removal to know about. The kit drops `flex: none` from .ldh-ctor-card, which the vendored copy carried with a note explaining that without it the cards shrink to absorb whatever height the dialog is short of and clip their own rows. Upstream replaced it with a different arrangement - .ldh-ctor-modal as the flex column and .ldh-ctor-body as the scrollport - and LDH does not emit .ldh-ctor-modal yet, so between this commit and the constructor markup landing, the modal is running without either fix. Worth a look when that work lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e66a69a stopped an ontology view declared ldh:showWhenEmpty false from being painted before its query could say whether it was empty, and shipped without a test. This is that test, left untracked in the working tree since. It measures at the DOM rather than by screenshot, and in two independent ways, because the defect is a single frame: a MutationObserver reads the display the block was inserted with - delivered at the microtask checkpoint after insertion and before any paint, so it IS what the browser would first have rendered - and a requestAnimationFrame sampler beside it asks whether any frame of the whole load ever gave the block a box. The Broader view on the same page is the control: a build that simply hid every declared view would satisfy the first assertion and fail that one. Unrelated to the block-controls chrome two commits back, and needed no adjustment for it: it names no element that change touches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The constructor editor's range slot is the class combobox in its committed state: a chip holding the class, its edit button reopening the lookup. A row whose range was undeclared came up as an open lookup instead, and the author had to name a class before Save would take the row - with no way to say "any resource", which is what the platform's own constructors write as [ a rdfs:Resource ] for owl:imports, sd:endpoint, ac:mode and foaf:primaryTopic, and what the SHACL side already defaults a shape with no sh:class to. ldh:ConstructorResourceObject now coalesces its range to rdfs:Resource and always renders the chip. Defaulting in the slot rather than in the save path is what keeps it small: the row always carries its hidden ou input, so ldh:constructor-valid-rows, the missing-value check and the object-type read need no notion of a range left open. Nothing in /ns describes rdfs:Resource, and the resource branch had no fallback for a type it cannot resolve - the same hole that emptied the slot for rdf:langString and let the save drop the row. It now synthesizes the node the chip renders from, the way the predicate slot does, labelled by ldh:class-label(): the localized word for rdfs:Resource, the URI tail for anything else. The hand-written [ a rdfs:Resource ] rows render for the first time because of it. Both chips now carry the lookup scope they were committed with, which is what the edit button reopens with - a chip rendered from stored data had none, where one committed through the UI always did. Two specs cover the narrowing the default makes possible: a range defaulted into a fresh row and one stored as rdfs:Resource, each narrowed to skos:Concept through the chip's lookup and saved. addLiteralRow splits into addRow plus its literal half so both share the predicate-commit sequence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sixteen test groups each ran as `run_tests <group> <files>` followed by `(( error_count += $? ))`, and one of the sixteen - imports - had no such line. Every failure under imports/ was therefore printed, counted inside run_tests, and then dropped: the run reported "### Failed tests: 0", exited 0, and GitHub showed a green check over a red test. Measured on run 35997259851, where imports/import-rdf-no-query.sh printed "failed" and the tally said zero. It had been quietly under-reporting locally too, which is how it surfaced: a grep for failures kept returning a larger number than the runner's own tally, by exactly the number of failing tests in that one group. The accumulation moves inside run_tests, which already counts what it needs, and the sixteen paired lines go. A tally that lives at the call site is a tally that can be forgotten at one call site; there is now one place that counts, one that reports and one that exits. Verified: with three imports tests failing, the runner now says 28, a grep for failures says 28, and make exits 28 - the three numbers agreed nowhere before this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…llapsed. The chart assertion failed on CI and passed on every laptop. Its trace holds no click at all: revealControls() snapshotted `.ldh-block-head .tb-controls` with .all(), got an empty list, ran its loop body zero times and said nothing, and the miss surfaced 30s later as ".chart-controls never rendered" against a grid that nothing had revealed. Throttling the CPU 10x reproduces it exactly - the snapshot resolves to 0 toggles at 10x and 20x, and to 1 at normal speed. Two things were wrong, and the second only showed up once the first was fixed. The wait has to be for a BAND, not for the button. The toggle is in the server's first paint, but a view's bars are rendered client-side and the handler is explicitly a no-op while they are missing (client/block.xsl) - a click that lands early does nothing and is not replayed. settled() cannot stand in: its signal is the .ldh-block-row count, which on a slow machine goes quiet BETWEEN injections, so it hands back a page whose chrome does not exist yet. And one pass is not enough. Waiting for a band says the page has begun rendering chrome, not that it has finished: under a 10x throttle the fixture container was still two blocks short at that moment, and those blocks were never pressed. The pass repeats until nothing is collapsed. Repeating is only safe because each press is conditional on that block's own first band still reading collapsed - the button is a toggle, not a reveal, so an unconditional second press puts the chrome back, which is what the first attempt at this did until it spun. The helper now asserts that nothing stayed collapsed, so the next miss fails here naming itself rather than as a timeout three lines later. That assertion is what caught the insufficient single pass, reporting two collapsed bands at the reveal. Verified against a live instance: 12/12 at normal speed, 2/2 for the chart and desktop assertions at 10x, 12/12 at 20x. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cause three emitters composed one row. An existing tagged literal's language field rode a strip the row wrapped around it; a constructor's rdf:langString marker emitted its field bare; and every chip carried a strip of its own. With each strip at flex 1 1 0 the field landed halfway along one row and flush right on the next, hover-revealed on one and always visible on the other. Measured on the concept form: 1376 / 1391 / 1391. Now the row composes: the control, the hidden datatype input, then ONE div.ldh-annot holding whatever ac:ValueAnnotations says about the value. The language field is an annotation of the @xml:lang that makes the literal an rdf:langString, so it lives in that template; a marker synthesises the attribute and reaches the same one. The xsd:* marker's datatype chip moves out of its two controls (xsd, boolean) into one annotation template, and an XMLLiteral annotates once instead of once per text node. The chip override no longer wraps. The class a constructor declares for a property's objects is a fact about the property, so the row derives it once (ldh:constructor-range, lifted from two copies) and hands it down as $forClass to the control, for its combobox scope, and to the annotation, for its chip; nothing below the row sees the constructor any more. Kit: .ldh-annot > .ldh-lang no longer shrinks, so a short strip clips a chip before the input. Verified on the live stack: all three label rows put the field at x=1356 inside one strip, hidden at rest; concept-tree, constructor-editor and view-create specs pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… against (#392) * A write to a document that exists must say which state it was written against. Every write in DocumentHierarchyGraphStoreImpl is a whole-graph read-modify-write: the graph is read, changed in memory and written back. Two writers that name no precondition therefore overwrite each other with nothing to show that anything was lost. The machinery to prevent that was already here and already worked - a stale If-Match answers 412 - but it was opt-in, and evaluatePreconditions() is vacuous for a request that carries no conditional header, so a client simply not sending one got last-write-wins. Measured in the constructor editor: one save's DELETE was correct in every detail, answered 204, and was reinstated by a second write that had read the graph before it landed. A write to an existing document without If-Match is now 428 Precondition Required. Creating a document is exempt: it has no state to have been written against, which is what If-None-Match rather than If-Match would express. The CLI writes conditionally to match, in LDHClient rather than at fifteen call sites, so every command inherits it. Two things the running stack taught, both of which would otherwise 412 every time: - an entity tag identifies a NEGOTIATED VARIANT, not the graph. The same document answers "5a4df28aea61e278" as RDF/XML and "5a4df28aec9db937" as Turtle - one model digest, two variant suffixes - so the conditional read has to send the same Accept as the write it is for. - delete() and post() defaulted to an empty accept list, which gets the HTML shell, whose tag no write will match. CLI writes now always negotiate RDF, which is what an RDF client wanted in any case. PushOutputTest asserted the old request sequence and its fixed indices; both now assert the conditional protocol, including that a failed write still stops the run before the next document. Verified against the running stack: no header 428, stale 412, correct 204, and ldh create -> patch -> delete end to end. /settings is a separate resource with no precondition check and is deliberately not covered here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * The HTTP tests write conditionally, and a blank If-Match no longer counts as one. Making the precondition mandatory left 40 tests writing unconditionally to documents that already exist, so they answered 428 where they expected 204, 400 or 422. Under `curl -f` that prints nothing and `set -e` aborts, so most of them failed with an empty log - including imports/import-internal-url-400.sh, where the 428 arrived before the SSRF check and the test stopped proving that a link-local ldh:file is rejected at all. A security assertion that fails for an unrelated reason is worse than one that fails loudly, which is what made this worth finishing rather than leaving the tests red. run.sh grows an exported `etag` helper. It reads the validator with the same Accept the write will send, because an entity tag identifies a NEGOTIATED VARIANT and not the graph: the same document answers "5a4df28aea61e278" as RDF/XML and "5a4df28aec9db937" as Turtle, and quoting the wrong one is refused with 412 rather than accepted. It says so on stderr when a document has no tag, because the alternative - handing back an empty string - produced exactly the unexplained 428 it was meant to prevent. Two writes stay unconditional, on purpose: - those asserting 401/403. A request that is going to be refused for authorization carries no precondition, because the agent cannot read the document to get one; requiring it would turn every "access is unauthorized" test into a 428 and stop testing authorization. - those creating a document. There is no state to have been written against, and the server exempts them. A proxied write reads its validator through the proxy (`?uri=`), not from the proxy's own base document, which is a different resource with a different tag. The server now treats a blank If-Match as absent. Measured while fixing the tests: `-H "If-Match: "` answered 204, because getHeaderString returns "" rather than null - so a client could opt out of the precondition entirely by sending the header empty, and a test whose validator lookup silently failed would have gone on writing unconditionally. Locally 140/73 ok/failed becomes 182/31. Of what remains, 15 need the test.localhost dataspace this working copy does not define, one needs SSRF protection that .env disables here (ALLOW_INTERNAL_URLS=true), and the rest are left to CI, which runs the suite in a clean environment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Every client write says which state it was written against. Eight writes went out unconditionally, and against a server that now requires If-Match of any write to a document that already exists, each of them is a 428 the agent cannot act on. The Import ontology dialog is how it surfaced: its chain appends twice into the target, and with the first append refused the chain stops, so #add-data never closes and the UI test waits ninety seconds for a modal that will not go. The same shape as the constructor editor's Save, and the same cause - the close lives at the end of a chain, so any earlier failure is indistinguishable from a hang. The rest of the client already wrote conditionally, taking the validator from LinkedDataHub.contents. That only holds documents THIS browser fetched, which is why the gap falls where it does: every write that was unconditional targets a document the page never loaded, or one it loaded but whose write path predates the convention. - client.xsl: the document DELETE and the RDF file drop, both on the document being read, so the cached validator serves. - block.xsl: removing a saved block and reordering blocks, likewise on the current document. - memento.xsl: restoring a version, which overwrites the whole graph - without a validator it would discard anything written since the timemap was rendered. - modal.xsl: the import chain's two appends and the add/clone append. These target a document the agent NAMES in the form rather than one the browser loaded, so nothing is cached for it: the chain HEADs the target once, and the second append reuses the entity tag the first one's response carries, because a GSP write answers with the tag of the graph it just wrote (DocumentHierarchyGraphStoreImpl:392). One extra request for the whole flow. ldh:document-etag and ldh:conditional-headers in client/functions.xsl carry the two decisions that were being made ad hoc: where a validator comes from, and that an absent one sends NO If-Match rather than an empty one - the server reads blank as absent, so an empty value would have looked like a precondition that passed. Driven in a browser: ontology-import, which was failing, now passes, and the full UI suite is green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * A writer may HEAD the document it is allowed to write. Requiring If-Match of every write quietly redefined acl:Write as acl:Write AND acl:Read. HEAD mapped to acl:Read, HEAD is the only way to learn an entity tag, and a conditional write has to quote one - so an agent granted write access and nothing else could no longer write at all. Measured in CI on nine admin/acl tests, each created with `ldh admin create authorization --write` and each failing with the helper saying why: "### etag: no entity tag for <https://localhost:4443/>". That coupling appears in no authorization document and no vocabulary; it was an emergent property of the precondition rule. The alternative considered and rejected was exempting agents who cannot read. It keeps them writing, but their writes are then unconditional - they overwrite a concurrent edit with nothing to show for it, which is the failure this branch exists to remove. Blind overwrite is not an acceptable price for the rule that was meant to prevent it. So HEAD is granted to any agent holding a mode on the document: it may be answered for acl:Read, acl:Append or acl:Write. GET is untouched and content still needs acl:Read; an agent with no mode at all - anonymous included - is refused as before. What that discloses, stated rather than left implicit: the entity tag is a hash of the graph (Core, GraphStoreBase:283), so an agent that may write but not read gains a way to CONFIRM a guess at the content - reproduce the graph exactly, hash it, compare. It cannot extract anything. Every graph carries a dct:modified stamped to the millisecond on each write, so the guess must include that too, which is why a non-reader's HEAD is trimmed here to the validator and its own modes: Last-Modified would otherwise narrow the search from milliseconds to seconds, and Content-Length describes a representation the agent may not have. Closing it completely means an opaque validator - a keyed or stored one - which is a change to Core and its own piece of work. Verified against the running stack: a reader's HEAD still carries ETag, Last-Modified and Content-Length; anonymous is still refused HEAD and GET alike; the UI suite is green. The nine acl tests cannot run in this working copy, which does not define the test.localhost dataspace they need - they die before reaching the write - so CI adjudicates those. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * If-None-Match is a precondition too, and the import never got past it. Requiring If-Match of every write broke every RDF import into a document that already existed. The import writer asks to create: PUT with If-None-Match: *, and on the 412 that says the document is already there, POST to append instead. Demanding If-Match of that PUT answered 428 - which is not 412, so the fallback never ran and the import failed with "could not be successfully created using PUT. Status code: 428". Measured on the running stack, and it is what CI was reporting as imports/import-rdf-no-query.sh. The rule was wrong, not the import. A write must be CONDITIONAL; If-Match is one way to say so and If-None-Match is the other, and they are mutually exclusive by construction - a request asking to create cannot quote the entity tag of a thing it is asserting does not exist. So a write carrying either satisfies the requirement. That was only the first of three links: - the 412 carried no entity tag, so the fallback had nothing to quote and its own POST went out unconditional - 428 again. A failed precondition now names the current validator, which is what lets a client retry without a second read, and is what a 304 is supposed to carry anyway. - the fallback append now quotes it. A 412 from the append would then mean the document changed between the two requests, which is the thing being guarded against and no longer silent. Three places use that idiom - twice in RDFGraphStoreOutput, once in CSVGraphStoreRowProcessor - and all three are fixed. Only one test caught it because the fallback only runs when the target document already exists: import-csv.sh and import-rdf.sh write to graphs they create, so their PUT succeeds and the 412 branch never executes. Verified against the running stack: PUT with If-None-Match: * on an existing document answers 412 carrying its tag, a write with no precondition at all still answers 428, and an RDF import into an existing document lands its triples. The UI suite is green and imports/import-rdf-no-query.sh passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Tests for the preconditions themselves, which nothing was asserting. This branch changed what a write must carry, what a HEAD may be answered for, and what a failed precondition says back - and leant on tests that touch none of it deliberately. It showed: the If-None-Match break was caught by imports/import-rdf-no-query.sh, which is about importing RDF and noticed only because its fallback keys on a status. Every other answer here was verified by hand at a terminal and then left unpinned. Three tests, one per thing that was actually wrong at some point while this was written: - document-hierarchy/PATCH-preconditions.sh: a write to an existing document with no precondition is 428, with a BLANK If-Match is 428 (an empty header is not a validator, and reading it as one let a client opt out of the rule entirely), with a stale one is 412, and with the current one succeeds. The last case reads the tag with the same Accept the write sends, because an entity tag identifies a negotiated variant rather than the graph. - document-hierarchy/PUT-if-none-match.sh: If-None-Match: * creates a document that is absent, and on one that exists answers 412 - not 428 - carrying the current entity tag, which the append that follows then quotes. That is the import writer's whole flow, and all three steps of it were broken in turn. - admin/acl/HEAD-write-without-read.sh: an agent granted acl:Write and nothing else is refused before the authorization exists, may then HEAD for the validator it has to quote, is still refused GET, and can complete the conditional write. Without this, making HEAD require acl:Read again would only show up as nine unrelated acl tests failing with an unexplained 428. The last of those found something while being written: Content-Length survives a non-reader's HEAD, because Jersey computes it when it serializes, after the response filter has run - so the line removing it was dead. It is gone, and the reason is written down rather than left as code that looks like it works. Nothing is lost by it: anyone able to confirm content by its hash knows its length. Last-Modified, which does matter - it narrows the millisecond dct:modified the hash is taken over - is removed and now asserted on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The panel is a lookup result, so the gesture that opens it is typing. It opened on focus as well: an ixsl:onfocusin handler in client/form.xsl, headed "show a combobox panel with instances in the form", built a second item list out of the form's own RDF/POST subject inputs and showed it without ever reading the input's value. Clicking or tabbing into the field was enough, and so was the focus() the committed chip's edit button performs on the input it creates. The handler is from 759e911 (2021) and only its visibility is new: the placement work of 2ad4ae9, f7719fe and 8c32483 means a fixed panel that used to land at the flex column's origin - over the field, hidden for a frame - now measures the field and drops neatly under it in the same tick. Reported as intermittent, and it is: the panel appears only where the row's class scope admits something, and the first keystroke hides it again. It reproduces on any document's edit form, whose foaf:primaryTopic the platform's own constructor gives the range [ a rdfs:Resource ] - the case the filter read as "no type filter" - where it offered the document as the object of its own property. It is deleted rather than repaired. Its items were titled with the subject's own value, so it could only ever show identifiers, which is why it went unused; and it was the one place in the UI where a blank node could be picked, since ldh:ComboboxLoaded passes ldh:ComboboxProcess only rdf:RDF/*[@Rdf:about] and every other lookup is therefore URI-only by construction. Under the rule that only resources with URIs are referenced in the LinkedDataHub UI it is an inconsistency, not a capability. Four latent defects go with it: an untokenized dataset.forClass read, unlike every other reader; a type filter on the static @value attribute rather than the live property, so a type picked after render was invisible to it; the $forClass = rdfs:Resource branch that turned any such scope into "list everything"; and the identifier titles. Existing blank-node references still render and round-trip - values.xsl renders an owl:NamedIndividual bnode object as a chip, and the subject-type flip keeps sb/ob in sync - they simply cannot be authored. ui-tests/specs/combobox.spec.mjs is the general spec the control never had. Sixteen tests over the two places it appears: the document edit modal's unscoped foaf:primaryTopic row, and a concept's inline form, whose skos:related row is scoped to skos:Concept and whose skos:inScheme row arrives committed. It covers what opens the panel and what does not, committing by mouse and by keyboard, arrow navigation, the two things Escape means depending on whether a chip is behind the lookup, the empty-focusout restore, both placements, a refused lookup reporting where its suggestions would have been, and the type combobox's SHACL+SPIN fieldset rebuild. Before this, the only coverage was incidental: constructor-editor.spec.mjs drives type-then-pick to reach a constructor it asserts on, and lib/blocks.mjs fillResource deliberately avoids the lookup altogether. Writing it found two defects, both fixed here. Enter committed the highlighted suggestion and submitted the form. The Enter branch called preventDefault on ixsl:onkeyup, commented "prevent form submit", but a form's implicit submission rides Enter's keydown: by keyup the request has already been asked for, and a PATCH of the half-edited row went out and was answered 400. An ixsl:onkeydown template now refuses the default under exactly the condition the commit runs under - a panel with an active item - so an Enter with nothing active stays an ordinary Enter and still submits. The panel settled a few pixels off the field it was aimed at. .ac-cb-panel carried `animation: ac-cb-in`, whose keyframe animates transform - the property ldh:ComboboxPlace expresses its correction in - so the keyframe overrode the placement for the animation's length, and, because the placement reads getBoundingClientRect() while it is mid-flight, the origin it measured from was displaced by however far the animation had got. Measured: 3.3px past the intended 4px gap, varying run to run. The keyframe now fades only, which is what lets the two placement tests assert the gap exactly. The comment above the rule, still describing the visibility/one-frame dance 8c32483 removed, is corrected to say why a moving entry animation is forbidden on this element. controls.css is vendored, and the same fix is pushed to ui_kits/core/controls.css in the Claude Design project and read back, since nothing on that path compares the two ends - see its own uploads/DS-NOTE-vendoring-channel.md. Web-Client vendors the kit too, but its copy is an older revision whose panel is absolutely positioned and placed by CSS, so the defect does not apply there; that copy being stale is the note's finding 3, not this. Verified against the running stack: the focus assertion fails on the unmodified stylesheet and passes after, and the full owner suite is 89 passed, 1 skipped, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It reported as one. Pressing F5 while a block chain still had a request in flight made the
browser cancel that fetch, which rejects with a plain TypeError ("NetworkError when attempting
to fetch resource." in Firefox, "Failed to fetch" in Chromium). SaxonJS wraps it as SXJS0009
"HTTP request failed: ...", and ldh:promise-failure read SXJS0009 as a request that did not
complete - which it is - and painted "Failed to load block data / The request did not
complete." into the document the reader had already left. The old document stays on screen
until the new one arrives, so the alert flashed and vanished, about one refresh in fifteen:
whenever a chain was still in flight. The taxonomy package widened the window, since its views
live in an ontology fetched through the proxy on every concept page. Before 937a215 this path
called alert(), which browsers suppress during unload, so it was invisible.
The function already knew one way a request is superseded, its own abort controller, and said
nothing for it. Now it knows the other: an ixsl:onbeforeunload listener raises
LinkedDataHub.unloading, initialised false at bootstrap, and a rejection arriving after that
reports nothing. beforeunload rather than pagehide, because the browsers cancel the old
document's loads between the two events. Nothing matches the browsers' message text.
unload-in-flight.spec.mjs holds every block request open, reloads from inside the page as F5
does - Playwright's page.reload() tears the document down before the rejections run and never
shows it - and asserts the old document paints nothing; a second test drops the view lookup
with the page staying put and asserts the failure still reports. Red on the previous build with
exactly the observed alert, green after; the Firefox probe records the cancelled request
rejecting and nothing painted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ested. specs/ now mirrors the anatomy of the rendered page - layout.xsl's shell, document.xsl's pane and content body, resource.xsl's block card and the kinds under it - so a spec's path is what it is about. Two rules decide where one goes: it lives under the component it asserts ABOUT rather than the one it navigates THROUGH, which is why the annotation dialog is an overlay spec and the XHTML block it is reached through stays honestly untested; and a leaf component with a single spec may be that file rather than a folder. Claims that cross components - the responsive axis, the authorization axis - live in axes/, because filing either under one component would claim coverage of the four it happens to touch. Two things are deliberately NOT in the path, neither being a fact about the component: which app serves the fixture (the import dialog is the admin app's, the constructor editor spans both), and who owns the markup (the concept tree is the SKOS package's, rendered in the platform's ldh:ContentColumn - which is also why it sits under document/ rather than in the shell drawer it resembles). Grouping by component exposed four duplications, each now one module: lib/mode.mjs (the mode URI and ?mode=, written out in seven specs), lib/block.mjs (the ancestor-token XPath and the card's control toggle, in three), lib/tree.mjs (the row vocabulary the drawer tree and the concept tree share, taking the tree root as a parameter so neither knows which it is walking) and lib/drawer.mjs (the edge-hover gesture, in two). lib/blocks.mjs is lib/view.mjs, being about the view block rather than the card. lib/ stays flat: specs/ is containment, lib/ is vocabulary, and a helper reached across regions must not live inside one region's subtree. A folder cannot represent an absence - git does not track an empty directory - so coverage/components.mjs declares the tree, the 24 components nothing asserts included, and a third Playwright project joins the three things that together say what is tested: what is declared, which folders hold specs, and what renders on five probe pages. It replaces specs/calibration.spec.mjs, whose header asked to be deleted once its findings were folded in, and reports its retired-class list unasserted - clean on this run. The mandatory xsl field earns itself immediately: .ldh-nblock, .ldh-query-block, .ldh-auth, .ldh-import-flow and .ldh-onto-grid are styled in app.css and emitted by no stylesheet, so they are not components and cannot be declared as ones. Every test title survived the move unchanged. Full suite green against the execution order the new paths give it: 104 passed, 78 skipped, calibration's six probes replaced by the coverage project's four. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Making writes conditional meant handing validators to agents who may not read what they describe: HEAD is answered for any access mode so that an agent with acl:Append and no acl:Read - a dropbox depositor, which is what the ACL vocabulary recommends the mode for - can obtain the tag its writes must quote. The tag it gets is a XOR fold of a hash per triple, and XOR is linear:
hash(G + t) = hash(G) XOR hashTriple(t)
hashTriple is computable offline, and a set gains nothing when you add a member it already has. So a depositor appends a triple, reads the tag the write answers with, and learns whether that triple was already in a document it cannot read. One bit per request, using only the capability it was granted. On /settings it is cleaner still: PATCH stamps no dct:modified, so there is not even a timestamp to cancel out - h1 == h0 XOR hashTriple(t) exactly.
The tag is now a SHA-256 digest of the graph URI and a sorted N-Triples serialization, truncated to 128 bits of lowercase hex. What changes is only the function:
- the URI is digested with the content, so identical triples in two documents no longer share a tag and a guess cannot be materialized somewhere readable and compared;
- the digest is not linear, so the difference between two tags says nothing about what was added;
- and it no longer collides the way an XOR fold over an overlapping-shift per-triple mix does, which mattered beyond confidentiality: two different graphs sharing a tag means If-Match can pass against content that changed, which is the lost update this mechanism exists to prevent.
Everything that made a content hash the right shape for RDF caching is kept: still derived from the content, still strong as If-Match requires, still identical for identical content, still sensitive to writes that bypass the resource, still no stored state and no secret. A stored revision - CouchDB's _rev, Kubernetes' resourceVersion - would be the idiomatic choice elsewhere, but not here: it would desynchronize the moment PackageService or a test harness writes straight to the graph store, and there is nowhere to keep one.
Two sites, established by audit rather than assumption. AuthorizationFilter is @PreMatching and global, so the broadened HEAD reaches every resource, and the question for each is not where a tag is produced but where a NON-READER can move one. Documents can (POST is acl:Append) and /settings can (PATCH is acl:Write while GET is acl:Read). acl/Access has no write method and SPARQL results are only seen by agents who could read them anyway, so both keep theirs.
toSortedNTriples moves out of GraphVersioningService, which keeps working by delegation: a validator depending on a GitHub-mirroring service reads wrong, and its existing test already pins the property the tag needs - two models with the same triples serialize identically.
PackageService now skolemizes. It writes straight to the graph store, to avoid a self-request deadlock from inside OntologyFilter, and was the one write path not upholding the invariant that the data LDH writes is blank-node-free. That invariant is what makes a sorted serialization canonical at all: Jena's _:bN labels are not stable across reads, so a stored blank node would give its document a different tag on every read and no conditional request against it could ever succeed. Measured zero blank-node triples in both stores, but the local package's vocabulary happens to be free of them - one delivering an owl:Restriction or an rdf:List would not have been.
Tests: EntityTagsTest asserts the two properties an HTTP test cannot construct - two graphs with identical triples and different URIs do not share a tag, and tag(G+a) XOR tag(G+b) != tag(G) XOR tag(G+a+b), the identity a linear hash satisfies and a digest does not. POST-blank-node-skolemized.sh covers the one write path of three that had no test. no-blank-nodes-in-store.sh asks the store instead of enumerating paths, which is how PackageService was missed in the first place.
All 17 entity-tag tests pass unchanged - conditional GET, conditional PUT/POST/DELETE and their 412s, the language dimension, proxied tags byte-identical to direct ones, settings, and the precondition tests - as do the UI suite and the four skolemization tests.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rted one.
The tree said where a spec would go; the inventory said which components had none. This
closes the list. Every region that only nested others is marked as such - shell/header,
document, document/action-bar and overlays own no behaviour of their own, and printing
them as gaps was asking for four specs nobody should write.
What the covered-below regions turned out to be hiding, now asserted: the drawer is owned
by the pointer OR the focus and each event checks the other, so leaving with the pointer
while the search field holds focus must not close it - and a closed drawer is `inert`,
which is what actually takes the hidden tree out of reach. A dialog dismisses on its close
button, on a press on the backdrop and on Escape, and must NOT dismiss on a press inside
the card - the negative is the half worth having, since a handler that drops the
containment test passes every positive. The form bar's Reset is the one affordance with no
handler behind it, so nothing would report it if the markup stopped being a reset button;
the assertion is that the abandoned edit never reached the document, read back from the
server. The content column renders only where a package fills it, absent rather than empty.
The shell survives a navigation, asserted by stamping the chrome and finding the same
elements afterwards, because a re-rendered header is indistinguishable from a surviving one
by inspection.
The dropdown is one control with four hosts, so it is asserted once - opens, says so to a
screen reader, yields to the next menu opened, and hands the focus back on Escape - and
each host's spec says only what its own menu holds.
FIVE THINGS THE WRITING FOUND, none of which an existing test could see:
· THE FIXTURE'S CHART HAS NEVER DRAWN. It renders "No results. The query ran cleanly and
matched nothing" while the view beside it renders 25 rows from the same query, so every
.chart-controls measurement in the responsive axis has been chrome around an empty
chart. The spec asserts the canvas says SOMETHING - a drawing or the designed empty
state - which is true either way and still catches silence.
· concept-tree's "renders no tree in ContentMode" asserted nothing: it used
client#ContentMode, which does not exist (the mode is linkeddatahub#ContentMode), and
an unknown mode renders no tree either. Centralising the URIs in lib/mode.mjs is what
exposed it.
· .ldh-rdf-type is emitted by no stylesheet - CSS only, like .ldh-nblock. The inventory
check that was supposed to prevent exactly this only verified the FILE existed, so the
record survived against a real module that never mentions it. The check now looks for
the class across every stylesheet.
· The action bar's Create and overflow toggles ship without aria-expanded while the
header's account and apps buttons ship it - so a screen reader is told nothing about
those two until they have been opened once.
· Nothing in the product opens a dialog from inside a dialog, so Escape's `[last()]` rule
cannot be driven. Both candidates were tried: a dh:Item's edit dialog has no
constructor to edit, and a view in a dialog offers no Create because a search has no
container to create into.
THREE COMPONENTS ARE DELIBERATELY LEFT UNCOVERED, each with its reason in the inventory
rather than as an absence: the query block (seeding one made 14 green tests fail - YASQE
fetches http://prefix.cc over plain HTTP and the browser blocks it as mixed content, which
lib/console.mjs rightly refuses to ignore), the file upload (the fixture application
declares no File class for the Create menu to offer) and the version diff (a fixture
document with a TimeMap renders no history link at all, which is a question rather than a
spec).
lib/ grew two modules on the same principle as the last commit: form.mjs (the control group
a property is edited in, the two inputs a value arrives through, the bar that ends a form)
and modal.mjs (the dialog anatomy, and opening the document's own edit form), both moved
out of lib/view.mjs, which is about the view block rather than about every form that a view
happens to open.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The inventory asserts that every component names an XSL module that exists, and the taxonomy package's record names packages/editor-taxonomy/skos.xsl. .gitignore excludes /packages: it is a working copy, and a package reaches an instance as an import from its publisher rather than as a file in the tree. So the path resolves on a laptop that happens to have one checked out and on nothing else, which is how the suite went green here and red on CI in the same commit (run 36064289151, one failed of 182). A package-owned record is now checked only where its working copy exists. The platform's own modules are checked always - that is the half a typo would otherwise slip through, and the half that is always present. The same guard covers the stylesheet sweep the token check added since, which called readdirSync on packages/ unconditionally and would have thrown ENOENT on CI rather than merely failing an assertion. Verified against the CI condition rather than trusted: simulating no packages/, 43 of the 44 records are checked and none is missing, the one skipped being the concept tree. Every declared path is also case-exact against the directory listing - existsSync on macOS is case-insensitive, so a wrong-case path would pass here and fail on Linux in exactly this way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The header's five toggles ship aria-haspopup="menu" aria-expanded="false" from the server. The action bar's two menus, the two mode switchers and the block menu shipped neither, and the generic handler set ariaExpanded only when it first ran - so until a reader pressed one, assistive technology was told nothing about it at all, and the state it was eventually told had no resting value to return to. One attribute pair each, matching what layout.xsl already does: the Actions kebab, the export menu, the document mode switcher, both Create menus, the block menu and the view mode switcher. Found by writing specs/controls/menu.spec.mjs, which asserted the resting state and could not pass. The spec now asserts it from the server's first paint rather than after a click, which is the half that was actually broken - a click-then-assert test passes against markup that says nothing until it is touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It rendered "No results. The query ran cleanly and matched nothing." Nothing noticed, because no spec had ever asserted that a chart DREW anything - not here, and not in the responsive axis, which measures .chart-controls around whatever the canvas holds. The first cause: the query asked for sioc:has_parent, and an item created in a container states sioc:has_container. Against the application's endpoint with the owner certificate, 0 rows before and 25 after. (The 25 rows a view renders beside it come from the built-in Children view, not from this query - which is what made the emptiness look like the chart's fault rather than the query's.) Correcting that produced a second message rather than a picture: "Data column(s) for axis #0 cannot be of type string". A bar chart's value axis must be numeric and this one plotted ?title. The chart now has its own query counting items per kind, and the item query stays row-shaped for the view - the same division the product's own rule makes, aggregates belonging in charts rather than in views. The chart block was right both times and reported exactly what it was given. The spec now demands a drawing and fails on the blank state, so the next time either happens it is a test failure rather than a picture nobody looked at. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… checking. The anonymous project ran 12 of its 153 tests. 141 skipped, and inside that wall sat two dozen skipping on the claim that "the fixture container is owner-owned" and "on a virgin instance the fixture is not readable anonymously at all". Both false. lib/fixtures.mjs:69 sets fixtures.readable = itemUri(1), and the authorization it seeds grants the dataspace root, the container, that document and the SPARQL endpoint to foaf:Agent. The evidence was in plain sight twice over: curl without a certificate answers 200 for the container, and document-tree/hierarchy.spec.mjs has been running anonymously against itemUri(1) all along. So thirteen specs had their skip deleted, and the run was left to decide which deserved it back. None did: 28 tests pass as an anonymous reader - the breadcrumb, the mode switcher, the timestamp, the property list, all five block kinds, the drawer, the class list, the saved views, the footer and the address bar all render the same without a certificate. That was an assumption for as long as it was a skip, and it is a measurement now. What genuinely cannot run anonymously - it writes, or it is about who you are - carries `@owner`, on its describe where there is one and per test where there is not, and the project declines to collect it rather than collecting it to skip it. The anonymous project is now 42 collected, 40 passed, 2 skipped, and both of those are real: they live in anonymous-affordances, the one spec that tests both agents in one file. A skip means something again, which is the point - the false ones were invisible precisely because they had 141 neighbours. The test.skip hooks stay in the tagged specs. They are unreachable in a normal run and are the safety net for a test added later without the tag: it skips rather than fails. owner 152 passed 1 skipped; anonymous+coverage 44 passed 2 skipped; no failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seven lines against 6.0.0. The two in Migration are what a deployment has to act on: a write to a document that already exists is now refused unless it carries a precondition, and the validator it carries has to have been read with the same Accept the write sends - an entity tag identifies a negotiated representation, so a tag read as Turtle does not satisfy a write negotiating RDF/XML. Security carries the reason the rule exists rather than leaving it implied by Changed. Entity tags were a XOR fold of per-statement hashes, which made them linear: appending a statement moved a document's tag by a value the appender could compute, so an agent with acl:Append and no acl:Read could add one, read the tag off its own write, and learn whether it was already there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… gone stale. The constructor editor's last round is the bulk of it: the "+ Constructor" button and its fan-out are gone, a class with no constructor is given one by its first saved property, an undeclared range reads as any resource, the affordance is revealed only where the constructor's own document is writable, and saving one on a package ontology no longer answers 500. Beside them, the block control bands ship collapsed behind a tune button, the combobox no longer opens on focus, a form row's language field stays in one place, and a reload no longer paints a failure into the document the reader has left. Web-Client is 6.0.1 in the pom; the entry still said 6.0.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every component in the inventory named the XSL module it came from, and the check asserted that module exists. That check failed on CI twice and passed here every time. The concept tree is the taxonomy package's, and a package's stylesheet is not a file in the tree: the running platform downloads it into /var/www/linkeddatahub/packages, which the container owns, mode 0750. On Linux the test process is not that user and cannot read it - and existsSync answers "no" for a file it may not open exactly as it does for one that is not there. On a Mac it answers "yes", because Docker Desktop remaps container-written files to the host user: uid 0 inside, uid 501 outside, same bits, opposite result. Run 36120639034 copied editor-taxonomy/skos.xsl eighteen minutes before the check that called it missing. Two guards were written for this before the field itself was questioned - first "skip it when packages/ is absent" (it is not absent: docker-compose bind-mounts it, and Docker creates the directory), then "skip it when packages/ holds no stylesheet". Both were elaborate ways to keep a field that nothing needed. Nothing in the suite reads it. The coverage join needs an id, a name and a selector. The one check it appeared to serve - a class styled in app.css and emitted by nothing, which is not a component - searches EVERY stylesheet for the selector's own tokens and never consulted the field. That check stays, keyed on `owner` instead: a platform component's class must be emitted somewhere, and a package's is exempt for the reason above. As documentation it was wrong as often as it helped: the drawer's tree was declared against client/tree.xsl, which owns its lazy loading, while client/navigation.xsl emits the markup - and three others were wrong the same way, which is how the token check found them. 44 records lose a field, the report loses a column, and about thirty lines of guard and comment go with them. coverage: 4 passed, 44 declared components unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ite. PATCH /settings updates a dataspace's description - adding an ldh:import is how a package is installed - and Application.updateApp() wrote the result to the context dataset, which is /WEB-INF/classes/com/atomgraph/linkeddatahub/system.nq. entrypoint.sh regenerates that file on every boot by merging config/dataspaces.trig, config/system.trig and the credentials. So the write succeeded, was atomic, logged as successful, took effect immediately - and was discarded by the next start. config/dataspaces.trig has said so in a comment for a while: "local dev: survives a restart, unlike a PATCH /settings install". Runtime settings now go to an overlay outside the deployed application: /var/www/linkeddatahub/settings/dataspaces.trig, on a volume, beside uploads/, sef/ and packages/ - the other three directories the platform writes and the WAR must not hold. It is applied over the context dataset as that loads. A dataspace the overlay carries REPLACES the configured one rather than adding to it, because a runtime change can remove a statement as well as add one: uninstalling a package removes an ldh:import, and a union would put it straight back. The consequence is worth knowing - once a dataspace has been written at runtime, its configuration file stops deciding its settings, and deleting the overlay hands that decision back. Every restored dataspace says so in the log. A deployment that configures no overlay keeps the old behaviour rather than losing its writes somewhere new. The atomic write - temp file in the target's directory, moved onto it - is now writeDataset() and shared by both paths, where it was inline in updateApp(). NOT YET VERIFIED END TO END. The Dockerfile, the entrypoint and the Java are all baked into the image, so proving it needs `make up -- --build`, a package install, a restart and a re-read. What has been checked: mvn compile succeeds, bash -n and xmllint pass, and the ROOT.xml transform run with the new parameter emits <Parameter name="...config#settingsOverlay" value="file:///var/www/linkeddatahub/settings/dataspaces.trig"/>. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… being Application's business. Dataspace descriptions are a dataset of named graphs, one per dataspace. EndpointAccessor - query and update, which core already defines - is the interface that fits them, and the only one that can: the dataspaces are named with urn: URIs and are not dereferenceable, so the Graph Store Protocol cannot address them directly, and GSP cannot enumerate them at startup either. A query does both. ContextEndpointAccessor implements it over the descriptions the platform is running on. Reads delegate to core's dataset-backed implementation, which is what serves them from memory - the context model is serialised into the XSLT source of every HTML response and must never leave the process. Writes are the half core leaves undone: its local implementation logs the UpdateRequest and discards it (verified in the 5.0.5-SNAPSHOT bytecode), which is honest for a dataset loaded read-only from a file and useless here. This applies it, and keeps the three steps that have to happen in order - copy, store, publish. FileContextPersistence is where the files went. Application held the overlay URI, applied the overlay, diffed nothing and wrote two kinds of file; it now holds an accessor and knows none of that. Which dataspaces to write is worked out rather than tracked, because a SPARQL update may touch any graph it likes: the overlay is every dataspace whose description differs from the configured one. A deployment that has changed nothing writes nothing. What this buys is the substitution: a deployment that keeps its configuration in a triplestore constructs core's remote EndpointAccessorImpl instead, and writes no LDH code. What it does not yet buy is invalidation - with a shared store, something else can change a dataspace behind this cache, and nothing here would notice. That is the next design, not an oversight. 20 new unit tests, and they exist because the logic left Application: that a reader holding the previous snapshot never sees a write land, that a change is stored before it is published and not after, that a failed store leaves the old configuration running, that an untouched dataspace still follows its configuration file while a changed one no longer does. Full suite 223 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ConversionException, Objects and UriBuilder each occur exactly once in Application.java -
on their own import line. None of them is fallout from moving the configuration out; they
had simply outlived their callers.
Checked by matching every imported simple name against the rest of the file, counting a
mention in javadoc as a use, so an {@link} does not lose its target. 237 imports, 3 without
a mention. Compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…half the suite may not read. The address bar spec filled the input with item-02 and then asserted the input held item-02, which fill() had made true before Enter was ever pressed - so a bar wired to nothing would have passed it. What it actually measured was timing: the anonymous reader is granted item-01 and not item-02, so pressing Enter fetched a 403, and whether that response landed before the page closed decided the run. It landed in CI for the first time today. The dereference now targets the fixture container, which both axes are granted, and the assertion is on the document that ends up being read - document-body/@about, which ldh:DocumentNavigate stamps when it swaps the pane body. The bar's own value stays asserted as the other half of its contract. Verified against the local stack, both projects green, and with a negative control: the same test with the Enter removed fails on the @about assertion while the value assertion still passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ogy (#394) * The ontology said "application" and the namespace said w3.org. Neither was true. Two names had outlived what they identified, and both are in the vocabulary, so both are breaking. https://www.w3.org/ns/ldt is the Linked Data Templates namespace. LDH stopped being an LDT implementation when the API became the Graph Store Protocol, and the template machinery - ldt:Template, ldt:match, ldt:path - has been dead code since. Three terms were still doing ordinary work under the borrowed name, on a host AtomGraph does not own: ldt:ontology, ldt:service and ldt:base. They move to lds:ontology, lds:service and lds:base. ldt.ttl, the vendored LDT Java class, the rdfs:subClassOf ldt:Application on the dataspace class and the two dead sibling namespaces go with them; lacl:requestAccess, the one live use of ns/ldt/core/domain#, now ranges over foaf:Document. dh: is re-hosted rather than reshaped - w3.org/ns/ldt/document-hierarchy# becomes w3id.org/atomgraph/linkeddatahub/document-hierarchy#. Prefix, class names and both DH.java classes are untouched, so only the namespace string moves. lapp.ttl becomes lds.ttl, .../apps# becomes .../dataspaces#, and lapp:Application, lapp:EndUserApplication and lapp:AdminApplication become lds:Dataspace, lds:EndUserDataspace and lds:AdminDataspace with their constructors. lapp:application, the Link relation naming the dataspace a response belongs to, becomes lds:dataspace. The file was never an "applications" vocabulary - it is the deployment-configuration one, holding Dataset and Package alongside the app classes - so the three incoming LDT terms land here too, and config/dataspaces.trig and config/system.trig each need one prefix where they needed two. Three terms used in config and read in Java were never declared; lds:prefix and lds:versioningRepository now are. lds:Context is not, matching ldh:requestUri: it is an XSLT parameter name, not a term any data carries. lapp:frontendProxy is dropped rather than renamed - nothing read it, and the proxy that cache-invalidation BANs use has always come from the ldhc:frontendProxy servlet parameter. Java follows the model: the apps package is dataspaces, LAPP is LDS, and getApplication()/getAdminApplication()/getEndUserApplication() are getDataspace()/getAdminDataspace()/getEndUserDataspace() - 208 call sites that would otherwise return a Dataspace from a method named for an application. The JAX-RS Application class and the three Application*Factory/Filter classes keep their names; renaming the model actually removes the collision that had 169 references written fully qualified. There is no migration. 6.0.0 is unreleased and its migration notes already run long; an existing deployment re-seeds, because every dataspace is typed in the old namespace and every document in the old document-hierarchy one. Verified: 223 platform tests and 109 CLI tests green, including the two that read the real prefix-mapping.ttl and the Link relations. make sef compiles the client SEF, and both SSR entry stylesheets compile under Saxon-HE Java - SaxonJS cannot compile them, since the use-when duals leave ac:langs() undefined for it. 173 RDF and SPARQL files validate with riot and qparse, the five SPIN bodies in lds.ttl included, and linkeddatahub.com's and LTLOD's own stylesheets still compile against the renamed platform tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * The ACL ontology declared a class nobody emits; a property nobody reads is gone. Two slips in opposite directions, found while checking whether lacl: could be retired alongside the LDT namespace. It cannot - it is LDH-owned and the rest of it is live - but the vocabulary and its Java mirror had drifted. lacl:CreatorAuthorization was declared and referenced nowhere. lacl:OwnerAuthorization is what AuthorizationFilter actually mints - LACL.java, createOwnerAuthorization(), its three tests and http-tests/access/owner-authorization.sh all name it - and it was declared nowhere. That reads as a rename that landed in the code and not in the ontology, so the class is renamed rather than added, and its comment now says what the filter does with it: granted in memory to a document's owner with acl:Read, acl:Append and acl:Write, before the ACL query runs, and never stored. lacl:requestAgentGroup goes the other way. AccessRequest copied acl:agentGroup off a submitted authorization onto the stored request, and nothing ever read it back: imports/acl.xsl rebuilds the authorization from lacl:requestAgent, lacl:requestAccessTo, lacl:requestAccessToClass and lacl:requestMode, and has no branch for the group. The property was written into every group request and dropped again at approval. It was also the one term LACL.java declared that lacl.ttl did not, so rather than declare it and keep writing data no reader consumes, the constant, the declaration and the write are removed. Group authorizations are not wired through the UI at all - modal.xsl still carries "TO-DO: support acl:agentGroup" - so completing the round trip is a feature, not a fix, and can add the term back when it lands. Audited both directions afterwards: all nine LACL.java constants resolve in lacl.ttl, every one of them is used, and every term lacl.ttl declares is either used outside it or is a SPIN constraint or constructor attached to lacl:AuthorizationRequest, which is how those four are meant to be reached. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…6.0.2. Web-Client 6.0.2 (AtomGraph/Web-Client#95) drops the rdfs:domain ldt:Application that ac:stylesheet still carried. The class had not been resolvable since Web-Client deleted its bundled ldt.ttl and the prefix-mapping entry for the namespace, and ns.ttl reaches this project from the published jar, so the reference sat in every ontology closure the platform built - inert, since nothing imports the namespace and no fetch is attempted, but there. Both client dependencies move: the classes artifact and the war overlay. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The guide's component list already pointed at com.atomgraph.linkeddatahub.dataspaces.model but still named AdminApplication and EndUserApplication inside it, so every name in the entry was wrong except the one that had been updated. On disk they are AdminDataspace and EndUserDataspace; the heading and the line under them follow. A test carried the same name past the accessor it exercises: testDocumentURINullWithout- AdminApplication asserts on endUserApp.getAdminDataspace() returning null. Nothing refers to the method but its own declaration, and JUnit discovers it by annotation, so the rename is just the name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The drop handler decided one thing: whether the file's extension or type was one of the five triple syntaxes. Anything else was reported as an unsupported RDF syntax, although the server has taken multipart uploads on every document all along - the same RDF/POST body `ldh add file` sends, stored under uploads/ by content hash and described in the document as an nfo:FileDataObject. The otherwise branch now builds that body and sends it; RDF is still matched first, and the upload is the fallback for everything it does not match. The overlay advertises both outcomes as two lanes led by the kit's Tag, since a drag exposes no file names to predict from, and each outcome reports a failure under its own label. The lane rules go into the DropzoneOverlay section of the vendored overlays.css, pushed to the design system alongside. Four Playwright tests drive the drop with a DataTransfer built in the page: the overlay's anatomy, an RDF import checked at the API, a PNG upload checked on the page and by a certificate-authenticated GET of its content-addressed URI, and an untyped file under an unknown extension that still lands as application/octet-stream. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s own graph outgrew it. MAX_CONTENT_LENGTH exists to bound what the Linked Data proxy and the imports pull in from origins nobody controls. ContentLengthLimitFilter's response half was registered on all four outbound clients, and those clients are not split by trust: noCertClient serves both ServiceContext and WebIDFilter's remote WebID dereferencing. So the guard also sat on the path that reads this deployment's own documents, where nothing bounds the write that grows a graph. A document whose graph serialized past the limit could then not be read at all, and could no longer be written either - a PATCH reads the graph, changes it and writes it back, so the read-back was refused too. Measured: 40,540 triples came back from Fuseki as 5,250,315 bytes of RDF/XML against the 5,242,880 limit, over by 0.14%, and every GET answered 413. Raising the limit is not a fix, because input and output are the same number applied to different bytes. The three protocol clients ServiceContext builds for its own service, and the GraphStoreClient the imports write through, opt their responses out per request via ContentLengthLimitFilter.UNLIMITED - the lever ClientStylesheetService already uses for the ~18 MiB composed SEF. Per request rather than per client is what keeps the proxy, the import sources and WebID dereferencing bounded while they share the same Client instances. An oversize response now answers 502 rather than 413: what was too large is the upstream's response, and 413 states that the caller's request body was, which is untrue of a GET carrying none. The streaming half of the same method already threw ResponseContentTooLargeException, so a response with a Content-Length and the same response chunked answered with different statuses; the unit test pinned the 413 and is updated. The inbound half is unchanged: a request body over the limit is still refused, and cannot opt out. Verified with ui-tests/specs/document/large-graph.spec.mjs, which grows a graph to 7.2 MB through appends that each stay under the limit and asserts the browser receives the document as RDF with 200. Confirmed red with the Graph Store registration removed from the running container, green with it restored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
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.