@longzai-intelligence-issues/ledger 0.0.1
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/CHANGELOG.md +18 -0
- package/README.md +26 -0
- package/dist/index.d.ts +2123 -0
- package/dist/index.js +78 -0
- package/dist/rolldown-runtime-BqT_7tdF.js +1 -0
- package/lzi-builder.config.ts +15 -0
- package/lzi-bun.config.ts +8 -0
- package/oxlint.config.ts +3 -0
- package/package.json +39 -0
- package/src/__tests__/close/verify-close.commands.test.ts +560 -0
- package/src/__tests__/docs-refs/docs-refs.commands.test.ts +213 -0
- package/src/__tests__/fs/mini-glob.utils.test.ts +71 -0
- package/src/__tests__/fs/walk-surface.utils.test.ts +170 -0
- package/src/__tests__/health/health.commands.test.ts +131 -0
- package/src/__tests__/lint/lint.core.test.ts +332 -0
- package/src/__tests__/numbering/numbering.commands.test.ts +194 -0
- package/src/__tests__/numbering/numbering.core.test.ts +318 -0
- package/src/__tests__/parser/guide-fixture.utils.test.ts +45 -0
- package/src/__tests__/parser/registry.parser.test.ts +316 -0
- package/src/__tests__/prompts/prompts.commands.test.ts +47 -0
- package/src/__tests__/template/issue-template.renderer.test.ts +133 -0
- package/src/close/evidence-reader.utils.ts +201 -0
- package/src/close/green-flip.commands.ts +199 -0
- package/src/close/verify-close.commands.ts +676 -0
- package/src/docs-refs/docs-refs.commands.ts +693 -0
- package/src/freeze/freeze.commands.ts +261 -0
- package/src/fs/mini-glob.utils.ts +216 -0
- package/src/fs/walk-surface.utils.ts +298 -0
- package/src/health/health.commands.ts +327 -0
- package/src/index.ts +46 -0
- package/src/lint/lint-baseline.commands.ts +147 -0
- package/src/lint/lint.core.ts +584 -0
- package/src/normalize/normalize-header.commands.ts +361 -0
- package/src/numbering/numbering.commands.ts +375 -0
- package/src/numbering/numbering.core.ts +685 -0
- package/src/parser/format.utils.ts +279 -0
- package/src/parser/guide-fixture.utils.ts +117 -0
- package/src/parser/registry.parser.ts +679 -0
- package/src/prompts/prompts.commands.ts +211 -0
- package/src/template/issue-template.renderer.ts +351 -0
- package/src/triage/triage.classify.ts +93 -0
- package/src/vault/vault.commands.ts +434 -0
- package/tsconfig/.cache/build.tsbuildinfo +1 -0
- package/tsconfig/.cache/node.tsbuildinfo +1 -0
- package/tsconfig/.cache/test.tsbuildinfo +1 -0
- package/tsconfig/app.json +13 -0
- package/tsconfig/build.json +15 -0
- package/tsconfig/node.json +12 -0
- package/tsconfig/test.json +15 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* normalize-header(方言头部迁移)
|
|
3
|
+
*
|
|
4
|
+
* 四种历史方言(单行管道引用行 / 粗体标签行 / `## 键: 值` 标题式 / `- ` 列表式)
|
|
5
|
+
* 解析后重排为裁定格式:粗体标签逐行 `**键**: 值`;字段规范序(FIELD_RANK);
|
|
6
|
+
* 「主题」问题陈述迁正文首个节标题后;residual 原样保留。零丢失守卫:每字段
|
|
7
|
+
* 必须在产物文本在场否则 failed 不落盘;幂等(产物 === 原文 → unchanged)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 字段规范序(排名值越小越靠前;未入表字段按首现序殿后)
|
|
15
|
+
*/
|
|
16
|
+
const FIELD_RANK: Readonly<Record<string, number>> = {
|
|
17
|
+
立案日期: 10,
|
|
18
|
+
优先级: 20,
|
|
19
|
+
严重程度: 30,
|
|
20
|
+
状态: 40,
|
|
21
|
+
处置日期: 50,
|
|
22
|
+
所属域: 60,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 单文件迁移入参
|
|
27
|
+
*/
|
|
28
|
+
export type NormalizeHeaderFileInput = {
|
|
29
|
+
/**
|
|
30
|
+
* 仓库根绝对路径
|
|
31
|
+
*/
|
|
32
|
+
root: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* issue 文件仓库相对路径
|
|
36
|
+
*/
|
|
37
|
+
file: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 单文件迁移结果
|
|
42
|
+
*/
|
|
43
|
+
export type NormalizeHeaderResult = {
|
|
44
|
+
/**
|
|
45
|
+
* issue 文件仓库相对路径
|
|
46
|
+
*/
|
|
47
|
+
file: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 处置形态:rewritten / unchanged / skipped / failed
|
|
51
|
+
*/
|
|
52
|
+
outcome: 'rewritten' | 'unchanged' | 'skipped' | 'failed';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 结果说明(failed 含原因;skipped 说明灯位缺失等)
|
|
56
|
+
*/
|
|
57
|
+
message: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 头部解析产物
|
|
62
|
+
*/
|
|
63
|
+
type ParsedHeader = {
|
|
64
|
+
/**
|
|
65
|
+
* 字段键值对(保序)
|
|
66
|
+
*/
|
|
67
|
+
fields: Array<{ key: string; value: string }>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* residual 行(解析不成字段的行,原样保留)
|
|
71
|
+
*/
|
|
72
|
+
residuals: string[];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 头部结束行下标(正文起点)
|
|
76
|
+
*/
|
|
77
|
+
bodyStartIndex: number;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 解析头部方言(四种形态逐行尝试)
|
|
82
|
+
*
|
|
83
|
+
* @param lines - 文档行数组
|
|
84
|
+
* @returns 头部解析产物
|
|
85
|
+
*/
|
|
86
|
+
function parseHeaderDialect(lines: string[]): ParsedHeader {
|
|
87
|
+
/**
|
|
88
|
+
* 字段收集器
|
|
89
|
+
*/
|
|
90
|
+
const fields: Array<{ key: string; value: string }> = [];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* residual 收集器
|
|
94
|
+
*/
|
|
95
|
+
const residuals: string[] = [];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 头部结束行下标(首个 `## ` 标题或空行后首段正文起点)
|
|
99
|
+
*/
|
|
100
|
+
let bodyStartIndex = lines.length;
|
|
101
|
+
|
|
102
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
103
|
+
/**
|
|
104
|
+
* 当前行
|
|
105
|
+
*/
|
|
106
|
+
const line = lines[index] ?? '';
|
|
107
|
+
|
|
108
|
+
if (line.startsWith('## ') || (line === '' && fields.length > 0 && index > 4)) {
|
|
109
|
+
bodyStartIndex = index;
|
|
110
|
+
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (line === '' || line.startsWith('# ')) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 形态三:`## 键: 值` 标题式(历史方言,`##` 单井号)
|
|
120
|
+
*/
|
|
121
|
+
const headingMatch = /^##\s+([^::]{1,8})\s*[::]\s*(.+)$/.exec(line);
|
|
122
|
+
|
|
123
|
+
if (headingMatch !== null) {
|
|
124
|
+
fields.push({ key: headingMatch[1]?.trim() ?? '', value: headingMatch[2]?.trim() ?? '' });
|
|
125
|
+
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 形态二/四:粗体标签行 / 列表式
|
|
131
|
+
*/
|
|
132
|
+
const labeledMatch = /^(?:\*\*|- )([^*::]{1,8})(?:\*\*)?\s*[::]\s*(.+)$/.exec(line);
|
|
133
|
+
|
|
134
|
+
if (labeledMatch !== null) {
|
|
135
|
+
fields.push({ key: labeledMatch[1]?.trim() ?? '', value: labeledMatch[2]?.trim() ?? '' });
|
|
136
|
+
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 形态一:单行管道引用行(`> 优先级 | 状态 | …`)
|
|
142
|
+
*/
|
|
143
|
+
if (line.startsWith('>')) {
|
|
144
|
+
/**
|
|
145
|
+
* 管道分段
|
|
146
|
+
*/
|
|
147
|
+
const segments = line
|
|
148
|
+
.slice(1)
|
|
149
|
+
.split(/[||]/)
|
|
150
|
+
.map((segment) => segment.trim());
|
|
151
|
+
|
|
152
|
+
for (const segment of segments) {
|
|
153
|
+
/**
|
|
154
|
+
* 段内键值切分
|
|
155
|
+
*/
|
|
156
|
+
const pairMatch = /^([^::]{1,8})\s*[::]\s*(.+)$/.exec(segment);
|
|
157
|
+
|
|
158
|
+
if (pairMatch !== null) {
|
|
159
|
+
fields.push({ key: pairMatch[1]?.trim() ?? '', value: pairMatch[2]?.trim() ?? '' });
|
|
160
|
+
} else {
|
|
161
|
+
residuals.push(segment);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
residuals.push(line);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { fields, residuals, bodyStartIndex };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 字段排序键
|
|
176
|
+
*
|
|
177
|
+
* @param key - 字段键
|
|
178
|
+
* @param firstSeenRank - 首现序
|
|
179
|
+
* @returns 排序键(规范序 + 首现序)
|
|
180
|
+
*/
|
|
181
|
+
function fieldSortKey(key: string, firstSeenRank: number): number {
|
|
182
|
+
return FIELD_RANK[key] ?? 1000 + firstSeenRank;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 迁移形态
|
|
187
|
+
*/
|
|
188
|
+
export type NormalizeOutcome = 'rewritten' | 'unchanged' | 'skipped';
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 迁移产物
|
|
192
|
+
*/
|
|
193
|
+
export type NormalizeResult = {
|
|
194
|
+
/**
|
|
195
|
+
* 产物全文
|
|
196
|
+
*/
|
|
197
|
+
content: string;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 迁移形态
|
|
201
|
+
*/
|
|
202
|
+
outcome: NormalizeOutcome;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 迁移单份文档头部
|
|
207
|
+
*
|
|
208
|
+
* @param raw - 文档全文
|
|
209
|
+
* @returns 迁移产物
|
|
210
|
+
*/
|
|
211
|
+
export function normalizeHeader(raw: string): NormalizeResult {
|
|
212
|
+
/**
|
|
213
|
+
* 文档行数组
|
|
214
|
+
*/
|
|
215
|
+
const lines = raw.split(/\r?\n/);
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 头部解析产物
|
|
219
|
+
*/
|
|
220
|
+
const parsed = parseHeaderDialect(lines);
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 「状态」字段定位(无状态行 skipped——灯位属语义判断,不猜灯)
|
|
224
|
+
*/
|
|
225
|
+
const hasStatus = parsed.fields.some((field) => field.key === '状态');
|
|
226
|
+
|
|
227
|
+
if (!hasStatus) {
|
|
228
|
+
return { content: raw, outcome: 'skipped' };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 首现序表
|
|
233
|
+
*/
|
|
234
|
+
const firstSeen = new Map<string, number>();
|
|
235
|
+
|
|
236
|
+
parsed.fields.forEach((field, index) => {
|
|
237
|
+
if (!firstSeen.has(field.key)) {
|
|
238
|
+
firstSeen.set(field.key, index);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 规范序字段列表(稳定排序)
|
|
244
|
+
*/
|
|
245
|
+
const sortedFields = [...parsed.fields].sort((a, b) => {
|
|
246
|
+
/**
|
|
247
|
+
* 左右排序键
|
|
248
|
+
*/
|
|
249
|
+
const left = fieldSortKey(a.key, firstSeen.get(a.key) ?? 0);
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 右字段排序键
|
|
253
|
+
*/
|
|
254
|
+
const right = fieldSortKey(b.key, firstSeen.get(b.key) ?? 0);
|
|
255
|
+
|
|
256
|
+
return left === right ? 0 : left - right;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 规范头部行(粗体标签逐行 + residual 殿后原样保留)
|
|
261
|
+
*/
|
|
262
|
+
const headerLines = [
|
|
263
|
+
...sortedFields.map((field) => `**${field.key}**: ${field.value}`),
|
|
264
|
+
...parsed.residuals,
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 产物全文(标题行保留 + 规范头部 + 正文余文)
|
|
269
|
+
*/
|
|
270
|
+
const titleEndIndex = lines.findIndex((line, index) => index > 0 && line.startsWith('# '));
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 标题区行(首个一级标题及其前空行)
|
|
274
|
+
*/
|
|
275
|
+
const titleLines = lines.slice(0, titleEndIndex === -1 ? 1 : titleEndIndex);
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 产物全文
|
|
279
|
+
*/
|
|
280
|
+
const content = [
|
|
281
|
+
...titleLines,
|
|
282
|
+
'',
|
|
283
|
+
...headerLines,
|
|
284
|
+
'',
|
|
285
|
+
...lines.slice(parsed.bodyStartIndex),
|
|
286
|
+
].join('\n');
|
|
287
|
+
|
|
288
|
+
if (content === raw) {
|
|
289
|
+
return { content: raw, outcome: 'unchanged' };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return { content, outcome: 'rewritten' };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 零丢失守卫:每字段键值必须在产物文本在场
|
|
297
|
+
*
|
|
298
|
+
* @param parsed - 头部解析产物
|
|
299
|
+
* @param content - 产物全文
|
|
300
|
+
* @returns 缺失字段键列表(空 = 通过)
|
|
301
|
+
*/
|
|
302
|
+
function findLostFields(parsed: ParsedHeader, content: string): string[] {
|
|
303
|
+
return parsed.fields
|
|
304
|
+
.filter((field) => !content.includes(`**${field.key}**: ${field.value}`))
|
|
305
|
+
.map((field) => field.key);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 迁移并落盘单份文档(守卫失败不落盘)
|
|
310
|
+
*
|
|
311
|
+
* @param input - 命令入参
|
|
312
|
+
* @param input.root - 仓库根绝对路径
|
|
313
|
+
* @param input.file - issue 文件仓库相对路径
|
|
314
|
+
* @returns 单文件迁移结果
|
|
315
|
+
*/
|
|
316
|
+
export function normalizeHeaderFile(input: NormalizeHeaderFileInput): NormalizeHeaderResult {
|
|
317
|
+
/**
|
|
318
|
+
* 文档全文
|
|
319
|
+
*/
|
|
320
|
+
let raw = '';
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
raw = readFileSync(join(input.root, input.file), 'utf8');
|
|
324
|
+
} catch {
|
|
325
|
+
return { file: input.file, outcome: 'failed', message: '读档失败(文件不存在或不可读)' };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* 迁移产物
|
|
330
|
+
*/
|
|
331
|
+
const normalized = normalizeHeader(raw);
|
|
332
|
+
|
|
333
|
+
if (normalized.outcome === 'skipped') {
|
|
334
|
+
return {
|
|
335
|
+
file: input.file,
|
|
336
|
+
outcome: 'skipped',
|
|
337
|
+
message: '无可识别状态行且未给 --status——灯位属语义判断,不猜灯',
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (normalized.outcome === 'unchanged') {
|
|
342
|
+
return { file: input.file, outcome: 'unchanged', message: '头部已是规范形态' };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* 零丢失守卫
|
|
347
|
+
*/
|
|
348
|
+
const lost = findLostFields(parseHeaderDialect(raw.split(/\r?\n/)), normalized.content);
|
|
349
|
+
|
|
350
|
+
if (lost.length > 0) {
|
|
351
|
+
return {
|
|
352
|
+
file: input.file,
|
|
353
|
+
outcome: 'failed',
|
|
354
|
+
message: `零丢失守卫失败:字段 ${lost.join('、')} 未在产物在场——不落盘`,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
writeFileSync(join(input.root, input.file), normalized.content);
|
|
359
|
+
|
|
360
|
+
return { file: input.file, outcome: 'rewritten', message: '头部已迁移为规范形态' };
|
|
361
|
+
}
|