@hawkeyexl/manni 4.3.0 → 4.4.0

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 (224) hide show
  1. package/README.md +9 -1
  2. package/dist/batch-DFRCLY77.js +36 -0
  3. package/dist/build-Q5C6SX5J.js +31 -0
  4. package/dist/calibrate-XQILKXS3.js +31 -0
  5. package/dist/capture-JQMOC3BW.js +32 -0
  6. package/dist/check-HYNGXYJV.js +18 -0
  7. package/dist/check-HYNGXYJV.js.map +1 -0
  8. package/dist/check-NFZQLJKN.js +27 -0
  9. package/dist/check-NFZQLJKN.js.map +1 -0
  10. package/dist/check-U54XV6BP.js +40 -0
  11. package/dist/check-U54XV6BP.js.map +1 -0
  12. package/dist/chunk-252LNOEG.js +622 -0
  13. package/dist/chunk-252LNOEG.js.map +1 -0
  14. package/dist/chunk-2H7UOFLK.js +11 -0
  15. package/dist/chunk-2H7UOFLK.js.map +1 -0
  16. package/dist/chunk-2U3ORT6D.js +230 -0
  17. package/dist/chunk-2U3ORT6D.js.map +1 -0
  18. package/dist/chunk-3AFUD75K.js +23 -0
  19. package/dist/chunk-3AFUD75K.js.map +1 -0
  20. package/dist/{chunk-3R23C5DT.js → chunk-4TSWU2SU.js} +2 -14
  21. package/dist/chunk-4TSWU2SU.js.map +1 -0
  22. package/dist/chunk-5KHIFVD2.js +1192 -0
  23. package/dist/chunk-5KHIFVD2.js.map +1 -0
  24. package/dist/chunk-5YXBLLAC.js +2165 -0
  25. package/dist/chunk-5YXBLLAC.js.map +1 -0
  26. package/dist/chunk-644JVKJ4.js +3872 -0
  27. package/dist/chunk-644JVKJ4.js.map +1 -0
  28. package/dist/chunk-6OYEI2VV.js +88 -0
  29. package/dist/chunk-6OYEI2VV.js.map +1 -0
  30. package/dist/chunk-77AINBYM.js +19 -0
  31. package/dist/chunk-77AINBYM.js.map +1 -0
  32. package/dist/chunk-77HX34KD.js +1287 -0
  33. package/dist/chunk-77HX34KD.js.map +1 -0
  34. package/dist/{chunk-MOQXWXSG.js → chunk-7QMCHA4N.js} +32 -240
  35. package/dist/chunk-7QMCHA4N.js.map +1 -0
  36. package/dist/chunk-BUXDLRDT.js +455 -0
  37. package/dist/chunk-BUXDLRDT.js.map +1 -0
  38. package/dist/chunk-C6OH6CNF.js +388 -0
  39. package/dist/chunk-C6OH6CNF.js.map +1 -0
  40. package/dist/chunk-CK7SKVHR.js +1042 -0
  41. package/dist/chunk-CK7SKVHR.js.map +1 -0
  42. package/dist/chunk-CYWHFSRT.js +6737 -0
  43. package/dist/chunk-CYWHFSRT.js.map +1 -0
  44. package/dist/chunk-ERMSO34F.js +17 -0
  45. package/dist/chunk-ERMSO34F.js.map +1 -0
  46. package/dist/{chunk-IOYXPBCL.js → chunk-G3T47CFV.js} +1 -7
  47. package/dist/{chunk-IOYXPBCL.js.map → chunk-G3T47CFV.js.map} +1 -1
  48. package/dist/chunk-G7NJSREW.js +979 -0
  49. package/dist/chunk-G7NJSREW.js.map +1 -0
  50. package/dist/chunk-GLP5PSXB.js +72 -0
  51. package/dist/chunk-GLP5PSXB.js.map +1 -0
  52. package/dist/chunk-H2DLH2JI.js +198 -0
  53. package/dist/chunk-H2DLH2JI.js.map +1 -0
  54. package/dist/chunk-H6FZ66XR.js +634 -0
  55. package/dist/chunk-H6FZ66XR.js.map +1 -0
  56. package/dist/{chunk-CI2GOU3A.js → chunk-IEQHSCZG.js} +56 -137
  57. package/dist/chunk-IEQHSCZG.js.map +1 -0
  58. package/dist/chunk-IKEWBMCN.js +98 -0
  59. package/dist/chunk-IKEWBMCN.js.map +1 -0
  60. package/dist/chunk-JOHKIAVH.js +5440 -0
  61. package/dist/chunk-JOHKIAVH.js.map +1 -0
  62. package/dist/chunk-JVTNMVYU.js +10921 -0
  63. package/dist/chunk-JVTNMVYU.js.map +1 -0
  64. package/dist/chunk-K3D6FPV6.js +164 -0
  65. package/dist/chunk-K3D6FPV6.js.map +1 -0
  66. package/dist/{chunk-MFDD3YGU.js → chunk-KCGV52AM.js} +69 -48
  67. package/dist/{chunk-MFDD3YGU.js.map → chunk-KCGV52AM.js.map} +1 -1
  68. package/dist/chunk-LAKAWRV5.js +1651 -0
  69. package/dist/chunk-LAKAWRV5.js.map +1 -0
  70. package/dist/chunk-LJ2RDH3F.js +49 -0
  71. package/dist/chunk-LJ2RDH3F.js.map +1 -0
  72. package/dist/chunk-M2JCK5F6.js +805 -0
  73. package/dist/chunk-M2JCK5F6.js.map +1 -0
  74. package/dist/chunk-MNH7QO4Z.js +15 -0
  75. package/dist/chunk-MNH7QO4Z.js.map +1 -0
  76. package/dist/{chunk-RVVCA4WL.js → chunk-MYC3O5EJ.js} +4 -4
  77. package/dist/chunk-MYMBCTDE.js +387 -0
  78. package/dist/chunk-MYMBCTDE.js.map +1 -0
  79. package/dist/chunk-NCPWXHDO.js +683 -0
  80. package/dist/chunk-NCPWXHDO.js.map +1 -0
  81. package/dist/chunk-NWZ2SLYL.js +2957 -0
  82. package/dist/chunk-NWZ2SLYL.js.map +1 -0
  83. package/dist/chunk-OBFENF3U.js +934 -0
  84. package/dist/chunk-OBFENF3U.js.map +1 -0
  85. package/dist/chunk-OWBRDNRZ.js +16 -0
  86. package/dist/chunk-OWBRDNRZ.js.map +1 -0
  87. package/dist/{chunk-KTSJ2ZHA.js → chunk-PAU352SV.js} +5 -3
  88. package/dist/{chunk-KTSJ2ZHA.js.map → chunk-PAU352SV.js.map} +1 -1
  89. package/dist/chunk-PCE7QHXJ.js +68 -0
  90. package/dist/chunk-PCE7QHXJ.js.map +1 -0
  91. package/dist/chunk-PCHZY2I7.js +299 -0
  92. package/dist/chunk-PCHZY2I7.js.map +1 -0
  93. package/dist/chunk-PHF3D7ER.js +1042 -0
  94. package/dist/chunk-PHF3D7ER.js.map +1 -0
  95. package/dist/chunk-Q35YIDBV.js +1202 -0
  96. package/dist/chunk-Q35YIDBV.js.map +1 -0
  97. package/dist/chunk-RLR4TKUZ.js +4398 -0
  98. package/dist/chunk-RLR4TKUZ.js.map +1 -0
  99. package/dist/chunk-RNVYGWIC.js +30 -0
  100. package/dist/chunk-RNVYGWIC.js.map +1 -0
  101. package/dist/chunk-SE4JN5XN.js +19 -0
  102. package/dist/chunk-SE4JN5XN.js.map +1 -0
  103. package/dist/chunk-TIUTIOFB.js +25 -0
  104. package/dist/chunk-TIUTIOFB.js.map +1 -0
  105. package/dist/chunk-TU2WUBVZ.js +2 -0
  106. package/dist/chunk-TU2WUBVZ.js.map +1 -0
  107. package/dist/chunk-TZOYZK2E.js +98 -0
  108. package/dist/chunk-TZOYZK2E.js.map +1 -0
  109. package/dist/chunk-UKZNLVJW.js +17 -0
  110. package/dist/chunk-UKZNLVJW.js.map +1 -0
  111. package/dist/chunk-UZC7LDAQ.js +178 -0
  112. package/dist/chunk-UZC7LDAQ.js.map +1 -0
  113. package/dist/chunk-V46OILVC.js +3090 -0
  114. package/dist/chunk-V46OILVC.js.map +1 -0
  115. package/dist/chunk-WQE54PNH.js +1975 -0
  116. package/dist/chunk-WQE54PNH.js.map +1 -0
  117. package/dist/chunk-X3BPBDGQ.js +52 -0
  118. package/dist/chunk-X3BPBDGQ.js.map +1 -0
  119. package/dist/chunk-XSLV76YK.js +54 -0
  120. package/dist/chunk-XSLV76YK.js.map +1 -0
  121. package/dist/{chunk-QROCFW5S.js → chunk-XYLUCIKQ.js} +3 -3
  122. package/dist/chunk-YEUYKR62.js +72 -0
  123. package/dist/chunk-YEUYKR62.js.map +1 -0
  124. package/dist/{chunk-ESJ5W7EV.js → chunk-YFQBFNQS.js} +8 -94
  125. package/dist/chunk-YFQBFNQS.js.map +1 -0
  126. package/dist/chunk-YIL4XLLJ.js +1661 -0
  127. package/dist/chunk-YIL4XLLJ.js.map +1 -0
  128. package/dist/chunk-YNHZHSSM.js +41 -0
  129. package/dist/chunk-YNHZHSSM.js.map +1 -0
  130. package/dist/chunk-YTCZRLTU.js +96 -0
  131. package/dist/chunk-YTCZRLTU.js.map +1 -0
  132. package/dist/chunk-YYBFZLAJ.js +374 -0
  133. package/dist/chunk-YYBFZLAJ.js.map +1 -0
  134. package/dist/chunk-ZXRAE55H.js +31 -0
  135. package/dist/chunk-ZXRAE55H.js.map +1 -0
  136. package/dist/cli-2QPR2OZG.js +232 -0
  137. package/dist/cli-2QPR2OZG.js.map +1 -0
  138. package/dist/cli-4IOPGZHR.js +1203 -0
  139. package/dist/cli-4IOPGZHR.js.map +1 -0
  140. package/dist/cli-5FL73WHA.js +346 -0
  141. package/dist/cli-5FL73WHA.js.map +1 -0
  142. package/dist/cli-C262QOWM.js +739 -0
  143. package/dist/cli-C262QOWM.js.map +1 -0
  144. package/dist/cli-LJLLIQB2.js +423 -0
  145. package/dist/cli-LJLLIQB2.js.map +1 -0
  146. package/dist/cli-OFPZ5MAS.js +626 -0
  147. package/dist/cli-OFPZ5MAS.js.map +1 -0
  148. package/dist/cli-SHSMSRQY.js +28 -0
  149. package/dist/cli-SHSMSRQY.js.map +1 -0
  150. package/dist/cli-SRHFUZHA.js +473 -0
  151. package/dist/cli-SRHFUZHA.js.map +1 -0
  152. package/dist/cli-URZQXL4K.js +552 -0
  153. package/dist/cli-URZQXL4K.js.map +1 -0
  154. package/dist/cli.d.ts +12 -2
  155. package/dist/cli.js +109 -7651
  156. package/dist/cli.js.map +1 -1
  157. package/dist/docmeta.js +15 -3
  158. package/dist/docmeta.js.map +1 -1
  159. package/dist/github-FE6KCNYJ.js +27 -0
  160. package/dist/github-FE6KCNYJ.js.map +1 -0
  161. package/dist/github-QXL3VMK2.js +32 -0
  162. package/dist/github-QXL3VMK2.js.map +1 -0
  163. package/dist/index.d.ts +65 -3
  164. package/dist/index.js +407 -337
  165. package/dist/index.js.map +1 -1
  166. package/dist/json-7WV7KL6H.js +17 -0
  167. package/dist/json-7WV7KL6H.js.map +1 -0
  168. package/dist/json-SGZ6ZRFD.js +35 -0
  169. package/dist/json-SGZ6ZRFD.js.map +1 -0
  170. package/dist/lint-DL4LJRAY.js +28 -0
  171. package/dist/lint-DL4LJRAY.js.map +1 -0
  172. package/dist/list-6BAXTZUM.js +29 -0
  173. package/dist/list-6BAXTZUM.js.map +1 -0
  174. package/dist/list-7BKNYPSB.js +31 -0
  175. package/dist/list-7BKNYPSB.js.map +1 -0
  176. package/dist/page-3U2GCFNE.js +41 -0
  177. package/dist/page-3U2GCFNE.js.map +1 -0
  178. package/dist/picker-3XF36MJF.js +14 -0
  179. package/dist/picker-3XF36MJF.js.map +1 -0
  180. package/dist/pretty-2H57QVTY.js +43 -0
  181. package/dist/pretty-2H57QVTY.js.map +1 -0
  182. package/dist/pretty-N6DO4OOL.js +37 -0
  183. package/dist/pretty-N6DO4OOL.js.map +1 -0
  184. package/dist/reporters-M6LJ2IYU.js +21 -0
  185. package/dist/reporters-M6LJ2IYU.js.map +1 -0
  186. package/dist/reporters-N6GHXSS7.js +42 -0
  187. package/dist/reporters-N6GHXSS7.js.map +1 -0
  188. package/dist/reporters-UCNEV7PH.js +71 -0
  189. package/dist/reporters-UCNEV7PH.js.map +1 -0
  190. package/dist/run-XZDAREJE.js +33 -0
  191. package/dist/run-XZDAREJE.js.map +1 -0
  192. package/dist/term-5ICGKPC3.js +1592 -0
  193. package/dist/term-5ICGKPC3.js.map +1 -0
  194. package/dist/types-Y6TXHM7M.js +10 -0
  195. package/dist/types-Y6TXHM7M.js.map +1 -0
  196. package/dist/validate-GQRM77YN.js +16 -0
  197. package/dist/validate-GQRM77YN.js.map +1 -0
  198. package/package.json +1 -1
  199. package/plugin/manni/.claude-plugin/plugin.json +9 -0
  200. package/plugin/manni/hooks/hooks.json +52 -0
  201. package/plugin/manni/skills/check/SKILL.md +20 -0
  202. package/plugin/manni/skills/evals/SKILL.md +27 -0
  203. package/plugin/manni/skills/fix/SKILL.md +56 -0
  204. package/plugin/manni/skills/setup/SKILL.md +41 -0
  205. package/dist/batch-7JJWXWSF.js +0 -18
  206. package/dist/calibrate-IA2ARFMH.js +0 -13
  207. package/dist/capture-C32JXIFN.js +0 -15
  208. package/dist/chunk-3R23C5DT.js.map +0 -1
  209. package/dist/chunk-CI2GOU3A.js.map +0 -1
  210. package/dist/chunk-DP2ZA3WH.js +0 -26518
  211. package/dist/chunk-DP2ZA3WH.js.map +0 -1
  212. package/dist/chunk-ESJ5W7EV.js.map +0 -1
  213. package/dist/chunk-MOQXWXSG.js.map +0 -1
  214. package/dist/chunk-O6RVQKK6.js +0 -27170
  215. package/dist/chunk-O6RVQKK6.js.map +0 -1
  216. package/dist/picker-6HRMETE7.js +0 -12
  217. package/plugin/tracevals/.claude-plugin/plugin.json +0 -9
  218. package/plugin/tracevals/hooks/hooks.json +0 -17
  219. /package/dist/{batch-7JJWXWSF.js.map → batch-DFRCLY77.js.map} +0 -0
  220. /package/dist/{calibrate-IA2ARFMH.js.map → build-Q5C6SX5J.js.map} +0 -0
  221. /package/dist/{capture-C32JXIFN.js.map → calibrate-XQILKXS3.js.map} +0 -0
  222. /package/dist/{picker-6HRMETE7.js.map → capture-JQMOC3BW.js.map} +0 -0
  223. /package/dist/{chunk-RVVCA4WL.js.map → chunk-MYC3O5EJ.js.map} +0 -0
  224. /package/dist/{chunk-QROCFW5S.js.map → chunk-XYLUCIKQ.js.map} +0 -0
