@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.
Files changed (200) hide show
  1. package/DISCLOSURE +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +545 -0
  4. package/dist/agent-context.js +35 -0
  5. package/dist/agent-guide-audit.js +18 -0
  6. package/dist/attachments.js +19 -0
  7. package/dist/authoring.js +33 -0
  8. package/dist/benchmark.js +19 -0
  9. package/dist/capture.js +49 -0
  10. package/dist/cli.js +3646 -0
  11. package/dist/clip/acquire.js +57 -0
  12. package/dist/clip/args.js +21 -0
  13. package/dist/clip/bounded-byte-buffer.js +9 -0
  14. package/dist/clip/browser-profiles.js +17 -0
  15. package/dist/clip/bundle-reader.js +11 -0
  16. package/dist/clip/cli.js +27 -0
  17. package/dist/clip/cookies.js +21 -0
  18. package/dist/clip/defuddle-worker.js +84 -0
  19. package/dist/clip/doctor.js +32 -0
  20. package/dist/clip/jobs.js +784 -0
  21. package/dist/clip/network-proxy.js +9 -0
  22. package/dist/clip/network.js +28 -0
  23. package/dist/clip/persist.js +30 -0
  24. package/dist/clip/refresh.js +11 -0
  25. package/dist/clip/terminal.js +11 -0
  26. package/dist/evaluation-builder.js +12238 -0
  27. package/dist/evaluation-kb.js +26 -0
  28. package/dist/evaluation.js +35 -0
  29. package/dist/git.js +39 -0
  30. package/dist/graph.js +39 -0
  31. package/dist/index-01jj6rbv.js +1103 -0
  32. package/dist/index-06c9ctr6.js +738 -0
  33. package/dist/index-0kavxzqj.js +2480 -0
  34. package/dist/index-1gwbassd.js +743 -0
  35. package/dist/index-1n418kb9.js +827 -0
  36. package/dist/index-1vrd1rmn.js +56 -0
  37. package/dist/index-1xxnjn0d.js +171 -0
  38. package/dist/index-3rm7cz6h.js +373 -0
  39. package/dist/index-3v2z4f0q.js +328 -0
  40. package/dist/index-48pz4jpc.js +392 -0
  41. package/dist/index-4j3tt0c3.js +234 -0
  42. package/dist/index-5n05se68.js +307 -0
  43. package/dist/index-5vwpzb5a.js +764 -0
  44. package/dist/index-6g2pv9d2.js +370 -0
  45. package/dist/index-7fzc32gf.js +712 -0
  46. package/dist/index-84x0vjjp.js +432 -0
  47. package/dist/index-adx6khj5.js +488 -0
  48. package/dist/index-b88v3vtm.js +805 -0
  49. package/dist/index-cv6fh7z5.js +462 -0
  50. package/dist/index-cxfrakt7.js +1067 -0
  51. package/dist/index-d13v9ckt.js +150 -0
  52. package/dist/index-dycjbhw2.js +1943 -0
  53. package/dist/index-dyqwejk5.js +531 -0
  54. package/dist/index-e5fbsywq.js +658 -0
  55. package/dist/index-f984hw45.js +927 -0
  56. package/dist/index-gh719d91.js +63 -0
  57. package/dist/index-hgve9rh2.js +2046 -0
  58. package/dist/index-hya40gb2.js +474 -0
  59. package/dist/index-j4zgmzjr.js +152 -0
  60. package/dist/index-jsmvyyvf.js +1166 -0
  61. package/dist/index-kh0pwvdd.js +1974 -0
  62. package/dist/index-mqx4nd6v.js +119 -0
  63. package/dist/index-mxxxytys.js +1254 -0
  64. package/dist/index-n5dd7r0v.js +711 -0
  65. package/dist/index-npg9z1a4.js +431 -0
  66. package/dist/index-pj501bh1.js +140 -0
  67. package/dist/index-qry4vhxk.js +1041 -0
  68. package/dist/index-s2gw5aw9.js +199 -0
  69. package/dist/index-vvdj6kca.js +70 -0
  70. package/dist/index-w2zc0vwa.js +571 -0
  71. package/dist/index-x3fthpsc.js +585 -0
  72. package/dist/index-zr53sf63.js +44 -0
  73. package/dist/index-zxdy5pby.js +2728 -0
  74. package/dist/index-zzhgcwyt.js +966 -0
  75. package/dist/index.js +453 -0
  76. package/dist/navigation.js +13 -0
  77. package/dist/pdf.js +85 -0
  78. package/dist/percolate.js +24 -0
  79. package/dist/portfolio.js +77 -0
  80. package/dist/query.js +29 -0
  81. package/dist/repository-memory.js +60 -0
  82. package/dist/sdk.js +36 -0
  83. package/dist/search-rules.js +34 -0
  84. package/dist/search.js +22 -0
  85. package/dist/semantic.js +39 -0
  86. package/dist/source-inbox.js +17 -0
  87. package/dist/untrusted-content.js +32 -0
  88. package/dist/url-intelligence.js +74 -0
  89. package/dist/workflow.js +23 -0
  90. package/dist/workflows/decision-context.js +20 -0
  91. package/dist/workflows/explain-change.js +8 -0
  92. package/dist/workflows/index.js +28 -0
  93. package/dist/workflows/plan-radar.js +10 -0
  94. package/package.json +409 -0
  95. package/skills/kb/AGENTS.md +19 -0
  96. package/skills/kb/SKILL.md +82 -0
  97. package/skills/kb/agents/openai.yaml +4 -0
  98. package/skills/kb/references/pdf-review.md +51 -0
  99. package/skills/kb/references/percolate.md +144 -0
  100. package/skills/kb/references/plan-structure.md +105 -0
  101. package/skills/kb/references/plan.md +115 -0
  102. package/skills/kb/references/query.md +231 -0
  103. package/skills/kb/references/refresh.md +133 -0
  104. package/skills/kb/references/save-pdf.md +125 -0
  105. package/skills/kb/references/save-url.md +178 -0
  106. package/skills/kb/references/url-authentication.md +75 -0
  107. package/skills/kb/references/url-platforms.md +25 -0
  108. package/src/agent-context.ts +1241 -0
  109. package/src/agent-guide-audit.ts +750 -0
  110. package/src/attachments.ts +723 -0
  111. package/src/authoring.ts +1725 -0
  112. package/src/benchmark.ts +362 -0
  113. package/src/capture.ts +68 -0
  114. package/src/cli.ts +3660 -0
  115. package/src/clip/acquire.ts +1273 -0
  116. package/src/clip/archive-today.ts +402 -0
  117. package/src/clip/args.ts +423 -0
  118. package/src/clip/assets.ts +262 -0
  119. package/src/clip/bounded-byte-buffer.ts +75 -0
  120. package/src/clip/browser-profiles.ts +372 -0
  121. package/src/clip/bundle-reader.ts +608 -0
  122. package/src/clip/capture.ts +1054 -0
  123. package/src/clip/cli.ts +181 -0
  124. package/src/clip/cookies.ts +512 -0
  125. package/src/clip/doctor.ts +1110 -0
  126. package/src/clip/extract.ts +868 -0
  127. package/src/clip/jobs.ts +959 -0
  128. package/src/clip/lib.ts +861 -0
  129. package/src/clip/media.ts +1474 -0
  130. package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
  131. package/src/clip/metadata-search-tool/Cargo.toml +18 -0
  132. package/src/clip/metadata-search-tool/runner.ts +685 -0
  133. package/src/clip/metadata-search-tool/src/main.rs +389 -0
  134. package/src/clip/metadata-search.ts +687 -0
  135. package/src/clip/network-proxy.ts +627 -0
  136. package/src/clip/network.ts +950 -0
  137. package/src/clip/package-root.ts +54 -0
  138. package/src/clip/persist.ts +1472 -0
  139. package/src/clip/platforms.ts +1824 -0
  140. package/src/clip/refresh.ts +197 -0
  141. package/src/clip/structured.ts +432 -0
  142. package/src/clip/terminal.ts +197 -0
  143. package/src/clip/url-intelligence.ts +581 -0
  144. package/src/clip/url-metadata-backfill.ts +616 -0
  145. package/src/clip/url-metadata-cli.ts +251 -0
  146. package/src/clip/url-metadata.ts +1397 -0
  147. package/src/evaluation-analysis-v2.ts +1440 -0
  148. package/src/evaluation-builder.ts +2539 -0
  149. package/src/evaluation-corpus-authoring.ts +2052 -0
  150. package/src/evaluation-evidence.ts +1147 -0
  151. package/src/evaluation-execution-v2.ts +426 -0
  152. package/src/evaluation-implementation.ts +131 -0
  153. package/src/evaluation-kb-closure.ts +1430 -0
  154. package/src/evaluation-kb-runner-v2.ts +735 -0
  155. package/src/evaluation-kb-v2.ts +2567 -0
  156. package/src/evaluation-kb.ts +1018 -0
  157. package/src/evaluation-measurement-v2.ts +1462 -0
  158. package/src/evaluation-packing-v2.ts +452 -0
  159. package/src/evaluation-redaction.ts +28 -0
  160. package/src/evaluation-v2.ts +3857 -0
  161. package/src/evaluation.ts +1220 -0
  162. package/src/git.ts +1276 -0
  163. package/src/graph.ts +1487 -0
  164. package/src/index.ts +20 -0
  165. package/src/init.ts +123 -0
  166. package/src/navigation.ts +277 -0
  167. package/src/note-lock.ts +529 -0
  168. package/src/pdf/args.ts +202 -0
  169. package/src/pdf/capture.ts +298 -0
  170. package/src/pdf/cli.ts +159 -0
  171. package/src/pdf/extract.ts +303 -0
  172. package/src/pdf/layout.ts +618 -0
  173. package/src/pdf/markdown.ts +154 -0
  174. package/src/pdf/model.ts +234 -0
  175. package/src/pdf/ocr.ts +190 -0
  176. package/src/pdf/persist.ts +314 -0
  177. package/src/pdf/source.ts +135 -0
  178. package/src/pdf/tools.ts +146 -0
  179. package/src/pdf.ts +92 -0
  180. package/src/percolate.ts +877 -0
  181. package/src/portfolio-audit.ts +574 -0
  182. package/src/portfolio-identity.ts +162 -0
  183. package/src/portfolio-registry.ts +473 -0
  184. package/src/portfolio.ts +765 -0
  185. package/src/query.ts +646 -0
  186. package/src/repository-memory.ts +1150 -0
  187. package/src/sdk.ts +1452 -0
  188. package/src/search-rules.ts +712 -0
  189. package/src/search.ts +710 -0
  190. package/src/semantic-runtime.ts +1139 -0
  191. package/src/semantic.ts +2480 -0
  192. package/src/source-inbox.ts +232 -0
  193. package/src/untrusted-content.ts +401 -0
  194. package/src/url-intelligence.ts +104 -0
  195. package/src/vault.ts +636 -0
  196. package/src/workflow.ts +653 -0
  197. package/src/workflows/decision-context.ts +81 -0
  198. package/src/workflows/explain-change.ts +73 -0
  199. package/src/workflows/index.ts +3 -0
  200. package/src/workflows/plan-radar.ts +101 -0
