@hraness/kb 0.17.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/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,1150 @@
|
|
|
1
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
isAbsolute,
|
|
4
|
+
join,
|
|
5
|
+
relative,
|
|
6
|
+
resolve,
|
|
7
|
+
sep,
|
|
8
|
+
} from "node:path";
|
|
9
|
+
|
|
10
|
+
import { normalizeRepositoryScope } from "./agent-context.js";
|
|
11
|
+
import type { MetadataObject, Note } from "./graph.js";
|
|
12
|
+
|
|
13
|
+
export const repositoryScopesMetadataKey = "repository_scopes";
|
|
14
|
+
export const MAX_REPOSITORY_SCOPES = 16;
|
|
15
|
+
export const MAX_REPOSITORY_SCOPE_UTF8_BYTES = 1_024;
|
|
16
|
+
export const MAX_REPOSITORY_SCOPES_UTF8_BYTES = 8 * 1_024;
|
|
17
|
+
export const DEFAULT_REPOSITORY_MEMORY_GROUP_LIMIT = 10;
|
|
18
|
+
export const MAX_REPOSITORY_MEMORY_GROUP_LIMIT = 100;
|
|
19
|
+
export const DEFAULT_REPOSITORY_MEMORY_DETAIL_LIMIT = 20;
|
|
20
|
+
export const MAX_REPOSITORY_MEMORY_DETAIL_LIMIT = 100;
|
|
21
|
+
export const MAX_REPOSITORY_MEMORY_SUMMARY_UTF8_BYTES = 2 * 1_024;
|
|
22
|
+
|
|
23
|
+
export const activePlanStatuses = [
|
|
24
|
+
"proposed",
|
|
25
|
+
"accepted",
|
|
26
|
+
"in-progress",
|
|
27
|
+
"blocked",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
export const terminalPlanStatuses = [
|
|
31
|
+
"completed",
|
|
32
|
+
"superseded",
|
|
33
|
+
"cancelled",
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
export const planStatuses = [
|
|
37
|
+
...activePlanStatuses,
|
|
38
|
+
...terminalPlanStatuses,
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
export type ActivePlanStatus = typeof activePlanStatuses[number];
|
|
42
|
+
export type TerminalPlanStatus = typeof terminalPlanStatuses[number];
|
|
43
|
+
export type PlanStatus = typeof planStatuses[number];
|
|
44
|
+
|
|
45
|
+
const activeStatusSet: ReadonlySet<string> = new Set(activePlanStatuses);
|
|
46
|
+
const terminalStatusSet: ReadonlySet<string> = new Set(terminalPlanStatuses);
|
|
47
|
+
const drivePathPattern = /^[A-Za-z]:/u;
|
|
48
|
+
const globCharacterPattern = /[*?[\]{}]/u;
|
|
49
|
+
|
|
50
|
+
export function isActivePlanStatus(value: unknown): value is ActivePlanStatus {
|
|
51
|
+
return typeof value === "string" && activeStatusSet.has(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isTerminalPlanStatus(value: unknown): value is TerminalPlanStatus {
|
|
55
|
+
return typeof value === "string" && terminalStatusSet.has(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isPlanStatus(value: unknown): value is PlanStatus {
|
|
59
|
+
return isActivePlanStatus(value) || isTerminalPlanStatus(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type RepositoryScopeValidationCode =
|
|
63
|
+
| "absolute"
|
|
64
|
+
| "case-fold-collision"
|
|
65
|
+
| "control-character"
|
|
66
|
+
| "duplicate"
|
|
67
|
+
| "empty"
|
|
68
|
+
| "glob"
|
|
69
|
+
| "noncanonical"
|
|
70
|
+
| "not-array"
|
|
71
|
+
| "not-string"
|
|
72
|
+
| "scope-bytes"
|
|
73
|
+
| "scope-count"
|
|
74
|
+
| "total-bytes"
|
|
75
|
+
| "traversal";
|
|
76
|
+
|
|
77
|
+
export type RepositoryScopeValidationIssue = {
|
|
78
|
+
readonly code: RepositoryScopeValidationCode;
|
|
79
|
+
readonly message: string;
|
|
80
|
+
readonly index?: number;
|
|
81
|
+
readonly value?: unknown;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export class RepositoryScopesError extends TypeError {
|
|
85
|
+
readonly issues: readonly RepositoryScopeValidationIssue[];
|
|
86
|
+
|
|
87
|
+
constructor(label: string, issues: readonly RepositoryScopeValidationIssue[]) {
|
|
88
|
+
super(`${label} is invalid: ${issues.map(({ message }) => message).join(" ")}`);
|
|
89
|
+
this.name = "RepositoryScopesError";
|
|
90
|
+
this.issues = issues;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formattedBytes(value: number): string {
|
|
95
|
+
return value.toLocaleString("en-US");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function hasControlCharacter(value: string): boolean {
|
|
99
|
+
for (const character of value) {
|
|
100
|
+
const point = character.codePointAt(0);
|
|
101
|
+
if (
|
|
102
|
+
point !== undefined
|
|
103
|
+
&& (point <= 0x1f || (point >= 0x7f && point <= 0x9f))
|
|
104
|
+
) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function pathValidationCode(value: string): RepositoryScopeValidationCode {
|
|
112
|
+
if (value.trim() === "") return "empty";
|
|
113
|
+
if (hasControlCharacter(value)) return "control-character";
|
|
114
|
+
if (/^[\\/]/u.test(value) || drivePathPattern.test(value)) return "absolute";
|
|
115
|
+
if (globCharacterPattern.test(value)) return "glob";
|
|
116
|
+
if (value.replaceAll("\\", "/").split("/").includes("..")) return "traversal";
|
|
117
|
+
return "noncanonical";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validate one already-authored canonical repository-relative file or directory path.
|
|
122
|
+
*
|
|
123
|
+
* Unlike `normalizeRepositoryScope`, this function never repairs input. Callers get
|
|
124
|
+
* back the exact string they supplied or a stable validation failure.
|
|
125
|
+
*/
|
|
126
|
+
export function canonicalRepositoryPath(value: string, label = "Repository scope"): string {
|
|
127
|
+
if (typeof value !== "string") throw new TypeError(`${label} must be a string.`);
|
|
128
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
129
|
+
if (bytes > MAX_REPOSITORY_SCOPE_UTF8_BYTES) {
|
|
130
|
+
throw new RangeError(
|
|
131
|
+
`${label} must be at most ${formattedBytes(MAX_REPOSITORY_SCOPE_UTF8_BYTES)} UTF-8 bytes.`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
let canonical: string;
|
|
135
|
+
try {
|
|
136
|
+
canonical = normalizeRepositoryScope(value);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new RepositoryScopesError(label, [{
|
|
139
|
+
code: pathValidationCode(value),
|
|
140
|
+
value,
|
|
141
|
+
message: error instanceof Error ? error.message : `${label} is invalid.`,
|
|
142
|
+
}]);
|
|
143
|
+
}
|
|
144
|
+
if (canonical !== value) {
|
|
145
|
+
throw new RepositoryScopesError(label, [{
|
|
146
|
+
code: "noncanonical",
|
|
147
|
+
value,
|
|
148
|
+
message: `${label} must use its exact NFC-normalized POSIX form ${JSON.stringify(canonical)}.`,
|
|
149
|
+
}]);
|
|
150
|
+
}
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type RepositoryScopesAnalysis =
|
|
155
|
+
| {
|
|
156
|
+
readonly present: false;
|
|
157
|
+
readonly valid: true;
|
|
158
|
+
readonly scopes: readonly [];
|
|
159
|
+
readonly issues: readonly [];
|
|
160
|
+
}
|
|
161
|
+
| {
|
|
162
|
+
readonly present: true;
|
|
163
|
+
readonly valid: boolean;
|
|
164
|
+
readonly scopes: readonly string[];
|
|
165
|
+
readonly issues: readonly RepositoryScopeValidationIssue[];
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
function analyzeScopeArray(
|
|
169
|
+
value: unknown,
|
|
170
|
+
options: { readonly allowEmpty: boolean; readonly label: string },
|
|
171
|
+
): RepositoryScopesAnalysis {
|
|
172
|
+
if (!Array.isArray(value)) {
|
|
173
|
+
return {
|
|
174
|
+
present: true,
|
|
175
|
+
valid: false,
|
|
176
|
+
scopes: [],
|
|
177
|
+
issues: [{
|
|
178
|
+
code: "not-array",
|
|
179
|
+
value,
|
|
180
|
+
message: `${options.label} must be an array of canonical repository paths.`,
|
|
181
|
+
}],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const issues: RepositoryScopeValidationIssue[] = [];
|
|
185
|
+
if (!options.allowEmpty && value.length === 0) {
|
|
186
|
+
issues.push({
|
|
187
|
+
code: "empty",
|
|
188
|
+
message: `${options.label} must contain at least one repository path when present.`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (value.length > MAX_REPOSITORY_SCOPES) {
|
|
192
|
+
issues.push({
|
|
193
|
+
code: "scope-count",
|
|
194
|
+
message: `${options.label} may contain at most ${MAX_REPOSITORY_SCOPES} paths.`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let totalBytes = 0;
|
|
199
|
+
const scopes: string[] = [];
|
|
200
|
+
const exact = new Map<string, number>();
|
|
201
|
+
const folded = new Map<string, { readonly scope: string; readonly index: number }>();
|
|
202
|
+
for (let index = 0; index < Math.min(value.length, MAX_REPOSITORY_SCOPES); index += 1) {
|
|
203
|
+
const candidate: unknown = value[index];
|
|
204
|
+
if (typeof candidate !== "string") {
|
|
205
|
+
issues.push({
|
|
206
|
+
code: "not-string",
|
|
207
|
+
index,
|
|
208
|
+
value: candidate,
|
|
209
|
+
message: `${options.label} entry ${index + 1} must be a string.`,
|
|
210
|
+
});
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const bytes = Buffer.byteLength(candidate, "utf8");
|
|
214
|
+
totalBytes += bytes;
|
|
215
|
+
if (bytes > MAX_REPOSITORY_SCOPE_UTF8_BYTES) {
|
|
216
|
+
issues.push({
|
|
217
|
+
code: "scope-bytes",
|
|
218
|
+
index,
|
|
219
|
+
value: candidate,
|
|
220
|
+
message: `${options.label} entry ${index + 1} must be at most `
|
|
221
|
+
+ `${formattedBytes(MAX_REPOSITORY_SCOPE_UTF8_BYTES)} UTF-8 bytes.`,
|
|
222
|
+
});
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
canonicalRepositoryPath(candidate, `${options.label} entry ${index + 1}`);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error instanceof RepositoryScopesError) {
|
|
229
|
+
issues.push(...error.issues.map((issue) => ({ ...issue, index })));
|
|
230
|
+
} else {
|
|
231
|
+
issues.push({
|
|
232
|
+
code: "noncanonical",
|
|
233
|
+
index,
|
|
234
|
+
value: candidate,
|
|
235
|
+
message: error instanceof Error ? error.message : "The repository path is invalid.",
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const duplicate = exact.get(candidate);
|
|
241
|
+
if (duplicate !== undefined) {
|
|
242
|
+
issues.push({
|
|
243
|
+
code: "duplicate",
|
|
244
|
+
index,
|
|
245
|
+
value: candidate,
|
|
246
|
+
message: `${options.label} entries ${duplicate + 1} and ${index + 1} are duplicates.`,
|
|
247
|
+
});
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
exact.set(candidate, index);
|
|
251
|
+
const caseFolded = candidate.toLocaleLowerCase("en-US");
|
|
252
|
+
const collision = folded.get(caseFolded);
|
|
253
|
+
if (collision !== undefined && collision.scope !== candidate) {
|
|
254
|
+
issues.push({
|
|
255
|
+
code: "case-fold-collision",
|
|
256
|
+
index,
|
|
257
|
+
value: candidate,
|
|
258
|
+
message: `${options.label} entries ${collision.index + 1} and ${index + 1} collide under case folding.`,
|
|
259
|
+
});
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
folded.set(caseFolded, { scope: candidate, index });
|
|
263
|
+
scopes.push(candidate);
|
|
264
|
+
}
|
|
265
|
+
if (totalBytes > MAX_REPOSITORY_SCOPES_UTF8_BYTES) {
|
|
266
|
+
issues.push({
|
|
267
|
+
code: "total-bytes",
|
|
268
|
+
message: `${options.label} may contain at most `
|
|
269
|
+
+ `${formattedBytes(MAX_REPOSITORY_SCOPES_UTF8_BYTES)} UTF-8 bytes in total.`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
present: true,
|
|
274
|
+
valid: issues.length === 0,
|
|
275
|
+
scopes,
|
|
276
|
+
issues,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Parse the exact authored `repository_scopes` field without key or value folding. */
|
|
281
|
+
export function analyzeAuthoredRepositoryScopes(
|
|
282
|
+
metadata: MetadataObject,
|
|
283
|
+
): RepositoryScopesAnalysis {
|
|
284
|
+
const exactKeys = Object.keys(metadata).filter((key) =>
|
|
285
|
+
key.toLocaleLowerCase("en-US") === repositoryScopesMetadataKey);
|
|
286
|
+
if (exactKeys.length === 0) {
|
|
287
|
+
return { present: false, valid: true, scopes: [], issues: [] };
|
|
288
|
+
}
|
|
289
|
+
if (exactKeys.length !== 1 || exactKeys[0] !== repositoryScopesMetadataKey) {
|
|
290
|
+
return {
|
|
291
|
+
present: true,
|
|
292
|
+
valid: false,
|
|
293
|
+
scopes: [],
|
|
294
|
+
issues: [{
|
|
295
|
+
code: "noncanonical",
|
|
296
|
+
message: `Repository scope metadata must use the exact key ${repositoryScopesMetadataKey}.`,
|
|
297
|
+
}],
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return analyzeScopeArray(metadata[repositoryScopesMetadataKey], {
|
|
301
|
+
allowEmpty: false,
|
|
302
|
+
label: repositoryScopesMetadataKey,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Validate an API selection of exact authored scopes. An empty selection disables filtering. */
|
|
307
|
+
export function validateRepositoryScopeSelection(value: unknown): readonly string[] {
|
|
308
|
+
const analysis = analyzeScopeArray(value, {
|
|
309
|
+
allowEmpty: true,
|
|
310
|
+
label: "Repository scope selection",
|
|
311
|
+
});
|
|
312
|
+
if (!analysis.valid) throw new RepositoryScopesError("Repository scope selection", analysis.issues);
|
|
313
|
+
return analysis.scopes;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** True when a canonical declaration is the exact path or a segment ancestor of a full path. */
|
|
317
|
+
export function repositoryScopeMatchesPath(scope: string, fullPath: string): boolean {
|
|
318
|
+
const checkedScope = canonicalRepositoryPath(scope, "Repository scope");
|
|
319
|
+
const checkedPath = canonicalRepositoryPath(fullPath, "Repository path");
|
|
320
|
+
return checkedScope === "."
|
|
321
|
+
|| checkedScope === checkedPath
|
|
322
|
+
|| checkedPath.startsWith(`${checkedScope}/`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export type RepositoryScopeMatch = {
|
|
326
|
+
readonly scope: string;
|
|
327
|
+
readonly kind: "ancestor" | "exact";
|
|
328
|
+
readonly depth: number;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
function scopeDepth(scope: string): number {
|
|
332
|
+
return scope === "." ? 0 : scope.split("/").length;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Return the deepest exact-or-ancestor declaration for a full repository path. */
|
|
336
|
+
export function deepestRepositoryScopeMatch(
|
|
337
|
+
scopes: readonly string[],
|
|
338
|
+
fullPath: string,
|
|
339
|
+
): RepositoryScopeMatch | null {
|
|
340
|
+
const checkedScopes = validateRepositoryScopeSelection(scopes);
|
|
341
|
+
const checkedPath = canonicalRepositoryPath(fullPath, "Repository path");
|
|
342
|
+
const matches = checkedScopes
|
|
343
|
+
.filter((scope) => repositoryScopeMatchesPath(scope, checkedPath))
|
|
344
|
+
.map((scope): RepositoryScopeMatch => ({
|
|
345
|
+
scope,
|
|
346
|
+
kind: scope === checkedPath ? "exact" : "ancestor",
|
|
347
|
+
depth: scopeDepth(scope),
|
|
348
|
+
}))
|
|
349
|
+
.toSorted((left, right) =>
|
|
350
|
+
right.depth - left.depth || left.scope.localeCompare(right.scope));
|
|
351
|
+
return matches[0] ?? null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Match any selected value against a record's valid declarations, exactly and case-sensitively. */
|
|
355
|
+
export function metadataMatchesExactRepositoryScopes(
|
|
356
|
+
metadata: MetadataObject,
|
|
357
|
+
selectedScopes: readonly string[],
|
|
358
|
+
): boolean {
|
|
359
|
+
const selected = validateRepositoryScopeSelection(selectedScopes);
|
|
360
|
+
if (selected.length === 0) return true;
|
|
361
|
+
const authored = analyzeAuthoredRepositoryScopes(metadata);
|
|
362
|
+
if (!authored.present || !authored.valid) return false;
|
|
363
|
+
const declarations = new Set(authored.scopes);
|
|
364
|
+
return selected.some((scope) => declarations.has(scope));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isMissingFileError(error: unknown): boolean {
|
|
368
|
+
return error !== null
|
|
369
|
+
&& typeof error === "object"
|
|
370
|
+
&& "code" in error
|
|
371
|
+
&& error.code === "ENOENT";
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function pathInside(root: string, candidate: string): boolean {
|
|
375
|
+
const fromRoot = relative(root, candidate);
|
|
376
|
+
return fromRoot === ""
|
|
377
|
+
|| (fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export type RepositoryScopeFilesystemState =
|
|
381
|
+
| {
|
|
382
|
+
readonly status: "present";
|
|
383
|
+
readonly scope: string;
|
|
384
|
+
readonly kind: "directory" | "file";
|
|
385
|
+
}
|
|
386
|
+
| {
|
|
387
|
+
readonly status: "absent";
|
|
388
|
+
readonly scope: string;
|
|
389
|
+
readonly firstMissingPath: string;
|
|
390
|
+
}
|
|
391
|
+
| {
|
|
392
|
+
readonly status: "invalid";
|
|
393
|
+
readonly scope: string;
|
|
394
|
+
readonly path: string;
|
|
395
|
+
readonly reason: "ancestor-not-directory" | "symlink" | "unsupported-type";
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
async function canonicalRepositoryRoot(rootInput: string): Promise<string> {
|
|
399
|
+
const root = await realpath(resolve(rootInput));
|
|
400
|
+
const metadata = await lstat(root);
|
|
401
|
+
if (!metadata.isDirectory()) throw new TypeError("The repository root must be a directory.");
|
|
402
|
+
return root;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function inspectScopeAtRoot(
|
|
406
|
+
root: string,
|
|
407
|
+
scope: string,
|
|
408
|
+
): Promise<RepositoryScopeFilesystemState> {
|
|
409
|
+
if (scope === ".") return { status: "present", scope, kind: "directory" };
|
|
410
|
+
let current = root;
|
|
411
|
+
const traversed: string[] = [];
|
|
412
|
+
const segments = scope.split("/");
|
|
413
|
+
for (const [index, segment] of segments.entries()) {
|
|
414
|
+
traversed.push(segment);
|
|
415
|
+
current = join(current, segment);
|
|
416
|
+
if (!pathInside(root, current)) {
|
|
417
|
+
throw new TypeError("A repository scope resolved outside the repository root.");
|
|
418
|
+
}
|
|
419
|
+
let metadata;
|
|
420
|
+
try {
|
|
421
|
+
metadata = await lstat(current);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
if (isMissingFileError(error)) {
|
|
424
|
+
return {
|
|
425
|
+
status: "absent",
|
|
426
|
+
scope,
|
|
427
|
+
firstMissingPath: traversed.join("/"),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
const repositoryPath = traversed.join("/");
|
|
433
|
+
if (metadata.isSymbolicLink()) {
|
|
434
|
+
return { status: "invalid", scope, path: repositoryPath, reason: "symlink" };
|
|
435
|
+
}
|
|
436
|
+
if (index < segments.length - 1 && !metadata.isDirectory()) {
|
|
437
|
+
return {
|
|
438
|
+
status: "invalid",
|
|
439
|
+
scope,
|
|
440
|
+
path: repositoryPath,
|
|
441
|
+
reason: "ancestor-not-directory",
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
if (index === segments.length - 1) {
|
|
445
|
+
if (metadata.isDirectory()) return { status: "present", scope, kind: "directory" };
|
|
446
|
+
if (metadata.isFile()) return { status: "present", scope, kind: "file" };
|
|
447
|
+
return {
|
|
448
|
+
status: "invalid",
|
|
449
|
+
scope,
|
|
450
|
+
path: repositoryPath,
|
|
451
|
+
reason: "unsupported-type",
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
throw new Error("Repository scope inspection did not reach its final segment.");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Inspect one canonical declaration without following symlinks or repairing absent paths. */
|
|
459
|
+
export async function inspectRepositoryScopeState(
|
|
460
|
+
repositoryRoot: string,
|
|
461
|
+
scopeInput: string,
|
|
462
|
+
): Promise<RepositoryScopeFilesystemState> {
|
|
463
|
+
const scope = canonicalRepositoryPath(scopeInput);
|
|
464
|
+
return await inspectScopeAtRoot(await canonicalRepositoryRoot(repositoryRoot), scope);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export type RepositoryMemoryGroupKey =
|
|
468
|
+
| "maintainedKnowledge"
|
|
469
|
+
| "activePlans"
|
|
470
|
+
| "datedResearch"
|
|
471
|
+
| "reports"
|
|
472
|
+
| "historicalPlans";
|
|
473
|
+
|
|
474
|
+
export const repositoryMemoryGroupKeys = [
|
|
475
|
+
"maintainedKnowledge",
|
|
476
|
+
"activePlans",
|
|
477
|
+
"datedResearch",
|
|
478
|
+
"reports",
|
|
479
|
+
"historicalPlans",
|
|
480
|
+
] as const satisfies readonly RepositoryMemoryGroupKey[];
|
|
481
|
+
|
|
482
|
+
export type RepositoryMemoryClassification =
|
|
483
|
+
| {
|
|
484
|
+
readonly kind: "record";
|
|
485
|
+
readonly group: RepositoryMemoryGroupKey;
|
|
486
|
+
readonly current: boolean;
|
|
487
|
+
readonly status?: string;
|
|
488
|
+
readonly date?: string;
|
|
489
|
+
}
|
|
490
|
+
| { readonly kind: "ignored" }
|
|
491
|
+
| {
|
|
492
|
+
readonly kind: "invalid";
|
|
493
|
+
readonly reason: string;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
function metadataString(metadata: MetadataObject, key: string): string | undefined {
|
|
497
|
+
const value = metadata[key];
|
|
498
|
+
return typeof value === "string" ? value : undefined;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function validCalendarDate(value: string): boolean {
|
|
502
|
+
if (!/^\d{4}-\d{2}-\d{2}$/u.test(value)) return false;
|
|
503
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
504
|
+
if (year === undefined || month === undefined || day === undefined) return false;
|
|
505
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
506
|
+
return date.getUTCFullYear() === year
|
|
507
|
+
&& date.getUTCMonth() + 1 === month
|
|
508
|
+
&& date.getUTCDate() === day;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Classify one scoped Markdown record without consulting inferred links or search state. */
|
|
512
|
+
export function classifyRepositoryMemoryRecord(note: Note): RepositoryMemoryClassification {
|
|
513
|
+
const type = metadataString(note.metadata, "type");
|
|
514
|
+
const status = metadataString(note.metadata, "status");
|
|
515
|
+
if (type === "note" || type === "concept") {
|
|
516
|
+
return { kind: "record", group: "maintainedKnowledge", current: true };
|
|
517
|
+
}
|
|
518
|
+
if (type === "plan") {
|
|
519
|
+
if (isActivePlanStatus(status)) {
|
|
520
|
+
return { kind: "record", group: "activePlans", current: true, status };
|
|
521
|
+
}
|
|
522
|
+
if (isTerminalPlanStatus(status)) {
|
|
523
|
+
return { kind: "record", group: "historicalPlans", current: false, status };
|
|
524
|
+
}
|
|
525
|
+
return { kind: "invalid", reason: "A scoped plan must declare one supported lifecycle status." };
|
|
526
|
+
}
|
|
527
|
+
if (type === "market-research") {
|
|
528
|
+
const asOf = metadataString(note.metadata, "as_of");
|
|
529
|
+
if (!/^projects\/[^/]+\/market\/.+\.md$/u.test(note.path)) {
|
|
530
|
+
return {
|
|
531
|
+
kind: "invalid",
|
|
532
|
+
reason: "Scoped market research must live under projects/<domain>/market/.",
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
if (status !== "snapshot" || asOf === undefined || !validCalendarDate(asOf)) {
|
|
536
|
+
return {
|
|
537
|
+
kind: "invalid",
|
|
538
|
+
reason: "Scoped market research requires status snapshot and a valid as_of date.",
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
kind: "record",
|
|
543
|
+
group: "datedResearch",
|
|
544
|
+
current: true,
|
|
545
|
+
status,
|
|
546
|
+
date: asOf,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
if (type === "report") {
|
|
550
|
+
const generated = metadataString(note.metadata, "generated");
|
|
551
|
+
if (generated === undefined || !validCalendarDate(generated)) {
|
|
552
|
+
return { kind: "invalid", reason: "A scoped report requires a valid generated date." };
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
kind: "record",
|
|
556
|
+
group: "reports",
|
|
557
|
+
current: true,
|
|
558
|
+
date: generated,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
return type === undefined
|
|
562
|
+
? { kind: "invalid", reason: "A scoped repository-memory record must declare a type." }
|
|
563
|
+
: { kind: "ignored" };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export type RepositoryMemoryRecord = {
|
|
567
|
+
readonly id: string;
|
|
568
|
+
readonly path: string;
|
|
569
|
+
readonly title: string;
|
|
570
|
+
readonly summary: string;
|
|
571
|
+
readonly summaryTruncated: boolean;
|
|
572
|
+
readonly type: string;
|
|
573
|
+
readonly status?: string;
|
|
574
|
+
readonly date?: string;
|
|
575
|
+
readonly area?: string;
|
|
576
|
+
readonly description?: string;
|
|
577
|
+
readonly repositoryScopes: readonly string[];
|
|
578
|
+
readonly matchedScope: string;
|
|
579
|
+
readonly match: "ancestor" | "exact";
|
|
580
|
+
readonly scopeState: RepositoryScopeFilesystemState;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
export type RepositoryMemoryGroup = {
|
|
584
|
+
readonly total: number;
|
|
585
|
+
readonly returned: number;
|
|
586
|
+
readonly truncated: boolean;
|
|
587
|
+
readonly records: readonly RepositoryMemoryRecord[];
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
export type RepositoryMemoryInvalidRecord = {
|
|
591
|
+
readonly path: string;
|
|
592
|
+
readonly issues: readonly string[];
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
export type RepositoryMemoryAdvisory = {
|
|
596
|
+
readonly kind: "absent-current-scope";
|
|
597
|
+
readonly path: string;
|
|
598
|
+
readonly scope: string;
|
|
599
|
+
readonly message: string;
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
export type RepositoryMemoryDetailList<T> = {
|
|
603
|
+
readonly total: number;
|
|
604
|
+
readonly returned: number;
|
|
605
|
+
readonly truncated: boolean;
|
|
606
|
+
readonly details: readonly T[];
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
export type RepositoryMemoryContext = {
|
|
610
|
+
readonly repositoryRoot: string;
|
|
611
|
+
readonly target: string;
|
|
612
|
+
readonly targetState: RepositoryScopeFilesystemState;
|
|
613
|
+
readonly groups: Readonly<Record<RepositoryMemoryGroupKey, RepositoryMemoryGroup>>;
|
|
614
|
+
readonly counts: {
|
|
615
|
+
readonly matched: number;
|
|
616
|
+
readonly returned: number;
|
|
617
|
+
readonly invalid: number;
|
|
618
|
+
readonly advisories: number;
|
|
619
|
+
};
|
|
620
|
+
readonly invalidRecords: RepositoryMemoryDetailList<RepositoryMemoryInvalidRecord>;
|
|
621
|
+
readonly advisories: RepositoryMemoryDetailList<RepositoryMemoryAdvisory>;
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
export type BuildRepositoryMemoryContextOptions = {
|
|
625
|
+
readonly repositoryRoot: string;
|
|
626
|
+
readonly target: string;
|
|
627
|
+
readonly groupLimit?: number;
|
|
628
|
+
readonly detailLimit?: number;
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
type Candidate = {
|
|
632
|
+
readonly note: Note;
|
|
633
|
+
readonly scopes: readonly string[];
|
|
634
|
+
readonly classification: Extract<RepositoryMemoryClassification, { readonly kind: "record" }>;
|
|
635
|
+
readonly match: RepositoryScopeMatch;
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
function checkedLimit(
|
|
639
|
+
value: number | undefined,
|
|
640
|
+
fallback: number,
|
|
641
|
+
maximum: number,
|
|
642
|
+
label: string,
|
|
643
|
+
): number {
|
|
644
|
+
const result = value ?? fallback;
|
|
645
|
+
if (!Number.isSafeInteger(result) || result < 0 || result > maximum) {
|
|
646
|
+
throw new RangeError(`${label} must be a safe integer from 0 through ${maximum}.`);
|
|
647
|
+
}
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function utf8Prefix(value: string, maximumBytes: number): {
|
|
652
|
+
readonly value: string;
|
|
653
|
+
readonly truncated: boolean;
|
|
654
|
+
} {
|
|
655
|
+
let bytes = 0;
|
|
656
|
+
let end = 0;
|
|
657
|
+
for (const character of value) {
|
|
658
|
+
const width = Buffer.byteLength(character, "utf8");
|
|
659
|
+
if (bytes + width > maximumBytes) return { value: value.slice(0, end), truncated: true };
|
|
660
|
+
bytes += width;
|
|
661
|
+
end += character.length;
|
|
662
|
+
}
|
|
663
|
+
return { value, truncated: false };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function boundedOptionalMetadata(metadata: MetadataObject, key: string): string | undefined {
|
|
667
|
+
const value = metadataString(metadata, key);
|
|
668
|
+
return value === undefined ? undefined : utf8Prefix(value, 1_024).value;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const statusOrder: ReadonlyMap<string, number> = new Map(
|
|
672
|
+
planStatuses.map((status, index) => [status, index]),
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
function compareCandidates(left: Candidate, right: Candidate): number {
|
|
676
|
+
const leftDate = left.classification.date ?? "";
|
|
677
|
+
const rightDate = right.classification.date ?? "";
|
|
678
|
+
return right.match.depth - left.match.depth
|
|
679
|
+
|| rightDate.localeCompare(leftDate)
|
|
680
|
+
|| (statusOrder.get(left.classification.status ?? "") ?? planStatuses.length)
|
|
681
|
+
- (statusOrder.get(right.classification.status ?? "") ?? planStatuses.length)
|
|
682
|
+
|| left.note.title.localeCompare(right.note.title)
|
|
683
|
+
|| left.note.path.localeCompare(right.note.path);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function boundedDetails<T>(values: readonly T[], limit: number): RepositoryMemoryDetailList<T> {
|
|
687
|
+
const details = values.slice(0, limit);
|
|
688
|
+
return {
|
|
689
|
+
total: values.length,
|
|
690
|
+
returned: details.length,
|
|
691
|
+
truncated: details.length < values.length,
|
|
692
|
+
details,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function invalidRecord(path: string, ...issues: string[]): RepositoryMemoryInvalidRecord {
|
|
697
|
+
return { path, issues };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
type ScopedRepositoryMemoryEntry = {
|
|
701
|
+
readonly note: Note;
|
|
702
|
+
readonly scopes: readonly string[];
|
|
703
|
+
readonly classification: RepositoryMemoryClassification;
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
type ScopedRepositoryMemoryAnalysis = {
|
|
707
|
+
readonly authoredRecords: number;
|
|
708
|
+
readonly entries: readonly ScopedRepositoryMemoryEntry[];
|
|
709
|
+
readonly records: readonly (ScopedRepositoryMemoryEntry & {
|
|
710
|
+
readonly classification: Extract<
|
|
711
|
+
RepositoryMemoryClassification,
|
|
712
|
+
{ readonly kind: "record" }
|
|
713
|
+
>;
|
|
714
|
+
})[];
|
|
715
|
+
readonly invalid: readonly RepositoryMemoryInvalidRecord[];
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
function addInvalidIssue(
|
|
719
|
+
issuesByPath: Map<string, string[]>,
|
|
720
|
+
path: string,
|
|
721
|
+
issues: readonly string[],
|
|
722
|
+
): void {
|
|
723
|
+
const existing = issuesByPath.get(path) ?? [];
|
|
724
|
+
existing.push(...issues);
|
|
725
|
+
issuesByPath.set(path, existing);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function analyzeScopedRepositoryMemory(
|
|
729
|
+
notes: readonly Note[],
|
|
730
|
+
): ScopedRepositoryMemoryAnalysis {
|
|
731
|
+
let authoredRecords = 0;
|
|
732
|
+
const entries: ScopedRepositoryMemoryEntry[] = [];
|
|
733
|
+
const issuesByPath = new Map<string, string[]>();
|
|
734
|
+
|
|
735
|
+
for (const note of notes) {
|
|
736
|
+
const scopes = analyzeAuthoredRepositoryScopes(note.metadata);
|
|
737
|
+
if (!scopes.present) continue;
|
|
738
|
+
authoredRecords += 1;
|
|
739
|
+
if (!scopes.valid) {
|
|
740
|
+
addInvalidIssue(
|
|
741
|
+
issuesByPath,
|
|
742
|
+
note.path,
|
|
743
|
+
scopes.issues.map(({ message }) => message),
|
|
744
|
+
);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
entries.push({
|
|
748
|
+
note,
|
|
749
|
+
scopes: scopes.scopes,
|
|
750
|
+
classification: classifyRepositoryMemoryRecord(note),
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const foldedDeclarations = new Map<string, Map<string, Set<string>>>();
|
|
755
|
+
for (const entry of entries) {
|
|
756
|
+
for (const scope of entry.scopes) {
|
|
757
|
+
const folded = scope.toLocaleLowerCase("en-US");
|
|
758
|
+
const declarations = foldedDeclarations.get(folded) ?? new Map<string, Set<string>>();
|
|
759
|
+
const paths = declarations.get(scope) ?? new Set<string>();
|
|
760
|
+
paths.add(entry.note.path);
|
|
761
|
+
declarations.set(scope, paths);
|
|
762
|
+
foldedDeclarations.set(folded, declarations);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
for (const declarations of foldedDeclarations.values()) {
|
|
766
|
+
if (declarations.size < 2) continue;
|
|
767
|
+
const scopes = [...declarations.keys()].toSorted((left, right) => left.localeCompare(right));
|
|
768
|
+
const displayedScopes = scopes.slice(0, MAX_REPOSITORY_SCOPES);
|
|
769
|
+
const scopeSummary = displayedScopes.join(", ")
|
|
770
|
+
+ (displayedScopes.length < scopes.length
|
|
771
|
+
? `, and ${scopes.length - displayedScopes.length} more`
|
|
772
|
+
: "");
|
|
773
|
+
for (const paths of declarations.values()) {
|
|
774
|
+
for (const path of paths) {
|
|
775
|
+
addInvalidIssue(
|
|
776
|
+
issuesByPath,
|
|
777
|
+
path,
|
|
778
|
+
[`Repository scopes ${scopeSummary} collide under case folding.`],
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
for (const entry of entries) {
|
|
785
|
+
if (entry.classification.kind === "invalid") {
|
|
786
|
+
addInvalidIssue(issuesByPath, entry.note.path, [entry.classification.reason]);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const invalid = [...issuesByPath]
|
|
791
|
+
.map(([path, issues]) => invalidRecord(path, ...issues))
|
|
792
|
+
.toSorted((left, right) => left.path.localeCompare(right.path));
|
|
793
|
+
const records = entries.filter((entry): entry is ScopedRepositoryMemoryEntry & {
|
|
794
|
+
readonly classification: Extract<
|
|
795
|
+
RepositoryMemoryClassification,
|
|
796
|
+
{ readonly kind: "record" }
|
|
797
|
+
>;
|
|
798
|
+
} => entry.classification.kind === "record" && !issuesByPath.has(entry.note.path));
|
|
799
|
+
return { authoredRecords, entries, records, invalid };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function inspectRepositoryScopesAtRoot(
|
|
803
|
+
root: string,
|
|
804
|
+
scopes: readonly string[],
|
|
805
|
+
): Promise<ReadonlyMap<string, RepositoryScopeFilesystemState>> {
|
|
806
|
+
const sorted = [...new Set(scopes)].toSorted((left, right) => left.localeCompare(right));
|
|
807
|
+
const states = new Map<string, RepositoryScopeFilesystemState>();
|
|
808
|
+
let nextIndex = 0;
|
|
809
|
+
const inspectNext = async (): Promise<void> => {
|
|
810
|
+
while (nextIndex < sorted.length) {
|
|
811
|
+
const index = nextIndex;
|
|
812
|
+
nextIndex += 1;
|
|
813
|
+
const scope = sorted[index];
|
|
814
|
+
if (scope === undefined) continue;
|
|
815
|
+
states.set(scope, await inspectScopeAtRoot(root, scope));
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
await Promise.all(
|
|
819
|
+
Array.from({ length: Math.min(8, sorted.length) }, () => inspectNext()),
|
|
820
|
+
);
|
|
821
|
+
return states;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** Build the bounded non-authoritative memory lane for one full repository path. */
|
|
825
|
+
export async function buildRepositoryMemoryContext(
|
|
826
|
+
notes: readonly Note[],
|
|
827
|
+
options: BuildRepositoryMemoryContextOptions,
|
|
828
|
+
): Promise<RepositoryMemoryContext> {
|
|
829
|
+
const groupLimit = checkedLimit(
|
|
830
|
+
options.groupLimit,
|
|
831
|
+
DEFAULT_REPOSITORY_MEMORY_GROUP_LIMIT,
|
|
832
|
+
MAX_REPOSITORY_MEMORY_GROUP_LIMIT,
|
|
833
|
+
"Repository-memory group limit",
|
|
834
|
+
);
|
|
835
|
+
const detailLimit = checkedLimit(
|
|
836
|
+
options.detailLimit,
|
|
837
|
+
DEFAULT_REPOSITORY_MEMORY_DETAIL_LIMIT,
|
|
838
|
+
MAX_REPOSITORY_MEMORY_DETAIL_LIMIT,
|
|
839
|
+
"Repository-memory detail limit",
|
|
840
|
+
);
|
|
841
|
+
const target = canonicalRepositoryPath(options.target, "Repository-memory target");
|
|
842
|
+
const repositoryRoot = await canonicalRepositoryRoot(options.repositoryRoot);
|
|
843
|
+
const targetState = await inspectScopeAtRoot(repositoryRoot, target);
|
|
844
|
+
const scoped = analyzeScopedRepositoryMemory(notes);
|
|
845
|
+
const invalid = [...scoped.invalid];
|
|
846
|
+
const candidates: Candidate[] = [];
|
|
847
|
+
for (const entry of scoped.records) {
|
|
848
|
+
const match = deepestRepositoryScopeMatch(entry.scopes, target);
|
|
849
|
+
if (match === null) continue;
|
|
850
|
+
candidates.push({
|
|
851
|
+
note: entry.note,
|
|
852
|
+
scopes: entry.scopes,
|
|
853
|
+
classification: entry.classification,
|
|
854
|
+
match,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const states = await inspectRepositoryScopesAtRoot(
|
|
859
|
+
repositoryRoot,
|
|
860
|
+
candidates.map(({ match }) => match.scope),
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
const accepted: Candidate[] = [];
|
|
864
|
+
const advisories: RepositoryMemoryAdvisory[] = [];
|
|
865
|
+
for (const candidate of candidates) {
|
|
866
|
+
const state = states.get(candidate.match.scope);
|
|
867
|
+
if (state === undefined) throw new Error("A matched repository scope was not inspected.");
|
|
868
|
+
if (state.status === "invalid") {
|
|
869
|
+
invalid.push(invalidRecord(
|
|
870
|
+
candidate.note.path,
|
|
871
|
+
`Repository scope ${state.scope} is invalid at ${state.path}: ${state.reason}.`,
|
|
872
|
+
));
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
if (state.status === "present" && state.kind === "file" && candidate.match.kind === "ancestor") {
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
accepted.push(candidate);
|
|
879
|
+
if (state.status === "absent" && candidate.classification.current) {
|
|
880
|
+
advisories.push({
|
|
881
|
+
kind: "absent-current-scope",
|
|
882
|
+
path: candidate.note.path,
|
|
883
|
+
scope: state.scope,
|
|
884
|
+
message: `Current repository memory ${candidate.note.path} declares absent scope ${state.scope}.`,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const groups = Object.create(null) as Record<RepositoryMemoryGroupKey, RepositoryMemoryGroup>;
|
|
890
|
+
for (const key of repositoryMemoryGroupKeys) {
|
|
891
|
+
const matches = accepted
|
|
892
|
+
.filter(({ classification }) => classification.group === key)
|
|
893
|
+
.toSorted(compareCandidates);
|
|
894
|
+
const selected = matches.slice(0, groupLimit);
|
|
895
|
+
const records = selected.map(({ note, scopes, classification, match }): RepositoryMemoryRecord => {
|
|
896
|
+
const state = states.get(match.scope);
|
|
897
|
+
if (state === undefined) throw new Error("A returned repository scope was not inspected.");
|
|
898
|
+
const summary = utf8Prefix(note.summary, MAX_REPOSITORY_MEMORY_SUMMARY_UTF8_BYTES);
|
|
899
|
+
const type = metadataString(note.metadata, "type");
|
|
900
|
+
if (type === undefined) throw new Error("A classified repository-memory record lost its type.");
|
|
901
|
+
const area = boundedOptionalMetadata(note.metadata, "area");
|
|
902
|
+
const description = boundedOptionalMetadata(note.metadata, "description");
|
|
903
|
+
return {
|
|
904
|
+
id: note.id,
|
|
905
|
+
path: note.path,
|
|
906
|
+
title: note.title,
|
|
907
|
+
summary: summary.value,
|
|
908
|
+
summaryTruncated: summary.truncated,
|
|
909
|
+
type,
|
|
910
|
+
...(classification.status === undefined ? {} : { status: classification.status }),
|
|
911
|
+
...(classification.date === undefined ? {} : { date: classification.date }),
|
|
912
|
+
...(area === undefined ? {} : { area }),
|
|
913
|
+
...(description === undefined ? {} : { description }),
|
|
914
|
+
repositoryScopes: scopes,
|
|
915
|
+
matchedScope: match.scope,
|
|
916
|
+
match: match.kind,
|
|
917
|
+
scopeState: state,
|
|
918
|
+
};
|
|
919
|
+
});
|
|
920
|
+
groups[key] = {
|
|
921
|
+
total: matches.length,
|
|
922
|
+
returned: records.length,
|
|
923
|
+
truncated: records.length < matches.length,
|
|
924
|
+
records,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
invalid.sort((left, right) => left.path.localeCompare(right.path));
|
|
929
|
+
advisories.sort((left, right) =>
|
|
930
|
+
left.path.localeCompare(right.path) || left.scope.localeCompare(right.scope));
|
|
931
|
+
const invalidRecords = boundedDetails(invalid, detailLimit);
|
|
932
|
+
const boundedAdvisories = boundedDetails(advisories, detailLimit);
|
|
933
|
+
const returned = repositoryMemoryGroupKeys.reduce(
|
|
934
|
+
(count, key) => count + groups[key].returned,
|
|
935
|
+
0,
|
|
936
|
+
);
|
|
937
|
+
return {
|
|
938
|
+
repositoryRoot,
|
|
939
|
+
target,
|
|
940
|
+
targetState,
|
|
941
|
+
groups,
|
|
942
|
+
counts: {
|
|
943
|
+
matched: accepted.length,
|
|
944
|
+
returned,
|
|
945
|
+
invalid: invalid.length,
|
|
946
|
+
advisories: advisories.length,
|
|
947
|
+
},
|
|
948
|
+
invalidRecords,
|
|
949
|
+
advisories: boundedAdvisories,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const MAX_REPOSITORY_MEMORY_STATE_RECORD_PATHS = 20;
|
|
954
|
+
|
|
955
|
+
export type RepositoryMemoryScopeAuditRecord = {
|
|
956
|
+
readonly path: string;
|
|
957
|
+
readonly repositoryScopes: readonly string[];
|
|
958
|
+
readonly classification: RepositoryMemoryClassification;
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
export type RepositoryMemoryScopeAuditState = {
|
|
962
|
+
readonly scope: string;
|
|
963
|
+
readonly state: RepositoryScopeFilesystemState;
|
|
964
|
+
readonly recordCount: number;
|
|
965
|
+
readonly recordPaths: readonly string[];
|
|
966
|
+
readonly recordPathsTruncated: boolean;
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
export type RepositoryMemoryScopeAuditError =
|
|
970
|
+
| {
|
|
971
|
+
readonly kind: "invalid-record";
|
|
972
|
+
readonly path: string;
|
|
973
|
+
readonly issues: readonly string[];
|
|
974
|
+
}
|
|
975
|
+
| {
|
|
976
|
+
readonly kind: "invalid-scope-state";
|
|
977
|
+
readonly scope: string;
|
|
978
|
+
readonly state: Extract<RepositoryScopeFilesystemState, { readonly status: "invalid" }>;
|
|
979
|
+
readonly recordCount: number;
|
|
980
|
+
readonly recordPaths: readonly string[];
|
|
981
|
+
readonly recordPathsTruncated: boolean;
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
export type RepositoryMemoryScopeAudit = {
|
|
985
|
+
readonly repositoryRoot: string;
|
|
986
|
+
readonly counts: {
|
|
987
|
+
readonly authoredRecords: number;
|
|
988
|
+
readonly validDeclarationRecords: number;
|
|
989
|
+
readonly classifiedRecords: number;
|
|
990
|
+
readonly currentRecords: number;
|
|
991
|
+
readonly terminalRecords: number;
|
|
992
|
+
readonly ignoredRecords: number;
|
|
993
|
+
readonly invalidRecords: number;
|
|
994
|
+
readonly distinctScopes: number;
|
|
995
|
+
readonly presentScopes: number;
|
|
996
|
+
readonly absentScopes: number;
|
|
997
|
+
readonly invalidScopes: number;
|
|
998
|
+
readonly errors: number;
|
|
999
|
+
readonly advisories: number;
|
|
1000
|
+
};
|
|
1001
|
+
readonly groups: Readonly<Record<RepositoryMemoryGroupKey, number>>;
|
|
1002
|
+
readonly records: RepositoryMemoryDetailList<RepositoryMemoryScopeAuditRecord>;
|
|
1003
|
+
readonly states: RepositoryMemoryDetailList<RepositoryMemoryScopeAuditState>;
|
|
1004
|
+
readonly errors: RepositoryMemoryDetailList<RepositoryMemoryScopeAuditError>;
|
|
1005
|
+
readonly advisories: RepositoryMemoryDetailList<RepositoryMemoryAdvisory>;
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
export type AuditRepositoryMemoryScopesOptions = {
|
|
1009
|
+
readonly repositoryRoot: string;
|
|
1010
|
+
readonly detailLimit?: number;
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
function scopeRecordPaths(
|
|
1014
|
+
pathsByScope: ReadonlyMap<string, ReadonlySet<string>>,
|
|
1015
|
+
scope: string,
|
|
1016
|
+
): {
|
|
1017
|
+
readonly recordCount: number;
|
|
1018
|
+
readonly recordPaths: readonly string[];
|
|
1019
|
+
readonly recordPathsTruncated: boolean;
|
|
1020
|
+
} {
|
|
1021
|
+
const paths = [...(pathsByScope.get(scope) ?? [])]
|
|
1022
|
+
.toSorted((left, right) => left.localeCompare(right));
|
|
1023
|
+
const recordPaths = paths.slice(0, MAX_REPOSITORY_MEMORY_STATE_RECORD_PATHS);
|
|
1024
|
+
return {
|
|
1025
|
+
recordCount: paths.length,
|
|
1026
|
+
recordPaths,
|
|
1027
|
+
recordPathsTruncated: recordPaths.length < paths.length,
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Audit every authored repository-memory scope against one repository snapshot.
|
|
1033
|
+
*
|
|
1034
|
+
* This whole-vault lane is read-only. It reports malformed declarations,
|
|
1035
|
+
* cross-record case collisions, invalid classifications, and unsafe filesystem
|
|
1036
|
+
* states as bounded errors. Absent paths remain valid authored history: only
|
|
1037
|
+
* current records receive an advisory, while terminal plans remain silent.
|
|
1038
|
+
*/
|
|
1039
|
+
export async function auditRepositoryMemoryScopes(
|
|
1040
|
+
notes: readonly Note[],
|
|
1041
|
+
options: AuditRepositoryMemoryScopesOptions,
|
|
1042
|
+
): Promise<RepositoryMemoryScopeAudit> {
|
|
1043
|
+
const detailLimit = checkedLimit(
|
|
1044
|
+
options.detailLimit,
|
|
1045
|
+
DEFAULT_REPOSITORY_MEMORY_DETAIL_LIMIT,
|
|
1046
|
+
MAX_REPOSITORY_MEMORY_DETAIL_LIMIT,
|
|
1047
|
+
"Repository-memory audit detail limit",
|
|
1048
|
+
);
|
|
1049
|
+
const repositoryRoot = await canonicalRepositoryRoot(options.repositoryRoot);
|
|
1050
|
+
const scoped = analyzeScopedRepositoryMemory(notes);
|
|
1051
|
+
const pathsByScope = new Map<string, Set<string>>();
|
|
1052
|
+
for (const entry of scoped.entries) {
|
|
1053
|
+
for (const scope of entry.scopes) {
|
|
1054
|
+
const paths = pathsByScope.get(scope) ?? new Set<string>();
|
|
1055
|
+
paths.add(entry.note.path);
|
|
1056
|
+
pathsByScope.set(scope, paths);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const statesByScope = await inspectRepositoryScopesAtRoot(
|
|
1060
|
+
repositoryRoot,
|
|
1061
|
+
[...pathsByScope.keys()],
|
|
1062
|
+
);
|
|
1063
|
+
const states = [...statesByScope]
|
|
1064
|
+
.map(([scope, state]): RepositoryMemoryScopeAuditState => ({
|
|
1065
|
+
scope,
|
|
1066
|
+
state,
|
|
1067
|
+
...scopeRecordPaths(pathsByScope, scope),
|
|
1068
|
+
}))
|
|
1069
|
+
.toSorted((left, right) => left.scope.localeCompare(right.scope));
|
|
1070
|
+
|
|
1071
|
+
const errors: RepositoryMemoryScopeAuditError[] = scoped.invalid.map((record) => ({
|
|
1072
|
+
kind: "invalid-record",
|
|
1073
|
+
path: record.path,
|
|
1074
|
+
issues: record.issues,
|
|
1075
|
+
}));
|
|
1076
|
+
for (const detail of states) {
|
|
1077
|
+
if (detail.state.status !== "invalid") continue;
|
|
1078
|
+
errors.push({
|
|
1079
|
+
kind: "invalid-scope-state",
|
|
1080
|
+
scope: detail.scope,
|
|
1081
|
+
state: detail.state,
|
|
1082
|
+
recordCount: detail.recordCount,
|
|
1083
|
+
recordPaths: detail.recordPaths,
|
|
1084
|
+
recordPathsTruncated: detail.recordPathsTruncated,
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
errors.sort((left, right) => {
|
|
1088
|
+
const leftPath = left.kind === "invalid-record" ? left.path : left.scope;
|
|
1089
|
+
const rightPath = right.kind === "invalid-record" ? right.path : right.scope;
|
|
1090
|
+
return left.kind.localeCompare(right.kind) || leftPath.localeCompare(rightPath);
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
const advisories: RepositoryMemoryAdvisory[] = [];
|
|
1094
|
+
for (const entry of scoped.records) {
|
|
1095
|
+
if (!entry.classification.current) continue;
|
|
1096
|
+
for (const scope of entry.scopes) {
|
|
1097
|
+
const state = statesByScope.get(scope);
|
|
1098
|
+
if (state?.status !== "absent") continue;
|
|
1099
|
+
advisories.push({
|
|
1100
|
+
kind: "absent-current-scope",
|
|
1101
|
+
path: entry.note.path,
|
|
1102
|
+
scope,
|
|
1103
|
+
message: `Current repository memory ${entry.note.path} declares absent scope ${scope}.`,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
advisories.sort((left, right) =>
|
|
1108
|
+
left.path.localeCompare(right.path) || left.scope.localeCompare(right.scope));
|
|
1109
|
+
|
|
1110
|
+
const groupCounts = Object.create(null) as Record<RepositoryMemoryGroupKey, number>;
|
|
1111
|
+
for (const key of repositoryMemoryGroupKeys) {
|
|
1112
|
+
groupCounts[key] = scoped.entries.filter(({ classification }) =>
|
|
1113
|
+
classification.kind === "record" && classification.group === key).length;
|
|
1114
|
+
}
|
|
1115
|
+
const records = scoped.entries
|
|
1116
|
+
.map((entry): RepositoryMemoryScopeAuditRecord => ({
|
|
1117
|
+
path: entry.note.path,
|
|
1118
|
+
repositoryScopes: entry.scopes,
|
|
1119
|
+
classification: entry.classification,
|
|
1120
|
+
}))
|
|
1121
|
+
.toSorted((left, right) => left.path.localeCompare(right.path));
|
|
1122
|
+
const classifications = scoped.entries.map(({ classification }) => classification);
|
|
1123
|
+
const classified = classifications.filter((classification) => classification.kind === "record");
|
|
1124
|
+
const presentScopes = states.filter(({ state }) => state.status === "present").length;
|
|
1125
|
+
const absentScopes = states.filter(({ state }) => state.status === "absent").length;
|
|
1126
|
+
const invalidScopes = states.filter(({ state }) => state.status === "invalid").length;
|
|
1127
|
+
return {
|
|
1128
|
+
repositoryRoot,
|
|
1129
|
+
counts: {
|
|
1130
|
+
authoredRecords: scoped.authoredRecords,
|
|
1131
|
+
validDeclarationRecords: scoped.entries.length,
|
|
1132
|
+
classifiedRecords: classified.length,
|
|
1133
|
+
currentRecords: classified.filter(({ current }) => current).length,
|
|
1134
|
+
terminalRecords: classified.filter(({ current }) => !current).length,
|
|
1135
|
+
ignoredRecords: classifications.filter(({ kind }) => kind === "ignored").length,
|
|
1136
|
+
invalidRecords: scoped.invalid.length,
|
|
1137
|
+
distinctScopes: states.length,
|
|
1138
|
+
presentScopes,
|
|
1139
|
+
absentScopes,
|
|
1140
|
+
invalidScopes,
|
|
1141
|
+
errors: errors.length,
|
|
1142
|
+
advisories: advisories.length,
|
|
1143
|
+
},
|
|
1144
|
+
groups: groupCounts,
|
|
1145
|
+
records: boundedDetails(records, detailLimit),
|
|
1146
|
+
states: boundedDetails(states, detailLimit),
|
|
1147
|
+
errors: boundedDetails(errors, detailLimit),
|
|
1148
|
+
advisories: boundedDetails(advisories, detailLimit),
|
|
1149
|
+
};
|
|
1150
|
+
}
|