From d7c4e556c76784db6a08ee07d9e561187ffb2165 Mon Sep 17 00:00:00 2001 From: Doraemon <159792165+Doraemonblogs@users.noreply.github.com> Date: Thu, 13 Aug 2026 10:44:07 +0000 Subject: [PATCH 01/12] fix: escape raw angle brackets in inside-the-scaffold translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 裸写的 / 标签会被 Vue 编译器 当作组件解析(VitePress 构建直接报错),在 GitHub 的 Markdown 渲染中 也会被 HTML 清洗吞掉。以反引号内联代码转义后两边渲染都恢复正确。 Taken from PR #21 with authorship preserved. --- works/inside-the-scaffold-paper-translation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/works/inside-the-scaffold-paper-translation.md b/works/inside-the-scaffold-paper-translation.md index 0e89888..c1773ee 100644 --- a/works/inside-the-scaffold-paper-translation.md +++ b/works/inside-the-scaffold-paper-translation.md @@ -181,7 +181,7 @@ Table 2:控制循环策略。智能体按探索策略的灵活性从低到高 在最简单的一端,Agentless 使用独立采样:在定位到相关代码后,它提示 LLM 独立生成候选补丁(默认配置生成 20 个;原始论文使用 4 个定位样本 × 每个 10 个补丁,共计 ≈$ 40 个)。每次生成看到相同的上下文,但可能产生不同的修复方案。然后通过对这些候选方案进行多数投票来选择最终补丁。不存在树结构,候选方案之间也没有交互;每个补丁都是独立生成的。 -DARS-Agent 在其主执行循环中引入了树结构搜索。与 Agentless 将定位和修复分为不同阶段不同,DARS-Agent 没有阶段分离:搜索命令与编辑和执行命令在整个过程中并行可用。该智能体构建一棵搜索树,其中每个节点代表一个动作(如编辑文件、创建文件或提交补丁)。在每个分支点,智能体生成多个备选动作,然后使用一个 LLM 评论者在其中进行选择:评论者接收这些备选方案作为提示,并以 标签响应以指示其选择。然而,与经典树搜索不同,DARS-Agent 没有数值奖励信号,也没有将结果反向传播到较早节点;评论者做出贪心的局部决策,不考虑较早的选择如何影响了后续结果。 +DARS-Agent 在其主执行循环中引入了树结构搜索。与 Agentless 将定位和修复分为不同阶段不同,DARS-Agent 没有阶段分离:搜索命令与编辑和执行命令在整个过程中并行可用。该智能体构建一棵搜索树,其中每个节点代表一个动作(如编辑文件、创建文件或提交补丁)。在每个分支点,智能体生成多个备选动作,然后使用一个 LLM 评论者在其中进行选择:评论者接收这些备选方案作为提示,并以 `` 标签响应以指示其选择。然而,与经典树搜索不同,DARS-Agent 没有数值奖励信号,也没有将结果反向传播到较早节点;评论者做出贪心的局部决策,不考虑较早的选择如何影响了后续结果。 Moatless Tools 实现了完整的蒙特卡洛树搜索 (MCTS) [^11],这与 AlphaGo [^49] 等博弈系统使用的算法相同。搜索树中的每个节点接收数值奖励值(范围从 -100 到 +100),算法维护访问计数以平衡对未尝试路径的探索与对有前景路径的利用。在扩展节点后,奖励沿树反向传播以更新祖先节点,使搜索能够从后续结果中学习,并将精力重新导向更有前景的分支(search_tree.py:326--345)。 @@ -279,7 +279,7 @@ Aider 以 13 种注册编辑格式成为异类,每种格式作为独立的 cod Agentless 在其 Anthropic 路径中的"模拟工具使用"在架构上是独特的。当使用 Anthropic 的 API 时,LLM 调用 str_replace_editor 工具,但每次调用都收到相同的硬编码响应:"File is successfully edited",无论输入是什么。LLM 在编辑后从不看到实际的文件状态;工具调用被提取并在事后应用。这将工具调用 API 用作结构化输出提取技术,而非用于实际执行。 -其余智能体使用同一方法的变体。AutoCodeRover 使用自定义的类 XML 标签 /,而 Gemini CLI 和 Cline 将全文件写入工具与功能上类似于 str_replace_editor 的搜索替换工具相结合。mini-swe-agent 是唯一通过 shell 命令直接编辑文件而非生成补丁供脚手架应用的智能体。 +其余智能体使用同一方法的变体。AutoCodeRover 使用自定义的类 XML 标签 ``/``,而 Gemini CLI 和 Cline 将全文件写入工具与功能上类似于 str_replace_editor 的搜索替换工具相结合。mini-swe-agent 是唯一通过 shell 命令直接编辑文件而非生成补丁供脚手架应用的智能体。 #### 4.2.3 工具发现策略 From efce5c36c1b3e2abf42a3a87f3526f7eda013dec Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 13 Aug 2026 10:44:07 +0000 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20VitePress=20docs=20site=20?= =?UTF-8?q?=E2=80=94=20filesystem-generated=20nav,=20deployed=20to=20harne?= =?UTF-8?q?ss.dyu.sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 站点自建而非合并 PR #21,但吸收其验证过的方案与多个设计想法(VitePress 选型、works 按来源系列分组的信息架构、中文排版细节、部署工作流骨架、 首页六概念卡结构)。 与 #21 的关键差异: - 侧边栏与首页统计不手写:.vitepress/sidebar.mjs 在构建时从文件系统生成, 未匹配到分组前缀的新作品落入「社区博客」兜底组——宁可分组不准, 不可静默丢失 - 自有域名 harness.dyu.sh:base=/、public/CNAME、sitemap/OG/canonical 全量对齐 - 智能体可读:每页伴生同路径 .md 副本 + /llms.txt + /llms-full.txt(Node 标准库实现,零运行时依赖) - /feed.xml RSS 2.0,条目时间取自 git 提交历史 - 自有视觉:缰绳青主题 + 缰绳曲线 favicon(与仓库自产海报同一视觉 DNA) - 部署工作流用 npm ci + Node 22,构建前先跑 sidebar --verify Design informed by #21. Suggested-by: Doraemon <159792165+Doraemonblogs@users.noreply.github.com> --- .github/workflows/deploy-docs.yml | 66 + .gitignore | 3 + .vitepress/config.ts | 237 +++ .vitepress/sidebar.mjs | 218 +++ .vitepress/theme/HomeStats.vue | 25 + .vitepress/theme/custom.css | 126 ++ .vitepress/theme/home-stats.data.mjs | 16 + .vitepress/theme/index.ts | 11 + index.md | 61 + package-lock.json | 2537 ++++++++++++++++++++++++++ package.json | 15 + public/CNAME | 1 + public/favicon.svg | Bin 0 -> 448 bytes public/robots.txt | 4 + 14 files changed, 3320 insertions(+) create mode 100644 .github/workflows/deploy-docs.yml create mode 100644 .vitepress/config.ts create mode 100644 .vitepress/sidebar.mjs create mode 100644 .vitepress/theme/HomeStats.vue create mode 100644 .vitepress/theme/custom.css create mode 100644 .vitepress/theme/home-stats.data.mjs create mode 100644 .vitepress/theme/index.ts create mode 100644 index.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/CNAME create mode 100644 public/favicon.svg create mode 100644 public/robots.txt diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..d45fdf4 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,66 @@ +name: deploy-docs + +# 构建 VitePress 站点并部署到 GitHub Pages(自定义域名 harness.dyu.sh 在 +# 仓库 Settings → Pages 配置,DNS 侧为 harness CNAME → deusyu.github.io)。 +# 工作流骨架吸收自 PR #21(@Doraemonblogs),本版差异:npm ci(锁定依赖)、 +# Node 22(与本地验证环境一致)。 + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # lastUpdated 与 RSS 条目时间都需要完整 git 历史 + + - name: Setup Pages + uses: actions/configure-pages@v5 + with: + enablement: true # 首次运行自动开启 GitHub Pages(Actions 部署模式) + + - name: Setup Node + uses: actions/setup-node@v5 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Verify sidebar completeness (C14) + run: node .vitepress/sidebar.mjs --verify + + - name: Build site + run: npm run docs:build + + - name: Disable Jekyll + run: touch .vitepress/dist/.nojekyll + + - name: Upload artifact + uses: actions/upload-pages-artifact@v4 + with: + path: .vitepress/dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index ca4182d..e2f5194 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,8 @@ CLAUDE.md .baoyu-skills/ translate/ +# VitePress 本地缓存(构建产物已被上面的 dist/ 规则覆盖) +.vitepress/cache/ + # 私密商务资料(内训洽谈、聊天截图等,永不入库) private/ diff --git a/.vitepress/config.ts b/.vitepress/config.ts new file mode 100644 index 0000000..6e02154 --- /dev/null +++ b/.vitepress/config.ts @@ -0,0 +1,237 @@ +import { defineConfig } from 'vitepress' +import { execSync } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +// @ts-ignore — 纯 ESM 生成器(Node 标准库,无类型声明) +import { ROOT, buildSidebar, collectPages, computeStats } from './sidebar.mjs' + +const HOST = 'https://harness.dyu.sh' +const SITE_TITLE = 'Harness Engineering' +const SITE_DESC = '驭缰工程中文学习档案——概念笔记、独立思考、系统性翻译与实践记录' + +export default defineConfig({ + lang: 'zh-CN', + base: '/', + title: SITE_TITLE, + description: SITE_DESC, + + cleanUrls: true, + lastUpdated: true, + + // 站点只发布内容页:智能体导航文件(AGENTS.md)、仓库门面(根 README)、 + // 本地过程稿(translate/)与私密资料(private/)都不属于站点。 + srcExclude: [ + '**/AGENTS.md', + 'README.md', + 'README.en.md', + 'CLAUDE.md', + 'translate/**', + 'private/**', + '.claude/**', + ], + + // 仓库内多数交叉链接为 GitHub 浏览而写(目录链接、指向 .py/AGENTS.md 的链接), + // VitePress 天然无法解析,故关闭死链阻断。站内导航完整性由 C14 的 + // sidebar --verify 从文件系统侧守护。 + ignoreDeadLinks: true, + + sitemap: { hostname: HOST }, + + markdown: { + image: { lazy: true }, + }, + + head: [ + ['link', { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }], + ['meta', { name: 'theme-color', content: '#0f766e' }], + ['meta', { property: 'og:site_name', content: SITE_TITLE }], + ['meta', { property: 'og:type', content: 'website' }], + ['link', { rel: 'alternate', type: 'application/rss+xml', title: `${SITE_TITLE} RSS`, href: `${HOST}/feed.xml` }], + ], + + transformPageData(pageData) { + const cleanPath = pageData.relativePath.replace(/(^|\/)index\.md$/, '$1').replace(/\.md$/, '') + pageData.frontmatter.head ??= [] + pageData.frontmatter.head.push( + ['meta', { property: 'og:title', content: pageData.title ? `${pageData.title} | ${SITE_TITLE}` : `${SITE_TITLE} 学习档案` }], + ['meta', { property: 'og:description', content: pageData.description || SITE_DESC }], + ['meta', { property: 'og:url', content: `${HOST}/${cleanPath}` }], + ['meta', { name: 'twitter:card', content: 'summary' }], + ) + }, + + themeConfig: { + logo: '/favicon.svg', + + nav: [ + { text: '首页', link: '/' }, + { text: '概念笔记', link: '/concepts/00-overview' }, + { text: '独立思考', link: '/thinking/why-this-project-exists' }, + { text: '翻译与作品', link: '/works/harness-engineering-chinese-interpretation' }, + { text: '文章索引', link: '/references/articles' }, + ], + + // 侧边栏由 .vitepress/sidebar.mjs 从文件系统生成(C14 守卫),不手写。 + sidebar: buildSidebar(), + + outline: { label: '本页目录', level: [2, 3] }, + + search: { + provider: 'local', + options: { + translations: { + button: { buttonText: '搜索', buttonAriaLabel: '搜索文档' }, + modal: { + noResultsText: '未找到相关结果', + resetButtonTitle: '清除查询条件', + footer: { selectText: '选择', navigateText: '切换', closeText: '关闭' }, + }, + }, + }, + }, + + socialLinks: [{ icon: 'github', link: 'https://github.com/deusyu/harness-engineering' }], + + footer: { + message: '人类掌舵,智能体执行 · Released under the MIT License', + copyright: 'Copyright © 2026 deusyu', + }, + + lastUpdated: { text: '最后更新于' }, + docFooter: { prev: '上一篇', next: '下一篇' }, + + editLink: { + pattern: 'https://github.com/deusyu/harness-engineering/edit/main/:path', + text: '在 GitHub 上编辑此页', + }, + + externalLinkIcon: true, + }, + + /** + * 构建尾钩子(Node 标准库实现,零运行时依赖): + * 1. 每个内容页生成同路径 .md 纯文本副本(URL + `.md` 即得); + * 2. /llms.txt 与 /llms-full.txt —— 面向智能体的站点索引与全文(llms.txt 约定); + * 3. /feed.xml —— RSS 2.0,条目时间取自 git 提交历史。 + */ + async buildEnd(siteConfig) { + const out = siteConfig.outDir + const pages = collectPages() + const stats = computeStats() + + // 1) 每页伴生 Markdown 副本 + for (const p of pages) { + const dest = path.join(out, `${p.link.slice(1)}.md`) + fs.mkdirSync(path.dirname(dest), { recursive: true }) + fs.copyFileSync(path.join(ROOT, p.file), dest) + } + + // 2) llms.txt / llms-full.txt + const model = groupedForLlms(pages) + const llms = [ + `# ${SITE_TITLE} 学习档案`, + '', + `> 中文 Harness Engineering(驭缰工程)知识库:${stats.concepts} 篇概念笔记、${stats.thinking} 篇独立思考、${stats.translations} 篇一手翻译,以及收录 ${stats.articles} 篇文章的深度摘要索引。人类掌舵,智能体执行。`, + '', + '本站每个页面都有同路径的 Markdown 版本:在页面 URL 后追加 `.md` 即可获取纯文本。', + '', + ...model, + '## 完整内容', + '', + `- [llms-full.txt](${HOST}/llms-full.txt):全站正文合并版`, + '', + ].join('\n') + fs.writeFileSync(path.join(out, 'llms.txt'), llms) + + const full = pages + .map((p) => { + const raw = fs.readFileSync(path.join(ROOT, p.file), 'utf8') + return `\n\n---\ntitle: ${p.text}\nurl: ${HOST}${p.link}\n---\n\n${raw}` + }) + .join('') + fs.writeFileSync(path.join(out, 'llms-full.txt'), `# ${SITE_TITLE} 学习档案 — 全站正文\n${full}`) + + // 3) RSS(feed.xml) + const dated = pages + .map((p) => ({ ...p, date: gitDate(p.file) })) + .sort((a, b) => b.date.getTime() - a.date.getTime()) + .slice(0, 30) + const items = dated + .map((p) => + [ + ' ', + ` ${xmlEscape(p.text)}`, + ` ${HOST}${p.link}`, + ` ${HOST}${p.link}`, + ` ${p.date.toUTCString()}`, + ' ', + ].join('\n') + ) + .join('\n') + const rss = [ + '', + '', + ' ', + ` ${SITE_TITLE} 学习档案`, + ` ${HOST}`, + ` ${xmlEscape(SITE_DESC)}`, + ' zh-cn', + ` ${new Date().toUTCString()}`, + items, + ' ', + '', + '', + ].join('\n') + fs.writeFileSync(path.join(out, 'feed.xml'), rss) + }, +}) + +function groupedForLlms(pages: Array<{ text: string; link: string; file: string }>): string[] { + const sections = new Map() + const sectionOf = (file: string) => { + const top = file.split('/')[0] + const names: Record = { + concepts: '概念笔记', + thinking: '独立思考', + practice: '动手实践', + feedback: '反馈记录', + works: '翻译与作品', + tools: '工具库', + prompts: '提示词', + references: '资源索引', + } + return names[top] ?? top + } + for (const p of pages) { + const key = sectionOf(p.file) + if (!sections.has(key)) sections.set(key, []) + sections.get(key)!.push(`- [${p.text}](${HOST}${p.link}.md)`) + } + const lines: string[] = [] + for (const [name, links] of sections) { + lines.push(`## ${name}`, '', ...links, '') + } + return lines +} + +function gitDate(file: string): Date { + try { + const iso = execSync(`git log -1 --format=%cI -- "${file}"`, { + cwd: ROOT, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim() + if (iso) return new Date(iso) + } catch { + /* 无 git 历史(浅克隆/未跟踪文件)时回退到构建时间 */ + } + return new Date() +} + +function xmlEscape(s: string): string { + return s + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') +} diff --git a/.vitepress/sidebar.mjs b/.vitepress/sidebar.mjs new file mode 100644 index 0000000..372857c --- /dev/null +++ b/.vitepress/sidebar.mjs @@ -0,0 +1,218 @@ +/** + * 站点导航与统计的唯一生成器 —— 一切从文件系统派生,杜绝手写漂移。 + * + * 注意:本文件不能带 shebang——VitePress 用 esbuild 打包 config.ts 时会把 + * 本文件拼接进 bundle,`#!` 不在文件首字节即语法错误。 + * + * 本仓库的方法论是机械化一致性检查(C1–C14),手写侧边栏会成为检查覆盖 + * 不到的漂移面。因此: + * - 侧边栏永远不手写。新增内容文件自动进入侧边栏;works/ 下未匹配到 + * 任何分组前缀的新文件落入「社区博客」兜底组 —— 宁可分组不准, + * 不可静默丢失。 + * - 站点源码不写裸计数。所有展示数字由 computeStats() 构建时统计。 + * - `node .vitepress/sidebar.mjs --verify` 断言每个一等内容页在侧边栏 + * 恰好出现一次,是 scripts/check-consistency.sh C14 的机械化入口。 + * + * 仅依赖 Node 标准库,可独立执行: + * node .vitepress/sidebar.mjs # 打印生成的侧边栏 JSON + * node .vitepress/sidebar.mjs --verify # 完整性校验(C14 调用) + * node .vitepress/sidebar.mjs --stats # 打印构建时统计 + */ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') + +function readText(rel) { + return fs.readFileSync(path.join(ROOT, rel), 'utf8') +} + +/** 列出目录下的内容 md 文件(排除 AGENTS.md),返回仓库相对路径,按文件名排序。 */ +function listMd(dir, { recursive = false } = {}) { + const abs = path.join(ROOT, dir) + if (!fs.existsSync(abs)) return [] + const out = [] + for (const ent of fs.readdirSync(abs, { withFileTypes: true })) { + if (ent.isDirectory()) { + if (recursive) out.push(...listMd(path.join(dir, ent.name), { recursive })) + continue + } + if (!ent.name.endsWith('.md') || ent.name === 'AGENTS.md') continue + out.push(path.join(dir, ent.name)) + } + return out.sort() +} + +/** 子目录形态的作品/实验:以其 README.md 作为入口页。 */ +function subdirReadmes(dir) { + const abs = path.join(ROOT, dir) + if (!fs.existsSync(abs)) return [] + const out = [] + for (const ent of fs.readdirSync(abs, { withFileTypes: true })) { + if (!ent.isDirectory()) continue + const rel = path.join(dir, ent.name, 'README.md') + if (fs.existsSync(path.join(ROOT, rel))) out.push(rel) + } + return out.sort() +} + +/** 标题优先级:frontmatter title > 首个 H1 > 文件名;去掉结尾全角括注以适配侧边栏宽度。 */ +export function extractTitle(rel) { + const text = readText(rel) + let title = null + const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---/) + if (fm) { + const m = fm[1].match(/^title:\s*(.+)\s*$/m) + if (m) title = m[1].trim().replace(/^["'](.*)["']$/, '$1') + } + if (!title) { + const h1 = text.match(/^# (.+)$/m) + if (h1) title = h1[1].trim() + } + if (!title) title = path.basename(rel, '.md') + return title.replace(/([^()]*)\s*$/, '').trim() +} + +function page(rel) { + return { text: extractTitle(rel), link: '/' + rel.replace(/\.md$/, ''), file: rel } +} + +/** + * works/ 分组规则:按文件名前缀归入来源系列(分组式信息架构吸收自 PR #21, + * by @Doraemonblogs)。match 按数组顺序生效,最后一组恒真兜底。 + */ +const WORKS_GROUPS = [ + { text: '原创分析', match: (n) => n === 'harness-engineering-chinese-interpretation.md' }, + { text: 'Martin Fowler 系列', match: (n) => n.startsWith('fowler-') }, + { text: 'Anthropic 系列', match: (n) => n.startsWith('anthropic-') }, + { text: 'LangChain 系列', match: (n) => /^(langchain|langsmith|deep-agents)-/.test(n) }, + { text: '学术论文', match: (n) => /^(arxiv-|meta-harness-paper|inside-the-scaffold-paper)/.test(n) }, + { text: '工程实践', match: (n) => /^(openai|github|cursor|metr|bun)-/.test(n) }, + { text: '中文收录', match: (n) => /-(zh-cn-repost|original)\.md$/.test(n) }, + { text: '社区博客', match: () => true }, +] + +function worksSection() { + const groups = WORKS_GROUPS.map((g) => ({ text: g.text, match: g.match, items: [] })) + for (const rel of listMd('works')) { + const name = path.basename(rel) + groups.find((g) => g.match(name)).items.push(page(rel)) + } + for (const rel of subdirReadmes('works')) groups[0].items.push(page(rel)) + return { + text: '翻译与作品', + collapsed: true, + items: groups + .filter((g) => g.items.length > 0) + .map((g) => ({ text: g.text, collapsed: true, items: g.items })), + } +} + +export function articlesCount() { + return (readText('references/articles.md').match(/^### \d+\./gm) ?? []).length +} + +/** 内部模型:与最终侧边栏同构,但每个页面节点额外带 file 字段供校验/构建用。 */ +export function buildModel() { + return [ + { text: '概念笔记', collapsed: false, items: listMd('concepts').map(page) }, + { text: '独立思考', collapsed: false, items: listMd('thinking').map(page) }, + { text: '动手实践', collapsed: false, items: subdirReadmes('practice').map(page) }, + { text: '反馈记录', collapsed: false, items: listMd('feedback').map(page) }, + worksSection(), + { text: '工具库', collapsed: false, items: listMd('tools', { recursive: true }).map(page) }, + { text: '提示词', collapsed: false, items: listMd('prompts').map(page) }, + { + text: '资源索引', + collapsed: false, + items: [{ ...page('references/articles.md'), text: `文章索引(${articlesCount()} 篇深度摘要)` }], + }, + ] +} + +function stripFile(nodes) { + return nodes.map(({ file, items, ...rest }) => ({ + ...rest, + ...(items ? { items: stripFile(items) } : {}), + })) +} + +/** VitePress themeConfig.sidebar 直接消费的形态。 */ +export function buildSidebar() { + return stripFile(buildModel()) +} + +/** 展平出全部页面节点(含 file),供 --verify 与 buildEnd(md 副本 / llms / RSS)使用。 */ +export function collectPages() { + const out = [] + const walk = (nodes) => { + for (const n of nodes) { + if (n.link) out.push(n) + if (n.items) walk(n.items) + } + } + walk(buildModel()) + return out +} + +/** 首页与 llms.txt 使用的构建时统计 —— 站点里出现的每个数字都来自这里。 */ +export function computeStats() { + return { + articles: articlesCount(), + translations: listMd('works').filter((f) => f.endsWith('-translation.md')).length, + concepts: listMd('concepts').length, + thinking: listMd('thinking').length, + checks: (readText('scripts/check-consistency.sh').match(/^echo "\[C\d+\]/gm) ?? []).length, + } +} + +/** 一等内容页集合:这些文件必须出现在侧边栏中(PROMPT.md、style.md 等附属材料不在此列)。 */ +function requiredPages() { + const req = new Set() + for (const d of ['concepts', 'thinking', 'feedback', 'prompts', 'works']) { + for (const f of listMd(d)) req.add(f) + } + for (const f of listMd('tools', { recursive: true })) req.add(f) + for (const f of subdirReadmes('practice')) req.add(f) + for (const f of subdirReadmes('works')) req.add(f) + req.add('references/articles.md') + return req +} + +export function verify() { + const files = collectPages().map((p) => p.file) + const seen = new Set() + const dups = [] + for (const f of files) (seen.has(f) ? dups.push(f) : seen.add(f)) + const req = requiredPages() + const missing = [...req].filter((f) => !seen.has(f)) + const orphans = files.filter((f) => !fs.existsSync(path.join(ROOT, f))) + const problems = [] + if (missing.length) problems.push(`missing from generated sidebar: ${missing.join(', ')}`) + if (dups.length) problems.push(`duplicated in generated sidebar: ${dups.join(', ')}`) + if (orphans.length) problems.push(`sidebar links to nonexistent files: ${orphans.join(', ')}`) + return { ok: problems.length === 0, problems, pageCount: files.length, requiredCount: req.size } +} + +const invokedDirectly = + process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) + +if (invokedDirectly) { + const mode = process.argv[2] + if (mode === '--verify') { + const r = verify() + if (r.ok) { + console.log( + `sidebar derives ${r.pageCount} pages from the filesystem; all ${r.requiredCount} first-class content files present exactly once` + ) + process.exit(0) + } + for (const p of r.problems) console.error(p) + process.exit(1) + } else if (mode === '--stats') { + console.log(JSON.stringify(computeStats(), null, 2)) + } else { + console.log(JSON.stringify(buildSidebar(), null, 2)) + } +} diff --git a/.vitepress/theme/HomeStats.vue b/.vitepress/theme/HomeStats.vue new file mode 100644 index 0000000..1a034d6 --- /dev/null +++ b/.vitepress/theme/HomeStats.vue @@ -0,0 +1,25 @@ + + + diff --git a/.vitepress/theme/custom.css b/.vitepress/theme/custom.css new file mode 100644 index 0000000..fe426d2 --- /dev/null +++ b/.vitepress/theme/custom.css @@ -0,0 +1,126 @@ +/** + * Harness Engineering 文档站 —— 自有视觉系统 + * + * 主色取「缰绳青」(teal),与仓库自产海报的暖纸底 × 缰绳橙形成同族 + * 但更适合长文阅读的数字端演绎;辅以天青做首页标题渐变。 + * 中文排版细节(字体栈、1.85 行高)吸收自 PR #21(@Doraemonblogs)。 + */ + +:root { + --vp-c-brand-1: #0f766e; + --vp-c-brand-2: #115e59; + --vp-c-brand-3: #14b8a6; + --vp-c-brand-soft: rgba(20, 184, 166, 0.14); + + --vp-font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans SC', sans-serif; + + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: linear-gradient(135deg, #0f766e 30%, #0284c7); +} + +.dark { + --vp-c-brand-1: #2dd4bf; + --vp-c-brand-2: #14b8a6; + --vp-c-brand-3: #5eead4; + --vp-c-brand-soft: rgba(45, 212, 191, 0.16); + + --vp-home-hero-name-background: linear-gradient(135deg, #2dd4bf 30%, #38bdf8); +} + +/* ── 中文长文阅读排版 ─────────────────────────────────────────── */ + +.vp-doc p, +.vp-doc li { + line-height: 1.85; +} + +.vp-doc h1, +.vp-doc h2, +.vp-doc h3 { + letter-spacing: 0.01em; +} + +/* 宽表格横向滚动,避免撑破移动端版面 */ +.vp-doc table { + display: block; + overflow-x: auto; + border-radius: 8px; +} + +div[class*='language-'] { + border-radius: 8px; +} + +/* ── 首页 ─────────────────────────────────────────────────────── */ + +.VPFeature { + transition: border-color 0.25s ease, transform 0.25s ease, box-shadow 0.25s ease; +} + +.VPFeature:hover { + border-color: var(--vp-c-brand-1); + transform: translateY(-2px); + box-shadow: 0 0 0 1px var(--vp-c-brand-soft), 0 10px 24px rgba(0, 0, 0, 0.06); +} + +/* 构建时统计卡片(HomeStats.vue) */ + +.home-stats { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 12px; + margin: 40px auto 0; + max-width: 1152px; + padding: 0 24px; +} + +@media (max-width: 960px) { + .home-stats { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .home-stats > a:last-child { + grid-column: span 2; + } +} + +.home-stats-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 20px 12px; + border: 1px solid var(--vp-c-divider); + border-radius: 12px; + background: var(--vp-c-bg-soft); + text-decoration: none; + transition: border-color 0.25s ease, transform 0.25s ease; +} + +.home-stats-card:hover { + border-color: var(--vp-c-brand-1); + transform: translateY(-2px); +} + +.home-stats-value { + font-size: 30px; + font-weight: 700; + line-height: 1.2; + color: var(--vp-c-brand-1); + font-variant-numeric: tabular-nums; +} + +.home-stats-label { + font-size: 13px; + color: var(--vp-c-text-2); + text-align: center; +} + +.home-stats-note { + margin: 12px auto 0; + max-width: 1152px; + padding: 0 24px; + font-size: 13px; + color: var(--vp-c-text-3); + text-align: center; +} diff --git a/.vitepress/theme/home-stats.data.mjs b/.vitepress/theme/home-stats.data.mjs new file mode 100644 index 0000000..039e4c7 --- /dev/null +++ b/.vitepress/theme/home-stats.data.mjs @@ -0,0 +1,16 @@ +// 首页统计的数据加载器:构建/开发时执行 computeStats(), +// 让首页数字与仓库文件系统永远同步(C14 禁止站点源码手写计数)。 +import { computeStats } from '../sidebar.mjs' + +export default { + watch: [ + '../../references/articles.md', + '../../works/*.md', + '../../concepts/*.md', + '../../thinking/*.md', + '../../scripts/check-consistency.sh', + ], + load() { + return computeStats() + }, +} diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts new file mode 100644 index 0000000..b9502d0 --- /dev/null +++ b/.vitepress/theme/index.ts @@ -0,0 +1,11 @@ +import type { Theme } from 'vitepress' +import DefaultTheme from 'vitepress/theme' +import HomeStats from './HomeStats.vue' +import './custom.css' + +export default { + extends: DefaultTheme, + enhanceApp({ app }) { + app.component('HomeStats', HomeStats) + }, +} satisfies Theme diff --git a/index.md b/index.md new file mode 100644 index 0000000..81fd5ab --- /dev/null +++ b/index.md @@ -0,0 +1,61 @@ +--- +layout: home + +hero: + name: Harness Engineering + text: 驭缰工程 · 中文学习档案 + tagline: 从概念拆解、独立思考到系统性翻译与动手实践——人类掌舵,智能体执行 + actions: + - theme: brand + text: 开始阅读 + link: /concepts/00-overview + - theme: alt + text: 为什么有这个项目 + link: /thinking/why-this-project-exists + - theme: alt + text: 文章索引 + link: /references/articles + +features: + - icon: 📦 + title: 仓库即记录系统 + details: 不在仓库里的东西,对智能体不存在。决策、规范、计划一律以版本化工件入库。 + link: /concepts/01-repo-as-source-of-truth + - icon: 🗺️ + title: 地图而非手册 + details: AGENTS.md 是目录页,不是百科全书。小入口点 + 渐进式披露,指向更深层文档。 + link: /concepts/00-overview + - icon: ⚙️ + title: 机械化执行 + details: 文档会腐烂,检查不会。自定义 linter 与结构测试是不变量的守护者。 + link: /concepts/02-mechanical-enforcement + - icon: 🤖 + title: 智能体可读性 + details: 为智能体的推理能力优化技术选型——偏爱 API 稳定、训练集覆盖好的「无聊」技术。 + link: /concepts/04-agent-readability + - icon: 🚀 + title: 吞吐量改变合并理念 + details: 纠错成本低、等待成本高时,短 PR 生命周期是理性选择,偶发失败靠重跑解决。 + link: /concepts/05-throughput-changes-merge + - icon: ♻️ + title: 熵管理即垃圾回收 + details: 智能体会复现仓库中已有的一切模式——包括坏模式。技术债是高息贷款。 + link: /concepts/03-entropy-and-garbage-collection +--- + + + +## 这个站本身也是一个 harness {#self-harness} + +本站遵循它所记录的方法论运转: + +- **内容即仓库**:每个页面就是 [GitHub 仓库](https://github.com/deusyu/harness-engineering)里的一个 Markdown 文件,站点只是它的一种渲染。 +- **导航与计数不手写**:侧边栏和首页统计在构建时从文件系统生成,由一致性检查(C14)机械化守护——新内容合入即出现,无需任何人记得去更新菜单。 +- **对智能体可读**:任意页面 URL 追加 `.md` 即得纯文本版;[/llms.txt](/llms.txt) 提供站点索引,[/feed.xml](/feed.xml) 提供 RSS 订阅。 + +## 从哪里开始 {#start} + +- 想快速建立框架:[概念总览](/concepts/00-overview),六大核心概念一页看完。 +- 想看批判性视角:[独立思考](/thinking/why-this-project-exists),包括对评估短板、个人开发者适用性的质疑。 +- 想读一手材料:[翻译与作品](/works/harness-engineering-chinese-interpretation),按来源系列分组的社区关键文章中译。 +- 想按图索骥:[文章索引](/references/articles),每篇附深度摘要与来源信息。 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a6cd75b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2537 @@ +{ + "name": "harness-engineering-docs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "harness-engineering-docs", + "version": "1.0.0", + "devDependencies": { + "vitepress": "^1.6.4" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.22.0.tgz", + "integrity": "sha512-BFR6zNowNKcY7Ou7TaJc9QWexES4YKPbmf/OTFofpdsdhz4x6q0lbxp3duO0EHnyrN7rE4ba/TSXuY+BDGu4+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.56.0.tgz", + "integrity": "sha512-7r4Z3NC7yU1oAQVWJNA2HX7tX481F3pJvCGyLIXiTdBcthz4Q/o21jwcMYDFkuI92UWTNBQQmHYgwHo1zS5dzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.56.0.tgz", + "integrity": "sha512-avmjXQSq+jadFO8Xl2em05/uQdQnEmHsJyOAdVbZkmVgpMfxL12aJwVVfGNwYr9nulcpuJN1X0lTaQ5wxuNGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.56.0.tgz", + "integrity": "sha512-v2TPStUhY//ripPjIVclZ8AWc7DEGooXULZGFlFu37zNatgHjw34oZZ+OSbbc/YHO+xZwPl62I1k8xH1m4S2eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.56.0.tgz", + "integrity": "sha512-P0ehROpM4Sem3Sqo5x2cKPgj67D3G3jy0rh1Amwkcvsfr6tkvIcdCmerieanqTF7NxUMPNFLkpIFeMO8Rpa50w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.56.0.tgz", + "integrity": "sha512-SXK3Vn3WVxyzbm31oePZBJkp1wpOyuWdd4B/Pv7n0aXDxmeSWhC1R1FC1517mMrFAIaPH4Rt0x6RUe7ZNjz8FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.56.0.tgz", + "integrity": "sha512-5+ZdX8garFnmycnZgKhtXHePEaLj5zqDxI/0lkhhluzCcvTn0/PvvTirTg8hHYetQHvn7GDyeAiqTAieMvMW4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.56.0.tgz", + "integrity": "sha512-+mKUdYvqOi0BcvpAEyCEw49vSBptufIcfibtHz2bdr1pI789M46Yt0uQEk/sxtK3teh71OQvVFHaTDzShUWewQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.56.0.tgz", + "integrity": "sha512-9g/zj+AZx5moFcdFIrYQoVrueXivjUcc3MQHtCYT8WhIuk1lUh1AyEhvJCS0XBZld09cLvd1AZ3BvDBpVpX2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.56.0.tgz", + "integrity": "sha512-Qf3Sr6f9A9uxCZUf3MXS0d2b877uYzEB5yxqpVGXAhcJnBCQjrRRon0KvefpGkxy+BshrIJs96OUoMtGqXTFDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.56.0.tgz", + "integrity": "sha512-GXWG1rWc5wu8hY4N33Y3b6ernY6sAdAvmKWN/zHAiACOx40WnpG0TVX5YazCAr/9gOYGInSiM2A0y2jy2xbiDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.56.0.tgz", + "integrity": "sha512-7t24cBxaInS3mZb7ddEaZT/tp6q+/aR4YttsQVyP1/i+LmwPR34atO35KjaLFCcRVrlP7sYOAqkCfg6lIRB+ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.56.0.tgz", + "integrity": "sha512-R7ePHgVYmDFjZpvrsVAfbDz/d4RxKAYZ5/vgLfIsCVRZRryjWl/3INOxpOICzitehQ5FjNtNjcLQTrmHPTcHBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.56.0.tgz", + "integrity": "sha512-PIOUXlSnrqM0S+WOgDRb4RzotydJH7ZoT6tOyL7tAO7qJOfvX5wsEW8Pe+PMKMwvuI4/gIyK9cg2H7lJXqnc4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/react": "3.8.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.93", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.93.tgz", + "integrity": "sha512-/XhANjfGYOuqvSR3TmUnkQkINvQ4GVjVuukvymRbxtVFBvIq/yiXJqCDycKcQPT401OYT9H2vIY6ihAlz1QIAw==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", + "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", + "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz", + "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.41", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", + "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz", + "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.41", + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-ssr": "3.5.41", + "@vue/shared": "3.5.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", + "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.10.tgz", + "integrity": "sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.10" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.10.tgz", + "integrity": "sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.10", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.10.tgz", + "integrity": "sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.41.tgz", + "integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.41.tgz", + "integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz", + "integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/runtime-core": "3.5.41", + "@vue/shared": "3.5.41", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.41.tgz", + "integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz", + "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/algoliasearch": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.56.0.tgz", + "integrity": "sha512-PrqppUmhT4ENdas2pH9caE7efUcxy6EcSFhWzosiVuQBzu2tQ5yLTI6jwomT/1cuBnivzGfxiJCqDNN9FRRh+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.22.0", + "@algolia/client-abtesting": "5.56.0", + "@algolia/client-analytics": "5.56.0", + "@algolia/client-common": "5.56.0", + "@algolia/client-insights": "5.56.0", + "@algolia/client-personalization": "5.56.0", + "@algolia/client-query-suggestions": "5.56.0", + "@algolia/client-search": "5.56.0", + "@algolia/ingestion": "1.56.0", + "@algolia/monitoring": "1.56.0", + "@algolia/recommend": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-to-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", + "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.8", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.8.tgz", + "integrity": "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact-render-to-string": ">=5" + }, + "peerDependenciesMeta": { + "preact-render-to-string": { + "optional": true + } + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/shiki": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", + "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/langs": "2.5.0", + "@shikijs/themes": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.41.tgz", + "integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-sfc": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/server-renderer": "3.5.41", + "@vue/shared": "3.5.41" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9978777 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "harness-engineering-docs", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "docs:dev": "vitepress dev", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview", + "docs:verify": "node .vitepress/sidebar.mjs --verify" + }, + "devDependencies": { + "vitepress": "^1.6.4" + } +} diff --git a/public/CNAME b/public/CNAME new file mode 100644 index 0000000..90d9d1c --- /dev/null +++ b/public/CNAME @@ -0,0 +1 @@ +harness.dyu.sh diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..5af354c0df9e2b200c1fca9ac7f2fc0af24d1b78 GIT binary patch literal 448 zcmb7>Jx;?w5QXW0C_>Pxpll?5+R$)<$l1ge#*Q{iHo!4BLnImw01X`y zH8+78iH+k>P$03IVrTT;`$n4XbT-EPD$A#m5@RMuzHhDdY|AV3*bh19zEcopsj?@< zToMOIBwSIi26Xor1ONPZX*}r0lmC2Ie-xX$w_g`qJMZ^)QQvrd!`b=qHW(}ATEzx7 z9hyXvi$J2%abi|~?a~7`8>Lw$X`hb{qDYZn`r=f_nL@mjM1=T8l5kQjsr_RZm9H{P z1}4E!Qa^|w1Wpk~2ze8NBj_~I<_J4$XI|urFg3ckR1RF_R0CFR>o_X~%hFuMgNY Date: Thu, 13 Aug 2026 10:44:07 +0000 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20C14=20guard=20=E2=80=94=20docs=20?= =?UTF-8?q?site=20must=20derive=20nav/counts=20from=20the=20filesystem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 手写侧边栏是 C1–C13 覆盖不到的漂移面。C14 守两条不变量: - 站点源码(index.md、.vitepress/)不得出现裸计数("N 篇"), 数字只能来自 computeStats() 的构建时统计 - 每个一等内容文件必须在生成的侧边栏中恰好出现一次 (node .vitepress/sidebar.mjs --verify) 站点脚手架不存在时 SKIP;node 不可用时 verify 半边 SKIP、裸计数 grep 照常执行。同步 AGENTS.md 检查清单说明与两份 README 的在线阅读徽章。 --- AGENTS.md | 1 + README.en.md | 1 + README.md | 1 + scripts/check-consistency.sh | 49 +++++++++++++++++++++++++++++++++++- 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 4cf30ca..095b9a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,6 +49,7 @@ - **C11** — markdown 表格形状:README ×2、`references/AGENTS.md`、`references/articles.md`、`works/AGENTS.md` 里每一行表格的单元格数须与表头一致 - **C12** — 条目字段完整性:`references/articles.md` 每个 `### N.` 编号条目必须带 **作者:** 与 **日期:** 字段 - **C13** — 零插图声明须留痕:C10 只能证伪"多报"(嵌图数 < 声明数才 FAIL),因此 `sourceFigureCount: 0` 在本地**永远无法被证伪**——不管你有没有真去核对原文,它都是绿的。2026-07-27 就是这个洞放行了一个假 0(原文实有 4 张配图)。C10 刻意零网络、无法回查原文,所以改为要求留痕:**声明 0 的译文必须同时带 `sourceFigureAudit` 字段,值里要有 `YYYY-MM-DD` 核对日期**,写清怎么核对的、结论是什么。`null` 仍然 SKIP——它本来就自陈未审计 +- **C14** — 文档站防漂移:VitePress 站点(部署到 harness.dyu.sh)的侧边栏与首页统计一律由 `.vitepress/sidebar.mjs` 在构建时从文件系统生成,不手写。两条不变量:站点源码(`index.md`、`.vitepress/`)不得出现裸计数("N 篇"),数字只能来自 `computeStats()`;每个一等内容文件必须在生成的侧边栏中恰好出现一次(`node .vitepress/sidebar.mjs --verify`)。站点脚手架不存在时 SKIP;node 不可用时 verify 半边 SKIP、裸计数 grep 照常执行 执行:`bash scripts/check-consistency.sh`(仓库根目录) 启用 pre-commit 阻断:`git config core.hooksPath .githooks` diff --git a/README.en.md b/README.en.md index 86ad777..85c6346 100644 --- a/README.en.md +++ b/README.en.md @@ -3,6 +3,7 @@ ![License: MIT](https://img.shields.io/badge/license-MIT-blue) ![Articles](https://img.shields.io/badge/articles-74-green) ![Translations](https://img.shields.io/badge/translations-34-orange) +[![Read online](https://img.shields.io/badge/read%20online-harness.dyu.sh-0f766e)](https://harness.dyu.sh) # Harness Engineering Study Guide diff --git a/README.md b/README.md index 9e98dd9..8a4b593 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ ![License: MIT](https://img.shields.io/badge/license-MIT-blue) ![Articles](https://img.shields.io/badge/articles-74-green) ![Translations](https://img.shields.io/badge/translations-34-orange) +[![在线阅读](https://img.shields.io/badge/在线阅读-harness.dyu.sh-0f766e)](https://harness.dyu.sh) # Harness Engineering 学习指南 diff --git a/scripts/check-consistency.sh b/scripts/check-consistency.sh index 1597e4b..37fc074 100755 --- a/scripts/check-consistency.sh +++ b/scripts/check-consistency.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # check-consistency.sh — guard against drift between articles.md and downstream caches. # -# Twelve checks: +# Fourteen checks: # C1 — articles.md heading numbering is contiguous 1..N # C2 — that N matches every downstream count claim # (README.md / README.en.md / prompts/deep-research-tracker.md / references/AGENTS.md) @@ -45,6 +45,15 @@ # sourceFigureAudit field containing a YYYY-MM-DD date, recording when # and how the "no figures" claim was verified. null keeps SKIPping — # it already self-declares as unaudited. +# C14 — docs-site harness integrity. The VitePress site must derive its +# sidebar and every displayed count from the filesystem at build time +# (.vitepress/sidebar.mjs), never from hand-written config. Two +# invariants: (a) site sources (index.md, .vitepress/) must not +# hardcode library counts ("N 篇") — numbers belong to computeStats(); +# (b) every first-class content file must appear in the generated +# sidebar exactly once (node .vitepress/sidebar.mjs --verify). SKIPs +# when the site scaffold is absent; the verify half additionally SKIPs +# when node is unavailable. # # Locale pitfall (do NOT reintroduce): bracket expressions containing multibyte # characters — e.g. [├└] or [^。] — silently break under LC_ALL=C with BSD grep: @@ -509,6 +518,44 @@ if [ "$c13_fail" -eq 0 ]; then echo " $(green PASS) — $c13_zero zero-figure claim(s), all carrying a dated audit trail" fi +# ─── C14 ─────────────────────────────────────────────────────────────── +# Docs-site harness integrity: the sidebar and all displayed counts are +# generated from the filesystem (.vitepress/sidebar.mjs). Hand-written nav +# or hardcoded counts would be a drift surface no other check covers. +echo "[C14] docs site derives nav/counts from the filesystem" +if [ ! -f .vitepress/sidebar.mjs ]; then + echo " $(yellow SKIP) — no .vitepress/sidebar.mjs (docs site scaffold not present)" +else + c14_ok=1 + c14_hits=$(grep -rnE '[0-9]+ ?篇' index.md .vitepress/config.ts .vitepress/sidebar.mjs .vitepress/theme 2>/dev/null || true) + if [ -n "$c14_hits" ]; then + while IFS= read -r c14_hit; do + [ -z "$c14_hit" ] && continue + echo " $(red FAIL) — hardcoded count in site source: $c14_hit" + done </dev/null 2>&1; then + c14_verify=$(node .vitepress/sidebar.mjs --verify 2>&1) + if [ $? -ne 0 ]; then + while IFS= read -r c14_line; do + [ -z "$c14_line" ] && continue + echo " $(red FAIL) — $c14_line" + done < Date: Fri, 14 Aug 2026 06:17:59 +0000 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20redesign=20docs=20site=20?= =?UTF-8?q?=E2=80=94=20=E7=BA=B8=E5=A2=A8=E7=BC=B0=E7=BB=B3=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E9=83=A8=E8=A7=86=E8=A7=89=EF=BC=88=E5=BB=B6=E7=BB=AD?= =?UTF-8?q?=E4=BB=93=E5=BA=93=E6=B5=B7=E6=8A=A5=E8=AE=BE=E8=AE=A1=E8=AF=AD?= =?UTF-8?q?=E5=A2=83=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一版视觉与仓库既有品牌资产脱节(无来历的 teal,默认模板感)。本版按 baoyu-design(Claude Design 引擎,upstream HEAD 2026-07-29)方法论重做, 设计语境直接取自仓库自产海报(works/harness-engineering-intro-deck): - 配色:暖纸底 #F5F1E8 × 深墨 #1F1C17 × 缰绳橙红 #C2481D(唯一强调色), 明暗两套 token 映射到 VitePress 主题变量 - 字体:标题用宋体系衬线(Noto Serif SC,unicode-range 切片按需加载, 不可达时回退系统宋体),正文保持无衬线 + 1.85 行高 - 首页重写为 HomeArchive.vue:口号做主标题、缰绳曲线 + 「驭缰」印章、 「档案总目」统计带(数字仍来自构建时统计,C14 约束不变)、 编号档案卡网格、导览索引;入场动效尊重 prefers-reduced-motion - 文档页重皮:衬线标题 + 橙红题标、纸色侧边栏 / 引用块 / 表格、 低调纸张颗粒质感 - favicon 与 README 在线阅读徽章同步新视觉 --- .vitepress/config.ts | 7 +- .vitepress/theme/HomeArchive.vue | 519 +++++++++++++++++++++++++++++++ .vitepress/theme/HomeStats.vue | 25 -- .vitepress/theme/custom.css | 293 ++++++++++++----- .vitepress/theme/index.ts | 4 +- README.en.md | 2 +- README.md | 2 +- index.md | 62 +--- public/favicon.svg | Bin 448 -> 450 bytes 9 files changed, 744 insertions(+), 170 deletions(-) create mode 100644 .vitepress/theme/HomeArchive.vue delete mode 100644 .vitepress/theme/HomeStats.vue diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 6e02154..ef2b37d 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -43,7 +43,12 @@ export default defineConfig({ head: [ ['link', { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }], - ['meta', { name: 'theme-color', content: '#0f766e' }], + // 标题衬线(Noto Serif SC):Google Fonts 按 unicode-range 切片按需加载, + // 不可达时回退系统宋体(Songti SC / STSong)。 + ['link', { rel: 'preconnect', href: 'https://fonts.googleapis.com' }], + ['link', { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }], + ['link', { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@600;700;900&display=swap' }], + ['meta', { name: 'theme-color', content: '#f5f1e8' }], ['meta', { property: 'og:site_name', content: SITE_TITLE }], ['meta', { property: 'og:type', content: 'website' }], ['link', { rel: 'alternate', type: 'application/rss+xml', title: `${SITE_TITLE} RSS`, href: `${HOST}/feed.xml` }], diff --git a/.vitepress/theme/HomeArchive.vue b/.vitepress/theme/HomeArchive.vue new file mode 100644 index 0000000..e8a6aad --- /dev/null +++ b/.vitepress/theme/HomeArchive.vue @@ -0,0 +1,519 @@ + + + + + diff --git a/.vitepress/theme/HomeStats.vue b/.vitepress/theme/HomeStats.vue deleted file mode 100644 index 1a034d6..0000000 --- a/.vitepress/theme/HomeStats.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/.vitepress/theme/custom.css b/.vitepress/theme/custom.css index fe426d2..1b1adc7 100644 --- a/.vitepress/theme/custom.css +++ b/.vitepress/theme/custom.css @@ -1,126 +1,255 @@ /** - * Harness Engineering 文档站 —— 自有视觉系统 + * Harness Engineering 文档站 —— 「纸墨缰绳」视觉系统 * - * 主色取「缰绳青」(teal),与仓库自产海报的暖纸底 × 缰绳橙形成同族 - * 但更适合长文阅读的数字端演绎;辅以天青做首页标题渐变。 - * 中文排版细节(字体栈、1.85 行高)吸收自 PR #21(@Doraemonblogs)。 + * 设计语境直接延续仓库自产海报(works/harness-engineering-intro-deck): + * 暖纸底 #F5F1E8 × 深墨 #1F1C17 × 缰绳橙红 #C2481D(唯一强调色), + * 视觉 DNA 是一条缰绳曲线——墨色锚点是人类,箭头指向智能体。 + * 方向:编辑部 / 档案馆气质,宋体系衬线做标题,长中文阅读优先。 + * 设计流程按 baoyu-design(Claude Design 引擎)方法论执行。 */ +/* ── 品牌 token ─────────────────────────────────────────────── */ + :root { - --vp-c-brand-1: #0f766e; - --vp-c-brand-2: #115e59; - --vp-c-brand-3: #14b8a6; - --vp-c-brand-soft: rgba(20, 184, 166, 0.14); + --he-paper: #f5f1e8; + --he-paper-2: #ede7d8; + --he-paper-3: #e5ddc9; + --he-ink: #1f1c17; + --he-ink-2: rgba(31, 28, 23, 0.74); + --he-ink-3: rgba(31, 28, 23, 0.52); + --he-rein: #c2481d; + --he-rein-strong: #a63a14; + --he-rein-soft: rgba(194, 72, 29, 0.12); + --he-hairline: rgba(31, 28, 23, 0.16); + --he-hairline-soft: rgba(31, 28, 23, 0.09); + + --he-serif: 'Noto Serif SC', 'Songti SC', 'STSong', 'SimSun', serif; +} + +.dark { + --he-paper: #171310; + --he-paper-2: #1f1a15; + --he-paper-3: #282218; + --he-ink: #f0eadf; + --he-ink-2: rgba(240, 234, 223, 0.76); + --he-ink-3: rgba(240, 234, 223, 0.55); + --he-rein: #e0602f; + --he-rein-strong: #ef7a4a; + --he-rein-soft: rgba(224, 96, 47, 0.16); + --he-hairline: rgba(240, 234, 223, 0.18); + --he-hairline-soft: rgba(240, 234, 223, 0.1); +} - --vp-font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, - 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans SC', sans-serif; +/* ── 映射到 VitePress 主题变量 ─────────────────────────────── */ - --vp-home-hero-name-color: transparent; - --vp-home-hero-name-background: linear-gradient(135deg, #0f766e 30%, #0284c7); +:root { + --vp-c-bg: var(--he-paper); + --vp-c-bg-alt: var(--he-paper-2); + --vp-c-bg-soft: var(--he-paper-2); + --vp-c-bg-elv: var(--he-paper); + + --vp-c-text-1: var(--he-ink); + --vp-c-text-2: var(--he-ink-2); + --vp-c-text-3: var(--he-ink-3); + + --vp-c-divider: var(--he-hairline); + --vp-c-gutter: var(--he-hairline); + --vp-c-border: var(--he-hairline); + + --vp-c-brand-1: var(--he-rein); + --vp-c-brand-2: var(--he-rein-strong); + --vp-c-brand-3: var(--he-rein); + --vp-c-brand-soft: var(--he-rein-soft); + + --vp-font-family-base: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', + 'PingFang SC', 'Hiragino Sans GB', 'Noto Sans SC', 'Microsoft YaHei', sans-serif; + + --vp-nav-bg-color: var(--he-paper); + --vp-sidebar-bg-color: var(--he-paper-2); + + --vp-code-block-bg: #eee8d9; + --vp-code-bg: var(--he-paper-3); + --vp-code-color: var(--he-rein-strong); + + --vp-custom-block-tip-border: transparent; + --vp-custom-block-tip-text: var(--he-ink); + --vp-custom-block-tip-bg: var(--he-rein-soft); + --vp-custom-block-info-bg: var(--he-paper-2); + + --vp-button-brand-bg: var(--he-ink); + --vp-button-brand-text: var(--he-paper); + --vp-button-brand-border: var(--he-ink); + --vp-button-brand-hover-bg: var(--he-rein); + --vp-button-brand-hover-text: #fff; + --vp-button-brand-hover-border: var(--he-rein); + --vp-button-brand-active-bg: var(--he-rein-strong); + --vp-button-brand-active-border: var(--he-rein-strong); } .dark { - --vp-c-brand-1: #2dd4bf; - --vp-c-brand-2: #14b8a6; - --vp-c-brand-3: #5eead4; - --vp-c-brand-soft: rgba(45, 212, 191, 0.16); + --vp-code-block-bg: #221d16; + --vp-code-bg: var(--he-paper-3); + --vp-code-color: var(--he-rein-strong); + --vp-button-brand-bg: var(--he-ink); + --vp-button-brand-text: #171310; + --vp-button-brand-border: var(--he-ink); +} - --vp-home-hero-name-background: linear-gradient(135deg, #2dd4bf 30%, #38bdf8); +::selection { + background: var(--he-rein-soft); } -/* ── 中文长文阅读排版 ─────────────────────────────────────────── */ +/* ── 纸张颗粒(低调的档案质感) ───────────────────────────── */ -.vp-doc p, -.vp-doc li { - line-height: 1.85; +body::after { + content: ''; + position: fixed; + inset: 0; + z-index: 999; + pointer-events: none; + opacity: 0.05; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E"); +} + +.dark body::after { + opacity: 0.07; +} + +/* ── 导航栏 ──────────────────────────────────────────────────── */ + +.VPNav { + border-bottom: 1px solid var(--he-hairline-soft); +} + +.VPNavBar:not(.home) { + background: var(--he-paper); +} + +.VPNavBarTitle .title { + font-family: var(--he-serif); + font-weight: 700; + letter-spacing: 0.02em; +} + +.VPNavBarMenuLink.active { + color: var(--he-rein); +} + +/* ── 侧边栏:档案目录气质 ────────────────────────────────────── */ + +.VPSidebar { + border-right: 1px solid var(--he-hairline-soft); +} + +.VPSidebarItem.level-0 > .item > .text { + font-family: var(--he-serif); + font-weight: 700; + letter-spacing: 0.06em; + color: var(--he-ink); +} + +.VPSidebarItem .item .text { + line-height: 1.6; +} + +/* ── 页内目录(outline) ─────────────────────────────────────── */ + +.VPDocAsideOutline .content { + border-left: 1px solid var(--he-hairline); } +.VPDocAsideOutline .outline-marker { + background-color: var(--he-rein); +} + +/* ── 正文排版:衬线标题 + 长中文阅读 ────────────────────────── */ + .vp-doc h1, .vp-doc h2, -.vp-doc h3 { - letter-spacing: 0.01em; +.vp-doc h3, +.vp-doc h4 { + font-family: var(--he-serif); + letter-spacing: 0.015em; } -/* 宽表格横向滚动,避免撑破移动端版面 */ -.vp-doc table { +.vp-doc h1 { + font-weight: 900; +} + +.vp-doc h2 { + font-weight: 700; + border-top: none; + padding-top: 0; + margin-top: 48px; +} + +.vp-doc h2::before { + content: ''; display: block; - overflow-x: auto; - border-radius: 8px; + width: 28px; + height: 3px; + background: var(--he-rein); + margin-bottom: 18px; } -div[class*='language-'] { - border-radius: 8px; +.vp-doc h3 { + font-weight: 700; } -/* ── 首页 ─────────────────────────────────────────────────────── */ +.vp-doc p, +.vp-doc li { + line-height: 1.85; +} -.VPFeature { - transition: border-color 0.25s ease, transform 0.25s ease, box-shadow 0.25s ease; +.vp-doc a { + text-underline-offset: 4px; + text-decoration-thickness: 1px; } -.VPFeature:hover { - border-color: var(--vp-c-brand-1); - transform: translateY(-2px); - box-shadow: 0 0 0 1px var(--vp-c-brand-soft), 0 10px 24px rgba(0, 0, 0, 0.06); +.vp-doc blockquote { + border-left: 2px solid var(--he-rein); + background: var(--he-paper-2); + padding: 12px 20px; + color: var(--he-ink-2); } -/* 构建时统计卡片(HomeStats.vue) */ +.vp-doc blockquote p { + font-size: 15px; +} -.home-stats { - display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); - gap: 12px; - margin: 40px auto 0; - max-width: 1152px; - padding: 0 24px; +/* 宽表格横向滚动;表头压纸色 */ +.vp-doc table { + display: block; + overflow-x: auto; } -@media (max-width: 960px) { - .home-stats { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - .home-stats > a:last-child { - grid-column: span 2; - } +.vp-doc th { + background: var(--he-paper-2); + font-family: var(--he-serif); + font-weight: 600; + letter-spacing: 0.04em; } -.home-stats-card { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - padding: 20px 12px; - border: 1px solid var(--vp-c-divider); - border-radius: 12px; - background: var(--vp-c-bg-soft); - text-decoration: none; - transition: border-color 0.25s ease, transform 0.25s ease; +.vp-doc tr:nth-child(2n) { + background: transparent; } -.home-stats-card:hover { - border-color: var(--vp-c-brand-1); - transform: translateY(-2px); +div[class*='language-'] { + border-radius: 4px; + border: 1px solid var(--he-hairline-soft); } -.home-stats-value { - font-size: 30px; - font-weight: 700; - line-height: 1.2; - color: var(--vp-c-brand-1); - font-variant-numeric: tabular-nums; +/* 上一篇 / 下一篇 */ +.pager-link { + border-radius: 0 !important; } -.home-stats-label { - font-size: 13px; - color: var(--vp-c-text-2); - text-align: center; +.pager-link:hover { + border-color: var(--he-rein) !important; } -.home-stats-note { - margin: 12px auto 0; - max-width: 1152px; - padding: 0 24px; - font-size: 13px; - color: var(--vp-c-text-3); - text-align: center; +/* ── 本地搜索弹层贴合纸色 ───────────────────────────────────── */ + +.VPLocalSearchBox .shell { + background: var(--he-paper); } diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts index b9502d0..9c437f0 100644 --- a/.vitepress/theme/index.ts +++ b/.vitepress/theme/index.ts @@ -1,11 +1,11 @@ import type { Theme } from 'vitepress' import DefaultTheme from 'vitepress/theme' -import HomeStats from './HomeStats.vue' +import HomeArchive from './HomeArchive.vue' import './custom.css' export default { extends: DefaultTheme, enhanceApp({ app }) { - app.component('HomeStats', HomeStats) + app.component('HomeArchive', HomeArchive) }, } satisfies Theme diff --git a/README.en.md b/README.en.md index 85c6346..41c6db5 100644 --- a/README.en.md +++ b/README.en.md @@ -3,7 +3,7 @@ ![License: MIT](https://img.shields.io/badge/license-MIT-blue) ![Articles](https://img.shields.io/badge/articles-74-green) ![Translations](https://img.shields.io/badge/translations-34-orange) -[![Read online](https://img.shields.io/badge/read%20online-harness.dyu.sh-0f766e)](https://harness.dyu.sh) +[![Read online](https://img.shields.io/badge/read%20online-harness.dyu.sh-c2481d)](https://harness.dyu.sh) # Harness Engineering Study Guide diff --git a/README.md b/README.md index 8a4b593..a3c0f56 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![License: MIT](https://img.shields.io/badge/license-MIT-blue) ![Articles](https://img.shields.io/badge/articles-74-green) ![Translations](https://img.shields.io/badge/translations-34-orange) -[![在线阅读](https://img.shields.io/badge/在线阅读-harness.dyu.sh-0f766e)](https://harness.dyu.sh) +[![在线阅读](https://img.shields.io/badge/在线阅读-harness.dyu.sh-c2481d)](https://harness.dyu.sh) # Harness Engineering 学习指南 diff --git a/index.md b/index.md index 81fd5ab..1efe6e6 100644 --- a/index.md +++ b/index.md @@ -1,61 +1,7 @@ --- -layout: home - -hero: - name: Harness Engineering - text: 驭缰工程 · 中文学习档案 - tagline: 从概念拆解、独立思考到系统性翻译与动手实践——人类掌舵,智能体执行 - actions: - - theme: brand - text: 开始阅读 - link: /concepts/00-overview - - theme: alt - text: 为什么有这个项目 - link: /thinking/why-this-project-exists - - theme: alt - text: 文章索引 - link: /references/articles - -features: - - icon: 📦 - title: 仓库即记录系统 - details: 不在仓库里的东西,对智能体不存在。决策、规范、计划一律以版本化工件入库。 - link: /concepts/01-repo-as-source-of-truth - - icon: 🗺️ - title: 地图而非手册 - details: AGENTS.md 是目录页,不是百科全书。小入口点 + 渐进式披露,指向更深层文档。 - link: /concepts/00-overview - - icon: ⚙️ - title: 机械化执行 - details: 文档会腐烂,检查不会。自定义 linter 与结构测试是不变量的守护者。 - link: /concepts/02-mechanical-enforcement - - icon: 🤖 - title: 智能体可读性 - details: 为智能体的推理能力优化技术选型——偏爱 API 稳定、训练集覆盖好的「无聊」技术。 - link: /concepts/04-agent-readability - - icon: 🚀 - title: 吞吐量改变合并理念 - details: 纠错成本低、等待成本高时,短 PR 生命周期是理性选择,偶发失败靠重跑解决。 - link: /concepts/05-throughput-changes-merge - - icon: ♻️ - title: 熵管理即垃圾回收 - details: 智能体会复现仓库中已有的一切模式——包括坏模式。技术债是高息贷款。 - link: /concepts/03-entropy-and-garbage-collection +layout: page +title: 驭缰工程 · 中文学习档案 +sidebar: false --- - - -## 这个站本身也是一个 harness {#self-harness} - -本站遵循它所记录的方法论运转: - -- **内容即仓库**:每个页面就是 [GitHub 仓库](https://github.com/deusyu/harness-engineering)里的一个 Markdown 文件,站点只是它的一种渲染。 -- **导航与计数不手写**:侧边栏和首页统计在构建时从文件系统生成,由一致性检查(C14)机械化守护——新内容合入即出现,无需任何人记得去更新菜单。 -- **对智能体可读**:任意页面 URL 追加 `.md` 即得纯文本版;[/llms.txt](/llms.txt) 提供站点索引,[/feed.xml](/feed.xml) 提供 RSS 订阅。 - -## 从哪里开始 {#start} - -- 想快速建立框架:[概念总览](/concepts/00-overview),六大核心概念一页看完。 -- 想看批判性视角:[独立思考](/thinking/why-this-project-exists),包括对评估短板、个人开发者适用性的质疑。 -- 想读一手材料:[翻译与作品](/works/harness-engineering-chinese-interpretation),按来源系列分组的社区关键文章中译。 -- 想按图索骥:[文章索引](/references/articles),每篇附深度摘要与来源信息。 + diff --git a/public/favicon.svg b/public/favicon.svg index 5af354c0df9e2b200c1fca9ac7f2fc0af24d1b78..ebf74285373b263da2bd01f0a9a872faa599c021 100644 GIT binary patch delta 85 zcmX@We295Mpt!;3$VF>S*afTp26If?F_nk!=FVd$c_vIu(-JUJQb^0p$+1;Zb~AM| lbhVhcK%33b&CuD Date: Fri, 14 Aug 2026 09:35:58 +0000 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20=E9=A6=96=E9=A1=B5=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=E4=B8=BA=E5=8D=B7=E5=AE=97=E7=BB=93=E6=9E=84=EF=BC=88?= =?UTF-8?q?=E5=90=B8=E6=94=B6=E7=94=A8=E6=88=B7=20Claude=20Design=20?= =?UTF-8?q?=E6=8E=A2=E7=B4=A2=E7=A8=BF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 结构与叙事吸收自用户以 Claude Design 产出的探索稿(2026-08-14): - 语义化缰绳图:从装饰升级为带节点标注的方法论路径 (人类·设计约束 → AGENTS.md → 自定义 linter → CI 反馈回路 → 智能体·如约交付) - § 卷宗分节:01 一句话理解 / 02 六大核心概念 / 03 档案总目 / 04 仓库即 harness·自我指涉 / 05 从哪里开始,配英文微标签 - § 01 范式转变对照(传统工程 vs HARNESS ENG. 约束链) - § 03 台账式总目(路径小注 + 右对齐大号衬线数字) - § 04 通栏暗色自指章节:人类闸门 / 机械护栏 / 反馈回路 + 驭缰印章 - § 05 五阶段阅读路线,对应仓库 Phase 1–5 学习路线 与探索稿的差异(守住 C14 纪律与事实核查): - 稿内硬编码计数(74/34/C1–C13)全部改回构建时动态统计, computeStats 新增 feedback 字段;C 检查数在文案中同样动态渲染 - Ralph Demo 的 321 秒 / $0.31 已对照 practice/ 核实为真实数据 --- .vitepress/sidebar.mjs | 1 + .vitepress/theme/HomeArchive.vue | 812 +++++++++++++++++++-------- .vitepress/theme/home-stats.data.mjs | 1 + 3 files changed, 565 insertions(+), 249 deletions(-) diff --git a/.vitepress/sidebar.mjs b/.vitepress/sidebar.mjs index 372857c..7307807 100644 --- a/.vitepress/sidebar.mjs +++ b/.vitepress/sidebar.mjs @@ -163,6 +163,7 @@ export function computeStats() { translations: listMd('works').filter((f) => f.endsWith('-translation.md')).length, concepts: listMd('concepts').length, thinking: listMd('thinking').length, + feedback: listMd('feedback').length, checks: (readText('scripts/check-consistency.sh').match(/^echo "\[C\d+\]/gm) ?? []).length, } } diff --git a/.vitepress/theme/HomeArchive.vue b/.vitepress/theme/HomeArchive.vue index e8a6aad..3047a51 100644 --- a/.vitepress/theme/HomeArchive.vue +++ b/.vitepress/theme/HomeArchive.vue @@ -1,38 +1,47 @@ @@ -40,80 +49,142 @@ const starts = [
-
-

