@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
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* migrate-legacy 纯变换核(无 IO,可测性单一来源)
|
|
3
3
|
*
|
|
4
4
|
* 五方言解析(含表格)→ 字段别名 → pre-emoji 状态词规范化 → H1 编号规范 →
|
|
5
|
-
* 处置段标题链对齐 →
|
|
6
|
-
*
|
|
5
|
+
* 处置段标题链对齐 → 必填元信息回填 → 体首残留元信息块吸收与键去重收敛。
|
|
6
|
+
* 全部变换可幂等重放,变换台账逐条记录,供命令层零丢失守卫与人工复核消费。
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { parseHeaderDialect } from '@/normalize/normalize-header.commands';
|
|
@@ -12,9 +12,12 @@ import {
|
|
|
12
12
|
classifyStatusLine,
|
|
13
13
|
DISPOSITION_PLACEHOLDER_PATTERN,
|
|
14
14
|
DISPOSITION_PLACEHOLDER_WINDOW_LINES,
|
|
15
|
-
LEGACY_STATUS_VOCABULARY,
|
|
16
15
|
} from '@/parser/format.utils';
|
|
17
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
|
+
|
|
18
21
|
/**
|
|
19
22
|
* 单文件迁移变换台账条目
|
|
20
23
|
*/
|
|
@@ -31,7 +34,9 @@ export type MigrationTransformation = {
|
|
|
31
34
|
| 'h1-numbered'
|
|
32
35
|
| 'disposition-renamed'
|
|
33
36
|
| 'disposition-appended'
|
|
34
|
-
| 'required-backfilled'
|
|
37
|
+
| 'required-backfilled'
|
|
38
|
+
| 'header-converged'
|
|
39
|
+
| 'duplicate-field-converged';
|
|
35
40
|
|
|
36
41
|
/**
|
|
37
42
|
* 变换说明(原值 → 新值)
|
|
@@ -62,6 +67,11 @@ export type MigrateCoreResult = {
|
|
|
62
67
|
* 变换台账(rewritten 时非空)
|
|
63
68
|
*/
|
|
64
69
|
transformations: MigrationTransformation[];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 来源让位标记(回填缺省让位原始语义——守卫对来源键放行依据)
|
|
73
|
+
*/
|
|
74
|
+
sourceReplaced: boolean;
|
|
65
75
|
};
|
|
66
76
|
|
|
67
77
|
/**
|
|
@@ -101,241 +111,6 @@ export type MigrateCoreOptions = {
|
|
|
101
111
|
status?: 'red' | 'green';
|
|
102
112
|
};
|
|
103
113
|
|
|
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
114
|
/**
|
|
340
115
|
* 处置段候选标题链(apex-energy-ktv 全量段名词频盘点驱动;验证族 > 治理记录族 >
|
|
341
116
|
* 变更/修复族。计划性段名(治理方案/治理方向/修复方向/处置方向)不属处置记录,排除)
|
|
@@ -485,6 +260,218 @@ const HEADER_FIELD_ORDER: readonly string[] = [
|
|
|
485
260
|
'阻塞',
|
|
486
261
|
];
|
|
487
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
|
+
|
|
488
475
|
/**
|
|
489
476
|
* 执行单文档纯变换
|
|
490
477
|
*
|
|
@@ -519,6 +506,7 @@ export function migrateLegacyCore(raw: string, options: MigrateCoreOptions): Mig
|
|
|
519
506
|
content: raw,
|
|
520
507
|
message: '无可识别状态行——灯位属语义判断,交人工(--status red|green 显式定档)',
|
|
521
508
|
transformations: [],
|
|
509
|
+
sourceReplaced: false,
|
|
522
510
|
};
|
|
523
511
|
}
|
|
524
512
|
|
|
@@ -530,14 +518,27 @@ export function migrateLegacyCore(raw: string, options: MigrateCoreOptions): Mig
|
|
|
530
518
|
: cleanValue(parsed.fields[statusIndex ?? 0]?.value ?? '');
|
|
531
519
|
|
|
532
520
|
/**
|
|
533
|
-
*
|
|
521
|
+
* 已规范状态标记(规范五态或 emoji 旧变体——值原样保留,仅走收敛通道:
|
|
522
|
+
* 体首残留元信息块吸收与键去重,不做状态词规范化)
|
|
523
|
+
*/
|
|
524
|
+
const alreadyCanonical = !injected && classifyStatusLine(`状态: ${originalStatus}`) !== 'unknown';
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* 超长状态拆分产物(null 未触发;'manual' 词表未命中交人工)
|
|
534
528
|
*/
|
|
535
|
-
|
|
529
|
+
const longSplit = probeLongStatusSplit({
|
|
530
|
+
injected,
|
|
531
|
+
alreadyCanonical,
|
|
532
|
+
originalStatus,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
if (longSplit === 'manual') {
|
|
536
536
|
return {
|
|
537
|
-
outcome: '
|
|
537
|
+
outcome: 'skipped',
|
|
538
538
|
content: raw,
|
|
539
|
-
message:
|
|
539
|
+
message: `状态值超长(${originalStatus.length} 字符——段落级文本混入状态行且词表未命中)——交人工拆分`,
|
|
540
540
|
transformations: [],
|
|
541
|
+
sourceReplaced: false,
|
|
541
542
|
};
|
|
542
543
|
}
|
|
543
544
|
|
|
@@ -561,102 +562,23 @@ export function migrateLegacyCore(raw: string, options: MigrateCoreOptions): Mig
|
|
|
561
562
|
}
|
|
562
563
|
|
|
563
564
|
/**
|
|
564
|
-
*
|
|
565
|
-
*/
|
|
566
|
-
const filingIndex = fields.findIndex((field) => field.key === '立案日期');
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* 旧字段名「日期」下标(方言主流的立案日期载体)
|
|
565
|
+
* 必填回填(别名/立案日期/优先级——缺省未给交人工)
|
|
570
566
|
*/
|
|
571
|
-
const
|
|
567
|
+
const backfill = backfillRequiredFields({
|
|
568
|
+
fields,
|
|
569
|
+
transformations,
|
|
570
|
+
defaultPriority: options.defaultPriority,
|
|
571
|
+
defaultFilingDate: options.defaultFilingDate,
|
|
572
|
+
});
|
|
572
573
|
|
|
573
|
-
if (
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
});
|
|
574
|
+
if (backfill.skipMessage !== null) {
|
|
575
|
+
return {
|
|
576
|
+
outcome: 'skipped',
|
|
577
|
+
content: raw,
|
|
578
|
+
message: backfill.skipMessage,
|
|
579
|
+
transformations: [],
|
|
580
|
+
sourceReplaced: false,
|
|
581
|
+
};
|
|
660
582
|
}
|
|
661
583
|
|
|
662
584
|
/**
|
|
@@ -665,7 +587,7 @@ export function migrateLegacyCore(raw: string, options: MigrateCoreOptions): Mig
|
|
|
665
587
|
const injectedTier: 'green' | 'red' = options.status === 'green' ? 'green' : 'red';
|
|
666
588
|
|
|
667
589
|
/**
|
|
668
|
-
*
|
|
590
|
+
* 状态词规范化(注入/已规范通道跳过词表判定:注入为规范标签;已规范值原样保留)
|
|
669
591
|
*/
|
|
670
592
|
const canonicalStatus = injected
|
|
671
593
|
? {
|
|
@@ -673,7 +595,23 @@ export function migrateLegacyCore(raw: string, options: MigrateCoreOptions): Mig
|
|
|
673
595
|
tier: injectedTier,
|
|
674
596
|
word: '<无状态行>',
|
|
675
597
|
}
|
|
676
|
-
:
|
|
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
|
|
609
|
+
? {
|
|
610
|
+
value: longSplit.canonical,
|
|
611
|
+
tier: longSplit.tier,
|
|
612
|
+
word: '<超长拆分>',
|
|
613
|
+
}
|
|
614
|
+
: canonicalizeStatusValue(originalStatus);
|
|
677
615
|
|
|
678
616
|
if (canonicalStatus === null) {
|
|
679
617
|
return {
|
|
@@ -681,9 +619,22 @@ export function migrateLegacyCore(raw: string, options: MigrateCoreOptions): Mig
|
|
|
681
619
|
content: raw,
|
|
682
620
|
message: `状态值「${originalStatus}」不可机械迁移(冻结/取消无据/黄灯无因/词表未识别)——交人工`,
|
|
683
621
|
transformations: [],
|
|
622
|
+
sourceReplaced: false,
|
|
684
623
|
};
|
|
685
624
|
}
|
|
686
625
|
|
|
626
|
+
/**
|
|
627
|
+
* 超长状态拆分产物:短规范状态承载词表档位,全文说明落处置摘要字段
|
|
628
|
+
*/
|
|
629
|
+
if (longSplit !== null && !fields.some((field) => field.key === '处置摘要')) {
|
|
630
|
+
fields.push({ key: '处置摘要', value: longSplit.summary });
|
|
631
|
+
|
|
632
|
+
transformations.push({
|
|
633
|
+
kind: 'status-canonicalized',
|
|
634
|
+
detail: `状态值超长拆分:${longSplit.summary.slice(0, 24)}… → 短规范状态 + 处置摘要字段(${longSplit.summary.length} 字符全文承载)`,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
687
638
|
/**
|
|
688
639
|
* 处置段对齐(绿灯族;黄灯降级案同样需要对齐后的段供回填)
|
|
689
640
|
*/
|
|
@@ -694,6 +645,26 @@ export function migrateLegacyCore(raw: string, options: MigrateCoreOptions): Mig
|
|
|
694
645
|
*/
|
|
695
646
|
let bodyLines = lines.slice(parsed.bodyStartIndex);
|
|
696
647
|
|
|
648
|
+
/**
|
|
649
|
+
* 体首残留元信息块吸收(历史批量清偿事故形态:方言行未被解析而落 residual、
|
|
650
|
+
* 注入回填后又在正文首部原样存留——吸收回头部字段并随后键去重收敛)
|
|
651
|
+
*/
|
|
652
|
+
const absorbed = absorbLeadingMetadata(bodyLines, fields);
|
|
653
|
+
|
|
654
|
+
if (absorbed.consumedLines > 0) {
|
|
655
|
+
bodyLines = absorbed.restLines;
|
|
656
|
+
|
|
657
|
+
transformations.push({
|
|
658
|
+
kind: 'header-converged',
|
|
659
|
+
detail: `体首残留元信息块吸收 ${absorbed.consumedLines} 行(键去重收敛见产物)`,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* 键去重与语义让位(状态/来源:注入定档/回填标记让位于吸收到的原始语义)
|
|
665
|
+
*/
|
|
666
|
+
const convergence = convergeDuplicateFields(fields, transformations);
|
|
667
|
+
|
|
697
668
|
/**
|
|
698
669
|
* 处置段对齐产物(非绿灯族不触发)
|
|
699
670
|
*/
|
|
@@ -709,11 +680,12 @@ export function migrateLegacyCore(raw: string, options: MigrateCoreOptions): Mig
|
|
|
709
680
|
* 最终状态值(占位降级:历史绿词但验证口径为占位——诚实黄灯;注入通道定档为人工语义不降级)
|
|
710
681
|
*/
|
|
711
682
|
const finalStatus =
|
|
712
|
-
|
|
683
|
+
convergence.statusValue ??
|
|
684
|
+
(!injected && alignment.placeholder
|
|
713
685
|
? `🟡 处理中(历史标记已处置但验证口径为占位,清偿时待核实;原「${canonicalStatus.word}」)`
|
|
714
|
-
: canonicalStatus.value;
|
|
686
|
+
: canonicalStatus.value);
|
|
715
687
|
|
|
716
|
-
if (!injected) {
|
|
688
|
+
if (!injected && !alreadyCanonical) {
|
|
717
689
|
if (alignment.placeholder) {
|
|
718
690
|
transformations.push({
|
|
719
691
|
kind: 'status-downgraded-placeholder',
|
|
@@ -737,51 +709,42 @@ export function migrateLegacyCore(raw: string, options: MigrateCoreOptions): Mig
|
|
|
737
709
|
}
|
|
738
710
|
|
|
739
711
|
/**
|
|
740
|
-
*
|
|
712
|
+
* 来源与所属域回填(缺省未给交人工)
|
|
741
713
|
*/
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
transformations: [],
|
|
749
|
-
};
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
fields.push({ key: '来源', value: options.defaultSource });
|
|
753
|
-
|
|
754
|
-
transformations.push({ kind: 'required-backfilled', detail: '来源 ← 缺省回填' });
|
|
755
|
-
}
|
|
714
|
+
const tailBackfill = backfillSourceAndDomain({
|
|
715
|
+
fields,
|
|
716
|
+
transformations,
|
|
717
|
+
defaultSource: options.defaultSource,
|
|
718
|
+
scopeTitle: options.scopeTitle,
|
|
719
|
+
});
|
|
756
720
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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: [] };
|
|
721
|
+
if (tailBackfill.skipMessage !== null) {
|
|
722
|
+
return {
|
|
723
|
+
outcome: 'skipped',
|
|
724
|
+
content: raw,
|
|
725
|
+
message: tailBackfill.skipMessage,
|
|
726
|
+
transformations: [],
|
|
727
|
+
sourceReplaced: false,
|
|
728
|
+
};
|
|
773
729
|
}
|
|
774
730
|
|
|
775
731
|
/**
|
|
776
|
-
*
|
|
732
|
+
* H1 规范化(无一级标题交人工;编号规范见变换台账)
|
|
777
733
|
*/
|
|
778
|
-
const
|
|
734
|
+
const h1 = canonicalizeHeaderH1({
|
|
735
|
+
lines,
|
|
736
|
+
number: options.number,
|
|
737
|
+
transformations,
|
|
738
|
+
});
|
|
779
739
|
|
|
780
|
-
if (
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
740
|
+
if (h1 === null) {
|
|
741
|
+
return {
|
|
742
|
+
outcome: 'skipped',
|
|
743
|
+
content: raw,
|
|
744
|
+
message: '无一级标题——交人工',
|
|
745
|
+
transformations: [],
|
|
746
|
+
sourceReplaced: false,
|
|
747
|
+
};
|
|
785
748
|
}
|
|
786
749
|
|
|
787
750
|
/**
|
|
@@ -792,21 +755,42 @@ export function migrateLegacyCore(raw: string, options: MigrateCoreOptions): Mig
|
|
|
792
755
|
...fields.filter((field) => !HEADER_FIELD_ORDER.includes(field.key)),
|
|
793
756
|
];
|
|
794
757
|
|
|
758
|
+
/**
|
|
759
|
+
* 正文前导空行剥离(分隔空行由组装层统一供给——保证已规范产物逐字幂等)
|
|
760
|
+
*/
|
|
761
|
+
let bodyStart = 0;
|
|
762
|
+
|
|
763
|
+
while (bodyStart < bodyLines.length && (bodyLines[bodyStart] ?? '').trim() === '') {
|
|
764
|
+
bodyStart += 1;
|
|
765
|
+
}
|
|
766
|
+
|
|
795
767
|
/**
|
|
796
768
|
* 产物全文
|
|
797
769
|
*/
|
|
798
770
|
const content = [
|
|
799
|
-
|
|
771
|
+
h1.line,
|
|
800
772
|
'',
|
|
801
773
|
...orderedFields.map((field) => `**${field.key}**: ${field.value}`),
|
|
802
774
|
...(parsed.residuals.length > 0 ? ['', ...parsed.residuals] : []),
|
|
803
775
|
'',
|
|
804
|
-
...bodyLines,
|
|
776
|
+
...bodyLines.slice(bodyStart),
|
|
805
777
|
].join('\n');
|
|
806
778
|
|
|
807
779
|
if (content === raw) {
|
|
808
|
-
return {
|
|
780
|
+
return {
|
|
781
|
+
outcome: 'unchanged',
|
|
782
|
+
content: raw,
|
|
783
|
+
message: '已是规范形态',
|
|
784
|
+
transformations: [],
|
|
785
|
+
sourceReplaced: false,
|
|
786
|
+
};
|
|
809
787
|
}
|
|
810
788
|
|
|
811
|
-
return {
|
|
789
|
+
return {
|
|
790
|
+
outcome: 'rewritten',
|
|
791
|
+
content,
|
|
792
|
+
message: '存量头部已清偿为规范形态',
|
|
793
|
+
transformations,
|
|
794
|
+
sourceReplaced: convergence.sourceReplaced,
|
|
795
|
+
};
|
|
812
796
|
}
|