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.
-
Fixed a stale-claim bug class in the README (issue #97): the enforced CI floor and the last measured value were collapsed into one number.
README.md's AI Evaluation row read "judge-calibration ... gated at agreement 0.955 / κ 0.906" — those are the values the last run achieved, not the thresholds it had to clear (MIN_AGREEMENT = 0.8,MIN_KAPPA = 0.6insrc/sprout/eval/calibration.py). A regression from 0.955/0.906 down to 0.81/0.61 would still pass CI and the README would still claim the gate sits at 0.955/0.906 — a 15-point and 30-point margin between what the README promised and what the pipeline enforced. Every other doc in the repo already stated floor and measurement separately (docs/ROADMAP.md,docs/audits/judge-calibration.md,docs/audits/gate-inventory.md); the README was the one exception, and the one documentdocs/claims.yaml's drift guard did not reach.README.md:139now states the enforced floor (agreement ≥ 0.80, κ ≥ 0.60) and the last measured value (0.955 / 0.906) as separately labeled claims. A second, same-root-cause drift in the same section — "120+ YAML cases across five suites" — undercounted: the harness gates on 8 suites (calibration, completeness, conversation, groundedness, multilingual, refusal, safety, toxicity-coverage, perdocs/audits/eval-report.json), not 5; the suite table and repo-layout tree diagram are corrected and the table no longer omitscompleteness,conversation, andtoxicity-coverage.sprout claims-check(docs/claims.yaml) now covers the README: six new claims (readme-refusal-target,readme-judge-calibration-floor-agreement,readme-judge-calibration-floor-kappa,readme-coverage-floor,readme-eval-suite-count,readme-eval-suite-names) pin these values to their live source of truth, gaining two new resolver kinds along the way: a generalizedsuite:source (suite:calibration.min_agreement/suite:calibration.min_kappa, alongside the existingsuite:refusal.threshold) andeval-report:suites.names/eval-report:suites.count(the report's real suite list/count) andpytest:cov-fail-under(the real--cov-fail-undervalue inpyproject.toml). The registry deliberately does not pin the measured 0.955/0.906 — unlike a floor, a measurement is expected to legitimately drift run to run; pinning it would make the gate brittle rather than honest.docs/claims.yaml's header comment documents all three new source kinds for the next claim added against them. -
Every
uv sync --frozenis nowuv sync --locked, acrossci.yml,release.yml,pages.yml,corpus-freshness.yml,redteam.yml, both Dockerfiles, and the CI-parity test fixture.--frozennever readspyproject.toml, so it installs the locked set and exits 0 when the lock no longer satisfies the manifest;--lockedmakes the comparison and exits 1. Nineteen invocations across the repository were the weaker flag, which means a drifted lock could have gone through CI, the docs build, the red-team run, the release, and both images without one of them saying so. -
The README standards-conformance table declares all fifteen standards and is now machine-readable. Performance, Incident Response, Data Governance, and AI Development Measurement had no row, so none of the four was recorded as met, exempt, or a gap; the first three point at gates and artifacts already in the repository (
tests/test_latency.py,slos/,SECURITY.md,docs/cards/) with their remaining shortfalls named, and AI Development Measurement is declared an open gap. The state column was headed "Applies" and filled with check marks, and the Quality & Metrics row carried a parenthetical in its label, which together made the table unparseable; the header is "State", the verdicts are the word "Applies", and the parenthetical moved into the posture cell. The phrase "gap tracked" reads as a reference to an issue tracker, which is not where this repository records gaps, so those notes now say "open gap" and namedocs/ROADMAP.md. No row's meaning changed. -
Release authorization now runs from reviewed
mainthrough the immutable portfolio authorizer; verification and builds use the exact selected commit, while separate checkout-free jobs recheck the tag object before PyPI and GitHub Release publication.
- Moved the offline deterministic quickstart into the README's opening screen, before the product and safety deep dive.
- The SME corpus-contribution path (research item E5):
sprout propose. A contributor proposes a new species as one self-contained YAML file — the passage in every supported language, its provenance (source, license,fetch_date, topic), the eval case the passage must satisfy, and a representational-harm checklist — fromsprout propose template, or via the new no-code corpus proposal issue form, which collects the parts only a contributor can supply and which a maintainer transcribes into a proposal file (a deliberate subset of the schema, not a field-for-field mirror of it).sprout propose checkreviews it offline and deterministically: license allowlist, ISO dates, E7's citation-freshness SLA applied against the topic the passage carries so toxicity prose gets the stricter SLA, every-language coverage with EN/ES structural parity, the corpus's canonical topic taxonomy, EXP-12's chunk lint reused rather than reimplemented, the shipped never-certify-"safe" guard run over every proposed sentence, an EN/ES medicinal-or-edibility claim scan that cross-checks the harm checklist instead of trusting it, and an eval case that must load, cite one of the proposed documents, and assert only facts appearing verbatim in the passage. Merge-blocking on errors for every submitted proposal, not only the committed example:make propose-checkand thepropose-checkstep of theeval-a11yCI job runsprout propose checkwith no arguments, which discovers every proposal-shaped file in the repository, reports one filed outside the declared submission locations (proposals/,examples/corpus-proposal/) as an error rather than skipping it, and fails rather than passing if it discovers nothing at all. A mechanically clean proposal carrying toxicity prose reportsready-for-expert-review— the veterinary toxicologist / native-Spanish review this repo already requires, recorded as a machine-checked state rather than assumed; theexpert_reviewsign-off that clears it must be a committed Markdown artifact underdocs/audits/naming the species, reviewer, and sign-off date, so the strongest gate cannot be discharged by pointing at any file that happens to exist. Worked example:examples/corpus-proposal/(Chamaedorea elegans, EN + ES, zero findings). Nothing is ever written tocorpus/. - 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. -
Tier-A observability for the optional serverless API (
src/sprout/otel.py,infra/).observability.tier: Anow wires real OTel traces + RED-per-endpoint metrics (REDMiddleware, W3Ctraceparentpropagation, the standard's fixed-second histogram buckets), trace-correlated JSON logs (obs.py), schema-checked SLO/burn-rate-alert files (slos/*.yaml,alerts/burn-rate.yml,sprout slo-check), and a deployable AWS CDK stack (infra/sprout_stack.py: Lambda via the AWS Lambda Web Adapter, an API Gateway HTTP API, a monthly budget alarm). Degrades to a no-op — never a crash — for tier B/C or when theobservabilityextra isn't installed. Seedocs/ROADMAP.md's Observability tier section for what is unit/e2e-tested versus not yet exercised (a livecdk deploy). -
Explicit season/light context qualifiers (EXP-05,
answer.py,providers/). Optional--season/--lightCLI flags (and matchingseason/lightfields onPOST /api/chatandGET /api/chat/stream) let a user state context like "winter" or "north window" for a single request. The words are taken exactly as given — never inferred from locale or the system clock — and only nudge which already-cited, already-supported sentence the generator selects; they never admit an otherwise-ungrounded sentence, are never treated as a citation, and are never persisted anywhere. The same selector-not-fact contract ADR-0010 built for photo-ID, generalized. The qualifier is echoed back onAnswer.season/Answer.lightand a localizedAnswer.context_note("As you stated (winter) — your own context, not a cited fact, and not saved."), rendered by the CLI and available to the UI, never folded into the answer's cited prose. -
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. -
Local review console for flagged/refused answers (
review.py, EXP-17). An opt-in, off-by-default queue that capturesAnswer.low_confidence/Answer.refusedtraces for maintainer labeling ({correct, incomplete, wrong-plant, should-have-refused}), plus three exporters that turn labeled traces intoeval/judge_probes.yaml-shaped judge probes, a confidence re-fit dataset (feeds FIX-08), and draft eval cases foreval/suites/curation — never written directly, always a standalone file for a maintainer to review and hand-merge. Newsprout reviewsub-commands (queue/show/label/run/export), wired intosprout askbehindReviewConfig.enabled(config.py). Stores question text locally on the maintainer's own machine only; see its own DPIA delta inRESPONSIBLE-TECH-AUDITS.md§C and ADR-0020. -
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. -
Verbalized, screen-reader-first confidence bands (
confidence.py, EXP-06): the raw confidence float is joined by a calibrated-language band — "well-supported" or "partially supported — verify" — derived from the committed reliability diagram (derive_band_cutoff), never invented. The band leads and the number follows in every surface (sprout ask, the/api/chat/streamdoneevent, the chat UI'saria-livemeta line), so a screen reader announces the calibrated language first while the number stays visible; localized in EN/ES viaConfig.prompts.confidence_band_labels. -
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. -
sprout.evalas a plugin-based package (eval/suite.py, ADR-0019): third-party suites are discovered via thesprout.eval.suitesimportlib.metadataentry-point group, alongside the five built-in suites, fail-closed on a name collision. The public plugin surface —Dataset,Judge,Suite/EvalContext,SuiteResult,MetricDefinition, and the registry — is frozen and re-exported with an explicit__all__fromsprout.eval, with a semver commitment. Proven by a worked, independently pip-installable second-domain example atexamples/herb-garden-plugin/(a culinary-herb corpus + a domain-specific suite, zero changes to this repo's source, committed reproducible report). -
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. -
Eval score trend ledger across releases (
eval/history.py).sprout eval --release <tag>appends this run's fingerprinted per-suite scores todocs/audits/eval-history.jsonl(append-only, one line per release — never per PR) and the report gains a trend section (per-suite sparkline plus its required accessible data-table equivalent, in both the Markdown and HTML reports). A drift rule fails the release gate if any suite declined for--drift-k(default 3) consecutive releases in a row, even when every individual decline was insidediff_against_baseline's tolerance — the single pinned-baseline diff cannot see a slow, multi-release bleed.make eval RELEASE_TAG=<tag>wires this into the release flow;release.yml's tag-triggered re-verification runs the same gate at the tagged commit. Seedocs/ideation/03-expansions.mdEXP-13.
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.