@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,47 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { listPrompts, showPrompt } from '@/prompts/prompts.commands';
4
+
5
+ describe('listPrompts', () => {
6
+ test('内置两模板在场且内容完整', () => {
7
+ const prompts = listPrompts({});
8
+
9
+ expect(prompts.map((entry) => entry.id)).toEqual(['issue-create-and-process', 'issue-process']);
10
+ expect(prompts.every((entry) => entry.source === 'builtin')).toBe(true);
11
+
12
+ /**
13
+ * create 模板关键节
14
+ */
15
+ const createPrompt = showPrompt('issue-create-and-process', {});
16
+
17
+ expect(createPrompt?.content).toContain('范围判定先于建档');
18
+ expect(createPrompt?.content).toContain('门禁取号');
19
+ expect(createPrompt?.content).toContain('【1】备选方案和分析');
20
+ expect(createPrompt?.content).toContain('verify-close');
21
+
22
+ /**
23
+ * process 模板关键节
24
+ */
25
+ const processPrompt = showPrompt('issue-process', {});
26
+
27
+ expect(processPrompt?.content).toContain('引用必须唯一定位');
28
+ expect(processPrompt?.content).toContain('终态拦截');
29
+ });
30
+
31
+ test('builtin 关闭时仅 extra', () => {
32
+ expect(listPrompts({ builtin: false })).toEqual([]);
33
+ });
34
+
35
+ test('extraDir 加载并按 id 覆盖内置', () => {
36
+ /**
37
+ * extra 目录(复用本测试文件目录下的临时文件不可行——直接以 fixtures 目录断言缺席容错)
38
+ */
39
+ const prompts = listPrompts({ extraDir: '/nonexistent/prompts-dir' });
40
+
41
+ expect(prompts).toHaveLength(2);
42
+ });
43
+
44
+ test('showPrompt 未命中返回 null', () => {
45
+ expect(showPrompt('ghost-prompt', {})).toBeNull();
46
+ });
47
+ });
@@ -0,0 +1,133 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { parseCriteriaBlock, parseIssueDoc } from '@/parser/registry.parser';
4
+ import {
5
+ renderCriteriaBlockExample,
6
+ renderIssueTemplate,
7
+ renderIssueTemplateSkeleton,
8
+ renderRegistryReadmeStub,
9
+ } from '@/template/issue-template.renderer';
10
+
11
+ /**
12
+ * instance 模式渲染入参(含沿革与阻塞)
13
+ */
14
+ const SAMPLE_VALUES = {
15
+ number: '0035',
16
+ slug: 'runs-list-grace',
17
+ title: '宽限期趟数合成兜底缺失',
18
+ priority: 'P2' as const,
19
+ scopeTitle: 'issues 项目域',
20
+ date: '2026-09-14',
21
+ source: '用户裁定(2026-09-14,会话正文)',
22
+ historyRefs: [{ number: '0034', slug: 'prior-case' }],
23
+ blockedByRefs: [{ number: '0036', slug: 'blocker-case' }, { number: '0037' }],
24
+ };
25
+
26
+ describe('renderIssueTemplateSkeleton', () => {
27
+ test('骨架头部七键齐全且次序规范', () => {
28
+ const skeleton = renderIssueTemplateSkeleton();
29
+
30
+ expect(skeleton.startsWith('# <NNNN> - ')).toBe(true);
31
+ expect(skeleton.indexOf('**优先级**')).toBeLessThan(skeleton.indexOf('**状态**'));
32
+ expect(skeleton.indexOf('**状态**')).toBeLessThan(skeleton.indexOf('**立案日期**'));
33
+ expect(skeleton.indexOf('**立案日期**')).toBeLessThan(skeleton.indexOf('**所属域**'));
34
+ expect(skeleton.indexOf('**所属域**')).toBeLessThan(skeleton.indexOf('**来源**'));
35
+ expect(skeleton.indexOf('**来源**')).toBeLessThan(skeleton.indexOf('**沿革**'));
36
+ expect(skeleton.indexOf('**沿革**')).toBeLessThan(skeleton.indexOf('**阻塞**'));
37
+ });
38
+
39
+ test('骨架正文四段齐全且次序规范', () => {
40
+ const skeleton = renderIssueTemplateSkeleton();
41
+
42
+ expect(skeleton.indexOf('## 一、问题动机')).toBeLessThan(skeleton.indexOf('## 二、现状与证据'));
43
+ expect(skeleton.indexOf('## 二、现状与证据')).toBeLessThan(skeleton.indexOf('## 三、处置方向'));
44
+ expect(skeleton.indexOf('## 三、处置方向')).toBeLessThan(
45
+ skeleton.indexOf('## 四、处置结果/验证口径'),
46
+ );
47
+ });
48
+
49
+ test('骨架本身可被解析器读档(建档态 red)', () => {
50
+ const record = parseIssueDoc('0700-format-sample.md', renderIssueTemplateSkeleton(), 0);
51
+
52
+ expect(record?.status).toBe('red');
53
+ });
54
+ });
55
+
56
+ describe('renderIssueTemplate', () => {
57
+ test('实例渲染回环:落盘名解析 + 状态 red + 标题提取', () => {
58
+ const content = renderIssueTemplate(SAMPLE_VALUES);
59
+
60
+ const record = parseIssueDoc(`${SAMPLE_VALUES.number}-${SAMPLE_VALUES.slug}.md`, content, 0);
61
+
62
+ expect(record?.id).toBe('0035');
63
+ expect(record?.slug).toBe('runs-list-grace');
64
+ expect(record?.title).toBe('0035 - 宽限期趟数合成兜底缺失');
65
+ expect(record?.status).toBe('red');
66
+ expect(record?.pendingReason).toBeNull();
67
+ });
68
+
69
+ test('实例渲染沿革阻塞引用回环(链接与纯编号两形态)', () => {
70
+ const record = parseIssueDoc(
71
+ `${SAMPLE_VALUES.number}-${SAMPLE_VALUES.slug}.md`,
72
+ renderIssueTemplate(SAMPLE_VALUES),
73
+ 0,
74
+ );
75
+
76
+ expect(record?.historyRefs).toEqual([{ number: '0034', slug: 'prior-case' }]);
77
+ expect(record?.blockedByRefs).toEqual([
78
+ { number: '0036', slug: 'blocker-case' },
79
+ { number: '0037', slug: null },
80
+ ]);
81
+ });
82
+
83
+ test('可选引用缺席时省略对应元信息行', () => {
84
+ const content = renderIssueTemplate({
85
+ ...SAMPLE_VALUES,
86
+ historyRefs: undefined,
87
+ blockedByRefs: undefined,
88
+ });
89
+
90
+ expect(content.includes('**沿革**')).toBe(false);
91
+ expect(content.includes('**阻塞**')).toBe(false);
92
+ });
93
+
94
+ test('实例骨架与骨架正文四段共享占位文本(结构零漂移)', () => {
95
+ const instance = renderIssueTemplate(SAMPLE_VALUES);
96
+
97
+ for (const heading of [
98
+ '## 一、问题动机',
99
+ '## 二、现状与证据',
100
+ '## 三、处置方向',
101
+ '## 四、处置结果/验证口径',
102
+ ]) {
103
+ expect(instance.includes(heading)).toBe(true);
104
+ }
105
+
106
+ expect(instance.includes('> <问题原文逐字记录,禁止转述改写>')).toBe(true);
107
+ });
108
+ });
109
+
110
+ describe('renderCriteriaBlockExample', () => {
111
+ test('示例块本身通过解析器机检(照抄即合法)', () => {
112
+ const parsed = parseCriteriaBlock(renderCriteriaBlockExample());
113
+
114
+ expect(parsed.summary).toContain('示例');
115
+ expect(parsed.criteria).toHaveLength(1);
116
+ expect(parsed.criteria[0]?.channel).toBe('test');
117
+ });
118
+ });
119
+
120
+ describe('renderRegistryReadmeStub', () => {
121
+ test('存根包含 scope 名、建档入口与指南链接', () => {
122
+ const stub = renderRegistryReadmeStub(
123
+ 'issues 项目域',
124
+ '../../docs/guides/issue-file-format.md',
125
+ );
126
+
127
+ expect(stub.includes('# issues 项目域 issue 注册表')).toBe(true);
128
+ expect(stub.includes('lzi-issues create')).toBe(true);
129
+ expect(stub.includes('../../docs/guides/issue-file-format.md')).toBe(true);
130
+ expect(stub.includes('🔴 待处理')).toBe(true);
131
+ expect(stub.includes('⚪ 已冻结')).toBe(true);
132
+ });
133
+ });
@@ -0,0 +1,201 @@
1
+ /**
2
+ * 绿证据锚点判读(证据收口缺省模式的判定核)
3
+ *
4
+ * 处置段定位(标题行起至下一 `## ` 或文末,跨内嵌围栏);段内围栏里行整体跳过
5
+ * (判据声明非执行结果),围栏外逐行按优先级归类锚点:certificate > exit-zero >
6
+ * all-green > adjudicated;零锚点拒收。挂起词拦截按状态行全文判定。
7
+ */
8
+
9
+ import { DISPOSITION_TITLE_KEYWORD, PENDING_REASON_PATTERN } from '@/parser/format.utils';
10
+
11
+ /**
12
+ * 锚点类别
13
+ */
14
+ export type EvidenceAnchorKind = 'certificate' | 'exit-zero' | 'all-green' | 'adjudicated';
15
+
16
+ /**
17
+ * 绿证据锚点
18
+ */
19
+ export type EvidenceAnchor = {
20
+ /**
21
+ * 锚点类别
22
+ */
23
+ kind: EvidenceAnchorKind;
24
+
25
+ /**
26
+ * 锚点原文行
27
+ */
28
+ line: string;
29
+ };
30
+
31
+ /**
32
+ * 证据判读结果
33
+ */
34
+ export type EvidenceReadResult = {
35
+ /**
36
+ * 锚点列表(按判读优先级序)
37
+ */
38
+ anchors: EvidenceAnchor[];
39
+
40
+ /**
41
+ * 处置段文本(定位失败为空串)
42
+ */
43
+ dispositionText: string;
44
+ };
45
+
46
+ /**
47
+ * 证书指纹锚点模式(64 位 hex)
48
+ */
49
+ const CERTIFICATE_PATTERN = /证书指纹\s+([0-9a-f]{64})/;
50
+
51
+ /**
52
+ * 退出码零锚点模式(两形态)
53
+ */
54
+ const EXIT_ZERO_PATTERN = /退出码\s*0(?!\d)|EXIT\s*=\s*0(?!\d)/i;
55
+
56
+ /**
57
+ * 全绿族词语锚点模式
58
+ */
59
+ const ALL_GREEN_PATTERN = /(?<![未待])全(?:绿|过|部通过)/;
60
+
61
+ /**
62
+ * 经裁定自验锚点模式
63
+ */
64
+ const ADJUDICATED_PATTERN = /用户[^。\n]{0,30}自验/;
65
+
66
+ /**
67
+ * 定位处置段文本(跨内嵌围栏)
68
+ *
69
+ * @param raw - 文档全文
70
+ * @returns 处置段文本(含标题行;缺席为空串)
71
+ */
72
+ export function locateDispositionText(raw: string): string {
73
+ /**
74
+ * 文档行数组
75
+ */
76
+ const lines = raw.split(/\r?\n/);
77
+
78
+ /**
79
+ * 处置段标题行下标
80
+ */
81
+ const startIndex = lines.findIndex(
82
+ (line) => line.startsWith('## ') && line.includes(DISPOSITION_TITLE_KEYWORD),
83
+ );
84
+
85
+ if (startIndex === -1) {
86
+ return '';
87
+ }
88
+
89
+ /**
90
+ * 段末行下标(下一 `## ` 标题或文末)
91
+ */
92
+ let endIndex = lines.length;
93
+
94
+ for (let index = startIndex + 1; index < lines.length; index += 1) {
95
+ if ((lines[index] ?? '').startsWith('## ')) {
96
+ endIndex = index;
97
+
98
+ break;
99
+ }
100
+ }
101
+
102
+ return lines.slice(startIndex, endIndex).join('\n');
103
+ }
104
+
105
+ /**
106
+ * 判读处置段围栏外锚点
107
+ *
108
+ * @param dispositionText - 处置段文本
109
+ * @returns 锚点列表(优先级降序归类,首个命中类别只取一次)
110
+ */
111
+ export function readEvidenceAnchors(dispositionText: string): EvidenceAnchor[] {
112
+ /**
113
+ * 锚点收集器
114
+ */
115
+ const anchors: EvidenceAnchor[] = [];
116
+
117
+ /**
118
+ * 已命中类别集合(每类别只取首个)
119
+ */
120
+ const hitKinds = new Set<EvidenceAnchorKind>();
121
+
122
+ /**
123
+ * 围栏深度(处置段内围栏里行整体跳过)
124
+ */
125
+ let fenceDepth = 0;
126
+
127
+ for (const line of dispositionText.split(/\r?\n/)) {
128
+ if (line.trim().startsWith('```')) {
129
+ fenceDepth = fenceDepth === 0 ? 1 : 0;
130
+
131
+ continue;
132
+ }
133
+
134
+ if (fenceDepth > 0) {
135
+ continue;
136
+ }
137
+
138
+ /**
139
+ * 候选锚点(类别 + 模式)
140
+ */
141
+ const candidates: ReadonlyArray<{ kind: EvidenceAnchorKind; pattern: RegExp }> = [
142
+ { kind: 'certificate', pattern: CERTIFICATE_PATTERN },
143
+ { kind: 'exit-zero', pattern: EXIT_ZERO_PATTERN },
144
+ { kind: 'all-green', pattern: ALL_GREEN_PATTERN },
145
+ { kind: 'adjudicated', pattern: ADJUDICATED_PATTERN },
146
+ ];
147
+
148
+ for (const candidate of candidates) {
149
+ if (!hitKinds.has(candidate.kind) && candidate.pattern.test(line)) {
150
+ anchors.push({ kind: candidate.kind, line });
151
+ hitKinds.add(candidate.kind);
152
+ }
153
+ }
154
+ }
155
+
156
+ return anchors;
157
+ }
158
+
159
+ /**
160
+ * 证据判读组合入口
161
+ *
162
+ * @param raw - 文档全文
163
+ * @returns 判读结果
164
+ */
165
+ export function readEvidence(raw: string): EvidenceReadResult {
166
+ /**
167
+ * 处置段文本
168
+ */
169
+ const dispositionText = locateDispositionText(raw);
170
+
171
+ return { anchors: readEvidenceAnchors(dispositionText), dispositionText };
172
+ }
173
+
174
+ /**
175
+ * 挂起词拦截判定(状态行含任一挂起词即拒绝收口——诚实黄灯维持)
176
+ *
177
+ * @param statusLine - 状态行全文
178
+ * @param pendingWords - 挂起词表
179
+ * @returns 命中的挂起词;未命中为 null
180
+ */
181
+ export function findPendingWord(
182
+ statusLine: string,
183
+ pendingWords: readonly string[],
184
+ ): string | null {
185
+ return pendingWords.find((word) => statusLine.includes(word)) ?? null;
186
+ }
187
+
188
+ /**
189
+ * 挂起原因提取(冻结/收口报文引用;复用解析器括注口径)
190
+ *
191
+ * @param statusLine - 状态行全文
192
+ * @returns 挂起原因;无括注为 null
193
+ */
194
+ export function readPendingReason(statusLine: string): string | null {
195
+ /**
196
+ * 括注命中
197
+ */
198
+ const match = PENDING_REASON_PATTERN.exec(statusLine);
199
+
200
+ return match === null ? null : (match[1] ?? '').trim();
201
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * 翻绿回填内核(green-flip)
3
+ *
4
+ * 状态行改写保留前缀粗体/冒号原形态(读/写口径统一);处置段回填证据锚点块
5
+ * (两种收口形态标题)。守卫分诊:replace 原样返回且正则仍命中(已绿且概要
6
+ * 逐字一致)→ 幂等零变更;正则零命中 → 抛错拒绝(防「处置段在场而状态行未
7
+ * 翻」假绿)。
8
+ */
9
+
10
+ import { STATUS_LINE_PREFIX_SOURCE } from '@/parser/format.utils';
11
+
12
+ /**
13
+ * 翻绿入参
14
+ */
15
+ export type GreenFlipInput = {
16
+ /**
17
+ * 文档全文
18
+ */
19
+ raw: string;
20
+
21
+ /**
22
+ * 收口概要(状态行括注与证据块首行)
23
+ */
24
+ summary: string;
25
+
26
+ /**
27
+ * 证据锚点块全文(含标题行;空串表示只翻状态行)
28
+ */
29
+ evidenceBlock: string;
30
+ };
31
+
32
+ /**
33
+ * 翻绿结果
34
+ */
35
+ export type GreenFlipResult = {
36
+ /**
37
+ * 翻绿产物全文(幂等时与原文一致)
38
+ */
39
+ content: string;
40
+
41
+ /**
42
+ * 幂等标记(原文已为目标形态)
43
+ */
44
+ idempotent: boolean;
45
+ };
46
+
47
+ /**
48
+ * 状态行改写正则(由前缀源动态构造——读/写单一真源)
49
+ */
50
+ const STATUS_LINE_REWRITE_PATTERN = new RegExp(
51
+ `(${STATUS_LINE_PREFIX_SOURCE}\\s*)(🔴|🟡|🟢)[^\\n]*`,
52
+ );
53
+
54
+ /**
55
+ * 目标状态行文本(概要 + 锚点指引)
56
+ */
57
+ const GREEN_STATUS_SUFFIX = '——判据证据锚点见处置段)';
58
+
59
+ /**
60
+ * 执行翻绿回填
61
+ *
62
+ * @param input - 翻绿入参
63
+ * @returns 翻绿结果
64
+ * @throws {@link Error} 状态行零命中(缺状态行的文档不得走翻绿路径)
65
+ */
66
+ export function applyGreenFlip(input: GreenFlipInput): GreenFlipResult {
67
+ /**
68
+ * 目标状态行全文
69
+ */
70
+ const targetStatusLine = `🟢 已处置(${input.summary}${GREEN_STATUS_SUFFIX}`;
71
+
72
+ /**
73
+ * 状态行改写产物
74
+ */
75
+ const rewritten = input.raw.replace(STATUS_LINE_REWRITE_PATTERN, `$1${targetStatusLine}`);
76
+
77
+ if (rewritten === input.raw) {
78
+ /**
79
+ * 守卫分诊:正则仍命中说明原文已含目标态(幂等);零命中说明无状态行
80
+ */
81
+ if (STATUS_LINE_REWRITE_PATTERN.test(input.raw)) {
82
+ return { content: input.raw, idempotent: true };
83
+ }
84
+
85
+ throw new Error('状态行零命中——缺状态行的文档不得走翻绿路径(先补状态行)');
86
+ }
87
+
88
+ return {
89
+ content: appendEvidenceBlock(rewritten, input.evidenceBlock),
90
+ idempotent: false,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * 回填证据锚点块到处置段(有段则段末追加,缺段则文末补段)
96
+ *
97
+ * @param raw - 文档全文(状态行已翻绿)
98
+ * @param evidenceBlock - 证据锚点块全文
99
+ * @returns 回填产物
100
+ */
101
+ function appendEvidenceBlock(raw: string, evidenceBlock: string): string {
102
+ if (evidenceBlock.trim() === '') {
103
+ return raw;
104
+ }
105
+
106
+ /**
107
+ * 文档行数组
108
+ */
109
+ const lines = raw.split(/\r?\n/);
110
+
111
+ /**
112
+ * 处置段标题行下标
113
+ */
114
+ const dispositionIndex = lines.findIndex(
115
+ (line) => line.startsWith('## ') && line.includes('处置结果'),
116
+ );
117
+
118
+ if (dispositionIndex === -1) {
119
+ return `${raw.replace(/\n+$/, '')}\n\n## 四、处置结果/验证口径\n\n${evidenceBlock}\n`;
120
+ }
121
+
122
+ /**
123
+ * 段末行下标(下一 `## ` 标题或文末)
124
+ */
125
+ let endIndex = lines.length;
126
+
127
+ for (let index = dispositionIndex + 1; index < lines.length; index += 1) {
128
+ if ((lines[index] ?? '').startsWith('## ')) {
129
+ endIndex = index;
130
+
131
+ break;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * 段内既有内容(剥尾部空行)
137
+ */
138
+ const sectionBody = lines
139
+ .slice(dispositionIndex + 1, endIndex)
140
+ .join('\n')
141
+ .replace(/\n+$/, '');
142
+
143
+ /**
144
+ * 头部行(处置段标题之前的全部行——标题、元信息、动机等段落原样保留)
145
+ */
146
+ const head = lines.slice(0, dispositionIndex).join('\n').replace(/\n+$/, '');
147
+
148
+ /**
149
+ * 段后余文
150
+ */
151
+ const rest = lines.slice(endIndex).join('\n');
152
+
153
+ /**
154
+ * 回填段(标题 + 既有内容 + 证据块)
155
+ */
156
+ const section = [lines[dispositionIndex] ?? '', '', sectionBody, '', evidenceBlock]
157
+ .filter((part) => part !== '')
158
+ .join('\n');
159
+
160
+ return `${head}\n\n${section}\n\n${rest.replace(/^\n+/, '')}`.replace(/\n+$/, '\n');
161
+ }
162
+
163
+ /**
164
+ * 零执行证据收口证据块渲染
165
+ *
166
+ * @param anchorLines - 锚点原文行列表
167
+ * @param decisionRef - 收口依据(如「docs/decisions/0005」)
168
+ * @returns 证据锚点块全文
169
+ */
170
+ export function renderEvidenceCloseBlock(
171
+ anchorLines: readonly string[],
172
+ decisionRef: string,
173
+ ): string {
174
+ return [
175
+ `证据锚点(末次通过零执行收口,${decisionRef}):`,
176
+ ...anchorLines.map((line) => `- ${line}`),
177
+ ].join('\n');
178
+ }
179
+
180
+ /**
181
+ * 证书化真跑证据块渲染(逐判据证书指纹行)
182
+ *
183
+ * @param summary - 收口概要
184
+ * @param evidenceLines - 逐判据证据行(已含证书指纹与退出码)
185
+ * @param caseRef - 收口先例指针
186
+ * @returns 证据锚点块全文
187
+ */
188
+ export function renderRerunEvidenceBlock(
189
+ summary: string,
190
+ evidenceLines: readonly string[],
191
+ caseRef: string,
192
+ ): string {
193
+ return [
194
+ `${summary}`,
195
+ '',
196
+ `判据执行证据(证书化收口,${caseRef}):`,
197
+ ...evidenceLines.map((line) => `- ${line}`),
198
+ ].join('\n');
199
+ }