@@ -0,0 +1,764 @@
1
+ // @bun
2
+ // src/agent-context.ts
3
+ import { createHash } from "crypto";
4
+ import { constants } from "fs";
5
+ import {
6
+ lstat,
7
+ open,
8
+ realpath
9
+ } from "fs/promises";
10
+ import {
11
+ isAbsolute,
12
+ join,
13
+ posix,
14
+ relative,
15
+ resolve,
16
+ sep
17
+ } from "path";
18
+ var agentContextType = "agent-context";
19
+ var agentContextDirectory = "scopes";
20
+ var agentContextSlugMaximumLength = 48;
21
+ var agentContextHashLength = 12;
22
+ var drivePathPattern = /^[A-Za-z]:/u;
23
+ var globCharacterPattern = /[*?[\]{}]/u;
24
+ var canonicalContextIdPattern = /^scopes\/([a-z0-9]+(?:-[a-z0-9]+)*)--([0-9a-f]{12})$/u;
25
+ function hasControlCharacter(value) {
26
+ for (const character of value) {
27
+ const codePoint = character.codePointAt(0);
28
+ if (codePoint !== undefined && (codePoint <= 31 || codePoint >= 127 && codePoint <= 159)) {
29
+ return true;
30
+ }
31
+ }
32
+ return false;
33
+ }
34
+
35
+ class RepositoryScopeError extends Error {
36
+ code;
37
+ input;
38
+ constructor(code, input, message) {
39
+ super(message);
40
+ this.name = "RepositoryScopeError";
41
+ this.code = code;
42
+ this.input = input;
43
+ }
44
+ }
45
+ function normalizedRepositoryPath(input, unicodeForm) {
46
+ if (input === "" || input.trim() === "") {
47
+ throw new RepositoryScopeError("empty", input, "A repository scope must not be empty.");
48
+ }
49
+ if (hasControlCharacter(input)) {
50
+ throw new RepositoryScopeError("control-character", input, "A repository scope must not contain control characters.");
51
+ }
52
+ if (/^[\\/]/u.test(input) || drivePathPattern.test(input)) {
53
+ throw new RepositoryScopeError("absolute", input, "A repository scope must be relative, not absolute, UNC, or drive-qualified.");
54
+ }
55
+ if (globCharacterPattern.test(input)) {
56
+ throw new RepositoryScopeError("glob", input, "A repository scope must name one exact directory, not a glob.");
57
+ }
58
+ const separated = input.replaceAll("\\", "/");
59
+ if (separated.split("/").includes("..")) {
60
+ throw new RepositoryScopeError("traversal", input, "A repository scope must not contain parent traversal.");
61
+ }
62
+ const unicodeNormalized = unicodeForm === null ? separated : separated.normalize(unicodeForm);
63
+ const normalized = posix.normalize(unicodeNormalized).replace(/^\.\//u, "");
64
+ if (normalized === ".." || normalized.startsWith("../") || posix.isAbsolute(normalized)) {
65
+ throw new RepositoryScopeError("traversal", input, "A repository scope must stay within the repository.");
66
+ }
67
+ return normalized === "" ? "." : normalized;
68
+ }
69
+ function normalizeRepositoryScope(input) {
70
+ return normalizedRepositoryPath(input, "NFC");
71
+ }
72
+ function readableScopeSlug(scope) {
73
+ if (scope === ".")
74
+ return "repository";
75
+ const readable = scope.normalize("NFKD").replace(/\p{Mark}+/gu, "").replace(/[^\p{ASCII}]/gu, "-").toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "").replace(/-+/gu, "-");
76
+ const fallback = readable === "" ? "scope" : readable;
77
+ return fallback.slice(0, agentContextSlugMaximumLength).replace(/-+$/gu, "") || "scope";
78
+ }
79
+ function scopeHash(scope) {
80
+ return createHash("sha256").update(scope, "utf8").digest("hex").slice(0, agentContextHashLength);
81
+ }
82
+ function agentContextNoteId(scopeInput) {
83
+ const scope = normalizeRepositoryScope(scopeInput);
84
+ return `${agentContextDirectory}/${readableScopeSlug(scope)}--${scopeHash(scope)}`;
85
+ }
86
+ function agentContextNotePath(scopeInput) {
87
+ return `${agentContextNoteId(scopeInput)}.md`;
88
+ }
89
+ function agentContextGuidePath(scopeInput) {
90
+ const scope = normalizeRepositoryScope(scopeInput);
91
+ return scope === "." ? "AGENTS.md" : `${scope}/AGENTS.md`;
92
+ }
93
+ function isCanonicalContextId(value) {
94
+ const match = canonicalContextIdPattern.exec(value);
95
+ const slug = match?.[1];
96
+ return slug !== undefined && slug.length <= agentContextSlugMaximumLength;
97
+ }
98
+ function formatAgentContextMarker(noteId) {
99
+ if (!isCanonicalContextId(noteId)) {
100
+ throw new TypeError("An agent-context marker requires a canonical context note ID.");
101
+ }
102
+ return `<!-- kb:context ${noteId} -->`;
103
+ }
104
+ function agentContextMarkerForScope(scopeInput) {
105
+ return formatAgentContextMarker(agentContextNoteId(scopeInput));
106
+ }
107
+ function fenceDelimiter(line) {
108
+ const match = /^\s{0,3}(`{3,}|~{3,})/u.exec(line);
109
+ const delimiter = match?.[1];
110
+ if (delimiter === undefined)
111
+ return null;
112
+ const character = delimiter[0];
113
+ return character === "`" || character === "~" ? { character, length: delimiter.length } : null;
114
+ }
115
+ function parseAgentContextMarker(source) {
116
+ const markers = [];
117
+ const malformed = [];
118
+ let firstHeadingLine = null;
119
+ let fence = null;
120
+ const lines = source.split(/\r?\n/u);
121
+ for (let index = 0;index < lines.length; index += 1) {
122
+ const line = lines[index] ?? "";
123
+ const delimiter = fenceDelimiter(line);
124
+ if (fence !== null) {
125
+ if (delimiter?.character === fence.character && delimiter.length >= fence.length) {
126
+ fence = null;
127
+ }
128
+ continue;
129
+ }
130
+ if (delimiter !== null) {
131
+ fence = delimiter;
132
+ continue;
133
+ }
134
+ const lineNumber = index + 1;
135
+ if (firstHeadingLine === null && /^\s{0,3}#{1,6}(?:\s+|$)/u.test(line)) {
136
+ firstHeadingLine = lineNumber;
137
+ }
138
+ if (!/<!--[^>]*\bkb:context\b/u.test(line))
139
+ continue;
140
+ const match = /^<!-- kb:context (scopes\/[a-z0-9]+(?:-[a-z0-9]+)*--[0-9a-f]{12}) -->$/u.exec(line);
141
+ const noteId = match?.[1];
142
+ if (noteId === undefined || !isCanonicalContextId(noteId)) {
143
+ malformed.push({ line: lineNumber, source: line, reason: "syntax" });
144
+ continue;
145
+ }
146
+ if (firstHeadingLine !== null && lineNumber > firstHeadingLine) {
147
+ malformed.push({ line: lineNumber, source: line, reason: "after-heading" });
148
+ continue;
149
+ }
150
+ markers.push({ noteId, line: lineNumber, source: line });
151
+ }
152
+ const kind = malformed.length > 0 ? "malformed" : markers.length === 0 ? "missing" : markers.length === 1 ? "found" : "multiple";
153
+ return { kind, markers, malformed };
154
+ }
155
+ function noteIsUnderContextDirectory(note) {
156
+ return note.path.startsWith(`${agentContextDirectory}/`);
157
+ }
158
+ function normalizedGuideSource(guide) {
159
+ const path = normalizeRepositoryScope(guide.path);
160
+ if (posix.basename(path) !== "AGENTS.md") {
161
+ throw new TypeError("An agent guide source path must end in AGENTS.md.");
162
+ }
163
+ const parent = posix.dirname(path);
164
+ return {
165
+ path,
166
+ scope: parent === "" ? "." : normalizeRepositoryScope(parent),
167
+ source: guide.source
168
+ };
169
+ }
170
+ function issueSortKey(issue) {
171
+ const record = issue;
172
+ return [
173
+ issue.kind,
174
+ typeof record.notePath === "string" ? record.notePath : "",
175
+ typeof record.guidePath === "string" ? record.guidePath : "",
176
+ typeof record.scope === "string" ? record.scope : "",
177
+ issue.message
178
+ ].join("\x00");
179
+ }
180
+ function sortedIssues(issues) {
181
+ return [...issues].toSorted((left, right) => issueSortKey(left).localeCompare(issueSortKey(right)));
182
+ }
183
+ function sortedUnique(values) {
184
+ return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
185
+ }
186
+ function groupBy(values, keyOf) {
187
+ const groups = new Map;
188
+ for (const value of values) {
189
+ const key = keyOf(value);
190
+ const group = groups.get(key) ?? [];
191
+ group.push(value);
192
+ groups.set(key, group);
193
+ }
194
+ return groups;
195
+ }
196
+ function scopeDepth(scope) {
197
+ return scope === "." ? 0 : scope.split("/").length;
198
+ }
199
+ function scopeIsAncestor(ancestor, descendant) {
200
+ return ancestor === "." || ancestor === descendant || descendant.startsWith(`${ancestor}/`);
201
+ }
202
+ function analyzeAgentContexts(notes, guideSources = []) {
203
+ const issues = [];
204
+ const candidates = [];
205
+ for (const [index, note] of notes.entries()) {
206
+ const underScopes = noteIsUnderContextDirectory(note);
207
+ const hasType = Object.hasOwn(note.metadata, "type");
208
+ const type = note.metadata.type;
209
+ if (underScopes && type !== agentContextType) {
210
+ issues.push({
211
+ kind: "non-context-note-under-scopes",
212
+ notePath: note.path,
213
+ message: `The note ${note.path} is under scopes/ but is not an agent-context note.`
214
+ });
215
+ if (hasType) {
216
+ issues.push({
217
+ kind: "malformed-context-type",
218
+ notePath: note.path,
219
+ actual: type,
220
+ message: `The note ${note.path} has an invalid agent-context type.`
221
+ });
222
+ }
223
+ continue;
224
+ }
225
+ if (type !== agentContextType)
226
+ continue;
227
+ if (!underScopes) {
228
+ issues.push({
229
+ kind: "context-note-outside-scopes",
230
+ notePath: note.path,
231
+ message: `The agent-context note ${note.path} must live under scopes/.`
232
+ });
233
+ }
234
+ const rawScope = note.metadata.scope;
235
+ if (typeof rawScope !== "string") {
236
+ issues.push({
237
+ kind: "malformed-context-scope",
238
+ notePath: note.path,
239
+ actual: rawScope,
240
+ reason: "The scope must be one string.",
241
+ message: `The agent-context note ${note.path} must declare one string scope.`
242
+ });
243
+ continue;
244
+ }
245
+ let scope;
246
+ let preNfcScope;
247
+ try {
248
+ scope = normalizeRepositoryScope(rawScope);
249
+ preNfcScope = normalizedRepositoryPath(rawScope, null);
250
+ } catch (error) {
251
+ const reason = error instanceof Error ? error.message : "The scope is invalid.";
252
+ issues.push({
253
+ kind: "malformed-context-scope",
254
+ notePath: note.path,
255
+ actual: rawScope,
256
+ reason,
257
+ message: `The agent-context note ${note.path} has an invalid repository scope.`
258
+ });
259
+ continue;
260
+ }
261
+ const canonicalId = agentContextNoteId(scope);
262
+ const canonicalPath = `${canonicalId}.md`;
263
+ const canonical = note.id === canonicalId && note.path === canonicalPath;
264
+ if (!canonical) {
265
+ issues.push({
266
+ kind: "noncanonical-context-note",
267
+ notePath: note.path,
268
+ noteId: note.id,
269
+ scope,
270
+ expectedPath: canonicalPath,
271
+ expectedId: canonicalId,
272
+ message: `The agent-context note for ${scope} must be ${canonicalPath}.`
273
+ });
274
+ }
275
+ candidates.push({
276
+ index,
277
+ note,
278
+ rawScope,
279
+ preNfcScope,
280
+ scope,
281
+ canonicalId,
282
+ canonicalPath,
283
+ guidePath: agentContextGuidePath(scope),
284
+ marker: formatAgentContextMarker(canonicalId),
285
+ underScopes,
286
+ canonical
287
+ });
288
+ }
289
+ const conflictedCandidateIndexes = new Set;
290
+ const byScope = groupBy(candidates, (candidate) => candidate.scope);
291
+ for (const [scope, matches] of byScope) {
292
+ if (matches.length < 2)
293
+ continue;
294
+ for (const match of matches)
295
+ conflictedCandidateIndexes.add(match.index);
296
+ const preNfcScopes = sortedUnique(matches.map((match) => match.preNfcScope));
297
+ const notePaths = matches.map((match) => match.note.path).toSorted((left, right) => left.localeCompare(right));
298
+ if (preNfcScopes.length > 1) {
299
+ issues.push({
300
+ kind: "nfc-context-scope-collision",
301
+ scope,
302
+ rawScopes: sortedUnique(matches.map((match) => match.rawScope)),
303
+ notePaths,
304
+ message: `Multiple context scopes normalize to the same NFC directory ${scope}.`
305
+ });
306
+ } else {
307
+ issues.push({
308
+ kind: "duplicate-context-scope",
309
+ scope,
310
+ notePaths,
311
+ message: `The repository scope ${scope} has more than one context note.`
312
+ });
313
+ }
314
+ }
315
+ const byCaseFoldedScope = groupBy(candidates, (candidate) => candidate.scope.toLocaleLowerCase("en-US"));
316
+ for (const matches of byCaseFoldedScope.values()) {
317
+ const scopes = sortedUnique(matches.map((match) => match.scope));
318
+ if (scopes.length < 2)
319
+ continue;
320
+ for (const match of matches)
321
+ conflictedCandidateIndexes.add(match.index);
322
+ issues.push({
323
+ kind: "case-fold-context-scope-collision",
324
+ scopes,
325
+ notePaths: matches.map((match) => match.note.path).toSorted((left, right) => left.localeCompare(right)),
326
+ message: `Context scopes ${scopes.join(", ")} collide under case folding.`
327
+ });
328
+ }
329
+ const guides = [];
330
+ for (const guideSource of guideSources) {
331
+ try {
332
+ const guide = normalizedGuideSource(guideSource);
333
+ guides.push({ ...guide, marker: parseAgentContextMarker(guide.source) });
334
+ } catch (error) {
335
+ issues.push({
336
+ kind: "invalid-guide-path",
337
+ guidePath: guideSource.path,
338
+ reason: error instanceof Error ? error.message : "The guide path is invalid.",
339
+ message: `The guide source path ${guideSource.path} is invalid.`
340
+ });
341
+ }
342
+ }
343
+ guides.sort((left, right) => left.path.localeCompare(right.path));
344
+ const mappableCandidates = candidates.filter((candidate) => candidate.underScopes && candidate.canonical);
345
+ const mappableByScope = groupBy(mappableCandidates, (candidate) => candidate.scope);
346
+ const notesById = groupBy(notes, (note) => note.id);
347
+ const reciprocalCandidateIndexes = new Set;
348
+ for (const guide of guides) {
349
+ const scopedCandidates = mappableByScope.get(guide.scope) ?? [];
350
+ if (guide.marker.kind === "missing" && scopedCandidates.length > 0) {
351
+ issues.push({
352
+ kind: "guide-marker-missing",
353
+ guidePath: guide.path,
354
+ scope: guide.scope,
355
+ message: `The mapped guide ${guide.path} is missing its kb:context marker.`
356
+ });
357
+ }
358
+ if (guide.marker.markers.length > 1) {
359
+ issues.push({
360
+ kind: "guide-marker-multiple",
361
+ guidePath: guide.path,
362
+ lines: guide.marker.markers.map((marker2) => marker2.line),
363
+ message: `The guide ${guide.path} has more than one kb:context marker.`
364
+ });
365
+ }
366
+ if (guide.marker.malformed.length > 0) {
367
+ issues.push({
368
+ kind: "guide-marker-malformed",
369
+ guidePath: guide.path,
370
+ lines: guide.marker.malformed.map((marker2) => marker2.line),
371
+ message: `The guide ${guide.path} has a malformed kb:context marker.`
372
+ });
373
+ }
374
+ if (guide.marker.markers.length !== 1 || guide.marker.malformed.length !== 0) {
375
+ continue;
376
+ }
377
+ const marker = guide.marker.markers[0];
378
+ if (marker === undefined)
379
+ continue;
380
+ const pointedNotes = notesById.get(marker.noteId) ?? [];
381
+ if (pointedNotes.length === 0) {
382
+ issues.push({
383
+ kind: "guide-pointer-missing",
384
+ guidePath: guide.path,
385
+ noteId: marker.noteId,
386
+ message: `The guide ${guide.path} points to missing note ${marker.noteId}.`
387
+ });
388
+ continue;
389
+ }
390
+ const pointedCandidates = mappableCandidates.filter((candidate) => candidate.canonicalId === marker.noteId && pointedNotes.includes(candidate.note));
391
+ const reciprocal = pointedCandidates.filter((candidate) => candidate.scope === guide.scope && candidate.canonicalId === agentContextNoteId(guide.scope));
392
+ if (reciprocal.length === 0) {
393
+ issues.push({
394
+ kind: "guide-pointer-mismatch",
395
+ guidePath: guide.path,
396
+ scope: guide.scope,
397
+ noteId: marker.noteId,
398
+ expectedId: agentContextNoteId(guide.scope),
399
+ actualNotePaths: pointedNotes.map((note) => note.path).toSorted((left, right) => left.localeCompare(right)),
400
+ message: `The guide ${guide.path} does not point to the context note for ${guide.scope}.`
401
+ });
402
+ continue;
403
+ }
404
+ for (const candidate of reciprocal) {
405
+ reciprocalCandidateIndexes.add(candidate.index);
406
+ }
407
+ }
408
+ for (const candidate of mappableCandidates) {
409
+ if (reciprocalCandidateIndexes.has(candidate.index))
410
+ continue;
411
+ issues.push({
412
+ kind: "context-note-missing-reciprocal-marker",
413
+ notePath: candidate.note.path,
414
+ scope: candidate.scope,
415
+ guidePath: candidate.guidePath,
416
+ expectedMarker: candidate.marker,
417
+ message: `The context note ${candidate.note.path} is missing a reciprocal marker in ${candidate.guidePath}.`
418
+ });
419
+ }
420
+ const contexts = candidates.map((candidate) => {
421
+ const reciprocal = reciprocalCandidateIndexes.has(candidate.index);
422
+ const canonical = candidate.underScopes && candidate.canonical;
423
+ return {
424
+ note: candidate.note,
425
+ rawScope: candidate.rawScope,
426
+ scope: candidate.scope,
427
+ canonicalId: candidate.canonicalId,
428
+ canonicalPath: candidate.canonicalPath,
429
+ guidePath: candidate.guidePath,
430
+ marker: candidate.marker,
431
+ canonical,
432
+ reciprocal,
433
+ valid: canonical && reciprocal && !conflictedCandidateIndexes.has(candidate.index)
434
+ };
435
+ }).toSorted((left, right) => left.scope.localeCompare(right.scope) || left.note.path.localeCompare(right.note.path));
436
+ return {
437
+ contexts,
438
+ guides,
439
+ issues: sortedIssues(issues)
440
+ };
441
+ }
442
+
443
+ class AgentContextRepositoryPathError extends Error {
444
+ code;
445
+ path;
446
+ constructor(code, path, message) {
447
+ super(message);
448
+ this.name = "AgentContextRepositoryPathError";
449
+ this.code = code;
450
+ this.path = path;
451
+ }
452
+ }
453
+ function isMissingFileError(error) {
454
+ return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
455
+ }
456
+ function isUnresolvableSymlinkError(error) {
457
+ return isMissingFileError(error) || error !== null && typeof error === "object" && "code" in error && error.code === "ELOOP";
458
+ }
459
+ async function realpathIfPresent(path) {
460
+ try {
461
+ return await realpath(path);
462
+ } catch (error) {
463
+ if (isUnresolvableSymlinkError(error))
464
+ return null;
465
+ throw error;
466
+ }
467
+ }
468
+ function pathIsWithin(root, candidate) {
469
+ const fromRoot = relative(root, candidate);
470
+ return fromRoot === "" || fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot);
471
+ }
472
+ function repositoryPath(root, scope) {
473
+ return scope === "." ? root : join(root, ...scope.split("/"));
474
+ }
475
+ function repositoryRelativePath(root, path) {
476
+ const fromRoot = relative(root, path).split(sep).join("/");
477
+ return fromRoot === "" ? "." : fromRoot;
478
+ }
479
+ async function assertTargetPrefixConfined(root, normalizedTarget) {
480
+ if (normalizedTarget === ".")
481
+ return;
482
+ const segments = normalizedTarget.split("/");
483
+ let current = root;
484
+ for (const [index, segment] of segments.entries()) {
485
+ const next = join(current, segment);
486
+ let metadata;
487
+ try {
488
+ metadata = await lstat(next);
489
+ } catch (error) {
490
+ if (isMissingFileError(error))
491
+ return;
492
+ throw error;
493
+ }
494
+ if (metadata.isSymbolicLink()) {
495
+ const resolvedPath = await realpathIfPresent(next);
496
+ if (resolvedPath !== null && !pathIsWithin(root, resolvedPath)) {
497
+ throw new AgentContextRepositoryPathError("target-symlink-escape", repositoryRelativePath(root, next), `The target traverses a symbolic link outside the repository: ${next}.`);
498
+ }
499
+ throw new AgentContextRepositoryPathError("target-symlink", repositoryRelativePath(root, next), `The target traverses a symbolic link: ${next}.`);
500
+ }
501
+ if (index < segments.length - 1 && !metadata.isDirectory()) {
502
+ throw new AgentContextRepositoryPathError("target-parent-not-directory", repositoryRelativePath(root, next), `A target parent is not a directory: ${next}.`);
503
+ }
504
+ current = next;
505
+ }
506
+ }
507
+ async function targetScope(root, target, kind, directoryHint) {
508
+ if (kind === "directory")
509
+ return target;
510
+ if (kind === "file") {
511
+ const parent = posix.dirname(target);
512
+ return parent === "" ? "." : parent;
513
+ }
514
+ const absoluteTarget = repositoryPath(root, target);
515
+ try {
516
+ const metadata = await lstat(absoluteTarget);
517
+ if (metadata.isDirectory())
518
+ return target;
519
+ const parent = posix.dirname(target);
520
+ return parent === "" ? "." : parent;
521
+ } catch (error) {
522
+ if (!isMissingFileError(error))
523
+ throw error;
524
+ }
525
+ if (directoryHint)
526
+ return target;
527
+ const basename = posix.basename(target);
528
+ if (basename.includes(".")) {
529
+ const parent = posix.dirname(target);
530
+ return parent === "" ? "." : parent;
531
+ }
532
+ return target;
533
+ }
534
+ function scopeAncestors(scope) {
535
+ if (scope === ".")
536
+ return ["."];
537
+ const segments = scope.split("/");
538
+ const ancestors = ["."];
539
+ for (let length = 1;length <= segments.length; length += 1) {
540
+ ancestors.push(segments.slice(0, length).join("/"));
541
+ }
542
+ return ancestors;
543
+ }
544
+ async function inspectScopeDirectory(root, scope) {
545
+ const path = repositoryPath(root, scope);
546
+ const issues = [];
547
+ if (scope === ".")
548
+ return { path, issues };
549
+ let current = root;
550
+ for (const segment of scope.split("/")) {
551
+ current = join(current, segment);
552
+ let metadata;
553
+ try {
554
+ metadata = await lstat(current);
555
+ } catch (error) {
556
+ if (!isMissingFileError(error))
557
+ throw error;
558
+ issues.push({
559
+ kind: "scope-directory-missing",
560
+ scope,
561
+ repositoryPath: repositoryRelativePath(root, current),
562
+ message: `The mapped scope directory ${repositoryRelativePath(root, current)} does not exist.`
563
+ });
564
+ return { path, issues };
565
+ }
566
+ if (metadata.isSymbolicLink()) {
567
+ issues.push({
568
+ kind: "scope-directory-symlink",
569
+ scope,
570
+ repositoryPath: repositoryRelativePath(root, current),
571
+ message: `The mapped scope directory ${repositoryRelativePath(root, current)} is a symbolic link.`
572
+ });
573
+ const resolvedPath2 = await realpathIfPresent(current);
574
+ if (resolvedPath2 !== null && !pathIsWithin(root, resolvedPath2)) {
575
+ issues.push({
576
+ kind: "repository-symlink-escape",
577
+ scope,
578
+ repositoryPath: repositoryRelativePath(root, current),
579
+ resolvedPath: resolvedPath2,
580
+ message: `The mapped scope directory ${repositoryRelativePath(root, current)} resolves outside the repository.`
581
+ });
582
+ }
583
+ return { path, issues };
584
+ }
585
+ if (!metadata.isDirectory()) {
586
+ issues.push({
587
+ kind: "scope-directory-not-directory",
588
+ scope,
589
+ repositoryPath: repositoryRelativePath(root, current),
590
+ message: `The mapped scope path ${repositoryRelativePath(root, current)} is not a directory.`
591
+ });
592
+ return { path, issues };
593
+ }
594
+ }
595
+ const resolvedPath = await realpath(path);
596
+ if (!pathIsWithin(root, resolvedPath)) {
597
+ issues.push({
598
+ kind: "repository-symlink-escape",
599
+ scope,
600
+ repositoryPath: repositoryRelativePath(root, path),
601
+ resolvedPath,
602
+ message: `The mapped scope directory ${repositoryRelativePath(root, path)} resolves outside the repository.`
603
+ });
604
+ }
605
+ return { path, issues };
606
+ }
607
+ async function readGuide(root, scope, required) {
608
+ const absolutePath = join(repositoryPath(root, scope), "AGENTS.md");
609
+ const path = agentContextGuidePath(scope);
610
+ const issues = [];
611
+ let metadata;
612
+ try {
613
+ metadata = await lstat(absolutePath);
614
+ } catch (error) {
615
+ if (!isMissingFileError(error))
616
+ throw error;
617
+ if (required) {
618
+ issues.push({
619
+ kind: "guide-file-missing",
620
+ scope,
621
+ repositoryPath: path,
622
+ message: `The mapped guide ${path} does not exist.`
623
+ });
624
+ }
625
+ return { absolutePath, issues };
626
+ }
627
+ if (metadata.isSymbolicLink()) {
628
+ issues.push({
629
+ kind: "guide-file-symlink",
630
+ scope,
631
+ repositoryPath: path,
632
+ message: `The mapped guide ${path} is a symbolic link.`
633
+ });
634
+ const resolvedPath = await realpathIfPresent(absolutePath);
635
+ if (resolvedPath !== null && !pathIsWithin(root, resolvedPath)) {
636
+ issues.push({
637
+ kind: "repository-symlink-escape",
638
+ scope,
639
+ repositoryPath: path,
640
+ resolvedPath,
641
+ message: `The mapped guide ${path} resolves outside the repository.`
642
+ });
643
+ }
644
+ return { absolutePath, issues };
645
+ }
646
+ if (!metadata.isFile()) {
647
+ issues.push({
648
+ kind: "guide-file-not-regular",
649
+ scope,
650
+ repositoryPath: path,
651
+ message: `The mapped guide ${path} is not a regular file.`
652
+ });
653
+ return { absolutePath, issues };
654
+ }
655
+ const handle = await open(absolutePath, constants.O_RDONLY | constants.O_NOFOLLOW);
656
+ try {
657
+ const openedMetadata = await handle.stat();
658
+ if (!openedMetadata.isFile()) {
659
+ issues.push({
660
+ kind: "guide-file-not-regular",
661
+ scope,
662
+ repositoryPath: path,
663
+ message: `The mapped guide ${path} is not a regular file.`
664
+ });
665
+ return { absolutePath, issues };
666
+ }
667
+ const resolvedPath = await realpath(absolutePath);
668
+ if (!pathIsWithin(root, resolvedPath)) {
669
+ issues.push({
670
+ kind: "repository-symlink-escape",
671
+ scope,
672
+ repositoryPath: path,
673
+ resolvedPath,
674
+ message: `The mapped guide ${path} resolves outside the repository.`
675
+ });
676
+ return { absolutePath, issues };
677
+ }
678
+ return {
679
+ absolutePath,
680
+ guide: {
681
+ path,
682
+ source: await handle.readFile({ encoding: "utf8" })
683
+ },
684
+ issues
685
+ };
686
+ } finally {
687
+ await handle.close();
688
+ }
689
+ }
690
+ function uniqueIssues(issues) {
691
+ const unique = new Map;
692
+ for (const issue of issues) {
693
+ unique.set(JSON.stringify(issue), issue);
694
+ }
695
+ return sortedIssues([...unique.values()]);
696
+ }
697
+ async function inspectAgentContextRepository(notes, options) {
698
+ const requestedRoot = resolve(options.repositoryRoot);
699
+ const root = await realpath(requestedRoot);
700
+ const rootMetadata = await lstat(root);
701
+ if (!rootMetadata.isDirectory()) {
702
+ throw new AgentContextRepositoryPathError("root-not-directory", options.repositoryRoot, "The repository root must be a directory.");
703
+ }
704
+ const target = normalizeRepositoryScope(options.target ?? ".");
705
+ await assertTargetPrefixConfined(root, target);
706
+ const resolvedTargetScope = await targetScope(root, target, options.targetKind ?? "auto", /[\\/]$/u.test(options.target ?? "."));
707
+ const preliminary = analyzeAgentContexts(notes);
708
+ const validateAllMappings = options.validationMode === "all";
709
+ const applicableCaseFoldedScopes = new Set(preliminary.contexts.filter((context) => scopeIsAncestor(context.scope, resolvedTargetScope)).map((context) => context.scope.toLocaleLowerCase("en-US")));
710
+ const contextsToValidate = preliminary.contexts.filter((context) => validateAllMappings || applicableCaseFoldedScopes.has(context.scope.toLocaleLowerCase("en-US")));
711
+ const notesToAnalyze = validateAllMappings ? notes : contextsToValidate.map(({ note }) => note);
712
+ const scopesToInspect = sortedUnique(contextsToValidate.filter((context) => context.canonical).map((context) => context.scope));
713
+ const filesystemIssues = [];
714
+ const guidesByPath = new Map;
715
+ for (const scope of scopesToInspect) {
716
+ const directory = await inspectScopeDirectory(root, scope);
717
+ filesystemIssues.push(...directory.issues);
718
+ if (directory.issues.length > 0)
719
+ continue;
720
+ const read = await readGuide(root, scope, true);
721
+ filesystemIssues.push(...read.issues);
722
+ if (read.guide !== undefined) {
723
+ guidesByPath.set(read.guide.path, {
724
+ guide: read.guide,
725
+ absolutePath: read.absolutePath
726
+ });
727
+ }
728
+ }
729
+ const inheritedGuidePaths = [];
730
+ for (const scope of scopeAncestors(resolvedTargetScope)) {
731
+ const guidePath = agentContextGuidePath(scope);
732
+ let entry = guidesByPath.get(guidePath);
733
+ if (entry === undefined) {
734
+ const read = await readGuide(root, scope, false);
735
+ filesystemIssues.push(...read.issues);
736
+ if (read.guide !== undefined) {
737
+ entry = { guide: read.guide, absolutePath: read.absolutePath };
738
+ guidesByPath.set(guidePath, entry);
739
+ }
740
+ }
741
+ if (entry !== undefined)
742
+ inheritedGuidePaths.push(guidePath);
743
+ }
744
+ const analysis = analyzeAgentContexts(notesToAnalyze, [...guidesByPath.values()].map((entry) => entry.guide));
745
+ const guidesByAnalyzedPath = new Map(analysis.guides.map((guide) => [guide.path, guide]));
746
+ const inheritedGuides = inheritedGuidePaths.map((path) => {
747
+ const entry = guidesByPath.get(path);
748
+ const guide = guidesByAnalyzedPath.get(path);
749
+ return entry === undefined || guide === undefined ? null : { ...guide, absolutePath: entry.absolutePath };
750
+ }).filter((guide) => guide !== null);
751
+ const matchingContexts = analysis.contexts.filter((context) => context.valid && scopeIsAncestor(context.scope, resolvedTargetScope)).toSorted((left, right) => scopeDepth(right.scope) - scopeDepth(left.scope) || left.scope.localeCompare(right.scope));
752
+ return {
753
+ repositoryRoot: root,
754
+ target,
755
+ targetScope: resolvedTargetScope,
756
+ contexts: analysis.contexts,
757
+ guides: analysis.guides,
758
+ inheritedGuides,
759
+ matchingContexts,
760
+ issues: uniqueIssues([...analysis.issues, ...filesystemIssues])
761
+ };
762
+ }
763
+
764
+ export { agentContextType, agentContextDirectory, agentContextSlugMaximumLength, agentContextHashLength, RepositoryScopeError, normalizeRepositoryScope, agentContextNoteId, agentContextNotePath, agentContextGuidePath, formatAgentContextMarker, agentContextMarkerForScope, parseAgentContextMarker, analyzeAgentContexts, AgentContextRepositoryPathError, inspectAgentContextRepository };