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/lsp.ts CHANGED
@@ -18,8 +18,12 @@
18
18
 
19
19
  import type { Val } from './type'
20
20
 
21
- import { Aontu } from './aontu'
21
+ import { Aontu, VERSION } from './aontu'
22
22
  import { getHint } from './err'
23
+ import { collectNils } from './walk'
24
+ import { collectDeprecations, deprecationMessage } from './utility'
25
+ import { why } from './query'
26
+ import type { WhyConjunct } from './provenance'
23
27
 
24
28
 
25
29
  // LSP DiagnosticSeverity subset.
@@ -28,8 +32,12 @@ const SEVERITY_WARNING = 2
28
32
  const SEVERITY_INFORMATION = 3
29
33
  const SEVERITY_HINT = 4
30
34
 
31
- // Reported to the client in the initialize response.
32
- const LSP_VERSION = '0.1.0'
35
+ // Reported to the client in the initialize response. It is the
36
+ // ENGINE's version, not a number of the server's own: a separately
37
+ // maintained one drifts, and had -- the server answered 0.1.0 against
38
+ // a package at 0.52.1, so a client could not tell which engine it was
39
+ // talking to (status-2026-08-21.md section 10).
40
+ const LSP_VERSION = VERSION
33
41
 
34
42
 
35
43
  // Zero-based line / UTF-16 character offset (LSP Position).
@@ -45,6 +53,9 @@ type Diagnostic = {
45
53
  code?: string
46
54
  source: string
47
55
  message: string
56
+ // LSP DiagnosticTag values; [1] is Unnecessary, [2] is Deprecated —
57
+ // the native tag editors strike through (G3 phase 4).
58
+ tags?: number[]
48
59
  }
49
60
 
50
61
 
