@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,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 扫描面分层遍历基座
|
|
3
|
+
*
|
|
4
|
+
* include glob 反推最小目录前缀集(截断=保守超集,glob 可匹配路径必落某推导
|
|
5
|
+
* 前缀下);前缀遍历宽容(缺前缀静默跳过=未来目录合法),全仓遍历严格(缺根
|
|
6
|
+
* 抛错防扫描面缩水)。剪枝:.git 恒跳过 / 排除 glob 整树剪枝 / 符号链接跳过;
|
|
7
|
+
* 文件名级零内容读取、零子进程、零网络;输出 posix 相对路径升序。
|
|
8
|
+
*
|
|
9
|
+
* 前缀约定:无尾斜杠的 posix 相对目录路径,'' 表示全仓根。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
13
|
+
import { join, posix } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { matchMiniGlob } from '@/fs/mini-glob.utils';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 遍历选项
|
|
19
|
+
*/
|
|
20
|
+
export type WalkOptions = {
|
|
21
|
+
/**
|
|
22
|
+
* 排除 glob 列表(目录命中即整树剪枝,按原相对路径 / 尾斜杠 / 子树三口径测试)
|
|
23
|
+
*/
|
|
24
|
+
excludeGlobs?: readonly string[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 通配判定字符集(`*` `?` `{` `}`)
|
|
29
|
+
*/
|
|
30
|
+
const WILDCARD_CHARS = new Set(['*', '?', '{', '}']);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 判定 glob 段是否含通配
|
|
34
|
+
*
|
|
35
|
+
* @param segment - 路径段
|
|
36
|
+
* @returns 是否含通配字符
|
|
37
|
+
*/
|
|
38
|
+
function hasWildcard(segment: string): boolean {
|
|
39
|
+
return [...segment].some((char) => WILDCARD_CHARS.has(char));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* include glob 反推最小目录前缀集
|
|
44
|
+
*
|
|
45
|
+
* 末段恒为文件名模式不参与推导;字面段续推、通配段截断(截断=更短前缀=
|
|
46
|
+
* 覆盖超集)、`{a,b}` 分叉(候选项含通配即截断该分支);任一模式贡献空前缀
|
|
47
|
+
* 则全仓标记('')。候选去重后按包含关系裁剪(宽前缀吞并其下窄前缀)。
|
|
48
|
+
* 空模式集产出空前缀集(无扫描面)。
|
|
49
|
+
*
|
|
50
|
+
* @param includeGlobs - include glob 列表
|
|
51
|
+
* @returns 最小目录前缀集(无尾斜杠 posix 相对路径,'' 表示全仓)
|
|
52
|
+
*/
|
|
53
|
+
export function deriveWalkPrefixes(includeGlobs: readonly string[]): string[] {
|
|
54
|
+
/**
|
|
55
|
+
* 候选前缀收集器
|
|
56
|
+
*/
|
|
57
|
+
const candidates: string[] = [];
|
|
58
|
+
|
|
59
|
+
for (const glob of includeGlobs) {
|
|
60
|
+
/**
|
|
61
|
+
* glob 路径段列表
|
|
62
|
+
*/
|
|
63
|
+
const segments = glob.split('/').filter((segment) => segment !== '');
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 末段(文件名模式)剔除后的目录段
|
|
67
|
+
*/
|
|
68
|
+
const dirSegments = segments.slice(0, -1);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 分叉游标集(当前所有字面前缀,'' 为根)
|
|
72
|
+
*/
|
|
73
|
+
let prefixes: string[] = [''];
|
|
74
|
+
|
|
75
|
+
for (const segment of dirSegments) {
|
|
76
|
+
/**
|
|
77
|
+
* 整段大括号多选一形态
|
|
78
|
+
*/
|
|
79
|
+
const braceMatch = /^\{([^{}]+)\}$/.exec(segment);
|
|
80
|
+
|
|
81
|
+
if (braceMatch !== null) {
|
|
82
|
+
/**
|
|
83
|
+
* 下一层游标收集器
|
|
84
|
+
*/
|
|
85
|
+
const forked: string[] = [];
|
|
86
|
+
|
|
87
|
+
for (const option of (braceMatch[1] ?? '').split(',')) {
|
|
88
|
+
if (hasWildcard(option)) {
|
|
89
|
+
/**
|
|
90
|
+
* 通配候选:截断该分支(保留当前前缀)
|
|
91
|
+
*/
|
|
92
|
+
forked.push(...prefixes);
|
|
93
|
+
} else {
|
|
94
|
+
forked.push(...prefixes.map((prefix) => joinPrefix(prefix, option)));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
prefixes = forked;
|
|
99
|
+
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (hasWildcard(segment)) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
prefixes = prefixes.map((prefix) => joinPrefix(prefix, segment));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
candidates.push(...prefixes);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 去重后的候选集
|
|
115
|
+
*/
|
|
116
|
+
const unique = [...new Set(candidates)];
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 包含关系裁剪:宽前缀(含 '')吞并其下窄前缀
|
|
120
|
+
*/
|
|
121
|
+
return unique.filter(
|
|
122
|
+
(prefix) =>
|
|
123
|
+
prefix === '' ||
|
|
124
|
+
!unique.some((other) => other !== prefix && (other === '' || prefix.startsWith(`${other}/`))),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 拼接前缀(根前缀 '' 直接取段名)
|
|
130
|
+
*
|
|
131
|
+
* @param prefix - 当前前缀('' 为根)
|
|
132
|
+
* @param segment - 追加段
|
|
133
|
+
* @returns 拼接后前缀
|
|
134
|
+
*/
|
|
135
|
+
function joinPrefix(prefix: string, segment: string): string {
|
|
136
|
+
return prefix === '' ? segment : `${prefix}/${segment}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 判定目录是否命中排除 glob(原相对路径 / 尾斜杠 / 子树三口径)
|
|
141
|
+
*
|
|
142
|
+
* @param excludeGlobs - 排除 glob 列表
|
|
143
|
+
* @param dirRel - 目录相对路径(posix,无尾斜杠)
|
|
144
|
+
* @returns 是否整树排除
|
|
145
|
+
*/
|
|
146
|
+
function isDirExcluded(excludeGlobs: readonly string[], dirRel: string): boolean {
|
|
147
|
+
return excludeGlobs.some(
|
|
148
|
+
(glob) =>
|
|
149
|
+
matchMiniGlob(glob, dirRel) ||
|
|
150
|
+
matchMiniGlob(glob, `${dirRel}/`) ||
|
|
151
|
+
matchMiniGlob(glob, `${dirRel}/**`),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 判定文件是否命中排除 glob
|
|
157
|
+
*
|
|
158
|
+
* @param excludeGlobs - 排除 glob 列表
|
|
159
|
+
* @param fileRel - 文件相对路径(posix)
|
|
160
|
+
* @returns 是否排除
|
|
161
|
+
*/
|
|
162
|
+
function isFileExcluded(excludeGlobs: readonly string[], fileRel: string): boolean {
|
|
163
|
+
return excludeGlobs.some((glob) => matchMiniGlob(glob, fileRel));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 判定目录是否需要访问(在通往某前缀的路径上,或整体落于某前缀内)
|
|
168
|
+
*
|
|
169
|
+
* @param prefixes - 目录前缀集('' 表示全仓)
|
|
170
|
+
* @param dirRel - 目录相对路径
|
|
171
|
+
* @returns 是否访问
|
|
172
|
+
*/
|
|
173
|
+
function shouldVisitDir(prefixes: readonly string[], dirRel: string): boolean {
|
|
174
|
+
return prefixes.some(
|
|
175
|
+
(prefix) =>
|
|
176
|
+
prefix === '' ||
|
|
177
|
+
dirRel === prefix ||
|
|
178
|
+
dirRel.startsWith(`${prefix}/`) ||
|
|
179
|
+
prefix.startsWith(`${dirRel}/`),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 判定文件是否在采集面内(落于某前缀下;'' 全仓)
|
|
185
|
+
*
|
|
186
|
+
* @param prefixes - 目录前缀集
|
|
187
|
+
* @param fileRel - 文件相对路径
|
|
188
|
+
* @returns 是否采集
|
|
189
|
+
*/
|
|
190
|
+
function isFileInSurface(prefixes: readonly string[], fileRel: string): boolean {
|
|
191
|
+
return prefixes.some((prefix) => prefix === '' || fileRel.startsWith(`${prefix}/`));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 目录遍历核心(递归)
|
|
196
|
+
*
|
|
197
|
+
* @param dirAbs - 当前目录绝对路径
|
|
198
|
+
* @param dirRel - 当前目录相对路径('' 为根层)
|
|
199
|
+
* @param prefixes - 目录前缀集
|
|
200
|
+
* @param excludeGlobs - 排除 glob 列表
|
|
201
|
+
* @param strict - 严格模式(目录缺失抛错;宽容模式静默跳过)
|
|
202
|
+
* @param collector - 文件相对路径收集器
|
|
203
|
+
* @throws {@link Error} 严格模式下目录缺失
|
|
204
|
+
*/
|
|
205
|
+
function walkDir(
|
|
206
|
+
dirAbs: string,
|
|
207
|
+
dirRel: string,
|
|
208
|
+
prefixes: readonly string[],
|
|
209
|
+
excludeGlobs: readonly string[],
|
|
210
|
+
strict: boolean,
|
|
211
|
+
collector: string[],
|
|
212
|
+
): void {
|
|
213
|
+
if (!existsSync(dirAbs)) {
|
|
214
|
+
if (strict) {
|
|
215
|
+
throw new Error(`扫描面目录缺失:${dirAbs}(严格模式防扫描面静默缩水)`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 当前目录条目列表(按名排序保证输出确定)
|
|
223
|
+
*/
|
|
224
|
+
const entries = readdirSync(dirAbs, { withFileTypes: true }).sort((a, b) =>
|
|
225
|
+
a.name.localeCompare(b.name),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
/**
|
|
230
|
+
* 条目相对路径(posix)
|
|
231
|
+
*/
|
|
232
|
+
const rel = dirRel === '' ? entry.name : `${dirRel}/${entry.name}`;
|
|
233
|
+
|
|
234
|
+
if (entry.isDirectory()) {
|
|
235
|
+
if (entry.name === '.git' || isDirExcluded(excludeGlobs, rel)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!shouldVisitDir(prefixes, rel)) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
walkDir(join(dirAbs, entry.name), rel, prefixes, excludeGlobs, strict, collector);
|
|
244
|
+
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (entry.isFile()) {
|
|
249
|
+
if (isFileExcluded(excludeGlobs, rel) || !isFileInSurface(prefixes, rel)) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
collector.push(posix.normalize(rel));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 前缀定向遍历(宽容:前缀目录缺失静默跳过——scope 登记未来目录属合法现状)
|
|
260
|
+
*
|
|
261
|
+
* @param root - 遍历根绝对路径
|
|
262
|
+
* @param prefixes - deriveWalkPrefixes 产物('' 表示全仓)
|
|
263
|
+
* @param options - 遍历选项(排除 glob)
|
|
264
|
+
* @returns 命中文件相对路径列表(posix 升序)
|
|
265
|
+
*/
|
|
266
|
+
export function walkFilesUnderPrefixes(
|
|
267
|
+
root: string,
|
|
268
|
+
prefixes: readonly string[],
|
|
269
|
+
options: WalkOptions = {},
|
|
270
|
+
): string[] {
|
|
271
|
+
/**
|
|
272
|
+
* 文件收集器
|
|
273
|
+
*/
|
|
274
|
+
const collector: string[] = [];
|
|
275
|
+
|
|
276
|
+
walkDir(root, '', prefixes, options.excludeGlobs ?? [], false, collector);
|
|
277
|
+
|
|
278
|
+
return collector.sort((a, b) => a.localeCompare(b));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 全仓严格遍历(缺根抛错,audit 防缩水口径)
|
|
283
|
+
*
|
|
284
|
+
* @param root - 遍历根绝对路径
|
|
285
|
+
* @param options - 遍历选项(排除 glob)
|
|
286
|
+
* @returns 命中文件相对路径列表(posix 升序)
|
|
287
|
+
* @throws {@link Error} 根目录缺失
|
|
288
|
+
*/
|
|
289
|
+
export function walkFilesUnderRoots(root: string, options: WalkOptions = {}): string[] {
|
|
290
|
+
/**
|
|
291
|
+
* 文件收集器
|
|
292
|
+
*/
|
|
293
|
+
const collector: string[] = [];
|
|
294
|
+
|
|
295
|
+
walkDir(root, '', [''], options.excludeGlobs ?? [], true, collector);
|
|
296
|
+
|
|
297
|
+
return collector.sort((a, b) => a.localeCompare(b));
|
|
298
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 黄灯巡检观测(health)与分诊台账(triage)
|
|
3
|
+
*
|
|
4
|
+
* health:状态分布、黄灯总数、最老黄灯账龄(mtime 天)、A-B-C 分类分布;
|
|
5
|
+
* triage:只收黄灯,B>C>A 优先级分类,台账 append-only 落
|
|
6
|
+
* .lzi/issues/runs/ledger/triage-<ts>.json(temp+rename 原子写)。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { LziIssuesConfig } from '@longzai-intelligence-issues/config';
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { type IssueDocRecord, scanRegistryDir } from '@/parser/registry.parser';
|
|
15
|
+
import { classifyIssue } from '@/triage/triage.classify';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 巡检报告
|
|
19
|
+
*/
|
|
20
|
+
export type HealthReport = {
|
|
21
|
+
/**
|
|
22
|
+
* 状态分布(状态归类 → 计数)
|
|
23
|
+
*/
|
|
24
|
+
statusCounts: Record<string, number>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 黄灯总数(含旧格式 legacy-yellow)
|
|
28
|
+
*/
|
|
29
|
+
yellowTotal: number;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 最老黄灯账龄(天,mtime 口径向下取整;无黄灯为 0)
|
|
33
|
+
*/
|
|
34
|
+
oldestYellowAgeDays: number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A-B-C 分类分布
|
|
38
|
+
*/
|
|
39
|
+
categoryCounts: Record<string, number>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 分诊台账条目
|
|
44
|
+
*/
|
|
45
|
+
export type TriageItem = {
|
|
46
|
+
/**
|
|
47
|
+
* issue 编号
|
|
48
|
+
*/
|
|
49
|
+
id: string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* issue 文件仓库相对路径
|
|
53
|
+
*/
|
|
54
|
+
file: string;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 状态归类
|
|
58
|
+
*/
|
|
59
|
+
status: string;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 分类(A/B/C)
|
|
63
|
+
*/
|
|
64
|
+
category: 'A' | 'B' | 'C';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 分类依据(中文说明)
|
|
68
|
+
*/
|
|
69
|
+
reason: string;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 判据命令列表(空表示待补录)
|
|
73
|
+
*/
|
|
74
|
+
criteriaCommands: string[];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 分诊台账文件结构
|
|
79
|
+
*/
|
|
80
|
+
export type TriageLedgerFile = {
|
|
81
|
+
/**
|
|
82
|
+
* 生成时刻(毫秒)
|
|
83
|
+
*/
|
|
84
|
+
generatedAtMs: number;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 参与扫描的注册表目录
|
|
88
|
+
*/
|
|
89
|
+
registryDirs: string[];
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 台账条目(黄灯)
|
|
93
|
+
*/
|
|
94
|
+
items: TriageItem[];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 采集条目(解析产物 + 仓库相对路径 + 注册表目录)
|
|
99
|
+
*/
|
|
100
|
+
type CollectedEntry = {
|
|
101
|
+
/**
|
|
102
|
+
* 解析产物
|
|
103
|
+
*/
|
|
104
|
+
record: IssueDocRecord;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* issue 文件仓库相对路径
|
|
108
|
+
*/
|
|
109
|
+
file: string;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 注册表目录
|
|
113
|
+
*/
|
|
114
|
+
registryDir: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 收集各注册表解析产物(附仓库相对路径与注册表目录)
|
|
119
|
+
*
|
|
120
|
+
* @param root - 仓库根绝对路径
|
|
121
|
+
* @param config - 解析后配置
|
|
122
|
+
* @param dirRels - 限定注册表目录(缺省全部)
|
|
123
|
+
* @returns 采集条目列表
|
|
124
|
+
*/
|
|
125
|
+
function collectRecords(
|
|
126
|
+
root: string,
|
|
127
|
+
config: LziIssuesConfig,
|
|
128
|
+
dirRels?: readonly string[],
|
|
129
|
+
): CollectedEntry[] {
|
|
130
|
+
/**
|
|
131
|
+
* 参与扫描的 scope 列表
|
|
132
|
+
*/
|
|
133
|
+
const scopes =
|
|
134
|
+
dirRels === undefined
|
|
135
|
+
? config.scopes
|
|
136
|
+
: config.scopes.filter((scope) => dirRels.includes(scope.registryDir));
|
|
137
|
+
|
|
138
|
+
return scopes.flatMap((scope) =>
|
|
139
|
+
scanRegistryDir(join(root, scope.registryDir)).map((record) => ({
|
|
140
|
+
record,
|
|
141
|
+
file: `${scope.registryDir}/${record.file}`,
|
|
142
|
+
registryDir: scope.registryDir,
|
|
143
|
+
})),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 黄灯巡检命令入参
|
|
149
|
+
*/
|
|
150
|
+
export type RunHealthInput = {
|
|
151
|
+
/**
|
|
152
|
+
* 仓库根绝对路径
|
|
153
|
+
*/
|
|
154
|
+
root: string;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 解析后配置
|
|
158
|
+
*/
|
|
159
|
+
config: LziIssuesConfig;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 限定注册表目录(缺省全部)
|
|
163
|
+
*/
|
|
164
|
+
dirRels?: readonly string[];
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 黄灯巡检
|
|
169
|
+
*
|
|
170
|
+
* @param input - 命令入参
|
|
171
|
+
* @returns 巡检报告
|
|
172
|
+
*/
|
|
173
|
+
export function runHealth(input: RunHealthInput): HealthReport {
|
|
174
|
+
/**
|
|
175
|
+
* 采集结果
|
|
176
|
+
*/
|
|
177
|
+
const collected = collectRecords(input.root, input.config, input.dirRels);
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 状态分布
|
|
181
|
+
*/
|
|
182
|
+
const statusCounts: Record<string, number> = {};
|
|
183
|
+
|
|
184
|
+
for (const { record } of collected) {
|
|
185
|
+
statusCounts[record.status] = (statusCounts[record.status] ?? 0) + 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 黄灯记录列表
|
|
190
|
+
*/
|
|
191
|
+
const yellows = collected.filter(
|
|
192
|
+
({ record }) => record.status === 'yellow' || record.status === 'legacy-yellow',
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 最老黄灯 mtime(0 = 无黄灯)
|
|
197
|
+
*/
|
|
198
|
+
const oldestMtime = yellows.reduce((max, { record }) => Math.max(max, record.modifiedAtMs), 0);
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* A-B-C 分类分布
|
|
202
|
+
*/
|
|
203
|
+
const categoryCounts: Record<string, number> = {};
|
|
204
|
+
|
|
205
|
+
for (const { record } of yellows) {
|
|
206
|
+
/**
|
|
207
|
+
* 分类结果
|
|
208
|
+
*/
|
|
209
|
+
const { category } = classifyIssue(record);
|
|
210
|
+
|
|
211
|
+
categoryCounts[category] = (categoryCounts[category] ?? 0) + 1;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
statusCounts,
|
|
216
|
+
yellowTotal: yellows.length,
|
|
217
|
+
oldestYellowAgeDays:
|
|
218
|
+
oldestMtime === 0 ? 0 : Math.max(0, Math.floor((Date.now() - oldestMtime) / 86_400_000)),
|
|
219
|
+
categoryCounts,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 分诊命令入参
|
|
225
|
+
*/
|
|
226
|
+
export type RunTriageInput = {
|
|
227
|
+
/**
|
|
228
|
+
* 仓库根绝对路径
|
|
229
|
+
*/
|
|
230
|
+
root: string;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 解析后配置
|
|
234
|
+
*/
|
|
235
|
+
config: LziIssuesConfig;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 台账输出目录覆盖(缺省 <root>/.lzi/issues/runs/ledger)
|
|
239
|
+
*/
|
|
240
|
+
outDir?: string;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 分诊命令结果
|
|
245
|
+
*/
|
|
246
|
+
export type RunTriageResult = {
|
|
247
|
+
/**
|
|
248
|
+
* 台账绝对路径
|
|
249
|
+
*/
|
|
250
|
+
ledgerPath: string;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 台账对象
|
|
254
|
+
*/
|
|
255
|
+
ledger: TriageLedgerFile;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 分诊台账落盘(append-only:triage-<时刻>.json 不覆盖历史)
|
|
260
|
+
*
|
|
261
|
+
* @param input - 命令入参
|
|
262
|
+
* @returns 台账绝对路径与台账对象
|
|
263
|
+
*/
|
|
264
|
+
export function runTriage(input: RunTriageInput): RunTriageResult {
|
|
265
|
+
/**
|
|
266
|
+
* 采集结果
|
|
267
|
+
*/
|
|
268
|
+
const collected = collectRecords(input.root, input.config);
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 台账条目(黄灯,B>C>A 分类)
|
|
272
|
+
*/
|
|
273
|
+
const items: TriageItem[] = collected
|
|
274
|
+
.filter(({ record }) => record.status === 'yellow' || record.status === 'legacy-yellow')
|
|
275
|
+
.map(({ record, file }) => {
|
|
276
|
+
/**
|
|
277
|
+
* 分类结果
|
|
278
|
+
*/
|
|
279
|
+
const classified = classifyIssue(record);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
id: record.id,
|
|
283
|
+
file,
|
|
284
|
+
status: record.status,
|
|
285
|
+
category: classified.category,
|
|
286
|
+
reason: classified.reason,
|
|
287
|
+
criteriaCommands: record.criteria?.criteria.map((c) => c.command) ?? [],
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 台账对象
|
|
293
|
+
*/
|
|
294
|
+
const ledger: TriageLedgerFile = {
|
|
295
|
+
generatedAtMs: Date.now(),
|
|
296
|
+
registryDirs: input.config.scopes.map((scope) => scope.registryDir),
|
|
297
|
+
items,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 台账目录
|
|
302
|
+
*/
|
|
303
|
+
const ledgerDir = input.outDir ?? join(input.root, '.lzi', 'issues', 'runs', 'ledger');
|
|
304
|
+
|
|
305
|
+
mkdirSync(ledgerDir, { recursive: true });
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 目标路径(时刻命名,append-only;同毫秒碰撞时顺延 +1 保证不覆盖)
|
|
309
|
+
*/
|
|
310
|
+
let ledgerPath = join(ledgerDir, `triage-${ledger.generatedAtMs}.json`);
|
|
311
|
+
|
|
312
|
+
while (existsSync(ledgerPath) || existsSync(`${ledgerPath}.tmp`)) {
|
|
313
|
+
ledger.generatedAtMs += 1;
|
|
314
|
+
ledgerPath = join(ledgerDir, `triage-${ledger.generatedAtMs}.json`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 临时路径(temp+rename 原子写)
|
|
319
|
+
*/
|
|
320
|
+
const tempPath = `${ledgerPath}.tmp`;
|
|
321
|
+
|
|
322
|
+
writeFileSync(tempPath, `${JSON.stringify(ledger, null, 2)}\n`);
|
|
323
|
+
|
|
324
|
+
renameSync(tempPath, ledgerPath);
|
|
325
|
+
|
|
326
|
+
return { ledgerPath, ledger };
|
|
327
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 引擎包入口
|
|
3
|
+
*
|
|
4
|
+
* 汇出格式真源、解析器、模板渲染、fs 基座、编号门禁、lint 与观测、收口与
|
|
5
|
+
* 冻结、证书库、引用检查、提示词全部引擎面。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export * from './close/evidence-reader.utils.js';
|
|
9
|
+
|
|
10
|
+
export * from './docs-refs/docs-refs.commands.js';
|
|
11
|
+
|
|
12
|
+
export * from './prompts/prompts.commands.js';
|
|
13
|
+
|
|
14
|
+
export * from './close/green-flip.commands.js';
|
|
15
|
+
|
|
16
|
+
export * from './close/verify-close.commands.js';
|
|
17
|
+
|
|
18
|
+
export * from './fs/mini-glob.utils.js';
|
|
19
|
+
|
|
20
|
+
export * from './fs/walk-surface.utils.js';
|
|
21
|
+
|
|
22
|
+
export * from './freeze/freeze.commands.js';
|
|
23
|
+
|
|
24
|
+
export * from './health/health.commands.js';
|
|
25
|
+
|
|
26
|
+
export * from './lint/lint-baseline.commands.js';
|
|
27
|
+
|
|
28
|
+
export * from './lint/lint.core.js';
|
|
29
|
+
|
|
30
|
+
export * from './normalize/normalize-header.commands.js';
|
|
31
|
+
|
|
32
|
+
export * from './numbering/numbering.commands.js';
|
|
33
|
+
|
|
34
|
+
export * from './numbering/numbering.core.js';
|
|
35
|
+
|
|
36
|
+
export * from './parser/format.utils.js';
|
|
37
|
+
|
|
38
|
+
export * from './parser/guide-fixture.utils.js';
|
|
39
|
+
|
|
40
|
+
export * from './parser/registry.parser.js';
|
|
41
|
+
|
|
42
|
+
export * from './template/issue-template.renderer.js';
|
|
43
|
+
|
|
44
|
+
export * from './triage/triage.classify.js';
|
|
45
|
+
|
|
46
|
+
export * from './vault/vault.commands.js';
|