Skip to content

Develop - #406

Draft
namedgraph wants to merge 6 commits into
masterfrom
develop
Draft

namedgraph wants to merge 6 commits into
masterfrom
develop

Conversation

@namedgraph

Copy link
Copy Markdown
Member

No description provided.

namedgraph and others added 3 commits October 5, 2026 00:15
The cli job built cli/ alone, so Maven resolved linkeddatahub-rdf at the release version from
Central. release.sh deploys the library only after pushing the tag, and Central publishes it
minutes later, while the job starts seconds after the push: it failed for 6.0.1 and 6.1.0, and
since it is the job that creates the GitHub release, neither got one. 85fde19 taught the
three test workflows to install rdf/ from the checkout first and missed this one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
namedgraph and others added 3 commits October 5, 2026 23:04
…t block of its own (#407)

* An assistant drawer: a question becomes a Web-Algebra plan, shown before it runs

The drawer on the right edge of every page for a signed-in reader sends a question
in natural language to the web-algebra sidecar's plan service, with the document the
reader is on, its dataspace's SPARQL endpoint and the earlier turns. The plan comes
back as its summary, the operations it invokes and the XML itself, and runs only on
Execute (or at once with "Execute by default"); the steps report as the executor
does, the result set or the documents written are shown, and the page catches up.

Each card keeps its plan and, once run, what it returned, so a follow-up that refers
to "them" is planned on that result as a VALUES block rather than on a new query. A
Clear button empties the log and both stores. LDH's operation family is addressed as
`waldh:` elements. The stack gains the web-algebra service and nginx's reserved
/webalgebra path with the caller's certificate; OPENAI_MODEL picks the planner's
model. The chat section of ldh.css parks the core Drawer under the sticky header.

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

* Serve the agent guide at /AGENTS.md, and say in it that the default graph is empty

A client writing a query against a dataspace's endpoint has to know that every
document is a named graph and a pattern outside GRAPH matches nothing; the
assistant's SPARQLString reads an endpoint's AGENTS.md as service documentation and
was answering "no results" for want of it. The repository's guide is shipped in the
WAR from where it lives, mapped to the default servlet beside robots.txt and
sitemap.xml so it is public and never enters the dispatcher, served as
text/markdown, and gains the named-graph paragraph in its Querying section.

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

* The assistant is part of the dataspace's panel, and its rows name what they return

The drawer moves inside #tab-body, beside the panes and under the dataspace tab bar:
the header and the tab bar keep the full width, the drawer starts where the panel
does, and its head takes the action bar's height so the two seams are one line. An
open drawer insets the panes and the footer rather than the frame. The create dock's
full-bleed margin and padding, computed on the viewport, slid its buttons under the
drawer; they now share --ldh-pane-w, the panes' width, which the open drawer narrows.

A result set's rows get their labels: the distinct URIs are looked up with the two
metadata queries a view block runs, against the endpoint the plan queried, and the
rows are drawn again with the metadata tunnelled to the link leaf when the labels
land. The UI spec measures the panes and the seam, and expects the written container
by its title.

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

* The model key is an optional secret, declared in the override like the OAuth ones

The base compose file no longer binds secrets/openai_api_key.txt, so a stack without a
key - the test workflows, a deployment that only runs plans - comes up; a deployment
that writes plans declares the openai_api_key secret and OPENAI_API_KEY_FILE in its
override, which is where the dev stack now has them.

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

* The assistant hands the planner the dataspace's ontology, and the agent guide says a join crosses documents

The dataspace's ontology, which every document advertises with Link rel=lds:ontology, is the
vocabulary its domain data is described in: labels, domains and ranges the planner's induced
schema lacks, and none of the documents' plumbing the induced one picks up. The client now keeps
the Link target the way it keeps the endpoint, lds:ontology() reads it, and the question goes to
the plan service with it, so a SPARQLString against this dataspace is written from GET of the
ontology rather than from ExtractOntology (REST-VKG #26).

The guide the query writer reads from /AGENTS.md gains the two rules the Northwind evaluation
showed the model breaking: a resource is described in the document about it, so a pattern that
follows a link to another resource needs a GRAPH of its own for each; and a property is used on
instances of its domain, so what the ontology declares on Person is reached through the
corporation's employee, not written on the corporation.

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

* An ASK is answered with a boolean, which the assistant's query check depends on

The SPARQL endpoint answered an ASK with an empty result set, so the Web-Algebra SPARQLString
check - an ASK with the query's own pattern, to send a query that matches nothing back to the
model - never bit against this platform. The boolean result is Core 5.0.6: SPARQLResult, its
provider, and the ASK path of SPARQLEndpointImpl. Core is pinned at 5.0.6-SNAPSHOT ahead of
Web-Client's transitive 5.0.5, and SPARQLResultProvider is registered beside ResultSetProvider
on the server and on both clients. The pin resolves from a local install only until Core is
released, so an image build needs that release, or the snapshot deployed, first.

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

* A plan the service refused reports as a failure in the drawer, with the service's reason

An envelope holding a message instead of a plan was shown as a reply whatever the status, which
read a validator's refusal - a parser's line and column - as the assistant declining in words. A
message on a 200 is still a reply; on any other status it is the service's reason for failing, and
the card now reports it as the failure it is, through the chain's own failure path, with that
reason as the detail rather than the status line.

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

* A plan that stopped folds out the XML of the steps it did enter

A step row borrowed its operation's XML from the plan only when the executor's steps and the
plan's operations matched one to one, so a plan that failed at its tenth operation of nineteen
rendered every row without a fold-out, and so did one still running - only a plan that ran to the
end lined up. The steps are now paired with the plan's leading operations, as many as were
entered, in the order the executor enters them; a ForEach's iterations are more steps than
operations and still get none.

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

* The assistant is a block in the content body, with its composer docked onto the create bar

The assistant produces blocks, and the drawer kept it where blocks are not: a plan wrote into the
document, then the whole page reloaded to catch up, in a panel beside the content it changed. The
placement rule already said where it belongs - a form for what is added to the current document
renders inline - so it moves there. The conversation is an ephemeral block at the end of the
content body, before the create bar, so what a step writes lands above it in order; the composer
is a panel docked onto the bar's upper edge, opened by the bar's own button beside Create and
closed by it or by Escape. A page with no bar - a proxied resource, a search result - gets a bar
holding the composer alone, so reads work everywhere.

None of it is server-rendered but the button: a conversation exists only once the client runs,
and the body it sits in is replaced on every navigation and every write's catch-up. So the
conversation is not the DOM: each document's log is an element kept under the document's URI, the
one composer form is kept for the session, and ldh:ChatMount moves them into whatever body was
just rendered. A card is painted into its log whether the log is in the page or not, so a plan
still running when the body goes finishes into its log and is there when the document is next
shown; a card that reported a write comes through the catch-up with its steps, its documents and
its fold-outs. Conversations are per document, and the history the planner hears is that
document's.

The drawer, its edge sensor and its CSS go; the mount is guarded on the WebID itself rather than
the agent's document, which document() only has once something loaded it. The assistant spec
covers the new surface: opened from the bar and docked above it, a card in the body, Clear from
the block's head, and the card surviving the catch-up.

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

* The composer is as wide as the content column, and starts where it starts

Docked onto the create bar, the composer took the bar's full width: the bar is full-bleed on the
left and padded on the right to the column's edge, so the composer ran from the viewport's edge to
the Create button while the content it serves sits in a centred column. Its left margin now makes
up the column's inset over the bar's own padding, and its width is the column's inside its padding;
margin plus width fill the bar's line exactly, which is what wraps the buttons below it. The spec
measures it against the column.

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

* The assistant is a component of the app kit: ldh-chat- classes, the kit's block header, its skin in app.css

The assistant had no place in the design system - no component, no card, no rule - so its markup
could only be read off the XSL. It is one now (ui_kits/app/Assistant.jsx, preview/component-assistant.html),
and the product uses the kit's vocabulary: the chat-* classes become ldh-chat-*, the block's head is
THE block header at compact density with Clear in its actions cluster, and the rules that are the
component's look - the create bar's, the block's, the turns', the plan card's, the step rows', the
composer's - move from ldh.css into the vendored app.css, pushed upstream in the same change.
What stays in ldh.css is the page's geometry: where the bar parks and how wide the docked
composer is, which only this page's column and pane widths can say.

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

* The card answers in words, with the trace folded under it

A plan's card showed its rows and its step tree, which is the machinery; the answer the reader
asked for was theirs to read off it. Once a plan ends, and once its rows have their labels, the
card asks the answer service beside the plan service for a reading - the question, the plan's
summary, the steps, the documents written and the labelled rows go, a sentence or two comes back
- and that sentence becomes the card's first line. The steps now live under a trace disclosure
whose summary is their count and the plan's time: open while the plan is being read and run,
since the rows are what there is to see, folded once there is an answer to read instead. The rows
stay on the card and stay the authority; a service that cannot answer leaves the card as it was.

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

* A result is a block in the card, drawn by the plan's hint, and Keep makes it the document's

A plan's result rendered as a bare table whatever it was. It now sits in a well of its own under
the card, headed like a block with the mode's label, and drawn through the document's own block
rendering by the plan's presentation hint: a result set as the results table or, when the plan
said chart, as a chart drawn into the card with the hint's axes; a graph as the view block's list,
grid or table. What the reader sees in the card is what a block in the document would show.

Keep, in the well's actions, writes it there: a plan of the card's own making - the query stored
on the document, the endpoint registered as a service first when it is not the dataspace's own, a
view in the mode drawn or a chart with the axes drawn, and an object block placing it after the
document's last block - run at once as its own card. A document the assistant wrote to comes back
in ContentMode, which is the mode that shows its blocks, so the kept block appears above the
conversation. The fragments carry their '#', since the waldh: operations resolve <fragment>
against <url> as a relative URI.

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

* A write's report is not a result, and the folded trace shows it opens and how it went

Every write answers a one-row ?status ?url result set, and the card drew each one as a result
well with Keep, sent them to the answer pass as the rows - which then said no value was returned -
and kept them as the next question's "them". They are the documents written, which the card
already lists, so they are left out of all three, and the list counts each document once.

The trace folded under a summary that read as a caption: no chevron, no hover, and the green rows
seemed to vanish. The summary now has a chevron that turns when open and the outcome's glyph, a
green check or a red error, so the folded trace still says how the steps went.

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

* Keep goes: a result reaches the document by asking for it

Keep pinned a result into the document as a view or chart built from the card's query. It
chose the block by the card's rendering, and turned "what's the current year" into a view over
SELECT (YEAR(NOW()) AS ?currentYear), which a view - a list of resources - cannot show, and whose
block then failed to render at all. With chats about to be stored in the document, every
result persists anyway, and putting one into the document's flow is a request in words that
the planner writes as blocks. The button, its write plan and its translations are removed; the
result wells stay.

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

* A view lists what an aliased first variable binds, and says when it binds no resources

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

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

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

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

* A conversation is a block: each chat has its own composer, is stored turn by turn, and shows its plans resolved

The assistant moves from one ephemeral, client-kept log docked on the create bar to an ldh:Chat
resource placed by an ldh:Object block, like a view or a chart. A page holds as many chats as it has
chat blocks, each ending in its own composer; the bar's Assistant button starts a new one before the
bar, which is written - chat, object block and the document's next rdf:_N, in one conditional POST -
on its first question, so a click that asks nothing leaves nothing behind.

Every turn is an ldh:ChatTurn, the chat's next rdf:_N, written to the chat's own document once its
answer lands: the question, the answer, the plan and what the execution reported (result capped at
100), as XMLLiterals, and the outcome. The write catch-up waits for it, and the chat block's RowHook
draws every turn from the store, so a chat survives leaving the page, can be embedded, and continues
with its stored history. A reader without acl:Append sees the transcript and no composer.

The literals travel escaped, typed rdf:XMLLiteral: Jena's RDF/XML parser refuses an rdf:RDF inside
rdf:parseType="Literal", and a turn whose plan read a graph answered 400.

The trace shows the plan as it ran: a step that reported a value at run time (the query a
SPARQLString wrote, as wa:value) folds it out, and the operation around it shows the call replaced by
that value.

ldh.ttl and the rdf/ vocabulary gain ldh:Chat, ldh:ChatTurn and the turn's properties; the global
mount, the session composer and Clear are gone. tests/ui/specs/shell/assistant.spec.mjs is rewritten
around chat blocks (8 specs, 5 without a model).

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

* The assistant reads how to show a result as the client's mode URIs, and draws a grid and a timeline

A plan's <wa:present> now names the layout mode as ac:mode - ac:TableMode, ac:ListMode, ac:GridMode,
ac:ChartMode, the values a view block's ac:mode takes - and a chart's details as the properties a
chart block has: ldh:chartType, ldh:categoryVarName, ldh:seriesVarName. ldh:present-mode and
ldh:present-chart-type read them, and still read the table/list/grid/chart tokens and the
type/category/series attributes of turns stored before, so those draw as they did. A result well
carries its mode's URI in data-mode.

Specs: a stubbed grid plan whose graph depicts its resources draws one card per resource with its
image, live and again from the store; a stubbed timeline plan draws one labelled span per row, the
one whose end came from COALESCE included. Both run without a model.

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

* An operation's XML fills its code field

The trace folds out each operation's XML in the kit's code field, which is a flex row built for the
kit's own text area, the one child that grows. The trace puts a plain pre there, which nothing told
to grow, so a plan of short lines showed its XML in a box as wide as its longest line inside a
field as wide as the card. The pre now takes flex: 1 and min-width: 0, as the text area does; long
lines still scroll inside it.

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

* The assistant spec passes the admin base positionally, as ldh takes it since #404

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

* Core 5.0.6 arrives through Web-Client 6.0.4, so the snapshot pin and the overlay exclude go

The boolean result of an ASK, which the assistant checks its queries with, shipped in Core 5.0.6 and
Web-Client 6.0.4 depends on it. The direct core:5.0.6-SNAPSHOT dependency, which only resolved from a
local repository, and the overlay's exclude of its own Core jar are no longer needed; the
SPARQLResultProvider registrations compile against the released Core.

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

* A bar chart's value axis starts at zero, a chart's Save hides with its controls, and a maximised graph fills the viewport

- chart.xsl: a bar's length is its value, so the bar chart's value axis takes minValue 0 (ignored when
  the data goes below it)
- app.css: the chart's Save stores what its controls picked, so its footer is hidden while the
  controls are collapsed
- ldh.css: a maximised 3D graph canvas is pinned with inset: 0 and auto size, rather than 100% of a
  containing block that is not the viewport

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…wherever it is rendered

A chart, a map and a 3D graph render into a canvas a library draws on, and each place that rendered
one started it by hand: a document in its mode (client.xsl), an object block in its mode (object.xsl,
whose copy said "TO-DO: reuse similar initialization code from client.xsl"), a view switched to it
(view.xsl), a chart block's three control handlers and its results response (chart.xsl), the query
block's chart (query.xsl) and the assistant's (chat.xsl).

Now the canvas element is the thing dispatched on: ldh:InitCanvas, one template per canvas class.
- chart-canvas (chart.xsl): the data table from a graph or a result set, by templates on rdf:RDF and
  srx:sparql (ldh:ChartDataTable), the chart type, category and series the caller gives or a table of
  every property or variable (ldh:ChartSeries), the data table kept on the caller's cache.
- map-canvas (map.xsl): the OpenLayers map kept on the cache, created the first time and moved onto
  the canvas after; features from the view's query when there is one, else from the graph.
- graph-3d-canvas (graph3d.xsl): the graph started the first time and fed new results after, so a
  view coming back to it redraws the same graph; graph-results lets an object block show its whole
  document. A canvas emitted just now falls back to 600px, having no height yet.

Callers apply the mode to the canvases they rendered, with the results, the cache and what else the
canvas needs as tunnel parameters. The chart block's three onchange handlers are one; the view's
three mode branches are one call.

Specs: view/modes.spec switches a view to chart, map and graph (and away from the graph and back);
object.spec draws an object block in each canvas mode.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…el when there is none

The coverage inventory names the create bar's Assistant button instead of the retired chat
drawer. The assistant's model-backed tests skip when the plan service answers 503, its reply
when no model key is configured, as on CI; any other failure still fails them.

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant