aontu 0.52.1 → 0.54.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 (310) hide show
  1. package/README.md +88 -0
  2. package/bin/aontu-mcp.js +4 -0
  3. package/dist/agentsmd.d.ts +16 -0
  4. package/dist/agentsmd.js +107 -0
  5. package/dist/agentsmd.js.map +1 -0
  6. package/dist/aontu.d.ts +15 -3
  7. package/dist/aontu.js +126 -5
  8. package/dist/aontu.js.map +1 -1
  9. package/dist/cli.d.ts +45 -1
  10. package/dist/cli.js +2845 -69
  11. package/dist/cli.js.map +1 -1
  12. package/dist/ctx.d.ts +18 -0
  13. package/dist/ctx.js +44 -0
  14. package/dist/ctx.js.map +1 -1
  15. package/dist/diff.d.ts +22 -0
  16. package/dist/diff.js +141 -0
  17. package/dist/diff.js.map +1 -0
  18. package/dist/err.d.ts +3 -1
  19. package/dist/err.js +45 -8
  20. package/dist/err.js.map +1 -1
  21. package/dist/graph.d.ts +13 -0
  22. package/dist/graph.js +110 -0
  23. package/dist/graph.js.map +1 -0
  24. package/dist/hcanon.d.ts +3 -0
  25. package/dist/hcanon.js +145 -0
  26. package/dist/hcanon.js.map +1 -0
  27. package/dist/hints.js +235 -8
  28. package/dist/hints.js.map +1 -1
  29. package/dist/jsonschema.d.ts +20 -0
  30. package/dist/jsonschema.js +425 -0
  31. package/dist/jsonschema.js.map +1 -0
  32. package/dist/lang.js +955 -110
  33. package/dist/lang.js.map +1 -1
  34. package/dist/lsp.d.ts +9 -2
  35. package/dist/lsp.js +343 -48
  36. package/dist/lsp.js.map +1 -1
  37. package/dist/mcp-server.d.ts +20 -0
  38. package/dist/mcp-server.js +147 -0
  39. package/dist/mcp-server.js.map +1 -0
  40. package/dist/mcp.d.ts +42 -0
  41. package/dist/mcp.js +941 -0
  42. package/dist/mcp.js.map +1 -0
  43. package/dist/mod-tool.d.ts +58 -0
  44. package/dist/mod-tool.js +532 -0
  45. package/dist/mod-tool.js.map +1 -0
  46. package/dist/mod.d.ts +35 -0
  47. package/dist/mod.js +345 -0
  48. package/dist/mod.js.map +1 -0
  49. package/dist/patch.d.ts +49 -0
  50. package/dist/patch.js +506 -0
  51. package/dist/patch.js.map +1 -0
  52. package/dist/provenance.d.ts +41 -0
  53. package/dist/provenance.js +336 -0
  54. package/dist/provenance.js.map +1 -0
  55. package/dist/query.d.ts +27 -0
  56. package/dist/query.js +294 -0
  57. package/dist/query.js.map +1 -0
  58. package/dist/reach.d.ts +15 -0
  59. package/dist/reach.js +168 -0
  60. package/dist/reach.js.map +1 -0
  61. package/dist/relation.d.ts +23 -0
  62. package/dist/relation.js +230 -0
  63. package/dist/relation.js.map +1 -0
  64. package/dist/report-sarif.d.ts +14 -0
  65. package/dist/report-sarif.js +102 -0
  66. package/dist/report-sarif.js.map +1 -0
  67. package/dist/sig.d.ts +25 -0
  68. package/dist/sig.js +277 -0
  69. package/dist/sig.js.map +1 -0
  70. package/dist/sigdecl.d.ts +2 -0
  71. package/dist/sigdecl.js +11 -0
  72. package/dist/sigdecl.js.map +1 -0
  73. package/dist/siggate.d.ts +4 -0
  74. package/dist/siggate.js +90 -0
  75. package/dist/siggate.js.map +1 -0
  76. package/dist/site.d.ts +4 -0
  77. package/dist/site.js +31 -0
  78. package/dist/site.js.map +1 -1
  79. package/dist/std.d.ts +1 -0
  80. package/dist/std.js +129 -0
  81. package/dist/std.js.map +1 -0
  82. package/dist/subsume.d.ts +39 -0
  83. package/dist/subsume.js +573 -0
  84. package/dist/subsume.js.map +1 -0
  85. package/dist/trim.d.ts +19 -0
  86. package/dist/trim.js +155 -0
  87. package/dist/trim.js.map +1 -0
  88. package/dist/tsconfig.tsbuildinfo +1 -1
  89. package/dist/type.d.ts +17 -1
  90. package/dist/type.js.map +1 -1
  91. package/dist/unify.d.ts +2 -1
  92. package/dist/unify.js +234 -38
  93. package/dist/unify.js.map +1 -1
  94. package/dist/utility.d.ts +8 -1
  95. package/dist/utility.js +68 -1
  96. package/dist/utility.js.map +1 -1
  97. package/dist/val/AggFuncVal.d.ts +44 -0
  98. package/dist/val/AggFuncVal.js +364 -0
  99. package/dist/val/AggFuncVal.js.map +1 -0
  100. package/dist/val/ArithFuncVal.d.ts +31 -0
  101. package/dist/val/ArithFuncVal.js +62 -0
  102. package/dist/val/ArithFuncVal.js.map +1 -0
  103. package/dist/val/BagVal.d.ts +7 -0
  104. package/dist/val/BagVal.js +150 -11
  105. package/dist/val/BagVal.js.map +1 -1
  106. package/dist/val/CloseFuncVal.js +9 -1
  107. package/dist/val/CloseFuncVal.js.map +1 -1
  108. package/dist/val/ConjunctVal.d.ts +1 -1
  109. package/dist/val/ConjunctVal.js +19 -0
  110. package/dist/val/ConjunctVal.js.map +1 -1
  111. package/dist/val/ConstraintVal.d.ts +7 -1
  112. package/dist/val/ConstraintVal.js +523 -45
  113. package/dist/val/ConstraintVal.js.map +1 -1
  114. package/dist/val/ContainerKindVal.d.ts +35 -0
  115. package/dist/val/ContainerKindVal.js +99 -0
  116. package/dist/val/ContainerKindVal.js.map +1 -0
  117. package/dist/val/CopyFuncVal.d.ts +1 -2
  118. package/dist/val/CopyFuncVal.js.map +1 -1
  119. package/dist/val/Decimal.d.ts +1 -0
  120. package/dist/val/Decimal.js +13 -0
  121. package/dist/val/Decimal.js.map +1 -1
  122. package/dist/val/DeprecateFuncVal.d.ts +11 -0
  123. package/dist/val/DeprecateFuncVal.js +47 -0
  124. package/dist/val/DeprecateFuncVal.js.map +1 -0
  125. package/dist/val/DisjunctVal.d.ts +1 -2
  126. package/dist/val/DisjunctVal.js +269 -53
  127. package/dist/val/DisjunctVal.js.map +1 -1
  128. package/dist/val/EachFuncVal.d.ts +15 -0
  129. package/dist/val/EachFuncVal.js +75 -0
  130. package/dist/val/EachFuncVal.js.map +1 -0
  131. package/dist/val/ExpectVal.js +62 -4
  132. package/dist/val/ExpectVal.js.map +1 -1
  133. package/dist/val/FeatureVal.js +1 -1
  134. package/dist/val/FeatureVal.js.map +1 -1
  135. package/dist/val/FilterFuncVal.d.ts +15 -0
  136. package/dist/val/FilterFuncVal.js +91 -0
  137. package/dist/val/FilterFuncVal.js.map +1 -0
  138. package/dist/val/FuncBaseVal.d.ts +7 -1
  139. package/dist/val/FuncBaseVal.js +183 -2
  140. package/dist/val/FuncBaseVal.js.map +1 -1
  141. package/dist/val/GraphAtomVal.d.ts +39 -0
  142. package/dist/val/GraphAtomVal.js +184 -0
  143. package/dist/val/GraphAtomVal.js.map +1 -0
  144. package/dist/val/HideFuncVal.js.map +1 -1
  145. package/dist/val/JunctionVal.js +24 -1
  146. package/dist/val/JunctionVal.js.map +1 -1
  147. package/dist/val/KeyFuncVal.d.ts +1 -1
  148. package/dist/val/KeyFuncVal.js +38 -30
  149. package/dist/val/KeyFuncVal.js.map +1 -1
  150. package/dist/val/ListVal.js +94 -14
  151. package/dist/val/ListVal.js.map +1 -1
  152. package/dist/val/LowerFuncVal.js.map +1 -1
  153. package/dist/val/MapVal.d.ts +1 -0
  154. package/dist/val/MapVal.js +146 -14
  155. package/dist/val/MapVal.js.map +1 -1
  156. package/dist/val/MatchFuncVal.d.ts +15 -0
  157. package/dist/val/MatchFuncVal.js +107 -0
  158. package/dist/val/MatchFuncVal.js.map +1 -0
  159. package/dist/val/MoveFuncVal.js.map +1 -1
  160. package/dist/val/NilVal.js +24 -0
  161. package/dist/val/NilVal.js.map +1 -1
  162. package/dist/val/OpBaseVal.d.ts +1 -1
  163. package/dist/val/OpBaseVal.js +19 -1
  164. package/dist/val/OpBaseVal.js.map +1 -1
  165. package/dist/val/OpenFuncVal.js +4 -1
  166. package/dist/val/OpenFuncVal.js.map +1 -1
  167. package/dist/val/PackFuncVal.d.ts +15 -0
  168. package/dist/val/PackFuncVal.js +108 -0
  169. package/dist/val/PackFuncVal.js.map +1 -0
  170. package/dist/val/PathFuncVal.d.ts +2 -2
  171. package/dist/val/PathFuncVal.js +75 -16
  172. package/dist/val/PathFuncVal.js.map +1 -1
  173. package/dist/val/PathVal.d.ts +25 -0
  174. package/dist/val/PathVal.js +150 -0
  175. package/dist/val/PathVal.js.map +1 -0
  176. package/dist/val/PlaceVal.d.ts +13 -0
  177. package/dist/val/PlaceVal.js +131 -0
  178. package/dist/val/PlaceVal.js.map +1 -0
  179. package/dist/val/PlusOpVal.d.ts +2 -1
  180. package/dist/val/PlusOpVal.js +61 -37
  181. package/dist/val/PlusOpVal.js.map +1 -1
  182. package/dist/val/PrefFuncVal.js.map +1 -1
  183. package/dist/val/PrefVal.d.ts +4 -2
  184. package/dist/val/PrefVal.js +223 -41
  185. package/dist/val/PrefVal.js.map +1 -1
  186. package/dist/val/RecurseVal.d.ts +19 -0
  187. package/dist/val/RecurseVal.js +217 -0
  188. package/dist/val/RecurseVal.js.map +1 -0
  189. package/dist/val/RefVal.d.ts +3 -2
  190. package/dist/val/RefVal.js +181 -66
  191. package/dist/val/RefVal.js.map +1 -1
  192. package/dist/val/ReferFuncVal.d.ts +59 -0
  193. package/dist/val/ReferFuncVal.js +604 -0
  194. package/dist/val/ReferFuncVal.js.map +1 -0
  195. package/dist/val/ScalarKindVal.d.ts +4 -3
  196. package/dist/val/ScalarKindVal.js +12 -12
  197. package/dist/val/ScalarKindVal.js.map +1 -1
  198. package/dist/val/SuperFuncVal.d.ts +4 -2
  199. package/dist/val/SuperFuncVal.js +118 -14
  200. package/dist/val/SuperFuncVal.js.map +1 -1
  201. package/dist/val/TopVal.d.ts +1 -1
  202. package/dist/val/TopVal.js.map +1 -1
  203. package/dist/val/TypeFuncVal.js.map +1 -1
  204. package/dist/val/UpperFuncVal.js.map +1 -1
  205. package/dist/val/Val.d.ts +7 -1
  206. package/dist/val/Val.js +170 -2
  207. package/dist/val/Val.js.map +1 -1
  208. package/dist/val/VarVal.js.map +1 -1
  209. package/dist/val/arith.d.ts +6 -0
  210. package/dist/val/arith.js +173 -0
  211. package/dist/val/arith.js.map +1 -0
  212. package/dist/vet.d.ts +45 -0
  213. package/dist/vet.js +802 -0
  214. package/dist/vet.js.map +1 -0
  215. package/dist/view.d.ts +85 -0
  216. package/dist/view.js +1948 -0
  217. package/dist/view.js.map +1 -0
  218. package/dist/walk.d.ts +2 -0
  219. package/dist/walk.js +91 -0
  220. package/dist/walk.js.map +1 -0
  221. package/grammar/aontu.gbnf +138 -0
  222. package/grammar/aontu.lark +118 -0
  223. package/grammar/aontu.tmLanguage.json +184 -0
  224. package/package.json +30 -8
  225. package/skill/SKILL.md +37 -0
  226. package/skill/error-codes.md +62 -0
  227. package/skill/examples.md +99 -0
  228. package/skill/grammar-card.md +56 -0
  229. package/src/agentsmd.ts +135 -0
  230. package/src/aontu.ts +166 -5
  231. package/src/cli.ts +3324 -72
  232. package/src/ctx.ts +93 -1
  233. package/src/diff.ts +198 -0
  234. package/src/err.ts +50 -8
  235. package/src/graph.ts +185 -0
  236. package/src/hcanon.ts +167 -0
  237. package/src/hints.ts +291 -8
  238. package/src/jsonschema.ts +552 -0
  239. package/src/lang.ts +1049 -110
  240. package/src/lsp.ts +351 -49
  241. package/src/mcp-server.ts +187 -0
  242. package/src/mcp.ts +1130 -0
  243. package/src/mod-tool.ts +718 -0
  244. package/src/mod.ts +453 -0
  245. package/src/patch.ts +628 -0
  246. package/src/provenance.ts +437 -0
  247. package/src/query.ts +381 -0
  248. package/src/reach.ts +213 -0
  249. package/src/relation.ts +298 -0
  250. package/src/report-sarif.ts +137 -0
  251. package/src/sig.ts +345 -0
  252. package/src/sigdecl.ts +11 -0
  253. package/src/siggate.ts +144 -0
  254. package/src/site.ts +36 -1
  255. package/src/std.ts +131 -0
  256. package/src/subsume.ts +739 -0
  257. package/src/trim.ts +195 -0
  258. package/src/tsconfig.json +10 -4
  259. package/src/type.ts +51 -2
  260. package/src/unify.ts +253 -38
  261. package/src/utility.ts +79 -1
  262. package/src/val/AggFuncVal.ts +546 -0
  263. package/src/val/ArithFuncVal.ts +108 -0
  264. package/src/val/BagVal.ts +158 -12
  265. package/src/val/CloseFuncVal.ts +9 -1
  266. package/src/val/ConjunctVal.ts +20 -0
  267. package/src/val/ConstraintVal.ts +576 -48
  268. package/src/val/ContainerKindVal.ts +158 -0
  269. package/src/val/CopyFuncVal.ts +0 -1
  270. package/src/val/Decimal.ts +15 -0
  271. package/src/val/DeprecateFuncVal.ts +84 -0
  272. package/src/val/DisjunctVal.ts +282 -59
  273. package/src/val/EachFuncVal.ts +133 -0
  274. package/src/val/ExpectVal.ts +65 -8
  275. package/src/val/FeatureVal.ts +1 -1
  276. package/src/val/FilterFuncVal.ts +154 -0
  277. package/src/val/FuncBaseVal.ts +206 -2
  278. package/src/val/GraphAtomVal.ts +264 -0
  279. package/src/val/HideFuncVal.ts +0 -2
  280. package/src/val/JunctionVal.ts +24 -1
  281. package/src/val/KeyFuncVal.ts +39 -35
  282. package/src/val/ListVal.ts +101 -15
  283. package/src/val/LowerFuncVal.ts +0 -1
  284. package/src/val/MapVal.ts +153 -14
  285. package/src/val/MatchFuncVal.ts +176 -0
  286. package/src/val/MoveFuncVal.ts +0 -2
  287. package/src/val/NilVal.ts +25 -0
  288. package/src/val/OpBaseVal.ts +20 -1
  289. package/src/val/OpenFuncVal.ts +4 -2
  290. package/src/val/PackFuncVal.ts +175 -0
  291. package/src/val/PathFuncVal.ts +107 -20
  292. package/src/val/PathVal.ts +221 -0
  293. package/src/val/PlaceVal.ts +193 -0
  294. package/src/val/PlusOpVal.ts +67 -39
  295. package/src/val/PrefFuncVal.ts +0 -1
  296. package/src/val/PrefVal.ts +251 -60
  297. package/src/val/RecurseVal.ts +285 -0
  298. package/src/val/RefVal.ts +183 -78
  299. package/src/val/ReferFuncVal.ts +732 -0
  300. package/src/val/ScalarKindVal.ts +12 -13
  301. package/src/val/SuperFuncVal.ts +137 -13
  302. package/src/val/TopVal.ts +1 -2
  303. package/src/val/TypeFuncVal.ts +0 -2
  304. package/src/val/UpperFuncVal.ts +0 -1
  305. package/src/val/Val.ts +216 -4
  306. package/src/val/VarVal.ts +0 -1
  307. package/src/val/arith.ts +319 -0
  308. package/src/vet.ts +1025 -0
  309. package/src/view.ts +2563 -0
  310. package/src/walk.ts +99 -0
