All notable changes to Sprout are documented here.
The format is based on Keep a Changelog 1.1.0, and this
project adheres to Semantic Versioning 2.0.0. Pre-1.0
(0.y.z): the public API may change in a MINOR release; breaking changes are called out here.
Per SECURITY.md, only the latest minor on the latest major receives security
fixes. Security entries reference the advisory (GHSA) per the portfolio release standard.
- Deploy-grade app-level server hardening (FIX-10): security headers (CSP, HSTS,
anti-framing/sniffing), a streaming-safe request-size cap, per-client-IP token-bucket rate
limits (with a stricter bucket and a concurrency bound on
/api/identify), all pure-stdlib and independent of any reverse proxy — delta checklist atdocs/audits/asvs-l2-delta.md.
- Re-armed CodeQL on pull requests, pushes to
main, and a weekly schedule; corrected the Standards Conformance label consumed by the portfolio checker; and assigned the homoglyph hardening decision its unique ADR number. - Made the tuning-scope gate compare ordinary Python tunable files by parsed syntax, so
comment/format-only documentation edits no longer demand a fabricated
Tunes-Againstcase; semantic edits and invalid syntax remain fail-closed.
2026-07-05 correction: this project has never been tagged or released —
git tagreturns nothing, and no release workflow has ever run. A previous version of this file carried a[0.1.0] - 2026-06-22released section,CITATION.cffclaimeddate-released: 2026-06-22, and a locally-built (never-published) wheel sat indist/. That was a documentation defect (REL-03): a version was declared released that was never tagged, published, or verified. That section's content is folded back into[Unreleased]below, un-dated, until an actual signedv0.1.0tag is cut andrelease.ymlruns end to end. See the 2026-07-05 remediation execution log at the end of this repo's audit trail for the full discrepancy.
An offline-first, grounded, evaluated, multilingual (EN/ES) houseplant-care RAG assistant, with the public evaluation harness as the headline artifact.
- A deployed zero-server reference surface at
https://sprout.chelseakr.com. The custom-domain GitHub Pages workflow now builds the deterministic TypeScript pipeline and same-origin corpus bundle, publishes the interactive assurance UI at the site root, preserves every MkDocs route, and runs a structural accessibility check on the assembled artifact. Questions execute entirely in the browser and are never sent, saved, or logged; household state remains the Family Greenhouse product boundary described in ADR-0015. - Family Greenhouse read-only integration: HMAC-authenticated, replay-bounded API contract
accepting only minimized household selectors, with strict provenance, PII sentinel tests,
persisted citations, a scoped ASVS L2 review, and
sprout.chelseakr.comcustom-domain support. - Mechanical enforcement of the "tune only against committed eval failures" rule
(
src/sprout/eval/tuning_scope.py,sprout check-tuning-scopeCLI command,tuning-scopeCI job). Previously a sentence indocs/ROADMAP.mdPhase 3; now a fail-closed gate — a change touching retrieval/generation/guards/calibration/lexical/config surface must carry aTunes-Against: <case-id>[, ...]commit trailer whose ids already appear in the committeddocs/audits/eval-baseline.jsonfailing_examples, so tuning can only be justified against a failure that was public before the change, never the held-out set or a local-only run. SeeCONTRIBUTING.md. - False-positive-safe tuning classification: comment-only YAML is compared semantically, and only the exact named operational lifecycle wrapper around an otherwise-identical provider constructor is normalized. The lifecycle module's initial addition is admitted once by exact digest; every future hunk is gated. Model, prompt, decoding, real-config, retrieval/guard, lifecycle-output, and unknown provider edits remain fail-closed, with adversarial regression tests for each category. Case authorization is read from the branch merge-base baseline.
web-static/— the deterministic pipeline ported to TypeScript, runnable with zero server (EXP-08,docs/ideation/03-expansions.md).text.ts,lexical.ts(BM25),sha256.ts+hashEmbedding.ts(the hashing embedder),store.ts,retrieve.ts(hybrid dense+BM25 via RRF, species filter, dedup),generator.ts(extractive generation),guards.ts(citation guard, never-certify-"safe" deny-list, injection detection, PII redaction),confidence.ts, andlang.tsmirror their Python counterparts line-for-line, running entirely client-side over a staticindex.json+config.jsonpair exported byscripts/export_web_bundle.py. A cross-language conformance test (web-static/test/conformance.test.ts, fixtures generated byscripts/generate_conformance_fixtures.py) replays every question ineval/suites/*.yaml(142 cases across groundedness/safety/refusal/calibration/ multilingual) through both implementations and asserts byte-identical answers, citations, and confidence — wired into CI (web-staticjob) as a merge gate. A static reference page (web-static/public/) with a web-app manifest and a cache-first service worker shows it working end to end; seeweb-static/README.mdfor what's shipped versus deferred (a dedicated browser WCAG/Lighthouse audit, PWA icon assets, and subresource integrity remain follow-up work).- Facet-coverage answer planner + a
completenesseval metric (EXP-01,providers/deterministic.py,eval/suites/completeness.py). The extractive generator now splits a multi-part question into per-clause "facets" (text.extract_facets) and selects sentences greedily to maximise marginal facet coverage before raw relevance score, so a two-part question ("how often should I water, and does that change in winter?") surfaces both clauses instead of three near-duplicate answers to the first one — a single-clause question is unaffected (verified byte-for-byte identical output). A new deterministiccompletenesssuite measures the fraction of a case's authoredexpected_facts(for cases with two or more) that the rendered answer actually covers; three multi-facet cases were added toeval/suites/groundedness.yamlto exercise it. See EXP-01 indocs/ideation/03-expansions.md. - Photo-based plant identification → grounded care lookup (
identify.py,providers/plantnet.py). A photo is identified into candidate species, the best confident match is resolved to a species already in the cited corpus, and that species is routed back through the unchanged grounded pipeline — so every rendered claim is still cited and toxicity still routes to a vet. The identification is labelled "a visual match, not a cited fact" and never enters the answer's sentences. Offline by default (no network, always falls back to "type the plant's name"); aplantnetprovider calls the allowlisted Pl@ntNet API with its key fromPLANTNET_API_KEY(env only). Newsprout identifycommand andPOST /api/identify. See ADR-0010. - Local-first care reminders (
reminders.py). Watering/fertilizing/etc. reminders tied to a plant (and optionally the citation that motivated them), stored in one JSON file on the user's own machine — offline, opt-in, no database, content never logged. Newsprout remindsub-commands (add/list/due/done/remove), reminder endpoints under/api/reminders, and an accessible reminders panel in the chat UI. See ADR-0011. - Offline static-vector semantic embedding provider (
providers/static_embedding.py,embedding_provider: static, EXP-03). A third, fully offline, deterministicEmbeddingProvider: a curated EN/ES plant-care vocabulary table (data/embeddings/clusters.yaml→scripts/generate_static_vectors.py→static_vectors.json) with a hashing fallback for out-of-vocabulary tokens, so synonym/paraphrase and EN/ES cross-lingual questions can score higher than the hashing baseline without any network or cloud account. Opt-in, not the offline default — see ADR-0017 for the measured eval delta (refusal 0.9118 → 0.9412, over-refusal 10% → 0%, groundedness unchanged at 1.000) and why it doesn't yet clear the 0.95 excellence bar. - Grounded extractive assistant. Retrieval-mandatory pipeline
(
guards(input) → retrieve → extractive generate → guards(output) → confidence/abstention) that answers only from the cited corpus, with an inline citation to the governing passage and its fetch date — or an honest "not covered" refusal. - Hybrid retrieval (
retrieve.py): pure-Python BM25 + denseHashingEmbedding, with a species/topic filter and a confidence threshold that gates weak matches into abstention. - Offline-by-default, deterministic generator (
HashingEmbedding+ BM25 +ExtractiveGenerator): the whole project, including the eval, runs with no network and no cloud account. Groundedness is 100% by construction (extractive + citation guard). - Safety and citation guards (
guards.py): a post-generation citation guard (every rendered sentence resolves to a retrieved passage) and a never-certify-"safe" deny-list that blocks "safe"/"non-toxic" certifications in EN and ES and routes ingestion questions to vet / poison-control. - Exposure-type detection for the escalation card (FIX-13, scaffold) (
guards.detect_exposure_type): classifies a safety query's audience — child/human, animal, both, or unspecified — by exact-token matching against explicit audience keyword lists (EN + ES, including son/daughter/hijo/hija family terms), and a human-poison-control card variant (PromptConfig.human_escalation_card_by_lang, US Poison Control 1-800-222-1222) exists in config for child/human exposure. Gated off by default (human_card_reviewed = False): the animal-line card (ASPCA APCC, Pet Poison Helpline) keeps rendering unchanged for every query, including child-ingestion ones, until a poison-control clinician / medical toxicologist signs off on the human card's copy in both languages — seedocs/audits/human-poison-control-card-review.md(currently a pending stub, not a completed review). - Calibrated uncertainty (
confidence.py): stated confidence the assistant is held to; abstains below threshold rather than guessing. - English/Spanish parity with enforced |EN − ES| ≤ 5pp pass-rate parity and mirrored facts/citations.
- Provider seam (
providers/): deterministic offline generator as default; a Claude-on-Bedrock generator (answer model: Claude Haiku) behind a config switch as the production seam. - The eval harness (
src/sprout/eval/): five suites — groundedness, safety, calibration, refusal, multilingual — scored by deterministic checks blended with an LLM-as-judge (judge model: Claude Sonnet, deliberately ≠ the answer model). Reports emit Markdown + accessible HTML + JSON, plus JUnit and SARIF; runs are content-hashed and byte-identical for identical inputs. Fail-closed loader (eval/dataset.py) rejects hash mismatches, malformed cases, and empty suites. - Synthetic, CC0-1.0 corpus (
corpus/) with a dated, licensedmanifest.yaml; chunked by care topic with source/license/fetch-date metadata; UI shows "based on references as of <date>." - Accessible web UI (
web/dist/): framework-free WCAG 2.2 AA chat interface with a non-chat transcript/alternate view; SSE token streaming; copyable citations. sproutCLI (ingest,ask,serve,eval,eval-baseline,calibrate,a11y-check,ci-parity-check,demo) and a JSON/SSE API server.- Governance and process:
make verifyreproducing the full CI gate set (lint · type · test ≥90% · security · eval · a11y · docs · workflow-lint · ci-parity-check); CONTRIBUTING, SECURITY, CODE_OF_CONDUCT, DEFINITION_OF_DONE; CODEOWNERS over the safety guardrails; ADRs; dependabot; SHA-pinned Actions; Conventional Commits + DCO sign-off; theclaude/* → develop → mainbranch model. - Docs: ARCHITECTURE, THREAT-MODEL, ACCESSIBILITY (+ ACR via VPAT 2.5 Rev 508), ROADMAP,
RESPONSIBLE-TECH-AUDITS, model and data cards, and the committed
docs/audits/eval artifacts. - Promptfoo red-team config (
eval/redteam/promptfooconfig.yaml) covering OWASP Top 10 for LLM Applications (LLM01-LLM10:2025) against the livePOST /api/chatpipeline, in EN and ES. Fills the gapdocs/ROADMAP.mdhad been carrying since 2026-07-05 ("planned — no Promptfoo config exists"); complements the manual, dated exercise indocs/audits/red-team-2026-06-22.md. AdvisoryredteamCI job (opt-in, needsANTHROPIC_API_KEY, excluded fromci-gate) added to.github/workflows/ci.yml; seeeval/redteam/README.md.
create_appaccepts an optionalidentifieroverride (mirroring the existingassistantoverride) so the grounded photo path is testable offline.- New
identificationandremindersconfig blocks (config/sprout.yaml);identifyoptional dependency extra (httpx). - Accessibility CI gates are now fully merge-blocking.
pa11y-ci(axe-core + htmlcs runners) lost itscontinue-on-error: true/|| trueadvisory-only status, and a newlighthousejob runs Lighthouse's accessibility category (threshold 0.95) against the chat UI and the HTML eval report — previously not wired into CI at all. Both jobs are now required byci-gate. Fixed the one real finding this surfaced: the empty-state reminders table left header cells with zero data rows (axe'sth-has-data-cells), so the table is now hidden until it has at least one reminder, matching the existing plain-language empty-state message. - CI/local parity is now mechanically checked, not just asserted (
ci-parity-no-mechanical-diff, ROADMAP.md):src/sprout/ci_parity.py/sprout ci-parity-checkdiffs.github/workflows/ci.yml's required-job commands against theirMakefilecounterparts, wired asmake ci-parity-check(amake verifyprerequisite) and aci-parityCI job (aci-gatedependency). Its first run surfaced two real gaps — thedocsandzizmor(workflow-SAST)ci-gatejobs had no local equivalent inmake verify— now closed with newdocsandworkflow-lintprerequisites onverify.
docs/ROADMAP.mdPhase 3 status (2026-07-08): the "Outstanding" bullet still listed "commit the ACR and the OWASP-LLM red-team report" as not-yet-done, even though bothdocs/accessibility/ACR.md(VPAT 2.5 Rev 508) anddocs/audits/red-team-2026-06-22.md(OWASP LLM01–LLM10:2025 coverage) had already been committed in the 2026-07-05 conformance pass. Moved to "Done" with an honest caveat preserved: the red-team report remains a manual, dated exercise until an automated Promptfooredteamrun is wired and promoted into the blockingci-gate(tracked in the "Red-team (OWASP LLM01–LLM10)" ledger row).
- Gate-inventory audit (FIX-02,
docs/ideation/02-large-scale-fixes.md). Newsprout gate-inventorycommand (wired intomake verify/make auditsand CI) parsesdocs/ROADMAP.md's ledger fresh on every run and fails if anyAUTOrow'sMeasured bycell cannot be mechanically resolved to a real Makefile target, CI step, or repo file — the "declared but unenforced gate" class of defect this repo has otherwise caught only by manual audit. Closed the gaps it found: theConformance levelaccessibility row overclaimed axe/ pa11y/Lighthouse as unconditionalAUTO(only the structuralsprout a11y-checkis merge-blocking; corrected to say so, matchingRESPONSIBLE-TECH-AUDITS.md§E); the optional Wilson--statistical-gatewas described inDEFINITION_OF_DONE.mdas if always enforced when it is off by default in CI (turning it on today failsmultilingualon sample size, not quality — sequenced behind FIX-12); and roughly a dozenMeasured bycells named a mechanism in prose only (e.g. "per-language bundle diff", "transcript-view check") with no such check ever wired — each now names its real, resolvable mechanism, including a newtests/test_i18n_parity.pythat actually implements the previously-nonexistent EN/ES key-and-placeholder-parity diff. - Retrieval scale architecture (FIX-07).
BM25Indexis now an inverted-postings structure (term -> {doc_index: term_freq},lexical.py) built once per corpus instead of being retokenised on every query:ingest.pybuilds it over every chunk andstore.pypersists it inindex.json(format version bumped to 2; a v1 file now fails to load with a message pointing atsprout ingest).VectorStore.searchaccepts acandidate_idsfilter and selects withheapq.nlargestinstead of a full sort, so a species-scoped query's dense scan and BM25 scoring are both bounded by that species' chunk-id group rather than the whole corpus, and an unfiltered query no longer requeststop_k=len(store). Seedocs/ideation/02-large-scale-fixes.md(FIX-07) andtests/test_retrieval_scale.py.
- Offline-by-default posture (no auth, no network, no persisted user queries) establishing the OWASP ASVS L1 baseline; secrets via environment only; pip-audit, gitleaks, and Semgrep wired into CI. No advisories to date.
- Standards conformance remediation (2026-07-10, SEC-11/SEC-28): removed a stray
|| truedocumentation artifact from thepip-auditstep name that a mechanical conformance checker was misreading as a silenced gate (the gate itself was never muted); removed the redundant|| trueon thepa11y-cistep now that its one real finding (an empty<table>with header cells and no data cells, WCAGth-has-data-cells) is fixed — the reminders table and the "No reminders yet" message are now mutually exclusive in the DOM instead of both always being present (web/dist/index.html,web/dist/app.js); added a Trivy CVE scan (container-scan.yml, CRITICAL,HIGH, matchinghabitable's pattern) for the Dockerfile image, which currently scans clean.