Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Docs: a workspace knowledge base people and agents write together | 1 | /** |
| 2 | * Full-text search over pages (D1's FTS5): turning what someone typed into | |
| 3 | * a safe query, and deciding which spaces a search may look in. Pure. | |
| 4 | */ | |
| 5 | ||
| 6 | /** | |
| 7 | * A query as FTS5 understands it: each word quoted (so `-`, `:` and | |
| 8 | * operators are text, never syntax), every word required, the last one a | |
| 9 | * prefix so results come as you type. Null when nothing searchable is left. | |
| 10 | */ | |
| 11 | export function ftsQuery(text: string): string | null { | |
| 12 | const words = String(text ?? "") | |
| 13 | .toLowerCase() | |
| 14 | .split(/[^\p{L}\p{N}_]+/u) | |
| 15 | .filter(Boolean) | |
| 16 | .slice(0, 12); | |
| 17 | if (!words.length) return null; | |
| 18 | return words.map((w, i) => `"${w.replace(/"/g, "")}"${i === words.length - 1 ? "*" : ""}`).join(" "); | |
| 19 | } | |
| 20 | ||
| 21 | /** | |
| 22 | * Which spaces a search looks in: those the reader may read, narrowed to | |
| 23 | * one space when asked. An empty list means search nothing. | |
| 24 | */ | |
| 25 | export function searchSpaces(readable: string[], only: string | null | undefined): string[] { | |
| 26 | if (!only) return readable; | |
| 27 | return readable.includes(only) ? [only] : []; | |
| 28 | } | |
| 29 | ||
| 30 | /** Whether a page belongs under a project filter: it, or its space, is linked to the project. */ | |
| 31 | export function inProject(project: string | null | undefined, pageProjects: string[], spaceProjects: string[]): boolean { | |
| 32 | if (!project) return true; | |
| 33 | const want = project.toLowerCase(); | |
| 34 | return pageProjects.some((p) => p.toLowerCase() === want) || spaceProjects.some((p) => p.toLowerCase() === want); | |
| 35 | } | |
| 36 | ||
| 37 | /** A project reference as stored: `owner/name`, lowercased; null when it is not one. */ | |
| 38 | export function projectRef(value: string): string | null { | |
| 39 | const s = String(value ?? "").trim().replace(/^\/+|\/+$/g, "").toLowerCase(); | |
| 40 | return /^[a-z0-9][a-z0-9._-]{0,99}\/[a-z0-9._-]{1,100}$/.test(s) ? s : null; | |
| 41 | } | |
| Docs index by meaning: passages of every page and project doc, embedded on save and recalled for agents; hybrid search for people | 42 | |
| 43 | /** Words too common to say what a question is about. */ | |
| 44 | const STOP_WORDS = new Set( | |
| 45 | "a an and are as at be but by can could did do does for from had has have how i if in into is it its me my of on or our should so that the their them then there these they this to us was we were what when where which who why will with would you your about after before any been being both each few more most other over same some such than through too under until very".split(" "), | |
| 46 | ); | |
| 47 | ||
| 48 | /** | |
| 49 | * A question as a loose FTS5 query: its meaningful words (quoted, longer | |
| 50 | * than two letters, at most ten), any of them; bm25 ranks passages with | |
| 51 | * more of them first. For recall's word fallback, where a whole sentence | |
| 52 | * rarely matches every word. Null when no word is left. | |
| 53 | */ | |
| 54 | export function ftsAnyQuery(text: string): string | null { | |
| 55 | const words = [ | |
| 56 | ...new Set( | |
| 57 | String(text ?? "") | |
| 58 | .toLowerCase() | |
| 59 | .split(/[^\p{L}\p{N}_]+/u) | |
| 60 | .filter((w) => w.length > 2 && !STOP_WORDS.has(w)), | |
| 61 | ), | |
| 62 | ].slice(0, 10); | |
| 63 | if (!words.length) return null; | |
| 64 | return words.map((w) => `"${w.replace(/"/g, "")}"`).join(" OR "); | |
| 65 | } |