clone

a personal forge on plain git: static pages, stacked-diff reviews, signed local ci, repo sites and releases

Read code search queries with a parser of its own module

cat committed 1e513e3
Code search's query parsing (words split on whitespace, a quoted phrase
as one literal string, a word inside another dropped) moves out of
codesearch.ts into site/assets/query.ts, so the other search sections
can share it next instead of keeping their own copy in search.ts.

Site scripts had no imports, since a build ships each under a hashed
name. A build now rewrites a script's `from './x.ts'` import to the other
file's hashed name, so the importer's name changes with what it imports.
Code search behaves and fetches exactly as before (test/code-search-bench.ts
gives the same rounds, requests and bytes); code-search-index checks that
the shipped worker imports query.js by the name it is shipped under.
commit
1e513e382442f82210b53622d27fbd21477cc8ee
parent
d6847cf986c1e0201935d18ac3579379418e2a3a
author
cat <admin@dev.site>
ci
✓ passed in 80s, signed by cat
kind
refactor risk score 1.3 of 3, Jev's estimate
review
#3 Match quoted phrases in every search section
ci attestation
forge-attestation 1
type ci
commit 1e513e382442f82210b53622d27fbd21477cc8ee
tree df65082aa43a606f69b5698bfc94f794eef41d93
script .forge/ci e6f038ae810b87f90a4be115a762a9c91dd76a01
result pass
exit 0
started 2026-10-18T22:47:28Z
duration 80s
runner (not shown here; refs/notes/ci has it)
system Darwin 25.5.0 arm64
env host
log sha256:05dbb4473ae8cffae445eeab656c050ccceecb8f6092373cfb6df9748718896c 239 lines
|remote: review #4: /dami/greet/reviews/4/
|pushed wip/i18n
|e931f50 running .forge/ci: greet.sh: say what it is
|  | sh: /bin/sh
|  | ok   greet
|e931f50 pass in 0s, signed
|pushed main
|remote: review #4: /dami/greet/reviews/4/
|pushed wip/i18n
|faed5a6 running .forge/ci: wave at the world
|  | sh: /bin/sh
|  | FAIL greet : got 'hello, world o/', want 'hello, world'
|faed5a6 fail in 0s, signed
|forge: 1 commit(s) did not pass
|remote: review #6: /dami/greet/reviews/6/
|pushed ana/wave
|9f42789 running .forge/ci: update the test for the wave
|  | sh: /bin/sh
|  | ok   greet
|9f42789 pass in 0s, signed
|remote: review #6: /dami/greet/reviews/6/
|pushed ana/wave
|warning: You appear to have cloned an empty repository.
|site home: deployed 681385c -> /sites/ana/dotfiles/home/
|created ana/notes
|remote: review #1: /ana/notes/reviews/1/
|pushed ana/todo
|landing #1 A todo list: ana/todo -> main
|dba63e8 running .forge/ci: web, server and Python versions of greet
|  | sh: /bin/sh
|  | ok   greet
|dba63e8 pass in 0s, signed
|pushed main
|3199782 running .forge/ci: messages: notes for the later regions
|  | sh: /bin/sh
|  | ok   greet
|3199782 pass in 0s, signed
|remote: review #7: /dami/greet/reviews/7/
|pushed dami/regions
|created dami/forge
|warning: You appear to have cloned an empty repository.
|site guide: deployed b4c64cf -> /sites/dami/forge/guide/
|(node:82954) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built ana/dotfiles in 0s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www
|(node:83136) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built ana/notes in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www
|(node:83287) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built dami/forge in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www
|(node:83436) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built dami/greet in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www
|(node:84138) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built - in 0s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www
|226 of 226 pages render the same without 11ty
|guide: 4 files, links ok
|ok

Signature: good, cat

