aontu 0.52.0 → 0.53.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 (273) 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 +14 -3
  7. package/dist/aontu.js +145 -4
  8. package/dist/aontu.js.map +1 -1
  9. package/dist/cli.d.ts +44 -1
  10. package/dist/cli.js +2401 -44
  11. package/dist/cli.js.map +1 -1
  12. package/dist/ctx.d.ts +16 -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 +48 -8
  20. package/dist/err.js.map +1 -1
  21. package/dist/graph.d.ts +16 -0
  22. package/dist/graph.js +73 -0
  23. package/dist/graph.js.map +1 -0
  24. package/dist/hcanon.d.ts +3 -0
  25. package/dist/hcanon.js +146 -0
  26. package/dist/hcanon.js.map +1 -0
  27. package/dist/hints.js +223 -5
  28. package/dist/hints.js.map +1 -1
  29. package/dist/jsonschema.d.ts +20 -0
  30. package/dist/jsonschema.js +391 -0
  31. package/dist/jsonschema.js.map +1 -0
  32. package/dist/lang.js +698 -35
  33. package/dist/lang.js.map +1 -1
  34. package/dist/lsp.d.ts +9 -2
  35. package/dist/lsp.js +262 -46
  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 +814 -0
  42. package/dist/mcp.js.map +1 -0
  43. package/dist/mod-tool.d.ts +58 -0
  44. package/dist/mod-tool.js +498 -0
  45. package/dist/mod-tool.js.map +1 -0
  46. package/dist/mod.d.ts +31 -0
  47. package/dist/mod.js +250 -0
  48. package/dist/mod.js.map +1 -0
  49. package/dist/patch.d.ts +44 -0
  50. package/dist/patch.js +506 -0
  51. package/dist/patch.js.map +1 -0
  52. package/dist/provenance.d.ts +40 -0
  53. package/dist/provenance.js +335 -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 +14 -0
  59. package/dist/reach.js +140 -0
  60. package/dist/reach.js.map +1 -0
  61. package/dist/relation.d.ts +19 -0
  62. package/dist/relation.js +305 -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/site.d.ts +4 -0
  68. package/dist/site.js +31 -0
  69. package/dist/site.js.map +1 -1
  70. package/dist/std.d.ts +1 -0
  71. package/dist/std.js +73 -0
  72. package/dist/std.js.map +1 -0
  73. package/dist/subsume.d.ts +39 -0
  74. package/dist/subsume.js +526 -0
  75. package/dist/subsume.js.map +1 -0
  76. package/dist/trim.d.ts +19 -0
  77. package/dist/trim.js +155 -0
  78. package/dist/trim.js.map +1 -0
  79. package/dist/tsconfig.tsbuildinfo +1 -1
  80. package/dist/type.d.ts +17 -1
  81. package/dist/type.js.map +1 -1
  82. package/dist/unify.d.ts +3 -1
  83. package/dist/unify.js +287 -18
  84. package/dist/unify.js.map +1 -1
  85. package/dist/utility.d.ts +9 -1
  86. package/dist/utility.js +122 -1
  87. package/dist/utility.js.map +1 -1
  88. package/dist/val/AggFuncVal.d.ts +33 -0
  89. package/dist/val/AggFuncVal.js +202 -0
  90. package/dist/val/AggFuncVal.js.map +1 -0
  91. package/dist/val/ArithFuncVal.d.ts +31 -0
  92. package/dist/val/ArithFuncVal.js +62 -0
  93. package/dist/val/ArithFuncVal.js.map +1 -0
  94. package/dist/val/BagVal.d.ts +5 -0
  95. package/dist/val/BagVal.js +96 -5
  96. package/dist/val/BagVal.js.map +1 -1
  97. package/dist/val/CloseFuncVal.js +9 -1
  98. package/dist/val/CloseFuncVal.js.map +1 -1
  99. package/dist/val/ConjunctVal.d.ts +1 -1
  100. package/dist/val/ConjunctVal.js +19 -0
  101. package/dist/val/ConjunctVal.js.map +1 -1
  102. package/dist/val/ConstraintVal.d.ts +48 -1
  103. package/dist/val/ConstraintVal.js +1501 -110
  104. package/dist/val/ConstraintVal.js.map +1 -1
  105. package/dist/val/CopyFuncVal.d.ts +1 -2
  106. package/dist/val/CopyFuncVal.js +7 -0
  107. package/dist/val/CopyFuncVal.js.map +1 -1
  108. package/dist/val/Decimal.d.ts +1 -0
  109. package/dist/val/Decimal.js +13 -0
  110. package/dist/val/Decimal.js.map +1 -1
  111. package/dist/val/DeprecateFuncVal.d.ts +11 -0
  112. package/dist/val/DeprecateFuncVal.js +47 -0
  113. package/dist/val/DeprecateFuncVal.js.map +1 -0
  114. package/dist/val/DisjunctVal.js +130 -21
  115. package/dist/val/DisjunctVal.js.map +1 -1
  116. package/dist/val/EachFuncVal.d.ts +15 -0
  117. package/dist/val/EachFuncVal.js +75 -0
  118. package/dist/val/EachFuncVal.js.map +1 -0
  119. package/dist/val/ExpectVal.d.ts +1 -0
  120. package/dist/val/ExpectVal.js +41 -4
  121. package/dist/val/ExpectVal.js.map +1 -1
  122. package/dist/val/FeatureVal.js +1 -1
  123. package/dist/val/FeatureVal.js.map +1 -1
  124. package/dist/val/FilterFuncVal.d.ts +15 -0
  125. package/dist/val/FilterFuncVal.js +91 -0
  126. package/dist/val/FilterFuncVal.js.map +1 -0
  127. package/dist/val/FuncBaseVal.d.ts +6 -1
  128. package/dist/val/FuncBaseVal.js +178 -2
  129. package/dist/val/FuncBaseVal.js.map +1 -1
  130. package/dist/val/HideFuncVal.js.map +1 -1
  131. package/dist/val/IdFuncVal.d.ts +13 -0
  132. package/dist/val/IdFuncVal.js +54 -0
  133. package/dist/val/IdFuncVal.js.map +1 -0
  134. package/dist/val/JunctionVal.js +7 -1
  135. package/dist/val/JunctionVal.js.map +1 -1
  136. package/dist/val/KeyFuncVal.d.ts +1 -1
  137. package/dist/val/KeyFuncVal.js +38 -30
  138. package/dist/val/KeyFuncVal.js.map +1 -1
  139. package/dist/val/ListVal.js +117 -17
  140. package/dist/val/ListVal.js.map +1 -1
  141. package/dist/val/LowerFuncVal.js.map +1 -1
  142. package/dist/val/MapVal.js +102 -8
  143. package/dist/val/MapVal.js.map +1 -1
  144. package/dist/val/MatchFuncVal.d.ts +15 -0
  145. package/dist/val/MatchFuncVal.js +107 -0
  146. package/dist/val/MatchFuncVal.js.map +1 -0
  147. package/dist/val/MoveFuncVal.js.map +1 -1
  148. package/dist/val/NilVal.js +24 -0
  149. package/dist/val/NilVal.js.map +1 -1
  150. package/dist/val/OpBaseVal.d.ts +1 -1
  151. package/dist/val/OpBaseVal.js +24 -2
  152. package/dist/val/OpBaseVal.js.map +1 -1
  153. package/dist/val/OpenFuncVal.js +4 -1
  154. package/dist/val/OpenFuncVal.js.map +1 -1
  155. package/dist/val/PackFuncVal.d.ts +15 -0
  156. package/dist/val/PackFuncVal.js +108 -0
  157. package/dist/val/PackFuncVal.js.map +1 -0
  158. package/dist/val/PathFuncVal.js.map +1 -1
  159. package/dist/val/PlaceVal.d.ts +13 -0
  160. package/dist/val/PlaceVal.js +131 -0
  161. package/dist/val/PlaceVal.js.map +1 -0
  162. package/dist/val/PlusOpVal.js +11 -2
  163. package/dist/val/PlusOpVal.js.map +1 -1
  164. package/dist/val/PrefFuncVal.js.map +1 -1
  165. package/dist/val/PrefVal.d.ts +2 -2
  166. package/dist/val/PrefVal.js +78 -23
  167. package/dist/val/PrefVal.js.map +1 -1
  168. package/dist/val/RefVal.d.ts +1 -1
  169. package/dist/val/RefVal.js +158 -30
  170. package/dist/val/RefVal.js.map +1 -1
  171. package/dist/val/ReferFuncVal.d.ts +36 -0
  172. package/dist/val/ReferFuncVal.js +303 -0
  173. package/dist/val/ReferFuncVal.js.map +1 -0
  174. package/dist/val/ScalarKindVal.d.ts +1 -2
  175. package/dist/val/ScalarKindVal.js +0 -11
  176. package/dist/val/ScalarKindVal.js.map +1 -1
  177. package/dist/val/TopVal.js.map +1 -1
  178. package/dist/val/TypeFuncVal.js.map +1 -1
  179. package/dist/val/UpperFuncVal.js.map +1 -1
  180. package/dist/val/Val.d.ts +9 -2
  181. package/dist/val/Val.js +150 -4
  182. package/dist/val/Val.js.map +1 -1
  183. package/dist/val/VarVal.js.map +1 -1
  184. package/dist/val/arith.d.ts +6 -0
  185. package/dist/val/arith.js +170 -0
  186. package/dist/val/arith.js.map +1 -0
  187. package/dist/vet.d.ts +45 -0
  188. package/dist/vet.js +776 -0
  189. package/dist/vet.js.map +1 -0
  190. package/dist/walk.d.ts +2 -0
  191. package/dist/walk.js +91 -0
  192. package/dist/walk.js.map +1 -0
  193. package/grammar/aontu.gbnf +130 -0
  194. package/grammar/aontu.lark +113 -0
  195. package/package.json +30 -15
  196. package/skill/SKILL.md +37 -0
  197. package/skill/error-codes.md +62 -0
  198. package/skill/examples.md +99 -0
  199. package/skill/grammar-card.md +57 -0
  200. package/src/agentsmd.ts +135 -0
  201. package/src/aontu.ts +192 -4
  202. package/src/cli.ts +2858 -71
  203. package/src/ctx.ts +81 -0
  204. package/src/diff.ts +196 -0
  205. package/src/err.ts +52 -8
  206. package/src/graph.ts +135 -0
  207. package/src/hcanon.ts +169 -0
  208. package/src/hints.ts +271 -5
  209. package/src/jsonschema.ts +511 -0
  210. package/src/lang.ts +779 -37
  211. package/src/lsp.ts +281 -47
  212. package/src/mcp-server.ts +187 -0
  213. package/src/mcp.ts +993 -0
  214. package/src/mod-tool.ts +679 -0
  215. package/src/mod.ts +344 -0
  216. package/src/patch.ts +624 -0
  217. package/src/provenance.ts +430 -0
  218. package/src/query.ts +379 -0
  219. package/src/reach.ts +184 -0
  220. package/src/relation.ts +395 -0
  221. package/src/report-sarif.ts +137 -0
  222. package/src/site.ts +36 -1
  223. package/src/std.ts +73 -0
  224. package/src/subsume.ts +690 -0
  225. package/src/trim.ts +195 -0
  226. package/src/tsconfig.json +10 -4
  227. package/src/type.ts +51 -2
  228. package/src/unify.ts +311 -16
  229. package/src/utility.ts +139 -1
  230. package/src/val/AggFuncVal.ts +319 -0
  231. package/src/val/ArithFuncVal.ts +108 -0
  232. package/src/val/BagVal.ts +101 -4
  233. package/src/val/CloseFuncVal.ts +9 -1
  234. package/src/val/ConjunctVal.ts +20 -0
  235. package/src/val/ConstraintVal.ts +1699 -116
  236. package/src/val/CopyFuncVal.ts +7 -1
  237. package/src/val/Decimal.ts +15 -0
  238. package/src/val/DeprecateFuncVal.ts +84 -0
  239. package/src/val/DisjunctVal.ts +139 -28
  240. package/src/val/EachFuncVal.ts +133 -0
  241. package/src/val/ExpectVal.ts +43 -6
  242. package/src/val/FeatureVal.ts +1 -1
  243. package/src/val/FilterFuncVal.ts +154 -0
  244. package/src/val/FuncBaseVal.ts +200 -3
  245. package/src/val/HideFuncVal.ts +0 -2
  246. package/src/val/IdFuncVal.ts +91 -0
  247. package/src/val/JunctionVal.ts +7 -1
  248. package/src/val/KeyFuncVal.ts +39 -35
  249. package/src/val/ListVal.ts +125 -18
  250. package/src/val/LowerFuncVal.ts +0 -1
  251. package/src/val/MapVal.ts +110 -8
  252. package/src/val/MatchFuncVal.ts +176 -0
  253. package/src/val/MoveFuncVal.ts +0 -2
  254. package/src/val/NilVal.ts +25 -0
  255. package/src/val/OpBaseVal.ts +26 -3
  256. package/src/val/OpenFuncVal.ts +4 -2
  257. package/src/val/PackFuncVal.ts +175 -0
  258. package/src/val/PathFuncVal.ts +0 -1
  259. package/src/val/PlaceVal.ts +193 -0
  260. package/src/val/PlusOpVal.ts +11 -2
  261. package/src/val/PrefFuncVal.ts +0 -1
  262. package/src/val/PrefVal.ts +79 -36
  263. package/src/val/RefVal.ts +163 -30
  264. package/src/val/ReferFuncVal.ts +387 -0
  265. package/src/val/ScalarKindVal.ts +0 -13
  266. package/src/val/TopVal.ts +0 -1
  267. package/src/val/TypeFuncVal.ts +0 -2
  268. package/src/val/UpperFuncVal.ts +0 -1
  269. package/src/val/Val.ts +213 -3
  270. package/src/val/VarVal.ts +0 -1
  271. package/src/val/arith.ts +316 -0
  272. package/src/vet.ts +992 -0
  273. package/src/walk.ts +99 -0
