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