Skip to content

Bound the tree children query and CONSTRUCT only what a node renders - #390

Merged
namedgraph merged 2 commits into
developfrom
fix/bounded-tree-children-query
Sep 18, 2026
Merged

namedgraph merged 2 commits into
developfrom
fix/bounded-tree-children-query

Conversation

@namedgraph

Copy link
Copy Markdown
Member

The defect

ldh:tree-children-query emitted an unbounded DESCRIBE ?child, so opening the drawer tree fetched the full description of every child of a container. It fires on every page render, not on a click — ldh:NavigationUpdate walks down to the current document and fetches each level on the way.

Measured against a dataspace of ~158k documents, one entity per named graph:

container children DESCRIBE CONSTRUCT
taxonomies/colors/ 275 202 KB
sets/ 28,356 20.5 MB 6.6 MB
parts/ 64,647 502 18.1 MB

Both large containers exceeded the response limit, so the tree answered 502 on every page beneath them.

The fix

A node renders four things — @rdf:about, a label, rdf:type for the icon, and whether the tree relation is present for the disclosure — so a CONSTRUCT of just those is both correct and several times smaller. Making the size independent of container size needs the bound as well: ldh:tree-page-size (default 1000), which an app stylesheet can raise.

A bound alone would break the descent. ldh:doctree-descend picks its next step out of the rendered children and falls through silently when it is absent, which with a LIMIT it usually would be. The query now takes the document being opened to and includes the child leading to it whatever else the page holds.

Scope

The SKOS trees from the editor/taxonomy package call this same function and inherit the bound with no package change. Verified on a live instance: top concepts and narrower concepts both fetch through the new CONSTRUCT.

The package's own ancestor-path walk is deliberately left alone — it is a SELECT DISTINCT ?parent over a VALUES frontier, already minimal, and a LIMIT there would silently stop the tree opening to the target.

Tests

Three places in ui-tests matched the query by its verb and broke — two of them silently, which is why they are a separate commit:

  • tree-children-failure routes the injected 403 on that pattern; a route matching nothing fails open, so the refusal never happened and every assertion passed against a tree that was never refused anything.
  • concept-tree's counts.down stopped counting, so the DOM is the cache compared 0 against 0.
  • lib/console allowed the 502 the unlimited DESCRIBE produced — now removed rather than updated, since an allowance that outlives its defect is a blind spot over exactly the request this changes.

Two new specs cover the fix, with the negative control that makes the second worth having:

run container expanded target active children
baseline yes yes 1001
LIMIT 0 yes yes 1
LIMIT 0, path branch removed no row no 0

The suite was not run end to end: it requires an instance where nothing is readable by default, and the stack available granted anonymous read across the dataspace, which its preflight rejects by design.

🤖 Generated with Claude Code

namedgraph and others added 2 commits September 18, 2026 13:24
ldh:tree-children-query emitted an unbounded DESCRIBE ?child, so opening the
drawer tree fetched the full description of every child of a container. It fires
on every page render, not on a click: ldh:NavigationUpdate walks down to the
current document and fetches each level on the way.

Measured against the Rebrickable dataspace (~158k documents, one entity per
named graph):

  container            children   DESCRIBE   CONSTRUCT
  taxonomies/colors/        275     202 KB           -
  sets/                  28,356    20.5 MB      6.6 MB
  parts/                 64,647        502     18.1 MB

Both large containers exceeded the platform response limit, so the tree answered
502 on every page beneath them.

A node renders four things - @Rdf:about, a label, rdf:type for the icon, and
whether the tree relation is present for the disclosure - so a CONSTRUCT of just
those is both correct and several times smaller. Size independent of container
size needs the bound as well: ldh:tree-page-size (default 1000) limits the child
page, and an app stylesheet can raise it.

A bound alone would break the descent. ldh:doctree-descend picks the next step
out of the rendered children and falls through silently when it is absent, which
with a LIMIT it usually would be. The query now takes the document being opened
to and includes the child leading to it whatever else the page holds, so the
tree still opens to and highlights the current document.

Verified on that dataspace: sets/ 343 KB (was 20.5 MB), parts/ and inventories/
200, the tree opens to parts/3001/, and the SKOS trees from the editor/taxonomy
package inherit the same bound unchanged - they call this function, and their top
concepts and narrower concepts both fetch through the new CONSTRUCT.

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

Three places matched the children query by its verb, and bounding it broke all
of them - two silently, which is the reason this is its own commit rather than a
line in the last one.

  tree-children-failure  CHILDREN_QUERY routes the injected 403. A route pattern
                         that matches nothing fails open: the refusal never
                         happens and every assertion passes against a tree that
                         was never refused anything.
  concept-tree           counts.down stopped counting, so "the DOM is the cache"
                         compared 0 against 0 and passed asserting nothing.
  lib/console            the allowance for the 502 the unlimited DESCRIBE
                         produced. That 502 is gone, so the entry became a blind
                         spot over exactly the request the fix was about, and is
                         removed rather than updated.

Both patterns now match the projection rather than the verb: CONSTRUCT alone is
not distinctive, where `?child a ?Type` is only ever this query.

Two specs cover what the fix introduced:

  asks for a bounded page of children, and only what a node renders - a LIMIT is
  present and a DESCRIBE is not. Both halves matter: a CONSTRUCT that lost its
  bound would still grow without limit, and a bounded DESCRIBE would still carry
  every triple of every child it returned.

  opens the path even when it falls outside the page of children - rewrites the
  LIMIT to 0 on the wire, so a small fixture container behaves like one holding
  more children than a fetch returns. The bound applies to the inner subselect
  alone, so at zero the page contributes nothing and every row still rendered
  got there through the path branch.

Verified against a 64k-child container, including the negative control that
makes the second spec worth having:

  baseline                         container expanded, target active, 1001 children
  LIMIT 0                          container expanded, target active, 1 child
  LIMIT 0, path branch removed     no container row, target absent, not active

So the spec passes for the reason it claims to, and fails when the branch it is
written about is taken away.

The suite itself was not run end to end here: it wants an instance where nothing
is readable by default, and the stack available to me grants anonymous read
across the dataspace, which its preflight rejects by design.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@namedgraph
namedgraph merged commit 51a32da into develop Sep 18, 2026
2 checks passed
@namedgraph
namedgraph deleted the fix/bounded-tree-children-query branch September 18, 2026 12:40
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