@longzai-intelligence-issues/ledger 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,737 @@
1
+ import { parseLziIssuesConfig } from '@longzai-intelligence-issues/config';
2
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
3
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+
6
+ import { migrateLegacyFile } from '@/migrate/migrate-legacy.commands';
7
+
8
+ /**
9
+ * 测试夹具根(包内临时目录)
10
+ */
11
+ const FIXTURE_ROOT = join(import.meta.dir, '__tmp-migrate__');
12
+
13
+ /**
14
+ * 注册表目录
15
+ */
16
+ const REGISTRY_REL = 'docs/issues';
17
+
18
+ /**
19
+ * 基准配置(scope root)
20
+ */
21
+ const config = parseLziIssuesConfig({
22
+ schemaVersion: 1,
23
+ scopes: [
24
+ {
25
+ key: 'root',
26
+ title: '项目全局',
27
+ paths: ['.'],
28
+ registryDir: REGISTRY_REL,
29
+ },
30
+ ],
31
+ });
32
+
33
+ /**
34
+ * 写入注册表文件
35
+ *
36
+ * @param name - 文件名
37
+ * @param content - 文档全文
38
+ */
39
+ function writeIssue(name: string, content: string): void {
40
+ mkdirSync(join(FIXTURE_ROOT, REGISTRY_REL), { recursive: true });
41
+
42
+ writeFileSync(join(FIXTURE_ROOT, REGISTRY_REL, name), content);
43
+ }
44
+
45
+ /**
46
+ * 基准迁移入参(dry-run 只读形态)
47
+ *
48
+ * @param file - 仓库相对文件路径
49
+ * @returns 命令入参
50
+ */
51
+ function input(file: string) {
52
+ return {
53
+ root: FIXTURE_ROOT,
54
+ file,
55
+ config,
56
+ defaultPriority: 'P2' as const,
57
+ defaultSource: '格式清偿迁移回填(2026-09-14):原案未登记来源',
58
+ dryRun: true,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * 列表方言(KTV 主流形态)样例
64
+ */
65
+ const LIST_DIALECT_GREEN = [
66
+ '# 0400 - roles-matrix 观测两行 deny 错误码红于 deny-first 校验先行分叉族',
67
+ '',
68
+ '- 日期:2026-08-27',
69
+ '- 来源:docs/issues/0390 治理 sweep 复跑发现',
70
+ '- 状态:**已治理(2026-08-27 同日:守门与口径对齐完成)**',
71
+ '- 严重程度:中',
72
+ '- 影响包:packages/api',
73
+ '',
74
+ '## 现象',
75
+ '',
76
+ '正文。',
77
+ '',
78
+ '## 验证结果',
79
+ '',
80
+ '验证正文:lint 全绿。',
81
+ ].join('\n');
82
+
83
+ /**
84
+ * 表格方言(KTV 最新形态)样例
85
+ */
86
+ const TABLE_DIALECT = [
87
+ '# 0549 check:testing-coverage 门禁存在 15 处既有违规',
88
+ '',
89
+ '| 元数据 | 内容 |',
90
+ '| --- | --- |',
91
+ '| Issue 编号 | 0549 |',
92
+ '| 日期 | 2026-09-14 |',
93
+ '| 状态 | 已处理 |',
94
+ '| 来源 | 门禁收尾时发现 |',
95
+ '| 配对决策 | 无(纯欠账清偿) |',
96
+ '',
97
+ '## 现象',
98
+ '',
99
+ '正文。',
100
+ '',
101
+ '## 治理记录',
102
+ '',
103
+ '治理正文。',
104
+ ].join('\n');
105
+
106
+ /**
107
+ * 粗体旧式(KTV 最老形态)样例
108
+ */
109
+ const BOLD_DIALECT_RED = [
110
+ '# StreamableFile 与 typedResponse 类型不兼容问题',
111
+ '',
112
+ '**日期**: 2026-06-13',
113
+ '**状态**: 未处理',
114
+ '**涉及文件**: `apps/nest-server/src/x.ts`',
115
+ '',
116
+ '## 问题描述',
117
+ '',
118
+ '正文。',
119
+ ].join('\n');
120
+
121
+ /**
122
+ * 引用+粗体复合方言(academy 形态:`> **键**: 值` 逐行)样例
123
+ */
124
+ const QUOTE_BOLD_DIALECT = [
125
+ '# 0001 — auth 迁移标准化',
126
+ '',
127
+ '> **状态**: 已修复',
128
+ '> **日期**: 2026-08-04',
129
+ '> **责任方**: academy 项目',
130
+ '',
131
+ '## 迁移目标',
132
+ '',
133
+ '正文。',
134
+ ].join('\n');
135
+
136
+ /**
137
+ * 标题式隔行值方言(oxlint 形态:`## 键` 单独成行 + 下一非空行为值)样例
138
+ */
139
+ const HEADING_ALONE_DIALECT = [
140
+ '# 0001 — ignorePatterns 未生效',
141
+ '',
142
+ '## 状态',
143
+ '',
144
+ '已修复(未发布)',
145
+ '',
146
+ '## 日期',
147
+ '',
148
+ '2026-08-01',
149
+ '',
150
+ '## 现象',
151
+ '',
152
+ '正文。',
153
+ ].join('\n');
154
+
155
+ beforeEach(() => {
156
+ rmSync(FIXTURE_ROOT, { recursive: true, force: true });
157
+ });
158
+
159
+ afterEach(() => {
160
+ rmSync(FIXTURE_ROOT, { recursive: true, force: true });
161
+ });
162
+
163
+ describe('migrateLegacyFile 存量清偿(dry-run 只读)', () => {
164
+ test('列表方言:状态规范化 + 字段别名 + 处置段标题链 + 必填回填', () => {
165
+ writeIssue('0400-roles-matrix-deny-divergence.md', LIST_DIALECT_GREEN);
166
+
167
+ const result = migrateLegacyFile(input(`${REGISTRY_REL}/0400-roles-matrix-deny-divergence.md`));
168
+
169
+ expect(result.outcome).toBe('rewritten');
170
+ expect(result.content).toContain(
171
+ '**状态**: 🟢 已处置(2026-08-27 同日:守门与口径对齐完成;原「已治理」)',
172
+ );
173
+ expect(result.content).toContain('**立案日期**: 2026-08-27');
174
+ expect(result.content).toContain('**优先级**: P2');
175
+ expect(result.content).toContain('**所属域**: 项目全局');
176
+ expect(result.content).toContain('**来源**: docs/issues/0390 治理 sweep 复跑发现');
177
+ expect(result.content).toContain('**严重程度**: 中');
178
+ expect(result.content).toContain('**影响包**: packages/api');
179
+ expect(result.content).toContain('## 处置结果/验证口径');
180
+ expect(result.content).toContain('验证正文:lint 全绿。');
181
+ expect(result.content).toContain('## 现象');
182
+ expect(result.content.startsWith('# 0400 - ')).toBe(true);
183
+ });
184
+
185
+ test('表格方言:数据行提取为字段、脚手架丢弃、治理记录对齐处置段', () => {
186
+ writeIssue('0549-testing-coverage-gate.md', TABLE_DIALECT);
187
+
188
+ const result = migrateLegacyFile(input(`${REGISTRY_REL}/0549-testing-coverage-gate.md`));
189
+
190
+ expect(result.outcome).toBe('rewritten');
191
+ expect(result.content).toContain('**状态**: 🟢 已处置(原「已处理」)');
192
+ expect(result.content).toContain('**Issue 编号**: 0549');
193
+ expect(result.content).toContain('**配对决策**: 无(纯欠账清偿)');
194
+ expect(result.content).toContain('## 处置结果/治理记录');
195
+ expect(result.content.includes('| --- | --- |')).toBe(false);
196
+ expect(result.content.includes('| 元数据 | 内容 |')).toBe(false);
197
+ });
198
+
199
+ test('引用+粗体复合方言(> **键**: 值):键清粗体后入字段,状态词命中绿系', () => {
200
+ writeIssue('0001-quote-bold-dialect.md', QUOTE_BOLD_DIALECT);
201
+
202
+ const result = migrateLegacyFile(input(`${REGISTRY_REL}/0001-quote-bold-dialect.md`));
203
+
204
+ expect(result.outcome).toBe('rewritten');
205
+ expect(result.content).toContain('**状态**: 🟢 已处置(原「已修复」)');
206
+ expect(result.content).toContain('**立案日期**: 2026-08-04');
207
+ expect(result.content).toContain('**责任方**: academy 项目');
208
+ expect(result.content).toContain('## 迁移目标');
209
+ expect(result.content.startsWith('# 0001 - auth 迁移标准化')).toBe(true);
210
+ });
211
+
212
+ test('标题式隔行值方言(## 键 + 下一非空行):键值配对入字段且键行被消费', () => {
213
+ writeIssue('0001-heading-alone-dialect.md', HEADING_ALONE_DIALECT);
214
+
215
+ const result = migrateLegacyFile(input(`${REGISTRY_REL}/0001-heading-alone-dialect.md`));
216
+
217
+ expect(result.outcome).toBe('rewritten');
218
+ expect(result.content).toContain('未发布');
219
+ expect(result.content).toContain('原「已修复」');
220
+ expect(result.content).toContain('**立案日期**: 2026-08-01');
221
+ expect(result.content.includes('## 状态')).toBe(false);
222
+ expect(result.content.includes('## 日期')).toBe(false);
223
+ expect(result.content).toContain('## 现象');
224
+ expect(result.content.startsWith('# 0001 - ignorePatterns')).toBe(true);
225
+ });
226
+
227
+ test('粗体旧式:红灯映射 + H1 补编号 + 无验证段红灯不追加处置段', () => {
228
+ writeIssue('0001-streamablefile-typed-response.md', BOLD_DIALECT_RED);
229
+
230
+ const result = migrateLegacyFile(
231
+ input(`${REGISTRY_REL}/0001-streamablefile-typed-response.md`),
232
+ );
233
+
234
+ expect(result.outcome).toBe('rewritten');
235
+ expect(result.content).toContain('**状态**: 🔴 待处理(原「未处理」)');
236
+ expect(result.content).toContain('**立案日期**: 2026-06-13');
237
+ expect(result.content).toContain('**来源**: 格式清偿迁移回填(2026-09-14):原案未登记来源');
238
+ expect(result.content).toContain('**涉及文件**: `apps/nest-server/src/x.ts`');
239
+ expect(result.content.startsWith('# 0001 - StreamableFile')).toBe(true);
240
+ expect(result.content.includes('处置结果/')).toBe(false);
241
+ });
242
+
243
+ test('绿灯但处置段占位:降级黄灯并给挂起原因', () => {
244
+ writeIssue(
245
+ '0300-green-placeholder.md',
246
+ [
247
+ '# 0300 - 样例',
248
+ '',
249
+ '- 日期:2026-08-01',
250
+ '- 状态:已治理',
251
+ '- 来源:会话记录',
252
+ '',
253
+ '## 验证结果',
254
+ '',
255
+ '(待回填)',
256
+ ].join('\n'),
257
+ );
258
+
259
+ const result = migrateLegacyFile(input(`${REGISTRY_REL}/0300-green-placeholder.md`));
260
+
261
+ expect(result.outcome).toBe('rewritten');
262
+ expect(result.content).toContain(
263
+ '**状态**: 🟡 处理中(历史标记已处置但验证口径为占位,清偿时待核实;原「已治理」)',
264
+ );
265
+ expect(result.content).toContain('## 处置结果/验证口径');
266
+ });
267
+
268
+ test('候选段词汇表:落地治理/治理记录(带日期)/验证(治理后)/修复 均命中改名链,计划性段名不误吞', () => {
269
+ /**
270
+ * 落地治理族样例(KTV 真实段名词汇)
271
+ */
272
+ writeIssue(
273
+ '0320-landing-governance.md',
274
+ [
275
+ '# 0320 - 样例',
276
+ '',
277
+ '- 日期:2026-08-03',
278
+ '- 状态:已治理',
279
+ '- 来源:会话记录',
280
+ '',
281
+ '## 现象',
282
+ '',
283
+ '正文。',
284
+ '',
285
+ '## 落地治理',
286
+ '',
287
+ '治理正文。',
288
+ '',
289
+ '## 修复方向',
290
+ '',
291
+ '计划正文(不应被改名)。',
292
+ ].join('\n'),
293
+ );
294
+
295
+ /**
296
+ * 治理记录带日期样例
297
+ */
298
+ writeIssue(
299
+ '0321-dated-record.md',
300
+ [
301
+ '# 0321 - 样例',
302
+ '',
303
+ '- 日期:2026-08-04',
304
+ '- 状态:已治理',
305
+ '- 来源:会话记录',
306
+ '',
307
+ '## 现象',
308
+ '',
309
+ '正文。',
310
+ '',
311
+ '## 治理记录(2026-08-31)',
312
+ '',
313
+ '记录正文。',
314
+ ].join('\n'),
315
+ );
316
+
317
+ const first = migrateLegacyFile(input(`${REGISTRY_REL}/0320-landing-governance.md`));
318
+
319
+ expect(first.outcome).toBe('rewritten');
320
+ expect(first.content).toContain('## 处置结果/治理记录');
321
+ expect(first.content).toContain('## 修复方向');
322
+ expect(first.content).toContain('计划正文(不应被改名)。');
323
+ expect(first.content.includes('处置结果/验证口径')).toBe(false);
324
+
325
+ const second = migrateLegacyFile(input(`${REGISTRY_REL}/0321-dated-record.md`));
326
+
327
+ expect(second.outcome).toBe('rewritten');
328
+ expect(second.content).toContain('## 处置结果/治理记录');
329
+ });
330
+
331
+ test('绿灯无任何候选验证段:追加处置段与事实回填说明', () => {
332
+ writeIssue(
333
+ '0310-green-nosection.md',
334
+ [
335
+ '# 0310 - 样例',
336
+ '',
337
+ '- 日期:2026-08-02',
338
+ '- 状态:已解决',
339
+ '- 来源:会话记录',
340
+ '',
341
+ '## 解决方案',
342
+ '',
343
+ '改了 X。',
344
+ ].join('\n'),
345
+ );
346
+
347
+ const result = migrateLegacyFile(input(`${REGISTRY_REL}/0310-green-nosection.md`));
348
+
349
+ expect(result.outcome).toBe('rewritten');
350
+ expect(result.content).toContain('## 处置结果/变更记录');
351
+ expect(result.content).toContain('## 处置结果/验证口径');
352
+ });
353
+
354
+ test('无状态行且未注入灯位:skipped 交人工', () => {
355
+ writeIssue('0547-anomaly.md', '# 0547 · 无元信息异类\n\n## 现象\n\n正文。\n');
356
+
357
+ const result = migrateLegacyFile(input(`${REGISTRY_REL}/0547-anomaly.md`));
358
+
359
+ expect(result.outcome).toBe('skipped');
360
+ });
361
+
362
+ test('无状态行 + status 注入(green):规范标签落位且处置段对齐执行', () => {
363
+ writeIssue(
364
+ '0350-status-inject.md',
365
+ [
366
+ '# Git 换行符问题分析与处理',
367
+ '',
368
+ '## 问题描述',
369
+ '',
370
+ '正文。',
371
+ '',
372
+ '## 解决方案',
373
+ '',
374
+ '已落地 .gitattributes 统一换行。',
375
+ ].join('\n'),
376
+ );
377
+
378
+ const result = migrateLegacyFile({
379
+ ...input(`${REGISTRY_REL}/0350-status-inject.md`),
380
+ status: 'green',
381
+ defaultFilingDate: '2026-06-13',
382
+ });
383
+
384
+ expect(result.outcome).toBe('rewritten');
385
+ expect(result.content).toContain(
386
+ '**状态**: 🟢 已处置(存量清偿定档:原案无状态行,依结案内容人工定档)',
387
+ );
388
+ expect(result.content).toContain('**立案日期**: 2026-06-13');
389
+ expect(result.content).toContain('## 处置结果/变更记录');
390
+ expect(result.content).toContain('## 处置结果/验证口径');
391
+ expect(result.content.startsWith('# 0350 - Git 换行符')).toBe(true);
392
+ });
393
+
394
+ test('H1 编号句点分隔符变体(# 0279. 标题)规范为 # 0279 - 标题且守卫通过', () => {
395
+ writeIssue(
396
+ '0279-dot-separator.md',
397
+ [
398
+ '# 0279. 包厢预订配置页默认值全 0:分组缺 defaultValue 建模',
399
+ '',
400
+ '- 日期:2026-08-10',
401
+ '- 状态:已治理',
402
+ '- 来源:会话记录',
403
+ '',
404
+ '## 现象',
405
+ '',
406
+ '正文。',
407
+ '',
408
+ '## 验证结果',
409
+ '',
410
+ '验证正文。',
411
+ ].join('\n'),
412
+ );
413
+
414
+ const result = migrateLegacyFile(input(`${REGISTRY_REL}/0279-dot-separator.md`));
415
+
416
+ expect(result.outcome).toBe('rewritten');
417
+ expect(result.content.startsWith('# 0279 - 包厢预订配置页默认值全 0')).toBe(true);
418
+ expect(result.content.includes('0279.')).toBe(false);
419
+ });
420
+
421
+ test('状态值粗体形态三变体(全包裹/前导无尾随/词中包裹)均可命中词表', () => {
422
+ writeIssue(
423
+ '0330-bold-variants.md',
424
+ [
425
+ '# 0330 - 样例',
426
+ '',
427
+ '- 日期:2026-08-21',
428
+ '- 状态:**已治理(2026-08-21,commit 57a2d80ef9)',
429
+ '- 来源:会话记录',
430
+ '',
431
+ '## 现象',
432
+ '',
433
+ '正文。',
434
+ '',
435
+ '## 治理记录',
436
+ '',
437
+ '治理正文。',
438
+ ].join('\n'),
439
+ );
440
+
441
+ writeIssue(
442
+ '0331-bold-word-wrap.md',
443
+ [
444
+ '# 0331 - 样例',
445
+ '',
446
+ '- 日期:2026-08-25',
447
+ '- 状态:**已治理**(2026-08-25 同日收口)',
448
+ '- 来源:会话记录',
449
+ '',
450
+ '## 现象',
451
+ '',
452
+ '正文。',
453
+ '',
454
+ '## 验证结果',
455
+ '',
456
+ '验证正文。',
457
+ ].join('\n'),
458
+ );
459
+
460
+ const first = migrateLegacyFile(input(`${REGISTRY_REL}/0330-bold-variants.md`));
461
+
462
+ expect(first.outcome).toBe('rewritten');
463
+ expect(first.content).toContain(
464
+ '**状态**: 🟢 已处置(2026-08-21,commit 57a2d80ef9;原「已治理」)',
465
+ );
466
+
467
+ const second = migrateLegacyFile(input(`${REGISTRY_REL}/0331-bold-word-wrap.md`));
468
+
469
+ expect(second.outcome).toBe('rewritten');
470
+ expect(second.content).toContain('**状态**: 🟢 已处置(2026-08-25 同日收口;原「已治理」)');
471
+ });
472
+
473
+ test('规范词无 emoji 形态(已处置/已完成/已闭合)命中绿系词表', () => {
474
+ writeIssue(
475
+ '0334-canonical-word-no-emoji.md',
476
+ [
477
+ '# 0334 - 样例',
478
+ '',
479
+ '| 元数据 | 内容 |',
480
+ '| --- | --- |',
481
+ '| 状态 | 已处置(单例化修复 + e2e 复绿) |',
482
+ '',
483
+ '## 现象',
484
+ '',
485
+ '正文。',
486
+ '',
487
+ '## 验证结果',
488
+ '',
489
+ '验证正文。',
490
+ ].join('\n'),
491
+ );
492
+
493
+ const result = migrateLegacyFile({
494
+ ...input(`${REGISTRY_REL}/0334-canonical-word-no-emoji.md`),
495
+ defaultFilingDate: '2026-09-01',
496
+ });
497
+
498
+ expect(result.outcome).toBe('rewritten');
499
+ expect(result.content).toContain('**状态**: 🟢 已处置(单例化修复 + e2e 复绿;原「已处置」)');
500
+ });
501
+
502
+ test('扩展词表:待处理/未修复/已收口/已根治/终态达成 等命中', () => {
503
+ writeIssue(
504
+ '0332-vocab-extended.md',
505
+ [
506
+ '# 0332 - 样例',
507
+ '',
508
+ '- 日期:2026-08-22',
509
+ '- 状态:已收口(2026-08-19,D-3 维持现状)',
510
+ '- 来源:会话记录',
511
+ '',
512
+ '## 现象',
513
+ '',
514
+ '正文。',
515
+ ].join('\n'),
516
+ );
517
+
518
+ writeIssue(
519
+ '0333-vocab-red.md',
520
+ [
521
+ '# 0333 - 样例',
522
+ '',
523
+ '- 日期:2026-08-23',
524
+ '- 状态:未修复(本次质量检查发现并归档,待用户决策修复范围)',
525
+ '- 来源:会话记录',
526
+ '',
527
+ '## 现象',
528
+ '',
529
+ '正文。',
530
+ ].join('\n'),
531
+ );
532
+
533
+ const first = migrateLegacyFile(input(`${REGISTRY_REL}/0332-vocab-extended.md`));
534
+
535
+ expect(first.outcome).toBe('rewritten');
536
+ expect(first.content).toContain(
537
+ '**状态**: 🟢 已处置(2026-08-19,D-3 维持现状;原「已收口」)',
538
+ );
539
+
540
+ const second = migrateLegacyFile(input(`${REGISTRY_REL}/0333-vocab-red.md`));
541
+
542
+ expect(second.outcome).toBe('rewritten');
543
+ expect(second.content).toContain(
544
+ '🔴 待处理(本次质量检查发现并归档,待用户决策修复范围;原「未修复」)',
545
+ );
546
+ });
547
+
548
+ test('列表+粗体复合方言(- **键**: 值)解析为字段,原始语义保留且不落残留块', () => {
549
+ writeIssue(
550
+ '0340-list-bold-compound.md',
551
+ [
552
+ '# 静态导出产物托管纪律未登记',
553
+ '',
554
+ '- **严重度**:信息',
555
+ '- **状态**:未处理(已登记,待治理)',
556
+ '- **发现**:2026-08-24 issue 022 治理中的同类排查',
557
+ '',
558
+ '## 问题描述',
559
+ '',
560
+ '正文。',
561
+ ].join('\n'),
562
+ );
563
+
564
+ const result = migrateLegacyFile({
565
+ ...input(`${REGISTRY_REL}/0340-list-bold-compound.md`),
566
+ defaultFilingDate: '2026-08-24',
567
+ });
568
+
569
+ expect(result.outcome).toBe('rewritten');
570
+ expect(result.content).toContain('**状态**: 🔴 待处理(已登记,待治理;原「未处理」)');
571
+ expect(result.content).toContain('**严重度**: 信息');
572
+ expect(result.content).toContain('**发现**: 2026-08-24 issue 022 治理中的同类排查');
573
+ expect(result.content).not.toMatch(/^- \*\*/m);
574
+ });
575
+
576
+ test('污染修复:规范头部与体首残留元信息块收敛(键去重、注入定档让位原始语义)', () => {
577
+ writeIssue(
578
+ '0341-polluted-converge.md',
579
+ [
580
+ '# 0341 - 静态导出产物托管纪律未登记',
581
+ '',
582
+ '**优先级**: P2',
583
+ '**状态**: 🔴 待处理(存量清偿定档:原案无状态行,依结案内容人工定档)',
584
+ '**立案日期**: 2026-08-24',
585
+ '**所属域**: 项目全局',
586
+ '**来源**: 格式清偿迁移回填(2026-09-14):原案未登记来源',
587
+ '',
588
+ '- **严重度**:信息',
589
+ '- **状态**:未处理(已登记,待治理)',
590
+ '- **发现**:2026-08-24 issue 022 治理中的同类排查',
591
+ '',
592
+ '## 问题描述',
593
+ '',
594
+ '正文。',
595
+ ].join('\n'),
596
+ );
597
+
598
+ const result = migrateLegacyFile(input(`${REGISTRY_REL}/0341-polluted-converge.md`));
599
+
600
+ expect(result.outcome).toBe('rewritten');
601
+ expect(result.content).toContain('**状态**: 🔴 待处理(已登记,待治理;原「未处理」)');
602
+ expect(result.content).toContain('**严重度**: 信息');
603
+ expect(result.content).toContain('**发现**: 2026-08-24 issue 022 治理中的同类排查');
604
+ expect(result.content).not.toMatch(/存量清偿定档:原案无状态行/);
605
+ expect(result.content).not.toMatch(/^- \*\*(状态|严重度|发现)/m);
606
+ expect(result.content.match(/\*\*状态\*\*/g)).toHaveLength(1);
607
+
608
+ /**
609
+ * 收敛幂等:第一轮产物落盘后二轮 unchanged
610
+ */
611
+ const landing = migrateLegacyFile({
612
+ ...input(`${REGISTRY_REL}/0341-polluted-converge.md`),
613
+ dryRun: false,
614
+ });
615
+
616
+ expect(landing.outcome).toBe('rewritten');
617
+
618
+ const second = migrateLegacyFile(input(`${REGISTRY_REL}/0341-polluted-converge.md`));
619
+
620
+ expect(second.outcome).toBe('unchanged');
621
+ });
622
+
623
+ test('体首非元信息列表不被吸收(正文证据列表原样保留)', () => {
624
+ writeIssue(
625
+ '0342-body-bullet-kept.md',
626
+ [
627
+ '# 0342 - 样例',
628
+ '',
629
+ '**优先级**: P2',
630
+ '**状态**: 🔴 待处理',
631
+ '**立案日期**: 2026-08-24',
632
+ '**所属域**: 项目全局',
633
+ '**来源**: 会话记录',
634
+ '',
635
+ '- **next.config.ts:7** — 注释明示静态导出(正文证据,非元信息)',
636
+ '- **out/** 已被 gitignore(正文证据)',
637
+ '',
638
+ '## 问题描述',
639
+ '',
640
+ '正文。',
641
+ ].join('\n'),
642
+ );
643
+
644
+ const result = migrateLegacyFile(input(`${REGISTRY_REL}/0342-body-bullet-kept.md`));
645
+
646
+ expect(result.outcome).toBe('unchanged');
647
+ expect(result.content).toContain(
648
+ '- **next.config.ts:7** — 注释明示静态导出(正文证据,非元信息)',
649
+ );
650
+ });
651
+
652
+ test('同键多值合并:重复关联行并值为分号单行,不丢链接', () => {
653
+ writeIssue(
654
+ '0343-dup-join.md',
655
+ [
656
+ '# 0343 - 样例',
657
+ '',
658
+ '**状态**: 🟢 已处置(原「已治理」)',
659
+ '**立案日期**: 2026-06-13',
660
+ '**所属域**: 项目全局',
661
+ '**来源**: 会话记录',
662
+ '**关联**: [ADR-0010](../decisions/0010-form-error-no-document-flow-shift.md)',
663
+ '**关联**: [表单错误提示双重反馈机制规格](../specs/spec-20260510-form-error-tooltip-feedback/spec.md)',
664
+ '',
665
+ '## 验证结果',
666
+ '',
667
+ '验证正文。',
668
+ ].join('\n'),
669
+ );
670
+
671
+ const result = migrateLegacyFile(input(`${REGISTRY_REL}/0343-dup-join.md`));
672
+
673
+ expect(result.outcome).toBe('rewritten');
674
+ expect(result.content).toContain(
675
+ '**关联**: [ADR-0010](../decisions/0010-form-error-no-document-flow-shift.md);[表单错误提示双重反馈机制规格](../specs/spec-20260510-form-error-tooltip-feedback/spec.md)',
676
+ );
677
+ expect(result.content.match(/\*\*关联\*\*/g)).toHaveLength(1);
678
+ });
679
+
680
+ test('超长状态拆分:词表命中时长说明落处置摘要字段、状态行保持短规范', () => {
681
+ writeIssue(
682
+ '0344-long-status-split.md',
683
+ [
684
+ '# 0344 - 样例',
685
+ '',
686
+ '> 日期:2026-08-04',
687
+ '> 状态:**已修复**。删除 `src/index.ts` 源码 shebang,由 `lzi-builder.config.ts` 的 banner 作为唯一 shebang 来源,避免双 shebang 导致 node 以注释行开头的解释器路径执行失败。',
688
+ '',
689
+ '## 现象',
690
+ '',
691
+ '正文。',
692
+ ].join('\n'),
693
+ );
694
+
695
+ const result = migrateLegacyFile(input(`${REGISTRY_REL}/0344-long-status-split.md`));
696
+
697
+ expect(result.outcome).toBe('rewritten');
698
+ expect(result.content).toContain('**状态**: 🟢 已处置(原「已修复」)');
699
+ expect(result.content).toContain('**处置摘要**: 已修复。删除 `src/index.ts` 源码 shebang');
700
+ expect(result.content).toContain('banner 作为唯一 shebang 来源');
701
+ expect(result.content).toContain('## 现象');
702
+ });
703
+
704
+ test('幂等:对已规范产物重跑 unchanged', () => {
705
+ writeIssue('0400-roles-matrix-deny-divergence.md', LIST_DIALECT_GREEN);
706
+
707
+ /**
708
+ * 第一轮落盘
709
+ */
710
+ const first = migrateLegacyFile({
711
+ ...input(`${REGISTRY_REL}/0400-roles-matrix-deny-divergence.md`),
712
+ dryRun: false,
713
+ });
714
+
715
+ expect(first.outcome).toBe('rewritten');
716
+
717
+ /**
718
+ * 第二轮对产物重跑
719
+ */
720
+ const second = migrateLegacyFile(input(`${REGISTRY_REL}/0400-roles-matrix-deny-divergence.md`));
721
+
722
+ expect(second.outcome).toBe('unchanged');
723
+ });
724
+
725
+ test('dry-run 不落盘', () => {
726
+ writeIssue('0001-streamablefile-typed-response.md', BOLD_DIALECT_RED);
727
+
728
+ migrateLegacyFile(input(`${REGISTRY_REL}/0001-streamablefile-typed-response.md`));
729
+
730
+ expect(
731
+ readFileSync(
732
+ join(FIXTURE_ROOT, REGISTRY_REL, '0001-streamablefile-typed-response.md'),
733
+ 'utf8',
734
+ ),
735
+ ).toBe(BOLD_DIALECT_RED);
736
+ });
737
+ });