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
| M | lib/ | 26 | ++++++++++++++++++++------ |
| M | site/ | 16 | ++++++++-------- |
| A | site/ | 16 | ++++++++++++++++ |
| M | test/ | 4 | ++++ |
@@ -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 }@@ -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);@@ -0,0 +1,16 @@
1+// A search query's terms, as codesearch.ts reads them for the lines of2+// files: split on whitespace, except that a quoted phrase ("like this") is3+// one term, spaces and all, which must appear as written. A quote opens a4+// phrase only at the start of a term (in `it's"` or `a"b` it is just a5+// character), an unclosed one runs to the end of the query, and empty quotes6+// are no term. Every term must match, ignoring case.7+8+// A query's terms, lower case, each once.9+export function terms(q: string): string[] {10+ const all = [...q.toLowerCase().matchAll(/"([^"]*)"?|(\S+)/g)].map((m) => m[1] ?? m[2]).filter((t) => t.trim());11+ return [...new Set(all)];12+}13+14+// The terms a search within one line needs: a term inside another goes,15+// since every line with the longer one has it.16+export const narrow = (ts: string[]): string[] => ts.filter((t, i) => !ts.some((o, j) => j !== i && o.length > t.length && o.includes(t)));@@ -51,3 +51,7 @@ lacks "$FORGE_WWW/index.html" 'data-code='
5151 ls "$FORGE_WWW/assets" >"$T/assets"5252 has "$T/assets" '^sqlite3\.[0-9A-Za-z_-]{10}\.wasm$'5353 has "$FORGE_WWW/assets/versions.json" 'sqlite3\.[0-9A-Za-z_-]{10}\.wasm'54+# The worker imports the query parser (query.ts) by its shipped, hashed name.55+w=$(ls "$FORGE_WWW"/assets/codesearch.*.js)56+has "$w" "^import \{ narrow, terms \} from '\./query\.[0-9A-Za-z_-]{10}\.js';$"57+exists "$FORGE_WWW/assets/$(sed -n "s|^import .* from '\./\(query\.[^']*\)';\$|\1|p" "$w")"