Source Provenance Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Make sources: the canonical provenance field for Almanac pages, derive file mentions from file sources, and give agents explicit guidance for source-backed claims and readable links.
Architecture: Page frontmatter will use structured sources: entries with stable IDs, typed source metadata, and optional citation markers in prose. A dedicated source-normalization module will convert current and legacy frontmatter into one internal source model, then the indexer will derive file_refs from normalized file sources plus inline file wikilinks. Prompt and manual guidance will treat sources as evidence and links as navigation.
Tech Stack: TypeScript, js-yaml, better-sqlite3, Vitest, Markdown/YAML frontmatter.
Product Decisions#
sources:is the canonical provenance field.files:remains temporarily supported as legacy input, but new writer guidance should prefersources:.file_refsremains the index table used byalmanac search --mentions,health dead-refs,show --files, and the viewer.- Legacy compatibility must be isolated in
src/indexer/page-sources.tsor a sibling module, not scattered throughsrc/indexer/index.ts. - The indexer derives
file_refsfrom:- structured
sources:entries withtype: file - legacy
files:entries during migration - inline file and folder wikilinks such as
[[src/indexer/schema.ts]]
- structured
- Source entries use stable IDs so prose can cite them with
[@source-id]. - Citation validation starts as
healthwarnings, not hard publish failures. - Web sources are supported as metadata in page frontmatter, not fetched or archived in this slice.
- Deterministic migration belongs in
almanac health --fix, not in Garden and not in update-time side effects. - Link style guidance belongs in the same manual/prompt pass because source trust and graph navigation are the same product surface.
Source Entry Shape#
Use this canonical frontmatter form:
sources:
- id: capture-command
type: file
path: src/commands/capture.ts
note: Starts capture and records run metadata.
- id: writer-prompt
type: file
path: prompts/writer.md
note: Defines the Absorb writing contract.
- id: claude-agent-sdk-docs
type: web
url: https://docs.anthropic.com/
title: Claude Agent SDK documentation
retrieved_at: 2026-05-28
note: Documents SDK behavior used by the Claude provider.Required fields:
id: kebab-case page-local source ID.type: source type.note: one sentence explaining what the source supports.
Type-specific required fields:
file:pathweb:urlcommit:revpr:urlornumberconversation:pathorrun_idwiki:slugmanual:note
Supported source types for this slice:
filewebcommitprconversationwikimanual
The parser should ignore unknown source types for indexing, but health can report malformed known types.
Compatibility Boundary#
The old model had two fields:
files:
- src/commands/capture.ts
sources:
- https://example.com/docsThe new model has one field:
sources:
- id: capture
type: file
path: src/commands/capture.ts
note: Migrated from legacy files.
- id: example-docs
type: web
url: https://example.com/docs
retrieved_at: 2026-05-28
note: Migrated from legacy sources.Compatibility is a temporary parsing and rewrite concern, not the conceptual model. Keep all legacy handling in one module so it is easy to delete after the migration window:
// src/indexer/page-sources.ts
export function normalizePageSources(input: {
sources: FrontmatterSource[];
legacyFiles: string[];
legacySourceStrings: string[];
}): NormalizedPageSources;src/indexer/index.ts should operate on normalized pageSources and derived fileRefs. It should not separately loop over fm.files and fm.sources in multiple places.
Citation Shape#
Use citation markers in prose:
Capture starts a background Absorb job after supported sessions. [@capture-command]Rules:
- A citation marker must match a
sources[].idin the same page. - Multiple citations are allowed after one sentence.
- Citations are evidence, not navigation. Use wikilinks for navigation.
- Claims may cite file, web, commit, PR, conversation, wiki, or manual sources.
Link Style Shape#
Readable prose should prefer display text:
Capture updates pages through [[absorb-operation|the Absorb operation]].Use slug-only links when the slug is already readable in context:
See [[capture-flow]].The manual should say: link concepts, not words. Link the first meaningful mention of load-bearing concepts, related anchors, adjacent systems, constraints, workflows, and source files.
Task 1: Parse Structured Sources#
Files:
- Modify:
src/indexer/frontmatter.ts - Test:
test/frontmatter.test.ts
Step 1: Write failing parser tests
Add tests that assert:
- structured
sources:entries are parsed - legacy string
sources:entries are parsed separately for compatibility filesources keepid,type,path, andnotewebsources keepid,type,url,title,retrieved_at, andnote- malformed entries are ignored rather than throwing
- legacy
files:still parses
Example test body:
const fm = parseFrontmatter(`---
title: Capture
sources:
- id: capture-command
type: file
path: src/commands/capture.ts
note: Starts capture.
- id: sdk-docs
type: web
url: https://example.com/docs
title: SDK Docs
retrieved_at: 2026-05-28
note: External behavior.
- https://legacy.example.com/docs
files:
- src/legacy.ts
---
Body.`);
expect(fm.sources).toEqual([
{
id: "capture-command",
type: "file",
path: "src/commands/capture.ts",
note: "Starts capture.",
},
{
id: "sdk-docs",
type: "web",
url: "https://example.com/docs",
title: "SDK Docs",
retrieved_at: "2026-05-28",
note: "External behavior.",
},
]);
expect(fm.files).toEqual(["src/legacy.ts"]);
expect(fm.legacySourceStrings).toEqual(["https://legacy.example.com/docs"]);Step 2: Run the test and verify it fails
Run:
npm test -- test/frontmatter.test.tsExpected: tests fail because Frontmatter has no sources field.
Step 3: Implement parser support
Add source interfaces:
export type FrontmatterSource =
| { id: string; type: "file"; path: string; note?: string }
| { id: string; type: "web"; url: string; title?: string; retrieved_at?: string; note?: string }
| { id: string; type: "commit"; rev: string; note?: string }
| { id: string; type: "pr"; url?: string; number?: string; note?: string }
| { id: string; type: "conversation"; path?: string; run_id?: string; note?: string }
| { id: string; type: "wiki"; slug: string; note?: string }
| { id: string; type: "manual"; note: string };Add sources: FrontmatterSource[] and legacySourceStrings: string[] to Frontmatter, initialize both to [], and parse obj.sources with a narrow coercion helper. Structured mapping entries go into sources; string entries go into legacySourceStrings. The helper should require id and type, then require the type-specific field. Preserve retrieved_at as a YYYY-MM-DD string if YAML parsed it as a Date.
Step 4: Run parser tests
Run:
npm test -- test/frontmatter.test.tsExpected: parser tests pass.
Task 2: Derive File Refs From Sources#
Files:
- Create:
src/indexer/page-sources.ts - Modify:
src/indexer/index.ts - Test:
test/indexer.test.ts - Test:
test/search.test.ts - Test:
test/health.test.ts
Step 1: Write failing indexer tests
Add a test page with:
sources:
- id: schema
type: file
path: src/indexer/schema.ts
note: Defines index tables.Assert that file_refs contains src/indexer/schema.ts.
Add search coverage:
almanac search --mentions src/indexer/schema.tsExpected result includes the page whose file ref came from sources:.
Add health coverage:
- existing file source should not appear in
dead_refs - missing file source should appear in
dead_refs
Step 2: Run tests and verify they fail
Run:
npm test -- test/indexer.test.ts test/search.test.ts test/health.test.tsExpected: tests fail because only files: and inline wikilinks populate file_refs.
Step 3: Implement derived file refs
Create src/indexer/page-sources.ts with the current source model and the isolated legacy compatibility layer:
import type { FrontmatterSource } from "./frontmatter.js";
export interface IndexedPageSource {
id: string;
type: FrontmatterSource["type"];
target: string;
title?: string;
retrieved_at?: string;
note?: string;
legacy: boolean;
}
export interface DerivedFileRef {
rawPath: string;
source: "structured-source" | "legacy-files";
}
export interface NormalizedPageSources {
sources: IndexedPageSource[];
fileRefs: DerivedFileRef[];
hasLegacyFrontmatter: boolean;
}Add functions:
export function normalizePageSources(input: {
sources: FrontmatterSource[];
legacyFiles: string[];
legacySourceStrings: string[];
}): NormalizedPageSources;normalizePageSources should:
- preserve structured sources with
legacy: false - convert legacy
files:entries intotype: filesources withlegacy: true - convert legacy string URL
sources:entries intotype: websources withlegacy: true - preserve ambiguous non-URL legacy source strings for health reporting, but do not index them as source rows
- derive file refs from both structured file sources and legacy file entries
- generate deterministic legacy source IDs from the basename, de-duplicated with numeric suffixes
- set
hasLegacyFrontmatterto true whenlegacyFiles.length > 0orlegacySourceStrings.length > 0
In runIndexer, include normalized page sources and derived file refs in the planned page record:
const normalizedSources = normalizePageSources({
sources: fm.sources,
legacyFiles: fm.files,
legacySourceStrings: fm.legacySourceStrings,
});Insert file refs from the normalized model:
for (const ref of p.sourceFileRefs) {
const raw = ref.rawPath;
const isDir = looksLikeDir(raw);
const path = normalizePath(raw, isDir);
const originalPath = normalizePathPreservingCase(raw, isDir);
if (path.length === 0) continue;
insertFileRef.run(p.slug, path, originalPath, isDir ? 1 : 0);
}Keep inline wikilink insertion unchanged.
Step 4: Run mention and health tests
Run:
npm test -- test/indexer.test.ts test/search.test.ts test/health.test.tsExpected: tests pass.
Removal note: normalizePageSources is the only place that should know legacy files: can become file sources. When the compatibility window closes, deleting the legacy branch in this module should be sufficient for the indexer path.
Task 3: Index Source Metadata#
Files:
- Modify:
src/indexer/schema.ts - Modify:
src/indexer/index.ts - Test:
test/indexer.test.ts
Step 1: Write failing source table tests
Assert that a parsed page inserts rows into a new page_sources table:
SELECT source_id, source_type, target, title, retrieved_at, note
FROM page_sources
WHERE page_slug = ?
ORDER BY source_idExpected rows:
- file source target is the normalized path
- web source target is the URL
- note is preserved
- retrieved date is preserved for web sources
Step 2: Run test and verify it fails
Run:
npm test -- test/indexer.test.tsExpected: test fails because page_sources does not exist.
Step 3: Add schema
Add:
CREATE TABLE IF NOT EXISTS page_sources (
page_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,
source_id TEXT NOT NULL,
source_type TEXT NOT NULL,
target TEXT NOT NULL,
title TEXT,
retrieved_at TEXT,
note TEXT,
PRIMARY KEY (page_slug, source_id)
);
CREATE INDEX IF NOT EXISTS idx_page_sources_type ON page_sources(source_type);
CREATE INDEX IF NOT EXISTS idx_page_sources_target ON page_sources(target);Bump SCHEMA_VERSION to 4. In migration, drop page_sources if needed and clear pages.content_hash so all pages reindex.
Step 4: Insert source rows
In the indexer transaction:
- delete existing
page_sourcesfor the page - insert normalized rows for each source
- use
pathas target for file sources - use
url,rev,number,run_id,slug, ornoteas target depending on type
Step 5: Run tests
Run:
npm test -- test/indexer.test.tsExpected: source table tests pass.
Task 4: Validate Citations In Health#
Files:
- Modify:
src/commands/health.ts - Modify:
src/cli/register-query-commands.ts - Create:
src/sources/frontmatter-rewrite.ts - Test:
test/health.test.ts
Step 1: Add health report categories
Add three categories:
missing_sources: citation markers with no matchingsources[].idunused_sources: source entries never cited in proselegacy_frontmatter: pages still usingfiles:or legacy string sources
Use names that are specific enough for JSON consumers:
{
"missing_sources": [
{ "slug": "capture", "source_id": "missing-id" }
],
"unused_sources": [
{ "slug": "capture", "source_id": "writer-prompt" }
],
"legacy_frontmatter": [
{ "slug": "capture", "fields": ["files"] }
]
}Step 2: Write failing health tests
Add pages:
---
topics: [x]
sources:
- id: capture-command
type: file
path: src/commands/capture.ts
note: Starts capture.
---
Claim. [@missing-source]Expected:
missing_sourcescontainsmissing-sourceunused_sourcescontainscapture-command
Add a positive test where [@capture-command] is used and neither category reports it.
Step 3: Implement citation extraction
Extract citation markers from page body with:
const CITATION_RE = /\[@([a-z0-9][a-z0-9-]*)\]/g;Compare extracted IDs against page_sources.source_id.
Step 4: Add deterministic health --fix rewrite support
Add --fix to almanac health. health --fix may rewrite wiki pages only for safe mechanical source-frontmatter fixes. It must not invoke AI and must not alter page body prose.
Create src/sources/frontmatter-rewrite.ts with a deterministic rewriter that:
- preserves body bytes exactly
- preserves unrelated frontmatter fields
- converts
files:entries into structuredsources[type=file] - converts string URL entries in legacy
sources:into structuredsources[type=web] - removes
files:only after conversion - generates deterministic source IDs
- de-duplicates source IDs with numeric suffixes
- uses conservative notes such as
Migrated from legacy files. - leaves ambiguous non-URL legacy source strings unchanged and reports them as not fixable
Do not run this on package update. Updates may report that legacy frontmatter exists, but they must not create surprise project diffs.
Step 5: Update JSON shape tests
Update health JSON shape snapshots to include missing_sources, unused_sources, and legacy_frontmatter.
Step 6: Run health tests
Run:
npm test -- test/health.test.tsExpected: health tests pass.
Task 5: Display Sources In Show And Viewer#
Files:
- Modify:
src/query/page-view.ts - Modify:
src/commands/show.ts - Modify:
src/viewer/api.ts - Test:
test/show.test.ts - Test:
test/viewer-api.test.ts
Step 1: Extend page view query tests
Assert that page records include sources with source ID, type, target, title, retrieved date, and note.
Step 2: Update shared page view query
Query page_sources by page slug and return ordered source records.
Step 3: Update almanac show
When metadata is displayed, show a compact source summary:
sources: capture-command (file: src/commands/capture.ts), sdk-docs (web)Do not dump long notes in the default header. Full source notes remain in page frontmatter/body.
Step 4: Update viewer API
Include sources in the page JSON response so the UI can render them later.
Step 5: Run tests
Run:
npm test -- test/show.test.ts test/viewer-api.test.tsExpected: tests pass.
Task 6: Update Writer Guidance And Manual Home#
Files:
- Modify:
prompts/base/syntax.md - Create:
docs/manual/good-codebase-wikis.md - Test: no automated test required
Step 1: Update prompt syntax guidance
Change frontmatter guidance from "files: plus prompt-level sources:" to "sources: as provenance, with legacy files: still accepted."
New guidance should say:
- use
sources:for all evidence - use
type: filefor repo files, tests, migrations, prompts, and configs - cite non-obvious claims with
[@source-id] - use
[[slug|readable text]]when slug-only links interrupt sentence flow - do not cite sources the agent did not inspect
- code is current truth; conversations and old PRs are historical unless verified against current code
Step 2: Create the manual seed
Create docs/manual/good-codebase-wikis.md as the first manual page. Keep it short and readable. It should define:
- sources make pages trustworthy
- links make pages navigable
- page names make pages readable
- subject neighborhoods make large subjects understandable
- source entries must explain relevance
- links should use readable display text in prose
This is not the full manual. It is the stable home for the doctrine we are actively turning into product behavior.
Step 3: Run markdown-facing checks
Run:
npm testExpected: all tests pass.
Task 7: Migration And Compatibility#
Files:
- Modify:
prompts/operations/build.md - Modify:
prompts/operations/absorb.md - Modify:
prompts/operations/garden.md - Modify:
prompts/base/syntax.md - Test: prompt/manual review
Step 1: Keep legacy pages working
Do not require existing pages to migrate from files: immediately. The parser and indexer must continue supporting files:.
Step 2: Bias new pages toward sources:
Update operation prompts so new or substantially edited pages use structured sources: entries.
Step 3: Teach the deterministic migration surface
Update prompts and manual guidance so agents know:
- query commands read both old and new source formats
- new writing should emit only structured
sources: - deterministic cleanup is
almanac health --fix - Garden may recommend or run
almanac health --fixwhen the task is wiki maintenance, but Garden is not the migration engine - package update must not rewrite wiki files
Step 4: Document the fixed rewrite
The deterministic fixer migrates this:
files:
- src/foo.ts
sources:
- https://example.comto:
sources:
- id: foo
type: file
path: src/foo.ts
note: Migrated from legacy files.
- id: external-docs
type: web
url: https://example.com
retrieved_at: 2026-05-28
note: Migrated from legacy sources.It does not add [@source-id] citations, improve source notes, or decide which claims a source supports. Those are agent/human work.
Verification#
Run the full suite:
npm testRun a manual smoke test:
npm run build
node dist/bin/codealmanac.js search --mentions src/indexer/schema.ts
node dist/bin/codealmanac.js health --json
node dist/bin/codealmanac.js show wiki-organization-primitivesExpected:
- existing
files:pages still show up in--mentions sources[type=file]pages show up in--mentionsalmanac healthreports legacy frontmatteralmanac health --fixrewrites safe legacy frontmatter without changing body prose- missing file sources appear in
dead_refs - missing citation IDs appear in
missing_sources - uncited source IDs appear in
unused_sources - page display includes source summaries without making prose unreadable
Open Questions#
- Should
unused_sourceswarn for every source, or only for non-file sources? File sources can be useful for--mentionseven when not cited in prose. - Should
retrieved_atbe required for web sources immediately, or only warned by Garden? - Should web sources eventually be copied into
.almanac/sources/with snapshots, or is URL metadata enough for V1? - Should legacy
files:be documented only under compatibility notes, or omitted from normal user-facing syntax docs? - Should citation markers be rendered specially in
almanac serve, or left as normal Markdown text for now? - Should
health --fixrequire a clean working tree for.almanac/pages/, or should it operate file-by-file and trust Git review?
Commit Plan#
feat(sources): parse page source metadatafeat(sources): derive file refs from file sourcesfeat(sources): index source metadatafeat(sources): validate citation markersfeat(health): fix legacy source frontmatterdocs: add codebase wiki manual seed