@graphty/graph-io 0.0.0 → 0.2.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 (339) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -28
  3. package/dist/chunks/children-CL3Cy0ez.js +238 -0
  4. package/dist/chunks/children-CL3Cy0ez.js.map +1 -0
  5. package/dist/chunks/escape-DyI8JofU.js +938 -0
  6. package/dist/chunks/escape-DyI8JofU.js.map +1 -0
  7. package/dist/chunks/importer-CQnJuWJw.js +2987 -0
  8. package/dist/chunks/importer-CQnJuWJw.js.map +1 -0
  9. package/dist/chunks/importer-CpCpfbxr.js +2015 -0
  10. package/dist/chunks/importer-CpCpfbxr.js.map +1 -0
  11. package/dist/chunks/importer-DbnGYr3_.js +2342 -0
  12. package/dist/chunks/importer-DbnGYr3_.js.map +1 -0
  13. package/dist/chunks/importer-GozH8DkN.js +3050 -0
  14. package/dist/chunks/importer-GozH8DkN.js.map +1 -0
  15. package/dist/chunks/records-CGpxszm1.js +605 -0
  16. package/dist/chunks/records-CGpxszm1.js.map +1 -0
  17. package/dist/chunks/text-CajMdVFy.js +189 -0
  18. package/dist/chunks/text-CajMdVFy.js.map +1 -0
  19. package/dist/chunks/writer-DxSKC7TL.js +2842 -0
  20. package/dist/chunks/writer-DxSKC7TL.js.map +1 -0
  21. package/dist/csv.d.ts +1 -0
  22. package/dist/csv.js +1702 -0
  23. package/dist/csv.js.map +1 -0
  24. package/dist/dot.d.ts +1 -0
  25. package/dist/dot.js +8 -0
  26. package/dist/dot.js.map +1 -0
  27. package/dist/gexf.d.ts +1 -0
  28. package/dist/gexf.js +3466 -0
  29. package/dist/gexf.js.map +1 -0
  30. package/dist/gml.d.ts +1 -0
  31. package/dist/gml.js +2647 -0
  32. package/dist/gml.js.map +1 -0
  33. package/dist/graph-io.d.ts +1 -0
  34. package/dist/graph-io.js +790 -0
  35. package/dist/graph-io.js.map +1 -0
  36. package/dist/graphml.d.ts +1 -0
  37. package/dist/graphml.js +8 -0
  38. package/dist/graphml.js.map +1 -0
  39. package/dist/json.d.ts +1 -0
  40. package/dist/json.js +11 -0
  41. package/dist/json.js.map +1 -0
  42. package/dist/neo4j.d.ts +1 -0
  43. package/dist/neo4j.js +2046 -0
  44. package/dist/neo4j.js.map +1 -0
  45. package/dist/pajek.d.ts +1 -0
  46. package/dist/pajek.js +8 -0
  47. package/dist/pajek.js.map +1 -0
  48. package/dist/src/children.d.ts +134 -0
  49. package/dist/src/children.d.ts.map +1 -0
  50. package/dist/src/children.js +274 -0
  51. package/dist/src/children.js.map +1 -0
  52. package/dist/src/common/attributes.d.ts +229 -0
  53. package/dist/src/common/attributes.d.ts.map +1 -0
  54. package/dist/src/common/attributes.js +368 -0
  55. package/dist/src/common/attributes.js.map +1 -0
  56. package/dist/src/common/codes.d.ts +105 -0
  57. package/dist/src/common/codes.d.ts.map +1 -0
  58. package/dist/src/common/codes.js +107 -0
  59. package/dist/src/common/codes.js.map +1 -0
  60. package/dist/src/common/declared-types.d.ts +84 -0
  61. package/dist/src/common/declared-types.d.ts.map +1 -0
  62. package/dist/src/common/declared-types.js +326 -0
  63. package/dist/src/common/declared-types.js.map +1 -0
  64. package/dist/src/common/direction.d.ts +206 -0
  65. package/dist/src/common/direction.d.ts.map +1 -0
  66. package/dist/src/common/direction.js +370 -0
  67. package/dist/src/common/direction.js.map +1 -0
  68. package/dist/src/common/escape.d.ts +92 -0
  69. package/dist/src/common/escape.d.ts.map +1 -0
  70. package/dist/src/common/escape.js +212 -0
  71. package/dist/src/common/escape.js.map +1 -0
  72. package/dist/src/common/export.d.ts +249 -0
  73. package/dist/src/common/export.d.ts.map +1 -0
  74. package/dist/src/common/export.js +594 -0
  75. package/dist/src/common/export.js.map +1 -0
  76. package/dist/src/common/format.d.ts +59 -0
  77. package/dist/src/common/format.d.ts.map +1 -0
  78. package/dist/src/common/format.js +106 -0
  79. package/dist/src/common/format.js.map +1 -0
  80. package/dist/src/common/ids.d.ts +83 -0
  81. package/dist/src/common/ids.d.ts.map +1 -0
  82. package/dist/src/common/ids.js +158 -0
  83. package/dist/src/common/ids.js.map +1 -0
  84. package/dist/src/common/input.d.ts +100 -0
  85. package/dist/src/common/input.d.ts.map +1 -0
  86. package/dist/src/common/input.js +335 -0
  87. package/dist/src/common/input.js.map +1 -0
  88. package/dist/src/common/lists.d.ts +34 -0
  89. package/dist/src/common/lists.d.ts.map +1 -0
  90. package/dist/src/common/lists.js +185 -0
  91. package/dist/src/common/lists.js.map +1 -0
  92. package/dist/src/common/options.d.ts +108 -0
  93. package/dist/src/common/options.d.ts.map +1 -0
  94. package/dist/src/common/options.js +265 -0
  95. package/dist/src/common/options.js.map +1 -0
  96. package/dist/src/common/report.d.ts +187 -0
  97. package/dist/src/common/report.d.ts.map +1 -0
  98. package/dist/src/common/report.js +274 -0
  99. package/dist/src/common/report.js.map +1 -0
  100. package/dist/src/common/temporal.d.ts +71 -0
  101. package/dist/src/common/temporal.d.ts.map +1 -0
  102. package/dist/src/common/temporal.js +266 -0
  103. package/dist/src/common/temporal.js.map +1 -0
  104. package/dist/src/common/text.d.ts +104 -0
  105. package/dist/src/common/text.d.ts.map +1 -0
  106. package/dist/src/common/text.js +255 -0
  107. package/dist/src/common/text.js.map +1 -0
  108. package/dist/src/common/weights.d.ts +77 -0
  109. package/dist/src/common/weights.d.ts.map +1 -0
  110. package/dist/src/common/weights.js +156 -0
  111. package/dist/src/common/weights.js.map +1 -0
  112. package/dist/src/common/writer.d.ts +51 -0
  113. package/dist/src/common/writer.d.ts.map +1 -0
  114. package/dist/src/common/writer.js +108 -0
  115. package/dist/src/common/writer.js.map +1 -0
  116. package/dist/src/common/xml.d.ts +245 -0
  117. package/dist/src/common/xml.d.ts.map +1 -0
  118. package/dist/src/common/xml.js +942 -0
  119. package/dist/src/common/xml.js.map +1 -0
  120. package/dist/src/formats/csv/exporter.d.ts +70 -0
  121. package/dist/src/formats/csv/exporter.d.ts.map +1 -0
  122. package/dist/src/formats/csv/exporter.js +682 -0
  123. package/dist/src/formats/csv/exporter.js.map +1 -0
  124. package/dist/src/formats/csv/header.d.ts +66 -0
  125. package/dist/src/formats/csv/header.d.ts.map +1 -0
  126. package/dist/src/formats/csv/header.js +152 -0
  127. package/dist/src/formats/csv/header.js.map +1 -0
  128. package/dist/src/formats/csv/importer.d.ts +82 -0
  129. package/dist/src/formats/csv/importer.d.ts.map +1 -0
  130. package/dist/src/formats/csv/importer.js +849 -0
  131. package/dist/src/formats/csv/importer.js.map +1 -0
  132. package/dist/src/formats/csv/index.d.ts +60 -0
  133. package/dist/src/formats/csv/index.d.ts.map +1 -0
  134. package/dist/src/formats/csv/index.js +63 -0
  135. package/dist/src/formats/csv/index.js.map +1 -0
  136. package/dist/src/formats/csv/records.d.ts +188 -0
  137. package/dist/src/formats/csv/records.d.ts.map +1 -0
  138. package/dist/src/formats/csv/records.js +702 -0
  139. package/dist/src/formats/csv/records.js.map +1 -0
  140. package/dist/src/formats/csv/values.d.ts +105 -0
  141. package/dist/src/formats/csv/values.d.ts.map +1 -0
  142. package/dist/src/formats/csv/values.js +192 -0
  143. package/dist/src/formats/csv/values.js.map +1 -0
  144. package/dist/src/formats/dot/exporter.d.ts +52 -0
  145. package/dist/src/formats/dot/exporter.d.ts.map +1 -0
  146. package/dist/src/formats/dot/exporter.js +836 -0
  147. package/dist/src/formats/dot/exporter.js.map +1 -0
  148. package/dist/src/formats/dot/importer.d.ts +102 -0
  149. package/dist/src/formats/dot/importer.d.ts.map +1 -0
  150. package/dist/src/formats/dot/importer.js +1291 -0
  151. package/dist/src/formats/dot/importer.js.map +1 -0
  152. package/dist/src/formats/dot/index.d.ts +7 -0
  153. package/dist/src/formats/dot/index.d.ts.map +1 -0
  154. package/dist/src/formats/dot/index.js +7 -0
  155. package/dist/src/formats/dot/index.js.map +1 -0
  156. package/dist/src/formats/dot/names.d.ts +29 -0
  157. package/dist/src/formats/dot/names.d.ts.map +1 -0
  158. package/dist/src/formats/dot/names.js +28 -0
  159. package/dist/src/formats/dot/names.js.map +1 -0
  160. package/dist/src/formats/dot/tokenizer.d.ts +114 -0
  161. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -0
  162. package/dist/src/formats/dot/tokenizer.js +341 -0
  163. package/dist/src/formats/dot/tokenizer.js.map +1 -0
  164. package/dist/src/formats/gexf/exporter.d.ts +56 -0
  165. package/dist/src/formats/gexf/exporter.d.ts.map +1 -0
  166. package/dist/src/formats/gexf/exporter.js +1395 -0
  167. package/dist/src/formats/gexf/exporter.js.map +1 -0
  168. package/dist/src/formats/gexf/importer.d.ts +73 -0
  169. package/dist/src/formats/gexf/importer.d.ts.map +1 -0
  170. package/dist/src/formats/gexf/importer.js +1880 -0
  171. package/dist/src/formats/gexf/importer.js.map +1 -0
  172. package/dist/src/formats/gexf/index.d.ts +96 -0
  173. package/dist/src/formats/gexf/index.d.ts.map +1 -0
  174. package/dist/src/formats/gexf/index.js +97 -0
  175. package/dist/src/formats/gexf/index.js.map +1 -0
  176. package/dist/src/formats/gexf/schema.d.ts +135 -0
  177. package/dist/src/formats/gexf/schema.d.ts.map +1 -0
  178. package/dist/src/formats/gexf/schema.js +323 -0
  179. package/dist/src/formats/gexf/schema.js.map +1 -0
  180. package/dist/src/formats/gml/exporter.d.ts +69 -0
  181. package/dist/src/formats/gml/exporter.d.ts.map +1 -0
  182. package/dist/src/formats/gml/exporter.js +1093 -0
  183. package/dist/src/formats/gml/exporter.js.map +1 -0
  184. package/dist/src/formats/gml/importer.d.ts +66 -0
  185. package/dist/src/formats/gml/importer.d.ts.map +1 -0
  186. package/dist/src/formats/gml/importer.js +1331 -0
  187. package/dist/src/formats/gml/importer.js.map +1 -0
  188. package/dist/src/formats/gml/index.d.ts +85 -0
  189. package/dist/src/formats/gml/index.d.ts.map +1 -0
  190. package/dist/src/formats/gml/index.js +88 -0
  191. package/dist/src/formats/gml/index.js.map +1 -0
  192. package/dist/src/formats/gml/syntax.d.ts +186 -0
  193. package/dist/src/formats/gml/syntax.d.ts.map +1 -0
  194. package/dist/src/formats/gml/syntax.js +467 -0
  195. package/dist/src/formats/gml/syntax.js.map +1 -0
  196. package/dist/src/formats/graphml/constants.d.ts +169 -0
  197. package/dist/src/formats/graphml/constants.d.ts.map +1 -0
  198. package/dist/src/formats/graphml/constants.js +165 -0
  199. package/dist/src/formats/graphml/constants.js.map +1 -0
  200. package/dist/src/formats/graphml/exporter.d.ts +34 -0
  201. package/dist/src/formats/graphml/exporter.d.ts.map +1 -0
  202. package/dist/src/formats/graphml/exporter.js +1176 -0
  203. package/dist/src/formats/graphml/exporter.js.map +1 -0
  204. package/dist/src/formats/graphml/importer.d.ts +31 -0
  205. package/dist/src/formats/graphml/importer.d.ts.map +1 -0
  206. package/dist/src/formats/graphml/importer.js +1607 -0
  207. package/dist/src/formats/graphml/importer.js.map +1 -0
  208. package/dist/src/formats/graphml/index.d.ts +8 -0
  209. package/dist/src/formats/graphml/index.d.ts.map +1 -0
  210. package/dist/src/formats/graphml/index.js +8 -0
  211. package/dist/src/formats/graphml/index.js.map +1 -0
  212. package/dist/src/formats/graphml/tree.d.ts +72 -0
  213. package/dist/src/formats/graphml/tree.d.ts.map +1 -0
  214. package/dist/src/formats/graphml/tree.js +290 -0
  215. package/dist/src/formats/graphml/tree.js.map +1 -0
  216. package/dist/src/formats/json/dialect.d.ts +125 -0
  217. package/dist/src/formats/json/dialect.d.ts.map +1 -0
  218. package/dist/src/formats/json/dialect.js +262 -0
  219. package/dist/src/formats/json/dialect.js.map +1 -0
  220. package/dist/src/formats/json/exporter.d.ts +89 -0
  221. package/dist/src/formats/json/exporter.d.ts.map +1 -0
  222. package/dist/src/formats/json/exporter.js +1358 -0
  223. package/dist/src/formats/json/exporter.js.map +1 -0
  224. package/dist/src/formats/json/importer.d.ts +108 -0
  225. package/dist/src/formats/json/importer.d.ts.map +1 -0
  226. package/dist/src/formats/json/importer.js +1838 -0
  227. package/dist/src/formats/json/importer.js.map +1 -0
  228. package/dist/src/formats/json/index.d.ts +8 -0
  229. package/dist/src/formats/json/index.d.ts.map +1 -0
  230. package/dist/src/formats/json/index.js +8 -0
  231. package/dist/src/formats/json/index.js.map +1 -0
  232. package/dist/src/formats/neo4j/exporter.d.ts +68 -0
  233. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -0
  234. package/dist/src/formats/neo4j/exporter.js +1055 -0
  235. package/dist/src/formats/neo4j/exporter.js.map +1 -0
  236. package/dist/src/formats/neo4j/header.d.ts +52 -0
  237. package/dist/src/formats/neo4j/header.d.ts.map +1 -0
  238. package/dist/src/formats/neo4j/header.js +131 -0
  239. package/dist/src/formats/neo4j/header.js.map +1 -0
  240. package/dist/src/formats/neo4j/importer.d.ts +73 -0
  241. package/dist/src/formats/neo4j/importer.d.ts.map +1 -0
  242. package/dist/src/formats/neo4j/importer.js +932 -0
  243. package/dist/src/formats/neo4j/importer.js.map +1 -0
  244. package/dist/src/formats/neo4j/index.d.ts +79 -0
  245. package/dist/src/formats/neo4j/index.d.ts.map +1 -0
  246. package/dist/src/formats/neo4j/index.js +83 -0
  247. package/dist/src/formats/neo4j/index.js.map +1 -0
  248. package/dist/src/formats/pajek/exporter.d.ts +58 -0
  249. package/dist/src/formats/pajek/exporter.d.ts.map +1 -0
  250. package/dist/src/formats/pajek/exporter.js +825 -0
  251. package/dist/src/formats/pajek/exporter.js.map +1 -0
  252. package/dist/src/formats/pajek/importer.d.ts +88 -0
  253. package/dist/src/formats/pajek/importer.d.ts.map +1 -0
  254. package/dist/src/formats/pajek/importer.js +1047 -0
  255. package/dist/src/formats/pajek/importer.js.map +1 -0
  256. package/dist/src/formats/pajek/index.d.ts +7 -0
  257. package/dist/src/formats/pajek/index.d.ts.map +1 -0
  258. package/dist/src/formats/pajek/index.js +7 -0
  259. package/dist/src/formats/pajek/index.js.map +1 -0
  260. package/dist/src/formats/pajek/syntax.d.ts +112 -0
  261. package/dist/src/formats/pajek/syntax.d.ts.map +1 -0
  262. package/dist/src/formats/pajek/syntax.js +269 -0
  263. package/dist/src/formats/pajek/syntax.js.map +1 -0
  264. package/dist/src/index.d.ts +35 -0
  265. package/dist/src/index.d.ts.map +1 -0
  266. package/dist/src/index.js +39 -0
  267. package/dist/src/index.js.map +1 -0
  268. package/dist/src/registry.d.ts +207 -0
  269. package/dist/src/registry.d.ts.map +1 -0
  270. package/dist/src/registry.js +481 -0
  271. package/dist/src/registry.js.map +1 -0
  272. package/dist/src/sniff.d.ts +104 -0
  273. package/dist/src/sniff.d.ts.map +1 -0
  274. package/dist/src/sniff.js +357 -0
  275. package/dist/src/sniff.js.map +1 -0
  276. package/dist/src/types.d.ts +238 -0
  277. package/dist/src/types.d.ts.map +1 -0
  278. package/dist/src/types.js +29 -0
  279. package/dist/src/types.js.map +1 -0
  280. package/dist/tsconfig.build.tsbuildinfo +1 -0
  281. package/package.json +122 -7
  282. package/src/children.ts +335 -0
  283. package/src/common/attributes.ts +520 -0
  284. package/src/common/codes.ts +153 -0
  285. package/src/common/declared-types.ts +374 -0
  286. package/src/common/direction.ts +518 -0
  287. package/src/common/escape.ts +231 -0
  288. package/src/common/export.ts +817 -0
  289. package/src/common/format.ts +111 -0
  290. package/src/common/ids.ts +176 -0
  291. package/src/common/input.ts +378 -0
  292. package/src/common/lists.ts +196 -0
  293. package/src/common/options.ts +377 -0
  294. package/src/common/report.ts +352 -0
  295. package/src/common/temporal.ts +302 -0
  296. package/src/common/text.ts +294 -0
  297. package/src/common/weights.ts +202 -0
  298. package/src/common/writer.ts +123 -0
  299. package/src/common/xml.ts +1053 -0
  300. package/src/formats/csv/exporter.ts +894 -0
  301. package/src/formats/csv/header.ts +172 -0
  302. package/src/formats/csv/importer.ts +1104 -0
  303. package/src/formats/csv/index.ts +88 -0
  304. package/src/formats/csv/records.ts +813 -0
  305. package/src/formats/csv/values.ts +224 -0
  306. package/src/formats/dot/exporter.ts +1014 -0
  307. package/src/formats/dot/importer.ts +1549 -0
  308. package/src/formats/dot/index.ts +7 -0
  309. package/src/formats/dot/names.ts +40 -0
  310. package/src/formats/dot/tokenizer.ts +384 -0
  311. package/src/formats/gexf/exporter.ts +1696 -0
  312. package/src/formats/gexf/importer.ts +2333 -0
  313. package/src/formats/gexf/index.ts +142 -0
  314. package/src/formats/gexf/schema.ts +361 -0
  315. package/src/formats/gml/exporter.ts +1404 -0
  316. package/src/formats/gml/importer.ts +1591 -0
  317. package/src/formats/gml/index.ts +128 -0
  318. package/src/formats/gml/syntax.ts +545 -0
  319. package/src/formats/graphml/constants.ts +225 -0
  320. package/src/formats/graphml/exporter.ts +1458 -0
  321. package/src/formats/graphml/importer.ts +2027 -0
  322. package/src/formats/graphml/index.ts +8 -0
  323. package/src/formats/graphml/tree.ts +318 -0
  324. package/src/formats/json/dialect.ts +317 -0
  325. package/src/formats/json/exporter.ts +1616 -0
  326. package/src/formats/json/importer.ts +2271 -0
  327. package/src/formats/json/index.ts +8 -0
  328. package/src/formats/neo4j/exporter.ts +1287 -0
  329. package/src/formats/neo4j/header.ts +156 -0
  330. package/src/formats/neo4j/importer.ts +1220 -0
  331. package/src/formats/neo4j/index.ts +116 -0
  332. package/src/formats/pajek/exporter.ts +1000 -0
  333. package/src/formats/pajek/importer.ts +1311 -0
  334. package/src/formats/pajek/index.ts +7 -0
  335. package/src/formats/pajek/syntax.ts +307 -0
  336. package/src/index.ts +244 -0
  337. package/src/registry.ts +617 -0
  338. package/src/sniff.ts +397 -0
  339. package/src/types.ts +262 -0