@@ -73,9 +84,13 @@ type OutMessage = {
73
84
  // functions, syntax errors) produce diagnostics.
74
85
  function computeDiagnostics(
75
86
  src: string,
76
- opts?: { vars?: Record<string, Val> }
87
+ opts?: { vars?: Record<string, Val>, trust?: any }
77
88
  ): Diagnostic[] {
78
- const aontu = new Aontu()
89
+ // The trust profile (G5, docs/trust.md): the LSP is the
90
+ // highest-exposure surface — merely OPENING a hostile .aon file in an
91
+ // editor performs its reads — so the handler confines evaluation to
92
+ // the workspace root and threads the profile through here.
93
+ const aontu = new Aontu(null == opts?.trust ? {} : { trust: opts.trust })
79
94
 
80
95
  let root: any
81
96
  let ac: any
@@ -92,9 +107,10 @@ function computeDiagnostics(
92
107
  return [parseErrorDiagnostic(err)]
93
108
  }
94
109
 
95
- const nils: any[] = []
110
+ // The walk's `seen` set is reused below to dedup context errors
111
+ // against the nils already found in the tree.
96
112
  const seen = new Set()
97
- walkNils(root, nils, seen)
113
+ const nils: any[] = collectNils(root, seen)
98
114
 
99
115
  // Errors recorded on the context but not present in the tree — e.g. a
100
116
  // budget_passes exhaustion nil, which is about the whole evaluation
@@ -109,34 +125,35 @@ function computeDiagnostics(
109
125
  }
110
126
  }
111
127
 
112
- return nils.map(nilToDiagnostic)
113
- }
114
-
115
-
116
- // Walk a unified Val tree collecting every NilVal exactly once. A NilVal
117
- // in the result always represents an error; valid non-concrete values
118
- // (scalar kinds, refs, conjuncts) are never NilVals.
119
- function walkNils(v: any, out: any[], seen: Set<any>) {
120
- if (null == v || 'object' !== typeof v || true !== v.isVal) return
121
- if (seen.has(v)) return
122
- seen.add(v)
123
-
124
- if (v.isNil) {
125
- out.push(v)
126
- return
127
- }
128
-
129
- const peg = v.peg
130
- if (Array.isArray(peg)) {
131
- for (const c of peg) walkNils(c, out, seen)
132
- }
133
- else if (null != peg && 'object' === typeof peg) {
134
- for (const k in peg) walkNils(peg[k], out, seen)
128
+ const out = nils.map(nilToDiagnostic)
129
+
130
+ // Deprecation tags (G3 phase 4): every sited value carrying the
131
+ // deprecate() record — the declaration and, because the record rides
132
+ // meets and reference clones, every use resolving through it — gets
133
+ // the native Deprecated tag (2) at Hint severity, so editors strike
134
+ // it through without shouting.
135
+ for (const { val } of collectDeprecations(root)) {
136
+ const v: any = val
137
+ if (1 > (v.site?.row ?? -1) || 1 > (v.site?.col ?? -1)) {
138
+ continue
139
+ }
140
+ out.push({
141
+ range: {
142
+ start: { line: v.site.row - 1, character: v.site.col - 1 },
143
+ end: {
144
+ line: v.site.row - 1,
145
+ character: v.site.col - 1 + siteExtent(v),
146
+ },
147
+ },
148
+ severity: 4,
149
+ code: 'deprecated',
150
+ source: 'aontu',
151
+ message: deprecationMessage(v.deprecation),
152
+ tags: [2],
153
+ })
135
154
  }
136
155
 
137
- // Spread constraints live off-peg on Map/List Vals.
138
- const spreadCj = v.spread?.cj
139
- if (spreadCj) walkNils(spreadCj, out, seen)
156
+ return out
140
157
  }
141
158
 
142
159
 
@@ -169,9 +186,39 @@ function nilToDiagnostic(nil: any): Diagnostic {
169
186
 
170
187
 
171
188
  // Length (UTF-16 units, like LSP characters) of the offending value's
172
- // canonical form, used to size the diagnostic range (minimum 1).
189
+ // SOURCE TEXT, used to size the diagnostic range (minimum 1).
173
190
  function labelLength(nil: any): number {
174
- const c = nil.primary?.canon
191
+ return null == nil.primary ? 1 : siteExtent(nil.primary)
192
+ }
193
+
194
+
195
+ // The extent to underline for a value: its SOURCE TEXT's length, with
196
+ // the canon as the fallback and 1 as the floor.
197
+ //
198
+ // CANON IS NOT SOURCE TEXT, which is the whole point of Site.len:
199
+ // `0x1F` has canon `31`, so sizing by canon underlines two characters
200
+ // of a four-character literal — hovering `0x1F` highlighted `0x` and
201
+ // hovering `1F` answered nothing. The Go twin is the same fallback in
202
+ // go/check.go (Problem.Len, ValueSpan.Len), in bytes there because it
203
+ // is added to a byte offset before conversion.
204
+ //
205
+ // The fallback is for a value carrying no stamped span — one propagated
206
+ // onto a result rather than written by a document — where canon is all
207
+ // there is and an approximate underline beats none. A REPORT never
208
+ // guesses this way: vet and why emit len only when it is known, because
209
+ // there a wrong length is a corrupted document rather than a wonky
210
+ // highlight.
211
+ function siteExtent(v: any): number {
212
+ const len = v?.site?.len
213
+ if ('number' === typeof len && len > 0) {
214
+ return len
215
+ }
216
+ // CANON IS READ DEFENSIVELY, as every other reader here does: it is a
217
+ // getter that unifies, and a host-supplied value can throw from it
218
+ // (hover-refuses-bad-input). A hover that cannot measure a value
219
+ // still has to answer for the rest of the document.
220
+ let c = ''
221
+ try { c = v?.canon } catch { c = '' }
175
222
  return 'string' === typeof c && c.length > 0 ? c.length : 1
176
223
  }
177
224
 
@@ -251,10 +298,52 @@ type Hover = { contents: MarkupContent, range?: Range }
251
298
  // the position is not over a value with a known source location. Because
252
299
  // hover reads the *unified* tree, a literal shows its resolved value and
253
300
  // kind (e.g. a reference target resolves to the value it points at).
254
- function computeHover(src: string, position: Position): Hover | null {
301
+ // HOVER PROVENANCE (G7 phase 7) is CONFIG-GATED and off by default:
302
+ // the contributions that met at the hovered path, appended to the
303
+ // value's own hover. Hover already re-unifies the whole document per
304
+ // request, so an editor that asks for this pays a second instrumented
305
+ // evaluation knowingly, and one that does not pays nothing.
306
+ function provenanceMarkdown(
307
+ src: string, path: string[], trust?: any): string {
308
+ if (0 === path.length) {
309
+ return ''
310
+ }
311
+ // A document with an error ELSEWHERE still hovers — the tree the
312
+ // hover walked is there — while `why` refuses it, so the record may
313
+ // be absent for a value the cursor is sitting on.
314
+ const report = why(src, '$.' + path.join('.'), { trust } as any)
315
+ return contributionsMarkdown(report.record?.conjuncts ?? [])
316
+ }
317
+
318
+
319
+ // The contributions as hover markdown. Exported for the direct test
320
+ // (ADR-002): a siteless contribution and a named file are both shapes
321
+ // the record allows and no hover produces, hover evaluating one
322
+ // unnamed document.
323
+ export function contributionsMarkdown(conjuncts: WhyConjunct[]): string {
324
+ if (0 === conjuncts.length) {
325
+ return ''
326
+ }
327
+ return '\n\n---\n\nContributions:\n' + conjuncts.map((c) =>
328
+ '- `' + c.canon + '` — ' + c.role +
329
+ (0 > c.site.row ? '' : ' (' +
330
+ ('' === c.site.file ? '' : c.site.file + ':') +
331
+ c.site.row + ':' + c.site.col + ')')).join('\n')
332
+ }
333
+
334
+
335
+ // HOVER RUNS UNDER THE SAME CAPABILITY AS DIAGNOSTICS. It used to
336
+ // evaluate through `new Aontu()` -- the full system resolver -- BESIDE
337
+ // confined diagnostics in the same server, so a workspace-confined
338
+ // session still resolved an escaping include the moment a cursor rested
339
+ // on it (use-cases/REVIEW.md finding G). One document, two postures, is
340
+ // not a confinement.
341
+ function computeHover(
342
+ src: string, position: Position, provenance?: boolean,
343
+ trust?: any): Hover | null {
255
344
  let root: any
256
345
  try {
257
- root = new Aontu().unify(src, { collect: true })
346
+ root = new Aontu(null == trust ? {} : { trust }).unify(src, { collect: true })
258
347
  }
259
348
  catch {
260
349
  return null
@@ -274,7 +363,12 @@ function computeHover(src: string, position: Position): Hover | null {
274
363
  if (null == best) return null
275
364
 
276
365
  return {
277
- contents: { kind: 'markdown', value: hoverMarkdown(best.val) },
366
+ contents: {
367
+ kind: 'markdown',
368
+ value: hoverMarkdown(best.val) +
369
+ (true === provenance
370
+ ? provenanceMarkdown(src, best.val.path, trust) : ''),
371
+ },
278
372
  range: {
279
373
  start: { line: best.line, character: best.start },
280
374
  end: { line: best.line, character: best.end },
@@ -296,15 +390,24 @@ function collectHoverCandidates(
296
390
  const col = v.site.col
297
391
  let canon = ''
298
392
  try { canon = v.canon } catch { canon = '' }
393
+ // The span to highlight: the value's own source text where it has
394
+ // one, canon otherwise. See siteExtent.
395
+ const span = siteExtent(v)
396
+ const spanSrc = 'string' === typeof v.site?.src ? v.site.src : ''
299
397
  // Hover targets concrete values (scalars, kinds, refs, …), not
300
398
  // containers: a map/list source span is not reliably reconstructable
301
399
  // from a single site, and the same restriction in the Go port keeps
302
400
  // hover behaviour identical across implementations. The walk still
303
401
  // recurses into containers below to reach their leaf values. Canon is
304
402
  // single-line, so its length approximates the on-line source span.
305
- if (row >= 1 && col >= 1 && canon.length > 0 && !canon.includes('\n') &&
403
+ // Single-line is decided by the SOURCE TEXT when there is one: a
404
+ // multi-line token cannot be described by one line's start and end,
405
+ // and canon's newlines are not the token's. Falling back to canon
406
+ // keeps the old test for a value with no stamped span.
407
+ const multiline = '' === spanSrc ? canon.includes('\n') : spanSrc.includes('\n')
408
+ if (row >= 1 && col >= 1 && canon.length > 0 && !multiline &&
306
409
  !v.isMap && !v.isList) {
307
- out.push({ val: v, line: row - 1, start: col - 1, end: col - 1 + canon.length })
410
+ out.push({ val: v, line: row - 1, start: col - 1, end: col - 1 + span })
308
411
  }
309
412
 
310
413
  const peg = v.peg
@@ -358,11 +461,20 @@ type CompletionItem = {
358
461
  const COMPLETION_FUNCTION = 3
359
462
  const COMPLETION_KEYWORD = 14
360
463
 
361
- // The seventeen built-in functions. Kept in sync with the engine by
464
+ // The twenty-eight built-in functions. Kept in sync with the engine by
362
465
  // `lsp.test.ts`, which asserts each is recognised and no others are.
466
+ // The Go port derives its list from the engine's own name set
467
+ // (`BuiltinFuncNames`, go/func.go), which is why a name added there
468
+ // and forgotten here diverges silently — as `id` and `refer` did
469
+ // between G4 phases 1/2 and G8 phase 1.
363
470
  const BUILTIN_FUNCS = [
364
- 'above', 'below', 'close', 'copy', 'hide', 'key', 'lower', 'max',
365
- 'min', 'move', 'neq', 'open', 'path', 'pref', 'super', 'type', 'upper',
471
+ 'above', 'add', 'below', 'close', 'copy', 'deprecate', 'div', 'each',
472
+ 'filter', 'greatest',
473
+ 'hide', 'id', 'key', 'least', 'length', 'lower',
474
+ 'match', 'max', 'min', 'mod', 'move', 'mul', 'must', 'neq', 'open',
475
+ 'pack', 'path', 'pick',
476
+ 'pref', 're', 'refer', 'rem', 'sub', 'sum', 'super', 'type', 'unique',
477
+ 'upper',
366
478
  ]
367
479
 
368
480
  // Scalar-kind and literal keywords.
@@ -371,7 +483,9 @@ const BUILTIN_FUNCS = [
371
483
  const KIND_KEYWORDS = [
372
484
  'string', 'number', 'integer', 'float', 'biginteger', 'bigdecimal', 'boolean',
373
485
  ]
374
- const LITERAL_KEYWORDS = ['true', 'false', 'null', 'top']
486
+ // `_` joins these as of G8 phase 3: it is a literal of the language
487
+ // now, not text.
488
+ const LITERAL_KEYWORDS = ['_', 'true', 'false', 'null', 'top']
375
489
 
376
490
 
377
491
  // Context-free completion: the built-in functions, scalar-kind keywords
@@ -395,11 +509,103 @@ function computeCompletions(): CompletionItem[] {
395
509
  // messages and returns the messages to send back, tracking open document
396
510
  // text and recomputing diagnostics on open/change/close. Not safe for
397
511
  // concurrent use; drive it from a single loop (as the stdio server does).
512
+ // A file:// uri's filesystem path, for the workspace-root confinement.
513
+ // Percent-decoded; a non-file uri (or none) yields undefined.
514
+ //
515
+ // EXPORTED, inline as contributionsMarkdown is: part of the reusable
516
+ // LSP library surface, and the twin of the package-visible uriToPath in
517
+ // go/lsp/handler.go. Anything driving this module with its own
518
+ // transport has to turn a client's uri into a path the same way the
519
+ // confinement does, and the rules below are not guessable from outside.
520
+ //
521
+ // THE DRIVE-LETTER SLASH. A file uri names an absolute path after the
522
+ // authority, so on Windows the standard spelling every editor sends is
523
+ // file:///C:/Users/me/project — three slashes, and the third belongs to
524
+ // the PATH. Stripping only `file://` leaves `/C:/Users/me/project`,
525
+ // which is not a Windows path at all, so the workspace-root
526
+ // confinement below compared real paths against nonsense and an editor
527
+ // on Windows got no confinement it could rely on. Both ports carried
528
+ // the defect identically (go/lsp/handler.go uriToPath), and no test
529
+ // caught it because both ports' tests built the uri as `'file://' +
530
+ // path` — two slashes, which is not what a client sends and which
531
+ // accidentally produced a usable path.
532
+ //
533
+ // The leading slash is dropped only before a DRIVE LETTER, so a POSIX
534
+ // path keeps the root it needs: file:///tmp/x stays /tmp/x.
535
+ export function uriToPath(uri: unknown): string | undefined {
536
+ if ('string' !== typeof uri || !uri.startsWith('file://')) {
537
+ return undefined
538
+ }
539
+ const path = percentDecode(uri.slice('file://'.length))
540
+ // AN EMPTY PATH IS NOT A ROOT. `file://` on its own yields '', and
541
+ // '' is not nullish — so it won the `??` chain below and arrived as
542
+ // `{ include: { root: '' } }`, a confinement root that then resolves
543
+ // against the process working directory: the same client params made
544
+ // the server allow or deny an include depending on where it was
545
+ // started from. The Go twin never had it, because its chain tests
546
+ // `"" != folder` explicitly (go/lsp/handler.go). Answering undefined
547
+ // is what makes the two chains agree.
548
+ if ('' === path) {
549
+ return undefined
550
+ }
551
+ return driveLetterPath(path) ? path.slice(1) : path
552
+ }
553
+
554
+
555
+ // Percent-decoding that CANNOT THROW. `decodeURIComponent` raises a
556
+ // URIError on a malformed escape (`%ZZ`), and this runs on a uri a
557
+ // CLIENT sent — so a stray percent in a workspace path took the
558
+ // exception straight out of the initialize handler, where the Go twin
559
+ // swallowed the same failure and used the raw text
560
+ // (go/lsp/handler.go). Two ports, two behaviours, for an input neither
561
+ // of them controls.
562
+ //
563
+ // The agreement runs the other way for the SECOND way an escape can
564
+ // fail to decode. `%FF` is well-formed and names a raw byte — a
565
+ // perfectly good Linux filename — which Go produced and a JavaScript
566
+ // string cannot hold at all, so the ports derived different workspace
567
+ // roots for a uri a byte-oriented client really sends. Only one
568
+ // direction is reachable from both languages, so Go now declines to
569
+ // unescape a result that is not valid UTF-8 and both ports keep the
570
+ // raw text. An undecodable path is still a path, and refusing to serve
571
+ // a session over it helps nobody.
572
+ function percentDecode(text: string): string {
573
+ try {
574
+ return decodeURIComponent(text)
575
+ }
576
+ catch {
577
+ return text
578
+ }
579
+ }
580
+
581
+
582
+ // Whether p is `/X:…` for a drive letter X — the one shape whose
583
+ // leading slash is uri syntax rather than path. Mirrors the same
584
+ // predicate in go/lsp/handler.go.
585
+ function driveLetterPath(p: string): boolean {
586
+ return 3 <= p.length && '/' === p[0] && ':' === p[2] &&
587
+ /[A-Za-z]/.test(p[1])
588
+ }
589
+
590
+
398
591
  class LspHandler {
399
592
  private docs = new Map<string, string>()
400
593
  private shutdownOK = false
401
594
  private exited = false
402
595
 
596
+ // The trust profile evaluation runs under (G5, docs/trust.md):
597
+ // workspace-root confinement by default, set from the initialize
598
+ // params. An `initializationOptions.aontu.trust.include` of 'system',
599
+ // 'none' or { root } widens or narrows it explicitly. Undefined —
600
+ // no workspace root and no explicit option — falls back to today's
601
+ // unconfined behaviour, which single-file sessions rely on.
602
+ private trust: any = undefined
603
+
604
+ // Hover provenance (G7 phase 7): off unless an editor asks for it
605
+ // with `initializationOptions.aontu.provenance`. It costs a second,
606
+ // instrumented evaluation per hover, which is a cost to opt into.
607
+ private provenance = false
608
+
403
609
  // True once an `exit` notification has been received.
404
610
  get shouldExit(): boolean { return this.exited }
405
611
 
@@ -413,8 +619,34 @@ class LspHandler {
413
619
  // Process one incoming message, returning zero or more to send.
414
620
  handle(msg: Message): OutMessage[] {
415
621
  switch (msg.method) {
416
- case 'initialize':
622
+ case 'initialize': {
623
+ const params = msg.params ?? {}
624
+ this.provenance =
625
+ true === params.initializationOptions?.aontu?.provenance
626
+ const explicit = params.initializationOptions?.aontu?.trust?.include
627
+ if (null != explicit) {
628
+ // An explicit setting wins — validated, and an unrecognised
629
+ // value confines to NOTHING rather than silently widening:
630
+ // deny is the safe reading of a setting the server does not
631
+ // understand. The same rule as the Go handler.
632
+ this.trust =
633
+ 'system' === explicit ? undefined :
634
+ 'none' === explicit ? { include: 'none' } :
635
+ ('string' === typeof explicit?.root && '' !== explicit.root)
636
+ ? { include: { root: explicit.root } } :
637
+ (null != explicit?.mem && 'object' === typeof explicit.mem)
638
+ ? { include: { mem: explicit.mem } } :
639
+ { include: 'none' }
640
+ }
641
+ else {
642
+ const root = uriToPath(params.workspaceFolders?.[0]?.uri)
643
+ ?? uriToPath(params.rootUri)
644
+ ?? (('string' === typeof params.rootPath && '' !== params.rootPath)
645
+ ? params.rootPath : undefined)
646
+ this.trust = null != root ? { include: { root } } : undefined
647
+ }
417
648
  return [{ jsonrpc: '2.0', id: msg.id, result: initializeResult() }]
649
+ }
418
650
 
419
651
  case 'initialized':
420
652
  return []
@@ -455,7 +687,8 @@ class LspHandler {
455
687
  const uri = msg.params?.textDocument?.uri
456
688
  const pos = msg.params?.position
457
689
  const text = null != uri ? this.docs.get(uri) : undefined
458
- const hover = (null != text && null != pos) ? computeHover(text, pos) : null
690
+ const hover = (null != text && null != pos)
691
+ ? computeHover(text, pos, this.provenance, this.trust) : null
459
692
  return [{ jsonrpc: '2.0', id: msg.id, result: hover }]
460
693
  }
461
694
 
@@ -477,7 +710,8 @@ class LspHandler {
477
710
  }
478
711
 
479
712
  private publish(uri: string): OutMessage {
480
- return publishDiagnosticsMsg(uri, computeDiagnostics(this.docs.get(uri) ?? ''))
713
+ return publishDiagnosticsMsg(uri,
714
+ computeDiagnostics(this.docs.get(uri) ?? '', { trust: this.trust }))
481
715
  }
482
716
  } /* node:coverage ignore next 28 */
483
717
 
@@ -0,0 +1,187 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ // Aontu MCP server (stdio).
4
+ //
5
+ // aontu-mcp [--root <dir>]
6
+ //
7
+ // Speaks the Model Context Protocol over stdio: newline-delimited
8
+ // JSON-RPC 2.0, one message per line. This binary is intentionally
9
+ // thin — every tool and every protocol decision lives in the reusable
10
+ // library ./mcp, the same three-layer split the language server uses
11
+ // (docs/lsp.md).
12
+ //
13
+ // NDJSON, not the LSP's Content-Length framing: MCP stdio transport
14
+ // is line-delimited, and a server that invented its own framing would
15
+ // not be reachable by any client.
16
+ //
17
+ // The one startup decision is the PATH CAPABILITY: `--root <dir>`
18
+ // grants the served evaluation the CLI's `--trust root:<dir>` posture
19
+ // — includes resolve confined below the root, and every tool's
20
+ // document arguments accept `<name>Path` file alternatives, confined
21
+ // the same way. Without it the server denies all includes and refuses
22
+ // path arguments (./mcp). The root is realpath'd HERE, once, so the
23
+ // confinement prefix the library compares against is the real
24
+ // directory, not a spelling of it.
25
+
26
+ import { realpathSync, statSync } from 'node:fs'
27
+
28
+ import { handle, parseError } from './mcp'
29
+ import type { McpRequest, McpResponse } from './mcp'
30
+ import { VERSION } from './aontu'
31
+
32
+
33
+ const USAGE = 'aontu-mcp - Aontu MCP server (stdio, NDJSON JSON-RPC)\n' +
34
+ '\n' +
35
+ ' aontu-mcp [--root <dir>]\n' +
36
+ '\n' +
37
+ ' --root <dir> Serve <name>Path file arguments and resolve\n' +
38
+ ' @"..." includes, confined below <dir>\n' +
39
+ ' (realpath-checked). Without it, served evaluation\n' +
40
+ ' denies every include and refuses path arguments.\n'
41
+
42
+
43
+ // The startup arguments. Parsed here rather than in bin/aontu-mcp.js
44
+ // so the parsing is import-testable; unknown options REFUSE rather
45
+ // than warn, because a server whose operator typo'd --root must not
46
+ // come up quietly unconfined.
47
+ export type ServerArgs = {
48
+ root?: string
49
+ help?: boolean
50
+ err?: string
51
+ }
52
+
53
+ export function parseArgs(argv: string[]): ServerArgs {
54
+ let root: string | undefined
55
+ for (let i = 0; i < argv.length; i++) {
56
+ const arg = argv[i]
57
+ if ('-h' === arg || '--help' === arg) {
58
+ return { help: true }
59
+ }
60
+ if ('--root' === arg) {
61
+ const dir = argv[++i]
62
+ if (null == dir) {
63
+ return { err: 'aontu-mcp: --root needs a directory' }
64
+ }
65
+ root = dir
66
+ }
67
+ else {
68
+ return { err: `aontu-mcp: unknown option ${arg} (try --help)` }
69
+ }
70
+ }
71
+ return { root }
72
+ }
73
+
74
+
75
+ // A line-oriented JSON-RPC codec: feed it incoming chunks, and it
76
+ // splits lines, dispatches them, and writes replies. Kept
77
+ // transport-injectable (write/onExit) so the wiring is unit-testable
78
+ // without real stdio, exactly as the LSP's FrameCodec is.
79
+ class LineCodec {
80
+ private buffer = ''
81
+
82
+ constructor(
83
+ private write: (line: string) => void,
84
+ private onExit: (code: number) => void,
85
+ private version: string,
86
+ private root?: string,
87
+ ) { }
88
+
89
+ push(chunk: string | Buffer) {
90
+ this.buffer += chunk.toString()
91
+ for (; ;) {
92
+ const nl = this.buffer.indexOf('\n')
93
+ if (nl < 0) {
94
+ return
95
+ }
96
+ const line = this.buffer.slice(0, nl).trim()
97
+ this.buffer = this.buffer.slice(nl + 1)
98
+ if ('' !== line) {
99
+ this.line(line)
100
+ }
101
+ }
102
+ }
103
+
104
+ end() {
105
+ this.onExit(0)
106
+ }
107
+
108
+ private line(line: string) {
109
+ let msg: McpRequest
110
+ try {
111
+ msg = JSON.parse(line)
112
+ }
113
+ catch {
114
+ this.send(parseError())
115
+ return
116
+ }
117
+ const out = handle(msg, this.version, this.root)
118
+ if (null != out) {
119
+ this.send(out)
120
+ }
121
+ }
122
+
123
+ private send(out: McpResponse) {
124
+ this.write(JSON.stringify(out) + '\n')
125
+ }
126
+ }
127
+
128
+
129
+ // The streams and exit are injectable (defaulting to real stdio) so
130
+ // the full wiring is unit-testable. Returns undefined when the
131
+ // arguments end the run before a codec exists (--help, a bad option,
132
+ // a --root that is not a directory).
133
+ function main(
134
+ stdin: NodeJS.ReadableStream = process.stdin,
135
+ write: (line: string) => void = (line) => void process.stdout.write(line),
136
+ exit: (code: number) => void = (code) => process.exit(code),
137
+ version: string = VERSION,
138
+ argv: string[] = process.argv.slice(2),
139
+ errwrite: (line: string) => void =
140
+ (line) => void process.stderr.write(line),
141
+ ): LineCodec | undefined {
142
+ const args = parseArgs(argv)
143
+ if (true === args.help) {
144
+ write(USAGE)
145
+ exit(0)
146
+ return undefined
147
+ }
148
+ if (null != args.err) {
149
+ errwrite(args.err + '\n')
150
+ exit(2)
151
+ return undefined
152
+ }
153
+
154
+ // FAIL FAST on a root that is not a real directory: every later
155
+ // call would refuse anyway, but a misconfigured server that answers
156
+ // a thousand confusing refusals is worse than one that says so at
157
+ // startup.
158
+ let root: string | undefined
159
+ if (null != args.root) {
160
+ try {
161
+ root = realpathSync(args.root)
162
+ if (!statSync(root).isDirectory()) {
163
+ throw new Error('not a directory')
164
+ }
165
+ }
166
+ catch {
167
+ errwrite(`aontu-mcp: --root ${args.root} is not a directory\n`)
168
+ exit(2)
169
+ return undefined
170
+ }
171
+ }
172
+
173
+ const codec = new LineCodec(write, exit, version, root)
174
+ stdin.on('data', (chunk: Buffer) => codec.push(chunk))
175
+ stdin.on('end', () => codec.end())
176
+ return codec
177
+ } /* node:coverage ignore next 11 */
178
+
179
+
180
+ // No require.main guard here: bin/aontu-mcp.js is the executable entry
181
+ // and calls main() itself, so this module stays import-only.
182
+
183
+
184
+ export {
185
+ LineCodec,
186
+ main,
187
+ }