Prechádzať zdrojové kódy

fix(v7): parseMarkdownTable 接受 GFM 单字符/对齐分隔符

原 separator 判定 separatorLine.includes('---') 拒单字符 |--|--|--|--|
(GFM 合法,每格一横即可),致名册 parse 失败走软跳过,entities/aliases 不入库。
改为 isDelimiterRow GFM 判定:以 | 围栏、格数与表头一致、每格仅 - 与 : 且至少一横,
接受 |-| / |:--:| / |--:| / |:--|。格数不一致或含非法字符仍报错。

测试:267 绿(+4);drift check 通过。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lingfengQAQ 10 hodín pred
rodič
commit
e87df33df5

+ 1 - 1
.trellis/tasks/06-27-m4-ai-roles/implement.md

@@ -101,7 +101,7 @@ merged 收敛版之外的 deep 报告独有项(作者手改丢失、回滚范
 - [x] P0-3 `src/state-machine/persist.js` + `src/review/index.js`:多文件写入改成原子落盘,中途失败不留半成品。✅ 新增 `src/storage/atomic.js`(`writeAtomicBatch` 先 .tmp 再 rename);persist 全家 + `persistReviewReport` 接入;`rebuildCache` 包 BEGIN/COMMIT/ROLLBACK 事务。
 - [x] P0-3a (deep) `src/storage/adapters/ChapterWriter.js` + `src/cache/rebuilder.js`:重写同章改标题时旧文件残留 → `scanChapters` 两条同 `chapter_num` → 重建 `PRIMARY KEY` 冲突。✅ `writeChapter` 写盘前 `removeOldChapterFiles` 清同章旧文件 + `sanitizeFileName` 净化 Windows 非法字符(顺带修 P2-1)。
 
-> 附带修:`scanEntities` 名册缺失/解析失败从硬错降为软跳过(warning),避免事务回滚把 chapters/threads/secrets 一起清了——这是事务化后暴露的过度严格。`parseMarkdownTable` 拒单字符分隔符 `|--|--|--|--|`(GFM 合法)是潜伏 parser bug,留作 follow-up(非报告项)。
+> 附带修:`scanEntities` 名册缺失/解析失败从硬错降为软跳过(warning),避免事务回滚把 chapters/threads/secrets 一起清了——这是事务化后暴露的过度严格。`parseMarkdownTable` 拒单字符分隔符 `|--|--|--|--|`(GFM 合法)已修(加 `isDelimiterRow` GFM 判定,接受 `|-|`/`|:--:|`/`|--:|`/`|:--|`)。
 
 ### P1 两审 / 会话 / 校验
 

+ 13 - 2
v7/src/storage/parsers/markdown-table.js

@@ -40,9 +40,9 @@ export function parseMarkdownTable(content) {
     .split('|')
     .map((h) => h.trim())
 
-  // 跳过分隔符行(第二行,形如 |---|---|)
+  // 跳过分隔符行(第二行,GFM:每格仅 - 与 :,至少一横;支持 |---| 与 |-| 与对齐 |:--:|--:|)
   const separatorLine = lines[1]
-  if (!separatorLine.includes('---')) {
+  if (!isDelimiterRow(separatorLine, headers.length)) {
     return {
       ok: false,
       headers: [],
@@ -92,3 +92,14 @@ export function parseMarkdownTable(content) {
     error: '',
   }
 }
+
+/**
+ * GFM 分隔符行判定:以 | 围栏,格数与表头一致,每格仅 - 与 : 且至少一横。
+ * 接受 |---|、|-|、|:--:|、|--:|、|:--| 等所有合法形态。
+ */
+function isDelimiterRow(line, expectedCount) {
+  if (typeof line !== 'string' || !line.startsWith('|') || !line.endsWith('|')) return false
+  const cells = line.slice(1, -1).split('|').map((c) => c.trim())
+  if (cells.length !== expectedCount) return false
+  return cells.every((c) => /^:?-+:?$/.test(c))
+}

+ 38 - 0
v7/test/storage/parsers/markdown-table.test.js

@@ -70,3 +70,41 @@ test('边界:表头格式错误', () => {
   assert.equal(result.ok, false)
   assert.ok(result.error.includes('必须以 | 开头'))
 })
+
+test('单字符分隔符 |-| 也要接受(GFM 合法)', () => {
+  const content = `| 正名 | 别名 | 类型 | 首现章 |
+|--|--|--|--|
+| 林晚 | 晚晚 | character | 1 |`
+  const result = parseMarkdownTable(content)
+  assert.equal(result.ok, true, result.error)
+  assert.equal(result.rows.length, 1)
+  assert.equal(result.rows[0].正名, '林晚')
+  assert.equal(result.rows[0].别名, '晚晚')
+})
+
+test('对齐分隔符 |:--:|--:|:--| 也要接受', () => {
+  const content = `| A | B | C |
+|:--|--:|:--:|
+| 1 | 2 | 3 |`
+  const result = parseMarkdownTable(content)
+  assert.equal(result.ok, true, result.error)
+  assert.equal(result.rows[0].B, '2')
+})
+
+test('分隔符格数与表头不一致 → 报错', () => {
+  const content = `| A | B | C |
+|---|---|
+| 1 | 2 | 3 |`
+  const result = parseMarkdownTable(content)
+  assert.equal(result.ok, false)
+  assert.ok(result.error.includes('分隔符'))
+})
+
+test('分隔符含非 -: 字符 → 报错', () => {
+  const content = `| A | B |
+|--x--|---|
+| 1 | 2 |`
+  const result = parseMarkdownTable(content)
+  assert.equal(result.ok, false)
+  assert.ok(result.error.includes('分隔符'))
+})