# 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.md`
- `almanac show sqlite-indexer`
- `almanac show wikilink-syntax`
- `almanac show almanac-serve`
- `almanac show wiki-organization-primitives`
- `almanac show documenting-software-architectures`
- `almanac 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-125` reads the page markdown file to return body text.
- `src/cli/commands/show.ts:83-101` opens the index and calls `getPageView()`, so `almanac show` reads page content through the shared query helper.
- `src/viewer/api.ts:152-165` also calls `getPageView()`, so viewer page rendering reads markdown bodies.
- `src/wiki/health/index.ts:400-409`, `431-437`, `468-478`, `484-498`, and `544-570` read page files for source/citation and empty-page checks.
- `src/cli/register-query-commands.ts:169-178` registers `health --fix` under query commands, and `src/wiki/health/index.ts:96-105` can apply deterministic frontmatter fixes.
- `src/cli/register-query-commands.ts:202-209` registers `list --drop`, which writes the global registry through `src/cli/commands/list.ts:56-67`.
- Every query command in `src/cli/register-query-commands.ts:24-31`, `76-91`, `138-165`, `187-198`, and `212-217` can call `autoRegisterIfNeeded()`, which can write a registry entry at `src/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`, and `health --fix`."
- Rename `registerQueryCommands()` or split it into `registerReadCommands()` and `registerRegistryCommands()` / `registerMaintenanceCommands()` so `serve`, `health --fix`, and `list --drop` are 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.ts` for FTS builders and file-mention normalization.
- `src/wiki/query/page-view.ts` for one full page projection.

Most actual read projections still live in command or viewer modules:

- `src/cli/commands/search.ts:96-255` builds archive, topic, mention, freshness, orphan, and FTS SQL directly.
- `src/cli/commands/search.ts:238-252` attaches topics in a second pass.
- `src/viewer/api.ts:330-352` defines `recentPagesSql()`, `pagesBySlugSql()`, and `pageSearchSql()`.
- `src/viewer/api.ts:355-370` defines `pageSummaries()` and attaches topics in its own second pass.
- `src/viewer/api.ts:438-464` rebuilds file-mention SQL and params from the same shared `buildFileMentionFilter()` primitive used by CLI search.
- `src/cli/commands/topics/read.ts:17-51` owns topic page-list SQL for the CLI.
- `src/viewer/api.ts:169-214` owns a separate topic detail projection for the viewer.
- `src/viewer/api.ts:406-436` owns topic summary SQL and parent attachment.
- `src/cli/commands/topics/list.ts:23-42` owns 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.ts` with a `PageSummary` type, `recentPages()`, `pagesBySlug()`, `searchPages()`, and `pagesMentioningPath()`.
- Add `src/wiki/query/topics.ts` with `topicSummaryList()`, `topicDetail()`, `pagesDirectlyTagged()`, and `pagesForSubtree()`.
- 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.ts` responsible 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-19` imports review-store concerns.
- `src/viewer/api.ts:22-28` imports job/run concerns.
- `src/viewer/api.ts:29-30` imports config and connector status.
- `src/viewer/api.ts:82-92` exposes `overview`, `page`, `topic`, `search`, `suggest`, `file`, `review`, `connections`, `jobs`, and `job` in one `ViewerApi`.
- `src/viewer/server.ts:124-142` routes `/review`, `/connections`, `/jobs`, and `/jobs/:runId` beside wiki graph routes.
- `src/viewer/global-api.ts:36-67` builds a global registry-backed viewer API over all registered wikis.
- `src/cli/commands/serve.ts:9-18` accepts `cwd` but does not use it; `startViewerServer()` creates a global API at `src/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.ts` into smaller APIs: `wiki-api.ts`, `jobs-api.ts`, `review-api.ts`, and `connections-api.ts`.
- Make `server.ts` compose those APIs at the routing layer.
- Decide whether `almanac serve` is "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 from `runServe()`.
- Consider warning when `--host` is 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-24` says health owns checks and deterministic repairs.
- `collectHealthReport()` assembles thirteen categories at `src/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 at `src/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 imports `parseFrontmatter()`.
- `applyHealthFixes()` and `fixLegacySourceFrontmatter()` run writes at `src/wiki/health/index.ts:96-105` and `612-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.ts` or `src/wiki/maintenance/source-frontmatter.ts`.
- Keep `src/wiki/health/index.ts` as the report composition layer.
- Reuse `parseFrontmatter(raw).body` or a shared frontmatter stripper for empty-page detection instead of a second regex.
- Keep `health --fix` explicitly 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-51` implements topic page-list SQL.
- `src/cli/commands/topics/show.ts:36-66` implements topic metadata, parent, and child queries.
- `src/cli/commands/topics/list.ts:23-42` implements topic list/count queries.
- `src/viewer/api.ts:169-214` implements a viewer topic detail projection.
- `src/viewer/api.ts:406-436` implements viewer topic summary and root-topic queries.
- `src/wiki/topics/dag.ts:62-90` is 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-5` re-exports `toKebabCase` from the registry facade for compatibility, while production registry code imports from `src/slug.ts` directly. The only current use found is `test/registry.test.ts`.
- `src/wiki/registry/store.ts:116-121` defines `pathsEqual()`, while `src/wiki/registry/autoregister.ts:108-118` duplicates the same logic as `samePath()` 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 has `page_sources` (`src/wiki/indexer/schema.ts:71-83`).
- Ingest has source refs (`src/ingest/source-ref.ts`).
- The CLI has `almanac source github issue|pr` registered at `src/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

1. Clarify the invariant in `CLAUDE.md` / `MANUAL.md`: separate read commands, organization commands, lifecycle AI commands, derived-state writes, and page-prose writes.
2. Extract shared page and topic read projections into `src/wiki/query/pages.ts` and `src/wiki/query/topics.ts`.
3. Split `src/viewer/api.ts` into wiki, jobs, review, and connections APIs, with `server.ts` composing routes.
4. Move source/citation health helpers and the legacy frontmatter fixer out of `src/wiki/health/index.ts` into a source-maintenance module.
5. Remove the registry `toKebabCase` compatibility re-export after tests move, and centralize path equality inside registry internals.

