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/lang.ts CHANGED
@@ -3,6 +3,19 @@
3
3
 
4
4
  // import { performance } from 'node:perf_hooks'
5
5
 
6
+ // Named imports, not `import * as`: the namespace form makes tsc emit
7
+ // the __importStar downlevel helper, whose branches no supported Node
8
+ // takes (the same rule cli.ts records). Aliased because `Path` is
9
+ // already the @tabnas/path plugin below.
10
+ import { existsSync, readFileSync, realpathSync } from 'node:fs'
11
+ import {
12
+ basename as pathBasename,
13
+ dirname as pathDirname,
14
+ join as pathJoin,
15
+ resolve as pathResolve,
16
+ sep as pathSep,
17
+ } from 'node:path'
18
+
6
19
  import {
7
20
  Jsonic,
8
21
  Tabnas,
@@ -14,6 +27,20 @@ import {
14
27
  } from '@tabnas/jsonic'
15
28
 
16
29
 
30
+ // THE CONFIG-FORMAT READERS (ADR-012). Each is a jsonic plugin for one
31
+ // format, so an included `.toml` or `.yaml` is parsed by a real parser
32
+ // for that format rather than guessed at by this one. `@tabnas/json` is
33
+ // the strict RFC 8259 reader, used for `.json` and `.jsonld`.
34
+ import { funcSig } from './sig'
35
+ import type { FuncSig } from './sig'
36
+
37
+ import { make as makeJsonParser } from '@tabnas/json'
38
+ import { Toml } from '@tabnas/toml'
39
+ import { Jsonc } from '@tabnas/jsonc'
40
+ import { Json5 } from '@tabnas/json5'
41
+ import { Yaml } from '@tabnas/yaml'
42
+ import { Ini } from '@tabnas/ini'
43
+
17
44
  import { Debug } from '@tabnas/debug'
18
45
 
19
46
  import {
@@ -34,6 +61,21 @@ import {
34
61
  makeMemResolver
35
62
  } from '@tabnas/multisource/resolver/mem'
36
63
 
64
+ // The Aontu-source processor, TAKEN RATHER THAN ALIASED. The obvious
65
+ // spelling is the alias `aon: 'jsonic'`, which multisource resolves
66
+ // through its own processor map -- but `jsonic` is a FORMAT NAME in
67
+ // the include table now, so that alias resolved to the plain-jsonic
68
+ // DATA reader and every `.aon` include was suddenly parsed without the
69
+ // language in it. Naming the function leaves nothing to collide with.
70
+ import {
71
+ makeJsonicProcessor,
72
+ } from '@tabnas/multisource/processor/jsonic'
73
+
74
+ import { STD_SOURCES } from './std'
75
+ import {
76
+ parseModuleRef, resolveModule, modCacheDir, MODULE_REFUSAL_CODES,
77
+ } from './mod'
78
+
37
79
  import {
38
80
  Expr,
39
81
  Op,
@@ -96,8 +138,23 @@ import { CopyFuncVal } from './val/CopyFuncVal'
96
138
  import { KeyFuncVal } from './val/KeyFuncVal'
97
139
  import { TypeFuncVal } from './val/TypeFuncVal'
98
140
  import { HideFuncVal } from './val/HideFuncVal'
141
+ import { DeprecateFuncVal } from './val/DeprecateFuncVal'
142
+ import { ReferFuncVal, RelFuncVal } from './val/ReferFuncVal'
143
+ import { AcyclicFuncVal, InverseFuncVal } from './val/GraphAtomVal'
144
+ import { PackFuncVal } from './val/PackFuncVal'
145
+ import { EachFuncVal } from './val/EachFuncVal'
146
+ import { FilterFuncVal } from './val/FilterFuncVal'
147
+ import { MatchFuncVal } from './val/MatchFuncVal'
148
+ import {
149
+ AddFuncVal, SubFuncVal, MulFuncVal, DivFuncVal, ModFuncVal, RemFuncVal,
150
+ } from './val/ArithFuncVal'
151
+ import {
152
+ SumFuncVal, LeastFuncVal, GreatestFuncVal, PickFuncVal, JoinFuncVal,
153
+ } from './val/AggFuncVal'
154
+ import { PlaceVal } from './val/PlaceVal'
99
155
  import { MoveFuncVal } from './val/MoveFuncVal'
100
156
  import { PathFuncVal } from './val/PathFuncVal'
157
+ import { MapFuncVal, ListFuncVal } from './val/ContainerKindVal'
101
158
  import { PrefFuncVal } from './val/PrefFuncVal'
102
159
  import { CloseFuncVal } from './val/CloseFuncVal'
103
160
  import { OpenFuncVal } from './val/OpenFuncVal'
@@ -158,6 +215,13 @@ const CC_0 = 48
158
215
  const CC_d = 100
159
216
  const CC_D = 68
160
217
 
218
+ // THE ALIAS SIGIL. `%` is part of an alias's name, so the name is one
219
+ // lexeme wherever it appears and its meaning is decided by position:
220
+ // a BINDING in key position (`%uint8: …` declares), a USE in value
221
+ // position (`listen: %uint8` refers). docs/design/ALIASES.0.md §4.
222
+ const CC_PCT = 37
223
+ const ALIAS_RE = /^%[A-Za-z_][A-Za-z0-9_]*/
224
+
161
225
  let AontuJsonic: Plugin = function AontuLang(jsonic: Jsonic) {
162
226
 
163
227
  jsonic.use(asPlugin(Path))
@@ -169,6 +233,38 @@ let AontuJsonic: Plugin = function AontuLang(jsonic: Jsonic) {
169
233
  let dotRef = (r: Rule, ctx: JsonicContext, terms: any, prefix: boolean) => {
170
234
  terms = dropUnfilled(terms)
171
235
  if (0 === terms.length) return incompleteNil(r, ctx)
236
+
237
+ // AN ALIAS IS NOT A PATH SEGMENT. `$.%foo` is refused: the alias
238
+ // namespace and the path namespace are disjoint, and an alias is
239
+ // reached by writing `%foo` and only that.
240
+ //
241
+ // The engine spells an alias reference AS a root reference to the
242
+ // declaration -- which is what gives it order independence and a
243
+ // cycle check shared with paths -- but that is an implementation of
244
+ // the name, not a second way to write it. Left writable, the two
245
+ // spellings would drift apart the moment aliases stop being
246
+ // file-shaped, and `$.%b` inside an included file would reach the
247
+ // INCLUDER's `%b` rather than its own, which is exactly the
248
+ // cross-file capture the sigil exists to prevent.
249
+ // `%foo` lexes to the reference itself, so in `$.%foo` it arrives
250
+ // as a TERM rather than as a string segment -- both shapes are
251
+ // checked, since a quoted `$."%foo"` would arrive as the string.
252
+ // Terms here are always Vals -- dropUnfilled has removed the
253
+ // nulls, and the dot rules never hand over a raw string -- so the
254
+ // shapes are exactly three: a RefVal (peg is the segment array), a
255
+ // StringVal (peg is the segment), and anything else (a numeric or
256
+ // exact segment, which cannot be an alias name).
257
+ for (const t of terms) {
258
+ const segs: any[] =
259
+ Array.isArray(t.peg) ? t.peg :
260
+ ('string' === typeof t.peg ? [t.peg] : [])
261
+ for (const seg of segs) {
262
+ if ('string' === typeof seg && ALIAS_RE.test(seg)) {
263
+ return addsite(new NilVal({ why: 'alias_in_path' }), r, ctx)
264
+ }
265
+ }
266
+ }
267
+
172
268
  return addsite(new RefVal({ peg: terms, prefix }), r, ctx)
173
269
  }
174
270
 
@@ -243,9 +339,47 @@ let AontuJsonic: Plugin = function AontuLang(jsonic: Jsonic) {
243
339
  check: (lex: any) => {
244
340
  // Guard first, on char codes: this hook runs at every text
245
341
  // position, and the common case (any run that cannot be a `0d`
246
- // literal) must cost two char reads and no allocation.
342
+ // literal or an alias) must cost two char reads and no
343
+ // allocation.
247
344
  const pnt = lex.pnt
248
345
  const src = lex.src
346
+
347
+ // AN ALIAS NAME IS CLAIMED WHOLE, for the same reason the `0d`
348
+ // run below is: the text matcher's ender regexp would otherwise
349
+ // carve `%uint8` at the `%` and emit the sigil as its own token,
350
+ // leaving a bare `uint8` behind -- which is exactly the capture
351
+ // the sigil exists to prevent. Claiming it here, before that
352
+ // ender runs, keeps the name one lexeme.
353
+ //
354
+ // The token's SOURCE is the whole `%name`, which is what makes
355
+ // the same lexeme work in both positions: jsonic keys a pair by
356
+ // the token's source text (`0d1: 5` yields the key `0d1`), so a
357
+ // declaration reads as the key `%uint8`, while a value position
358
+ // calls the function below and gets the reference.
359
+ if (CC_PCT === src.charCodeAt(pnt.sI)) {
360
+ const ares = ALIAS_RE.exec(lex.refwd())
361
+ if (null == ares) {
362
+ return undefined
363
+ }
364
+ const asrc = ares[0]
365
+ const atkn = lex.token(
366
+ '#VL',
367
+ // AN ALIAS REFERENCE IS A PATH REFERENCE. `%uint8` is
368
+ // `$.%uint8`: root-absolute, one segment, spelled with the
369
+ // sigil the declaration is spelled with. Everything the
370
+ // design asks of it -- order independence, alias-of-alias,
371
+ // redeclaration unifying, cycle refusal spanning both
372
+ // namespaces -- is then the reference machinery already in
373
+ // the language, not a second resolver beside it.
374
+ (r: Rule, ctx: JsonicContext) =>
375
+ addsite(new RefVal({ peg: [asrc], absolute: true }), r, ctx),
376
+ asrc,
377
+ pnt)
378
+ pnt.sI += asrc.length
379
+ pnt.cI += asrc.length
380
+ return { done: true, token: atkn }
381
+ }
382
+
249
383
  if (CC_0 !== src.charCodeAt(pnt.sI)) {
250
384
  return undefined
251
385
  }
@@ -287,6 +421,16 @@ let AontuJsonic: Plugin = function AontuLang(jsonic: Jsonic) {
287
421
  v.site.row = null == r.o0 ? -1 : r.o0.rI
288
422
  v.site.col = null == r.o0 ? -1 : r.o0.cI
289
423
  v.site.url = ctx.meta.multisource ? ctx.meta.multisource.path : ''
424
+ // The source text, from the SAME token the row and column above
425
+ // come from. jsonic has carried it all along; not reading it is
426
+ // what left a site uneditable (ts/src/site.ts).
427
+ //
428
+ // A TOKEN WITH NO TEXT IS NO SPAN, in both ports, and the extent is
429
+ // DERIVED from the text rather than read from the token's own `len`
430
+ // — so the Go twin, whose token has no len field, computes the
431
+ // identical number with utf16Len and nothing has to be kept in step.
432
+ v.site.src = null == r.o0 ? '' : r.o0.src
433
+ v.site.len = '' === v.site.src ? -1 : v.site.src.length
290
434
  // A keyed rule always carries a path array; a keyless one has none.
291
435
  v.path = r.k ? [...r.k.path] : []
292
436
 
@@ -365,6 +509,14 @@ help isolate the syntax error.`,
365
509
 
366
510
  // TODO: FIX: need a TOP instance to hold path
367
511
  'top': { val: () => top() },
512
+
513
+ // G8 phase 3: the placeholder. A BARE `_` is the hole; `"_"`
514
+ // quoted, and any longer bare word containing it, stay text.
515
+ // Reserving it is a breaking change, pinned by place.tsv.
516
+ '_': {
517
+ val: (r: Rule, ctx: JsonicContext) =>
518
+ addsite(new PlaceVal({}), r, ctx)
519
+ },
368
520
  }
369
521
  },
370
522
 
@@ -411,6 +563,14 @@ help isolate the syntax error.`,
411
563
  move: MoveFuncVal,
412
564
  path: PathFuncVal,
413
565
  pref: PrefFuncVal,
566
+
567
+ // First-class paths and the container kinds
568
+ // (docs/design/PATHS.0.md). `path(p)` CAPTURES a path as a value;
569
+ // `path()`, `map()` and `list()` are kinds -- the vacuous
570
+ // constructor call admits its values and defaults to nothing,
571
+ // where the container LITERALS `{}`/`[]` default to empty.
572
+ map: MapFuncVal,
573
+ list: ListFuncVal,
414
574
  close: CloseFuncVal,
415
575
  open: OpenFuncVal,
416
576
  super: SuperFuncVal,
@@ -441,6 +601,72 @@ help isolate the syntax error.`,
441
601
  // reported with the author's own message, never simplified and
442
602
  // never consulted for emptiness or subsumption.
443
603
  must: MustConstraintVal,
604
+
605
+ // G3 phase 4: the deprecation mark. Unification-transparent; the
606
+ // record rides the result (Val.deprecation) and canon renders the
607
+ // call back (canonRiders).
608
+ deprecate: DeprecateFuncVal,
609
+
610
+ // G4 phase 2: the checked, typed, LINK-shaped reference. A
611
+ // constraint on a string field: the string must be a TREE ADDRESS
612
+ // (`$.a.b` or `.b`), the address must resolve, and the optional
613
+ // argument flows INTO the target. The field keeps the string.
614
+ refer: ReferFuncVal,
615
+ rel: RelFuncVal,
616
+
617
+ // RELATIONS P2 (docs/design/RELATIONS.0.md §3.3): the graph
618
+ // atoms, conjoined at the field whose key is the predicate they
619
+ // govern. Lattice-inert; the verdict lands at generation.
620
+ acyclic: AcyclicFuncVal,
621
+ inverse: InverseFuncVal,
622
+
623
+ // G8 phase 1: the generation combinators. `pack` makes one keyed
624
+ // child per child of its data, `each` one list element; both clone
625
+ // their template per destination exactly as a spread does, and both
626
+ // wait for the model to settle before they fire (the staging rule,
627
+ // G8 phase 0).
628
+ pack: PackFuncVal,
629
+ each: EachFuncVal,
630
+
631
+ // G8 phase 2: selection. `filter` keeps the children of a bag that
632
+ // unify with a condition; `match` picks the first arm whose
633
+ // pattern the scrutinee unifies with. Both select by
634
+ // UNIFIABILITY, tried in trial mode, so neither adds a predicate
635
+ // language to the one the lattice already is.
636
+ filter: FilterFuncVal,
637
+ match: MatchFuncVal,
638
+
639
+ // The arithmetic family (the review's finding I). Maths beyond `+`
640
+ // arrives as FUNCTIONS — `-` `*` `/` `%` stay reserved — and the
641
+ // family is numeric where the operator is polymorphic, which is
642
+ // what makes `add` more than a second spelling of `+`: it refuses
643
+ // the string concatenation that silently answers `"500m" + "500m"`.
644
+ // Every rule they obey is in ts/src/val/arith.ts.
645
+ add: AddFuncVal,
646
+ sub: SubFuncVal,
647
+ mul: MulFuncVal,
648
+ div: DivFuncVal,
649
+ mod: ModFuncVal,
650
+ rem: RemFuncVal,
651
+
652
+ // Aggregation over a finite, settled bag (the review's finding I).
653
+ // `least` and `greatest` rather than min and max, which are already
654
+ // the atoms for a lower and an upper BOUND -- an aggregate over a
655
+ // set and a bound on a value must not share a spelling.
656
+ sum: SumFuncVal,
657
+ least: LeastFuncVal,
658
+ greatest: GreatestFuncVal,
659
+
660
+ // Projection, which is what lets the aggregates reach a bag of
661
+ // RECORDS: `sum(pick($.lines, amountCents))`. Not a clever `each`
662
+ // template -- `each` MEETS each child, and a meet cannot select.
663
+ pick: PickFuncVal,
664
+
665
+ // G9 phase 2: the fold to a STRING. `sum` folds with `add`; this
666
+ // folds with `+`, so it inherits the one number-to-text rule and
667
+ // the language does not grow a second. It is the primitive that
668
+ // turns a bag of computed lines into a file.
669
+ join: JoinFuncVal,
444
670
  }
445
671
 
446
672
 
@@ -454,6 +680,74 @@ help isolate the syntax error.`,
454
680
  const incompleteNil = (r: Rule, ctx: JsonicContext) =>
455
681
  addsite(new NilVal({ why: 'incomplete_expression' }), r, ctx)
456
682
 
683
+ // Build a call from a NAME and the argument terms as the author
684
+ // wrote them: the arity check, the comma-group rule and the
685
+ // raw-value conversion, stated once.
686
+ const buildCall = (r: Rule, ctx: JsonicContext,
687
+ fname: string, argterms: any[]): any => {
688
+ const funcval = funcMap[fname]
689
+
690
+ // Arity is known for every built-in, so a surplus or missing
691
+ // argument is a mistake in the SOURCE, refused here where the
692
+ // author can see it (issue #51). It was previously left to each
693
+ // function to notice or not: the two ports disagreed on `upper()`
694
+ // and on `close()`, and `min(1,2)` noticed nothing at all -- it
695
+ // built a constraint that merely refused to generate later, with a
696
+ // message about the map rather than about the call.
697
+ //
698
+ // Counted BEFORE the rawToVal pass below, which is what makes the
699
+ // count possible: a comma group arrives as a RAW array and a
700
+ // written list literal as a ListVal, and rawToVal turns the first
701
+ // into the second.
702
+ const arity = funcArity[fname]
703
+ if (null != arity) {
704
+ const got = writtenArgCount(argterms)
705
+ if (got < arity[0] || (-1 !== arity[1] && got > arity[1])) {
706
+ // details is assigned AFTER construction: the NilVal
707
+ // constructor does not read it from its spec (only NilVal.make
708
+ // does), so passing it in the spec left the hint's
709
+ // {func}/{want}/{got} placeholders un-injected and printed
710
+ // literally.
711
+ const nil: any = new NilVal({ why: 'func_arity' })
712
+ nil.details = {
713
+ func: fname,
714
+ want: arityText(arity[0], arity[1]),
715
+ got: '' + got,
716
+ }
717
+ return addsite(nil, r, ctx)
718
+ }
719
+ }
720
+
721
+ // rawToVal EVERY argument. A degenerate expression can hand this
722
+ // handler raw parse values rather than Vals -- `pref(1-3)` arrives
723
+ // as the plain numbers 1 and -3 -- and a func's peg is unified
724
+ // element by element, so a raw one reached `arg.unify(...)` and
725
+ // threw. The unifier's catch-all turned that into an `internal`
726
+ // verdict: a crash reported as a unification result (issue #49).
727
+ // The Go port has always converted here (asVal in evaluate).
728
+ // A comma group is ONE raw-array term (see writtenArgCount).
729
+ // For a function whose arguments are distinct POSITIONS —
730
+ // deprecate's value and record, pack's and each's data and template
731
+ // — the group is expanded back into them here, while a written list
732
+ // literal, already a ListVal, stays one argument. The constraint
733
+ // atoms make the same move in their own constructor (atomArgs,
734
+ // ConstraintVal.ts), which is why they are not in this set:
735
+ // `neq(1,2)` is one argument LIST, not two positions, and expanding
736
+ // it here would take the list away from the code that reads it.
737
+ let terms = argterms
738
+ if (true === POSITIONAL_ARG_FUNCS[fname] && 1 === terms.length &&
739
+ Array.isArray(terms[0])) {
740
+ terms = terms[0]
741
+ }
742
+ const args = terms.map(rawToVal)
743
+ const val: any = null == funcval ?
744
+ new NilVal({ why: 'unknown_function' }) :
745
+ new funcval({ peg: args })
746
+
747
+ return val
748
+ }
749
+
750
+
457
751
  let opmap: any = {
458
752
  'conjunct-infix': (r: Rule, ctx: JsonicContext, _op: Op, terms: any) =>
459
753
  addsite(new ConjunctVal({ peg: dropUnfilled(terms) }), r, ctx),
@@ -472,11 +766,50 @@ help isolate the syntax error.`,
472
766
 
473
767
  'star-prefix': (r: Rule, ctx: JsonicContext, _op: Op, terms: any) => {
474
768
  if (null == terms[0]) return incompleteNil(r, ctx)
769
+
770
+ // A PREFERENCE MARKS A VALUE, AND A BARE KEY IS NOT ONE.
771
+ // `*a: 1` has no braces, so the prefix took the whole IMPLICIT
772
+ // map as its operand and the document silently became
773
+ // `*{"a":1}` -- `*a: 1, b: 2` became a one-element LIST, losing
774
+ // `b` outright. Neither is anything the author wrote.
775
+ //
776
+ // The accident is confined to the first position of the implicit
777
+ // top-level map, which is the only place no brace has yet
778
+ // committed the rule to a map: `{*a: 1}` and `a: 1, *b: 2` are
779
+ // ALREADY parse errors. This makes the third spelling agree with
780
+ // them rather than inventing a meaning for it.
781
+ //
782
+ // A BRACED operand is untouched, and that is the whole of the
783
+ // distinction: `*{x:1}` and `*[1]` are the real spelling, they
784
+ // are what `*{x:1} | *{y:2}` needs, and the shared spec pins them
785
+ // (11 rows). The open token's own source text is what separates
786
+ // the two -- `{` or `[` for a braced bag, the first key or
787
+ // element for an implicit one.
788
+ const bag: any = terms[0]
789
+ if ((bag.isMap && '{' !== bag.site.src) ||
790
+ (bag.isList && '[' !== bag.site.src)) {
791
+ return addsite(new NilVal({ why: 'pref_implicit_bag' }), r, ctx)
792
+ }
793
+
475
794
  return addsite(new PrefVal({ peg: terms[0] }), r, ctx)
476
795
  },
477
796
 
478
797
  'dollar-prefix': (r: Rule, ctx: JsonicContext, _op: Op, terms: any) => {
479
798
  if (null == terms[0]) return incompleteNil(r, ctx)
799
+ // A refusal from the dot rule below (an alias used as a path
800
+ // segment) rides straight through: wrapping it in a VarVal would
801
+ // replace `alias_in_path` with a var whose peg is a nil.
802
+ if (terms[0]?.isNil) {
803
+ return terms[0]
804
+ }
805
+ // `$%foo` -- the sigil directly after the root -- reaches here
806
+ // as the alias reference rather than through the dot rule, and
807
+ // is refused for the same reason.
808
+ if (terms[0] instanceof RefVal &&
809
+ terms[0].peg.some((seg: any) =>
810
+ 'string' === typeof seg && ALIAS_RE.test(seg))) {
811
+ return addsite(new NilVal({ why: 'alias_in_path' }), r, ctx)
812
+ }
480
813
  // $.a.b absolute path
481
814
  if (terms[0] instanceof RefVal) {
482
815
  terms[0].absolute = true
@@ -538,50 +871,7 @@ help isolate the syntax error.`,
538
871
  const fname = terms[0]
539
872
 
540
873
  if ('' !== fname) {
541
- const funcval = funcMap[fname]
542
- // Arity is known for every built-in, so a surplus or missing
543
- // argument is a mistake in the SOURCE, refused here where the
544
- // author can see it (issue #51). It was previously left to each
545
- // function to notice or not: the two ports disagreed on `upper()`
546
- // and on `close()`, and `min(1,2)` noticed nothing at all -- it
547
- // built a constraint that merely refused to generate later, with
548
- // a message about the map rather than about the call.
549
- //
550
- // Counted BEFORE the rawToVal pass below, which is what makes the
551
- // count possible: a comma group arrives as a RAW array and a
552
- // written list literal as a ListVal, and rawToVal turns the first
553
- // into the second.
554
- const arity = funcArity[fname]
555
- if (null != arity) {
556
- const got = writtenArgCount(terms.slice(1))
557
- if (got < arity[0] || (-1 !== arity[1] && got > arity[1])) {
558
- // details is assigned AFTER construction: the NilVal
559
- // constructor does not read it from its spec (only
560
- // NilVal.make does), so passing it in the spec left the
561
- // hint's {func}/{want}/{got} placeholders un-injected and
562
- // printed literally.
563
- const nil: any = new NilVal({ why: 'func_arity' })
564
- nil.details = {
565
- func: fname,
566
- want: arityText(arity[0], arity[1]),
567
- got: '' + got,
568
- }
569
- return addsite(nil, r, ctx)
570
- }
571
- }
572
- // rawToVal EVERY argument. A degenerate expression can hand this
573
- // handler raw parse values rather than Vals -- `pref(1-3)` arrives
574
- // as the plain numbers 1 and -3 -- and a func's peg is unified
575
- // element by element, so a raw one reached `arg.unify(...)` and
576
- // threw. The unifier's catch-all turned that into an `internal`
577
- // verdict: a crash reported as a unification result (issue #49).
578
- // The Go port has always converted here (asVal in evaluate).
579
- const args = terms.slice(1).map(rawToVal)
580
- val = null == funcval ?
581
- new NilVal({ why: 'unknown_function' }) :
582
- new funcval({
583
- peg: args
584
- })
874
+ val = buildCall(r, ctx, fname, terms.slice(1))
585
875
  }
586
876
  // `a:()` — grouping parens with nothing inside.
587
877
  if (null == val) return incompleteNil(r, ctx)
@@ -708,6 +998,8 @@ help isolate the syntax error.`,
708
998
 
709
999
  const QM = jsonic.token.QM
710
1000
 
1001
+ const VL = jsonic.token.VL
1002
+
711
1003
  const OPTKEY = [TX, ST, NR]
712
1004
 
713
1005
 
@@ -818,6 +1110,13 @@ help isolate the syntax error.`,
818
1110
  valnode.site.row = st.rI
819
1111
  valnode.site.col = st.cI
820
1112
  valnode.site.url = ctx.meta.multisource && ctx.meta.multisource.path
1113
+ // No `?? ''` and no empty-text arm here: this branch runs only
1114
+ // for a rule that HAS an open token, and a token that opens a
1115
+ // value always carries text — the coverage gate refuses both
1116
+ // guards as dead. The unset case is the one above, where r.o0
1117
+ // itself can be absent.
1118
+ valnode.site.src = st.src
1119
+ valnode.site.len = st.src.length
821
1120
  }
822
1121
  // else { ERROR? }
823
1122
 
@@ -842,6 +1141,7 @@ help isolate the syntax error.`,
842
1141
 
843
1142
  .bc((r: Rule, ctx: JsonicContext) => {
844
1143
  const optionalKeys = r.u.aontu_optional_keys ?? []
1144
+ const aliasKeys = r.u.aontu_alias_keys ?? []
845
1145
 
846
1146
  let mo = r.node
847
1147
 
@@ -908,6 +1208,7 @@ help isolate the syntax error.`,
908
1208
  // TODO: needs addpath?
909
1209
  let mopv = new MapVal({ peg: mop })
910
1210
  mopv.optionalKeys = optionalKeys
1211
+ mopv.aliasKeys = aliasKeys
911
1212
 
912
1213
  r.node =
913
1214
  addsite(new ConjunctVal({ peg: [mopv, ...mo.___merge] }), r, ctx)
@@ -915,6 +1216,7 @@ help isolate the syntax error.`,
915
1216
  else {
916
1217
  r.node = addsite(new MapVal({ peg: mo }), r, ctx)
917
1218
  r.node.optionalKeys = optionalKeys
1219
+ r.node.aliasKeys = aliasKeys
918
1220
  }
919
1221
 
920
1222
  return undefined
@@ -1065,6 +1367,33 @@ help isolate the syntax error.`,
1065
1367
  .bc((rule: Rule) => {
1066
1368
  // TRAVERSE PARENTS TO GET PATH
1067
1369
 
1370
+ // A DECLARATION IS A PAIR WHOSE KEY IS AN ALIAS NAME. The lexer
1371
+ // claims `%name` whole and hands it over as a #VL token whose
1372
+ // SOURCE is the name, so the key TEXT alone cannot be the test:
1373
+ // a quoted `"%a": 1` is an ordinary key that merely starts with
1374
+ // the sigil, and erasing that would be wrong. The token is what
1375
+ // separates them.
1376
+ //
1377
+ // Recorded on the enclosing map, never on the value, and that is
1378
+ // the point: a reference COPIES the value it resolves to, so a
1379
+ // mark riding the value would erase the referring field too.
1380
+ // Being a property of the map is also what carries it through a
1381
+ // meet, the way optional keys are carried.
1382
+ const ktkn: any = rule.o0
1383
+ if (null != ktkn && VL === ktkn.tin && ALIAS_RE.test('' + ktkn.src)) {
1384
+ const holder: any = rule.parent
1385
+ const aname = '' + ktkn.src
1386
+
1387
+ // Always recorded here; whether the map is ALLOWED to carry
1388
+ // declarations is decided on the VALUE (MapVal.unify), not at
1389
+ // the parse. The parse cannot see it: an INCLUDED file's
1390
+ // declarations are at the root of their own text, and only
1391
+ // once the loaded map is placed does it become apparent that
1392
+ // root is not the document's.
1393
+ holder.u.aontu_alias_keys = (holder.u.aontu_alias_keys || [])
1394
+ holder.u.aontu_alias_keys.push(aname)
1395
+ }
1396
+
1068
1397
  if (rule.u.spread) {
1069
1398
  rule.node[SPREAD] =
1070
1399
  (rule.node[SPREAD] || { o: rule.o0.src, v: [] })
@@ -1110,7 +1439,10 @@ help isolate the syntax error.`,
1110
1439
  s: [QM, CL],
1111
1440
  c: (r) => r.prev.u.aontu_optional,
1112
1441
  p: 'val',
1113
- u: { spread: true, done: true, list: true, pair: true },
1442
+ u: {
1443
+ spread: true, done: true, list: true, pair: true,
1444
+ aontu_optional_elem: true,
1445
+ },
1114
1446
  a: (r) => {
1115
1447
  pairkey(r.prev)
1116
1448
  r.u.key = r.prev.u.key
@@ -1119,18 +1451,17 @@ help isolate the syntax error.`,
1119
1451
  g: 'aontu-optional-elem'
1120
1452
  },
1121
1453
 
1122
- // A PLAIN pair in list position, `[k:v]`. It contributes no
1123
- // element either -- a key:value pair is simply not a list element,
1124
- // which is the rule the optional form above already followed, and
1125
- // the two spellings must not disagree (issue #40).
1126
- //
1127
- // It needed an alt of its own because only a NON-NUMERIC key was
1128
- // already inert: jsonic writes the pair at `node[key]`, and the
1129
- // node is an array, so `[x:1]` set a property that never showed up
1130
- // (`length` stays 0) while `[0:1]` set an INDEX and became an
1131
- // element -- `[1:2]` even filling the gap with a null. That is the
1132
- // shape of a JavaScript array, not a decision about the language,
1133
- // and it made the two ports disagree on generate as well as canon.
1454
+ // A PLAIN pair in list position IS A SINGLE-KEY MAP ELEMENT:
1455
+ // `[a:1, b:2]` is `[{a:1}, {b:2}]` (the rule @tabnas/jsonic
1456
+ // spells as `list.pair`). This REVERSES issue #40's "a pair is
1457
+ // not an element": that rule was chosen because jsonic wrote
1458
+ // the pair at `node[key]` -- an array PROPERTY that never
1459
+ // showed up for a text key and an INDEX for a numeric one --
1460
+ // and inert beat that incoherence. But inert was itself a
1461
+ // silent drop: `x: [a:1, b:2]` evaluated to `x: []`, the
1462
+ // author's data gone at exit 0. The element is built in the
1463
+ // bc below, where the value is already a Val; the snapshot
1464
+ // still neutralises jsonic's raw slot write first.
1134
1465
  {
1135
1466
  s: [OPTKEY, CL], p: 'val',
1136
1467
  u: { spread: true, done: true, list: true, pair: true },
@@ -1143,17 +1474,109 @@ help isolate the syntax error.`,
1143
1474
  ])
1144
1475
 
1145
1476
 
1146
- .bc((rule: Rule) => {
1477
+ // NOTE: manually adjust path - the twin of the `pair` rule's hook
1478
+ // above, and for the same reason, one layer down.
1479
+ //
1480
+ // Every alt above contributes NO element: a `&:` spread is a
1481
+ // constraint on the elements, and a `k:v` pair in list position is
1482
+ // simply not one (the `aontu-plain-pair-elem` note above). The array
1483
+ // slot they briefly occupy is already given back by
1484
+ // restorePairSlot. The PATH index was not: @tabnas/path's
1485
+ // `@elem-ao` increments `r.k.index` for every elem rule it sees, so
1486
+ // each of these stole an index and every later element's path was
1487
+ // one too high — `[&: integer, 10, 20, "bad"]` reported the bad
1488
+ // value at `$.l.3` while `aontu get $.l.2` returned it, and on a
1489
+ // one-element list the path pointed off the end. Generation was
1490
+ // never wrong, which is why nothing caught it: the array is right
1491
+ // and only the labels on it were shifted (BUGS.md 44).
1492
+ //
1493
+ // Rewinding here rather than in the plugin keeps the plugin's rule
1494
+ // ("in an array, the path property is the element index") true —
1495
+ // these alts are the aontu-specific exceptions to what counts as an
1496
+ // element, so the correction belongs with the grammar that
1497
+ // introduces them. The child is re-pathed because the plugin has
1498
+ // already stamped it with the index being given back: a spread
1499
+ // takes the `'&'` segment its map twin takes, and a pair takes its
1500
+ // key, as a map entry would.
1501
+ .ao((r) => {
1502
+ // A pair IS an element now, so it keeps the index @tabnas/path
1503
+ // gave it, and its VALUE is pathed through both the index and
1504
+ // the key (`[a: $.nope]` fails at $.l.0.a). Only the `&:`
1505
+ // spread still contributes no element and gives its index back
1506
+ // (BUGS.md 44).
1507
+ if (0 < r.d && r.u.spread && !r.u.pair) {
1508
+ r.k.index = r.k.index - 1
1509
+
1510
+ const seg = '&'
1511
+ r.child.k.path = [...r.k.path, seg]
1512
+ r.child.k.key = seg
1513
+ }
1514
+ else if (0 < r.d && r.u.pair) {
1515
+ // The element's index is the array length: everything before
1516
+ // it is already pushed, and the pair's own map is pushed at
1517
+ // close. `r.k.index` is not usable here -- the path plugin
1518
+ // counts only the elements it pushes itself, and this one is
1519
+ // aontu's.
1520
+ const seg = '' + r.u.key
1521
+ r.child.k.path =
1522
+ [...r.k.path, '' + (r.node?.length ?? 0), seg]
1523
+ r.child.k.key = seg
1524
+ }
1525
+ })
1526
+
1527
+ .bc((rule: Rule, ctx: JsonicContext) => {
1147
1528
  // TRAVERSE PARENTS TO GET PATH
1148
1529
 
1149
- if (rule.u.spread) {
1530
+ // Only the `&:` alternative is a SPREAD. All four alts above set
1531
+ // `spread: true` -- it is what marks them as contributing no
1532
+ // element -- so this guard needs the narrower test, and `pair`
1533
+ // is what distinguishes a `k:v` in list position from a spread.
1534
+ //
1535
+ // Without it a pair BUILT the spread record, with `o` taken from
1536
+ // its own key rather than '&': `[x:1, &:integer, "bad"]` left
1537
+ // `{o:'x', v:[1, integer]}`, and ListVal's `'&' === spread.o`
1538
+ // then discarded the real constraint -- so the element spread
1539
+ // was silently dropped and the bad value generated (BUGS.md 46).
1540
+ // A pair alone did it too: `[x:1, 10]` produced a spread record
1541
+ // out of nothing.
1542
+ if (rule.u.spread && !rule.u.pair) {
1150
1543
  rule.node[SPREAD] =
1151
1544
  (rule.node[SPREAD] || { o: rule.o0.src, v: [] })
1152
1545
  rule.node[SPREAD].v.push(rule.child.node)
1153
1546
  }
1154
1547
 
1548
+ // The slot is given back BEFORE the element is added: the
1549
+ // restore undoes jsonic's raw write (a property for a text
1550
+ // key, an INDEX for a numeric one -- restoring length is what
1551
+ // keeps `[1:2]` from padding with a null), and the push then
1552
+ // appends cleanly after it.
1155
1553
  restorePairSlot(rule)
1156
1554
 
1555
+ // THE SINGLE-KEY MAP ELEMENT, for both pair spellings. The
1556
+ // value is a Val already (`p: 'val'`), so the map is built
1557
+ // exactly as the map rule builds one -- and an elided value
1558
+ // (`[a:]`) is refused exactly as the map rule refuses one
1559
+ // (issue #48): a key with nothing after the colon is a
1560
+ // mistake, not an empty value.
1561
+ if (true === rule.u.pair) {
1562
+ const key = '' + rule.u.key
1563
+ let v: any = rule.child.node
1564
+ if (null == v) {
1565
+ v = addsite(new NilVal({ why: 'elided_value' }), rule, ctx)
1566
+ v.path = [...(rule.k?.path ?? []),
1567
+ '' + rule.node.length, key]
1568
+ }
1569
+ const mv: any = addsite(
1570
+ new MapVal({ peg: { [key]: v } }), rule, ctx)
1571
+ // `[a?: 1]` is `[{a?: 1}]`: the key is optional IN the
1572
+ // element, so the two spellings stay one rule apart rather
1573
+ // than two behaviours apart.
1574
+ if (true === rule.u.aontu_optional_elem) {
1575
+ mv.optionalKeys = [key]
1576
+ }
1577
+ rule.node.push(mv)
1578
+ }
1579
+
1157
1580
  return undefined
1158
1581
  })
1159
1582
 
@@ -1166,19 +1589,231 @@ help isolate the syntax error.`,
1166
1589
 
1167
1590
 
1168
1591
 
1169
- // SECURITY: the default resolver reads any file/package the process can
1170
- // reach — @"path" follows relative paths (`@"../../etc/passwd"`) and
1171
- // symlinks with no containment check, and @"pkg" can require() arbitrary
1172
- // installed modules. This is intentional for the CLI, but it means a
1173
- // `.aon` source can read referenced files; the LSP uses this same
1174
- // resolver, so treat opening an untrusted source as running it. Pass a
1175
- // confined `options.resolver` to restrict reads in less-trusted contexts.
1592
+ // INCLUDE_KINDS IS THE RULE FOR WHAT AN INCLUDE MEANS (ADR-012,
1593
+ // use-cases/BUGS.md §49). An extension is on this list or it is not
1594
+ // read at all, and its entry says WHICH OF TWO THINGS the file is.
1595
+ //
1596
+ // `source` — Aontu, with everything the language has: types, defaults,
1597
+ // references, constraints, its own includes. Two extensions, and they
1598
+ // are the ones this project owns.
1599
+ //
1600
+ // A FORMAT NAME — configuration DATA, parsed by that format's own
1601
+ // parser into the JSON value it denotes, which then becomes Aontu
1602
+ // values like any other data. Every one of these formats maps onto
1603
+ // JSON, which is why one word covers them: a `.toml` file is a map of
1604
+ // scalars, lists and maps, and so is the `.aon` file that unifies with
1605
+ // it. What the format does NOT get is the language — a `&` in a YAML
1606
+ // file is a YAML anchor, not a spread key, because the YAML parser
1607
+ // reads it, not this one.
1608
+ //
1609
+ // The parsers are @tabnas's, one per format, and the Go port uses the
1610
+ // same ones (ADR-001): the two implementations agree because they are
1611
+ // running the same grammar, not because two hand-written readers were
1612
+ // kept in step.
1613
+ //
1614
+ // This table and go/source.go's includeKinds are the same table.
1615
+ const INCLUDE_KINDS: { [kind: string]: string } = {
1616
+ aon: 'source',
1617
+ aontu: 'source',
1618
+
1619
+ json: 'json',
1620
+ // JSON-LD is JSON: a `@context` is a key like any other here, and
1621
+ // what it MEANS is the vocabulary's business, not the reader's.
1622
+ jsonld: 'json',
1623
+ jsonc: 'jsonc',
1624
+ json5: 'json5',
1625
+ jsonic: 'jsonic',
1626
+ jsc: 'jsonic',
1627
+ toml: 'toml',
1628
+ yaml: 'yaml',
1629
+ yml: 'yaml',
1630
+ ini: 'ini',
1631
+ }
1632
+
1633
+ // `.csv` IS DELIBERATELY ABSENT, and the reason is ADR-001 rather than
1634
+ // taste. The two ports' CSV parsers disagree about what a CSV file IS:
1635
+ // one answers header-keyed records with string fields, the other raw
1636
+ // rows including the header, with numbers parsed. Admitting it would
1637
+ // admit a divergence into the one thing this project refuses to have
1638
+ // one in. Recorded in ADR-012 and pinned by file.tsv's load-ext-csv.
1639
+
1640
+ // The multisource kind of a path: the LAST segment's extension, without
1641
+ // its dot, lowercased -- `''` for a name that has none. The rule is
1642
+ // @tabnas/multisource's own extKind (and Go's filepath.Ext), copied
1643
+ // rather than imported because it decides what a source IS: a dot in a
1644
+ // parent folder (`/my.app/conf`) must not read as an extension.
1645
+ function extKindOf(full: string): string {
1646
+ const seg = (full.match(/[^\\/]*$/) as string[])[0]
1647
+ return (seg.match(/\.([^.]*)$/) || ['', ''])[1].toLowerCase()
1648
+ }
1649
+
1650
+ // The refusal message, naming the extension -- because the extension is
1651
+ // the whole reason, and a reader told only "not readable" has to guess
1652
+ // which part of the path the engine objected to. Byte-identical to Go's
1653
+ // extensionMsg.
1654
+ function extensionMsg(path: string, ext: string): string {
1655
+ const which = '' === ext ? 'no extension' : 'extension: .' + ext
1656
+ return 'include not readable: ' + path + ' (' + which + ')'
1657
+ }
1658
+
1659
+ // THE RULE ALSO HOLDS FOR A RESOLVER THIS ENGINE DID NOT WRITE.
1660
+ // gateExtension refuses an unlisted extension inside makeModelResolver,
1661
+ // which is the default; a HOST may supply its own through
1662
+ // `AontuOptions.resolver`, and that one has never heard of
1663
+ // INCLUDE_KINDS. Without this the host's resolution would fall to
1664
+ // multisource's own default for an unnamed kind, which hands the file
1665
+ // back as TEXT — or, for `.js`, EXECUTES it. So the two roads end in
1666
+ // one place: whatever chose the source, an extension off the list is
1667
+ // refused with the same code and the same message.
1668
+ const refuseProcessor = (res: any) => {
1669
+ // `full` is the one part a host resolution may leave out -- it is the
1670
+ // path the resolver CHOSE, and a resolver that answers from something
1671
+ // other than a filesystem need not have one. The written path always
1672
+ // reaches here, so it is the fallback.
1673
+ const err: any = new Error(extensionMsg(res.path, extKindOf(res.full ?? res.path)))
1674
+ err.code = 'include_extension'
1675
+ throw err
1676
+ }
1677
+
1678
+
1679
+ // ONE READER PER FORMAT, BUILT ONCE. These are stateless parsers and
1680
+ // building a jsonic instance is not free, so they are made at module
1681
+ // load rather than per include. The file name is passed through so a
1682
+ // syntax error inside an included `.toml` names the `.toml`.
1683
+ const DATA_READERS: { [format: string]: (src: string, fileName: string) => any } =
1684
+ (() => {
1685
+ const viaPlugin = (plugin: any) => {
1686
+ const jsonic = Jsonic.make().use(plugin)
1687
+ return (src: string, fileName: string) => jsonic(src, { fileName })
1688
+ }
1689
+ const toml = viaPlugin(Toml)
1690
+ // The strict RFC 8259 reader is its own parser rather than a
1691
+ // plugin, and it is `make().parse` rather than the module's bare
1692
+ // `parse`: only the instance carries the meta bag, and without it
1693
+ // a syntax error in an included `.json` says `<no-file>`.
1694
+ const json = makeJsonParser()
1695
+ return {
1696
+ json: (src: string, fileName: string) => json.parse(src, { fileName }),
1697
+ jsonc: viaPlugin(Jsonc),
1698
+ json5: viaPlugin(Json5),
1699
+ // Plain jsonic needs no plugin: it IS the base parser.
1700
+ jsonic: (src: string, fileName: string) => Jsonic(src, { fileName }),
1701
+ toml: (src: string, fileName: string) => tomlDates(toml(src, fileName)),
1702
+ yaml: viaPlugin(Yaml),
1703
+ ini: viaPlugin(Ini),
1704
+ }
1705
+ })()
1706
+
1707
+ /**
1708
+ * A TOML document with its dates as the TEXT they were written as.
1709
+ *
1710
+ * TOML HAS DATES AND JSON DOES NOT, so the reader cannot hand one over
1711
+ * as itself: it answers with a marker object carrying the kind and the
1712
+ * source text. The value that reaches a document is that TEXT, which is
1713
+ * what a JSON document carries for a date anyway — and it is what the
1714
+ * Go port produces too, from a `*TomlTime` holding those same two
1715
+ * fields (`dataToValDepth`, go/source.go). Without this the same file
1716
+ * is a nested map in one port and a string in the other, which is the
1717
+ * class of divergence ADR-012 exists to stop.
1718
+ *
1719
+ * The guard is exact — one key, `__toml__`, holding a `kind` and a
1720
+ * `src` string — so a document whose own data happens to use the name
1721
+ * passes through untouched.
1722
+ */
1723
+ function tomlDates(node: any): any {
1724
+ if (Array.isArray(node)) {
1725
+ return node.map(tomlDates)
1726
+ }
1727
+ if (null === node || 'object' !== typeof node) {
1728
+ return node
1729
+ }
1730
+ const keys = Object.keys(node)
1731
+ const mark = node.__toml__
1732
+ if (1 === keys.length && '__toml__' === keys[0] && null != mark &&
1733
+ 'string' === typeof mark.kind && 'string' === typeof mark.src) {
1734
+ return mark.src
1735
+ }
1736
+ const out: Record<string, any> = {}
1737
+ for (const k of keys) {
1738
+ out[k] = tomlDates(node[k])
1739
+ }
1740
+ return out
1741
+ }
1742
+
1743
+ /**
1744
+ * Read one included file as DATA in the named format.
1745
+ *
1746
+ * The parser hands back the JSON value the file denotes — plain maps,
1747
+ * lists and scalars — and rawToVal turns that into Vals. THE
1748
+ * CONVERSION HAPPENS HERE, not at the top level, because an include is
1749
+ * usually not at the top level: `a: @"conf.toml"` puts the value under
1750
+ * a key, where a raw JavaScript object is something the tree cannot
1751
+ * unify with (the crash that was BUGS §49b).
1752
+ */
1753
+ const dataProcessor = (format: string) => (res: any) => {
1754
+ res.val = rawToVal(DATA_READERS[format](res.src, res.path))
1755
+ }
1756
+
1757
+ /**
1758
+ * The multisource processor map, built FROM the include table so the
1759
+ * two cannot drift: every extension the table names gets the reader
1760
+ * the table names for it, and the two kinds that are not in the table
1761
+ * refuse.
1762
+ */
1763
+ function includeProcessors(): { [kind: string]: any } {
1764
+ const map: { [kind: string]: any } = {
1765
+ // multisource's fallback for an extension no entry names, so it is
1766
+ // the one that catches whatever the resolver's gate did not.
1767
+ '': refuseProcessor,
1768
+ // ... and the one upstream default that would EXECUTE the file.
1769
+ js: refuseProcessor,
1770
+ }
1771
+ const source = makeJsonicProcessor()
1772
+ for (const kind of Object.keys(INCLUDE_KINDS)) {
1773
+ const format = INCLUDE_KINDS[kind]
1774
+ map[kind] = 'source' === format ? source : dataProcessor(format)
1775
+ }
1776
+ return map
1777
+ }
1778
+
1779
+
1780
+ // SECURITY: under the DEFAULT ('system') include capability this
1781
+ // resolver reads any file the process can reach — @"path" follows
1782
+ // relative paths (`@"../../etc/passwd.aon"`) and symlinks — so treat
1783
+ // opening an untrusted source as reading your disk. It no longer RUNS
1784
+ // one: @"pkg" could require() an arbitrary installed module until
1785
+ // ADR-012, which refuses a `.js` entry point by the same rule that
1786
+ // refuses `.txt`. The trust profile (G5, docs/trust.md) is the
1787
+ // confinement surface: `trust.include` of
1788
+ // 'none', `{ mem }` or `{ root }` restricts what `@"..."` may resolve,
1789
+ // and a denied resolution is a deterministic parse-stage
1790
+ // `include_denied` error.
1176
1791
  function makeModelResolver(options: any) {
1177
1792
  const useRequire = options.require || require
1178
-
1179
- let memResolver = makeMemResolver({
1180
- ...(options.resolver?.mem || {})
1181
- })
1793
+ const capability = options.trust?.include ?? 'system'
1794
+
1795
+ const memCapability =
1796
+ 'object' === typeof capability && null != (capability as any).mem
1797
+ const rootDir: string | undefined =
1798
+ 'object' === typeof capability &&
1799
+ 'string' === typeof (capability as any).root
1800
+ ? pathResolve((capability as any).root) : undefined
1801
+
1802
+ // Under the mem capability the CAPABILITY's file set is the whole
1803
+ // world; otherwise the host-injected `options.resolver.mem` entries
1804
+ // remain available under every capability but 'none' — they are
1805
+ // host-provided, not document-requested, so confining them would
1806
+ // confine the host against itself.
1807
+ // THE BUNDLED VOCABULARY (G4 phase 4, ts/src/std.ts) rides the
1808
+ // memory leg: served from the engine itself, so it needs neither the
1809
+ // filesystem nor package resolution and is available under every
1810
+ // capability but `none` — which denies every include outright, that
1811
+ // being what `none` means. Host entries and the capability's own set
1812
+ // WIN over it: a caller that supplies its own `std/system` gets the
1813
+ // one it supplied.
1814
+ let memResolver = makeMemResolver(memCapability
1815
+ ? { ...(capability as any).mem }
1816
+ : { ...(options.resolver?.mem || {}) })
1182
1817
 
1183
1818
  // TODO: make this consistent with other resolvers
1184
1819
  let fileResolver = makeFileResolver((spec: any) => {
@@ -1190,6 +1825,121 @@ function makeModelResolver(options: any) {
1190
1825
  ...(options.resolver?.pkg || {})
1191
1826
  })
1192
1827
 
1828
+ // Confinement is realpath-then-prefix-check (docs/trust.md): the
1829
+ // RESOLVED file's real path must sit below the root's real path, so a
1830
+ // symlink inside the root pointing outside it is an escape, not a
1831
+ // loophole. A path realpath cannot resolve falls back to the lexical
1832
+ // form — the comparison is then against what the resolver actually
1833
+ // read.
1834
+ // Real fs, deliberately: `options.fs` is not a sandbox (it feeds
1835
+ // parse text; the file leg reads through its own channel), so the
1836
+ // containment check must see the same filesystem that leg read from.
1837
+ // A path that does not (fully) exist cannot be realpath'd whole, and
1838
+ // falling back to the LEXICAL form compares apples to oranges when
1839
+ // the root itself sits behind a symlink -- on macOS a root under
1840
+ // /var realpaths to /private/var, so a merely-missing file inside it
1841
+ // reads as an escape. Realpath the deepest EXISTING ancestor and
1842
+ // re-attach the rest, so both sides of the check are in real
1843
+ // coordinates. (The MCP server's own confinement carries the twin of
1844
+ // this rule; its CI failure is what found the shape.)
1845
+ const realpath = (p: string): string => {
1846
+ try {
1847
+ return realpathSync(p)
1848
+ }
1849
+ catch {
1850
+ const parent = pathDirname(p)
1851
+ if (parent === p) {
1852
+ return p
1853
+ }
1854
+ return pathJoin(realpath(parent), pathBasename(p))
1855
+ }
1856
+ }
1857
+
1858
+ const outsideRoot = (root: string, full: string): boolean => {
1859
+ const rootReal = realpath(root)
1860
+ const fullReal = realpath(full)
1861
+ return fullReal !== rootReal && !fullReal.startsWith(rootReal + pathSep)
1862
+ }
1863
+
1864
+ // A denial THROWS with the code; Lang.parse converts it to the
1865
+ // parse-stage `include_denied` nil (the same shape a syntax failure
1866
+ // takes). Raising beats injecting a nil value: a bare-member include
1867
+ // (`@"denied.aon"` at the top of a file) MERGES into the enclosing
1868
+ // map, and a nil contributes no keys, so an injected denial would
1869
+ // vanish and leave a plausible, silently-partial document.
1870
+ const deny = (path: string): never => {
1871
+ // Only 'none' and 'root' can deny: the mem capability's misses are
1872
+ // not-found (its set is the whole world), so there is no third arm.
1873
+ const capname = 'none' === capability ? 'none' : 'root:' + rootDir
1874
+ const err: any = new Error(
1875
+ 'include denied: ' + path + ' (capability: ' + capname + ')')
1876
+ err.code = 'include_denied'
1877
+ throw err
1878
+ }
1879
+
1880
+ // AN UNREADABLE EXTENSION THROWS, exactly as a denial does, and for
1881
+ // the same reason: a bare-member include (`@"notes.txt"` at the top
1882
+ // of a file) MERGES into the enclosing map, and a nil contributes no
1883
+ // keys, so an injected refusal would vanish and leave a plausible,
1884
+ // silently-partial document. Lang.parse turns the throw into the
1885
+ // parse-stage `include_extension` nil.
1886
+ const refuseExtension = (path: string, full: string): never => {
1887
+ const err: any = new Error(extensionMsg(path, extKindOf(full)))
1888
+ err.code = 'include_extension'
1889
+ throw err
1890
+ }
1891
+
1892
+ // The gate every leg that RESOLVES A NAME passes through. The std and
1893
+ // module legs do not: both state `kind: 'aon'` because what they
1894
+ // serve is Aontu source by construction, not by its spelling.
1895
+ const gateExtension = (path: string, full: string): void => {
1896
+ if (undefined === INCLUDE_KINDS[extKindOf(full)]) {
1897
+ refuseExtension(path, full)
1898
+ }
1899
+ }
1900
+
1901
+ // The user cache: whatever the host named, else the platform rule
1902
+ // (`modCacheDir`, ts/src/mod.ts) the tooling writes by.
1903
+ const modCache = (opts: any): string | undefined => {
1904
+ const named = opts.mod?.cache
1905
+ return 'string' === typeof named ? named : modCacheDir()
1906
+ }
1907
+
1908
+ // The directory an include is being resolved FROM: the source that
1909
+ // holds it, or the entry path when the source is a string. Same base
1910
+ // the file leg computes (resolvePathSpec in @tabnas/multisource).
1911
+ const dirOf = (p: string | undefined): string =>
1912
+ null == p || '' === p ? pathResolve('.') : pathDirname(pathResolve(p))
1913
+
1914
+ // The module store reader: the host's filesystem when one was
1915
+ // injected, so a sandboxed evaluation stays in the filesystem the
1916
+ // host gave it.
1917
+ const modFs = (ctx: any): any => {
1918
+ const hostfs = ctx?.meta?.fs
1919
+ return null == hostfs ? { existsSync, readFileSync } : {
1920
+ existsSync: (p: string) => {
1921
+ try {
1922
+ hostfs.statSync(p)
1923
+ return true
1924
+ }
1925
+ catch {
1926
+ return false
1927
+ }
1928
+ },
1929
+ readFileSync: (p: string, enc: string) => hostfs.readFileSync(p, enc),
1930
+ }
1931
+ }
1932
+
1933
+ // The manifest sink rides the parse meta (Lang.parse seeds it, the
1934
+ // multisource plugin's child-meta spread carries it to every nested
1935
+ // include), so the recorded closure covers the whole include tree.
1936
+ const record = (ctx: any, path: string, cap: string) => {
1937
+ const manifest = ctx?.meta?.aontu?.manifest
1938
+ if (Array.isArray(manifest)) {
1939
+ manifest.push({ path, capability: cap })
1940
+ }
1941
+ }
1942
+
1193
1943
  return function ModelResolver(
1194
1944
  spec: any,
1195
1945
  popts: any,
@@ -1209,10 +1959,69 @@ function makeModelResolver(options: any) {
1209
1959
  return { found: false, path: '' + (path ?? ''), search: [] }
1210
1960
  }
1211
1961
 
1962
+ if ('none' === capability) {
1963
+ deny(path)
1964
+ }
1965
+
1966
+ // THE BUNDLED VOCABULARY (G4 phase 4, ts/src/std.ts): served from
1967
+ // the engine itself, so it needs neither the filesystem nor package
1968
+ // resolution and is available under every capability but `none` —
1969
+ // checked just above, that being what `none` means. Matched against
1970
+ // the name the author WROTE, before the memory leg, so the kind is
1971
+ // stated rather than guessed from an extension the bare name does
1972
+ // not have.
1973
+ const std = STD_SOURCES[path]
1974
+ if (null != std) {
1975
+ record(ctx, path, 'std')
1976
+ return { found: true, path, full: path, kind: 'aon', src: std, search: [] }
1977
+ }
1978
+
1212
1979
  let search: any = []
1213
1980
  let res = memResolver(path, popts, rule, ctx, jsonic)
1214
1981
  res.path = path
1215
1982
  if (res.found) {
1983
+ // THE EXTENSION DECIDES HERE TOO. A virtual file set is still a
1984
+ // file set: its keys carry extensions, and the same rule has to
1985
+ // read them, or the mem capability becomes a way to include what
1986
+ // the filesystem would refuse.
1987
+ gateExtension(path, res.full ?? path)
1988
+ record(ctx, res.full ?? path, 'mem')
1989
+ return res
1990
+ }
1991
+
1992
+ // THE MODULE LEG (G6 phase 2, ts/src/mod.ts): memory -> MODULE ->
1993
+ // filesystem -> package. Memory stays FIRST so a sandbox and the
1994
+ // spec suite can stub a module path without touching disk; a path
1995
+ // that is not module-shaped falls straight through, so no existing
1996
+ // include can be routed somewhere new by this.
1997
+ const modref = memCapability ? undefined : parseModuleRef(path)
1998
+ if (null != modref) {
1999
+ const msmeta = (ctx as any)?.meta?.multisource
2000
+ const from = dirOf(null != msmeta?.path ? msmeta.path : popts?.path)
2001
+ const found = resolveModule(modref, from, modFs(ctx), {
2002
+ // The user cache lives outside any confinement root, so it is
2003
+ // consulted only when nothing confines this evaluation. A
2004
+ // rooted profile sees the project's own `aon_vendor/` and
2005
+ // nothing else, which is what `root` means.
2006
+ ...(null == rootDir ? { cache: modCache(options) } : {}),
2007
+ eval: options.mod?.eval,
2008
+ depth: options.mod?.depth,
2009
+ })
2010
+ if (null != rootDir && outsideRoot(rootDir, found.full)) {
2011
+ deny(path)
2012
+ }
2013
+ record(ctx, found.full, 'mod')
2014
+ return {
2015
+ found: true, path, full: found.full,
2016
+ kind: 'aon', src: found.src, search: [],
2017
+ }
2018
+ }
2019
+
2020
+ if (memCapability) {
2021
+ // A miss in the declared virtual set is NOT-FOUND, not denial:
2022
+ // the allowed mechanism ran and missed. Denial is reserved for a
2023
+ // capability refusing a mechanism outright.
2024
+ res.search = search.concat(res.search)
1216
2025
  return res
1217
2026
  }
1218
2027
 
@@ -1221,14 +2030,47 @@ function makeModelResolver(options: any) {
1221
2030
  res = fileResolver(path, popts, rule, ctx, jsonic)
1222
2031
  res.path = path
1223
2032
  if (res.found) {
2033
+ // `res.full` asserted non-null: a FOUND file resolution always
2034
+ // carries the absolute path it read (and the pkg leg below is the
2035
+ // same), so a runtime fallback arm would be dead code.
2036
+ const full = res.full as string
2037
+ if (null != rootDir && outsideRoot(rootDir, full)) {
2038
+ deny(path)
2039
+ }
2040
+ // After the trust check, not before: a file outside the
2041
+ // confinement root is denied whatever it is called, and answering
2042
+ // "extension" there would say the file exists.
2043
+ gateExtension(path, full)
2044
+ // The warning window for the staged default flip (G5 phase 6):
2045
+ // under 'system', the CLI supplies trustWarn and the entry root,
2046
+ // and every resolution escaping that root names the flag a future
2047
+ // default will require.
2048
+ if (null == rootDir && null != options.trustWarn &&
2049
+ null != options.trustWarnRoot &&
2050
+ outsideRoot(options.trustWarnRoot, full)) {
2051
+ options.trustWarn('escape', full)
2052
+ }
2053
+ record(ctx, full, 'file')
1224
2054
  return res
1225
2055
  }
1226
2056
 
1227
2057
  search = search.concat(res.search)
1228
2058
 
2059
+ if (null != rootDir) {
2060
+ // Package resolution is not part of the root capability; the
2061
+ // miss stands as not-found with the searched paths listed.
2062
+ res.search = search
2063
+ return res
2064
+ }
2065
+
1229
2066
  res = pkgResolver(path, popts, rule, ctx, jsonic)
1230
2067
  res.path = path
1231
2068
  if (res.found) {
2069
+ gateExtension(path, res.full as string)
2070
+ if (null != options.trustWarn) {
2071
+ options.trustWarn('pkg', res.full as string)
2072
+ }
2073
+ record(ctx, res.full as string, 'pkg')
1232
2074
  return res
1233
2075
  }
1234
2076
 
@@ -1238,27 +2080,62 @@ function makeModelResolver(options: any) {
1238
2080
  }
1239
2081
 
1240
2082
 
1241
- // funcArity is the permitted WRITTEN argument count of each built-in, as
2083
+ // THE SIGNATURE REGISTRY (docs/design/SIGNATURES.0.md). The call
2084
+ // surface is DECLARED in test/spec/signature.tsv and parsed by the
2085
+ // signature grammar (ts/src/sig.ts) from the build-time-inlined copy;
2086
+ // the arity table and the positional set below are DERIVED from the
2087
+ // parsed registry (funcSig, ts/src/sig.ts), so the declaration is the
2088
+ // one source. go/func.go derives the same two tables from the same
2089
+ // text.
2090
+ // The functions whose comma-separated arguments are distinct POSITIONS
2091
+ // rather than one argument list. See the func-paren handler above: this
2092
+ // is the set whose comma group is expanded back into separate `peg`
2093
+ // entries. Derived: two or more declared argument slots, excluding the
2094
+ // residual producers (`constraint` results) -- the constraint atoms
2095
+ // make the same expansion in their own constructor (`atomArgs`,
2096
+ // ConstraintVal.ts, deliberately before the settled check), which is
2097
+ // why they are not in this set; `must` is the load-bearing example.
2098
+ // Arithmetic is here because `sub` is not commutative: `sub(a, b)`
2099
+ // reaching the engine as one two-element list would lose which is
2100
+ // which.
2101
+ const POSITIONAL_ARG_FUNCS: Record<string, boolean> = {}
2102
+ for (const name in funcSig) {
2103
+ if (2 <= funcSig[name].args.length && 'constraint' !== funcSig[name].out) {
2104
+ POSITIONAL_ARG_FUNCS[name] = true
2105
+ }
2106
+ }
2107
+
2108
+
1242
2109
  // [min, max]; a max of -1 is unbounded. Every name in funcMap has an
1243
2110
  // entry, and the arity is a property of the language rather than of
1244
- // either port -- go/func.go carries the same table.
1245
- //
1246
- // Nearly everything takes exactly one. The four exceptions earn their
1247
- // place: key() names how many levels UP the path to read, defaulting to
1248
- // the parent when omitted, neq takes a whole set of exclusions,
1249
- // unique() is a property of the container rather than a comparison
1250
- // against anything, so there is nothing for it to take, and must()
1251
- // takes a check AND the author's message for when it fails.
1252
- const funcArity: Record<string, [number, number]> = {
1253
- upper: [1, 1], lower: [1, 1], copy: [1, 1], pref: [1, 1],
1254
- super: [1, 1], type: [1, 1], hide: [1, 1], close: [1, 1],
1255
- open: [1, 1], move: [1, 1], path: [1, 1],
1256
- min: [1, 1], max: [1, 1], above: [1, 1], below: [1, 1], re: [1, 1],
1257
- length: [1, 1],
1258
- key: [0, 1],
1259
- unique: [0, 0],
1260
- neq: [1, -1],
1261
- must: [2, 2],
2111
+ // either port -- go/func.go derives the same table. A required slot
2112
+ // counts toward the minimum; a rest slot makes the maximum unbounded
2113
+ // and counts its group size (one, for a plain rest type) toward the
2114
+ // minimum, which is what gives `match` its floor of three and `neq`
2115
+ // its floor of one.
2116
+ function sigArity(sig: FuncSig): [number, number] {
2117
+ let min = 0
2118
+ let max = 0
2119
+ for (const a of sig.args) {
2120
+ if (true === a.rest) {
2121
+ min += undefined === a.group ? 1 : a.group.length
2122
+ max = -1
2123
+ }
2124
+ else {
2125
+ if (true !== a.opt) {
2126
+ min++
2127
+ }
2128
+ if (-1 !== max) {
2129
+ max++
2130
+ }
2131
+ }
2132
+ }
2133
+ return [min, max]
2134
+ }
2135
+
2136
+ const funcArity: Record<string, [number, number]> = {}
2137
+ for (const name in funcSig) {
2138
+ funcArity[name] = sigArity(funcSig[name])
1262
2139
  }
1263
2140
 
1264
2141
 
@@ -1293,8 +2170,10 @@ function arityText(lo: number, hi: number): string {
1293
2170
  return 'one or more arguments'
1294
2171
  }
1295
2172
  if (lo !== hi) {
1296
- return 'no arguments or one'
2173
+ return 0 === lo ? 'no arguments or one' : 'one argument or two'
1297
2174
  }
2175
+ // The {0,0} arm returned with the container kinds and acyclic()
2176
+ // (ADR-015): `map(1)` must not claim map takes exactly one.
1298
2177
  if (0 === hi) {
1299
2178
  return 'no arguments'
1300
2179
  }
@@ -1339,15 +2218,22 @@ function opCharHint(src: string): string {
1339
2218
 
1340
2219
 
1341
2220
  function rawToVal(n: any): Val {
1342
- if (null == n) {
1343
- return new NullVal({ peg: null })
1344
- }
1345
- if (true === n.isVal) {
2221
+ if (true === n?.isVal) {
1346
2222
  return n
1347
2223
  }
1348
2224
  if (Array.isArray(n)) {
1349
2225
  return new ListVal({ peg: n.map(rawToVal) })
1350
2226
  }
2227
+
2228
+ // THE SCALAR ARMS ARE WHERE A CONFIG FILE BECOMES VALUES. Every
2229
+ // format on the include table is read by its own parser into plain
2230
+ // JavaScript -- a string, a number, a map -- and this is the walk
2231
+ // that turns that into Vals (dataProcessor, ADR-012). The two arms
2232
+ // above are the other caller: a raw expression TERM, which the
2233
+ // expression grammar hands over already built.
2234
+ if (null == n) {
2235
+ return new NullVal({ peg: null })
2236
+ }
1351
2237
  const t = typeof n
1352
2238
  if ('string' === t) {
1353
2239
  return new StringVal({ peg: n })
@@ -1362,14 +2248,20 @@ function rawToVal(n: any): Val {
1362
2248
  if ('boolean' === t) {
1363
2249
  return new BooleanVal({ peg: n })
1364
2250
  }
1365
- if ('object' === t) {
1366
- const peg: Record<string, Val> = {}
1367
- for (const k in n) {
1368
- peg[k] = rawToVal(n[k])
1369
- }
1370
- return new MapVal({ peg })
2251
+ // AND EVERYTHING ELSE IS A MAP, with no arm after it because there is
2252
+ // nothing after it. Every reader on the include table answers with
2253
+ // the JSON kinds and no others -- probed, including the two that
2254
+ // could plausibly escape them: a big integer comes back a `number`,
2255
+ // and a TOML date is normalised to its text before it gets here. The
2256
+ // one include that could hand over a function was `.js`, which
2257
+ // ADR-012 refuses. `parse_unknown` lived here for that case and has
2258
+ // no producer left in this port; the Go twin keeps its own, where the
2259
+ // type switch really can be handed something unaccounted for.
2260
+ const peg: Record<string, Val> = {}
2261
+ for (const k in n) {
2262
+ peg[k] = rawToVal(n[k])
1371
2263
  }
1372
- return new NilVal({ why: 'parse_unknown' })
2264
+ return new MapVal({ peg })
1373
2265
  }
1374
2266
 
1375
2267
 
@@ -1401,11 +2293,22 @@ class Lang {
1401
2293
  // works. `.jsonic` is retired (no longer auto-resolved); the
1402
2294
  // default `['jsonic','jsc','json','js']` is overridden here.
1403
2295
  // (Upstream option name is the misspelled `implictExt`.)
2296
+ //
2297
+ // Only these two are SEARCHED for a bare `@"name"`; `.json` and
2298
+ // `.jsonld` are read when NAMED, which is how a vendored
2299
+ // vocabulary is always written.
1404
2300
  implictExt: ['aon', 'aontu'],
1405
- processor: {
1406
- aontu: 'jsonic',
1407
- aon: 'jsonic',
1408
- }
2301
+ // ONE ENTRY PER EXTENSION THE TABLE NAMES, built from it (see
2302
+ // includeProcessors) so the rule and its wiring cannot drift.
2303
+ //
2304
+ // The upstream defaults are REPLACED, not extended. Its `json`
2305
+ // entry is what made that extension the one that crashed: it
2306
+ // hands back a raw JS object where the aontu grammar produces
2307
+ // Vals, and the tree then met a value it could not convert
2308
+ // (BUGS §49b). Its `js` entry EXECUTES the file, which is not
2309
+ // something an extension should be able to ask for. And its
2310
+ // fallback hands any other file back as TEXT.
2311
+ processor: includeProcessors()
1409
2312
  })
1410
2313
  .use(AontuJsonic)
1411
2314
  }
@@ -1421,7 +2324,13 @@ class Lang {
1421
2324
  multisource: {
1422
2325
  path: opts?.path ?? this.opts.path,
1423
2326
  deps: (opts && opts.deps) || undefined
1424
- }
2327
+ },
2328
+ // The include-manifest sink (G5, docs/trust.md): the resolver
2329
+ // records every resolved include here, and the plugin's
2330
+ // child-meta spread carries the same array to nested includes.
2331
+ aontu: {
2332
+ manifest: (opts as any)?.manifest,
2333
+ },
1425
2334
  }
1426
2335
 
1427
2336
  if (null != opts?.idcount) {
@@ -1449,16 +2358,46 @@ class Lang {
1449
2358
  }
1450
2359
  }
1451
2360
  catch (e: any) {
1452
- if (e instanceof JsonicError || 'JsonicError' === e.constructor.name) {
2361
+ if ('include_denied' === e?.code || 'include_extension' === e?.code ||
2362
+ MODULE_REFUSAL_CODES.has(e?.code)) {
2363
+ // A denied include (G5), an include whose extension is not read
2364
+ // as Aontu source (ADR-012, INCLUDE_KINDS), and a module that is
2365
+ // missing, fails its pin, or names a path that escapes its store
2366
+ // (G6 phase 2) are refused the same way, for the same reason: the
2367
+ // resolver THROWS so a bare-member include cannot vanish in the
2368
+ // merge, and the code survives here as the parse-stage nil the
2369
+ // registry pins (errcodes.tsv).
1453
2370
  val = new NilVal({
1454
2371
  why: 'parse',
1455
2372
  err: new NilVal({
1456
- why: 'syntax',
1457
- msg: e.message + opCharHint(src),
2373
+ why: e.code,
2374
+ msg: e.message,
1458
2375
  err: e,
1459
2376
  })
1460
2377
  })
1461
2378
  }
2379
+ else if (e instanceof JsonicError || 'JsonicError' === e.constructor.name) {
2380
+ const syntax: any = new NilVal({
2381
+ why: 'syntax',
2382
+ msg: e.message + opCharHint(src),
2383
+ err: e,
2384
+ })
2385
+ // THE POSITION TRAVELS WITH IT. The parser knows exactly where
2386
+ // it stopped -- it draws a caret there -- and the rendered
2387
+ // message carried the only copy, so `vet --format json`
2388
+ // reported row -1, col -1 for a document whose fault the human
2389
+ // renderer located to the character. A machine-readable report
2390
+ // that says "somewhere in this file" is the one a repair loop
2391
+ // can do nothing with. Both numbers are already 1-based here,
2392
+ // which is the base a site uses (go/lang.go does the same).
2393
+ if ('number' === typeof e.lineNumber) {
2394
+ syntax.site.row = e.lineNumber
2395
+ }
2396
+ if ('number' === typeof e.columnNumber) {
2397
+ syntax.site.col = e.columnNumber
2398
+ }
2399
+ val = new NilVal({ why: 'parse', err: syntax })
2400
+ }
1462
2401
  else {
1463
2402
  throw e
1464
2403
  }