@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +26 -0
  3. package/dist/index.d.ts +2123 -0
  4. package/dist/index.js +78 -0
  5. package/dist/rolldown-runtime-BqT_7tdF.js +1 -0
  6. package/lzi-builder.config.ts +15 -0
  7. package/lzi-bun.config.ts +8 -0
  8. package/oxlint.config.ts +3 -0
  9. package/package.json +39 -0
  10. package/src/__tests__/close/verify-close.commands.test.ts +560 -0
  11. package/src/__tests__/docs-refs/docs-refs.commands.test.ts +213 -0
  12. package/src/__tests__/fs/mini-glob.utils.test.ts +71 -0
  13. package/src/__tests__/fs/walk-surface.utils.test.ts +170 -0
  14. package/src/__tests__/health/health.commands.test.ts +131 -0
  15. package/src/__tests__/lint/lint.core.test.ts +332 -0
  16. package/src/__tests__/numbering/numbering.commands.test.ts +194 -0
  17. package/src/__tests__/numbering/numbering.core.test.ts +318 -0
  18. package/src/__tests__/parser/guide-fixture.utils.test.ts +45 -0
  19. package/src/__tests__/parser/registry.parser.test.ts +316 -0
  20. package/src/__tests__/prompts/prompts.commands.test.ts +47 -0
  21. package/src/__tests__/template/issue-template.renderer.test.ts +133 -0
  22. package/src/close/evidence-reader.utils.ts +201 -0
  23. package/src/close/green-flip.commands.ts +199 -0
  24. package/src/close/verify-close.commands.ts +676 -0
  25. package/src/docs-refs/docs-refs.commands.ts +693 -0
  26. package/src/freeze/freeze.commands.ts +261 -0
  27. package/src/fs/mini-glob.utils.ts +216 -0
  28. package/src/fs/walk-surface.utils.ts +298 -0
  29. package/src/health/health.commands.ts +327 -0
  30. package/src/index.ts +46 -0
  31. package/src/lint/lint-baseline.commands.ts +147 -0
  32. package/src/lint/lint.core.ts +584 -0
  33. package/src/normalize/normalize-header.commands.ts +361 -0
  34. package/src/numbering/numbering.commands.ts +375 -0
  35. package/src/numbering/numbering.core.ts +685 -0
  36. package/src/parser/format.utils.ts +279 -0
  37. package/src/parser/guide-fixture.utils.ts +117 -0
  38. package/src/parser/registry.parser.ts +679 -0
  39. package/src/prompts/prompts.commands.ts +211 -0
  40. package/src/template/issue-template.renderer.ts +351 -0
  41. package/src/triage/triage.classify.ts +93 -0
  42. package/src/vault/vault.commands.ts +434 -0
  43. package/tsconfig/.cache/build.tsbuildinfo +1 -0
  44. package/tsconfig/.cache/node.tsbuildinfo +1 -0
  45. package/tsconfig/.cache/test.tsbuildinfo +1 -0
  46. package/tsconfig/app.json +13 -0
  47. package/tsconfig/build.json +15 -0
  48. package/tsconfig/node.json +12 -0
  49. package/tsconfig/test.json +15 -0
  50. package/tsconfig.json +23 -0
