diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7e98892..353272f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "second-brain", "source": "./", "description": "Self-evolving AI second brain. Auto-learns from sessions, discovers tools, maintains a local knowledge base, and self-critiques code quality.", - "version": "0.24.2" + "version": "0.24.3" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 3dd6148..87a42ae 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "second-brain", "description": "Self-evolving AI second brain. Automatically learns from sessions, discovers tools, maintains a local knowledge base, and self-critiques code quality — getting smarter with every interaction.", - "version": "0.24.2", + "version": "0.24.3", "author": { "name": "second-brain" }, diff --git a/agents/dream-runner.md b/agents/dream-runner.md index 0b52175..b7c8075 100644 --- a/agents/dream-runner.md +++ b/agents/dream-runner.md @@ -89,6 +89,12 @@ Phases 1–6 work ONLY on `~/.second-brain/dreams/{dream_id}/staging/wiki/`. frontmatter edits (the next reindex overwrites them). - Remove session-narrative noise - Optimize frontmatter for BM25 retrieval (title 3×, description 2×, tags 2×) +- **AI-blocks (surface-only; skip if `SB_DREAM_AI_BLOCKS=off`)** — count staging structured + pages (learnings/decisions/entities/issues/concepts/security) lacking an `` + block. **Do NOT author blocks in staging** — authoring stays a single path through the live + **knowledge-maintainer** (it grounds the block in current prose; the dream would re-derive + from prose it is still rewriting). Surface the count in the report: "N structured pages have + no ai-block — run `/second-brain:maintain` to backfill." **Phase 5: SUMMARIZE** (skip if `SB_DREAM_SUMMARIZE=off`) - Cluster the staging wiki's link graph and write one theme page per cluster, so a fresh diff --git a/agents/knowledge-maintainer.md b/agents/knowledge-maintainer.md index 3056b14..ee019a3 100644 --- a/agents/knowledge-maintainer.md +++ b/agents/knowledge-maintainer.md @@ -220,6 +220,59 @@ The knowledge search uses BM25 with field weights: **title 3×, description 2×, - **Description**: what the source covers - **Body**: why it's relevant, key takeaways +## Phase 4b: AI-block authoring / backfill (machine-first shared intermediate) + +Each structured page should carry an `` block — the schema'd, +machine-first summary an AI reads instead of re-deriving the page from prose. The extractor +authors it at capture; you **backfill** pages that predate the feature and **refresh** stale +ones. This is the same per-category understanding Phase 4 just applied, emitted as the closed +schema (one source of truth for "what a good X page contains"). + +1. **Get the deterministic work-list** (blockless, substantive, structured pages): + ```bash + bash "$CLAUDE_PLUGIN_ROOT/scripts/kb-ai-block-candidates.sh" --knowledge-dir "$KD" + ``` + Each TSV row is `\t\t`. A page that already has a block is skipped + (idempotent); the script never mutates. + +2. **For each candidate** — closed vocabulary: only the **six structured types** `learnings, + decisions, entities, issues, concepts, security` (schemas below; never invent a field or a + type): + - Read the page body. **Extract** field values from the EXISTING prose + frontmatter only. + **Never invent / hallucinate** a value — a field you can't ground in the page is left + unset (the block is gentle/optional). Values are SHORT plain-text propositions, never + `[[wiki-links]]` (relations live in the edge graph). + - Schemas: `learnings`: claim, trigger, action, scope, evidence, supersedes · `decisions`: + context, choice, alternatives, rationale, status, supersedes · `entities`: identity, + current_state, depends_on, owns, status · `issues`: symptom, cause, fix, severity, status + · `concepts`: problem, solution, where_applied, tradeoffs · `security`: threat, + mitigation, scope, status. + - **Render** the region deterministically (closed-vocab post-filter + marker-token + neutralization handled by `renderAiBlock` — do not hand-format the markers): + ```bash + jq -nc --arg t "" --argjson b '' '{type:$t,block:$b}' \ + | node "$CLAUDE_PLUGIN_ROOT/mcp/dist/tools/ai-block-render-cli.bundle.js" + ``` + `` must be a valid JSON object of `"field": "short value"` pairs (omit + ungrounded fields), e.g. `{"claim":"…","action":"…"}`. On malformed input `jq`/the CLI emit + **nothing** (fail-safe) — confirm the rendered region is non-empty before the `Edit`. + - **Inject** the rendered region with `Edit`: between the frontmatter close (`---`) and the + first `# Heading`. Replace an existing region in place (refresh); never add a second. + - **Self-check**: a follow-up `knowledge_validate` run must show no new `ai_block_incomplete` + /`ai_block_missing` for the page (required fields you genuinely couldn't ground stay a + gentle `validateAiBlock` warning — that's fine; do NOT fabricate a value to silence it). + +3. **Budget**: each block authored counts as **one change against the 50/run cap** (unlike the + Phase 1 autofix sweep, which is uncounted). If candidates exceed the remaining budget, author + the highest-value first and **report the rest for the next run** — never exceed the cap. + +**Boundary:** Phase 4b runs **only on an explicit `/second-brain:maintain`** (the user asks to +"maintain" / "clean up" the KB) — a **threshold/auto-dispatched** maintenance run (see +*Autonomous Dispatch* below) **skips Phase 4b**, and it is never triggered by extraction or dream +output directly. Bulk-authoring page content stays deliberate and reviewed, not unattended (the +§5b automation boundary). Never author blocks for non-structured types or generated +`projects/`/`themes/` pages. + ## Phase 5: REINDEX 1. Regenerate `wiki/index.md` via `knowledge_reindex` MCP tool @@ -270,3 +323,10 @@ This agent should be dispatched: - When the user asks to "clean up" or "maintain" the knowledge base The agent is self-sufficient. It reads the hot tier and wiki, identifies all work across all 6 phases, executes in order, and reports results. No human input needed during execution. + +**Exception — Phase 4b (ai-block authoring/backfill):** an **auto-dispatched** run (the +`SB_MAINTAINER_THRESHOLD` wiki-write counter, a `knowledge_reindex`-issues trigger, or +post-extraction) performs the consolidation phases (1–4, 5, 6) but **skips Phase 4b** — +backfilling ai-blocks bulk-authors page content, so it is **explicit-invocation only** (a +deliberate `/second-brain:maintain` or "maintain/clean up the KB" request). Unattended runs never +bulk-author blocks; this keeps the auto-dispatch lightweight and the §5b automation boundary intact. diff --git a/docs/plans/2026-06-02-ai-native-representation-phase3.md b/docs/plans/2026-06-02-ai-native-representation-phase3.md new file mode 100644 index 0000000..86e0d88 --- /dev/null +++ b/docs/plans/2026-06-02-ai-native-representation-phase3.md @@ -0,0 +1,543 @@ +# AI-Native Representation — Phase 3 (Maintenance + Backfill) Implementation Plan + +> **For agentic workers:** Implement this plan task-by-task following TDD. Steps use checkbox (`- [ ]`) syntax for tracking. See `second-brain:test-driven-development` and `second-brain:verification-before-completion`. + +**Goal:** Close the AI-native block lifecycle — blocks now get *refreshed* (not just created), missing blocks are *surfaced* (validate + lint), and existing blockless pages get *backfilled* by the maintainer over its normal passes. + +**Architecture:** Three deliverables from spec §12 P3, each honoring the automation boundary (extractor automatic; dream/maintainer explicit-invocation-only): +1. **Refresh on update** — the *automatic* half: `merge-project-update.sh` UPDATE path replaces/injects the authored block (the extractor already computes `ai_region` for updates; today only CREATE consumes it). Offline, mawk-safe, idempotent. +2. **Lint staleness** — `knowledge_validate` gains `ai_block_missing` (structured, substantive, blockless page) — spec §7 "warns on missing block"; surfaced standalone by a new bash `/second-brain:lint` Check 4. Offline, deterministic. +3. **Backfill** — a deterministic `kb-ai-block-candidates.sh` work-list + a new maintainer **Phase 4b** that authors blocks from existing prose (the `kb-project-backfill` deterministic-script + LLM-authors split). Explicit-invocation, bounded by the 50-change cap, closed-vocabulary. The dream stays **surface-only** (single authoring path = the maintainer; correct rationale — NOT "reindex overwrites", which is false: reindex never touches ai-blocks). + +**Tech Stack:** bash (mawk 1.3.4 — never string-interpolate, use `-v`/`ENVIRON`), TypeScript (vitest), the existing `ai-block.ts` pure module + `ai-block-render-cli` bundle, agent/skill prompt files. + +**Non-goals (deferred, with rationale):** +- **Timestamp block↔prose drift** (spec §7 "block older than body's `updated`"): the block carries no authored-time; a single-file deterministic drift check isn't computable without adding a per-block timestamp that would pollute the closed schema + the snippet. The robust offline signal is *structural* (missing block) — shipped here. A future phase could add a block-level `updated:`/content-hash facet to enable true drift detection. +- **Phase 2b** (the block's own embedding/vector) — needs embeddings; offline-first stays BM25. + +--- + +## File Structure + +| File | Responsibility | Change | +|---|---|---| +| `scripts/merge-project-update.sh` | capture-time page writer | Modify: UPDATE path refreshes the block (415 boundary) | +| `scripts/extract-prompt.txt` | extractor instruction | Modify: emit `ai_block` for `update` too (§65-75) | +| `mcp/src/tools/knowledge-validate.ts` | wiki health checks | Modify: `ai_block_missing` (40-48) | +| `mcp/src/tools/knowledge-validate.test.ts` | validate tests | Modify: add `ai_block_missing` cases | +| `skills/lint/SKILL.md` | standalone offline health-check | Modify: Check 4 (missing block) | +| `tests/test-lint-skill.sh` | lint guard | Modify: assert Check 4 present + mawk-safe | +| `scripts/kb-ai-block-candidates.sh` | deterministic backfill work-list | Create | +| `tests/test-kb-ai-block-candidates.sh` | candidate-script guard | Create | +| `agents/knowledge-maintainer.md` | live consolidation agent | Modify: Phase 4b authoring/backfill | +| `tests/test-maintainer-ai-block-backfill.sh` | maintainer guard | Create | +| `skills/dream/SKILL.md`, `agents/dream-runner.md` | dream consolidation | Modify: surface-only block-gap note | +| `tests/test-dream-ai-block-parity.sh` | dream guard | Create | +| `scripts/merge-project-update.sh` test | refresh guard | Create `tests/test-merge-ai-block-refresh.sh` | + +--- + +## Task 1: Refresh the authored ai-block on UPDATE (merge-project-update.sh) + +**Files:** +- Test: `tests/test-merge-ai-block-refresh.sh` (create) +- Modify: `scripts/merge-project-update.sh` (insert before the `else` create-branch at line 415) + +`ai_region` is already computed at 355-359 for any update carrying `.ai_block`; only the CREATE branch (424) consumes it. Phase 3 makes the UPDATE branch (395-414) refresh the block: **replace** a complete existing region in place, **inject** after frontmatter if absent, and **leave untouched** a malformed (begin-without-end) page — never eat the body (the FORGET-bug class). + +- [ ] **Step 1: Write the failing test** + +```bash +#!/bin/bash +# Phase 3: merge-project-update.sh refreshes the authored ai-block on UPDATE (not just create). +# Replace-in-place when a complete region exists; inject when absent; never corrupt a malformed page. +set -u +ROOT="$(cd "$(dirname "$0")"/.. && pwd)"; SCRIPT="$ROOT/scripts/merge-project-update.sh" +TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT +fail(){ echo "FAIL: $1"; exit 1; }; pass(){ echo "PASS: $1"; } +command -v jq >/dev/null 2>&1 || fail "jq required" +command -v node >/dev/null 2>&1 || { echo "SKIP: node required"; exit 0; } +[ -f "$ROOT/mcp/dist/tools/ai-block-render-cli.bundle.js" ] || { echo "SKIP: render CLI not built"; exit 0; } + +KD="$TMP/knowledge"; mkdir -p "$KD/wiki/learnings" +PROJ="$TMP/PROJECT.md" +printf '%s\n' '# PROJECT: t' '## Goal' 'g.' '## State' 's.' '' > "$PROJ" +run(){ jq -nc "$1" | "$SCRIPT" --project-md "$PROJ" --knowledge-dir "$KD" >/dev/null 2>&1; } + +# 1) create page WITH a block, then UPDATE with a fresh block → block REPLACED in place, one region. +run '{recent_decisions:[],open_blockers:[],cross_refs:[],files_touched:[], + wiki_updates:[{category:"learnings",slug:"refr",action:"create",title:"R",description:"d", + content:"original prose body line.",ai_block:{claim:"old claim",action:"old action"}}]}' || fail "create exited nonzero" +run '{recent_decisions:[],open_blockers:[],cross_refs:[],files_touched:[], + wiki_updates:[{category:"learnings",slug:"refr",action:"update",title:"R",description:"d", + content:"a brand new distinct second observation.",ai_block:{claim:"new claim",action:"new action"}}]}' || fail "update exited nonzero" +P="$KD/wiki/learnings/refr.md" +[ "$(grep -c '' 'claim: dangling' '' '# Bad' 'irreplaceable prose tail.' > "$KD/wiki/learnings/bad.md" +before=$(cat "$KD/wiki/learnings/bad.md") +run '{recent_decisions:[],open_blockers:[],cross_refs:[],files_touched:[], + wiki_updates:[{category:"learnings",slug:"bad",action:"update",title:"Bad",description:"d", + content:"new line for the malformed page.",ai_block:{claim:"replacement",action:"do"}}]}' || true +grep -q 'irreplaceable prose tail' "$KD/wiki/learnings/bad.md" || fail "malformed page body was eaten by the refresh" +[ "$(grep -c 'ai:begin' "$KD/wiki/learnings/bad.md")" -eq 1 ] || fail "refresh added a second begin marker to a malformed page" +pass "malformed (begin-without-end) page is not corrupted by refresh" + +echo; echo "ALL PASS" +``` + +- [ ] **Step 2: Run it — expect FAIL** (`bash tests/test-merge-ai-block-refresh.sh`) on test 1 ("block not refreshed (claim still old)") — UPDATE discards `ai_region` today. + +- [ ] **Step 3: Implement** — in `scripts/merge-project-update.sh`, insert immediately after the `updated:` bump (after line 414) and before the `else` at line 415: + +```bash + # Phase 3: refresh the authored ai-block on UPDATE (CREATE already injects it; 355-359 + # computed $ai_region for any update carrying .ai_block). Replace a COMPLETE region in + # place; inject after frontmatter if absent; leave a malformed begin-without-end page + # untouched (never eat the body — the FORGET-bug class). mawk-safe: $ai_region via ENVIRON. + if [ -n "$ai_region" ]; then + if grep -q '' "$target_file" 2>/dev/null; then + AI_REGION="$ai_region" awk ' + BEGIN { reg = ENVIRON["AI_REGION"] } + // { drop=0; next } + drop { next } + { print } + ' "$target_file" > "$target_file.tmp" && mv "$target_file.tmp" "$target_file" + fi + # else: malformed (begin without end) → no-op (safe) + else + AI_REGION="$ai_region" awk ' + BEGIN { reg = ENVIRON["AI_REGION"]; fm=0; done=0 } + /^---[[:space:]]*$/ && fm<2 { print; fm++; if (fm==2 && !done) { print ""; print reg; print ""; done=1 } next } + { print } + ' "$target_file" > "$target_file.tmp" && mv "$target_file.tmp" "$target_file" + fi + fi +``` + +- [ ] **Step 4: Run it — expect PASS** (all three sub-tests). +- [ ] **Step 5: Commit** (`test(ai-block): RED refresh-on-update` then `feat(ai-block): refresh authored block on UPDATE (Phase 3 Task 1)`). + +## Task 2: Extractor emits ai_block for `update` actions (extract-prompt.txt) + +**Files:** +- Modify: `scripts/extract-prompt.txt` (the `ai_block` rule, lines 65-75) +- Test: extend `tests/test-merge-ai-block.sh`? No — prompt-only; guard by grep in Task-1's suite is sufficient. Add a one-line assertion to `tests/test-merge-ai-block-refresh.sh`. + +- [ ] **Step 1: Add the failing assertion** to `tests/test-merge-ai-block-refresh.sh` (before `echo "ALL PASS"`): + +```bash +# Prompt instructs refresh on update (so the extractor actually emits a block for update actions). +grep -qiE 'ai_block.*(update|refresh)|(update|refresh).*ai.?block|refresh.*in place' "$ROOT/scripts/extract-prompt.txt" \ + || fail "extract-prompt.txt does not instruct emitting/refreshing ai_block on update" +pass "extract-prompt instructs ai_block refresh on update" +``` + +- [ ] **Step 2: Run — expect FAIL** (no such instruction yet). +- [ ] **Step 3: Implement** — append to the `ai_block` rule (after line 75 "Omit `ai_block` entirely if you can't summarise it."): + +``` + Emit `ai_block` for `update` actions too — the page's block is refreshed in place from the + current best understanding, not only authored on `create`. A stale block is worse than none. +``` + +- [ ] **Step 4: Run — expect PASS.** +- [ ] **Step 5: Commit** (`feat(ai-block): extractor refreshes ai_block on update (Phase 3 Task 2)`). + +## Task 3: `knowledge_validate` flags `ai_block_missing` (spec §7) + +**Files:** +- Test: `mcp/src/tools/knowledge-validate.test.ts` (add cases) +- Modify: `mcp/src/tools/knowledge-validate.ts` + +- [ ] **Step 1: Write failing tests** — append inside the `describe('addFrontmatter category typing', …)` block (or a new describe) in `knowledge-validate.test.ts`: + +```typescript + it('flags a structured, substantive page with NO ai-block as ai_block_missing', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-miss-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'learnings'), { recursive: true }); + await fs.writeFile(join(wiki, 'learnings', 'big.md'), + '---\ntitle: Big\ntype: learnings\n---\n# Big\n' + 'substantive prose detail. '.repeat(20)); + const res = await knowledgeValidate(dir, { autofix: false }); + const w = res.issues.find(i => i.type === 'ai_block_missing' && /big/.test(i.message)); + expect(w).toBeDefined(); + expect(w!.severity).toBe('warning'); + }); + it('does NOT flag a short structured stub as ai_block_missing', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-stub-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'learnings'), { recursive: true }); + await fs.writeFile(join(wiki, 'learnings', 's.md'), '---\ntitle: S\ntype: learnings\n---\n# S\ntiny.'); + const res = await knowledgeValidate(dir, { autofix: false }); + expect(res.issues.find(i => i.type === 'ai_block_missing')).toBeUndefined(); + }); + it('does NOT flag a non-structured type (state) or a generated MOC as ai_block_missing', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-nonstruct-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'state'), { recursive: true }); + await fs.mkdir(join(wiki, 'projects'), { recursive: true }); + await fs.writeFile(join(wiki, 'state', 'st.md'), '---\ntitle: St\ntype: state\n---\n# St\n' + 'long state prose. '.repeat(20)); + await fs.writeFile(join(wiki, 'projects', 'p.md'), '---\ntitle: P\ntype: projects\ngenerated: true\n---\n# P\n' + 'long moc prose. '.repeat(20)); + const res = await knowledgeValidate(dir, { autofix: false }); + expect(res.issues.find(i => i.type === 'ai_block_missing')).toBeUndefined(); + }); + it('does NOT double-flag: a page WITH a block is never ai_block_missing', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-has-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'learnings'), { recursive: true }); + await fs.writeFile(join(wiki, 'learnings', 'h.md'), + '---\ntitle: H\ntype: learnings\n---\n\nclaim: c\naction: a\n\n# H\n' + 'prose. '.repeat(20)); + const res = await knowledgeValidate(dir, { autofix: false }); + expect(res.issues.find(i => i.type === 'ai_block_missing')).toBeUndefined(); + }); +``` + +- [ ] **Step 2: Run — expect FAIL** (`cd mcp && npx vitest run src/tools/knowledge-validate.test.ts`) — `ai_block_missing` not emitted. +- [ ] **Step 3: Implement** in `knowledge-validate.ts`: + - Extend the import (line 4): `import { parseAiBlock, validateAiBlock, stripAiBlock, AI_BLOCK_SCHEMAS } from './ai-block.js';` + - Add `'ai_block_missing'` to the `ValidationIssue['type']` union (line 7). + - Add a constant near the top of the module: `const AI_BLOCK_MIN_PROSE = 200;` + - Replace lines 40-48 with: + +```typescript + // AI-block checks (gentle, additive — spec §7). A block present but missing a required + // field → ai_block_incomplete. A structured, SUBSTANTIVE page with NO block at all → + // ai_block_missing (predates the feature / never authored). Stubs + non-structured types + // + generated MOCs are exempt. + const aiBlock = parseAiBlock(content); + const ptype = doc.type || basename(dirname(filePath)); + if (aiBlock) { + const missing = validateAiBlock(ptype, aiBlock); + if (missing.length) issues.push({ + type: 'ai_block_incomplete', severity: 'warning', path: filePath, + message: `ai-block missing required field(s) for type ${ptype}: ${missing.join(', ')}`, + }); + } else if (AI_BLOCK_SCHEMAS[ptype] && !/[/\\](projects|themes)[/\\]/.test(filePath)) { + const prose = stripAiBlock(content) + .replace(//g, '') + .replace(//g, '') + .replace(/^---\n[\s\S]*?\n---\n/, ''); + if (prose.trim().length >= AI_BLOCK_MIN_PROSE) issues.push({ + type: 'ai_block_missing', severity: 'warning', path: filePath, + message: `${ptype} page has substantive prose but no ai-block: ${slug}`, + }); + } +``` + +- [ ] **Step 4: Run — expect PASS** (4 new + existing green). +- [ ] **Step 5: Commit** (`feat(ai-block): knowledge_validate flags ai_block_missing (Phase 3 Task 3)`). + +## Task 4: `/second-brain:lint` Check 4 — missing ai-block (standalone, offline) + +**Files:** +- Modify: `skills/lint/SKILL.md` (add Check 4 + reporting line) +- Modify: `tests/test-lint-skill.sh` (parse-check the new awk block) + +The lint skill is standalone bash (no MCP). Mirror `knowledge_validate`'s structural signal in the lint idiom: scan the six structured-type dirs for pages without `` block (the machine-first +shared intermediate). A *substantive* page (≥ 200 non-space prose chars) with no block +predates the feature or was never authored — surface it so the maintainer backfills it. +Stubs are exempt. (`infm`/`drop`, not the reserved `in` — see the awk header note.) + +```bash +for type in learnings decisions entities issues concepts security; do + d="$KD/wiki/$type"; [ -d "$d" ] || continue + find "$d" -name '*.md' -type f ! -name 'index.md' 2>/dev/null | while read -r f; do + grep -q '/) drop=0; next } + { print } + ' "$f" | tr -d '[:space:]' | wc -c) + [ "$prose" -ge 200 ] && echo "MISSING-BLOCK: $type/$(basename "$f" .md) ($f)" + done +done +``` + +Suggest: run `/second-brain:maintain` (the knowledge-maintainer backfills blocks from the +page's prose) — do NOT hand-author here. +```` + +Add to `## Reporting` example a `## Missing ai-blocks (N)` section line. + +- [ ] **Step 4: Run — expect PASS** (`bash tests/test-lint-skill.sh`). +- [ ] **Step 5: Commit** (`feat(ai-block): lint Check 4 surfaces missing blocks (Phase 3 Task 4)`). + +## Task 5: `kb-ai-block-candidates.sh` — deterministic backfill work-list + +**Files:** +- Create: `scripts/kb-ai-block-candidates.sh` +- Test: `tests/test-kb-ai-block-candidates.sh` (create) + +- [ ] **Step 1: Write the failing test** + +```bash +#!/bin/bash +# kb-ai-block-candidates.sh: deterministic, read-only enumeration of blockless structured +# pages with substantive prose. One TSV row per candidate: \t\t. +set -u +ROOT="$(cd "$(dirname "$0")"/.. && pwd)"; SC="$ROOT/scripts/kb-ai-block-candidates.sh" +TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT +fail(){ echo "FAIL: $1"; exit 1; }; pass(){ echo "PASS: $1"; } +[ -f "$SC" ] || fail "script missing" +W="$TMP/knowledge/wiki"; mkdir -p "$W"/{learnings,state,projects} +# candidate: structured, no block, long prose +printf '%s\n' '---' 'title: A' 'type: learnings' '---' '# A' "$(printf 'real prose detail. %.0s' $(seq 1 20))" > "$W/learnings/cand.md" +# NOT: has a block +printf '%s\n' '---' 'title: B' 'type: learnings' '---' '' 'claim: c' '' '# B' "$(printf 'prose. %.0s' $(seq 1 20))" > "$W/learnings/hasblock.md" +# NOT: stub (short) +printf '%s\n' '---' 'title: C' 'type: learnings' '---' '# C' 'tiny.' > "$W/learnings/stub.md" +# NOT: non-structured type +printf '%s\n' '---' 'title: D' 'type: state' '---' '# D' "$(printf 'long state prose. %.0s' $(seq 1 20))" > "$W/state/st.md" +# NOT: generated MOC dir +printf '%s\n' '---' 'title: P' 'type: projects' '---' '# P' "$(printf 'long moc prose. %.0s' $(seq 1 20))" > "$W/projects/p.md" + +OUT=$(bash "$SC" --knowledge-dir "$TMP/knowledge") +echo "$OUT" | grep -q $'^learnings\tcand\t' || fail "candidate not listed" +echo "$OUT" | grep -q 'hasblock' && fail "page WITH a block listed" +echo "$OUT" | grep -q 'stub' && fail "stub listed" +echo "$OUT" | grep -q $'^state\t' && fail "non-structured type listed" +echo "$OUT" | grep -q 'projects' && fail "generated MOC listed" +[ "$(echo "$OUT" | grep -c .)" -eq 1 ] || fail "expected exactly 1 candidate, got $(echo "$OUT" | grep -c .)" +pass "enumerates only blockless, substantive, structured pages" + +# idempotent / read-only: running twice yields identical output, mutates nothing +H1=$(md5sum "$W/learnings/cand.md"); bash "$SC" --knowledge-dir "$TMP/knowledge" >/dev/null; H2=$(md5sum "$W/learnings/cand.md") +[ "$H1" = "$H2" ] || fail "script mutated a page (must be read-only)" +pass "read-only + deterministic" +echo; echo "ALL PASS" +``` + +- [ ] **Step 2: Run — expect FAIL** ("script missing"). +- [ ] **Step 3: Implement** `scripts/kb-ai-block-candidates.sh`: + +```bash +#!/bin/bash +# Deterministic, read-only backfill work-list (AI-native Phase 3). One TSV row per blockless +# structured wiki page with substantive prose: \t\t. The knowledge-maintainer +# (Phase 4b) authors an ai-block for each. Idempotent (pure read; a page with +# is skipped). No mutation. Mirrors kb-project-* tooling. mawk-safe. +# +# Usage: bash kb-ai-block-candidates.sh --knowledge-dir +set -u +KDIR=""; MINPROSE="${SB_AI_BLOCK_MIN_PROSE:-200}" +while [ $# -gt 0 ]; do + case "$1" in + --knowledge-dir) KDIR="${2:-}"; shift 2 ;; + *) shift ;; + esac +done +[ -n "$KDIR" ] || KDIR="${CLAUDE_PLUGIN_OPTION_KNOWLEDGE_DIR:-${KNOWLEDGE_DIR:-$HOME/knowledge}}" +KDIR="${KDIR/#\~/$HOME}" +WIKI="$KDIR/wiki"; [ -d "$WIKI" ] || exit 0 + +for type in learnings decisions entities issues concepts security; do + dir="$WIKI/$type"; [ -d "$dir" ] || continue + find "$dir" -name '*.md' -type f ! -name 'index.md' 2>/dev/null | sort | while IFS= read -r f; do + grep -q '/) drop=0; next } + { print } + ' "$f" | tr -d '[:space:]' | wc -c) + [ "$prose" -ge "$MINPROSE" ] || continue + printf '%s\t%s\t%s\n' "$type" "$(basename "$f" .md)" "$f" + done +done +``` + +- [ ] **Step 4: Run — expect PASS.** Then `chmod +x scripts/kb-ai-block-candidates.sh`. +- [ ] **Step 5: Commit** (`feat(ai-block): deterministic backfill candidate script (Phase 3 Task 5)`). + +## Task 6: knowledge-maintainer Phase 4b — author/refresh blocks (backfill) + +**Files:** +- Modify: `agents/knowledge-maintainer.md` (insert Phase 4b after Phase 4 ENRICH, before Phase 5, ~line 222) +- Test: `tests/test-maintainer-ai-block-backfill.sh` (create) + +- [ ] **Step 1: Write the failing guard test** + +```bash +#!/bin/bash +# Guard: the knowledge-maintainer knows how to backfill ai-blocks (Phase 4b), uses the +# deterministic candidate script + render path, and inherits the closed-vocab / cap / +# explicit-invocation boundary. Prompt-only guard (greps the agent contract). +set -u +ROOT="$(cd "$(dirname "$0")"/.. && pwd)"; M="$ROOT/agents/knowledge-maintainer.md" +fail(){ echo "FAIL: $1"; exit 1; }; pass(){ echo "PASS: $1"; } +grep -qiE 'Phase 4b|ai-block authoring|backfill' "$M" || fail "no Phase 4b / backfill section" +grep -q 'kb-ai-block-candidates.sh' "$M" || fail "does not reference the candidate work-list script" +grep -qiE 'renderAiBlock|ai-block-render-cli|render CLI' "$M" || fail "no render path referenced" +grep -qiE 'validateAiBlock|knowledge_validate' "$M" || fail "no self-validation referenced" +grep -qiE 'six (structured|known) types|closed[- ]vocab' "$M" || fail "closed-vocabulary boundary not stated" +grep -qiE 'never invent|extract.*from.*(prose|existing)|do not hallucinate' "$M" || fail "never-invent-values rule absent" +grep -qiE '50.*change|counted against|cap' "$M" || fail "cap inheritance not stated" +pass "maintainer Phase 4b contract present (candidate script + render + validate + closed-vocab + cap + never-invent)" +echo; echo "ALL PASS" +``` + +- [ ] **Step 2: Run — expect FAIL.** +- [ ] **Step 3: Implement** — insert after Phase 4 ENRICH (after line 221, before `## Phase 5: REINDEX`): + +````markdown +## Phase 4b: AI-block authoring / backfill (machine-first shared intermediate) + +Each structured page should carry an `` block — the schema'd, +machine-first summary an AI reads instead of re-deriving from prose. The extractor authors it +at capture; you **backfill** the pages that predate the feature and **refresh** stale ones. +This is the same per-category understanding Phase 4 just applied, emitted as the closed schema. + +1. **Get the deterministic work-list** (blockless, substantive, structured pages): + ```bash + bash "$CLAUDE_PLUGIN_ROOT/scripts/kb-ai-block-candidates.sh" --knowledge-dir "$KD" + ``` + Each TSV row is `\t\t`. (A page that already has a block is skipped — + idempotent. The script never mutates.) + +2. **For each candidate** (closed vocabulary — only `learnings, decisions, entities, issues, + concepts, security`; the schema fields per type are below — same source of truth as §4 of + the design spec and your ENRICH guidance above): + - Read the page body. **Extract** field values from the EXISTING prose + frontmatter only. + **Never invent / hallucinate** a value — a field you can't ground in the page is left + unset (the block is gentle/optional). Values are SHORT plain-text propositions, never + `[[wiki-links]]`. + - Schemas: `learnings`: claim, trigger, action, scope, evidence, supersedes · `decisions`: + context, choice, alternatives, rationale, status, supersedes · `entities`: identity, + current_state, depends_on, owns, status · `issues`: symptom, cause, fix, severity, status + · `concepts`: problem, solution, where_applied, tradeoffs · `security`: threat, + mitigation, scope, status. + - **Render** the region deterministically (closed-vocab post-filter, marker-token + neutralization — do not hand-format): + ```bash + jq -nc --arg t "" --argjson b '' '{type:$t,block:$b}' \ + | node "$CLAUDE_PLUGIN_ROOT/mcp/dist/tools/ai-block-render-cli.bundle.js" + ``` + - **Inject** the rendered region with `Edit`: between the frontmatter close (`---`) and the + first `# Heading`. Replace an existing region in place (refresh); never add a second. + - **Self-check**: a follow-up `knowledge_validate` run must report no `ai_block_incomplete` + /`ai_block_missing` for the page (required fields you couldn't ground stay a gentle + warning — that's fine, don't fabricate to silence it). + +3. **Budget**: each block authored counts as **one change against the 50/run cap** (unlike the + Phase 1 autofix sweep, which is uncounted). If candidates exceed the remaining budget, + author the highest-value first and **report the rest for the next run** — never exceed the cap. + +**Boundary:** this runs only when you (the maintainer) are **explicitly invoked** — never +auto-dispatched on extraction or dream output (that would revert the 0.21.0 hardening). Never +author blocks for non-structured types or generated `projects/`/`themes/` pages. +```` + +- [ ] **Step 4: Run — expect PASS** (`bash tests/test-maintainer-ai-block-backfill.sh`). +- [ ] **Step 5: Commit** (`feat(ai-block): maintainer Phase 4b authors/backfills blocks (Phase 3 Task 6)`). + +## Task 7: Dream parity — surface-only block-gap note + +**Files:** +- Modify: `skills/dream/SKILL.md` (2d ENRICH), `agents/dream-runner.md` (Phase 4 ENRICH) +- Test: `tests/test-dream-ai-block-parity.sh` (create) + +Dream is **surface-only** for blocks. Rationale (corrected): authoring stays a **single path +through the maintainer** — the dream already defers relationship/edge curation to the +maintainer, and authoring a block in staging would re-derive from prose the dream just rewrote. +(NOT because "reindex overwrites blocks" — reindex never touches ai-blocks; they are authored +content, unlike `related:`/`graph:begin`.) Gated `SB_DREAM_AI_BLOCKS=off`. + +- [ ] **Step 1: Write the failing guard test** + +```bash +#!/bin/bash +# Guard: dream is ai-block AWARE but surface-only (never authors blocks in staging), and gates +# behind SB_DREAM_AI_BLOCKS. Both the skill and the runner agent must agree (inline/background parity). +set -u +ROOT="$(cd "$(dirname "$0")"/.. && pwd)" +S="$ROOT/skills/dream/SKILL.md"; R="$ROOT/agents/dream-runner.md" +fail(){ echo "FAIL: $1"; exit 1; }; pass(){ echo "PASS: $1"; } +for f in "$S" "$R"; do + grep -qiE 'ai-block|ai:begin' "$f" || fail "$(basename "$f"): no ai-block awareness" + grep -qiE 'surface|suggest|recommend|report' "$f" || fail "$(basename "$f"): no surface-only language" + grep -q 'SB_DREAM_AI_BLOCKS' "$f" || fail "$(basename "$f"): missing SB_DREAM_AI_BLOCKS kill switch" + grep -qiE 'do not author|never author|not.*hand-author|maintainer' "$f" || fail "$(basename "$f"): does not defer authoring to the maintainer" +done +pass "dream skill + runner: ai-block surface-only, defers to maintainer, kill-switch present" +echo; echo "ALL PASS" +``` + +- [ ] **Step 2: Run — expect FAIL.** +- [ ] **Step 3: Implement** — add to `skills/dream/SKILL.md` at the end of **2d. ENRICH** (after its existing bullets): + +```markdown +- **AI-blocks (surface-only; skip if `SB_DREAM_AI_BLOCKS=off`)** — scan staging for structured + pages (learnings/decisions/entities/issues/concepts/security) lacking an `` + block and count them. **Do NOT author blocks in staging** — block authoring stays a single + path through the live **knowledge-maintainer** (it grounds the block in the page's current + prose; the dream would re-derive from prose it is still rewriting). Surface the count in the + dream report: "N structured pages have no ai-block — run `/second-brain:maintain` to backfill." +``` + +Add the mirrored bullet to `agents/dream-runner.md` Phase 4 ENRICH (after its `related:` deferral note), same wording. + +- [ ] **Step 4: Run — expect PASS.** +- [ ] **Step 5: Commit** (`feat(ai-block): dream surfaces block gaps, defers authoring (Phase 3 Task 7)`). + +## Task 8: Build + release (0.24.3) + gate + +**Files:** `mcp/` (build), `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `mcp/src/server.ts`, `skills/upgrade/SKILL.md`. + +- [ ] **Step 1:** Build bundles — `cd mcp && npm run build` (recompiles validate + bundles; render CLI unchanged but rebuild is safe). Verify `dist/` updated. +- [ ] **Step 2:** Version lockstep → **0.24.3**: bump `version` in `.claude-plugin/plugin.json` and the matching entry in `.claude-plugin/marketplace.json`; bump the knowledge-base server version string in `mcp/src/server.ts` (validate gained an issue type — additive; patch the version, no MCP tool-signature change → protocol version unchanged unless the repo convention bumps it). +- [ ] **Step 3:** Add a `0.24.3` migration row to `skills/upgrade/SKILL.md` (target version, summary: refresh-on-update + ai_block_missing validate/lint + maintainer Phase 4b backfill + candidate script + dream surface-only; idempotent check: "no precondition — bumping the marker is sufficient; optional: run `/second-brain:maintain` once to backfill existing pages, bounded 50/run"). +- [ ] **Step 4:** Full suite — `bash tests/run-all.sh` (or the repo's runner): all shell + `cd mcp && npx vitest run` green. Read the output. +- [ ] **Step 5:** Deep-review gate — `/second-brain:code-review-deep` on the branch; fix every confirmed finding + regression-test; re-run suite green. (Release gate per the deep-review-release-gate preference.) +- [ ] **Step 6:** Commit + open PR `feat(kb): AI-native representation Phase 3 — maintenance + backfill — 0.24.3`. + +--- + +## Self-Review + +- **Spec coverage (§12 P3):** refresh (Task 1+2), lint staleness (Task 3 validate "warns on missing block" §7 + Task 4 lint), backfill (Task 5 candidates + Task 6 maintainer); dream parity (Task 7). Timestamp-drift (§7 second bullet) explicitly deferred with rationale. ✔ +- **Automation boundary (§5b):** Task 1/2 = automatic (extractor); Task 6/7 = explicit-invocation-only; no auto-dispatch added. ✔ +- **mawk-safety:** every awk uses `ENVIRON`/`-v` or literal programs — no shell interpolation into awk source. ✔ +- **Offline-first:** every Phase-3 path is BM25/bash; no embeddings introduced. ✔ +- **Type consistency:** `ai_block_missing` issue type used identically in validate code + tests; `kb-ai-block-candidates.sh` TSV `\t\t` consumed verbatim by maintainer Phase 4b. ✔ +- **Reversibility:** blocks are additive marked regions (delete to revert); candidate script + lint are read-only. ✔ diff --git a/mcp/dist/cli/sb-entry.bundle.js b/mcp/dist/cli/sb-entry.bundle.js index 18852be..3014ce8 100644 --- a/mcp/dist/cli/sb-entry.bundle.js +++ b/mcp/dist/cli/sb-entry.bundle.js @@ -6298,6 +6298,9 @@ var AI_BLOCK_SCHEMAS = { concepts: { fields: ["problem", "solution", "where_applied", "tradeoffs"], required: ["problem", "solution"] }, security: { fields: ["threat", "mitigation", "scope", "status"], required: ["threat", "mitigation"] } }; +function schemaFor(type) { + return Object.prototype.hasOwnProperty.call(AI_BLOCK_SCHEMAS, type) ? AI_BLOCK_SCHEMAS[type] : void 0; +} function parseAiBlock(content) { const m = content.match(AI_BLOCK_RE); if (!m) return null; @@ -6321,7 +6324,7 @@ function stripAiBlock(text) { return text.replace(AI_BLOCK_RE_G, ""); } function aiBlockSnippet(type, block) { - const schema = AI_BLOCK_SCHEMAS[type]; + const schema = schemaFor(type); const order = schema ? schema.fields : Object.keys(block); return order.filter((f) => (block[f] ?? "").trim()).map((f) => `${f}: ${block[f].trim()}`).join("; "); } diff --git a/mcp/dist/server.bundle.js b/mcp/dist/server.bundle.js index 02c2196..8d13b65 100644 --- a/mcp/dist/server.bundle.js +++ b/mcp/dist/server.bundle.js @@ -27638,6 +27638,9 @@ var AI_BLOCK_SCHEMAS = { concepts: { fields: ["problem", "solution", "where_applied", "tradeoffs"], required: ["problem", "solution"] }, security: { fields: ["threat", "mitigation", "scope", "status"], required: ["threat", "mitigation"] } }; +function schemaFor(type) { + return Object.prototype.hasOwnProperty.call(AI_BLOCK_SCHEMAS, type) ? AI_BLOCK_SCHEMAS[type] : void 0; +} function parseAiBlock(content) { const m = content.match(AI_BLOCK_RE); if (!m) return null; @@ -27661,12 +27664,12 @@ function stripAiBlock(text) { return text.replace(AI_BLOCK_RE_G, ""); } function aiBlockSnippet(type, block) { - const schema = AI_BLOCK_SCHEMAS[type]; + const schema = schemaFor(type); const order = schema ? schema.fields : Object.keys(block); return order.filter((f) => (block[f] ?? "").trim()).map((f) => `${f}: ${block[f].trim()}`).join("; "); } function validateAiBlock(type, block) { - const schema = AI_BLOCK_SCHEMAS[type]; + const schema = schemaFor(type); if (!schema) return []; return schema.required.filter((f) => !block[f] || !block[f].trim()); } @@ -28149,6 +28152,7 @@ import { join as join10 } from "path"; // src/tools/knowledge-validate.ts import { promises as fs9 } from "fs"; import { join as join8, basename, dirname as dirname2, relative as relative2 } from "path"; +var AI_BLOCK_MIN_PROSE = Number(process.env.SB_AI_BLOCK_MIN_PROSE) || 200; async function knowledgeValidate(knowledgeDir, opts = {}) { const wikiDir = join8(knowledgeDir, "wiki"); const issues = []; @@ -28162,8 +28166,8 @@ async function knowledgeValidate(knowledgeDir, opts = {}) { const doc = parseDoc(content, filePath); parsedDocs.push(doc); const aiBlock = parseAiBlock(content); + const ptype = doc.type || basename(dirname2(filePath)); if (aiBlock) { - const ptype = doc.type || basename(dirname2(filePath)); const missing = validateAiBlock(ptype, aiBlock); if (missing.length) issues.push({ type: "ai_block_incomplete", @@ -28171,6 +28175,14 @@ async function knowledgeValidate(knowledgeDir, opts = {}) { path: filePath, message: `ai-block missing required field(s) for type ${ptype}: ${missing.join(", ")}` }); + } else if (schemaFor(ptype) && !/[/\\](projects|themes)[/\\]/.test(filePath)) { + const prose = stripAiBlock(content).replace(//g, "").replace(//g, "").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, ""); + if (prose.trim().length >= AI_BLOCK_MIN_PROSE) issues.push({ + type: "ai_block_missing", + severity: "warning", + path: filePath, + message: `${ptype} page has substantive prose but no ai-block: ${slug}` + }); } if (!/[/\\](projects|themes)[/\\]/.test(filePath)) { if (!slugMap.has(slug)) slugMap.set(slug, []); @@ -29317,7 +29329,7 @@ function resolveActiveSlug() { return slugFromProjectDir(activeProjectDir()); } var server = new McpServer( - { name: "knowledge-base", version: "2.6.0" }, + { name: "knowledge-base", version: "2.6.1" }, { capabilities: { logging: {} }, instructions: "BM25-scored search over the local knowledge base. Use knowledge_search to find relevant wiki pages (searches full content with field-weighted scoring), knowledge_reindex to regenerate the wiki index.md catalog (also runs validation with autofix), knowledge_validate to check wiki health (broken links, orphans, duplicates, session-narrative pages), knowledge_stats for an overview of wiki size and categories, pin_to_user to record a user-level preference, pin_to_project to append blockers/decisions to a project's PROJECT.md, and archive_to_wiki to graduate a [resolved] entry from a project file into the wiki. Dream tools: dream_create to start a background consolidation job (snapshots wiki + selects transcripts), dream_status to check progress, dream_list to see all dreams, dream_accept to apply a completed dream's changes, dream_discard to reject changes, and dream_cancel to stop a running dream. Episodic memory: episodic_search to search past conversation transcripts (hybrid vector + text, multi-concept AND), episodic_read to read a specific transcript section. Relational graph: knowledge_relate to assert/invalidate a typed bi-temporal relationship (requires|affects|relates|part_of|supersedes) between two pages, and knowledge_neighbors to walk a page's dependency neighbourhood (multi-hop, directional, point-in-time via as_of)." diff --git a/mcp/dist/server.js b/mcp/dist/server.js index 0e9c442..0340090 100644 --- a/mcp/dist/server.js +++ b/mcp/dist/server.js @@ -48,7 +48,7 @@ function resolveActiveSlug() { // launch dir and unreliable). Fall back to cwd on older CLIs that don't set it. return slugFromProjectDir(activeProjectDir()); } -const server = new McpServer({ name: "knowledge-base", version: "2.6.0" }, { +const server = new McpServer({ name: "knowledge-base", version: "2.6.1" }, { capabilities: { logging: {} }, instructions: "BM25-scored search over the local knowledge base. Use knowledge_search to find relevant wiki pages (searches full content with field-weighted scoring), knowledge_reindex to regenerate the wiki index.md catalog (also runs validation with autofix), knowledge_validate to check wiki health (broken links, orphans, duplicates, session-narrative pages), knowledge_stats for an overview of wiki size and categories, pin_to_user to record a user-level preference, pin_to_project to append blockers/decisions to a project's PROJECT.md, and archive_to_wiki to graduate a [resolved] entry from a project file into the wiki. Dream tools: dream_create to start a background consolidation job (snapshots wiki + selects transcripts), dream_status to check progress, dream_list to see all dreams, dream_accept to apply a completed dream's changes, dream_discard to reject changes, and dream_cancel to stop a running dream. Episodic memory: episodic_search to search past conversation transcripts (hybrid vector + text, multi-concept AND), episodic_read to read a specific transcript section. Relational graph: knowledge_relate to assert/invalidate a typed bi-temporal relationship (requires|affects|relates|part_of|supersedes) between two pages, and knowledge_neighbors to walk a page's dependency neighbourhood (multi-hop, directional, point-in-time via as_of).", }); diff --git a/mcp/dist/tools/ai-block-render-cli.bundle.js b/mcp/dist/tools/ai-block-render-cli.bundle.js index de3a959..f5ebfba 100644 --- a/mcp/dist/tools/ai-block-render-cli.bundle.js +++ b/mcp/dist/tools/ai-block-render-cli.bundle.js @@ -8,11 +8,14 @@ var AI_BLOCK_SCHEMAS = { concepts: { fields: ["problem", "solution", "where_applied", "tradeoffs"], required: ["problem", "solution"] }, security: { fields: ["threat", "mitigation", "scope", "status"], required: ["threat", "mitigation"] } }; +function schemaFor(type) { + return Object.prototype.hasOwnProperty.call(AI_BLOCK_SCHEMAS, type) ? AI_BLOCK_SCHEMAS[type] : void 0; +} var AI_BLOCK_RE_G = new RegExp(AI_BLOCK_RE.source, "g"); var AI_BLOCK_RENDER_BEGIN = ""; var AI_BLOCK_RENDER_END = ""; function renderAiBlock(type, block) { - const schema = AI_BLOCK_SCHEMAS[type]; + const schema = schemaFor(type); if (!schema) return ""; const lines = []; for (const f of schema.fields) { diff --git a/mcp/dist/tools/ai-block.d.ts b/mcp/dist/tools/ai-block.d.ts index 7cedba8..b15f94b 100644 --- a/mcp/dist/tools/ai-block.d.ts +++ b/mcp/dist/tools/ai-block.d.ts @@ -4,6 +4,11 @@ export interface AiBlockSchema { required: string[]; } export declare const AI_BLOCK_SCHEMAS: Record; +/** Own-key-safe schema lookup. A page's `type` is author-controlled (frontmatter or wiki + * sub-dir name), so a bare `AI_BLOCK_SCHEMAS[type]` index would resolve inherited + * `Object.prototype` keys — `type === "constructor"` returns the Object constructor (truthy, + * no `.fields`/`.required`), crashing the consumers with a TypeError. Guard with hasOwnProperty. */ +export declare function schemaFor(type: string): AiBlockSchema | undefined; /** Parse the flat-YAML `key: value` body of the ai:begin…ai:end region into an object. * A line not matching `key:` is folded (appended) into the previous field's value. * Returns null when the page has no block. */ diff --git a/mcp/dist/tools/ai-block.d.ts.map b/mcp/dist/tools/ai-block.d.ts.map index 9e89994..fc30489 100644 --- a/mcp/dist/tools/ai-block.d.ts.map +++ b/mcp/dist/tools/ai-block.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"ai-block.d.ts","sourceRoot":"","sources":["../../src/tools/ai-block.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,WAAW,QAA8D,CAAC;AAEvF,MAAM,WAAW,aAAa;IAAG,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;CAAE;AAIxE,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAO1D,CAAC;AAEF;;+CAE+C;AAC/C,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAa3E;AAID,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,eAAO,MAAM,qBAAqB,uEAAkE,CAAC;AACrG,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;4FAE4F;AAC5F,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAYjF;AAED;kFACkF;AAClF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAIlF;AAED,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,CAIrF"} \ No newline at end of file +{"version":3,"file":"ai-block.d.ts","sourceRoot":"","sources":["../../src/tools/ai-block.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,WAAW,QAA8D,CAAC;AAEvF,MAAM,WAAW,aAAa;IAAG,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;CAAE;AAIxE,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAO1D,CAAC;AAEF;;;qGAGqG;AACrG,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAEjE;AAED;;+CAE+C;AAC/C,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAa3E;AAID,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,eAAO,MAAM,qBAAqB,uEAAkE,CAAC;AACrG,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;4FAE4F;AAC5F,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAYjF;AAED;kFACkF;AAClF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAIlF;AAED,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,CAIrF"} \ No newline at end of file diff --git a/mcp/dist/tools/ai-block.js b/mcp/dist/tools/ai-block.js index 80538b8..590943e 100644 --- a/mcp/dist/tools/ai-block.js +++ b/mcp/dist/tools/ai-block.js @@ -16,6 +16,13 @@ export const AI_BLOCK_SCHEMAS = { concepts: { fields: ['problem', 'solution', 'where_applied', 'tradeoffs'], required: ['problem', 'solution'] }, security: { fields: ['threat', 'mitigation', 'scope', 'status'], required: ['threat', 'mitigation'] }, }; +/** Own-key-safe schema lookup. A page's `type` is author-controlled (frontmatter or wiki + * sub-dir name), so a bare `AI_BLOCK_SCHEMAS[type]` index would resolve inherited + * `Object.prototype` keys — `type === "constructor"` returns the Object constructor (truthy, + * no `.fields`/`.required`), crashing the consumers with a TypeError. Guard with hasOwnProperty. */ +export function schemaFor(type) { + return Object.prototype.hasOwnProperty.call(AI_BLOCK_SCHEMAS, type) ? AI_BLOCK_SCHEMAS[type] : undefined; +} /** Parse the flat-YAML `key: value` body of the ai:begin…ai:end region into an object. * A line not matching `key:` is folded (appended) into the previous field's value. * Returns null when the page has no block. */ @@ -51,7 +58,7 @@ export const AI_BLOCK_RENDER_END = ''; * fields emitted in the type's schema order (closed vocabulary: unknown fields dropped), * empty values skipped. Returns '' when no schema field has a value (→ inject nothing). */ export function renderAiBlock(type, block) { - const schema = AI_BLOCK_SCHEMAS[type]; + const schema = schemaFor(type); if (!schema) return ''; // closed vocabulary: only the six known types produce a block const lines = []; @@ -69,13 +76,13 @@ export function renderAiBlock(type, block) { /** Compact one-line summary of a block (schema-ordered, present fields only) — the * "shared intermediate" returned as a search snippet / injected into context. */ export function aiBlockSnippet(type, block) { - const schema = AI_BLOCK_SCHEMAS[type]; + const schema = schemaFor(type); const order = schema ? schema.fields : Object.keys(block); return order.filter(f => (block[f] ?? '').trim()).map(f => `${f}: ${block[f].trim()}`).join('; '); } /** Missing REQUIRED fields for the page type (empty when type unknown or all present). */ export function validateAiBlock(type, block) { - const schema = AI_BLOCK_SCHEMAS[type]; + const schema = schemaFor(type); if (!schema) return []; return schema.required.filter(f => !block[f] || !block[f].trim()); diff --git a/mcp/dist/tools/ai-block.js.map b/mcp/dist/tools/ai-block.js.map index 4704da8..761e06d 100644 --- a/mcp/dist/tools/ai-block.js.map +++ b/mcp/dist/tools/ai-block.js.map @@ -1 +1 @@ -{"version":3,"file":"ai-block.js","sourceRoot":"","sources":["../../src/tools/ai-block.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"ai-block.js","sourceRoot":"","sources":["../../src/tools/ai-block.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/mcp/dist/tools/knowledge-reindex.bundle.js b/mcp/dist/tools/knowledge-reindex.bundle.js index d115ad7..e0478a1 100644 --- a/mcp/dist/tools/knowledge-reindex.bundle.js +++ b/mcp/dist/tools/knowledge-reindex.bundle.js @@ -6155,6 +6155,9 @@ var AI_BLOCK_SCHEMAS = { concepts: { fields: ["problem", "solution", "where_applied", "tradeoffs"], required: ["problem", "solution"] }, security: { fields: ["threat", "mitigation", "scope", "status"], required: ["threat", "mitigation"] } }; +function schemaFor(type) { + return Object.prototype.hasOwnProperty.call(AI_BLOCK_SCHEMAS, type) ? AI_BLOCK_SCHEMAS[type] : void 0; +} function parseAiBlock(content) { const m = content.match(AI_BLOCK_RE); if (!m) return null; @@ -6178,7 +6181,7 @@ function stripAiBlock(text) { return text.replace(AI_BLOCK_RE_G, ""); } function validateAiBlock(type, block) { - const schema = AI_BLOCK_SCHEMAS[type]; + const schema = schemaFor(type); if (!schema) return []; return schema.required.filter((f) => !block[f] || !block[f].trim()); } @@ -6276,6 +6279,7 @@ function extractYamlList(yaml, key) { // src/tools/knowledge-validate.ts import { promises as fs2 } from "fs"; import { join as join2, basename, dirname, relative } from "path"; +var AI_BLOCK_MIN_PROSE = Number(process.env.SB_AI_BLOCK_MIN_PROSE) || 200; async function knowledgeValidate(knowledgeDir, opts = {}) { const wikiDir = join2(knowledgeDir, "wiki"); const issues = []; @@ -6289,8 +6293,8 @@ async function knowledgeValidate(knowledgeDir, opts = {}) { const doc = parseDoc(content, filePath); parsedDocs.push(doc); const aiBlock = parseAiBlock(content); + const ptype = doc.type || basename(dirname(filePath)); if (aiBlock) { - const ptype = doc.type || basename(dirname(filePath)); const missing = validateAiBlock(ptype, aiBlock); if (missing.length) issues.push({ type: "ai_block_incomplete", @@ -6298,6 +6302,14 @@ async function knowledgeValidate(knowledgeDir, opts = {}) { path: filePath, message: `ai-block missing required field(s) for type ${ptype}: ${missing.join(", ")}` }); + } else if (schemaFor(ptype) && !/[/\\](projects|themes)[/\\]/.test(filePath)) { + const prose = stripAiBlock(content).replace(//g, "").replace(//g, "").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, ""); + if (prose.trim().length >= AI_BLOCK_MIN_PROSE) issues.push({ + type: "ai_block_missing", + severity: "warning", + path: filePath, + message: `${ptype} page has substantive prose but no ai-block: ${slug}` + }); } if (!/[/\\](projects|themes)[/\\]/.test(filePath)) { if (!slugMap.has(slug)) slugMap.set(slug, []); diff --git a/mcp/dist/tools/knowledge-search-cli.bundle.js b/mcp/dist/tools/knowledge-search-cli.bundle.js index 620293e..ccaa22c 100644 --- a/mcp/dist/tools/knowledge-search-cli.bundle.js +++ b/mcp/dist/tools/knowledge-search-cli.bundle.js @@ -6290,6 +6290,9 @@ var AI_BLOCK_SCHEMAS = { concepts: { fields: ["problem", "solution", "where_applied", "tradeoffs"], required: ["problem", "solution"] }, security: { fields: ["threat", "mitigation", "scope", "status"], required: ["threat", "mitigation"] } }; +function schemaFor(type) { + return Object.prototype.hasOwnProperty.call(AI_BLOCK_SCHEMAS, type) ? AI_BLOCK_SCHEMAS[type] : void 0; +} function parseAiBlock(content) { const m = content.match(AI_BLOCK_RE); if (!m) return null; @@ -6313,7 +6316,7 @@ function stripAiBlock(text) { return text.replace(AI_BLOCK_RE_G, ""); } function aiBlockSnippet(type, block) { - const schema = AI_BLOCK_SCHEMAS[type]; + const schema = schemaFor(type); const order = schema ? schema.fields : Object.keys(block); return order.filter((f) => (block[f] ?? "").trim()).map((f) => `${f}: ${block[f].trim()}`).join("; "); } diff --git a/mcp/dist/tools/knowledge-validate.bundle.js b/mcp/dist/tools/knowledge-validate.bundle.js index 8225bc0..2c1b0dc 100644 --- a/mcp/dist/tools/knowledge-validate.bundle.js +++ b/mcp/dist/tools/knowledge-validate.bundle.js @@ -6076,6 +6076,9 @@ var AI_BLOCK_SCHEMAS = { concepts: { fields: ["problem", "solution", "where_applied", "tradeoffs"], required: ["problem", "solution"] }, security: { fields: ["threat", "mitigation", "scope", "status"], required: ["threat", "mitigation"] } }; +function schemaFor(type) { + return Object.prototype.hasOwnProperty.call(AI_BLOCK_SCHEMAS, type) ? AI_BLOCK_SCHEMAS[type] : void 0; +} function parseAiBlock(content) { const m = content.match(AI_BLOCK_RE); if (!m) return null; @@ -6099,7 +6102,7 @@ function stripAiBlock(text) { return text.replace(AI_BLOCK_RE_G, ""); } function validateAiBlock(type, block) { - const schema = AI_BLOCK_SCHEMAS[type]; + const schema = schemaFor(type); if (!schema) return []; return schema.required.filter((f) => !block[f] || !block[f].trim()); } @@ -6195,6 +6198,7 @@ function extractYamlList(yaml, key) { } // src/tools/knowledge-validate.ts +var AI_BLOCK_MIN_PROSE = Number(process.env.SB_AI_BLOCK_MIN_PROSE) || 200; async function knowledgeValidate(knowledgeDir, opts = {}) { const wikiDir = join2(knowledgeDir, "wiki"); const issues = []; @@ -6208,8 +6212,8 @@ async function knowledgeValidate(knowledgeDir, opts = {}) { const doc = parseDoc(content, filePath); parsedDocs.push(doc); const aiBlock = parseAiBlock(content); + const ptype = doc.type || basename(dirname(filePath)); if (aiBlock) { - const ptype = doc.type || basename(dirname(filePath)); const missing = validateAiBlock(ptype, aiBlock); if (missing.length) issues.push({ type: "ai_block_incomplete", @@ -6217,6 +6221,14 @@ async function knowledgeValidate(knowledgeDir, opts = {}) { path: filePath, message: `ai-block missing required field(s) for type ${ptype}: ${missing.join(", ")}` }); + } else if (schemaFor(ptype) && !/[/\\](projects|themes)[/\\]/.test(filePath)) { + const prose = stripAiBlock(content).replace(//g, "").replace(//g, "").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, ""); + if (prose.trim().length >= AI_BLOCK_MIN_PROSE) issues.push({ + type: "ai_block_missing", + severity: "warning", + path: filePath, + message: `${ptype} page has substantive prose but no ai-block: ${slug}` + }); } if (!/[/\\](projects|themes)[/\\]/.test(filePath)) { if (!slugMap.has(slug)) slugMap.set(slug, []); diff --git a/mcp/dist/tools/knowledge-validate.d.ts b/mcp/dist/tools/knowledge-validate.d.ts index a94eeb5..e86bd81 100644 --- a/mcp/dist/tools/knowledge-validate.d.ts +++ b/mcp/dist/tools/knowledge-validate.d.ts @@ -1,5 +1,5 @@ export interface ValidationIssue { - type: 'orphan_file' | 'broken_link' | 'missing_frontmatter' | 'duplicate_slug' | 'stale_page' | 'empty_page' | 'root_orphan' | 'ai_block_incomplete'; + type: 'orphan_file' | 'broken_link' | 'missing_frontmatter' | 'duplicate_slug' | 'stale_page' | 'empty_page' | 'root_orphan' | 'ai_block_incomplete' | 'ai_block_missing'; severity: 'error' | 'warning'; path: string; message: string; diff --git a/mcp/dist/tools/knowledge-validate.d.ts.map b/mcp/dist/tools/knowledge-validate.d.ts.map index 13f9084..cb6aacb 100644 --- a/mcp/dist/tools/knowledge-validate.d.ts.map +++ b/mcp/dist/tools/knowledge-validate.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"knowledge-validate.d.ts","sourceRoot":"","sources":["../../src/tools/knowledge-validate.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,aAAa,GAAG,aAAa,GAAG,qBAAqB,GAAG,gBAAgB,GAAG,YAAY,GAAG,YAAY,GAAG,aAAa,GAAG,qBAAqB,CAAC;IACrJ,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,iBAAiB,CACrC,YAAY,EAAE,MAAM,EACpB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAC/B,OAAO,CAAC,gBAAgB,CAAC,CAsJ3B;AAOD,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4DrF"} \ No newline at end of file +{"version":3,"file":"knowledge-validate.d.ts","sourceRoot":"","sources":["../../src/tools/knowledge-validate.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,aAAa,GAAG,aAAa,GAAG,qBAAqB,GAAG,gBAAgB,GAAG,YAAY,GAAG,YAAY,GAAG,aAAa,GAAG,qBAAqB,GAAG,kBAAkB,CAAC;IAC1K,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,iBAAiB,CACrC,YAAY,EAAE,MAAM,EACpB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAC/B,OAAO,CAAC,gBAAgB,CAAC,CAiK3B;AAOD,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4DrF"} \ No newline at end of file diff --git a/mcp/dist/tools/knowledge-validate.js b/mcp/dist/tools/knowledge-validate.js index fb792bb..754aace 100644 --- a/mcp/dist/tools/knowledge-validate.js +++ b/mcp/dist/tools/knowledge-validate.js @@ -1,7 +1,11 @@ import { promises as fs } from 'fs'; import { join, basename, dirname, relative } from 'path'; import { parseDoc } from './knowledge-search.js'; -import { parseAiBlock, validateAiBlock } from './ai-block.js'; +import { parseAiBlock, validateAiBlock, stripAiBlock, schemaFor } from './ai-block.js'; +// A structured page with this much prose (non-frontmatter, marked regions stripped) but no +// ai-block is a backfill candidate; shorter pages are legitimate stubs, exempt. Env-overridable +// in lockstep with kb-ai-block-candidates.sh / lint Check 4 (default 200). +const AI_BLOCK_MIN_PROSE = Number(process.env.SB_AI_BLOCK_MIN_PROSE) || 200; export async function knowledgeValidate(knowledgeDir, opts = {}) { const wikiDir = join(knowledgeDir, 'wiki'); const issues = []; @@ -14,11 +18,13 @@ export async function knowledgeValidate(knowledgeDir, opts = {}) { const slug = basename(filePath, '.md'); const doc = parseDoc(content, filePath); parsedDocs.push(doc); - // AI-block schema check (gentle): only when a block exists — an absent block is fine - // (additive during migration). Missing required field for the page's type → a warning. + // AI-block checks (gentle, additive — spec §7). Block present but missing a required field + // → ai_block_incomplete. A structured, SUBSTANTIVE page with NO block at all → + // ai_block_missing (predates the feature / never authored). Stubs + non-structured types + + // generated MOCs (projects/, themes/) are exempt. const aiBlock = parseAiBlock(content); + const ptype = doc.type || basename(dirname(filePath)); if (aiBlock) { - const ptype = doc.type || basename(dirname(filePath)); const missing = validateAiBlock(ptype, aiBlock); if (missing.length) issues.push({ @@ -26,6 +32,17 @@ export async function knowledgeValidate(knowledgeDir, opts = {}) { message: `ai-block missing required field(s) for type ${ptype}: ${missing.join(', ')}`, }); } + else if (schemaFor(ptype) && !/[/\\](projects|themes)[/\\]/.test(filePath)) { + const prose = stripAiBlock(content) + .replace(//g, '') + .replace(//g, '') + .replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, ''); + if (prose.trim().length >= AI_BLOCK_MIN_PROSE) + issues.push({ + type: 'ai_block_missing', severity: 'warning', path: filePath, + message: `${ptype} page has substantive prose but no ai-block: ${slug}`, + }); + } // Generated MOC dirs (projects/, themes/) are derived VIEWS, not source pages — a MOC that // shares a slug with a real page (e.g. project "architecture-v1" + page architecture-v1.md) // is not a true duplicate, so exclude them from the duplicate_slug check. diff --git a/mcp/dist/tools/knowledge-validate.js.map b/mcp/dist/tools/knowledge-validate.js.map index ee3905e..e7ac4e1 100644 --- a/mcp/dist/tools/knowledge-validate.js.map +++ b/mcp/dist/tools/knowledge-validate.js.map @@ -1 +1 @@ -{"version":3,"file":"knowledge-validate.js","sourceRoot":"","sources":["../../src/tools/knowledge-validate.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"knowledge-validate.js","sourceRoot":"","sources":["../../src/tools/knowledge-validate.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/mcp/dist/tools/knowledge-validate.test.js b/mcp/dist/tools/knowledge-validate.test.js index 7a9977d..fca6e8e 100644 --- a/mcp/dist/tools/knowledge-validate.test.js +++ b/mcp/dist/tools/knowledge-validate.test.js @@ -52,6 +52,64 @@ describe('addFrontmatter category typing', () => { const res = await knowledgeValidate(dir, { autofix: false }); expect(res.issues.find(i => i.type === 'duplicate_slug' && /architecture-v1/.test(i.message))).toBeUndefined(); }); + it('flags a structured, substantive page with NO ai-block as ai_block_missing', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-miss-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'learnings'), { recursive: true }); + await fs.writeFile(join(wiki, 'learnings', 'big.md'), '---\ntitle: Big\ntype: learnings\n---\n# Big\n' + 'substantive prose detail. '.repeat(20)); + const res = await knowledgeValidate(dir, { autofix: false }); + const w = res.issues.find(i => i.type === 'ai_block_missing' && /big/.test(i.message)); + expect(w).toBeDefined(); + expect(w.severity).toBe('warning'); + }); + it('does NOT flag a short structured stub as ai_block_missing', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-stub-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'learnings'), { recursive: true }); + await fs.writeFile(join(wiki, 'learnings', 's.md'), '---\ntitle: S\ntype: learnings\n---\n# S\ntiny.'); + const res = await knowledgeValidate(dir, { autofix: false }); + expect(res.issues.find(i => i.type === 'ai_block_missing')).toBeUndefined(); + }); + it('does NOT flag a non-structured type (state) or a generated MOC as ai_block_missing', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-nonstruct-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'state'), { recursive: true }); + await fs.mkdir(join(wiki, 'projects'), { recursive: true }); + await fs.writeFile(join(wiki, 'state', 'st.md'), '---\ntitle: St\ntype: state\n---\n# St\n' + 'long state prose. '.repeat(20)); + await fs.writeFile(join(wiki, 'projects', 'p.md'), '---\ntitle: P\ntype: projects\ngenerated: true\n---\n# P\n' + 'long moc prose. '.repeat(20)); + const res = await knowledgeValidate(dir, { autofix: false }); + expect(res.issues.find(i => i.type === 'ai_block_missing')).toBeUndefined(); + }); + it('does not crash or flag a page typed with an inherited Object key (constructor/__proto__)', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-proto-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'learnings'), { recursive: true }); + // a structured-dir page whose type: is a prototype key, substantive prose, no block + await fs.writeFile(join(wiki, 'learnings', 'a.md'), '---\ntitle: A\ntype: constructor\n---\n# A\n' + 'long prose detail. '.repeat(20)); + // and one WITH a block — exercises validateAiBlock's lookup (the TypeError crash path) + await fs.writeFile(join(wiki, 'learnings', 'b.md'), '---\ntitle: B\ntype: __proto__\n---\n\nclaim: c\n\n# B\nprose'); + const res = await knowledgeValidate(dir, { autofix: false }); // must not throw + expect(res.issues.find(i => i.type === 'ai_block_missing')).toBeUndefined(); + expect(res.issues.find(i => i.type === 'ai_block_incomplete')).toBeUndefined(); + }); + it('strips CRLF frontmatter before measuring prose (no false-positive ai_block_missing)', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-crlf-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'learnings'), { recursive: true }); + // CRLF page: frontmatter ALONE > 200 chars, body tiny — only flagged if FM isn't stripped + const page = ['---', 'title: C', 'type: learnings', `description: ${'x'.repeat(260)}`, '---', '# C', 'tiny.'].join('\r\n'); + await fs.writeFile(join(wiki, 'learnings', 'c.md'), page); + const res = await knowledgeValidate(dir, { autofix: false }); + expect(res.issues.find(i => i.type === 'ai_block_missing')).toBeUndefined(); + }); + it('does NOT double-flag: a page WITH a block is never ai_block_missing', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-has-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'learnings'), { recursive: true }); + await fs.writeFile(join(wiki, 'learnings', 'h.md'), '---\ntitle: H\ntype: learnings\n---\n\nclaim: c\naction: a\n\n# H\n' + 'prose. '.repeat(20)); + const res = await knowledgeValidate(dir, { autofix: false }); + expect(res.issues.find(i => i.type === 'ai_block_missing')).toBeUndefined(); + }); it('does NOT flag a valid [[target|alias]] related link as broken (alias split)', async () => { const dir = await fs.mkdtemp(join(tmpdir(), 'kv-alias-')); const wiki = join(dir, 'wiki'); diff --git a/mcp/dist/tools/knowledge-validate.test.js.map b/mcp/dist/tools/knowledge-validate.test.js.map index 886bbbc..3fe7c60 100644 --- a/mcp/dist/tools/knowledge-validate.test.js.map +++ b/mcp/dist/tools/knowledge-validate.test.js.map @@ -1 +1 @@ -{"version":3,"file":"knowledge-validate.test.js","sourceRoot":"","sources":["../../src/tools/knowledge-validate.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"knowledge-validate.test.js","sourceRoot":"","sources":["../../src/tools/knowledge-validate.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/mcp/src/server.ts b/mcp/src/server.ts index e96192e..776a217 100644 --- a/mcp/src/server.ts +++ b/mcp/src/server.ts @@ -51,7 +51,7 @@ function resolveActiveSlug(): string | undefined { } const server = new McpServer( - { name: "knowledge-base", version: "2.6.0" }, + { name: "knowledge-base", version: "2.6.1" }, { capabilities: { logging: {} }, instructions: "BM25-scored search over the local knowledge base. Use knowledge_search to find relevant wiki pages (searches full content with field-weighted scoring), knowledge_reindex to regenerate the wiki index.md catalog (also runs validation with autofix), knowledge_validate to check wiki health (broken links, orphans, duplicates, session-narrative pages), knowledge_stats for an overview of wiki size and categories, pin_to_user to record a user-level preference, pin_to_project to append blockers/decisions to a project's PROJECT.md, and archive_to_wiki to graduate a [resolved] entry from a project file into the wiki. Dream tools: dream_create to start a background consolidation job (snapshots wiki + selects transcripts), dream_status to check progress, dream_list to see all dreams, dream_accept to apply a completed dream's changes, dream_discard to reject changes, and dream_cancel to stop a running dream. Episodic memory: episodic_search to search past conversation transcripts (hybrid vector + text, multi-concept AND), episodic_read to read a specific transcript section. Relational graph: knowledge_relate to assert/invalidate a typed bi-temporal relationship (requires|affects|relates|part_of|supersedes) between two pages, and knowledge_neighbors to walk a page's dependency neighbourhood (multi-hop, directional, point-in-time via as_of).", diff --git a/mcp/src/tools/ai-block.ts b/mcp/src/tools/ai-block.ts index bf7db23..243319b 100644 --- a/mcp/src/tools/ai-block.ts +++ b/mcp/src/tools/ai-block.ts @@ -21,6 +21,14 @@ export const AI_BLOCK_SCHEMAS: Record = { security: { fields: ['threat', 'mitigation', 'scope', 'status'], required: ['threat', 'mitigation'] }, }; +/** Own-key-safe schema lookup. A page's `type` is author-controlled (frontmatter or wiki + * sub-dir name), so a bare `AI_BLOCK_SCHEMAS[type]` index would resolve inherited + * `Object.prototype` keys — `type === "constructor"` returns the Object constructor (truthy, + * no `.fields`/`.required`), crashing the consumers with a TypeError. Guard with hasOwnProperty. */ +export function schemaFor(type: string): AiBlockSchema | undefined { + return Object.prototype.hasOwnProperty.call(AI_BLOCK_SCHEMAS, type) ? AI_BLOCK_SCHEMAS[type] : undefined; +} + /** Parse the flat-YAML `key: value` body of the ai:begin…ai:end region into an object. * A line not matching `key:` is folded (appended) into the previous field's value. * Returns null when the page has no block. */ @@ -53,7 +61,7 @@ export const AI_BLOCK_RENDER_END = ''; * fields emitted in the type's schema order (closed vocabulary: unknown fields dropped), * empty values skipped. Returns '' when no schema field has a value (→ inject nothing). */ export function renderAiBlock(type: string, block: Record): string { - const schema = AI_BLOCK_SCHEMAS[type]; + const schema = schemaFor(type); if (!schema) return ''; // closed vocabulary: only the six known types produce a block const lines: string[] = []; for (const f of schema.fields) { @@ -69,14 +77,14 @@ export function renderAiBlock(type: string, block: Record): stri /** Compact one-line summary of a block (schema-ordered, present fields only) — the * "shared intermediate" returned as a search snippet / injected into context. */ export function aiBlockSnippet(type: string, block: Record): string { - const schema = AI_BLOCK_SCHEMAS[type]; + const schema = schemaFor(type); const order = schema ? schema.fields : Object.keys(block); return order.filter(f => (block[f] ?? '').trim()).map(f => `${f}: ${block[f].trim()}`).join('; '); } /** Missing REQUIRED fields for the page type (empty when type unknown or all present). */ export function validateAiBlock(type: string, block: Record): string[] { - const schema = AI_BLOCK_SCHEMAS[type]; + const schema = schemaFor(type); if (!schema) return []; return schema.required.filter(f => !block[f] || !block[f].trim()); } diff --git a/mcp/src/tools/knowledge-validate.test.ts b/mcp/src/tools/knowledge-validate.test.ts index 4b458aa..2cc8408 100644 --- a/mcp/src/tools/knowledge-validate.test.ts +++ b/mcp/src/tools/knowledge-validate.test.ts @@ -63,6 +63,69 @@ describe('addFrontmatter category typing', () => { expect(res.issues.find(i => i.type === 'duplicate_slug' && /architecture-v1/.test(i.message))).toBeUndefined(); }); + it('flags a structured, substantive page with NO ai-block as ai_block_missing', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-miss-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'learnings'), { recursive: true }); + await fs.writeFile(join(wiki, 'learnings', 'big.md'), + '---\ntitle: Big\ntype: learnings\n---\n# Big\n' + 'substantive prose detail. '.repeat(20)); + const res = await knowledgeValidate(dir, { autofix: false }); + const w = res.issues.find(i => i.type === 'ai_block_missing' && /big/.test(i.message)); + expect(w).toBeDefined(); + expect(w!.severity).toBe('warning'); + }); + it('does NOT flag a short structured stub as ai_block_missing', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-stub-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'learnings'), { recursive: true }); + await fs.writeFile(join(wiki, 'learnings', 's.md'), '---\ntitle: S\ntype: learnings\n---\n# S\ntiny.'); + const res = await knowledgeValidate(dir, { autofix: false }); + expect(res.issues.find(i => i.type === 'ai_block_missing')).toBeUndefined(); + }); + it('does NOT flag a non-structured type (state) or a generated MOC as ai_block_missing', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-nonstruct-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'state'), { recursive: true }); + await fs.mkdir(join(wiki, 'projects'), { recursive: true }); + await fs.writeFile(join(wiki, 'state', 'st.md'), '---\ntitle: St\ntype: state\n---\n# St\n' + 'long state prose. '.repeat(20)); + await fs.writeFile(join(wiki, 'projects', 'p.md'), '---\ntitle: P\ntype: projects\ngenerated: true\n---\n# P\n' + 'long moc prose. '.repeat(20)); + const res = await knowledgeValidate(dir, { autofix: false }); + expect(res.issues.find(i => i.type === 'ai_block_missing')).toBeUndefined(); + }); + it('does not crash or flag a page typed with an inherited Object key (constructor/__proto__)', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-proto-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'learnings'), { recursive: true }); + // a structured-dir page whose type: is a prototype key, substantive prose, no block + await fs.writeFile(join(wiki, 'learnings', 'a.md'), + '---\ntitle: A\ntype: constructor\n---\n# A\n' + 'long prose detail. '.repeat(20)); + // and one WITH a block — exercises validateAiBlock's lookup (the TypeError crash path) + await fs.writeFile(join(wiki, 'learnings', 'b.md'), + '---\ntitle: B\ntype: __proto__\n---\n\nclaim: c\n\n# B\nprose'); + const res = await knowledgeValidate(dir, { autofix: false }); // must not throw + expect(res.issues.find(i => i.type === 'ai_block_missing')).toBeUndefined(); + expect(res.issues.find(i => i.type === 'ai_block_incomplete')).toBeUndefined(); + }); + it('strips CRLF frontmatter before measuring prose (no false-positive ai_block_missing)', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-crlf-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'learnings'), { recursive: true }); + // CRLF page: frontmatter ALONE > 200 chars, body tiny — only flagged if FM isn't stripped + const page = ['---', 'title: C', 'type: learnings', `description: ${'x'.repeat(260)}`, '---', '# C', 'tiny.'].join('\r\n'); + await fs.writeFile(join(wiki, 'learnings', 'c.md'), page); + const res = await knowledgeValidate(dir, { autofix: false }); + expect(res.issues.find(i => i.type === 'ai_block_missing')).toBeUndefined(); + }); + it('does NOT double-flag: a page WITH a block is never ai_block_missing', async () => { + const dir = await fs.mkdtemp(join(tmpdir(), 'kv-has-')); + const wiki = join(dir, 'wiki'); + await fs.mkdir(join(wiki, 'learnings'), { recursive: true }); + await fs.writeFile(join(wiki, 'learnings', 'h.md'), + '---\ntitle: H\ntype: learnings\n---\n\nclaim: c\naction: a\n\n# H\n' + 'prose. '.repeat(20)); + const res = await knowledgeValidate(dir, { autofix: false }); + expect(res.issues.find(i => i.type === 'ai_block_missing')).toBeUndefined(); + }); + it('does NOT flag a valid [[target|alias]] related link as broken (alias split)', async () => { const dir = await fs.mkdtemp(join(tmpdir(), 'kv-alias-')); const wiki = join(dir, 'wiki'); diff --git a/mcp/src/tools/knowledge-validate.ts b/mcp/src/tools/knowledge-validate.ts index b913685..e6461a0 100644 --- a/mcp/src/tools/knowledge-validate.ts +++ b/mcp/src/tools/knowledge-validate.ts @@ -1,10 +1,15 @@ import { promises as fs } from 'fs'; import { join, basename, dirname, relative } from 'path'; import { parseDoc, ParsedDoc } from './knowledge-search.js'; -import { parseAiBlock, validateAiBlock } from './ai-block.js'; +import { parseAiBlock, validateAiBlock, stripAiBlock, schemaFor } from './ai-block.js'; + +// A structured page with this much prose (non-frontmatter, marked regions stripped) but no +// ai-block is a backfill candidate; shorter pages are legitimate stubs, exempt. Env-overridable +// in lockstep with kb-ai-block-candidates.sh / lint Check 4 (default 200). +const AI_BLOCK_MIN_PROSE = Number(process.env.SB_AI_BLOCK_MIN_PROSE) || 200; export interface ValidationIssue { - type: 'orphan_file' | 'broken_link' | 'missing_frontmatter' | 'duplicate_slug' | 'stale_page' | 'empty_page' | 'root_orphan' | 'ai_block_incomplete'; + type: 'orphan_file' | 'broken_link' | 'missing_frontmatter' | 'duplicate_slug' | 'stale_page' | 'empty_page' | 'root_orphan' | 'ai_block_incomplete' | 'ai_block_missing'; severity: 'error' | 'warning'; path: string; message: string; @@ -35,16 +40,27 @@ export async function knowledgeValidate( const doc = parseDoc(content, filePath); parsedDocs.push(doc); - // AI-block schema check (gentle): only when a block exists — an absent block is fine - // (additive during migration). Missing required field for the page's type → a warning. + // AI-block checks (gentle, additive — spec §7). Block present but missing a required field + // → ai_block_incomplete. A structured, SUBSTANTIVE page with NO block at all → + // ai_block_missing (predates the feature / never authored). Stubs + non-structured types + + // generated MOCs (projects/, themes/) are exempt. const aiBlock = parseAiBlock(content); + const ptype = doc.type || basename(dirname(filePath)); if (aiBlock) { - const ptype = doc.type || basename(dirname(filePath)); const missing = validateAiBlock(ptype, aiBlock); if (missing.length) issues.push({ type: 'ai_block_incomplete', severity: 'warning', path: filePath, message: `ai-block missing required field(s) for type ${ptype}: ${missing.join(', ')}`, }); + } else if (schemaFor(ptype) && !/[/\\](projects|themes)[/\\]/.test(filePath)) { + const prose = stripAiBlock(content) + .replace(//g, '') + .replace(//g, '') + .replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, ''); + if (prose.trim().length >= AI_BLOCK_MIN_PROSE) issues.push({ + type: 'ai_block_missing', severity: 'warning', path: filePath, + message: `${ptype} page has substantive prose but no ai-block: ${slug}`, + }); } // Generated MOC dirs (projects/, themes/) are derived VIEWS, not source pages — a MOC that diff --git a/scripts/extract-prompt.txt b/scripts/extract-prompt.txt index e397dad..ad0c81e 100644 --- a/scripts/extract-prompt.txt +++ b/scripts/extract-prompt.txt @@ -73,6 +73,8 @@ Rules: Each value is a SHORT, canonical, one-line proposition. Values are PLAIN SLUGS or text — never `[[wiki-links]]` (relations belong in `relations`, not the block). Omit fields you can't fill; unknown fields are dropped. Omit `ai_block` entirely if you can't summarise it. + Emit `ai_block` for `update` actions too — the page's block is refreshed in place from the + current best understanding, not only authored on `create`. A stale block is worse than none. - Persona signals: implicit or explicit behavioral patterns observed in the user's actions during this session. Focus on HOW the user works, not WHAT they built. - "high" confidence: user explicitly stated a preference or corrected your approach ("don't do X", "always Y") - "medium" confidence: user's behavior consistently implies a pattern (e.g., always uses early returns, never writes comments, prefers terse responses) diff --git a/scripts/kb-ai-block-candidates.sh b/scripts/kb-ai-block-candidates.sh new file mode 100755 index 0000000..6b073a9 --- /dev/null +++ b/scripts/kb-ai-block-candidates.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Deterministic, read-only backfill work-list (AI-native Phase 3). One TSV row per blockless +# structured wiki page with substantive prose: \t\t. The knowledge-maintainer +# (Phase 4b) authors an ai-block for each. Idempotent (pure read; a page with +# is skipped). No mutation. Mirrors kb-project-* tooling. mawk-safe. +# +# Usage: bash kb-ai-block-candidates.sh --knowledge-dir +set -u +KDIR=""; MINPROSE="${SB_AI_BLOCK_MIN_PROSE:-200}" +while [ $# -gt 0 ]; do + case "$1" in + --knowledge-dir) KDIR="${2:-}"; shift 2 ;; + *) shift ;; + esac +done +[ -n "$KDIR" ] || KDIR="${CLAUDE_PLUGIN_OPTION_KNOWLEDGE_DIR:-${KNOWLEDGE_DIR:-$HOME/knowledge}}" +KDIR="${KDIR/#\~/$HOME}" +WIKI="$KDIR/wiki"; [ -d "$WIKI" ] || exit 0 + +for type in learnings decisions entities issues concepts security; do + dir="$WIKI/$type"; [ -d "$dir" ] || continue + find "$dir" -name '*.md' -type f ! -name 'index.md' 2>/dev/null | sort | while IFS= read -r f; do + # idempotent: skip pages that already have a block (any spacing). `grep -l` (whole-file) + # not `grep -q` -- `grep -q`'s early exit SIGPIPEs the upstream `find` under job-control + # (monitor) shells, ending the loop after one page; keep this identical to lint Check 4. + [ -n "$(grep -lE '/ { drop=0; next } + drop { buf = buf $0 "\n"; next } + { print } + END { if (drop) printf "%s", buf } + ' "$f" | tr -d '[:space:]' | wc -c) + [ "$prose" -ge "$MINPROSE" ] || continue + printf '%s\t%s\t%s\n' "$type" "$(basename "$f" .md)" "$f" + done +done diff --git a/scripts/merge-project-update.sh b/scripts/merge-project-update.sh index 56f260c..170cd99 100755 --- a/scripts/merge-project-update.sh +++ b/scripts/merge-project-update.sh @@ -393,6 +393,34 @@ if [ "$WIKI_UPDATES_COUNT" -gt 0 ]; then fi if [ "$action" = "update" ] && [ -f "$target_file" ]; then + # Phase 3: refresh the authored ai-block FIRST -- before the prose-dedup early-continue -- + # so a sharpened block is applied even when the prose is a duplicate ("a stale block is + # worse than none"). Replace a COMPLETE region in place (FIRST ai:begin only, via a + # `replaced` latch + a `drop` that never re-arms -- so a stray ai:begin in prose can't + # duplicate the region or run away to EOF); inject after a real frontmatter fence if absent; + # leave a malformed begin-without-end page untouched (never eat the body). mawk-safe: ENVIRON. + if [ -n "$ai_region" ]; then + if grep -qE '' "$target_file" 2>/dev/null; then + AI_REGION="$ai_region" awk ' + BEGIN { reg = ENVIRON["AI_REGION"] } + // { drop=0; next } + drop { next } + { print } + ' "$target_file" > "$target_file.tmp" && mv "$target_file.tmp" "$target_file" + fi + # else: malformed (begin without end) -> no-op (safe) + else + AI_REGION="$ai_region" awk ' + BEGIN { reg = ENVIRON["AI_REGION"]; fm=0; done=0 } + NR==1 && !/^---[[:space:]]*$/ { print; noinject=1; next } + noinject { print; next } + /^---[[:space:]]*$/ && fm<2 { print; fm++; if (fm==2 && !done) { print ""; print reg; print ""; done=1 } next } + { print } + ' "$target_file" > "$target_file.tmp" && mv "$target_file.tmp" "$target_file" + fi + fi # Content-aware dedup: skip if the first 60 chars of new content already appear in the page content_check=$(echo "$content" | head -c 60) if grep -qF "$content_check" "$target_file" 2>/dev/null; then diff --git a/skills/dream/SKILL.md b/skills/dream/SKILL.md index a7ef6e3..fa6d065 100644 --- a/skills/dream/SKILL.md +++ b/skills/dream/SKILL.md @@ -95,6 +95,12 @@ in the report (per 2c), not frontmatter edits (the next reindex overwrites them) - **Entities**: overview + current state + a body section referencing linked learnings/decisions - **Concepts**: Problem → Solution → Where Applied → Trade-offs - Apply transcript mining insights from Step 1 (new pages, updated content) +- **AI-blocks (surface-only; skip if `SB_DREAM_AI_BLOCKS=off`)** — scan staging for structured + pages (learnings/decisions/entities/issues/concepts/security) lacking an `` + block and count them. **Do NOT author blocks in staging** — block authoring stays a single + path through the live **knowledge-maintainer** (it grounds the block in the page's current + prose; the dream would re-derive from prose it is still rewriting). Surface the count in the + dream report: "N structured pages have no ai-block — run `/second-brain:maintain` to backfill." **2e. SUMMARIZE** — whole-corpus theme pages (skip if `SB_DREAM_SUMMARIZE=off`). Cluster the staging wiki's link graph and write one summary page per cluster, so a fresh session can be diff --git a/skills/lint/SKILL.md b/skills/lint/SKILL.md index e63a3ed..3686bc4 100644 --- a/skills/lint/SKILL.md +++ b/skills/lint/SKILL.md @@ -150,6 +150,49 @@ done Suggest: drop the broken slug from `Cross-references`, or create the missing wiki page. +### 4. Missing ai-block on structured pages + +A page in one of the six structured categories (learnings, decisions, entities, issues, +concepts, security) should carry an `` block — the machine-first +"shared intermediate" an AI reads instead of re-deriving the page from prose. A *substantive* +page (≥ 200 non-space prose chars) with no block predates the feature or was never authored. +Stubs are exempt. (`infm`/`drop`, not the reserved `in` — see the awk header note.) + +```bash +for type in learnings decisions entities issues concepts security; do + d="$KD/wiki/$type"; [ -d "$d" ] || continue + find "$d" -name '*.md' -type f ! -name 'index.md' 2>/dev/null | while read -r f; do + # Skip pages that already have a block. Use `grep -l` (whole-file, capture) NOT `grep -q`: + # `grep -q` exits at the first match, and when this block is pasted into a job-control shell + # (monitor mode, how /second-brain:lint runs it) that early exit SIGPIPEs the upstream `find` + # and ends the loop after one page -> the check would silently report 0. `grep -l` reads to EOF. + [ -n "$(grep -lE '/ { drop=0; next } + drop { buf = buf $0 "\n"; next } + { print } + END { if (drop) printf "%s", buf } + ' "$f" | tr -d '[:space:]' | wc -c) + [ "$prose" -ge 200 ] && echo "MISSING-BLOCK: $type/$(basename "$f" .md) ($f)" + done +done +``` + +Suggest: run `/second-brain:maintain` — the knowledge-maintainer (Phase 4b) backfills the block +from the page's own prose. Do **not** hand-author the block here (lint is read-only by default). + ## Reporting Present findings in three sections; keep counts visible. Example: @@ -168,6 +211,9 @@ Present findings in three sections; keep counts visible. Example: ## Broken Cross-references (1) - my-repo -> obsolete-concept (no wiki page) +## Missing ai-blocks (1) +- learnings/oauth-bare-flag (substantive page, no ai:begin block) + ## Summary - Wiki pages scanned: 47 - Issues found: 6 diff --git a/skills/upgrade/SKILL.md b/skills/upgrade/SKILL.md index 52de816..42aff61 100644 --- a/skills/upgrade/SKILL.md +++ b/skills/upgrade/SKILL.md @@ -35,6 +35,7 @@ Each migration is identified by its target version. Run only migrations whose ta | To version | Migration | Idempotent check | |---|---|---| | **vector-deps health** (re-runs every upgrade) | Smoke-import `@huggingface/transformers` from `$CLAUDE_PLUGIN_ROOT/mcp`. On failure, run `bash $CLAUDE_PLUGIN_ROOT/bin/install-vector-deps.sh`. **Why**: mcp bundles mark `@huggingface/transformers` as esbuild `--external` because its native binaries (`onnxruntime-node`, `sharp`) can't be statically packed. A plugin cache refresh ships `dist/` but does not touch `node_modules/`, so vector search silently degrades to text-only on every fresh install or cache wipe. Without this gate the user sees no error — just empty embeddings and degraded recall. Idempotent: the script is a no-op when the package and import smoke-check both succeed. | `cd "$CLAUDE_PLUGIN_ROOT/mcp" && node --input-type=module -e 'await import("@huggingface/transformers"); console.log("ok")' >/dev/null 2>&1` — if exit 0, skip. Otherwise run `bash "$CLAUDE_PLUGIN_ROOT/bin/install-vector-deps.sh"`; report the ~70MB network requirement before installing. | +| **0.24.3** | AI-native knowledge representation — **Phase 3** (maintenance + backfill — the block lifecycle closes). (a) **Refresh on update**: `merge-project-update.sh`'s UPDATE path now replaces a complete `` region in place (injects one after the frontmatter when absent), so an authored block is *refreshed* with the page, not only created — and `extract-prompt.txt` instructs the extractor to emit `ai_block` for `update` actions too. Idempotent, mawk-safe, leaves a malformed begin-without-end page untouched (never eats the body). (b) **Lint staleness**: `knowledge_validate` gains `ai_block_missing` (a structured, substantive — ≥200 prose chars — page with no block → gentle warning; stubs / non-structured types / generated `projects`+`themes` MOCs exempt), and `/second-brain:lint` Check 4 surfaces the same signal standalone (offline bash). (c) **Backfill**: new deterministic, read-only `scripts/kb-ai-block-candidates.sh` (idempotent work-list of blockless structured pages) feeds the **knowledge-maintainer's new Phase 4b**, which authors a block per page from its *existing prose only* (never invents values), renders via the CLI, injects between frontmatter and H1, self-checks via `knowledge_validate`, counts each against the 50/run cap, and runs **explicit-invocation only**. The **dream** stays surface-only (counts blockless staging pages, recommends `/second-brain:maintain`; never authors in staging — single authoring path through the maintainer; gated `SB_DREAM_AI_BLOCKS=off`). MCP server → 2.6.1. Additive + back-compat. The §7 timestamp block↔prose drift heuristic is deferred (the block carries no authored-time; structural missing-block is the robust offline signal). New tests: `test-merge-ai-block-refresh.sh`, validate `ai_block_missing` cases, lint Check 4, `test-kb-ai-block-candidates.sh`, `test-maintainer-ai-block-backfill.sh`, `test-dream-ai-block-parity.sh`. | No precondition — bumping the marker is sufficient. **Optional one-shot backfill** of existing pages: run `/second-brain:maintain` (Phase 4b authors blocks from prose, bounded 50/run — re-run until `kb-ai-block-candidates.sh` reports none). New writes already refresh their block automatically. Reversible: delete the marked region to revert a page to pure prose. | | **0.24.2** | AI-native knowledge representation — **Phase 2** (consumption: the block gets *used*). The machine-first shared intermediate now drives retrieval + injection: (a) `knowledge_search` indexes the block as a **proposition-level BM25 field** (weight 1.5; the prose body field excludes the block so terms aren't double-counted) and **returns the block as the result snippet** when present — so `session-load` + `persona-context`, which inject the search snippet, now hand the reader the structured proposition instead of a prose fragment; (b) new `knowledge_fetch` **`block` tier** reads just the shared intermediate (falls back to the summary when a page has no block); (c) new pure `aiBlockSnippet` renders the compact schema-ordered one-line form. MCP server → 2.6.0 (new fetch tier). Additive + back-compat: a page with no block behaves exactly as before. Phase 2b (the block's own embedding/vector) needs embeddings → deferred (offline-first stays BM25). Dream/maintainer refresh + lint staleness + backfill are Phase 3. New tests: `aiBlockSnippet`, search Phase-2 case, `knowledge-fetch.test.ts`. | No precondition — bumping the marker is sufficient. | | **0.24.1** | AI-native knowledge representation — **Phase 1b** (auto-authoring at capture). The capture-time **extractor** now authors the `` block automatically, so blocks populate without user interaction: (a) `extract-prompt.txt` emits a structured `ai_block` per `wiki_update` (per-type schema fields; values are plain slugs, never `[[links]]`); (b) new `renderAiBlock` (`ai-block.ts`) deterministically renders a block object → the marked region (schema-ordered, closed-vocabulary, empty-skipping) + a thin `ai-block-render-cli` bundle so bash can call it with no TS/bash schema drift; (c) `merge-project-update.sh` renders the `ai_block` and injects it into a newly-created page (between frontmatter and the H1), fail-safe (no block / no node ⇒ inject nothing). MCP server → 2.5.1. Consumption (search/session-load/`knowledge_fetch`) is Phase 2; dream/maintainer **refresh** of blocks on update is Phase 3 (P1b authors on create only). New tests: `renderAiBlock` cases, `test-merge-ai-block.sh`, `test-extract-prompt-ai-block.sh`. Additive — no state migration. | No precondition — bumping the marker is sufficient. New pages written after upgrade carry an ai-block when the extractor can summarise one; existing pages backfill in Phase 3. | | **0.24.0** | AI-native knowledge representation — **Phase 1** (design `docs/specs/2026-06-02-ai-native-knowledge-representation-design`, plan `docs/plans/2026-06-02-ai-native-representation-phase1`). Reframe: the KB is AI-to-AI, so prose forces every reader to re-derive structure. Phase 1 makes a per-page `` **structured block** (flat YAML `key: value`, the "shared intermediate") a *recognized, parsed, schema-validated, strip-safe* construct — the deterministic foundation. (a) new pure `mcp/src/tools/ai-block.ts` (parse + per-type schemas {learnings,decisions,entities,issues,concepts,security} + `validateAiBlock` + `stripAiBlock`); (b) `parseDoc` exposes `doc.aiBlock`, and the body-`[[link]]`→`related:` fallback strips the block (block values are plain slugs, never `[[links]]`); (c) every length/first-sentence consumer excludes the block — `firstSentence` (reindex), the FORGET `wc -c` + stub-floor (`wiki-forget-score.sh`), the search stub-penalty — so a uniform block can't skew FORGET scores or BM25; (d) `knowledge_validate` warns (gentle, not error) on a block missing a required field for its type. MCP server → 2.5.0. **Additive + back-compat:** no block ⇒ behaviour unchanged. **Authoring** (extractor at capture) is Phase 1b; **consumption** (search weight/return + session-load injection + a `knowledge_fetch` block tier) is Phase 2; dream/maintainer refresh + lint staleness + backfill are Phase 3. New tests: `ai-block.test.ts`, `test-wiki-forget-ai-block.sh`, + parseDoc/validate/reindex cases. | No precondition — bumping the marker is sufficient. Pages gain blocks lazily as they are (re)written; nothing to migrate. | diff --git a/tests/test-dream-ai-block-parity.sh b/tests/test-dream-ai-block-parity.sh new file mode 100755 index 0000000..b7f4a7b --- /dev/null +++ b/tests/test-dream-ai-block-parity.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Guard: dream is ai-block AWARE but surface-only (never authors blocks in staging), and gates +# behind SB_DREAM_AI_BLOCKS. Both the skill and the runner agent must agree (inline/background parity). +set -u +ROOT="$(cd "$(dirname "$0")"/.. && pwd)" +S="$ROOT/skills/dream/SKILL.md"; R="$ROOT/agents/dream-runner.md" +fail(){ echo "FAIL: $1"; exit 1; }; pass(){ echo "PASS: $1"; } +for f in "$S" "$R"; do + b=$(basename "$f") + [ -f "$f" ] || fail "$b not found" + grep -qiE 'ai-block|ai:begin' "$f" || fail "$b: no ai-block awareness" + grep -qiE 'surface|count|recommend|report' "$f" || fail "$b: no surface-only language" + grep -q 'SB_DREAM_AI_BLOCKS' "$f" || fail "$b: missing SB_DREAM_AI_BLOCKS kill switch" + # require the literal do-NOT-author negation (not a loose 'maintainer' match that an + # author-in-staging instruction would also satisfy) + grep -qiE 'do not author|never author|not author' "$f" || fail "$b: missing the explicit do-NOT-author-in-staging negation" + grep -qiE '/second-brain:maintain|run .*maintain' "$f" || fail "$b: does not point at the maintainer for authoring" + pass "$b: ai-block surface-only, defers authoring to maintainer, kill-switch present" +done +echo; echo "ALL PASS" diff --git a/tests/test-kb-ai-block-candidates.sh b/tests/test-kb-ai-block-candidates.sh new file mode 100755 index 0000000..414c25e --- /dev/null +++ b/tests/test-kb-ai-block-candidates.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# kb-ai-block-candidates.sh: deterministic, read-only enumeration of blockless structured +# pages with substantive prose. One TSV row per candidate: \t\t. +set -u +ROOT="$(cd "$(dirname "$0")"/.. && pwd)"; SC="$ROOT/scripts/kb-ai-block-candidates.sh" +TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT +fail(){ echo "FAIL: $1"; exit 1; }; pass(){ echo "PASS: $1"; } +[ -f "$SC" ] || fail "script missing" +W="$TMP/knowledge/wiki"; mkdir -p "$W"/{learnings,state,projects} +# candidate: structured, no block, long prose +printf '%s\n' '---' 'title: A' 'type: learnings' '---' '# A' "$(printf 'real prose detail. %.0s' $(seq 1 20))" > "$W/learnings/cand.md" +# NOT: has a block +printf '%s\n' '---' 'title: B' 'type: learnings' '---' '' 'claim: c' '' '# B' "$(printf 'prose. %.0s' $(seq 1 20))" > "$W/learnings/hasblock.md" +# NOT: stub (short) +printf '%s\n' '---' 'title: C' 'type: learnings' '---' '# C' 'tiny.' > "$W/learnings/stub.md" +# NOT: non-structured type +printf '%s\n' '---' 'title: D' 'type: state' '---' '# D' "$(printf 'long state prose. %.0s' $(seq 1 20))" > "$W/state/st.md" +# NOT: generated MOC dir +printf '%s\n' '---' 'title: P' 'type: projects' '---' '# P' "$(printf 'long moc prose. %.0s' $(seq 1 20))" > "$W/projects/p.md" + +OUT=$(bash "$SC" --knowledge-dir "$TMP/knowledge") +printf '%s\n' "$OUT" | grep -q $'^learnings\tcand\t' || fail "candidate not listed" +printf '%s\n' "$OUT" | grep -q 'hasblock' && fail "page WITH a block listed" +printf '%s\n' "$OUT" | grep -q 'stub' && fail "stub listed" +printf '%s\n' "$OUT" | grep -q $'^state\t' && fail "non-structured type listed" +printf '%s\n' "$OUT" | grep -q 'projects' && fail "generated MOC listed" +[ "$(printf '%s\n' "$OUT" | grep -c .)" -eq 1 ] || fail "expected exactly 1 candidate, got $(printf '%s\n' "$OUT" | grep -c .)" +pass "enumerates only blockless, substantive, structured pages" + +# idempotent / read-only: running twice yields identical output, mutates nothing +H1=$(md5sum "$W/learnings/cand.md"); OUT2=$(bash "$SC" --knowledge-dir "$TMP/knowledge"); H2=$(md5sum "$W/learnings/cand.md") +[ "$H1" = "$H2" ] || fail "script mutated a page (must be read-only)" +[ "$OUT" = "$OUT2" ] || fail "non-deterministic output across runs" +pass "read-only + deterministic" + +# unterminated marked region: prose AFTER an unclosed must still be counted +# (drop-to-EOF would silently omit the page that most needs a block). +printf '%s\n' '---' 'title: U' 'type: learnings' '---' '# U' 'short.' '' \ + "$(printf 'substantive trailing prose. %.0s' $(seq 1 20))" > "$W/learnings/unterm.md" +OUT3=$(bash "$SC" --knowledge-dir "$TMP/knowledge") +printf '%s\n' "$OUT3" | grep -q $'^learnings\tunterm\t' || fail "page with prose after an unterminated region not counted (drop-to-EOF regression)" +pass "unterminated marked region does not silently drop trailing prose" + +# a page mis-filed in a structured dir but declaring a non-structured/typo'd type is NOT a candidate +# (canonical frontmatter type, in lockstep with knowledge_validate -- e.g. type: index/concept) +printf '%s\n' '---' 'title: M' 'type: index' '---' '# M' "$(printf 'long prose detail. %.0s' $(seq 1 20))" > "$W/learnings/mistype.md" +OUT5=$(bash "$SC" --knowledge-dir "$TMP/knowledge") +printf '%s\n' "$OUT5" | grep -q 'mistype' && fail "page declaring a non-structured type: was listed (should match validate's doc.type skip)" +pass "explicit non-structured frontmatter type: excludes the page (lockstep with validate)" + +# single-quoted structured type: must still be recognized (quote-style parity with validate) +printf "%s\n" '---' 'title: SQ' "type: 'learnings'" '---' '# SQ' "$(printf 'long prose detail. %.0s' $(seq 1 20))" > "$W/learnings/sqtype.md" +OUT6=$(bash "$SC" --knowledge-dir "$TMP/knowledge") +printf '%s\n' "$OUT6" | grep -q $'^learnings\tsqtype\t' || fail "single-quoted type: 'learnings' not recognized as structured (quote-strip parity gap)" +pass "single-quoted frontmatter type: is recognized (strips \\047 like validate)" + +# odd-spaced COMPLETE block (, no spaces) must be recognized as a block and skipped +printf '%s\n' '---' 'title: O' 'type: learnings' '---' '' 'claim: c' '' '# O' "$(printf 'long prose. %.0s' $(seq 1 20))" > "$W/learnings/oddspace.md" +OUT4=$(bash "$SC" --knowledge-dir "$TMP/knowledge") +printf '%s\n' "$OUT4" | grep -q 'oddspace' && fail "odd-spaced complete block listed as a candidate (grep skip too strict)" +pass "odd-spaced complete block is recognized and skipped" + +echo; echo "ALL PASS" diff --git a/tests/test-lint-skill.sh b/tests/test-lint-skill.sh index 9c1095a..e236877 100755 --- a/tests/test-lint-skill.sh +++ b/tests/test-lint-skill.sh @@ -185,5 +185,40 @@ echo "$EXTRACTED" | grep -qx 'retired-ghost' && fail "Test 4: 'retired-ghost' in [ "$GUARDED" -ge 1 ] || fail "Test 4: SKILL.md link-extractor is missing the in_graph guard (graph block not skipped in the real skill)" pass "dead-link extractor skips the generated graph block, keeps real body links" +# --- Test 5: Check 4 (missing ai-block) is present + functional ----------- +# The lint skill surfaces structured pages with no ai:begin block (Phase 3). +grep -q 'Missing ai-block' "$SCRIPT" || fail "Test 5: lint skill missing Check 4 (ai-block)" +grep -q 'MISSING-BLOCK:' "$SCRIPT" || fail "Test 5: Check 4 has no MISSING-BLOCK report marker" +# Anti-regression (static): Check 4's per-file skip must use `grep -l`, NOT `grep -q`. `grep -q` +# early-exits and SIGPIPEs the upstream `find` under a job-control (monitor) shell -- the way the +# skill is pasted at runtime -- silently zeroing the check. A script-context dynamic test can't +# catch this (grep -q works in a plain script), so pin it statically. +grep -qE "grep -lE '' 'claim: c' '' '# B' "$(printf 'prose. %.0s' $(seq 1 20))" > "$KD/wiki/learnings/hasblock.md" +printf '%s\n' '---' 'title: C' 'type: learnings' '---' '# C' 'tiny.' > "$KD/wiki/learnings/stub.md" +MB=$(for type in learnings decisions entities issues concepts security; do + d="$KD/wiki/$type"; [ -d "$d" ] || continue + find "$d" -name '*.md' -type f ! -name 'index.md' 2>/dev/null | while read -r f; do + grep -q '/) drop=0; next } + { print } + ' "$f" | tr -d '[:space:]' | wc -c) + [ "$prose" -ge 200 ] && echo "MISSING-BLOCK: $type/$(basename "$f" .md) ($f)" + done +done) +echo "$MB" | grep -q 'learnings/cand' || fail "Test 5: blockless substantive page not flagged" +echo "$MB" | grep -q 'hasblock' && fail "Test 5: page WITH a block was flagged" +echo "$MB" | grep -q 'stub' && fail "Test 5: stub was flagged" +pass "Check 4 flags only blockless, substantive, structured pages" + echo echo "ALL PASS" diff --git a/tests/test-maintainer-ai-block-backfill.sh b/tests/test-maintainer-ai-block-backfill.sh new file mode 100755 index 0000000..4e2b568 --- /dev/null +++ b/tests/test-maintainer-ai-block-backfill.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Guard: the knowledge-maintainer knows how to backfill ai-blocks (Phase 4b), uses the +# deterministic candidate script + render path, and inherits the closed-vocab / cap / +# explicit-invocation boundary. Prompt-only guard (greps the agent contract). +set -u +ROOT="$(cd "$(dirname "$0")"/.. && pwd)"; M="$ROOT/agents/knowledge-maintainer.md" +fail(){ echo "FAIL: $1"; exit 1; }; pass(){ echo "PASS: $1"; } +[ -f "$M" ] || fail "knowledge-maintainer.md not found" +grep -qiE 'Phase 4b|ai-block authoring|backfill' "$M" || fail "no Phase 4b / backfill section" +grep -q 'kb-ai-block-candidates.sh' "$M" || fail "does not reference the candidate work-list script" +grep -qiE 'renderAiBlock|ai-block-render-cli|render CLI' "$M" || fail "no render path referenced" +grep -qiE 'validateAiBlock|knowledge_validate' "$M" || fail "no self-validation referenced" +grep -qiE 'six (structured|known) types|closed[- ]vocab' "$M" || fail "closed-vocabulary boundary not stated" +grep -qiE 'never invent|extract.*from.*(prose|existing)|do not hallucinate' "$M" || fail "never-invent-values rule absent" +grep -qiE '50.*change|counted against|cap' "$M" || fail "cap inheritance not stated" +grep -qiE 'explicitly invoked|explicit-invocation|only on an explicit' "$M" || fail "explicit-invocation boundary not stated" +# The Autonomous Dispatch section must carve out Phase 4b (else it contradicts the boundary). +grep -qiE 'skip.*Phase 4b|Phase 4b.*skip' "$M" || fail "Autonomous Dispatch does not carve out Phase 4b (contradiction)" +pass "maintainer Phase 4b contract present (candidate script + render + validate + closed-vocab + cap + never-invent + explicit-invocation + dispatch carve-out)" +echo; echo "ALL PASS" diff --git a/tests/test-merge-ai-block-refresh.sh b/tests/test-merge-ai-block-refresh.sh new file mode 100755 index 0000000..97ba579 --- /dev/null +++ b/tests/test-merge-ai-block-refresh.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# Phase 3: merge-project-update.sh refreshes the authored ai-block on UPDATE (not just create). +# Replace-in-place when a complete region exists; inject when absent; never corrupt a malformed page. +set -u +ROOT="$(cd "$(dirname "$0")"/.. && pwd)"; SCRIPT="$ROOT/scripts/merge-project-update.sh" +TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT +fail(){ echo "FAIL: $1"; exit 1; }; pass(){ echo "PASS: $1"; } +command -v jq >/dev/null 2>&1 || fail "jq required" +command -v node >/dev/null 2>&1 || { echo "SKIP: node required"; exit 0; } +[ -f "$ROOT/mcp/dist/tools/ai-block-render-cli.bundle.js" ] || { echo "SKIP: render CLI not built"; exit 0; } + +KD="$TMP/knowledge"; mkdir -p "$KD/wiki/learnings" +PROJ="$TMP/PROJECT.md" +printf '%s\n' '# PROJECT: t' '## Goal' 'g.' '## State' 's.' '' > "$PROJ" +run(){ jq -nc "$1" | "$SCRIPT" --project-md "$PROJ" --knowledge-dir "$KD" >/dev/null 2>&1; } + +# 1) create page WITH a block, then UPDATE with a fresh block -> block REPLACED in place, one region. +run '{recent_decisions:[],open_blockers:[],cross_refs:[],files_touched:[], + wiki_updates:[{category:"learnings",slug:"refr",action:"create",title:"R",description:"d", + content:"original prose body line.",ai_block:{claim:"old claim",action:"old action"}}]}' || fail "create exited nonzero" +run '{recent_decisions:[],open_blockers:[],cross_refs:[],files_touched:[], + wiki_updates:[{category:"learnings",slug:"refr",action:"update",title:"R",description:"d", + content:"a brand new distinct second observation.",ai_block:{claim:"new claim",action:"new action"}}]}' || fail "update exited nonzero" +P="$KD/wiki/learnings/refr.md" +[ "$(grep -c '' 'claim: dangling' '' '# Bad' 'irreplaceable prose tail.' > "$KD/wiki/learnings/bad.md" +run '{recent_decisions:[],open_blockers:[],cross_refs:[],files_touched:[], + wiki_updates:[{category:"learnings",slug:"bad",action:"update",title:"Bad",description:"d", + content:"new line for the malformed page.",ai_block:{claim:"replacement",action:"do"}}]}' || true +grep -q 'irreplaceable prose tail' "$KD/wiki/learnings/bad.md" || fail "malformed page body was eaten by the refresh" +[ "$(grep -c 'ai:begin' "$KD/wiki/learnings/bad.md")" -eq 1 ] || fail "refresh added a second begin marker to a malformed page" +pass "malformed (begin-without-end) page is not corrupted by refresh" + +# 4) a real COMPLETE block at top + an inline in PROSE: refresh must replace ONLY +# the first (real) block and must NOT re-arm drop on the prose mention (else it eats to EOF -- +# the FORGET-bug class, realistic on this repo's own ai-block-documenting pages). +{ + printf '%s\n' '---' 'title: Inline' 'type: learnings' 'updated: 2026-05-01T00:00:00Z' '---' + printf '%s\n' '' 'claim: orig claim' 'action: orig' '' + printf '%s\n' '' '# Inline' '' 'Prose before the mention.' \ + 'Docs note: a page may contain in its prose.' \ + 'stray inline line' 'FINAL TAIL must survive.' +} > "$KD/wiki/learnings/inline.md" +run '{recent_decisions:[],open_blockers:[],cross_refs:[],files_touched:[], + wiki_updates:[{category:"learnings",slug:"inline",action:"update",title:"Inline",description:"d", + content:"a fresh distinct observation for the inline page.",ai_block:{claim:"fresh claim",action:"do"}}]}' || fail "inline-update nonzero" +N="$KD/wiki/learnings/inline.md" +grep -q 'FINAL TAIL must survive' "$N" || fail "prose tail after an inline ai:begin was eaten (FORGET-bug re-introduced)" +grep -q '^claim: fresh claim$' "$N" || fail "real block not refreshed" +grep -q 'orig claim' "$N" && fail "stale real-block field survived" +grep -q 'Docs note: a page may contain' "$N" || fail "inline prose mention was lost" +pass "a stray ai:begin in prose cannot re-arm the drop or eat the body" + +# 5) dedup-skip must STILL refresh the block (refresh runs before the prose-dedup early-continue). +run '{recent_decisions:[],open_blockers:[],cross_refs:[],files_touched:[], + wiki_updates:[{category:"learnings",slug:"dedup",action:"create",title:"Dedup",description:"d", + content:"IDENTICAL DEDUP SENTINEL CONTENT for the dedup page.",ai_block:{claim:"v1 claim",action:"a"}}]}' || fail "dedup-create nonzero" +run '{recent_decisions:[],open_blockers:[],cross_refs:[],files_touched:[], + wiki_updates:[{category:"learnings",slug:"dedup",action:"update",title:"Dedup",description:"d", + content:"IDENTICAL DEDUP SENTINEL CONTENT for the dedup page.",ai_block:{claim:"v2 claim",action:"a"}}]}' || fail "dedup-update nonzero" +D="$KD/wiki/learnings/dedup.md" +grep -q '^claim: v2 claim$' "$D" || fail "block NOT refreshed when prose was a duplicate (refresh must precede the dedup continue)" +grep -q 'v1 claim' "$D" && fail "stale block survived the dedup-skipped refresh" +pass "block refresh runs even when the prose content is a duplicate" + +# Prompt instructs refresh on update (so the extractor actually emits a block for update actions). +grep -qiE 'ai_block.*(update|refresh)|(update|refresh).*ai.?block|refresh.*in place' "$ROOT/scripts/extract-prompt.txt" \ + || fail "extract-prompt.txt does not instruct emitting/refreshing ai_block on update" +pass "extract-prompt instructs ai_block refresh on update" + +echo; echo "ALL PASS"