package/src/sniff.ts ADDED
@@ -0,0 +1,397 @@
1
+ /**
2
+ * Format detection (design sections 8.2 and 13.1: the registry's `sniff()`, the successor of
3
+ * graphty-element's `format-detection.ts`): which of the registered importers a file is for, from
4
+ * any combination of a filename (its extension), a MIME type and the first bytes of the content.
5
+ *
6
+ * Every importer declares `extensions`, `mimeTypes` and a `sniff(head)` content confidence in
7
+ * 0..1 (design section 8.4); this module combines them into one ranked answer so that content
8
+ * always beats a hint: a `.csv` whose first line is a neo4j-admin header is Neo4j, a `.xml` is
9
+ * GEXF or GraphML by its root element, a `.txt` holding `graph [` is GML. The confidence of a
10
+ * candidate is
11
+ *
12
+ * - `0.5 + 0.35 * content + 0.1 * [extension matches] + 0.05 * [MIME type matches]` when the
13
+ * importer recognises the content (`content > 0`), so a content match always scores at least
14
+ * 0.5 and at most 1;
15
+ * - `0.3 * [extension matches] + 0.1 * [MIME type matches]` when the content is absent or the
16
+ * importer rejects it, so a hint alone never reaches 0.5;
17
+ * - nothing (the format is not a candidate) otherwise.
18
+ *
19
+ * Ties are broken by the order the importers were registered in, which is why the default
20
+ * registry lists the common formats first. The JSON dialect is sniffed from the head as a hint
21
+ * (the importer's detection on the parsed document is authoritative, design section 8.2).
22
+ */
23
+
24
+ import { type JsonDialect, sniffJsonDialect } from "./formats/json/dialect.js";
25
+ import { type GraphImporter } from "./types.js";
26
+
27
+ /** The format names of the eight built-in importers and exporters. */
28
+ export type GraphFormatName = "gexf" | "graphml" | "gml" | "dot" | "pajek" | "csv" | "json" | "neo4j";
29
+
30
+ /**
31
+ * The built-in format names in the default registry's order, which is also the tie-break order of
32
+ * sniffing: the more common format wins an extension two formats claim (`.xml` GraphML before GEXF,
33
+ * `.csv` CSV before Neo4j) when the content does not decide.
34
+ */
35
+ export const GRAPH_FORMATS: readonly GraphFormatName[] = Object.freeze([
36
+ "json",
37
+ "graphml",
38
+ "gexf",
39
+ "csv",
40
+ "gml",
41
+ "dot",
42
+ "pajek",
43
+ "neo4j",
44
+ ]);
45
+
46
+ /** How many bytes of the input the sniffers look at; the registry reads no more than this before deciding. */
47
+ export const SNIFF_HEAD_BYTES = 8192;
48
+
49
+ /** What is known about an input before it is read. */
50
+ export interface SniffHints {
51
+ /** A file name or path; only its extension is used. */
52
+ readonly filename?: string | null | undefined;
53
+ /** A MIME type, with or without parameters (`text/csv; charset=utf-8`). */
54
+ readonly mimeType?: string | null | undefined;
55
+ /** The first bytes (or characters) of the content. */
56
+ readonly head?: Uint8Array | string | null | undefined;
57
+ }
58
+
59
+ /** One ranked candidate of a sniff. */
60
+ export interface SniffResult {
61
+ /** The importer's format name. */
62
+ readonly format: string;
63
+ /** The combined confidence in 0..1 (at least 0.5 when the content was recognised). */
64
+ readonly confidence: number;
65
+ /** The importer's own content confidence, 0 when no head was given or it rejected the head. */
66
+ readonly content: number;
67
+ /** Whether the filename's extension is one the importer claims. */
68
+ readonly extension: boolean;
69
+ /** Whether the MIME type is one the importer claims. */
70
+ readonly mimeType: boolean;
71
+ /** For the JSON format: the dialect the head suggests, or null when unknown; always null for other formats. */
72
+ readonly dialect: JsonDialect | null;
73
+ }
74
+
75
+ /**
76
+ * The lower-cased extension of a file name or path, with its dot.
77
+ * @param filename - the name or path
78
+ * @returns the extension (`.gexf`), or null when the name has none
79
+ */
80
+ export function extensionOf(filename: string): string | null {
81
+ const base = filename.slice(Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\")) + 1);
82
+ const dot = base.lastIndexOf(".");
83
+ if (dot <= 0 || dot === base.length - 1) {
84
+ return null;
85
+ }
86
+ return base.slice(dot).toLowerCase();
87
+ }
88
+
89
+ /**
90
+ * A MIME type without parameters, lower-cased, for comparison with an importer's list.
91
+ * @param mimeType - the type as received (`Text/CSV; charset=utf-8`)
92
+ * @returns the bare type (`text/csv`)
93
+ */
94
+ export function normalizeMimeType(mimeType: string): string {
95
+ const semicolon = mimeType.indexOf(";");
96
+ return (semicolon < 0 ? mimeType : mimeType.slice(0, semicolon)).trim().toLowerCase();
97
+ }
98
+
99
+ /**
100
+ * The head as bytes for the importers' sniff functions: at most SNIFF_HEAD_BYTES, a string
101
+ * encoded as UTF-8.
102
+ * @param head - the head as given
103
+ * @returns the bytes
104
+ */
105
+ export function headBytes(head: Uint8Array | string): Uint8Array {
106
+ if (typeof head === "string") {
107
+ return new TextEncoder().encode(head.slice(0, SNIFF_HEAD_BYTES)).subarray(0, SNIFF_HEAD_BYTES);
108
+ }
109
+ return head.byteLength > SNIFF_HEAD_BYTES ? head.subarray(0, SNIFF_HEAD_BYTES) : head;
110
+ }
111
+
112
+ /**
113
+ * Rank the registered importers for an input by the rule of the module comment: every importer
114
+ * whose content confidence is positive or whose extension / MIME type matches is a candidate,
115
+ * ordered by confidence (ties in registration order).
116
+ * @param hints - what is known about the input
117
+ * @param importers - the registered importers, in registration order
118
+ * @returns the candidates, best first; empty when nothing matches
119
+ */
120
+ export function rankFormats(hints: SniffHints, importers: Iterable<GraphImporter>): readonly SniffResult[] {
121
+ const extension = typeof hints.filename === "string" ? extensionOf(hints.filename) : null;
122
+ const mime = typeof hints.mimeType === "string" ? normalizeMimeType(hints.mimeType) : null;
123
+ const head = hints.head === undefined || hints.head === null ? null : headBytes(hints.head);
124
+ const candidates: { readonly result: SniffResult; readonly rank: number }[] = [];
125
+ let rank = 0;
126
+ for (const importer of importers) {
127
+ const extensionMatch = extension !== null && importer.extensions.some((e) => e.toLowerCase() === extension);
128
+ const mimeMatch = mime !== null && importer.mimeTypes.some((m) => m.toLowerCase() === mime);
129
+ let content = 0;
130
+ if (head !== null && head.byteLength > 0 && typeof importer.sniff === "function") {
131
+ content = clamp(importer.sniff(head));
132
+ }
133
+ let confidence: number;
134
+ if (content > 0) {
135
+ confidence = 0.5 + 0.35 * content + (extensionMatch ? 0.1 : 0) + (mimeMatch ? 0.05 : 0);
136
+ } else if (extensionMatch || mimeMatch) {
137
+ confidence = (extensionMatch ? 0.3 : 0) + (mimeMatch ? 0.1 : 0);
138
+ } else {
139
+ continue;
140
+ }
141
+ const dialect = importer.format === "json" && head !== null ? sniffJsonDialectHead(head) : null;
142
+ const result: SniffResult = Object.freeze({
143
+ format: importer.format,
144
+ confidence: Math.min(1, confidence),
145
+ content,
146
+ extension: extensionMatch,
147
+ mimeType: mimeMatch,
148
+ dialect,
149
+ });
150
+ candidates.push({ result, rank: rank++ });
151
+ }
152
+ candidates.sort((a, b) => b.result.confidence - a.result.confidence || a.rank - b.rank);
153
+ return candidates.map((c) => c.result);
154
+ }
155
+
156
+ /**
157
+ * The best candidate of rankFormats(), or null when no importer matches.
158
+ * @param hints - what is known about the input
159
+ * @param importers - the registered importers, in registration order
160
+ * @returns the best candidate, or null
161
+ */
162
+ export function sniffFormat(hints: SniffHints, importers: Iterable<GraphImporter>): SniffResult | null {
163
+ const ranked = rankFormats(hints, importers);
164
+ return ranked.length > 0 ? ranked[0] : null;
165
+ }
166
+
167
+ /**
168
+ * The JSON dialect a head suggests. A head that parses as a whole document is classified exactly
169
+ * (sniffJsonDialect); a truncated head is scanned for its top-level keys, the keys of `graph` and
170
+ * `options`, and the keys of the first object under `nodes` / `edges` / `links`, and the same
171
+ * rule is applied to that skeleton. The result is a hint: the importer decides on the parsed
172
+ * document.
173
+ * @param head - the first bytes or characters of the document
174
+ * @returns the dialect, or null when the head is not a JSON graph document
175
+ */
176
+ export function sniffJsonDialectHead(head: Uint8Array | string): JsonDialect | null {
177
+ const text = typeof head === "string" ? head : new TextDecoder("utf-8", { fatal: false }).decode(head);
178
+ const body = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
179
+ const trimmed = body.trimStart();
180
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
181
+ return null;
182
+ }
183
+ let root: unknown;
184
+ try {
185
+ root = JSON.parse(trimmed);
186
+ } catch {
187
+ root = skeletonOf(trimmed);
188
+ }
189
+ return sniffJsonDialect(root);
190
+ }
191
+
192
+ /** Where a key was seen while scanning a truncated head. */
193
+ type KeyPath = "" | "graph" | "options" | "nodes[0]" | "edges[0]" | "links[0]";
194
+
195
+ /**
196
+ * A partial document rebuilt from the keys a truncated head reveals: every key gets a placeholder
197
+ * value of the shape the dialect rule tests for (an object for `graph`, `options`, `attributes`
198
+ * and `data`, an array holding the first element's skeleton for `nodes` / `edges` / `links` /
199
+ * `graphs`, `true` otherwise), so sniffJsonDialect() can classify it.
200
+ * @param text - the head, starting with `{` or `[`
201
+ * @returns the skeleton, or null when the scan finds no key at all
202
+ */
203
+ function skeletonOf(text: string): unknown {
204
+ const keys = scanKeys(text);
205
+ if (text.startsWith("[")) {
206
+ // a cut array whose first element has not been seen tells nothing (a whole `[]` is Cytoscape)
207
+ const first = keys.get("nodes[0]");
208
+ return first === undefined ? null : [objectOf(first)];
209
+ }
210
+ const rootKeys = keys.get("");
211
+ if (rootKeys === undefined) {
212
+ return null;
213
+ }
214
+ const root: Record<string, unknown> = {};
215
+ for (const key of rootKeys) {
216
+ switch (key) {
217
+ case "graph":
218
+ case "options":
219
+ root[key] = objectOf(keys.get(key));
220
+ break;
221
+ case "graphs":
222
+ root[key] = [];
223
+ break;
224
+ case "nodes":
225
+ case "edges":
226
+ case "links": {
227
+ const first = keys.get(`${key}[0]`);
228
+ root[key] = first === undefined ? [] : [objectOf(first)];
229
+ break;
230
+ }
231
+ default:
232
+ root[key] = true;
233
+ break;
234
+ }
235
+ }
236
+ return root;
237
+ }
238
+
239
+ /**
240
+ * An object holding placeholder values for a key set: `{}` for the keys the rule tests with
241
+ * isJsonObject (`attributes`, `data`), `true` otherwise.
242
+ * @param names - the keys, possibly undefined
243
+ * @returns the object
244
+ */
245
+ function objectOf(names: ReadonlySet<string> | undefined): Record<string, unknown> {
246
+ const out: Record<string, unknown> = {};
247
+ if (names !== undefined) {
248
+ for (const name of names) {
249
+ out[name] = name === "attributes" || name === "data" ? {} : true;
250
+ }
251
+ }
252
+ return out;
253
+ }
254
+
255
+ /**
256
+ * Scan a (possibly truncated) JSON text for the keys at the paths the dialect rule reads. The
257
+ * scanner tracks a container stack (the key each object sits under, the index of each array
258
+ * element) and records a key when it is at one of the watched paths; a head cut inside a string
259
+ * or a number simply ends the scan. For a top-level array the first element's keys are recorded
260
+ * under `nodes[0]`.
261
+ * @param text - the head, starting with `{` or `[`
262
+ * @returns key sets by path
263
+ */
264
+ function scanKeys(text: string): Map<KeyPath, Set<string>> {
265
+ const found = new Map<KeyPath, Set<string>>();
266
+ // one frame per open container: its kind, the key or index it sits under, and the element index
267
+ const kinds: ("object" | "array")[] = [];
268
+ const labels: string[] = [];
269
+ const counts: number[] = [];
270
+ let pendingKey: string | null = null;
271
+ let expectingKey = false;
272
+ const pathOf = (): KeyPath | null => {
273
+ const depth = kinds.length;
274
+ if (depth === 1) {
275
+ return kinds[0] === "object" ? "" : null;
276
+ }
277
+ if (depth === 2 && kinds[0] === "array" && kinds[1] === "object" && labels[1] === "0") {
278
+ return "nodes[0]";
279
+ }
280
+ if (depth === 2 && kinds[0] === "object" && kinds[1] === "object") {
281
+ return labels[1] === "graph" || labels[1] === "options" ? labels[1] : null;
282
+ }
283
+ if (depth === 3 && kinds[0] === "object" && kinds[1] === "array" && kinds[2] === "object") {
284
+ const section = labels[1];
285
+ if ((section === "nodes" || section === "edges" || section === "links") && labels[2] === "0") {
286
+ return `${section}[0]`;
287
+ }
288
+ }
289
+ return null;
290
+ };
291
+ let i = 0;
292
+ const n = text.length;
293
+ while (i < n) {
294
+ const ch = text[i];
295
+ if (ch === '"') {
296
+ const end = scanString(text, i + 1);
297
+ if (end < 0) {
298
+ break;
299
+ }
300
+ const literal = text.slice(i + 1, end);
301
+ i = end + 1;
302
+ if (expectingKey) {
303
+ pendingKey = decodeKey(literal);
304
+ expectingKey = false;
305
+ }
306
+ continue;
307
+ }
308
+ if (ch === "{" || ch === "[") {
309
+ let label = "";
310
+ if (kinds.length > 0) {
311
+ label = kinds[kinds.length - 1] === "object" ? (pendingKey ?? "") : String(counts[counts.length - 1]);
312
+ }
313
+ kinds.push(ch === "{" ? "object" : "array");
314
+ labels.push(label);
315
+ counts.push(0);
316
+ expectingKey = ch === "{";
317
+ pendingKey = null;
318
+ } else if (ch === "}" || ch === "]") {
319
+ kinds.pop();
320
+ labels.pop();
321
+ counts.pop();
322
+ expectingKey = false;
323
+ pendingKey = null;
324
+ } else if (ch === ":") {
325
+ if (pendingKey !== null) {
326
+ const path = pathOf();
327
+ if (path !== null) {
328
+ let set = found.get(path);
329
+ if (set === undefined) {
330
+ set = new Set();
331
+ found.set(path, set);
332
+ }
333
+ set.add(pendingKey);
334
+ }
335
+ }
336
+ } else if (ch === ",") {
337
+ const top = kinds.length - 1;
338
+ if (top >= 0) {
339
+ counts[top]++;
340
+ expectingKey = kinds[top] === "object";
341
+ }
342
+ pendingKey = null;
343
+ }
344
+ i++;
345
+ }
346
+ return found;
347
+ }
348
+
349
+ /**
350
+ * The index of the closing quote of a JSON string starting after its opening quote.
351
+ * @param text - the text
352
+ * @param from - the index after the opening quote
353
+ * @returns the index of the closing quote, or -1 when the text ends first
354
+ */
355
+ function scanString(text: string, from: number): number {
356
+ let i = from;
357
+ while (i < text.length) {
358
+ const ch = text[i];
359
+ if (ch === "\\") {
360
+ i += 2;
361
+ continue;
362
+ }
363
+ if (ch === '"') {
364
+ return i;
365
+ }
366
+ i++;
367
+ }
368
+ return -1;
369
+ }
370
+
371
+ /**
372
+ * A key literal decoded when it holds escapes, kept as written otherwise.
373
+ * @param literal - the text between the quotes
374
+ * @returns the key
375
+ */
376
+ function decodeKey(literal: string): string {
377
+ if (!literal.includes("\\")) {
378
+ return literal;
379
+ }
380
+ try {
381
+ return JSON.parse(`"${literal}"`) as string;
382
+ } catch {
383
+ return literal;
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Coerce a sniff confidence into 0..1 (NaN counts as 0).
389
+ * @param value - the importer's answer
390
+ * @returns the clamped value
391
+ */
392
+ function clamp(value: number): number {
393
+ if (!(value > 0)) {
394
+ return 0;
395
+ }
396
+ return value > 1 ? 1 : value;
397
+ }
package/src/types.ts ADDED
@@ -0,0 +1,262 @@
1
+ /**
2
+ * The io contract types of @graphty/graph-io (design section 12.4, normative): what every importer
3
+ * and exporter implements and what every caller of one programs against. They are declared here
4
+ * rather than in the core because ImportInput and CommonImportOptions reference ReadableStream and
5
+ * AbortSignal, which need the DOM lib (or @types/node >= 18); the core's public types reference only
6
+ * ES2020 globals (decision D-IO-TYPES). Every declaration is transcribed verbatim from the design;
7
+ * the behaviour behind each option is specified in sections 8.4 (import), 8.5 (export) and 8.6
8
+ * (report and error aggregation).
9
+ */
10
+
11
+ import {
12
+ type Dtype,
13
+ type DuplicatePolicy,
14
+ GraphFormatError,
15
+ type GraphSink,
16
+ type GraphSnapshot,
17
+ type IdCoercion,
18
+ } from "@graphty/graph-format";
19
+
20
+ /**
21
+ * What an importer reads (design section 8.4): whole text, whole bytes, a byte stream (a browser
22
+ * `File.stream()`, a fetch body) or an async iterable of text or byte chunks. Bytes are decoded as
23
+ * UTF-8 with `fatal: true`, so an invalid sequence is a parse-error and never a silent U+FFFD that
24
+ * could alias two ids.
25
+ */
26
+ export type ImportInput = string | Uint8Array | ReadableStream<Uint8Array> | AsyncIterable<string | Uint8Array>;
27
+
28
+ /**
29
+ * The options every importer accepts next to its format-specific ones (design section 8.4). The
30
+ * builder-policy fields (`addMissingNodes`, `duplicateEdges`, `selfLoops`, `weightDtype`) seed the
31
+ * registry's builder; on a caller's sink they are read back from `sink.options` and every option the
32
+ * sink cannot honour is reported.
33
+ */
34
+ export interface CommonImportOptions {
35
+ /**
36
+ * Id coercion rule applied before an id reaches the sink (design section 4.1): "canonical" by default for
37
+ * text-cell formats, "keep" for JSON.
38
+ */
39
+ ids?: IdCoercion | undefined;
40
+ /** Which field becomes the node id; "id" by default; "label" / "index" resolve the GML / Pajek / d3 ambiguity. */
41
+ nodeIdFrom?: "id" | "label" | "index" | undefined;
42
+ /** Whether an edge may reference an undeclared node (default true; the GEXF importer defaults false for edges). */
43
+ addMissingNodes?: boolean | undefined;
44
+ /** The builder's duplicate-edge policy seed; default "keep". */
45
+ duplicateEdges?: DuplicatePolicy | undefined;
46
+ /** The builder's self-loop policy seed; default "keep". */
47
+ selfLoops?: "keep" | "drop" | "error" | undefined;
48
+ /** What to do with a file whose edges disagree on direction (design section 3.6); default "expand". */
49
+ onMixedDirection?: "expand" | "directed" | "undirected" | "error" | undefined;
50
+ /** The direction assumed for a file that declares none (GEXF: undirected per spec). */
51
+ defaultDirected?: boolean | undefined;
52
+ /** The attribute that becomes THE weight (per-format default: "weight", GML "value"); null = unweighted. */
53
+ weightFrom?: string | null | undefined;
54
+ /** Weight staging precision; "f64" for every importer by default so 0.1 and 16777217 survive. */
55
+ weightDtype?: "f32" | "f64" | undefined;
56
+ /** How a declared `long` column is stored: "f64" (default) or "string" (design section 5.1). */
57
+ long?: "f64" | "string" | undefined;
58
+ /** Restore ids mangled by sanitizeIds "mangle" from the graphty:originalId attribute (default true). */
59
+ restoreMangledIds?: boolean | undefined;
60
+ /** GraphML / JGF hyperedges: refuse, skip with a report entry (default), or expand to a star / clique. */
61
+ hyperedges?: "error" | "skip" | "star" | "clique" | undefined;
62
+ /** Errors tolerated before the importer aborts with E_IMPORT (default 100). */
63
+ errorLimit?: number | undefined;
64
+ /** Cancellation; the importer stops between chunks and rejects with the signal's reason. */
65
+ signal?: AbortSignal | undefined;
66
+ /** Progress in bytes; `bytesTotal` is known for in-memory input only. */
67
+ onProgress?: ((bytesDone: number, bytesTotal?: number) => void) | undefined;
68
+ }
69
+
70
+ /**
71
+ * An importer plugin (design section 8.4): pushes scalars into the caller's sink in one pass and
72
+ * never freezes. `Opts` is its format-specific option set; the default `unknown` lets a caller pass
73
+ * the common options to an importer typed without one.
74
+ */
75
+ export interface GraphImporter<Opts = unknown> {
76
+ /** The format name: "gexf", "graphml", "gml", "dot", "pajek", "csv", "json", "neo4j". */
77
+ readonly format: string;
78
+ /** File extensions with the leading dot. */
79
+ readonly extensions: readonly string[];
80
+ /** MIME types the format is served as. */
81
+ readonly mimeTypes: readonly string[];
82
+ /**
83
+ * Confidence that `head` is this format, for the registry's sniff().
84
+ * @param head - the first bytes of the input
85
+ * @returns a confidence in 0..1
86
+ */
87
+ sniff?(head: Uint8Array): number;
88
+ /**
89
+ * Read `input` into `sink`; per-element errors are aggregated into the report until the error
90
+ * limit, then the importer throws ImportError with the partial report.
91
+ * @param input - the text, bytes or stream to read
92
+ * @param sink - the builder (or a recording sink) to push into
93
+ * @param options - format-specific and common options
94
+ * @returns the import report
95
+ */
96
+ import(input: ImportInput, sink: GraphSink, options?: Opts & CommonImportOptions): Promise<ImportReport>;
97
+ }
98
+
99
+ /**
100
+ * What a format can express without loss (design section 8.5): the fidelity matrix of research
101
+ * note 07 section 9 as a table, and the acceptance test list for check().
102
+ */
103
+ export interface ExportCapabilities {
104
+ /** Directed and undirected edges in one file. */
105
+ readonly mixedDirection: boolean;
106
+ /** Parallel edges. */
107
+ readonly multiEdges: boolean;
108
+ /** Self-loops. */
109
+ readonly selfLoops: boolean;
110
+ /** Whether edge ids are required (generated when absent), optional or unsupported. */
111
+ readonly edgeIds: "required" | "optional" | "none";
112
+ /** Which node ids can be written unchanged. */
113
+ readonly idCharset: "any" | "nmtoken" | "integer" | "dense-1-based";
114
+ /** The column dtypes the format keeps as declared. */
115
+ readonly dtypes: readonly Dtype[];
116
+ /** Multi-component (stride) columns. */
117
+ readonly components: boolean;
118
+ /** List columns. */
119
+ readonly lists: boolean;
120
+ /** Nested json columns. */
121
+ readonly json: boolean;
122
+ /** Declared defaults. */
123
+ readonly defaults: boolean;
124
+ /** Declared enumerations (GEXF options). */
125
+ readonly options: boolean;
126
+ /** Containment (parent / parents roles). */
127
+ readonly hierarchy: boolean;
128
+ /** Temporal support level. */
129
+ readonly temporal: "none" | "intervals" | "spells" | "dynamic-values";
130
+ /** Graph-level attributes. */
131
+ readonly graphAttributes: boolean;
132
+ /** The position role. */
133
+ readonly positions: boolean;
134
+ /** The visual roles (color, size, shape, thickness). */
135
+ readonly viz: boolean;
136
+ }
137
+
138
+ /** One thing an exporter cannot represent, reported by check() before anything is written (design section 8.5). */
139
+ export interface LossNote {
140
+ /** A stable code such as "W_OPEN_INTERVAL". */
141
+ readonly code: string;
142
+ /** A plain-ASCII human-readable explanation. */
143
+ readonly message: string;
144
+ /** The affected column, or null when the note is not about a column. */
145
+ readonly column: string | null;
146
+ /** How many rows or elements are affected, or null when not counted. */
147
+ readonly count: number | null;
148
+ }
149
+
150
+ /** The options every exporter accepts next to its format-specific ones (design section 8.5). */
151
+ export interface CommonExportOptions {
152
+ /** "error" (default): never rename a node; "mangle": rewrite ids the format cannot hold and keep the original. */
153
+ sanitizeIds?: "error" | "mangle" | undefined;
154
+ /** For formats without mixed-direction support: refuse (default) or write every edge one way. */
155
+ onMixedDirection?: "error" | "directed" | "undirected" | undefined;
156
+ }
157
+
158
+ /**
159
+ * An exporter plugin (design section 8.5): iterates nodes and logical edges (never arcs) in index
160
+ * order and reads the role columns of section 3.6 / 3.7 to fold expanded pairs and emit explicit
161
+ * weights only.
162
+ */
163
+ export interface GraphExporter<Opts = unknown> {
164
+ /** The format name. */
165
+ readonly format: string;
166
+ /** What the format can express. */
167
+ readonly capabilities: ExportCapabilities;
168
+ /**
169
+ * Pre-flight: what export() would lose, without writing anything.
170
+ * @param snapshot - the snapshot to check
171
+ * @param options - format-specific and common options
172
+ * @returns the loss notes, empty when the export is exact
173
+ */
174
+ check(snapshot: GraphSnapshot, options?: Opts & CommonExportOptions): readonly LossNote[];
175
+ /**
176
+ * Write the snapshot as UTF-8 chunks.
177
+ * @param snapshot - the snapshot to write
178
+ * @param options - format-specific and common options
179
+ * @returns the encoded chunks
180
+ */
181
+ export(snapshot: GraphSnapshot, options?: Opts & CommonExportOptions): AsyncIterable<Uint8Array>;
182
+ /**
183
+ * Write the snapshot as one string.
184
+ * @param snapshot - the snapshot to write
185
+ * @param options - format-specific and common options
186
+ * @returns the whole document
187
+ */
188
+ exportToString(snapshot: GraphSnapshot, options?: Opts & CommonExportOptions): Promise<string>;
189
+ }
190
+
191
+ /** The categories of an ImportIssue (design section 8.6). */
192
+ export type IssueCategory =
193
+ "parse-error" | "missing-value" | "validation-error" | "unsupported" | "precision" | "coercion" | "merged";
194
+
195
+ /** One problem found while importing (design section 8.6). */
196
+ export interface ImportIssue {
197
+ /** The category. */
198
+ readonly category: IssueCategory;
199
+ /** Errors count toward the error limit; warnings do not. */
200
+ readonly severity: "error" | "warning";
201
+ /** A stable code such as "E_UNKNOWN_NODE" or "W_WIDENED". */
202
+ readonly code: string;
203
+ /** A plain-ASCII human-readable message. */
204
+ readonly message: string;
205
+ /** The 1-based source line, or null when unknown. */
206
+ readonly line: number | null;
207
+ /** The element (a node or edge id, an attribute name), or null when unknown. */
208
+ readonly element: string | null;
209
+ }
210
+
211
+ /** What an import produced besides the sink's contents (design section 8.6). */
212
+ export interface ImportReport {
213
+ /** The importer's format name. */
214
+ readonly format: string;
215
+ /** Element counts. */
216
+ readonly counts: {
217
+ /** Nodes pushed. */
218
+ readonly nodes: number;
219
+ /** Logical edges pushed (both halves of an expanded edge count). */
220
+ readonly edges: number;
221
+ /** Nodes skipped after an error. */
222
+ readonly skippedNodes: number;
223
+ /** Edges skipped after an error. */
224
+ readonly skippedEdges: number;
225
+ /** Edges expanded under onMixedDirection "expand" (design section 3.6). */
226
+ readonly expandedMixed: number;
227
+ };
228
+ /** Every issue recorded, in order. */
229
+ readonly issues: readonly ImportIssue[];
230
+ /** Issues with severity "error". */
231
+ readonly errorCount: number;
232
+ /** Issues with severity "warning". */
233
+ readonly warningCount: number;
234
+ /** Whether the error limit was reached and the import aborted. */
235
+ readonly truncated: boolean;
236
+ /** What the importer could not represent. */
237
+ readonly lossy: readonly LossNote[];
238
+ /** Wall time of the parse phase; the caller's freeze reports separately. */
239
+ readonly durationMs: number;
240
+ }
241
+
242
+ /**
243
+ * The error an importer throws when the error limit is reached or the input cannot be read at all
244
+ * (design section 8.6): a GraphFormatError with code "E_IMPORT" (reserved in the core's union so
245
+ * `err.code === "E_IMPORT"` narrows) carrying the partial report.
246
+ */
247
+ export class ImportError extends GraphFormatError {
248
+ /** The report as it stood when the import aborted. */
249
+ readonly report: ImportReport;
250
+
251
+ /**
252
+ * Create an ImportError.
253
+ * @param message - a plain-ASCII human-readable message
254
+ * @param report - the partial report
255
+ * @param details - optional machine-readable context
256
+ */
257
+ constructor(message: string, report: ImportReport, details?: Readonly<Record<string, unknown>>) {
258
+ super("E_IMPORT", message, details);
259
+ this.name = "ImportError";
260
+ this.report = report;
261
+ }
262
+ }