@@ -0,0 +1,3872 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ LOCALIZATIONS_FILENAME,
4
+ STDIN,
5
+ UNDETERMINED,
6
+ analyzeDoc,
7
+ assertInputFormat,
8
+ canonicalLanguageTag,
9
+ deriveGraph,
10
+ documentBase,
11
+ documentSetPatterns,
12
+ emitLocalizations,
13
+ formatOf,
14
+ isLanguageTag,
15
+ isSafeRelativePath,
16
+ mergePage,
17
+ openMetaView,
18
+ ownMetadata,
19
+ parseLocalizations,
20
+ resolveDocumentSet,
21
+ routesFrom,
22
+ searchIndexFilename,
23
+ vectorIndexFilename
24
+ } from "./chunk-YIL4XLLJ.js";
25
+ import {
26
+ bundledShapesPath,
27
+ compactIri,
28
+ expandTerm,
29
+ loadGraph,
30
+ storeToQuads,
31
+ validateGraph
32
+ } from "./chunk-MYMBCTDE.js";
33
+ import {
34
+ COVERAGE_FIELDS,
35
+ DEFAULT_MODEL,
36
+ GRAPH_NOT_APPLICABLE_TO_VARIANT,
37
+ GRAPH_NOT_SOFTWARE_SUBJECT,
38
+ GraphError,
39
+ IIRDS_CREATOR,
40
+ IIRDS_FORMAT,
41
+ IIRDS_HAS_PARTY_ROLE,
42
+ IIRDS_HAS_RENDITION,
43
+ IIRDS_HAS_SUBJECT,
44
+ IIRDS_HAS_TOPIC_TYPE,
45
+ IIRDS_IIRDS_VERSION,
46
+ IIRDS_IS_PART_OF_PACKAGE,
47
+ IIRDS_LANGUAGE,
48
+ IIRDS_PACKAGE,
49
+ IIRDS_PARTY,
50
+ IIRDS_PRODUCT_VARIANT,
51
+ IIRDS_RELATES_TO_LIFECYCLE_PHASE,
52
+ IIRDS_RELATES_TO_PARTY,
53
+ IIRDS_RELATES_TO_PRODUCT_VARIANT,
54
+ IIRDS_RELATES_TO_VCARD,
55
+ IIRDS_RENDITION,
56
+ IIRDS_SOURCE,
57
+ IIRDS_TITLE,
58
+ IIRDS_TOPIC,
59
+ NS,
60
+ PREFIXES,
61
+ RDF_TYPE,
62
+ SECTION_COVERAGE_FIELDS,
63
+ SOFTWARE_SUBJECT_IRIS,
64
+ STDIN_PATH,
65
+ VCARD_NS,
66
+ VCARD_ORGANIZATION,
67
+ VCARD_ORGANIZATION_NAME,
68
+ byCodeUnit,
69
+ loadRunConfig,
70
+ profileFor,
71
+ withPrefix
72
+ } from "./chunk-M2JCK5F6.js";
73
+ import {
74
+ mergeMetaProvenance
75
+ } from "./chunk-K3D6FPV6.js";
76
+ import {
77
+ isMissing,
78
+ spliceManifestValue
79
+ } from "./chunk-RLR4TKUZ.js";
80
+ import {
81
+ urlManifestMessage,
82
+ writeFileAtomic
83
+ } from "./chunk-JVTNMVYU.js";
84
+ import {
85
+ assertKnownProvider,
86
+ assertLocalFlag,
87
+ assertModelHasProvider,
88
+ localNotice,
89
+ noticeOnce,
90
+ providerSpecFor,
91
+ resolveIdentity,
92
+ selectProvider,
93
+ warn
94
+ } from "./chunk-Q35YIDBV.js";
95
+ import {
96
+ errorMessage
97
+ } from "./chunk-G3T47CFV.js";
98
+
99
+ // src/graph/commands/export.ts
100
+ import { createHash } from "node:crypto";
101
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
102
+ import { dirname, join, resolve as resolve3 } from "node:path";
103
+
104
+ // src/graph/core/emit-jsonld.ts
105
+ var XSD_STRING = `${NS.xsd}string`;
106
+ function renderValue(term) {
107
+ if (term.kind === "iri") {
108
+ return {
109
+ value: { "@id": term.value },
110
+ kind: 0,
111
+ primary: term.value,
112
+ datatype: ""
113
+ };
114
+ }
115
+ if (term.datatype && term.datatype !== XSD_STRING) {
116
+ return {
117
+ value: { "@value": term.value, "@type": compactIri(term.datatype) },
118
+ kind: 1,
119
+ primary: term.value,
120
+ datatype: term.datatype
121
+ };
122
+ }
123
+ return { value: term.value, kind: 1, primary: term.value, datatype: "" };
124
+ }
125
+ function compareRendered(a, b) {
126
+ if (a.kind !== b.kind) return a.kind - b.kind;
127
+ const p = byCodeUnit(a.primary, b.primary);
128
+ if (p !== 0) return p;
129
+ return byCodeUnit(a.datatype, b.datatype);
130
+ }
131
+ function collapse(values) {
132
+ const rendered = [...values].sort(compareRendered).map((v) => v.value);
133
+ return rendered.length === 1 ? rendered[0] : rendered;
134
+ }
135
+ function emitJsonLd(quads) {
136
+ const context = {};
137
+ for (const [prefix, ns] of PREFIXES) context[prefix] = ns;
138
+ const subjects = /* @__PURE__ */ new Map();
139
+ for (const quad of quads) {
140
+ let preds = subjects.get(quad.s);
141
+ if (!preds) {
142
+ preds = /* @__PURE__ */ new Map();
143
+ subjects.set(quad.s, preds);
144
+ }
145
+ let terms = preds.get(quad.p);
146
+ if (!terms) {
147
+ terms = [];
148
+ preds.set(quad.p, terms);
149
+ }
150
+ terms.push(quad.o);
151
+ }
152
+ const graph = [...subjects.entries()].sort(([a], [b]) => byCodeUnit(a, b)).map(([subject, preds]) => {
153
+ const node = { "@id": subject };
154
+ const types = preds.get(RDF_TYPE);
155
+ if (types) {
156
+ const compacted = types.map((t) => compactIri(t.value)).sort(byCodeUnit);
157
+ node["@type"] = compacted.length === 1 ? compacted[0] : compacted;
158
+ }
159
+ const predEntries = [...preds.entries()].filter(([p]) => p !== RDF_TYPE).sort(([a], [b]) => byCodeUnit(a, b));
160
+ for (const [p, terms] of predEntries) {
161
+ node[compactIri(p)] = collapse(terms.map(renderValue));
162
+ }
163
+ return node;
164
+ });
165
+ return `${JSON.stringify({ "@context": context, "@graph": graph }, null, 2)}
166
+ `;
167
+ }
168
+
169
+ // src/graph/core/emit-rdfxml.ts
170
+ var XSD_STRING2 = `${NS.xsd}string`;
171
+ function escapeText(value) {
172
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
173
+ }
174
+ function escapeAttr(value) {
175
+ return escapeText(value).replace(/"/g, "&quot;");
176
+ }
177
+ function toQName(iri2, prefixes) {
178
+ let best;
179
+ let bestNs = "";
180
+ for (const [prefix, ns] of prefixes) {
181
+ if (iri2.startsWith(ns) && iri2.length > ns.length && ns.length > bestNs.length) {
182
+ best = `${prefix}:${iri2.slice(ns.length)}`;
183
+ bestNs = ns;
184
+ }
185
+ }
186
+ if (best === void 0) {
187
+ throw new Error(`emitRdfXml: no prefix covers the IRI ${iri2}`);
188
+ }
189
+ return best;
190
+ }
191
+ function compareTerms(a, b) {
192
+ if (a.kind !== b.kind) return a.kind === "iri" ? -1 : 1;
193
+ const v = byCodeUnit(a.value, b.value);
194
+ if (v !== 0) return v;
195
+ const da = a.kind === "literal" ? a.datatype ?? "" : "";
196
+ const db = b.kind === "literal" ? b.datatype ?? "" : "";
197
+ return byCodeUnit(da, db);
198
+ }
199
+ function emitRdfXml(quads, prefixes) {
200
+ const sortedPrefixes = Object.entries(prefixes).sort(
201
+ ([a], [b]) => byCodeUnit(a, b)
202
+ );
203
+ const lines = ['<?xml version="1.0" encoding="UTF-8"?>'];
204
+ lines.push("<rdf:RDF");
205
+ sortedPrefixes.forEach(([prefix, ns], i) => {
206
+ const tail = i === sortedPrefixes.length - 1 ? ">" : "";
207
+ lines.push(` xmlns:${prefix}="${escapeAttr(ns)}"${tail}`);
208
+ });
209
+ const subjects = /* @__PURE__ */ new Map();
210
+ for (const quad of quads) {
211
+ let preds = subjects.get(quad.s);
212
+ if (!preds) {
213
+ preds = /* @__PURE__ */ new Map();
214
+ subjects.set(quad.s, preds);
215
+ }
216
+ let terms = preds.get(quad.p);
217
+ if (!terms) {
218
+ terms = [];
219
+ preds.set(quad.p, terms);
220
+ }
221
+ terms.push(quad.o);
222
+ }
223
+ const sortedSubjects = [...subjects.entries()].sort(
224
+ ([a], [b]) => byCodeUnit(a, b)
225
+ );
226
+ for (const [subject, preds] of sortedSubjects) {
227
+ lines.push(` <rdf:Description rdf:about="${escapeAttr(subject)}">`);
228
+ const sortedPreds = [...preds.entries()].sort(
229
+ ([a], [b]) => byCodeUnit(a, b)
230
+ );
231
+ for (const [p, terms] of sortedPreds) {
232
+ const qname = toQName(p, sortedPrefixes);
233
+ for (const term of [...terms].sort(compareTerms)) {
234
+ if (term.kind === "iri") {
235
+ lines.push(
236
+ ` <${qname} rdf:resource="${escapeAttr(term.value)}"/>`
237
+ );
238
+ } else if (term.datatype && term.datatype !== XSD_STRING2) {
239
+ lines.push(
240
+ ` <${qname} rdf:datatype="${escapeAttr(term.datatype)}">${escapeText(term.value)}</${qname}>`
241
+ );
242
+ } else {
243
+ lines.push(` <${qname}>${escapeText(term.value)}</${qname}>`);
244
+ }
245
+ }
246
+ }
247
+ lines.push(" </rdf:Description>");
248
+ }
249
+ lines.push("</rdf:RDF>");
250
+ return `${lines.join("\n")}
251
+ `;
252
+ }
253
+
254
+ // src/graph/core/iirds-package.ts
255
+ import { existsSync } from "node:fs";
256
+ import { resolve } from "node:path";
257
+ import { DataFactory } from "n3";
258
+
259
+ // src/graph/core/zip.ts
260
+ import { crc32, deflateRawSync } from "node:zlib";
261
+ function escapesArchiveRoot(name) {
262
+ const segments = name.split(/[/\\]/);
263
+ return name.startsWith("/") || name.startsWith("\\") || /^[A-Za-z]:/.test(name) || segments.includes("..");
264
+ }
265
+ var DOS_DATE = 33;
266
+ var DOS_TIME = 0;
267
+ var VERSION = 20;
268
+ function writeZip(entries) {
269
+ const locals = [];
270
+ const centrals = [];
271
+ let offset = 0;
272
+ for (const entry2 of entries) {
273
+ if (escapesArchiveRoot(entry2.name)) {
274
+ throw new GraphError(
275
+ `Refusing ZIP entry "${entry2.name}": an archive entry may not be absolute or climb above the archive root.`
276
+ );
277
+ }
278
+ const name = Buffer.from(entry2.name, "utf8");
279
+ const flags = name.some((b) => b > 127) ? 2048 : 0;
280
+ const method = entry2.store ? 0 : 8;
281
+ const stored = entry2.store ? entry2.data : deflateRawSync(entry2.data);
282
+ const crc = crc32(entry2.data) >>> 0;
283
+ const local = Buffer.alloc(30);
284
+ local.writeUInt32LE(67324752, 0);
285
+ local.writeUInt16LE(VERSION, 4);
286
+ local.writeUInt16LE(flags, 6);
287
+ local.writeUInt16LE(method, 8);
288
+ local.writeUInt16LE(DOS_TIME, 10);
289
+ local.writeUInt16LE(DOS_DATE, 12);
290
+ local.writeUInt32LE(crc, 14);
291
+ local.writeUInt32LE(stored.length, 18);
292
+ local.writeUInt32LE(entry2.data.length, 22);
293
+ local.writeUInt16LE(name.length, 26);
294
+ local.writeUInt16LE(0, 28);
295
+ locals.push(local, name, stored);
296
+ const central = Buffer.alloc(46);
297
+ central.writeUInt32LE(33639248, 0);
298
+ central.writeUInt16LE(VERSION, 4);
299
+ central.writeUInt16LE(VERSION, 6);
300
+ central.writeUInt16LE(flags, 8);
301
+ central.writeUInt16LE(method, 10);
302
+ central.writeUInt16LE(DOS_TIME, 12);
303
+ central.writeUInt16LE(DOS_DATE, 14);
304
+ central.writeUInt32LE(crc, 16);
305
+ central.writeUInt32LE(stored.length, 20);
306
+ central.writeUInt32LE(entry2.data.length, 24);
307
+ central.writeUInt16LE(name.length, 28);
308
+ central.writeUInt16LE(0, 30);
309
+ central.writeUInt16LE(0, 32);
310
+ central.writeUInt16LE(0, 34);
311
+ central.writeUInt16LE(0, 36);
312
+ central.writeUInt32LE(0, 38);
313
+ central.writeUInt32LE(offset, 42);
314
+ centrals.push(central, name);
315
+ offset += local.length + name.length + stored.length;
316
+ }
317
+ const centralBuf = Buffer.concat(centrals);
318
+ const eocd = Buffer.alloc(22);
319
+ eocd.writeUInt32LE(101010256, 0);
320
+ eocd.writeUInt16LE(0, 4);
321
+ eocd.writeUInt16LE(0, 6);
322
+ eocd.writeUInt16LE(entries.length, 8);
323
+ eocd.writeUInt16LE(entries.length, 10);
324
+ eocd.writeUInt32LE(centralBuf.length, 12);
325
+ eocd.writeUInt32LE(offset, 16);
326
+ eocd.writeUInt16LE(0, 20);
327
+ return Buffer.concat([...locals, centralBuf, eocd]);
328
+ }
329
+
330
+ // src/graph/core/iirds-package.ts
331
+ var GRAPH_DOCUMENT = `${NS.graph}Document`;
332
+ var XSD_STRING3 = `${NS.xsd}string`;
333
+ var CARRIED = [
334
+ IIRDS_HAS_TOPIC_TYPE,
335
+ IIRDS_HAS_SUBJECT,
336
+ IIRDS_RELATES_TO_PRODUCT_VARIANT,
337
+ IIRDS_RELATES_TO_LIFECYCLE_PHASE
338
+ ];
339
+ var iri = (value) => ({ kind: "iri", value });
340
+ var lit = (value, datatype) => datatype ? { kind: "literal", value, datatype } : { kind: "literal", value };
341
+ function firstObject(store, s, p) {
342
+ const q = store.getQuads(DataFactory.namedNode(s), DataFactory.namedNode(p), null, null)[0];
343
+ return q?.object.value;
344
+ }
345
+ function projectPackage(store, opts, cwd) {
346
+ const quads = [];
347
+ const contentFiles = [];
348
+ const warnings = [];
349
+ const add = (s, p, o) => {
350
+ quads.push({ s, p, o });
351
+ };
352
+ const pkg = `${opts.baseIri}package`;
353
+ add(pkg, RDF_TYPE, iri(IIRDS_PACKAGE));
354
+ add(pkg, IIRDS_IIRDS_VERSION, lit(opts.version));
355
+ add(pkg, IIRDS_TITLE, lit(opts.title ?? "manni graph export"));
356
+ if (opts.creator) {
357
+ const party = `${opts.baseIri}party/creator`;
358
+ const org = `${opts.baseIri}vcard/creator`;
359
+ add(pkg, IIRDS_RELATES_TO_PARTY, iri(party));
360
+ add(party, RDF_TYPE, iri(IIRDS_PARTY));
361
+ add(party, IIRDS_HAS_PARTY_ROLE, iri(IIRDS_CREATOR));
362
+ add(party, IIRDS_RELATES_TO_VCARD, iri(org));
363
+ add(org, RDF_TYPE, iri(VCARD_ORGANIZATION));
364
+ add(org, VCARD_ORGANIZATION_NAME, lit(opts.creator));
365
+ }
366
+ const docs = store.getQuads(null, DataFactory.namedNode(RDF_TYPE), DataFactory.namedNode(GRAPH_DOCUMENT), null).map((q) => q.subject.value).sort(byCodeUnit);
367
+ const variants = /* @__PURE__ */ new Set();
368
+ for (const doc of docs) {
369
+ add(doc, RDF_TYPE, iri(IIRDS_TOPIC));
370
+ add(doc, IIRDS_IS_PART_OF_PACKAGE, iri(pkg));
371
+ const title = firstObject(store, doc, `${NS.dcterms}title`);
372
+ if (title) add(doc, IIRDS_TITLE, lit(title));
373
+ const language = firstObject(store, doc, `${NS.dcterms}language`);
374
+ if (language) add(doc, IIRDS_LANGUAGE, lit(language));
375
+ const path2 = firstObject(store, doc, `${NS.graph}path`);
376
+ if (path2) {
377
+ if (escapesArchiveRoot(path2)) {
378
+ throw new GraphError(
379
+ `Cannot package "${path2}": an iiRDS package has one root, and a path that climbs above it has no place inside it. Re-run \`manni graph build\` from a directory that contains every document, then export again.`
380
+ );
381
+ }
382
+ const absPath = resolve(cwd, path2);
383
+ if (!existsSync(absPath)) {
384
+ throw new GraphError(
385
+ `Content file for ${path2} not found at ${absPath} \u2014 re-run \`manni graph build\`, or the source moved.`
386
+ );
387
+ }
388
+ const zipPath = `content/${path2}`;
389
+ const rendition = `${doc}/rendition`;
390
+ add(doc, IIRDS_HAS_RENDITION, iri(rendition));
391
+ add(rendition, RDF_TYPE, iri(IIRDS_RENDITION));
392
+ add(rendition, IIRDS_SOURCE, lit(zipPath));
393
+ add(rendition, IIRDS_FORMAT, lit("text/markdown"));
394
+ contentFiles.push({ zipPath, absPath });
395
+ } else {
396
+ warnings.push(
397
+ `Document ${doc} has no graph:path \u2014 no rendition emitted.`
398
+ );
399
+ }
400
+ for (const pred of CARRIED) {
401
+ for (const q of store.getQuads(
402
+ DataFactory.namedNode(doc),
403
+ DataFactory.namedNode(pred),
404
+ null,
405
+ null
406
+ )) {
407
+ const o = q.object;
408
+ if (o.termType === "Literal") {
409
+ const dt = o.datatype.value;
410
+ add(doc, pred, lit(o.value, dt !== XSD_STRING3 ? dt : void 0));
411
+ } else {
412
+ add(doc, pred, iri(o.value));
413
+ if (pred === IIRDS_RELATES_TO_PRODUCT_VARIANT) variants.add(o.value);
414
+ }
415
+ }
416
+ }
417
+ }
418
+ for (const v of [...variants].sort(byCodeUnit)) {
419
+ add(v, RDF_TYPE, iri(IIRDS_PRODUCT_VARIANT));
420
+ const label = firstObject(store, v, `${NS.dcterms}title`);
421
+ if (label) add(v, `${NS.dcterms}title`, lit(label));
422
+ }
423
+ const prefixes = {
424
+ dcterms: NS.dcterms,
425
+ iirds: NS.iirds,
426
+ iirdsSft: NS.iirdsSft,
427
+ rdf: NS.rdf,
428
+ vcard: VCARD_NS
429
+ };
430
+ return { quads, prefixes, contentFiles, warnings };
431
+ }
432
+
433
+ // src/graph/core/search-index.ts
434
+ import { existsSync as existsSync2, readFileSync } from "node:fs";
435
+ import { resolve as resolve2 } from "node:path";
436
+
437
+ // src/graph/runtime/resolve.ts
438
+ var GRAPH_PATH = `${NS.graph}path`;
439
+ var DCTERMS_TITLE = `${NS.dcterms}title`;
440
+ var DCTERMS_HAS_PART = `${NS.dcterms}hasPart`;
441
+ var GRAPH_LEVEL = `${NS.graph}level`;
442
+ var GRAPH_ORDER = `${NS.graph}order`;
443
+ function splitFragment(iri2) {
444
+ const hash = iri2.indexOf("#");
445
+ return hash < 0 ? { doc: iri2 } : { doc: iri2.slice(0, hash), fragment: iri2.slice(hash + 1) };
446
+ }
447
+ function fencedLines(lines) {
448
+ const mask = new Array(lines.length).fill(false);
449
+ let open;
450
+ for (const [i, line] of lines.entries()) {
451
+ const m = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
452
+ const delimiter = m?.[1];
453
+ const info = m?.[2];
454
+ const isFence = delimiter !== void 0 && info !== void 0;
455
+ if (open === void 0) {
456
+ if (isFence && !(delimiter.startsWith("`") && info.includes("`"))) {
457
+ open = delimiter;
458
+ mask[i] = true;
459
+ }
460
+ continue;
461
+ }
462
+ mask[i] = true;
463
+ if (isFence && delimiter[0] === open[0] && delimiter.length >= open.length && info.trim() === "") {
464
+ open = void 0;
465
+ }
466
+ }
467
+ return mask;
468
+ }
469
+ var ATX = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?[ \t]*$/;
470
+ var SETEXT_UNDERLINE = /^ {0,3}(=+|-+)[ \t]*$/;
471
+ function isParagraphLine(line) {
472
+ if (line.trim() === "") return false;
473
+ if (ATX.test(line)) return false;
474
+ if (/^ {0,3}([-+*]|\d{1,9}[.)])([ \t]|$)/.test(line)) return false;
475
+ if (/^ {0,3}>/.test(line)) return false;
476
+ if (/^ {0,3}([-*_])([ \t]*\1){2,}[ \t]*$/.test(line)) return false;
477
+ return !/^ {4}/.test(line);
478
+ }
479
+ function frontmatterLength(lines) {
480
+ if (lines[0]?.trim() !== "---") return 0;
481
+ for (let i = 1; i < lines.length; i++) {
482
+ const line = lines[i]?.trim();
483
+ if (line === "---" || line === "...") return i + 1;
484
+ }
485
+ return 0;
486
+ }
487
+ function sourceHeadings(lines) {
488
+ const fenced = fencedLines(lines);
489
+ const skip = frontmatterLength(lines);
490
+ const headings = [];
491
+ let paragraph = -1;
492
+ for (const [i, line] of lines.entries()) {
493
+ if (i < skip || fenced[i]) {
494
+ paragraph = -1;
495
+ continue;
496
+ }
497
+ const atx = ATX.exec(line);
498
+ const hashes = atx?.[1];
499
+ if (hashes !== void 0) {
500
+ const text = (atx?.[2] ?? "").replace(/(^|[ \t]+)#+$/, "");
501
+ headings.push({ start: i, level: hashes.length, text });
502
+ paragraph = -1;
503
+ continue;
504
+ }
505
+ const underline = SETEXT_UNDERLINE.exec(line)?.[1];
506
+ if (underline !== void 0 && paragraph >= 0) {
507
+ headings.push({
508
+ start: paragraph,
509
+ level: underline.startsWith("=") ? 1 : 2,
510
+ text: lines.slice(paragraph, i).join("\n")
511
+ });
512
+ paragraph = -1;
513
+ continue;
514
+ }
515
+ if (isParagraphLine(line)) {
516
+ if (paragraph < 0) paragraph = i;
517
+ } else {
518
+ paragraph = -1;
519
+ }
520
+ }
521
+ return headings;
522
+ }
523
+ var ENTITIES = {
524
+ amp: "&",
525
+ lt: "<",
526
+ gt: ">",
527
+ quot: '"',
528
+ apos: "'",
529
+ nbsp: "\xA0"
530
+ };
531
+ function decodeEntities(text) {
532
+ return text.replace(
533
+ /&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi,
534
+ (whole, name) => {
535
+ if (name.startsWith("#")) {
536
+ const hex = name[1] === "x" || name[1] === "X";
537
+ const code = Number.parseInt(name.slice(hex ? 2 : 1), hex ? 16 : 10);
538
+ return Number.isNaN(code) || code > 1114111 ? whole : String.fromCodePoint(code);
539
+ }
540
+ return ENTITIES[name.toLowerCase()] ?? whole;
541
+ }
542
+ );
543
+ }
544
+ function headingKey(text) {
545
+ return text.replace(/[`*_~\\[\]]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
546
+ }
547
+ function stripInline(raw) {
548
+ return decodeEntities(
549
+ raw.replace(/!?\[([^\]]*)\]\((?:[^()]|\([^()]*\))*\)/g, "$1").replace(/!?\[([^\]]*)\]\[[^\]]*\]/g, "$1").replace(/<((?:https?|mailto):[^>\s]+)>/gi, "$1")
550
+ );
551
+ }
552
+ function documentSectionOrder(graph, docIri) {
553
+ const ordered = [];
554
+ const seen = /* @__PURE__ */ new Set();
555
+ const childrenOf = (iri2) => graph.out(iri2, DCTERMS_HAS_PART).map((e) => e.target).sort((a, b) => {
556
+ const oa = Number.parseInt(graph.literal(a, GRAPH_ORDER) ?? "", 10);
557
+ const ob = Number.parseInt(graph.literal(b, GRAPH_ORDER) ?? "", 10);
558
+ if (Number.isNaN(oa) || Number.isNaN(ob) || oa === ob) {
559
+ return a < b ? -1 : a > b ? 1 : 0;
560
+ }
561
+ return oa - ob;
562
+ });
563
+ const walk = (iri2) => {
564
+ for (const child of childrenOf(iri2)) {
565
+ if (seen.has(child)) continue;
566
+ seen.add(child);
567
+ ordered.push(child);
568
+ walk(child);
569
+ }
570
+ };
571
+ walk(docIri);
572
+ return ordered;
573
+ }
574
+ function sectionOccurrences(graph, docIri) {
575
+ const occurrences = /* @__PURE__ */ new Map();
576
+ const counts = /* @__PURE__ */ new Map();
577
+ for (const section of documentSectionOrder(graph, docIri)) {
578
+ const rawTitle = graph.literal(section, DCTERMS_TITLE);
579
+ if (rawTitle === void 0) {
580
+ occurrences.set(section, 0);
581
+ continue;
582
+ }
583
+ const title = headingKey(rawTitle);
584
+ const level = graph.literal(section, GRAPH_LEVEL) ?? "";
585
+ let byLevel = counts.get(title);
586
+ if (!byLevel) {
587
+ byLevel = /* @__PURE__ */ new Map();
588
+ counts.set(title, byLevel);
589
+ }
590
+ const seen = byLevel.get(level) ?? 0;
591
+ occurrences.set(section, seen);
592
+ byLevel.set(level, seen + 1);
593
+ }
594
+ return occurrences;
595
+ }
596
+ function sectionOccurrence(graph, sectionIri) {
597
+ const { doc } = splitFragment(sectionIri);
598
+ return sectionOccurrences(graph, doc).get(sectionIri) ?? 0;
599
+ }
600
+ function documentPreamble(markdown) {
601
+ const lines = markdown.split(/\r?\n/);
602
+ const end = sourceHeadings(lines)[0]?.start ?? lines.length;
603
+ const text = lines.slice(0, end).join("\n").trim();
604
+ return text === "" ? void 0 : text;
605
+ }
606
+ function sliceSection(markdown, title, level, occurrence = 0) {
607
+ return slice(markdown, title, level, occurrence, false);
608
+ }
609
+ function sectionOwnText(markdown, title, level, occurrence = 0) {
610
+ return slice(markdown, title, level, occurrence, true);
611
+ }
612
+ function slice(markdown, title, level, occurrence, ownTextOnly) {
613
+ const lines = markdown.split(/\r?\n/);
614
+ const headings = sourceHeadings(lines);
615
+ const wanted = headingKey(title);
616
+ let remaining = occurrence;
617
+ let found = -1;
618
+ for (const [n, heading] of headings.entries()) {
619
+ if (level !== void 0 && heading.level !== level) continue;
620
+ if (headingKey(stripInline(heading.text)) !== wanted) continue;
621
+ if (remaining > 0) {
622
+ remaining -= 1;
623
+ continue;
624
+ }
625
+ found = n;
626
+ break;
627
+ }
628
+ const match = headings[found];
629
+ if (match === void 0) return void 0;
630
+ const next = headings.slice(found + 1).find((h) => ownTextOnly || h.level <= match.level);
631
+ const end = next?.start ?? lines.length;
632
+ return lines.slice(match.start, end).join("\n").trimEnd();
633
+ }
634
+ function createFetchResolver(graph, options = {}) {
635
+ const doFetch = options.fetch ?? ((url) => globalThis.fetch(url));
636
+ const toUrl = options.pathToUrl ?? ((path2) => `${options.baseUrl ?? ""}${path2}`);
637
+ const cache = /* @__PURE__ */ new Map();
638
+ const occurrenceCache = /* @__PURE__ */ new Map();
639
+ const occurrenceOf = (docIri, sectionIri) => {
640
+ let map = occurrenceCache.get(docIri);
641
+ if (!map) {
642
+ map = sectionOccurrences(graph, docIri);
643
+ occurrenceCache.set(docIri, map);
644
+ }
645
+ return map.get(sectionIri) ?? 0;
646
+ };
647
+ const fetchDoc = (url) => {
648
+ let pending = cache.get(url);
649
+ if (!pending) {
650
+ pending = doFetch(url).then((r) => r.ok ? r.text() : void 0).catch(() => void 0);
651
+ cache.set(url, pending);
652
+ }
653
+ return pending;
654
+ };
655
+ return {
656
+ async resolve(iri2) {
657
+ const { doc, fragment } = splitFragment(iri2);
658
+ const path2 = graph.literal(doc, GRAPH_PATH);
659
+ if (!path2) return void 0;
660
+ const sourceUrl = toUrl(path2);
661
+ const body = await fetchDoc(sourceUrl);
662
+ const title = graph.literal(iri2, DCTERMS_TITLE);
663
+ if (body === void 0) {
664
+ options.trace?.resolutions.push({
665
+ iri: iri2,
666
+ sourceUrl,
667
+ ok: false,
668
+ error: "fetch failed"
669
+ });
670
+ return void 0;
671
+ }
672
+ let text = body;
673
+ if (fragment) {
674
+ const levelText = graph.literal(iri2, GRAPH_LEVEL);
675
+ const level = levelText ? Number.parseInt(levelText, 10) : void 0;
676
+ const slice2 = title === void 0 ? void 0 : sliceSection(
677
+ body,
678
+ title,
679
+ Number.isNaN(level) ? void 0 : level,
680
+ occurrenceOf(doc, iri2)
681
+ );
682
+ if (slice2 === void 0) {
683
+ options.trace?.resolutions.push({
684
+ iri: iri2,
685
+ sourceUrl,
686
+ ok: false,
687
+ error: "section heading not found"
688
+ });
689
+ return void 0;
690
+ }
691
+ text = slice2;
692
+ }
693
+ options.trace?.resolutions.push({ iri: iri2, sourceUrl, ok: true });
694
+ return { iri: iri2, text, sourceUrl, title };
695
+ }
696
+ };
697
+ }
698
+
699
+ // src/graph/core/search-index.ts
700
+ var GRAPH_DOCUMENT2 = `${NS.graph}Document`;
701
+ var GRAPH_SECTION = `${NS.graph}Section`;
702
+ var SKOS_CONCEPT = `${NS.skos}Concept`;
703
+ var GRAPH_PATH2 = `${NS.graph}path`;
704
+ var GRAPH_LEVEL2 = `${NS.graph}level`;
705
+ var DCTERMS_TITLE2 = `${NS.dcterms}title`;
706
+ var DCTERMS_DESCRIPTION = `${NS.dcterms}description`;
707
+ var SKOS_PREF_LABEL = `${NS.skos}prefLabel`;
708
+ var SKOS_ALT_LABEL = `${NS.skos}altLabel`;
709
+ var DCTERMS_LANGUAGE = `${NS.dcterms}language`;
710
+ function joinLabels(values) {
711
+ const seen = /* @__PURE__ */ new Map();
712
+ for (const v of values) {
713
+ const key = v.trim().toLowerCase();
714
+ if (key && !seen.has(key)) seen.set(key, v.trim());
715
+ }
716
+ const out = [...seen.values()].sort(byCodeUnit);
717
+ return out.length > 0 ? out.join(" ") : void 0;
718
+ }
719
+ var FRONTMATTER = /^---[ \t]*\r?\n(?:[\s\S]*?\r?\n)?(?:---|\.\.\.)[ \t]*(?:\r?\n|$)(?:\r?\n)*/;
720
+ function stripFrontmatter(markdown) {
721
+ return markdown.replace(FRONTMATTER, "");
722
+ }
723
+ function entry(id, type, fields) {
724
+ const out = { id, type };
725
+ if (fields.title !== void 0) out.title = fields.title;
726
+ if (fields.labels !== void 0) out.labels = fields.labels;
727
+ if (fields.description !== void 0) out.description = fields.description;
728
+ if (fields.text !== void 0) out.text = fields.text;
729
+ return out;
730
+ }
731
+ function buildSearchIndex(graph, cwd, options = {}) {
732
+ const warnings = options.warnings ?? [];
733
+ const readDoc = options.readDoc ?? ((path2) => {
734
+ const abs = resolve2(cwd, path2);
735
+ return existsSync2(abs) ? readFileSync(abs, "utf8") : void 0;
736
+ });
737
+ const entries = [];
738
+ const sourceOf = /* @__PURE__ */ new Map();
739
+ const allSections = graph.instancesOf(GRAPH_SECTION);
740
+ const sectionsOf = /* @__PURE__ */ new Map();
741
+ for (const section of allSections) {
742
+ const hash = section.indexOf("#");
743
+ if (hash < 0) continue;
744
+ const doc = section.slice(0, hash);
745
+ const siblings = sectionsOf.get(doc);
746
+ if (siblings) siblings.push(section);
747
+ else sectionsOf.set(doc, [section]);
748
+ }
749
+ for (const doc of graph.instancesOf(GRAPH_DOCUMENT2)) {
750
+ const path2 = graph.literal(doc, GRAPH_PATH2);
751
+ const source = path2 === void 0 ? void 0 : readDoc(path2);
752
+ sourceOf.set(doc, source);
753
+ if (path2 !== void 0 && source === void 0) {
754
+ warnings.push(
755
+ `Source not found for ${path2} \u2014 indexed without body text.`
756
+ );
757
+ }
758
+ const body = source === void 0 ? void 0 : stripFrontmatter(source).replace(/\r\n/g, "\n");
759
+ const text = body === void 0 ? void 0 : (sectionsOf.get(doc)?.length ?? 0) > 0 ? documentPreamble(body) : body;
760
+ entries.push(
761
+ entry(doc, compactIri(GRAPH_DOCUMENT2), {
762
+ title: graph.literal(doc, DCTERMS_TITLE2),
763
+ description: graph.literal(doc, DCTERMS_DESCRIPTION),
764
+ text
765
+ })
766
+ );
767
+ }
768
+ const occurrencesOf = /* @__PURE__ */ new Map();
769
+ for (const section of allSections) {
770
+ const hash = section.indexOf("#");
771
+ const doc = hash < 0 ? section : section.slice(0, hash);
772
+ const title = graph.literal(section, DCTERMS_TITLE2);
773
+ const source = sourceOf.get(doc);
774
+ const levelText = graph.literal(section, GRAPH_LEVEL2);
775
+ const parsed = levelText === void 0 ? NaN : Number.parseInt(levelText, 10);
776
+ const level = Number.isNaN(parsed) ? void 0 : parsed;
777
+ let occurrences = occurrencesOf.get(doc);
778
+ if (!occurrences) {
779
+ occurrences = sectionOccurrences(graph, doc);
780
+ occurrencesOf.set(doc, occurrences);
781
+ }
782
+ const text = source !== void 0 && title !== void 0 ? sectionOwnText(source, title, level, occurrences.get(section) ?? 0) : void 0;
783
+ entries.push(entry(section, compactIri(GRAPH_SECTION), { title, text }));
784
+ }
785
+ for (const concept of graph.instancesOf(SKOS_CONCEPT)) {
786
+ const pref = graph.literals(concept, SKOS_PREF_LABEL);
787
+ const alt = graph.literals(concept, SKOS_ALT_LABEL);
788
+ entries.push(
789
+ entry(concept, compactIri(SKOS_CONCEPT), {
790
+ title: pref[0],
791
+ labels: joinLabels([...pref, ...alt])
792
+ })
793
+ );
794
+ }
795
+ entries.sort((a, b) => byCodeUnit(a.id, b.id));
796
+ return { version: 1, entries };
797
+ }
798
+ function emitSearchIndex(doc) {
799
+ return `${JSON.stringify(doc, null, 2)}
800
+ `;
801
+ }
802
+ function partitionByLanguage(graph, index) {
803
+ const documentType = compactIri(GRAPH_DOCUMENT2);
804
+ const sectionType = compactIri(GRAPH_SECTION);
805
+ const languageOf = (entry2) => {
806
+ if (entry2.type !== documentType && entry2.type !== sectionType) {
807
+ return void 0;
808
+ }
809
+ const hash = entry2.id.indexOf("#");
810
+ const doc = hash === -1 ? entry2.id : entry2.id.slice(0, hash);
811
+ return languageOfDocument(graph, doc);
812
+ };
813
+ const out = /* @__PURE__ */ new Map();
814
+ const shared = [];
815
+ for (const entry2 of index.entries) {
816
+ const language = languageOf(entry2);
817
+ if (language === void 0) {
818
+ shared.push(entry2);
819
+ continue;
820
+ }
821
+ const bucket = out.get(language);
822
+ if (bucket) bucket.entries.push(entry2);
823
+ else out.set(language, { version: 1, entries: [entry2] });
824
+ }
825
+ if (out.size === 0 && shared.length > 0) {
826
+ out.set(UNDETERMINED, { version: 1, entries: [] });
827
+ }
828
+ for (const bucket of out.values()) {
829
+ bucket.entries.push(...shared);
830
+ bucket.entries.sort((a, b) => byCodeUnit(a.id, b.id));
831
+ }
832
+ return out;
833
+ }
834
+ function languageOfDocument(graph, doc) {
835
+ const tag = graph.literal(doc, DCTERMS_LANGUAGE);
836
+ return tag === void 0 ? UNDETERMINED : canonicalLanguageTag(tag);
837
+ }
838
+ function documentsByLanguage(graph) {
839
+ const out = /* @__PURE__ */ new Map();
840
+ for (const doc of graph.instancesOf(GRAPH_DOCUMENT2)) {
841
+ const language = languageOfDocument(graph, doc);
842
+ out.set(language, (out.get(language) ?? 0) + 1);
843
+ }
844
+ return out;
845
+ }
846
+
847
+ // src/graph/runtime/graph.ts
848
+ var XSD_STRING4 = "http://www.w3.org/2001/XMLSchema#string";
849
+ function compareValues(a, b) {
850
+ if (a.kind !== b.kind) return a.kind === "iri" ? -1 : 1;
851
+ const v = byCodeUnit(a.value, b.value);
852
+ if (v !== 0) return v;
853
+ const da = a.kind === "literal" ? a.datatype ?? "" : "";
854
+ const db = b.kind === "literal" ? b.datatype ?? "" : "";
855
+ return byCodeUnit(da, db);
856
+ }
857
+ var GraphIndex = class _GraphIndex {
858
+ nodes = /* @__PURE__ */ new Map();
859
+ /** subject → predicate → object IRIs (sorted). */
860
+ outbound = /* @__PURE__ */ new Map();
861
+ /** object → predicate → subject IRIs (sorted). */
862
+ inbound = /* @__PURE__ */ new Map();
863
+ /** class IRI → instance IRIs (sorted). */
864
+ byType = /* @__PURE__ */ new Map();
865
+ /** prefix → namespace, from the artifact's own `@context`. */
866
+ context;
867
+ constructor(context) {
868
+ this.context = context;
869
+ }
870
+ /**
871
+ * Build from a `graph.jsonld` document — a JSON string or the parsed object.
872
+ * CURIE keys and `@type` values are expanded using the document's own
873
+ * `@context`, so the index is self-describing.
874
+ */
875
+ static fromJsonLd(input) {
876
+ const doc = typeof input === "string" ? JSON.parse(input) : input;
877
+ const context = doc["@context"] ?? {};
878
+ const index = new _GraphIndex({ ...context });
879
+ for (const raw of doc["@graph"] ?? []) {
880
+ index.addJsonLdNode(raw);
881
+ }
882
+ index.finalize();
883
+ return index;
884
+ }
885
+ /** Build from dockg's internal quad shape (the Node/CLI path). */
886
+ static fromQuads(quads, context = {}) {
887
+ const index = new _GraphIndex({ ...context });
888
+ for (const q of quads) {
889
+ const o = q.o;
890
+ const value = o.kind === "iri" ? { kind: "iri", value: o.value } : o.datatype && o.datatype !== XSD_STRING4 ? { kind: "literal", value: o.value, datatype: o.datatype } : { kind: "literal", value: o.value };
891
+ index.addTriple(q.s, q.p, value);
892
+ }
893
+ index.finalize();
894
+ return index;
895
+ }
896
+ /** Expand a CURIE (`dcterms:title`) to a full IRI using `@context`. */
897
+ expand(term) {
898
+ const colon = term.indexOf(":");
899
+ if (colon <= 0) return term;
900
+ const ns = this.context[term.slice(0, colon)];
901
+ return ns ? `${ns}${term.slice(colon + 1)}` : term;
902
+ }
903
+ /** Compact a full IRI to a CURIE when a context namespace matches. */
904
+ compact(iri2) {
905
+ let best = iri2;
906
+ let bestNs = "";
907
+ for (const [prefix, ns] of Object.entries(this.context)) {
908
+ if (iri2.startsWith(ns) && iri2.length > ns.length && ns.length > bestNs.length) {
909
+ best = `${prefix}:${iri2.slice(ns.length)}`;
910
+ bestNs = ns;
911
+ }
912
+ }
913
+ return best;
914
+ }
915
+ addJsonLdNode(raw) {
916
+ const id = typeof raw["@id"] === "string" ? this.expand(raw["@id"]) : "";
917
+ if (!id) return;
918
+ this.ensureNode(id);
919
+ const rawTypes = raw["@type"];
920
+ for (const t of toArray(rawTypes)) {
921
+ if (typeof t === "string") {
922
+ this.addTriple(id, RDF_TYPE, { kind: "iri", value: this.expand(t) });
923
+ }
924
+ }
925
+ for (const [key, rawValue] of Object.entries(raw)) {
926
+ if (key === "@id" || key === "@type") continue;
927
+ const predicate = this.expand(key);
928
+ for (const v of toArray(rawValue)) {
929
+ const value = this.toValue(v);
930
+ if (value) this.addTriple(id, predicate, value);
931
+ }
932
+ }
933
+ }
934
+ toValue(v) {
935
+ if (typeof v === "string") return { kind: "literal", value: v };
936
+ if (typeof v === "number" || typeof v === "boolean") {
937
+ return { kind: "literal", value: String(v) };
938
+ }
939
+ if (v && typeof v === "object") {
940
+ const o = v;
941
+ if (typeof o["@id"] === "string") {
942
+ return { kind: "iri", value: this.expand(o["@id"]) };
943
+ }
944
+ if (o["@value"] !== void 0) {
945
+ const raw = o["@value"];
946
+ const lexical = typeof raw === "string" ? raw : JSON.stringify(raw);
947
+ const dt = typeof o["@type"] === "string" ? this.expand(o["@type"]) : void 0;
948
+ return dt && dt !== XSD_STRING4 ? { kind: "literal", value: lexical, datatype: dt } : { kind: "literal", value: lexical };
949
+ }
950
+ }
951
+ return void 0;
952
+ }
953
+ ensureNode(id) {
954
+ let node = this.nodes.get(id);
955
+ if (!node) {
956
+ node = { id, types: [], props: /* @__PURE__ */ new Map() };
957
+ this.nodes.set(id, node);
958
+ }
959
+ return node;
960
+ }
961
+ addTriple(s, p, o) {
962
+ const node = this.ensureNode(s);
963
+ let values = node.props.get(p);
964
+ if (!values) {
965
+ values = [];
966
+ node.props.set(p, values);
967
+ }
968
+ values.push(o);
969
+ if (p === RDF_TYPE && o.kind === "iri") {
970
+ node.types.push(o.value);
971
+ const instances = this.byType.get(o.value);
972
+ if (instances) instances.push(s);
973
+ else this.byType.set(o.value, [s]);
974
+ }
975
+ if (o.kind === "iri") {
976
+ addEdge(this.outbound, s, p, o.value);
977
+ addEdge(this.inbound, o.value, p, s);
978
+ this.ensureNode(o.value);
979
+ }
980
+ }
981
+ /** Sort every collection so traversal order is deterministic. */
982
+ finalize() {
983
+ for (const node of this.nodes.values()) {
984
+ node.types.sort(byCodeUnit);
985
+ for (const values of node.props.values()) values.sort(compareValues);
986
+ }
987
+ for (const instances of this.byType.values()) instances.sort(byCodeUnit);
988
+ for (const map of [this.outbound, this.inbound]) {
989
+ for (const preds of map.values()) {
990
+ for (const targets of preds.values()) targets.sort(byCodeUnit);
991
+ }
992
+ }
993
+ }
994
+ /** Every node IRI, sorted. */
995
+ ids() {
996
+ return [...this.nodes.keys()].sort(byCodeUnit);
997
+ }
998
+ size() {
999
+ return this.nodes.size;
1000
+ }
1001
+ node(iri2) {
1002
+ return this.nodes.get(iri2);
1003
+ }
1004
+ has(iri2) {
1005
+ return this.nodes.has(iri2);
1006
+ }
1007
+ /** Class IRIs of a node, sorted. */
1008
+ types(iri2) {
1009
+ return this.nodes.get(iri2)?.types ?? [];
1010
+ }
1011
+ /** Instances of a class, sorted. */
1012
+ instancesOf(classIri) {
1013
+ return this.byType.get(classIri) ?? [];
1014
+ }
1015
+ /** All values for a predicate, sorted. */
1016
+ values(iri2, predicate) {
1017
+ return this.nodes.get(iri2)?.props.get(predicate) ?? [];
1018
+ }
1019
+ /** Literal lexical forms for a predicate, sorted. */
1020
+ literals(iri2, predicate) {
1021
+ return this.values(iri2, predicate).filter((v) => v.kind === "literal").map((v) => v.value);
1022
+ }
1023
+ /** The first literal for a predicate, if any. */
1024
+ literal(iri2, predicate) {
1025
+ return this.literals(iri2, predicate)[0];
1026
+ }
1027
+ /** Outbound IRI targets: all predicates, or one. Sorted. */
1028
+ out(iri2, predicate) {
1029
+ return edges(this.outbound, iri2, predicate);
1030
+ }
1031
+ /** Inbound IRI sources (who points at this node). Sorted. */
1032
+ in(iri2, predicate) {
1033
+ return edges(this.inbound, iri2, predicate);
1034
+ }
1035
+ };
1036
+ function addEdge(map, key, predicate, target) {
1037
+ let preds = map.get(key);
1038
+ if (!preds) {
1039
+ preds = /* @__PURE__ */ new Map();
1040
+ map.set(key, preds);
1041
+ }
1042
+ const targets = preds.get(predicate);
1043
+ if (targets) targets.push(target);
1044
+ else preds.set(predicate, [target]);
1045
+ }
1046
+ function edges(map, iri2, predicate) {
1047
+ const preds = map.get(iri2);
1048
+ if (!preds) return [];
1049
+ const out = [];
1050
+ const keys = predicate ? [predicate] : [...preds.keys()].sort(byCodeUnit);
1051
+ for (const p of keys) {
1052
+ for (const target of preds.get(p) ?? []) out.push({ predicate: p, target });
1053
+ }
1054
+ return out;
1055
+ }
1056
+ function toArray(v) {
1057
+ if (v === void 0 || v === null) return [];
1058
+ return Array.isArray(v) ? v : [v];
1059
+ }
1060
+
1061
+ // src/graph/commands/export.ts
1062
+ var EXPORT_TARGETS = ["jsonld", "iirds", "search"];
1063
+ var EXTENSION = {
1064
+ jsonld: ".jsonld",
1065
+ iirds: ".iirds"
1066
+ };
1067
+ function withExtension(path2, ext) {
1068
+ return path2.replace(/\.[^./\\]*$/, "") + ext;
1069
+ }
1070
+ function runJsonLd(cwd, graphPath, out) {
1071
+ const store = loadGraph(graphPath);
1072
+ const quads = storeToQuads(store);
1073
+ const serialized = emitJsonLd(quads);
1074
+ const nodes = new Set(quads.map((q) => q.s)).size;
1075
+ const outPath = out ? resolve3(cwd, out) : withExtension(graphPath, EXTENSION.jsonld);
1076
+ mkdirSync(dirname(outPath), { recursive: true });
1077
+ writeFileSync(outPath, serialized, "utf8");
1078
+ return { outPath, format: "jsonld", nodes, warnings: [] };
1079
+ }
1080
+ function runIirds(cwd, sources, graphPath, iirds, baseIri, out) {
1081
+ const store = loadGraph(graphPath);
1082
+ const projection = projectPackage(
1083
+ store,
1084
+ {
1085
+ baseIri,
1086
+ version: iirds.version,
1087
+ title: iirds.title,
1088
+ creator: iirds.creator
1089
+ },
1090
+ sources
1091
+ );
1092
+ const metadata = emitRdfXml(projection.quads, projection.prefixes);
1093
+ const content = [...projection.contentFiles].sort((a, b) => byCodeUnit(a.zipPath, b.zipPath)).map((f) => ({ name: f.zipPath, data: readFileSync2(f.absPath) }));
1094
+ const entries = [
1095
+ {
1096
+ name: "mimetype",
1097
+ data: Buffer.from("application/iirds+zip"),
1098
+ store: true
1099
+ },
1100
+ { name: "META-INF/metadata.rdf", data: Buffer.from(metadata, "utf8") },
1101
+ ...content
1102
+ ];
1103
+ const outPath = out ? resolve3(cwd, out) : withExtension(graphPath, EXTENSION.iirds);
1104
+ mkdirSync(dirname(outPath), { recursive: true });
1105
+ writeFileSync(outPath, writeZip(entries));
1106
+ const nodes = new Set(projection.quads.map((q) => q.s)).size;
1107
+ return { outPath, format: "iirds", nodes, warnings: projection.warnings };
1108
+ }
1109
+ function searchIndexDigest(raw) {
1110
+ return `sha256:${createHash("sha256").update(raw, "utf8").digest("hex")}`;
1111
+ }
1112
+ function runSearchIndex(cwd, sources, graphPath, out) {
1113
+ const store = loadGraph(graphPath);
1114
+ const graph = GraphIndex.fromQuads(
1115
+ storeToQuads(store),
1116
+ Object.fromEntries(PREFIXES)
1117
+ );
1118
+ const warnings = [];
1119
+ const index = buildSearchIndex(graph, sources, { warnings });
1120
+ const byLanguage = partitionByLanguage(graph, index);
1121
+ const docCounts = documentsByLanguage(graph);
1122
+ const outDir = out ? resolve3(cwd, out) : dirname(graphPath);
1123
+ mkdirSync(outDir, { recursive: true });
1124
+ const languages = [];
1125
+ for (const [language, doc] of byLanguage) {
1126
+ if (!isLanguageTag(language)) {
1127
+ throw new GraphError(
1128
+ `Cannot write an index for language "${language}": not a BCP-47 tag. Fix the \`lang\`/\`language\` frontmatter, or the route's \`language\`, and rebuild \u2014 \`manni graph check\` reports which document carries it.`
1129
+ );
1130
+ }
1131
+ const filename = searchIndexFilename(language);
1132
+ const serialized = emitSearchIndex(doc);
1133
+ writeFileSync(join(outDir, filename), serialized, "utf8");
1134
+ languages.push({
1135
+ language,
1136
+ documents: docCounts.get(language) ?? 0,
1137
+ search: {
1138
+ path: filename,
1139
+ entries: doc.entries.length,
1140
+ digest: searchIndexDigest(serialized)
1141
+ }
1142
+ });
1143
+ }
1144
+ const manifestPath = join(outDir, LOCALIZATIONS_FILENAME);
1145
+ writeFileSync(
1146
+ manifestPath,
1147
+ emitLocalizations({ version: 1, languages }),
1148
+ "utf8"
1149
+ );
1150
+ return {
1151
+ outPath: manifestPath,
1152
+ format: "search",
1153
+ nodes: index.entries.length,
1154
+ warnings
1155
+ };
1156
+ }
1157
+ function runExport(opts) {
1158
+ return new Promise((ok) => {
1159
+ ok(exportOnce(opts));
1160
+ });
1161
+ }
1162
+ function exportOnce(opts) {
1163
+ const cwd = opts.cwd ?? process.cwd();
1164
+ if (!EXPORT_TARGETS.includes(opts.format)) {
1165
+ throw new GraphError(
1166
+ `Unknown export target "${opts.format}". Use ${EXPORT_TARGETS.join(" | ")}.`
1167
+ );
1168
+ }
1169
+ const config = loadRunConfig(
1170
+ {
1171
+ ...opts.config === void 0 ? {} : { configPath: opts.config },
1172
+ ...opts.noConfig === void 0 ? {} : { noConfig: opts.noConfig }
1173
+ },
1174
+ cwd
1175
+ );
1176
+ const graphPath = resolve3(cwd, opts.graph ?? config.out);
1177
+ const sources = documentBase(config, {}, cwd);
1178
+ if (opts.format === "jsonld") return runJsonLd(cwd, graphPath, opts.out);
1179
+ if (opts.format === "search") {
1180
+ return runSearchIndex(cwd, sources, graphPath, opts.out);
1181
+ }
1182
+ return runIirds(
1183
+ cwd,
1184
+ sources,
1185
+ graphPath,
1186
+ config.export.iirds,
1187
+ config.baseIri,
1188
+ opts.out
1189
+ );
1190
+ }
1191
+
1192
+ // src/graph/commands/query.ts
1193
+ import { resolve as resolve4 } from "node:path";
1194
+ import { DataFactory as DataFactory2 } from "n3";
1195
+ function runQuery(opts = {}) {
1196
+ const cwd = opts.cwd ?? process.cwd();
1197
+ const config = loadRunConfig(
1198
+ {
1199
+ ...opts.config === void 0 ? {} : { configPath: opts.config },
1200
+ ...opts.noConfig === void 0 ? {} : { noConfig: opts.noConfig }
1201
+ },
1202
+ cwd
1203
+ );
1204
+ const store = loadGraph(resolve4(cwd, opts.graph ?? config.out));
1205
+ const s = opts.s ? DataFactory2.namedNode(expandTerm(opts.s)) : null;
1206
+ const p = opts.p ? DataFactory2.namedNode(expandTerm(opts.p)) : null;
1207
+ const oValue = opts.o ? expandTerm(opts.o) : null;
1208
+ const matches = [];
1209
+ for (const quad of store.getQuads(s, p, null, null)) {
1210
+ const obj = quad.object;
1211
+ if (oValue !== null && obj.value !== oValue) continue;
1212
+ matches.push({
1213
+ s: quad.subject.value,
1214
+ p: quad.predicate.value,
1215
+ o: obj.termType === "Literal" ? {
1216
+ kind: "literal",
1217
+ value: obj.value,
1218
+ ...obj.datatype.value !== "http://www.w3.org/2001/XMLSchema#string" ? { datatype: obj.datatype.value } : {}
1219
+ } : { kind: "iri", value: obj.value }
1220
+ });
1221
+ }
1222
+ matches.sort(
1223
+ (a, b) => byCodeUnit(a.s, b.s) || byCodeUnit(a.p, b.p) || byCodeUnit(a.o.kind, b.o.kind) || byCodeUnit(a.o.value, b.o.value)
1224
+ );
1225
+ return { matches };
1226
+ }
1227
+ function renderQuery(result, format) {
1228
+ if (format === "json") return JSON.stringify(result, null, 2);
1229
+ if (result.matches.length === 0) return "No matches.";
1230
+ const lines = result.matches.map((m) => {
1231
+ const o = m.o.kind === "iri" ? `<${m.o.value}>` : `"${m.o.value}"${m.o.datatype ? `^^${compactIri(m.o.datatype)}` : ""}`;
1232
+ return `<${m.s}> ${compactIri(m.p)} ${o}`;
1233
+ });
1234
+ lines.push(`
1235
+ ${result.matches.length} match(es)`);
1236
+ return lines.join("\n");
1237
+ }
1238
+
1239
+ // src/graph/commands/fill.ts
1240
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
1241
+ import { readFile } from "node:fs/promises";
1242
+ import { resolve as resolve6 } from "node:path";
1243
+
1244
+ // src/graph/core/frontmatter-edit.ts
1245
+ import { Document, YAMLMap, YAMLSeq, isMap, parseDocument } from "yaml";
1246
+ function frontmatterKind(content) {
1247
+ const body = content.charCodeAt(0) === 65279 ? content.slice(1) : content;
1248
+ if (/^---(\r?\n)/.test(body)) return "yaml";
1249
+ if (/^(\+\+\+|;;;)(\r?\n)/.test(body)) return "unsupported";
1250
+ return "none";
1251
+ }
1252
+ function splitYamlFrontmatter(content, path2) {
1253
+ const bom = content.charCodeAt(0) === 65279 ? content.slice(0, 1) : "";
1254
+ const body = bom ? content.slice(1) : content;
1255
+ const openMatch = /^---(\r?\n)/.exec(body);
1256
+ if (!openMatch) return null;
1257
+ const eol = openMatch[1] === "\r\n" ? "\r\n" : "\n";
1258
+ const [openLine = "", ...rest] = body.split(/(?<=\n)/);
1259
+ let offset = openLine.length;
1260
+ for (const line of rest) {
1261
+ const stripped = line.replace(/\r?\n$/, "");
1262
+ if (stripped === "---" || stripped === "...") {
1263
+ const blockEnd = offset;
1264
+ return {
1265
+ open: bom + openLine,
1266
+ block: body.slice(openLine.length, blockEnd),
1267
+ suffix: body.slice(blockEnd),
1268
+ eol
1269
+ };
1270
+ }
1271
+ offset += line.length;
1272
+ }
1273
+ throw new GraphError(`${path2}: unterminated frontmatter block`);
1274
+ }
1275
+ function flowSeqs(node) {
1276
+ if (node instanceof YAMLSeq) {
1277
+ node.flow = true;
1278
+ for (const item of node.items) flowSeqs(item);
1279
+ } else if (node instanceof YAMLMap) {
1280
+ for (const item of node.items) flowSeqs(item.value);
1281
+ }
1282
+ }
1283
+ function setField(doc, map, field, value) {
1284
+ const node = doc.createNode(value);
1285
+ flowSeqs(node);
1286
+ map.set(field, node);
1287
+ }
1288
+ function applyGraphFields(content, path2, values, options = {}) {
1289
+ const entries = Object.entries(values).filter(
1290
+ ([, v]) => v !== void 0 && v !== null && !(Array.isArray(v) && v.length === 0)
1291
+ );
1292
+ const pageEntries = Object.entries(options.page ?? {});
1293
+ if (entries.length === 0 && pageEntries.length === 0) {
1294
+ return { content, applied: [], skipped: [] };
1295
+ }
1296
+ if (frontmatterKind(content) === "unsupported") {
1297
+ throw new GraphError(
1298
+ `${path2}: only YAML frontmatter can be edited (found a TOML/JSON fence)`
1299
+ );
1300
+ }
1301
+ const split = splitYamlFrontmatter(content, path2);
1302
+ if (split === null) {
1303
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
1304
+ const nested = {};
1305
+ for (const [field, value] of entries) {
1306
+ const segments = field.split(".");
1307
+ let target = nested;
1308
+ for (const segment of segments.slice(0, -1)) {
1309
+ target[segment] ??= {};
1310
+ target = target[segment];
1311
+ }
1312
+ target[segments.at(-1) ?? field] = value;
1313
+ }
1314
+ const doc2 = new Document({
1315
+ ...Object.fromEntries(pageEntries),
1316
+ ...entries.length > 0 ? { graph: nested } : {}
1317
+ });
1318
+ const graph2 = doc2.get("graph", true);
1319
+ if (isMap(graph2)) flowSeqs(graph2);
1320
+ let block = doc2.toString();
1321
+ if (eol === "\r\n") block = block.replace(/(?<!\r)\n/g, "\r\n");
1322
+ return {
1323
+ content: `---${eol}${block}---${eol}${eol}${content}`,
1324
+ applied: entries.map(([k]) => k),
1325
+ skipped: []
1326
+ };
1327
+ }
1328
+ const doc = parseDocument(split.block);
1329
+ if (doc.errors.length > 0) {
1330
+ throw new GraphError(
1331
+ `${path2}: cannot edit frontmatter \u2014 ${doc.errors[0]?.message ?? "parse error"}`
1332
+ );
1333
+ }
1334
+ let graph = doc.get("graph", true);
1335
+ if (graph !== void 0 && !isMap(graph)) {
1336
+ throw new GraphError(`${path2}: frontmatter key "graph" is not a map`);
1337
+ }
1338
+ if (graph === void 0 && entries.length > 0) {
1339
+ graph = doc.createNode({});
1340
+ doc.set("graph", graph);
1341
+ }
1342
+ const graphMap = graph ?? doc.createNode({});
1343
+ const applied = [];
1344
+ const skipped = [];
1345
+ for (const [field, value] of entries) {
1346
+ const segments = field.split(".");
1347
+ const leaf = segments.at(-1) ?? field;
1348
+ let target = graphMap;
1349
+ let missingParent = false;
1350
+ for (const segment of segments.slice(0, -1)) {
1351
+ const existing = target.get(segment, true);
1352
+ if (existing === void 0) {
1353
+ const created = doc.createNode({});
1354
+ target.set(segment, created);
1355
+ target = created;
1356
+ } else if (isMap(existing)) {
1357
+ target = existing;
1358
+ } else {
1359
+ missingParent = true;
1360
+ break;
1361
+ }
1362
+ }
1363
+ if (missingParent) {
1364
+ skipped.push(field);
1365
+ continue;
1366
+ }
1367
+ if (target.has(leaf) && !options.force) {
1368
+ skipped.push(field);
1369
+ continue;
1370
+ }
1371
+ setField(doc, target, leaf, value);
1372
+ applied.push(field);
1373
+ }
1374
+ for (const [key, value] of pageEntries) {
1375
+ doc.set(key, doc.createNode(value));
1376
+ }
1377
+ if (applied.length === 0 && pageEntries.length === 0) {
1378
+ return { content, applied, skipped };
1379
+ }
1380
+ let newBlock = doc.toString();
1381
+ if (split.eol === "\r\n") newBlock = newBlock.replace(/(?<!\r)\n/g, "\r\n");
1382
+ return { content: split.open + newBlock + split.suffix, applied, skipped };
1383
+ }
1384
+ function frontmatterData(content) {
1385
+ const split = splitYamlFrontmatter(content, "");
1386
+ if (split === null) return {};
1387
+ const doc = parseDocument(split.block);
1388
+ if (doc.errors.length > 0) return {};
1389
+ const plain = doc.toJS();
1390
+ return plain !== null && typeof plain === "object" && !Array.isArray(plain) ? plain : {};
1391
+ }
1392
+ function existingMetaProvenance(content) {
1393
+ return frontmatterData(content)["meta-provenance"];
1394
+ }
1395
+ function existingGraphFields(content) {
1396
+ const split = splitYamlFrontmatter(content, "");
1397
+ if (split === null) return [];
1398
+ const doc = parseDocument(split.block);
1399
+ if (doc.errors.length > 0) return [];
1400
+ const graph = doc.get("graph", true);
1401
+ if (!isMap(graph)) return [];
1402
+ return graph.items.map((item) => {
1403
+ const value = item.key.value;
1404
+ return typeof value === "string" || typeof value === "number" ? String(value) : "";
1405
+ }).filter((k) => k.length > 0);
1406
+ }
1407
+
1408
+ // src/graph/core/fill-guard.ts
1409
+ import { readFileSync as readFileSync3 } from "node:fs";
1410
+ import { resolve as resolve5 } from "node:path";
1411
+ import { DataFactory as DataFactory3, Store } from "n3";
1412
+ var GUARDED_FIELDS = [
1413
+ "label",
1414
+ "broader",
1415
+ "narrower",
1416
+ "related-concepts",
1417
+ "applies-to",
1418
+ "not-applicable-to",
1419
+ "about-product-aspect",
1420
+ "not-about-product-aspect"
1421
+ ];
1422
+ var GUARD_SOURCES = ["frontmatter", "tags", "sections"];
1423
+ function leafOf(field) {
1424
+ const dot = field.lastIndexOf(".");
1425
+ return dot === -1 ? field : field.slice(dot + 1);
1426
+ }
1427
+ function toStore(quads) {
1428
+ const store = new Store();
1429
+ for (const q of quads) {
1430
+ store.addQuad(
1431
+ DataFactory3.quad(
1432
+ DataFactory3.namedNode(q.s),
1433
+ DataFactory3.namedNode(q.p),
1434
+ q.o.kind === "iri" ? DataFactory3.namedNode(q.o.value) : DataFactory3.literal(
1435
+ q.o.value,
1436
+ q.o.datatype ? DataFactory3.namedNode(q.o.datatype) : void 0
1437
+ )
1438
+ )
1439
+ );
1440
+ }
1441
+ return store;
1442
+ }
1443
+ var FillGuard = class _FillGuard {
1444
+ constructor(allPaths, analyzeOptions, baseIri, sources, shapesPaths, force) {
1445
+ this.allPaths = allPaths;
1446
+ this.analyzeOptions = analyzeOptions;
1447
+ this.baseIri = baseIri;
1448
+ this.sources = sources;
1449
+ this.shapesPaths = shapesPaths;
1450
+ this.force = force;
1451
+ }
1452
+ allPaths;
1453
+ analyzeOptions;
1454
+ baseIri;
1455
+ sources;
1456
+ shapesPaths;
1457
+ force;
1458
+ models = /* @__PURE__ */ new Map();
1459
+ baselineKeys = null;
1460
+ /**
1461
+ * Pages left out of the simulation because they would not analyze, sorted.
1462
+ * The caller decides what each one means: a named page fails on its own,
1463
+ * and a page read only for context is worth a warning.
1464
+ */
1465
+ unreadable = [];
1466
+ /**
1467
+ * Read and analyze the whole corpus once, up front. A path in `inline` is
1468
+ * taken from memory rather than disk: that is how a page read from stdin
1469
+ * joins the corpus. `format` is `--as`, applied to the run's `inputs`. Any
1470
+ * other page is parsed by its extension, as `build` reads it. A page that
1471
+ * will not read or analyze is left out and listed in `unreadable`.
1472
+ */
1473
+ static create(files, cwd, config, shapesPaths, force, {
1474
+ inline = /* @__PURE__ */ new Map(),
1475
+ format,
1476
+ inputs = /* @__PURE__ */ new Set()
1477
+ } = {}) {
1478
+ const sources = config.build.derive.filter(
1479
+ (s) => GUARD_SOURCES.includes(s)
1480
+ );
1481
+ const all = [...files, ...inline.keys()];
1482
+ const guard = new _FillGuard(
1483
+ new Set(all),
1484
+ {
1485
+ routes: config.routes,
1486
+ ...format === void 0 ? {} : { format }
1487
+ },
1488
+ config.baseIri,
1489
+ sources,
1490
+ shapesPaths,
1491
+ force
1492
+ );
1493
+ const byExtension = { routes: config.routes };
1494
+ for (const path2 of all) {
1495
+ try {
1496
+ guard.models.set(
1497
+ path2,
1498
+ analyzeDoc(
1499
+ inline.get(path2) ?? readFileSync3(resolve5(cwd, path2), "utf8"),
1500
+ path2,
1501
+ guard.allPaths,
1502
+ inputs.has(path2) ? guard.analyzeOptions : byExtension
1503
+ )
1504
+ );
1505
+ } catch {
1506
+ guard.unreadable.push(path2);
1507
+ }
1508
+ }
1509
+ guard.unreadable.sort(byCodeUnit);
1510
+ return guard;
1511
+ }
1512
+ buildStore(override) {
1513
+ const models = [];
1514
+ for (const [path2, model] of this.models) {
1515
+ models.push(override && path2 === override.path ? override.model : model);
1516
+ }
1517
+ return toStore(
1518
+ deriveGraph(models, { baseIri: this.baseIri, derive: this.sources })
1519
+ );
1520
+ }
1521
+ static key(f) {
1522
+ return `${f.severity}|${f.focusNode}|${f.path ?? ""}|${f.message}`;
1523
+ }
1524
+ /** Findings already present before any proposal — never blamed on one. */
1525
+ async baseline() {
1526
+ if (this.baselineKeys === null) {
1527
+ const findings = await validateGraph(this.buildStore(), this.shapesPaths);
1528
+ this.baselineKeys = new Set(findings.map((f) => _FillGuard.key(f)));
1529
+ }
1530
+ return this.baselineKeys;
1531
+ }
1532
+ /** Fields a finding condemns, restricted to what the proposal contains. */
1533
+ static offendingFields(finding, proposed) {
1534
+ const hash = finding.focusNode.indexOf("#");
1535
+ const slug = hash === -1 ? void 0 : finding.focusNode.slice(hash + 1);
1536
+ const pick = (leaf) => {
1537
+ if (slug !== void 0) {
1538
+ const dotted = `sections.${slug}.${leaf}`;
1539
+ if (proposed.has(dotted)) return [dotted];
1540
+ return [];
1541
+ }
1542
+ return proposed.has(leaf) ? [leaf] : [];
1543
+ };
1544
+ const hierarchy = ["broader", "narrower"].flatMap(pick);
1545
+ if (finding.path === `${NS.skos}prefLabel`) {
1546
+ return pick("label");
1547
+ }
1548
+ if (finding.message.includes("cycle")) return hierarchy;
1549
+ if (finding.path === `${NS.skos}broader`) return hierarchy;
1550
+ if (finding.path === `${NS.skos}narrower`) return hierarchy;
1551
+ if (finding.path === `${NS.skos}related`) {
1552
+ return pick("related-concepts");
1553
+ }
1554
+ if (finding.path === GRAPH_NOT_APPLICABLE_TO_VARIANT) {
1555
+ const negative = pick("not-applicable-to");
1556
+ if (negative.length > 0) return negative;
1557
+ return pick("applies-to");
1558
+ }
1559
+ if (finding.path === GRAPH_NOT_SOFTWARE_SUBJECT) {
1560
+ const negative = pick("not-about-product-aspect");
1561
+ if (negative.length > 0) return negative;
1562
+ return pick("about-product-aspect");
1563
+ }
1564
+ return [];
1565
+ }
1566
+ /**
1567
+ * Simulate `values` applied to the doc and drop fields until the graph
1568
+ * stays clean. New violations always reject; new label warnings
1569
+ * reject the proposed label (an LLM must not introduce a second
1570
+ * spelling of an existing concept).
1571
+ */
1572
+ async vet(path2, content, values) {
1573
+ const current = { ...values };
1574
+ const rejected = [];
1575
+ const guardedLeaves = new Set(GUARDED_FIELDS);
1576
+ const guarded = () => Object.keys(current).filter(
1577
+ (f) => current[f] !== void 0 && guardedLeaves.has(leafOf(f))
1578
+ );
1579
+ if (guarded().length === 0) return { values: current, rejected };
1580
+ const baseline = await this.baseline();
1581
+ while (guarded().length > 0) {
1582
+ const applied = applyGraphFields(content, path2, current, {
1583
+ force: this.force
1584
+ });
1585
+ const model = analyzeDoc(
1586
+ applied.content,
1587
+ path2,
1588
+ this.allPaths,
1589
+ this.analyzeOptions
1590
+ );
1591
+ const findings = await validateGraph(
1592
+ this.buildStore({ path: path2, model }),
1593
+ this.shapesPaths
1594
+ );
1595
+ const introduced = findings.filter(
1596
+ (f) => !baseline.has(_FillGuard.key(f))
1597
+ );
1598
+ const bad = introduced.filter(
1599
+ (f) => f.severity === "error" || f.severity === "warning" && f.path === `${NS.skos}prefLabel`
1600
+ );
1601
+ if (bad.length === 0) break;
1602
+ const proposed = new Set(guarded());
1603
+ const condemned = /* @__PURE__ */ new Map();
1604
+ for (const f of bad) {
1605
+ for (const field of _FillGuard.offendingFields(f, proposed)) {
1606
+ if (!condemned.has(field)) condemned.set(field, f.message);
1607
+ }
1608
+ }
1609
+ const firstBad = bad[0];
1610
+ if (condemned.size === 0 && firstBad !== void 0) {
1611
+ for (const field of guarded()) {
1612
+ condemned.set(field, firstBad.message);
1613
+ }
1614
+ }
1615
+ for (const [field, reason] of condemned) {
1616
+ rejected.push({ field, reason });
1617
+ Reflect.deleteProperty(current, field);
1618
+ }
1619
+ }
1620
+ rejected.sort((a, b) => byCodeUnit(a.field, b.field));
1621
+ return { values: current, rejected };
1622
+ }
1623
+ /** Fold an accepted (written or would-be-written) doc into guard state. */
1624
+ commit(path2, content) {
1625
+ this.models.set(
1626
+ path2,
1627
+ analyzeDoc(content, path2, this.allPaths, this.analyzeOptions)
1628
+ );
1629
+ this.baselineKeys = null;
1630
+ }
1631
+ };
1632
+
1633
+ // src/graph/commands/fill.ts
1634
+ import {
1635
+ completeValidatedJSON,
1636
+ validatorFor
1637
+ } from "@hawkeyexl/inference";
1638
+
1639
+ // src/graph/llm/cache.ts
1640
+ import { JsonCache, buildCacheKey, sha256 } from "@hawkeyexl/inference";
1641
+
1642
+ // src/graph/llm/prompt.ts
1643
+ var PROMPT_VERSION = 4;
1644
+ var TOPIC_TYPES = [
1645
+ "task",
1646
+ "concept",
1647
+ "reference",
1648
+ "learning",
1649
+ "troubleshooting",
1650
+ "form"
1651
+ ];
1652
+ var LIFECYCLE_PHASES = [
1653
+ "administration",
1654
+ "customization",
1655
+ "update",
1656
+ "deployment",
1657
+ "integration",
1658
+ "deinstallation"
1659
+ ];
1660
+ var SOFTWARE_SUBJECTS = ["architecture", "interface", "system-requirement"];
1661
+ var labelArray = (description) => ({
1662
+ type: "array",
1663
+ items: { type: "string", minLength: 1 },
1664
+ uniqueItems: true,
1665
+ description
1666
+ });
1667
+ var enumArray = (values, description) => ({
1668
+ type: "array",
1669
+ items: { enum: values },
1670
+ uniqueItems: true,
1671
+ description
1672
+ });
1673
+ var FIELD_SCHEMAS = {
1674
+ label: {
1675
+ type: "string",
1676
+ minLength: 1,
1677
+ description: "Preferred label of the single concept this document is primarily about."
1678
+ },
1679
+ "alt-labels": labelArray(
1680
+ "Alternative labels: synonyms, abbreviations, common variants."
1681
+ ),
1682
+ broader: labelArray("Labels of broader (parent) concepts."),
1683
+ narrower: labelArray("Labels of narrower (child) concepts."),
1684
+ "related-concepts": labelArray("Labels of associatively related concepts."),
1685
+ concepts: labelArray("Subject labels for the document, like tags."),
1686
+ type: {
1687
+ enum: TOPIC_TYPES,
1688
+ description: "iiRDS topic type \u2014 the functional kind of this page. Only if the page clearly fits one."
1689
+ },
1690
+ "applies-to": labelArray(
1691
+ "Product/variant names this page applies to. ONLY names the text explicitly states; never guess product names."
1692
+ ),
1693
+ "about-product-lifecycle": enumArray(
1694
+ LIFECYCLE_PHASES,
1695
+ "Software lifecycle phases this page covers, only if clearly evidenced."
1696
+ ),
1697
+ "about-product-aspect": enumArray(
1698
+ SOFTWARE_SUBJECTS,
1699
+ "Software information subjects this page is about, only if clearly evidenced."
1700
+ ),
1701
+ "not-applicable-to": labelArray(
1702
+ "Product/variant names the text EXPLICITLY says this page does NOT apply to. Only with explicit textual evidence."
1703
+ ),
1704
+ "not-about-product-aspect": enumArray(
1705
+ SOFTWARE_SUBJECTS,
1706
+ "Software subjects the text EXPLICITLY says this page is NOT about. Only with explicit textual evidence."
1707
+ )
1708
+ };
1709
+ var schemaCache = /* @__PURE__ */ new Map();
1710
+ var SECTION_FILL_FIELDS = [
1711
+ "type",
1712
+ "applies-to",
1713
+ "about-product-lifecycle",
1714
+ "about-product-aspect",
1715
+ "not-applicable-to",
1716
+ "not-about-product-aspect",
1717
+ "concepts"
1718
+ ];
1719
+ function proposalSchema(fields, options = {}) {
1720
+ const sorted = [...fields].sort();
1721
+ const sortedSections = options.sections ? [...options.sections].filter((f) => SECTION_FILL_FIELDS.includes(f)).sort() : void 0;
1722
+ const lenient = options.lenient === true;
1723
+ const key = [
1724
+ sortedSections ? `s:${sortedSections.join(",")}` : "",
1725
+ lenient ? "l:" : "",
1726
+ sorted.join(",")
1727
+ ].join("|");
1728
+ const memoized = schemaCache.get(key);
1729
+ if (memoized) return memoized;
1730
+ const built = buildProposalSchema(sorted, sortedSections, lenient);
1731
+ schemaCache.set(key, built);
1732
+ return built;
1733
+ }
1734
+ function buildProposalSchema(fields, sectionFields, lenient) {
1735
+ const scoreSchema = lenient ? {} : { type: "number", minimum: 0, maximum: 1 };
1736
+ const noteSchema = lenient ? {} : { type: "string" };
1737
+ const properties = {};
1738
+ const confidence = {};
1739
+ const reasoning = {};
1740
+ for (const field of fields) {
1741
+ properties[field] = FIELD_SCHEMAS[field];
1742
+ confidence[field] = scoreSchema;
1743
+ reasoning[field] = noteSchema;
1744
+ }
1745
+ properties["confidence"] = {
1746
+ type: "object",
1747
+ additionalProperties: false,
1748
+ properties: confidence,
1749
+ description: "For every field you propose a value for, a confidence 0..1 that the value is correct."
1750
+ };
1751
+ properties["reasoning"] = {
1752
+ type: "object",
1753
+ additionalProperties: false,
1754
+ properties: reasoning,
1755
+ description: "For every field you propose a value for, a one-sentence justification grounded in the page text."
1756
+ };
1757
+ if (sectionFields !== void 0) {
1758
+ const sectionProps = {
1759
+ slug: {
1760
+ type: "string",
1761
+ minLength: 1,
1762
+ description: "The heading slug, copied exactly from the outline. Do not invent one."
1763
+ }
1764
+ };
1765
+ const sectionConfidence = {};
1766
+ const sectionReasoning = {};
1767
+ for (const field of sectionFields) {
1768
+ sectionProps[field] = FIELD_SCHEMAS[field];
1769
+ sectionConfidence[field] = scoreSchema;
1770
+ sectionReasoning[field] = noteSchema;
1771
+ }
1772
+ sectionProps["confidence"] = {
1773
+ type: "object",
1774
+ additionalProperties: false,
1775
+ properties: sectionConfidence,
1776
+ description: "For every field you propose on this section, a confidence 0..1."
1777
+ };
1778
+ sectionProps["reasoning"] = {
1779
+ type: "object",
1780
+ additionalProperties: false,
1781
+ properties: sectionReasoning,
1782
+ description: "For every field you propose on this section, a one-sentence justification."
1783
+ };
1784
+ properties["sections"] = {
1785
+ type: "array",
1786
+ items: {
1787
+ type: "object",
1788
+ additionalProperties: false,
1789
+ required: ["slug"],
1790
+ properties: sectionProps
1791
+ },
1792
+ description: "Per-section metadata. Only for sections whose own content differs meaningfully from the page as a whole; omit a section rather than repeating the page's values."
1793
+ };
1794
+ }
1795
+ return {
1796
+ type: "object",
1797
+ additionalProperties: false,
1798
+ properties
1799
+ };
1800
+ }
1801
+ var SYSTEM_PROMPT = [
1802
+ "You classify documentation pages into a controlled metadata vocabulary",
1803
+ "(SKOS concepts and iiRDS typing). For each requested field, first reason",
1804
+ "about whether the page's own text supports a value; propose a value only",
1805
+ "when it clearly does, and omit the field otherwise. Reuse the document's",
1806
+ "own terminology; do not invent concepts, product names, or classifications",
1807
+ "the document does not discuss.",
1808
+ "",
1809
+ "For every field you propose a value for, include a `confidence` (0..1) and",
1810
+ "a one-sentence `reasoning` grounded in the text. Score honestly and",
1811
+ "conservatively. Product/variant fields (applies-to, not-applicable-to) and the",
1812
+ "negative fields (not-applicable-to, not-about-product-aspect) require EXPLICIT",
1813
+ "textual evidence \u2014 score them low and prefer to omit unless the page states",
1814
+ "them outright. A field with no value needs no confidence or reasoning."
1815
+ ].join("\n");
1816
+ var EXCERPT_CHARS = 2e3;
1817
+ function buildUserPrompt(doc, body, fields, options = {}) {
1818
+ const title = typeof doc.frontmatter["title"] === "string" && doc.frontmatter["title"] || doc.firstH1 || "(untitled)";
1819
+ const tags = doc.frontmatter["tags"] ?? doc.frontmatter["keywords"];
1820
+ const withSections = options.sections === true && doc.sections.length > 0;
1821
+ const outline = doc.sections.map(
1822
+ (s) => `${" ".repeat(Math.max(0, s.level - 1))}- ${s.title}` + (withSections ? ` [slug: ${s.slug}]` : "")
1823
+ ).join("\n");
1824
+ return [
1825
+ `Propose the following frontmatter fields for this documentation page, with per-field confidence and reasoning: ${fields.join(", ")}.`,
1826
+ withSections ? "Also propose per-section metadata in `sections`, using the exact slug shown in the outline. Include a section ONLY when its own content differs meaningfully from the page as a whole \u2014 repeating the page's values on every heading adds noise, not granularity." : "",
1827
+ "",
1828
+ `Path: ${doc.path}`,
1829
+ `Title: ${title}`,
1830
+ Array.isArray(tags) && tags.length > 0 ? `Existing tags: ${tags.join(", ")}` : "",
1831
+ "",
1832
+ "Heading outline:",
1833
+ outline || "(no headings)",
1834
+ "",
1835
+ "Body excerpt:",
1836
+ body.slice(0, EXCERPT_CHARS)
1837
+ ].join("\n");
1838
+ }
1839
+
1840
+ // src/graph/llm/cache.ts
1841
+ function cacheKey(provider, model, content, fields, sections = false) {
1842
+ return buildCacheKey([
1843
+ provider,
1844
+ model,
1845
+ `v${PROMPT_VERSION}`,
1846
+ // Pre-hashed: documents are large and key parts should stay short.
1847
+ sha256(content),
1848
+ fields.join(","),
1849
+ // Same fields, different request: a sections run sends a different schema
1850
+ // and a different prompt, so its answer must not be served from a
1851
+ // doc-only entry (or the other way round).
1852
+ sections ? "sections" : "doc-only"
1853
+ ]);
1854
+ }
1855
+ var FillCache = class extends JsonCache {
1856
+ constructor(dir, enabled = true) {
1857
+ super(dir, enabled, "manni");
1858
+ }
1859
+ };
1860
+
1861
+ // src/graph/llm/provider.ts
1862
+ import {
1863
+ makeProvider as makeInferenceProvider
1864
+ } from "@hawkeyexl/inference";
1865
+ var CONFIG_KEY = "graph.provider";
1866
+ var toGraphError = (message) => new GraphError(message);
1867
+ function selectProvider2(config, flags = {}) {
1868
+ assertLocalFlag(flags, toGraphError);
1869
+ return selectProvider(flags, [
1870
+ {
1871
+ ...config.provider !== null ? { provider: config.provider } : {},
1872
+ ...config.model !== null ? { model: config.model } : {},
1873
+ origin: CONFIG_KEY
1874
+ },
1875
+ {
1876
+ ...config.providers.provider !== void 0 ? { provider: config.providers.provider } : {},
1877
+ ...config.providers.model !== void 0 ? { model: config.providers.model } : {},
1878
+ origin: "providers.provider"
1879
+ }
1880
+ ]);
1881
+ }
1882
+ function announceSelection(selection) {
1883
+ const message = localNotice(selection);
1884
+ if (message !== void 0) noticeOnce(message);
1885
+ }
1886
+ function assertProviderSelection(selection) {
1887
+ assertKnownProvider(selection.provider, toGraphError);
1888
+ assertModelHasProvider(
1889
+ selection.provider,
1890
+ selection.model,
1891
+ CONFIG_KEY,
1892
+ toGraphError
1893
+ );
1894
+ }
1895
+ function providerSpecFor2(config, selection) {
1896
+ return providerSpecFor(config.providers, selection);
1897
+ }
1898
+ async function resolveProviderIdentity(config, flags = {}) {
1899
+ const selection = selectProvider2(config, flags);
1900
+ const { provider, model } = selection;
1901
+ assertKnownProvider(provider, toGraphError);
1902
+ assertModelHasProvider(provider, model, CONFIG_KEY, toGraphError);
1903
+ announceSelection(selection);
1904
+ return resolveIdentity(
1905
+ providerSpecFor2(config, { provider, model: model ?? null }),
1906
+ toGraphError
1907
+ );
1908
+ }
1909
+ function constructProvider(config, identity) {
1910
+ try {
1911
+ return makeInferenceProvider(providerSpecFor2(config, identity));
1912
+ } catch (e) {
1913
+ throw new GraphError(errorMessage(e));
1914
+ }
1915
+ }
1916
+ async function makeProvider(config, flags = {}) {
1917
+ return constructProvider(config, await resolveProviderIdentity(config, flags));
1918
+ }
1919
+
1920
+ // src/graph/commands/fill.ts
1921
+ var TURN_BUDGET_REASON = "turn budget";
1922
+ var RELATION_FIELDS = [
1923
+ "alt-labels",
1924
+ "broader",
1925
+ "narrower",
1926
+ "related-concepts"
1927
+ ];
1928
+ function numberMap(v) {
1929
+ const out = {};
1930
+ if (v && typeof v === "object" && !Array.isArray(v)) {
1931
+ for (const [k, val] of Object.entries(v)) {
1932
+ if (typeof val === "number" && val >= 0 && val <= 1) out[k] = val;
1933
+ }
1934
+ }
1935
+ return out;
1936
+ }
1937
+ function stringMap(v) {
1938
+ const out = {};
1939
+ if (v && typeof v === "object" && !Array.isArray(v)) {
1940
+ for (const [k, val] of Object.entries(v)) {
1941
+ if (typeof val === "string") out[k] = val;
1942
+ }
1943
+ }
1944
+ return out;
1945
+ }
1946
+ function round2(n) {
1947
+ return Math.round(n * 100) / 100;
1948
+ }
1949
+ var META_PROVENANCE_KEY = "meta-provenance";
1950
+ function graphPointer(field) {
1951
+ return `/graph/${field.replace(/~/g, "~0").replace(/\//g, "~1")}`;
1952
+ }
1953
+ async function runFill(opts = {}) {
1954
+ const cwd = opts.cwd ?? process.cwd();
1955
+ const config = loadRunConfig(
1956
+ {
1957
+ ...opts.config === void 0 ? {} : { configPath: opts.config },
1958
+ ...opts.noConfig === void 0 ? {} : { noConfig: opts.noConfig },
1959
+ ...opts.paths === void 0 ? {} : { paths: opts.paths },
1960
+ ...opts.collection === void 0 ? {} : { collection: opts.collection }
1961
+ },
1962
+ cwd
1963
+ );
1964
+ const flags = {
1965
+ ...opts.provider === void 0 ? {} : { provider: opts.provider },
1966
+ ...opts.model === void 0 ? {} : { model: opts.model },
1967
+ ...opts.local === void 0 ? {} : { local: opts.local }
1968
+ };
1969
+ const selection = selectProvider2(config, flags);
1970
+ assertProviderSelection(selection);
1971
+ const format = assertInputFormat(opts.paths ?? [], opts.as);
1972
+ const base2 = documentBase(config, opts, cwd);
1973
+ const files = resolveDocumentSet(config, opts, "fill", cwd, base2);
1974
+ const usingStdin = (opts.paths ?? []).includes(STDIN);
1975
+ if (files.length === 0 && !usingStdin) {
1976
+ if (opts.allowEmpty) {
1977
+ return {
1978
+ results: [],
1979
+ turnsUsed: 0,
1980
+ maxTurns: opts.maxTurns ?? config.fill.maxTurns,
1981
+ warnings: [],
1982
+ exitCode: 0
1983
+ };
1984
+ }
1985
+ throw new GraphError(
1986
+ `No input files matched: ${documentSetPatterns(config, opts).join(", ")} (cwd: ${cwd})`
1987
+ );
1988
+ }
1989
+ if (usingStdin && opts.stdinContent === void 0) {
1990
+ throw new GraphError("graph fill was given `-` but no stdin content.");
1991
+ }
1992
+ const stdinContent = usingStdin ? opts.stdinContent : void 0;
1993
+ const analyzeOptions = {
1994
+ routes: routesFrom(config, base2),
1995
+ ...format === void 0 ? {} : { format }
1996
+ };
1997
+ let identity;
1998
+ let construct;
1999
+ if (opts.providerInstance) {
2000
+ const injected = opts.providerInstance;
2001
+ identity = { provider: injected.provider(), model: injected.modelName() };
2002
+ construct = () => injected;
2003
+ announceSelection(selection);
2004
+ } else {
2005
+ const resolved = await resolveProviderIdentity(config, flags);
2006
+ identity = resolved;
2007
+ construct = () => constructProvider(config, resolved);
2008
+ }
2009
+ let provider;
2010
+ const getProvider = () => provider ??= construct();
2011
+ const fields = opts.fields ?? config.fill.fields;
2012
+ const withSections = opts.sections ?? config.fill.sections;
2013
+ const sectionFields = withSections ? fields.filter((f) => SECTION_FILL_FIELDS.includes(f)) : void 0;
2014
+ const validateProposal = validatorFor(
2015
+ proposalSchema(fields, { sections: sectionFields, lenient: true })
2016
+ );
2017
+ const cache = new FillCache(
2018
+ resolve6(cwd, config.fill.cacheDir),
2019
+ !opts.noCache
2020
+ );
2021
+ const confidenceThreshold = opts.confidence ?? config.fill.confidenceThreshold;
2022
+ const maxTurns = opts.maxTurns ?? config.fill.maxTurns;
2023
+ const warnings = [];
2024
+ const sectionsUnrecorded = [];
2025
+ const allPaths = new Set(
2026
+ stdinContent === void 0 ? files : [...files, STDIN_PATH]
2027
+ );
2028
+ const results = [];
2029
+ let turnsUsed = 0;
2030
+ const shapesPaths = config.check.shapes.length > 0 ? config.check.shapes.map((p) => resolve6(cwd, p)) : [bundledShapesPath(import.meta.url)];
2031
+ const guardFiles = [
2032
+ .../* @__PURE__ */ new Set([
2033
+ ...config.collections.length > 0 ? resolveDocumentSet(config, {}, "fill", cwd, base2) : [],
2034
+ ...files
2035
+ ])
2036
+ ];
2037
+ const inputs = new Set(files);
2038
+ const formatFor = (path2) => inputs.has(path2) ? format : void 0;
2039
+ const mergeErrors = /* @__PURE__ */ new Map();
2040
+ const view = await openMetaViewPerPage(
2041
+ {
2042
+ ...opts.config === void 0 ? {} : { configPath: opts.config },
2043
+ ...opts.noConfig === void 0 ? {} : { noConfig: opts.noConfig }
2044
+ },
2045
+ cwd,
2046
+ guardFiles,
2047
+ base2,
2048
+ mergeErrors
2049
+ );
2050
+ const mergedTexts = /* @__PURE__ */ new Map();
2051
+ const inline = /* @__PURE__ */ new Map();
2052
+ for (const path2 of guardFiles) {
2053
+ if (mergeErrors.has(path2)) continue;
2054
+ let text;
2055
+ try {
2056
+ text = readFileSync4(resolve6(base2, path2), "utf8");
2057
+ } catch (e) {
2058
+ mergeErrors.set(path2, e);
2059
+ continue;
2060
+ }
2061
+ let asMetaReadsIt;
2062
+ try {
2063
+ asMetaReadsIt = await mergedText(
2064
+ view,
2065
+ path2,
2066
+ text,
2067
+ formatOf(path2, formatFor(path2))
2068
+ );
2069
+ } catch (e) {
2070
+ mergeErrors.set(path2, e);
2071
+ continue;
2072
+ }
2073
+ mergedTexts.set(path2, asMetaReadsIt);
2074
+ if (asMetaReadsIt !== text) inline.set(path2, asMetaReadsIt);
2075
+ }
2076
+ if (stdinContent !== void 0) inline.set(STDIN_PATH, stdinContent);
2077
+ const guard = !opts.noValidateGraph && config.fill.validateGraph ? FillGuard.create(
2078
+ guardFiles.filter((path2) => !mergeErrors.has(path2)),
2079
+ base2,
2080
+ { ...config, routes: routesFrom(config, base2) },
2081
+ shapesPaths,
2082
+ opts.force ?? false,
2083
+ {
2084
+ inline,
2085
+ ...format === void 0 ? {} : { format },
2086
+ inputs: /* @__PURE__ */ new Set([...files, STDIN_PATH])
2087
+ }
2088
+ ) : void 0;
2089
+ if (guard) {
2090
+ const leftOut = [
2091
+ ...guardFiles.filter((path2) => !inputs.has(path2) && mergeErrors.has(path2)),
2092
+ ...guard.unreadable.filter((path2) => !inputs.has(path2) && path2 !== STDIN_PATH)
2093
+ ].sort(byCodeUnit);
2094
+ if (leftOut.length > 0) {
2095
+ warn(
2096
+ `The graph check left out ${String(leftOut.length)} page${leftOut.length === 1 ? "" : "s"} it could not read: ${leftOut.join(", ")}. Proposals were vetted without them. \`manni meta validate\` says what is wrong with each.`
2097
+ );
2098
+ }
2099
+ }
2100
+ for (const path2 of files) {
2101
+ const absPath = resolve6(base2, path2);
2102
+ let content;
2103
+ try {
2104
+ content = readFileSync4(absPath, "utf8");
2105
+ } catch (e) {
2106
+ throw new GraphError(
2107
+ `cannot read ${path2}: ${errorMessage(e)}`
2108
+ );
2109
+ }
2110
+ try {
2111
+ results.push(await fillOne(path2, absPath, content));
2112
+ } catch (e) {
2113
+ results.push({
2114
+ path: path2,
2115
+ status: "error",
2116
+ fields: [],
2117
+ preserved: [],
2118
+ cached: false,
2119
+ error: errorMessage(e)
2120
+ });
2121
+ }
2122
+ }
2123
+ let stdinDocument;
2124
+ if (stdinContent !== void 0) {
2125
+ try {
2126
+ const result = await fillOne(STDIN_PATH, void 0, stdinContent);
2127
+ results.push(result);
2128
+ stdinDocument = result.filledContent ?? stdinContent;
2129
+ Reflect.deleteProperty(result, "filledContent");
2130
+ } catch (e) {
2131
+ results.push({
2132
+ path: STDIN_PATH,
2133
+ status: "error",
2134
+ fields: [],
2135
+ preserved: [],
2136
+ cached: false,
2137
+ error: errorMessage(e)
2138
+ });
2139
+ }
2140
+ }
2141
+ if (sectionsUnrecorded.length > 0) {
2142
+ warnings.push(
2143
+ "Section metadata was written but is NOT recorded in meta-provenance: /graph/sections is one of the three hand-curated pointers graph refuses, with /graph/revision-of and /graph/derived-from, so recording it would make `manni graph check` report an error. Review section values by hand \u2014 the review queue will not list them."
2144
+ );
2145
+ }
2146
+ const hasErrors = results.some((r) => r.status === "error");
2147
+ return {
2148
+ results,
2149
+ turnsUsed,
2150
+ maxTurns,
2151
+ warnings,
2152
+ exitCode: hasErrors ? 1 : 0,
2153
+ ...stdinDocument === void 0 ? {} : { stdinDocument }
2154
+ };
2155
+ async function fillOne(path2, absPath, content) {
2156
+ if (frontmatterKind(content) === "unsupported") {
2157
+ throw new GraphError(
2158
+ "only YAML frontmatter can be edited (found a TOML/JSON fence) \u2014 exclude this file or convert its frontmatter"
2159
+ );
2160
+ }
2161
+ const effective = absPath === void 0 ? content : mergedTexts.get(path2);
2162
+ if (effective === void 0) {
2163
+ const failure = mergeErrors.get(path2);
2164
+ throw failure instanceof Error ? failure : new GraphError(errorMessage(failure));
2165
+ }
2166
+ if (absPath !== void 0) {
2167
+ const own2 = ownMetadata(content).data;
2168
+ if (GRAPH_KEY in own2) {
2169
+ const home = await view.home(path2, own2, GRAPH_KEY);
2170
+ if (home.kind !== "unowned") {
2171
+ throw new GraphError(
2172
+ `"${GRAPH_KEY}" is owned by manifest ${home.file} (collection ${home.collection}); remove it from the document`
2173
+ );
2174
+ }
2175
+ }
2176
+ }
2177
+ const present = new Set(existingGraphFields(effective));
2178
+ const missing = opts.force ? fields : fields.filter((f) => !present.has(f));
2179
+ if (missing.length === 0 && !withSections) {
2180
+ return {
2181
+ path: path2,
2182
+ status: "complete",
2183
+ fields: [],
2184
+ preserved: [],
2185
+ cached: false
2186
+ };
2187
+ }
2188
+ const key = cacheKey(
2189
+ identity.provider,
2190
+ identity.model,
2191
+ content,
2192
+ missing,
2193
+ withSections
2194
+ );
2195
+ let proposal = cache.get(key);
2196
+ if (proposal !== void 0 && !validateProposal(proposal)) {
2197
+ proposal = void 0;
2198
+ }
2199
+ const cached = proposal !== void 0;
2200
+ let docModel;
2201
+ const doc = () => docModel ??= analyzeDoc(effective, path2, allPaths, analyzeOptions);
2202
+ if (proposal === void 0) {
2203
+ if (maxTurns !== null && turnsUsed >= maxTurns) {
2204
+ return {
2205
+ path: path2,
2206
+ status: "skipped",
2207
+ reason: TURN_BUDGET_REASON,
2208
+ fields: [],
2209
+ preserved: [],
2210
+ cached: false
2211
+ };
2212
+ }
2213
+ turnsUsed++;
2214
+ const run = await completeValidatedJSON({
2215
+ provider: getProvider(),
2216
+ system: SYSTEM_PROMPT,
2217
+ user: buildUserPrompt(doc(), content, missing, {
2218
+ sections: withSections
2219
+ }),
2220
+ schema: proposalSchema(missing, { sections: sectionFields }),
2221
+ validate: validateProposal,
2222
+ temperature: config.fill.temperature
2223
+ });
2224
+ if (run.result === void 0) {
2225
+ throw new Error(run.error ?? "provider returned no proposal");
2226
+ }
2227
+ proposal = run.result;
2228
+ cache.set(key, proposal);
2229
+ }
2230
+ const confidence = numberMap(proposal["confidence"]);
2231
+ const reasoning = stringMap(proposal["reasoning"]);
2232
+ const narrowed = Object.fromEntries(
2233
+ Object.entries(proposal).filter(([k]) => missing.includes(k)).map(([k, v]) => [k, Array.isArray(v) ? [...new Set(v)] : v])
2234
+ );
2235
+ const unknownSlugs = [];
2236
+ if (withSections) {
2237
+ const realSlugs = new Set(doc().sections.map((s) => s.slug));
2238
+ for (const entry2 of Array.isArray(proposal["sections"]) ? proposal["sections"] : []) {
2239
+ const slug = typeof entry2["slug"] === "string" ? entry2["slug"] : "";
2240
+ if (!realSlugs.has(slug)) {
2241
+ if (slug) unknownSlugs.push(slug);
2242
+ continue;
2243
+ }
2244
+ const entryConfidence = numberMap(entry2["confidence"]);
2245
+ const entryReasoning = stringMap(entry2["reasoning"]);
2246
+ for (const [field, value] of Object.entries(entry2)) {
2247
+ if (!SECTION_FILL_FIELDS.includes(field)) continue;
2248
+ if (!fields.includes(field)) continue;
2249
+ const name = `sections.${slug}.${field}`;
2250
+ narrowed[name] = Array.isArray(value) ? [...new Set(value)] : value;
2251
+ if (entryConfidence[field] !== void 0)
2252
+ confidence[name] = entryConfidence[field];
2253
+ if (entryReasoning[field] !== void 0)
2254
+ reasoning[name] = entryReasoning[field];
2255
+ }
2256
+ }
2257
+ }
2258
+ const gateLabel = () => {
2259
+ const hasLabel = present.has("label") || typeof narrowed["label"] === "string" && narrowed["label"].length > 0;
2260
+ if (!hasLabel) {
2261
+ for (const field of RELATION_FIELDS) Reflect.deleteProperty(narrowed, field);
2262
+ }
2263
+ };
2264
+ gateLabel();
2265
+ const lowConfidence = [];
2266
+ for (const field of Object.keys(narrowed)) {
2267
+ const c = confidence[field] ?? 0;
2268
+ if (c < confidenceThreshold) {
2269
+ lowConfidence.push({
2270
+ field,
2271
+ confidence: c,
2272
+ ...reasoning[field] ? { reasoning: reasoning[field] } : {}
2273
+ });
2274
+ Reflect.deleteProperty(narrowed, field);
2275
+ }
2276
+ }
2277
+ if (lowConfidence.length > 0) gateLabel();
2278
+ const lowConf = lowConfidence.length > 0 ? { lowConfidence } : {};
2279
+ let rejected;
2280
+ if (guard) {
2281
+ const vetted = await guard.vet(path2, effective, narrowed);
2282
+ if (vetted.rejected.length > 0) {
2283
+ rejected = vetted.rejected.map((r) => r.field);
2284
+ for (const field of rejected) Reflect.deleteProperty(narrowed, field);
2285
+ gateLabel();
2286
+ }
2287
+ }
2288
+ const realFields = Object.keys(narrowed).filter((k) => {
2289
+ const v = narrowed[k];
2290
+ return v !== void 0 && v !== null && !(Array.isArray(v) && v.length === 0);
2291
+ });
2292
+ if (realFields.length === 0) {
2293
+ return {
2294
+ path: path2,
2295
+ status: "nothing-proposed",
2296
+ fields: [],
2297
+ preserved: [],
2298
+ ...rejected ? { rejected } : {},
2299
+ ...unknownSlugs.length > 0 ? { unknownSections: unknownSlugs } : {},
2300
+ ...lowConf,
2301
+ cached
2302
+ };
2303
+ }
2304
+ let provenanceList;
2305
+ if (config.fill.writeProvenance) {
2306
+ const recordable = realFields.filter((f) => !f.includes(".")).sort();
2307
+ if (recordable.length < realFields.length) sectionsUnrecorded.push(path2);
2308
+ const held = existingMetaProvenance(effective);
2309
+ const priorEntry = Array.isArray(held) ? held.find(
2310
+ (e) => !!e && typeof e === "object" && !Array.isArray(e) && e["generated-by"] === identity.model
2311
+ ) : void 0;
2312
+ const priorConfidence = numberMap(priorEntry?.["confidence"]);
2313
+ const proposed = recordable.map((f) => {
2314
+ const name = graphPointer(f);
2315
+ const c = confidence[f];
2316
+ return {
2317
+ name,
2318
+ confidence: c === void 0 ? priorConfidence[name] ?? 0 : round2(c)
2319
+ };
2320
+ });
2321
+ const unscored = recordable.map((f) => [f, graphPointer(f)]).filter(
2322
+ ([f, name]) => confidence[f] === void 0 && priorConfidence[name] === void 0
2323
+ ).map(([, name]) => name);
2324
+ const merged = mergeMetaProvenance(
2325
+ held,
2326
+ identity.model,
2327
+ "fields",
2328
+ proposed
2329
+ );
2330
+ if (merged) {
2331
+ for (const name of unscored)
2332
+ Reflect.deleteProperty(merged.entry.confidence, name);
2333
+ provenanceList = merged.list;
2334
+ }
2335
+ }
2336
+ const applied = applyGraphFields(effective, path2, narrowed, {
2337
+ force: opts.force
2338
+ });
2339
+ const reportedFields = applied.applied;
2340
+ if (reportedFields.length === 0) {
2341
+ return {
2342
+ path: path2,
2343
+ status: "nothing-proposed",
2344
+ fields: [],
2345
+ preserved: applied.skipped,
2346
+ ...rejected ? { rejected } : {},
2347
+ ...unknownSlugs.length > 0 ? { unknownSections: unknownSlugs } : {},
2348
+ ...lowConf,
2349
+ cached
2350
+ };
2351
+ }
2352
+ const own = ownMetadata(content).data;
2353
+ const graphHome = absPath === void 0 ? void 0 : await view.home(path2, own, GRAPH_KEY);
2354
+ const provenanceHome = absPath === void 0 || provenanceList === void 0 ? void 0 : await view.home(path2, own, META_PROVENANCE_KEY);
2355
+ const graphTarget = writeTarget(path2, graphHome, GRAPH_KEY);
2356
+ if (typeof graphTarget === "string") throw new GraphError(graphTarget);
2357
+ let provenanceTarget = writeTarget(path2, provenanceHome, META_PROVENANCE_KEY);
2358
+ if (typeof provenanceTarget === "string") {
2359
+ warnings.push(`${provenanceTarget} The fill of ${path2} is not recorded.`);
2360
+ provenanceList = void 0;
2361
+ provenanceTarget = null;
2362
+ }
2363
+ const pageKeys = {};
2364
+ if (provenanceList !== void 0 && provenanceTarget === null) {
2365
+ pageKeys[META_PROVENANCE_KEY] = provenanceList;
2366
+ }
2367
+ const pageContent = applyGraphFields(
2368
+ content,
2369
+ path2,
2370
+ graphTarget === null ? narrowed : {},
2371
+ { force: opts.force, page: pageKeys }
2372
+ ).content;
2373
+ const manifestWrites = [];
2374
+ if (graphTarget !== null) {
2375
+ manifestWrites.push({
2376
+ home: graphTarget,
2377
+ key: GRAPH_KEY,
2378
+ value: ownMetadata(applied.content).data[GRAPH_KEY]
2379
+ });
2380
+ }
2381
+ if (provenanceList !== void 0 && provenanceTarget !== null) {
2382
+ manifestWrites.push({
2383
+ home: provenanceTarget,
2384
+ key: META_PROVENANCE_KEY,
2385
+ value: provenanceList
2386
+ });
2387
+ }
2388
+ const manifests = await spliceAll(manifestWrites);
2389
+ if (!opts.dryRun && absPath !== void 0) {
2390
+ if (pageContent !== content) writeFileSync2(absPath, pageContent, "utf8");
2391
+ for (const m of manifests) {
2392
+ if (m.text !== m.before) {
2393
+ await writeFileAtomic(m.absPath, m.text, { createParents: true });
2394
+ }
2395
+ }
2396
+ }
2397
+ guard?.commit(path2, applied.content);
2398
+ return {
2399
+ path: path2,
2400
+ status: opts.dryRun ? "proposed" : "filled",
2401
+ fields: reportedFields,
2402
+ preserved: applied.skipped,
2403
+ ...rejected ? { rejected } : {},
2404
+ ...unknownSlugs.length > 0 ? { unknownSections: unknownSlugs } : {},
2405
+ ...lowConf,
2406
+ cached,
2407
+ ...absPath === void 0 && !opts.dryRun ? { filledContent: pageContent } : {}
2408
+ };
2409
+ }
2410
+ }
2411
+ var GRAPH_KEY = "graph";
2412
+ function writeTarget(path2, home, key) {
2413
+ if (home === void 0 || home.kind === "unowned") return null;
2414
+ if (home.kind === "url") return urlManifestMessage(key, home.file);
2415
+ if (home.entry === void 0) {
2416
+ return `${path2} carries no ${home.join}, which ${home.file} joins on, so its ${key} has no entry there.`;
2417
+ }
2418
+ return {
2419
+ file: home.file,
2420
+ absPath: home.absPath,
2421
+ join: home.join,
2422
+ entry: home.entry,
2423
+ perPage: home.perPage
2424
+ };
2425
+ }
2426
+ async function spliceAll(writes) {
2427
+ const out = /* @__PURE__ */ new Map();
2428
+ for (const w of writes) {
2429
+ let held = out.get(w.home.absPath);
2430
+ if (held === void 0) {
2431
+ let before;
2432
+ try {
2433
+ before = await readFile(w.home.absPath, "utf8");
2434
+ } catch (e) {
2435
+ if (!(w.home.perPage && isMissing(e))) {
2436
+ throw new GraphError(`${w.home.file} could not be read: ${errorMessage(e)}`);
2437
+ }
2438
+ before = "";
2439
+ }
2440
+ held = { absPath: w.home.absPath, before, text: before };
2441
+ out.set(w.home.absPath, held);
2442
+ }
2443
+ try {
2444
+ held.text = spliceManifestValue(held.text, {
2445
+ entry: w.home.entry,
2446
+ key: w.key,
2447
+ value: w.value,
2448
+ join: w.home.join,
2449
+ file: w.home.file
2450
+ }).text;
2451
+ } catch (e) {
2452
+ throw new GraphError(errorMessage(e));
2453
+ }
2454
+ }
2455
+ return [...out.values()];
2456
+ }
2457
+ async function mergedText(view, path2, content, format) {
2458
+ if (frontmatterKind(content) === "unsupported") return content;
2459
+ let own;
2460
+ try {
2461
+ own = ownMetadata(content);
2462
+ } catch (e) {
2463
+ throw new GraphError(`${path2}: ${errorMessage(e)}`);
2464
+ }
2465
+ const { supplied } = await mergePage(view, path2, own.data, own.present, format);
2466
+ if (Object.keys(supplied).length === 0) return content;
2467
+ return applyGraphFields(content, path2, {}, { page: supplied }).content;
2468
+ }
2469
+ async function openMetaViewPerPage(source, cwd, paths, base2, failures) {
2470
+ try {
2471
+ return await openMetaView(source, cwd, paths, base2);
2472
+ } catch (whole) {
2473
+ await openMetaView(source, cwd, [], base2);
2474
+ const readable = [];
2475
+ for (const path2 of paths) {
2476
+ try {
2477
+ await openMetaView(source, cwd, [path2], base2);
2478
+ readable.push(path2);
2479
+ } catch (e) {
2480
+ failures.set(path2, e);
2481
+ }
2482
+ }
2483
+ if (readable.length === paths.length) throw whole;
2484
+ return openMetaView(source, cwd, readable, base2);
2485
+ }
2486
+ }
2487
+ function renderFill(report, format) {
2488
+ if (format === "json") {
2489
+ return JSON.stringify({ ...report, stdinDocument: void 0 }, null, 2);
2490
+ }
2491
+ const lines = [];
2492
+ for (const r of report.results) {
2493
+ const dropped = r.rejected && r.rejected.length > 0 ? ` [graph check rejected: ${r.rejected.join(", ")}]` : "";
2494
+ const lowConf = r.lowConfidence && r.lowConfidence.length > 0 ? ` [low confidence, not written: ${r.lowConfidence.map((l) => `${l.field} ${l.confidence.toFixed(2)}`).join(", ")}]` : "";
2495
+ const unknown = r.unknownSections && r.unknownSections.length > 0 ? ` [no such section: ${r.unknownSections.join(", ")}]` : "";
2496
+ switch (r.status) {
2497
+ case "filled":
2498
+ lines.push(
2499
+ `filled ${r.path} (${r.fields.join(", ")})${r.cached ? " [cached]" : ""}${dropped}${unknown}${lowConf}`
2500
+ );
2501
+ break;
2502
+ case "proposed":
2503
+ lines.push(
2504
+ `proposed ${r.path} (${r.fields.join(", ")})${r.cached ? " [cached]" : ""}${dropped}${unknown}${lowConf} \u2014 dry run, not written`
2505
+ );
2506
+ break;
2507
+ case "complete":
2508
+ lines.push(`complete ${r.path}`);
2509
+ break;
2510
+ case "nothing-proposed":
2511
+ lines.push(
2512
+ `no-op ${r.path} (model proposed nothing new)${dropped}${unknown}${lowConf}`
2513
+ );
2514
+ break;
2515
+ case "skipped":
2516
+ lines.push(`skipped ${r.path} (${r.reason ?? "not processed"})`);
2517
+ break;
2518
+ case "error":
2519
+ lines.push(`error ${r.path}: ${r.error ?? ""}`);
2520
+ break;
2521
+ }
2522
+ }
2523
+ if (report.maxTurns !== null && report.results.some((r) => r.status === "skipped")) {
2524
+ lines.push(
2525
+ "",
2526
+ `--max-turns reached (${report.maxTurns}); some pages were not processed.`
2527
+ );
2528
+ }
2529
+ return lines.join("\n");
2530
+ }
2531
+
2532
+ // src/graph/commands/embed.ts
2533
+ import { createHash as createHash2 } from "node:crypto";
2534
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
2535
+ import path, { dirname as dirname2, join as join2, resolve as resolve7 } from "node:path";
2536
+
2537
+ // src/graph/core/vector-index.ts
2538
+ var MAGIC = new Uint8Array([77, 71, 82, 86]);
2539
+ var FORMAT_VERSION = 2;
2540
+ var HEADER_OFFSET = 12;
2541
+ var VectorIndexError = class extends Error {
2542
+ constructor(message) {
2543
+ super(message);
2544
+ this.name = "VectorIndexError";
2545
+ }
2546
+ };
2547
+ function normalize(vector) {
2548
+ let sum = 0;
2549
+ for (const value of vector) sum += value * value;
2550
+ if (sum === 0) return vector;
2551
+ const inv = 1 / Math.sqrt(sum);
2552
+ vector.set(vector.map((value) => value * inv));
2553
+ return vector;
2554
+ }
2555
+ function encodeVectorIndex(entries, meta) {
2556
+ const sorted = [...entries].sort((a, b) => byCodeUnit(a.id, b.id));
2557
+ const dims = sorted[0]?.vector.length ?? 0;
2558
+ for (const entry2 of sorted) {
2559
+ if (entry2.vector.length !== dims) {
2560
+ throw new VectorIndexError(
2561
+ `Inconsistent vector dimensions: ${entry2.id} has ${entry2.vector.length}, expected ${dims}.`
2562
+ );
2563
+ }
2564
+ }
2565
+ const header = {
2566
+ model: meta.model,
2567
+ dtype: meta.dtype,
2568
+ dims,
2569
+ count: sorted.length,
2570
+ ids: sorted.map((e) => e.id),
2571
+ source: meta.source,
2572
+ language: meta.language
2573
+ };
2574
+ const headerBytes = new TextEncoder().encode(JSON.stringify(header));
2575
+ const payloadBytes = sorted.length * dims * 4;
2576
+ const out = new Uint8Array(HEADER_OFFSET + headerBytes.length + payloadBytes);
2577
+ const view = new DataView(out.buffer);
2578
+ out.set(MAGIC, 0);
2579
+ view.setUint32(4, FORMAT_VERSION, true);
2580
+ view.setUint32(8, headerBytes.length, true);
2581
+ out.set(headerBytes, HEADER_OFFSET);
2582
+ let offset = HEADER_OFFSET + headerBytes.length;
2583
+ for (const entry2 of sorted) {
2584
+ const vector = normalize(Float32Array.from(entry2.vector));
2585
+ for (const value of vector) {
2586
+ view.setFloat32(offset, value, true);
2587
+ offset += 4;
2588
+ }
2589
+ }
2590
+ return out;
2591
+ }
2592
+ function decodeVectorIndex(bytes) {
2593
+ if (bytes.length < HEADER_OFFSET) {
2594
+ throw new VectorIndexError("Not a manni graph vector index: file is too short.");
2595
+ }
2596
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2597
+ if (!MAGIC.every((byte, i) => bytes[i] === byte)) {
2598
+ throw new VectorIndexError(
2599
+ "Not a manni graph vector index: bad magic \u2014 is this the right file?"
2600
+ );
2601
+ }
2602
+ const version = view.getUint32(4, true);
2603
+ if (version !== FORMAT_VERSION) {
2604
+ throw new VectorIndexError(
2605
+ `Unsupported vector index version ${version} (this build reads ${FORMAT_VERSION}) \u2014 re-run \`manni graph embed\`.`
2606
+ );
2607
+ }
2608
+ const headerLength = view.getUint32(8, true);
2609
+ if (HEADER_OFFSET + headerLength > bytes.length) {
2610
+ throw new VectorIndexError("Vector index is truncated: header overruns.");
2611
+ }
2612
+ let header;
2613
+ try {
2614
+ header = JSON.parse(
2615
+ new TextDecoder().decode(
2616
+ bytes.subarray(HEADER_OFFSET, HEADER_OFFSET + headerLength)
2617
+ )
2618
+ );
2619
+ } catch {
2620
+ throw new VectorIndexError("Vector index header is not valid JSON.");
2621
+ }
2622
+ if (!Array.isArray(header.ids) || typeof header.dims !== "number" || header.ids.length !== header.count) {
2623
+ throw new VectorIndexError("Vector index header is malformed.");
2624
+ }
2625
+ const start = HEADER_OFFSET + headerLength;
2626
+ const expected = header.count * header.dims * 4;
2627
+ if (bytes.length - start !== expected) {
2628
+ throw new VectorIndexError(
2629
+ `Vector index is truncated: expected ${expected} payload bytes, found ${bytes.length - start}.`
2630
+ );
2631
+ }
2632
+ const vectors = new Float32Array(header.count * header.dims);
2633
+ for (let i = 0; i < vectors.length; i++) {
2634
+ vectors[i] = view.getFloat32(start + i * 4, true);
2635
+ }
2636
+ return { header, vectors };
2637
+ }
2638
+
2639
+ // src/graph/embed/local.ts
2640
+ var EmbedderUnavailableError = class extends Error {
2641
+ constructor(detail) {
2642
+ super(
2643
+ `Local embeddings need @huggingface/transformers, which is an optional peer dependency.
2644
+ Install it: npm install @huggingface/transformers
2645
+ (${detail})`
2646
+ );
2647
+ this.name = "EmbedderUnavailableError";
2648
+ }
2649
+ };
2650
+ async function loadTransformers(injected) {
2651
+ if (injected) return injected;
2652
+ try {
2653
+ return await import("@huggingface/transformers");
2654
+ } catch (e) {
2655
+ throw new EmbedderUnavailableError(
2656
+ errorMessage(e)
2657
+ );
2658
+ }
2659
+ }
2660
+ async function createLocalEmbedder(options = {}) {
2661
+ const model = options.model ?? DEFAULT_MODEL;
2662
+ const dtype = options.dtype ?? "q8";
2663
+ const role = options.role ?? "passage";
2664
+ const profile = profileFor(model);
2665
+ const transformers = await loadTransformers(options.transformers);
2666
+ if (transformers.env?.backends?.onnx?.wasm) {
2667
+ transformers.env.backends.onnx.wasm.numThreads = 1;
2668
+ }
2669
+ const extractor = await transformers.pipeline("feature-extraction", model, {
2670
+ // No `device`: its accepted values are disjoint across platforms, so any
2671
+ // hardcoded choice throws on one of them (ADR 01025).
2672
+ ...options.device === void 0 ? {} : { device: options.device },
2673
+ dtype
2674
+ });
2675
+ let dims = 0;
2676
+ return {
2677
+ model,
2678
+ dtype,
2679
+ get dims() {
2680
+ return dims;
2681
+ },
2682
+ async embed(text) {
2683
+ const output = await extractor(withPrefix(profile, role, text), {
2684
+ pooling: "mean",
2685
+ normalize: true
2686
+ });
2687
+ const data = Float32Array.from(output.data);
2688
+ dims = data.length;
2689
+ return data;
2690
+ }
2691
+ };
2692
+ }
2693
+
2694
+ // src/graph/embed/mock.ts
2695
+ function fnv1a(text, seed) {
2696
+ let hash = (2166136261 ^ seed) >>> 0;
2697
+ for (let i = 0; i < text.length; i++) {
2698
+ hash = (hash ^ text.charCodeAt(i)) >>> 0;
2699
+ hash = hash + ((hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)) >>> 0;
2700
+ }
2701
+ return hash >>> 0;
2702
+ }
2703
+ function createMockEmbedder(options = {}) {
2704
+ const dims = options.dims ?? 8;
2705
+ const model = options.model ?? "mock";
2706
+ const dtype = options.dtype ?? "mock";
2707
+ return {
2708
+ model,
2709
+ dtype,
2710
+ dims,
2711
+ embed(text) {
2712
+ const vector = new Float32Array(dims);
2713
+ for (let i = 0; i < dims; i++) {
2714
+ vector[i] = fnv1a(text, i) / 4294967296 * 2 - 1;
2715
+ }
2716
+ return Promise.resolve(vector);
2717
+ }
2718
+ };
2719
+ }
2720
+
2721
+ // src/graph/commands/embed.ts
2722
+ function embedText(entry2) {
2723
+ return [entry2.title, entry2.labels, entry2.description, entry2.text].filter((part) => part !== void 0 && part !== "").join("\n\n");
2724
+ }
2725
+ function sha2562(text) {
2726
+ return createHash2("sha256").update(text, "utf8").digest("hex");
2727
+ }
2728
+ var VectorCache = class {
2729
+ constructor(dir, enabled) {
2730
+ this.dir = dir;
2731
+ this.enabled = enabled;
2732
+ }
2733
+ dir;
2734
+ enabled;
2735
+ path(key) {
2736
+ return join2(this.dir, `${key}.json`);
2737
+ }
2738
+ get(key) {
2739
+ if (!this.enabled) return void 0;
2740
+ const file = this.path(key);
2741
+ if (!existsSync3(file)) return void 0;
2742
+ try {
2743
+ const parsed = JSON.parse(readFileSync5(file, "utf8"));
2744
+ return Array.isArray(parsed) ? Float32Array.from(parsed) : void 0;
2745
+ } catch {
2746
+ return void 0;
2747
+ }
2748
+ }
2749
+ set(key, vector) {
2750
+ if (!this.enabled) return;
2751
+ mkdirSync2(this.dir, { recursive: true });
2752
+ writeFileSync3(this.path(key), JSON.stringify([...vector]), "utf8");
2753
+ }
2754
+ };
2755
+ async function makeEmbedder(opts, model, dtype) {
2756
+ if (opts.embedder) return opts.embedder;
2757
+ if (model === "mock") return createMockEmbedder({ dtype });
2758
+ try {
2759
+ return await createLocalEmbedder({ model, dtype, role: "passage" });
2760
+ } catch (e) {
2761
+ if (e instanceof EmbedderUnavailableError) {
2762
+ throw new GraphError(e.message);
2763
+ }
2764
+ throw new GraphError(
2765
+ `Failed to load embedding model ${model}: ${errorMessage(e)}`
2766
+ );
2767
+ }
2768
+ }
2769
+ function manifestRelativePath(indexDir, outPath, pathApi = path) {
2770
+ const rel = pathApi.relative(indexDir, outPath);
2771
+ const recorded = rel.split(pathApi.sep).join("/");
2772
+ if (pathApi.isAbsolute(rel) || !isSafeRelativePath(recorded)) {
2773
+ throw new GraphError(
2774
+ `Cannot record vectors at ${outPath}: the localization manifest needs a path relative to ${indexDir}. Write the sidecars on the index's drive.`
2775
+ );
2776
+ }
2777
+ return recorded;
2778
+ }
2779
+ async function runEmbed(opts = {}) {
2780
+ const cwd = opts.cwd ?? process.cwd();
2781
+ const config = loadRunConfig(
2782
+ {
2783
+ ...opts.config === void 0 ? {} : { configPath: opts.config },
2784
+ ...opts.noConfig === void 0 ? {} : { noConfig: opts.noConfig }
2785
+ },
2786
+ cwd
2787
+ );
2788
+ const graphPath = resolve7(cwd, opts.graph ?? config.out);
2789
+ const indexDir = opts.index ? resolve7(cwd, opts.index) : dirname2(graphPath);
2790
+ const manifestPath = join2(indexDir, LOCALIZATIONS_FILENAME);
2791
+ if (!existsSync3(manifestPath)) {
2792
+ throw new GraphError(
2793
+ `Localization manifest not found: ${manifestPath} \u2014 run \`manni graph export search\` first.`
2794
+ );
2795
+ }
2796
+ const manifest = parseLocalizations(readFileSync5(manifestPath, "utf8"));
2797
+ if (!manifest) {
2798
+ throw new GraphError(
2799
+ `Not a manni graph localization manifest: ${manifestPath} \u2014 re-run \`manni graph export search\`.`
2800
+ );
2801
+ }
2802
+ const outSetting = opts.out ?? config.embed.out;
2803
+ const outDir = outSetting ? resolve7(cwd, outSetting) : indexDir;
2804
+ mkdirSync2(outDir, { recursive: true });
2805
+ const cache = new VectorCache(
2806
+ resolve7(cwd, config.embed.cacheDir),
2807
+ !opts.noCache
2808
+ );
2809
+ const embedders = /* @__PURE__ */ new Map();
2810
+ const getEmbedder = (model, dtype) => {
2811
+ const key = JSON.stringify([model, dtype]);
2812
+ const existing = embedders.get(key);
2813
+ if (existing) return existing;
2814
+ const built = makeEmbedder(opts, model, dtype);
2815
+ embedders.set(key, built);
2816
+ return built;
2817
+ };
2818
+ const languages = [];
2819
+ const updated = [];
2820
+ for (const entry2 of manifest.languages) {
2821
+ const indexPath = join2(indexDir, entry2.search.path);
2822
+ if (!existsSync3(indexPath)) {
2823
+ throw new GraphError(
2824
+ `Search index not found: ${indexPath} \u2014 the manifest names it; re-run \`manni graph export search\`.`
2825
+ );
2826
+ }
2827
+ const raw = readFileSync5(indexPath, "utf8");
2828
+ const source = `sha256:${sha2562(raw)}`;
2829
+ if (entry2.search.digest !== source) {
2830
+ throw new GraphError(
2831
+ `Stale manifest: ${entry2.search.path} does not match the digest recorded for "${entry2.language}" \u2014 re-run \`manni graph export search\`.`
2832
+ );
2833
+ }
2834
+ let parsed;
2835
+ try {
2836
+ parsed = JSON.parse(raw);
2837
+ } catch (e) {
2838
+ throw new GraphError(
2839
+ `Failed to parse ${indexPath}: ${errorMessage(e)} \u2014 re-run \`manni graph export search\`.`
2840
+ );
2841
+ }
2842
+ const entries = parsed?.entries;
2843
+ if (!Array.isArray(entries)) {
2844
+ throw new GraphError(
2845
+ `Not a manni graph search index: ${indexPath} \u2014 expected an \`entries\` array.`
2846
+ );
2847
+ }
2848
+ const wanted = canonicalLanguageTag(entry2.language);
2849
+ const perLanguage = Object.entries(config.embed.byLanguage).find(
2850
+ ([tag]) => canonicalLanguageTag(tag) === wanted
2851
+ )?.[1];
2852
+ const model = opts.model ?? perLanguage?.model ?? config.embed.model;
2853
+ const dtype = opts.dtype ?? perLanguage?.dtype ?? config.embed.dtype;
2854
+ const identity = opts.embedder ? { model: opts.embedder.model, dtype: opts.embedder.dtype } : { model, dtype };
2855
+ const vectors = [];
2856
+ let embedded = 0;
2857
+ let cached = 0;
2858
+ for (const item of entries) {
2859
+ const text = embedText(item);
2860
+ if (text === "") continue;
2861
+ const key = sha2562(
2862
+ JSON.stringify([identity.model, identity.dtype, text])
2863
+ );
2864
+ let vector = cache.get(key);
2865
+ if (vector) {
2866
+ cached += 1;
2867
+ } else {
2868
+ vector = await (await getEmbedder(model, dtype)).embed(text);
2869
+ cache.set(key, vector);
2870
+ embedded += 1;
2871
+ }
2872
+ vectors.push({ id: item.id, vector });
2873
+ }
2874
+ const filename = vectorIndexFilename(entry2.language);
2875
+ const outPath = join2(outDir, filename);
2876
+ const manifestRelative = manifestRelativePath(indexDir, outPath);
2877
+ writeFileSync3(
2878
+ outPath,
2879
+ encodeVectorIndex(vectors, {
2880
+ model: identity.model,
2881
+ dtype: identity.dtype,
2882
+ source,
2883
+ language: entry2.language
2884
+ })
2885
+ );
2886
+ const dims = vectors[0]?.vector.length ?? 0;
2887
+ languages.push({
2888
+ language: entry2.language,
2889
+ outPath,
2890
+ model: identity.model,
2891
+ dims,
2892
+ embedded,
2893
+ cached,
2894
+ total: vectors.length
2895
+ });
2896
+ updated.push({
2897
+ ...entry2,
2898
+ vectors: {
2899
+ path: manifestRelative,
2900
+ model: identity.model,
2901
+ dtype: identity.dtype,
2902
+ dims,
2903
+ count: vectors.length
2904
+ }
2905
+ });
2906
+ }
2907
+ writeFileSync3(
2908
+ manifestPath,
2909
+ emitLocalizations({ version: 1, languages: updated }),
2910
+ "utf8"
2911
+ );
2912
+ return {
2913
+ manifestPath,
2914
+ languages,
2915
+ embedded: languages.reduce((n, l) => n + l.embedded, 0),
2916
+ cached: languages.reduce((n, l) => n + l.cached, 0),
2917
+ total: languages.reduce((n, l) => n + l.total, 0)
2918
+ };
2919
+ }
2920
+ function renderEmbed(report, format) {
2921
+ if (format === "json") return JSON.stringify(report, null, 2);
2922
+ const lines = report.languages.map(
2923
+ (l) => ` ${l.language}: ${l.total} vector${l.total === 1 ? "" : "s"} (${l.dims}-d, ${l.model}) -> ${l.outPath}`
2924
+ );
2925
+ if (lines.length === 0) lines.push(" (no languages in the manifest)");
2926
+ const n = report.languages.length;
2927
+ return [
2928
+ // Not "beside": `-o` may put the sidecars in a different directory from
2929
+ // the manifest, and each line below names the path it actually wrote.
2930
+ `Wrote ${n} sidecar${n === 1 ? "" : "s"}, recorded in ${report.manifestPath}`,
2931
+ ...lines,
2932
+ ` ${report.embedded} embedded, ${report.cached} from cache`
2933
+ ].join("\n");
2934
+ }
2935
+
2936
+ // src/graph/commands/search.ts
2937
+ import { createHash as createHash3 } from "node:crypto";
2938
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "node:fs";
2939
+ import { dirname as dirname3, join as join3, resolve as resolve8 } from "node:path";
2940
+
2941
+ // src/graph/runtime/vector.ts
2942
+ var VectorMismatchError = class extends Error {
2943
+ reason;
2944
+ constructor(mismatch) {
2945
+ super(mismatch.detail);
2946
+ this.name = "VectorMismatchError";
2947
+ this.reason = mismatch.reason;
2948
+ }
2949
+ };
2950
+ async function searchIndexDigest2(raw) {
2951
+ const webCrypto = globalThis.crypto;
2952
+ const subtle = webCrypto?.subtle;
2953
+ if (!subtle) {
2954
+ throw new Error(
2955
+ "searchIndexDigest needs Web Crypto (globalThis.crypto.subtle), which is unavailable here \u2014 a page served over plain HTTP does not get it."
2956
+ );
2957
+ }
2958
+ const digest = await subtle.digest("SHA-256", new TextEncoder().encode(raw));
2959
+ const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
2960
+ return `sha256:${hex}`;
2961
+ }
2962
+ function createVectorIndex(bytes) {
2963
+ const { header, vectors } = decodeVectorIndex(bytes);
2964
+ const { dims, ids } = header;
2965
+ return {
2966
+ model: header.model,
2967
+ dtype: header.dtype,
2968
+ dims,
2969
+ source: header.source,
2970
+ size: () => ids.length,
2971
+ idAt: (row) => ids[row],
2972
+ check(expected) {
2973
+ if (expected.model !== void 0 && expected.model !== header.model) {
2974
+ return {
2975
+ reason: "model",
2976
+ detail: `Vector index was built with ${header.model}, but the embedder is ${expected.model}. Re-run \`manni graph embed\`.`
2977
+ };
2978
+ }
2979
+ if (expected.dtype !== void 0 && expected.dtype !== header.dtype) {
2980
+ return {
2981
+ reason: "dtype",
2982
+ detail: `Vector index was built at dtype ${header.dtype}, but the embedder is at ${expected.dtype} \u2014 different weights are a different function. Re-run \`manni graph embed\`.`
2983
+ };
2984
+ }
2985
+ if (expected.dims !== void 0 && expected.dims !== dims) {
2986
+ return {
2987
+ reason: "dims",
2988
+ detail: `Vector index has ${dims} dimensions, but the embedder produces ${expected.dims}.`
2989
+ };
2990
+ }
2991
+ if (expected.source !== void 0 && expected.source !== header.source) {
2992
+ return {
2993
+ reason: "stale-source",
2994
+ detail: "Vector index was built from a different search index \u2014 the corpus changed. Re-run `manni graph embed`."
2995
+ };
2996
+ }
2997
+ return void 0;
2998
+ },
2999
+ search(query, options = {}) {
3000
+ if (dims === 0 || ids.length === 0) return [];
3001
+ if (query.length !== dims) {
3002
+ throw new VectorMismatchError({
3003
+ reason: "dims",
3004
+ detail: `Query vector has ${query.length} dimensions, but the index has ${dims}.`
3005
+ });
3006
+ }
3007
+ const limit = normalizeLimit(options.limit);
3008
+ if (limit === 0) return [];
3009
+ const minScore = options.minScore ?? 0;
3010
+ const q = normalize(Float32Array.from(query));
3011
+ const scored = [];
3012
+ for (const [row, iri2] of ids.entries()) {
3013
+ const stored = vectors.subarray(row * dims, (row + 1) * dims);
3014
+ let score = 0;
3015
+ for (const [i, value] of q.entries()) score += value * (stored[i] ?? 0);
3016
+ if (score >= minScore) {
3017
+ scored.push({ iri: iri2, score, via: "vector" });
3018
+ }
3019
+ }
3020
+ scored.sort(
3021
+ (a, b) => a.score === b.score ? byCodeUnit(a.iri, b.iri) : b.score - a.score
3022
+ );
3023
+ return scored.slice(0, limit);
3024
+ }
3025
+ };
3026
+ }
3027
+ function normalizeLimit(limit) {
3028
+ return typeof limit === "number" && Number.isInteger(limit) && limit >= 0 ? limit : 10;
3029
+ }
3030
+
3031
+ // src/graph/runtime/trace.ts
3032
+ function createTrace() {
3033
+ return { entry: [], hops: [], exclusions: [], resolutions: [] };
3034
+ }
3035
+ function reachedNodes(trace) {
3036
+ const reached = /* @__PURE__ */ new Set();
3037
+ for (const e of trace.entry) reached.add(e.iri);
3038
+ for (const h of trace.hops) reached.add(h.to);
3039
+ return reached;
3040
+ }
3041
+
3042
+ // src/graph/runtime/entry.ts
3043
+ function rrfMerge(rankings, k = 60) {
3044
+ const fused = /* @__PURE__ */ new Map();
3045
+ for (const ranking of rankings) {
3046
+ ranking.forEach((candidate, i) => {
3047
+ const current = fused.get(candidate.iri) ?? {
3048
+ score: 0,
3049
+ vias: /* @__PURE__ */ new Set()
3050
+ };
3051
+ current.score += 1 / (k + i + 1);
3052
+ current.vias.add(candidate.via);
3053
+ fused.set(candidate.iri, current);
3054
+ });
3055
+ }
3056
+ return [...fused.entries()].map(([iri2, { score, vias }]) => {
3057
+ const [only] = vias;
3058
+ return {
3059
+ iri: iri2,
3060
+ score,
3061
+ via: vias.size === 1 && only !== void 0 ? only : "hybrid"
3062
+ };
3063
+ }).sort(
3064
+ (a, b) => a.score === b.score ? byCodeUnit(a.iri, b.iri) : b.score - a.score
3065
+ );
3066
+ }
3067
+ async function findEntry(query, options) {
3068
+ const trace = options.trace ?? createTrace();
3069
+ const limit = typeof options.limit === "number" && Number.isInteger(options.limit) && options.limit >= 0 ? options.limit : 10;
3070
+ const lexical = options.lexical.search(query, { limit });
3071
+ const embedder = options.embedder;
3072
+ const embed = embedder ? (q) => embedder.embed(q) : options.embedQuery;
3073
+ let vector = [];
3074
+ if (options.vectors && embed && query.trim() !== "") {
3075
+ const mismatch = options.vectors.check({
3076
+ ...options.embedder ? { model: options.embedder.model, dtype: options.embedder.dtype } : {},
3077
+ // Staleness is checked whichever way the query is embedded — it is a
3078
+ // property of the corpus, not of the embedder, so even the unverified
3079
+ // `embedQuery` path gets it when the caller supplies a digest.
3080
+ ...options.source === void 0 ? {} : { source: options.source }
3081
+ });
3082
+ if (mismatch) throw new VectorMismatchError(mismatch);
3083
+ const queryVector = await embed(query);
3084
+ vector = options.vectors.search(queryVector, { limit });
3085
+ }
3086
+ const rankings = vector.length > 0 ? [lexical, vector] : [lexical];
3087
+ const candidates = rrfMerge(rankings).slice(0, limit);
3088
+ for (const candidate of candidates) trace.entry.push(candidate);
3089
+ return { candidates, lexical, vector, trace };
3090
+ }
3091
+
3092
+ // src/graph/runtime/lexical.ts
3093
+ import MiniSearch from "minisearch";
3094
+ function createLexicalIndex(input) {
3095
+ const doc = typeof input === "string" ? JSON.parse(input) : input;
3096
+ const entries = Array.isArray(doc?.entries) ? doc.entries : [];
3097
+ const mini = new MiniSearch({
3098
+ fields: ["title", "labels", "description", "text"],
3099
+ storeFields: ["id"],
3100
+ idField: "id",
3101
+ // Titles and labels are the strongest signal that a node *is* the subject;
3102
+ // body text is evidence it merely mentions it.
3103
+ searchOptions: {
3104
+ boost: { title: 3, labels: 2, description: 1.5 },
3105
+ prefix: true,
3106
+ fuzzy: 0.2
3107
+ }
3108
+ });
3109
+ mini.addAll(entries);
3110
+ const byIri = new Map(entries.map((e) => [e.id, e]));
3111
+ return {
3112
+ size: () => entries.length,
3113
+ entry: (iri2) => byIri.get(iri2),
3114
+ search(query, options = {}) {
3115
+ const trimmed = query.trim();
3116
+ if (trimmed === "") return [];
3117
+ const hits = mini.search(trimmed);
3118
+ hits.sort(
3119
+ (a, b) => a.score === b.score ? byCodeUnit(String(a.id), String(b.id)) : b.score - a.score
3120
+ );
3121
+ return hits.slice(0, options.limit ?? 10).map((h) => ({ iri: String(h.id), score: h.score, via: "lexical" }));
3122
+ }
3123
+ };
3124
+ }
3125
+
3126
+ // src/graph/commands/search.ts
3127
+ var SEARCH_MODES = ["lexical", "hybrid", "vector"];
3128
+ function loadSearchIndex(indexPath) {
3129
+ const raw = readFileSync6(indexPath, "utf8");
3130
+ let parsed;
3131
+ try {
3132
+ parsed = JSON.parse(raw);
3133
+ } catch (e) {
3134
+ throw new GraphError(
3135
+ `Failed to parse ${indexPath}: ${errorMessage(e)} \u2014 re-run \`manni graph export search\`.`
3136
+ );
3137
+ }
3138
+ const entries = parsed?.entries;
3139
+ if (!Array.isArray(entries)) {
3140
+ throw new GraphError(
3141
+ `Not a manni graph search index: ${indexPath} \u2014 expected an \`entries\` array; re-run \`manni graph export search\`.`
3142
+ );
3143
+ }
3144
+ return {
3145
+ doc: parsed,
3146
+ // Same recipe `manni graph embed` uses, so the two digests are comparable.
3147
+ source: `sha256:${createHash3("sha256").update(raw, "utf8").digest("hex")}`
3148
+ };
3149
+ }
3150
+ function resolveLocalization(indexDir, lang) {
3151
+ const manifestPath = join3(indexDir, LOCALIZATIONS_FILENAME);
3152
+ if (!existsSync4(manifestPath)) {
3153
+ throw new GraphError(
3154
+ `Localization manifest not found: ${manifestPath} \u2014 run \`manni graph export search\` first.`
3155
+ );
3156
+ }
3157
+ const manifest = parseLocalizations(readFileSync6(manifestPath, "utf8"));
3158
+ if (!manifest) {
3159
+ throw new GraphError(
3160
+ `Not a manni graph localization manifest: ${manifestPath} \u2014 re-run \`manni graph export search\`.`
3161
+ );
3162
+ }
3163
+ const available = manifest.languages.map((l) => l.language);
3164
+ if (lang !== void 0) {
3165
+ const wanted = canonicalLanguageTag(lang);
3166
+ const hit = manifest.languages.find(
3167
+ (l) => canonicalLanguageTag(l.language) === wanted
3168
+ );
3169
+ if (!hit) {
3170
+ throw new GraphError(
3171
+ `No index for language "${lang}" \u2014 this corpus has ${available.join(", ") || "none"}.`
3172
+ );
3173
+ }
3174
+ return hit;
3175
+ }
3176
+ const only = manifest.languages[0];
3177
+ if (!only) {
3178
+ throw new GraphError(
3179
+ `The localization manifest lists no languages: ${manifestPath} \u2014 re-run \`manni graph export search\`.`
3180
+ );
3181
+ }
3182
+ if (manifest.languages.length > 1) {
3183
+ throw new GraphError(
3184
+ `This corpus has more than one localization (${available.join(", ")}) \u2014 pass --lang to choose one.`
3185
+ );
3186
+ }
3187
+ return only;
3188
+ }
3189
+ async function runSearch(opts) {
3190
+ const cwd = opts.cwd ?? process.cwd();
3191
+ const config = loadRunConfig(
3192
+ {
3193
+ ...opts.config === void 0 ? {} : { configPath: opts.config },
3194
+ ...opts.noConfig === void 0 ? {} : { noConfig: opts.noConfig }
3195
+ },
3196
+ cwd
3197
+ );
3198
+ const graphPath = resolve8(cwd, opts.graph ?? config.out);
3199
+ const indexDir = opts.index ? resolve8(cwd, opts.index) : dirname3(graphPath);
3200
+ const localization = resolveLocalization(indexDir, opts.lang);
3201
+ const indexPath = join3(indexDir, localization.search.path);
3202
+ if (!existsSync4(indexPath)) {
3203
+ throw new GraphError(
3204
+ `Search index not found: ${indexPath} \u2014 the manifest names it; re-run \`manni graph export search\`.`
3205
+ );
3206
+ }
3207
+ const { doc, source } = loadSearchIndex(indexPath);
3208
+ if (localization.search.digest !== source) {
3209
+ throw new GraphError(
3210
+ `Stale manifest: ${localization.search.path} does not match the digest recorded for "${localization.language}". Re-run \`manni graph export search\`.`
3211
+ );
3212
+ }
3213
+ const lexical = createLexicalIndex(doc);
3214
+ const explicitVectors = opts.vectors !== void 0 && opts.vectors !== "" ? opts.vectors : void 0;
3215
+ const wantsVector = opts.mode === "vector" || opts.mode === "hybrid" || explicitVectors !== void 0 && opts.mode !== "lexical";
3216
+ const vectorsPath = explicitVectors ? resolve8(cwd, explicitVectors) : localization.vectors ? resolve8(indexDir, localization.vectors.path) : void 0;
3217
+ let vectors;
3218
+ if (opts.mode !== "lexical" && vectorsPath !== void 0 && existsSync4(vectorsPath)) {
3219
+ try {
3220
+ vectors = createVectorIndex(readFileSync6(vectorsPath));
3221
+ } catch (e) {
3222
+ if (e instanceof VectorIndexError) {
3223
+ throw new GraphError(
3224
+ `${e.message} (${vectorsPath}) \u2014 re-run \`manni graph embed\`, or use \`--mode lexical\`.`
3225
+ );
3226
+ }
3227
+ throw e;
3228
+ }
3229
+ const stale = vectors.check({ source });
3230
+ if (stale && wantsVector) throw new GraphError(stale.detail);
3231
+ if (stale) {
3232
+ warn(`${stale.detail} Falling back to lexical.`);
3233
+ vectors = void 0;
3234
+ }
3235
+ } else if (wantsVector) {
3236
+ const named = vectorsPath ?? `no sidecar recorded for "${localization.language}"`;
3237
+ throw new GraphError(
3238
+ `Vector index not found: ${named} \u2014 run \`manni graph embed\` first, or use \`--mode lexical\`.`
3239
+ );
3240
+ }
3241
+ let embedder;
3242
+ if (vectors) {
3243
+ const requireEmbedder = opts.mode === "vector" || opts.mode === "hybrid";
3244
+ embedder = await resolveEmbedder(opts, vectors, requireEmbedder);
3245
+ }
3246
+ const entry2 = await findEntry(opts.query, {
3247
+ lexical,
3248
+ // Pass the embedder, not a bare function: findEntry then verifies it
3249
+ // against the index itself rather than trusting the CLI to have done so.
3250
+ ...embedder && vectors ? { vectors, embedder } : {},
3251
+ limit: opts.limit
3252
+ }).catch((e) => {
3253
+ if (e instanceof VectorMismatchError) {
3254
+ throw new GraphError(`${e.message} Re-run \`manni graph embed\`.`);
3255
+ }
3256
+ throw e;
3257
+ });
3258
+ const decorate = (c) => {
3259
+ const found = lexical.entry(c.iri);
3260
+ return {
3261
+ iri: c.iri,
3262
+ score: c.score,
3263
+ via: c.via,
3264
+ ...found?.type === void 0 ? {} : { type: found.type },
3265
+ ...found?.title === void 0 ? {} : { title: found.title }
3266
+ };
3267
+ };
3268
+ const mode = opts.mode ?? (embedder && vectors ? "hybrid" : "lexical");
3269
+ const results = opts.mode === "vector" ? entry2.vector : entry2.candidates;
3270
+ if (opts.mode === "vector") {
3271
+ entry2.trace.entry.length = 0;
3272
+ entry2.trace.entry.push(...entry2.vector);
3273
+ }
3274
+ return {
3275
+ query: opts.query,
3276
+ mode,
3277
+ results: results.map(decorate),
3278
+ lexical: entry2.lexical.map(decorate),
3279
+ vector: entry2.vector.map(decorate),
3280
+ trace: entry2.trace
3281
+ };
3282
+ }
3283
+ async function resolveEmbedder(opts, vectors, required) {
3284
+ const { model, dtype } = vectors;
3285
+ try {
3286
+ const embedder = opts.embedder ?? (model === "mock" ? createMockEmbedder({ dtype }) : await createLocalEmbedder({ model, dtype, role: "query" }));
3287
+ const mismatch = vectors.check({
3288
+ model: embedder.model,
3289
+ dtype: embedder.dtype,
3290
+ // Defense in depth: a future model that changes output size without
3291
+ // changing its id would otherwise only surface deep inside search().
3292
+ ...embedder.dims > 0 ? { dims: embedder.dims } : {}
3293
+ });
3294
+ if (mismatch) throw new GraphError(mismatch.detail);
3295
+ return embedder;
3296
+ } catch (e) {
3297
+ if (e instanceof GraphError) throw e;
3298
+ if (e instanceof EmbedderUnavailableError) {
3299
+ if (required) throw new GraphError(e.message);
3300
+ return void 0;
3301
+ }
3302
+ throw new GraphError(
3303
+ `Failed to load embedding model ${model}: ${errorMessage(e)}`
3304
+ );
3305
+ }
3306
+ }
3307
+ function renderSearch(report, format) {
3308
+ if (format === "json") return JSON.stringify(report, null, 2);
3309
+ const lines = [];
3310
+ for (const r of report.results) {
3311
+ const label = r.title ? ` \u2014 ${r.title}` : "";
3312
+ const type = r.type ? ` [${r.type}]` : "";
3313
+ lines.push(`${r.score.toFixed(4)} ${compactIri(r.iri)}${type}${label}`);
3314
+ }
3315
+ if (report.results.length === 0) {
3316
+ lines.push("(no matches)");
3317
+ }
3318
+ lines.push("");
3319
+ lines.push(
3320
+ `${report.results.length} result${report.results.length === 1 ? "" : "s"} (${report.mode})`
3321
+ );
3322
+ return lines.join("\n");
3323
+ }
3324
+
3325
+ // src/graph/commands/stats.ts
3326
+ import { resolve as resolve9 } from "node:path";
3327
+ import { DataFactory as DataFactory4 } from "n3";
3328
+ function subjectsOfType(store, typeIri) {
3329
+ return store.getQuads(null, DataFactory4.namedNode(RDF_TYPE), DataFactory4.namedNode(typeIri), null).map((q) => q.subject.value).sort();
3330
+ }
3331
+ function base(iri2) {
3332
+ const hash = iri2.indexOf("#");
3333
+ return hash === -1 ? iri2 : iri2.slice(0, hash);
3334
+ }
3335
+ function round1(pct) {
3336
+ return Math.round(pct * 10) / 10;
3337
+ }
3338
+ function runStats(opts = {}) {
3339
+ const cwd = opts.cwd ?? process.cwd();
3340
+ const config = loadRunConfig(
3341
+ {
3342
+ ...opts.config === void 0 ? {} : { configPath: opts.config },
3343
+ ...opts.noConfig === void 0 ? {} : { noConfig: opts.noConfig }
3344
+ },
3345
+ cwd
3346
+ );
3347
+ const store = loadGraph(resolve9(cwd, opts.graph ?? config.out));
3348
+ const top = opts.top ?? 5;
3349
+ const docIris = subjectsOfType(store, `${NS.graph}Document`);
3350
+ const docSet = new Set(docIris);
3351
+ const pathOf = new Map(docIris.map((d) => [d, d]));
3352
+ for (const quad of store.getQuads(
3353
+ null,
3354
+ DataFactory4.namedNode(`${NS.graph}path`),
3355
+ null,
3356
+ null
3357
+ )) {
3358
+ pathOf.set(quad.subject.value, quad.object.value);
3359
+ }
3360
+ const refQuads = store.getQuads(
3361
+ null,
3362
+ DataFactory4.namedNode(`${NS.dcterms}references`),
3363
+ null,
3364
+ null
3365
+ );
3366
+ const degree = new Map(docIris.map((d) => [d, 0]));
3367
+ for (const quad of refQuads) {
3368
+ const from = quad.subject.value;
3369
+ const to = base(quad.object.value);
3370
+ if (docSet.has(from)) degree.set(from, (degree.get(from) ?? 0) + 1);
3371
+ if (docSet.has(to) && to !== from)
3372
+ degree.set(to, (degree.get(to) ?? 0) + 1);
3373
+ }
3374
+ const orphans = docIris.filter((d) => (degree.get(d) ?? 0) === 0).map((d) => pathOf.get(d) ?? d).sort();
3375
+ const brokenLinks = store.getQuads(null, DataFactory4.namedNode(`${NS.graph}brokenLink`), null, null).map((q) => ({
3376
+ doc: pathOf.get(q.subject.value) ?? q.subject.value,
3377
+ target: q.object.value
3378
+ })).sort((a, b) => a.doc + a.target < b.doc + b.target ? -1 : 1);
3379
+ const brokenSectionRefs = store.getQuads(null, DataFactory4.namedNode(`${NS.graph}brokenSectionRef`), null, null).map((q) => ({
3380
+ doc: pathOf.get(q.subject.value) ?? q.subject.value,
3381
+ slug: q.object.value
3382
+ })).sort((a, b) => a.doc + a.slug < b.doc + b.slug ? -1 : 1);
3383
+ const mostConnected = [...degree.entries()].filter(([, deg]) => deg > 0).map(([doc, deg]) => ({ doc: pathOf.get(doc) ?? doc, degree: deg })).sort((a, b) => b.degree - a.degree || (a.doc < b.doc ? -1 : 1)).slice(0, top);
3384
+ const total = docIris.length;
3385
+ const coverageOver = (subjects, fields) => fields.map(({ field, iri: iri2 }) => {
3386
+ let docs = 0;
3387
+ for (const s of subjects) {
3388
+ if (store.countQuads(DataFactory4.namedNode(s), DataFactory4.namedNode(iri2), null, null) > 0)
3389
+ docs++;
3390
+ }
3391
+ const ratio = subjects.length === 0 ? 100 : docs / subjects.length * 100;
3392
+ return { field, predicate: compactIri(iri2), docs, pct: round1(ratio) };
3393
+ });
3394
+ const coverage = coverageOver(docIris, COVERAGE_FIELDS);
3395
+ const sectionCoverage = coverageOver(
3396
+ subjectsOfType(store, `${NS.graph}Section`),
3397
+ SECTION_COVERAGE_FIELDS
3398
+ );
3399
+ const languageOf = /* @__PURE__ */ new Map();
3400
+ for (const q of store.getQuads(
3401
+ null,
3402
+ DataFactory4.namedNode(`${NS.dcterms}language`),
3403
+ null,
3404
+ null
3405
+ )) {
3406
+ if (docSet.has(q.subject.value))
3407
+ languageOf.set(q.subject.value, q.object.value);
3408
+ }
3409
+ const byLanguage = /* @__PURE__ */ new Map();
3410
+ for (const d of docIris) {
3411
+ const tag = languageOf.get(d);
3412
+ if (tag === void 0) continue;
3413
+ const bucket = byLanguage.get(tag);
3414
+ if (bucket) bucket.push(d);
3415
+ else byLanguage.set(tag, [d]);
3416
+ }
3417
+ const sources = docIris.filter(
3418
+ (d) => store.countQuads(
3419
+ DataFactory4.namedNode(d),
3420
+ DataFactory4.namedNode(`${NS.schema}translationOfWork`),
3421
+ null,
3422
+ null
3423
+ ) === 0
3424
+ );
3425
+ const translatedInto = /* @__PURE__ */ new Map();
3426
+ for (const q of store.getQuads(
3427
+ null,
3428
+ DataFactory4.namedNode(`${NS.schema}workTranslation`),
3429
+ null,
3430
+ null
3431
+ )) {
3432
+ const target = languageOf.get(q.object.value);
3433
+ if (target === void 0) continue;
3434
+ const known = translatedInto.get(q.subject.value);
3435
+ if (known) known.add(target);
3436
+ else translatedInto.set(q.subject.value, /* @__PURE__ */ new Set([target]));
3437
+ }
3438
+ const localization = {
3439
+ unlabelled: docIris.length - languageOf.size,
3440
+ // `entries()`, not `keys()` and a lookup: the docs come out of the map
3441
+ // with the tag they are keyed by, so there is nothing left to assert.
3442
+ languages: [...byLanguage.entries()].sort(([a], [b]) => byCodeUnit(a, b)).map(([language, docsIn]) => {
3443
+ const untranslated = sources.filter((s) => languageOf.get(s) !== language).filter((s) => !translatedInto.get(s)?.has(language)).map((s) => pathOf.get(s) ?? s).sort(byCodeUnit);
3444
+ return {
3445
+ language,
3446
+ docs: docsIn.length,
3447
+ coverage: coverageOver(docsIn, COVERAGE_FIELDS),
3448
+ untranslated
3449
+ };
3450
+ })
3451
+ };
3452
+ const uniform = opts.coverageThreshold;
3453
+ const thresholds = uniform != null ? Object.fromEntries(coverage.map((c) => [c.field, uniform])) : config.stats.coverageThreshold;
3454
+ const coverageFindings = coverage.flatMap((c) => {
3455
+ const threshold = thresholds[c.field];
3456
+ if (threshold === void 0) return [];
3457
+ const pct = total === 0 ? 100 : c.docs / total * 100;
3458
+ return pct < threshold ? [{ field: c.field, pct: c.pct, threshold }] : [];
3459
+ });
3460
+ const failed = !!opts.check && (brokenLinks.length > 0 || brokenSectionRefs.length > 0 || coverageFindings.length > 0);
3461
+ return {
3462
+ triples: store.size,
3463
+ docs: docIris.length,
3464
+ // countQuads avoids materializing + sorting arrays used only for counting.
3465
+ sections: store.countQuads(
3466
+ null,
3467
+ DataFactory4.namedNode(RDF_TYPE),
3468
+ DataFactory4.namedNode(`${NS.graph}Section`),
3469
+ null
3470
+ ),
3471
+ concepts: store.countQuads(
3472
+ null,
3473
+ DataFactory4.namedNode(RDF_TYPE),
3474
+ DataFactory4.namedNode(`${NS.skos}Concept`),
3475
+ null
3476
+ ),
3477
+ references: refQuads.length,
3478
+ orphans,
3479
+ brokenLinks,
3480
+ brokenSectionRefs,
3481
+ mostConnected,
3482
+ coverage,
3483
+ sectionCoverage,
3484
+ localization,
3485
+ coverageFindings,
3486
+ exitCode: failed ? 1 : 0
3487
+ };
3488
+ }
3489
+ function renderStats(report, format) {
3490
+ if (format === "json") {
3491
+ const { exitCode: _exitCode, ...rest } = report;
3492
+ return JSON.stringify(rest, null, 2);
3493
+ }
3494
+ const lines = [
3495
+ `Triples: ${report.triples}`,
3496
+ `Documents: ${report.docs}`,
3497
+ `Sections: ${report.sections}`,
3498
+ `Concepts: ${report.concepts}`,
3499
+ `References: ${report.references}`
3500
+ ];
3501
+ lines.push("", "Most connected:");
3502
+ if (report.mostConnected.length === 0) lines.push(" (none)");
3503
+ for (const { doc, degree } of report.mostConnected) {
3504
+ lines.push(` ${doc} (${degree})`);
3505
+ }
3506
+ lines.push("", `Orphan docs (${report.orphans.length}):`);
3507
+ for (const orphan of report.orphans) lines.push(` ${orphan}`);
3508
+ if (report.orphans.length === 0) lines.push(" (none)");
3509
+ lines.push("", `Broken internal links (${report.brokenLinks.length}):`);
3510
+ for (const { doc, target } of report.brokenLinks) {
3511
+ lines.push(` ${doc} -> ${target}`);
3512
+ }
3513
+ if (report.brokenLinks.length === 0) lines.push(" (none)");
3514
+ lines.push("", `Broken section refs (${report.brokenSectionRefs.length}):`);
3515
+ for (const { doc, slug } of report.brokenSectionRefs) {
3516
+ lines.push(` ${doc} -> #${slug}`);
3517
+ }
3518
+ if (report.brokenSectionRefs.length === 0) lines.push(" (none)");
3519
+ const belowThreshold = new Set(report.coverageFindings.map((f) => f.field));
3520
+ const width = Math.max(
3521
+ ...report.coverage.map((c) => c.field.length),
3522
+ ...report.sectionCoverage.map((c) => c.field.length)
3523
+ );
3524
+ lines.push("", "Coverage (documents):");
3525
+ for (const { field, docs, pct } of report.coverage) {
3526
+ const flag = belowThreshold.has(field) ? " ! below threshold" : "";
3527
+ lines.push(
3528
+ ` ${field.padEnd(width)} ${docs}/${report.docs} ${pct.toFixed(1)}%${flag}`
3529
+ );
3530
+ }
3531
+ lines.push("", "Coverage (sections, not gated):");
3532
+ if (report.sections === 0) {
3533
+ lines.push(" (no sections in this graph)");
3534
+ } else {
3535
+ for (const { field, docs, pct } of report.sectionCoverage) {
3536
+ lines.push(
3537
+ ` ${field.padEnd(width)} ${docs}/${report.sections} ${pct.toFixed(1)}%`
3538
+ );
3539
+ }
3540
+ }
3541
+ if (report.localization.languages.length > 0) {
3542
+ const tagWidth = Math.max(
3543
+ ...report.localization.languages.map((l) => l.language.length)
3544
+ );
3545
+ lines.push("", "Localization:");
3546
+ for (const { language, docs, untranslated } of report.localization.languages) {
3547
+ const plural = docs === 1 ? "doc" : "docs";
3548
+ const backlog = untranslated.length === 0 ? "" : ` ${untranslated.length} untranslated`;
3549
+ lines.push(` ${language.padEnd(tagWidth)} ${docs} ${plural}${backlog}`);
3550
+ }
3551
+ lines.push(` no language: ${report.localization.unlabelled}`);
3552
+ }
3553
+ return lines.join("\n");
3554
+ }
3555
+
3556
+ // src/graph/commands/traverse.ts
3557
+ import { resolve as resolve10 } from "node:path";
3558
+
3559
+ // src/graph/runtime/traverse.ts
3560
+ var DCTERMS_TITLE3 = `${NS.dcterms}title`;
3561
+ var DCTERMS_REFERENCES = `${NS.dcterms}references`;
3562
+ var DCTERMS_LANGUAGE2 = `${NS.dcterms}language`;
3563
+ function documentOf(iri2) {
3564
+ const hash = iri2.indexOf("#");
3565
+ return hash === -1 ? iri2 : iri2.slice(0, hash);
3566
+ }
3567
+ function resolveVariant(graph, input) {
3568
+ if (graph.has(input)) return input;
3569
+ const wanted = input.toLowerCase();
3570
+ const variants = graph.instancesOf(`${NS.iirds}ProductVariant`);
3571
+ for (const v of variants) {
3572
+ if (graph.literal(v, DCTERMS_TITLE3)?.toLowerCase() === wanted) return v;
3573
+ }
3574
+ for (const v of variants) {
3575
+ if (v.slice(v.lastIndexOf("/") + 1).toLowerCase() === wanted) return v;
3576
+ }
3577
+ return void 0;
3578
+ }
3579
+ function resolveSubject(graph, input) {
3580
+ const mapped = SOFTWARE_SUBJECT_IRIS[input.toLowerCase()];
3581
+ if (mapped) return mapped;
3582
+ const expanded = graph.expand(input);
3583
+ return expanded.startsWith("http") ? expanded : void 0;
3584
+ }
3585
+ function scopeExclusion(graph, iri2, scope) {
3586
+ const checks = [
3587
+ [
3588
+ scope.variantIri,
3589
+ IIRDS_RELATES_TO_PRODUCT_VARIANT,
3590
+ GRAPH_NOT_APPLICABLE_TO_VARIANT,
3591
+ "iri"
3592
+ ],
3593
+ [scope.subjectIri, IIRDS_HAS_SUBJECT, GRAPH_NOT_SOFTWARE_SUBJECT, "iri"],
3594
+ [scope.language, DCTERMS_LANGUAGE2, void 0, "literal"]
3595
+ ];
3596
+ for (const [target, positive, negative, kind] of checks) {
3597
+ if (!target) continue;
3598
+ if (negative !== void 0) {
3599
+ const denied = graph.values(iri2, negative).some((v) => v.kind === kind && v.value === target);
3600
+ if (denied) return { node: iri2, rule: negative, value: target };
3601
+ }
3602
+ const subject = positive === DCTERMS_LANGUAGE2 ? documentOf(iri2) : iri2;
3603
+ const claimed = graph.values(subject, positive);
3604
+ if (claimed.length > 0) {
3605
+ const matches = claimed.some(
3606
+ (v) => v.kind === kind && v.value === target
3607
+ );
3608
+ if (!matches) return { node: iri2, rule: positive, value: target };
3609
+ }
3610
+ }
3611
+ return void 0;
3612
+ }
3613
+ function traverse(graph, options) {
3614
+ const trace = options.trace ?? createTrace();
3615
+ const maxDepth = options.depth ?? 1;
3616
+ const direction = options.direction ?? "out";
3617
+ const allowed = options.predicates && options.predicates.length > 0 ? new Set(options.predicates.map((p) => graph.expand(p))) : void 0;
3618
+ const variantIri = options.variant ? resolveVariant(graph, options.variant) : void 0;
3619
+ const subjectIri = options.subject ? resolveSubject(graph, options.subject) : void 0;
3620
+ const language = options.language === void 0 ? void 0 : canonicalLanguageTag(options.language);
3621
+ const scope = { variantIri, subjectIri, language };
3622
+ const followTypes = options.includeTypeEdges === true || allowed?.has(RDF_TYPE) === true;
3623
+ const excluded = /* @__PURE__ */ new Set();
3624
+ const keep = (iri2) => {
3625
+ if (excluded.has(iri2)) return false;
3626
+ const exclusion = scopeExclusion(graph, iri2, scope);
3627
+ if (exclusion) {
3628
+ excluded.add(iri2);
3629
+ trace.exclusions.push(exclusion);
3630
+ return false;
3631
+ }
3632
+ return true;
3633
+ };
3634
+ const nodes = [];
3635
+ const seen = /* @__PURE__ */ new Set();
3636
+ let frontier = [];
3637
+ const alreadySeeded = new Set(trace.entry.map((e) => e.iri));
3638
+ for (const seed of [...options.seeds].sort(byCodeUnit)) {
3639
+ if (!graph.has(seed) || seen.has(seed)) continue;
3640
+ seen.add(seed);
3641
+ if (!alreadySeeded.has(seed)) {
3642
+ trace.entry.push({ iri: seed, score: 1, via: "explicit" });
3643
+ }
3644
+ if (keep(seed)) nodes.push({ iri: seed, depth: 0 });
3645
+ frontier.push(seed);
3646
+ }
3647
+ const limit = options.limit ?? Infinity;
3648
+ for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
3649
+ const next = [];
3650
+ for (const from of frontier) {
3651
+ if (nodes.length >= limit) break;
3652
+ for (const { predicate, target, dir } of neighbors(
3653
+ graph,
3654
+ from,
3655
+ direction
3656
+ )) {
3657
+ if (allowed && !allowed.has(predicate)) continue;
3658
+ if (!followTypes && predicate === RDF_TYPE) continue;
3659
+ trace.hops.push({ from, predicate, to: target, depth, direction: dir });
3660
+ if (seen.has(target)) continue;
3661
+ seen.add(target);
3662
+ if (!keep(target)) continue;
3663
+ if (nodes.length >= limit) break;
3664
+ nodes.push({ iri: target, depth });
3665
+ next.push(target);
3666
+ }
3667
+ }
3668
+ frontier = next;
3669
+ }
3670
+ return { nodes, trace };
3671
+ }
3672
+ function neighbors(graph, iri2, direction) {
3673
+ const out = [];
3674
+ if (direction === "out" || direction === "both") {
3675
+ for (const e of graph.out(iri2)) out.push({ ...e, dir: "out" });
3676
+ }
3677
+ if (direction === "in" || direction === "both") {
3678
+ for (const e of graph.in(iri2)) out.push({ ...e, dir: "in" });
3679
+ }
3680
+ return out;
3681
+ }
3682
+ function reverseReferences(graph, iri2, options = {}) {
3683
+ return traverse(graph, {
3684
+ ...options,
3685
+ seeds: [iri2],
3686
+ direction: "in",
3687
+ predicates: [DCTERMS_REFERENCES],
3688
+ depth: options.depth ?? 1
3689
+ });
3690
+ }
3691
+ function impact(graph, iri2, options = {}) {
3692
+ const result = traverse(graph, {
3693
+ ...options,
3694
+ seeds: [iri2],
3695
+ direction: "in",
3696
+ depth: options.depth ?? 3,
3697
+ // The seed *may* occupy a slot in the walker's limit, so ask for one more:
3698
+ // `limit` means "this many *affected* nodes". Whether it actually takes
3699
+ // one depends on the scope filter — a seed the filter excludes still
3700
+ // expands but is not in `nodes` — so the slack is trimmed below rather
3701
+ // than assumed spent.
3702
+ limit: options.limit === void 0 ? void 0 : options.limit + 1
3703
+ });
3704
+ const affected = result.nodes.filter((n) => n.iri !== iri2);
3705
+ return {
3706
+ nodes: options.limit === void 0 ? affected : affected.slice(0, options.limit),
3707
+ trace: result.trace
3708
+ };
3709
+ }
3710
+
3711
+ // src/graph/commands/traverse.ts
3712
+ function runTraverse(opts) {
3713
+ const cwd = opts.cwd ?? process.cwd();
3714
+ const config = loadRunConfig(
3715
+ {
3716
+ ...opts.config === void 0 ? {} : { configPath: opts.config },
3717
+ ...opts.noConfig === void 0 ? {} : { noConfig: opts.noConfig }
3718
+ },
3719
+ cwd
3720
+ );
3721
+ const store = loadGraph(resolve10(cwd, opts.graph ?? config.out));
3722
+ const quads = storeToQuads(store);
3723
+ const graph = GraphIndex.fromQuads(quads, Object.fromEntries(PREFIXES));
3724
+ const node = expandTerm(opts.node);
3725
+ if (!graph.has(node)) {
3726
+ throw new GraphError(
3727
+ `Node not found in the graph: ${opts.node} \u2014 check the IRI, or run \`manni graph build\` first.`
3728
+ );
3729
+ }
3730
+ if (opts.variant && !resolveVariant(graph, opts.variant)) {
3731
+ throw new GraphError(
3732
+ `Unknown product variant: ${opts.variant} \u2014 no iirds:ProductVariant matches that IRI, title, or slug.`
3733
+ );
3734
+ }
3735
+ if (opts.subject && !resolveSubject(graph, opts.subject)) {
3736
+ throw new GraphError(
3737
+ `Unknown software subject: ${opts.subject} \u2014 expected one of ${Object.keys(SOFTWARE_SUBJECT_IRIS).sort().join(", ")}, or a full IRI.`
3738
+ );
3739
+ }
3740
+ const predicates = opts.predicates?.map(expandTerm);
3741
+ if (predicates && predicates.length > 0) {
3742
+ const used = /* @__PURE__ */ new Set();
3743
+ for (const quad of quads) {
3744
+ if (quad.o.kind === "iri") used.add(quad.p);
3745
+ }
3746
+ predicates.forEach((predicate, i) => {
3747
+ if (!used.has(predicate)) {
3748
+ throw new GraphError(
3749
+ `Unknown predicate: ${opts.predicates?.[i] ?? predicate}. No edge in the graph uses it.`
3750
+ );
3751
+ }
3752
+ });
3753
+ }
3754
+ const direction = opts.reverse ? "in" : "out";
3755
+ const result = opts.impact ? impact(graph, node, {
3756
+ depth: opts.depth,
3757
+ predicates,
3758
+ variant: opts.variant,
3759
+ subject: opts.subject,
3760
+ language: opts.lang,
3761
+ limit: opts.limit
3762
+ }) : traverse(graph, {
3763
+ seeds: [node],
3764
+ depth: opts.depth,
3765
+ predicates,
3766
+ direction,
3767
+ variant: opts.variant,
3768
+ subject: opts.subject,
3769
+ language: opts.lang,
3770
+ limit: opts.limit
3771
+ });
3772
+ return {
3773
+ node,
3774
+ nodes: result.nodes.map((n) => {
3775
+ const title = graph.literal(n.iri, `${NS.dcterms}title`);
3776
+ return title === void 0 ? { iri: n.iri, depth: n.depth } : { iri: n.iri, depth: n.depth, title };
3777
+ }),
3778
+ trace: result.trace
3779
+ };
3780
+ }
3781
+ function renderTraverse(report, format) {
3782
+ if (format === "json") return JSON.stringify(report, null, 2);
3783
+ const lines = [];
3784
+ for (const n of report.nodes) {
3785
+ const indent = " ".repeat(n.depth);
3786
+ const title = n.title ? ` \u2014 ${n.title}` : "";
3787
+ lines.push(`${indent}${compactIri(n.iri)}${title}`);
3788
+ }
3789
+ if (report.nodes.length === 0) lines.push("(no nodes)");
3790
+ if (report.trace.exclusions.length > 0) {
3791
+ lines.push("");
3792
+ lines.push("excluded by scope:");
3793
+ for (const e of report.trace.exclusions) {
3794
+ lines.push(
3795
+ ` ${compactIri(e.node)} \u2014 ${compactIri(e.rule)} ${compactIri(e.value)}`
3796
+ );
3797
+ }
3798
+ }
3799
+ lines.push("");
3800
+ lines.push(
3801
+ `${report.nodes.length} node${report.nodes.length === 1 ? "" : "s"}, ${report.trace.hops.length} hop${report.trace.hops.length === 1 ? "" : "s"}, ${report.trace.exclusions.length} excluded`
3802
+ );
3803
+ return lines.join("\n");
3804
+ }
3805
+
3806
+ export {
3807
+ emitJsonLd,
3808
+ emitRdfXml,
3809
+ writeZip,
3810
+ projectPackage,
3811
+ splitFragment,
3812
+ documentSectionOrder,
3813
+ sectionOccurrences,
3814
+ sectionOccurrence,
3815
+ documentPreamble,
3816
+ sliceSection,
3817
+ sectionOwnText,
3818
+ createFetchResolver,
3819
+ buildSearchIndex,
3820
+ emitSearchIndex,
3821
+ partitionByLanguage,
3822
+ documentsByLanguage,
3823
+ GraphIndex,
3824
+ EXPORT_TARGETS,
3825
+ runExport,
3826
+ runQuery,
3827
+ renderQuery,
3828
+ frontmatterKind,
3829
+ applyGraphFields,
3830
+ existingMetaProvenance,
3831
+ existingGraphFields,
3832
+ selectProvider2 as selectProvider,
3833
+ assertProviderSelection,
3834
+ providerSpecFor2 as providerSpecFor,
3835
+ resolveProviderIdentity,
3836
+ constructProvider,
3837
+ makeProvider,
3838
+ runFill,
3839
+ renderFill,
3840
+ VectorIndexError,
3841
+ normalize,
3842
+ encodeVectorIndex,
3843
+ decodeVectorIndex,
3844
+ EmbedderUnavailableError,
3845
+ createLocalEmbedder,
3846
+ createMockEmbedder,
3847
+ embedText,
3848
+ runEmbed,
3849
+ renderEmbed,
3850
+ VectorMismatchError,
3851
+ searchIndexDigest2 as searchIndexDigest,
3852
+ createVectorIndex,
3853
+ createTrace,
3854
+ reachedNodes,
3855
+ rrfMerge,
3856
+ findEntry,
3857
+ createLexicalIndex,
3858
+ SEARCH_MODES,
3859
+ runSearch,
3860
+ renderSearch,
3861
+ runStats,
3862
+ renderStats,
3863
+ resolveVariant,
3864
+ resolveSubject,
3865
+ scopeExclusion,
3866
+ traverse,
3867
+ reverseReferences,
3868
+ impact,
3869
+ runTraverse,
3870
+ renderTraverse
3871
+ };
3872
+ //# sourceMappingURL=chunk-644JVKJ4.js.map