registry: search page #241

Closed
opened 2026-10-09 01:17:24 +00:00 by orion · 10 comments
Collaborator

Goal

A search page for the package registry: a visitor types a query, sees the ranked packages of GET /api/1/search (name, version, description, keywords, capabilities), and can follow each to its package page.

Source

Design

  • The page is a static page of the registry's site (/search/) with a plain <form method="get"> and one small script that calls /api/1/search on the same origin and renders the answer; without the script the page says so and links to the API. No new route is needed (/api already goes to the write service), no server-rendered HTML in the write service.
  • It uses the shell of the docs site and brand/tokens.css once #12 has landed; until then it is blocked, not built twice.
  • The script is an external file (no inline script), escapes every field it renders (descriptions are third-party text) and works under a strict Content-Security-Policy.

Acceptance criteria

  • /search/ renders the results of a query in the registry's shell; empty result and API error states are shown.
  • Every rendered field is escaped as text (test with a description containing markup); no inline script or handler.
  • Result rows link to the package page of #12.
  • Documented in RELEASE.md and the site's page list; generated skills/ and the site build (torb docs site --check) green.

Seed impact

None expected (site generator and static assets; no syntax, native or driver flag).

Size and dependencies

One PR, one or two Conventional Commits. Depends on #18 (the API) and #12 (the shell and package page). Deploying the site is outside the loop.

## Goal A search page for the package registry: a visitor types a query, sees the ranked packages of `GET /api/1/search` (name, version, description, keywords, capabilities), and can follow each to its package page. ## Source - [ROADMAP.md, the package manager](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/ROADMAP.md#L96) - RELEASE.md readiness table, slice 13 "The registry's site: package pages, capabilities, documentation, search", and 7.9 ## Design - The page is a static page of the registry's site (`/search/`) with a plain `<form method="get">` and one small script that calls `/api/1/search` on the same origin and renders the answer; without the script the page says so and links to the API. No new route is needed (`/api` already goes to the write service), no server-rendered HTML in the write service. - It uses the shell of the docs site and `brand/tokens.css` once #12 has landed; until then it is blocked, not built twice. - The script is an external file (no inline script), escapes every field it renders (descriptions are third-party text) and works under a strict Content-Security-Policy. ## Acceptance criteria - [ ] `/search/` renders the results of a query in the registry's shell; empty result and API error states are shown. - [ ] Every rendered field is escaped as text (test with a description containing markup); no inline script or handler. - [ ] Result rows link to the package page of #12. - [ ] Documented in RELEASE.md and the site's page list; generated `skills/` and the site build (`torb docs site --check`) green. ## Seed impact None expected (site generator and static assets; no syntax, native or driver flag). ## Size and dependencies One PR, one or two Conventional Commits. Depends on #18 (the API) and #12 (the shell and package page). Deploying the site is outside the loop.
Author
Collaborator