package/src/query.ts ADDED
@@ -0,0 +1,381 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ // THE QUERY SURFACE (G7 phase 1,
4
+ // docs/capability-review/g7-machine-access.md): select one node of an
5
+ // evaluated document by path and render it — the slice an agent asks
6
+ // for, instead of the whole file as one JSON blob.
7
+ //
8
+ // Evaluation is still GLOBAL. Unification has no partial mode to sell:
9
+ // the whole document is evaluated and then one node is selected. What
10
+ // `get` buys is the SIZE OF THE ANSWER, not the cost of producing it.
11
+ //
12
+ // The three projections are lattice ABSTRACTIONS, and each is defined
13
+ // so that the view it prints is a valid Aontu document that SUBSUMES
14
+ // the truth — generalisation, never distortion:
15
+ //
16
+ // - `types` replaces every concrete leaf with its own kind, using
17
+ // the lattice's own superior() rather than a table of this file's
18
+ // opinions: {"replicas":3} becomes {"replicas":integer}.
19
+ // - `depth n` keeps structure to depth n and renders every elided
20
+ // subtree as `top` — "no further information at this tier".
21
+ // - `keys` is `depth 1` degenerated to a listing.
22
+ //
23
+ // That property is not a promise here: every projection row of
24
+ // test/spec/query.tsv asserts subsume(view, truth) == 'subsumes' in
25
+ // both runners, which G3 made mechanically checkable.
26
+ //
27
+ // Projections are NOT canonical form and are never fed to G6's hash,
28
+ // which is why they are named views rather than spellings of --canon.
29
+
30
+ import { Aontu } from './aontu'
31
+ import type { TrustOptions } from './type'
32
+ import { exactJSON } from './exactjson'
33
+ import { anchorAt } from './vet'
34
+ import type { VetFinding } from './vet'
35
+ import { cmpCodePoint } from './keyorder'
36
+ import { Provenance } from './provenance'
37
+ import type { WhyRecord } from './provenance'
38
+
39
+
40
+ export type QueryView = 'json' | 'canon' | 'types' | 'keys'
41
+
42
+ export type QueryOptions = {
43
+ view?: QueryView
44
+ // Levels of structure kept below the selected node; everything
45
+ // deeper renders as `top`. Undefined means the whole subtree.
46
+ depth?: number
47
+ // Where the document CAME FROM, so a relative `@"file"` load inside
48
+ // it resolves from its own directory (vet's schemaPath precedent).
49
+ path?: string
50
+
51
+ // The trust profile this run evaluates under (G5, docs/trust.md).
52
+ // The source arrives from a caller, so the caller must be able to
53
+ // say what it may reach: without this the include chain is the
54
+ // default one, and `@"../../etc/passwd.aon"` reads whatever the
55
+ // process can. (`@"x.js"` no longer executes -- ADR-012 refuses the
56
+ // extension -- but reading is enough.) A server passes
57
+ // `{include:'none'}`.
58
+ trust?: TrustOptions
59
+ }
60
+
61
+ export type QueryReport = {
62
+ ok: boolean
63
+ out: string
64
+ // Empty when ok. G2's finding shape, deliberately: `get` invents no
65
+ // error format (G7's own rule).
66
+ findings: VetFinding[]
67
+ }
68
+
69
+
70
+ const TOP = 'top'
71
+
72
+
73
+ // The nearest key at the parent of a path that named nothing, by a
74
+ // plain edit distance over the sibling names — the "did you mean"
75
+ // half of the no_path contract. Undefined when nothing is close
76
+ // enough to be worth suggesting.
77
+ export function nearestKey(
78
+ want: string, have: string[]): string | undefined {
79
+ let best: string | undefined
80
+ let bestd = Infinity
81
+ for (const k of have) {
82
+ const d = editDistance(want, k)
83
+ if (d < bestd) {
84
+ bestd = d
85
+ best = k
86
+ }
87
+ }
88
+ // Half the name may differ, no more: past that the suggestion is
89
+ // noise, and a wrong suggestion costs more than none.
90
+ return bestd <= Math.max(1, Math.floor(want.length / 2)) ? best : undefined
91
+ }
92
+
93
+
94
+ function editDistance(a: string, b: string): number {
95
+ const prev: number[] = []
96
+ for (let j = 0; j <= b.length; j++) {
97
+ prev[j] = j
98
+ }
99
+ for (let i = 1; i <= a.length; i++) {
100
+ let diag = prev[0]
101
+ prev[0] = i
102
+ for (let j = 1; j <= b.length; j++) {
103
+ const tmp = prev[j]
104
+ prev[j] = Math.min(
105
+ prev[j] + 1,
106
+ prev[j - 1] + 1,
107
+ diag + (a[i - 1] === b[j - 1] ? 0 : 1))
108
+ diag = tmp
109
+ }
110
+ }
111
+ return prev[b.length]
112
+ }
113
+
114
+
115
+ // The path split anchorAt walks: `$` and empty segments dropped, so
116
+ // `$`, `$.` and `` all name the root.
117
+ export function pathParts(path: string): string[] {
118
+ const trimmed = path.startsWith('$') ? path.slice(1) : path
119
+ return trimmed.split('.').filter((p) => '' !== p)
120
+ }
121
+
122
+
123
+ // The queried path, normalised the way anchorAt reads it — so a
124
+ // finding names `$.a.b` whether the caller wrote that, `a.b` or
125
+ // `$.a.b.` — and `$` for the root.
126
+ function pathText(path: string): string {
127
+ const parts = pathParts(path)
128
+ return '$' + (0 < parts.length ? '.' + parts.join('.') : '')
129
+ }
130
+
131
+
132
+ // The projection walk, exported for the direct unit tests (ADR-002,
133
+ // ts/test/coverage3.test.ts): a junction member that is itself a
134
+ // junction of more than one term keeps its parens, and no SOURCE
135
+ // reaches that arm because norm flattens junctions at unification.
136
+ export function projectFor(
137
+ v: any, view: QueryView, depth: number): string {
138
+ return project(v, view, depth)
139
+ }
140
+
141
+
142
+ // The canon-shaped views. One walk, two knobs: `types` generalises
143
+ // each leaf through the lattice, `depth` elides below its level. Bags
144
+ // recurse (their canon getters would render children through plain
145
+ // canon, which neither knob can reach); everything else is a leaf.
146
+ function project(v: any, view: QueryView, depth: number): string {
147
+ if (depth <= 0) {
148
+ return TOP
149
+ }
150
+ if (true === v?.isMap) {
151
+ const keys = Object.keys(v.peg).sort(cmpCodePoint)
152
+ return '{' +
153
+ (v.spread.cj ? '&:' + project(v.spread.cj, view, depth - 1) +
154
+ (0 < keys.length ? ',' : '') : '') +
155
+ keys.map((k) =>
156
+ JSON.stringify(k) +
157
+ (v.optionalKeys.includes(k) ? '?' : '') +
158
+ ':' +
159
+ project(v.peg[k], view, depth - 1)).join(',') +
160
+ '}'
161
+ }
162
+ if (true === v?.isList) {
163
+ const keys = Object.keys(v.peg)
164
+ return '[' +
165
+ (v.spread.cj ? '&:' + project(v.spread.cj, view, depth - 1) +
166
+ (0 < keys.length ? ',' : '') : '') +
167
+ keys.map((k) => project(v.peg[k], view, depth - 1)).join(',') +
168
+ ']'
169
+ }
170
+ // Junctions and prefs are TRANSPARENT: not a structural tier (so
171
+ // they do not spend a level of depth) but not a leaf either (so
172
+ // `*8080|integer` generalises to `*integer|integer` rather than
173
+ // collapsing to `top` and throwing the alternatives away).
174
+ if (true === v?.isPref) {
175
+ return '*' + project(v.peg, view, depth)
176
+ }
177
+ if (true === v?.isConjunct || true === v?.isDisjunct) {
178
+ return v.peg.map((m: any) =>
179
+ true === m?.isJunction && 1 < m.peg.length
180
+ ? '(' + project(m, view, depth) + ')'
181
+ : project(m, view, depth))
182
+ .join(true === v.isConjunct ? '&' : '|')
183
+ }
184
+
185
+ // A LEAF. Under `types` a CONCRETE scalar lifts to its own kind —
186
+ // superior() is the lattice's answer, so the view subsumes the truth
187
+ // by construction and not by this file's good intentions. Everything
188
+ // else is already an abstraction (a kind marker, a constraint, an
189
+ // unresolved reference) and is left alone: lifting `integer` to
190
+ // `number` would generalise a shape view that was already a shape.
191
+ return 'types' === view && true === v?.isScalar ? v.superior().canon : v.canon
192
+ }
193
+
194
+
195
+ // The `keys` listing: the node's own key names (or list indices), one
196
+ // per line, code-point ordered as canon orders them. A leaf has none,
197
+ // which is an empty answer rather than an error — "nothing below
198
+ // here" is a true statement about a scalar.
199
+ function keyList(v: any): string {
200
+ if (true === v?.isMap) {
201
+ return Object.keys(v.peg).sort(cmpCodePoint).join('\n')
202
+ }
203
+ if (true === v?.isList) {
204
+ return Object.keys(v.peg).join('\n')
205
+ }
206
+ return ''
207
+ }
208
+
209
+
210
+ function finding(
211
+ code: string, path: string, message: string, note?: string): VetFinding {
212
+ return {
213
+ code,
214
+ class: 'reference',
215
+ severity: 'error',
216
+ path,
217
+ message,
218
+ sites: [],
219
+ ...(null == note ? {} : { note }),
220
+ }
221
+ }
222
+
223
+
224
+ // A document that does not stand up has no node to select. The
225
+ // engine's own first error IS the report: the query surface adds
226
+ // nothing to a diagnosis the evaluator already made. The path is the
227
+ // DOCUMENT — what failed is the whole thing standing up, not the node
228
+ // the caller asked about, which may never have existed.
229
+ export function evalFailure(ctx: any): VetFinding {
230
+ // ctx.err is never empty at a call site: every failure that reaches
231
+ // one collected an error first — a parse that did not stand up, a
232
+ // root that came back nil. Not coalesced, on the vet siteOf
233
+ // precedent: an impossible state should fail loudly rather than be
234
+ // quietly papered over with a made-up code.
235
+ const err: any = ctx.err[0]
236
+ return finding(err.why, '$', err.msg)
237
+ }
238
+
239
+
240
+ // The refusal for a path that names nothing, shared by `get` and
241
+ // `why`: WHICH segment failed, and what was there instead — the "did
242
+ // you mean" the no_path contract promises. Walking again is cheap (the
243
+ // tree is in hand) and is the only way to name the parent.
244
+ export function noPathFinding(root: any, path: string): VetFinding {
245
+ const parts = pathParts(path)
246
+ let at: any = root
247
+ let want = ''
248
+ for (const part of parts) {
249
+ const next: any = anchorAt(at, part)
250
+ if (null == next) {
251
+ want = part
252
+ break
253
+ }
254
+ at = next
255
+ }
256
+ const have = true === at?.isMap
257
+ ? Object.keys(at.peg).sort(cmpCodePoint)
258
+ : (true === at?.isList ? Object.keys(at.peg) : [])
259
+ const near = nearestKey(want, have)
260
+ return finding(
261
+ 'no_path',
262
+ pathText(path),
263
+ `The path ${path} names nothing in this document.`,
264
+ null == near ? undefined : `did you mean ${near}?`)
265
+ }
266
+
267
+
268
+ // Evaluate the document, select the node at `path`, and render it.
269
+ export function get(
270
+ src: string, path: string, opts?: QueryOptions): QueryReport {
271
+ const options = opts ?? {}
272
+ const view: QueryView = options.view ?? 'json'
273
+
274
+ const aontu = new Aontu(
275
+ null == options.trust ? undefined : { trust: options.trust })
276
+ const ctx = aontu.ctx({ collect: true })
277
+ const parseOpts = null == options.path ? undefined : { path: options.path }
278
+ const root: any = aontu.unify(src, parseOpts, ctx)
279
+
280
+ if (0 < ctx.err.length || null == root || true === root.isNil) {
281
+ return { ok: false, out: '', findings: [evalFailure(ctx)] }
282
+ }
283
+
284
+ const node: any = anchorAt(root, path)
285
+ if (null == node) {
286
+ return { ok: false, out: '', findings: [noPathFinding(root, path)] }
287
+ }
288
+
289
+ if ('json' === view) {
290
+ // GENERATION CAN FAIL WHERE UNIFICATION DID NOT: `k: integer` is a
291
+ // perfectly good unified document and not a concrete value, so the
292
+ // json view of it is an error, exactly as `aontu file.aon` on the
293
+ // same document is. Under `collect` the failure lands on the
294
+ // context rather than throwing, so it has to be read back — the Go
295
+ // port's Gen returns it as an error and the two must agree.
296
+ const before = ctx.err.length
297
+ const gen = node.gen(ctx)
298
+ if (before < ctx.err.length) {
299
+ const err: any = ctx.err[before]
300
+ return {
301
+ ok: false,
302
+ out: '',
303
+ findings: [finding(
304
+ err?.why ?? 'no_gen',
305
+ pathText(path),
306
+ err?.msg ?? 'The value at this path is not concrete.')],
307
+ }
308
+ }
309
+ return { ok: true, out: exactJSON(gen, 2), findings: [] }
310
+ }
311
+ if ('keys' === view) {
312
+ return { ok: true, out: keyList(node), findings: [] }
313
+ }
314
+ return {
315
+ ok: true,
316
+ out: project(node, view, options.depth ?? Infinity),
317
+ findings: [],
318
+ }
319
+ }
320
+
321
+
322
+ export type WhyReport = {
323
+ ok: boolean
324
+ record?: WhyRecord
325
+ findings: VetFinding[]
326
+ }
327
+
328
+
329
+ // WHY does the value at this path hold? Evaluate with the provenance
330
+ // recorder on, select the node, and answer the ordered contributions
331
+ // that met there — the positive twin of G2's error report.
332
+ //
333
+ // Two evaluations are NOT needed: the recorder rides the one run this
334
+ // call makes. What it costs is site materialisation and one map entry
335
+ // per path met, which an instrumented run pays knowingly.
336
+ export function why(
337
+ src: string, path: string, opts?: QueryOptions): WhyReport {
338
+ const options = opts ?? {}
339
+ const aontu = new Aontu(
340
+ null == options.trust ? undefined : { trust: options.trust })
341
+ const prov = new Provenance()
342
+ const ctx = aontu.ctx({ collect: true, prov })
343
+ const parseOpts = null == options.path ? undefined : { path: options.path }
344
+
345
+ // Parse and unify SEPARATELY, so the parsed tree can be stamped
346
+ // before the fixpoint runs: a contribution is a value the author
347
+ // wrote, and after unification there is no longer any way to tell
348
+ // one from a value the engine minted on the way.
349
+ const parsed: any = aontu.parse(src, parseOpts, ctx)
350
+ if (0 < ctx.err.length || null == parsed) {
351
+ return { ok: false, findings: [evalFailure(ctx)] }
352
+ }
353
+ prov.writtenFrom(parsed)
354
+
355
+ const root: any = aontu.unify(parsed, parseOpts, ctx)
356
+ if (0 < ctx.err.length || null == root || true === root.isNil) {
357
+ return { ok: false, findings: [evalFailure(ctx)] }
358
+ }
359
+
360
+ const node: any = anchorAt(root, path)
361
+ if (null == node) {
362
+ return { ok: false, findings: [noPathFinding(root, path)] }
363
+ }
364
+
365
+ // The value that stands here is a contribution when nothing met
366
+ // (see Provenance.stands): a generator places a value without a
367
+ // meet, and "nothing met at this path" is not an answer to "where
368
+ // did this come from".
369
+ const parts = pathParts(path)
370
+ prov.stands(parts, node)
371
+
372
+ return {
373
+ ok: true,
374
+ record: {
375
+ conjuncts: prov.at(parts),
376
+ path: pathText(path),
377
+ value: node.canon,
378
+ },
379
+ findings: [],
380
+ }
381
+ }
package/src/reach.ts ADDED
@@ -0,0 +1,213 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ // REACHABILITY OVER THE LINK GRAPH (the review's finding J,
4
+ // use-cases/REVIEW.md): "ship a transitive `reaches(a, b)` check verb".
5
+ //
6
+ // `relations` answers questions about the edge set as a whole --- is it
7
+ // acyclic, does every edge have its inverse, is every far end what the
8
+ // relation says it is. This answers the question that needs the CLOSURE
9
+ // rather than the edges: does anything `a` depends on, at any remove,
10
+ // end up at `b`? That is the shape of every blast-radius question an
11
+ // operator asks ("if the billing database goes, what falls over?") and
12
+ // every containment question a policy asks ("nothing in the public tier
13
+ // may reach the ledger"), and neither can be expressed by looking one
14
+ // edge at a time.
15
+ //
16
+ // It is a VERB and not a constraint, for the same reason acyclicity is
17
+ // (docs/reference-language.md, "Declared relations"): reachability is
18
+ // global and non-monotone. One more edge can make an unreachable pair
19
+ // reachable, so a lattice citizen asserting non-reachability could be
20
+ // true and then false, and the lattice guarantee is that more
21
+ // information never falsifies what has already been observed.
22
+ //
23
+ // TRANSITIVE, NOT REFLEXIVE-TRANSITIVE: `reaches(a, a)` is true only
24
+ // when a path of one or more edges returns to `a`, which is the useful
25
+ // answer (it says the graph has a cycle through `a`) rather than the
26
+ // vacuous one.
27
+ //
28
+ // The Go twin is go/reach.go; what the two ports must agree on --- the
29
+ // verdict and the path --- is test/spec/reach.tsv.
30
+
31
+ import { Aontu } from './aontu'
32
+ import { failureFinding } from './vet'
33
+ import type { VetFinding } from './vet'
34
+ import type { TrustOptions } from './type'
35
+ import { graphOf } from './graph'
36
+ import { cmpCodePoint } from './keyorder'
37
+
38
+
39
+ export type ReachVerdict = 'reaches' | 'unreachable' | 'error'
40
+
41
+ export type ReachReport = {
42
+ verdict: ReachVerdict
43
+
44
+ // The path found, as `$.dotted` node paths from the source to the
45
+ // destination, both included. Present ONLY on `reaches`: a path is
46
+ // the evidence for the answer, and there is no evidence for a
47
+ // negative one.
48
+ path?: string[]
49
+
50
+ // WHY the graph could not be looked at, in vet's finding shape (the
51
+ // review's finding F): a document that does not stand up, or an
52
+ // endpoint that names no node. Present ONLY on `error`.
53
+ errors?: VetFinding[]
54
+ }
55
+
56
+ export type ReachOptions = {
57
+ // Where the document CAME FROM, so a relative `@"file"` load inside
58
+ // it resolves from its own directory (relationCheck's precedent).
59
+ path?: string
60
+ // The include capability this document evaluates under (G5,
61
+ // docs/trust.md).
62
+ trust?: TrustOptions
63
+ // Follow only edges under this relation. Absent means follow every
64
+ // edge, which is the whole graph and the commoner question.
65
+ relation?: string
66
+ }
67
+
68
+
69
+ // The segments a `$.dotted` endpoint spells, or undefined when it is
70
+ // not one. Reachability is between TREE POSITIONS (ADR-014), so an
71
+ // endpoint is a path and nothing else --- the same spelling the report
72
+ // prints back.
73
+ export function parseNodePath(s: string): string[] | undefined {
74
+ if ('$' === s) {
75
+ return []
76
+ }
77
+ if (!s.startsWith('$.')) {
78
+ return undefined
79
+ }
80
+ const parts = s.slice(2).split('.')
81
+ return parts.every((p) => /^[A-Za-z0-9_-]+$/.test(p)) ? parts : undefined
82
+ }
83
+
84
+
85
+ // Whether a path names a node of the evaluated tree. An endpoint that
86
+ // exists but has no edges is a perfectly good question with the answer
87
+ // `unreachable`; only one that names NOTHING is an error.
88
+ function nodeAt(root: any, path: string[]): boolean {
89
+ let node: any = root
90
+ for (const seg of path) {
91
+ if (true !== node?.isMap && true !== node?.isList) {
92
+ return false
93
+ }
94
+ node = node.peg[seg]
95
+ if (null == node) {
96
+ return false
97
+ }
98
+ }
99
+ return null != node
100
+ }
101
+
102
+
103
+ function endpointFinding(name: string, known: string[]): VetFinding {
104
+ return {
105
+ code: 'refer_unresolved',
106
+ class: 'reference',
107
+ severity: 'error',
108
+ path: '$',
109
+ // NOT "unreachable". An endpoint that names no node is a question
110
+ // the document cannot answer, and answering it `no` would report a
111
+ // typo as a fact about the model --- the fail-open shape this
112
+ // review exists to retire.
113
+ message: `${name} names no node in this document.`,
114
+ sites: [],
115
+ ...(0 === known.length ? {} : {
116
+ note: 'nodes with links: ' + known.join(', '),
117
+ }),
118
+ }
119
+ }
120
+
121
+
122
+ // The reachability check for one document.
123
+ export function reachCheck(
124
+ src: string, from: string, to: string, opts?: ReachOptions
125
+ ): ReachReport {
126
+ const options = opts ?? {}
127
+ const aontu = new Aontu(
128
+ null == options.trust ? undefined : { trust: options.trust })
129
+ const ctx = aontu.ctx({ collect: true })
130
+ const parseOpts = null == options.path ? undefined : { path: options.path }
131
+ const root: any = aontu.unify(src, parseOpts, ctx)
132
+
133
+ // A document that does not stand up is not a document with an
134
+ // unreachable pair: the errors it already has are the answer.
135
+ if (0 < ctx.err.length || true === root?.isNil) {
136
+ return {
137
+ verdict: 'error',
138
+ errors: [failureFinding(ctx, options.path, root)],
139
+ }
140
+ }
141
+
142
+ const graph = graphOf(root)
143
+ // The nodes the graph actually touches, for the error note: a
144
+ // document has every path in it, and listing them all would drown the
145
+ // one fact a mistyped endpoint needs.
146
+ const linked = [...new Set(graph.edges
147
+ .flatMap((e) => [e.from, e.to]))].sort(cmpCodePoint)
148
+ const missing = [from, to].filter((n) => {
149
+ const parts = parseNodePath(n)
150
+ return undefined === parts || !nodeAt(root, parts)
151
+ })
152
+ if (0 < missing.length) {
153
+ return {
154
+ verdict: 'error',
155
+ errors: missing.map((n) => endpointFinding(n, linked)),
156
+ }
157
+ }
158
+
159
+ // The successor map, restricted to one relation when the caller asked
160
+ // for one. Sorted, so the path the search finds is the same one in
161
+ // both ports.
162
+ const succ = new Map<string, string[]>()
163
+ for (const e of graph.edges) {
164
+ if (null != options.relation && options.relation !== e.key) {
165
+ continue
166
+ }
167
+ const list = succ.get(e.from)
168
+ const dest = e.to
169
+ if (undefined === list) {
170
+ succ.set(e.from, [dest])
171
+ }
172
+ else if (!list.includes(dest)) {
173
+ list.push(dest)
174
+ }
175
+ }
176
+ for (const list of succ.values()) {
177
+ list.sort(cmpCodePoint)
178
+ }
179
+
180
+ // BREADTH-FIRST, so the path reported is a SHORTEST one --- the
181
+ // evidence an operator wants is the tightest chain, not whichever the
182
+ // walk happened to find first --- and, with the successors sorted, a
183
+ // determined one: among shortest paths, the first in code-point order
184
+ // at the first step that distinguishes them.
185
+ const prev = new Map<string, string>()
186
+ const seen = new Set<string>()
187
+ let front: string[] = [from]
188
+ while (0 < front.length) {
189
+ const next: string[] = []
190
+ for (const node of front) {
191
+ for (const dest of succ.get(node) ?? []) {
192
+ if (dest === to) {
193
+ const path = [dest]
194
+ let step = node
195
+ while (step !== from) {
196
+ path.unshift(step)
197
+ step = prev.get(step) as string
198
+ }
199
+ path.unshift(from)
200
+ return { verdict: 'reaches', path }
201
+ }
202
+ if (!seen.has(dest)) {
203
+ seen.add(dest)
204
+ prev.set(dest, node)
205
+ next.push(dest)
206
+ }
207
+ }
208
+ }
209
+ front = next
210
+ }
211
+
212
+ return { verdict: 'unreachable' }
213
+ }