@longzai-intelligence-records/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 +12 -0
- package/dist/index.d.ts +410 -0
- package/dist/index.js +5 -0
- package/dist/rolldown-runtime-BqT_7tdF.js +1 -0
- package/lzi-builder.config.ts +11 -0
- package/oxlint.config.ts +6 -0
- package/package.json +34 -0
- package/src/__tests__/index-check/index-check.core.test.ts +99 -0
- package/src/__tests__/lint/lint.core.test.ts +163 -0
- package/src/__tests__/parser/format.utils.test.ts +74 -0
- package/src/__tests__/template/record-template.renderer.test.ts +92 -0
- package/src/fs/scan-registry.utils.ts +74 -0
- package/src/index-check/index-check.core.ts +200 -0
- package/src/index.ts +18 -0
- package/src/lint/lint.core.ts +228 -0
- package/src/parser/format.utils.ts +151 -0
- package/src/parser/record.parser.ts +202 -0
- package/src/template/record-template.renderer.ts +218 -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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 更新日志
|
|
2
|
+
|
|
3
|
+
本项目的所有重要更改都将记录在此文件中。
|
|
4
|
+
|
|
5
|
+
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
|
6
|
+
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
7
|
+
|
|
8
|
+
## [0.0.1] - 2026-09-27
|
|
9
|
+
|
|
10
|
+
### 新增
|
|
11
|
+
|
|
12
|
+
- 新增 @longzai-intelligence-records/ledger 包提供模板渲染、解析、lint 与索引差量引擎核
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import "./rolldown-runtime-BqT_7tdF.js";
|
|
2
|
+
import { LziRecordsConfig } from "@longzai-intelligence-records/config";
|
|
3
|
+
//#region src/fs/scan-registry.utils.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* 注册表目录扫描工具(fs 编排层)
|
|
6
|
+
*
|
|
7
|
+
* 读侧统一入口:lint / index check / list 共用本扫描,文件枚举口径单一。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 扫描到的注册表条目
|
|
11
|
+
*/
|
|
12
|
+
type ScannedRecord = {
|
|
13
|
+
/**
|
|
14
|
+
* 文件基名(含扩展名)
|
|
15
|
+
*/
|
|
16
|
+
basename: string;
|
|
17
|
+
/**
|
|
18
|
+
* 编号键(数字字符串原样;宽口径非编号形态为 null)
|
|
19
|
+
*/
|
|
20
|
+
key: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* 绝对路径
|
|
23
|
+
*/
|
|
24
|
+
absPath: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* 扫描注册表目录(仅 .md 文件,按编号键升序;目录不存在返回空数组)
|
|
28
|
+
*
|
|
29
|
+
* @param dirAbs - 注册表目录绝对路径
|
|
30
|
+
* @returns 扫描条目列表
|
|
31
|
+
*/
|
|
32
|
+
declare function scanRecordsDir(dirAbs: string): ScannedRecord[];
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/index-check/index-check.core.d.ts
|
|
35
|
+
/**
|
|
36
|
+
* records README 索引差量核(只查不写)
|
|
37
|
+
*
|
|
38
|
+
* 索引行人工登记、工具只检查不写入(与 decision 项目 index 先例同款裁定)。
|
|
39
|
+
* 判定三态:缺行(文件在、索引无)/ 多行(索引有、文件无)/ slug 漂移(索引
|
|
40
|
+
* 链接 slug 与实际文件 slug 不一致);报文附期望行供人工粘贴。
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* 索引差量发现条目
|
|
44
|
+
*/
|
|
45
|
+
type IndexCheckFinding = {
|
|
46
|
+
/**
|
|
47
|
+
* 发现类别:readme-missing(README 未建)/ missing(缺行)/ stale(多行)/ slug-drift(slug 漂移)
|
|
48
|
+
*/
|
|
49
|
+
kind: 'readme-missing' | 'missing' | 'stale' | 'slug-drift';
|
|
50
|
+
/**
|
|
51
|
+
* 相关文件(posix 相对注册表目录)
|
|
52
|
+
*/
|
|
53
|
+
file: string;
|
|
54
|
+
/**
|
|
55
|
+
* 发现消息
|
|
56
|
+
*/
|
|
57
|
+
message: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* 索引差量结果
|
|
61
|
+
*/
|
|
62
|
+
type IndexCheckResult = {
|
|
63
|
+
/**
|
|
64
|
+
* 新鲜判定(无任何发现)
|
|
65
|
+
*/
|
|
66
|
+
ok: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* 发现列表
|
|
69
|
+
*/
|
|
70
|
+
findings: IndexCheckFinding[];
|
|
71
|
+
/**
|
|
72
|
+
* 期望索引行列表(供人工粘贴)
|
|
73
|
+
*/
|
|
74
|
+
expectedLines: string[];
|
|
75
|
+
/**
|
|
76
|
+
* 注册表文件数
|
|
77
|
+
*/
|
|
78
|
+
fileCount: number;
|
|
79
|
+
/**
|
|
80
|
+
* 索引引用数
|
|
81
|
+
*/
|
|
82
|
+
indexedCount: number;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* 索引差量入参
|
|
86
|
+
*/
|
|
87
|
+
type IndexCheckInput = {
|
|
88
|
+
/**
|
|
89
|
+
* 注册表目录绝对路径
|
|
90
|
+
*/
|
|
91
|
+
registryDirAbs: string;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* 运行索引差量检查(只读——本函数不写任何文件)
|
|
95
|
+
*
|
|
96
|
+
* @param input - 检查入参
|
|
97
|
+
* @returns 差量结果
|
|
98
|
+
*/
|
|
99
|
+
declare function runIndexCheck(input: IndexCheckInput): IndexCheckResult;
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/lint/lint.core.d.ts
|
|
102
|
+
/**
|
|
103
|
+
* lint 违例条目
|
|
104
|
+
*/
|
|
105
|
+
type RecordsLintViolation = {
|
|
106
|
+
/**
|
|
107
|
+
* 规则名
|
|
108
|
+
*/
|
|
109
|
+
rule: string;
|
|
110
|
+
/**
|
|
111
|
+
* 文件(posix 相对仓库根)
|
|
112
|
+
*/
|
|
113
|
+
file: string;
|
|
114
|
+
/**
|
|
115
|
+
* 行号(1 起算)
|
|
116
|
+
*/
|
|
117
|
+
line: number;
|
|
118
|
+
/**
|
|
119
|
+
* 违例消息
|
|
120
|
+
*/
|
|
121
|
+
message: string;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* lint 结果
|
|
125
|
+
*/
|
|
126
|
+
type RecordsLintResult = {
|
|
127
|
+
/**
|
|
128
|
+
* 基线豁免后的违例列表
|
|
129
|
+
*/
|
|
130
|
+
violations: RecordsLintViolation[];
|
|
131
|
+
/**
|
|
132
|
+
* 扫描文件数
|
|
133
|
+
*/
|
|
134
|
+
fileCount: number;
|
|
135
|
+
/**
|
|
136
|
+
* 基线豁免命中数(棘轮观测面)
|
|
137
|
+
*/
|
|
138
|
+
exemptedCount: number;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* lint 入参
|
|
142
|
+
*/
|
|
143
|
+
type RecordsLintInput = {
|
|
144
|
+
/**
|
|
145
|
+
* 仓库根绝对路径
|
|
146
|
+
*/
|
|
147
|
+
root: string;
|
|
148
|
+
/**
|
|
149
|
+
* 解析后配置
|
|
150
|
+
*/
|
|
151
|
+
config: LziRecordsConfig;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* 运行 lint(全 scope 注册表;只读)
|
|
155
|
+
*
|
|
156
|
+
* @param input - lint 入参
|
|
157
|
+
* @returns lint 结果(违例空 = 通过)
|
|
158
|
+
*/
|
|
159
|
+
declare function runRecordsLint(input: RecordsLintInput): RecordsLintResult;
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/parser/format.utils.d.ts
|
|
162
|
+
/**
|
|
163
|
+
* records 格式口径单一真源
|
|
164
|
+
*
|
|
165
|
+
* 全部格式口径(文件名形态、H1 形态、头部元数据行、占位符)集中于此模块。
|
|
166
|
+
* 读侧(解析器 / lint / 索引差量)与写侧(模板渲染)共用同一真源,杜绝两套
|
|
167
|
+
* 口径漂移——create 产物在构造上天然通过 lint。
|
|
168
|
+
*
|
|
169
|
+
* records 定位(decisions/021):广义降级 fallback 通道,无语义分型——格式面
|
|
170
|
+
* 只有头部元数据强制(记录日期 / 来源 / 可选关联),正文自由格式、无固定节。
|
|
171
|
+
*/
|
|
172
|
+
/**
|
|
173
|
+
* 文件名形态(编号 3-4 位零填充 + ASCII kebab slug + .md)
|
|
174
|
+
*/
|
|
175
|
+
declare const RECORD_FILENAME_PATTERN: RegExp;
|
|
176
|
+
/**
|
|
177
|
+
* slug 合法形态(ASCII kebab,小写字母/数字/连字符——与 issues 家族口径一致)
|
|
178
|
+
*/
|
|
179
|
+
declare const ASCII_KEBAB_SLUG_PATTERN: RegExp;
|
|
180
|
+
/**
|
|
181
|
+
* H1 形态(编号入 H1,em dash 分隔——产品群 records 主流形态)
|
|
182
|
+
*/
|
|
183
|
+
declare const RECORD_H1_PATTERN: RegExp;
|
|
184
|
+
/**
|
|
185
|
+
* 记录日期行形态(blockquote 元数据,YYYY-MM-DD)
|
|
186
|
+
*/
|
|
187
|
+
declare const DATE_LINE_PATTERN: RegExp;
|
|
188
|
+
/**
|
|
189
|
+
* 来源行形态(blockquote 元数据,非空)
|
|
190
|
+
*/
|
|
191
|
+
declare const SOURCE_LINE_PATTERN: RegExp;
|
|
192
|
+
/**
|
|
193
|
+
* 关联行形态(blockquote 元数据,可选行,非空)
|
|
194
|
+
*/
|
|
195
|
+
declare const REF_LINE_PATTERN: RegExp;
|
|
196
|
+
/**
|
|
197
|
+
* 头部元数据键:记录日期
|
|
198
|
+
*/
|
|
199
|
+
declare const DATE_LINE_KEY = "记录日期";
|
|
200
|
+
/**
|
|
201
|
+
* 头部元数据键:来源
|
|
202
|
+
*/
|
|
203
|
+
declare const SOURCE_LINE_KEY = "来源";
|
|
204
|
+
/**
|
|
205
|
+
* 头部元数据键:关联
|
|
206
|
+
*/
|
|
207
|
+
declare const REF_LINE_KEY = "关联";
|
|
208
|
+
/**
|
|
209
|
+
* 正文占位符(建档即骨架,内容随工作回填)
|
|
210
|
+
*/
|
|
211
|
+
declare const SECTION_PLACEHOLDER = "(待补充)";
|
|
212
|
+
/**
|
|
213
|
+
* 正文自由格式指引行(渲染产物与骨架共享——fallback 通道定位的显式声明)
|
|
214
|
+
*/
|
|
215
|
+
declare const BODY_GUIDANCE = "(正文自由格式——所有不清楚归属、被去除的内容都落此通道;无分型,无固定节)";
|
|
216
|
+
/**
|
|
217
|
+
* 索引行引用形态(README 索引行中的注册表文件链接——非全局模式,
|
|
218
|
+
* matchAll 消费侧自建全局实例,避免模块级正则共享 lastIndex 状态)
|
|
219
|
+
*/
|
|
220
|
+
declare const INDEX_LINK_PATTERN: RegExp;
|
|
221
|
+
/**
|
|
222
|
+
* 正则捕获组安全取值(守卫形态——替代非空断言;未命中即内部口径错误)
|
|
223
|
+
*
|
|
224
|
+
* @param match - 正则命中结果
|
|
225
|
+
* @param index - 捕获组下标
|
|
226
|
+
* @returns 捕获组值
|
|
227
|
+
* @throws {@link Error} 未命中或捕获组越界(格式口径与调用侧失配)
|
|
228
|
+
*/
|
|
229
|
+
declare function captureGroup(match: RegExpMatchArray | null, index: number): string;
|
|
230
|
+
/**
|
|
231
|
+
* 提取 README 内容中的注册表引用集(编号 → slug,重复引用以后者为准)
|
|
232
|
+
*
|
|
233
|
+
* @param content - README 全文
|
|
234
|
+
* @returns 编号键 → slug 映射
|
|
235
|
+
*/
|
|
236
|
+
declare function extractIndexRefs(content: string): Map<string, string>;
|
|
237
|
+
/**
|
|
238
|
+
* 从文件名提取编号键(宽口径:任何 `NNNN-*` 形态的 .md 基名)
|
|
239
|
+
*
|
|
240
|
+
* @param basename - 文件基名
|
|
241
|
+
* @returns 编号键(数字字符串原样);非编号形态为 null
|
|
242
|
+
*/
|
|
243
|
+
declare function extractRecordKey(basename: string): string | null;
|
|
244
|
+
/**
|
|
245
|
+
* 判定日期字符串是否为真实存在的日历日期(YYYY-MM-DD)
|
|
246
|
+
*
|
|
247
|
+
* @param date - 日期字符串
|
|
248
|
+
* @returns 真实日历日期为 true
|
|
249
|
+
*/
|
|
250
|
+
declare function isRealCalendarDate(date: string): boolean;
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/parser/record.parser.d.ts
|
|
253
|
+
/**
|
|
254
|
+
* record 文档解析器
|
|
255
|
+
*
|
|
256
|
+
* 判定全部基于本解析器产物(口径单一真源 format.utils)。解析头部:H1(编号
|
|
257
|
+
* 入 H1)+ blockquote 元数据行(记录日期 / 来源 / 可选关联);正文自由格式
|
|
258
|
+
* 不解析(无分型——decisions/021)。
|
|
259
|
+
*/
|
|
260
|
+
/**
|
|
261
|
+
* 解析后的 record 头部
|
|
262
|
+
*/
|
|
263
|
+
type ParsedRecord = {
|
|
264
|
+
/**
|
|
265
|
+
* H1 编号(数字字符串原样)
|
|
266
|
+
*/
|
|
267
|
+
number: string;
|
|
268
|
+
/**
|
|
269
|
+
* H1 标题
|
|
270
|
+
*/
|
|
271
|
+
title: string;
|
|
272
|
+
/**
|
|
273
|
+
* 记录日期(YYYY-MM-DD)
|
|
274
|
+
*/
|
|
275
|
+
date: string | null;
|
|
276
|
+
/**
|
|
277
|
+
* 来源
|
|
278
|
+
*/
|
|
279
|
+
source: string | null;
|
|
280
|
+
/**
|
|
281
|
+
* 关联(可选行缺席为 null)
|
|
282
|
+
*/
|
|
283
|
+
ref: string | null;
|
|
284
|
+
/**
|
|
285
|
+
* 头部元数据结束行号(1 起算;正文起始参考)
|
|
286
|
+
*/
|
|
287
|
+
headerEndLine: number;
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* 解析问题条目(行级)
|
|
291
|
+
*/
|
|
292
|
+
type RecordParseIssue = {
|
|
293
|
+
/**
|
|
294
|
+
* 行号(1 起算;0 = 全文级)
|
|
295
|
+
*/
|
|
296
|
+
line: number;
|
|
297
|
+
/**
|
|
298
|
+
* 问题消息
|
|
299
|
+
*/
|
|
300
|
+
message: string;
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* 解析结果
|
|
304
|
+
*/
|
|
305
|
+
type RecordParseResult = {
|
|
306
|
+
/**
|
|
307
|
+
* 解析后的头部(H1 非法时为 null)
|
|
308
|
+
*/
|
|
309
|
+
record: ParsedRecord | null;
|
|
310
|
+
/**
|
|
311
|
+
* 解析问题列表(空 = 合法建档态)
|
|
312
|
+
*/
|
|
313
|
+
issues: RecordParseIssue[];
|
|
314
|
+
};
|
|
315
|
+
/**
|
|
316
|
+
* 解析 record 文档全文
|
|
317
|
+
*
|
|
318
|
+
* @param content - 文档全文
|
|
319
|
+
* @returns 解析结果:头部字段 + 行级问题(骨架建档态 = 占位值可解析、无问题)
|
|
320
|
+
*/
|
|
321
|
+
declare function parseRecordDoc(content: string): RecordParseResult;
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/template/record-template.renderer.d.ts
|
|
324
|
+
/**
|
|
325
|
+
* record 标准模板渲染器(唯一真源)
|
|
326
|
+
*
|
|
327
|
+
* 标准格式三路共用本渲染器:template 命令(skeleton 骨架展示)、create 建档
|
|
328
|
+
* (instance 实际值骨架)、init 注册表 README 存根。正文自由格式(无分型——
|
|
329
|
+
* decisions/021),仅占位指引共享;结构漂移在构造上不可能发生,渲染-解析回环
|
|
330
|
+
* 由 template 测试守卫。
|
|
331
|
+
*/
|
|
332
|
+
/**
|
|
333
|
+
* instance 模式渲染入参
|
|
334
|
+
*/
|
|
335
|
+
type RecordTemplateValues = {
|
|
336
|
+
/**
|
|
337
|
+
* 编号(3-4 位零填充,编号门禁取号分配)
|
|
338
|
+
*/
|
|
339
|
+
number: string;
|
|
340
|
+
/**
|
|
341
|
+
* slug(ASCII kebab,中文标题须显式 --slug)
|
|
342
|
+
*/
|
|
343
|
+
slug: string;
|
|
344
|
+
/**
|
|
345
|
+
* 一句话标题
|
|
346
|
+
*/
|
|
347
|
+
title: string;
|
|
348
|
+
/**
|
|
349
|
+
* 记录日期(YYYY-MM-DD)
|
|
350
|
+
*/
|
|
351
|
+
date: string;
|
|
352
|
+
/**
|
|
353
|
+
* 来源(出处 + 日期)
|
|
354
|
+
*/
|
|
355
|
+
source: string;
|
|
356
|
+
/**
|
|
357
|
+
* 关联(可选;缺席时省略关联行)
|
|
358
|
+
*/
|
|
359
|
+
ref?: string;
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* 渲染标准模板骨架(skeleton 模式)
|
|
363
|
+
*
|
|
364
|
+
* 占位符形态,供 template 命令展示与指南活样板消费。
|
|
365
|
+
*
|
|
366
|
+
* @returns 骨架全文(以单个换行结尾)
|
|
367
|
+
*/
|
|
368
|
+
declare function renderRecordTemplateSkeleton(): string;
|
|
369
|
+
/**
|
|
370
|
+
* 渲染标准模板实例(instance 模式)
|
|
371
|
+
*
|
|
372
|
+
* 头部元数据代入实际值;正文保留占位指引文本(建档即骨架,内容随工作回填)。
|
|
373
|
+
*
|
|
374
|
+
* @param values - 实际值入参
|
|
375
|
+
* @returns 可直接落盘的 record 文档全文(以单个换行结尾)
|
|
376
|
+
*/
|
|
377
|
+
declare function renderRecordTemplate(values: RecordTemplateValues): string;
|
|
378
|
+
/**
|
|
379
|
+
* 渲染注册表 README 格式存根(init 命令落盘)
|
|
380
|
+
*
|
|
381
|
+
* 一页式格式速览:建档入口、头部元数据、索引纪律(只查不写——索引行人工
|
|
382
|
+
* 登记,工具只检查,与 decision 项目 index 先例同款裁定)。
|
|
383
|
+
*
|
|
384
|
+
* @param scopeTitle - scope 人类可读名
|
|
385
|
+
* @returns README 存根全文
|
|
386
|
+
*/
|
|
387
|
+
declare function renderRegistryReadmeStub(scopeTitle: string): string;
|
|
388
|
+
/**
|
|
389
|
+
* 模板字段说明条目(template --format json 载荷)
|
|
390
|
+
*/
|
|
391
|
+
type TemplateFieldDoc = {
|
|
392
|
+
/**
|
|
393
|
+
* 字段名(渲染入参名或元信息键)
|
|
394
|
+
*/
|
|
395
|
+
name: string;
|
|
396
|
+
/**
|
|
397
|
+
* 是否必填
|
|
398
|
+
*/
|
|
399
|
+
required: boolean;
|
|
400
|
+
/**
|
|
401
|
+
* 字段说明
|
|
402
|
+
*/
|
|
403
|
+
description: string;
|
|
404
|
+
};
|
|
405
|
+
/**
|
|
406
|
+
* 标准模板字段说明全集(供 agent / 使用者程序化消费)
|
|
407
|
+
*/
|
|
408
|
+
declare const TEMPLATE_FIELDS: readonly TemplateFieldDoc[];
|
|
409
|
+
//#endregion
|
|
410
|
+
export { ASCII_KEBAB_SLUG_PATTERN, BODY_GUIDANCE, DATE_LINE_KEY, DATE_LINE_PATTERN, INDEX_LINK_PATTERN, IndexCheckFinding, IndexCheckInput, IndexCheckResult, ParsedRecord, RECORD_FILENAME_PATTERN, RECORD_H1_PATTERN, REF_LINE_KEY, REF_LINE_PATTERN, RecordParseIssue, RecordParseResult, RecordTemplateValues, RecordsLintInput, RecordsLintResult, RecordsLintViolation, SECTION_PLACEHOLDER, SOURCE_LINE_KEY, SOURCE_LINE_PATTERN, ScannedRecord, TEMPLATE_FIELDS, TemplateFieldDoc, captureGroup, extractIndexRefs, extractRecordKey, isRealCalendarDate, parseRecordDoc, renderRecordTemplate, renderRecordTemplateSkeleton, renderRegistryReadmeStub, runIndexCheck, runRecordsLint, scanRecordsDir };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import"./rolldown-runtime-BqT_7tdF.js";import{existsSync as e,readFileSync as t,readdirSync as n}from"node:fs";import{join as r,posix as i}from"node:path";const a=/^(\d{3,4})-([a-z0-9]+(?:-[a-z0-9]+)*)\.md$/,o=/^[a-z0-9]+(?:-[a-z0-9]+)*$/,s=/^# (\d{3,4}) — (.+)$/,c=/^> 记录日期:(\d{4}-\d{2}-\d{2})$/,l=/^> 来源:(.+)$/,u=/^> 关联:(.+)$/,d=`记录日期`,f=`来源`,p=`关联`,m=`(待补充)`,h=`(正文自由格式——所有不清楚归属、被去除的内容都落此通道;无分型,无固定节)`,g=/\[?\d{3,4}[^\]\n]*\]?\(\.\/(\d{3,4})-([a-z0-9]+(?:-[a-z0-9]+)*)\.md\)/;function _(e,t){if(e===null||e[t]===void 0)throw Error(`正则捕获组 ${t} 未命中(格式口径内部错误)`);return e[t]}function v(e){let t=/* @__PURE__ */ new Map;for(let n of e.matchAll(new RegExp(g.source,`g`)))t.set(_(n,1),_(n,2));return t}function y(e){let t=/^(\d{1,4})[-._]/.exec(e);return t===null?null:_(t,1)}function b(e){let[t,n,r]=e.split(`-`).map(e=>Number.parseInt(e,10));if(t===void 0||n===void 0||r===void 0)return!1;let i=new Date(Date.UTC(t,n-1,r));return i.getUTCFullYear()===t&&i.getUTCMonth()===n-1&&i.getUTCDate()===r}function x(e){return S(e)?n(e).filter(e=>e.endsWith(`.md`)&&e!==`README.md`).map(t=>({basename:t,key:y(t),absPath:r(e,t)})).sort((e,t)=>(e.key??`zzzz`).localeCompare(t.key??`zzzz`)):[]}function S(e){try{return n(e).length>=0}catch{return!1}}function C(e){let t=e.split(`
|
|
2
|
+
`),n=[],r=s.exec(t[0]??``);if(r===null)return n.push({line:1,message:`首行须为「# NNNN — 标题」形态 H1(编号入 H1,em dash 分隔)`}),{record:null,issues:n};let i=/* @__PURE__ */ new Map,a=1;for(;a<t.length;){let e=t[a]??``;if(e.trim()===``){a+=1;continue}let n=c.exec(e);if(n!==null){i.set(d,_(n,1)),a+=1;continue}let r=l.exec(e);if(r!==null){i.set(f,_(r,1)),a+=1;continue}let o=u.exec(e);if(o!==null){i.set(p,_(o,1)),a+=1;continue}break}return i.has(`记录日期`)||n.push({line:a+1,message:`头部缺「> ${d}:YYYY-MM-DD」元数据行`}),i.has(`来源`)||n.push({line:a+1,message:`头部缺「> ${f}:<出处>」元数据行`}),{record:{number:_(r,1),title:_(r,2),date:i.get(`记录日期`)??null,source:i.get(`来源`)??null,ref:i.get(`关联`)??null,headerEndLine:a},issues:n}}function w(n){let i=[],o=x(n.registryDirAbs).filter(e=>a.test(e.basename)),s=new Map(o.map(e=>{let t=a.exec(e.basename);return[t===null?e.key??e.basename:_(t,1),e]})),c=r(n.registryDirAbs,`README.md`),l=e(c)?v(t(c,`utf8`)):/* @__PURE__ */ new Map;e(c)||i.push({kind:`readme-missing`,file:`README.md`,message:`注册表 README 未建(init 可落存根;索引行人工登记)`});let u=[];for(let e of o){let n=a.exec(e.basename);if(n===null)continue;let r=_(n,1),o=_(n,2),s=C(t(e.absPath,`utf8`));if(u.push(`- [${r} — ${s.record?.title??o}](./${e.basename})`),!l.has(r)){i.push({kind:`missing`,file:e.basename,message:`索引缺行:${e.basename}(期望行见报文,人工登记)`});continue}l.get(r)!==o&&i.push({kind:`slug-drift`,file:e.basename,message:`索引 slug 漂移:索引指向 ${r}-${l.get(r)}.md,实际为 ${e.basename}`})}for(let[e,t]of l)s.has(e)||i.push({kind:`stale`,file:`${e}-${t}.md`,message:`索引多行:引用 ${e}-${t}.md 但文件不存在(删除索引行或补文件)`});return{ok:i.length===0,findings:i,expectedLines:u,fileCount:o.length,indexedCount:l.size}}function T(e){let n=[],r=0;for(let r of e.config.scopes){let o=x(E(e.root,r.registryDir));for(let e of o){let o=i.join(r.registryDir,e.basename),s=a.exec(e.basename);if(s===null){n.push({rule:`R-FILENAME`,file:o,line:0,message:`文件名须为「NNNN-<slug>.md」(编号 3-4 位零填充 + ASCII kebab)`});continue}let c=C(t(e.absPath,`utf8`));if(c.record===null){for(let e of c.issues)n.push({rule:`R-H1`,file:o,line:e.line,message:e.message});continue}c.record.number!==s[1]&&n.push({rule:`R-H1-NUMBER`,file:o,line:1,message:`H1 编号「${c.record.number}」与文件名编号「${s[1]}」不一致`}),c.record.date===null?n.push({rule:`R-DATE-LINE`,file:o,line:c.record.headerEndLine+1,message:`头部缺「> 记录日期:YYYY-MM-DD」元数据行`}):b(c.record.date)||n.push({rule:`R-DATE-REAL`,file:o,line:2,message:`记录日期「${c.record.date}」非真实日历日期`}),c.record.source===null&&n.push({rule:`R-SOURCE-LINE`,file:o,line:c.record.headerEndLine+1,message:`头部缺「> 来源:<出处>」元数据行`})}}let o=new Set(e.config.lint.baseline.map(e=>`${e.file}|${e.rule}`));return{violations:n.filter(e=>{let t=o.has(`${e.file}|${e.rule}`);return t&&(r+=1),!t}),fileCount:e.config.scopes.reduce((t,n)=>t+x(E(e.root,n.registryDir)).length,0),exemptedCount:r}}function E(e,t){return`${e.replace(/\/+$/,``)}/${t}`}function D(e,t,n,r,i){let a=[`# ${e} — ${t}`,``,`> 记录日期:${n}`,`> 来源:${r}`];return i!==void 0&&a.push(`> 关联:${i}`),a}function O(){return[...D(`<NNNN>`,`<一句话标题>`,`<YYYY-MM-DD>`,`<出处(用户裁定 / 会话记录 / 巡检 + 日期)>`,`<可选:关联条目链接,缺席省略本行>`),``,h,``,m,``].join(`
|
|
3
|
+
`)}function k(e){return[...D(e.number,e.title,e.date,e.source,e.ref),``,h,``,m,``].join(`
|
|
4
|
+
`)}function A(e){return[`# ${e} records 注册表`,``,"本目录是 lzi-records 注册表(`NNNN-<slug>.md`,编号独立序列)。",``,`records 定位:**广义降级 fallback 通道,无语义分型**——所有不清楚归属、被去除的内容都落此通道;正文自由格式,仅头部元数据(记录日期/来源/关联)强制。`,``,`## 建档`,``,"- `lzi-records create --title <t>`(取号经 numbering 插件,写入时刻确定,禁止手工定号)","- 标准模板:`lzi-records template`(字段契约 `--format json`)",``,`## 索引`,``,"- 索引行**人工登记**(工具只检查不写入):`lzi-records index check` 报差量并附期望行",``,`## 头部元数据`,``,"- `> 记录日期:YYYY-MM-DD`(强制)","- `> 来源:<出处 + 日期>`(强制)","- `> 关联:<条目链接>`(可选)",``].join(`
|
|
5
|
+
`)}const j=[{name:`number`,required:!0,description:`scope 内编号,3-4 位零填充,由编号门禁取号分配(写入时刻确定)`},{name:`slug`,required:!0,description:`ASCII kebab slug(小写字母/数字/连字符,禁中文),中文标题须显式 --slug`},{name:`title`,required:!0,description:`一句话标题`},{name:`记录日期`,required:!0,description:`YYYY-MM-DD(头部 blockquote 元数据行,强制)`},{name:`来源`,required:!0,description:`出处(用户裁定 / 会话记录 / 巡检 + 日期)(头部 blockquote 元数据行,强制)`},{name:`关联`,required:!1,description:`关联条目链接(头部 blockquote 元数据行,可选;缺席省略整行)`},{name:`正文`,required:!0,description:`自由格式(无分型、无固定节——fallback 通道定位),建档为占位(待补充)`}];export{o as ASCII_KEBAB_SLUG_PATTERN,h as BODY_GUIDANCE,d as DATE_LINE_KEY,c as DATE_LINE_PATTERN,g as INDEX_LINK_PATTERN,a as RECORD_FILENAME_PATTERN,s as RECORD_H1_PATTERN,p as REF_LINE_KEY,u as REF_LINE_PATTERN,m as SECTION_PLACEHOLDER,f as SOURCE_LINE_KEY,l as SOURCE_LINE_PATTERN,j as TEMPLATE_FIELDS,_ as captureGroup,v as extractIndexRefs,y as extractRecordKey,b as isRealCalendarDate,C as parseRecordDoc,k as renderRecordTemplate,O as renderRecordTemplateSkeleton,A as renderRegistryReadmeStub,w as runIndexCheck,T as runRecordsLint,x as scanRecordsDir};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"node:module";export{};
|
package/oxlint.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@longzai-intelligence-records/ledger",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "UNLICENSED",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "lzi-builder",
|
|
16
|
+
"build:prod": "NODE_ENV=production bun run build",
|
|
17
|
+
"prepublishOnly": "bun run build:prod",
|
|
18
|
+
"typecheck": "bun run typecheck:app && bun run typecheck:node && bun run typecheck:test",
|
|
19
|
+
"typecheck:app": "lzi-tsgo typecheck tsconfig/app.json",
|
|
20
|
+
"typecheck:node": "lzi-tsgo typecheck tsconfig/node.json",
|
|
21
|
+
"typecheck:test": "lzi-tsgo typecheck tsconfig/test.json",
|
|
22
|
+
"lint": "oxlint && oxfmt --check",
|
|
23
|
+
"lint:fix": "oxlint --fix && oxfmt",
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"test:watch": "bun test --watch",
|
|
26
|
+
"test:coverage": "bun test --coverage"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@longzai-intelligence-records/config": "0.0.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/bun": "latest"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { runIndexCheck } from '@/index-check/index-check.core';
|
|
7
|
+
import { renderRecordTemplate } from '@/template/record-template.renderer';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 索引差量核测试:缺行 / 多行 / slug 漂移 / README 未建四态 + 只读铁律
|
|
11
|
+
* (检查前后文件内容逐字节一致)。
|
|
12
|
+
*/
|
|
13
|
+
describe('records index check core', () => {
|
|
14
|
+
/**
|
|
15
|
+
* 夹具注册表目录
|
|
16
|
+
*/
|
|
17
|
+
let registryDir: string;
|
|
18
|
+
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
registryDir = join(mkdtempSync(join(tmpdir(), 'lzi-records-index-')), 'docs/records');
|
|
21
|
+
mkdirSync(registryDir, { recursive: true });
|
|
22
|
+
writeFileSync(
|
|
23
|
+
join(registryDir, '0001-first.md'),
|
|
24
|
+
renderRecordTemplate({
|
|
25
|
+
number: '0001',
|
|
26
|
+
slug: 'first',
|
|
27
|
+
title: '首条',
|
|
28
|
+
date: '2026-09-27',
|
|
29
|
+
source: 't',
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
rmSync(registryDir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('README 未建:readme-missing + 全量缺行 + 期望行可粘贴', () => {
|
|
39
|
+
/**
|
|
40
|
+
* 检查结果
|
|
41
|
+
*/
|
|
42
|
+
const result = runIndexCheck({ registryDirAbs: registryDir });
|
|
43
|
+
|
|
44
|
+
expect(result.ok).toBe(false);
|
|
45
|
+
expect(result.findings.some((f) => f.kind === 'readme-missing')).toBe(true);
|
|
46
|
+
expect(result.findings.some((f) => f.kind === 'missing' && f.file === '0001-first.md')).toBe(
|
|
47
|
+
true,
|
|
48
|
+
);
|
|
49
|
+
expect(result.expectedLines[0]).toBe('- [0001 — 首条](./0001-first.md)');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('索引齐全:新鲜通过', () => {
|
|
53
|
+
writeFileSync(
|
|
54
|
+
join(registryDir, 'README.md'),
|
|
55
|
+
'# records\n\n- [0001 — 首条](./0001-first.md)\n',
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 检查结果
|
|
60
|
+
*/
|
|
61
|
+
const result = runIndexCheck({ registryDirAbs: registryDir });
|
|
62
|
+
|
|
63
|
+
expect(result.ok).toBe(true);
|
|
64
|
+
expect(result.findings).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('slug 漂移与多行检测', () => {
|
|
68
|
+
writeFileSync(
|
|
69
|
+
join(registryDir, 'README.md'),
|
|
70
|
+
'# records\n\n- [0001 — 首条](./0001-old-slug.md)\n- [0002 — 幽灵](./0002-ghost.md)\n',
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 检查结果
|
|
75
|
+
*/
|
|
76
|
+
const result = runIndexCheck({ registryDirAbs: registryDir });
|
|
77
|
+
|
|
78
|
+
expect(result.findings.some((f) => f.kind === 'slug-drift')).toBe(true);
|
|
79
|
+
expect(result.findings.some((f) => f.kind === 'stale' && f.file === '0002-ghost.md')).toBe(
|
|
80
|
+
true,
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('只读铁律:检查前后全部文件内容逐字节一致(工具零写入)', () => {
|
|
85
|
+
/**
|
|
86
|
+
* 检查前快照(文件名 → 内容)
|
|
87
|
+
*/
|
|
88
|
+
const before = new Map<string, string>();
|
|
89
|
+
before.set('0001-first.md', readFileSync(join(registryDir, '0001-first.md'), 'utf8'));
|
|
90
|
+
before.set('README.md', readFileSync(join(registryDir, 'README.md'), 'utf8'));
|
|
91
|
+
|
|
92
|
+
runIndexCheck({ registryDirAbs: registryDir });
|
|
93
|
+
|
|
94
|
+
expect(readFileSync(join(registryDir, '0001-first.md'), 'utf8')).toBe(
|
|
95
|
+
before.get('0001-first.md'),
|
|
96
|
+
);
|
|
97
|
+
expect(readFileSync(join(registryDir, 'README.md'), 'utf8')).toBe(before.get('README.md'));
|
|
98
|
+
});
|
|
99
|
+
});
|