get-shit-done 对抗性 Frontmatter 语料库设计:用 6 类恶意输入固化 YAML 解析器不变量

get-shit-done 对抗性 Frontmatter 语料库设计:用 6 类恶意输入固化 YAML 解析器不变量 get-shit-done 对抗性 Frontmatter 语料库设计用 6 类恶意输入固化 YAML 解析器不变量【免费下载链接】get-shit-doneA light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.项目地址: https://gitcode.com/GitHub_Trending/getshi/get-shit-doneget-shit-doneTÂCHES 出品的 Claude Code 规划驱动开发系统以 Markdown 文件的 YAML frontmatter 承载 phase、plan、status 等关键状态因此 frontmatter 解析器extractFrontmatter()的健壮性直接决定整个系统的数据安全。tests/fixtures/adversarial/frontmatter/目录沉淀了一批敌意但真实的输入语料重复键、CRLF 换行、未闭合块、Unicode、空字节、64KB 大块配套测试 feat-3594-parser-adversarial-frontmatter.test.cjs 将每条不变量钉死为可回归的断言。读完本文你将掌握这套fixture 文件名编码滥用类别 属性式不变量断言的测试方法论并理解解析器在源码层面的防御行为。为什么需要对抗性 Frontmatter 语料库规划文件plan、roadmap、milestone不会只被 get-shit-done 一个工具编辑——用户可能用 VSCode、编辑器、其他 Agent、甚至不同操作系统的换行习惯来修改同一个文件。这些编辑行为会无意识地产出hostile-but-realistic的输入形态同一个 key 出现两次、CRLF 与 LF 混排、---块忘了闭合、中文标题、甚至粘贴进来的控制字符。如果解析器在这些输入下半解析half-parsed garbage、静默丢数据silently lose data或直接崩溃那么下游所有依赖 frontmatter 的调度、验证、进度展示都会拿到错误状态。#3594为此引入的对抗性语料库核心目标是固化三个底线不变量不抛异常does not throw不返回半解析垃圾does not return half-parsed garbage不静默丢数据does not silently lose data。语料库设计文件名编码滥用类别语料库的设计有一个关键约束每个 fixture 是单个 Markdown 文件文件名本身就编码了滥用类别见 READMEtests/fixtures/adversarial/frontmatter/ ├── README.md ├── crlf-mixed.md # CRLF 结尾 ├── duplicate-keys.md # 重复键 ├── huge-bounded.md # 约 64KB 的有界大块 ├── null-byte-value.md # 值内含 U0000 空字节 ├── unclosed-block.md # 缺少闭合 --- └── unicode-keys-and-values.md # 非 ASCII 键与值测试通过相对路径按名加载语料fs.readFileSync读取FIXTURE_DIR下的文件因此新增 fixture 不需要修改加载逻辑语料库可以持续增长。加载时特意先按 buffer 读取再转为 utf-8 字符串保证空字节能存活到字符串里同时不做换行归一化让 CRLF fixture 保持原样见 feat-3594-parser-adversarial-frontmatter.test.cjs。新增一个 fixture 的完整流程是把文件放进该目录 → 在测试文件的语料矩阵中登记条目 → 明确该解析器必须满足的不变量通常是不抛异常、不返回半解析垃圾、不静默丢数据。六类滥用输入逐一拆解1. 重复键duplicate-keyslast-wins 是当前契约duplicate-keys.md 让title和status各出现两次--- title: First title: Second status: active status: blocked phase: 01 ---解析器必须对每个键产出单一确定值不是数组、不是半成品条目。当前行为是last-wins第二次出现的值覆盖第一次。测试显式钉死这一契约断言fm.title Second、fm.status blocked其目的在于如果未来有人想改成 first-wins这个测试会立刻变成红灯语义变更不再悄悄发生。从源码看这一行为源于 extractFrontmatter() 的解析主循环——每行命中key: value正则后直接执行current.obj[key] value覆盖写入同一键的后续行自然覆盖先前行。2. CRLF 混排crlf-mixed\r 不得渗入值crlf-mixed.md 全程使用 CRLF 行尾并包含数组块--- title: CRLF Title phase: 02 plans: - 02-01 - 02-02 ---契约是解析结果中任何值都不得残留\r包括数组项。测试用assert.ok(!/\r/.test(JSON.stringify(fm)))全局扫描并逐项断言fm.plans深等于[02-01, 02-02]见测试。源码侧有两处配合边界匹配正则content.match(/^---\r?\n([\s\S]?)\r?\n---/)和按行分割的yaml.split(/\r?\n/)都显式兼容\r?\n从根上避免\r漏进捕获组frontmatter.cjs。3. 未闭合块unclosed-block返回空对象而非半解析unclosed-block.md 只有开头---没有闭合---正文直接跟在后面--- title: Unclosed Block phase: 03 This frontmatter has no closing --- line...契约是返回空对象{}绝不允许部分解析。若返回{title: Unclosed Block}就意味着正文内容被静默吞进frontmatter——这正是测试要防的>--- title: 日本語のタイトル 相: 04 status: in-flight tags: [α, β, γ] ---测试断言三层行为见测试非 ASCII值必须原样往返fm.title 日本語のタイトル、fm.status in-flight、内联数组fm.tags深等于[α, β, γ]非 ASCII键如相:当前不被识别键正则/^(\s*)([a-zA-Z0-9_-]):\s*(.*)/只接受 ASCII 键因此fm[相]必须为undefined这个ASCII-only 键被显式断言既防止未来放宽键字符集时不被察觉也防止有人静默依赖这一行为。内联数组[α, β, γ]的保真则要归功于splitInlineArray()的引号感知分割逻辑——它在逗号切分时跟踪/引号状态确保带引号的逗号不会被误切frontmatter.cjs。5. 空字节null-byte-value不崩溃、不截断、不提前终止null-byte-value.md 在值中嵌入 U0000--- title: Has null byte weird: beforeNULLafter phase: 05 ---契约有三条见测试解析不得崩溃phase键必须继续被解析——空字节所在行之后的内容绝不能因提前终止而丢失空字节值本身必须是字符串且before部分必须保留。这是对截断 bug最典型的回归防线许多字符串处理在遇到\x00时会把剩余输入当不存在。值得注意cmdFrontmatterGet/cmdFrontmatterSet的文件路径参数另有独立的空字节防护if (filePath.includes(\0))直接报错拒绝防止路径穿越类攻击frontmatter.cjs——空字节防护在文件内容与文件路径两个维度同时存在。6. 有界大块huge-bounded64KB / 2000 数组项2 秒内完成huge-bounded.md 构造了一个故意很大但有界的 frontmatterplans数组含 2000 项约 64KB。测试用Date.now()测量耗时断言 2000ms完成、fm.plans.length 2000、首尾项分别为item-00000与item-01999见测试。这个时间上界的价值在于如果未来有人把解析改成 O(n²) 的嵌套扫描该 fixture 会立刻让测试远超 2 秒而报警——它同时守护了正确性与复杂度。解析器采用单趟按行 缩进栈stack 记录嵌套对象的{obj, key, indent}的设计天然是线性复杂度这是它能轻松通过该测试的实现基础frontmatter.cjs。跨语料属性式扫描全语料不抛异常除了逐条 fixture 的定向断言测试文件最后还有一段横切整个语料库的属性式扫描feat-3594-parser-adversarial-frontmatter.test.cjs#L133-L152const fixtures fs.readdirSync(FIXTURE_DIR) .filter((f) f.endsWith(.md) f ! README.md); for (const fixture of fixtures) { test(fixture ${fixture} — extractFrontmatter returns a plain object without throwing, () { // assert.doesNotThrow 必须返回非 null 的对象且不是数组 }); }它遍历目录里每一个.md排除 README 自身无论新加什么语料都强制extractFrontmatter返回普通对象、不抛异常、不为 null、不为数组。这层扫底兜住了作者新增 fixture 却忘了写单测的遗漏场景——语料库可以无限扩张但底线永远有人看门。从测试到生产解析器的配套能力对抗性测试保护的 frontmatter.cjs 不止有解析还提供完整的 frontmatter 生命周期读取cmdFrontmatterGet(cwd, filePath, field, raw)读取整个 frontmatter 或单个字段写入/更新cmdFrontmatterSet解析 → 改字段 →spliceFrontmatter()重建并替换块cmdFrontmatterMerge以 JSON 做多字段合并Object.assign校验cmdFrontmatterValidate依据FRONTMATTER_SCHEMASplan / summary / verification 三套必填键清单报告缺失字段深层次块解析parseMustHavesBlock()专门抽取must_haves下的truths/artifacts/key_links三级嵌套结构且会在块有内容却解析出 0 项时向 stderr 输出警告——空must_haves会让验证静默降级为 LLM 推导的 Option C这个诊断正是为了暴露这类 YAML 格式问题frontmatter.cjs。序列化侧reconstructFrontmatter()还会对含:或#的字符串值自动加引号保证解析→重建往返不回退——对抗性测试守护的解析健壮性正是这套 CRUD 能力的基石。如何运行与扩展这套测试运行对抗性测试与仓库其他测试同构使用 Node 内置node:testnode --test tests/feat-3594-parser-adversarial-frontmatter.test.cjs或在仓库根目录跑全量测试套件tests/run-tests-harness.test.cjs提供了统一的 harness 入口。相关配套测试还包括tests/feat-3594-parser-adversarial-roadmap.test.cjsroadmap 解析器的对抗输入与tests/feat-3594-parser-property-style.test.cjsroadmap 解析器的属性式不变量。扩展语料库只需三步放入新 fixture 文件文件名即类别→ 在测试矩阵登记 → 声明该解析器必须满足的不变量。这是一个语料可增长、契约可回归、扫描可兜底的完整测试闭环任何解析器的行为漂移——无论 last-wins 变 first-wins、CRLF 渗漏、还是大块超时——都会在 CI 中变成一眼可见的红灯。【免费下载链接】get-shit-doneA light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.项目地址: https://gitcode.com/GitHub_Trending/getshi/get-shit-done创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考