HARNESS ENGINEERING · 驭缰工程中文学习档案

-

- 人类掌舵,
智能体执行。 -

-

- 从概念拆解、独立思考到系统性翻译与动手实践——把「驭缰工程」学透的完整过程,全程以仓库为记录系统。 -

- -
-
- -
- - {{ s.value }} - {{ s.label }} - -

总目数字在每次构建时从仓库文件系统统计——没有手写计数,也就没有漂移。

+ +
+
+

§ 01一句话理解

+ THE PARADIGM SHIFT +
+
+
+ 传统工程 + 人类写代码 + + 机器执行代码 +
+
+ HARNESS ENG. + 人类设计约束 + + 智能体写代码 + + 机器执行代码 +
+
+

+ 核心转变:工程师的产出,从「代码」变成了「约束系统」——AGENTS.md、架构规则、自定义 linter、反馈回路。 +

- -
-

六大核心概念

+ +
+
+

§ 02六大核心概念

+ SIX CORE CONCEPTS +
+

像档案卡一样编号归档——每一张都指向仓库里一篇可追溯的概念笔记。

+
+ + +
+
+

§ 03档案总目

+ BUILD-TIME · 自动清点 +
+

+ 导航与下面每一个数字,都在构建时由脚本从仓库文件系统清点生成,再由 C1–C{{ data.checks }} 一致性检查守护,不随文档腐烂而漂移。这不是营销数据,是一座档案馆的总目。 +