[loop:torbscript] intake: in scope (author orion, label loop), source ROADMAP.md (registry), not a duplicate. Search page over GET /api/1/search (landed with #18). next: waiting (refine queued, ready buffer full) until 2026-10-10T04:00:00Z

[loop:torbscript] intake: in scope (author orion, label loop), source ROADMAP.md (registry), not a duplicate. Search page over GET /api/1/search (landed with #18). next: waiting (refine queued, ready buffer full) until 2026-10-10T04:00:00Z
Author
Collaborator

[loop:torbscript] intake: unchanged. Blocked on #12 (the site shell and package page), which is itself still in refine; refining #12 first this tick. next: waiting (#12) until 2026-10-10T04:00:00Z

[loop:torbscript] intake: unchanged. Blocked on #12 (the site shell and package page), which is itself still in refine; refining #12 first this tick. next: waiting (#12) until 2026-10-10T04:00:00Z
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261010 until 2026-10-10T12:44Z (#12 is merged, the dependency is gone)

[loop:torbscript] claim refine by session:loop-torbscript-20261010 until 2026-10-10T12:44Z (#12 is merged, the dependency is gone)
Author
Collaborator

[loop:torbscript] refine: ready-to-build spec (main 1724b82; #12 and #18 are merged).

Goal

https://packages.torb.dev/search/ lets a visitor type a query and see the ranked packages of GET /api/1/search (name, version, description, keywords, capabilities), each linking to its package page /packages/<owner>/<name>/.

Implements

ROADMAP.md "the package manager" (registry); docs/design/RELEASE.md 7.9 (search and ranking, the API is built), 7.11 (the pages: "The search page (#241) ... not built"), readiness slice 13. The shell is compiler/src/documentation/site-registry.trb (registryPageHtml, registryScript, registryStylesheet).

Design (as found in main)

  • The shell already supports searchAction: Some("/search/") (a plain <form method="get">), and nginx.conf for packages.torb.dev already has connect-src 'self' and form-action 'self' in the /packages/ policy; / answers 404 there, so /search/ needs a location. /api is routed to the write service by Traefik and is untouched.
  • The page is one static file search/index.html in the registry's storage, written by the write service at every start (overwrite, so a new toolchain's shell reaches it), next to fillPages in tools/registry/src/main.trb/registry.trb. Its HTML is a pure function in a new tools/registry/src/searchpage.trb (or next to packagePageHtml) built with registryPageHtml("Search", body, searchAction: Some("/search/")). No server-rendered results.
  • The script is a new asset assets/registry-search.js (function registrySearchScript() in site-registry.trb, written by torb docs site beside assets/registry.js, texts.set in site.trb). It is a separate file so package pages do not load it; the search page loads registry.js (theme) and registry-search.js. registryPageHtml gets an optional scripts: List<String> = [] parameter for the extra <script src> (no inline script).
  • Script behaviour: reads q and offset from location.search, puts q in the field, calls /api/1/search?q=..&limit=20&offset=.. on the same origin, renders with createElement/textContent only (never innerHTML, never insertAdjacentHTML), sets hrefs only from owner/name validated against ^[a-z0-9][a-z0-9-]*$ as /packages/<owner>/<name>/ (otherwise the row is shown as text without a link). Previous/next are plain links ?q=..&offset=... States: no query (a hint), loading, total 0 ("No package matches"), API error (400 shows the API's message/problems as text; network or 5xx a short sentence with a link to /api/1/search?q=...). A no-script note in the page ("Search needs JavaScript; the API answers at /api/1/search?q=...") is visible by default and hidden by the script via the hidden attribute, no style=.
  • Row content: owner/name (link), version, description, keywords and capabilities as chips (existing .chip rules), dependents count. CSS goes into registryStylesheet by tokens only (the existing tests forbid own colours).
  • nginx: location /search/ { index index.html; try_files $uri $uri/ =404; Cache-Control public, max-age=60; same CSP and nosniff as /packages/ }; the CSP stays strict (script-src 'self').

Acceptance criteria (testable)

  • registrySearchScript() is written as assets/registry-search.js by torb docs site; compiler/tests/site-registry.test.trb asserts it exists, makes exactly one request to /api/1/search, contains no innerHTML/outerHTML/insertAdjacentHTML/document.write/eval, and that every state (empty query, no result, API error, network error) has its text in the script.
  • The search page HTML (test in tools/registry/tests) has the shell, the packages label, a <form method="get" action="/search/"> with a field q, both scripts as <script src>, no inline script, no style=, no handler attribute, and the no-script note naming /api/1/search.
  • The result renderer is covered by a test that runs it on a result whose description, keyword and capability contain <img src=x onerror=alert(1)> and "><script> and shows the output is text (a pure TorbScript twin foundRow(...) used to generate a documented fixture is not required; if the script cannot be run in the test runner, the criterion is the source assertions above plus the manual check recorded in the PR: the page loaded in a browser with the markup description from a staging registry).
  • Result rows link to /packages/<owner>/<name>/; a name that does not match the name rule is not linked.
  • The write service writes search/index.html at start (registry test on a temp storage: absent, then present; present with other content, overwritten).
  • tools/deploy/nginx.conf has the /search/ location with the same CSP; the existing nginx config check in CI stays green.
  • docs/design/RELEASE.md 7.11 no longer says the search page is not built and names /search/; tools/registry/README.md lists the page; torb docs site --check, the regenerated skills/ and the full CI gates are green.

Seed impact

None. No syntax, no native, no driver flag, no std name looked up by string; the code uses the std the registry already uses (std/storage) and string constants. The two-commit rule is not triggered.

Size

One PR, two Conventional Commits: (1) feat(site): the registry's search script and styles (script, CSS, scripts parameter, site.trb asset, compiler tests); (2) feat(registry): the search page at /search/ (page function, write at start, nginx location, tests, docs). Deploying is outside the loop.

Out of scope

The search field in the header of package pages (existing package pages are only written when missing, so adding it needs a rewrite of every page): follow-up issue filed, depends on this one. Ranking on package checks (RELEASE.md 7.9, waits for the checks).

[loop:torbscript] refine: ready-to-build spec (main 1724b82; #12 and #18 are merged). ## Goal `https://packages.torb.dev/search/` lets a visitor type a query and see the ranked packages of `GET /api/1/search` (name, version, description, keywords, capabilities), each linking to its package page `/packages/<owner>/<name>/`. ## Implements ROADMAP.md "the package manager" (registry); `docs/design/RELEASE.md` 7.9 (search and ranking, the API is built), 7.11 (the pages: "The search page (#241) ... not built"), readiness slice 13. The shell is `compiler/src/documentation/site-registry.trb` (`registryPageHtml`, `registryScript`, `registryStylesheet`). ## Design (as found in main) - The shell already supports `searchAction: Some("/search/")` (a plain `<form method="get">`), and `nginx.conf` for packages.torb.dev already has `connect-src 'self'` and `form-action 'self'` in the `/packages/` policy; `/` answers 404 there, so `/search/` needs a location. `/api` is routed to the write service by Traefik and is untouched. - The page is one static file `search/index.html` in the registry's storage, written by the write service at every start (overwrite, so a new toolchain's shell reaches it), next to `fillPages` in `tools/registry/src/main.trb`/`registry.trb`. Its HTML is a pure function in a new `tools/registry/src/searchpage.trb` (or next to `packagePageHtml`) built with `registryPageHtml("Search", body, searchAction: Some("/search/"))`. No server-rendered results. - The script is a new asset `assets/registry-search.js` (function `registrySearchScript()` in `site-registry.trb`, written by `torb docs site` beside `assets/registry.js`, `texts.set` in `site.trb`). It is a separate file so package pages do not load it; the search page loads `registry.js` (theme) and `registry-search.js`. `registryPageHtml` gets an optional `scripts: List<String> = []` parameter for the extra `<script src>` (no inline script). - Script behaviour: reads `q` and `offset` from `location.search`, puts `q` in the field, calls `/api/1/search?q=..&limit=20&offset=..` on the same origin, renders with `createElement`/`textContent` only (never `innerHTML`, never `insertAdjacentHTML`), sets hrefs only from `owner`/`name` validated against `^[a-z0-9][a-z0-9-]*$` as `/packages/<owner>/<name>/` (otherwise the row is shown as text without a link). Previous/next are plain links `?q=..&offset=..`. States: no query (a hint), loading, `total` 0 ("No package matches"), API error (400 shows the API's `message`/`problems` as text; network or 5xx a short sentence with a link to `/api/1/search?q=...`). A `no-script` note in the page ("Search needs JavaScript; the API answers at /api/1/search?q=...") is visible by default and hidden by the script via the `hidden` attribute, no `style=`. - Row content: `owner/name` (link), version, description, keywords and capabilities as chips (existing `.chip` rules), dependents count. CSS goes into `registryStylesheet` by tokens only (the existing tests forbid own colours). - nginx: `location /search/ { index index.html; try_files $uri $uri/ =404; Cache-Control public, max-age=60; same CSP and nosniff as /packages/ }`; the CSP stays strict (`script-src 'self'`). ## Acceptance criteria (testable) - [ ] `registrySearchScript()` is written as `assets/registry-search.js` by `torb docs site`; `compiler/tests/site-registry.test.trb` asserts it exists, makes exactly one request to `/api/1/search`, contains no `innerHTML`/`outerHTML`/`insertAdjacentHTML`/`document.write`/`eval`, and that every state (empty query, no result, API error, network error) has its text in the script. - [ ] The search page HTML (test in `tools/registry/tests`) has the shell, the `packages` label, a `<form method="get" action="/search/">` with a field `q`, both scripts as `<script src>`, no inline script, no `style=`, no handler attribute, and the no-script note naming `/api/1/search`. - [ ] The result renderer is covered by a test that runs it on a result whose description, keyword and capability contain `<img src=x onerror=alert(1)>` and `"><script>` and shows the output is text (a pure TorbScript twin `foundRow(...)` used to generate a documented fixture is not required; if the script cannot be run in the test runner, the criterion is the source assertions above plus the manual check recorded in the PR: the page loaded in a browser with the markup description from a staging registry). - [ ] Result rows link to `/packages/<owner>/<name>/`; a name that does not match the name rule is not linked. - [ ] The write service writes `search/index.html` at start (registry test on a temp storage: absent, then present; present with other content, overwritten). - [ ] `tools/deploy/nginx.conf` has the `/search/` location with the same CSP; the existing nginx config check in CI stays green. - [ ] `docs/design/RELEASE.md` 7.11 no longer says the search page is not built and names `/search/`; `tools/registry/README.md` lists the page; `torb docs site --check`, the regenerated `skills/` and the full CI gates are green. ## Seed impact None. No syntax, no native, no driver flag, no std name looked up by string; the code uses the std the registry already uses (`std/storage`) and string constants. The two-commit rule is not triggered. ## Size One PR, two Conventional Commits: (1) `feat(site): the registry's search script and styles` (script, CSS, `scripts` parameter, site.trb asset, compiler tests); (2) `feat(registry): the search page at /search/` (page function, write at start, nginx location, tests, docs). Deploying is outside the loop. ## Out of scope The search field in the header of package pages (existing package pages are only written when missing, so adding it needs a rewrite of every page): follow-up issue filed, depends on this one. Ranking on package checks (RELEASE.md 7.9, waits for the checks).
Author
Collaborator

[loop:torbscript] decided by the loop: (1) the search script is its own asset assets/registry-search.js, not part of registry.js, so package pages do not download code they never run; (2) the page is a file the write service writes at every start into the storage (like the package pages, overwritten so a toolchain update reaches it) and not a page of torb docs site, because the docs site is served at torb.dev and a registry page there would be a second, wrong URL; the registry host's nginx serves storage, the assets come from the image as before; (3) results are rendered with textContent only and package links are built from names that match the name rule, so third-party text never becomes markup; (4) the header search field on package pages is split off (follow-up) because existing pages are not rewritten at start.

[loop:torbscript] decided by the loop: (1) the search script is its own asset `assets/registry-search.js`, not part of `registry.js`, so package pages do not download code they never run; (2) the page is a file the write service writes at every start into the storage (like the package pages, overwritten so a toolchain update reaches it) and not a page of `torb docs site`, because the docs site is served at torb.dev and a registry page there would be a second, wrong URL; the registry host's nginx serves storage, the assets come from the image as before; (3) results are rendered with `textContent` only and package links are built from names that match the name rule, so third-party text never becomes markup; (4) the header search field on package pages is split off (follow-up) because existing pages are not rewritten at start.
Author
Collaborator

[loop:torbscript] refine: spec, criteria, design points and seed impact (none) recorded above, one PR of two commits; split-off follow-up #306 (search field in package page header, depends on #241), next: ready

[loop:torbscript] refine: spec, criteria, design points and seed impact (none) recorded above, one PR of two commits; split-off follow-up #306 (search field in package page header, depends on #241), next: ready
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261010 until 2026-10-10T16:52Z

[loop:torbscript] claim implement by session:loop-torbscript-20261010 until 2026-10-10T16:52Z
Author
Collaborator

[loop:torbscript] implement: PR #308 (head 95c0f460, CI pending: pdisc fixed and green, bootstrap green, tier/gates jobs still queued on the two torb runners after 58 min, argus requested), est. tokens 120k, next: waiting (review by argus)

[loop:torbscript] implement: PR https://git.torb.dev/torbscript/language/pulls/308 (head 95c0f460, CI pending: pdisc fixed and green, bootstrap green, tier/gates jobs still queued on the two torb runners after 58 min, argus requested), est. tokens 120k, next: waiting (review by argus)
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261010 until 2026-10-10T15:30Z

[loop:torbscript] claim implement by session:loop-torbscript-20261010 until 2026-10-10T15:30Z
Author
Collaborator

[loop:torbscript] implement: PR #308 (head 14d394d8, rebased onto main after #307, CI pending, argus requested), est. tokens 60000, next: waiting (review by argus)

[loop:torbscript] implement: PR https://git.torb.dev/torbscript/language/pulls/308 (head 14d394d8, rebased onto main after #307, CI pending, argus requested), est. tokens 60000, next: waiting (review by argus)
argus closed this issue 2026-10-10 18:40:12 +00:00
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#241
No description provided.