@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,679 @@
1
+ /**
2
+ * issue 注册表解析器(读侧唯一入口)
3
+ *
4
+ * 文件名级收集 + 头部状态行 / 处置段 / criteria 围栏块 / 沿革阻塞引用抽取。
5
+ * 零子进程、零网络;全部口径来自 format.utils 单一真源。
6
+ */
7
+
8
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+
11
+ import {
12
+ BLOCKING_LINE_KEY,
13
+ buildMetaLineLocatePattern,
14
+ classifyStatusLine,
15
+ CRITERIA_CHANNEL_PATTERN,
16
+ CRITERIA_FENCE_CLOSE_PATTERN,
17
+ CRITERIA_FENCE_OPEN_PATTERN,
18
+ DISPOSITION_PLACEHOLDER_PATTERN,
19
+ DISPOSITION_PLACEHOLDER_WINDOW_LINES,
20
+ DISPOSITION_TITLE_KEYWORD,
21
+ EVIDENCE_ANCHOR_MARKERS,
22
+ extractIssueRefTokens,
23
+ HISTORY_LINE_KEY,
24
+ IssueRefToken,
25
+ IssueStatusKind,
26
+ PENDING_REASON_PATTERN,
27
+ REGISTRY_FILE_PATTERN,
28
+ STATUS_LINE_LOCATE_PATTERN,
29
+ STATUS_LINE_SCAN_LINES,
30
+ } from './format.utils.js';
31
+
32
+ /**
33
+ * criteria 判据条目(一行一判据,四段竖线分隔)
34
+ */
35
+ export type ParsedCriterion = {
36
+ /**
37
+ * 判据标签(人类可读名)
38
+ */
39
+ label: string;
40
+
41
+ /**
42
+ * 判据通道(证书库的通道维度)
43
+ */
44
+ channel: string;
45
+
46
+ /**
47
+ * 判据工作目录(相对仓库根)
48
+ */
49
+ cwd: string;
50
+
51
+ /**
52
+ * 判据命令(退出码即判据)
53
+ */
54
+ command: string;
55
+ };
56
+
57
+ /**
58
+ * criteria 围栏块解析产物
59
+ */
60
+ export type ParsedCriteriaBlock = {
61
+ /**
62
+ * 概要行(唯一非空;缺席为 null)
63
+ */
64
+ summary: string | null;
65
+
66
+ /**
67
+ * 判据条目列表(至少一条)
68
+ */
69
+ criteria: ParsedCriterion[];
70
+ };
71
+
72
+ /**
73
+ * issue 文档解析产物(IssueDocRecord)
74
+ */
75
+ export type IssueDocRecord = {
76
+ /**
77
+ * 编号(文件名 3-4 位数字前缀)
78
+ */
79
+ id: string;
80
+
81
+ /**
82
+ * slug(文件名剥前缀与扩展名)
83
+ */
84
+ slug: string;
85
+
86
+ /**
87
+ * 文件名(basename)
88
+ */
89
+ file: string;
90
+
91
+ /**
92
+ * 标题(首个 `# ` 行内容;缺席为空串)
93
+ */
94
+ title: string;
95
+
96
+ /**
97
+ * 状态归类
98
+ */
99
+ status: IssueStatusKind;
100
+
101
+ /**
102
+ * 状态行全文(未定位为空串)
103
+ */
104
+ statusLine: string;
105
+
106
+ /**
107
+ * 状态行行号(1 起;未定位为 0)
108
+ */
109
+ statusLineNumber: number;
110
+
111
+ /**
112
+ * 挂起原因(状态行首个 ≥4 字括注;缺席为 null)
113
+ */
114
+ pendingReason: string | null;
115
+
116
+ /**
117
+ * 是否存在处置段标题
118
+ */
119
+ hasDispositionSection: boolean;
120
+
121
+ /**
122
+ * 处置段是否命中占位模式(标题起 8 行窗口)
123
+ */
124
+ dispositionIsPlaceholder: boolean;
125
+
126
+ /**
127
+ * 证据锚点块是否在位
128
+ */
129
+ hasEvidenceAnchorBlock: boolean;
130
+
131
+ /**
132
+ * criteria 围栏块解析产物(块在位且解析成功)
133
+ */
134
+ criteria: ParsedCriteriaBlock | null;
135
+
136
+ /**
137
+ * criteria 围栏块解析错误消息(块缺席为 null;多块/畸形带行号)
138
+ */
139
+ criteriaError: string | null;
140
+
141
+ /**
142
+ * 沿革引用 token 列表
143
+ */
144
+ historyRefs: IssueRefToken[];
145
+
146
+ /**
147
+ * 阻塞引用 token 列表
148
+ */
149
+ blockedByRefs: IssueRefToken[];
150
+
151
+ /**
152
+ * 文档全文
153
+ */
154
+ rawContent: string;
155
+
156
+ /**
157
+ * 文件 mtime(毫秒;账龄计算口径)
158
+ */
159
+ modifiedAtMs: number;
160
+ };
161
+
162
+ /**
163
+ * criteria 围栏块范围定位
164
+ */
165
+ type CriteriaBlockRange = {
166
+ /**
167
+ * 开栏行下标(0 起)
168
+ */
169
+ start: number;
170
+
171
+ /**
172
+ * 闭栏行下标(0 起)
173
+ */
174
+ end: number;
175
+ };
176
+
177
+ /**
178
+ * 注册表文件名解析产物
179
+ */
180
+ export type RegistryFileNameParts = {
181
+ /**
182
+ * 编号(3-4 位零填充)
183
+ */
184
+ id: string;
185
+
186
+ /**
187
+ * slug 段
188
+ */
189
+ slug: string;
190
+ };
191
+
192
+ /**
193
+ * 解析文件名为编号与 slug
194
+ *
195
+ * @param file - 文件名(basename)
196
+ * @returns 编号与 slug;不符合收集口径返回 null
197
+ */
198
+ export function parseRegistryFileName(file: string): RegistryFileNameParts | null {
199
+ /**
200
+ * 收集口径命中结果
201
+ */
202
+ const match = /^([0-9]{3,4})-(.+)\.md$/.exec(file);
203
+
204
+ if (match === null) {
205
+ return null;
206
+ }
207
+
208
+ return {
209
+ id: match[1] ?? '',
210
+ slug: match[2] ?? '',
211
+ };
212
+ }
213
+
214
+ /**
215
+ * 定位全部 criteria 围栏块范围
216
+ *
217
+ * @param lines - 文档行数组
218
+ * @returns 围栏块范围列表(未闭合块以 end = -1 标记)
219
+ */
220
+ function locateCriteriaBlockRanges(lines: string[]): CriteriaBlockRange[] {
221
+ /**
222
+ * 已开栏待闭范围收集器
223
+ */
224
+ const ranges: CriteriaBlockRange[] = [];
225
+
226
+ /**
227
+ * 当前未闭合开栏行下标
228
+ */
229
+ let openIndex = -1;
230
+
231
+ lines.forEach((line, index) => {
232
+ if (openIndex === -1) {
233
+ if (CRITERIA_FENCE_OPEN_PATTERN.test(line)) {
234
+ openIndex = index;
235
+ }
236
+
237
+ return;
238
+ }
239
+
240
+ if (CRITERIA_FENCE_CLOSE_PATTERN.test(line)) {
241
+ ranges.push({ start: openIndex, end: index });
242
+ openIndex = -1;
243
+ }
244
+ });
245
+
246
+ if (openIndex !== -1) {
247
+ ranges.push({ start: openIndex, end: -1 });
248
+ }
249
+
250
+ return ranges;
251
+ }
252
+
253
+ /**
254
+ * 解析 criteria 围栏块(宁可拒收不可猜)
255
+ *
256
+ * 全文必须单块唯一;`summary:` 唯一非空;判据行四段逐段非空、channel 合法、
257
+ * 段内禁多余竖线;块内至少一条判据。所有违规抛带行号错误。
258
+ *
259
+ * @param raw - 文档全文
260
+ * @returns 围栏块解析产物
261
+ * @throws {@link Error} 块缺席 / 多块 / 未闭合 / 行内违规(错误消息含行号)
262
+ */
263
+ export function parseCriteriaBlock(raw: string): ParsedCriteriaBlock {
264
+ /**
265
+ * 文档行数组
266
+ */
267
+ const lines = raw.split(/\r?\n/);
268
+
269
+ /**
270
+ * 围栏块范围列表
271
+ */
272
+ const ranges = locateCriteriaBlockRanges(lines);
273
+
274
+ if (ranges.length === 0) {
275
+ throw new Error('未找到 criteria 围栏块(```criteria 开栏)');
276
+ }
277
+
278
+ if (ranges.length > 1) {
279
+ /**
280
+ * 第二块开栏行号(1 起)
281
+ */
282
+ const secondOpenLine = (ranges[1]?.start ?? 0) + 1;
283
+
284
+ throw new Error(`criteria 围栏块必须唯一:第 ${secondOpenLine} 行出现第二块`);
285
+ }
286
+
287
+ /**
288
+ * 唯一块范围
289
+ */
290
+ const range = ranges[0];
291
+
292
+ if (range === undefined) {
293
+ throw new Error('未找到 criteria 围栏块(```criteria 开栏)');
294
+ }
295
+
296
+ if (range.end === -1) {
297
+ throw new Error(`criteria 围栏块未闭合(第 ${range.start + 1} 行开栏后无闭栏)`);
298
+ }
299
+
300
+ /**
301
+ * 概要行内容
302
+ */
303
+ let summary: string | null = null;
304
+
305
+ /**
306
+ * 概要行出现次数
307
+ */
308
+ let summaryCount = 0;
309
+
310
+ /**
311
+ * 判据条目收集器
312
+ */
313
+ const criteria: ParsedCriterion[] = [];
314
+
315
+ for (let index = range.start + 1; index < range.end; index += 1) {
316
+ /**
317
+ * 当前块内行
318
+ */
319
+ const line = lines[index] ?? '';
320
+
321
+ /**
322
+ * 当前行行号(1 起)
323
+ */
324
+ const lineNumber = index + 1;
325
+
326
+ if (line.trim() === '' || line.trimStart().startsWith('#')) {
327
+ continue;
328
+ }
329
+
330
+ if (line.startsWith('summary:')) {
331
+ /**
332
+ * 概要值
333
+ */
334
+ const value = line.slice('summary:'.length).trim();
335
+
336
+ if (value === '') {
337
+ throw new Error(`第 ${lineNumber} 行 summary 值为空(须唯一且非空)`);
338
+ }
339
+
340
+ summaryCount += 1;
341
+
342
+ if (summaryCount > 1) {
343
+ throw new Error(`第 ${lineNumber} 行 summary 重复(summary 行必须唯一)`);
344
+ }
345
+
346
+ summary = value;
347
+
348
+ continue;
349
+ }
350
+
351
+ /**
352
+ * 判据行四段切分结果
353
+ */
354
+ const segments = line.split('|');
355
+
356
+ if (segments.length !== 4) {
357
+ throw new Error(
358
+ `第 ${lineNumber} 行判据须为四段 label|channel|cwd|command(命令含竖线须以脚本承载)`,
359
+ );
360
+ }
361
+
362
+ /**
363
+ * 判据标签段
364
+ */
365
+ const label = (segments[0] ?? '').trim();
366
+
367
+ /**
368
+ * 判据通道段
369
+ */
370
+ const channel = (segments[1] ?? '').trim();
371
+
372
+ /**
373
+ * 判据工作目录段
374
+ */
375
+ const cwd = (segments[2] ?? '').trim();
376
+
377
+ /**
378
+ * 判据命令段
379
+ */
380
+ const command = (segments[3] ?? '').trim();
381
+
382
+ if (label === '' || channel === '' || cwd === '' || command === '') {
383
+ throw new Error(`第 ${lineNumber} 行判据存在空段(四段逐段非空)`);
384
+ }
385
+
386
+ if (!CRITERIA_CHANNEL_PATTERN.test(channel)) {
387
+ throw new Error(
388
+ `第 ${lineNumber} 行 channel「${channel}」不合法(限字母数字起首的 [A-Za-z0-9._-])`,
389
+ );
390
+ }
391
+
392
+ criteria.push({ label, channel, cwd, command });
393
+ }
394
+
395
+ if (criteria.length === 0) {
396
+ throw new Error(`criteria 围栏块内至少一条判据(第 ${range.start + 1} 行块为空)`);
397
+ }
398
+
399
+ return { summary, criteria };
400
+ }
401
+
402
+ /**
403
+ * 在头部窗口内定位元信息行并提取引用 token
404
+ *
405
+ * @param lines - 文档行数组
406
+ * @param key - 元信息键
407
+ * @returns 引用 token 列表(行缺席为空列表)
408
+ */
409
+ function extractMetaLineRefs(lines: string[], key: string): IssueRefToken[] {
410
+ /**
411
+ * 键定位正则
412
+ */
413
+ const locatePattern = buildMetaLineLocatePattern(key);
414
+
415
+ /**
416
+ * 扫描窗口上界(头部前 N 行,与状态行窗口一致)
417
+ */
418
+ const scanLimit = Math.min(lines.length, STATUS_LINE_SCAN_LINES);
419
+
420
+ for (let index = 0; index < scanLimit; index += 1) {
421
+ /**
422
+ * 当前行
423
+ */
424
+ const line = lines[index] ?? '';
425
+
426
+ if (locatePattern.test(line)) {
427
+ return extractIssueRefTokens(line);
428
+ }
429
+ }
430
+
431
+ return [];
432
+ }
433
+
434
+ /**
435
+ * 解析单份 issue 文档
436
+ *
437
+ * @param file - 文件名(basename,用于编号与 slug 提取)
438
+ * @param raw - 文档全文
439
+ * @param modifiedAtMs - 文件 mtime(毫秒)
440
+ * @returns 解析产物(收集口径外的文件返回 null)
441
+ */
442
+ export function parseIssueDoc(
443
+ file: string,
444
+ raw: string,
445
+ modifiedAtMs: number,
446
+ ): IssueDocRecord | null {
447
+ /**
448
+ * 文件名解析结果
449
+ */
450
+ const nameParts = parseRegistryFileName(file);
451
+
452
+ if (nameParts === null) {
453
+ return null;
454
+ }
455
+
456
+ /**
457
+ * 文档行数组
458
+ */
459
+ const lines = raw.split(/\r?\n/);
460
+
461
+ /**
462
+ * 标题:首个 `# ` 行内容
463
+ */
464
+ let title = '';
465
+
466
+ for (const line of lines) {
467
+ if (line.startsWith('# ')) {
468
+ title = line.slice(2).trim();
469
+
470
+ break;
471
+ }
472
+ }
473
+
474
+ /**
475
+ * 状态行定位结果
476
+ */
477
+ let statusLine = '';
478
+
479
+ /**
480
+ * 状态行行号(1 起,0 = 未定位)
481
+ */
482
+ let statusLineNumber = 0;
483
+
484
+ /**
485
+ * 状态归类
486
+ */
487
+ let status: IssueStatusKind = 'unknown';
488
+
489
+ /**
490
+ * 状态行扫描窗口上界
491
+ */
492
+ const statusScanLimit = Math.min(lines.length, STATUS_LINE_SCAN_LINES);
493
+
494
+ for (let index = 0; index < statusScanLimit; index += 1) {
495
+ /**
496
+ * 当前行
497
+ */
498
+ const line = lines[index] ?? '';
499
+
500
+ if (STATUS_LINE_LOCATE_PATTERN.test(line)) {
501
+ statusLine = line.trim();
502
+ statusLineNumber = index + 1;
503
+ status = classifyStatusLine(line);
504
+
505
+ break;
506
+ }
507
+ }
508
+
509
+ /**
510
+ * 挂起原因:状态行首个 ≥4 字括注(无括注或不足 4 字为 null)
511
+ */
512
+ const pendingMatch = statusLine === '' ? null : PENDING_REASON_PATTERN.exec(statusLine);
513
+
514
+ /**
515
+ * 挂起原因值
516
+ */
517
+ const pendingReason = pendingMatch === null ? null : (pendingMatch[1] ?? '').trim();
518
+
519
+ /**
520
+ * 处置段标题行下标(-1 = 缺席)
521
+ */
522
+ let dispositionIndex = -1;
523
+
524
+ lines.forEach((line, index) => {
525
+ if (
526
+ dispositionIndex === -1 &&
527
+ line.startsWith('## ') &&
528
+ line.includes(DISPOSITION_TITLE_KEYWORD)
529
+ ) {
530
+ dispositionIndex = index;
531
+ }
532
+ });
533
+
534
+ /**
535
+ * 处置段占位判定:标题起 8 行窗口内逐行匹配占位模式
536
+ */
537
+ let dispositionIsPlaceholder = false;
538
+
539
+ if (dispositionIndex !== -1) {
540
+ /**
541
+ * 占位窗口上界
542
+ */
543
+ const placeholderLimit = Math.min(
544
+ lines.length,
545
+ dispositionIndex + DISPOSITION_PLACEHOLDER_WINDOW_LINES,
546
+ );
547
+
548
+ for (let index = dispositionIndex; index < placeholderLimit; index += 1) {
549
+ if (DISPOSITION_PLACEHOLDER_PATTERN.test(lines[index] ?? '')) {
550
+ dispositionIsPlaceholder = true;
551
+
552
+ break;
553
+ }
554
+ }
555
+ }
556
+
557
+ /**
558
+ * 证据锚点块在位判定
559
+ */
560
+ const hasEvidenceAnchorBlock = EVIDENCE_ANCHOR_MARKERS.some((marker) => raw.includes(marker));
561
+
562
+ /**
563
+ * criteria 围栏块解析(缺席 / 多块 / 畸形记录错误消息,不中断读档)
564
+ */
565
+ /**
566
+ * criteria 围栏块解析产物(块在位且解析成功时非 null)
567
+ */
568
+ let criteria: ParsedCriteriaBlock | null = null;
569
+
570
+ /**
571
+ * criteria 围栏块解析错误消息(多块 / 畸形时非 null,不中断读档)
572
+ */
573
+ let criteriaError: string | null = null;
574
+
575
+ if (locateCriteriaBlockRanges(lines).length > 0) {
576
+ try {
577
+ criteria = parseCriteriaBlock(raw);
578
+ } catch (error) {
579
+ criteriaError = error instanceof Error ? error.message : String(error);
580
+ }
581
+ }
582
+
583
+ return {
584
+ id: nameParts.id,
585
+ slug: nameParts.slug,
586
+ file,
587
+ title,
588
+ status,
589
+ statusLine,
590
+ statusLineNumber,
591
+ pendingReason,
592
+ hasDispositionSection: dispositionIndex !== -1,
593
+ dispositionIsPlaceholder,
594
+ hasEvidenceAnchorBlock,
595
+ criteria,
596
+ criteriaError,
597
+ historyRefs: extractMetaLineRefs(lines, HISTORY_LINE_KEY),
598
+ blockedByRefs: extractMetaLineRefs(lines, BLOCKING_LINE_KEY),
599
+ rawContent: raw,
600
+ modifiedAtMs,
601
+ };
602
+ }
603
+
604
+ /**
605
+ * 扫描注册表目录
606
+ *
607
+ * 只收集口径内文件(NNNN-slug.md),逐份解析,按编号升序;目录缺席返回空数组;
608
+ * 单份读档失败降级为 unknown 最小形态(不中断批次,apex 同款语义)。
609
+ *
610
+ * @param registryDir - 注册表目录绝对或相对路径
611
+ * @returns 解析产物列表(编号升序)
612
+ */
613
+ export function scanRegistryDir(registryDir: string): IssueDocRecord[] {
614
+ if (!existsSync(registryDir)) {
615
+ return [];
616
+ }
617
+
618
+ /**
619
+ * 口径内文件名列表
620
+ */
621
+ const files = readdirSync(registryDir)
622
+ .filter((file) => REGISTRY_FILE_PATTERN.test(file))
623
+ .sort((a, b) => a.localeCompare(b));
624
+
625
+ /**
626
+ * 解析产物收集器
627
+ */
628
+ const records: IssueDocRecord[] = [];
629
+
630
+ for (const file of files) {
631
+ /**
632
+ * 文档绝对路径
633
+ */
634
+ const filePath = join(registryDir, file);
635
+
636
+ try {
637
+ /**
638
+ * 文档全文
639
+ */
640
+ const raw = readFileSync(filePath, 'utf8');
641
+
642
+ /**
643
+ * mtime(毫秒)
644
+ */
645
+ const modifiedAtMs = statSync(filePath).mtimeMs;
646
+
647
+ /**
648
+ * 解析产物(口径内文件必非 null)
649
+ */
650
+ const record = parseIssueDoc(file, raw, modifiedAtMs);
651
+
652
+ if (record !== null) {
653
+ records.push(record);
654
+ }
655
+ } catch {
656
+ records.push({
657
+ id: parseRegistryFileName(file)?.id ?? '',
658
+ slug: parseRegistryFileName(file)?.slug ?? '',
659
+ file,
660
+ title: '',
661
+ status: 'unknown',
662
+ statusLine: '',
663
+ statusLineNumber: 0,
664
+ pendingReason: null,
665
+ hasDispositionSection: false,
666
+ dispositionIsPlaceholder: false,
667
+ hasEvidenceAnchorBlock: false,
668
+ criteria: null,
669
+ criteriaError: null,
670
+ historyRefs: [],
671
+ blockedByRefs: [],
672
+ rawContent: '',
673
+ modifiedAtMs: 0,
674
+ });
675
+ }
676
+ }
677
+
678
+ return records;
679
+ }