+
- -
-

这个站本身也是一个 harness

-
-
- {{ h.num }} -
-

{{ h.title }}

-

{{ h.desc }}

+ +
+
+
+

§ 04仓库即 harness · 自我指涉

+ THE ARCHIVE RUNS ON WHAT IT RECORDS +
+

这个仓库,开始策展自己了

+
+
+ {{ p.tag }} + {{ p.title }} + {{ p.desc }}
+

+ 于是约束本身成了产品——正是「约束即产品」讲的东西,只不过这一次,实验的对象是仓库自己。 +

+
-

- 源码与全部内容在 - GitHub 仓库;智能体入口见 - /llms.txt,订阅见 /feed.xml。 -

- -
-

从哪里开始

-
- - {{ s.title }} - {{ s.desc }} - + +
+
+

§ 05从哪里开始

+ A READING ROUTE · 5 PHASES +
+
@@ -135,29 +206,25 @@ const starts = [ /* ── 卷首 ─────────────────────────────────────────────────── */ .ha-hero { - display: grid; - grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); - align-items: center; - gap: 24px; - padding: 88px 0 72px; + padding: 80px 0 8px; } .ha-kicker { font-size: 13px; font-weight: 600; - letter-spacing: 0.22em; + letter-spacing: 0.24em; color: var(--he-rein); - margin: 0 0 20px; + margin: 0 0 22px; } .ha-title { font-family: var(--he-serif); font-weight: 900; - font-size: clamp(44px, 6.2vw, 74px); - line-height: 1.18; + font-size: clamp(42px, 6.4vw, 78px); + line-height: 1.16; letter-spacing: 0.01em; color: var(--he-ink); - margin: 0 0 26px; + margin: 0 0 24px; } .ha-title em { @@ -166,139 +233,209 @@ const starts = [ } .ha-lede { - font-size: 17px; - line-height: 1.9; + font-size: 16.5px; + line-height: 1.95; color: var(--he-ink-2); - max-width: 34em; - margin: 0 0 36px; + max-width: 36em; + margin: 0 0 34px; } .ha-actions { display: flex; align-items: center; flex-wrap: wrap; - gap: 28px; + gap: 14px; } .ha-btn { display: inline-block; background: var(--he-ink); + border: 1px solid var(--he-ink); color: var(--he-paper) !important; - font-size: 15px; + font-size: 14.5px; font-weight: 600; - letter-spacing: 0.08em; - padding: 13px 34px; - transition: background 0.25s ease; + letter-spacing: 0.06em; + padding: 12px 26px; + transition: background 0.25s ease, color 0.25s ease, border-color 0.25s ease; } .ha-btn:hover { background: var(--he-rein); + border-color: var(--he-rein); color: #fff !important; } -.ha-link { - font-size: 14.5px; - color: var(--he-ink-2) !important; - border-bottom: 1px solid var(--he-hairline); - padding-bottom: 3px; - transition: color 0.2s ease, border-color 0.2s ease; +.ha-btn-ghost { + background: transparent; + border-color: var(--he-hairline); + color: var(--he-ink) !important; } -.ha-link:hover { - color: var(--he-rein) !important; +.ha-btn-ghost:hover { + background: transparent; border-color: var(--he-rein); + color: var(--he-rein) !important; } -.ha-hero-art { - position: relative; - min-height: 240px; +.ha-btn sup { + font-size: 10px; + color: var(--he-rein); + margin-left: 2px; } +.ha-btn:hover sup { + color: inherit; +} + +/* 语义化缰绳图 */ + .ha-rein { + display: block; width: 100%; height: auto; - display: block; + margin: 30px 0 0; } -.ha-seal { - position: absolute; - right: 8%; - bottom: 4%; - width: 76px; - height: 76px; - display: grid; - place-items: center; - background: var(--he-rein); - color: #f5f1e8; - font-family: var(--he-serif); - font-weight: 900; - font-size: 26px; - letter-spacing: 0.14em; - writing-mode: vertical-rl; - transform: rotate(3deg); +.ha-rein-t { + font-family: var(--vp-font-family-base); + font-size: 15px; + font-weight: 700; + fill: var(--he-ink); } -/* ── 档案总目 ─────────────────────────────────────────────── */ +.ha-rein-end { + fill: var(--he-rein); +} -.ha-stats { - display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); - border-top: 2px solid var(--he-ink); - border-bottom: 1px solid var(--he-hairline); - padding: 26px 0 22px; +.ha-rein-s { + font-family: var(--vp-font-family-base); + font-size: 11.5px; + letter-spacing: 0.06em; + fill: var(--he-ink-3); } -.ha-stat { - display: flex; - flex-direction: column; - gap: 6px; - padding: 2px 22px; - border-left: 1px solid var(--he-hairline-soft); +@media (max-width: 640px) { + .ha-rein { + display: none; + } } -.ha-stat:first-child { - border-left: none; - padding-left: 0; +/* ── 分节标头 ─────────────────────────────────────────────── */ + +.ha-sec { + padding: 84px 0 0; +} + +.ha-last { + padding-bottom: 112px; } -.ha-stat-value { +.ha-sec-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16px; + margin-bottom: 18px; +} + +.ha-sec-title { font-family: var(--he-serif); font-weight: 900; - font-size: clamp(30px, 3.4vw, 42px); - line-height: 1.1; + font-size: clamp(24px, 3vw, 32px); color: var(--he-ink); - font-variant-numeric: tabular-nums; - transition: color 0.2s ease; + margin: 0; + border: none; + padding: 0; } -.ha-stat:hover .ha-stat-value { +.ha-sec-num { + font-size: 13px; + font-weight: 700; + letter-spacing: 0.12em; color: var(--he-rein); + margin-right: 14px; + vertical-align: 3px; } -.ha-stat-label { - font-size: 12.5px; - letter-spacing: 0.18em; +.ha-sec-en { + font-size: 10.5px; + letter-spacing: 0.28em; color: var(--he-ink-3); + white-space: nowrap; +} + +.ha-sec-intro { + font-size: 14px; + line-height: 1.9; + color: var(--he-ink-2); + max-width: 44em; + margin: 0 0 30px; +} + +/* ── § 01 范式转变 ───────────────────────────────────────── */ + +.ha-shift { + display: grid; + gap: 14px; + border-top: 2px solid var(--he-ink); + padding-top: 26px; + margin-top: 26px; +} + +.ha-shift-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12px; } -.ha-stats-note { - grid-column: 1 / -1; - margin: 18px 0 0; - font-size: 12.5px; +.ha-shift-label { + width: 118px; + font-size: 11px; + letter-spacing: 0.18em; color: var(--he-ink-3); + white-space: nowrap; } -/* ── 区块通用 ─────────────────────────────────────────────── */ +.ha-shift-label-hot { + color: var(--he-rein); + font-weight: 700; +} -.ha-section { - padding: 72px 0 0; +.ha-chip { + border: 1px solid var(--he-hairline); + padding: 9px 16px; + font-size: 13.5px; + color: var(--he-ink); + background: var(--he-paper); } -.ha-last { - padding-bottom: 104px; +.ha-chip-hot { + border-color: var(--he-rein); + color: var(--he-rein); + font-weight: 600; } -/* ── 概念档案卡 ───────────────────────────────────────────── */ +.ha-shift-arrow { + color: var(--he-ink-3); + font-size: 14px; +} + +.ha-shift-note { + margin: 26px 0 0; + font-size: 14.5px; + line-height: 1.9; + color: var(--he-ink-2); + max-width: 44em; +} + +.ha-shift-note em { + font-style: normal; + color: var(--he-rein); + font-weight: 600; +} + +/* ── § 02 概念档案卡 ─────────────────────────────────────── */ .ha-grid { display: grid; @@ -313,7 +450,7 @@ const starts = [ flex-direction: column; gap: 10px; background: var(--he-paper); - padding: 26px 24px 30px; + padding: 24px 24px 20px; transition: background 0.25s ease; } @@ -321,12 +458,26 @@ const starts = [ background: var(--he-paper-2); } +.ha-card-top { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 8px; +} + .ha-card-num { font-family: var(--he-serif); - font-weight: 700; - font-size: 13px; - letter-spacing: 0.1em; + font-weight: 900; + font-size: 30px; color: var(--he-rein); + line-height: 1; +} + +.ha-card-tag { + font-size: 9.5px; + letter-spacing: 0.22em; + color: var(--he-ink-3); + white-space: nowrap; } .ha-card-title { @@ -341,113 +492,284 @@ const starts = [ color: var(--he-rein); } +.ha-card-desc { + font-size: 13px; + line-height: 1.85; + color: var(--he-ink-2); + flex: 1; +} + +.ha-card-foot { + display: flex; + align-items: center; + justify-content: space-between; + border-top: 1px solid var(--he-hairline-soft); + padding-top: 12px; + font-size: 11px; + letter-spacing: 0.1em; + color: var(--he-ink-3); +} + .ha-card-arrow { - display: inline-block; - margin-left: 6px; - font-size: 14px; - opacity: 0; - transform: translate(-3px, 3px); - transition: opacity 0.2s ease, transform 0.2s ease; + color: var(--he-rein); + transform: translateX(-4px); + opacity: 0.4; + transition: transform 0.2s ease, opacity 0.2s ease; } .ha-card:hover .ha-card-arrow { - opacity: 1; transform: none; + opacity: 1; } -.ha-card-desc { - font-size: 13.5px; - line-height: 1.85; - color: var(--he-ink-2); -} +/* ── § 03 台账 ───────────────────────────────────────────── */ -/* ── 自指三条 ─────────────────────────────────────────────── */ +.ha-ledger { + border-top: 2px solid var(--he-ink); +} -.ha-rows { +.ha-ledger-row { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 40px; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: baseline; + column-gap: 10px; + padding: 22px 4px; + border-bottom: 1px solid var(--he-hairline); + transition: background 0.2s ease, padding 0.2s ease; +} + +.ha-ledger-row:hover { + background: var(--he-paper-2); + padding-left: 14px; + padding-right: 14px; } -.ha-row { +.ha-ledger-main { display: flex; - gap: 16px; - align-items: flex-start; + flex-direction: column; + gap: 6px; +} + +.ha-ledger-title { + font-family: var(--he-serif); + font-weight: 700; + font-size: 19px; + color: var(--he-ink); } -.ha-row-num { +.ha-ledger-sub { + font-size: 11px; + letter-spacing: 0.08em; + color: var(--he-ink-3); +} + +.ha-ledger-value { font-family: var(--he-serif); font-weight: 900; - font-size: 20px; - line-height: 1.4; + font-size: clamp(36px, 4.6vw, 52px); + line-height: 1; + color: var(--he-ink); + font-variant-numeric: tabular-nums; + transition: color 0.2s ease; +} + +.ha-ledger-row:hover .ha-ledger-value { + color: var(--he-rein); +} + +.ha-ledger-hot { color: var(--he-rein); } -.ha-row-title { +.ha-ledger-unit { + font-size: 12px; + color: var(--he-ink-3); +} + +/* ── § 04 通栏暗色自指 ───────────────────────────────────── */ + +.ha-band { + width: 100vw; + margin: 92px 0 0 calc(50% - 50vw); + background: #17130e; + color: #f0eadf; +} + +.dark .ha-band { + background: #100d09; +} + +.ha-band-in { + position: relative; + max-width: 1104px; + margin: 0 auto; + padding: 72px 32px 76px; +} + +.ha-sec-title-dark { + color: #f0eadf; +} + +.ha-sec-en-dark { + color: rgba(240, 234, 223, 0.5); +} + +.ha-band-title { + font-family: var(--he-serif); + font-weight: 900; + font-size: clamp(32px, 4.6vw, 52px); + line-height: 1.3; + color: #f0eadf; + margin: 26px 0 40px; + max-width: 15em; +} + +.ha-band-title em { + font-style: normal; + color: #e0602f; +} + +.ha-band-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1px; + background: rgba(240, 234, 223, 0.18); + border: 1px solid rgba(240, 234, 223, 0.18); +} + +.ha-band-card { + display: flex; + flex-direction: column; + gap: 10px; + background: #17130e; + padding: 24px; +} + +.dark .ha-band-card { + background: #100d09; +} + +.ha-band-tag { + font-size: 9.5px; + letter-spacing: 0.24em; + color: #e0602f; +} + +.ha-band-card-title { font-family: var(--he-serif); font-weight: 700; - font-size: 16.5px; - color: var(--he-ink); - margin: 0 0 6px; + font-size: 18px; + color: #f0eadf; } -.ha-row-desc { - font-size: 13.5px; +.ha-band-card-desc { + font-size: 13px; line-height: 1.85; - color: var(--he-ink-2); - margin: 0; + color: rgba(240, 234, 223, 0.72); } -.ha-fineprint { - margin: 36px 0 0; - font-size: 13px; - color: var(--he-ink-3); +.ha-band-note { + margin: 34px 0 0; + font-size: 14px; + line-height: 1.9; + color: rgba(240, 234, 223, 0.72); + max-width: 44em; } -.ha-fineprint a { - color: var(--he-rein) !important; - border-bottom: 1px solid var(--he-rein-soft); +.ha-band-note a { + color: #e0602f !important; + border-bottom: 1px solid rgba(224, 96, 47, 0.4); } -/* ── 导览 ─────────────────────────────────────────────────── */ +.ha-seal { + position: absolute; + top: 68px; + right: 40px; + width: 72px; + height: 72px; + display: grid; + place-items: center; + background: #c2481d; + color: #f5f1e8; + font-family: var(--he-serif); + font-weight: 900; + font-size: 24px; + letter-spacing: 0.14em; + writing-mode: vertical-rl; + transform: rotate(3deg); +} + +/* ── § 05 阅读路线 ───────────────────────────────────────── */ -.ha-starts { +.ha-route { border-top: 2px solid var(--he-ink); + display: grid; } -.ha-start { +.ha-phase { display: grid; - grid-template-columns: 220px minmax(0, 1fr) auto; - align-items: baseline; - gap: 20px; - padding: 20px 4px; + grid-template-columns: 46px minmax(0, 1fr); + gap: 18px; + padding: 24px 4px; border-bottom: 1px solid var(--he-hairline); transition: background 0.2s ease, padding 0.2s ease; } -.ha-start:hover { +.ha-phase:hover { background: var(--he-paper-2); - padding-left: 12px; - padding-right: 12px; + padding-left: 14px; + padding-right: 14px; } -.ha-start-title { +.ha-phase-n { + width: 30px; + height: 30px; + display: grid; + place-items: center; + border: 1.5px solid var(--he-rein); + border-radius: 50%; font-family: var(--he-serif); font-weight: 700; - font-size: 17px; - color: var(--he-ink); + font-size: 14px; + color: var(--he-rein); + margin-top: 4px; } -.ha-start-desc { - font-size: 13.5px; - color: var(--he-ink-3); +.ha-phase-n-filled { + background: var(--he-rein); + color: #f5f1e8; +} + +.ha-phase-body { + display: flex; + flex-direction: column; + gap: 5px; } -.ha-start-arrow { - font-size: 16px; +.ha-phase-meta { + font-size: 10.5px; + letter-spacing: 0.18em; color: var(--he-rein); } +.ha-phase-title { + font-family: var(--he-serif); + font-weight: 700; + font-size: 19px; + color: var(--he-ink); + transition: color 0.2s ease; +} + +.ha-phase:hover .ha-phase-title { + color: var(--he-rein); +} + +.ha-phase-desc { + font-size: 13.5px; + line-height: 1.85; + color: var(--he-ink-2); +} + /* ── 入场动效(尊重减少动态偏好) ────────────────────────── */ @media (prefers-reduced-motion: no-preference) { @@ -460,7 +782,7 @@ const starts = [ .ha-d1 { animation-delay: 0.1s; } .ha-d2 { animation-delay: 0.18s; } .ha-d3 { animation-delay: 0.26s; } - .ha-d4 { animation-delay: 0.34s; } + .ha-d4 { animation-delay: 0.38s; } } @keyframes ha-up { @@ -477,43 +799,35 @@ const starts = [ padding: 0 24px; } .ha-hero { - grid-template-columns: 1fr; - padding: 56px 0 48px; + padding: 52px 0 0; } - .ha-hero-art { - order: -1; - min-height: 0; - max-width: 320px; - } - .ha-seal { - width: 60px; - height: 60px; - font-size: 20px; - } - .ha-stats { - grid-template-columns: repeat(2, minmax(0, 1fr)); - row-gap: 20px; - } - .ha-stat { - padding: 2px 16px; - } - .ha-stat:nth-child(odd) { - border-left: none; - padding-left: 0; + .ha-sec { + padding-top: 64px; } .ha-grid { grid-template-columns: 1fr; } - .ha-rows { + .ha-band-grid { grid-template-columns: 1fr; - gap: 24px; } - .ha-start { - grid-template-columns: minmax(0, 1fr) auto; + .ha-band-in { + padding: 56px 24px 60px; + } + .ha-seal { + top: 52px; + right: 24px; + width: 58px; + height: 58px; + font-size: 19px; + } + .ha-shift-label { + width: 100%; + } + .ha-ledger-value { + font-size: 34px; } - .ha-start-desc { - grid-column: 1 / -1; - order: 3; + .ha-sec-en { + display: none; } } diff --git a/.vitepress/theme/home-stats.data.mjs b/.vitepress/theme/home-stats.data.mjs index 039e4c7..7cf79f7 100644 --- a/.vitepress/theme/home-stats.data.mjs +++ b/.vitepress/theme/home-stats.data.mjs @@ -8,6 +8,7 @@ export default { '../../works/*.md', '../../concepts/*.md', '../../thinking/*.md', + '../../feedback/*.md', '../../scripts/check-consistency.sh', ], load() { From b7cd4c9301dca8bf001958a17075712077ad5eb2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 14 Aug 2026 10:58:40 +0000 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20=E6=96=87=E6=A1=A3=E9=A1=B5?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=B8=BA=E6=A1=A3=E6=A1=88=E5=8D=B7=E5=AE=97?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=EF=BC=88=E5=90=B8=E6=94=B6=E7=94=A8=E6=88=B7?= =?UTF-8?q?=20Claude=20Design=20=E7=AC=AC=E4=BA=8C=E7=A8=BF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 吸收自用户以 Claude Design 产出的文档页探索稿(concepts/06 重绘): - 文档页头:档案面包屑(档案 · 概念 · CONCEPTS · 06)+ 构建时估算的 阅读时长 + 最近更新(git 时间),经 doc-before 插槽注入(DocMeta.vue) - 小节自动编号:h2 上方细线 + 橙色 01/02 编号,由 CSS 计数器机械生成, 源文件零改动 - 侧边栏档案化:分组头「中文名 + EN 微标签 + 右对齐计数徽标」,编号 文件名显示条目编号(00–07)——由 sidebar.mjs 构建时注入展示装饰, collectPages/llms.txt/RSS 拿到的仍是干净文本 - 右栏 outline 下缰绳 doodle(Agent = Model + Harness,AsideMark.vue) - 导航精简:概念 / 思考 / 实践 / 作品 / 资料库;站名改「驭缰工程」 - 引用盒全边框、橙色列表符、衬线翻页卡;资料库条目改「文章深度摘要」 (计数上移到分组徽标) 计数纪律不变:徽标数字与阅读时长均为构建时计算,C1–C14 全绿。 --- .vitepress/config.ts | 27 +++++++--- .vitepress/sidebar.mjs | 51 +++++++++++++------ .vitepress/theme/AsideMark.vue | 31 ++++++++++++ .vitepress/theme/DocMeta.vue | 91 ++++++++++++++++++++++++++++++++++ .vitepress/theme/custom.css | 75 ++++++++++++++++++++++++---- .vitepress/theme/index.ts | 8 +++ 6 files changed, 251 insertions(+), 32 deletions(-) create mode 100644 .vitepress/theme/AsideMark.vue create mode 100644 .vitepress/theme/DocMeta.vue diff --git a/.vitepress/config.ts b/.vitepress/config.ts index ef2b37d..1a8986e 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -63,23 +63,38 @@ export default defineConfig({ ['meta', { property: 'og:url', content: `${HOST}/${cleanPath}` }], ['meta', { name: 'twitter:card', content: 'summary' }], ) + // 构建时估算阅读时长(中文按字数计),供 DocMeta 文档页头使用。 + if (pageData.relativePath !== 'index.md') { + try { + const raw = fs + .readFileSync(path.join(ROOT, pageData.relativePath), 'utf8') + .replace(/^---[\s\S]*?\n---/, '') + .replace(/```[\s\S]*?```/g, '') + const cjk = (raw.match(/[\u4e00-\u9fff]/g) ?? []).length + const words = (raw.match(/[A-Za-z0-9]+/g) ?? []).length + pageData.frontmatter.haReadingTime = Math.max(1, Math.round((cjk + words * 1.5) / 400)) + } catch { + /* 文件不可读时跳过阅读时长 */ + } + } }, themeConfig: { logo: '/favicon.svg', + siteTitle: '驭缰工程', nav: [ - { text: '首页', link: '/' }, - { text: '概念笔记', link: '/concepts/00-overview' }, - { text: '独立思考', link: '/thinking/why-this-project-exists' }, - { text: '翻译与作品', link: '/works/harness-engineering-chinese-interpretation' }, - { text: '文章索引', link: '/references/articles' }, + { text: '概念', link: '/concepts/00-overview' }, + { text: '思考', link: '/thinking/why-this-project-exists' }, + { text: '实践', link: '/practice/01-ralph-demo/README' }, + { text: '作品', link: '/works/harness-engineering-chinese-interpretation' }, + { text: '资料库', link: '/references/articles' }, ], // 侧边栏由 .vitepress/sidebar.mjs 从文件系统生成(C14 守卫),不手写。 sidebar: buildSidebar(), - outline: { label: '本页目录', level: [2, 3] }, + outline: { label: '本页目录 · ON THIS PAGE', level: [2, 3] }, search: { provider: 'local', diff --git a/.vitepress/sidebar.mjs b/.vitepress/sidebar.mjs index 7307807..df91b1e 100644 --- a/.vitepress/sidebar.mjs +++ b/.vitepress/sidebar.mjs @@ -116,31 +116,52 @@ export function articlesCount() { /** 内部模型:与最终侧边栏同构,但每个页面节点额外带 file 字段供校验/构建用。 */ export function buildModel() { return [ - { text: '概念笔记', collapsed: false, items: listMd('concepts').map(page) }, - { text: '独立思考', collapsed: false, items: listMd('thinking').map(page) }, - { text: '动手实践', collapsed: false, items: subdirReadmes('practice').map(page) }, - { text: '反馈记录', collapsed: false, items: listMd('feedback').map(page) }, - worksSection(), - { text: '工具库', collapsed: false, items: listMd('tools', { recursive: true }).map(page) }, - { text: '提示词', collapsed: false, items: listMd('prompts').map(page) }, + { text: '概念笔记', en: 'CONCEPTS', collapsed: false, items: listMd('concepts').map(page) }, + { text: '独立思考', en: 'THINKING', collapsed: false, items: listMd('thinking').map(page) }, + { text: '动手实践', en: 'PRACTICE', collapsed: false, items: subdirReadmes('practice').map(page) }, + { text: '反馈记录', en: 'FEEDBACK', collapsed: false, items: listMd('feedback').map(page) }, + { en: 'WORKS', ...worksSection() }, + { text: '工具库', en: 'TOOLS', collapsed: false, items: listMd('tools', { recursive: true }).map(page) }, + { text: '提示词', en: 'PROMPTS', collapsed: false, items: listMd('prompts').map(page) }, { - text: '资源索引', + text: '资料库', + en: 'REFERENCES', + countValue: articlesCount(), collapsed: false, - items: [{ ...page('references/articles.md'), text: `文章索引(${articlesCount()} 篇深度摘要)` }], + items: [{ ...page('references/articles.md'), text: '文章深度摘要' }], }, ] } -function stripFile(nodes) { - return nodes.map(({ file, items, ...rest }) => ({ - ...rest, - ...(items ? { items: stripFile(items) } : {}), - })) +function countLinks(nodes) { + let n = 0 + for (const node of nodes) { + if (node.link) n += 1 + if (node.items) n += countLinks(node.items) + } + return n +} + +/** 展示层装饰(仅 buildSidebar 使用,collectPages/llms/RSS 拿到的仍是干净文本): + * 顶级分组加英文微标签 + 右对齐计数徽标;带数字前缀的文件名在条目前显示编号。 */ +function decorateItem(node) { + if (node.items) { + return { text: node.text, collapsed: node.collapsed, items: node.items.map(decorateItem) } + } + const m = node.file.match(/(?:^|\/)(\d+)-[^/]*\.md$/) + const text = m ? `${m[1]}${node.text}` : node.text + return { text, link: node.link } } /** VitePress themeConfig.sidebar 直接消费的形态。 */ export function buildSidebar() { - return stripFile(buildModel()) + return buildModel().map((section) => ({ + text: `${section.text}${section.en}${ + section.countValue ?? countLinks(section.items) + }`, + collapsed: section.collapsed, + items: section.items.map(decorateItem), + })) } /** 展平出全部页面节点(含 file),供 --verify 与 buildEnd(md 副本 / llms / RSS)使用。 */ diff --git a/.vitepress/theme/AsideMark.vue b/.vitepress/theme/AsideMark.vue new file mode 100644 index 0000000..d8e4a97 --- /dev/null +++ b/.vitepress/theme/AsideMark.vue @@ -0,0 +1,31 @@ + + + diff --git a/.vitepress/theme/DocMeta.vue b/.vitepress/theme/DocMeta.vue new file mode 100644 index 0000000..9c097ec --- /dev/null +++ b/.vitepress/theme/DocMeta.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/.vitepress/theme/custom.css b/.vitepress/theme/custom.css index 1b1adc7..e712d65 100644 --- a/.vitepress/theme/custom.css +++ b/.vitepress/theme/custom.css @@ -141,11 +141,46 @@ body::after { border-right: 1px solid var(--he-hairline-soft); } +/* 分组标头:中文名 + 英文微标签 + 右对齐计数徽标(由 sidebar.mjs 构建时注入) */ .VPSidebarItem.level-0 > .item > .text { - font-family: var(--he-serif); + display: flex; + align-items: baseline; + width: 100%; + font-size: 11px; font-weight: 700; + letter-spacing: 0.16em; + color: var(--he-ink-2); +} + +.ha-side-en { + margin-left: 7px; + font-size: 9px; + font-weight: 400; + letter-spacing: 0.22em; + color: var(--he-ink-3); +} + +.ha-side-count { + margin-left: auto; + font-size: 10px; + font-weight: 400; + letter-spacing: 0; + color: var(--he-ink-3); + font-variant-numeric: tabular-nums; +} + +.ha-side-num { + display: inline-block; + min-width: 26px; + font-size: 11px; letter-spacing: 0.06em; - color: var(--he-ink); + color: var(--he-ink-3); + font-variant-numeric: tabular-nums; +} + +.VPSidebarItem .link:hover .ha-side-num, +.VPSidebarItem .is-active .ha-side-num { + color: inherit; } .VPSidebarItem .item .text { @@ -176,26 +211,38 @@ body::after { font-weight: 900; } +/* 小节自动编号(01 02 …):源文件不写编号,编号由 CSS 计数器机械生成 */ +.vp-doc { + counter-reset: ha-sec; +} + .vp-doc h2 { font-weight: 700; - border-top: none; - padding-top: 0; - margin-top: 48px; + border-top: 1px solid var(--he-hairline); + padding-top: 30px; + margin-top: 56px; } .vp-doc h2::before { - content: ''; + counter-increment: ha-sec; + content: counter(ha-sec, decimal-leading-zero); display: block; - width: 28px; - height: 3px; - background: var(--he-rein); - margin-bottom: 18px; + font-family: var(--vp-font-family-base); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.18em; + color: var(--he-rein); + margin-bottom: 10px; } .vp-doc h3 { font-weight: 700; } +.vp-doc ul > li::marker { + color: var(--he-rein); +} + .vp-doc p, .vp-doc li { line-height: 1.85; @@ -207,9 +254,10 @@ body::after { } .vp-doc blockquote { + border: 1px solid var(--he-hairline); border-left: 2px solid var(--he-rein); background: var(--he-paper-2); - padding: 12px 20px; + padding: 14px 20px; color: var(--he-ink-2); } @@ -244,6 +292,11 @@ div[class*='language-'] { border-radius: 0 !important; } +.pager-link .title { + font-family: var(--he-serif); + font-weight: 700; +} + .pager-link:hover { border-color: var(--he-rein) !important; } diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts index 9c437f0..d5fc1e2 100644 --- a/.vitepress/theme/index.ts +++ b/.vitepress/theme/index.ts @@ -1,10 +1,18 @@ +import { h } from 'vue' import type { Theme } from 'vitepress' import DefaultTheme from 'vitepress/theme' import HomeArchive from './HomeArchive.vue' +import DocMeta from './DocMeta.vue' +import AsideMark from './AsideMark.vue' import './custom.css' export default { extends: DefaultTheme, + Layout: () => + h(DefaultTheme.Layout, null, { + 'doc-before': () => h(DocMeta), + 'aside-outline-after': () => h(AsideMark), + }), enhanceApp({ app }) { app.component('HomeArchive', HomeArchive) }, From 081508a0cba93b7fe3157230377cdab1925d9a34 Mon Sep 17 00:00:00 2001 From: deusyu Date: Mon, 17 Aug 2026 23:41:00 +0800 Subject: [PATCH 07/12] =?UTF-8?q?fix:=20=E8=90=BD=E5=AE=9E=20codex=20revie?= =?UTF-8?q?w=E2=80=94=E2=80=94=E6=B3=A8=E5=85=A5/=E8=B6=8A=E7=95=8C/?= =?UTF-8?q?=E5=85=A8=E9=A1=B5=20.md=20=E5=A5=91=E7=BA=A6=E4=B8=89=E9=98=BB?= =?UTF-8?q?=E6=96=AD=20+=20=E5=85=A8=E9=83=A8=E6=AC=A1=E7=BA=A7=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 阻断项: - gitDate 改 execFileSync 参数数组 + '--' 分隔,消除构建期命令注入 - sidebar.mjs 全面拒绝 symlink(isFile/lstat/realpath 越界防御), buildEnd 所有文件读取/复制经 assertContentFile - 新增发布页面全集模型 collectPublishedPages:66 页全部获得同路径 .md 副本(含首页 /index.md、PROMPT、poster/style),llms.txt 增附属页面 节,llms-full.txt 收齐全文;新增 scripts/verify-dist.mjs 在构建后 机械断言 html↔md 一一对应并接入 CI(也堵住本地 output/ 目录泄露) 次级项: - CI 最小权限:顶层 contents:read,build 只加 pages:read (configure-pages 必需),pages/id-token 写权限仅 deploy job; 删除 enablement:true 与误导注释 - 死链检查重新开启:构建期把指向未发布目标的相对链接改写为 GitHub blob/tree 链接(仅限 git 已跟踪目标),目录若有已发布 README 则路由 站内页 - works/ 子目录作品改走同一套分组匹配,「社区博客」兜底真正生效 - pre-commit 触发范围扩至 index.md、.vitepress/、works/imgs/ 与嵌套 内容目录,补收尾锚点并加 --no-renames - README×2 更新为十四层校验(C1–C14)并补 C14 条目;favicon 移除损坏 注释(xmllint 通过) --- .githooks/pre-commit | 13 ++- .github/workflows/deploy-docs.yml | 17 ++-- .vitepress/config.ts | 140 ++++++++++++++++++++++++------ .vitepress/sidebar.mjs | 121 +++++++++++++++++++++++--- README.en.md | 7 +- README.md | 7 +- package.json | 3 +- public/favicon.svg | 2 +- scripts/verify-dist.mjs | 71 +++++++++++++++ 9 files changed, 324 insertions(+), 57 deletions(-) create mode 100644 scripts/verify-dist.mjs diff --git a/.githooks/pre-commit b/.githooks/pre-commit index f2b0666..75007ad 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -8,10 +8,15 @@ set -u # Files whose change can break consistency. Keep in sync with -# scripts/check-consistency.sh sources. -WATCHED_REGEX='^(references/(articles\.md|AGENTS\.md)|README\.md|README\.en\.md|AGENTS\.md|prompts/deep-research-tracker\.md|(concepts|thinking|feedback|works)/[^/]+\.md)$' - -staged=$(git diff --cached --name-only --diff-filter=ACMRD) +# scripts/check-consistency.sh sources: C1-C13 read the README/AGENTS/tracker, +# content markdown (any depth — nested works/practice/tools files count), and +# the images referenced by translations (C10 checks works/imgs/* existence); +# C14 additionally reads index.md, .vitepress/**, and the check script itself. +WATCHED_REGEX='^(references/(articles|AGENTS)\.md$|README(\.en)?\.md$|AGENTS\.md$|index\.md$|\.vitepress/|works/imgs/|scripts/check-consistency\.sh$|(concepts|thinking|feedback|works|practice|tools|prompts)/.+\.md$)' + +# --no-renames: a rename lists only the destination path by default, letting +# `git mv README.md elsewhere` slip past the watch list unseen. +staged=$(git diff --cached --no-renames --name-only --diff-filter=ACMRD) if ! echo "$staged" | grep -qE "$WATCHED_REGEX"; then exit 0 fi diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index d45fdf4..2ddc527 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -10,10 +10,10 @@ on: branches: [main] workflow_dispatch: +# 最小权限:build job 只读源码;pages/id-token 写权限只授给 deploy job, +# 构建(会执行仓库内脚本)不持有任何部署凭据。 permissions: contents: read - pages: write - id-token: write concurrency: group: pages @@ -22,6 +22,9 @@ concurrency: jobs: build: runs-on: ubuntu-latest + permissions: + contents: read + pages: read # configure-pages 读取 Pages 配置(GET /pages)需要 steps: - uses: actions/checkout@v6 with: @@ -29,8 +32,6 @@ jobs: - name: Setup Pages uses: actions/configure-pages@v5 - with: - enablement: true # 首次运行自动开启 GitHub Pages(Actions 部署模式) - name: Setup Node uses: actions/setup-node@v5 @@ -42,11 +43,14 @@ jobs: run: npm ci - name: Verify sidebar completeness (C14) - run: node .vitepress/sidebar.mjs --verify + run: npm run docs:verify - name: Build site run: npm run docs:build + - name: Verify dist contract (page ↔ markdown-copy parity) + run: npm run docs:verify:dist + - name: Disable Jekyll run: touch .vitepress/dist/.nojekyll @@ -58,6 +62,9 @@ jobs: deploy: needs: build runs-on: ubuntu-latest + permissions: + pages: write + id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 1a8986e..ceb4d4b 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -1,11 +1,20 @@ import { defineConfig } from 'vitepress' -import { execSync } from 'node:child_process' +import { execFileSync } from 'node:child_process' import fs from 'node:fs' import path from 'node:path' // @ts-ignore — 纯 ESM 生成器(Node 标准库,无类型声明) -import { ROOT, buildSidebar, collectPages, computeStats } from './sidebar.mjs' +import { + ROOT, + SRC_EXCLUDE, + assertContentFile, + buildSidebar, + collectPages, + collectPublishedPages, + computeStats, +} from './sidebar.mjs' const HOST = 'https://harness.dyu.sh' +const REPO_URL = 'https://github.com/deusyu/harness-engineering' const SITE_TITLE = 'Harness Engineering' const SITE_DESC = '驭缰工程中文学习档案——概念笔记、独立思考、系统性翻译与实践记录' @@ -19,26 +28,94 @@ export default defineConfig({ lastUpdated: true, // 站点只发布内容页:智能体导航文件(AGENTS.md)、仓库门面(根 README)、 - // 本地过程稿(translate/)与私密资料(private/)都不属于站点。 - srcExclude: [ - '**/AGENTS.md', - 'README.md', - 'README.en.md', - 'CLAUDE.md', - 'translate/**', - 'private/**', - '.claude/**', - ], - - // 仓库内多数交叉链接为 GitHub 浏览而写(目录链接、指向 .py/AGENTS.md 的链接), - // VitePress 天然无法解析,故关闭死链阻断。站内导航完整性由 C14 的 - // sidebar --verify 从文件系统侧守护。 - ignoreDeadLinks: true, + // 本地过程稿(translate/、output/)与私密资料(private/)都不属于站点。 + // 排除清单的唯一事实源在 sidebar.mjs(SRC_EXCLUDE),与发布页面模型同源。 + srcExclude: SRC_EXCLUDE, + // 死链阻断保持开启:仓库内为 GitHub 浏览而写的交叉链接(目录链接、指向 + // .py/AGENTS.md 的链接)由下方 markdown.config 的构建期改写规则统一转成 + // GitHub 链接,站内不应残留任何死链。 sitemap: { hostname: HOST }, markdown: { image: { lazy: true }, + config(md) { + // 构建期链接改写:相对链接若指向站点未发布的目标(AGENTS.md、源码文件、 + // 目录……),改写为 GitHub 链接,内容在站点与 GitHub 两个语境下都可读; + // 指向的目录若有已发布的 README,则直接路由到站内该页。改写发生在 parse + // 阶段,VitePress 的死链检查看到的已是改写后的链接,因此无需关闭检查。 + // (published 在渲染器创建时快照一次;dev 模式新增页面需重启才会进集合, + // 生产构建每次全新快照,不受影响。) + const published = new Set(collectPublishedPages().map((p) => p.file)) + // 只有 git 跟踪的目标才配得到 GitHub 链接——本地存在但未跟踪的文件改写 + // 过去只会得到 404;这类链接留给死链检查大声报错。git 不可用时放行兜底。 + let trackedFiles: Set | null = null + const trackedDirs = new Set() + try { + trackedFiles = new Set( + execFileSync('git', ['ls-files', '-z'], { + cwd: ROOT, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + maxBuffer: 32 * 1024 * 1024, + }) + .split('\0') + .filter(Boolean) + ) + for (const f of trackedFiles) { + let d = path.posix.dirname(f) + while (d !== '.' && !trackedDirs.has(d)) { + trackedDirs.add(d) + d = path.posix.dirname(d) + } + } + } catch { + trackedFiles = null + } + const isTracked = (rel: string, isDir: boolean) => + trackedFiles === null || (isDir ? trackedDirs.has(rel) : trackedFiles.has(rel)) + md.core.ruler.push('ha_rewrite_repo_links', (state) => { + const pagePath = (state.env as { relativePath?: string })?.relativePath + if (!pagePath) return + for (const block of state.tokens) { + if (block.type !== 'inline' || !block.children) continue + for (const token of block.children) { + if (token.type !== 'link_open') continue + const href = token.attrGet('href') + // 只处理相对链接;协议链接、锚点、站内绝对路径原样保留。 + if (!href || /^(?:[a-z][a-z0-9+.-]*:|\/\/|#|\/)/i.test(href)) continue + const hash = href.indexOf('#') + const target = hash === -1 ? href : href.slice(0, hash) + const anchor = hash === -1 ? '' : href.slice(hash) + if (!target) continue + let decoded = target + try { + decoded = decodeURI(target) + } catch { + /* 非法转义序列:按原文处理 */ + } + const rel = path.posix + .normalize(path.posix.join(path.posix.dirname(pagePath), decoded)) + .replace(/\/+$/, '') + if (rel.startsWith('..')) continue // 仓库外相对路径:留给死链检查报错 + if (published.has(rel)) continue // 已发布页面:VitePress 自会路由 + let stat + try { + stat = fs.statSync(path.join(ROOT, rel)) + } catch { + continue // 目标不存在:留给死链检查报错 + } + if (stat.isDirectory() && published.has(path.posix.join(rel, 'README.md'))) { + token.attrSet('href', `/${rel}/README${anchor}`) + } else if (isTracked(rel, stat.isDirectory())) { + const kind = stat.isDirectory() ? 'tree' : 'blob' + token.attrSet('href', `${REPO_URL}/${kind}/main/${rel}${anchor}`) + } + // 未跟踪的本地目标不改写:留给死链检查报错,胜过发布一个 404 链接 + } + } + }) + }, }, head: [ @@ -67,7 +144,7 @@ export default defineConfig({ if (pageData.relativePath !== 'index.md') { try { const raw = fs - .readFileSync(path.join(ROOT, pageData.relativePath), 'utf8') + .readFileSync(assertContentFile(pageData.relativePath), 'utf8') .replace(/^---[\s\S]*?\n---/, '') .replace(/```[\s\S]*?```/g, '') const cjk = (raw.match(/[\u4e00-\u9fff]/g) ?? []).length @@ -136,26 +213,33 @@ export default defineConfig({ */ async buildEnd(siteConfig) { const out = siteConfig.outDir - const pages = collectPages() + const pages = collectPages() // 侧栏内容页:llms 分组索引与 RSS 用 + const published = collectPublishedPages() // 发布全集(含首页与附属页):md 副本与全文用 const stats = computeStats() - // 1) 每页伴生 Markdown 副本 - for (const p of pages) { - const dest = path.join(out, `${p.link.slice(1)}.md`) + // 1) 每个发布页面伴生同路径 Markdown 副本(首页 → /index.md)。 + // 与产物的一一对应由 scripts/verify-dist.mjs 在构建后机械校验。 + for (const p of published) { + const dest = path.join(out, p.link === '/' ? 'index.md' : `${p.link.slice(1)}.md`) fs.mkdirSync(path.dirname(dest), { recursive: true }) - fs.copyFileSync(path.join(ROOT, p.file), dest) + fs.copyFileSync(assertContentFile(p.file), dest) } // 2) llms.txt / llms-full.txt + const sidebarFiles = new Set(pages.map((p) => p.file)) + const extras = published.filter((p) => p.file !== 'index.md' && !sidebarFiles.has(p.file)) const model = groupedForLlms(pages) const llms = [ `# ${SITE_TITLE} 学习档案`, '', `> 中文 Harness Engineering(驭缰工程)知识库:${stats.concepts} 篇概念笔记、${stats.thinking} 篇独立思考、${stats.translations} 篇一手翻译,以及收录 ${stats.articles} 篇文章的深度摘要索引。人类掌舵,智能体执行。`, '', - '本站每个页面都有同路径的 Markdown 版本:在页面 URL 后追加 `.md` 即可获取纯文本。', + '本站每个页面都有同路径的 Markdown 版本:在页面 URL 后追加 `.md` 即可获取纯文本(首页为 `/index.md`)。', '', ...model, + ...(extras.length + ? ['## 附属页面', '', ...extras.map((p) => `- [${p.text}](${HOST}${p.link}.md)`), ''] + : []), '## 完整内容', '', `- [llms-full.txt](${HOST}/llms-full.txt):全站正文合并版`, @@ -163,9 +247,10 @@ export default defineConfig({ ].join('\n') fs.writeFileSync(path.join(out, 'llms.txt'), llms) - const full = pages + // 全文 = 侧栏内容页(按分区顺序)+ 附属页;首页是纯组件壳,无正文可并。 + const full = [...pages, ...extras] .map((p) => { - const raw = fs.readFileSync(path.join(ROOT, p.file), 'utf8') + const raw = fs.readFileSync(assertContentFile(p.file), 'utf8') return `\n\n---\ntitle: ${p.text}\nurl: ${HOST}${p.link}\n---\n\n${raw}` }) .join('') @@ -236,7 +321,8 @@ function groupedForLlms(pages: Array<{ text: string; link: string; file: string function gitDate(file: string): Date { try { - const iso = execSync(`git log -1 --format=%cI -- "${file}"`, { + // 参数数组 + `--` 分隔符:文件名永远只是参数,不进 shell,杜绝命令注入。 + const iso = execFileSync('git', ['log', '-1', '--format=%cI', '--', file], { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], diff --git a/.vitepress/sidebar.mjs b/.vitepress/sidebar.mjs index df91b1e..f439522 100644 --- a/.vitepress/sidebar.mjs +++ b/.vitepress/sidebar.mjs @@ -23,12 +23,43 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const REAL_ROOT = fs.realpathSync(ROOT) + +/** + * 站点不发布的源文件(VitePress srcExclude 的唯一事实源,config.ts 直接 import)。 + * collectPublishedFiles() 的排除规则与此保持同构;两者的最终一致性由 + * scripts/verify-dist.mjs 在构建产物上机械校验。 + */ +export const SRC_EXCLUDE = [ + '**/AGENTS.md', + 'README.md', + 'README.en.md', + 'CLAUDE.md', + 'translate/**', + 'private/**', + 'output/**', + '.claude/**', +] + +/** + * 安全解析内容文件:必须是仓库根内的普通文件。拒绝 symlink 与越界路径, + * 防止仓库外内容(哪怕通过一个恶意/失误的软链)被读入并复制进公开产物。 + */ +export function assertContentFile(rel) { + const expected = path.join(REAL_ROOT, rel) + if (!expected.startsWith(REAL_ROOT + path.sep)) throw new Error(`refusing path outside repo root: ${rel}`) + const abs = path.join(ROOT, rel) + const st = fs.lstatSync(abs, { throwIfNoEntry: false }) + if (!st || !st.isFile()) throw new Error(`refusing non-regular file (symlink/missing): ${rel}`) + if (fs.realpathSync(abs) !== expected) throw new Error(`refusing symlinked path: ${rel}`) + return abs +} function readText(rel) { - return fs.readFileSync(path.join(ROOT, rel), 'utf8') + return fs.readFileSync(assertContentFile(rel), 'utf8') } -/** 列出目录下的内容 md 文件(排除 AGENTS.md),返回仓库相对路径,按文件名排序。 */ +/** 列出目录下的内容 md 文件(排除 AGENTS.md 与一切非普通文件),返回仓库相对路径,按文件名排序。 */ function listMd(dir, { recursive = false } = {}) { const abs = path.join(ROOT, dir) if (!fs.existsSync(abs)) return [] @@ -38,13 +69,14 @@ function listMd(dir, { recursive = false } = {}) { if (recursive) out.push(...listMd(path.join(dir, ent.name), { recursive })) continue } + if (!ent.isFile()) continue // symlink 一律不进内容集 if (!ent.name.endsWith('.md') || ent.name === 'AGENTS.md') continue out.push(path.join(dir, ent.name)) } return out.sort() } -/** 子目录形态的作品/实验:以其 README.md 作为入口页。 */ +/** 子目录形态的作品/实验:以其 README.md 作为入口页(必须是普通文件,symlink 不算)。 */ function subdirReadmes(dir) { const abs = path.join(ROOT, dir) if (!fs.existsSync(abs)) return [] @@ -52,11 +84,47 @@ function subdirReadmes(dir) { for (const ent of fs.readdirSync(abs, { withFileTypes: true })) { if (!ent.isDirectory()) continue const rel = path.join(dir, ent.name, 'README.md') - if (fs.existsSync(path.join(ROOT, rel))) out.push(rel) + const st = fs.lstatSync(path.join(ROOT, rel), { throwIfNoEntry: false }) + if (st?.isFile()) out.push(rel) } return out.sort() } +/** SRC_EXCLUDE 的谓词形式:collectPublishedFiles 用它判断一个文件是否被站点排除。 */ +function isExcludedFromSite(rel) { + if (path.basename(rel) === 'AGENTS.md') return true + if (rel === 'README.md' || rel === 'README.en.md' || rel === 'CLAUDE.md') return true + return ['translate/', 'private/', 'output/', '.claude/'].some((p) => rel.startsWith(p)) +} + +/** + * 站点实际发布的全部 md 源文件(含首页、PROMPT.md、poster/style.md 等不进侧栏的 + * 附属页)。遍历规则与 VitePress 的源扫描一致:跳过点目录与 node_modules,再应用 + * SRC_EXCLUDE。md 副本、llms 输出与产物校验共享这一个模型,杜绝「发布了却没有 + * 机器可读副本」的缝隙。 + */ +export function collectPublishedFiles() { + const out = [] + const walk = (dir) => { + for (const ent of fs.readdirSync(path.join(ROOT, dir || '.'), { withFileTypes: true })) { + const rel = dir ? path.join(dir, ent.name) : ent.name + if (ent.isDirectory()) { + // 与 VitePress 源扫描的忽略规则对齐:点目录、node_modules、任意层级的 dist + if (ent.name.startsWith('.') || ent.name === 'node_modules' || ent.name === 'dist') continue + walk(rel) + continue + } + if (!ent.isFile() || !ent.name.endsWith('.md')) continue + if (ent.name.startsWith('.')) continue // glob dot:false,点文件不被 VitePress 构建 + if (/\[\w+?\]/.test(rel)) continue // VitePress 视作动态路由,无 .paths 时不产出 html + if (isExcludedFromSite(rel)) continue + out.push(rel) + } + } + walk('') + return out.sort() +} + /** 标题优先级:frontmatter title > 首个 H1 > 文件名;去掉结尾全角括注以适配侧边栏宽度。 */ export function extractTitle(rel) { const text = readText(rel) @@ -79,11 +147,16 @@ function page(rel) { } /** - * works/ 分组规则:按文件名前缀归入来源系列(分组式信息架构吸收自 PR #21, - * by @Doraemonblogs)。match 按数组顺序生效,最后一组恒真兜底。 + * works/ 分组规则:按文件名(子目录作品按目录名)前缀归入来源系列(分组式信息 + * 架构吸收自 PR #21,by @Doraemonblogs)。match 按数组顺序生效,最后一组恒真 + * 兜底——未匹配任何前缀的新作品落入「社区博客」,宁可分组不准,不可静默丢失。 */ const WORKS_GROUPS = [ - { text: '原创分析', match: (n) => n === 'harness-engineering-chinese-interpretation.md' }, + { + text: '原创分析', + match: (n) => + n === 'harness-engineering-chinese-interpretation.md' || n === 'harness-engineering-intro-deck', + }, { text: 'Martin Fowler 系列', match: (n) => n.startsWith('fowler-') }, { text: 'Anthropic 系列', match: (n) => n.startsWith('anthropic-') }, { text: 'LangChain 系列', match: (n) => /^(langchain|langsmith|deep-agents)-/.test(n) }, @@ -96,10 +169,12 @@ const WORKS_GROUPS = [ function worksSection() { const groups = WORKS_GROUPS.map((g) => ({ text: g.text, match: g.match, items: [] })) for (const rel of listMd('works')) { - const name = path.basename(rel) - groups.find((g) => g.match(name)).items.push(page(rel)) + groups.find((g) => g.match(path.basename(rel))).items.push(page(rel)) + } + // 子目录作品与平铺文件走同一套匹配规则(按目录名),同样受兜底组保护。 + for (const rel of subdirReadmes('works')) { + groups.find((g) => g.match(path.basename(path.dirname(rel)))).items.push(page(rel)) } - for (const rel of subdirReadmes('works')) groups[0].items.push(page(rel)) return { text: '翻译与作品', collapsed: true, @@ -164,7 +239,7 @@ export function buildSidebar() { })) } -/** 展平出全部页面节点(含 file),供 --verify 与 buildEnd(md 副本 / llms / RSS)使用。 */ +/** 展平出侧栏全部页面节点(含 file),供 --verify、llms.txt 分组索引与 RSS 使用。 */ export function collectPages() { const out = [] const walk = (nodes) => { @@ -177,6 +252,16 @@ export function collectPages() { return out } +/** + * 发布页面全集的页面对象(含首页与附属页),供 buildEnd 生成 .md 副本与 + * llms 全文使用——「每个页面都有同路径 Markdown 版本」这句对外承诺以此为准。 + */ +export function collectPublishedPages() { + return collectPublishedFiles().map((rel) => + rel === 'index.md' ? { text: '首页', link: '/', file: rel } : page(rel) + ) +} + /** 首页与 llms.txt 使用的构建时统计 —— 站点里出现的每个数字都来自这里。 */ export function computeStats() { return { @@ -210,11 +295,21 @@ export function verify() { const req = requiredPages() const missing = [...req].filter((f) => !seen.has(f)) const orphans = files.filter((f) => !fs.existsSync(path.join(ROOT, f))) + // 侧栏页必须是发布全集的子集——srcExclude 误伤侧栏页会在这里现形。 + const published = new Set(collectPublishedFiles()) + const unpublished = files.filter((f) => !published.has(f)) const problems = [] if (missing.length) problems.push(`missing from generated sidebar: ${missing.join(', ')}`) if (dups.length) problems.push(`duplicated in generated sidebar: ${dups.join(', ')}`) if (orphans.length) problems.push(`sidebar links to nonexistent files: ${orphans.join(', ')}`) - return { ok: problems.length === 0, problems, pageCount: files.length, requiredCount: req.size } + if (unpublished.length) problems.push(`sidebar links to files excluded from the site: ${unpublished.join(', ')}`) + return { + ok: problems.length === 0, + problems, + pageCount: files.length, + requiredCount: req.size, + publishedCount: published.size, + } } const invokedDirectly = @@ -226,7 +321,7 @@ if (invokedDirectly) { const r = verify() if (r.ok) { console.log( - `sidebar derives ${r.pageCount} pages from the filesystem; all ${r.requiredCount} first-class content files present exactly once` + `sidebar derives ${r.pageCount} pages from the filesystem; all ${r.requiredCount} first-class content files present exactly once; site publishes ${r.publishedCount} markdown sources` ) process.exit(0) } diff --git a/README.en.md b/README.en.md index 41c6db5..0c5fc58 100644 --- a/README.en.md +++ b/README.en.md @@ -228,7 +228,7 @@ The "Ralph Wiggum Loop" is the core implementation pattern of Harness Engineerin ## 🛠️ Development Notes -The repo ships with a consistency checker, `scripts/check-consistency.sh`, guarding against count and fidelity drift across thirteen layers of checks: +The repo ships with a consistency checker, `scripts/check-consistency.sh`, guarding against count and fidelity drift across fourteen layers of checks: - **C1-C2** — `references/articles.md` article count + its 4 downstream claim sites (README × 2 badges, `prompts/deep-research-tracker.md` header, `references/AGENTS.md` overview) - **C3** — actual `*.md` file counts in `concepts/` / `thinking/` / `feedback/` match the README "X 篇" claims @@ -242,6 +242,7 @@ The repo ships with a consistency checker, `scripts/check-consistency.sh`, guard - **C11** — markdown table shape: in the checked files, every table row must carry the same cell count as its header - **C12** — every numbered entry in `references/articles.md` must carry the **作者:** and **日期:** fields - **C13** — zero-figure claims need an audit trail. C10 can only falsify OVER-claiming, so `sourceFigureCount: 0` is unfalsifiable locally — that hole shipped a false 0 on 2026-07-27 (the source had 4 body figures). Any translation claiming 0 must therefore also carry `sourceFigureAudit` containing a `YYYY-MM-DD` date, stating how the claim was verified +- **C14** — docs-site harness integrity: the VitePress sidebar and every displayed count must be derived from the filesystem at build time by `.vitepress/sidebar.mjs`; site sources (`index.md`, `.vitepress/**`) must not hardcode counts, and `node .vitepress/sidebar.mjs --verify` asserts every first-class content file appears in the generated sidebar exactly once. On the artifact side, `scripts/verify-dist.mjs` asserts published pages and their `.md` copies correspond one-to-one **Enable the pre-commit hook after first clone:** @@ -249,7 +250,7 @@ The repo ships with a consistency checker, `scripts/check-consistency.sh`, guard git config core.hooksPath .githooks ``` -Once enabled, every commit touching the README, `AGENTS.md`, `references/articles.md`, `references/AGENTS.md`, `prompts/deep-research-tracker.md`, or any `*.md` under `concepts/` / `thinking/` / `feedback/` / `works/` runs the checks automatically; unrelated commits are left alone. +Once enabled, every commit touching the README, `AGENTS.md`, `references/articles.md`, `references/AGENTS.md`, `index.md`, `.vitepress/`, `scripts/check-consistency.sh`, or any `*.md` (nested included) under `concepts/` / `thinking/` / `feedback/` / `works/` / `practice/` / `tools/` / `prompts/` runs the checks automatically; unrelated commits are left alone. **Run manually:** `bash scripts/check-consistency.sh` @@ -261,7 +262,7 @@ See the "机械化检查" section of the root `AGENTS.md` for details. > This archive now curates itself. > -> Bringing in outside research no longer runs on vibes — it follows a pipeline frozen into a skill, [`curate-research`](.claude/skills/curate-research/SKILL.md): review is automated by parallel agents (the feedback loop), `scripts/check-consistency.sh` keeps counts and fidelity from drifting via C1–C13 (the mechanical rail), and whether something gets in is always a human gate (humans steer, agents execute). +> Bringing in outside research no longer runs on vibes — it follows a pipeline frozen into a skill, [`curate-research`](.claude/skills/curate-research/SKILL.md): review is automated by parallel agents (the feedback loop), `scripts/check-consistency.sh` keeps counts and fidelity from drifting via C1–C14 (the mechanical rail), and whether something gets in is always a human gate (humans steer, agents execute). > > So the constraints themselves became the product — exactly what [concepts/07-spec-as-product.md](concepts/07-spec-as-product.md) argues, except this time the subject is the repo itself. diff --git a/README.md b/README.md index a3c0f56..3296baf 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,7 @@ harness-engineering/ ## 🛠️ 开发须知 -仓库自带一致性检查脚本 `scripts/check-consistency.sh`,守护数量与保真类漂移,覆盖十三层校验: +仓库自带一致性检查脚本 `scripts/check-consistency.sh`,守护数量与保真类漂移,覆盖十四层校验: - **C1-C2** — `references/articles.md` 文章数 + 下游 4 处引用(README × 2 badges、`prompts/deep-research-tracker.md` 头部、`references/AGENTS.md` 概览) - **C3** — `concepts/` / `thinking/` / `feedback/` 三个目录的 `*.md` 实际数与 README "X 篇" 声明一致 @@ -241,6 +241,7 @@ harness-engineering/ - **C11** — markdown 表格形状:受检文件里每行表格的单元格数须与表头一致 - **C12** — `references/articles.md` 每个编号条目必须带 **作者:** 与 **日期:** 字段 - **C13** — 零插图声明须留痕。C10 只能证伪"多报",`sourceFigureCount: 0` 在本地永远无法被证伪——2026-07-27 就是这个洞放行了一个假 0(原文实有 4 张图)。因此声明 0 的译文必须同时带 `sourceFigureAudit`,值里要有 `YYYY-MM-DD` 核对日期,写清怎么核对、结论是什么 +- **C14** — 文档站驭缰完整性:站点侧边栏与所有展示计数必须由 `.vitepress/sidebar.mjs` 构建时从文件系统派生,站点源码(`index.md`、`.vitepress/**`)不得手写计数;`node .vitepress/sidebar.mjs --verify` 断言每个一等内容页在生成的侧边栏中恰好出现一次。构建产物侧另有 `scripts/verify-dist.mjs` 断言发布页面与 `.md` 副本一一对应 **首次 clone 后启用 pre-commit hook:** @@ -248,7 +249,7 @@ harness-engineering/ git config core.hooksPath .githooks ``` -启用后,每次 commit 涉及 README、`AGENTS.md`、`references/articles.md`、`references/AGENTS.md`、`prompts/deep-research-tracker.md`、或 `concepts/` / `thinking/` / `feedback/` / `works/` 中的 `*.md` 时会自动跑检查;不涉及则不打扰。 +启用后,每次 commit 涉及 README、`AGENTS.md`、`references/articles.md`、`references/AGENTS.md`、`index.md`、`.vitepress/`、`scripts/check-consistency.sh`、或 `concepts/` / `thinking/` / `feedback/` / `works/` / `practice/` / `tools/` / `prompts/` 下(含嵌套目录)的 `*.md` 时会自动跑检查;不涉及则不打扰。 **手动跑:** `bash scripts/check-consistency.sh` @@ -260,7 +261,7 @@ git config core.hooksPath .githooks > 这个仓库开始策展自己了。 > -> 收录外部调研不再靠手感——它走一条固化成 skill 的流水线 [`curate-research`](.claude/skills/curate-research/SKILL.md):评审由并行 agent 自动完成(反馈回路),`scripts/check-consistency.sh` 的 C1–C13 守着计数与保真不漂移(机械护栏),而"收不收进来"始终是一道人类闸门(人类掌舵、智能体执行)。 +> 收录外部调研不再靠手感——它走一条固化成 skill 的流水线 [`curate-research`](.claude/skills/curate-research/SKILL.md):评审由并行 agent 自动完成(反馈回路),`scripts/check-consistency.sh` 的 C1–C14 守着计数与保真不漂移(机械护栏),而"收不收进来"始终是一道人类闸门(人类掌舵、智能体执行)。 > > 于是约束本身成了产品——正是本仓库 [concepts/07-spec-as-product.md](concepts/07-spec-as-product.md) 讲的东西,只不过这次的实验对象是仓库自己。 diff --git a/package.json b/package.json index 9978777..5497103 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "docs:dev": "vitepress dev", "docs:build": "vitepress build", "docs:preview": "vitepress preview", - "docs:verify": "node .vitepress/sidebar.mjs --verify" + "docs:verify": "node .vitepress/sidebar.mjs --verify", + "docs:verify:dist": "node scripts/verify-dist.mjs" }, "devDependencies": { "vitepress": "^1.6.4" diff --git a/public/favicon.svg b/public/favicon.svg index ebf7428..c6b5372 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1,5 +1,5 @@ - + diff --git a/scripts/verify-dist.mjs b/scripts/verify-dist.mjs new file mode 100644 index 0000000..d359b5c --- /dev/null +++ b/scripts/verify-dist.mjs @@ -0,0 +1,71 @@ +/** + * 构建产物契约校验(C14 在产物侧的延伸)—— 对 .vitepress/dist 断言三件事: + * + * 1. 页面一一对应:dist 中的 .html 页面集合与发布页面模型 + * (sidebar.mjs collectPublishedPages)完全一致。多出的页面意味着源目录 + * 泄露(例如某个本地目录忘了进 srcExclude),缺失意味着构建不完整。 + * 2. 机器可读副本:每个页面都有同路径 .md 副本(llms.txt 对外承诺的 + * 「URL + .md」契约,首页对应 /index.md)。 + * 3. 智能体出口存在:llms.txt / llms-full.txt / feed.xml / sitemap.xml。 + * + * 用法:npm run docs:build && node scripts/verify-dist.mjs + * CI 在 build 与 upload 之间调用;任何断言失败都以非零码阻断部署。 + */ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const HERE = path.dirname(fileURLToPath(import.meta.url)) +const { collectPublishedPages } = await import( + new URL('../.vitepress/sidebar.mjs', import.meta.url).href +) + +const DIST = path.resolve(HERE, '../.vitepress/dist') +if (!fs.existsSync(DIST)) { + console.error('verify-dist: .vitepress/dist not found — run `npm run docs:build` first') + process.exit(1) +} + +const problems = [] +const pages = collectPublishedPages() +const htmlOf = (p) => (p.link === '/' ? 'index.html' : `${p.link.slice(1)}.html`) +const mdOf = (p) => (p.link === '/' ? 'index.md' : `${p.link.slice(1)}.md`) + +const expected = new Set(pages.map(htmlOf)) +expected.add('404.html') + +const actual = [] +const walk = (dir) => { + for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { + const abs = path.join(dir, ent.name) + if (ent.isDirectory()) walk(abs) + else if (ent.name.endsWith('.html')) actual.push(path.relative(DIST, abs)) + } +} +walk(DIST) + +const actualSet = new Set(actual) +for (const h of actual) { + if (!expected.has(h)) problems.push(`unexpected page in dist (source leak?): ${h}`) +} +for (const h of expected) { + if (h !== '404.html' && !actualSet.has(h)) problems.push(`page missing from dist: ${h}`) +} + +for (const p of pages) { + if (!fs.existsSync(path.join(DIST, mdOf(p)))) { + problems.push(`page has no same-path markdown copy: ${p.link} (expected /${mdOf(p)})`) + } +} + +for (const f of ['llms.txt', 'llms-full.txt', 'feed.xml', 'sitemap.xml']) { + if (!fs.existsSync(path.join(DIST, f))) problems.push(`missing agent-facing output: /${f}`) +} + +if (problems.length) { + for (const p of problems) console.error(`verify-dist FAIL — ${p}`) + process.exit(1) +} +console.log( + `verify-dist: ${pages.length} published pages ↔ ${actual.length - 1} html pages (+404); every page has a same-path .md copy; llms/feed/sitemap present` +) From f7336904079dacdf31034b8bfcd3f6bcb6f4664f Mon Sep 17 00:00:00 2001 From: deusyu Date: Tue, 18 Aug 2026 10:58:29 +0800 Subject: [PATCH 08/12] =?UTF-8?q?fix:=20=E8=90=BD=E5=AE=9E=E7=AC=AC?= =?UTF-8?q?=E4=BA=8C=E8=BD=AE=20codex=20review=E2=80=94=E2=80=94symlink=20?= =?UTF-8?q?=E7=A6=81=E4=BB=A4/=E9=A6=96=E9=A1=B5=E6=9C=BA=E8=AF=BB/?= =?UTF-8?q?=E5=89=AF=E6=9C=AC=E8=87=AA=E8=B6=B3/PR=20=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E9=97=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1·安全边界: - 全仓禁止 symlink,四层防线——sidebar.mjs findForbiddenSymlinks() (lstat 遍历,悬空/嵌套目录软链均拦)、--verify 纳入 C14、config.ts 加载即拒绝(dev 与 build 同判)、verify-dist 扫 dist 产物。Vite 会 解引用 public/ 软链、图片管线会读链接目标,故一律 fail-closed P2·首页机器可读: - 首页文案抽出为 theme/home-copy.mjs 唯一事实源,HomeArchive.vue 与 buildEnd 共同消费;/index.md 副本改为生成的完整 Markdown(五个分节 + 台账 + 阅读路线,站内链接为绝对 URL),llms-full.txt 收录首页正文, HOME_TITLE 统一副本 frontmatter 与 llms-full 条目标题 P2·副本自足: - 新增 sidebar.mjs mapMarkdownLinks:行级围栏状态机(含 4+ 反引号嵌套 围栏)、行内代码保护、徽章嵌套两遍匹配、引用式定义、单双引号标题—— 构建期改写(config.ts transformCopy)与产物校验(verify-dist)共用 同一套解析,改写什么就校验什么 - 副本内图片→raw.githubusercontent、未发布资产→GitHub blob/tree、 目录→站内 README.md 副本;verify-dist 同时校验相对链接与站内绝对 链接可达(含生成式首页副本的全部链接),目标必须是 dist 内普通文件 P2·CI 缺口: - 新增 docs-build.yml:PR 上跑完整构建(死链检查随构建)+ 侧栏校验 + 产物契约,contents:read 只读、带并发取消组;构建回归不再等合并后发现 P3: - llms-full 元数据块标题 JSON.stringify(Task: 冒号标题不再是非法 YAML), 首页条目无 frontmatter 正文,消除双定界块歧义 - C14 裸计数扫描扩至整个 .vitepress/(--exclude-dir=dist/cache/.temp), 与 AGENTS.md 约定范围一致 --- .github/workflows/docs-build.yml | 42 ++++++ .vitepress/config.ts | 217 +++++++++++++++++++++---------- .vitepress/sidebar.mjs | 85 ++++++++++++ .vitepress/theme/HomeArchive.vue | 111 +++++----------- .vitepress/theme/home-copy.mjs | 149 +++++++++++++++++++++ README.en.md | 2 +- README.md | 2 +- scripts/check-consistency.sh | 16 ++- scripts/verify-dist.mjs | 60 ++++++++- 9 files changed, 527 insertions(+), 157 deletions(-) create mode 100644 .github/workflows/docs-build.yml create mode 100644 .vitepress/theme/home-copy.mjs diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml new file mode 100644 index 0000000..574a33a --- /dev/null +++ b/.github/workflows/docs-build.yml @@ -0,0 +1,42 @@ +name: docs-build + +# PR 上的只读构建门:完整 VitePress 构建(死链检查随构建执行)+ 侧栏完整性 +# + 产物契约(html ↔ .md 副本一一对应、副本自足、无 symlink)。 +# 部署仍只走 deploy-docs.yml(合并到 main 之后);本工作流不持有任何写权限。 + +on: + pull_request: + +permissions: + contents: read + +# 同一 PR 连续 push 时取消上一次未完成的构建,不浪费 runner。 +concurrency: + group: docs-build-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # gitDate/lastUpdated 需要完整历史,与部署构建保持同参 + + - name: Setup Node + uses: actions/setup-node@v5 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Verify sidebar completeness (C14) + run: npm run docs:verify + + - name: Build site + run: npm run docs:build + + - name: Verify dist contract (page ↔ markdown-copy parity) + run: npm run docs:verify:dist diff --git a/.vitepress/config.ts b/.vitepress/config.ts index ceb4d4b..b93d32b 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -11,13 +11,103 @@ import { collectPages, collectPublishedPages, computeStats, + findForbiddenSymlinks, + mapMarkdownLinks, + SITE_HOST, } from './sidebar.mjs' +// @ts-ignore — 首页文案唯一事实源(HomeArchive.vue 消费同一模块) +import { HOME_TITLE, homeMarkdown } from './theme/home-copy.mjs' -const HOST = 'https://harness.dyu.sh' +const HOST = SITE_HOST const REPO_URL = 'https://github.com/deusyu/harness-engineering' +const RAW_URL = 'https://raw.githubusercontent.com/deusyu/harness-engineering/main' const SITE_TITLE = 'Harness Engineering' const SITE_DESC = '驭缰工程中文学习档案——概念笔记、独立思考、系统性翻译与实践记录' +// 门闩:config 一被加载(dev 与 build 皆然)就拒绝任何 symlink。Vite 会解引用 +// public/ 下的软链,Markdown 图片管线会读取链接目标——参与发布的每一个字节都 +// 必须来自仓库本身。 +const forbiddenSymlinks = findForbiddenSymlinks() +if (forbiddenSymlinks.length) { + throw new Error( + `symlinks are forbidden in this repo (they can leak external files into the published site): ${forbiddenSymlinks.join(', ')}` + ) +} + +// git 跟踪集:只有跟踪中的目标才配改写成 GitHub/raw 链接——本地存在但未跟踪 +// 的文件改写过去只会得到 404。git 不可用(如脱离仓库的裸目录)时放行兜底。 +const TRACKED = (() => { + try { + const files = new Set( + execFileSync('git', ['ls-files', '-z'], { + cwd: ROOT, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + maxBuffer: 32 * 1024 * 1024, + }) + .split('\0') + .filter(Boolean) + ) + const dirs = new Set() + for (const f of files) { + let d = path.posix.dirname(f) + while (d !== '.' && !dirs.has(d)) { + dirs.add(d) + d = path.posix.dirname(d) + } + } + return { files, dirs } as { files: Set | null; dirs: Set } + } catch { + return { files: null, dirs: new Set() } + } +})() + +const isTracked = (rel: string, isDir: boolean) => + TRACKED.files === null || (isDir ? TRACKED.dirs.has(rel) : TRACKED.files.has(rel)) + +/** GitHub URL 路径段编码(CJK/特殊字符安全;现有文件名均为 ASCII,属于防御)。 */ +const encodePath = (rel: string) => rel.split('/').map(encodeURIComponent).join('/') + +const PUBLISHED_FILES = new Set(collectPublishedPages().map((p) => p.file)) + +type LinkClass = + | { kind: 'skip' } // 协议链接/锚点/站内绝对路径/无法解析 —— 原样保留 + | { kind: 'published'; rel: string; anchor: string } // 相对链接指向已发布 md + | { kind: 'dir-readme'; rel: string; anchor: string } // 目录且其 README 已发布 + | { kind: 'github'; rel: string; anchor: string; isDir: boolean } // 已跟踪的仓库资产 + | { kind: 'unknown' } // 不存在/未跟踪 —— 留给死链检查大声报错 + +/** 站点渲染与 .md 副本共用的链接判定:一个决策表,两个消费端。 */ +function classifyLink(pageRel: string, href: string): LinkClass { + if (!href || /^(?:[a-z][a-z0-9+.-]*:|\/\/|#|\/)/i.test(href)) return { kind: 'skip' } + const hash = href.indexOf('#') + const target = hash === -1 ? href : href.slice(0, hash) + const anchor = hash === -1 ? '' : href.slice(hash) + if (!target) return { kind: 'skip' } + let decoded = target + try { + decoded = decodeURI(target) + } catch { + /* 非法转义序列:按原文处理 */ + } + const rel = path.posix + .normalize(path.posix.join(path.posix.dirname(pageRel), decoded)) + .replace(/\/+$/, '') + if (!rel || rel.startsWith('..')) return { kind: 'skip' } + if (PUBLISHED_FILES.has(rel)) return { kind: 'published', rel, anchor } + let stat + try { + stat = fs.statSync(path.join(ROOT, rel)) + } catch { + return { kind: 'unknown' } + } + if (stat.isDirectory() && PUBLISHED_FILES.has(path.posix.join(rel, 'README.md'))) { + return { kind: 'dir-readme', rel, anchor } + } + if (!isTracked(rel, stat.isDirectory())) return { kind: 'unknown' } + return { kind: 'github', rel, anchor, isDir: stat.isDirectory() } +} + export default defineConfig({ lang: 'zh-CN', base: '/', @@ -44,36 +134,9 @@ export default defineConfig({ // 目录……),改写为 GitHub 链接,内容在站点与 GitHub 两个语境下都可读; // 指向的目录若有已发布的 README,则直接路由到站内该页。改写发生在 parse // 阶段,VitePress 的死链检查看到的已是改写后的链接,因此无需关闭检查。 - // (published 在渲染器创建时快照一次;dev 模式新增页面需重启才会进集合, - // 生产构建每次全新快照,不受影响。) - const published = new Set(collectPublishedPages().map((p) => p.file)) - // 只有 git 跟踪的目标才配得到 GitHub 链接——本地存在但未跟踪的文件改写 - // 过去只会得到 404;这类链接留给死链检查大声报错。git 不可用时放行兜底。 - let trackedFiles: Set | null = null - const trackedDirs = new Set() - try { - trackedFiles = new Set( - execFileSync('git', ['ls-files', '-z'], { - cwd: ROOT, - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - maxBuffer: 32 * 1024 * 1024, - }) - .split('\0') - .filter(Boolean) - ) - for (const f of trackedFiles) { - let d = path.posix.dirname(f) - while (d !== '.' && !trackedDirs.has(d)) { - trackedDirs.add(d) - d = path.posix.dirname(d) - } - } - } catch { - trackedFiles = null - } - const isTracked = (rel: string, isDir: boolean) => - trackedFiles === null || (isDir ? trackedDirs.has(rel) : trackedFiles.has(rel)) + // 判定逻辑与 .md 副本改写共用 classifyLink 一张决策表。 + // (PUBLISHED_FILES 在 config 加载时快照一次;dev 模式新增页面需重启才 + // 会进集合,生产构建每次全新快照,不受影响。) md.core.ruler.push('ha_rewrite_repo_links', (state) => { const pagePath = (state.env as { relativePath?: string })?.relativePath if (!pagePath) return @@ -82,36 +145,14 @@ export default defineConfig({ for (const token of block.children) { if (token.type !== 'link_open') continue const href = token.attrGet('href') - // 只处理相对链接;协议链接、锚点、站内绝对路径原样保留。 - if (!href || /^(?:[a-z][a-z0-9+.-]*:|\/\/|#|\/)/i.test(href)) continue - const hash = href.indexOf('#') - const target = hash === -1 ? href : href.slice(0, hash) - const anchor = hash === -1 ? '' : href.slice(hash) - if (!target) continue - let decoded = target - try { - decoded = decodeURI(target) - } catch { - /* 非法转义序列:按原文处理 */ - } - const rel = path.posix - .normalize(path.posix.join(path.posix.dirname(pagePath), decoded)) - .replace(/\/+$/, '') - if (rel.startsWith('..')) continue // 仓库外相对路径:留给死链检查报错 - if (published.has(rel)) continue // 已发布页面:VitePress 自会路由 - let stat - try { - stat = fs.statSync(path.join(ROOT, rel)) - } catch { - continue // 目标不存在:留给死链检查报错 - } - if (stat.isDirectory() && published.has(path.posix.join(rel, 'README.md'))) { - token.attrSet('href', `/${rel}/README${anchor}`) - } else if (isTracked(rel, stat.isDirectory())) { - const kind = stat.isDirectory() ? 'tree' : 'blob' - token.attrSet('href', `${REPO_URL}/${kind}/main/${rel}${anchor}`) + if (!href) continue + const c = classifyLink(pagePath, href) + if (c.kind === 'dir-readme') { + token.attrSet('href', `/${c.rel}/README${c.anchor}`) + } else if (c.kind === 'github') { + token.attrSet('href', `${REPO_URL}/${c.isDir ? 'tree' : 'blob'}/main/${encodePath(c.rel)}${c.anchor}`) } - // 未跟踪的本地目标不改写:留给死链检查报错,胜过发布一个 404 链接 + // skip/published/unknown:原样保留(unknown 留给死链检查大声报错) } } }) @@ -217,12 +258,24 @@ export default defineConfig({ const published = collectPublishedPages() // 发布全集(含首页与附属页):md 副本与全文用 const stats = computeStats() - // 1) 每个发布页面伴生同路径 Markdown 副本(首页 → /index.md)。 - // 与产物的一一对应由 scripts/verify-dist.mjs 在构建后机械校验。 + // 首页的 Markdown 版本由 home-copy.mjs(与 HomeArchive.vue 同源)生成—— + // index.md 源文件只是组件壳,直接复制对机器不可读。副本带 frontmatter; + // llms-full 用无 frontmatter 的正文(那里已有生成的元数据块)。 + const homeMd = homeMarkdown(stats, HOST) + const copyOf = (p: { file: string; link: string }) => + p.file === 'index.md' + ? `---\ntitle: ${JSON.stringify(HOME_TITLE)}\n---\n\n${homeMd}` + : transformCopy(p.file, fs.readFileSync(assertContentFile(p.file), 'utf8')) + + // 1) 每个发布页面伴生同路径 Markdown 副本(首页 → /index.md)。副本内的 + // 仓库专用链接与图片同步改写为 GitHub/raw 绝对地址,保证脱离仓库语境 + // 也能解析。与产物的一一对应由 scripts/verify-dist.mjs 在构建后机械校验。 + const copies = new Map() + for (const p of published) copies.set(p.file, copyOf(p)) for (const p of published) { const dest = path.join(out, p.link === '/' ? 'index.md' : `${p.link.slice(1)}.md`) fs.mkdirSync(path.dirname(dest), { recursive: true }) - fs.copyFileSync(assertContentFile(p.file), dest) + fs.writeFileSync(dest, copies.get(p.file)!) } // 2) llms.txt / llms-full.txt @@ -247,12 +300,15 @@ export default defineConfig({ ].join('\n') fs.writeFileSync(path.join(out, 'llms.txt'), llms) - // 全文 = 侧栏内容页(按分区顺序)+ 附属页;首页是纯组件壳,无正文可并。 - const full = [...pages, ...extras] - .map((p) => { - const raw = fs.readFileSync(assertContentFile(p.file), 'utf8') - return `\n\n---\ntitle: ${p.text}\nurl: ${HOST}${p.link}\n---\n\n${raw}` - }) + // 全文 = 首页 + 侧栏内容页(按分区顺序)+ 附属页,与 .md 副本同一份改写 + // 后内容;标题经 JSON.stringify 保证元数据块是合法 YAML。首页条目用无 + // frontmatter 的正文,避免元数据块后紧跟第二个 --- 定界块造成解析歧义。 + const fullEntries = [ + { text: HOME_TITLE, link: '/', content: homeMd }, + ...[...pages, ...extras].map((p) => ({ text: p.text, link: p.link, content: copies.get(p.file)! })), + ] + const full = fullEntries + .map((p) => `\n\n---\ntitle: ${JSON.stringify(p.text)}\nurl: ${HOST}${p.link}\n---\n\n${p.content}`) .join('') fs.writeFileSync(path.join(out, 'llms-full.txt'), `# ${SITE_TITLE} 学习档案 — 全站正文\n${full}`) @@ -291,6 +347,29 @@ export default defineConfig({ }, }) +/** + * .md 副本 / llms-full.txt 的构建期改写:链接解析共用 sidebar.mjs 的 + * mapMarkdownLinks(围栏/行内代码/嵌套徽章/引用式定义都在那里统一处理), + * 判定共用 classifyLink 决策表。 + * - 图片:已跟踪的仓库图片改写为 raw.githubusercontent 绝对地址——站点把 + * 图片打包成哈希资产,原相对路径在副本语境是死的; + * - 链接:指向已发布 md 的相对链接原样保留(副本目录结构与站点一致,依然 + * 成立);目录改写为其 README 副本;其余已跟踪资产改写为 GitHub 链接。 + */ +function transformCopy(pageRel: string, raw: string): string { + return mapMarkdownLinks(raw, (kind: 'image' | 'link', href: string) => { + const c = classifyLink(pageRel, href) + if (kind === 'image') { + return c.kind === 'github' && !c.isDir ? `${RAW_URL}/${encodePath(c.rel)}` : null + } + if (c.kind === 'dir-readme') return `${HOST}/${c.rel}/README.md${c.anchor}` + if (c.kind === 'github') { + return `${REPO_URL}/${c.isDir ? 'tree' : 'blob'}/main/${encodePath(c.rel)}${c.anchor}` + } + return null // skip/published/unknown:原样保留 + }) +} + function groupedForLlms(pages: Array<{ text: string; link: string; file: string }>): string[] { const sections = new Map() const sectionOf = (file: string) => { diff --git a/.vitepress/sidebar.mjs b/.vitepress/sidebar.mjs index f439522..c125b56 100644 --- a/.vitepress/sidebar.mjs +++ b/.vitepress/sidebar.mjs @@ -25,6 +25,9 @@ import { fileURLToPath } from 'node:url' export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const REAL_ROOT = fs.realpathSync(ROOT) +/** 站点对外域名——config.ts(页面渲染/llms 输出)与 verify-dist(副本校验)同源。 */ +export const SITE_HOST = 'https://harness.dyu.sh' + /** * 站点不发布的源文件(VitePress srcExclude 的唯一事实源,config.ts 直接 import)。 * collectPublishedFiles() 的排除规则与此保持同构;两者的最终一致性由 @@ -90,6 +93,85 @@ function subdirReadmes(dir) { return out.sort() } +/** + * 遍历 Markdown 源里「真实导航」位置的链接——行内链接、图片、引用式定义—— + * 跳过围栏代码块(含 4+ 反引号嵌套围栏)与行内代码里的语法示例。 + * fn(kind, href) 返回字符串则把该链接目标替换为返回值,返回 null 保持原样。 + * config.ts 的副本改写与 scripts/verify-dist.mjs 的副本校验共用本函数, + * 保证「改写什么」与「校验什么」永远是同一套解析,不会各自漂移。 + */ +export function mapMarkdownLinks(raw, fn) { + const IMG_RE = /!\[([^\]]*)\]\(([^)\s]+?)( +("[^"]*"|'[^']*'))?\)/g + // 链接文本允许嵌套一个图片(徽章形态 [![alt](img)](target));(? + seg + .replace(IMG_RE, (m, text, href, titlePart) => { + const r = fn('image', href) + return r == null ? m : `![${text}](${r}${titlePart ?? ''})` + }) + .replace(LINK_RE, (m, text, href, titlePart) => { + const r = fn('link', href) + return r == null ? m : `[${text}](${r}${titlePart ?? ''})` + }) + let fence = null + return raw + .split('\n') + .map((line) => { + if (fence) { + const close = line.match(/^\s*(`{3,}|~{3,})\s*$/) + if (close && close[1][0] === fence[0] && close[1].length >= fence.length) fence = null + return line + } + const open = line.match(/^\s*(`{3,}|~{3,})/) + if (open) { + fence = open[1] + return line + } + const def = line.match(DEF_RE) + if (def) { + const r = fn('link', def[2]) + return r == null ? line : `${def[1]}${r}${def[3]}` + } + // 行内代码保护:`...` 里的链接是语法示例,不是导航。 + return line + .split(/(`+[^`]*`+)/) + .map((part, i) => (i % 2 === 1 ? part : mapSeg(part))) + .join('') + }) + .join('\n') +} + +/** + * 全仓库禁止 symlink(构建产物与依赖目录除外):Vite 会解引用 public/ 下的 + * symlink,Markdown 图片管线会读取链接目标,随后整个 dist 被原样发布—— + * 一条恶意或失误的软链即可把仓库外文件带进公开站点。本仓库没有任何合法 + * symlink,因此一律拒绝,而不是逐目录白名单。 + */ +export function findForbiddenSymlinks() { + const found = [] + const SKIP = new Set(['.git', 'node_modules']) + const SKIP_REL = new Set(['.vitepress/dist', '.vitepress/cache', '.vitepress/.temp']) + const walk = (dir) => { + for (const ent of fs.readdirSync(path.join(ROOT, dir || '.'), { withFileTypes: true })) { + const rel = dir ? path.join(dir, ent.name) : ent.name + if (ent.isSymbolicLink()) { + found.push(rel) + continue + } + if (ent.isDirectory()) { + if (SKIP.has(ent.name) || SKIP_REL.has(rel)) continue + walk(rel) + } + } + } + walk('') + return found.sort() +} + /** SRC_EXCLUDE 的谓词形式:collectPublishedFiles 用它判断一个文件是否被站点排除。 */ function isExcludedFromSite(rel) { if (path.basename(rel) === 'AGENTS.md') return true @@ -298,11 +380,14 @@ export function verify() { // 侧栏页必须是发布全集的子集——srcExclude 误伤侧栏页会在这里现形。 const published = new Set(collectPublishedFiles()) const unpublished = files.filter((f) => !published.has(f)) + const symlinks = findForbiddenSymlinks() const problems = [] if (missing.length) problems.push(`missing from generated sidebar: ${missing.join(', ')}`) if (dups.length) problems.push(`duplicated in generated sidebar: ${dups.join(', ')}`) if (orphans.length) problems.push(`sidebar links to nonexistent files: ${orphans.join(', ')}`) if (unpublished.length) problems.push(`sidebar links to files excluded from the site: ${unpublished.join(', ')}`) + if (symlinks.length) + problems.push(`symlinks are forbidden in this repo (they can leak external files into the published site): ${symlinks.join(', ')}`) return { ok: problems.length === 0, problems, diff --git a/.vitepress/theme/HomeArchive.vue b/.vitepress/theme/HomeArchive.vue index 3047a51..2db8261 100644 --- a/.vitepress/theme/HomeArchive.vue +++ b/.vitepress/theme/HomeArchive.vue @@ -1,59 +1,29 @@
-

HARNESS ENGINEERING —— 学习档案 · 中文

-

人类掌舵,智能体执行

-

- 一座从概念理解到独立实践的 Harness Engineering 深度学习档案。工程师不再逐行写代码——设计约束、明确意图、构建反馈回路,让智能体可靠地交付。 -

+

{{ c.hero.kicker }}

+

{{ c.hero.titleLead }}{{ c.hero.titleEm }}

+

{{ c.hero.lede }}

从概念开始 读一手翻译 {{ data.translations }} @@ -87,37 +57,30 @@ const phases = [
-

§ 01一句话理解

- THE PARADIGM SHIFT +

§ 01{{ c.shift.title }}

+ {{ c.shift.en }}
-
- 传统工程 - 人类写代码 - - 机器执行代码 -
-
- HARNESS ENG. - 人类设计约束 - - 智能体写代码 - - 机器执行代码 +
+ {{ row.label }} +

- 核心转变:工程师的产出,从「代码」变成了「约束系统」——AGENTS.md、架构规则、自定义 linter、反馈回路。 + {{ c.shift.noteLead }}{{ c.shift.noteEm }}{{ c.shift.noteTail }}

-

§ 02六大核心概念

- SIX CORE CONCEPTS +

§ 02{{ c.conceptsSection.title }}

+ {{ c.conceptsSection.en }}
-

像档案卡一样编号归档——每一张都指向仓库里一篇可追溯的概念笔记。

+

{{ c.conceptsSection.intro }}

{{ c.num }}{{ c.tag }} @@ -131,12 +94,10 @@ const phases = [
-

§ 03档案总目

- BUILD-TIME · 自动清点 +

§ 03{{ c.ledgerSection.title }}

+ {{ c.ledgerSection.en }}
-

- 导航与下面每一个数字,都在构建时由脚本从仓库文件系统清点生成,再由 C1–C{{ data.checks }} 一致性检查守护,不随文档腐烂而漂移。这不是营销数据,是一座档案馆的总目。 -

+

{{ c.ledgerSection.intro }}

@@ -153,10 +114,10 @@ const phases = [
@@ -174,8 +135,8 @@ const phases = [
-

§ 05从哪里开始

- A READING ROUTE · 5 PHASES +

§ 05{{ c.routeSection.title }}

+ {{ c.routeSection.en }}
diff --git a/.vitepress/theme/home-copy.mjs b/.vitepress/theme/home-copy.mjs new file mode 100644 index 0000000..a29949c --- /dev/null +++ b/.vitepress/theme/home-copy.mjs @@ -0,0 +1,149 @@ +/** + * 首页文案的唯一事实源 —— HomeArchive.vue(渲染层)与 config.ts buildEnd + * (/index.md 机器可读副本 + llms-full.txt 首页正文)共同消费本模块, + * 保证「人看到的首页」与「智能体读到的首页」永不漂移。 + * + * 纪律(C14):本模块不得出现任何硬编码计数——数字一律来自入参 stats + * (computeStats() 的返回值)。纯数据模块,可被 Node 与浏览器两端打包。 + */ + +/** 首页标题——/index.md 副本 frontmatter 与 llms-full 首页条目共用,避免三处硬编码。 */ +export const HOME_TITLE = '驭缰工程 · 中文学习档案' + +export function homeCopy(stats) { + return { + hero: { + kicker: 'HARNESS ENGINEERING —— 学习档案 · 中文', + titleLead: '人类掌舵,', + titleEm: '智能体执行', + lede: '一座从概念理解到独立实践的 Harness Engineering 深度学习档案。工程师不再逐行写代码——设计约束、明确意图、构建反馈回路,让智能体可靠地交付。', + }, + shift: { + title: '一句话理解', + en: 'THE PARADIGM SHIFT', + rows: [ + { label: '传统工程', chips: ['人类写代码', '机器执行代码'], hot: false }, + { label: 'HARNESS ENG.', chips: ['人类设计约束', '智能体写代码', '机器执行代码'], hot: true }, + ], + noteLead: '核心转变:工程师的产出,从「代码」变成了', + noteEm: '「约束系统」', + noteTail: '——AGENTS.md、架构规则、自定义 linter、反馈回路。', + }, + conceptsSection: { + title: '六大核心概念', + en: 'SIX CORE CONCEPTS', + intro: '像档案卡一样编号归档——每一张都指向仓库里一篇可追溯的概念笔记。', + cards: [ + { num: '01', tag: 'REPO = RECORD', title: '仓库即记录系统', desc: '不在仓库里的东西,对智能体不存在。决策、规范、计划一律以版本化工件入库。', path: 'concepts/01', link: '/concepts/01-repo-as-source-of-truth' }, + { num: '02', tag: 'MAP, NOT MANUAL', title: '地图而非手册', desc: 'AGENTS.md 是目录页,不是百科全书。渐进式披露,从小入口点指向更深的文档。', path: 'concepts/00', link: '/concepts/00-overview' }, + { num: '03', tag: 'MECHANICAL', title: '机械化执行', desc: '文档会腐烂,lint 规则不会。自定义 linter 与结构测试,是不变量的守护者。', path: 'concepts/02', link: '/concepts/02-mechanical-enforcement' }, + { num: '04', tag: 'AGENT READABLE', title: '智能体可读性', desc: '优先为智能体的推理优化。选「无聊」的稳定技术,让应用可按 worktree 隔离启动。', path: 'concepts/04', link: '/concepts/04-agent-readability' }, + { num: '05', tag: 'THROUGHPUT', title: '吞吐量改变合并理念', desc: '纠错成本低、等待成本高。PR 生命周期很短,偶发失败靠续跑重跑解决。', path: 'concepts/05', link: '/concepts/05-throughput-changes-merge' }, + { num: '06', tag: 'ENTROPY & GC', title: '熵管理 = 垃圾回收', desc: '技术债是高息贷款。把「黄金规则」编码进仓库,后台任务定期扫描并修复偏差。', path: 'concepts/03', link: '/concepts/03-entropy-and-garbage-collection' }, + ], + }, + ledgerSection: { + title: '档案总目', + en: 'BUILD-TIME · 自动清点', + intro: `导航与下面每一个数字,都在构建时由脚本从仓库文件系统清点生成,再由 C1–C${stats.checks} 一致性检查守护,不随文档腐烂而漂移。这不是营销数据,是一座档案馆的总目。`, + rows: [ + { title: '文章索引', sub: 'references/articles.md · 深度摘要', value: stats.articles, unit: '篇', link: '/references/articles' }, + { title: '一手翻译', sub: 'works/*-translation.md', value: stats.translations, unit: '篇', link: '/works/harness-engineering-chinese-interpretation' }, + { title: '概念笔记', sub: 'concepts/', value: stats.concepts, unit: '篇', link: '/concepts/00-overview' }, + { title: '独立思考', sub: 'thinking/', value: stats.thinking, unit: '篇', link: '/thinking/why-this-project-exists' }, + { + title: '一致性检查', + sub: `scripts/check-consistency.sh · C1–C${stats.checks}`, + value: stats.checks, + unit: '项', + accent: true, + link: 'https://github.com/deusyu/harness-engineering/blob/main/scripts/check-consistency.sh', + }, + ], + }, + band: { + title: '仓库即 harness · 自我指涉', + en: 'THE ARCHIVE RUNS ON WHAT IT RECORDS', + leadLead: '这个仓库,', + leadEm: '开始策展自己了', + leadTail: '。', + pillars: [ + { tag: 'HUMAN GATE', title: '人类闸门', desc: '「收不收进来」始终是一道人类闸门。人类掌舵,决定什么值得进入档案。' }, + { tag: 'MECHANICAL RAIL', title: '机械护栏', desc: `C1–C${stats.checks} 一致性检查守着计数与保真,不让任何数字悄悄腐烂。` }, + { tag: 'FEEDBACK LOOP', title: '反馈回路', desc: '外部调研的评审,由智能体沿一条固化成 skill 的流水线自动完成。' }, + ], + noteLead: '于是约束本身成了产品——正是', + noteLinkText: '「约束即产品」', + noteLink: '/concepts/07-spec-as-product', + noteTail: '讲的东西,只不过这一次,实验的对象是仓库自己。', + }, + routeSection: { + title: '从哪里开始', + en: 'A READING ROUTE · 5 PHASES', + phases: [ + { n: '1', meta: `concepts/ · ${stats.concepts} 篇`, title: '理解核心概念', desc: '覆盖 OpenAI 六大概念,加上控制论扩展与「约束即产品」的延伸。', link: '/concepts/00-overview' }, + { n: '2', meta: `thinking/ · ${stats.thinking} 篇`, title: '形成自己的观点', desc: '质疑、延伸与跨文章洞察——把别人的范式变成自己能用的判断(持续中)。', link: '/thinking/why-this-project-exists' }, + { n: '3', meta: 'practice/ · Ralph Demo', title: '选一个小项目实践', desc: '跑通一个自主循环:321 秒 · $0.31——用最小成本亲手验证方法论。', link: '/practice/01-ralph-demo/README' }, + { n: '4', meta: `feedback/ · ${stats.feedback} 篇`, title: '记录反馈迭代', desc: '把踩坑与修正留成轨迹——「翻译即 harness」是第一篇(持续中)。', link: '/feedback/2026-04-14-translation-as-harness' }, + { n: '5', meta: `works/ · ${stats.translations} 篇翻译 + 原创`, title: '输出可展示的作品', desc: '专业一手翻译加原创综合分析——学习闭环在这里交付。', link: '/works/harness-engineering-chinese-interpretation', last: true }, + ], + }, + } +} + +/** + * 首页的 Markdown 渲染(/index.md 副本正文与 llms-full.txt 首页条目): + * 与 HomeArchive.vue 消费同一份 homeCopy,站内路由输出为 host 绝对链接, + * 让抓走这份 Markdown 的智能体拿到可直接跟进的 URL。 + * 不含 frontmatter——/index.md 副本的 frontmatter 由 buildEnd 统一加 + * (llms-full 里该内容紧跟生成的元数据块,再带 frontmatter 会形成歧义双块)。 + */ +export function homeMarkdown(stats, host) { + const c = homeCopy(stats) + const abs = (link) => (link.startsWith('/') ? `${host}${link}` : link) + const lines = [ + `# ${c.hero.titleLead}${c.hero.titleEm}`, + '', + c.hero.kicker, + '', + c.hero.lede, + '', + `## § 01 ${c.shift.title}(${c.shift.en})`, + '', + ...c.shift.rows.map((r) => `- ${r.label}:${r.chips.join(' → ')}`), + '', + `${c.shift.noteLead}${c.shift.noteEm}${c.shift.noteTail}`, + '', + `## § 02 ${c.conceptsSection.title}(${c.conceptsSection.en})`, + '', + c.conceptsSection.intro, + '', + ...c.conceptsSection.cards.map( + (card) => `- **${card.num} ${card.title}**(${card.tag}):${card.desc}([${card.path}](${abs(card.link)}))` + ), + '', + `## § 03 ${c.ledgerSection.title}(${c.ledgerSection.en})`, + '', + c.ledgerSection.intro, + '', + ...c.ledgerSection.rows.map( + (row) => `- [${row.title}](${abs(row.link)})(${row.sub}):${row.value} ${row.unit}` + ), + '', + `## § 04 ${c.band.title}(${c.band.en})`, + '', + `${c.band.leadLead}${c.band.leadEm}${c.band.leadTail}`, + '', + ...c.band.pillars.map((p) => `- **${p.title}**(${p.tag}):${p.desc}`), + '', + `${c.band.noteLead}[${c.band.noteLinkText}](${abs(c.band.noteLink)})${c.band.noteTail}`, + '', + `## § 05 ${c.routeSection.title}(${c.routeSection.en})`, + '', + ...c.routeSection.phases.map( + (p) => `${p.n}. **${p.title}**(PHASE ${p.n} · ${p.meta}):${p.desc}([阅读](${abs(p.link)}))` + ), + '', + ] + return lines.join('\n') +} diff --git a/README.en.md b/README.en.md index 0c5fc58..972aaaf 100644 --- a/README.en.md +++ b/README.en.md @@ -242,7 +242,7 @@ The repo ships with a consistency checker, `scripts/check-consistency.sh`, guard - **C11** — markdown table shape: in the checked files, every table row must carry the same cell count as its header - **C12** — every numbered entry in `references/articles.md` must carry the **作者:** and **日期:** fields - **C13** — zero-figure claims need an audit trail. C10 can only falsify OVER-claiming, so `sourceFigureCount: 0` is unfalsifiable locally — that hole shipped a false 0 on 2026-07-27 (the source had 4 body figures). Any translation claiming 0 must therefore also carry `sourceFigureAudit` containing a `YYYY-MM-DD` date, stating how the claim was verified -- **C14** — docs-site harness integrity: the VitePress sidebar and every displayed count must be derived from the filesystem at build time by `.vitepress/sidebar.mjs`; site sources (`index.md`, `.vitepress/**`) must not hardcode counts, and `node .vitepress/sidebar.mjs --verify` asserts every first-class content file appears in the generated sidebar exactly once. On the artifact side, `scripts/verify-dist.mjs` asserts published pages and their `.md` copies correspond one-to-one +- **C14** — docs-site harness integrity: the VitePress sidebar and every displayed count must be derived from the filesystem at build time by `.vitepress/sidebar.mjs`; site sources (`index.md`, `.vitepress/**`) must not hardcode counts, and `node .vitepress/sidebar.mjs --verify` asserts every first-class content file appears in the generated sidebar exactly once and rejects any symlink in the repo (symlinks can leak external files into the published artifact). On the artifact side, `scripts/verify-dist.mjs` asserts published pages and their `.md` copies correspond one-to-one, that relative links and images inside the copies resolve, and that dist contains no symlinks **Enable the pre-commit hook after first clone:** diff --git a/README.md b/README.md index 3296baf..c232fe4 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ harness-engineering/ - **C11** — markdown 表格形状:受检文件里每行表格的单元格数须与表头一致 - **C12** — `references/articles.md` 每个编号条目必须带 **作者:** 与 **日期:** 字段 - **C13** — 零插图声明须留痕。C10 只能证伪"多报",`sourceFigureCount: 0` 在本地永远无法被证伪——2026-07-27 就是这个洞放行了一个假 0(原文实有 4 张图)。因此声明 0 的译文必须同时带 `sourceFigureAudit`,值里要有 `YYYY-MM-DD` 核对日期,写清怎么核对、结论是什么 -- **C14** — 文档站驭缰完整性:站点侧边栏与所有展示计数必须由 `.vitepress/sidebar.mjs` 构建时从文件系统派生,站点源码(`index.md`、`.vitepress/**`)不得手写计数;`node .vitepress/sidebar.mjs --verify` 断言每个一等内容页在生成的侧边栏中恰好出现一次。构建产物侧另有 `scripts/verify-dist.mjs` 断言发布页面与 `.md` 副本一一对应 +- **C14** — 文档站驭缰完整性:站点侧边栏与所有展示计数必须由 `.vitepress/sidebar.mjs` 构建时从文件系统派生,站点源码(`index.md`、`.vitepress/**`)不得手写计数;`node .vitepress/sidebar.mjs --verify` 断言每个一等内容页在生成的侧边栏中恰好出现一次,并拒绝仓库内出现任何 symlink(软链会把仓库外文件带进公开产物)。构建产物侧另有 `scripts/verify-dist.mjs` 断言发布页面与 `.md` 副本一一对应、副本内相对链接与图片可达、dist 无 symlink **首次 clone 后启用 pre-commit hook:** diff --git a/scripts/check-consistency.sh b/scripts/check-consistency.sh index 37fc074..e497e56 100755 --- a/scripts/check-consistency.sh +++ b/scripts/check-consistency.sh @@ -47,13 +47,17 @@ # it already self-declares as unaudited. # C14 — docs-site harness integrity. The VitePress site must derive its # sidebar and every displayed count from the filesystem at build time -# (.vitepress/sidebar.mjs), never from hand-written config. Two +# (.vitepress/sidebar.mjs), never from hand-written config. Three # invariants: (a) site sources (index.md, .vitepress/) must not # hardcode library counts ("N 篇") — numbers belong to computeStats(); # (b) every first-class content file must appear in the generated -# sidebar exactly once (node .vitepress/sidebar.mjs --verify). SKIPs -# when the site scaffold is absent; the verify half additionally SKIPs -# when node is unavailable. +# sidebar exactly once; (c) no symlink may exist anywhere in the repo +# — Vite dereferences symlinks under public/ and the markdown image +# pipeline follows link targets, so a single symlink can leak files +# from outside the repo into the published artifact. (b) and (c) run +# via node .vitepress/sidebar.mjs --verify. SKIPs when the site +# scaffold is absent; the verify half additionally SKIPs when node is +# unavailable. # # Locale pitfall (do NOT reintroduce): bracket expressions containing multibyte # characters — e.g. [├└] or [^。] — silently break under LC_ALL=C with BSD grep: @@ -527,7 +531,9 @@ if [ ! -f .vitepress/sidebar.mjs ]; then echo " $(yellow SKIP) — no .vitepress/sidebar.mjs (docs site scaffold not present)" else c14_ok=1 - c14_hits=$(grep -rnE '[0-9]+ ?篇' index.md .vitepress/config.ts .vitepress/sidebar.mjs .vitepress/theme 2>/dev/null || true) + # 扫描整个 .vitepress/(AGENTS.md 的约定范围),构建产物与缓存除外—— + # 新增任何站点源码文件都自动落入裸计数检查,无需回来改这行。 + c14_hits=$(grep -rnE --exclude-dir=dist --exclude-dir=cache --exclude-dir=.temp '[0-9]+ ?篇' index.md .vitepress 2>/dev/null || true) if [ -n "$c14_hits" ]; then while IFS= read -r c14_hit; do [ -z "$c14_hit" ] && continue diff --git a/scripts/verify-dist.mjs b/scripts/verify-dist.mjs index d359b5c..16c918b 100644 --- a/scripts/verify-dist.mjs +++ b/scripts/verify-dist.mjs @@ -1,12 +1,15 @@ /** - * 构建产物契约校验(C14 在产物侧的延伸)—— 对 .vitepress/dist 断言三件事: + * 构建产物契约校验(C14 在产物侧的延伸)—— 对 .vitepress/dist 断言五件事: * * 1. 页面一一对应:dist 中的 .html 页面集合与发布页面模型 * (sidebar.mjs collectPublishedPages)完全一致。多出的页面意味着源目录 * 泄露(例如某个本地目录忘了进 srcExclude),缺失意味着构建不完整。 * 2. 机器可读副本:每个页面都有同路径 .md 副本(llms.txt 对外承诺的 * 「URL + .md」契约,首页对应 /index.md)。 - * 3. 智能体出口存在:llms.txt / llms-full.txt / feed.xml / sitemap.xml。 + * 3. 副本自足:.md 副本内的相对图片与相对 .md 链接必须在 dist 内可达 + * ——改写管线漏掉的断链在这里现形。 + * 4. dist 内不得存在任何 symlink(防止上传产物间接携带仓库外文件)。 + * 5. 智能体出口存在:llms.txt / llms-full.txt / feed.xml / sitemap.xml。 * * 用法:npm run docs:build && node scripts/verify-dist.mjs * CI 在 build 与 upload 之间调用;任何断言失败都以非零码阻断部署。 @@ -16,7 +19,7 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' const HERE = path.dirname(fileURLToPath(import.meta.url)) -const { collectPublishedPages } = await import( +const { collectPublishedPages, mapMarkdownLinks, SITE_HOST } = await import( new URL('../.vitepress/sidebar.mjs', import.meta.url).href ) @@ -35,15 +38,22 @@ const expected = new Set(pages.map(htmlOf)) expected.add('404.html') const actual = [] +const symlinks = [] const walk = (dir) => { for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { const abs = path.join(dir, ent.name) + if (ent.isSymbolicLink()) { + symlinks.push(path.relative(DIST, abs)) + continue + } if (ent.isDirectory()) walk(abs) else if (ent.name.endsWith('.html')) actual.push(path.relative(DIST, abs)) } } walk(DIST) +for (const s of symlinks) problems.push(`symlink in dist (would upload external content): ${s}`) + const actualSet = new Set(actual) for (const h of actual) { if (!expected.has(h)) problems.push(`unexpected page in dist (source leak?): ${h}`) @@ -52,10 +62,48 @@ for (const h of expected) { if (h !== '404.html' && !actualSet.has(h)) problems.push(`page missing from dist: ${h}`) } +// .md 副本存在性 + 自足性:副本内的相对链接/图片,以及指向本站的绝对链接 +// (生成式首页副本与目录改写都输出 host 绝对 URL),都必须在 dist 内可达。 +// 链接解析与构建期改写共用 sidebar.mjs 的 mapMarkdownLinks——围栏与行内 +// 代码里的语法示例天然不进校验,改写器看得见的链接校验器同样看得见。 for (const p of pages) { - if (!fs.existsSync(path.join(DIST, mdOf(p)))) { - problems.push(`page has no same-path markdown copy: ${p.link} (expected /${mdOf(p)})`) + const copyRel = mdOf(p) + const copyAbs = path.join(DIST, copyRel) + if (!fs.existsSync(copyAbs)) { + problems.push(`page has no same-path markdown copy: ${p.link} (expected /${copyRel})`) + continue } + mapMarkdownLinks(fs.readFileSync(copyAbs, 'utf8'), (kind, href) => { + let target + if (href.startsWith(`${SITE_HOST}/`) || href === SITE_HOST) { + target = href.slice(SITE_HOST.length + 1).split('#')[0] + } else if (href.startsWith('/')) { + target = href.slice(1).split('#')[0] + } else if (/^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(href)) { + return null // 站外链接/锚点不校验 + } else { + const t = href.split('#')[0] + if (!t) return null + target = path.posix.normalize(path.posix.join(path.posix.dirname(copyRel), t)) + } + try { + target = decodeURI(target) + } catch { + /* 非法转义:按原文校验 */ + } + // 页面路由(无扩展名)对应 .html;带扩展名的按文件本体校验;根 → index.html。 + const cand = target === '' ? 'index.html' : /\.[a-z0-9]+$/i.test(target) ? target : `${target}.html` + let st = null + try { + st = fs.statSync(path.join(DIST, cand)) + } catch { + /* missing */ + } + if (target.startsWith('..') || !st || !st.isFile()) { + problems.push(`markdown copy has unreachable ${kind}: /${copyRel} → ${href}`) + } + return null // 只校验,不改写 + }) } for (const f of ['llms.txt', 'llms-full.txt', 'feed.xml', 'sitemap.xml']) { @@ -67,5 +115,5 @@ if (problems.length) { process.exit(1) } console.log( - `verify-dist: ${pages.length} published pages ↔ ${actual.length - 1} html pages (+404); every page has a same-path .md copy; llms/feed/sitemap present` + `verify-dist: ${pages.length} published pages ↔ ${actual.length - 1} html pages (+404); every page has a self-contained same-path .md copy; no symlinks in dist; llms/feed/sitemap present` ) From 3a8f7cb780d74a837f8539542aee23d465ee62d3 Mon Sep 17 00:00:00 2001 From: deusyu Date: Tue, 18 Aug 2026 16:12:37 +0800 Subject: [PATCH 09/12] =?UTF-8?q?fix:=20=E8=90=BD=E5=AE=9E=E7=AC=AC?= =?UTF-8?q?=E4=B8=89=E8=BD=AE=20codex=20review=E2=80=94=E2=80=94symlink=20?= =?UTF-8?q?=E8=B1=81=E5=85=8D=E6=94=B6=E7=AA=84/AST=20=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C/URL=20=E7=BC=96=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standards: - P1 symlink 禁令绕过:findForbiddenSymlinks 豁免从「按目录名任意层级」 收窄为仓库根精确路径(.git、node_modules、.vitepress 构建目录)—— public/node_modules/ 里的软链不再有死角,实测被 verify/config 门闩双拦 - AGENTS.md C14 同步为三条不变量(补全仓禁 symlink + verify-dist 产物侧) - pre-commit:staged symlink(mode 120000)无条件触发检查,且 diff-filter 补 T——删普通文件后原地换软链是 typechange,ACMR/ACMRD 都看不见 Spec: - 改写器与校验器解析路径分离:verify-dist 改用 VitePress 自带渲染器的 AST 提取链接(生产同款解析器),不再与 mapMarkdownLinks 正则共享盲区; 对拍基线 171/171(修复行内代码占位实现顺带补齐了链接文本含行内代码、 尖括号目标、平衡括号目标三类形态) - 首页人机同源补全:CTA 三按钮与缰绳图七段文字入 home-copy.mjs, Vue 与 /index.md、llms-full 首页条目同源 - 对外 URL 全部路径段编码(llms.txt / llms-full url / RSS link+guid / og:url / 副本改写),RSS link/guid 补 XML 转义,括号类 sub-delims 强制 编码;verify-dist 按段 decodeURIComponent 与编码出口构成逆运算; 发布文件名含 '#'/'?' 被 C14 verify 机械禁止(VitePress 对其 fail-open) - rel 路径统一 '/' 拼接,Windows 反斜杠不再破坏排除规则 --- .githooks/pre-commit | 12 ++++- .vitepress/config.ts | 40 ++++++++++----- .vitepress/sidebar.mjs | 83 ++++++++++++++++++++++---------- .vitepress/theme/HomeArchive.vue | 22 ++++----- .vitepress/theme/home-copy.mjs | 20 +++++++- AGENTS.md | 2 +- README.en.md | 2 +- README.md | 2 +- scripts/verify-dist.mjs | 51 ++++++++++++++------ 9 files changed, 164 insertions(+), 70 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 75007ad..05441bf 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -16,8 +16,16 @@ WATCHED_REGEX='^(references/(articles|AGENTS)\.md$|README(\.en)?\.md$|AGENTS\.md # --no-renames: a rename lists only the destination path by default, letting # `git mv README.md elsewhere` slip past the watch list unseen. -staged=$(git diff --cached --no-renames --name-only --diff-filter=ACMRD) -if ! echo "$staged" | grep -qE "$WATCHED_REGEX"; then +# T (typechange) included: replacing a tracked file with a symlink in place +# stages as T, not A/M — excluding it would blind both branches below. +staged=$(git diff --cached --no-renames --name-only --diff-filter=ACMRDT) + +# Symlinks are banned repo-wide (C14 invariant c) — the watch list above is +# path-scoped, so a staged symlink (mode 120000) triggers the checks +# unconditionally, whatever its path. +staged_symlink=$(git diff --cached --no-renames --raw --diff-filter=ACMRT | awk '$2 == "120000" { print; exit }') + +if [ -z "$staged_symlink" ] && ! echo "$staged" | grep -qE "$WATCHED_REGEX"; then exit 0 fi diff --git a/.vitepress/config.ts b/.vitepress/config.ts index b93d32b..76d484b 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -65,8 +65,21 @@ const TRACKED = (() => { const isTracked = (rel: string, isDir: boolean) => TRACKED.files === null || (isDir ? TRACKED.dirs.has(rel) : TRACKED.files.has(rel)) -/** GitHub URL 路径段编码(CJK/特殊字符安全;现有文件名均为 ASCII,属于防御)。 */ -const encodePath = (rel: string) => rel.split('/').map(encodeURIComponent).join('/') +/** URL 路径段编码(CJK/空格/& 等合法文件名安全;'#'/'?' 文件名被 C14 verify 禁止)。 + * 括号等 RFC3986 sub-delims 也强制编码——它们会截断 Markdown 行内链接语法。 */ +const encodePath = (rel: string) => + rel + .split('/') + .map((seg) => + encodeURIComponent(seg).replace(/[()!'*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`) + ) + .join('/') + +/** 锚点编码:改写产出的 URL 若透传原始锚点,空格等字符会截断 Markdown 链接。 */ +const encodeAnchor = (anchor: string) => (anchor ? `#${encodeURIComponent(anchor.slice(1))}` : '') + +/** 站内页面路由 → 对外绝对 URL(llms/RSS/og 的唯一出口,统一做路径段编码)。 */ +const pageUrl = (link: string) => `${HOST}${encodePath(link)}` const PUBLISHED_FILES = new Set(collectPublishedPages().map((p) => p.file)) @@ -148,9 +161,12 @@ export default defineConfig({ if (!href) continue const c = classifyLink(pagePath, href) if (c.kind === 'dir-readme') { - token.attrSet('href', `/${c.rel}/README${c.anchor}`) + token.attrSet('href', `/${c.rel}/README${encodeAnchor(c.anchor)}`) } else if (c.kind === 'github') { - token.attrSet('href', `${REPO_URL}/${c.isDir ? 'tree' : 'blob'}/main/${encodePath(c.rel)}${c.anchor}`) + token.attrSet( + 'href', + `${REPO_URL}/${c.isDir ? 'tree' : 'blob'}/main/${encodePath(c.rel)}${encodeAnchor(c.anchor)}` + ) } // skip/published/unknown:原样保留(unknown 留给死链检查大声报错) } @@ -178,7 +194,7 @@ export default defineConfig({ pageData.frontmatter.head.push( ['meta', { property: 'og:title', content: pageData.title ? `${pageData.title} | ${SITE_TITLE}` : `${SITE_TITLE} 学习档案` }], ['meta', { property: 'og:description', content: pageData.description || SITE_DESC }], - ['meta', { property: 'og:url', content: `${HOST}/${cleanPath}` }], + ['meta', { property: 'og:url', content: pageUrl(`/${cleanPath}`) }], ['meta', { name: 'twitter:card', content: 'summary' }], ) // 构建时估算阅读时长(中文按字数计),供 DocMeta 文档页头使用。 @@ -291,7 +307,7 @@ export default defineConfig({ '', ...model, ...(extras.length - ? ['## 附属页面', '', ...extras.map((p) => `- [${p.text}](${HOST}${p.link}.md)`), ''] + ? ['## 附属页面', '', ...extras.map((p) => `- [${p.text}](${pageUrl(p.link)}.md)`), ''] : []), '## 完整内容', '', @@ -308,7 +324,7 @@ export default defineConfig({ ...[...pages, ...extras].map((p) => ({ text: p.text, link: p.link, content: copies.get(p.file)! })), ] const full = fullEntries - .map((p) => `\n\n---\ntitle: ${JSON.stringify(p.text)}\nurl: ${HOST}${p.link}\n---\n\n${p.content}`) + .map((p) => `\n\n---\ntitle: ${JSON.stringify(p.text)}\nurl: ${pageUrl(p.link)}\n---\n\n${p.content}`) .join('') fs.writeFileSync(path.join(out, 'llms-full.txt'), `# ${SITE_TITLE} 学习档案 — 全站正文\n${full}`) @@ -322,8 +338,8 @@ export default defineConfig({ [ ' ', ` ${xmlEscape(p.text)}`, - ` ${HOST}${p.link}`, - ` ${HOST}${p.link}`, + ` ${xmlEscape(pageUrl(p.link))}`, + ` ${xmlEscape(pageUrl(p.link))}`, ` ${p.date.toUTCString()}`, ' ', ].join('\n') @@ -362,9 +378,9 @@ function transformCopy(pageRel: string, raw: string): string { if (kind === 'image') { return c.kind === 'github' && !c.isDir ? `${RAW_URL}/${encodePath(c.rel)}` : null } - if (c.kind === 'dir-readme') return `${HOST}/${c.rel}/README.md${c.anchor}` + if (c.kind === 'dir-readme') return `${HOST}/${encodePath(c.rel)}/README.md${encodeAnchor(c.anchor)}` if (c.kind === 'github') { - return `${REPO_URL}/${c.isDir ? 'tree' : 'blob'}/main/${encodePath(c.rel)}${c.anchor}` + return `${REPO_URL}/${c.isDir ? 'tree' : 'blob'}/main/${encodePath(c.rel)}${encodeAnchor(c.anchor)}` } return null // skip/published/unknown:原样保留 }) @@ -389,7 +405,7 @@ function groupedForLlms(pages: Array<{ text: string; link: string; file: string for (const p of pages) { const key = sectionOf(p.file) if (!sections.has(key)) sections.set(key, []) - sections.get(key)!.push(`- [${p.text}](${HOST}${p.link}.md)`) + sections.get(key)!.push(`- [${p.text}](${pageUrl(p.link)}.md)`) } const lines: string[] = [] for (const [name, links] of sections) { diff --git a/.vitepress/sidebar.mjs b/.vitepress/sidebar.mjs index c125b56..28ea5b7 100644 --- a/.vitepress/sidebar.mjs +++ b/.vitepress/sidebar.mjs @@ -62,19 +62,20 @@ function readText(rel) { return fs.readFileSync(assertContentFile(rel), 'utf8') } -/** 列出目录下的内容 md 文件(排除 AGENTS.md 与一切非普通文件),返回仓库相对路径,按文件名排序。 */ +/** 列出目录下的内容 md 文件(排除 AGENTS.md 与一切非普通文件),返回仓库相对路径 + * (统一 `/` 分隔——排除规则与 URL 逻辑都以此为标识),按文件名排序。 */ function listMd(dir, { recursive = false } = {}) { const abs = path.join(ROOT, dir) if (!fs.existsSync(abs)) return [] const out = [] for (const ent of fs.readdirSync(abs, { withFileTypes: true })) { if (ent.isDirectory()) { - if (recursive) out.push(...listMd(path.join(dir, ent.name), { recursive })) + if (recursive) out.push(...listMd(`${dir}/${ent.name}`, { recursive })) continue } if (!ent.isFile()) continue // symlink 一律不进内容集 if (!ent.name.endsWith('.md') || ent.name === 'AGENTS.md') continue - out.push(path.join(dir, ent.name)) + out.push(`${dir}/${ent.name}`) } return out.sort() } @@ -86,7 +87,7 @@ function subdirReadmes(dir) { const out = [] for (const ent of fs.readdirSync(abs, { withFileTypes: true })) { if (!ent.isDirectory()) continue - const rel = path.join(dir, ent.name, 'README.md') + const rel = `${dir}/${ent.name}/README.md` const st = fs.lstatSync(path.join(ROOT, rel), { throwIfNoEntry: false }) if (st?.isFile()) out.push(rel) } @@ -97,24 +98,35 @@ function subdirReadmes(dir) { * 遍历 Markdown 源里「真实导航」位置的链接——行内链接、图片、引用式定义—— * 跳过围栏代码块(含 4+ 反引号嵌套围栏)与行内代码里的语法示例。 * fn(kind, href) 返回字符串则把该链接目标替换为返回值,返回 null 保持原样。 - * config.ts 的副本改写与 scripts/verify-dist.mjs 的副本校验共用本函数, - * 保证「改写什么」与「校验什么」永远是同一套解析,不会各自漂移。 + * 改写侧(config.ts transformCopy)用本函数;校验侧(scripts/verify-dist.mjs) + * 用 VitePress 自带渲染器的 AST 独立提取——两条解析路径互为对方的探测器。 + * + * 已知边界(逐行正则解析的固有限制,与生产解析器对拍基线 171/171 一致): + * 跨行行内代码、4 空格缩进代码块、打断段落的伪引用定义行会被当普通文本处理。 + * 这些形态改坏真实链接时会被 AST 校验以断链形式抓住;改坏代码示例属静默面, + * 仓库内容约定不在正文用这些形态(现存内容为零)。 */ export function mapMarkdownLinks(raw, fn) { - const IMG_RE = /!\[([^\]]*)\]\(([^)\s]+?)( +("[^"]*"|'[^']*'))?\)/g + // 目标形态:<尖括号目标>(可含空格)或普通目标(允许一层平衡括号)。 + const DEST = /(?:<([^<>\n]*)>|((?:\([^()\s]*\)|[^()\s])+?))/.source + const TITLE = /( +("[^"]*"|'[^']*'))?/.source + const IMG_RE = new RegExp(`!\\[([^\\]]*)\\]\\(${DEST}${TITLE}\\)`, 'g') // 链接文本允许嵌套一个图片(徽章形态 [![alt](img)](target));(?\n]*)>|(\S+))(.*)$/ const mapSeg = (seg) => seg - .replace(IMG_RE, (m, text, href, titlePart) => { - const r = fn('image', href) + .replace(IMG_RE, (m, text, angle, plain, titlePart) => { + const r = fn('image', angle ?? plain) return r == null ? m : `![${text}](${r}${titlePart ?? ''})` }) - .replace(LINK_RE, (m, text, href, titlePart) => { - const r = fn('link', href) + .replace(LINK_RE, (m, text, angle, plain, titlePart) => { + const r = fn('link', angle ?? plain) return r == null ? m : `[${text}](${r}${titlePart ?? ''})` }) let fence = null @@ -131,16 +143,23 @@ export function mapMarkdownLinks(raw, fn) { fence = open[1] return line } - const def = line.match(DEF_RE) + // 行内代码先挖空成占位符:`...` 里的链接是语法示例、不参与解析, + // 但链接文本里的行内代码([\`file.md\`](path) 形态)不能阻断链接识别, + // 所以不能按代码段切开整行——挖空后整行统一匹配,最后回填。 + // 行内已含 NUL 的病态输入(文本文件不该有)直接跳过挖空,防止占位符冲突。 + const codes = [] + const masked = line.includes('\x00') + ? line + : line.replace(/`+[^`\n]*`+/g, (m) => `\x00${codes.push(m) - 1}\x00`) + const def = masked.match(DEF_RE) + let mapped if (def) { - const r = fn('link', def[2]) - return r == null ? line : `${def[1]}${r}${def[3]}` + const r = fn('link', def[2] ?? def[3]) + mapped = r == null ? masked : `${def[1]}${r}${def[4]}` + } else { + mapped = mapSeg(masked) } - // 行内代码保护:`...` 里的链接是语法示例,不是导航。 - return line - .split(/(`+[^`]*`+)/) - .map((part, i) => (i % 2 === 1 ? part : mapSeg(part))) - .join('') + return mapped.replace(/\x00(\d+)\x00/g, (_, i) => codes[+i]) }) .join('\n') } @@ -153,17 +172,24 @@ export function mapMarkdownLinks(raw, fn) { */ export function findForbiddenSymlinks() { const found = [] - const SKIP = new Set(['.git', 'node_modules']) - const SKIP_REL = new Set(['.vitepress/dist', '.vitepress/cache', '.vitepress/.temp']) + // 豁免只认精确的仓库根路径——按目录名任意层级豁免会留出 + // public/node_modules/ 这类被 Vite 原样复制、却躲过扫描的死角。 + const SKIP_REL = new Set([ + '.git', + 'node_modules', + '.vitepress/dist', + '.vitepress/cache', + '.vitepress/.temp', + ]) const walk = (dir) => { for (const ent of fs.readdirSync(path.join(ROOT, dir || '.'), { withFileTypes: true })) { - const rel = dir ? path.join(dir, ent.name) : ent.name + const rel = dir ? `${dir}/${ent.name}` : ent.name if (ent.isSymbolicLink()) { found.push(rel) continue } if (ent.isDirectory()) { - if (SKIP.has(ent.name) || SKIP_REL.has(rel)) continue + if (SKIP_REL.has(rel)) continue walk(rel) } } @@ -189,7 +215,7 @@ export function collectPublishedFiles() { const out = [] const walk = (dir) => { for (const ent of fs.readdirSync(path.join(ROOT, dir || '.'), { withFileTypes: true })) { - const rel = dir ? path.join(dir, ent.name) : ent.name + const rel = dir ? `${dir}/${ent.name}` : ent.name if (ent.isDirectory()) { // 与 VitePress 源扫描的忽略规则对齐:点目录、node_modules、任意层级的 dist if (ent.name.startsWith('.') || ent.name === 'node_modules' || ent.name === 'dist') continue @@ -381,7 +407,12 @@ export function verify() { const published = new Set(collectPublishedFiles()) const unpublished = files.filter((f) => !published.has(f)) const symlinks = findForbiddenSymlinks() + // '#'/'?' 在 URL 中是定界符:VitePress 对这类文件名会静默产出 NotFound 壳页 + //(fail-open),对外出口的 URL 也无法与文件一一对应,机械禁止。 + const badNames = [...published].filter((f) => /[#?]/.test(f)) const problems = [] + if (badNames.length) + problems.push(`publishable filenames must not contain '#' or '?': ${badNames.join(', ')}`) if (missing.length) problems.push(`missing from generated sidebar: ${missing.join(', ')}`) if (dups.length) problems.push(`duplicated in generated sidebar: ${dups.join(', ')}`) if (orphans.length) problems.push(`sidebar links to nonexistent files: ${orphans.join(', ')}`) diff --git a/.vitepress/theme/HomeArchive.vue b/.vitepress/theme/HomeArchive.vue index 2db8261..f4fd51f 100644 --- a/.vitepress/theme/HomeArchive.vue +++ b/.vitepress/theme/HomeArchive.vue @@ -25,32 +25,30 @@ const phases = c.routeSection.phases

{{ c.hero.titleLead }}{{ c.hero.titleEm }}

{{ c.hero.lede }}

- - + + - 人类 - 设计约束 · 握住缰绳 + {{ c.hero.rein.start.title }} + {{ c.hero.rein.start.sub }} - AGENTS.md + {{ c.hero.rein.milestones[0] }} - 自定义 linter + {{ c.hero.rein.milestones[1] }} - CI 反馈回路 + {{ c.hero.rein.milestones[2] }} - 智能体 - 如约交付 + {{ c.hero.rein.end.title }} + {{ c.hero.rein.end.sub }}
diff --git a/.vitepress/theme/home-copy.mjs b/.vitepress/theme/home-copy.mjs index a29949c..3626f5d 100644 --- a/.vitepress/theme/home-copy.mjs +++ b/.vitepress/theme/home-copy.mjs @@ -17,6 +17,18 @@ export function homeCopy(stats) { titleLead: '人类掌舵,', titleEm: '智能体执行', lede: '一座从概念理解到独立实践的 Harness Engineering 深度学习档案。工程师不再逐行写代码——设计约束、明确意图、构建反馈回路,让智能体可靠地交付。', + actions: [ + { text: '从概念开始', link: '/concepts/00-overview' }, + { text: '读一手翻译', link: '/works/harness-engineering-chinese-interpretation', sup: stats.translations }, + { text: '浏览档案总目', link: '#ledger' }, + ], + // 语义化缰绳图:从人类的约束设计到智能体的如约交付 + rein: { + aria: '从人类设计约束到智能体如约交付的路径', + start: { title: '人类', sub: '设计约束 · 握住缰绳' }, + milestones: ['AGENTS.md', '自定义 linter', 'CI 反馈回路'], + end: { title: '智能体', sub: '如约交付' }, + }, }, shift: { title: '一句话理解', @@ -100,7 +112,9 @@ export function homeCopy(stats) { */ export function homeMarkdown(stats, host) { const c = homeCopy(stats) - const abs = (link) => (link.startsWith('/') ? `${host}${link}` : link) + const abs = (link) => + link.startsWith('/') ? `${host}${link}` : link.startsWith('#') ? `${host}/${link}` : link + const rein = c.hero.rein const lines = [ `# ${c.hero.titleLead}${c.hero.titleEm}`, '', @@ -108,6 +122,10 @@ export function homeMarkdown(stats, host) { '', c.hero.lede, '', + ...c.hero.actions.map((a) => `- [${a.text}${a.sup != null ? `(${a.sup} 篇)` : ''}](${abs(a.link)})`), + '', + `${rein.aria}:${rein.start.title}(${rein.start.sub})→ ${rein.milestones.join(' → ')} → ${rein.end.title}(${rein.end.sub})`, + '', `## § 01 ${c.shift.title}(${c.shift.en})`, '', ...c.shift.rows.map((r) => `- ${r.label}:${r.chips.join(' → ')}`), diff --git a/AGENTS.md b/AGENTS.md index 095b9a9..83631ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,7 @@ - **C11** — markdown 表格形状:README ×2、`references/AGENTS.md`、`references/articles.md`、`works/AGENTS.md` 里每一行表格的单元格数须与表头一致 - **C12** — 条目字段完整性:`references/articles.md` 每个 `### N.` 编号条目必须带 **作者:** 与 **日期:** 字段 - **C13** — 零插图声明须留痕:C10 只能证伪"多报"(嵌图数 < 声明数才 FAIL),因此 `sourceFigureCount: 0` 在本地**永远无法被证伪**——不管你有没有真去核对原文,它都是绿的。2026-07-27 就是这个洞放行了一个假 0(原文实有 4 张配图)。C10 刻意零网络、无法回查原文,所以改为要求留痕:**声明 0 的译文必须同时带 `sourceFigureAudit` 字段,值里要有 `YYYY-MM-DD` 核对日期**,写清怎么核对的、结论是什么。`null` 仍然 SKIP——它本来就自陈未审计 -- **C14** — 文档站防漂移:VitePress 站点(部署到 harness.dyu.sh)的侧边栏与首页统计一律由 `.vitepress/sidebar.mjs` 在构建时从文件系统生成,不手写。两条不变量:站点源码(`index.md`、`.vitepress/`)不得出现裸计数("N 篇"),数字只能来自 `computeStats()`;每个一等内容文件必须在生成的侧边栏中恰好出现一次(`node .vitepress/sidebar.mjs --verify`)。站点脚手架不存在时 SKIP;node 不可用时 verify 半边 SKIP、裸计数 grep 照常执行 +- **C14** — 文档站防漂移:VitePress 站点(部署到 harness.dyu.sh)的侧边栏与首页统计一律由 `.vitepress/sidebar.mjs` 在构建时从文件系统生成,不手写。三条不变量:站点源码(`index.md`、`.vitepress/`)不得出现裸计数("N 篇"),数字只能来自 `computeStats()`;每个一等内容文件必须在生成的侧边栏中恰好出现一次;仓库内禁止任何 symlink——Vite 会解引用 `public/` 下的软链、图片管线会读取链接目标,一条软链即可把仓库外文件带进公开产物(后两条经 `node .vitepress/sidebar.mjs --verify` 执行)。构建产物侧另有 `scripts/verify-dist.mjs` 断言页面与 `.md` 副本一一对应、副本链接可达、dist 无 symlink。站点脚手架不存在时 SKIP;node 不可用时 verify 半边 SKIP、裸计数 grep 照常执行 执行:`bash scripts/check-consistency.sh`(仓库根目录) 启用 pre-commit 阻断:`git config core.hooksPath .githooks` diff --git a/README.en.md b/README.en.md index 972aaaf..7b20649 100644 --- a/README.en.md +++ b/README.en.md @@ -250,7 +250,7 @@ The repo ships with a consistency checker, `scripts/check-consistency.sh`, guard git config core.hooksPath .githooks ``` -Once enabled, every commit touching the README, `AGENTS.md`, `references/articles.md`, `references/AGENTS.md`, `index.md`, `.vitepress/`, `scripts/check-consistency.sh`, or any `*.md` (nested included) under `concepts/` / `thinking/` / `feedback/` / `works/` / `practice/` / `tools/` / `prompts/` runs the checks automatically; unrelated commits are left alone. +Once enabled, every commit touching the README, `AGENTS.md`, `references/articles.md`, `references/AGENTS.md`, `index.md`, `.vitepress/`, `scripts/check-consistency.sh`, or any `*.md` (nested included) under `concepts/` / `thinking/` / `feedback/` / `works/` / `practice/` / `tools/` / `prompts/` runs the checks automatically; staging a symlink at any path also triggers them unconditionally (the repo-wide C14 ban). Unrelated commits are left alone. **Run manually:** `bash scripts/check-consistency.sh` diff --git a/README.md b/README.md index c232fe4..837a41d 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,7 @@ harness-engineering/ git config core.hooksPath .githooks ``` -启用后,每次 commit 涉及 README、`AGENTS.md`、`references/articles.md`、`references/AGENTS.md`、`index.md`、`.vitepress/`、`scripts/check-consistency.sh`、或 `concepts/` / `thinking/` / `feedback/` / `works/` / `practice/` / `tools/` / `prompts/` 下(含嵌套目录)的 `*.md` 时会自动跑检查;不涉及则不打扰。 +启用后,每次 commit 涉及 README、`AGENTS.md`、`references/articles.md`、`references/AGENTS.md`、`index.md`、`.vitepress/`、`scripts/check-consistency.sh`、或 `concepts/` / `thinking/` / `feedback/` / `works/` / `practice/` / `tools/` / `prompts/` 下(含嵌套目录)的 `*.md` 时会自动跑检查;此外任何路径下 stage 了 symlink 都会无条件触发(C14 全仓禁令)。不涉及则不打扰。 **手动跑:** `bash scripts/check-consistency.sh` diff --git a/scripts/verify-dist.mjs b/scripts/verify-dist.mjs index 16c918b..342323c 100644 --- a/scripts/verify-dist.mjs +++ b/scripts/verify-dist.mjs @@ -19,9 +19,13 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' const HERE = path.dirname(fileURLToPath(import.meta.url)) -const { collectPublishedPages, mapMarkdownLinks, SITE_HOST } = await import( +const { collectPublishedPages, SITE_HOST } = await import( new URL('../.vitepress/sidebar.mjs', import.meta.url).href ) +// 独立解析路径:校验器用 VitePress 自带的 markdown 渲染器(生产站点同款)提取 +// 副本里的链接,与构建期改写器(sidebar.mjs mapMarkdownLinks 的正则)不共享 +// 实现——改写器的解析盲区会在这里以断链形式现形,而不是被同一套盲区放行。 +const { createMarkdownRenderer } = await import('vitepress') const DIST = path.resolve(HERE, '../.vitepress/dist') if (!fs.existsSync(DIST)) { @@ -64,8 +68,20 @@ for (const h of expected) { // .md 副本存在性 + 自足性:副本内的相对链接/图片,以及指向本站的绝对链接 // (生成式首页副本与目录改写都输出 host 绝对 URL),都必须在 dist 内可达。 -// 链接解析与构建期改写共用 sidebar.mjs 的 mapMarkdownLinks——围栏与行内 -// 代码里的语法示例天然不进校验,改写器看得见的链接校验器同样看得见。 +// 围栏与行内代码里的语法示例由 markdown 解析天然排除在校验之外。 +const MD = await createMarkdownRenderer(path.resolve(HERE, '..')) +const extractLinks = (raw) => { + const found = [] + const walk = (tokens) => { + for (const t of tokens) { + if (t.type === 'link_open') found.push({ kind: 'link', href: t.attrGet('href') }) + if (t.type === 'image') found.push({ kind: 'image', href: t.attrGet('src') }) + if (t.children) walk(t.children) + } + } + walk(MD.parse(raw, {})) + return found +} for (const p of pages) { const copyRel = mdOf(p) const copyAbs = path.join(DIST, copyRel) @@ -73,24 +89,32 @@ for (const p of pages) { problems.push(`page has no same-path markdown copy: ${p.link} (expected /${copyRel})`) continue } - mapMarkdownLinks(fs.readFileSync(copyAbs, 'utf8'), (kind, href) => { + for (const { kind, href } of extractLinks(fs.readFileSync(copyAbs, 'utf8'))) { let target if (href.startsWith(`${SITE_HOST}/`) || href === SITE_HOST) { target = href.slice(SITE_HOST.length + 1).split('#')[0] } else if (href.startsWith('/')) { target = href.slice(1).split('#')[0] - } else if (/^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(href)) { - return null // 站外链接/锚点不校验 + } else if (!href || /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(href)) { + continue // 站外链接/锚点不校验 } else { const t = href.split('#')[0] - if (!t) return null + if (!t) continue target = path.posix.normalize(path.posix.join(path.posix.dirname(copyRel), t)) } - try { - target = decodeURI(target) - } catch { - /* 非法转义:按原文校验 */ - } + // 按路径段 decodeURIComponent——与编码出口(config.ts encodePath 的 + // encodeURIComponent)互为逆运算;decodeURI 不解码 %26 等保留字符,会把 + // 含 & 等合法文件名误报为 unreachable。 + target = target + .split('/') + .map((seg) => { + try { + return decodeURIComponent(seg) + } catch { + return seg // 非法转义:按原文校验 + } + }) + .join('/') // 页面路由(无扩展名)对应 .html;带扩展名的按文件本体校验;根 → index.html。 const cand = target === '' ? 'index.html' : /\.[a-z0-9]+$/i.test(target) ? target : `${target}.html` let st = null @@ -102,8 +126,7 @@ for (const p of pages) { if (target.startsWith('..') || !st || !st.isFile()) { problems.push(`markdown copy has unreachable ${kind}: /${copyRel} → ${href}`) } - return null // 只校验,不改写 - }) + } } for (const f of ['llms.txt', 'llms-full.txt', 'feed.xml', 'sitemap.xml']) { From ecf35e9b00143995335db496c1af1cc8c4d9a646 Mon Sep 17 00:00:00 2001 From: deusyu Date: Tue, 18 Aug 2026 16:43:14 +0800 Subject: [PATCH 10/12] =?UTF-8?q?fix:=20=E8=90=BD=E5=AE=9E=20gpt-5.6-sol?= =?UTF-8?q?=20codex=20review=E2=80=94=E2=80=94llms-full=20=E7=BB=9D?= =?UTF-8?q?=E5=AF=B9=E5=8C=96=20+=20=E6=A0=A1=E9=AA=8C=E7=9B=B2=E5=8C=BA?= =?UTF-8?q?=E8=A1=A5=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [P1] llms-full.txt 是无所在目录的合并文档,复用同路径副本的相对链接 全数失效(68/71 断)。现改用 absolute 模式重新改写:已发布页相对 链接 → 各页 .md 副本绝对 URL,根相对路由补 host;verify-dist 新增 llms-full 校验——不允许任何相对链接,站内绝对链接逐一在 dist 验证 [P2] - llms-full 条目剥离源 frontmatter,消除生成元数据块后紧跟第二个 --- 定界块的解析歧义(此前只对首页条目处理过,译文页同样存在) - 引用式定义按目标扩展名判定图片:![alt][img] 的定义不再被改写成 GitHub blob 页面地址,而是 raw 图片地址 - 裸 HTML 标签(/)进入校验面:改写器看不见它们, 校验器现在扫 html_inline/html_block token,两侧不再同盲 - query string 与文件路径分离解析(classifyLink 与 verify-dist 一致), articles.md?x=1#s 不再被拼成 articles.md?x=1.html 误判 - pre-commit 对 staged symlink 从「触发全量检查」改为「按暂存区状态 直接拒绝」——工作树可在 stage 后换回普通文件(TOCTOU),暂存区状态 才是将被提交的状态 --- .githooks/pre-commit | 17 +++++++---- .vitepress/config.ts | 50 +++++++++++++++++++++++++------- .vitepress/sidebar.mjs | 4 ++- README.en.md | 2 +- README.md | 2 +- scripts/verify-dist.mjs | 64 ++++++++++++++++++++++++++++++++--------- 6 files changed, 106 insertions(+), 33 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 05441bf..d8eddb8 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -20,12 +20,19 @@ WATCHED_REGEX='^(references/(articles|AGENTS)\.md$|README(\.en)?\.md$|AGENTS\.md # stages as T, not A/M — excluding it would blind both branches below. staged=$(git diff --cached --no-renames --name-only --diff-filter=ACMRDT) -# Symlinks are banned repo-wide (C14 invariant c) — the watch list above is -# path-scoped, so a staged symlink (mode 120000) triggers the checks -# unconditionally, whatever its path. -staged_symlink=$(git diff --cached --no-renames --raw --diff-filter=ACMRT | awk '$2 == "120000" { print; exit }') +# Symlinks are banned repo-wide (C14 invariant c). Reject a staged symlink +# directly from the INDEX — running the full checks instead would scan the +# working tree, which can be swapped back to a regular file after staging +# (TOCTOU): the staged state is what gets committed, so it is what we judge. +staged_symlinks=$(git diff --cached --no-renames --raw --diff-filter=ACMRT | awk -F'\t' '$1 ~ / 120000 / { print $2 }') +if [ -n "$staged_symlinks" ]; then + echo "pre-commit blocked: symlinks are forbidden in this repo (C14) — staged:" + echo "$staged_symlinks" | sed 's/^/ /' + echo "Vite dereferences symlinks into the published site; commit the real file instead." + exit 1 +fi -if [ -z "$staged_symlink" ] && ! echo "$staged" | grep -qE "$WATCHED_REGEX"; then +if ! echo "$staged" | grep -qE "$WATCHED_REGEX"; then exit 0 fi diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 76d484b..7f5d4e5 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -94,8 +94,11 @@ type LinkClass = function classifyLink(pageRel: string, href: string): LinkClass { if (!href || /^(?:[a-z][a-z0-9+.-]*:|\/\/|#|\/)/i.test(href)) return { kind: 'skip' } const hash = href.indexOf('#') - const target = hash === -1 ? href : href.slice(0, hash) const anchor = hash === -1 ? '' : href.slice(hash) + let target = hash === -1 ? href : href.slice(0, hash) + // query string 不是文件路径的一部分(仓库文件对 query 无语义,解析时剥离)。 + const q = target.indexOf('?') + if (q !== -1) target = target.slice(0, q) if (!target) return { kind: 'skip' } let decoded = target try { @@ -316,12 +319,20 @@ export default defineConfig({ ].join('\n') fs.writeFileSync(path.join(out, 'llms.txt'), llms) - // 全文 = 首页 + 侧栏内容页(按分区顺序)+ 附属页,与 .md 副本同一份改写 - // 后内容;标题经 JSON.stringify 保证元数据块是合法 YAML。首页条目用无 - // frontmatter 的正文,避免元数据块后紧跟第二个 --- 定界块造成解析歧义。 + // 全文 = 首页 + 侧栏内容页(按分区顺序)+ 附属页。合并文档没有「所在 + // 目录」,因此不能复用同路径副本的内容:改用 absolute 模式重新改写 + //(相对链接 → 各页 .md 副本的绝对 URL),并剥离源 frontmatter——生成的 + // 元数据块后紧跟第二个 --- 块会让按块解析的消费端产生歧义。标题经 + // JSON.stringify 保证元数据块是合法 YAML。 const fullEntries = [ { text: HOME_TITLE, link: '/', content: homeMd }, - ...[...pages, ...extras].map((p) => ({ text: p.text, link: p.link, content: copies.get(p.file)! })), + ...[...pages, ...extras].map((p) => ({ + text: p.text, + link: p.link, + content: stripFrontmatter( + transformCopy(p.file, fs.readFileSync(assertContentFile(p.file), 'utf8'), true) + ), + })), ] const full = fullEntries .map((p) => `\n\n---\ntitle: ${JSON.stringify(p.text)}\nurl: ${pageUrl(p.link)}\n---\n\n${p.content}`) @@ -363,29 +374,46 @@ export default defineConfig({ }, }) +const IMAGE_EXT_RE = /\.(png|jpe?g|gif|svg|webp|avif)$/i + /** * .md 副本 / llms-full.txt 的构建期改写:链接解析共用 sidebar.mjs 的 * mapMarkdownLinks(围栏/行内代码/嵌套徽章/引用式定义都在那里统一处理), * 判定共用 classifyLink 决策表。 * - 图片:已跟踪的仓库图片改写为 raw.githubusercontent 绝对地址——站点把 - * 图片打包成哈希资产,原相对路径在副本语境是死的; - * - 链接:指向已发布 md 的相对链接原样保留(副本目录结构与站点一致,依然 - * 成立);目录改写为其 README 副本;其余已跟踪资产改写为 GitHub 链接。 + * 图片打包成哈希资产,原相对路径在副本语境是死的;引用式定义按目标扩展名 + * 判定是否图片(定义处看不到使用侧语法); + * - 链接:指向已发布 md 的相对链接在同路径副本里原样保留(副本目录结构与 + * 站点一致,依然成立);目录改写为其 README 副本;其余已跟踪资产改写为 + * GitHub 链接。 + * - absolute 模式(llms-full.txt 专用):合并文档没有「所在目录」,任何 + * 相对/根相对链接都无从解析——已发布页改写为其 .md 副本的绝对 URL, + * 根相对路由补上 host。 */ -function transformCopy(pageRel: string, raw: string): string { - return mapMarkdownLinks(raw, (kind: 'image' | 'link', href: string) => { +function transformCopy(pageRel: string, raw: string, absolute = false): string { + return mapMarkdownLinks(raw, (kind: 'image' | 'link' | 'def', href: string) => { const c = classifyLink(pageRel, href) - if (kind === 'image') { + const asImage = kind === 'image' || (kind === 'def' && c.kind === 'github' && IMAGE_EXT_RE.test(c.rel)) + if (asImage) { return c.kind === 'github' && !c.isDir ? `${RAW_URL}/${encodePath(c.rel)}` : null } if (c.kind === 'dir-readme') return `${HOST}/${encodePath(c.rel)}/README.md${encodeAnchor(c.anchor)}` if (c.kind === 'github') { return `${REPO_URL}/${c.isDir ? 'tree' : 'blob'}/main/${encodePath(c.rel)}${encodeAnchor(c.anchor)}` } + if (absolute) { + if (c.kind === 'published') { + return `${pageUrl(`/${c.rel.replace(/\.md$/, '')}`)}.md${encodeAnchor(c.anchor)}` + } + if (/^\/[^/]/.test(href)) return `${HOST}${href}` // 根相对路由补 host + } return null // skip/published/unknown:原样保留 }) } +/** 剥离源 frontmatter(llms-full 专用——生成的元数据块后再跟一个 --- 块会产生解析歧义)。 */ +const stripFrontmatter = (raw: string) => raw.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '') + function groupedForLlms(pages: Array<{ text: string; link: string; file: string }>): string[] { const sections = new Map() const sectionOf = (file: string) => { diff --git a/.vitepress/sidebar.mjs b/.vitepress/sidebar.mjs index 28ea5b7..0a7b7a5 100644 --- a/.vitepress/sidebar.mjs +++ b/.vitepress/sidebar.mjs @@ -154,7 +154,9 @@ export function mapMarkdownLinks(raw, fn) { const def = masked.match(DEF_RE) let mapped if (def) { - const r = fn('link', def[2] ?? def[3]) + // 定义处看不到使用侧是链接还是图片,交给消费端按 'def' 自行判定 + //(config.ts 按目标扩展名区分 raw 图片地址与 GitHub 页面地址)。 + const r = fn('def', def[2] ?? def[3]) mapped = r == null ? masked : `${def[1]}${r}${def[4]}` } else { mapped = mapSeg(masked) diff --git a/README.en.md b/README.en.md index 7b20649..6355fdd 100644 --- a/README.en.md +++ b/README.en.md @@ -250,7 +250,7 @@ The repo ships with a consistency checker, `scripts/check-consistency.sh`, guard git config core.hooksPath .githooks ``` -Once enabled, every commit touching the README, `AGENTS.md`, `references/articles.md`, `references/AGENTS.md`, `index.md`, `.vitepress/`, `scripts/check-consistency.sh`, or any `*.md` (nested included) under `concepts/` / `thinking/` / `feedback/` / `works/` / `practice/` / `tools/` / `prompts/` runs the checks automatically; staging a symlink at any path also triggers them unconditionally (the repo-wide C14 ban). Unrelated commits are left alone. +Once enabled, every commit touching the README, `AGENTS.md`, `references/articles.md`, `references/AGENTS.md`, `index.md`, `.vitepress/`, `scripts/check-consistency.sh`, or any `*.md` (nested included) under `concepts/` / `thinking/` / `feedback/` / `works/` / `practice/` / `tools/` / `prompts/` runs the checks automatically; staging a symlink at any path is rejected outright (the repo-wide C14 ban, judged on the staged state). Unrelated commits are left alone. **Run manually:** `bash scripts/check-consistency.sh` diff --git a/README.md b/README.md index 837a41d..6d55270 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,7 @@ harness-engineering/ git config core.hooksPath .githooks ``` -启用后,每次 commit 涉及 README、`AGENTS.md`、`references/articles.md`、`references/AGENTS.md`、`index.md`、`.vitepress/`、`scripts/check-consistency.sh`、或 `concepts/` / `thinking/` / `feedback/` / `works/` / `practice/` / `tools/` / `prompts/` 下(含嵌套目录)的 `*.md` 时会自动跑检查;此外任何路径下 stage 了 symlink 都会无条件触发(C14 全仓禁令)。不涉及则不打扰。 +启用后,每次 commit 涉及 README、`AGENTS.md`、`references/articles.md`、`references/AGENTS.md`、`index.md`、`.vitepress/`、`scripts/check-consistency.sh`、或 `concepts/` / `thinking/` / `feedback/` / `works/` / `practice/` / `tools/` / `prompts/` 下(含嵌套目录)的 `*.md` 时会自动跑检查;此外任何路径下 stage 了 symlink 会被直接拒绝提交(C14 全仓禁令,按暂存区状态判定)。不涉及则不打扰。 **手动跑:** `bash scripts/check-consistency.sh` diff --git a/scripts/verify-dist.mjs b/scripts/verify-dist.mjs index 342323c..03ef6e1 100644 --- a/scripts/verify-dist.mjs +++ b/scripts/verify-dist.mjs @@ -76,12 +76,43 @@ const extractLinks = (raw) => { for (const t of tokens) { if (t.type === 'link_open') found.push({ kind: 'link', href: t.attrGet('href') }) if (t.type === 'image') found.push({ kind: 'image', href: t.attrGet('src') }) + // 裸 HTML 标签里的 src/href 不产出 link/image token——改写器也看不见 + // 它们,必须在这里补上校验面,否则两侧同盲。 + if ((t.type === 'html_inline' || t.type === 'html_block') && t.content) { + for (const m of t.content.matchAll(/(?:src|href)\s*=\s*["']([^"']+)["']/gi)) { + found.push({ kind: 'html', href: m[1] }) + } + } if (t.children) walk(t.children) } } walk(MD.parse(raw, {})) return found } + +/** + * href 分类:{ scope: 'external' } 不校验;{ scope: 'site', target } 在 dist 内 + * 校验;{ scope: 'relative', target } 相对链接(target 相对 baseDir 已解析, + * baseDir 为 null 表示无所在目录的语境——llms-full——此时相对链接本身即违规)。 + * query 与锚点都不是文件路径的一部分,解析前剥离。 + */ +const classifyHref = (baseDir, href) => { + if (!href) return { scope: 'external' } + const strip = (s) => s.split('#')[0].split('?')[0] + if (href.startsWith(`${SITE_HOST}/`) || href === SITE_HOST) { + return { scope: 'site', target: strip(href.slice(SITE_HOST.length + 1)) } + } + if (href.startsWith('/') && !href.startsWith('//')) { + return { scope: 'site', target: strip(href.slice(1)) } + } + if (/^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(href)) return { scope: 'external' } + const t = strip(href) + if (!t) return { scope: 'external' } + return { + scope: 'relative', + target: baseDir === null ? null : path.posix.normalize(path.posix.join(baseDir, t)), + } +} for (const p of pages) { const copyRel = mdOf(p) const copyAbs = path.join(DIST, copyRel) @@ -89,23 +120,28 @@ for (const p of pages) { problems.push(`page has no same-path markdown copy: ${p.link} (expected /${copyRel})`) continue } - for (const { kind, href } of extractLinks(fs.readFileSync(copyAbs, 'utf8'))) { - let target - if (href.startsWith(`${SITE_HOST}/`) || href === SITE_HOST) { - target = href.slice(SITE_HOST.length + 1).split('#')[0] - } else if (href.startsWith('/')) { - target = href.slice(1).split('#')[0] - } else if (!href || /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(href)) { - continue // 站外链接/锚点不校验 - } else { - const t = href.split('#')[0] - if (!t) continue - target = path.posix.normalize(path.posix.join(path.posix.dirname(copyRel), t)) + checkLinks(fs.readFileSync(copyAbs, 'utf8'), path.posix.dirname(copyRel), `/${copyRel}`) +} + +// llms-full.txt:合并文档没有所在目录,其中不允许任何相对链接(baseDir=null +// 时 classifyHref 直接判违规);站内绝对链接照常在 dist 内校验。 +const llmsFullPath = path.join(DIST, 'llms-full.txt') +if (fs.existsSync(llmsFullPath)) { + checkLinks(fs.readFileSync(llmsFullPath, 'utf8'), null, '/llms-full.txt') +} + +function checkLinks(raw, baseDir, label) { + for (const { kind, href } of extractLinks(raw)) { + const c = classifyHref(baseDir, href) + if (c.scope === 'external') continue + if (c.scope === 'relative' && c.target === null) { + problems.push(`llms-full.txt must not contain relative links (no base dir): ${href}`) + continue } // 按路径段 decodeURIComponent——与编码出口(config.ts encodePath 的 // encodeURIComponent)互为逆运算;decodeURI 不解码 %26 等保留字符,会把 // 含 & 等合法文件名误报为 unreachable。 - target = target + const target = c.target .split('/') .map((seg) => { try { @@ -124,7 +160,7 @@ for (const p of pages) { /* missing */ } if (target.startsWith('..') || !st || !st.isFile()) { - problems.push(`markdown copy has unreachable ${kind}: /${copyRel} → ${href}`) + problems.push(`unreachable ${kind}: ${label} → ${href}`) } } } From 34abc187b2203abac3e101bf2cdb78a6f2a1fad5 Mon Sep 17 00:00:00 2001 From: deusyu Date: Tue, 18 Aug 2026 17:38:31 +0800 Subject: [PATCH 11/12] =?UTF-8?q?fix:=20=E8=90=BD=E5=AE=9E=E7=AC=AC?= =?UTF-8?q?=E4=BA=94=E8=BD=AE=20codex=20review=E2=80=94=E2=80=94index=20?= =?UTF-8?q?=E4=BE=A7=20symlink=20=E6=A3=80=E6=9F=A5=20+=20=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E8=BE=B9=E7=95=8C=E6=94=B6=E7=B4=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 硬违规: - C14 symlink 禁令补 git index 检查:被强制跟踪进豁免目录 (node_modules/.git/构建目录)的 mode 120000 此前躲得过工作树扫描、 却真实存在于 CI checkout——现在先扫 git ls-files -s 的全部跟踪项, 再做带豁免的工作树扫描抓未跟踪软链,两道互补 P2: - verify-dist 解码后重锁 dist 边界:path.resolve + path.relative 严格 确认目标在 dist 内——编码过的 ../ 解码后才现形,statSync 在仓库根 找到文件不等于部署后可达 - HTML 校验面补全:src/href 无引号形态与 srcset 逐项 URL 全部入检 - 新增源↔副本代码 token 序列一致性断言(生产解析器提取 fence/ code_block/code_inline):逐行正则改写器若触碰任何代码区内容, CI 大声失败——「静默损坏代码示例」这类盲区整类清零 - 编码幂等:classifyLink 按段 decodeURIComponent(a%26b.md 能匹配 含 & 的文件名);encodeAnchor 先解后编(%20/UTF-8 锚点不再二次 编码成 %2520);absolute 模式根相对图片同样补 host --- .vitepress/config.ts | 27 ++++++++++++++++++--------- .vitepress/sidebar.mjs | 30 +++++++++++++++++++++++++----- scripts/verify-dist.mjs | Bin 7611 -> 9641 bytes 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 7f5d4e5..8753827 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -75,8 +75,18 @@ const encodePath = (rel: string) => ) .join('/') -/** 锚点编码:改写产出的 URL 若透传原始锚点,空格等字符会截断 Markdown 链接。 */ -const encodeAnchor = (anchor: string) => (anchor ? `#${encodeURIComponent(anchor.slice(1))}` : '') +/** 容错解码:非法转义序列按原文返回(幂等编码的前置步骤)。 */ +const safeDecode = (s: string) => { + try { + return decodeURIComponent(s) + } catch { + return s + } +} + +/** 锚点编码:改写产出的 URL 若透传原始锚点,空格等字符会截断 Markdown 链接。 + * 先解码再编码保证幂等——源里已写成 %20 / UTF-8 转义的锚点不会被二次编码。 */ +const encodeAnchor = (anchor: string) => (anchor ? `#${encodeURIComponent(safeDecode(anchor.slice(1)))}` : '') /** 站内页面路由 → 对外绝对 URL(llms/RSS/og 的唯一出口,统一做路径段编码)。 */ const pageUrl = (link: string) => `${HOST}${encodePath(link)}` @@ -100,12 +110,9 @@ function classifyLink(pageRel: string, href: string): LinkClass { const q = target.indexOf('?') if (q !== -1) target = target.slice(0, q) if (!target) return { kind: 'skip' } - let decoded = target - try { - decoded = decodeURI(target) - } catch { - /* 非法转义序列:按原文处理 */ - } + // 按路径段 decodeURIComponent(decodeURI 不解 %26/%3A 等保留字符,源里写 + // 成 a%26b.md 的链接会匹配不上文件名含 & 的已发布页)。 + const decoded = target.split('/').map(safeDecode).join('/') const rel = path.posix .normalize(path.posix.join(path.posix.dirname(pageRel), decoded)) .replace(/\/+$/, '') @@ -395,7 +402,9 @@ function transformCopy(pageRel: string, raw: string, absolute = false): string { const c = classifyLink(pageRel, href) const asImage = kind === 'image' || (kind === 'def' && c.kind === 'github' && IMAGE_EXT_RE.test(c.rel)) if (asImage) { - return c.kind === 'github' && !c.isDir ? `${RAW_URL}/${encodePath(c.rel)}` : null + if (c.kind === 'github' && !c.isDir) return `${RAW_URL}/${encodePath(c.rel)}` + if (absolute && /^\/[^/]/.test(href)) return `${HOST}${href}` // 根相对图片同样要补 host + return null } if (c.kind === 'dir-readme') return `${HOST}/${encodePath(c.rel)}/README.md${encodeAnchor(c.anchor)}` if (c.kind === 'github') { diff --git a/.vitepress/sidebar.mjs b/.vitepress/sidebar.mjs index 0a7b7a5..e1fc775 100644 --- a/.vitepress/sidebar.mjs +++ b/.vitepress/sidebar.mjs @@ -18,6 +18,7 @@ * node .vitepress/sidebar.mjs --verify # 完整性校验(C14 调用) * node .vitepress/sidebar.mjs --stats # 打印构建时统计 */ +import { execFileSync } from 'node:child_process' import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' @@ -173,9 +174,28 @@ export function mapMarkdownLinks(raw, fn) { * symlink,因此一律拒绝,而不是逐目录白名单。 */ export function findForbiddenSymlinks() { - const found = [] - // 豁免只认精确的仓库根路径——按目录名任意层级豁免会留出 - // public/node_modules/ 这类被 Vite 原样复制、却躲过扫描的死角。 + const found = new Set() + // 第一道:git index。被跟踪的 symlink(mode 120000)无论藏在哪个路径—— + // 包括下面工作树扫描豁免的目录(有人 git add -f node_modules/x 也逃不掉) + // ——都会出现在 CI 的 checkout 里,必须从索引侧兜住。git 不可用时退化为 + // 纯工作树扫描。 + try { + for (const line of execFileSync('git', ['ls-files', '-s', '-z'], { + cwd: ROOT, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + maxBuffer: 32 * 1024 * 1024, + }) + .split('\0') + .filter(Boolean)) { + if (line.startsWith('120000 ')) found.add(line.split('\t')[1]) + } + } catch { + /* 非 git 环境:仅工作树扫描 */ + } + // 第二道:工作树扫描,抓未跟踪的 symlink。豁免只认精确的仓库根路径—— + // 按目录名任意层级豁免会留出 public/node_modules/ 这类被 Vite 原样复制、 + // 却躲过扫描的死角。 const SKIP_REL = new Set([ '.git', 'node_modules', @@ -187,7 +207,7 @@ export function findForbiddenSymlinks() { for (const ent of fs.readdirSync(path.join(ROOT, dir || '.'), { withFileTypes: true })) { const rel = dir ? `${dir}/${ent.name}` : ent.name if (ent.isSymbolicLink()) { - found.push(rel) + found.add(rel) continue } if (ent.isDirectory()) { @@ -197,7 +217,7 @@ export function findForbiddenSymlinks() { } } walk('') - return found.sort() + return [...found].sort() } /** SRC_EXCLUDE 的谓词形式:collectPublishedFiles 用它判断一个文件是否被站点排除。 */ diff --git a/scripts/verify-dist.mjs b/scripts/verify-dist.mjs index 03ef6e1486b33e77652f5af16fa4278748f77194..2f8a00c161d5fd92ce13ed102ff36a2acde19d22 100644 GIT binary patch delta 1825 zcmaJ>ZEw?76rQFrv`VxSlu6KOx03awNhyOgFOBwLe_%gh^S!n|Rr?1!H%?16O+=33+zR0!I?{~_QN9Gi@rT(2mb*TDrI*~s0iYZnh8pC}>5BMP#DI6;wmkmy59cl+rY{FzSHAqV`t!q}Vb|4Bv2 z$_q+<s$+o}SxrY!T9vC2pRj)PJ4$|a4cF%s_hT8Dq{t(Pl zD=z4B{dg@#MxVeaw@VXb<}NN4$keQJEOMQ#ip@$8gNl0Mz@-i!CwMqJ>g`2}L+J@{$mMb&JI(&_ieL0x z|Nkvs@bdy^->Y5#bOeHO7o#|UK>-1>>U2Zr0lI!oa%z|F`WRD2&(3m=1SzaZWt z271n2QBNMO+gJ6w+TV2OE4qjz?uNuG93)FpFraW=2 zK2DO6PZGP)@mp3$`q<0#{CyjLym9aq>T%FG^>}i`9Ey{4&P;*dC(!xN!BNK6ral@2 z*XyM;#2A$!dJ8)oU?b4#P9BNI!BV(nGFWy5d0|`#4-V|y^Og$S z&m|ynvgrSbW9R{zk;g4~t*gxnlsJEzc%EWZ6{G(1+F|w2qwntn0Jcs|9%)ohH@*4M oZj=MgBPsGFUg=`&4hLuz#8FimFN5qve^B9=-RKQZ)Auj`3+i?}g#Z8m delta 154 zcmZ4Kz1w<&72jk>K3(DGbGo1G=zX!gkxKy#^z{`cH}cs}Hs`NV*VnMODlST{$tX%q z(~K$BvIXL3CG}X1=r|y$tqEf5r)N%P5in({)!dvWXvV&If-E1)e>BBo?Komgp4&>5}5`%#sWZbv-@x&4J1hOaOtSG6nzu From 087bfd555ef3f0c3e633e4d12e807ef17697f9f7 Mon Sep 17 00:00:00 2001 From: deusyu Date: Tue, 18 Aug 2026 17:59:29 +0800 Subject: [PATCH 12/12] =?UTF-8?q?chore:=20=E5=90=88=E5=B9=B6=E5=89=8D?= =?UTF-8?q?=E6=94=B6=E5=B0=BE=E2=80=94=E2=80=94output/=20=E5=85=A5=20gitig?= =?UTF-8?q?nore=20+=20verify-dist=20=E5=8E=BB=E9=99=A4=E5=AD=97=E9=9D=A2?= =?UTF-8?q?=20NUL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - output/(审计报告、发布稿、课程大纲 PDF 等本地过程产物)补 git 侧 防线:站点侧 SRC_EXCLUDE 已排除,但 git add -A 仍可能把商务产物带进 公开仓库 - verify-dist.mjs 的代码 token 分隔符由字面 NUL 字节改为 backslash-u0000 转义写法:字面 NUL 使 git/GitHub 将整个文件按二进制处理,历史 diff 在 review 界面不可见,形成审计盲区 --- .gitignore | 4 ++++ scripts/verify-dist.mjs | Bin 9641 -> 9646 bytes 2 files changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index e2f5194..fcbd504 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,7 @@ translate/ # 私密商务资料(内训洽谈、聊天截图等,永不入库) private/ + +# 本地过程产物(审计报告、发布稿、课程大纲 PDF 等,站点侧已由 SRC_EXCLUDE +# 排除;这里补 git 侧防线,防止 git add -A 把商务产物带进公开仓库) +output/ diff --git a/scripts/verify-dist.mjs b/scripts/verify-dist.mjs index 2f8a00c161d5fd92ce13ed102ff36a2acde19d22..86d3c599e93d8c86add31c544d2e31313ee29f0b 100644 GIT binary patch delta 19 ZcmZ4Kz0P~XJZZL=QUf5^JYSlN3jj+^25$fW delta 20 ccmZ4Iz0!NbJZWYIb