@longzai-intelligence-issues/ledger 0.0.2 → 0.0.4
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.
- package/.turbo/turbo-lint.log +2 -2
- package/CHANGELOG.md +17 -0
- package/dist/index.d.ts +137 -74
- package/dist/index.js +16 -16
- package/package.json +2 -2
- package/src/__tests__/lint/lint.core.test.ts +24 -0
- package/src/__tests__/migrate/migrate-legacy.commands.test.ts +218 -0
- package/src/__tests__/parser/registry.parser.test.ts +15 -0
- package/src/index.ts +4 -0
- package/src/lint/lint.core.ts +51 -0
- package/src/migrate/migrate-legacy.backfill.ts +149 -0
- package/src/migrate/migrate-legacy.commands.ts +66 -8
- package/src/migrate/migrate-legacy.converge.ts +384 -0
- package/src/migrate/migrate-legacy.core.ts +368 -384
- package/src/migrate/migrate-legacy.vocab.ts +242 -0
- package/src/normalize/normalize-header.commands.ts +151 -9
- package/src/parser/registry.parser.ts +8 -0
- package/tsconfig/.cache/build.tsbuildinfo +1 -1
- package/tsconfig/.cache/test.tsbuildinfo +1 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migrate-legacy 词表与纯规范化函数(无 IO,自 migrate-legacy.core 拆出守文件行上限)
|
|
3
|
+
*
|
|
4
|
+
* 字段值粗体清理、pre-emoji 状态词表匹配与五态规范化、优先级档位映射、H1 规范化。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LEGACY_STATUS_VOCABULARY } from '@/parser/format.utils';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 状态词匹配产物
|
|
11
|
+
*/
|
|
12
|
+
type StatusWordMatch = {
|
|
13
|
+
/**
|
|
14
|
+
* 词表档位
|
|
15
|
+
*/
|
|
16
|
+
tier: 'green' | 'red' | 'deferred' | 'canceled' | 'inProgress' | 'frozen';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 命中词
|
|
20
|
+
*/
|
|
21
|
+
word: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 状态值规范化产物
|
|
26
|
+
*/
|
|
27
|
+
type CanonicalStatusValue = {
|
|
28
|
+
/**
|
|
29
|
+
* 规范状态值(五态灯位 + 溯源括注)
|
|
30
|
+
*/
|
|
31
|
+
value: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 词表档位
|
|
35
|
+
*/
|
|
36
|
+
tier: StatusWordMatch['tier'];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 命中词(原词溯源)
|
|
40
|
+
*/
|
|
41
|
+
word: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 清理字段值(去首尾空白与粗体星号——覆盖三种历史形态:整体包裹 `**值**`、
|
|
46
|
+
* 前导无尾随 `**值`、词中包裹 `**词**(括注)`)
|
|
47
|
+
*
|
|
48
|
+
* @param value - 原值
|
|
49
|
+
* @returns 清理后值
|
|
50
|
+
*/
|
|
51
|
+
export function cleanValue(value: string): string {
|
|
52
|
+
return value
|
|
53
|
+
.trim()
|
|
54
|
+
.replace(/^\*\*/, '')
|
|
55
|
+
.replace(/\*\*$/, '')
|
|
56
|
+
.replace(/\*\*(?=[((])/, '')
|
|
57
|
+
.trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 在状态值中匹配 pre-emoji 词表
|
|
62
|
+
*
|
|
63
|
+
* @param value - 清理后的状态值
|
|
64
|
+
* @returns 命中档位与词;未命中为 null
|
|
65
|
+
*/
|
|
66
|
+
function matchStatusWord(value: string): StatusWordMatch | null {
|
|
67
|
+
/**
|
|
68
|
+
* 档位遍历序(frozen 最先——冻结态优先识别并拒绝迁移)
|
|
69
|
+
*/
|
|
70
|
+
const tiers: StatusWordMatch['tier'][] = [
|
|
71
|
+
'frozen',
|
|
72
|
+
'green',
|
|
73
|
+
'deferred',
|
|
74
|
+
'canceled',
|
|
75
|
+
'inProgress',
|
|
76
|
+
'red',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const tier of tiers) {
|
|
80
|
+
for (const word of LEGACY_STATUS_VOCABULARY[tier]) {
|
|
81
|
+
if (value.startsWith(word)) {
|
|
82
|
+
return { tier, word };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 状态值切分产物(首个括注内容与词后剩余文本)
|
|
92
|
+
*/
|
|
93
|
+
type StatusValueSplit = {
|
|
94
|
+
/**
|
|
95
|
+
* 首个括注内容(无括注为空串)
|
|
96
|
+
*/
|
|
97
|
+
paren: string;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 括注后的剩余文本
|
|
101
|
+
*/
|
|
102
|
+
rest: string;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 提取状态值中的首个括注与其余文本
|
|
107
|
+
*
|
|
108
|
+
* @param value - 清理后的状态值
|
|
109
|
+
* @param word - 命中词
|
|
110
|
+
* @returns 括注内容与词后剩余文本
|
|
111
|
+
*/
|
|
112
|
+
function splitStatusValue(value: string, word: string): StatusValueSplit {
|
|
113
|
+
/**
|
|
114
|
+
* 词后文本
|
|
115
|
+
*/
|
|
116
|
+
const afterWord = value.slice(word.length).trim();
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 首个括注匹配
|
|
120
|
+
*/
|
|
121
|
+
const parenMatch = /^[((]([^))]*)[))]/.exec(afterWord);
|
|
122
|
+
|
|
123
|
+
if (parenMatch === null) {
|
|
124
|
+
return { paren: '', rest: afterWord };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
paren: (parenMatch[1] ?? '').trim(),
|
|
129
|
+
rest: afterWord.slice((parenMatch[0] ?? '').length).trim(),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 规范化状态值(pre-emoji 词 → 五态灯位;原词与括注零丢失保留)
|
|
135
|
+
*
|
|
136
|
+
* @param value - 清理后的状态值
|
|
137
|
+
* @returns 规范值与档位;不可机械迁移为 null(须人工)
|
|
138
|
+
*/
|
|
139
|
+
export function canonicalizeStatusValue(value: string): CanonicalStatusValue | null {
|
|
140
|
+
/**
|
|
141
|
+
* 词表命中
|
|
142
|
+
*/
|
|
143
|
+
const match = matchStatusWord(value);
|
|
144
|
+
|
|
145
|
+
if (match === null) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 括注与剩余文本
|
|
151
|
+
*/
|
|
152
|
+
const { paren, rest } = splitStatusValue(value, match.word);
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 括注段收集器(顺序:括注 → 剩余 → 原词溯源)
|
|
156
|
+
*/
|
|
157
|
+
const parts = [paren, rest, `原「${match.word}」`].filter((part) => part !== '');
|
|
158
|
+
|
|
159
|
+
switch (match.tier) {
|
|
160
|
+
case 'frozen':
|
|
161
|
+
return null;
|
|
162
|
+
|
|
163
|
+
case 'green':
|
|
164
|
+
return { value: `🟢 已处置(${parts.join(';')})`, tier: match.tier, word: match.word };
|
|
165
|
+
|
|
166
|
+
case 'red':
|
|
167
|
+
return { value: `🔴 待处理(${parts.join(';')})`, tier: match.tier, word: match.word };
|
|
168
|
+
|
|
169
|
+
case 'deferred':
|
|
170
|
+
return {
|
|
171
|
+
value: `🔴 待处理(${[paren === '' ? '延期' : paren, `原「${match.word}」`].join(';')})`,
|
|
172
|
+
tier: match.tier,
|
|
173
|
+
word: match.word,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
case 'canceled':
|
|
177
|
+
return paren === '' || paren.length < 4
|
|
178
|
+
? null
|
|
179
|
+
: { value: `⚫ 已取消(${parts.join(';')})`, tier: match.tier, word: match.word };
|
|
180
|
+
|
|
181
|
+
case 'inProgress':
|
|
182
|
+
return paren.length < 4
|
|
183
|
+
? null
|
|
184
|
+
: { value: `🟡 处理中(${paren})`, tier: match.tier, word: match.word };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 严重程度/中文优先级 → P 档位映射
|
|
190
|
+
*
|
|
191
|
+
* @param value - 原值(高/中/低或 P1/P2/P3)
|
|
192
|
+
* @returns P 档位;不可映射为 null
|
|
193
|
+
*/
|
|
194
|
+
export function mapPriorityValue(value: string): 'P1' | 'P2' | 'P3' | null {
|
|
195
|
+
/**
|
|
196
|
+
* 严重程度中文档位与 P 档位的等价映射表(P 档位原样透传)
|
|
197
|
+
*/
|
|
198
|
+
const table: Record<string, 'P1' | 'P2' | 'P3'> = {
|
|
199
|
+
高: 'P1',
|
|
200
|
+
中: 'P2',
|
|
201
|
+
低: 'P3',
|
|
202
|
+
P1: 'P1',
|
|
203
|
+
P2: 'P2',
|
|
204
|
+
P3: 'P3',
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return table[cleanValue(value)] ?? null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* H1 规范化产物
|
|
212
|
+
*/
|
|
213
|
+
type CanonicalH1 = {
|
|
214
|
+
/**
|
|
215
|
+
* 规范标题行(`# NNNN - 标题核心`)
|
|
216
|
+
*/
|
|
217
|
+
line: string;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 标题核心(剥离既有编号与分隔符变体后的标题文本)
|
|
221
|
+
*/
|
|
222
|
+
titleCore: string;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* H1 标题规范化:`# NNNN - 标题`(剥离既有编号与分隔符变体)
|
|
227
|
+
*
|
|
228
|
+
* @param h1 - 原一级标题行
|
|
229
|
+
* @param number - 文件名编号
|
|
230
|
+
* @returns 规范标题行与标题核心
|
|
231
|
+
*/
|
|
232
|
+
export function canonicalizeH1(h1: string, number: string): CanonicalH1 {
|
|
233
|
+
/**
|
|
234
|
+
* 标题核心(去行首 `# ` 与既有编号 + 分隔符变体——含 em-dash 与叠用形态)
|
|
235
|
+
*/
|
|
236
|
+
const titleCore = h1
|
|
237
|
+
.replace(/^#\s+/, '')
|
|
238
|
+
.replace(new RegExp(`^${number}\\s*(?:[-—―·..::-]+\\s*)?`), '')
|
|
239
|
+
.trim();
|
|
240
|
+
|
|
241
|
+
return { line: `# ${number} - ${titleCore}`, titleCore };
|
|
242
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* normalize-header(方言头部迁移)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* 五种历史方言(单行管道引用行 / 粗体标签行 / `## 键: 值` 标题式 / `- ` 列表式 /
|
|
5
|
+
* `| 键 | 值 |` 表格)与两类复合形态(引用行内粗体键 `> **键**: 值`、标题式隔行值
|
|
6
|
+
* `## 键` + 下一非空独立行)解析后重排为裁定格式:粗体标签逐行 `**键**: 值`;字段
|
|
7
|
+
* 规范序(FIELD_RANK);「主题」问题陈述迁正文首个节标题后;residual 原样保留。
|
|
8
|
+
* 零丢失守卫:每字段必须在产物文本在场否则 failed 不落盘;幂等(产物 === 原文 →
|
|
9
|
+
* unchanged)。
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
@@ -12,6 +14,23 @@ import { join } from 'node:path';
|
|
|
12
14
|
|
|
13
15
|
import { CANONICAL_STATUS_LABELS } from '@/parser/format.utils';
|
|
14
16
|
|
|
17
|
+
/**
|
|
18
|
+
* 标题式隔行值认定键集(`## 键` + 下一非空独立行为值的复合方言)
|
|
19
|
+
*
|
|
20
|
+
* 键限集合防误吞:非元信息键的 `## 节` 仍按正文边界终止头部;值行另有独立
|
|
21
|
+
* 短行约束(见 parseHeaderDialect 形态七),流水段落首行不受影响。
|
|
22
|
+
*/
|
|
23
|
+
export const HEADING_ALONE_METADATA_KEYS: ReadonlySet<string> = new Set([
|
|
24
|
+
'优先级',
|
|
25
|
+
'状态',
|
|
26
|
+
'立案日期',
|
|
27
|
+
'日期',
|
|
28
|
+
'所属域',
|
|
29
|
+
'来源',
|
|
30
|
+
'严重程度',
|
|
31
|
+
'严重度',
|
|
32
|
+
]);
|
|
33
|
+
|
|
15
34
|
/**
|
|
16
35
|
* 字段规范序(排名值越小越靠前;未入表字段按首现序殿后)
|
|
17
36
|
*/
|
|
@@ -86,7 +105,8 @@ export type ParsedHeader = {
|
|
|
86
105
|
};
|
|
87
106
|
|
|
88
107
|
/**
|
|
89
|
-
* 解析头部方言(五种形态逐行尝试:粗体标签 / 列表式 / 标题式 / 管道引用行 /
|
|
108
|
+
* 解析头部方言(五种形态逐行尝试:粗体标签 / 列表式 / 标题式 / 管道引用行 / 表格;
|
|
109
|
+
* 另含两类复合形态:引用行内粗体键、标题式隔行值)
|
|
90
110
|
*
|
|
91
111
|
* @param lines - 文档行数组
|
|
92
112
|
* @returns 头部解析产物
|
|
@@ -113,6 +133,71 @@ export function parseHeaderDialect(lines: string[]): ParsedHeader {
|
|
|
113
133
|
*/
|
|
114
134
|
const line = lines[index] ?? '';
|
|
115
135
|
|
|
136
|
+
/**
|
|
137
|
+
* 形态七:标题式隔行值(`## 键` 单独成行 + 下一非空独立行为值)
|
|
138
|
+
*
|
|
139
|
+
* 值行须为独立短行:非结构前缀(# > * | ` -)、非键值形态自身、长度上限
|
|
140
|
+
* 40——三者合力防吞流水段落首行;键限 HEADING_ALONE_METADATA_KEYS。
|
|
141
|
+
*/
|
|
142
|
+
const aloneMatch = /^##\s+([^::]{1,10})\s*$/.exec(line);
|
|
143
|
+
|
|
144
|
+
if (aloneMatch !== null && HEADING_ALONE_METADATA_KEYS.has(aloneMatch[1]?.trim() ?? '')) {
|
|
145
|
+
/**
|
|
146
|
+
* 值行下标(跳过标题行与值行之间的空行)
|
|
147
|
+
*/
|
|
148
|
+
let valueIndex = index + 1;
|
|
149
|
+
|
|
150
|
+
while (valueIndex < lines.length && (lines[valueIndex] ?? '').trim() === '') {
|
|
151
|
+
valueIndex += 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 候选值行(去空白)
|
|
156
|
+
*/
|
|
157
|
+
const valueLine = (lines[valueIndex] ?? '').trim();
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 独立短行判定(非结构前缀 / 非键值形态 / 长度上限)
|
|
161
|
+
*/
|
|
162
|
+
const isBareValue =
|
|
163
|
+
valueLine !== '' &&
|
|
164
|
+
!/^[#>*|`-]/.test(valueLine) &&
|
|
165
|
+
!/^[^::]{1,8}[::]/.test(valueLine) &&
|
|
166
|
+
valueLine.length <= 40;
|
|
167
|
+
|
|
168
|
+
if (isBareValue) {
|
|
169
|
+
fields.push({ key: aloneMatch[1]?.trim() ?? '', value: valueLine });
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 值后空行下标(跳过值行与下一非空行之间的空行)
|
|
173
|
+
*/
|
|
174
|
+
let afterBlanks = valueIndex + 1;
|
|
175
|
+
|
|
176
|
+
while (afterBlanks < lines.length && (lines[afterBlanks] ?? '').trim() === '') {
|
|
177
|
+
afterBlanks += 1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 后继非空行(判定是否又一标题式隔行值对——是则越过空行续扫,否则
|
|
182
|
+
* 空行照常终止头部区)
|
|
183
|
+
*/
|
|
184
|
+
const nextLine = lines[afterBlanks] ?? '';
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 后继行的标题式隔行值匹配(连续元信息对判定)
|
|
188
|
+
*/
|
|
189
|
+
const nextAloneMatch = /^##\s+([^::]{1,10})\s*$/.exec(nextLine);
|
|
190
|
+
|
|
191
|
+
index =
|
|
192
|
+
nextAloneMatch !== null &&
|
|
193
|
+
HEADING_ALONE_METADATA_KEYS.has(nextAloneMatch[1]?.trim() ?? '')
|
|
194
|
+
? afterBlanks - 1
|
|
195
|
+
: valueIndex;
|
|
196
|
+
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
116
201
|
if (line.startsWith('## ') || (line === '' && fields.length > 0 && index > 4)) {
|
|
117
202
|
bodyStartIndex = index;
|
|
118
203
|
|
|
@@ -135,9 +220,9 @@ export function parseHeaderDialect(lines: string[]): ParsedHeader {
|
|
|
135
220
|
}
|
|
136
221
|
|
|
137
222
|
/**
|
|
138
|
-
*
|
|
223
|
+
* 形态二/四/六:粗体标签行 / 列表式 / 列表+粗体复合(`- **键**: 值`)
|
|
139
224
|
*/
|
|
140
|
-
const labeledMatch = /^(?:\*\*|- )([^*::]{1,8})(?:\*\*)?\s*[::]\s*(.+)$/.exec(line);
|
|
225
|
+
const labeledMatch = /^(?:\*\*|- \*\*|- )([^*::]{1,8})(?:\*\*)?\s*[::]\s*(.+)$/.exec(line);
|
|
141
226
|
|
|
142
227
|
if (labeledMatch !== null) {
|
|
143
228
|
fields.push({ key: labeledMatch[1]?.trim() ?? '', value: labeledMatch[2]?.trim() ?? '' });
|
|
@@ -145,6 +230,60 @@ export function parseHeaderDialect(lines: string[]): ParsedHeader {
|
|
|
145
230
|
continue;
|
|
146
231
|
}
|
|
147
232
|
|
|
233
|
+
/**
|
|
234
|
+
* 形态七:多行元信息(`- 键:` 空值 + 缩进子项——子项文本以「;」并入单行值)
|
|
235
|
+
*/
|
|
236
|
+
const multiLineKeyMatch = /^(?:\*\*|- \*\*|- )([^*::]{1,8})(?:\*\*)?\s*[::]\s*$/.exec(line);
|
|
237
|
+
|
|
238
|
+
if (multiLineKeyMatch !== null) {
|
|
239
|
+
/**
|
|
240
|
+
* 子项行收集器(缩进深于键行者;子项文本去列表标记后原样保留)
|
|
241
|
+
*/
|
|
242
|
+
const childTexts: string[] = [];
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 键行缩进宽度
|
|
246
|
+
*/
|
|
247
|
+
const keyIndent = line.length - line.trimStart().length;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 子项游标
|
|
251
|
+
*/
|
|
252
|
+
let childIndex = index + 1;
|
|
253
|
+
|
|
254
|
+
while (childIndex < lines.length) {
|
|
255
|
+
/**
|
|
256
|
+
* 候选子项行
|
|
257
|
+
*/
|
|
258
|
+
const childLine = lines[childIndex] ?? '';
|
|
259
|
+
|
|
260
|
+
if (childLine.trim() === '') {
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 子项缩进
|
|
266
|
+
*/
|
|
267
|
+
const childIndent = childLine.length - childLine.trimStart().length;
|
|
268
|
+
|
|
269
|
+
if (childIndent <= keyIndent) {
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
childTexts.push(childLine.trim().replace(/^-\s*/, ''));
|
|
274
|
+
|
|
275
|
+
childIndex += 1;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (childTexts.length > 0) {
|
|
279
|
+
fields.push({ key: multiLineKeyMatch[1]?.trim() ?? '', value: childTexts.join(';') });
|
|
280
|
+
|
|
281
|
+
index = childIndex - 1;
|
|
282
|
+
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
148
287
|
/**
|
|
149
288
|
* 形态一:单行管道引用行(`> 优先级 | 状态 | …`)
|
|
150
289
|
*/
|
|
@@ -164,12 +303,15 @@ export function parseHeaderDialect(lines: string[]): ParsedHeader {
|
|
|
164
303
|
|
|
165
304
|
for (const segment of segments) {
|
|
166
305
|
/**
|
|
167
|
-
*
|
|
306
|
+
* 段内键值切分(键清理粗体包裹——`> **键**: 值` 复合形态与粗体标签行同口径)
|
|
168
307
|
*/
|
|
169
308
|
const pairMatch = /^([^::]{1,8})\s*[::]\s*(.+)$/.exec(segment);
|
|
170
309
|
|
|
171
310
|
if (pairMatch !== null) {
|
|
172
|
-
fields.push({
|
|
311
|
+
fields.push({
|
|
312
|
+
key: (pairMatch[1] ?? '').replace(/^\*+|\*+$/g, '').trim(),
|
|
313
|
+
value: pairMatch[2]?.trim() ?? '',
|
|
314
|
+
});
|
|
173
315
|
} else {
|
|
174
316
|
unmatched.push(segment);
|
|
175
317
|
}
|
|
@@ -497,6 +497,14 @@ export function parseIssueDoc(
|
|
|
497
497
|
*/
|
|
498
498
|
const line = lines[index] ?? '';
|
|
499
499
|
|
|
500
|
+
/**
|
|
501
|
+
* 标题行跳过(标题正文可含「连接状态:」类词组——如 printer 刷屏案
|
|
502
|
+
* 标题,非元信息状态行;命中会吞掉后续真状态行致 status-line-missing)
|
|
503
|
+
*/
|
|
504
|
+
if (line.startsWith('#')) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
|
|
500
508
|
if (STATUS_LINE_LOCATE_PATTERN.test(line)) {
|
|
501
509
|
statusLine = line.trim();
|
|
502
510
|
statusLineNumber = index + 1;
|