From ab3215780766753ff3429847411132dd84f73e59 Mon Sep 17 00:00:00 2001 From: Focus Date: Mon, 14 Sep 2026 12:21:00 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=A7=20update=20(tools):=20validate?= =?UTF-8?q?=20internal=20links=20in=20the=20book=20index=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- code/tools/gen-book-index.mjs | 52 +++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/code/tools/gen-book-index.mjs b/code/tools/gen-book-index.mjs index 2d1dd4b..7698e10 100755 --- a/code/tools/gen-book-index.mjs +++ b/code/tools/gen-book-index.mjs @@ -6,13 +6,15 @@ * Usage: cd code && node tools/gen-book-index.mjs * (or: npm run book:index) * - * Convention: every document starts with frontmatter whose `description` - * is a one-line, plain-language summary of the content. Files missing it - * are listed on stderr and the script exits 1 — zero warnings is the - * only passing state. Zero dependencies; runs on plain Node (and bun). + * Conventions: + * 1. Every document starts with frontmatter whose `description` is a + * one-line, plain-language summary. Missing → warning, exit 1. + * 2. Internal links must resolve. Broken relative links → warning, exit 1 — + * the book doubles as an Obsidian vault, so a broken link is a build bug. + * Zero dependencies; runs on plain Node (and bun). */ -import { readdirSync, readFileSync, writeFileSync } from "node:fs"; -import { join, dirname, relative } from "node:path"; +import { readdirSync, readFileSync, writeFileSync, existsSync } from "node:fs"; +import { join, dirname, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const TOOLS_DIR = dirname(fileURLToPath(import.meta.url)); @@ -66,8 +68,7 @@ function walkMd(dir) { return out; } -function parseDescription(abs) { - const text = readFileSync(abs, "utf8"); +function parseDescription(text) { if (!text.startsWith("---")) return null; const lines = text.split("\n"); for (let i = 1; i < lines.length; i++) { @@ -78,6 +79,17 @@ function parseDescription(abs) { return null; } +function collectInternalLinks(text) { + const out = []; + const re = /(?:\]\(<([^>]+)>\)|\]\(([^)\s]+)\))/g; + let m; + while ((m = re.exec(text))) { + const target = (m[1] || m[2]).split("#")[0]; + if (target && !/^[a-z]+:/.test(target)) out.push(target); // skip http:, mailto:, obsidian:… + } + return out; +} + function link(rel) { return /[\s()]/.test(rel) ? `<${rel}>` : rel; } @@ -87,13 +99,19 @@ const files = walkMd(BOOK_ROOT) .sort(); const warnings = []; +const brokenLinks = []; const byDir = new Map(); for (const abs of files) { const rel = relative(BOOK_ROOT, abs); const dir = dirname(rel) === "." ? "" : dirname(rel); - const desc = parseDescription(abs); + const text = readFileSync(abs, "utf8"); + const desc = parseDescription(text); if (!desc) warnings.push(rel); + for (const target of collectInternalLinks(text)) { + const resolved = resolve(dirname(abs), target); + if (!existsSync(resolved)) brokenLinks.push(`${rel} → ${target}`); + } if (!byDir.has(dir)) byDir.set(dir, []); byDir.get(dir).push({ name: rel.split("/").pop(), rel, desc: desc ?? "(missing description)" }); } @@ -132,10 +150,16 @@ out += `\n`; writeFileSync(OUTPUT, out); -if (warnings.length > 0) { - console.error(`Files missing frontmatter description (${warnings.length}):`); - for (const w of warnings) console.error(` - ${w}`); - console.error(`Index written to ${OUTPUT} (${files.length} documents), but the files above lack a summary. Fix them and rerun.`); +if (warnings.length > 0 || brokenLinks.length > 0) { + if (warnings.length > 0) { + console.error(`Files missing frontmatter description (${warnings.length}):`); + for (const w of warnings) console.error(` - ${w}`); + } + if (brokenLinks.length > 0) { + console.error(`Broken internal links (${brokenLinks.length}):`); + for (const l of brokenLinks) console.error(` - ${l}`); + } + console.error(`Index written to ${OUTPUT} (${files.length} documents), but the issues above fail the build. Fix them and rerun.`); process.exit(1); } -console.log(`Index written to ${OUTPUT} — ${files.length} documents, zero warnings.`); +console.log(`Index written to ${OUTPUT} — ${files.length} documents, zero warnings, all links resolve.`); From 78474c9ec6203b085ad0174f32cfa4e527461b0c Mon Sep 17 00:00:00 2001 From: Focus Date: Mon, 14 Sep 2026 12:21:00 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=96=20docs:=20promise=20obsidian-g?= =?UTF-8?q?rade=20readability=20with=20link=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ADOPT.md | 2 +- README.ja.md | 6 +++++- README.md | 6 +++++- README.zh-CN.md | 6 +++++- book/guidelines/workflow.md | 2 +- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/ADOPT.md b/ADOPT.md index cb173fa..d250f7f 100644 --- a/ADOPT.md +++ b/ADOPT.md @@ -49,7 +49,7 @@ Every migrated file gets a frontmatter `description` — one plain-language line 1. Copy `code/tools/gen-book-index.mjs` and wire the script (see the `scripts` block in BoCode's `code/package.json`). If the project has no JavaScript tooling at all, a shell wrapper calling `node` is fine — the script is zero-dependency. 2. Optional, recommended: copy `scripts/check-commit-message.mjs` and `.githooks/`, then run `git config core.hooksPath .githooks`. 3. Optional: copy `.github/workflows/git-policy.yml`. **Check the branch names** — if the project's integration branch isn't `dev`, either ask the user to adopt the branch model or adapt the workflow's branch filters. Don't silently rewrite their branch model. -4. Run the index generator: every book file must carry a `description`; zero warnings is the only passing state. +4. Run the index generator: every book file must carry a `description` and every internal link must resolve; zero warnings is the only passing state. ## Step 5 — Install the skills diff --git a/README.ja.md b/README.ja.md index a656720..78ec622 100644 --- a/README.ja.md +++ b/README.ja.md @@ -135,8 +135,9 @@ diff を読まないと分からない summary は summary ではありません - すべての book ドキュメントは frontmatter の `description` で始まる——体裁ではなく内容を語る、人の言葉で一行 - `code/tools/gen-book-index.mjs`(依存ゼロ、プレーン Node)が `book/README.md`——全ドキュメントのマスターインデックス——を再生成する - description の欠けたドキュメントはビルドを**失敗させる**(exit 1)。ゼロ警告だけが合格です +- 内部リンクは解決必須——切れたリンクもビルドを失敗させます。book は vault を兼ねるため、切れたリンクはビルドバグです -`book/README.md` は入口も兼ねます:エージェントはまずここで地図を手に入れる。フォルダはそのまま [Obsidian](https://obsidian.md) の vault として開けます。 +`book/README.md` は入口も兼ねます:エージェントはまずここで地図を手に入れる。フォルダはそのまま [Obsidian](https://obsidian.md) の vault として開けます——インデックス、タグ、バックリンク、グラフがすべて使えます(FAQ 参照)。 ### bowrite(薄写)——人の言葉で書く @@ -198,6 +199,9 @@ cp -r skills/bocode skills/boscope skills/book-writeback skills/bowrite