DRAFT (phase 3, after T063): T088, the lens's two seed actions are offered only where a binding answers them (R1Q19 (a)) (plan 034) - #65
Conversation
…swers them (plan 034) R1Q19 (a), openxFactory#656 comment 5850003126. The lens's `draft seed` (a drill row's register seed) and `draft staging seed` (the pick bar) call two routes that only a host's binding answers. A standalone install has no host, so neither control is offered there. `lens.js` stays the web census's one declared `?` row; moving the controls into a view extension that openxFactory contributes is R1Q19 (b), later and outside release 1. "A binding answers it" is read off what the shell already fetches. `/capabilities` carries `views.contributed_routes`, which `serve.build_server()` builds from the same `route_bindings` table its POST dispatch consults. The new `bindingAnswers(capabilities, method, path)` mirrors `RouteBinding.matches` and fails closed on a payload it cannot read. No `serve.py` change, no new field. The staging seed's selection goes with the button. Every selection site (the matrix's checkbox column, the clickable dots, the `picked` mark, the pick bar) is guarded by `ctx.pickDoc`, and the matrix's own test says the column exists "to feed ONE action", so `pickDoc` is null where the staging seed is not offered. The matrix draws no empty gutter then, and the drill note stops naming a seed that is not there. Both controls carry `data-seed-action` for the browser half (T096, AT-R1 step 6) to read. Falsifier, AT-R1 step 6, as far as a unit test can drive it (`tests/test_lens_seed_actions.py`, real `views/lens.js` under node): with the payload a standalone serve publishes the radar draws its documents as dots, the empty-radar text is absent and no seed control is offered, and with a host that contributes the routes both are. The lens's answer is held to `route_extension.match()` across every method, exact and prefix routes. Census: `views/lens.js` loc 1495 -> 1560 and the `?` total; the two `until` lines now name R1Q19 (b). `tests/test_display_facet.py`'s lens render test asks the lens as a host that answers both routes (it reads the pick bar's words). Measured with LANG=C.UTF-8: 2393 passed / 177 skipped on main, 2401 passed / 177 skipped here (the same skips node for node; the 177 are the database-backed cases that CI runs against its service). Arc: neutral-product-standalone-operability Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reviewer's GuideThe PR makes lens seed actions capability-driven: standalone installations hide both seed controls and all staging-selection affordances, while hosts contribute only the actions backed by their route bindings. It implements a fail-closed client-side mirror of server route matching, adds comprehensive Node-backed and consistency tests, and updates the affected display test and census metadata. Sequence diagram for capability-driven lens seed actionssequenceDiagram
participant Shell
participant Lens as lens.js
participant Capabilities as capabilities payload
participant Binding as route binding manifest
Shell->>Lens: renderLens(root, snapshot, opts)
Lens->>Capabilities: read views.contributed_routes
Lens->>Binding: bindingAnswers(caps, POST, seed route)
Binding-->>Lens: answered or false
alt seed route is answered
Lens->>Lens: render corresponding seed control
else route is absent or unreadable
Lens->>Lens: omit seed control
end
Lens-->>Shell: render lens with only supported actions
Flow diagram for standalone lens affordance gatingflowchart TD
A[Read capabilities payload] --> B{POST /actions/dtn-seed answered?}
A --> C{POST /actions/staging-seed answered?}
B -->|yes| D[Offer register seed control]
B -->|no| E[Omit register seed control]
C -->|yes| F[Offer staging seed and selection affordances]
C -->|no| G[Omit staging seed, pick bar, checkboxes, clickable selection]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Malformed capability manifests can still enable seed controls, and the stated T063 prerequisite remains unmet.
Review effort: Balanced
Findings: 1
What changed in this PR
Gates lens seed actions and related selection UI on contributed route capabilities.
Changes:
- Adds fail-closed route capability matching.
- Hides unavailable seed controls and selection UI.
- Adds Node-based coverage and updates census bookkeeping.
| File | Description |
|---|---|
src/opendox/web/views/lens.js |
Gates seed actions and selection controls. |
tests/test_lens_seed_actions.py |
Tests route matching and rendered controls. |
tests/test_display_facet.py |
Supplies seed capabilities to the render test. |
tests/fixtures/web_boundary_census.yaml |
Updates lens census metadata. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…pilot review, round 1)
`bindingAnswers` tested `routes.some(...)`, so a manifest such as
`[validSeedRoute, null]` answered yes, and an entry like
`{method: "POST", pattern: "", is_prefix: true}`, which `RouteBinding` refuses
and which prefix-matches every path, could answer for a route nobody
contributed. `manifestRoutes()` refuses the whole payload for one bad entry;
this now does the same, quietly, because it gates a control and must not throw.
Every entry is first held to what `RouteBinding.__post_init__` accepts: a known
method, a pattern rooted at a slash with no query or fragment, a boolean
`is_prefix`, and a prefix ending in a slash. Any entry that fails leaves every
route unanswered. `route_extension.METHODS` is mirrored as `ROUTE_METHODS`.
Tests: the fail-closed case gains one-bad-entry-beside-a-good-one cases in both
orders, an empty prefix and a missing `is_prefix`, with two controls that must
still answer. A new case holds the lens's notion of a well-formed entry to the
server's own: over 21 candidate entries that sit on both sides of each clause
(including JSON shapes that were never constructed), a list holding one beside
a good seed route answers yes exactly when `RouteBinding` accepts it. The
mutation battery is now 16, each killed (the four well-formedness clauses, the
methods table and the whole-list poisoning are new).
Census: `views/lens.js` loc 1560 -> 1577 and the `?` total.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|




Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Arc: neutral-product-standalone-operability
DRAFT. This PR never goes READY before T063 (the phase-2 checkpoint) has landed and the holder says so. It is authored ahead under Brett's phase-3 draft-ahead word, "Install chain + lens (Recommended)" (openxFactory#656 comment 5901112350, 2026-09-29). Claim: 5901192386.
T088 (plan 034, slice P3-L): the lens's two seed actions
The task, from
specs/034-opendox-standalone-operation/tasks.mdat openxFactorymain91e4685f:spec.md): "#tab-lensrenders the bullseye with the corpus's documents as dots, and not the text 'nothing on the radar'. Neither of its two openxFactory seed actions is offered, since no binding answers them (R1Q19 (a))."5817152735; R1Q19 (a),5850003126. No #1144 line changes (R1Q19 amends none), so there is no batch amendment to carry.What changed
src/opendox/web/views/lens.js, and the census's own bookkeeping for that file. Nothing else insrc/./capabilitiesalready carriesviews.contributed_routes, whichserve.build_server()builds from the veryroute_bindingstable its POST dispatch consults (route_extension.match(self.route_bindings, "POST", path)). The lens reads its answer off the payload the shell has already fetched, so there is no second fetch and no new field, and nothing that can drift from the dispatcher.bindingAnswers(capabilities, method, path)(exported) mirrorsRouteBinding.matches: method, then path, exact or under a prefix, a GET binding answering HEAD too. It fails closed on anything it cannot read (no payload, the probe's fallback, noviews), and on the WHOLE manifest, asmanifestRoutes()does: every entry is first held to whatRouteBinding.__post_init__accepts (a known method, a pattern rooted at a slash with no query or fragment, a booleanis_prefix, a prefix ending in a slash), and one malformed entry leaves every route unanswered (Copilot's round-1 finding, taken in d716ded). UnlikemanifestRoutes()it never throws, because it gates a control.draft seed, a drill row on a set two or more repositories share):ctx.onSeedis null unless the DTN route is answered.draft staging seed, the pick bar):ctx.pickDocis null unless the staging route is answered. Every selection site (the matrix's checkbox column and select-all, the clickable dots, thepickedmark, the pick bar) was already guarded byctx.pickDoc, andtest_the_matrix_selection_is_the_seeds_only_inputsays the column "exists to feed ONE action". So the selection goes with the button, and a standalone lens never says "tick documents to draft from them". The matrix then draws no empty gutter (and its empty-rowcolspanfollows), and the drill note stops naming a seed that is not there.data-seed-actionhook (dtn-seed,staging-seed) for the browser half (T096) to assert absence by, without matching on a label that changes (re-draft).views/lens.jsloc1495 to 1577 and the?class total. The row stays?(the two route literals and theirroute_ownership_exceptionsentry are untouched). The twountillines named "a future ruling"; that ruling has now been made in part, so they name R1Q19 (b).tests/test_display_facet.py's lens render test drove the lens with no capability payload and read the pick bar's words. It now asks the lens as a host that answers both routes (5 added lines plus 1 changed). The standalone lens is the new file's.The falsifier, before and after
tests/test_lens_seed_actions.py(new, 9 cases) drives the REALviews/lens.jsunder node with the/capabilitiespayload a serve publishes, built with the realroute_extension.collect_bindingsandview_extension.view_manifest. Both vocabularies are read, because the two controls live in different ones.Before (the new file against
mainfa8862cc): 7 failed, 1 passed. The AT-R1 step 6 case fails on the behaviour, not on a missing symbol:(The one that passes is the source ratchet that the manifest and the dispatcher name the same table.)
After (this branch, d716ded):
9 passed in 0.43s. Against the round-0lens.js(196e827) the two cases added for Copilot's finding fail (2 failed, 7 passed), so they test what they claim.What each case asserts:
bindingAnswersover the published manifest equalsroute_extension.match()across GET, HEAD and POST and exact and prefix routes.RouteBinding's own: over 21 candidate entries, on both sides of every clause and including JSON shapes that were never constructed, a list holding one beside a good seed route answers yes exactly whenRouteBindingaccepts it.serve.pynames the same table for the manifest and the POST arm (build_server()cannot yet run in a lone checkout, so this is read as text).Mutants killed (each applied to
lens.jsalone, the new file run, then restored): staging gate removed; register gate removed; fail open on a payload with no manifest; method ignored; prefix and exact inverted; register seed asking the staging route; GET no longer answering HEAD; pick column drawn with no selection; drill note always naming the seed;is_prefixboolean check dropped; gate reading the wrong caps object; one bad entry no longer poisoning the list; each of the rooted-pattern, no-query-or-fragment and prefix-ends-in-slash clauses dropped; the methods table widened. 16 of 16 killed, 0 survivors.Measured
LANG=C.UTF-8, the whole suite, in the foreground:main2393 passed, 177 skipped; this branch (d716ded) 2402 passed, 177 skipped. The skip sets are identical node for node (measured at 196e827, +1 case since). The 177 are the database-backed cases (tests_runtime, and others that need a service) that CI runs against its PostgreSQL service. Neighbours:test_web_boundary.py,test_display_facet.py,test_bullseye_widget.py,test_lens_labels_at_scale.pyandtest_view_registry.pypass unchanged apart from the one respell above.Overlap, and what this deliberately does not touch
gh pr diff --name-onlyagainst every open openDox-code draft (T054, openDox's small neutral projection (5.1-5.3) (plan 034) #57 to DRAFT (phase 3, after T063): broker-path credential hardening (follows T080): a minted token keeps a built-in credential's rules (plan 034) #64) before editing. None touchesviews/lens.jsor the census fixture. T054, openDox's small neutral projection (5.1-5.3) (plan 034) #57, T057, openDox's own validator and its input set (7.1, 7.1a, 7.1b, 7.2) (plan 034) #58 and T055, serve and generate standalone (5.5, 4.3 part) (plan 034) #59 changetests/test_display_facet.py, at lines 298 to 1207 only; the one respell here is at about line 1379, so git merges the file cleanly. Noserve.pyedit (it is in T057, openDox's own validator and its input set (7.1, 7.1a, 7.1b, 7.2) (plan 034) #58 and T055, serve and generate standalone (5.5, 4.3 part) (plan 034) #59)..github/workflows/validate.ymlis untouched: its pinned floors are>=and permit the rise of +8 collected, and re-pinning them is a cross-draft hotspot better done once, with the phase's bookkeeping.🤖 Generated with Claude Code
Summary by Sourcery
Gate the lens’s seed actions and their supporting selection UI on the host bindings that answer those actions.
New Features:
Bug Fixes:
Enhancements:
Tests: