Repository navigation
v0.4.0 — last release with the marimo interface
The last release with the marimo interface: the next one replaces it with a web
interface.
Changed
- The application logic moved out of the marimo cells into
base/services/.
services/wiki.pyopens a wiki, lists and reads its pages, resolves their
links, and saves an edited page (front-matter kept, references rebuilt, one
git commit, refused when the page changed since it was opened).
services/chat.pybuilds a wiki's two agents and answers one turn in the
pre-retrieval, strict or streaming mode.services/ingest.pyholds the upload
path with its reconciliation and cross-links, the maintenance passes, and the
views of the sources. The three marimo apps now call these functions; their
behaviour is unchanged. This is the layer a web interface will call.
Changed
- Every pre-retrieval refusal states its cause. The single text "Eso no
está en mi base de conocimiento." is replaced by one message per cause,
built by the code (base/domain/chat/refusal.py): the blacklist term, a
question that names no topic of the wiki, a topic with no text found, an
answer not grounded in its source document, an answer with no evidence, and
the model's own refusal. Every message except the blacklist's lists up to
five related page titles, or points to the wiki's index when there are none.
The chat system prompt of every language now asks the model to open its own
refusal with "No encontré la respuesta en los documentos." / "I did not
find the answer in the documents.", and the three demo prompts use that
sentence. The strict and streaming modes are unchanged.
Changed
- Search, the coverage gate and the Tier-2 check stem words in the wiki's
language. One Snowball stemmer per wiki language (snowballstemmer,
base/domain/text/stemming.py) serves the FTS index, the queries, the roster
and the Tier-2 overlap. "¿Cuánto rinden los plazos fijos?" now enters the
roster through the categoryplazo_fijo; before, the plural was refused.
The blacklist and the dataset keys still compare by whole word, soceder
is no longer at risk of matchingcedear.document_chunksgains
content_stemmed, whichchunks_ftsindexes withunicode61(no more
porter); an existing index is rebuilt on first open, with no model call.
The pre-retrieval turn now checks the blacklist first and searches the wiki
only for a question in the roster. The Tier-2 threshold is configurable as
[pre_retrieval] min_coverage(default 0.2).
Fixed
- Pre-retrieval Tier 1 now carries the dataset the question names. A data
question that also matches wiki pages (for example "¿Qué tasa de plazo fijo
ofrece el Banco Galicia?") was answered from the pages alone: the pages hold
no rates, and the model calledquery_datasetonly when the bank matched the
example in the tool's docstring (0 calls in 3 runs for Banco Galicia). The
code now injects the named key's rows next to the pages, or only the
category's key names when the question names no key, and cites the injected
rows (dataset_injection,base/domain/chat/preretrieval.py; spansQ7a–Q7d).
When the model asksquery_datasetfor a key the category lacks, such as
Banco Comafi, the code appends the keys that have data to the answer
(offer_available_keys).
Changed
- The chat trace and the ingestion trace are replaced by one OpenTelemetry
span stream.base/domain/chat/trace.py(WIKI_CHAT_TRACE=1,
<workspace>/.llmwiki/chat_trace.jsonl) and
base/domain/ingestion/trace.py(per-runtrace.jsonlunder
<workspace>/.llmwiki/traces/<run_id>/) are removed.WIKI_TRACE=1now
activates a single module,base/domain/tracing.py: every step of a chat
turn or an ingest opens a span named by its identifier in the diagrams of
design_stemming.md(Q5,I4,W1, …), so the span names of one trace
are the path the run took through the diagram. Spans append to
<workspace>/.llmwiki/traces/spans.jsonl, one JSON object per line
(ReadableSpan.to_json); heavy payloads still go to content-addressed
files under.llmwiki/traces/payloads/, gated by the unchanged
WIKI_TRACE_CAPTURE. Model calls are spans withgen_ai.*attributes,
written by Pydantic AI's own instrumentation for the chat agent and by a
proxied OpenAI client (tracing.wrap_openai) for ingestion.
scripts/render_trace.pyis rewritten to render the span file — one
timeline per trace, filterable by--trace,--conversation, or--doc—
andmarimo/trace_report_app.pyis removed with no replacement viewer.
WIKI_CHAT_TRACEno longer has any effect.
Fixed
- "See also" links now survive an accent.
slugifystrips combining marks
when it builds a page's file name, so a page titled "Panel Líder" is filed as
panel-lider.md, andinject_see_alsolooks for the slug — "panel lider" —
in the text of every other page. The text keeps its accents, so prose that
spells "Panel Líder" never matched, and the link was never offered. Counted on
the bundledfinanzas-argentinaswiki: thirty page pairs in that shape, among
them the summary of Acciones Locales, which names both "Panel Líder" and
"Riesgos estructurales de inversión" without linking either. The bundled
English wiki has none, which is why the gap stayed invisible. The comparison
now strips the accents from the text as well, throughstrip_accentsin
wiki_generator, and_crosslink_candidatesstrips them the same way — that
candidate set is a superset of the pages that end up linked only while both
halves compare the same string.
Added
-
An index built with an older schema is reported instead of crashing. The
schema indatabase/sqlite_schema.sqlis applied with
CREATE TABLE IF NOT EXISTSevery time a database is opened, and no
ALTER TABLEever runs, so a table created by an earlier version keeps the
columns that version declared. The mismatch used to surface later, as a raw
sqlite3.OperationalError— "no such column: relative_path" — thrown by
whatever query happened to run first, which named neither the cause nor a way
out.open_dbnow compares, before applying the schema, the columns of every
existing table against the columns the current schema declares, and raises
SchemaMismatchErrornaming the table, the missing columns, the file, and the
recovery: the wiki pages are markdown under git and are unaffected, so
deleting the index and re-ingestingsources/rebuilds it. The reference is
built by applying the schema to an in-memory database rather than by parsing
SQL, so the comparison stays exact as the schema evolves, and the reference is
computed once per process. A table the schema declares and the database lacks
is not a mismatch: that table is created, as before. A table the schema never
declares is left alone. -
Ingestion checks for a Java runtime before it starts.
opendataloader-pdf
runs a bundled.jarthrough thejavacommand, and a DOCX is converted to
PDF before that same extractor reads it, so both supported file types need a
Java runtime. Nothing said so: no document mentioned Java, and there was no
check matching thecheck_libreoffice()that has always guarded the DOCX
path, so a machine without a Java runtime failed inside asubprocesscall in
a dependency.check_java()now mirrorscheck_libreoffice(), honouring
JAVA_HOMEfor a runtime that was never added toPATH;extractraises
JavaNotInstalledErrorwith installation commands for the three platforms;
the pipeline reports the same message as a failed ingest rather than an
exception; and the ingest app shows the runtime's status next to LibreOffice's.
The prerequisite is now stated in both READMEs, in the format table of both,
indocs/manual/workflows.md, and byquickstart.py, which warns without
blocking — the bundled demos ship pre-ingested and open without a Java runtime.
Fixed
-
Concept pages of a Spanish wiki recorded no citation edge.
references.pylooked for the literal## Sourcesheading when parsing the
plain- file.pdfbullets a concept page lists its sources with, while
build_concept_pagewrites that heading from the wiki's locale —## Fuentes
forlanguage = "es". Every concept page of a Spanish wiki therefore had zero
citesedges, and everything downstream read that silence as "nothing to do":
contradiction_checkandmissing_xref_checkbuild their pairs by joining on
those edges and found none,staleness_checkcould never mark a concept page
when its source changed, anddelete_sourceleft the concept pages of a
deleted source unmarked. Both shipped Spanish demos measure it — 0 concept
citesedges inexamples/cuentos-de-hadasand in
examples/finanzas-argentinas, against 14 in the English
examples/fairy-tales. The pattern now accepts theh_sourcesof every
registered locale, so adding a language keeps adding oneLocaleentry.
Existing Spanish wikis repopulate their graph by re-running
update_referencesover their pages, which calls no model. -
A Java runtime installed only under
JAVA_HOMEstill failed the ingest.
check_javaaccepts a runtime found throughJAVA_HOMEfor an installation
that was never added toPATH, but the extractor it clears the way for runs
the barejavacommand throughsubprocess, which resolves againstPATH
alone. That configuration therefore passed the preflight added last month and
failed a few frames later withPDF extraction failed: [Errno 2] No such file or directory: 'java'— the raw dependency error the preflight exists to
replace.extractnow prepends the found runtime'sbindirectory toPATH
for the duration of the extraction and restores it afterwards; whenjavais
already onPATH, nothing changes. -
quickstart.pydied at step 5 on a stock Debian or Ubuntu. Those
distributions shippython3withoutensurepip, sopython3 -m venvfails
with "ensurepip is not available" — after the demo choice, the provider wizard
and the.envwrite, with a message that names neither the cause nor the fix.
The installer now checks forensurepipbefore creating the environment and
namespython3-venv. The README's "all you need is Python 3.12+ and git" was
wrong there and nothing said so. -
The
E2E_FULL=1sweep test could not pass. Its first recorded run failed:
after confirming the widget it polled the Activity Log fortotal:, but the
panel still showed the previous scan, which ends intotal: 0.0s, so the
first read matched the stale marker and the assertions ran against the wrong
log while the sweep was still working. The test now waits for the sweep's own
opening line first. The sweep itself is sound — driven directly against the
same workspace it makes 31 model calls in 34 s, reports 7 findings and repairs
6 of them. -
Documentation figures that had gone stale. Both READMEs said "nine lint
checks" and listed nine;lint_wikiruns ten (unpaged_source_checkwas the
missing one). Both said "a 72 KB programmer manual", a size from before that
manual was split intodocs/manual/; the figure is replaced by what the
documentation is rather than how big it was.ROADMAP.mdlinked PR #7 as
github.com/Clod/llmwiki/pull/7, which returns 404 — the repository is
llmwiki-marimo, so the only pointer to the rollback design was broken.
CONTRIBUTING.mdanddocs/manual/apps.mdstill told a reader to run
marimo/read_app.pyand testtest_read_app.py, the three-column app the
rest of the documentation has moved off. -
The installer launched the app the documentation does not show.
quickstart.pyended withmarimo run marimo/read_app.py, the three-column
app, while the README screenshots, the demo video and the rest of the
documentation show the tabbed app. A first run therefore opened a different
interface from the advertised one. The installer now launches
marimo/read_app_tabs.py, and its repository-layout check looks for that same
file. -
Lint reports a source that produced no wiki page. Ingestion commits the
source row asstatus='ready'at step 6, before the model writes anything, so
that a failure in the steps after it cannot lose the document. The cost of that
order is a state nothing reported: a source stored, indexed and searchable with
no page written from it. Nothing recovered it either —needs_ingestion
compares the file's modification time and its hash, never whether the document
has pages, so every later scan called the file up to date and wrote nothing,
and touching the file did not help because the hash still matched. No check
covered it:orphanruns the other way round, flagging pages nothing links to
and deleting them, andthin_pagemeasures how well a source's pages cover it
and skips a source that has none. Finding it meant noticing the absence in a
page list.unpaged_source_checknow lists every source withstatus='ready'
that no wiki page cites. It is advisory rather than repairable: producing the
pages means deleting the source and ingesting it again, which is a decision.
Two of its four tests fix the boundary — a source markedfailedis not
reported, because there ingestion already said so, and a wiki page is not
mistaken for a source. -
Stale pages can be deleted from the ingest app. A page is marked stale when
a source it cited is deleted: the page is kept, because it may still rest on
other sources, and flagged for a human. Nothing surfaced that flag —
find_stale_pageshad been written for exactly this and was called from
nowhere — so acting on it meant knowing it existed and deleting pages by hand.
Two cells, per the project's rule against stacking controls in one: the first
counts the marked pages, puts the count in the button label, names them in the
confirmation (five, then "+N more") and disables itself when there are none;
the second deletes each one, continuing past a failure so a single bad page
cannot strand the rest. Offering this in bulk is only safe because of the two
fixes below — inbound links are stripped, the catalogue entry is pruned, and
the mark is cleared on regeneration, so the button sees pages that lost
evidence and were never revisited rather than every page ever marked. -
docs/from_idea_to_product.md, linked from the top of the README. Seventeen
ways the LLM-wiki idea leaks when a generic agent is pointed at a folder, each
with a real example and what this project does about it — the answer to the
obvious question, "why not just use Claude Code on my notes?". It existed in
Spanish since July, unlinked from either README, gated behind its own rule that
it would not be published until every point was green. Reviewed against the
code, and that gate turns out to be all but met: point 10 ("the configuration
itself rots") shipped as designed, unbuilt and is now built — vocabulary is
generated at ingest and watched by four lint checks — point 11 counted six
checks where there are nine, the thin-page detector it listed as pending
exists, and the live validation point 2 was waiting on happened on 2026-07-19.
Rewritten in English with those corrections, plus two limits measured this week
that the July text could not have known: the coverage roster matches page
titles literally, and the blacklist does not stem. The Spanish original is
marked stale and kept until it is re-translated from the English. -
ROADMAP.md— what is planned next, and what is built but known to be
imperfect. The second half is the point: the coverage gate matching page
titles literally, a documentation check that cannot see a link cut in two, a
guard written for a step that was never built, and the vocabulary lists having
no interface by design. These were tracked in a working file outside version
control, which meant the only copy lived on one machine. Four entries turned
out to be already fixed and were closed rather than published, and one lost
its rationale to an earlier PR and was dropped. Linked from both READMEs and
covered by the docs link checker. -
The pre-retrieval switch is documented where you would look for it. Both
wiki_config.example.tomltemplates now carry the[pre_retrieval]section and
its three scope lists, commented out, and both READMEs explain the trade in the
chat-configuration section. The flag decides the entire shape of the read path,
and until now it appeared only in the manual, the walkthrough and the finance
demo's own config — so a user copying the template had no way to learn it exists.
Changed
-
The cross-link pass is scoped to the pages an ingest touched.
crosslink_wiki_pagescompared every wiki page against every other one, with
a regular-expression search per pair, and ran in full after any scan that
ingested a single file. Measured on synthetic corpora through the real
pipeline: 0.1 s at 73 pages, 2.0 s at 460, 7.2 s at 601 — quadratic in the
page count, in a step that runs on the way in. It now takes the set of pages
the ingest wrote (from the newpages_touched_by) and rewrites those plus the
pages whose text mentions them. On a 601-page wiki, ingesting one further
document: 0.07 s and 5 pages linked, with a full pass straight afterwards
finding 0 further pages to link, so the scoped pass loses nothing. The
wiki-wide button and regeneration still run the full sweep. -
Both walkthroughs were reviewed line by line by a first-time reader, and
repaired. Twenty-seven commits of prose, no production code touched and the
generated appendix unchanged. The review found errors that a check against the
code could not: the compounding figure was 15 → 30links_toedges rather than
15 → 25, and the argument attached to it did not hold either, since six new
pages produced fifteen new edges — the same ratio as the first act — so what
supports the claim is that most new edges come fromrepair_missing_xref, not
from the model writing pages. The capture script re-runs eleven questions
through the gate, ten of them through a live model, not seven — a figure the
same document stated correctly sixty lines earlier.ensure_citationappends
two independently decided labels,Referencia:andFuente:, not one.
overlap.pywas described as able to judge whether a fragment answers a
question, when it measures whether an answer draws on a fragment; the ROADMAP
entry repeating that claim is corrected too. "Either case ends inrefuse" is
false for a wiki with datasets, wherehas_datadiverts the question to the
tools.Terms used before being defined were introduced where the behaviour first
appears — "positions", "agentic", the document's own division into parts, the
coverage roster, and the three runs the summary table refers to — so every
reference resolves backwards. Figurative phrasing was replaced with the
mechanism it stood for throughout: "it nearly went the other way" does not say
what the other way was, and "structurally blind" does not say what goes
undetected. -
The programmer manual is four files, and gained a layer map. It had grown
to 1227 lines on top of the 1090 already split intoworkflows.md. Now:
programmer_manual.mdkeeps orientation (§1 §2 §3 §10 §11 §13, 421 lines),
manual/internals.mdtakes schema, tool layer and tracing (§4 §5 §14),
manual/apps.mdtakes the Marimo apps, configuration, testing and datasets
(§7 §8 §9 §15), andmanual/workflows.mdstays as §6. Section numbers stay
global — a§Nmeans the same section wherever cited, which is what keeps
~190 cross-references between these documents valid.§2 now carries the nine architectural layers, derived from the project's own
knowledge graph (449 nodes, 975 edges) rather than drawn by hand, with the two
boundaries that are decisions rather than folder consequences: lint and repair
are one layer because they are a producer/consumer pair, and datasets plus the
finance overlay sit outside the engine because they are inactive on most wikis.Two tests now guard the arrangement, both verified failing: every
§Ncited
anywhere in the maintained docs must be defined by one of the four files, and
no two files may define the same number. The first immediately found two
CODEMAPS still citing§12, orphaned when that section moved to the roadmap. -
Pending work lives in one place. The programmer manual carried §11 "Pending
Work / Roadmap" and §12 "Future Enhancements";ROADMAP.mdarrived three days
ago and made a third. Three lists of the same thing diverge, so §11 and §12
moved wholesale into the roadmap — including the full five-step
reindex_from_diskdesign, and the deliberate deferrals with their reasoning
intact (web search at query time and as an ingest loop, two-step reviewed
ingestion, image handling, output formats). The manual keeps a pointer and is
178 lines shorter. Its "no open bugs" line was true of bugs and never of
known limits, of which the roadmap now records five. 27 dangling§11.N/§12
cross-references across both READMEs,workflows.mdand the data dictionary
were repointed. -
The read app's interface is entirely in English. It was already English
almost everywhere — "Refresh", "Save to wiki", "Category", "is not a
directory" — but a handful of Spanish labels had drifted in with the tabs
variant and stayed: the two chat checkboxes (Modo estricto,
Pre-retrieval: el código recupera…), the tab names (📖 Lectura,
💬 Diálogo), the chat heading and the save accordion.read_app.pyalready
said "Chat with your Wiki" whileread_app_tabs.pysaid "Chat con tu Wiki",
which is what gave the drift away. Wiki content remains per-wiki
multilingual and the Spanish system prompts are untouched — this is the
chrome only. -
Both walkthroughs are rewritten for a reader who has never seen the project.
They no longer assume you can read a schema, and every term borrowed from the
LLM and search worlds — token, embedding, RAG, chunk, agentic,
corpus, system prompt — is defined where it first appears. The Spanish terms
in the finance demo's captured answers are glossed in place rather than
translated away, since the answers are evidence. Both documents now open on
Karpathy's note and close on an honest account of what this implementation adds
to it and where it falls short, and both gained diagrams: the workspace layout,
the two chat checkboxes, and the chat-to-wiki save flow. -
The query walkthrough describes the mode the app actually ships in. It was
built around a single pre-retrieval toggle, but the read app has two independent
checkboxes, andStrict modeis on by default — so the "unticked mode" it
documented was a configuration nobody runs. Its two showcased failures are now
measured against that default: a missing citation is repaired by
ensure_citation, while a passage retrieved for a question it does not answer
passes through untouched.capture_query_walkthrough.pyrecords both outcomes
by replaying the guardrail over the captured run, so the claim costs no second
model call and cannot drift from the app. -
Wiki-page front-matter is written by code, not by the model, and follows the
Open Knowledge Format.
The prompt templates used to show the LLM atags:/sources:block and ask it to
reproduce values the code already held; on update the block round-tripped through
the model and could drift.create_pagenow renders it from what it is given, so
every page carries atype(concept,summary,overview— OKF's one mandatory
field), atitle, itstags, andsourcesas OKF provenance mappings rather than
bare strings. Reading tolerates the old string form, so existing wikis keep working.
Fixed
- Deleting a wiki page left broken links and a catalogue entry behind. Two
cleanups that were supposed to run on delete did not._strip_dead_linksbuilt
its pattern from the page's full path (wiki/concepts/x.md), while neighbours
link as[Title](x.md)and across folders as[Title](../summaries/x.md)—
whatinject_see_alsoandrepair_missing_xrefactually emit, and what all 26
links in the fairy-tale demo look like. The pattern matched nothing, so
deleting a page left a broken link in every page pointing at it. It now matches
every markdown link and resolves each href against the directory of the page
containing it, the workspace-root-relative form included, since older pages may
carry it.delete_pagealso never touchedwiki/index.md: that file has no row
indocuments, so no cascade reaches it, and the deleted page stayed listed in
the one page whose job is to list what exists.remove_index_entryalready
existed for this and is now called. - A page marked stale stayed marked forever.
stale_sincemeans "a source
this page cited was deleted; it may now be under-supported, review it". Nothing
ever cleared it, so any list built from the flag filled with pages long since
dealt with. Clearing is opt-in rather than tied tooverwrite=True, because
only four of the eight overwrite call sites are regenerations — ingesting a
concept or summary page,regenerate_wiki_pagesandrepair_staleclear it;
rollback after a failed ingest,crosslink_wiki_pages,repair_gap_filledand
save_to_wikikeep it. None of those four revisits the page's prose, which is
what the mark asks about, so clearing on any overwrite would erase the signal
on a See-also append and make the flag meaningless by the opposite route. - Wiki pages were being recorded as citation sources. A
citesrecord means
"this page was written from that source document", and deletion, lint and
provenance all assume the target is a source —delete_sourcedecides what to
destroy by following them. Two defects stored page-to-page relationships as
citations:repair_missing_xrefappended its "See also" link with
append_to_page, which writes to the end of the file, so on a generated page —
where "See also" precedes "Sources" — the link landed under## Sources; and
update_referencesmatches a citation against every document by filename and
by title, both of which a wiki page has, so the misplaced bullet resolved to it.
Fixed at both levels: the repair now writes into the See also section, opening
one above Sources when absent, and the parser refuses to point a citation at a
wiki page whatever the markdown says. - Two Spanish column headers survived the interface translation. The read
app's page table renderedTítulo/Rutaagainst an English wiki — missed by
the sweep in this release because they are dictionary keys built in
_page_row, notlabel=arguments, so the grep that found the checkboxes and
tab names did not reach them. Found by looking at a screenshot. - The read-app E2E was broken by this release's own label translation. The
strict-mode toggle was located byget_by_text("Strict mode"), and the
pre-retrieval checkbox beside it now reads "…(supersedes strict mode)" — a
case-insensitive match hits both, so the locator failed with a strict-mode
violation. Invisible until someone ran it, which the parity work below finally
did. Both suites now anchor on the unique tail of the label
(answer only from wiki sources). The Spanish labels did not collide only
because the second said "modo estricto" in lower case.
Changed
- The demo video is the new interface, and the README says what it shows.
https://youtu.be/VLX5kLczQbkreplaces a recording of the three-column app. The
blurb changed with it, because the video did: the old one walked an ingest, the
new one reads a generated wiki and then contrasts the two answering modes — a
cited cross-document answer in 9s against the same kind of question refused in
1.2s, which is the design argument and was not demonstrable before. - The README screenshots show the tabs read app, and there are two of them.
The old hero image showed the three-column grid doing everything at once,
which the tabs app cannot: it renders only the active tab. So one image per
tab — the page table with a generated concept page, and a real chat answer
making three claims with five citations, above an expanded Save to wiki
form. Both READMEs' alt text rewritten to match, and with them the quick-start
command, the project-structure listing, the E2E command and the "malleable UI"
bullet, which still described three columns while the picture above it showed
tabs.docs/assets/read_app.pngis removed as superseded.
Added
scripts/record_demo.py— records the demo video by driving the read app,
replacing a rig that lived in/tmpand did not survive. The previous demo
outlived its accuracy with no way to regenerate it; this is one command in
version control. It shows what the tabbed interface can and the three-column
one could not: both tabs, both mode checkboxes, a real cited cross-document
answer — and then the same off-corpus question under Pre-retrieval,
refused in 1.2s against the 9s the model-backed answer took. Not because the
model is fast: because it was never called. The waits are sped up rather than
cut, so the work is still visibly work. A failed turn aborts instead of
recording an error, same guard as the screenshots. The.mp4is gitignored —
the READMEs point at YouTube, which GitHub will not play from a repo path
anyway.scripts/capture_screenshots.py— regenerates the README screenshots by
driving the running read app with Playwright. The images indocs/assets/went
stale because taking them was a manual ceremony; this makes it one command. Two
images rather than one, because the tabs app renders only the active tab and no
single frame shows both halves. The chat capture makes a real model call, so
the answer in the picture has real citations — and a failed turn aborts
rather than writing: the first run against an exhausted key produced a
perfectly convincing screenshot of marimo's red error box, which quoted the
provider's response back, including a key-management URL carrying the key's
identifier. It was caught by looking, which is not a control. Now it is one.tests/e2e/test_read_app_tabs.py— the parity gate for promoting the tabs
read app. A copy oftest_read_app.pywith the assertions untouched:
both files'assertlines and test names are byte-identical, so passing one
and passing the other is the same claim. Every mechanical difference follows
from one fact — the tabs app renders only the active tab — so chat tests
switch to 💬 Chat first, where the grid app shows every panel at once. Both
suites now pass 7/7 against a live LLM. That clears the gate ROADMAP.md set
for deletingmarimo/read_app.pyand its suite.
Fixed
-
The "needs a model" skip message speaks to whoever reads it. When lint
finds astaleormissing_conceptissue and the repair pass was given no
model — the default after every ingest — it logs a skip. That skip used to read
"LLM client required for 'stale' repair — pass llm_client": accurate, and no
use to its only audience, who is looking at the ingest app's Activity Log where
there is no argument to pass and two buttons that do the job. It now names
those buttons. Two tests pin it, one of them checking the button names against
ingest_app.pyitself, since nothing in code reads those strings and they
could otherwise drift apart unnoticed. -
Injected passages are labelled by page, and their front-matter is dropped.
With pre-retrieval on, every curated block reaching the model was labelled
[/wiki/concepts/]— the folder, identical for all six — so in the one mode
whose prompt asks the model to cite, it could not tell the blocks apart.
filenamewas already in the search row and unused. Each block also carried
its YAML front-matter (type,tags,sources) as if it were prose: 8% of
the injected context on the shipped demo, and metadata rather than text to
answer from — the reasonretrieve_collection_pagesalready stripped it on
its own path. The two are fixed together on purpose: that front-matter's
sources:line was in practice what the model cited from, so removing it
before the label identified the page would have taken the attribution away and
put nothing back. Four tests; the existing fixture had foldedpathand
filenameinto one string, which is why the suite never saw the defect. -
Stop words are per language, which unblocks the Tier-2 fallback. The list
of ubiquitous words dropped before building the full-text query
(preretrieval.py) held only Spanish entries. In an English wikithe— three
characters, past the length filter — reached FTS5 and matched nearly every
chunk, sowiki_hitswas never empty; and becausedoc_hitsis computed only
whenwiki_hitsis empty, the raw-source tier could not be reached at all.
An off-topic question also had six unrelated curated chunks injected rather
than none. Now_STOPWORDSis keyed by ISO code and the wiki's language
selects the set; adding a language means adding an entry, documented at the
constant and inworkflows.md§6.7. The sets stay separate rather than merged
on purpose: Spanishsonis an English content word appearing in six chunks of
the fairy-tale corpus, and one shared list would drop the key word of "Who is
the king's son?". Measured on the shipped demo: "What is the capital of
France?" went from 6 injected chunks to 0, and "Tell me about Cinderwench" —
an ingest-generated alias no curated page mentions — now reaches Tier 2 with 4
source fragments, which was unreachable before. -
thin_pagefindings were reported asUnknown check type. The check has no
automatic repair on purpose — it says the wiki under-covers a source, and the
choice between expanding the page and accepting the Tier-2 fallback is a human
one — but it was missing from_ADVISORY_CHECKS, so every ingestion log that
hit it printed what looked like an internal error, including the walkthrough
appendix that ships with the project. A new test now asserts that every check
lint can emit is either repairable or declared advisory, so the next one added
cannot reopen the gap. -
Ingestion-walkthrough figures can no longer go stale unnoticed. The appendix
is regenerated by really running the pipeline, so the model picks different
concept names and link counts every time and silently invalidates the prose
quoting them.tests/unit/test_docs_ingestion_acts.pycompares the two and
names the figure to fix. -
Summary pages had no front-matter at all.
build_summary_pageis pure code and
never emitted a block, so every summary in every shipped wiki was missing one. They
have one now, which also makes a generated wiki OKF-conformant end to end. -
A rollback no longer claims a source it does not have. Restoring a page after a
failed ingest used to keep the filename that ingest had added, because sources only
ever accumulated. Restores are now authoritative. -
Three prompts wrapped existing page content in
---while that content itself
started with---, leaving the model to guess where the delimiters ended. -
A ticked wiki can answer questions about itself. With pre-retrieval on, a
question about the collection as a whole — "what is in this wiki?", "compare all
of them" — was refused, because coverage is derived from concept-page names and
such a question names no concept. Widening that list does not help and was
measured not to: no roster of item names can contain a question about the
collection.wiki/overview.mdandwiki/index.mdexist to describe it, and are
now injected directly when the question is shaped that way — they carry no
documentsrow, so search could never have reached them. The branch cannot fire
without a page to inject, so a wiki with neither refuses exactly as before.
What's Changed
- Write wiki-page front-matter in code, in OKF shape by @Clod in #14
- Make the pre-retrieval switch discoverable from the config templates by @Clod in #15
- Answer questions about the collection under pre-retrieval by @Clod in #16
- Page deletion, pre-retrieval fixes, and the two walkthroughs by @Clod in #17
- Line-by-line review of both walkthroughs by @Clod in #18
- Repair two broken passages in the ingestion walkthrough by @Clod in #19
- Four repairs in the ingestion walkthrough, found by reading it cold by @Clod in #20
- Nine smaller repairs in the ingestion walkthrough by @Clod in #21
- Define "contract" where the word first appears by @Clod in #22
- Spec the three contracts the page-deletion work introduced by @Clod in #23
- Fix the three first-run defects a full review found by @Clod in #25
- Five defects a review of master reproduced, with the tests that pin them by @Clod in #26
- Wiki rollback: git markdown + DB snapshot cache + reindex floor (design) by @Clod in #7
- Count a citation written as a markdown link once by @Clod in #27
- docs: the manual in one directory, §6 one file per workflow by @Clod in #28
- feat(tracing): one OpenTelemetry span per diagram node, for chat and ingestion by @Clod in #29
- fix(chat): Tier 1 carries the dataset the question names by @Clod in #30
- feat(search): Snowball stemming per wiki language for search, gate and Tier 2 by @Clod in #31
- feat(chat): pre-retrieval refusals state their cause and suggest pages by @Clod in #32
- refactor(app): move the application logic out of the marimo cells into base/services by @Clod in #33
Full Changelog: v0.3.0...v0.4.0