@longzai-intelligence-issues/ledger 0.0.1 → 0.0.2
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-build.log +5 -0
- package/.turbo/turbo-lint.log +7 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +17 -0
- package/dist/index.d.ts +317 -7
- package/dist/index.js +23 -22
- package/package.json +4 -8
- package/src/__tests__/close/verify-close.commands.test.ts +57 -0
- package/src/__tests__/lint/lint.core.test.ts +23 -0
- package/src/__tests__/migrate/migrate-legacy.commands.test.ts +519 -0
- package/src/__tests__/template/issue-template.renderer.test.ts +10 -0
- package/src/index.ts +4 -0
- package/src/lint/lint.core.ts +12 -1
- package/src/migrate/migrate-legacy.commands.ts +417 -0
- package/src/migrate/migrate-legacy.core.ts +812 -0
- package/src/normalize/normalize-header.commands.ts +99 -8
- package/src/numbering/numbering.core.ts +3 -3
- package/src/parser/format.utils.ts +76 -0
- package/src/template/issue-template.renderer.ts +15 -4
- package/tsconfig/.cache/build.tsbuildinfo +1 -1
- package/tsconfig/.cache/node.tsbuildinfo +1 -1
- package/tsconfig/.cache/test.tsbuildinfo +1 -1
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migrate-legacy 纯变换核(无 IO,可测性单一来源)
|
|
3
|
+
*
|
|
4
|
+
* 五方言解析(含表格)→ 字段别名 → pre-emoji 状态词规范化 → H1 编号规范 →
|
|
5
|
+
* 处置段标题链对齐 → 必填元信息回填。全部变换可幂等重放,变换台账逐条记录,
|
|
6
|
+
* 供命令层零丢失守卫与人工复核消费。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { parseHeaderDialect } from '@/normalize/normalize-header.commands';
|
|
10
|
+
import {
|
|
11
|
+
CANONICAL_STATUS_LABELS,
|
|
12
|
+
classifyStatusLine,
|
|
13
|
+
DISPOSITION_PLACEHOLDER_PATTERN,
|
|
14
|
+
DISPOSITION_PLACEHOLDER_WINDOW_LINES,
|
|
15
|
+
LEGACY_STATUS_VOCABULARY,
|
|
16
|
+
} from '@/parser/format.utils';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 单文件迁移变换台账条目
|
|
20
|
+
*/
|
|
21
|
+
export type MigrationTransformation = {
|
|
22
|
+
/**
|
|
23
|
+
* 变换类别(人工复核消费的稳定标识)
|
|
24
|
+
*/
|
|
25
|
+
kind:
|
|
26
|
+
| 'status-canonicalized'
|
|
27
|
+
| 'status-downgraded-placeholder'
|
|
28
|
+
| 'field-alias'
|
|
29
|
+
| 'priority-mapped'
|
|
30
|
+
| 'priority-backfilled'
|
|
31
|
+
| 'h1-numbered'
|
|
32
|
+
| 'disposition-renamed'
|
|
33
|
+
| 'disposition-appended'
|
|
34
|
+
| 'required-backfilled';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 变换说明(原值 → 新值)
|
|
38
|
+
*/
|
|
39
|
+
detail: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 纯变换产物
|
|
44
|
+
*/
|
|
45
|
+
export type MigrateCoreResult = {
|
|
46
|
+
/**
|
|
47
|
+
* 迁移形态
|
|
48
|
+
*/
|
|
49
|
+
outcome: 'rewritten' | 'unchanged' | 'skipped';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 产物全文(skipped 时为原文)
|
|
53
|
+
*/
|
|
54
|
+
content: string;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* skip/unchanged 说明
|
|
58
|
+
*/
|
|
59
|
+
message: string;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 变换台账(rewritten 时非空)
|
|
63
|
+
*/
|
|
64
|
+
transformations: MigrationTransformation[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 迁移选项(引擎零仓库知识——仓库特有缺省值全部经入参注入)
|
|
69
|
+
*/
|
|
70
|
+
export type MigrateCoreOptions = {
|
|
71
|
+
/**
|
|
72
|
+
* 编号(文件名前缀,H1 规范化用)
|
|
73
|
+
*/
|
|
74
|
+
number: string;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 所属域回填值(scope title)
|
|
78
|
+
*/
|
|
79
|
+
scopeTitle: string;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 优先级缺省(原案无优先级与严重程度时回填)
|
|
83
|
+
*/
|
|
84
|
+
defaultPriority?: 'P1' | 'P2' | 'P3';
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 来源回填缺省文案
|
|
88
|
+
*/
|
|
89
|
+
defaultSource?: string;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 立案日期回填缺省(原案无日期字段时;未给则 skipped 交人工)
|
|
93
|
+
*/
|
|
94
|
+
defaultFilingDate?: string;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 无状态行历史文档的显式灯位注入(--status;灯位属语义判断由操作者定档,
|
|
98
|
+
* 引擎仅承接机械迁移。红/绿两态——黄灯须 ≥4 字挂起原因不适用固定文案注入,
|
|
99
|
+
* 冻结唯一合法路径是 freeze 命令)
|
|
100
|
+
*/
|
|
101
|
+
status?: 'red' | 'green';
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 状态词匹配产物
|
|
106
|
+
*/
|
|
107
|
+
type StatusWordMatch = {
|
|
108
|
+
/**
|
|
109
|
+
* 词表档位
|
|
110
|
+
*/
|
|
111
|
+
tier: 'green' | 'red' | 'deferred' | 'canceled' | 'inProgress' | 'frozen';
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 命中词
|
|
115
|
+
*/
|
|
116
|
+
word: string;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 状态值规范化产物
|
|
121
|
+
*/
|
|
122
|
+
type CanonicalStatusValue = {
|
|
123
|
+
/**
|
|
124
|
+
* 规范状态值(五态灯位 + 溯源括注)
|
|
125
|
+
*/
|
|
126
|
+
value: string;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 词表档位
|
|
130
|
+
*/
|
|
131
|
+
tier: StatusWordMatch['tier'];
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 命中词(原词溯源)
|
|
135
|
+
*/
|
|
136
|
+
word: string;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 清理字段值(去首尾空白与粗体星号——覆盖三种历史形态:整体包裹 `**值**`、
|
|
141
|
+
* 前导无尾随 `**值`、词中包裹 `**词**(括注)`)
|
|
142
|
+
*
|
|
143
|
+
* @param value - 原值
|
|
144
|
+
* @returns 清理后值
|
|
145
|
+
*/
|
|
146
|
+
export function cleanValue(value: string): string {
|
|
147
|
+
return value
|
|
148
|
+
.trim()
|
|
149
|
+
.replace(/^\*\*/, '')
|
|
150
|
+
.replace(/\*\*$/, '')
|
|
151
|
+
.replace(/\*\*(?=[((])/, '')
|
|
152
|
+
.trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 在状态值中匹配 pre-emoji 词表
|
|
157
|
+
*
|
|
158
|
+
* @param value - 清理后的状态值
|
|
159
|
+
* @returns 命中档位与词;未命中为 null
|
|
160
|
+
*/
|
|
161
|
+
function matchStatusWord(value: string): StatusWordMatch | null {
|
|
162
|
+
/**
|
|
163
|
+
* 档位遍历序(frozen 最先——冻结态优先识别并拒绝迁移)
|
|
164
|
+
*/
|
|
165
|
+
const tiers: StatusWordMatch['tier'][] = [
|
|
166
|
+
'frozen',
|
|
167
|
+
'green',
|
|
168
|
+
'deferred',
|
|
169
|
+
'canceled',
|
|
170
|
+
'inProgress',
|
|
171
|
+
'red',
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
for (const tier of tiers) {
|
|
175
|
+
for (const word of LEGACY_STATUS_VOCABULARY[tier]) {
|
|
176
|
+
if (value.startsWith(word)) {
|
|
177
|
+
return { tier, word };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 状态值切分产物(首个括注内容与词后剩余文本)
|
|
187
|
+
*/
|
|
188
|
+
type StatusValueSplit = {
|
|
189
|
+
/**
|
|
190
|
+
* 首个括注内容(无括注为空串)
|
|
191
|
+
*/
|
|
192
|
+
paren: string;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 括注后的剩余文本
|
|
196
|
+
*/
|
|
197
|
+
rest: string;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 提取状态值中的首个括注与其余文本
|
|
202
|
+
*
|
|
203
|
+
* @param value - 清理后的状态值
|
|
204
|
+
* @param word - 命中词
|
|
205
|
+
* @returns 括注内容与词后剩余文本
|
|
206
|
+
*/
|
|
207
|
+
function splitStatusValue(value: string, word: string): StatusValueSplit {
|
|
208
|
+
/**
|
|
209
|
+
* 词后文本
|
|
210
|
+
*/
|
|
211
|
+
const afterWord = value.slice(word.length).trim();
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 首个括注匹配
|
|
215
|
+
*/
|
|
216
|
+
const parenMatch = /^[((]([^))]*)[))]/.exec(afterWord);
|
|
217
|
+
|
|
218
|
+
if (parenMatch === null) {
|
|
219
|
+
return { paren: '', rest: afterWord };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
paren: (parenMatch[1] ?? '').trim(),
|
|
224
|
+
rest: afterWord.slice((parenMatch[0] ?? '').length).trim(),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 规范化状态值(pre-emoji 词 → 五态灯位;原词与括注零丢失保留)
|
|
230
|
+
*
|
|
231
|
+
* @param value - 清理后的状态值
|
|
232
|
+
* @returns 规范值与档位;不可机械迁移为 null(须人工)
|
|
233
|
+
*/
|
|
234
|
+
export function canonicalizeStatusValue(value: string): CanonicalStatusValue | null {
|
|
235
|
+
/**
|
|
236
|
+
* 词表命中
|
|
237
|
+
*/
|
|
238
|
+
const match = matchStatusWord(value);
|
|
239
|
+
|
|
240
|
+
if (match === null) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 括注与剩余文本
|
|
246
|
+
*/
|
|
247
|
+
const { paren, rest } = splitStatusValue(value, match.word);
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 括注段收集器(顺序:括注 → 剩余 → 原词溯源)
|
|
251
|
+
*/
|
|
252
|
+
const parts = [paren, rest, `原「${match.word}」`].filter((part) => part !== '');
|
|
253
|
+
|
|
254
|
+
switch (match.tier) {
|
|
255
|
+
case 'frozen':
|
|
256
|
+
return null;
|
|
257
|
+
|
|
258
|
+
case 'green':
|
|
259
|
+
return { value: `🟢 已处置(${parts.join(';')})`, tier: match.tier, word: match.word };
|
|
260
|
+
|
|
261
|
+
case 'red':
|
|
262
|
+
return { value: `🔴 待处理(${parts.join(';')})`, tier: match.tier, word: match.word };
|
|
263
|
+
|
|
264
|
+
case 'deferred':
|
|
265
|
+
return {
|
|
266
|
+
value: `🔴 待处理(${[paren === '' ? '延期' : paren, `原「${match.word}」`].join(';')})`,
|
|
267
|
+
tier: match.tier,
|
|
268
|
+
word: match.word,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
case 'canceled':
|
|
272
|
+
return paren === '' || paren.length < 4
|
|
273
|
+
? null
|
|
274
|
+
: { value: `⚫ 已取消(${parts.join(';')})`, tier: match.tier, word: match.word };
|
|
275
|
+
|
|
276
|
+
case 'inProgress':
|
|
277
|
+
return paren.length < 4
|
|
278
|
+
? null
|
|
279
|
+
: { value: `🟡 处理中(${paren})`, tier: match.tier, word: match.word };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* 严重程度/中文优先级 → P 档位映射
|
|
285
|
+
*
|
|
286
|
+
* @param value - 原值(高/中/低或 P1/P2/P3)
|
|
287
|
+
* @returns P 档位;不可映射为 null
|
|
288
|
+
*/
|
|
289
|
+
export function mapPriorityValue(value: string): 'P1' | 'P2' | 'P3' | null {
|
|
290
|
+
/**
|
|
291
|
+
* 严重程度中文档位与 P 档位的等价映射表(P 档位原样透传)
|
|
292
|
+
*/
|
|
293
|
+
const table: Record<string, 'P1' | 'P2' | 'P3'> = {
|
|
294
|
+
高: 'P1',
|
|
295
|
+
中: 'P2',
|
|
296
|
+
低: 'P3',
|
|
297
|
+
P1: 'P1',
|
|
298
|
+
P2: 'P2',
|
|
299
|
+
P3: 'P3',
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
return table[cleanValue(value)] ?? null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* H1 规范化产物
|
|
307
|
+
*/
|
|
308
|
+
type CanonicalH1 = {
|
|
309
|
+
/**
|
|
310
|
+
* 规范标题行(`# NNNN - 标题核心`)
|
|
311
|
+
*/
|
|
312
|
+
line: string;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 标题核心(剥离既有编号与分隔符变体后的标题文本)
|
|
316
|
+
*/
|
|
317
|
+
titleCore: string;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* H1 标题规范化:`# NNNN - 标题`(剥离既有编号与分隔符变体)
|
|
322
|
+
*
|
|
323
|
+
* @param h1 - 原一级标题行
|
|
324
|
+
* @param number - 文件名编号
|
|
325
|
+
* @returns 规范标题行与标题核心
|
|
326
|
+
*/
|
|
327
|
+
export function canonicalizeH1(h1: string, number: string): CanonicalH1 {
|
|
328
|
+
/**
|
|
329
|
+
* 标题核心(去行首 `# ` 与既有编号 + 分隔符变体)
|
|
330
|
+
*/
|
|
331
|
+
const titleCore = h1
|
|
332
|
+
.replace(/^#\s+/, '')
|
|
333
|
+
.replace(new RegExp(`^${number}\\s*(?:[-·..::]\\s*|\\s*)`), '')
|
|
334
|
+
.trim();
|
|
335
|
+
|
|
336
|
+
return { line: `# ${number} - ${titleCore}`, titleCore };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 处置段候选标题链(apex-energy-ktv 全量段名词频盘点驱动;验证族 > 治理记录族 >
|
|
341
|
+
* 变更/修复族。计划性段名(治理方案/治理方向/修复方向/处置方向)不属处置记录,排除)
|
|
342
|
+
*/
|
|
343
|
+
export const DISPOSITION_CANDIDATES: ReadonlyArray<{ pattern: RegExp; title: string }> = [
|
|
344
|
+
{ pattern: /^验证(结果)?(?!口径)/, title: '处置结果/验证口径' },
|
|
345
|
+
{
|
|
346
|
+
pattern: /^(治理记录|治理落地记录|治理落地|落地治理|治理(落地)|治理动作|治理$)/,
|
|
347
|
+
title: '处置结果/治理记录',
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
pattern: /^(修复(?!方向)|解决方案|变更内容|处理报告|处置(?!方向))/,
|
|
351
|
+
title: '处置结果/变更记录',
|
|
352
|
+
},
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 处置段对齐产物
|
|
357
|
+
*/
|
|
358
|
+
type DispositionAlignment = {
|
|
359
|
+
/**
|
|
360
|
+
* 对齐后正文行
|
|
361
|
+
*/
|
|
362
|
+
lines: string[];
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 对齐变换台账
|
|
366
|
+
*/
|
|
367
|
+
transformations: MigrationTransformation[];
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* 是否探测到处置段占位(触发黄灯降级)
|
|
371
|
+
*/
|
|
372
|
+
placeholder: boolean;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* 对齐处置段(绿灯族专属):候选标题改名 + 无候选段时追加验证口径段 + 占位降级探测
|
|
377
|
+
*
|
|
378
|
+
* @param bodyLines - 正文行(自 bodyStartIndex 起)
|
|
379
|
+
* @returns 对齐产物(正文行 + 台账 + 是否占位降级)
|
|
380
|
+
*/
|
|
381
|
+
function alignDisposition(bodyLines: string[]): DispositionAlignment {
|
|
382
|
+
/**
|
|
383
|
+
* 台账收集器
|
|
384
|
+
*/
|
|
385
|
+
const transformations: MigrationTransformation[] = [];
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* 候选命中下标与目标标题
|
|
389
|
+
*/
|
|
390
|
+
let hitIndex = -1;
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* 命中候选的改名目标标题(如「处置结果/验证口径」)
|
|
394
|
+
*/
|
|
395
|
+
let hitTitle = '';
|
|
396
|
+
|
|
397
|
+
for (const candidate of DISPOSITION_CANDIDATES) {
|
|
398
|
+
/**
|
|
399
|
+
* 候选标题行下标(精确行首匹配 `## ` + 模式)
|
|
400
|
+
*/
|
|
401
|
+
const index = bodyLines.findIndex(
|
|
402
|
+
(line) => line.startsWith('## ') && candidate.pattern.test(line.slice(3).trim()),
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
if (index !== -1) {
|
|
406
|
+
hitIndex = index;
|
|
407
|
+
|
|
408
|
+
hitTitle = candidate.title;
|
|
409
|
+
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* 已有含「处置结果」标题(前次迁移或原生)——不重复改名,不追加
|
|
416
|
+
*/
|
|
417
|
+
const alreadyDisposition = bodyLines.some(
|
|
418
|
+
(line) => line.startsWith('## ') && line.includes('处置结果'),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* 产物行
|
|
423
|
+
*/
|
|
424
|
+
const lines = [...bodyLines];
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* 占位降级标记
|
|
428
|
+
*/
|
|
429
|
+
let placeholder = false;
|
|
430
|
+
|
|
431
|
+
if (!alreadyDisposition && hitIndex !== -1) {
|
|
432
|
+
/**
|
|
433
|
+
* 原标题行
|
|
434
|
+
*/
|
|
435
|
+
const original = lines[hitIndex] ?? '';
|
|
436
|
+
|
|
437
|
+
lines[hitIndex] = `## ${hitTitle}`;
|
|
438
|
+
|
|
439
|
+
transformations.push({
|
|
440
|
+
kind: 'disposition-renamed',
|
|
441
|
+
detail: `${original} → ## ${hitTitle}`,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* 改名段起 8 行内占位探测
|
|
446
|
+
*/
|
|
447
|
+
const window = lines.slice(hitIndex + 1, hitIndex + 1 + DISPOSITION_PLACEHOLDER_WINDOW_LINES);
|
|
448
|
+
|
|
449
|
+
placeholder = window.some((line) => DISPOSITION_PLACEHOLDER_PATTERN.test(line));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* 追加条件:无任何处置段且改名来源非验证语义(变更记录族——缺独立验证段)
|
|
454
|
+
*/
|
|
455
|
+
const appendedNeeded =
|
|
456
|
+
!alreadyDisposition && (hitIndex === -1 || hitTitle === '处置结果/变更记录');
|
|
457
|
+
|
|
458
|
+
if (appendedNeeded && !placeholder) {
|
|
459
|
+
lines.push(
|
|
460
|
+
'',
|
|
461
|
+
'## 处置结果/验证口径',
|
|
462
|
+
'',
|
|
463
|
+
'(原案无独立验证段;处置证据见上文各节——格式清偿迁移回填)',
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
transformations.push({
|
|
467
|
+
kind: 'disposition-appended',
|
|
468
|
+
detail: '追加 ## 处置结果/验证口径(无独立验证段的事实回填说明)',
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return { lines, transformations, placeholder };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* 头部字段渲染序(模板 §八 规范序;未入表字段按首现序殿后)
|
|
477
|
+
*/
|
|
478
|
+
const HEADER_FIELD_ORDER: readonly string[] = [
|
|
479
|
+
'优先级',
|
|
480
|
+
'状态',
|
|
481
|
+
'立案日期',
|
|
482
|
+
'所属域',
|
|
483
|
+
'来源',
|
|
484
|
+
'沿革',
|
|
485
|
+
'阻塞',
|
|
486
|
+
];
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* 执行单文档纯变换
|
|
490
|
+
*
|
|
491
|
+
* @param raw - 文档全文
|
|
492
|
+
* @param options - 迁移选项
|
|
493
|
+
* @returns 纯变换产物
|
|
494
|
+
*/
|
|
495
|
+
export function migrateLegacyCore(raw: string, options: MigrateCoreOptions): MigrateCoreResult {
|
|
496
|
+
/**
|
|
497
|
+
* 文档行数组
|
|
498
|
+
*/
|
|
499
|
+
const lines = raw.split(/\r?\n/);
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* 头部解析产物(五方言)
|
|
503
|
+
*/
|
|
504
|
+
const parsed = parseHeaderDialect(lines);
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* 状态字段定位
|
|
508
|
+
*/
|
|
509
|
+
const statusIndex = parsed.fields.findIndex((field) => field.key === '状态');
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* 显式灯位注入标记(--status:无状态行历史文档的人工定档通道)
|
|
513
|
+
*/
|
|
514
|
+
const injected = statusIndex === -1 && (options.status === 'red' || options.status === 'green');
|
|
515
|
+
|
|
516
|
+
if (statusIndex === -1 && !injected) {
|
|
517
|
+
return {
|
|
518
|
+
outcome: 'skipped',
|
|
519
|
+
content: raw,
|
|
520
|
+
message: '无可识别状态行——灯位属语义判断,交人工(--status red|green 显式定档)',
|
|
521
|
+
transformations: [],
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* 原状态值(清理粗体包裹;注入通道为规范标签本身)
|
|
527
|
+
*/
|
|
528
|
+
const originalStatus = injected
|
|
529
|
+
? `${CANONICAL_STATUS_LABELS[options.status ?? 'red']}(存量清偿定档:原案无状态行,依结案内容人工定档)`
|
|
530
|
+
: cleanValue(parsed.fields[statusIndex ?? 0]?.value ?? '');
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* 已规范形态幂等判定(规范五态或 emoji 旧变体均不再迁移;注入通道不适用)
|
|
534
|
+
*/
|
|
535
|
+
if (!injected && classifyStatusLine(`状态: ${originalStatus}`) !== 'unknown') {
|
|
536
|
+
return {
|
|
537
|
+
outcome: 'unchanged',
|
|
538
|
+
content: raw,
|
|
539
|
+
message: '状态行已是规范五态形态',
|
|
540
|
+
transformations: [],
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* 变换台账
|
|
546
|
+
*/
|
|
547
|
+
const transformations: MigrationTransformation[] = [];
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* 字段工作副本(键值清理粗体包裹;注入通道补入状态字段)
|
|
551
|
+
*/
|
|
552
|
+
const fields = parsed.fields.map((field) => ({ key: field.key, value: cleanValue(field.value) }));
|
|
553
|
+
|
|
554
|
+
if (injected) {
|
|
555
|
+
fields.push({ key: '状态', value: originalStatus });
|
|
556
|
+
|
|
557
|
+
transformations.push({
|
|
558
|
+
kind: 'status-canonicalized',
|
|
559
|
+
detail: `<无状态行> → ${originalStatus}(--status 注入,人工定档)`,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* 字段别名:日期 → 立案日期(已有立案日期则保留原字段不动)
|
|
565
|
+
*/
|
|
566
|
+
const filingIndex = fields.findIndex((field) => field.key === '立案日期');
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* 旧字段名「日期」下标(方言主流的立案日期载体)
|
|
570
|
+
*/
|
|
571
|
+
const dateIndex = fields.findIndex((field) => field.key === '日期');
|
|
572
|
+
|
|
573
|
+
if (filingIndex === -1 && dateIndex !== -1) {
|
|
574
|
+
/**
|
|
575
|
+
* 待改名的日期字段
|
|
576
|
+
*/
|
|
577
|
+
const dateField = fields[dateIndex];
|
|
578
|
+
|
|
579
|
+
if (dateField !== undefined) {
|
|
580
|
+
fields[dateIndex] = { key: '立案日期', value: dateField.value };
|
|
581
|
+
|
|
582
|
+
transformations.push({
|
|
583
|
+
kind: 'field-alias',
|
|
584
|
+
detail: `日期 → 立案日期(${dateField.value})`,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* 立案日期缺省回填(未给缺省则 skipped 交人工)
|
|
591
|
+
*/
|
|
592
|
+
if (!fields.some((field) => field.key === '立案日期')) {
|
|
593
|
+
if (options.defaultFilingDate === undefined) {
|
|
594
|
+
return {
|
|
595
|
+
outcome: 'skipped',
|
|
596
|
+
content: raw,
|
|
597
|
+
message: '缺立案日期且未给回填缺省——交人工',
|
|
598
|
+
transformations: [],
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
fields.push({ key: '立案日期', value: options.defaultFilingDate });
|
|
603
|
+
|
|
604
|
+
transformations.push({
|
|
605
|
+
kind: 'required-backfilled',
|
|
606
|
+
detail: `立案日期 ← ${options.defaultFilingDate}`,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* 优先级:既有值映射 / 严重程度映射 / 缺省回填
|
|
612
|
+
*/
|
|
613
|
+
const priorityIndex = fields.findIndex((field) => field.key === '优先级');
|
|
614
|
+
|
|
615
|
+
if (priorityIndex !== -1) {
|
|
616
|
+
/**
|
|
617
|
+
* 既有优先级值
|
|
618
|
+
*/
|
|
619
|
+
const existing = fields[priorityIndex]?.value ?? '';
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* 映射产物(中文档位 → P 档位)
|
|
623
|
+
*/
|
|
624
|
+
const mapped = mapPriorityValue(existing);
|
|
625
|
+
|
|
626
|
+
if (mapped !== null && mapped !== existing) {
|
|
627
|
+
fields[priorityIndex] = { key: '优先级', value: mapped };
|
|
628
|
+
|
|
629
|
+
transformations.push({ kind: 'priority-mapped', detail: `优先级 ${existing} → ${mapped}` });
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
/**
|
|
633
|
+
* 严重程度映射或缺省回填
|
|
634
|
+
*/
|
|
635
|
+
const severityField = fields.find((field) => field.key === '严重程度');
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* 回填值
|
|
639
|
+
*/
|
|
640
|
+
const backfill =
|
|
641
|
+
(severityField !== undefined ? mapPriorityValue(severityField.value) : null) ??
|
|
642
|
+
options.defaultPriority ??
|
|
643
|
+
null;
|
|
644
|
+
|
|
645
|
+
if (backfill === null) {
|
|
646
|
+
return {
|
|
647
|
+
outcome: 'skipped',
|
|
648
|
+
content: raw,
|
|
649
|
+
message: '缺优先级且未给回填缺省——交人工',
|
|
650
|
+
transformations: [],
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
fields.push({ key: '优先级', value: backfill });
|
|
655
|
+
|
|
656
|
+
transformations.push({
|
|
657
|
+
kind: severityField !== undefined ? 'priority-mapped' : 'priority-backfilled',
|
|
658
|
+
detail: `优先级 ← ${backfill}${severityField !== undefined ? `(严重程度 ${severityField.value})` : '(缺省回填)'}`,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* 注入通道档位(红/绿直取;声明式注解替代断言窄化)
|
|
664
|
+
*/
|
|
665
|
+
const injectedTier: 'green' | 'red' = options.status === 'green' ? 'green' : 'red';
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* 状态词规范化(注入通道状态已是规范形态,跳过词表判定)
|
|
669
|
+
*/
|
|
670
|
+
const canonicalStatus = injected
|
|
671
|
+
? {
|
|
672
|
+
value: originalStatus,
|
|
673
|
+
tier: injectedTier,
|
|
674
|
+
word: '<无状态行>',
|
|
675
|
+
}
|
|
676
|
+
: canonicalizeStatusValue(originalStatus);
|
|
677
|
+
|
|
678
|
+
if (canonicalStatus === null) {
|
|
679
|
+
return {
|
|
680
|
+
outcome: 'skipped',
|
|
681
|
+
content: raw,
|
|
682
|
+
message: `状态值「${originalStatus}」不可机械迁移(冻结/取消无据/黄灯无因/词表未识别)——交人工`,
|
|
683
|
+
transformations: [],
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* 处置段对齐(绿灯族;黄灯降级案同样需要对齐后的段供回填)
|
|
689
|
+
*/
|
|
690
|
+
const isGreenFamily = canonicalStatus.tier === 'green';
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* 正文行
|
|
694
|
+
*/
|
|
695
|
+
let bodyLines = lines.slice(parsed.bodyStartIndex);
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* 处置段对齐产物(非绿灯族不触发)
|
|
699
|
+
*/
|
|
700
|
+
const alignment = isGreenFamily
|
|
701
|
+
? alignDisposition(bodyLines)
|
|
702
|
+
: { lines: bodyLines, transformations: [], placeholder: false };
|
|
703
|
+
|
|
704
|
+
bodyLines = alignment.lines;
|
|
705
|
+
|
|
706
|
+
transformations.push(...alignment.transformations);
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* 最终状态值(占位降级:历史绿词但验证口径为占位——诚实黄灯;注入通道定档为人工语义不降级)
|
|
710
|
+
*/
|
|
711
|
+
const finalStatus =
|
|
712
|
+
!injected && alignment.placeholder
|
|
713
|
+
? `🟡 处理中(历史标记已处置但验证口径为占位,清偿时待核实;原「${canonicalStatus.word}」)`
|
|
714
|
+
: canonicalStatus.value;
|
|
715
|
+
|
|
716
|
+
if (!injected) {
|
|
717
|
+
if (alignment.placeholder) {
|
|
718
|
+
transformations.push({
|
|
719
|
+
kind: 'status-downgraded-placeholder',
|
|
720
|
+
detail: `${originalStatus} → 🟡 处理中(处置段占位降级)`,
|
|
721
|
+
});
|
|
722
|
+
} else {
|
|
723
|
+
transformations.push({
|
|
724
|
+
kind: 'status-canonicalized',
|
|
725
|
+
detail: `${originalStatus} → ${canonicalStatus.value}`,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* 状态字段写回
|
|
732
|
+
*/
|
|
733
|
+
const statusWriteIndex = fields.findIndex((field) => field.key === '状态');
|
|
734
|
+
|
|
735
|
+
if (statusWriteIndex !== -1) {
|
|
736
|
+
fields[statusWriteIndex] = { key: '状态', value: finalStatus };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* 来源回填
|
|
741
|
+
*/
|
|
742
|
+
if (!fields.some((field) => field.key === '来源')) {
|
|
743
|
+
if (options.defaultSource === undefined) {
|
|
744
|
+
return {
|
|
745
|
+
outcome: 'skipped',
|
|
746
|
+
content: raw,
|
|
747
|
+
message: '缺来源且未给回填缺省——交人工',
|
|
748
|
+
transformations: [],
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
fields.push({ key: '来源', value: options.defaultSource });
|
|
753
|
+
|
|
754
|
+
transformations.push({ kind: 'required-backfilled', detail: '来源 ← 缺省回填' });
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* 所属域回填(scope title)
|
|
759
|
+
*/
|
|
760
|
+
if (!fields.some((field) => field.key === '所属域')) {
|
|
761
|
+
fields.push({ key: '所属域', value: options.scopeTitle });
|
|
762
|
+
|
|
763
|
+
transformations.push({ kind: 'required-backfilled', detail: `所属域 ← ${options.scopeTitle}` });
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* H1 规范化
|
|
768
|
+
*/
|
|
769
|
+
const h1Index = lines.findIndex((line) => line.startsWith('# '));
|
|
770
|
+
|
|
771
|
+
if (h1Index === -1) {
|
|
772
|
+
return { outcome: 'skipped', content: raw, message: '无一级标题——交人工', transformations: [] };
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* 规范 H1
|
|
777
|
+
*/
|
|
778
|
+
const canonicalH1 = canonicalizeH1(lines[h1Index] ?? '', options.number);
|
|
779
|
+
|
|
780
|
+
if (canonicalH1.line !== (lines[h1Index] ?? '')) {
|
|
781
|
+
transformations.push({
|
|
782
|
+
kind: 'h1-numbered',
|
|
783
|
+
detail: `${lines[h1Index]} → ${canonicalH1.line}`,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* 头部渲染序(规范序在前,未入表字段按首现序殿后)
|
|
789
|
+
*/
|
|
790
|
+
const orderedFields = [
|
|
791
|
+
...HEADER_FIELD_ORDER.flatMap((key) => fields.filter((field) => field.key === key)),
|
|
792
|
+
...fields.filter((field) => !HEADER_FIELD_ORDER.includes(field.key)),
|
|
793
|
+
];
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* 产物全文
|
|
797
|
+
*/
|
|
798
|
+
const content = [
|
|
799
|
+
canonicalH1.line,
|
|
800
|
+
'',
|
|
801
|
+
...orderedFields.map((field) => `**${field.key}**: ${field.value}`),
|
|
802
|
+
...(parsed.residuals.length > 0 ? ['', ...parsed.residuals] : []),
|
|
803
|
+
'',
|
|
804
|
+
...bodyLines,
|
|
805
|
+
].join('\n');
|
|
806
|
+
|
|
807
|
+
if (content === raw) {
|
|
808
|
+
return { outcome: 'unchanged', content: raw, message: '已是规范形态', transformations: [] };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return { outcome: 'rewritten', content, message: '存量头部已清偿为规范形态', transformations };
|
|
812
|
+
}
|