This is the live reference surface at https://sprout.chelseakr.com. GitHub Pages
publishes it at the custom-domain root while the MkDocs handbook remains available at
its existing paths such as /architecture/ and /audits/eval-report/.
EXP-08 (docs/ideation/03-expansions.md): a TypeScript port of Sprout's deterministic
stack — hashing embedder, BM25, extractive generator, guards — that runs entirely
client-side over a static, exported index.json and config.json. No backend, no
telemetry, nothing a question ever leaves this tab for.
Python (src/sprout/) |
TypeScript (web-static/src/) |
What it does |
|---|---|---|
text.py |
text.ts |
Tokenize, stem, stop-word/negation filter, sentence split, coverage, jaccard — the shared vocabulary every other module tokenises through. |
lexical.py |
lexical.ts |
Okapi BM25. |
providers/deterministic.py's HashingEmbedding |
sha256.ts + hashEmbedding.ts |
SHA-256 token hashing → signed, L2-normalised bag-of-tokens vector. |
store.py |
store.ts |
Flat cosine vector store, loaded from index.json (read-only in the browser — there is no ingest path here). |
retrieve.py |
retrieve.ts |
Hybrid dense+BM25 retrieval via Reciprocal Rank Fusion, species/topic filter, near-duplicate dedup, the min_score grounding gate. |
providers/deterministic.py's ExtractiveGenerator |
generator.ts |
Verbatim sentence selection by query-token overlap. |
guards.py |
guards.ts |
Citation guard (structural anti-fabrication gate), never-certify-"safe" deny-list, injection detection, PII redaction. |
confidence.py |
confidence.ts |
The ADR-0012 logistic confidence + abstain/low-confidence thresholds. |
lang.py |
lang.ts |
Deterministic EN/ES language detection. |
answer.py's Assistant |
answer.ts |
Orchestrates the above into .answer(query, language). |
config.ts types the exported config bundle; models.ts types Chunk/Answer/etc.
index.ts is the public entry point: loadAssistant(dataBaseUrl) fetches
config.json + index.json and returns a ready Assistant.
The public page shares its visual system with web/dist/: npm run build:site copies
web/dist/styles.css into the generated web-static/public/ artifact. That keeps the
server-backed development UI and the zero-server public reference from drifting apart.
The algorithms are reimplemented in TypeScript (per EXP-08's own estimate, "all
reimplementable in ~1k lines of TS"), but the data they run on — the vector/BM25
index, the guards deny-lists and toxicity keywords, species aliases, per-language
prompt strings — must stay byte-identical to the Python side or the two
implementations silently drift. scripts/export_web_bundle.py dumps that data
straight from the loaded, validated sprout.config.Config (not by hand-transcription)
to web-static/public/data/config.json, and copies the built var/index.json
alongside it as data/index.json. Both are plain static assets — the only two network
requests the whole assistant ever makes, both same-origin.
Per EXP-08's own risk assessment: "dual-implementation drift is the big one — the conformance test is the deliverable's spine."
scripts/generate_conformance_fixtures.py runs the real Python Assistant over every
question in eval/suites/*.yaml — the same 142-case set (groundedness, safety,
refusal, calibration, multilingual EN+ES) the Python eval harness scores against — and
records the answer text, citations, confidence, refusal reason, and safety notice to
web-static/test/fixtures/conformance.json. web-static/test/conformance.test.ts
replays every one of those questions through the TypeScript port loaded from the same
exported bundle and asserts the two implementations agree exactly. It is wired into CI
(.github/workflows/ci.yml's web-static job) as part of the required ci-gate, so a
port/pipeline change that breaks parity fails the PR, not a later release.
# From the repo root: build the Python index, then export it + generate fixtures.
make ingest
make web-static-bundle # -> web-static/public/data/{index,config}.json
make web-static-fixtures # -> web-static/test/fixtures/conformance.json
cd web-static
npm ci
npm test # compiles TS, runs the 142-case conformance test (node:test)
npm run build:site # compiles + copies dist/src/*.js into public/assets/
python3 -m http.server 8080 --directory public # or any static file serverThen open http://localhost:8080/.
EXP-08's full shape has four parts. This change delivers (a) and half of (b)/(c); the rest is real follow-up work, not claimed here:
- (a) Algorithm port + conformance test — done. All five deterministic modules ported, 142/142 fixture cases passing, wired into CI.
- (b) Index + locales as static assets with subresource integrity — partial. The
data assets are exported and fetched statically; SRI hashes are not yet wired into
index.html(there is no build-time hash-and-rewrite step here). - (c) Installable offline PWA — a minimal version. A
manifest.webmanifestand a cache-first service worker are included and were smoke-tested against a real static HTTP server.manifest.webmanifest'siconsarray is empty — there are no designed app icons in this repo to ship honestly, and Lighthouse's PWA installability check will fail without them. The deployed HTML is covered by Sprout's structural accessibility gate. A dedicated browser-based WCAG 2.2 AAA / Lighthouse a11y+perf ≥ 0.95 audit of this zero-server route remains follow-up work. - (d) The never-certify-safe deny-list and escalation card ship in the bundle —
done, via the exported
config.json'sguards/promptssections (same source of truth as the Python side, not a second hand-authored copy). - (e) Continuous deployment — done.
.github/workflows/pages.ymlrebuilds the corpus bundle and TypeScript modules on every push tomain, overlays the resultingweb-static/public/artifact onto the MkDocs site, and deploys the combined site tosprout.chelseakr.comthrough GitHub Pages.
- No cloud generator path (
bedrock/anthropicgeneration.provider) — the browser port only implements the offline deterministic path;export_web_bundle.pyrefuses to export a bundle configured for a non-deterministicembedding provider. - No photo identification or reminders in the demo page — both need either network egress or local-storage wiring this change does not add. Type the plant's name instead, exactly as the Python CLI's fallback does.