Wiki Storage / Query Boundary Audit
Repo root: /Users/rohan/Desktop/Projects/codealmanac
Scope: src/wiki, src/viewer, and CLI command boundaries for search, show, tag, source, health, topics, and list.
Read first:
MANUAL.md.almanac/README.mdalmanac show sqlite-indexeralmanac show wikilink-syntaxalmanac show almanac-servealmanac show wiki-organization-primitivesalmanac show documenting-software-architecturesalmanac show accidental-special-case-architecture.claude/agents/review.md
No source code was modified during this audit.
Overall Take#
The storage/indexer core is mostly coherent. src/wiki/indexer/ owns markdown-to-SQL projection, path normalization, source projection, wikilink classification, schema creation, and implicit freshness. That matches the wiki's intended architecture.
The weak boundary is the read/query layer. src/wiki/query/ is too small for the amount of read projection the product now exposes. As a result, src/cli/commands/search.ts, src/cli/commands/topics/*.ts, and src/viewer/api.ts each assemble their own SQL result shapes. The viewer is also no longer just a wiki viewer; it has become a local console for global registry browsing, review decisions, connector status, and job observability.
The "pure query" invariant does not hold literally. It holds only if interpreted as: non-lifecycle commands do not invoke AI and do not write page prose. Query-looking commands still refresh index.db, may auto-register the repo in ~/.almanac/registry.json, may read markdown bodies for show and viewer rendering, and some commands registered under query have explicit write modes.
Findings#
1. Restructure: The CLI Pure-Query Invariant Is Too Broad For The Code#
CLAUDE.md:97 says "The CLI never reads or writes page content" and that every other command operates on index.db and the filesystem. MANUAL.md:149-150 says everything except capture and bootstrap is pure query/organization over SQLite.
The code contradicts the literal wording:
src/wiki/query/page-view.ts:122-125reads the page markdown file to return body text.src/cli/commands/show.ts:83-101opens the index and callsgetPageView(), soalmanac showreads page content through the shared query helper.src/viewer/api.ts:152-165also callsgetPageView(), so viewer page rendering reads markdown bodies.src/wiki/health/index.ts:400-409,431-437,468-478,484-498, and544-570read page files for source/citation and empty-page checks.src/cli/register-query-commands.ts:169-178registershealth --fixunder query commands, andsrc/wiki/health/index.ts:96-105can apply deterministic frontmatter fixes.src/cli/register-query-commands.ts:202-209registerslist --drop, which writes the global registry throughsrc/cli/commands/list.ts:56-67.- Every query command in
src/cli/register-query-commands.ts:24-31,76-91,138-165,187-198, and212-217can callautoRegisterIfNeeded(), which can write a registry entry atsrc/wiki/registry/autoregister.ts:55-62.
The behavior is mostly defensible. show cannot print a page body without reading markdown, implicit reindex is a documented derived-state write, and auto-registration is a stated registry policy. The problem is the invariant's wording. Future agents reading it can either over-restrict useful read behavior or miss the fact that "query" commands have allowed side effects.
Recommended shape:
- Restate the invariant as: "Only lifecycle operations invoke AI or write page prose. Read commands may refresh derived local index state and read committed markdown for display/validation. Organization commands may rewrite wiki metadata through explicit verbs such as
tag,topics,review, andhealth --fix." - Rename
registerQueryCommands()or split it intoregisterReadCommands()andregisterRegistryCommands()/registerMaintenanceCommands()soserve,health --fix, andlist --dropare not hidden under a pure-query label. - Keep
ensureFreshIndex()as the common read-side entrypoint; the issue is the contract language, not the mechanism.
Why it matters: invariants teach future agents where exceptions are allowed, and the current wording is stricter than the product actually is.
2. Restructure: The Shared Query Layer Is Underpowered#
src/wiki/query/ currently has only:
src/wiki/query/search.tsfor FTS builders and file-mention normalization.src/wiki/query/page-view.tsfor one full page projection.
Most actual read projections still live in command or viewer modules:
src/cli/commands/search.ts:96-255builds archive, topic, mention, freshness, orphan, and FTS SQL directly.src/cli/commands/search.ts:238-252attaches topics in a second pass.src/viewer/api.ts:330-352definesrecentPagesSql(),pagesBySlugSql(), andpageSearchSql().src/viewer/api.ts:355-370definespageSummaries()and attaches topics in its own second pass.src/viewer/api.ts:438-464rebuilds file-mention SQL and params from the same sharedbuildFileMentionFilter()primitive used by CLI search.src/cli/commands/topics/read.ts:17-51owns topic page-list SQL for the CLI.src/viewer/api.ts:169-214owns a separate topic detail projection for the viewer.src/viewer/api.ts:406-436owns topic summary SQL and parent attachment.src/cli/commands/topics/list.ts:23-42owns another topic summary/count projection.
Some divergence is intentional: CLI search uses token-prefix FTS (src/cli/commands/search.ts:220-231), while viewer submitted search uses quoted term FTS and suggestions use quoted prefix FTS (src/viewer/api.ts:217-237). That should stay explicit. The problem is that pagination, archive filtering, page summary shape, topic attachment, file mention matching, and topic counts are not centralized.
Recommended shape:
- Add
src/wiki/query/pages.tswith aPageSummarytype,recentPages(),pagesBySlug(),searchPages(), andpagesMentioningPath(). - Add
src/wiki/query/topics.tswithtopicSummaryList(),topicDetail(),pagesDirectlyTagged(), andpagesForSubtree(). - Keep FTS query expression builders in
src/wiki/query/search.ts, but have both CLI and viewer pass their chosen expression into shared page-query functions. - Leave command files responsible for flags and text formatting only.
- Leave
src/viewer/api.tsresponsible for API payload assembly only.
Why it matters: viewer and CLI are already projecting the same graph differently, and the next schema or archive/filter change will need synchronized edits in several places.
3. Restructure: src/viewer/api.ts Is A Mixed Local Console, Not Just A Wiki API#
The viewer page says almanac serve is a local read-only wiki viewer. The current API is broader:
src/viewer/api.ts:14-19imports review-store concerns.src/viewer/api.ts:22-28imports job/run concerns.src/viewer/api.ts:29-30imports config and connector status.src/viewer/api.ts:82-92exposesoverview,page,topic,search,suggest,file,review,connections,jobs, andjobin oneViewerApi.src/viewer/server.ts:124-142routes/review,/connections,/jobs, and/jobs/:runIdbeside wiki graph routes.src/viewer/global-api.ts:36-67builds a global registry-backed viewer API over all registered wikis.src/cli/commands/serve.ts:9-18acceptscwdbut does not use it;startViewerServer()creates a global API atsrc/viewer/server.ts:22-28.
This does not currently violate the no-AI rule or write page content. It does make the name "local read-only wiki viewer" less true. A user binding --host away from localhost is not just serving the current repo's wiki; they are exposing a registry-backed local console over reachable registered wikis and run records.
Recommended shape:
- Split
src/viewer/api.tsinto smaller APIs:wiki-api.ts,jobs-api.ts,review-api.ts, andconnections-api.ts. - Make
server.tscompose those APIs at the routing layer. - Decide whether
almanac serveis "current wiki viewer" or "local Almanac console." If it is global, rename help text and docs accordingly. If it should be current-repo scoped, resolve and pass the repo root fromrunServe(). - Consider warning when
--hostis not localhost, because the data exposed is broader than a single rendered wiki page.
Why it matters: the viewer's code shape now hides product scope decisions in one API file instead of making them explicit.
4. Fix: Health Owns Too Much Source/Markdown Cleanup Logic#
src/wiki/health/index.ts is no longer only graph integrity checks. It also owns source citation checks and fix orchestration:
- The module comment at
src/wiki/health/index.ts:19-24says health owns checks and deterministic repairs. collectHealthReport()assembles thirteen categories atsrc/wiki/health/index.ts:76-90.- Source checks read and parse markdown repeatedly at
src/wiki/health/index.ts:328-498. citationsForFile()contains the citation syntax regex atsrc/wiki/health/index.ts:484-498.- Empty-page detection strips frontmatter with a local regex at
src/wiki/health/index.ts:550-552, even though this module already importsparseFrontmatter(). applyHealthFixes()andfixLegacySourceFrontmatter()run writes atsrc/wiki/health/index.ts:96-105and612-628.- The actual writer lives in
src/wiki/health/legacy-frontmatter-fix.ts:88-96.
The current fix is bounded and documented: legacy files: / string sources: migration is deterministic and only exposed through health --fix. That exception is acceptable while it is the only repair. The issue is direction of travel. Health is becoming a report/fixer/source-parser module, and it already has duplicate frontmatter stripping.
Recommended shape:
- Move source/citation health helpers into
src/wiki/sources/health.tsorsrc/wiki/maintenance/source-frontmatter.ts. - Keep
src/wiki/health/index.tsas the report composition layer. - Reuse
parseFrontmatter(raw).bodyor a shared frontmatter stripper for empty-page detection instead of a second regex. - Keep
health --fixexplicitly narrow; do not let it become a general wiki-editing surface.
Why it matters: health is the place users trust for diagnosis, and bundling diagnosis, parsing, and repair makes future repair behavior easier to add casually.
5. Fix: Topic Read Queries Belong In src/wiki/query, Not CLI Command Files#
Topic write ownership is reasonably clear. src/wiki/topics/yaml.ts owns topics.yaml, src/wiki/topics/frontmatter-rewrite.ts owns page-topic frontmatter rewrites, and mutating commands mostly sequence validation and writes.
Topic read ownership is less clear:
src/cli/commands/topics/read.ts:17-51implements topic page-list SQL.src/cli/commands/topics/show.ts:36-66implements topic metadata, parent, and child queries.src/cli/commands/topics/list.ts:23-42implements topic list/count queries.src/viewer/api.ts:169-214implements a viewer topic detail projection.src/viewer/api.ts:406-436implements viewer topic summary and root-topic queries.src/wiki/topics/dag.ts:62-90is the only shared read primitive for descendants/subtree.
Recommended shape:
- Move topic list/detail/page-list projections into
src/wiki/query/topics.ts. - Keep
src/wiki/topics/focused on topic storage, DAG mechanics, and frontmatter mutation. - Keep
src/cli/commands/topics/*focused on parsing options and formatting command output.
Why it matters: topic page counts and descendant behavior are a trust surface. The code already comments about count consistency in src/cli/commands/topics/list.ts:28-32; the same consistency should be enforced by shared code, not comments across modules.
6. Polish: Registry And Source Naming Have Residue#
Registry storage is mostly clean, but two small residues are worth removing:
src/wiki/registry/index.ts:1-5re-exportstoKebabCasefrom the registry facade for compatibility, while production registry code imports fromsrc/slug.tsdirectly. The only current use found istest/registry.test.ts.src/wiki/registry/store.ts:116-121definespathsEqual(), whilesrc/wiki/registry/autoregister.ts:108-118duplicates the same logic assamePath()with a comment explaining the duplication.
The duplication is small, but path equality is a registry invariant. Keeping it private in two files is more brittle than exporting a small internal helper such as src/wiki/registry/path-equality.ts.
The term source is also overloaded:
- Frontmatter has
sources:and the index haspage_sources(src/wiki/indexer/schema.ts:71-83). - Ingest has source refs (
src/ingest/source-ref.ts). - The CLI has
almanac source github issue|prregistered atsrc/cli/register-setup-commands.ts:139-184.
The command is not wrong, but "source" now means evidence, external object, and ingest target depending on context. external-source, connector-source, or github-source would be clearer if this surface grows.
Why it matters: these are small examples of compatibility and naming drift. They are not urgent, but they teach future contributors that local exceptions are acceptable.
Boundary Answers#
Does The CLI Pure-Query Invariant Hold?#
No, not literally. Query-looking commands can refresh index.db, auto-register the current repo, read page markdown bodies, and in explicit modes mutate metadata or registry state. The stronger invariant should be narrowed to "no AI and no page-prose writes outside lifecycle operations; deterministic metadata organization is explicit."
Are Storage, Query, And Domain Responsibilities Clear?#
Partially. src/wiki/indexer/ and src/wiki/registry/ are understandable. src/wiki/topics/ is clear on writes but incomplete on read projections. src/wiki/query/ is underdeveloped, so query/domain knowledge leaks into CLI and viewer code.
Does Viewer Code Duplicate Query Projections?#
Yes. src/viewer/api.ts duplicates page summary, FTS result, file mention, topic summary, and topic detail SQL that should live under src/wiki/query/.
Does Health Have Cleanup Logic That Belongs Elsewhere?#
Yes, but it is currently bounded. The legacy-source fixer can stay as a narrow exception, but the source/citation health checks and fix orchestration deserve their own maintenance/source module before more repairs accrete.
Is Naming Simple Or Muddy?#
Mostly simple in the indexer and topics storage code. Muddy areas are query as a command grouping, viewer as a global local console, and source as evidence/ingest/external-object terminology.
Recommended Refactor Sequence#
- Clarify the invariant in
CLAUDE.md/MANUAL.md: separate read commands, organization commands, lifecycle AI commands, derived-state writes, and page-prose writes. - Extract shared page and topic read projections into
src/wiki/query/pages.tsandsrc/wiki/query/topics.ts. - Split
src/viewer/api.tsinto wiki, jobs, review, and connections APIs, withserver.tscomposing routes. - Move source/citation health helpers and the legacy frontmatter fixer out of
src/wiki/health/index.tsinto a source-maintenance module. - Remove the registry
toKebabCasecompatibility re-export after tests move, and centralize path equality inside registry internals.