provozni zaloha

This commit is contained in:
lachtan
2026-06-24 08:11:12 +02:00
parent 9295dba19f
commit 1db3ec4756
97 changed files with 7698 additions and 817 deletions

View File

@@ -0,0 +1,76 @@
# Architecture
This document explains the three-layer / three-operation architecture in detail. The main `SKILL.md` summarises it; this is the reference you reach for when you need to know *why* a design decision exists or how to handle an edge case.
## The three layers
### Raw sources
Raw sources are the user's curated input material. They live in `cml/raw/` (or wherever the project's `SCHEMA.md` declares). They are **immutable** — the LLM reads from them but never modifies them. This immutability is load-bearing: it means the wiki can always be re-derived from the raw sources if it gets corrupted, and it gives the user a stable ground truth they can audit independently.
What goes in `cml/raw/`: PDFs, web articles converted to markdown (the Obsidian Web Clipper is one popular path), transcripts, code repos, dataset descriptions, screenshots, hand-typed notes the user wants the wiki to incorporate. What does *not* go in `cml/raw/`: anything the LLM generated — that all belongs in the wiki layer.
A useful convention is one source per file (or per directory if the source has multiple pieces, e.g. a paper plus its appendix), with a slugified filename that ends up matching the wiki's source-summary page name. This makes the back-pointer from the wiki to the raw source trivial.
### The wiki
The wiki is a directory of LLM-generated markdown files. The LLM owns this layer entirely — it creates pages, updates them when new sources arrive, maintains cross-references, and keeps everything consistent. The user reads it (typically through Obsidian, but any markdown viewer works); the LLM writes it.
The wiki directory is conventionally split into subdirectories by **page type**:
- `cml/wiki/sources/` — one summary page per ingested source. Captures what the source said, in the LLM's words, with a citation back to the raw file. These are append-mostly: you write one when you ingest a source and rarely modify it after.
- `cml/wiki/entities/` — pages about specific things: people, products, papers, places, companies, events. Anything that has a proper noun or could plausibly be a Wikipedia article subject. These accumulate updates as new sources mention the entity.
- `cml/wiki/concepts/` — pages about ideas, methods, frameworks, abstractions. These are the most heavily cross-referenced pages and the ones most likely to evolve as understanding deepens.
- `cml/wiki/synthesis/` — cross-cutting analyses, comparisons, query answers filed back. This is where exploration compounds: a comparison the user asked for becomes a page the next query can build on.
`SCHEMA.md` may declare additional types (e.g. `cml/wiki/decisions/` for an engineering team, `cml/wiki/characters/` for a fan wiki, `cml/wiki/experiments/` for a research lab). Adding a new type costs nothing — make the directory, document it in the schema, update the index template.
### The schema
`SCHEMA.md` lives at the wiki root and is **the configuration file** that turns a generic LLM into a disciplined wiki maintainer for *this specific* knowledge base. It documents the page types in use, the tag taxonomy, the naming conventions, any custom workflow steps, and the user's stylistic preferences (e.g. "always include a 'Why this matters' section on concept pages", "never use bullet lists in summaries").
The schema is **co-evolved** with the user. On bootstrap it starts from the default template in `assets/SCHEMA.md.template`, but every user will customize it as they discover what fits their domain. When you notice a recurring pattern in the user's edits or feedback that isn't in the schema, propose adding it. When the schema starts contradicting itself or growing unwieldy, propose pruning it.
The schema is the first file you read when entering an existing wiki. Its conventions override the defaults documented in the skill.
### The graph (optional, compiled)
The wiki may carry a fourth layer at `cml/wiki/graph/`: a compiled, queryable view of the typed `graph:` metadata in page frontmatter and the body wikilinks. It contains a hand-edited `ontology.yaml` (the contract: which node types and predicates exist) plus generated artifacts (`nodes.jsonl`, `edges.jsonl`, `graph.sqlite`, `graph.graphml`) produced by `wiki_graph_extract.py`. **Markdown is canonical**; the graph can be deleted and regenerated from the markdown without losing knowledge.
Its purpose is to make typed, provenance-backed relationships machine-queryable — "who founded what", "what does Konvy depend on", "shortest path from A to B" — without giving up the editability and human-legibility of markdown. Typed edges require an explicit `source` (a source-page slug) and `evidence` quote; the extractor never invents them. Plain `[[wikilinks]]` in the body produce low-confidence `mentions` edges, which are useful for navigation but not for evidence.
Use the graph layer when the user's questions are predominantly relational and the cost of maintaining typed metadata is paying for itself. Skip it for purely textual wikis. Full reference: `graph-workflow.md`.
## The three operations
### Ingest
A new source has arrived. The user has dropped a file in `cml/raw/` (or pasted content and asked you to file it). The job is to integrate this source into the wiki such that future queries can benefit from it.
The shape of an ingest: read the source (chunked if large), discuss the key takeaways with the user briefly, write a summary page in `cml/wiki/sources/`, identify which existing pages are touched, surgically update those pages, create new pages for any new entities or concepts, update the relevant index, and append to the log.
The temptation to skip the discussion step is strong, but resist it — the user's reaction to the takeaways often reveals what should be emphasized in the wiki versus left out. Ingest is not a batch import; it's a collaborative reading.
For the full procedure, see `ingest-workflow.md`.
### Query
The user asks a question. The job is to answer it from the wiki, with citations, and to file the answer back if it represents new synthesis.
The shape of a query: read the index to identify candidate pages; read those pages; if needed, follow `[[wikilinks]]` from those pages or grep for backlinks; synthesize the answer with `[[wikilink]]` citations; offer to file the answer into `cml/wiki/synthesis/` if it's substantive enough to be worth keeping.
The compounding effect of the wiki only works if good answers get filed back. A comparison you generated, a connection you discovered, an analysis you produced — these should not evaporate into chat history. Default to offering to file; let the user decline if the answer was too trivial or too transient.
For the full procedure, see `query-workflow.md`.
### Lint
Periodic health check. The job is to find structural and semantic problems before they compound.
Structural problems are mechanical and the bundled `wiki_lint.py` script catches them: orphan pages with no inbound links, broken `[[wikilinks]]` to nonexistent pages, oversized pages that need splitting, missing or malformed frontmatter, suspicious staleness (a page that hasn't been updated despite many recent ingests touching its topic).
Semantic problems need the LLM: contradictions between pages, claims that newer sources have superseded, concepts mentioned in many pages but lacking their own page, cross-references that should exist but don't, knowledge gaps the user might want to fill.
Lint findings are presented as proposed edits, not silent rewrites. The user approves changes. This is essential for trust — a wiki that mutates under the user is not a wiki the user can rely on.
For the full procedure, see `lint-workflow.md`.

View File

@@ -0,0 +1,126 @@
# Graph Workflow
The graph layer is the optional **compiled index** over the markdown wiki. It does not replace the wiki — it sits alongside it under `cml/wiki/graph/` and is reproducible from the markdown at any time. The point is to make typed, provenance-backed relationships machine-queryable while keeping markdown canonical.
If `cml/wiki/graph/ontology.yaml` is absent, the wiki is pre-graph: don't run extract/lint/query and don't fabricate ontology files. Either propose adding the layer, or proceed without it.
## What the graph captures
Three classes of edge come out of an extract:
1. **Typed semantic edges** declared in a page's `graph.relationships[]` frontmatter. Examples: `founded`, `proposed`, `depends_on`. Each one carries an explicit `source` (a source-page slug), an `evidence` quote, a `confidence` (high/medium/low), and a `status` (current/historical/proposed/disputed/superseded). The extractor never invents these.
2. **`mentions` edges** — one per body `[[wikilink]]` (deduplicated per page). Confidence is `low`; they accelerate navigation but should not be cited as evidence of a typed relationship.
3. **`sourced_from`** edges — one per slug in a non-source page's frontmatter `sources:` list, pointing at the source page. **`summarizes_raw`** edges — one per source page's `raw:` field, with the raw file path as the (string-literal) object.
## Frontmatter schema
```yaml
graph:
node_id: person:praney-behl # optional; default <node_type>:<slug>
node_type: person # optional; default mapped from type/kind via ontology
canonical: true # mark canonical when multiple slugs alias the same entity
aliases: [Praney, praney@example.com]
relationships:
- predicate: founded
object: company:seedblocks
source: praney-founder-context-dump
evidence: "Solo technical founder and sole director..."
confidence: high
status: current
# optional:
# valid_from: 2025-01-15
# valid_to: 2026-03-01
# notes: "..."
# raw_ref: "cml/raw/founder-dump.md#L42"
# contradicts: <node-id-or-edge-id>
# supersedes: <node-id-or-edge-id>
```
Required relationship fields: `predicate`, `object`, `source`, `evidence`, `confidence`, `status`.
`node_id` format is `<node_type>:<slug>`. The default is derived from the page's `type`/`kind` via the ontology's `maps_from` block. `decision`, `claim`, and `raw` are explicit-only — they don't have wiki pages, so they only show up as edge objects. If a typed edge points at one of these, the `wiki_graph_lint.py` flag for "broken object reference" will fire until either (a) you create a page for it, or (b) you add it to the ontology with `explicit_only: true` and accept that lint will continue to flag the reference.
## The ontology
`cml/wiki/graph/ontology.yaml` is the contract. It declares:
- `node_types[*].maps_from` — how page `type`/`kind` projects onto a node type.
- `predicates[*]` — the allowed predicates, each with `subject_types`, `object_types`, and `requires_evidence`. `"*"` is a wildcard for either side.
Edit the ontology when you need a new domain predicate. Re-run `wiki_graph_lint.py` to validate; existing typed edges will be caught if they no longer match.
## When to add a typed edge vs a plain `[[wikilink]]`
Add a typed edge when:
- A specific source explicitly states the relationship.
- You can quote a snippet of evidence.
- The predicate is meaningful for downstream queries ("who founded what", "what did Stephanie propose").
Use a plain `[[wikilink]]` when:
- The relationship is implicit, atmospheric, or you're hedging.
- You cannot pin the claim to a single source quote.
- The predicate would be `mentions` anyway.
When in doubt, write the wikilink and skip the typed edge. The lint surfaces missing evidence; it does not punish under-claiming.
## Extract / lint / query loop
```bash
# Validate the typed metadata first; lint is conservative, never edits.
uv run skills/llm-wiki/scripts/wiki_graph_lint.py cml/wiki/
# Compile to nodes.jsonl, edges.jsonl, graph.sqlite, graph.graphml.
uv run skills/llm-wiki/scripts/wiki_graph_extract.py cml/wiki/
# Navigate.
uv run skills/llm-wiki/scripts/wiki_graph_query.py cml/wiki/ neighbors --node product:konvy
uv run skills/llm-wiki/scripts/wiki_graph_query.py cml/wiki/ edges --subject person:stephanie-emmanouel
uv run skills/llm-wiki/scripts/wiki_graph_query.py cml/wiki/ path --from person:praney-behl --to product:konvy
uv run skills/llm-wiki/scripts/wiki_graph_query.py cml/wiki/ facts --about product:konvy
```
`--json` works on both lint and query commands.
## Ingest workflow integration
After Step 6 of the standard ingest workflow (after surgical updates and source page creation), run:
1. If new typed edges were added on the page being ingested, run `wiki_graph_lint.py`. **Interactive:** triage findings with the user before extract. **Drain (headless):** if lint is clean, proceed to extract; if lint reports errors, record them in `log.md` and skip extract for this batch — never silently rewrite typed edges, never block waiting for a user.
2. Run `wiki_graph_extract.py` to refresh the compiled artifacts.
3. Append a sub-line under the ingest's `log.md` entry:
` graph: +N nodes, +M typed edges (predicates: founded, contains_product, ...)`
Skip extract if this ingest added no `graph:` metadata and created no new pages — the compiled artifacts are unchanged.
## Query workflow integration
When the user asks a question that smells relational ("what's connected to X", "who proposed Y", "trace the path from A to B"):
1. Read the index as usual.
2. If `cml/wiki/graph/graph.sqlite` exists and is fresher than the latest log entry, query it for typed edges around the candidate pages — `neighbors`, `edges`, `facts` are the most useful.
3. Read the wiki pages behind the relevant nodes/edges. Don't answer from graph rows alone for high-stakes claims; the `evidence` field is a hint, not the source of truth.
4. Cite with `[[wikilinks]]` to wiki pages, not graph rows.
If `graph.sqlite` is stale (older than the most recent ingest in `log.md`), use it as-is and note the staleness — do **not** regenerate inline. Extract is a compile/drain step; the query turn stays read-only, and the background drain refreshes the graph after each ingest.
## Generated artifact policy
| File | Canonical? | Default tracking |
|------|-----------|------------------|
| `cml/wiki/graph/ontology.yaml` | Yes — edit by hand | Tracked |
| `cml/wiki/graph/nodes.jsonl` | Generated | Optional — `cml/wiki/graph/.gitignore` does not ignore it; track if you want graph diffs in PRs |
| `cml/wiki/graph/edges.jsonl` | Generated | Same as above |
| `cml/wiki/graph/graph.sqlite` | Generated | **Gitignored** by default (large, binary) |
| `cml/wiki/graph/graph.graphml` | Generated | **Gitignored** by default |
The bootstrapped `cml/wiki/graph/.gitignore` ignores `graph.sqlite` and `graph.graphml`. Edit it if your team prefers different policy.
## Anti-patterns
- **Typed edges without evidence.** Defeats the entire point. Lint will flag them; do not silence.
- **Editing `nodes.jsonl` / `edges.jsonl` / `graph.sqlite` by hand.** Edit the markdown; regenerate.
- **Inventing ontology entries to make a typed edge "fit".** The ontology should reflect domain reality, not paper over a too-eager edge. Either add the predicate with proper `subject_types`/`object_types`, or use `mentions`.
- **Treating graph rows as evidence in answers.** Always cite the wiki page; the graph just told you which wiki page to read.
- **Forgetting to regenerate after an ingest.** The graph diverges silently. Tie extract to ingest in muscle memory.

View File

@@ -0,0 +1,131 @@
# Ingest Workflow
When a new source arrives, this is the procedure. The order matters — each step builds context for the next.
## Step 0: Check the schema
Before anything else, read `cml/wiki/SCHEMA.md`. The user may have customized the page-type structure, the tag taxonomy, the naming conventions, or the ingest workflow itself. Schema overrides everything documented here.
## Step 1: Place the raw source
If the source isn't already in `cml/raw/`, place it there. Use a slugified filename: lowercase, hyphens for spaces, no special characters, with the original extension. For web articles, save as `.md` (Obsidian Web Clipper output is ideal). For PDFs, keep the `.pdf`. For transcripts, save as `.md` or `.txt`.
The slug you pick here will become the slug of the source-summary page in `cml/wiki/sources/`, so make it descriptive and stable.
## Step 2: Read the source
For short sources (under ~5,000 words / ~25,000 tokens), read the whole thing in one pass.
For long sources (papers over ~30 pages, book chapters, multi-hour transcripts), **chunk-read**: read the table of contents or section headers first to build a mental map, then read sections sequentially, summarizing each section in working memory before moving to the next. Do not load the entire raw source into context at once if it would consume more than ~25% of your context window — that leaves no room for the rest of the operation.
For PDFs specifically, prefer the `pdf-reading` skill if available (it handles the chunking automatically). Otherwise extract text first with `pdftotext` or `pdfminer` and then chunk-read the extracted text.
For images embedded in the source: read the surrounding text first, then view only the images that the text suggests are load-bearing (a chart referenced in an argument, a diagram of a system, a figure the source explicitly walks through). Don't blindly load every image — many are decorative.
## Step 3: Discuss the takeaways with the user (interactive only)
**Skip this step entirely in the background drain** — the compile cron runs headless with no user present, so there is no one to discuss with, and the remaining steps must not depend on a user reaction.
When run interactively, do this briefly — three or four sentences. Surface what struck you as important, what was surprising, what connects to existing wiki content, and what's worth flagging. The user's reaction shapes the next steps. They might say "skip the methodology section, only the results matter for me" or "we already have a page on this — just update it" or "this contradicts the page on X, flag that prominently".
If there is no user to react (the background drain, or a hands-off "just process these 20 papers" batch), skip the discussion and be more conservative on the wiki edits — make smaller, safer updates and surface anything ambiguous in the log.
## Step 4: Identify what's touched
Before writing anything, do a survey pass against the wiki to determine the impact:
1. Read `cml/wiki/index.md` (or the relevant shard under `cml/wiki/indexes/`) to identify existing pages this source touches. Look for entity names, concept names, and topical overlap.
2. For each potentially-touched page, read the page to confirm. (Read, don't grep — the index summaries can mislead.)
3. List, in working memory: existing pages to update, new pages to create, contradictions to flag.
This survey is what prevents duplication. Without it, you'll create a new page on a topic that already has one under a slightly different name.
## Step 5: Write the source-summary page
Create `cml/wiki/sources/<source-slug>.md`. Use the page template (`assets/page.md.template`) as a starting point. Frontmatter should include at minimum:
```yaml
---
type: source
title: "Original title of the source"
authors: ["Author Name"]
url: "https://..." # if applicable
raw: "cml/raw/<source-slug>.<ext>"
ingested: 2026-04-15
tags: [tag1, tag2]
entities: [entity-page-1, entity-page-2]
concepts: [concept-page-1, concept-page-2]
---
```
Frontmatter list values are bare slugs — the `[[wikilink]]` syntax goes in the body, not in YAML.
The body should be the LLM's summary of the source — the key claims, the methodology if relevant, the conclusions, the open questions. Do not paraphrase the entire source; that defeats the purpose. Aim for a summary that captures what a future query would need to know without re-reading the raw file.
Keep the page atomic — under the 400-line soft cap. If the source is so dense that a single summary page can't capture it, split: one page per major section, each linking to the others, with a parent page that gives the overview.
End the body with a "Where this fits" section listing `[[wikilinks]]` to the entity and concept pages this source touches. This is the bidirectional link from source → existing structure.
## Step 6: Update touched pages
For each existing page identified in Step 4, surgically edit it to incorporate what the new source adds. Use `str_replace`, not full rewrites. The goal is to add a sentence or paragraph in the relevant section, with a `[[wikilinks]]` citation to the new source-summary page.
Common update patterns:
- A new source corroborates an existing claim → add a citation: `..., as established in [[paper-X]] and now corroborated by [[paper-Y]]`.
- A new source contradicts an existing claim → flag prominently: add a "## Contradictions" section if it doesn't exist, and document the contradiction with both sources cited. Do not silently overwrite the older claim.
- A new source adds a new dimension to an existing topic → add a new sub-section, don't dilute the existing prose.
- A new source mentions an entity or concept the page already discusses → update the `sources:` frontmatter and add the cross-reference where relevant in the body.
If updating a page would push it over the 800-line hard cap, that's the signal to split the page (extract the new dimension into its own page, link from the parent). Do that as part of the ingest, not as a deferred lint task.
## Step 7: Create new pages for new entities and concepts
For each new entity or concept the source introduces that doesn't have a page yet, decide whether it warrants its own page. Heuristic: if the source mentions it in passing and no other source is likely to expand on it, just mention it inline on a related page. If the source treats it as a first-class topic or you can foresee future sources building on it, create a page.
New pages need:
- Frontmatter with `type`, `tags`, `sources: [[[<source-slug>]]]`, `created`, `updated`.
- A body that introduces the entity/concept with what this source said about it.
- Inbound links — at least one existing page should `[[wikilink]]` to the new page, otherwise it's an instant orphan. Update the existing page to add the link.
A new page that nothing links to is a bug in the ingest, not just a lint finding.
## Step 8: Update the index
Add entries for any new pages to `cml/wiki/index.md` (or the appropriate shard). Each entry is one line: a wikilink to the page and a one-sentence summary. Keep the summary tight — the index is engineered to be cheap to read, and a fat index defeats the index-first navigation principle.
If `index.md` exceeds 300 lines after this update, that's the signal to shard. Do it now while the structure is fresh — see `scaling-playbook.md` for the procedure.
## Step 8b: Refresh the graph layer (only if `cml/wiki/graph/ontology.yaml` exists)
If the wiki has the optional graph layer:
1. Add typed `graph.relationships[]` only when the source explicitly supports them (predicate, source-page slug, evidence quote, confidence, status). When uncertain, prefer a plain `[[wikilink]]` in the body — the body wikilink already produces a `mentions` edge.
2. Run `uv run skills/llm-wiki/scripts/wiki_graph_lint.py cml/wiki/`. **Interactive:** triage findings with the user before extracting. **Drain (headless):** if lint is clean, proceed to extract; if lint reports errors, record them in `log.md` and skip extract for this batch. Never silently rewrite typed edges, and never block waiting for a user.
3. Run `uv run skills/llm-wiki/scripts/wiki_graph_extract.py cml/wiki/` to regenerate `nodes.jsonl`, `edges.jsonl`, `graph.sqlite`, `graph.graphml`.
Skip extract if this ingest added no `graph:` metadata and created no new pages — the compiled artifacts are unchanged. Full reference: `references/graph-workflow.md`.
## Step 9: Append to the log
One line in `cml/wiki/log.md`, with the prefix `## [YYYY-MM-DD] ingest | <source-title>`. Optionally add a sub-line listing the pages touched. If the graph layer was refreshed, add a second sub-line: ` graph: +N nodes, +M typed edges`. The log is parsed by simple unix tools (`grep "^## \[" log.md | tail -10`), so the prefix matters.
## Step 10: Close the loop with the user
Tell the user what you did, briefly: "Ingested. Created the source page and a new entity page for X; updated the concept pages for Y and Z. Flagged a contradiction with [[paper-A]] regarding the claim about W."
If the source revealed something worth following up on (an obvious gap, a question the source raised but didn't answer, a candidate next source to ingest), say so — this is where the wiki's compounding effect comes from.
## Anti-patterns to avoid
**Loading the whole source into context at once when it's large.** This is the most common scaling failure. Chunk-read.
**Rewriting whole pages instead of surgical edits.** This burns tokens, risks losing nuance, and erodes diff quality if the wiki is in git.
**Creating pages with no inbound links.** Orphans accumulate fast and become invisible. Always link.
**Ingesting silently in batch mode without surfacing surprises.** Batch ingest is fine, but a one-line "ingested 5 sources, 2 new entity pages, 1 contradiction with [[X]]" summary is the minimum.
**Treating prior wiki pages as ground truth instead of the raw sources.** When updating an existing claim, re-read the raw source for that claim before merging the new one. Don't compound on the wiki's own paraphrase.
**Letting the page split decision drift to a future lint pass.** If a page crossed the size cap during this ingest, split it during this ingest.

View File

@@ -0,0 +1,99 @@
# Lint Workflow
A health check on the wiki. Best run on a cadence — after every N ingests, weekly, or when the user explicitly requests it — not on every operation. Lint is split into a structural pass (handled by `wiki_lint.py`) and a semantic pass (handled by the LLM directly).
> **Report-only gate (this nanobot).** A lint turn runs the read steps (01, 34), presents findings, and **stops**. The mutation steps below — Step 2 (apply fixes), Step 5 (index), Step 6 (log) — happen **only in a separate turn after the user approves**, one category at a time. Inside a lint turn, never create/edit pages, write debug scripts, regenerate the graph, or loop re-lint. When fixing later: bounded batches, do **not** re-read the lint script to reverse-engineer it, re-lint once to confirm — if issues remain, report and ask, don't continue blind. The scripts are sub-second; a long lint turn means you slipped into fixing.
## Step 0: Check the schema
`cml/wiki/SCHEMA.md` may declare additional lint rules specific to this wiki (e.g. "every entity page must have an `aliases:` field", "no concept page without at least 2 sources"). Read it.
## Step 1: Run the structural lint script
```bash
uv run skills/llm-wiki/scripts/wiki_lint.py cml/wiki/
```
If the wiki has the optional graph layer (`cml/wiki/graph/ontology.yaml` exists), also run:
```bash
uv run skills/llm-wiki/scripts/wiki_graph_lint.py cml/wiki/
```
This catches typed-edge problems independently of the structural lint: unknown predicates, missing evidence, broken object references, alias collisions, invalid `confidence`/`status` values, broken `contradicts`/`supersedes` references. Triage findings the same way as structural lint — propose fixes, don't apply them silently. After approved fixes, run `wiki_graph_extract.py` to refresh the compiled artifacts.
This produces a report covering:
- **Orphan pages** — pages with no inbound `[[wikilinks]]` from anywhere else in the wiki. Orphans are usually a sign that an ingest forgot to update a parent page. They become invisible because the index-first navigation can't surface them.
- **Broken wikilinks** — `[[page-name]]` references pointing to nonexistent pages. Usually a typo or a page that got renamed without updating its referrers.
- **Oversized pages** — anything over the 800-line hard cap (or 400-line soft cap, with a warning).
- **Frontmatter issues** — pages missing required fields (`type`, `tags`, `sources`, `updated`), or with malformed YAML.
- **Stale pages** — pages whose `updated:` date is much older than the most recent ingest that touched their topic. (The script approximates this using the page's tags and the log.)
- **Duplicate slugs** — two pages with the same slug in different subdirectories (a sign of an ingest collision that wasn't resolved).
The script is conservative — it reports findings but doesn't fix them. Present the report to the user.
## Step 2: Triage the structural findings (propose only; apply in a later approved turn)
Walk through the findings with the user and propose a fix for each. Proposing is part of the lint turn; **applying** the edits is not — that waits for approval (see the report-only gate above).
- Orphans: either link them from a sensible parent page (preferred), or determine that the page is genuinely useless and delete it (rare). If many orphans pile up, the index is probably out of date — re-derive the index entries.
- Broken links: rename the link to match the actual page, or create the missing page if it should exist, or remove the link if the concept turned out not to warrant a page.
- Oversized pages: split. Extract sub-concepts into their own pages, link from the parent, update the index.
- Frontmatter issues: add the missing fields. If many pages have the same gap, consider whether the schema should be relaxed or the bootstrap template improved.
- Stale pages: read the recently-touched related pages and the relevant raw sources, update the stale page surgically.
- Duplicate slugs: one is canonical, the other should be merged in and deleted. Pick the better-named one as canonical and migrate inbound links with `grep` + `str_replace`.
Present each proposed fix as an edit, not a fait accompli. The user approves.
## Step 3: Run the semantic pass
The semantic pass is what the script can't do — it requires reading pages and reasoning about content. The good news is that you don't have to read the whole wiki: focus on the pages most likely to have semantic issues.
**Recently updated pages.** Read the last ~10 pages that were modified (sorted by `updated:` frontmatter or by the log). Look for:
- Contradictions with older pages on the same topic. The new claim may have superseded the old one (in which case mark the old as superseded with a note), or both may be true but in different contexts (in which case clarify each), or the new ingest may have been wrong (in which case revert).
- Internal contradictions within a page (an ingest layered on a new claim without reconciling against existing prose).
- Repeated mentions of an entity or concept that doesn't have its own page yet — candidate for promotion.
**Highly-linked pages (hubs).** These are the most likely to drift because every ingest touches them. Use `wiki_search.py --top-linked 10` (or grep `[[wikilinks]]` and count) to find them. Read each hub and check that the prose still hangs together and the cross-references still make sense.
**Pages flagged with explicit uncertainty.** During ingest, the convention is to hedge ("the source claims X, though this is not yet corroborated") rather than assert. The lint pass is when you check whether subsequent ingests have corroborated or contradicted, and update accordingly.
## Step 4: Surface gaps
Look at what's referenced but not pageified. Run:
```bash
uv run skills/llm-wiki/scripts/wiki_lint.py cml/wiki/ --suggest-pages
```
This finds entity-like and concept-like names that appear in many pages but lack their own page. The user can decide which to promote and which to leave as inline mentions.
Also look for what's not covered at all — topics the user has expressed interest in but that haven't been ingested yet. The log can help here ("you ingested 5 sources on diffusion models in March but nothing since; want to refresh?").
## Step 5: Update the index (fix turn — only after approved fixes)
After lint fixes, the index almost certainly needs updates: new pages, renamed pages, deleted pages, changed summaries. Re-derive the affected index entries surgically.
If `index.md` is over 300 lines and hasn't been sharded yet, this is a good moment to do it. See `scaling-playbook.md`.
## Step 6: Append to the log (fix turn)
One line: `## [YYYY-MM-DD] lint | <N> structural fixes, <M> semantic fixes, <K> proposed gaps`. Optionally a sub-line listing the highest-impact changes.
## Cadence
A reasonable default: structural lint after every 5 ingests, semantic lint weekly or after every 20 ingests, gap-finding monthly. Adjust based on the user's pace and the wiki's volatility. A wiki that's growing fast needs more frequent lint; a stable mature wiki needs less.
The user may also trigger lint explicitly ("clean up the wiki", "what's broken", "lint pass please"). Treat these as priority — the user is asking because they noticed something.
## Anti-patterns to avoid
**Silent rewrites.** Lint findings are proposals. The user approves changes. A wiki that mutates without the user's knowledge stops being trustworthy.
**Treating lint as cleanup-only.** The semantic pass is also where new connections get made. If you read 10 pages and notice that two of them point at the same underlying idea, that's a synthesis-page candidate, not just a lint finding.
**Letting the lint report grow until it's overwhelming.** If the report is too long for the user to triage, the cadence is wrong (lint more often) or the wiki has outgrown its conventions (revisit the schema).
**Skipping lint because everything seems fine.** Silent corruption is the failure mode that's hardest to detect and most damaging. Lint catches it.

View File

@@ -0,0 +1,89 @@
# Page Conventions
The structural rules every wiki page follows. The schema may extend or override these for a particular wiki, but these are the defaults.
## Frontmatter
Every wiki page begins with YAML frontmatter. The required fields:
```yaml
---
type: <source|entity|concept|synthesis|...>
title: "Human-readable title"
tags: [tag1, tag2, tag3]
sources: [source-slug-1, source-slug-2]
created: 2026-04-15
updated: 2026-04-15
---
```
Note that frontmatter list values are **bare slugs**, not `[[wikilinks]]`. The double-bracket syntax is only used in the page body. The bundled scripts treat frontmatter `sources:`, `entities:`, and `concepts:` lists as slug references and resolve them the same way as body wikilinks.
`type` determines the page-type and which subdirectory the page lives in. Standard types are `source`, `entity`, `concept`, `synthesis`. The schema can declare additional types.
`title` is the human-readable name. The filename slug is separate and may be a short version of the title. For pages about people, the convention is `Last, First` for sortability; for everything else, natural casing.
`tags` is a flat list. Tags are how `wiki_search.py` filters and how the user navigates topically in Obsidian. Keep the tag taxonomy small and disciplined — a wiki with 200 tags has effectively no tags. The schema should declare the canonical tag set and the lint script will warn on tags outside it.
`sources` is the list of source-summary pages this page draws from. Always populated for entity/concept/synthesis pages; for source pages themselves, this field is omitted (the source page is the source).
`created` and `updated` are dates in ISO format. `updated` is the load-bearing one — it powers the staleness check in lint and the "what's new" view.
Type-specific fields:
- Source pages add `authors`, `url`, `raw` (path to the raw file), `ingested`.
- Entity pages may add `aliases` (other names for the same entity), `kind` (person, paper, product, etc.).
- Synthesis pages add `question` (the original question, if it was filed back from a query) and `sources_consulted`.
## Wikilinks
Cross-references use the `[[page-slug]]` syntax. The slug is the filename without the `.md` extension and without the directory prefix — Obsidian-style. Aliasing is supported: `[[page-slug|display text]]`.
Every page should have at least one inbound link. New pages without inbound links are orphans and the lint pass will flag them.
The bundled `wiki_search.py --backlinks <slug>` returns inbound links. To find them manually:
```bash
grep -rln "\[\[<slug>\]\]" cml/wiki/
```
## Page sizing
**Soft cap: 400 lines or ~2,000 words.** When a page approaches this, consider whether it should split.
**Hard cap: 800 lines.** Pages over this must split. The lint script flags violations.
Atomicity heuristic: a page is about *one* thing. A page on "Diffusion Models" should not also be the page on "Stable Diffusion" — those are two pages with cross-references. If you find yourself writing "## Variants" with five sub-sections of substantive prose, those sub-sections are probably their own pages.
The reason for the size cap is the context-bottleneck principle: any single page read is bounded, so the LLM can confidently read several pages without exhausting context.
## Naming
Page slugs are lowercase, hyphenated, no special characters. Match the directory: a concept page lives at `cml/wiki/concepts/<slug>.md`.
For entity pages about people: prefer the full name slugified (`andrej-karpathy.md`), not just the surname. Add `aliases:` to frontmatter for common short names.
For source pages: use a slug derived from the source title, possibly with the year for disambiguation (`attention-is-all-you-need-2017.md`). The source page slug should match the raw file slug if possible — that makes the back-pointer trivial.
For synthesis pages: derive from the question or the topic, not the date. `comparing-rag-vs-llm-wiki.md`, not `2026-04-15-question.md`. Date-based names don't surface usefully in the index.
## Body structure
The body has no rigid template — the schema may declare one for specific page types — but a few defaults work well:
**Source pages**: lead paragraph summarizing the source's main contribution. Sections for key claims, methodology (if relevant), conclusions, open questions. End with a "Where this fits" section listing the entity and concept pages this source touches.
**Entity pages**: lead paragraph defining the entity. Sections for relevant attributes (for a paper: authors, venue, key claims; for a person: affiliation, notable work; for a product: what it does, who makes it). A "Mentioned in" section is unnecessary — the backlink discovery handles that.
**Concept pages**: lead paragraph defining the concept clearly. Sections for the key formulation, variants, contested aspects, related concepts. Heavy use of `[[wikilinks]]` is expected — concept pages are the connective tissue of the wiki.
**Synthesis pages**: lead with the question (if filed from a query) or the framing. The body is the answer/analysis. End with the sources consulted as wikilinks.
## Hedging language
When a source claims something that hasn't been corroborated by other sources in the wiki, hedge: "Source X claims Y, though this is not yet corroborated by other sources in the wiki." The lint pass will revisit hedged claims as the wiki accumulates more sources, either upgrading them to confirmed or flagging them as contested.
When two sources contradict, document both: "Source X claims Y; source Z claims not-Y. The contradiction is unresolved." Do not silently pick a side.
## Keeping pages voice-neutral
The wiki is the LLM's voice, not the source author's voice and not the user's voice. Paraphrase rather than quote, except for short load-bearing phrases where the exact wording matters. Maintain a consistent, neutral, encyclopedic tone — close to a Wikipedia article in register, not a chat reply.

View File

@@ -0,0 +1,102 @@
# Query Workflow
The user is asking a question against the wiki. The job is to answer it from the wiki, with citations, scaling navigation to the wiki's size, and to file the answer back as a synthesis page when warranted.
## Step 0: Check the schema
Read `cml/wiki/SCHEMA.md` if you haven't this session. Some wikis declare query-specific conventions (e.g. "always answer with a comparison table when the question is comparative", "answers go in `cml/wiki/synthesis/qa/` not `cml/wiki/synthesis/`"). Schema overrides defaults.
## Step 1: Read the index
Always start at `cml/wiki/index.md`. If the index has been sharded into `cml/wiki/indexes/`, read the top-level `index.md` first to identify which shard(s) are relevant, then read those.
The index is engineered for this — one line per page with a tight summary. You should be able to identify candidate pages from the index alone in most cases. If a query touches multiple shards (e.g. "compare the methodologies in papers A and B"), read all relevant shards.
## Step 2: Identify candidate pages
From the index, build a short list of pages that look relevant to the query. Be selective — reading 30 pages to answer a question is a sign you've fallen back to brute-force search, which doesn't scale. If the index summaries don't disambiguate well, that's a signal that the index entries are too terse — note it for the next lint pass.
If the index doesn't surface good candidates (the query uses fuzzy or domain-specific language that doesn't match the index summaries), fall back to the search script:
```bash
uv run skills/llm-wiki/scripts/wiki_search.py "your query terms" --top 10
```
This returns the top-N pages by BM25 score, with optional filters on frontmatter (`--type concept`, `--tag llms`, `--since 2026-01-01`). Use the search script *as a fallback*, not as the default — index-first is cheaper and produces more interpretable results when it works.
## Step 2b: Graph-assisted lookup (only if `cml/wiki/graph/graph.sqlite` exists)
For relational questions ("what's connected to X", "who proposed Y", "trace the path from A to B"), query the compiled graph after the index pass and before reading pages:
```bash
uv run skills/llm-wiki/scripts/wiki_graph_query.py cml/wiki/ neighbors --node <node-id>
uv run skills/llm-wiki/scripts/wiki_graph_query.py cml/wiki/ facts --about <node-id>
```
Use the structured neighbors/facts to pick the right wiki pages to read — but never answer from graph rows alone for high-stakes claims. The graph accelerates navigation; the wiki page and its raw source remain the evidence. If `graph.sqlite` is older than the most recent `## [YYYY-MM-DD] ingest |` entry in `log.md`, use it as-is and note the staleness — do **not** run `wiki_graph_extract.py` inline. Extract is a compile-phase step; a query turn stays read-only and the background drain refreshes the graph after each ingest. Full reference: `references/graph-workflow.md`.
## Step 3: Read the candidate pages
Read each candidate page in full. While reading, note any `[[wikilinks]]` to other pages that look relevant — those are pre-curated leads. Follow the most promising ones, but don't recursively chase every link or you'll exhaust your context window on tangentially-relevant pages.
If a page references a source-summary page in its frontmatter and the answer hinges on what that source actually said, read the source-summary page too. Avoid going all the way back to the raw source unless the wiki summaries are clearly insufficient — the whole point of the wiki is that the synthesis is already done.
## Step 4: Find backlinks if needed
If the query is "what does my wiki say about X" or "where is X mentioned", and X has its own page, the inbound links are often more interesting than the page itself. Find them with:
```bash
grep -rl "\[\[<page-slug>\]\]" cml/wiki/
```
This is faster than reading pages to look for mentions. Note that the bundled `wiki_search.py` has a `--backlinks <slug>` mode that does this and returns ranked results.
## Step 5: Synthesize the answer
Write the answer in your own words, with `[[wikilink]]` citations to the wiki pages you used and (where helpful) `cml/raw/<file>` references to specific raw sources. The citations matter — they let the user verify the answer and follow up.
If the wiki contains contradicting claims, surface the contradiction explicitly rather than picking one and presenting it as settled. The wiki's value is partly in tracking what's known versus what's contested.
If the wiki has no relevant content for the query, say so plainly. Do not confabulate — that's the surest way to corrupt the wiki when the answer gets filed back. Instead, suggest sources the user could ingest to fill the gap.
## Step 6: Offer to file the answer back
If the synthesized answer represents new connection-making — a comparison the wiki didn't already contain, an analysis that pulls together threads from multiple pages, an answer to a recurring question — offer to file it as a synthesis page. The user will say no for trivial answers and yes for substantive ones. Default to offering.
The file-back creates `cml/wiki/synthesis/<answer-slug>.md` with frontmatter:
```yaml
---
type: synthesis
question: "the original question, verbatim or lightly cleaned"
asked: 2026-04-15
sources_consulted: [page-1, page-2, page-3]
tags: [...]
---
```
Body: the answer as you gave it to the user, possibly lightly edited for the wiki's voice. Add the new synthesis page to the relevant index. Append to `log.md` with prefix `## [YYYY-MM-DD] query | <question-summary>`.
A filed synthesis page is itself queryable — the next time the user asks an adjacent question, the synthesis page may be the most relevant candidate. This is how exploration compounds.
## Special query types
**"What's missing on topic X?"** — Read existing pages on X, identify open questions or unstated assumptions, and propose ingest candidates. This is essentially a per-topic micro-lint.
**"Compare X and Y"** — Read both pages, look for explicit comparison pages already in `synthesis/`, generate a comparison table or contrast prose. Strong file-back candidate.
**"Show me the timeline of X"** — Use `log.md` to reconstruct chronology of ingests touching X, supplement with `created`/`updated` frontmatter on relevant pages.
**"What did source X say about Y?"** — Read `cml/wiki/sources/<x>.md` for the source's summary; if it doesn't directly answer, read `cml/raw/<x>` (chunk-read if large).
**"Lint check on this answer"** — Before filing back, ask the user to verify a key claim by pointing to its raw source. Especially valuable for high-stakes wikis (medical, legal, financial).
## Anti-patterns to avoid
**Reading every page in the wiki to be safe.** This doesn't scale and produces vague answers. Trust the index; if the index fails, fix the index, don't bypass it.
**Citing the wiki without citing the underlying sources for hard claims.** The wiki page is a paraphrase; for any claim the user might need to verify, the citation should chain back to the raw source.
**Filing back trivial answers.** Not every Q&A is worth a permanent page. If the answer is a one-line lookup or restates an existing page, don't pollute synthesis/. The threshold is "would I want to find this when I ask a similar question in three months?"
**Confabulating when the wiki is silent.** Better to say "the wiki doesn't cover this" than to invent an answer that gets filed back as authoritative.

View File

@@ -0,0 +1,91 @@
# Scaling Playbook
Thresholds at which the wiki's structure needs to evolve, and the migration steps. The goal is to keep the wiki's context cost roughly constant per query as the wiki grows — the LLM should never need to read more pages or larger pages just because the wiki got bigger.
## The bottleneck
Naive LLM Wiki implementations break at scale because of a few specific failure modes, each of which has a structural fix:
- **The index file becomes too large to read cheaply.** Fix: shard the index by category.
- **Pages grow unboundedly as more sources mention them.** Fix: enforce the page size cap; split when violated.
- **Index summaries become too vague to disambiguate candidates.** Fix: tighten summaries; introduce frontmatter filtering via the search script.
- **Brute-force grep over the wiki replaces index-first navigation.** Fix: make the index actually useful and use the search script as the explicit fallback.
## Threshold 1: ~50 pages
Below this scale, you need almost no structure. A flat `cml/wiki/` directory with `index.md` and `log.md` is enough. The categorical subdirectories (`entities/`, `concepts/`, etc.) are still worth using from the start because they're free, but the index doesn't need sharding and the search script is overkill.
## Threshold 2: ~150 pages OR `index.md` over 300 lines
Time to **shard the index**. The migration:
1. Create `cml/wiki/indexes/` directory.
2. Split `index.md` by category into `indexes/sources.md`, `indexes/entities.md`, `indexes/concepts.md`, `indexes/synthesis.md` (and any custom types from the schema). Each shard is a list of pages of that type, with the same one-line summaries.
3. Rewrite the top-level `index.md` to be a directory of shards: each shard linked, with a one-line description of what's in it and a count.
4. Update the schema to document the sharded structure.
5. Update the ingest workflow in your working memory: now you update `indexes/<type>.md`, not `index.md` directly.
The top-level `index.md` should now be tiny — under 50 lines — and the shards are each bounded by the type-specific volume.
If a single shard later exceeds 300 lines (most likely `entities.md` or `concepts.md` if the wiki has a strong topical focus), shard *that* by sub-category: `indexes/entities-people.md`, `indexes/entities-papers.md`, etc. The principle generalizes.
## Threshold 3: ~300 pages
Time to introduce the **search script as a routine fallback**. Index navigation still works for direct lookups ("the page on diffusion models"), but fuzzy queries ("which papers discuss training stability") benefit from BM25 ranking.
`scripts/wiki_search.py` provides:
- `uv run skills/llm-wiki/scripts/wiki_search.py "query terms"` — top-N pages by BM25 score.
- `--type concept` — filter by frontmatter type.
- `--tag <tag>` — filter by tag.
- `--since 2026-01-01` — filter by `updated` date.
- `--backlinks <slug>` — find pages that link to a given page.
- `--top-linked N` — find the N most-linked-to pages (hubs).
Update the schema to declare the search script as a sanctioned fallback, so that future LLM sessions know to reach for it rather than degenerating into recursive grep.
## Threshold 4: ~500 pages
At this scale, two things start to matter:
**Structural lint cadence becomes weekly or per-N-ingests.** Manual oversight stops scaling. Rely on `wiki_lint.py` to surface structural drift and triage with the user.
**The search script may want a real index.** The default `wiki_search.py` rebuilds its BM25 index on every run, which is fine up to a few thousand pages. Beyond that, persist the index to disk (the script supports `--cache .wiki-search-cache.json`).
Also consider whether the wiki has organically split into distinct topic clusters that don't really cross-reference each other. If so, a single wiki may be the wrong shape — splitting into per-topic wikis (each with its own `SCHEMA.md`, `index.md`, etc.) may be cleaner. The user should make this call.
## Threshold 5: ~1,000+ pages
At this scale, the question is whether the LLM Wiki pattern is still the right tool. Markdown + grep + frontmatter scales further than people expect (the gist author reports a wiki of ~100 articles and ~400K words working fine), but at some point a real database with structured queries beats markdown. Signals it's time to consider migrating:
- The user's queries are predominantly relational ("show me all papers from author X cited by papers in topic Y published after date Z"). A graph database serves this better.
- Lint reports are too long to triage even at high cadence.
- The schema has grown to specify dozens of types and hundreds of tags — at that point you've manually built a database schema in markdown.
If the user wants to migrate, the markdown wiki is excellent input for the migration: every page has frontmatter and `[[wikilinks]]` that map cleanly to a property graph.
## When to *not* shard
Sharding is irreversible-ish (you can un-shard, but it's annoying), so don't do it preemptively. Wait for the actual threshold. A wiki of 80 pages with a sharded index has worse usability than the same wiki with a flat index, because the shards add a navigation step without enough volume to justify it.
## When to introduce custom page types
The default types (`source`, `entity`, `concept`, `synthesis`) cover most use cases. Add a custom type only when the user has a clear category of pages that don't fit any default and that benefits from being a distinct subdirectory (queryable, distinct lint rules, distinct templates). Examples that justify it:
- `decision` pages for a team that documents architectural decisions
- `experiment` pages for a research lab logging trial results
- `character` pages for a fan wiki tracking a fictional cast
- `meeting` pages for a team logging meetings
Don't add a type for a one-off — use tags instead. Adding a type is a schema change that affects the index, the lint script, and every future ingest.
## Detecting "we've outgrown our conventions"
Some signals that the schema needs revision rather than just more lint:
- The same kind of frontmatter issue keeps reappearing — the field needs to be optional, or the bootstrap template needs an example.
- Pages keep getting created that don't fit cleanly into any existing type — a new type is wanted.
- The tag list has grown unmanageable — prune, consolidate, or formalize the taxonomy.
- The user keeps overriding the LLM on a particular kind of decision — encode their preference in the schema so it persists across sessions.
Schema revision is healthy. A schema that doesn't change after the first few weeks of a wiki's life is probably not being used.