@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
|
@@ -104,6 +104,21 @@ describe('parseIssueDoc 状态行', () => {
|
|
|
104
104
|
expect(record?.status).toBe('legacy-yellow');
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
test('标题行内含「连接状态:」词组不误定位(跳过标题行命中真状态行)', () => {
|
|
108
|
+
const record = parseIssueDoc(
|
|
109
|
+
'0001-sample.md',
|
|
110
|
+
[
|
|
111
|
+
'# 0001 - 调试面板被「连接状态: API=不可达, WS=未配置」刷屏',
|
|
112
|
+
'',
|
|
113
|
+
'**状态**: 🟢 已处置(按方案 B 落地)',
|
|
114
|
+
].join('\n'),
|
|
115
|
+
0,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(record?.status).toBe('green');
|
|
119
|
+
expect(record?.statusLineNumber).toBe(3);
|
|
120
|
+
});
|
|
121
|
+
|
|
107
122
|
test('规范绿灯归类 green', () => {
|
|
108
123
|
const record = parseIssueDoc(
|
|
109
124
|
'0001-sample.md',
|
|
@@ -130,4 +130,14 @@ describe('renderRegistryReadmeStub', () => {
|
|
|
130
130
|
expect(stub.includes('🔴 待处理')).toBe(true);
|
|
131
131
|
expect(stub.includes('⚪ 已冻结')).toBe(true);
|
|
132
132
|
});
|
|
133
|
+
|
|
134
|
+
test('指南不在采用仓时(null)回退机器真源提示且不含死链', () => {
|
|
135
|
+
const stub = renderRegistryReadmeStub('项目全局', null);
|
|
136
|
+
|
|
137
|
+
expect(stub.includes('# 项目全局 issue 注册表')).toBe(true);
|
|
138
|
+
expect(stub.includes('lzi-issues template')).toBe(true);
|
|
139
|
+
expect(stub.includes('docs/guides/issue-file-format.md')).toBe(true);
|
|
140
|
+
expect(stub.includes('](../')).toBe(false);
|
|
141
|
+
expect(stub.includes('](../../')).toBe(false);
|
|
142
|
+
});
|
|
133
143
|
});
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,14 @@ export * from './lint/lint.core.js';
|
|
|
29
29
|
|
|
30
30
|
export * from './normalize/normalize-header.commands.js';
|
|
31
31
|
|
|
32
|
+
export * from './migrate/migrate-legacy.commands.js';
|
|
33
|
+
|
|
34
|
+
export * from './migrate/migrate-legacy.core.js';
|
|
35
|
+
|
|
36
|
+
export * from './migrate/migrate-legacy.vocab.js';
|
|
37
|
+
|
|
38
|
+
export * from './migrate/migrate-legacy.backfill.js';
|
|
39
|
+
|
|
32
40
|
export * from './numbering/numbering.commands.js';
|
|
33
41
|
|
|
34
42
|
export * from './numbering/numbering.core.js';
|
package/src/lint/lint.core.ts
CHANGED
|
@@ -259,6 +259,17 @@ export const CORE_LINT_RULES: readonly LintRule[] = [
|
|
|
259
259
|
return null;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
/**
|
|
263
|
+
* 所属 scope 数字政策(allow 档位整域放行,缺省 forbid 走白名单例外通道)
|
|
264
|
+
*/
|
|
265
|
+
const scopePolicy = ctx.config.scopes.find(
|
|
266
|
+
(scope) => scope.key === ctx.scopeKey,
|
|
267
|
+
)?.slugDigitPolicy;
|
|
268
|
+
|
|
269
|
+
if (scopePolicy === 'allow') {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
262
273
|
/**
|
|
263
274
|
* 仓库相对路径(白名单匹配口径)
|
|
264
275
|
*/
|
|
@@ -273,7 +284,58 @@ export const CORE_LINT_RULES: readonly LintRule[] = [
|
|
|
273
284
|
|
|
274
285
|
return whitelisted
|
|
275
286
|
? null
|
|
276
|
-
: `slug
|
|
287
|
+
: `slug 段含数字(scope 政策 slugDigitPolicy 或白名单 slugDigitAllowlist 登记):${record.file}`;
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: 'duplicate-metadata-key',
|
|
292
|
+
evaluate: (record) => {
|
|
293
|
+
/**
|
|
294
|
+
* 头部区行(首行起至首个二级标题,上限 40 行——覆盖规范头部与其后
|
|
295
|
+
* 紧邻的残留方言块)
|
|
296
|
+
*/
|
|
297
|
+
const headerLines: string[] = [];
|
|
298
|
+
|
|
299
|
+
for (const line of record.rawContent.split(/\r?\n/)) {
|
|
300
|
+
if (line.startsWith('## ') || headerLines.length >= 40) {
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
headerLines.push(line);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 规范键命中计数器
|
|
309
|
+
*/
|
|
310
|
+
const counts = new Map<string, number>();
|
|
311
|
+
|
|
312
|
+
for (const line of headerLines) {
|
|
313
|
+
/**
|
|
314
|
+
* 元信息行匹配(粗体 / 列表 / 列表+粗体复合前缀通吃)
|
|
315
|
+
*/
|
|
316
|
+
const match =
|
|
317
|
+
/^(?:\*\*|- \*\*|- )(优先级|状态|立案日期|所属域|来源|严重程度)(?:\*\*)?\s*[::]/.exec(
|
|
318
|
+
line.trim(),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (match !== null) {
|
|
322
|
+
/**
|
|
323
|
+
* 命中的元信息键
|
|
324
|
+
*/
|
|
325
|
+
const key = match[1] ?? '';
|
|
326
|
+
|
|
327
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 重复键列表
|
|
333
|
+
*/
|
|
334
|
+
const duplicated = [...counts.entries()].filter(([, count]) => count > 1).map(([key]) => key);
|
|
335
|
+
|
|
336
|
+
return duplicated.length > 0
|
|
337
|
+
? `头部区元信息键重复:${duplicated.join('、')}(方言残留块与规范头部并存——须经 migrate-legacy 收敛)`
|
|
338
|
+
: null;
|
|
277
339
|
},
|
|
278
340
|
},
|
|
279
341
|
{
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migrate-legacy 必填元信息回填(自 core 拆出守函数行上限)
|
|
3
|
+
*
|
|
4
|
+
* 字段别名(日期 → 立案日期)、立案日期/优先级回填与档位映射。缺省未给时
|
|
5
|
+
* 返回 skip 语由调用方裁决,不在此抛错。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { MigrationTransformation } from './migrate-legacy.core';
|
|
9
|
+
|
|
10
|
+
import { mapPriorityValue } from './migrate-legacy.vocab';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 回填入参(与 MigrateCoreOptions 的回填子集 + 字段工作副本)
|
|
14
|
+
*/
|
|
15
|
+
export type BackfillInput = {
|
|
16
|
+
/**
|
|
17
|
+
* 头部字段工作副本(原地回填)
|
|
18
|
+
*/
|
|
19
|
+
fields: Array<{ key: string; value: string }>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 变换台账
|
|
23
|
+
*/
|
|
24
|
+
transformations: MigrationTransformation[];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 优先级缺省
|
|
28
|
+
*/
|
|
29
|
+
defaultPriority?: 'P1' | 'P2' | 'P3';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 立案日期回填缺省
|
|
33
|
+
*/
|
|
34
|
+
defaultFilingDate?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 回填产物
|
|
39
|
+
*/
|
|
40
|
+
export type BackfillResult = {
|
|
41
|
+
/**
|
|
42
|
+
* skip 语(缺省未给交人工时非空)
|
|
43
|
+
*/
|
|
44
|
+
skipMessage: string | null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 执行字段别名与必填回填
|
|
49
|
+
*
|
|
50
|
+
* @param input - 回填入参
|
|
51
|
+
* @returns 回填产物(skipMessage 非空即调用方应 skipped 返回)
|
|
52
|
+
*/
|
|
53
|
+
export function backfillRequiredFields(input: BackfillInput): BackfillResult {
|
|
54
|
+
/**
|
|
55
|
+
* 解构字段副本与台账
|
|
56
|
+
*/
|
|
57
|
+
const { fields, transformations } = input;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 字段别名:日期 → 立案日期(已有立案日期则保留原字段不动)
|
|
61
|
+
*/
|
|
62
|
+
const filingIndex = fields.findIndex((field) => field.key === '立案日期');
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 旧字段名「日期」下标(方言主流的立案日期载体)
|
|
66
|
+
*/
|
|
67
|
+
const dateIndex = fields.findIndex((field) => field.key === '日期');
|
|
68
|
+
|
|
69
|
+
if (filingIndex === -1 && dateIndex !== -1) {
|
|
70
|
+
/**
|
|
71
|
+
* 待改名的日期字段
|
|
72
|
+
*/
|
|
73
|
+
const dateField = fields[dateIndex];
|
|
74
|
+
|
|
75
|
+
if (dateField !== undefined) {
|
|
76
|
+
fields[dateIndex] = { key: '立案日期', value: dateField.value };
|
|
77
|
+
|
|
78
|
+
transformations.push({
|
|
79
|
+
kind: 'field-alias',
|
|
80
|
+
detail: `日期 → 立案日期(${dateField.value})`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 立案日期缺省回填(未给缺省则交人工)
|
|
87
|
+
*/
|
|
88
|
+
if (!fields.some((field) => field.key === '立案日期')) {
|
|
89
|
+
if (input.defaultFilingDate === undefined) {
|
|
90
|
+
return { skipMessage: '缺立案日期且未给回填缺省——交人工' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fields.push({ key: '立案日期', value: input.defaultFilingDate });
|
|
94
|
+
|
|
95
|
+
transformations.push({
|
|
96
|
+
kind: 'required-backfilled',
|
|
97
|
+
detail: `立案日期 ← ${input.defaultFilingDate}`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 优先级:既有值映射 / 严重程度映射 / 缺省回填
|
|
103
|
+
*/
|
|
104
|
+
const priorityIndex = fields.findIndex((field) => field.key === '优先级');
|
|
105
|
+
|
|
106
|
+
if (priorityIndex !== -1) {
|
|
107
|
+
/**
|
|
108
|
+
* 既有优先级值
|
|
109
|
+
*/
|
|
110
|
+
const existing = fields[priorityIndex]?.value ?? '';
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 映射产物(中文档位 → P 档位)
|
|
114
|
+
*/
|
|
115
|
+
const mapped = mapPriorityValue(existing);
|
|
116
|
+
|
|
117
|
+
if (mapped !== null && mapped !== existing) {
|
|
118
|
+
fields[priorityIndex] = { key: '优先级', value: mapped };
|
|
119
|
+
|
|
120
|
+
transformations.push({ kind: 'priority-mapped', detail: `优先级 ${existing} → ${mapped}` });
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
/**
|
|
124
|
+
* 严重程度映射或缺省回填
|
|
125
|
+
*/
|
|
126
|
+
const severityField = fields.find((field) => field.key === '严重程度');
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 回填值
|
|
130
|
+
*/
|
|
131
|
+
const backfill =
|
|
132
|
+
(severityField !== undefined ? mapPriorityValue(severityField.value) : null) ??
|
|
133
|
+
input.defaultPriority ??
|
|
134
|
+
null;
|
|
135
|
+
|
|
136
|
+
if (backfill === null) {
|
|
137
|
+
return { skipMessage: '缺优先级且未给回填缺省——交人工' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fields.push({ key: '优先级', value: backfill });
|
|
141
|
+
|
|
142
|
+
transformations.push({
|
|
143
|
+
kind: severityField !== undefined ? 'priority-mapped' : 'priority-backfilled',
|
|
144
|
+
detail: `优先级 ← ${backfill}${severityField !== undefined ? `(严重程度 ${severityField.value})` : '(缺省回填)'}`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { skipMessage: null };
|
|
149
|
+
}
|