@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,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 证书库(verify vault)
|
|
3
|
+
*
|
|
4
|
+
* 判据指纹 = sha256(git 可见树 + 命令身份)——git 仓取 `git ls-files` 可见面,
|
|
5
|
+
* 非 git 仓回退目录全树;证书按 <channel>/<指纹>.json 落 .lzi/issues/vault/
|
|
6
|
+
* certificates/,同指纹复用秒回;run.lock 防并行互踩(D3:FIFO 队列留增强)。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { walkFilesUnderRoots } from '@/fs/walk-surface.utils';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 判据(verify-close --criterion 四段)
|
|
18
|
+
*/
|
|
19
|
+
export type Criterion = {
|
|
20
|
+
/**
|
|
21
|
+
* 判据标签
|
|
22
|
+
*/
|
|
23
|
+
label: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 判据通道(证书库的通道维度)
|
|
27
|
+
*/
|
|
28
|
+
channel: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 判据工作目录(相对仓库根)
|
|
32
|
+
*/
|
|
33
|
+
cwd: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 判据命令(退出码即判据)
|
|
37
|
+
*/
|
|
38
|
+
command: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 判据证书
|
|
43
|
+
*/
|
|
44
|
+
export type CriterionCertificate = {
|
|
45
|
+
/**
|
|
46
|
+
* 内容寻址指纹(64 位 hex)
|
|
47
|
+
*/
|
|
48
|
+
fingerprint: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 通道
|
|
52
|
+
*/
|
|
53
|
+
channel: string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 判据命令
|
|
57
|
+
*/
|
|
58
|
+
command: string;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 工作目录
|
|
62
|
+
*/
|
|
63
|
+
cwd: string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 退出码
|
|
67
|
+
*/
|
|
68
|
+
exitCode: number;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 输出摘要(首尾各若干行)
|
|
72
|
+
*/
|
|
73
|
+
outputSummary: string[];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 执行时刻(ISO)
|
|
77
|
+
*/
|
|
78
|
+
executedAt: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 判据执行入参
|
|
83
|
+
*/
|
|
84
|
+
export type RunCriterionInput = {
|
|
85
|
+
/**
|
|
86
|
+
* 仓库根绝对路径
|
|
87
|
+
*/
|
|
88
|
+
root: string;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 判据
|
|
92
|
+
*/
|
|
93
|
+
criterion: Criterion;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 预计算指纹(缺省现算)
|
|
97
|
+
*/
|
|
98
|
+
fingerprint?: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 判据执行结果
|
|
103
|
+
*/
|
|
104
|
+
export type CriterionRunResult = {
|
|
105
|
+
/**
|
|
106
|
+
* 是否全绿(退出码 0)
|
|
107
|
+
*/
|
|
108
|
+
green: boolean;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 真实执行 / 复用标记
|
|
112
|
+
*/
|
|
113
|
+
reused: boolean;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 证书(执行失败时也落证书)
|
|
117
|
+
*/
|
|
118
|
+
certificate: CriterionCertificate;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 输出摘要行数上限(首尾各取)
|
|
123
|
+
*/
|
|
124
|
+
const OUTPUT_SUMMARY_LINES = 8;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 计算 git 可见树文本(非 git 仓回退全树)
|
|
128
|
+
*
|
|
129
|
+
* @param root - 仓库根绝对路径
|
|
130
|
+
* @returns 可见面清单文本(排序、换行连接)
|
|
131
|
+
*/
|
|
132
|
+
function readVisibleTree(root: string): string {
|
|
133
|
+
try {
|
|
134
|
+
/**
|
|
135
|
+
* git 可见面(cached + untracked,-z 分隔)
|
|
136
|
+
*/
|
|
137
|
+
const output = execFileSync(
|
|
138
|
+
'git',
|
|
139
|
+
['ls-files', '--cached', '--others', '--exclude-standard', '-z'],
|
|
140
|
+
{ cwd: root, maxBuffer: 64 * 1024 * 1024 },
|
|
141
|
+
).toString('utf8');
|
|
142
|
+
|
|
143
|
+
return output
|
|
144
|
+
.split('\0')
|
|
145
|
+
.filter((entry) => entry !== '')
|
|
146
|
+
.sort()
|
|
147
|
+
.join('\n');
|
|
148
|
+
} catch {
|
|
149
|
+
/**
|
|
150
|
+
* 非 git 仓回退:目录全树(排除 .git)
|
|
151
|
+
*/
|
|
152
|
+
return walkFilesUnderRoots(root).join('\n');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 计算判据指纹
|
|
158
|
+
*
|
|
159
|
+
* @param root - 仓库根绝对路径
|
|
160
|
+
* @param criterion - 判据
|
|
161
|
+
* @returns 64 位 hex 指纹
|
|
162
|
+
*/
|
|
163
|
+
export function computeCriterionFingerprint(root: string, criterion: Criterion): string {
|
|
164
|
+
/**
|
|
165
|
+
* 可见树文本
|
|
166
|
+
*/
|
|
167
|
+
const tree = readVisibleTree(root);
|
|
168
|
+
|
|
169
|
+
return createHash('sha256').update(`${tree}\nidentity:${criterion.command}`).digest('hex');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 证书路径
|
|
174
|
+
*
|
|
175
|
+
* @param root - 仓库根绝对路径
|
|
176
|
+
* @param channel - 通道
|
|
177
|
+
* @param fingerprint - 指纹
|
|
178
|
+
* @returns 证书文件绝对路径
|
|
179
|
+
*/
|
|
180
|
+
export function certificatePath(root: string, channel: string, fingerprint: string): string {
|
|
181
|
+
return join(root, '.lzi', 'issues', 'vault', 'certificates', channel, `${fingerprint}.json`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 查找在册证书(只读)
|
|
186
|
+
*
|
|
187
|
+
* @param root - 仓库根绝对路径
|
|
188
|
+
* @param channel - 通道
|
|
189
|
+
* @param fingerprint - 指纹
|
|
190
|
+
* @returns 证书;缺席为 null
|
|
191
|
+
*/
|
|
192
|
+
export function findCertificate(
|
|
193
|
+
root: string,
|
|
194
|
+
channel: string,
|
|
195
|
+
fingerprint: string,
|
|
196
|
+
): CriterionCertificate | null {
|
|
197
|
+
/**
|
|
198
|
+
* 证书路径
|
|
199
|
+
*/
|
|
200
|
+
const path = certificatePath(root, channel, fingerprint);
|
|
201
|
+
|
|
202
|
+
if (!existsSync(path)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
/**
|
|
208
|
+
* 解析值
|
|
209
|
+
*/
|
|
210
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
211
|
+
|
|
212
|
+
return isCriterionCertificate(parsed) ? parsed : null;
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 证书类型守卫(结构性校验:四关键字段形态)
|
|
220
|
+
*
|
|
221
|
+
* @param value - 待判定值
|
|
222
|
+
* @returns 是否证书对象
|
|
223
|
+
*/
|
|
224
|
+
function isCriterionCertificate(value: unknown): value is CriterionCertificate {
|
|
225
|
+
if (typeof value !== 'object' || value === null) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 指纹字段
|
|
231
|
+
*/
|
|
232
|
+
const fingerprint = Reflect.get(value, 'fingerprint');
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 退出码字段
|
|
236
|
+
*/
|
|
237
|
+
const exitCode = Reflect.get(value, 'exitCode');
|
|
238
|
+
|
|
239
|
+
return typeof fingerprint === 'string' && typeof exitCode === 'number';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 截取输出摘要(首尾各 N 行)
|
|
244
|
+
*
|
|
245
|
+
* @param output - 命令完整输出
|
|
246
|
+
* @returns 摘要行列表
|
|
247
|
+
*/
|
|
248
|
+
function summarizeOutput(output: string): string[] {
|
|
249
|
+
/**
|
|
250
|
+
* 输出行列表
|
|
251
|
+
*/
|
|
252
|
+
const lines = output.split(/\r?\n/).filter((line) => line !== '');
|
|
253
|
+
|
|
254
|
+
if (lines.length <= OUTPUT_SUMMARY_LINES * 2) {
|
|
255
|
+
return lines;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return [
|
|
259
|
+
...lines.slice(0, OUTPUT_SUMMARY_LINES),
|
|
260
|
+
`……(共 ${lines.length} 行)`,
|
|
261
|
+
...lines.slice(-OUTPUT_SUMMARY_LINES),
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 执行判据并落证书(同指纹在册直接复用秒回)
|
|
267
|
+
*
|
|
268
|
+
* @param input - 执行入参
|
|
269
|
+
* @param input.root - 仓库根绝对路径
|
|
270
|
+
* @param input.criterion - 判据
|
|
271
|
+
* @param input.fingerprint - 预计算指纹(缺省现算)
|
|
272
|
+
* @returns 执行结果
|
|
273
|
+
*/
|
|
274
|
+
export function runCriterionViaVault(input: RunCriterionInput): CriterionRunResult {
|
|
275
|
+
/**
|
|
276
|
+
* 判据指纹
|
|
277
|
+
*/
|
|
278
|
+
const fingerprint = input.fingerprint ?? computeCriterionFingerprint(input.root, input.criterion);
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 在册证书(复用秒回)
|
|
282
|
+
*/
|
|
283
|
+
const existing = findCertificate(input.root, input.criterion.channel, fingerprint);
|
|
284
|
+
|
|
285
|
+
if (existing !== null && existing.exitCode === 0) {
|
|
286
|
+
return { green: true, reused: true, certificate: existing };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 执行时刻
|
|
291
|
+
*/
|
|
292
|
+
const executedAt = new Date().toISOString();
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
/**
|
|
296
|
+
* 判据执行输出(/bin/sh -c,cwd 锚仓库根;stdio pipe 采集)
|
|
297
|
+
*/
|
|
298
|
+
const output = execFileSync('/bin/sh', ['-c', input.criterion.command], {
|
|
299
|
+
cwd: join(input.root, input.criterion.cwd),
|
|
300
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
301
|
+
}).toString('utf8');
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 绿证书
|
|
305
|
+
*/
|
|
306
|
+
const certificate: CriterionCertificate = {
|
|
307
|
+
fingerprint,
|
|
308
|
+
channel: input.criterion.channel,
|
|
309
|
+
command: input.criterion.command,
|
|
310
|
+
cwd: input.criterion.cwd,
|
|
311
|
+
exitCode: 0,
|
|
312
|
+
outputSummary: summarizeOutput(output),
|
|
313
|
+
executedAt,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
writeCertificate(input.root, certificate);
|
|
317
|
+
|
|
318
|
+
return { green: true, reused: false, certificate };
|
|
319
|
+
} catch (error) {
|
|
320
|
+
/**
|
|
321
|
+
* 非零退出异常对象(Reflect.get 无断言提取)
|
|
322
|
+
*/
|
|
323
|
+
const runError = typeof error === 'object' && error !== null ? error : {};
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 退出码
|
|
327
|
+
*/
|
|
328
|
+
const status = Reflect.get(runError, 'status');
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 标准输出
|
|
332
|
+
*/
|
|
333
|
+
const stdout = Reflect.get(runError, 'stdout');
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 标准错误
|
|
337
|
+
*/
|
|
338
|
+
const stderr = Reflect.get(runError, 'stderr');
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* 红证书(失败也落档)
|
|
342
|
+
*/
|
|
343
|
+
const certificate: CriterionCertificate = {
|
|
344
|
+
fingerprint,
|
|
345
|
+
channel: input.criterion.channel,
|
|
346
|
+
command: input.criterion.command,
|
|
347
|
+
cwd: input.criterion.cwd,
|
|
348
|
+
exitCode: typeof status === 'number' ? status : 127,
|
|
349
|
+
outputSummary: summarizeOutput(
|
|
350
|
+
`${typeof stdout === 'string' ? stdout : ''}\n${typeof stderr === 'string' ? stderr : ''}`.trim(),
|
|
351
|
+
),
|
|
352
|
+
executedAt,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
writeCertificate(input.root, certificate);
|
|
356
|
+
|
|
357
|
+
return { green: false, reused: false, certificate };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 落证书(temp+rename 原子写)
|
|
363
|
+
*
|
|
364
|
+
* @param root - 仓库根绝对路径
|
|
365
|
+
* @param certificate - 证书
|
|
366
|
+
*/
|
|
367
|
+
function writeCertificate(root: string, certificate: CriterionCertificate): void {
|
|
368
|
+
/**
|
|
369
|
+
* 证书路径
|
|
370
|
+
*/
|
|
371
|
+
const path = certificatePath(root, certificate.channel, certificate.fingerprint);
|
|
372
|
+
|
|
373
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* 临时路径
|
|
377
|
+
*/
|
|
378
|
+
const tempPath = `${path}.tmp`;
|
|
379
|
+
|
|
380
|
+
writeFileSync(tempPath, `${JSON.stringify(certificate, null, 2)}\n`);
|
|
381
|
+
|
|
382
|
+
renameSync(tempPath, path);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* 运行锁句柄
|
|
387
|
+
*/
|
|
388
|
+
export type RunLockHandle = {
|
|
389
|
+
/**
|
|
390
|
+
* 释放锁(仅释放本进程写入的锁)
|
|
391
|
+
*/
|
|
392
|
+
unlock: () => void;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* 运行锁(防并行真跑互踩;进程退出自动释放)
|
|
397
|
+
*
|
|
398
|
+
* @param root - 仓库根绝对路径
|
|
399
|
+
* @returns 锁句柄(unlock 释放;持锁失败抛错)
|
|
400
|
+
* @throws {@link Error} 已有并行真跑持锁
|
|
401
|
+
*/
|
|
402
|
+
export function acquireRunLock(root: string): RunLockHandle {
|
|
403
|
+
/**
|
|
404
|
+
* 锁文件路径
|
|
405
|
+
*/
|
|
406
|
+
const lockPath = join(root, '.lzi', 'issues', 'vault', 'run.lock');
|
|
407
|
+
|
|
408
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
409
|
+
|
|
410
|
+
if (existsSync(lockPath)) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`已有并行真跑持锁(${lockPath})——完成后自动释放;如为残留死锁请人工确认后删除`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
writeFileSync(lockPath, `${process.pid}\n`);
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
unlock: () => {
|
|
420
|
+
try {
|
|
421
|
+
/**
|
|
422
|
+
* 锁内 PID(仅释放本进程写入的锁)
|
|
423
|
+
*/
|
|
424
|
+
const holder = readFileSync(lockPath, 'utf8').trim();
|
|
425
|
+
|
|
426
|
+
if (holder === String(process.pid)) {
|
|
427
|
+
rmSync(lockPath, { force: true });
|
|
428
|
+
}
|
|
429
|
+
} catch {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
}
|