diff --git a/README.md b/README.md index b76a5736..f90d6229 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ The first line is the FlyBot core (package `ds-bot`); the two plugins are what t On DSH Desktop: **Plugins → Add plugin**, paste one `github:` address at a time, then **Install** and **Enable now**. -`0.2.0-rc.2` is the DeepSeek Harness release FlyBot is built and tested against — the same version `dev/harness` +`0.2.0-rc.2` is the DeepSeek Harness release FlyBot is built and tested against — the same version `core/dev/harness` pins and CI runs, so keep it in all four commands. **3. Run it** diff --git a/README.zh.md b/README.zh.md index 5b51db51..ac658eb8 100644 --- a/README.zh.md +++ b/README.zh.md @@ -102,7 +102,7 @@ npx -y @deepseek-ai/dsh@0.2.0-rc.2 plugin --profile web add "github:ThinkFlowLab 桌面版:**插件 → 添加插件**,一次粘一个 `github:` 地址,然后**安装**并**立即启用**。 -`0.2.0-rc.2` 是 FlyBot 构建与测试所用的 DeepSeek Harness 版本,也正是 `dev/harness` 钉住、CI 在跑的那一版, +`0.2.0-rc.2` 是 FlyBot 构建与测试所用的 DeepSeek Harness 版本,也正是 `core/dev/harness` 钉住、CI 在跑的那一版, 四条命令保持一致。 **3. 跑起来** diff --git a/core/test/check-doc-paths.test.js b/core/test/check-doc-paths.test.js new file mode 100644 index 00000000..c1e9e535 --- /dev/null +++ b/core/test/check-doc-paths.test.js @@ -0,0 +1,72 @@ +// A repo path written as inline code rots silently: `README.md:118` said `dev/harness` while the DSH +// pin is `core/dev/harness/package.json`, and nothing read it (#1403, #1397). The two root READMEs are +// the scanned surface on purpose — `docs/**`, `core/README.md` and the plugin READMEs write their +// inline paths relative to their own package or to the runtime home, so judging them against the repo +// root reports paths that are not broken. #1403 allows narrowing the file list; it forbids an allowlist. +import assert from 'node:assert/strict' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { test } from 'node:test' +import { fileURLToPath } from 'node:url' + +const ROOT = fileURLToPath(new URL('../../', import.meta.url)) +const DOCUMENTS = ['README.md', 'README.zh.md'] + +// A token claims to be a repo path when it holds a slash, no space, and its first segment names an +// entry at the repo root: that is what makes `dev/harness` a claim about this tree, while `src/host/` +// (package-relative), `origin/main` (a ref) and `ThinkFlowLab/FlyBot` (a remote) are not repo paths. +export function isRepoPath(token, root = ROOT) { + if (!token.includes('/') || /\s/.test(token)) return false + if (/^[a-z][a-z0-9+.-]*:\/\//i.test(token)) return false // URL + if (/[*<>{}]/.test(token)) return false // glob or placeholder + if (/^[\w.-]+\/[\w.-]+#\d+$/.test(token)) return false // owner/repo#123 + if (/^@/.test(token)) return false // npm scope + if (/^[/~$]/.test(token)) return false // absolute, home or env path + if (/[%:]/.test(token)) return false // env path or a namespaced id + if (/[=()|&;]/.test(token)) return false // a command line + const first = token.replace(/\/+$/, '').split('/')[0] + if (first === '.' || first === '..') return false + return existsSync(join(root, first)) +} + +// Every backticked token outside a fenced block, as `file:line: token` for the ones that miss on disk. +export function docPathMisses(root = ROOT, documents = DOCUMENTS) { + const misses = [] + for (const file of documents) { + let fenced = false + readFileSync(join(root, file), 'utf8').split('\n').forEach((line, index) => { + if (/^\s*(```|~~~)/.test(line)) { fenced = !fenced; return } + if (fenced) return + for (const [, token] of line.matchAll(/`([^`]+)`/g)) { + const path = token.trim().replace(/\/+$/, '') + if (!isRepoPath(path, root) || existsSync(join(root, path))) continue + misses.push(`${file}:${index + 1}: ${token.trim()}`) + } + }) + } + return misses +} + +test('every repo path named in inline code on the root READMEs exists on disk', () => { + assert.deepEqual(docPathMisses(), []) +}) + +test('the tokenizer skips refs, URLs, globs, placeholders, npm scopes and absolute paths, and names a miss', () => { + const root = mkdtempSync(join(tmpdir(), 'doc-paths-')) + mkdirSync(join(root, 'dev', 'harness'), { recursive: true }) + mkdirSync(join(root, 'docs'), { recursive: true }) + writeFileSync(join(root, 'docs', 'install.md'), '# install\n') + writeFileSync(join(root, 'README.md'), [ + 'A fenced block is prose, not a claim:', + '```', + 'dev/not-real', + '```', + 'Real: `dev/harness`, `dev/harness/`, `docs/install.md`.', + 'Not repo paths: `https://example.com/dev/x`, `docs/*.md`, `/dev`, `{id}/x`, `origin/main`,', + '`ThinkFlowLab/FlyBot#1403`, `@flybot/workflow`, `/etc/harness`, `../core/dev`, `container:x/y`,', + '`npm run verify`.', + 'One miss: `dev/nope`.', + ].join('\n')) + assert.deepEqual(docPathMisses(root, ['README.md']), ['README.md:9: dev/nope']) +}) diff --git a/patches/ds-bot/files/test/check-doc-paths.test.js b/patches/ds-bot/files/test/check-doc-paths.test.js new file mode 100644 index 00000000..c1e9e535 --- /dev/null +++ b/patches/ds-bot/files/test/check-doc-paths.test.js @@ -0,0 +1,72 @@ +// A repo path written as inline code rots silently: `README.md:118` said `dev/harness` while the DSH +// pin is `core/dev/harness/package.json`, and nothing read it (#1403, #1397). The two root READMEs are +// the scanned surface on purpose — `docs/**`, `core/README.md` and the plugin READMEs write their +// inline paths relative to their own package or to the runtime home, so judging them against the repo +// root reports paths that are not broken. #1403 allows narrowing the file list; it forbids an allowlist. +import assert from 'node:assert/strict' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { test } from 'node:test' +import { fileURLToPath } from 'node:url' + +const ROOT = fileURLToPath(new URL('../../', import.meta.url)) +const DOCUMENTS = ['README.md', 'README.zh.md'] + +// A token claims to be a repo path when it holds a slash, no space, and its first segment names an +// entry at the repo root: that is what makes `dev/harness` a claim about this tree, while `src/host/` +// (package-relative), `origin/main` (a ref) and `ThinkFlowLab/FlyBot` (a remote) are not repo paths. +export function isRepoPath(token, root = ROOT) { + if (!token.includes('/') || /\s/.test(token)) return false + if (/^[a-z][a-z0-9+.-]*:\/\//i.test(token)) return false // URL + if (/[*<>{}]/.test(token)) return false // glob or placeholder + if (/^[\w.-]+\/[\w.-]+#\d+$/.test(token)) return false // owner/repo#123 + if (/^@/.test(token)) return false // npm scope + if (/^[/~$]/.test(token)) return false // absolute, home or env path + if (/[%:]/.test(token)) return false // env path or a namespaced id + if (/[=()|&;]/.test(token)) return false // a command line + const first = token.replace(/\/+$/, '').split('/')[0] + if (first === '.' || first === '..') return false + return existsSync(join(root, first)) +} + +// Every backticked token outside a fenced block, as `file:line: token` for the ones that miss on disk. +export function docPathMisses(root = ROOT, documents = DOCUMENTS) { + const misses = [] + for (const file of documents) { + let fenced = false + readFileSync(join(root, file), 'utf8').split('\n').forEach((line, index) => { + if (/^\s*(```|~~~)/.test(line)) { fenced = !fenced; return } + if (fenced) return + for (const [, token] of line.matchAll(/`([^`]+)`/g)) { + const path = token.trim().replace(/\/+$/, '') + if (!isRepoPath(path, root) || existsSync(join(root, path))) continue + misses.push(`${file}:${index + 1}: ${token.trim()}`) + } + }) + } + return misses +} + +test('every repo path named in inline code on the root READMEs exists on disk', () => { + assert.deepEqual(docPathMisses(), []) +}) + +test('the tokenizer skips refs, URLs, globs, placeholders, npm scopes and absolute paths, and names a miss', () => { + const root = mkdtempSync(join(tmpdir(), 'doc-paths-')) + mkdirSync(join(root, 'dev', 'harness'), { recursive: true }) + mkdirSync(join(root, 'docs'), { recursive: true }) + writeFileSync(join(root, 'docs', 'install.md'), '# install\n') + writeFileSync(join(root, 'README.md'), [ + 'A fenced block is prose, not a claim:', + '```', + 'dev/not-real', + '```', + 'Real: `dev/harness`, `dev/harness/`, `docs/install.md`.', + 'Not repo paths: `https://example.com/dev/x`, `docs/*.md`, `/dev`, `{id}/x`, `origin/main`,', + '`ThinkFlowLab/FlyBot#1403`, `@flybot/workflow`, `/etc/harness`, `../core/dev`, `container:x/y`,', + '`npm run verify`.', + 'One miss: `dev/nope`.', + ].join('\n')) + assert.deepEqual(docPathMisses(root, ['README.md']), ['README.md:9: dev/nope']) +})