4 files changed, +48 -14
Mlib/assets.ts26++++++++++++++++++++------
Msite/assets/codesearch.ts16++++++++--------
Asite/assets/query.ts16++++++++++++++++
Mtest/scenarios/code-search-index.sh4++++
M lib/assets.ts +20 -6
@@ -42,7 +42,7 @@ export interface Asset {
4242 data: Buffer;4343 }4444 45-// By short name: style, logo, fixi, ssexi, diffs, keys, live, search,45+// By short name: style, logo, fixi, ssexi, diffs, keys, live, search, query,4646 // codesearch, login, ui, mermaid, sqlite3, sqlite3wasm, webauthn.4747 export type Assets = Record<string, Asset>;4848
@@ -56,17 +56,31 @@ export function assets(): Assets {
5656 // Rebuilt only when a source changes (the dev server rebuilds in one process).5757 const key = files.map(([, , f]) => `${f} ${fs.statSync(f).mtimeMs}`).join('\n');5858 if (memo?.key === key) return memo.assets;59- const out: Assets = {};60- for (const [short, base, f] of files) {59+ // Our scripts import each other as './query.ts', which Node and tsc read60+ // as it is; shipped, that is the other file's hashed name, so a script's61+ // name changes with what it imports.62+ const made = new Map<string, Asset>();63+ const make = ([short, base, f]: [string, string, string]): Asset => {64+ const done = made.get(short);65+ if (done) return done;6166 let data = fs.readFileSync(f);6267 let ext = path.extname(base);6368 const name = base.split('.')[0];6469 if (ext === '.ts') {65- data = Buffer.from(stripTypeScriptTypes(data.toString('utf8')));70+ const js = stripTypeScriptTypes(data.toString('utf8')).replace(/(\bfrom\s*)'\.\/([\w-]+\.ts)'/g, (_, from: string, dep: string) => {71+ const d = files.find(([, b, p]) => b === dep && path.dirname(p) === src);72+ if (!d) throw new Error(`${base} imports ./${dep}, which is not in site/assets`);73+ return `${from}'./${make(d).file}'`;74+ });75+ data = Buffer.from(js);6676 ext = '.js';6777 }68- out[short] = { file: `${name}.${hash(data)}${ext}`, data };69- }78+ const a = { file: `${name}.${hash(data)}${ext}`, data };79+ made.set(short, a);80+ return a;81+ };82+ const out: Assets = {};83+ for (const f of files) out[f[0]] = make(f);7084 memo = { key, assets: out };7185 return out;7286 }
M site/assets/codesearch.ts +8 -8
@@ -12,6 +12,8 @@
1212 // the index leaf of each term (from FTS5's small %_idx table), then the page1313 // of each matching chunk (from the chunk table's root page).1414 15+import { narrow, terms } from './query.ts';16+1517 // Bytes [from, to) of url (from < 0: the last -from bytes), or the whole1618 // file if the server ignores ranges; where they start, the file's size, and1719 // its validator, which must not change while it is open.
@@ -423,14 +425,12 @@ export function match(q: string): string {
423425 return n <= 5 ? str(q) : `NEAR(${cover(q).map(str).join(' ')}, ${n - 4})`;424426 }425427 426-// A query's words, lower case, as the other search sections split theirs:427-// on whitespace, except that a quoted phrase ("like this") is one literal428-// string, spaces and all (an unclosed quote runs to the end). A word that429-// is part of another goes, since every line with the longer one has it.430-export function words(q: string): string[] {431- const all = [...q.toLowerCase().replaceAll('\0', '').matchAll(/"([^"]*)"?|(\S+)/g)].map((m) => m[1] ?? m[2]).filter((w) => w.trim());432- return [...new Set(all)].filter((w, i, ws) => !ws.some((o, j) => j !== i && o.length > w.length && o.includes(w)));433-}428+// A query's words, lower case, its terms as query.ts reads them: split on429+// whitespace, except that a quoted phrase ("like this") is one literal430+// string, spaces and all (an unclosed quote runs to the end).431+// A word that is part of another goes, since every line with the longer one432+// has it, and so do NULs, which an FTS5 string can't hold.433+export const words = (q: string): string[] => narrow(terms(q.replaceAll('\0', '')));434434 435435 // The words the trigram index can look up; shorter ones only filter lines.436436 export const indexed = (ws: string[]): string[] => ws.filter((w) => [...w].length >= MIN);
A site/assets/query.ts +16 -0
M test/scenarios/code-search-index.sh +4 -0