---
name: support-wiki-lint
description: "Use to validate a wiki page against its schema — required frontmatter, required H2 sections, line caps, citation hash drift. Runs synchronously on every aiwiki/** write via PostToolUse hook, and on dream output before user review. Catches schema violations and stale citations the moment they happen."
---

# Support: Wiki Lint

## Overview

The wiki is curated to answer recurring questions. That only works if every page conforms to its declared schema (so AI can find what it needs predictably) and every code citation still points at the code that exists today (so claims don't go silently stale). This skill enforces both — fast, deterministic, surfaced to stderr on every aiwiki/** write (the PostToolUse hook can't reject the write itself; see "On 'error (surface)' vs 'block'" below).

**Core principle:** validation is mechanical. No judgment, no "this looks roughly right." Either the page matches its schema and citations resolve, or it doesn't.

**Announce at start:** "I'm using the support-wiki-lint skill to validate the wiki page."

## When to Use

- Synchronously on every `aiwiki/**` write (PostToolUse hook fires the script)
- On dream output in `aiwiki/proposed/{dream_id}/` before the dream is marked complete
- On-demand when validating a wiki page manually (e.g. after a refactor that may have moved cited code)

**Do NOT skip when:**
- The page "looks fine" — schema drift accumulates silently; that's why we lint
- The change is "just a typo" — a typo in frontmatter (`schema_id: decsion`) breaks downstream tooling
- The dream just produced the file — dreamer's output goes through lint like every other write

## Scope

Catches these classes of wiki defect:

| Defect | Example | Severity |
|---|---|---|
| Missing or malformed frontmatter | No `schema_id`; `schema_version` is a string instead of integer | error (surface) |
| Missing required H2 section | ADR with no `## Decision` | error (surface) |
| Section order violation when schema declares `section_order: strict` | `## Decision` before `## Context` | error (surface) |
| Line cap exceeded | ADR > 400 lines | error (surface) |
| Stale citation hash | `file:line@a3f2bc1` but recomputed hash is now `b1c2d3e` | error (surface) |
| Missing citation hash on a `file:line` reference | `src/auth.ts:42` instead of `src/auth.ts:42@a3f2bc1` | warning (auto-backfill) |
| Cited file does not exist | `src/old.ts:42@a3f2bc1` after `src/old.ts` was deleted | error (surface) |
| Broken `.md → .md` link | `[ADR-42](../decisions/0042-token-store.md)` but the file was renamed | error (surface) |
| Soft-target line count exceeded | Gotcha at 110 lines (soft target 50-100) | warning |

**`.md → .md` link resolution.** Catches link rot from wiki-page renames. Two forms are validated:

- **Markdown link syntax:** `[text](path.md)` or `[text](path.md#anchor)`. The anchor is stripped before the file-existence check (we don't verify the heading exists, only the file). Resolution tries the file's directory first, then falls back to repo-root (the forge convention `aiwiki/decisions/0042-foo.md`). External URLs (`http://`, `https://`, `mailto:`) are skipped. Image syntax (`![alt](x.md)`) is skipped. Targets that resolve outside the repo root are rejected as path-traversal escapes.
- **Bare wiki paths in prose:** matches `aiwiki/{subfolder}/{name}.md` where `{subfolder}` is one of the canonical names (`decisions`, `gotchas`, `conventions`, `architecture`, `sessions`, `raw`, `proposed`, `schemas`, `oracles`). Custom subfolders are not validated to avoid false positives against project-specific paths. Bare paths that also appear inside a markdown link are validated once (by the markdown-link pass), not twice.

**Code regions skipped.** Both forms are stripped from fenced code blocks (` ``` `) and inline code spans (`` ` ``) before scanning — example links inside documentation don't trigger false positives.

**Reference-style links not covered.** `[label][ref]` syntax + `[ref]: path.md` definitions are NOT validated; wrap cross-references as `[label](path.md)` to opt them in.

**On "error (surface)" vs "block":** wiki-lint runs in a PostToolUse hook — the write has already hit disk by the time lint fires, so the hook physically cannot reject it. Errors are surfaced to stderr (and become visible to the AI on the next turn) so the next edit corrects the issue. If you need a true block, run `node scripts/lint.mjs --file <path>` synchronously before writing (e.g. from a skill that has produced a draft) and gate on its exit code.

Out of scope (intentionally — these are someone else's job):
- Semantic content quality ("is this ADR's rationale sound?") → reviewer subagents
- Cross-page consistency ("does this ADR contradict another?") → dream consolidation
- Markdown rendering correctness → markdown linter (different tool)

## I/O Contract

| Field | Value |
|---|---|
| **Requires** | Target file path, repo root, schema directory (`aiwiki/schemas/`) |
| **Produces** | JSON to stdout: `{ok: bool, errors: [...], warnings: [...], updates: [...] }`. Exit 0 on pass, exit 1 on lint failure, exit 2 on internal error. |
| **Side effect** | Auto-backfills missing `@<sha7>` citation hashes in-place (via `updates[]`). Auto-backfill is the ONLY in-place modification; everything else is reported, not fixed. |
| **Feeds into** | PostToolUse hook (surfaces findings to stderr on exit ≠ 0; cannot block the write — it has already happened); dream-completion gate (synchronous, can block); manual `/wiki lint` invocation |

## Process

### Step 0: locate inputs

| Input | How to find |
|---|---|
| Repo root | `git rev-parse --show-toplevel`, or `${CLAUDE_PROJECT_DIR}` if set |
| Schema directory | `<repo-root>/aiwiki/schemas/`. If missing, the wiki isn't initialized — surface the gap and stop |
| Target file path | Provided by caller (hook passes the file being written; manual invocation passes user-specified path) |
| Script path | `<repo-root>/.claude/skills/support-wiki-lint/scripts/lint.mjs`. If missing, the skill isn't installed in this project — surface the gap |

### Step 1: invoke the lint script

```bash
node "<repo-root>/.claude/skills/support-wiki-lint/scripts/lint.mjs" \
  --file "<target-file-path>" \
  --schemas "<repo-root>/aiwiki/schemas/" \
  --root "<repo-root>"
```

Output: JSON to stdout. Exit codes: 0 = pass, 1 = lint failure, 2 = internal error.

Output shape:
```json
{
  "ok": false,
  "file": "aiwiki/decisions/0042-token-storage.md",
  "schema_id": "decision",
  "errors": [
    {"kind": "missing_section", "section": "## Review", "message": "Required section '## Review' not found"},
    {"kind": "stale_citation", "citation": "src/auth.ts:42@a3f2bc1", "message": "Hash mismatch: recomputed b1c2d3e", "expected": "a3f2bc1", "actual": "b1c2d3e"}
  ],
  "warnings": [
    {"kind": "soft_cap_exceeded", "lines": 247, "soft_target": [100, 200], "message": "Page is 247 lines; soft target is 100-200"}
  ],
  "updates": [
    {"kind": "citation_hash_backfilled", "before": "src/auth.ts:88", "after": "src/auth.ts:88@e5f6789"}
  ]
}
```

### Step 2: interpret and report

If `ok: true`: one line — `wiki-lint: <file> ok` (with note if `updates[]` non-empty).

If `ok: false`: structured report grouped by error kind. Example:

```markdown
## wiki-lint: FAILED — aiwiki/decisions/0042-token-storage.md

### Schema violations (2)
1. Missing required section `## Review` (decision schema requires it for status: accepted)
2. Section order: `## Decision` appears before `## Context` (schema declares strict order)

### Stale citations (1)
1. `src/auth.ts:42@a3f2bc1` — hash recomputed as `b1c2d3e`. Either:
   - Update the citation to the new line / hash
   - Remove the claim that depended on it
   - Annotate the citation line with `// ack-stale: <reason>` if the staleness is acceptable for now
```

The user fixes; re-run.

### Step 3: handle auto-backfills

When the script reports `updates[]`, the citation hashes have already been written in-place. Surface them as informational:

```
wiki-lint: 2 citation hash(es) auto-backfilled.
- src/auth.ts:88 → src/auth.ts:88@e5f6789
- src/cache.ts:14 → src/cache.ts:14@b1c2d3e
```

Do not require user action for backfills — they're a courtesy, not a finding.

## Schema validation rules

The script reads `aiwiki/schemas/{schema_id}.md` and applies its declared rules:

- `required_frontmatter` — every key must be present in target's frontmatter. Type-check (`type: integer`, `type: enum`, `type: date`) where declared. Optional fields (`optional: true`) may be absent or null.
- `required_sections` — every section name must appear as a `## <name>` line in target.
- `section_order: strict` — required sections must appear in the same order as declared.
- `section_order: flexible` — required sections must all appear, but in any order.
- `hard_cap_lines` — total file line count must not exceed.
- `soft_target_lines: [min, max]` — total line count outside this range = warning, not error.
- `citation_rule: required` — every code claim must have a `file:line@<sha7>` or `symbol` citation.
- `citation_rule: required-in-<section>` — only the named section requires citations.
- `citation_rule: required-where-claims-about-code` — heuristic mode (script only flags missing citations on lines that mention a function/class/file name without a citation).

Schemas live at `aiwiki/schemas/{schema_id}.md`. If the target's `schema_id` doesn't resolve to a schema file, that's a `missing_schema` error.

## Citation rules

Two citation forms:

- `file:line@<sha7>` — e.g. `src/auth.ts:42@a3f2bc1`. The `@<sha7>` is the first 7 chars of `sha256(content)`, where `content` is the cited line ±2 lines (5 lines total) joined with `\n` (LF). Padded with empty lines if the cited line is within 2 of file start/end.
- `symbol` — e.g. `src/auth.ts#login`. Matches the symbol name; no hash. Less precise but tolerant of line drift.

**Auto-backfill**: a `file:line` reference without `@<sha7>` is backfilled in-place on first lint. Same for hashless symbol citations (no hash to add — verified existence only).

**Staleness**: a `@<sha7>` that doesn't match the recomputed hash fails lint. Resolve by:
1. Updating the citation to point at the new location
2. Removing the claim that depended on it
3. Annotating with `// ack-stale: <reason>` on the citation line — the script accepts this as a deliberate mark and warns instead of failing

## Common errors

| Error | Cause | Fix |
|---|---|---|
| `missing_schema` | Target's `schema_id` doesn't have a corresponding `aiwiki/schemas/{id}.md` | Either typo'd `schema_id`, or the schema wasn't initialized — check `aiwiki/schemas/` |
| `frontmatter_invalid` | YAML parse failed, or required field missing | Compare target's frontmatter against the schema's `required_frontmatter` block |
| `missing_section` | Required H2 not present | Add the section (see schema's "Required sections" body for purpose); if you genuinely don't have content for it, the page may not match this schema type |
| `section_order_violation` | Schema declares `section_order: strict`, target's sections are reordered | Reorder to match the schema |
| `hard_cap_exceeded` | File too long | Split into multiple files (architecture/), or trim to fit (gotcha/convention) |
| `stale_citation` | Cited code moved or content changed | Update citation, remove claim, or annotate `// ack-stale: <reason>` |
| `missing_file_in_citation` | Cited file path no longer exists | Update or remove the claim |

## Red Flags

**Never:**
- Suppress lint findings to make a page pass — fix the page or fix the schema
- Annotate every stale citation as `ack-stale` to silence the gate (high `ack-stale` density signals architectural drift, not lint problems)
- Modify the page outside the auto-backfill (citation hashes) — every other change is the user's

**Always:**
- Run lint after every wiki write (the hook does this; manual edits via the agent should still trigger validation)
- Report all findings — never filter for brevity
- Re-run after fixing — drift in one section can mask drift in another

## Integration

| Caller | When |
|---|---|
| `PostToolUse` hook on `aiwiki/**` | Synchronous validation on every wiki write |
| `support-dream` skill | After dream produces `aiwiki/proposed/{dream_id}/`, before marking the dream complete |
| Manual `/wiki lint <file>` | User-invoked validation |

| Pairs with | For |
|---|---|
| `support-dream` | Dream output passes through this lint before user review |
| Wiki schemas in `aiwiki/schemas/` | The validation rules live there; this skill enforces them |
