@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,1166 @@
1
+ // @bun
2
+ import {
3
+ DEFAULT_SEARCH_RESULTS,
4
+ MAX_SEARCH_CANDIDATES,
5
+ MAX_SEARCH_RESULTS,
6
+ openKnowledgeBase,
7
+ validateKnowledgeBaseSearchHistory
8
+ } from "./index-zzhgcwyt.js";
9
+ import {
10
+ expandSearchRequest,
11
+ parseSearchRules
12
+ } from "./index-adx6khj5.js";
13
+ import {
14
+ scanVault
15
+ } from "./index-zxdy5pby.js";
16
+ import {
17
+ indexGitHistory
18
+ } from "./index-1gwbassd.js";
19
+ import {
20
+ MAX_ATTACHMENT_REFERENCES,
21
+ validateMarkdownAttachments
22
+ } from "./index-x3fthpsc.js";
23
+ import {
24
+ validateSearchQuery
25
+ } from "./index-cv6fh7z5.js";
26
+ import {
27
+ validateQueryOptions
28
+ } from "./index-48pz4jpc.js";
29
+ import {
30
+ documentIdState,
31
+ parseQualifiedDocumentUri,
32
+ parseVaultKey,
33
+ portfolioDocumentIdentity,
34
+ portfolioVaultIdentity
35
+ } from "./index-cxfrakt7.js";
36
+
37
+ // src/portfolio.ts
38
+ import { createHash as createHash2 } from "crypto";
39
+
40
+ // src/portfolio-registry.ts
41
+ import { constants } from "fs";
42
+ import { lstat, open, realpath } from "fs/promises";
43
+ import { join, relative, resolve, sep } from "path";
44
+ var PORTFOLIO_REGISTRY_CONTRACT = "hraness.kb-portfolio/v1";
45
+ var MAX_PORTFOLIO_REGISTRY_BYTES = 1024 * 1024;
46
+ var MAX_PORTFOLIO_VAULTS = 128;
47
+ var MAX_AUTHORIZED_VAULTS = 32;
48
+ var MAX_AUTHORITY_GROUPS = 64;
49
+ var MAX_PATH_BYTES = 2048;
50
+ var MAX_TEXT_BYTES = 1024;
51
+ function isRecord(value) {
52
+ return value !== null && typeof value === "object" && !Array.isArray(value);
53
+ }
54
+ function assertKeys(value, allowed, required, label) {
55
+ for (const key of Object.keys(value)) {
56
+ if (!allowed.has(key))
57
+ throw new TypeError(`${label} has unknown property ${JSON.stringify(key)}.`);
58
+ }
59
+ for (const key of required) {
60
+ if (!Object.hasOwn(value, key))
61
+ throw new TypeError(`${label} is missing property ${JSON.stringify(key)}.`);
62
+ }
63
+ }
64
+ function boundedString(value, maximum, label) {
65
+ if (typeof value !== "string")
66
+ throw new TypeError(`${label} must be a string.`);
67
+ if (value.normalize("NFC") !== value)
68
+ throw new TypeError(`${label} must use NFC-normalized text.`);
69
+ if ([...value].some((character) => {
70
+ const code = character.codePointAt(0) ?? 0;
71
+ return code <= 31 || code === 127;
72
+ })) {
73
+ throw new TypeError(`${label} must not contain control characters.`);
74
+ }
75
+ if (Buffer.byteLength(value, "utf8") > maximum) {
76
+ throw new RangeError(`${label} must be at most ${maximum} UTF-8 bytes.`);
77
+ }
78
+ return value;
79
+ }
80
+ function registryPath(value, label, allowDot) {
81
+ const path = boundedString(value, MAX_PATH_BYTES, label);
82
+ if (path === "." && allowDot)
83
+ return path;
84
+ if (path === "" || path === "." || path.startsWith("/") || path.endsWith("/") || path.includes("\\")) {
85
+ throw new TypeError(`${label} must be a canonical relative POSIX path.`);
86
+ }
87
+ const segments = path.split("/");
88
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
89
+ throw new TypeError(`${label} must not contain empty, dot, or parent segments.`);
90
+ }
91
+ return path;
92
+ }
93
+ function enumValue(value, values, label) {
94
+ if (typeof value !== "string" || !values.includes(value)) {
95
+ throw new TypeError(`${label} must be one of ${values.map((item) => JSON.stringify(item)).join(", ")}.`);
96
+ }
97
+ return value;
98
+ }
99
+ function parseVaultEntry(value, index) {
100
+ const label = `Portfolio vault ${index + 1}`;
101
+ if (!isRecord(value))
102
+ throw new TypeError(`${label} must be an object.`);
103
+ assertKeys(value, new Set([
104
+ "owner",
105
+ "id",
106
+ "repository",
107
+ "checkout",
108
+ "root",
109
+ "role",
110
+ "visibility",
111
+ "defaultRef",
112
+ "parserVersion"
113
+ ]), new Set([
114
+ "owner",
115
+ "id",
116
+ "repository",
117
+ "checkout",
118
+ "root",
119
+ "role",
120
+ "visibility",
121
+ "parserVersion"
122
+ ]), label);
123
+ const identity = portfolioVaultIdentity(value.owner, value.id);
124
+ const repository = parseVaultKey(value.repository).key;
125
+ if (value.parserVersion !== 1)
126
+ throw new TypeError(`${label} parserVersion must be exactly 1.`);
127
+ const defaultRef = value.defaultRef === undefined ? undefined : boundedString(value.defaultRef, MAX_TEXT_BYTES, `${label} defaultRef`);
128
+ if (defaultRef === "")
129
+ throw new TypeError(`${label} defaultRef must not be empty.`);
130
+ return Object.freeze({
131
+ ...identity,
132
+ repository,
133
+ checkout: registryPath(value.checkout, `${label} checkout`, false),
134
+ root: registryPath(value.root, `${label} root`, true),
135
+ role: enumValue(value.role, ["archive", "portfolio", "repository", "sample", "template"], `${label} role`),
136
+ visibility: enumValue(value.visibility, ["organization", "personal", "private", "public"], `${label} visibility`),
137
+ ...defaultRef === undefined ? {} : { defaultRef },
138
+ parserVersion: 1
139
+ });
140
+ }
141
+ function parseAuthorityGroup(value, index, vaultKeys) {
142
+ const label = `Portfolio authority group ${index + 1}`;
143
+ if (!isRecord(value))
144
+ throw new TypeError(`${label} must be an object.`);
145
+ assertKeys(value, new Set(["id", "members", "state", "canonical", "protected", "reason"]), new Set(["id", "members", "state"]), label);
146
+ const id = portfolioVaultIdentity("authority", value.id).id;
147
+ if (!Array.isArray(value.members) || value.members.length < 2 || value.members.length > MAX_AUTHORIZED_VAULTS) {
148
+ throw new RangeError(`${label} members must contain 2 through ${MAX_AUTHORIZED_VAULTS} vault keys.`);
149
+ }
150
+ const members = value.members.map((member) => parseVaultKey(member).key);
151
+ if (new Set(members).size !== members.length)
152
+ throw new TypeError(`${label} members must be unique.`);
153
+ for (const member of members) {
154
+ if (!vaultKeys.has(member))
155
+ throw new TypeError(`${label} references unknown vault ${JSON.stringify(member)}.`);
156
+ }
157
+ const state = enumValue(value.state, ["resolved", "unresolved"], `${label} state`);
158
+ const canonical = value.canonical === undefined ? undefined : parseVaultKey(value.canonical).key;
159
+ if (state === "resolved") {
160
+ if (canonical === undefined || !members.includes(canonical)) {
161
+ throw new TypeError(`${label} resolved state requires a canonical member.`);
162
+ }
163
+ } else if (canonical !== undefined) {
164
+ throw new TypeError(`${label} unresolved state must not declare a canonical member.`);
165
+ }
166
+ if (value.protected !== undefined && typeof value.protected !== "boolean") {
167
+ throw new TypeError(`${label} protected must be a boolean.`);
168
+ }
169
+ const reason = value.reason === undefined ? undefined : boundedString(value.reason, MAX_TEXT_BYTES, `${label} reason`);
170
+ if (reason === "")
171
+ throw new TypeError(`${label} reason must not be empty.`);
172
+ return Object.freeze({
173
+ id,
174
+ members: Object.freeze(members),
175
+ state,
176
+ ...canonical === undefined ? {} : { canonical },
177
+ ...value.protected === undefined ? {} : { protected: value.protected },
178
+ ...reason === undefined ? {} : { reason }
179
+ });
180
+ }
181
+ function parsePortfolioRegistry(value) {
182
+ if (!isRecord(value))
183
+ throw new TypeError("Portfolio registry must be an object.");
184
+ assertKeys(value, new Set(["$schema", "contract", "schemaVersion", "vaults", "authorityGroups"]), new Set(["contract", "schemaVersion", "vaults"]), "Portfolio registry");
185
+ if (value.contract !== PORTFOLIO_REGISTRY_CONTRACT) {
186
+ throw new TypeError(`Portfolio registry contract must be ${JSON.stringify(PORTFOLIO_REGISTRY_CONTRACT)}.`);
187
+ }
188
+ if (value.schemaVersion !== 1)
189
+ throw new TypeError("Portfolio registry schemaVersion must be exactly 1.");
190
+ if (!Array.isArray(value.vaults) || value.vaults.length > MAX_PORTFOLIO_VAULTS) {
191
+ throw new RangeError(`Portfolio registry vaults must contain at most ${MAX_PORTFOLIO_VAULTS} entries.`);
192
+ }
193
+ const vaults = value.vaults.map(parseVaultEntry);
194
+ const vaultKeys = new Set(vaults.map(({ key }) => key));
195
+ if (vaultKeys.size !== vaults.length)
196
+ throw new TypeError("Portfolio registry vault keys must be unique.");
197
+ const rawGroups = value.authorityGroups ?? [];
198
+ if (!Array.isArray(rawGroups) || rawGroups.length > MAX_AUTHORITY_GROUPS) {
199
+ throw new RangeError(`Portfolio authorityGroups must contain at most ${MAX_AUTHORITY_GROUPS} entries.`);
200
+ }
201
+ const authorityGroups = rawGroups.map((group, index) => parseAuthorityGroup(group, index, vaultKeys));
202
+ if (new Set(authorityGroups.map(({ id }) => id)).size !== authorityGroups.length) {
203
+ throw new TypeError("Portfolio authority group IDs must be unique.");
204
+ }
205
+ const schema = value.$schema === undefined ? undefined : boundedString(value.$schema, MAX_TEXT_BYTES, "Portfolio registry $schema");
206
+ return Object.freeze({
207
+ contract: PORTFOLIO_REGISTRY_CONTRACT,
208
+ schemaVersion: 1,
209
+ ...schema === undefined ? {} : { schema },
210
+ vaults: Object.freeze(vaults),
211
+ authorityGroups: Object.freeze(authorityGroups)
212
+ });
213
+ }
214
+ function snapshotPortfolioRegistry(registry) {
215
+ if (!isRecord(registry))
216
+ throw new TypeError("Portfolio registry snapshot must be an object.");
217
+ return parsePortfolioRegistry({
218
+ contract: registry.contract,
219
+ schemaVersion: registry.schemaVersion,
220
+ ...registry.schema === undefined ? {} : { $schema: registry.schema },
221
+ vaults: Array.isArray(registry.vaults) ? registry.vaults.map((entry) => ({
222
+ owner: entry.owner,
223
+ id: entry.id,
224
+ repository: entry.repository,
225
+ checkout: entry.checkout,
226
+ root: entry.root,
227
+ role: entry.role,
228
+ visibility: entry.visibility,
229
+ ...entry.defaultRef === undefined ? {} : { defaultRef: entry.defaultRef },
230
+ parserVersion: entry.parserVersion
231
+ })) : registry.vaults,
232
+ authorityGroups: Array.isArray(registry.authorityGroups) ? registry.authorityGroups.map((group) => ({
233
+ id: group.id,
234
+ members: group.members,
235
+ state: group.state,
236
+ ...group.canonical === undefined ? {} : { canonical: group.canonical },
237
+ ...group.protected === undefined ? {} : { protected: group.protected },
238
+ ...group.reason === undefined ? {} : { reason: group.reason }
239
+ })) : registry.authorityGroups
240
+ });
241
+ }
242
+ async function defaultReadRegistryFile(path, maximumBytes) {
243
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
244
+ try {
245
+ const before = await handle.stat({ bigint: true });
246
+ if (!before.isFile())
247
+ throw new Error("Portfolio registry must be a regular file.");
248
+ if (before.nlink !== 1n)
249
+ throw new Error("Portfolio registry must not be hard-linked.");
250
+ if (before.size > BigInt(maximumBytes)) {
251
+ throw new RangeError(`Portfolio registry must be at most ${maximumBytes} UTF-8 bytes.`);
252
+ }
253
+ const chunks = [];
254
+ let observedBytes = 0;
255
+ for (;; ) {
256
+ const buffer = new Uint8Array(Math.min(64 * 1024, maximumBytes - observedBytes + 1));
257
+ const read = await handle.read(buffer, 0, buffer.byteLength, null);
258
+ if (read.bytesRead === 0)
259
+ break;
260
+ observedBytes += read.bytesRead;
261
+ if (observedBytes > maximumBytes) {
262
+ throw new RangeError(`Portfolio registry must be at most ${maximumBytes} UTF-8 bytes.`);
263
+ }
264
+ chunks.push(buffer.slice(0, read.bytesRead));
265
+ }
266
+ const after = await handle.stat({ bigint: true });
267
+ if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.mtimeNs !== before.mtimeNs || after.size !== BigInt(observedBytes)) {
268
+ throw new Error("Portfolio registry changed while it was being read; retry.");
269
+ }
270
+ const bytes = new Uint8Array(observedBytes);
271
+ let offset = 0;
272
+ for (const chunk of chunks) {
273
+ bytes.set(chunk, offset);
274
+ offset += chunk.byteLength;
275
+ }
276
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
277
+ } finally {
278
+ await handle.close();
279
+ }
280
+ }
281
+ async function loadPortfolioRegistry(path, dependencies = {}) {
282
+ const source = await (dependencies.readRegistryFile ?? defaultReadRegistryFile)(resolve(path), MAX_PORTFOLIO_REGISTRY_BYTES);
283
+ if (Buffer.byteLength(source, "utf8") > MAX_PORTFOLIO_REGISTRY_BYTES) {
284
+ throw new RangeError(`Portfolio registry must be at most ${MAX_PORTFOLIO_REGISTRY_BYTES} UTF-8 bytes.`);
285
+ }
286
+ let value;
287
+ try {
288
+ value = JSON.parse(source);
289
+ } catch (error) {
290
+ throw new SyntaxError("Portfolio registry must be valid JSON.", { cause: error });
291
+ }
292
+ return parsePortfolioRegistry(value);
293
+ }
294
+ function selectAuthorizedVaults(registry, authorizedVaults) {
295
+ if (!Array.isArray(authorizedVaults) || authorizedVaults.length < 1 || authorizedVaults.length > MAX_AUTHORIZED_VAULTS) {
296
+ throw new RangeError(`authorizedVaults must contain 1 through ${MAX_AUTHORIZED_VAULTS} explicit vault keys.`);
297
+ }
298
+ const checked = authorizedVaults.map((key) => parseVaultKey(key).key);
299
+ if (new Set(checked).size !== checked.length)
300
+ throw new TypeError("authorizedVaults must not contain duplicates.");
301
+ const byKey = new Map(registry.vaults.map((entry) => [entry.key, entry]));
302
+ for (const key of checked) {
303
+ if (!byKey.has(key)) {
304
+ throw new Error(`Authorized portfolio vault ${JSON.stringify(key)} is not available.`);
305
+ }
306
+ }
307
+ const selected = new Set(checked);
308
+ return Object.freeze(registry.vaults.filter(({ key }) => selected.has(key)));
309
+ }
310
+ function insideOrSame(parent, child) {
311
+ const fromParent = relative(parent, child);
312
+ return fromParent === "" || fromParent !== ".." && !fromParent.startsWith(`..${sep}`);
313
+ }
314
+ async function assertDirectoryChain(base, path, metadata, label) {
315
+ const segments = path === "." ? [] : path.split("/");
316
+ let current = base;
317
+ for (const segment of segments) {
318
+ current = join(current, segment);
319
+ const entry = await metadata(current);
320
+ if (entry.isSymbolicLink())
321
+ throw new Error(`${label} must not traverse a symbolic link.`);
322
+ if (!entry.isDirectory())
323
+ throw new Error(`${label} must contain directories only.`);
324
+ }
325
+ }
326
+ async function resolvePortfolioVault(entry, workspaceRoot, dependencies = {}) {
327
+ const metadata = dependencies.lstat ?? ((path) => lstat(path));
328
+ const canonical = dependencies.realpath ?? ((path) => realpath(path));
329
+ const requestedWorkspace = resolve(workspaceRoot);
330
+ const workspaceMetadata = await metadata(requestedWorkspace);
331
+ if (workspaceMetadata.isSymbolicLink())
332
+ throw new Error("Portfolio workspace root must not be a symbolic link.");
333
+ if (!workspaceMetadata.isDirectory())
334
+ throw new Error("Portfolio workspace root must be a directory.");
335
+ const workspace = await canonical(requestedWorkspace);
336
+ await assertDirectoryChain(workspace, entry.checkout, metadata, "Portfolio checkout");
337
+ const repositoryRoot = await canonical(join(workspace, ...entry.checkout.split("/")));
338
+ if (!insideOrSame(workspace, repositoryRoot))
339
+ throw new Error("Portfolio checkout resolves outside the workspace root.");
340
+ await assertDirectoryChain(repositoryRoot, entry.root, metadata, "Portfolio vault root");
341
+ const root = entry.root === "." ? repositoryRoot : await canonical(join(repositoryRoot, ...entry.root.split("/")));
342
+ if (!insideOrSame(repositoryRoot, root))
343
+ throw new Error("Portfolio vault resolves outside its repository checkout.");
344
+ return Object.freeze({ entry, repositoryRoot, root });
345
+ }
346
+ function rootsOverlap(left, right) {
347
+ return insideOrSame(left, right) || insideOrSame(right, left);
348
+ }
349
+ function validateResolvedPortfolioVaults(vaults) {
350
+ for (let left = 0;left < vaults.length; left += 1) {
351
+ for (let right = left + 1;right < vaults.length; right += 1) {
352
+ const first = vaults[left];
353
+ const second = vaults[right];
354
+ if (first !== undefined && second !== undefined && rootsOverlap(first.root, second.root)) {
355
+ throw new Error(`Authorized portfolio vault roots overlap: ${JSON.stringify(first.entry.key)} and ${JSON.stringify(second.entry.key)}.`);
356
+ }
357
+ }
358
+ }
359
+ return Object.freeze([...vaults]);
360
+ }
361
+ async function resolveAuthorizedVaults(registry, workspaceRoot, authorizedVaults, dependencies = {}) {
362
+ const selected = selectAuthorizedVaults(registry, authorizedVaults);
363
+ const resolved = [];
364
+ for (const entry of selected) {
365
+ resolved.push(await resolvePortfolioVault(entry, workspaceRoot, dependencies));
366
+ }
367
+ return validateResolvedPortfolioVaults(resolved);
368
+ }
369
+
370
+ // src/portfolio-audit.ts
371
+ import { createHash } from "crypto";
372
+ var DEFAULT_PORTFOLIO_AUDIT_ISSUES = 500;
373
+ var MAX_PORTFOLIO_AUDIT_ISSUES = 5000;
374
+ function checkedLimit(value, fallback, maximum, label) {
375
+ const checked = value ?? fallback;
376
+ if (!Number.isSafeInteger(checked) || checked < 1 || checked > maximum) {
377
+ throw new RangeError(`${label} must be an integer from 1 through ${maximum}.`);
378
+ }
379
+ return checked;
380
+ }
381
+ function errorMessage(error) {
382
+ return error instanceof Error ? error.message : String(error);
383
+ }
384
+ function descriptor(entry) {
385
+ return Object.freeze({
386
+ owner: entry.owner,
387
+ id: entry.id,
388
+ key: entry.key,
389
+ role: entry.role,
390
+ visibility: entry.visibility
391
+ });
392
+ }
393
+ function defaultSha256(content) {
394
+ return createHash("sha256").update(content, "utf8").digest("hex");
395
+ }
396
+ function attachmentMessage(issue) {
397
+ return `${issue.kind}: ${issue.message}`;
398
+ }
399
+ function activeRole(role) {
400
+ return role === "portfolio" || role === "repository";
401
+ }
402
+ function protectedDuplicate(references, groups) {
403
+ const keys = new Set(references.map(({ vault }) => vault.key));
404
+ return groups.some((group) => group.protected === true && keys.size > 1 && [...keys].every((key) => group.members.includes(key)));
405
+ }
406
+ async function auditKnowledgePortfolio(options, dependencies = {}) {
407
+ const maximumIssues = checkedLimit(options.maxIssues, DEFAULT_PORTFOLIO_AUDIT_ISSUES, MAX_PORTFOLIO_AUDIT_ISSUES, "Portfolio audit issue limit");
408
+ const maximumAttachments = checkedLimit(options.maxAttachmentReferences, MAX_ATTACHMENT_REFERENCES, MAX_ATTACHMENT_REFERENCES, "Portfolio attachment reference limit");
409
+ const registry = snapshotPortfolioRegistry(options.registry ?? await (dependencies.loadPortfolioRegistry ?? loadPortfolioRegistry)(options.registryPath, dependencies));
410
+ const selected = selectAuthorizedVaults(registry, options.authorizedVaults);
411
+ const selectedKeys = new Set(selected.map(({ key }) => key));
412
+ const selectedAuthority = registry.authorityGroups.filter(({ members }) => members.every((member) => selectedKeys.has(member)));
413
+ const issues = [];
414
+ const counts = {
415
+ advisory: 0,
416
+ error: 0,
417
+ warning: 0
418
+ };
419
+ const severityRank = {
420
+ advisory: 0,
421
+ warning: 1,
422
+ error: 2
423
+ };
424
+ let truncated = false;
425
+ const addIssue = (issue) => {
426
+ counts[issue.severity] += 1;
427
+ if (issues.length >= maximumIssues) {
428
+ truncated = true;
429
+ let weakestIndex = 0;
430
+ for (let index = 1;index < issues.length; index += 1) {
431
+ const candidate = issues[index];
432
+ const weakest2 = issues[weakestIndex];
433
+ if (candidate !== undefined && weakest2 !== undefined && severityRank[candidate.severity] < severityRank[weakest2.severity]) {
434
+ weakestIndex = index;
435
+ }
436
+ }
437
+ const weakest = issues[weakestIndex];
438
+ if (weakest !== undefined && severityRank[issue.severity] > severityRank[weakest.severity]) {
439
+ issues[weakestIndex] = Object.freeze({
440
+ ...issue,
441
+ ...issue.related === undefined ? {} : { related: Object.freeze([...issue.related]) }
442
+ });
443
+ }
444
+ return;
445
+ }
446
+ issues.push(Object.freeze({
447
+ ...issue,
448
+ ...issue.related === undefined ? {} : { related: Object.freeze([...issue.related]) }
449
+ }));
450
+ };
451
+ for (const group of selectedAuthority) {
452
+ if (group.state !== "unresolved")
453
+ continue;
454
+ addIssue({
455
+ code: "authority-unresolved",
456
+ severity: "warning",
457
+ message: `Authority group ${JSON.stringify(group.id)} remains unresolved across explicitly selected members.`,
458
+ ...group.protected === undefined ? {} : { protected: group.protected }
459
+ });
460
+ }
461
+ const resolved = [];
462
+ const summaries = [];
463
+ for (const entry of selected) {
464
+ try {
465
+ resolved.push(await (dependencies.resolvePortfolioVault ?? resolvePortfolioVault)(entry, options.workspaceRoot, dependencies));
466
+ } catch (error) {
467
+ const vault = descriptor(entry);
468
+ addIssue({
469
+ code: "vault-unavailable",
470
+ severity: "error",
471
+ vault,
472
+ message: errorMessage(error)
473
+ });
474
+ summaries.push(Object.freeze({
475
+ vault,
476
+ status: "unavailable",
477
+ notes: 0,
478
+ stableDocuments: 0,
479
+ legacyDocuments: 0,
480
+ index: null,
481
+ head: null
482
+ }));
483
+ }
484
+ }
485
+ let auditable = resolved;
486
+ try {
487
+ auditable = validateResolvedPortfolioVaults(resolved);
488
+ } catch (error) {
489
+ addIssue({
490
+ code: "root-overlap",
491
+ severity: "error",
492
+ message: errorMessage(error)
493
+ });
494
+ for (const resolvedVault of resolved) {
495
+ summaries.push(Object.freeze({
496
+ vault: descriptor(resolvedVault.entry),
497
+ status: "unavailable",
498
+ notes: 0,
499
+ stableDocuments: 0,
500
+ legacyDocuments: 0,
501
+ index: null,
502
+ head: null
503
+ }));
504
+ }
505
+ auditable = [];
506
+ }
507
+ const duplicateCandidates = [];
508
+ const availableStableUris = new Set;
509
+ const externalRelationCandidates = [];
510
+ for (const resolvedVault of auditable) {
511
+ const vault = descriptor(resolvedVault.entry);
512
+ let snapshot;
513
+ try {
514
+ snapshot = await (dependencies.scanVault ?? scanVault)(resolvedVault.root, {
515
+ ...options.scan ?? {},
516
+ mentionScope: false
517
+ });
518
+ } catch (error) {
519
+ addIssue({
520
+ code: "scan-unavailable",
521
+ severity: "error",
522
+ vault,
523
+ message: errorMessage(error)
524
+ });
525
+ summaries.push(Object.freeze({
526
+ vault,
527
+ status: "unavailable",
528
+ notes: 0,
529
+ stableDocuments: 0,
530
+ legacyDocuments: 0,
531
+ index: null,
532
+ head: null
533
+ }));
534
+ continue;
535
+ }
536
+ if (snapshot.index === "stale") {
537
+ addIssue({
538
+ code: "catalog-stale",
539
+ severity: "warning",
540
+ vault,
541
+ message: "The managed vault catalog is stale."
542
+ });
543
+ }
544
+ let stableDocuments = 0;
545
+ let legacyDocuments = 0;
546
+ const pathsByDocumentId = new Map;
547
+ const hash = dependencies.sha256 ?? defaultSha256;
548
+ for (const note of snapshot.notes) {
549
+ const state = documentIdState(note.metadata);
550
+ const identity = portfolioDocumentIdentity(vault, note.path, note.metadata);
551
+ if (state.kind === "valid") {
552
+ stableDocuments += 1;
553
+ if (identity.kind === "stable")
554
+ availableStableUris.add(identity.uri);
555
+ const paths = pathsByDocumentId.get(state.documentId) ?? [];
556
+ paths.push(note.path);
557
+ pathsByDocumentId.set(state.documentId, paths);
558
+ } else {
559
+ legacyDocuments += 1;
560
+ addIssue(state.kind === "missing" ? {
561
+ code: "missing-document-id",
562
+ severity: activeRole(resolvedVault.entry.role) ? "warning" : "advisory",
563
+ vault,
564
+ path: note.path,
565
+ message: "Authored note has no stable document_id and is available only by legacy path."
566
+ } : {
567
+ code: "invalid-document-id",
568
+ severity: "error",
569
+ vault,
570
+ path: note.path,
571
+ message: "Authored note has an invalid document_id and is available only by legacy path."
572
+ });
573
+ }
574
+ duplicateCandidates.push(Object.freeze({
575
+ vault,
576
+ path: note.path,
577
+ identity,
578
+ hash: hash(note.content)
579
+ }));
580
+ }
581
+ for (const [documentId, paths] of pathsByDocumentId) {
582
+ if (paths.length < 2)
583
+ continue;
584
+ addIssue({
585
+ code: "duplicate-document-id",
586
+ severity: "error",
587
+ vault,
588
+ message: `document_id ${JSON.stringify(documentId)} is authored by multiple notes in this vault.`,
589
+ related: paths.toSorted().map((path) => ({
590
+ vault,
591
+ path,
592
+ identity: portfolioDocumentIdentity(vault, path, { document_id: documentId })
593
+ }))
594
+ });
595
+ }
596
+ for (const issue of snapshot.analysis.issues) {
597
+ addIssue({
598
+ code: issue.kind === "broken" ? "broken-link" : "ambiguous-link",
599
+ severity: issue.kind === "broken" ? "error" : "warning",
600
+ vault,
601
+ path: issue.source,
602
+ line: issue.line,
603
+ message: issue.kind === "broken" ? `Broken link target ${JSON.stringify(issue.target)}.` : `Ambiguous link target ${JSON.stringify(issue.target)}.`
604
+ });
605
+ }
606
+ for (const issue of snapshot.analysis.relationIssues) {
607
+ addIssue({
608
+ code: "relation",
609
+ severity: "error",
610
+ vault,
611
+ path: issue.source,
612
+ line: issue.line,
613
+ message: issue.kind === "malformed" ? issue.message : `${issue.kind} relation target ${JSON.stringify(issue.target)} for ${JSON.stringify(issue.predicate)}.`
614
+ });
615
+ }
616
+ for (const relation of snapshot.analysis.externalAuthoredRelations) {
617
+ externalRelationCandidates.push(Object.freeze({
618
+ vault,
619
+ path: relation.provenance.source,
620
+ line: relation.provenance.line,
621
+ predicate: relation.predicate,
622
+ target: relation.target
623
+ }));
624
+ }
625
+ try {
626
+ const attachmentReport = await (dependencies.validateMarkdownAttachments ?? validateMarkdownAttachments)({
627
+ root: snapshot.root,
628
+ documents: snapshot.notes.map(({ path, content }) => ({ path, content })),
629
+ maxReferences: maximumAttachments
630
+ });
631
+ for (const issue of attachmentReport.issues) {
632
+ addIssue({
633
+ code: "attachment",
634
+ severity: "error",
635
+ vault,
636
+ path: issue.source,
637
+ line: issue.line,
638
+ message: attachmentMessage(issue)
639
+ });
640
+ }
641
+ if (attachmentReport.truncated)
642
+ truncated = true;
643
+ } catch (error) {
644
+ addIssue({
645
+ code: "attachment",
646
+ severity: "error",
647
+ vault,
648
+ message: `Attachment audit failed: ${errorMessage(error)}`
649
+ });
650
+ }
651
+ let head = null;
652
+ try {
653
+ const indexed = await (dependencies.indexGitHistory ?? indexGitHistory)({
654
+ repository: resolvedVault.repositoryRoot,
655
+ root: resolvedVault.root,
656
+ notes: [],
657
+ maxCommits: 1
658
+ }, dependencies.git);
659
+ if (indexed.status === "ready")
660
+ head = indexed.head;
661
+ else {
662
+ addIssue({
663
+ code: "git-unavailable",
664
+ severity: "warning",
665
+ vault,
666
+ message: indexed.reason
667
+ });
668
+ }
669
+ } catch (error) {
670
+ addIssue({
671
+ code: "git-unavailable",
672
+ severity: "warning",
673
+ vault,
674
+ message: errorMessage(error)
675
+ });
676
+ }
677
+ summaries.push(Object.freeze({
678
+ vault,
679
+ status: "audited",
680
+ notes: snapshot.notes.length,
681
+ stableDocuments,
682
+ legacyDocuments,
683
+ index: snapshot.index,
684
+ head
685
+ }));
686
+ }
687
+ for (const relation of externalRelationCandidates) {
688
+ if (availableStableUris.has(relation.target))
689
+ continue;
690
+ addIssue({
691
+ code: "external-relation-unavailable",
692
+ severity: "error",
693
+ vault: relation.vault,
694
+ path: relation.path,
695
+ line: relation.line,
696
+ message: `Cross-vault relation target ${JSON.stringify(relation.target)} for ${JSON.stringify(relation.predicate)} is not available among the explicitly selected, successfully audited vaults.`
697
+ });
698
+ }
699
+ const candidatesByHash = new Map;
700
+ for (const candidate of duplicateCandidates) {
701
+ const group = candidatesByHash.get(candidate.hash) ?? [];
702
+ group.push({ vault: candidate.vault, path: candidate.path, identity: candidate.identity });
703
+ candidatesByHash.set(candidate.hash, group);
704
+ }
705
+ for (const [hash, references] of candidatesByHash) {
706
+ if (references.length < 2)
707
+ continue;
708
+ const related = references.toSorted((left, right) => left.vault.key.localeCompare(right.vault.key) || left.path.localeCompare(right.path));
709
+ const isProtected = protectedDuplicate(related, selectedAuthority);
710
+ addIssue({
711
+ code: "duplicate-content",
712
+ severity: related.some(({ vault }) => activeRole(vault.role)) ? "warning" : "advisory",
713
+ message: `Exact authored content SHA-256 ${hash} occurs in multiple selected notes; no authority was inferred.`,
714
+ ...isProtected ? { protected: true } : {},
715
+ related
716
+ });
717
+ }
718
+ const orderedSummaries = summaries.toSorted((left, right) => left.vault.key.localeCompare(right.vault.key));
719
+ const audited = orderedSummaries.filter(({ status }) => status === "audited");
720
+ const unavailable = orderedSummaries.filter(({ status }) => status === "unavailable");
721
+ return Object.freeze({
722
+ partial: unavailable.length > 0 || truncated,
723
+ truncated,
724
+ selectedVaults: selected.length,
725
+ auditedVaults: audited.length,
726
+ unavailableVaults: unavailable.length,
727
+ notes: audited.reduce((count, summary) => count + summary.notes, 0),
728
+ stableDocuments: audited.reduce((count, summary) => count + summary.stableDocuments, 0),
729
+ legacyDocuments: audited.reduce((count, summary) => count + summary.legacyDocuments, 0),
730
+ counts: Object.freeze(counts),
731
+ vaults: Object.freeze(orderedSummaries),
732
+ authority: Object.freeze(selectedAuthority.map((group) => Object.freeze({
733
+ id: group.id,
734
+ state: group.state,
735
+ protected: group.protected ?? false
736
+ }))),
737
+ issues: Object.freeze(issues)
738
+ });
739
+ }
740
+
741
+ // src/portfolio.ts
742
+ var MAX_PORTFOLIO_SEARCH_CONCURRENCY = 8;
743
+ var MAX_PORTFOLIO_REVISION_BYTES = 64 * 1024;
744
+
745
+ class PortfolioOpenError extends Error {
746
+ diagnostics;
747
+ constructor(message, diagnostics, options) {
748
+ super(message, options);
749
+ this.name = "PortfolioOpenError";
750
+ this.diagnostics = Object.freeze([...diagnostics]);
751
+ }
752
+ }
753
+
754
+ class PortfolioSearchError extends Error {
755
+ vault;
756
+ constructor(vault, message, options) {
757
+ super(message, options);
758
+ this.name = "PortfolioSearchError";
759
+ this.vault = vault;
760
+ }
761
+ }
762
+ function checkedInteger(value, fallback, maximum, label) {
763
+ const checked = value ?? fallback;
764
+ if (!Number.isSafeInteger(checked) || checked < 1 || checked > maximum) {
765
+ throw new RangeError(`${label} must be an integer from 1 through ${maximum}.`);
766
+ }
767
+ return checked;
768
+ }
769
+ function errorMessage2(error) {
770
+ return error instanceof Error ? error.message : String(error);
771
+ }
772
+ function descriptor2(entry) {
773
+ return Object.freeze({
774
+ owner: entry.owner,
775
+ id: entry.id,
776
+ key: entry.key,
777
+ role: entry.role,
778
+ visibility: entry.visibility
779
+ });
780
+ }
781
+ function defaultSha2562(content) {
782
+ return createHash2("sha256").update(content, "utf8").digest("hex");
783
+ }
784
+ function contentRevision(session, noteId, sha256) {
785
+ try {
786
+ const read = session.read(noteId, { maxBytes: MAX_PORTFOLIO_REVISION_BYTES });
787
+ return read.truncated ? Object.freeze({ complete: false, sha256: null }) : Object.freeze({ complete: true, sha256: sha256(read.content) });
788
+ } catch {
789
+ return Object.freeze({ complete: false, sha256: null });
790
+ }
791
+ }
792
+ function indexedRows(session) {
793
+ const rows = session.list();
794
+ const rowsById = new Map(rows.map((row) => [row.id, row]));
795
+ const mutableIds = new Map;
796
+ for (const row of rows) {
797
+ const identity = portfolioDocumentIdentity({ owner: "index", id: "index" }, row.path, row.metadata);
798
+ if (identity.kind !== "stable")
799
+ continue;
800
+ const ids = mutableIds.get(identity.documentId) ?? [];
801
+ ids.push(row.id);
802
+ mutableIds.set(identity.documentId, ids);
803
+ }
804
+ return {
805
+ rowsById,
806
+ idsByDocumentId: new Map([...mutableIds].map(([documentId, ids]) => [
807
+ documentId,
808
+ Object.freeze(ids.toSorted())
809
+ ]))
810
+ };
811
+ }
812
+ async function repositoryProbe(resolved, dependencies) {
813
+ const vault = descriptor2(resolved.entry);
814
+ const unavailable = (message) => ({
815
+ provenance: Object.freeze({
816
+ id: resolved.entry.repository,
817
+ ...resolved.entry.defaultRef === undefined ? {} : { defaultRef: resolved.entry.defaultRef },
818
+ head: null
819
+ }),
820
+ diagnostic: Object.freeze({ vault, lane: "git", status: "unavailable", message })
821
+ });
822
+ try {
823
+ const indexed = await (dependencies.indexGitHistory ?? indexGitHistory)({
824
+ repository: resolved.repositoryRoot,
825
+ root: resolved.root,
826
+ notes: [],
827
+ maxCommits: 1
828
+ }, dependencies.git);
829
+ if (indexed.status === "unavailable")
830
+ return unavailable(indexed.reason);
831
+ return {
832
+ provenance: Object.freeze({
833
+ id: resolved.entry.repository,
834
+ ...resolved.entry.defaultRef === undefined ? {} : { defaultRef: resolved.entry.defaultRef },
835
+ head: indexed.head
836
+ })
837
+ };
838
+ } catch (error) {
839
+ return unavailable(errorMessage2(error));
840
+ }
841
+ }
842
+ async function closeOpened(vaults) {
843
+ await Promise.all(vaults.map(({ session }) => session.close().catch(() => {
844
+ return;
845
+ })));
846
+ }
847
+ async function mapConcurrent(values, concurrency, operation) {
848
+ const results = new Array(values.length);
849
+ let cursor = 0;
850
+ const worker = async () => {
851
+ for (;; ) {
852
+ const index = cursor;
853
+ cursor += 1;
854
+ if (index >= values.length)
855
+ return;
856
+ const value = values[index];
857
+ if (value !== undefined)
858
+ results[index] = await operation(value);
859
+ }
860
+ };
861
+ await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, () => worker()));
862
+ return results;
863
+ }
864
+ function maybeQualifiedIdentity(query) {
865
+ try {
866
+ return parseQualifiedDocumentUri(query);
867
+ } catch {
868
+ return null;
869
+ }
870
+ }
871
+ function identitySortKey(identity) {
872
+ return identity.kind === "stable" ? identity.uri : identity.path;
873
+ }
874
+ async function openKnowledgePortfolio(options, dependencies = {}) {
875
+ const failurePolicy = options.failurePolicy ?? "partial";
876
+ if (failurePolicy !== "partial" && failurePolicy !== "required") {
877
+ throw new TypeError('Portfolio failurePolicy must be "partial" or "required".');
878
+ }
879
+ const searchConcurrency = checkedInteger(options.searchConcurrency, 4, MAX_PORTFOLIO_SEARCH_CONCURRENCY, "Portfolio search concurrency");
880
+ const configuredSearchRules = options.knowledgeBase?.searchRules === undefined ? null : parseSearchRules(options.knowledgeBase.searchRules);
881
+ const registry = snapshotPortfolioRegistry(options.registry ?? await (dependencies.loadPortfolioRegistry ?? loadPortfolioRegistry)(options.registryPath, dependencies));
882
+ const selected = selectAuthorizedVaults(registry, options.authorizedVaults);
883
+ const diagnostics = [];
884
+ const resolved = [];
885
+ for (const entry of selected) {
886
+ try {
887
+ resolved.push(await (dependencies.resolvePortfolioVault ?? resolvePortfolioVault)(entry, options.workspaceRoot, dependencies));
888
+ } catch (error) {
889
+ diagnostics.push(Object.freeze({
890
+ vault: descriptor2(entry),
891
+ lane: "resolve",
892
+ status: "unavailable",
893
+ message: errorMessage2(error)
894
+ }));
895
+ if (failurePolicy === "required") {
896
+ throw new PortfolioOpenError("A required portfolio vault could not be resolved.", diagnostics, {
897
+ cause: error
898
+ });
899
+ }
900
+ }
901
+ }
902
+ validateResolvedPortfolioVaults(resolved);
903
+ const opened = [];
904
+ for (const vault of resolved) {
905
+ const vaultDescriptor = descriptor2(vault.entry);
906
+ const probe = await repositoryProbe(vault, dependencies);
907
+ if (probe.diagnostic !== undefined)
908
+ diagnostics.push(probe.diagnostic);
909
+ let session;
910
+ try {
911
+ const knowledgeBase = options.knowledgeBase ?? {};
912
+ session = await (dependencies.openKnowledgeBase ?? openKnowledgeBase)({
913
+ ...knowledgeBase,
914
+ ...configuredSearchRules === null ? {} : { searchRules: configuredSearchRules },
915
+ vaultId: vaultDescriptor.key,
916
+ root: vault.root,
917
+ repository: vault.repositoryRoot,
918
+ scan: {
919
+ ...knowledgeBase.scan ?? {},
920
+ catalogMode: "authored"
921
+ }
922
+ }, dependencies.knowledgeBase);
923
+ const indexed = indexedRows(session);
924
+ opened.push({
925
+ resolved: vault,
926
+ descriptor: vaultDescriptor,
927
+ repository: probe.provenance,
928
+ session,
929
+ ...indexed
930
+ });
931
+ } catch (error) {
932
+ await session?.close().catch(() => {
933
+ return;
934
+ });
935
+ diagnostics.push(Object.freeze({
936
+ vault: vaultDescriptor,
937
+ lane: "open",
938
+ status: "unavailable",
939
+ message: errorMessage2(error)
940
+ }));
941
+ if (failurePolicy === "required") {
942
+ await closeOpened(opened);
943
+ throw new PortfolioOpenError("A required portfolio vault could not be opened.", diagnostics, {
944
+ cause: error
945
+ });
946
+ }
947
+ }
948
+ }
949
+ const openedByKey = new Map(opened.map((vault) => [vault.descriptor.key, vault]));
950
+ const hash = dependencies.sha256 ?? defaultSha2562;
951
+ let closed = false;
952
+ let closePromise;
953
+ const assertOpen = () => {
954
+ if (closed)
955
+ throw new Error("Knowledge-portfolio session is closed.");
956
+ };
957
+ const search = async (searchOptions) => {
958
+ assertOpen();
959
+ const { query } = validateSearchQuery(searchOptions.query);
960
+ if (searchOptions.graph !== undefined && searchOptions.graph !== false) {
961
+ throw new TypeError("Portfolio graph requests must be false; graph seeds require a qualified vault route.");
962
+ }
963
+ const limit = checkedInteger(searchOptions.limit, DEFAULT_SEARCH_RESULTS, MAX_SEARCH_RESULTS, "Portfolio search limit");
964
+ const candidateLimit = checkedInteger(searchOptions.candidateLimit, Math.max(40, limit * 4), MAX_SEARCH_CANDIDATES, "Portfolio candidate limit");
965
+ if (candidateLimit < limit) {
966
+ throw new RangeError("Portfolio candidate limit must be at least the result limit.");
967
+ }
968
+ const validationRequest = configuredSearchRules === null ? searchOptions : {
969
+ ...searchOptions,
970
+ ...expandSearchRequest({
971
+ query: searchOptions.query,
972
+ ...searchOptions.mode === undefined ? {} : { mode: searchOptions.mode },
973
+ ...searchOptions.filters === undefined ? {} : { filters: searchOptions.filters },
974
+ ...searchOptions.tags === undefined ? {} : { tags: searchOptions.tags },
975
+ ...searchOptions.repositoryScopes === undefined ? {} : { repositoryScopes: searchOptions.repositoryScopes }
976
+ }, configuredSearchRules).request
977
+ };
978
+ validateSearchQuery(validationRequest.query);
979
+ validateQueryOptions({
980
+ ...validationRequest.filters === undefined ? {} : { filters: validationRequest.filters },
981
+ ...validationRequest.tags === undefined ? {} : { tags: validationRequest.tags },
982
+ ...validationRequest.repositoryScopes === undefined ? {} : { repositoryScopes: validationRequest.repositoryScopes }
983
+ });
984
+ const validatedMode = validationRequest.mode ?? "hybrid";
985
+ if (validatedMode !== "exact" && validatedMode !== "hybrid" && validatedMode !== "keyword" && validatedMode !== "semantic") {
986
+ throw new TypeError("Portfolio search mode must be exact, hybrid, keyword, or semantic.");
987
+ }
988
+ const validatedOrdering = validationRequest.ordering ?? "relevance";
989
+ if (validatedOrdering !== "relevance" && validatedOrdering !== "priority-then-relevance") {
990
+ throw new TypeError('Portfolio search ordering must be "relevance" or "priority-then-relevance".');
991
+ }
992
+ if (validationRequest.minScore !== undefined && (!Number.isFinite(validationRequest.minScore) || validationRequest.minScore < 0 || validationRequest.minScore > 1)) {
993
+ throw new RangeError("Portfolio search minimum score must be a number from 0 through 1.");
994
+ }
995
+ if (validatedMode === "exact" && validationRequest.minScore !== undefined) {
996
+ throw new Error("Portfolio search minimum score applies only to hybrid, keyword, or semantic mode.");
997
+ }
998
+ validateKnowledgeBaseSearchHistory(validationRequest.history);
999
+ const routed = maybeQualifiedIdentity(query);
1000
+ const searchedVaults = routed === null ? opened : opened.filter(({ descriptor: selectedVault }) => selectedVault.key === routed.vault.key);
1001
+ const routedLocalIds = routed === null || searchedVaults[0] === undefined ? undefined : searchedVaults[0].idsByDocumentId.get(routed.documentId);
1002
+ if (routedLocalIds !== undefined && routedLocalIds.length > 1) {
1003
+ throw new Error("Qualified document identity is ambiguous in its selected vault.");
1004
+ }
1005
+ const localOptions = {
1006
+ ...searchOptions.mode === undefined ? {} : { mode: searchOptions.mode },
1007
+ ...searchOptions.ordering === undefined ? {} : { ordering: searchOptions.ordering },
1008
+ ...searchOptions.filters === undefined ? {} : { filters: searchOptions.filters },
1009
+ ...searchOptions.tags === undefined ? {} : { tags: searchOptions.tags },
1010
+ ...searchOptions.repositoryScopes === undefined ? {} : { repositoryScopes: searchOptions.repositoryScopes },
1011
+ limit,
1012
+ candidateLimit,
1013
+ ...searchOptions.minScore === undefined ? {} : { minScore: searchOptions.minScore },
1014
+ graph: false,
1015
+ ...searchOptions.history === undefined ? {} : { history: searchOptions.history }
1016
+ };
1017
+ const outcomes = await mapConcurrent(searchedVaults, searchConcurrency, async (vault) => {
1018
+ try {
1019
+ const routedLocalId = routed === null ? undefined : routedLocalIds?.[0];
1020
+ const result = await vault.session.search({
1021
+ ...localOptions,
1022
+ query: routedLocalId ?? routed?.documentId ?? query,
1023
+ ...routed === null ? {} : {
1024
+ mode: "exact",
1025
+ filters: Object.freeze([
1026
+ ...localOptions.filters ?? [],
1027
+ { kind: "equals", path: "document_id", value: routed.documentId }
1028
+ ])
1029
+ }
1030
+ });
1031
+ return { kind: "success", opened: vault, result };
1032
+ } catch (error) {
1033
+ return { kind: "failure", opened: vault, error };
1034
+ }
1035
+ });
1036
+ const vaultDiagnostics = diagnostics.filter(({ lane }) => lane !== "git").map((diagnostic) => Object.freeze({
1037
+ vault: diagnostic.vault,
1038
+ status: "unavailable",
1039
+ results: 0,
1040
+ message: diagnostic.message
1041
+ }));
1042
+ const candidates = [];
1043
+ for (const outcome of outcomes) {
1044
+ if (outcome.kind === "failure") {
1045
+ if (failurePolicy === "required") {
1046
+ throw new PortfolioSearchError(outcome.opened.descriptor, "A required portfolio vault search failed.", { cause: outcome.error });
1047
+ }
1048
+ vaultDiagnostics.push(Object.freeze({
1049
+ vault: outcome.opened.descriptor,
1050
+ status: "unavailable",
1051
+ results: 0,
1052
+ message: errorMessage2(outcome.error)
1053
+ }));
1054
+ continue;
1055
+ }
1056
+ vaultDiagnostics.push(Object.freeze({
1057
+ vault: outcome.opened.descriptor,
1058
+ status: outcome.result.partial ? "partial" : "ready",
1059
+ results: outcome.result.results.length,
1060
+ lanes: outcome.result.diagnostics.lanes,
1061
+ ...outcome.result.rules === undefined ? {} : { rules: outcome.result.rules }
1062
+ }));
1063
+ for (const hit of outcome.result.results) {
1064
+ const identity = portfolioDocumentIdentity(outcome.opened.descriptor, hit.path, hit.metadata);
1065
+ if (routed !== null && (identity.kind !== "stable" || identity.documentId !== routed.documentId)) {
1066
+ continue;
1067
+ }
1068
+ const localRank = hit.rank;
1069
+ candidates.push({
1070
+ opened: outcome.opened,
1071
+ hit,
1072
+ identity,
1073
+ exactIdentity: routed !== null || hit.identity,
1074
+ localRank,
1075
+ score: 1 / (60 + localRank)
1076
+ });
1077
+ }
1078
+ }
1079
+ const ordered = candidates.toSorted((left, right) => Number(right.exactIdentity) - Number(left.exactIdentity) || right.score - left.score || left.opened.descriptor.key.localeCompare(right.opened.descriptor.key) || identitySortKey(left.identity).localeCompare(identitySortKey(right.identity)) || left.hit.path.localeCompare(right.hit.path));
1080
+ const results = ordered.slice(0, limit).map((candidate, index) => Object.freeze({
1081
+ identity: candidate.identity,
1082
+ vault: candidate.opened.descriptor,
1083
+ repository: candidate.opened.repository,
1084
+ id: candidate.hit.id,
1085
+ path: candidate.hit.path,
1086
+ title: candidate.hit.title,
1087
+ rank: index + 1,
1088
+ score: candidate.score,
1089
+ exactIdentity: candidate.exactIdentity,
1090
+ localRank: candidate.localRank,
1091
+ revision: contentRevision(candidate.opened.session, candidate.hit.id, hash),
1092
+ snippet: candidate.hit.snippet,
1093
+ ...candidate.hit.line === undefined ? {} : { line: candidate.hit.line },
1094
+ tags: candidate.hit.tags,
1095
+ metadata: candidate.hit.metadata,
1096
+ evidence: candidate.hit.evidence,
1097
+ local: candidate.hit
1098
+ }));
1099
+ const partial = diagnostics.length > 0 || vaultDiagnostics.some(({ status }) => status !== "ready");
1100
+ const successfulModes = new Set(outcomes.flatMap((outcome) => outcome.kind === "success" ? [outcome.result.mode] : []));
1101
+ if (successfulModes.size > 1) {
1102
+ throw new Error("Selected vaults produced inconsistent effective search modes.");
1103
+ }
1104
+ const effectiveMode = routed === null ? successfulModes.values().next().value ?? validatedMode : "exact";
1105
+ return Object.freeze({
1106
+ query,
1107
+ mode: effectiveMode,
1108
+ results: Object.freeze(results),
1109
+ partial,
1110
+ diagnostics: Object.freeze({
1111
+ selectedVaults: selected.length,
1112
+ availableVaults: opened.length,
1113
+ notes: opened.reduce((count, vault) => count + vault.session.noteCount, 0),
1114
+ open: Object.freeze([...diagnostics]),
1115
+ vaults: Object.freeze(vaultDiagnostics)
1116
+ })
1117
+ });
1118
+ };
1119
+ const read = (value, readOptions = {}) => {
1120
+ assertOpen();
1121
+ const identity = parseQualifiedDocumentUri(value);
1122
+ const vault = openedByKey.get(identity.vault.key);
1123
+ if (vault === undefined)
1124
+ throw new Error("Qualified document is not available in this portfolio session.");
1125
+ const ids = vault.idsByDocumentId.get(identity.documentId);
1126
+ if (ids === undefined || ids.length === 0) {
1127
+ throw new Error("Qualified document is not available in this portfolio session.");
1128
+ }
1129
+ if (ids.length > 1) {
1130
+ throw new Error("Qualified document identity is ambiguous within its vault.");
1131
+ }
1132
+ const noteId = ids[0];
1133
+ if (noteId === undefined || !vault.rowsById.has(noteId)) {
1134
+ throw new Error("Qualified document is not available in this portfolio session.");
1135
+ }
1136
+ const result = vault.session.read(noteId, readOptions);
1137
+ return Object.freeze({
1138
+ ...result,
1139
+ identity,
1140
+ vault: vault.descriptor,
1141
+ repository: vault.repository,
1142
+ revision: result.truncated ? Object.freeze({ complete: false, sha256: null }) : Object.freeze({ complete: true, sha256: hash(result.content) })
1143
+ });
1144
+ };
1145
+ return Object.freeze({
1146
+ selectedVaultCount: selected.length,
1147
+ availableVaultCount: opened.length,
1148
+ noteCount: opened.reduce((count, vault) => count + vault.session.noteCount, 0),
1149
+ openDiagnostics: Object.freeze([...diagnostics]),
1150
+ search,
1151
+ read,
1152
+ close: () => {
1153
+ if (closePromise !== undefined)
1154
+ return closePromise;
1155
+ closed = true;
1156
+ closePromise = Promise.allSettled(opened.map(({ session }) => session.close())).then((settled) => {
1157
+ const errors = settled.filter((result) => result.status === "rejected").map(({ reason }) => reason);
1158
+ if (errors.length > 0)
1159
+ throw new AggregateError(errors, "One or more portfolio vault sessions failed to close.");
1160
+ });
1161
+ return closePromise;
1162
+ }
1163
+ });
1164
+ }
1165
+
1166
+ export { PORTFOLIO_REGISTRY_CONTRACT, MAX_PORTFOLIO_REGISTRY_BYTES, MAX_PORTFOLIO_VAULTS, MAX_AUTHORIZED_VAULTS, MAX_AUTHORITY_GROUPS, parsePortfolioRegistry, snapshotPortfolioRegistry, loadPortfolioRegistry, selectAuthorizedVaults, resolvePortfolioVault, validateResolvedPortfolioVaults, resolveAuthorizedVaults, DEFAULT_PORTFOLIO_AUDIT_ISSUES, MAX_PORTFOLIO_AUDIT_ISSUES, auditKnowledgePortfolio, MAX_PORTFOLIO_SEARCH_CONCURRENCY, MAX_PORTFOLIO_REVISION_BYTES, PortfolioOpenError, PortfolioSearchError, openKnowledgePortfolio };