@@ -0,0 +1,676 @@
1
+ /**
2
+ * verify-close 收口编排(两模式)
3
+ *
4
+ * 缺省=证据收口(零执行):冻结守卫→挂起词拦截→锚点判读→阻塞全绿判定→
5
+ * 概要合成→green-flip 幂等回填;批量串行一案失败不阻断。
6
+ * --rerun=证书化真跑:判据解析→两级门禁(禁用级全量 exit 2、确认级重度
7
+ * exit 2)→run.lock→逐判据经 vault 执行/复用→全绿才回填。
8
+ */
9
+
10
+ import type { LziIssuesConfig } from '@longzai-intelligence-issues/config';
11
+
12
+ import { readFileSync, writeFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+
15
+ import { findPendingWord, readEvidence } from '@/close/evidence-reader.utils';
16
+ import {
17
+ applyGreenFlip,
18
+ renderEvidenceCloseBlock,
19
+ renderRerunEvidenceBlock,
20
+ } from '@/close/green-flip.commands';
21
+ import { parseIssueDoc, scanRegistryDir } from '@/parser/registry.parser';
22
+ import {
23
+ acquireRunLock,
24
+ computeCriterionFingerprint,
25
+ type Criterion,
26
+ runCriterionViaVault,
27
+ } from '@/vault/vault.commands';
28
+
29
+ /**
30
+ * 证据收口入参
31
+ */
32
+ export type EvidenceCloseInput = {
33
+ /**
34
+ * 仓库根绝对路径
35
+ */
36
+ root: string;
37
+
38
+ /**
39
+ * 解析后配置
40
+ */
41
+ config: LziIssuesConfig;
42
+
43
+ /**
44
+ * issue 文件仓库相对路径列表
45
+ */
46
+ files: readonly string[];
47
+
48
+ /**
49
+ * 显式概要(批量禁用)
50
+ */
51
+ summaryOverride?: string;
52
+
53
+ /**
54
+ * 只呈现不落盘
55
+ */
56
+ dryRun?: boolean;
57
+ };
58
+
59
+ /**
60
+ * 单案收口结果
61
+ */
62
+ export type CloseCaseResult = {
63
+ /**
64
+ * issue 文件仓库相对路径
65
+ */
66
+ file: string;
67
+
68
+ /**
69
+ * 是否翻绿成功
70
+ */
71
+ ok: boolean;
72
+
73
+ /**
74
+ * 结果说明(失败原因 / 概要)
75
+ */
76
+ message: string;
77
+
78
+ /**
79
+ * 幂等标记(已绿且概要一致零变更)
80
+ */
81
+ idempotent: boolean;
82
+ };
83
+
84
+ /**
85
+ * 批次收口结果
86
+ */
87
+ export type CloseBatchResult = {
88
+ /**
89
+ * 逐案结果
90
+ */
91
+ cases: CloseCaseResult[];
92
+
93
+ /**
94
+ * 成功案数
95
+ */
96
+ succeeded: number;
97
+
98
+ /**
99
+ * 门禁拦截标记(exit 2 语义;真跑模式未过两级门禁时为真,零执行)
100
+ */
101
+ gateBlocked: boolean;
102
+
103
+ /**
104
+ * 门禁拦截说明
105
+ */
106
+ gateMessage: string;
107
+ };
108
+
109
+ /**
110
+ * 阻塞全绿判定(blockingMustBeGreen 门禁)
111
+ *
112
+ * @param root - 仓库根绝对路径
113
+ * @param registryDir - 注册表目录
114
+ * @param blockedByNumbers - 阻塞引用编号列表
115
+ * @returns 未全绿编号列表(空 = 通过)
116
+ */
117
+ function findUnGreenBlockers(
118
+ root: string,
119
+ registryDir: string,
120
+ blockedByNumbers: readonly string[],
121
+ ): string[] {
122
+ if (blockedByNumbers.length === 0) {
123
+ return [];
124
+ }
125
+
126
+ /**
127
+ * 注册表全部解析产物
128
+ */
129
+ const records = scanRegistryDir(join(root, registryDir));
130
+
131
+ return blockedByNumbers.filter((number) => {
132
+ /**
133
+ * 被阻塞 issue 解析产物
134
+ */
135
+ const blocker = records.find((record) => record.id === number);
136
+
137
+ return (
138
+ blocker === undefined || (blocker.status !== 'green' && blocker.status !== 'legacy-green')
139
+ );
140
+ });
141
+ }
142
+
143
+ /**
144
+ * 证据收口批(缺省模式,零执行)
145
+ *
146
+ * @param input - 命令入参
147
+ * @param input.root - 仓库根绝对路径
148
+ * @param input.config - 解析后配置
149
+ * @param input.files - issue 文件仓库相对路径列表
150
+ * @param input.summaryOverride - 显式概要(批量禁用)
151
+ * @param input.dryRun - 只呈现不落盘
152
+ * @returns 批次结果
153
+ */
154
+ export function evidenceCloseFlow(input: EvidenceCloseInput): CloseBatchResult {
155
+ /**
156
+ * 逐案结果收集器
157
+ */
158
+ const cases: CloseCaseResult[] = [];
159
+
160
+ for (const file of input.files) {
161
+ cases.push(
162
+ evidenceCloseCase(
163
+ input.root,
164
+ input.config,
165
+ file,
166
+ input.summaryOverride,
167
+ input.dryRun === true,
168
+ ),
169
+ );
170
+ }
171
+
172
+ return {
173
+ cases,
174
+ succeeded: cases.filter((entry) => entry.ok).length,
175
+ gateBlocked: false,
176
+ gateMessage: '',
177
+ };
178
+ }
179
+
180
+ /**
181
+ * 单案证据收口
182
+ *
183
+ * @param root - 仓库根绝对路径
184
+ * @param config - 解析后配置
185
+ * @param file - issue 文件仓库相对路径
186
+ * @param summaryOverride - 显式概要
187
+ * @param dryRun - 只呈现不落盘
188
+ * @returns 单案结果
189
+ */
190
+ function evidenceCloseCase(
191
+ root: string,
192
+ config: LziIssuesConfig,
193
+ file: string,
194
+ summaryOverride: string | undefined,
195
+ dryRun: boolean,
196
+ ): CloseCaseResult {
197
+ /**
198
+ * issue 文件绝对路径
199
+ */
200
+ const filePath = join(root, file);
201
+
202
+ /**
203
+ * 文档全文
204
+ */
205
+ let raw = '';
206
+
207
+ try {
208
+ raw = readFileSync(filePath, 'utf8');
209
+ } catch {
210
+ return { file, ok: false, message: '读档失败(文件不存在或不可读)', idempotent: false };
211
+ }
212
+
213
+ /**
214
+ * 解析产物
215
+ */
216
+ const record = parseIssueDoc(file.split('/').pop() ?? '', raw, 0);
217
+
218
+ if (record === null) {
219
+ return { file, ok: false, message: '文件名不符合注册表口径', idempotent: false };
220
+ }
221
+
222
+ /**
223
+ * 冻结守卫
224
+ */
225
+ if (record.status === 'frozen') {
226
+ return {
227
+ file,
228
+ ok: false,
229
+ message: '冻结案拒收(不翻绿、不回填证据——须先经 unfreeze 裁定)',
230
+ idempotent: false,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * 挂起词拦截
236
+ */
237
+ const pendingWord = findPendingWord(record.statusLine, config.close.pendingWords);
238
+
239
+ if (pendingWord !== null) {
240
+ return {
241
+ file,
242
+ ok: false,
243
+ message: `状态行含挂起词「${pendingWord}」——诚实黄灯维持,不得收口`,
244
+ idempotent: false,
245
+ };
246
+ }
247
+
248
+ /**
249
+ * 阻塞全绿判定
250
+ */
251
+ if (config.close.blockingMustBeGreen) {
252
+ /**
253
+ * 注册表目录(文件路径首段)
254
+ */
255
+ const registryDir = file.split('/').slice(0, -1).join('/');
256
+
257
+ /**
258
+ * 未全绿阻塞列表
259
+ */
260
+ const unGreen = findUnGreenBlockers(
261
+ root,
262
+ registryDir,
263
+ record.blockedByRefs.map((ref) => ref.number),
264
+ );
265
+
266
+ if (unGreen.length > 0) {
267
+ return {
268
+ file,
269
+ ok: false,
270
+ message: `「阻塞」引用未全 🟢:${unGreen.join('、')}——阻塞收口门禁拒收`,
271
+ idempotent: false,
272
+ };
273
+ }
274
+ }
275
+
276
+ /**
277
+ * 锚点判读
278
+ */
279
+ const evidence = readEvidence(raw);
280
+
281
+ if (evidence.anchors.length === 0) {
282
+ return {
283
+ file,
284
+ ok: false,
285
+ message: '处置段围栏外零绿证据锚点——不核验即翻绿红线维持(补证据或走 --rerun 真跑)',
286
+ idempotent: false,
287
+ };
288
+ }
289
+
290
+ /**
291
+ * 概要合成(--summary > criteria.summary > 自动)
292
+ */
293
+ const summary =
294
+ summaryOverride ??
295
+ record.criteria?.summary ??
296
+ `末次通过证据收口(${record.id}):${evidence.anchors.length} 条绿证据锚点`;
297
+
298
+ /**
299
+ * 证据锚点块
300
+ */
301
+ const evidenceBlock = renderEvidenceCloseBlock(
302
+ evidence.anchors.map((anchor) => anchor.line),
303
+ '末次通过即收口',
304
+ );
305
+
306
+ /**
307
+ * 翻绿回填
308
+ */
309
+ let flip;
310
+
311
+ try {
312
+ flip = applyGreenFlip({ raw, summary, evidenceBlock });
313
+ } catch (error) {
314
+ return {
315
+ file,
316
+ ok: false,
317
+ message: error instanceof Error ? error.message : String(error),
318
+ idempotent: false,
319
+ };
320
+ }
321
+
322
+ if (!dryRun && !flip.idempotent) {
323
+ writeFileSync(filePath, flip.content);
324
+ }
325
+
326
+ return {
327
+ file,
328
+ ok: true,
329
+ message: flip.idempotent
330
+ ? '已 🟢 且概要一致——幂等零变更'
331
+ : `翻绿完成(${evidence.anchors.length} 条锚点,零执行)`,
332
+ idempotent: flip.idempotent,
333
+ };
334
+ }
335
+
336
+ /**
337
+ * 两级门禁判定结果
338
+ */
339
+ export type CloseGateResult = {
340
+ /**
341
+ * 是否拦截
342
+ */
343
+ blocked: boolean;
344
+
345
+ /**
346
+ * 拦截级(full 禁用级 / heavy 确认级 / none 通过)
347
+ */
348
+ kind: 'full' | 'heavy' | 'none';
349
+
350
+ /**
351
+ * 拦截说明
352
+ */
353
+ message: string;
354
+ };
355
+
356
+ /**
357
+ * 真跑两级门禁判定
358
+ *
359
+ * @param config - 解析后配置
360
+ * @param criteria - 判据列表
361
+ * @param allowFullCriterion - --allow-full-criterion 豁免
362
+ * @param confirmHeavy - --confirm-heavy 确认
363
+ * @returns 拦截结果(blocked=false 通过)
364
+ */
365
+ export function resolveCloseGates(
366
+ config: LziIssuesConfig,
367
+ criteria: readonly Criterion[],
368
+ allowFullCriterion: boolean,
369
+ confirmHeavy: boolean,
370
+ ): CloseGateResult {
371
+ /**
372
+ * 禁用级:全量判据(fullPattern 命中)
373
+ */
374
+ if (!allowFullCriterion && config.close.fullPattern !== undefined) {
375
+ /**
376
+ * 全量命中判据列表
377
+ */
378
+ const fullHits = criteria.filter((criterion) =>
379
+ new RegExp(config.close.fullPattern ?? '').test(criterion.command),
380
+ );
381
+
382
+ if (fullHits.length > 0) {
383
+ return {
384
+ blocked: true,
385
+ kind: 'full',
386
+ message: `禁用级门禁:判据须 scope 定向,禁止全量(命中 ${fullHits.length} 条:${fullHits.map((c) => c.label).join('、')})——确需全量须 --allow-full-criterion 且留痕`,
387
+ };
388
+ }
389
+ }
390
+
391
+ /**
392
+ * 确认级:重度命令(heavyPattern 命中)
393
+ */
394
+ if (!confirmHeavy && config.close.heavyPattern !== undefined) {
395
+ /**
396
+ * 重度命中判据列表
397
+ */
398
+ const heavyHits = criteria.filter((criterion) =>
399
+ new RegExp(config.close.heavyPattern ?? '').test(criterion.command),
400
+ );
401
+
402
+ if (heavyHits.length > 0) {
403
+ return {
404
+ blocked: true,
405
+ kind: 'heavy',
406
+ message: `确认级门禁:判据含重度命令(命中 ${heavyHits.length} 条:${heavyHits.map((c) => c.label).join('、')})——预览未执行,确认执行须 --confirm-heavy(仅人类明令环节)`,
407
+ };
408
+ }
409
+ }
410
+
411
+ return { blocked: false, kind: 'none', message: '' };
412
+ }
413
+
414
+ /**
415
+ * 证书化真跑入参
416
+ */
417
+ export type RerunCloseInput = {
418
+ /**
419
+ * 仓库根绝对路径
420
+ */
421
+ root: string;
422
+
423
+ /**
424
+ * 解析后配置
425
+ */
426
+ config: LziIssuesConfig;
427
+
428
+ /**
429
+ * issue 文件仓库相对路径列表
430
+ */
431
+ files: readonly string[];
432
+
433
+ /**
434
+ * 显式判据(四段字符串列表,公共判据垫后)
435
+ */
436
+ explicitCriteria?: readonly string[];
437
+
438
+ /**
439
+ * 从各案 criteria 块取判据
440
+ */
441
+ criteriaFromIssue?: boolean;
442
+
443
+ /**
444
+ * 显式概要(批量禁用)
445
+ */
446
+ summaryOverride?: string;
447
+
448
+ /**
449
+ * 只呈现不执行不落盘
450
+ */
451
+ dryRun?: boolean;
452
+
453
+ /**
454
+ * 全量判据豁免(须留痕)
455
+ */
456
+ allowFullCriterion?: boolean;
457
+
458
+ /**
459
+ * 重度命令确认(仅人类明令)
460
+ */
461
+ confirmHeavy?: boolean;
462
+ };
463
+
464
+ /**
465
+ * 证书化真跑批(--rerun)
466
+ *
467
+ * @param input - 命令入参
468
+ * @param input.root - 仓库根绝对路径
469
+ * @param input.config - 解析后配置
470
+ * @param input.files - issue 文件仓库相对路径列表
471
+ * @param input.explicitCriteria - 显式判据(四段字符串列表,公共判据垫后)
472
+ * @param input.criteriaFromIssue - 从各案 criteria 块取判据
473
+ * @param input.summaryOverride - 显式概要(批量禁用)
474
+ * @param input.dryRun - 只呈现不执行不落盘(门禁拦截外的干跑)
475
+ * @param input.allowFullCriterion - 全量豁免
476
+ * @param input.confirmHeavy - 重度确认
477
+ * @returns 批次结果(gateBlocked=true 时零执行,exit 2 语义)
478
+ */
479
+ export function rerunCloseFlow(input: RerunCloseInput): CloseBatchResult {
480
+ /**
481
+ * 逐案判据计划
482
+ */
483
+ const plans: Array<{ file: string; raw: string; criteria: Criterion[] }> = [];
484
+
485
+ for (const file of input.files) {
486
+ /**
487
+ * 文档全文
488
+ */
489
+ let raw = '';
490
+
491
+ try {
492
+ raw = readFileSync(join(input.root, file), 'utf8');
493
+ } catch {
494
+ continue;
495
+ }
496
+
497
+ /**
498
+ * 解析产物
499
+ */
500
+ const record = parseIssueDoc(file.split('/').pop() ?? '', raw, 0);
501
+
502
+ if (record === null || record.status === 'frozen') {
503
+ continue;
504
+ }
505
+
506
+ /**
507
+ * 本案判据集(块内在前 + 显式垫后)
508
+ */
509
+ const criteria: Criterion[] = [];
510
+
511
+ if (input.criteriaFromIssue === true && record.criteria !== null) {
512
+ criteria.push(...record.criteria.criteria);
513
+ }
514
+
515
+ for (const line of input.explicitCriteria ?? []) {
516
+ /**
517
+ * 四段切分
518
+ */
519
+ const segments = line.split('|');
520
+
521
+ if (segments.length === 4) {
522
+ criteria.push({
523
+ label: segments[0]?.trim() ?? '',
524
+ channel: segments[1]?.trim() ?? '',
525
+ cwd: segments[2]?.trim() ?? '',
526
+ command: segments[3]?.trim() ?? '',
527
+ });
528
+ }
529
+ }
530
+
531
+ if (criteria.length === 0) {
532
+ continue;
533
+ }
534
+
535
+ plans.push({ file, raw, criteria });
536
+ }
537
+
538
+ /**
539
+ * 全案判据并集(两级门禁整批判定)
540
+ */
541
+ const allCriteria = plans.flatMap((plan) => plan.criteria);
542
+
543
+ /**
544
+ * 两级门禁
545
+ */
546
+ const gate = resolveCloseGates(
547
+ input.config,
548
+ allCriteria,
549
+ input.allowFullCriterion === true,
550
+ input.confirmHeavy === true,
551
+ );
552
+
553
+ if (gate.blocked) {
554
+ return {
555
+ cases: plans.map((plan) => ({
556
+ file: plan.file,
557
+ ok: false,
558
+ message: `门禁拦截(${gate.kind}),零执行`,
559
+ idempotent: false,
560
+ })),
561
+ succeeded: 0,
562
+ gateBlocked: true,
563
+ gateMessage: gate.message,
564
+ };
565
+ }
566
+
567
+ if (input.dryRun === true) {
568
+ return {
569
+ cases: plans.map((plan) => ({
570
+ file: plan.file,
571
+ ok: true,
572
+ message: `干跑预览(${plan.criteria.length} 条判据,未执行)`,
573
+ idempotent: false,
574
+ })),
575
+ succeeded: plans.length,
576
+ gateBlocked: false,
577
+ gateMessage: '',
578
+ };
579
+ }
580
+
581
+ /**
582
+ * 运行锁(防并行互踩)
583
+ */
584
+ const lock = acquireRunLock(input.root);
585
+
586
+ /**
587
+ * 逐案结果收集器
588
+ */
589
+ const cases: CloseCaseResult[] = [];
590
+
591
+ try {
592
+ for (const plan of plans) {
593
+ /**
594
+ * 逐判据执行结果
595
+ */
596
+ const runs = plan.criteria.map((criterion) => {
597
+ /**
598
+ * 预计算指纹
599
+ */
600
+ const fingerprint = computeCriterionFingerprint(input.root, criterion);
601
+
602
+ return {
603
+ criterion,
604
+ fingerprint,
605
+ run: runCriterionViaVault({ root: input.root, criterion, fingerprint }),
606
+ };
607
+ });
608
+
609
+ /**
610
+ * 未绿判据
611
+ */
612
+ const redCriterions = runs.filter((entry) => !entry.run.green);
613
+
614
+ if (redCriterions.length > 0) {
615
+ cases.push({
616
+ file: plan.file,
617
+ ok: false,
618
+ message: `判据未全绿(${redCriterions.map((e) => `${e.criterion.label} 退出码 ${e.run.certificate.exitCode}`).join('、')})——跳过翻绿`,
619
+ idempotent: false,
620
+ });
621
+
622
+ continue;
623
+ }
624
+
625
+ /**
626
+ * 解析产物(概要合成)
627
+ */
628
+ const record = parseIssueDoc(plan.file.split('/').pop() ?? '', plan.raw, 0);
629
+
630
+ /**
631
+ * 概要
632
+ */
633
+ const summary =
634
+ input.summaryOverride ??
635
+ record?.criteria?.summary ??
636
+ `批量证书化收口(${record?.id ?? ''}):${runs.length} 条判据全绿`;
637
+
638
+ /**
639
+ * 证据行(逐判据证书指纹)
640
+ */
641
+ const evidenceLines = runs.map(
642
+ (entry) =>
643
+ `${entry.criterion.label}:证书指纹 ${entry.run.certificate.fingerprint}(${entry.run.reused ? '复用' : '真实执行'},退出码 ${entry.run.certificate.exitCode})`,
644
+ );
645
+
646
+ /**
647
+ * 翻绿回填
648
+ */
649
+ const flip = applyGreenFlip({
650
+ raw: plan.raw,
651
+ summary,
652
+ evidenceBlock: renderRerunEvidenceBlock(summary, evidenceLines, 'issues:verify-close'),
653
+ });
654
+
655
+ if (!flip.idempotent) {
656
+ writeFileSync(join(input.root, plan.file), flip.content);
657
+ }
658
+
659
+ cases.push({
660
+ file: plan.file,
661
+ ok: true,
662
+ message: `证书化收口完成(${runs.length} 条判据全绿${flip.idempotent ? ',幂等零变更' : ''})`,
663
+ idempotent: flip.idempotent,
664
+ });
665
+ }
666
+ } finally {
667
+ lock.unlock();
668
+ }
669
+
670
+ return {
671
+ cases,
672
+ succeeded: cases.filter((entry) => entry.ok).length,
673
+ gateBlocked: false,
674
+ gateMessage: '',
675
+ };
676
+ }