package/src/query.ts ADDED
@@ -0,0 +1,379 @@
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 `@"x.js"` is arbitrary code execution in the
55
+ // evaluating process. A server passes `{include:'none'}`.
56
+ trust?: TrustOptions
57
+ }
58
+
59
+ export type QueryReport = {
60
+ ok: boolean
61
+ out: string
62
+ // Empty when ok. G2's finding shape, deliberately: `get` invents no
63
+ // error format (G7's own rule).
64
+ findings: VetFinding[]
65
+ }
66
+
67
+
68
+ const TOP = 'top'
69
+
70
+
71
+ // The nearest key at the parent of a path that named nothing, by a
72
+ // plain edit distance over the sibling names — the "did you mean"
73
+ // half of the no_path contract. Undefined when nothing is close
74
+ // enough to be worth suggesting.
75
+ export function nearestKey(
76
+ want: string, have: string[]): string | undefined {
77
+ let best: string | undefined
78
+ let bestd = Infinity
79
+ for (const k of have) {
80
+ const d = editDistance(want, k)
81
+ if (d < bestd) {
82
+ bestd = d
83
+ best = k
84
+ }
85
+ }
86
+ // Half the name may differ, no more: past that the suggestion is
87
+ // noise, and a wrong suggestion costs more than none.
88
+ return bestd <= Math.max(1, Math.floor(want.length / 2)) ? best : undefined
89
+ }
90
+
91
+
92
+ function editDistance(a: string, b: string): number {
93
+ const prev: number[] = []
94
+ for (let j = 0; j <= b.length; j++) {
95
+ prev[j] = j
96
+ }
97
+ for (let i = 1; i <= a.length; i++) {
98
+ let diag = prev[0]
99
+ prev[0] = i
100
+ for (let j = 1; j <= b.length; j++) {
101
+ const tmp = prev[j]
102
+ prev[j] = Math.min(
103
+ prev[j] + 1,
104
+ prev[j - 1] + 1,
105
+ diag + (a[i - 1] === b[j - 1] ? 0 : 1))
106
+ diag = tmp
107
+ }
108
+ }
109
+ return prev[b.length]
110
+ }
111
+
112
+
113
+ // The path split anchorAt walks: `$` and empty segments dropped, so
114
+ // `$`, `$.` and `` all name the root.
115
+ export function pathParts(path: string): string[] {
116
+ const trimmed = path.startsWith('$') ? path.slice(1) : path
117
+ return trimmed.split('.').filter((p) => '' !== p)
118
+ }
119
+
120
+
121
+ // The queried path, normalised the way anchorAt reads it — so a
122
+ // finding names `$.a.b` whether the caller wrote that, `a.b` or
123
+ // `$.a.b.` — and `$` for the root.
124
+ function pathText(path: string): string {
125
+ const parts = pathParts(path)
126
+ return '$' + (0 < parts.length ? '.' + parts.join('.') : '')
127
+ }
128
+
129
+
130
+ // The projection walk, exported for the direct unit tests (ADR-002,
131
+ // ts/test/coverage3.test.ts): a junction member that is itself a
132
+ // junction of more than one term keeps its parens, and no SOURCE
133
+ // reaches that arm because norm flattens junctions at unification.
134
+ export function projectFor(
135
+ v: any, view: QueryView, depth: number): string {
136
+ return project(v, view, depth)
137
+ }
138
+
139
+
140
+ // The canon-shaped views. One walk, two knobs: `types` generalises
141
+ // each leaf through the lattice, `depth` elides below its level. Bags
142
+ // recurse (their canon getters would render children through plain
143
+ // canon, which neither knob can reach); everything else is a leaf.
144
+ function project(v: any, view: QueryView, depth: number): string {
145
+ if (depth <= 0) {
146
+ return TOP
147
+ }
148
+ if (true === v?.isMap) {
149
+ const keys = Object.keys(v.peg).sort(cmpCodePoint)
150
+ return '{' +
151
+ (v.spread.cj ? '&:' + project(v.spread.cj, view, depth - 1) +
152
+ (0 < keys.length ? ',' : '') : '') +
153
+ keys.map((k) =>
154
+ JSON.stringify(k) +
155
+ (v.optionalKeys.includes(k) ? '?' : '') +
156
+ ':' +
157
+ project(v.peg[k], view, depth - 1)).join(',') +
158
+ '}'
159
+ }
160
+ if (true === v?.isList) {
161
+ const keys = Object.keys(v.peg)
162
+ return '[' +
163
+ (v.spread.cj ? '&:' + project(v.spread.cj, view, depth - 1) +
164
+ (0 < keys.length ? ',' : '') : '') +
165
+ keys.map((k) => project(v.peg[k], view, depth - 1)).join(',') +
166
+ ']'
167
+ }
168
+ // Junctions and prefs are TRANSPARENT: not a structural tier (so
169
+ // they do not spend a level of depth) but not a leaf either (so
170
+ // `*8080|integer` generalises to `*integer|integer` rather than
171
+ // collapsing to `top` and throwing the alternatives away).
172
+ if (true === v?.isPref) {
173
+ return '*' + project(v.peg, view, depth)
174
+ }
175
+ if (true === v?.isConjunct || true === v?.isDisjunct) {
176
+ return v.peg.map((m: any) =>
177
+ true === m?.isJunction && 1 < m.peg.length
178
+ ? '(' + project(m, view, depth) + ')'
179
+ : project(m, view, depth))
180
+ .join(true === v.isConjunct ? '&' : '|')
181
+ }
182
+
183
+ // A LEAF. Under `types` a CONCRETE scalar lifts to its own kind —
184
+ // superior() is the lattice's answer, so the view subsumes the truth
185
+ // by construction and not by this file's good intentions. Everything
186
+ // else is already an abstraction (a kind marker, a constraint, an
187
+ // unresolved reference) and is left alone: lifting `integer` to
188
+ // `number` would generalise a shape view that was already a shape.
189
+ return 'types' === view && true === v?.isScalar ? v.superior().canon : v.canon
190
+ }
191
+
192
+
193
+ // The `keys` listing: the node's own key names (or list indices), one
194
+ // per line, code-point ordered as canon orders them. A leaf has none,
195
+ // which is an empty answer rather than an error — "nothing below
196
+ // here" is a true statement about a scalar.
197
+ function keyList(v: any): string {
198
+ if (true === v?.isMap) {
199
+ return Object.keys(v.peg).sort(cmpCodePoint).join('\n')
200
+ }
201
+ if (true === v?.isList) {
202
+ return Object.keys(v.peg).join('\n')
203
+ }
204
+ return ''
205
+ }
206
+
207
+
208
+ function finding(
209
+ code: string, path: string, message: string, note?: string): VetFinding {
210
+ return {
211
+ code,
212
+ class: 'reference',
213
+ severity: 'error',
214
+ path,
215
+ message,
216
+ sites: [],
217
+ ...(null == note ? {} : { note }),
218
+ }
219
+ }
220
+
221
+
222
+ // A document that does not stand up has no node to select. The
223
+ // engine's own first error IS the report: the query surface adds
224
+ // nothing to a diagnosis the evaluator already made. The path is the
225
+ // DOCUMENT — what failed is the whole thing standing up, not the node
226
+ // the caller asked about, which may never have existed.
227
+ export function evalFailure(ctx: any): VetFinding {
228
+ // ctx.err is never empty at a call site: every failure that reaches
229
+ // one collected an error first — a parse that did not stand up, a
230
+ // root that came back nil. Not coalesced, on the vet siteOf
231
+ // precedent: an impossible state should fail loudly rather than be
232
+ // quietly papered over with a made-up code.
233
+ const err: any = ctx.err[0]
234
+ return finding(err.why, '$', err.msg)
235
+ }
236
+
237
+
238
+ // The refusal for a path that names nothing, shared by `get` and
239
+ // `why`: WHICH segment failed, and what was there instead — the "did
240
+ // you mean" the no_path contract promises. Walking again is cheap (the
241
+ // tree is in hand) and is the only way to name the parent.
242
+ export function noPathFinding(root: any, path: string): VetFinding {
243
+ const parts = pathParts(path)
244
+ let at: any = root
245
+ let want = ''
246
+ for (const part of parts) {
247
+ const next: any = anchorAt(at, part)
248
+ if (null == next) {
249
+ want = part
250
+ break
251
+ }
252
+ at = next
253
+ }
254
+ const have = true === at?.isMap
255
+ ? Object.keys(at.peg).sort(cmpCodePoint)
256
+ : (true === at?.isList ? Object.keys(at.peg) : [])
257
+ const near = nearestKey(want, have)
258
+ return finding(
259
+ 'no_path',
260
+ pathText(path),
261
+ `The path ${path} names nothing in this document.`,
262
+ null == near ? undefined : `did you mean ${near}?`)
263
+ }
264
+
265
+
266
+ // Evaluate the document, select the node at `path`, and render it.
267
+ export function get(
268
+ src: string, path: string, opts?: QueryOptions): QueryReport {
269
+ const options = opts ?? {}
270
+ const view: QueryView = options.view ?? 'json'
271
+
272
+ const aontu = new Aontu(
273
+ null == options.trust ? undefined : { trust: options.trust })
274
+ const ctx = aontu.ctx({ collect: true })
275
+ const parseOpts = null == options.path ? undefined : { path: options.path }
276
+ const root: any = aontu.unify(src, parseOpts, ctx)
277
+
278
+ if (0 < ctx.err.length || null == root || true === root.isNil) {
279
+ return { ok: false, out: '', findings: [evalFailure(ctx)] }
280
+ }
281
+
282
+ const node: any = anchorAt(root, path)
283
+ if (null == node) {
284
+ return { ok: false, out: '', findings: [noPathFinding(root, path)] }
285
+ }
286
+
287
+ if ('json' === view) {
288
+ // GENERATION CAN FAIL WHERE UNIFICATION DID NOT: `k: integer` is a
289
+ // perfectly good unified document and not a concrete value, so the
290
+ // json view of it is an error, exactly as `aontu file.aon` on the
291
+ // same document is. Under `collect` the failure lands on the
292
+ // context rather than throwing, so it has to be read back — the Go
293
+ // port's Gen returns it as an error and the two must agree.
294
+ const before = ctx.err.length
295
+ const gen = node.gen(ctx)
296
+ if (before < ctx.err.length) {
297
+ const err: any = ctx.err[before]
298
+ return {
299
+ ok: false,
300
+ out: '',
301
+ findings: [finding(
302
+ err?.why ?? 'no_gen',
303
+ pathText(path),
304
+ err?.msg ?? 'The value at this path is not concrete.')],
305
+ }
306
+ }
307
+ return { ok: true, out: exactJSON(gen, 2), findings: [] }
308
+ }
309
+ if ('keys' === view) {
310
+ return { ok: true, out: keyList(node), findings: [] }
311
+ }
312
+ return {
313
+ ok: true,
314
+ out: project(node, view, options.depth ?? Infinity),
315
+ findings: [],
316
+ }
317
+ }
318
+
319
+
320
+ export type WhyReport = {
321
+ ok: boolean
322
+ record?: WhyRecord
323
+ findings: VetFinding[]
324
+ }
325
+
326
+
327
+ // WHY does the value at this path hold? Evaluate with the provenance
328
+ // recorder on, select the node, and answer the ordered contributions
329
+ // that met there — the positive twin of G2's error report.
330
+ //
331
+ // Two evaluations are NOT needed: the recorder rides the one run this
332
+ // call makes. What it costs is site materialisation and one map entry
333
+ // per path met, which an instrumented run pays knowingly.
334
+ export function why(
335
+ src: string, path: string, opts?: QueryOptions): WhyReport {
336
+ const options = opts ?? {}
337
+ const aontu = new Aontu(
338
+ null == options.trust ? undefined : { trust: options.trust })
339
+ const prov = new Provenance()
340
+ const ctx = aontu.ctx({ collect: true, prov })
341
+ const parseOpts = null == options.path ? undefined : { path: options.path }
342
+
343
+ // Parse and unify SEPARATELY, so the parsed tree can be stamped
344
+ // before the fixpoint runs: a contribution is a value the author
345
+ // wrote, and after unification there is no longer any way to tell
346
+ // one from a value the engine minted on the way.
347
+ const parsed: any = aontu.parse(src, parseOpts, ctx)
348
+ if (0 < ctx.err.length || null == parsed) {
349
+ return { ok: false, findings: [evalFailure(ctx)] }
350
+ }
351
+ prov.writtenFrom(parsed)
352
+
353
+ const root: any = aontu.unify(parsed, parseOpts, ctx)
354
+ if (0 < ctx.err.length || null == root || true === root.isNil) {
355
+ return { ok: false, findings: [evalFailure(ctx)] }
356
+ }
357
+
358
+ const node: any = anchorAt(root, path)
359
+ if (null == node) {
360
+ return { ok: false, findings: [noPathFinding(root, path)] }
361
+ }
362
+
363
+ // The value that stands here is a contribution when nothing met
364
+ // (see Provenance.stands): a generator places a value without a
365
+ // meet, and "nothing met at this path" is not an answer to "where
366
+ // did this come from".
367
+ const parts = pathParts(path)
368
+ prov.stands(parts, node)
369
+
370
+ return {
371
+ ok: true,
372
+ record: {
373
+ conjuncts: prov.at(parts),
374
+ path: pathText(path),
375
+ value: node.canon,
376
+ },
377
+ findings: [],
378
+ }
379
+ }
package/src/reach.ts ADDED
@@ -0,0 +1,184 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ // REACHABILITY OVER THE ENTITY 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 entity names 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 entity. 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 entity an address names --- everything before the first dot. A
70
+ // link into `svc/auth.ports.http` reaches `svc/auth`: reachability is
71
+ // between ENTITIES, and the path inside one says which part of it the
72
+ // link arrives at. Same rule as relation.ts's entityOf, and it has to
73
+ // be, or the two verbs would disagree about what an edge connects.
74
+ function entityOf(addr: string): string {
75
+ const dot = addr.indexOf('.')
76
+ return dot < 0 ? addr : addr.slice(0, dot)
77
+ }
78
+
79
+
80
+ function endpointFinding(name: string, known: string[]): VetFinding {
81
+ return {
82
+ code: 'refer_unresolved',
83
+ class: 'reference',
84
+ severity: 'error',
85
+ path: '$',
86
+ // NOT "unreachable". An endpoint that names no entity is a
87
+ // question the document cannot answer, and answering it `no` would
88
+ // report a typo as a fact about the model --- the fail-open shape
89
+ // this review exists to retire.
90
+ message: `${name} names no entity in this document.`,
91
+ sites: [],
92
+ ...(0 === known.length ? {} : {
93
+ note: 'known entities: ' + known.join(', '),
94
+ }),
95
+ }
96
+ }
97
+
98
+
99
+ // The reachability check for one document.
100
+ export function reachCheck(
101
+ src: string, from: string, to: string, opts?: ReachOptions
102
+ ): ReachReport {
103
+ const options = opts ?? {}
104
+ const aontu = new Aontu(
105
+ null == options.trust ? undefined : { trust: options.trust })
106
+ const ctx = aontu.ctx({ collect: true })
107
+ const parseOpts = null == options.path ? undefined : { path: options.path }
108
+ const root: any = aontu.unify(src, parseOpts, ctx)
109
+
110
+ // A document that does not stand up is not a document with an
111
+ // unreachable pair: the errors it already has are the answer.
112
+ if (0 < ctx.err.length || true === root?.isNil) {
113
+ return {
114
+ verdict: 'error',
115
+ errors: [failureFinding(ctx, options.path, root)],
116
+ }
117
+ }
118
+
119
+ const graph = graphOf(root)
120
+ const known = graph.entities.map((e) => e.id).sort(cmpCodePoint)
121
+ const missing = [from, to].filter((n) => !known.includes(n))
122
+ if (0 < missing.length) {
123
+ return {
124
+ verdict: 'error',
125
+ errors: missing.map((n) => endpointFinding(n, known)),
126
+ }
127
+ }
128
+
129
+ // The successor map, restricted to one relation when the caller asked
130
+ // for one. Sorted, so the path the search finds is the same one in
131
+ // both ports.
132
+ const succ = new Map<string, string[]>()
133
+ for (const e of graph.edges) {
134
+ if ('' === e.from ||
135
+ (null != options.relation && options.relation !== e.key)) {
136
+ continue
137
+ }
138
+ const list = succ.get(e.from)
139
+ const dest = entityOf(e.to)
140
+ if (undefined === list) {
141
+ succ.set(e.from, [dest])
142
+ }
143
+ else if (!list.includes(dest)) {
144
+ list.push(dest)
145
+ }
146
+ }
147
+ for (const list of succ.values()) {
148
+ list.sort(cmpCodePoint)
149
+ }
150
+
151
+ // BREADTH-FIRST, so the path reported is a SHORTEST one --- the
152
+ // evidence an operator wants is the tightest chain, not whichever the
153
+ // walk happened to find first --- and, with the successors sorted, a
154
+ // determined one: among shortest paths, the first in code-point order
155
+ // at the first step that distinguishes them.
156
+ const prev = new Map<string, string>()
157
+ const seen = new Set<string>()
158
+ let front: string[] = [from]
159
+ while (0 < front.length) {
160
+ const next: string[] = []
161
+ for (const node of front) {
162
+ for (const dest of succ.get(node) ?? []) {
163
+ if (dest === to) {
164
+ const path = [dest]
165
+ let step = node
166
+ while (step !== from) {
167
+ path.unshift(step)
168
+ step = prev.get(step) as string
169
+ }
170
+ path.unshift(from)
171
+ return { verdict: 'reaches', path }
172
+ }
173
+ if (!seen.has(dest)) {
174
+ seen.add(dest)
175
+ prev.set(dest, node)
176
+ next.push(dest)
177
+ }
178
+ }
179
+ }
180
+ front = next
181
+ }
182
+
183
+ return { verdict: 'unreachable' }
184
+ }