aontu 0.62.0 → 0.64.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 (373) hide show
  1. package/README.md +7 -7
  2. package/dist/agentsmd.js +0 -27
  3. package/dist/agentsmd.js.map +1 -1
  4. package/dist/alias.js.map +1 -1
  5. package/dist/allow.js +0 -92
  6. package/dist/allow.js.map +1 -1
  7. package/dist/aontu.d.ts +3 -3
  8. package/dist/aontu.js +4 -84
  9. package/dist/aontu.js.map +1 -1
  10. package/dist/aontumodel.d.ts +4 -0
  11. package/dist/aontumodel.js +27 -0
  12. package/dist/aontumodel.js.map +1 -0
  13. package/dist/casing.d.ts +5 -0
  14. package/dist/casing.js +90 -0
  15. package/dist/casing.js.map +1 -0
  16. package/dist/cli.d.ts +2 -2
  17. package/dist/cli.js +228 -935
  18. package/dist/cli.js.map +1 -1
  19. package/dist/ctx.js +0 -48
  20. package/dist/ctx.js.map +1 -1
  21. package/dist/diff.js +0 -32
  22. package/dist/diff.js.map +1 -1
  23. package/dist/err.js +0 -40
  24. package/dist/err.js.map +1 -1
  25. package/dist/escape.js +0 -45
  26. package/dist/escape.js.map +1 -1
  27. package/dist/exactjson.d.ts +0 -35
  28. package/dist/exactjson.js +0 -131
  29. package/dist/exactjson.js.map +1 -1
  30. package/dist/format.js +13 -203
  31. package/dist/format.js.map +1 -1
  32. package/dist/grammar.d.ts +9 -0
  33. package/dist/grammar.js +54 -0
  34. package/dist/grammar.js.map +1 -0
  35. package/dist/graph.js +0 -26
  36. package/dist/graph.js.map +1 -1
  37. package/dist/hcanon.js +0 -82
  38. package/dist/hcanon.js.map +1 -1
  39. package/dist/helpdoc.js +2 -2
  40. package/dist/helpdoc.js.map +1 -1
  41. package/dist/hints.d.ts +0 -6
  42. package/dist/hints.js +57 -55
  43. package/dist/hints.js.map +1 -1
  44. package/dist/jsonschema.js +0 -114
  45. package/dist/jsonschema.js.map +1 -1
  46. package/dist/keyorder.d.ts +0 -7
  47. package/dist/keyorder.js +0 -41
  48. package/dist/keyorder.js.map +1 -1
  49. package/dist/lang.js +17 -915
  50. package/dist/lang.js.map +1 -1
  51. package/dist/lsp-server.js +0 -16
  52. package/dist/lsp-server.js.map +1 -1
  53. package/dist/lsp.d.ts +1 -1
  54. package/dist/lsp.js +12 -159
  55. package/dist/lsp.js.map +1 -1
  56. package/dist/mcp-server.js +0 -26
  57. package/dist/mcp-server.js.map +1 -1
  58. package/dist/mcp.js +0 -149
  59. package/dist/mcp.js.map +1 -1
  60. package/dist/mod-tool.js +16 -131
  61. package/dist/mod-tool.js.map +1 -1
  62. package/dist/mod.js +0 -162
  63. package/dist/mod.js.map +1 -1
  64. package/dist/patch.js +0 -217
  65. package/dist/patch.js.map +1 -1
  66. package/dist/profile.d.ts +9 -0
  67. package/dist/profile.js +28 -0
  68. package/dist/profile.js.map +1 -0
  69. package/dist/provenance.js +0 -140
  70. package/dist/provenance.js.map +1 -1
  71. package/dist/query.js +0 -75
  72. package/dist/query.js.map +1 -1
  73. package/dist/reach.js +0 -43
  74. package/dist/reach.js.map +1 -1
  75. package/dist/relation.js +0 -61
  76. package/dist/relation.js.map +1 -1
  77. package/dist/report-sarif.d.ts +0 -11
  78. package/dist/report-sarif.js +0 -28
  79. package/dist/report-sarif.js.map +1 -1
  80. package/dist/sig.js +0 -35
  81. package/dist/sig.js.map +1 -1
  82. package/dist/sigdecl.js +1 -1
  83. package/dist/sigdecl.js.map +1 -1
  84. package/dist/siggate.js +0 -4
  85. package/dist/siggate.js.map +1 -1
  86. package/dist/site.js +3 -29
  87. package/dist/site.js.map +1 -1
  88. package/dist/subsume.d.ts +0 -10
  89. package/dist/subsume.js +0 -137
  90. package/dist/subsume.js.map +1 -1
  91. package/dist/template.d.ts +2 -1
  92. package/dist/template.js +58 -138
  93. package/dist/template.js.map +1 -1
  94. package/dist/trace.d.ts +21 -0
  95. package/dist/trace.js +107 -0
  96. package/dist/trace.js.map +1 -0
  97. package/dist/trim.js +0 -41
  98. package/dist/trim.js.map +1 -1
  99. package/dist/tsconfig.tsbuildinfo +1 -1
  100. package/dist/type.js.map +1 -1
  101. package/dist/unify.js +13 -248
  102. package/dist/unify.js.map +1 -1
  103. package/dist/utility.js +0 -22
  104. package/dist/utility.js.map +1 -1
  105. package/dist/val/AbnfFuncVal.d.ts +18 -0
  106. package/dist/val/AbnfFuncVal.js +132 -0
  107. package/dist/val/AbnfFuncVal.js.map +1 -0
  108. package/dist/val/AbsentVal.d.ts +11 -0
  109. package/dist/val/AbsentVal.js +30 -0
  110. package/dist/val/AbsentVal.js.map +1 -0
  111. package/dist/val/AggFuncVal.d.ts +10 -1
  112. package/dist/val/AggFuncVal.js +104 -116
  113. package/dist/val/AggFuncVal.js.map +1 -1
  114. package/dist/val/ArithFuncVal.js +0 -12
  115. package/dist/val/ArithFuncVal.js.map +1 -1
  116. package/dist/val/BagVal.js +1 -78
  117. package/dist/val/BagVal.js.map +1 -1
  118. package/dist/val/BigDecimalVal.js +0 -16
  119. package/dist/val/BigDecimalVal.js.map +1 -1
  120. package/dist/val/BigIntegerVal.js +0 -16
  121. package/dist/val/BigIntegerVal.js.map +1 -1
  122. package/dist/val/CloseFuncVal.js +0 -9
  123. package/dist/val/CloseFuncVal.js.map +1 -1
  124. package/dist/val/CmpFuncVal.d.ts +2 -0
  125. package/dist/val/CmpFuncVal.js +49 -72
  126. package/dist/val/CmpFuncVal.js.map +1 -1
  127. package/dist/val/ConjunctVal.js +0 -29
  128. package/dist/val/ConjunctVal.js.map +1 -1
  129. package/dist/val/ConstraintVal.js +0 -500
  130. package/dist/val/ConstraintVal.js.map +1 -1
  131. package/dist/val/ContainerKindVal.js +0 -2
  132. package/dist/val/ContainerKindVal.js.map +1 -1
  133. package/dist/val/CopyFuncVal.js +0 -3
  134. package/dist/val/CopyFuncVal.js.map +1 -1
  135. package/dist/val/Decimal.js +0 -179
  136. package/dist/val/Decimal.js.map +1 -1
  137. package/dist/val/DeprecateFuncVal.js.map +1 -1
  138. package/dist/val/DisjunctVal.js +0 -152
  139. package/dist/val/DisjunctVal.js.map +1 -1
  140. package/dist/val/EachFuncVal.js +0 -3
  141. package/dist/val/EachFuncVal.js.map +1 -1
  142. package/dist/val/EmitFuncVal.d.ts +1 -1
  143. package/dist/val/EmitFuncVal.js +6 -119
  144. package/dist/val/EmitFuncVal.js.map +1 -1
  145. package/dist/val/ExpectVal.js +0 -62
  146. package/dist/val/ExpectVal.js.map +1 -1
  147. package/dist/val/FilterFuncVal.js +0 -25
  148. package/dist/val/FilterFuncVal.js.map +1 -1
  149. package/dist/val/FuncBaseVal.d.ts +1 -0
  150. package/dist/val/FuncBaseVal.js +7 -127
  151. package/dist/val/FuncBaseVal.js.map +1 -1
  152. package/dist/val/GraphAtomVal.js +0 -15
  153. package/dist/val/GraphAtomVal.js.map +1 -1
  154. package/dist/val/HideFuncVal.js +0 -13
  155. package/dist/val/HideFuncVal.js.map +1 -1
  156. package/dist/val/IntegerVal.js +0 -61
  157. package/dist/val/IntegerVal.js.map +1 -1
  158. package/dist/val/JunctionVal.js +0 -20
  159. package/dist/val/JunctionVal.js.map +1 -1
  160. package/dist/val/KeyFuncVal.js +0 -46
  161. package/dist/val/KeyFuncVal.js.map +1 -1
  162. package/dist/val/ListVal.js +0 -57
  163. package/dist/val/ListVal.js.map +1 -1
  164. package/dist/val/LowerFuncVal.js +0 -11
  165. package/dist/val/LowerFuncVal.js.map +1 -1
  166. package/dist/val/MapVal.js +0 -151
  167. package/dist/val/MapVal.js.map +1 -1
  168. package/dist/val/MatchFuncVal.js +0 -27
  169. package/dist/val/MatchFuncVal.js.map +1 -1
  170. package/dist/val/{FormFuncVal.d.ts → MaybeFuncVal.d.ts} +5 -5
  171. package/dist/val/MaybeFuncVal.js +50 -0
  172. package/dist/val/MaybeFuncVal.js.map +1 -0
  173. package/dist/val/MoveFuncVal.js +0 -18
  174. package/dist/val/MoveFuncVal.js.map +1 -1
  175. package/dist/val/NilVal.js +0 -60
  176. package/dist/val/NilVal.js.map +1 -1
  177. package/dist/val/NomFuncVal.js +18 -54
  178. package/dist/val/NomFuncVal.js.map +1 -1
  179. package/dist/val/NumberVal.js +0 -15
  180. package/dist/val/NumberVal.js.map +1 -1
  181. package/dist/val/OpBaseVal.d.ts +1 -0
  182. package/dist/val/OpBaseVal.js +3 -15
  183. package/dist/val/OpBaseVal.js.map +1 -1
  184. package/dist/val/PackFuncVal.js +0 -34
  185. package/dist/val/PackFuncVal.js.map +1 -1
  186. package/dist/val/PathFuncVal.js +0 -6
  187. package/dist/val/PathFuncVal.js.map +1 -1
  188. package/dist/val/PathVal.js +0 -41
  189. package/dist/val/PathVal.js.map +1 -1
  190. package/dist/val/PlaceVal.js +0 -25
  191. package/dist/val/PlaceVal.js.map +1 -1
  192. package/dist/val/PlusOpVal.d.ts +1 -7
  193. package/dist/val/PlusOpVal.js +13 -74
  194. package/dist/val/PlusOpVal.js.map +1 -1
  195. package/dist/val/PrefFuncVal.js +0 -1
  196. package/dist/val/PrefFuncVal.js.map +1 -1
  197. package/dist/val/PrefVal.js +0 -167
  198. package/dist/val/PrefVal.js.map +1 -1
  199. package/dist/val/RecurseVal.js +0 -55
  200. package/dist/val/RecurseVal.js.map +1 -1
  201. package/dist/val/RefVal.js +0 -282
  202. package/dist/val/RefVal.js.map +1 -1
  203. package/dist/val/ReferFuncVal.js +3 -232
  204. package/dist/val/ReferFuncVal.js.map +1 -1
  205. package/dist/val/ScalarKindVal.js +0 -49
  206. package/dist/val/ScalarKindVal.js.map +1 -1
  207. package/dist/val/ScalarVal.js +0 -11
  208. package/dist/val/ScalarVal.js.map +1 -1
  209. package/dist/val/StrFuncVal.js +0 -18
  210. package/dist/val/StrFuncVal.js.map +1 -1
  211. package/dist/val/SuperFuncVal.js +0 -32
  212. package/dist/val/SuperFuncVal.js.map +1 -1
  213. package/dist/val/TopVal.js +0 -1
  214. package/dist/val/TopVal.js.map +1 -1
  215. package/dist/val/TranslateFuncVal.js +1 -3
  216. package/dist/val/TranslateFuncVal.js.map +1 -1
  217. package/dist/val/UpperFuncVal.js +0 -11
  218. package/dist/val/UpperFuncVal.js.map +1 -1
  219. package/dist/val/Val.d.ts +1 -0
  220. package/dist/val/Val.js +2 -133
  221. package/dist/val/Val.js.map +1 -1
  222. package/dist/val/VarVal.js +0 -12
  223. package/dist/val/VarVal.js.map +1 -1
  224. package/dist/val/arith.js +0 -37
  225. package/dist/val/arith.js.map +1 -1
  226. package/dist/val/caserange.js +0 -61
  227. package/dist/val/caserange.js.map +1 -1
  228. package/dist/val/members.js +0 -6
  229. package/dist/val/members.js.map +1 -1
  230. package/dist/val/numcmp.js +0 -11
  231. package/dist/val/numcmp.js.map +1 -1
  232. package/dist/val/numkind.js +0 -145
  233. package/dist/val/numkind.js.map +1 -1
  234. package/dist/val/valutil.js +0 -16
  235. package/dist/val/valutil.js.map +1 -1
  236. package/dist/vet.js +0 -461
  237. package/dist/vet.js.map +1 -1
  238. package/dist/view.js +0 -414
  239. package/dist/view.js.map +1 -1
  240. package/dist/walk.js +0 -41
  241. package/dist/walk.js.map +1 -1
  242. package/grammar/aontu.abnf +8 -6
  243. package/grammar/aontu.gbnf +4 -4
  244. package/grammar/aontu.lark +4 -4
  245. package/grammar/aontu.tmLanguage.json +1 -1
  246. package/package.json +4 -2
  247. package/skill/tasks.md +9 -7
  248. package/src/agentsmd.ts +0 -35
  249. package/src/alias.ts +0 -39
  250. package/src/allow.ts +1 -96
  251. package/src/aontu.ts +4 -116
  252. package/src/aontumodel.ts +26 -0
  253. package/src/casing.ts +95 -0
  254. package/src/cli.ts +258 -1024
  255. package/src/ctx.ts +0 -103
  256. package/src/diff.ts +0 -40
  257. package/src/err.ts +0 -40
  258. package/src/escape.ts +0 -46
  259. package/src/exactjson.ts +0 -131
  260. package/src/format.ts +14 -257
  261. package/src/grammar.ts +72 -0
  262. package/src/graph.ts +0 -61
  263. package/src/hcanon.ts +0 -82
  264. package/src/helpdoc.ts +2 -2
  265. package/src/hints.ts +69 -57
  266. package/src/jsonschema.ts +0 -123
  267. package/src/keyorder.ts +0 -42
  268. package/src/lang.ts +19 -931
  269. package/src/lsp-server.ts +0 -16
  270. package/src/lsp.ts +12 -180
  271. package/src/mcp-server.ts +0 -31
  272. package/src/mcp.ts +0 -169
  273. package/src/mod-tool.ts +18 -159
  274. package/src/mod.ts +0 -178
  275. package/src/patch.ts +0 -232
  276. package/src/profile.ts +42 -0
  277. package/src/provenance.ts +0 -183
  278. package/src/query.ts +0 -84
  279. package/src/reach.ts +0 -53
  280. package/src/relation.ts +0 -84
  281. package/src/report-sarif.ts +0 -48
  282. package/src/sig.ts +0 -35
  283. package/src/sigdecl.ts +1 -1
  284. package/src/siggate.ts +0 -30
  285. package/src/site.ts +3 -29
  286. package/src/subsume.ts +1 -161
  287. package/src/template.ts +69 -140
  288. package/src/trace.ts +157 -0
  289. package/src/trim.ts +0 -53
  290. package/src/type.ts +2 -45
  291. package/src/unify.ts +14 -257
  292. package/src/utility.ts +0 -31
  293. package/src/val/AbnfFuncVal.ts +181 -0
  294. package/src/val/AbsentVal.ts +54 -0
  295. package/src/val/AggFuncVal.ts +152 -188
  296. package/src/val/ArithFuncVal.ts +0 -20
  297. package/src/val/BagVal.ts +1 -78
  298. package/src/val/BigDecimalVal.ts +0 -16
  299. package/src/val/BigIntegerVal.ts +0 -16
  300. package/src/val/CloseFuncVal.ts +0 -9
  301. package/src/val/CmpFuncVal.ts +68 -184
  302. package/src/val/ConjunctVal.ts +0 -33
  303. package/src/val/ConstraintVal.ts +2 -537
  304. package/src/val/ContainerKindVal.ts +0 -18
  305. package/src/val/CopyFuncVal.ts +0 -5
  306. package/src/val/Decimal.ts +1 -185
  307. package/src/val/DeprecateFuncVal.ts +0 -10
  308. package/src/val/DisjunctVal.ts +0 -157
  309. package/src/val/EachFuncVal.ts +0 -40
  310. package/src/val/EmitFuncVal.ts +8 -208
  311. package/src/val/ExpectVal.ts +0 -62
  312. package/src/val/FilterFuncVal.ts +0 -55
  313. package/src/val/FuncBaseVal.ts +9 -130
  314. package/src/val/GraphAtomVal.ts +0 -42
  315. package/src/val/HideFuncVal.ts +0 -15
  316. package/src/val/IntegerVal.ts +0 -61
  317. package/src/val/JunctionVal.ts +0 -20
  318. package/src/val/KeyFuncVal.ts +0 -48
  319. package/src/val/ListVal.ts +0 -59
  320. package/src/val/LowerFuncVal.ts +0 -12
  321. package/src/val/MapVal.ts +0 -151
  322. package/src/val/MatchFuncVal.ts +0 -59
  323. package/src/val/MaybeFuncVal.ts +86 -0
  324. package/src/val/MoveFuncVal.ts +0 -20
  325. package/src/val/NilVal.ts +0 -60
  326. package/src/val/NomFuncVal.ts +10 -99
  327. package/src/val/NumberVal.ts +0 -16
  328. package/src/val/OpBaseVal.ts +4 -17
  329. package/src/val/PackFuncVal.ts +0 -63
  330. package/src/val/PathFuncVal.ts +0 -32
  331. package/src/val/PathVal.ts +0 -66
  332. package/src/val/PlaceVal.ts +0 -45
  333. package/src/val/PlusOpVal.ts +18 -75
  334. package/src/val/PrefFuncVal.ts +0 -1
  335. package/src/val/PrefVal.ts +0 -179
  336. package/src/val/RecurseVal.ts +0 -81
  337. package/src/val/RefVal.ts +1 -285
  338. package/src/val/ReferFuncVal.ts +4 -255
  339. package/src/val/ScalarKindVal.ts +0 -50
  340. package/src/val/ScalarVal.ts +0 -12
  341. package/src/val/StrFuncVal.ts +0 -44
  342. package/src/val/SuperFuncVal.ts +0 -42
  343. package/src/val/TopVal.ts +0 -1
  344. package/src/val/TranslateFuncVal.ts +1 -51
  345. package/src/val/UpperFuncVal.ts +0 -12
  346. package/src/val/Val.ts +3 -192
  347. package/src/val/VarVal.ts +0 -15
  348. package/src/val/arith.ts +0 -92
  349. package/src/val/caserange.ts +0 -62
  350. package/src/val/members.ts +0 -23
  351. package/src/val/numcmp.ts +1 -27
  352. package/src/val/numkind.ts +0 -149
  353. package/src/val/valutil.ts +0 -16
  354. package/src/vet.ts +1 -582
  355. package/src/view.ts +0 -507
  356. package/src/walk.ts +0 -41
  357. package/dist/lower.d.ts +0 -23
  358. package/dist/lower.js +0 -578
  359. package/dist/lower.js.map +0 -1
  360. package/dist/render.d.ts +0 -53
  361. package/dist/render.js +0 -547
  362. package/dist/render.js.map +0 -1
  363. package/dist/std.d.ts +0 -3
  364. package/dist/std.js +0 -672
  365. package/dist/std.js.map +0 -1
  366. package/dist/val/FormFuncVal.js +0 -55
  367. package/dist/val/FormFuncVal.js.map +0 -1
  368. package/dist/val/NamerFuncVal.d.ts +0 -12
  369. package/dist/val/NamerFuncVal.js +0 -176
  370. package/dist/val/NamerFuncVal.js.map +0 -1
  371. package/src/lower.ts +0 -636
  372. package/src/render.ts +0 -732
  373. package/src/std.ts +0 -683
package/src/cli.ts CHANGED
@@ -1,12 +1,5 @@
1
1
  /* Copyright (c) 2025 Richard Rodger, MIT License */
2
2
 
3
- // Command-line interface for Aontu.
4
- //
5
- // aontu [options] [file]
6
- //
7
- // With a file argument, the file is evaluated and the result printed.
8
- // With no file on an interactive terminal, a REPL is started. With no
9
- // file and piped input, the source is read from stdin. See HELP below.
10
3
 
11
4
  // Named imports, not `import * as`: the namespace form makes tsc emit the
12
5
  import { evalFailure } from './query'
@@ -24,13 +17,14 @@ import {
24
17
  hcanon, canonHash,
25
18
  get, why, patch, agentsMd,
26
19
  allow,
27
- render,
28
- renderProfile,
20
+ loadProfile,
29
21
  } from './aontu'
30
22
  import type { AllowDecision, AllowReport, AllowVerdict } from './allow'
31
- import type { RenderCoverage, RenderReport } from './render'
32
- import { desugarTemplate, resugarTemplate, templateOutputs, markerFor } from './template'
33
- import { outsideRoot } from './mcp'
23
+ import { traceRun } from './trace'
24
+ import {
25
+ desugarTemplate, resugarTemplate, templateOutputs, markerFor,
26
+ markerFromProfiles,
27
+ } from './template'
34
28
  import { sarifReport } from './report-sarif'
35
29
  import { main as lspMain } from './lsp-server'
36
30
  import { main as mcpMain } from './mcp-server'
@@ -87,10 +81,10 @@ const HELP = `Usage: aontu [options] [file]
87
81
  aontu view <kind> [options] <file>...
88
82
  aontu view --views <path> [--check] [options] <file>
89
83
  aontu jsonschema [--at <path>] [--strict] [options] <file>
90
- aontu render [--at <path>] [--profile <file>]... [--unit <path>]
91
- [--stdout | --out <dir> | --check <dir> | --coverage]
92
- [--coverage-at <path>] [--strict] <file>
93
- aontu template [--resugar] [--check] [--marker <token>] <file>
84
+ aontu template [--resugar] [--check] [--marker <token>]
85
+ [--profile <file>] <file>
86
+ aontu trace [--at <path>] [--format json] [--marker <token>]
87
+ [--profile <file>] <file>
94
88
  aontu hash [options] <file>
95
89
  aontu mod tidy|verify|vendor|manifest [options] [dir]
96
90
  aontu get <path> [options] <file>
@@ -98,7 +92,8 @@ const HELP = `Usage: aontu [options] [file]
98
92
  aontu set <path>=<value>... --entry <file> --overlay <file>
99
93
  aontu allow --role <role> [--at <path>] <roles-file> <path>...
100
94
  aontu agentsmd [--write <AGENTS.md>] [--depth <n>] <file>
101
- aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>] <file>...
95
+ aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>]
96
+ [--profile <file>] <file>...
102
97
  aontu help [topic] [--format text|json]
103
98
  aontu explain <code> | --list [--format text|json]
104
99
  aontu init [dir]
@@ -335,36 +330,11 @@ View exit codes: 0 rendered, 1 --check mismatch or lossy under
335
330
  --strict, 2 usage or --max-rows exceeded, 4 the document does not stand
336
331
  up on its own, or a relation, root or path that names nothing.
337
332
 
338
- Render options:
339
- --at <path> Render the value at this path ($.a.b); the root by
340
- default
341
- --profile <file> A profile document, profile: {lang, ...}, vetted
342
- against aontu:profile; repeatable, one per language
343
- --unit <path> Render only the unit with this path
344
- --stdout One unit's bytes and nothing else (with --unit when
345
- the instance has several)
346
- --out <dir> Write every unit below dir, or nothing; never deletes
347
- --check <dir> Compare every unit with dir/<path>; drift is listed
348
- --coverage Report what the render read and what it did not:
349
- model paths no output consumed, and rendered
350
- declarations no rule produced. Writes nothing
351
- --coverage-at <p> Measure coverage under this path only, instead of
352
- the document root
353
- --strict Refuse the opaque escapes (a text declaration, a raw
354
- block)
355
- --format <f> text (default) or json, the whole report; json
356
- carries the dispatch trace, one entry per emitted
357
- piece
358
-
359
- Render exit codes: 0 rendered, 1 lossy under --strict or drift under
360
- --check, 2 usage or I/O (a refused unit path included), 4 the document
361
- does not stand up or the instance is not aontu:code.
362
-
363
- A render entry file whose extension is not .aon is a TEMPLATE: a
364
- generator in the target's own syntax, whose marker lines carry aontu
365
- and whose other lines are output. It is desugared before it is
366
- evaluated, and --marker names the marker for a language the table does
367
- not know.
333
+ A template entry file whose extension is not .aon is a GENERATOR: a
334
+ document in the target's own syntax, whose marker lines carry aontu and
335
+ whose other lines are output. It is desugared before it is evaluated,
336
+ and a language the table does not know names its marker with --marker,
337
+ or declares it once in a profile file that --profile reads.
368
338
 
369
339
  Template options:
370
340
  --resugar The file is the canonical aontu; print the template
@@ -372,7 +342,9 @@ Template options:
372
342
  --check Desugar and resugar, and exit 1 if the file is not
373
343
  what the round trip answers
374
344
  --marker <t> The marker, when the extension does not name it
375
- (default //-, and #- --- /*- by extension)
345
+ (default //-, and #- --- /*- <!--- by extension)
346
+ --profile <f> A profile file, whose template.ext names the
347
+ extensions it marks and template.marker the marker
376
348
 
377
349
  The template verb prints the canonical aontu form of a generator
378
350
  written in the target's own syntax: a marked line is aontu source, and
@@ -465,16 +437,18 @@ Fmt options:
465
437
  shapes, on standard error, and print nothing else
466
438
  --strict With --lint, and exit 1 when there is a finding
467
439
  --marker <t> The file is a generator, and this is its marker
468
- (default //-, and #- --- /*- by extension)
440
+ (default //-, and #- --- /*- <!--- by extension)
441
+ --profile <f> A profile file, whose template.ext names the
442
+ extensions it marks and template.marker the marker
469
443
 
470
444
  The fmt verb prints one document in the agreed form; with no file it
471
445
  reads standard input. Several files need one of the options above.
472
446
 
473
- A file whose extension is not .aon is a GENERATOR, as it is for render:
474
- the aontu its marker lines carry is formatted, the marker stands at the
475
- left margin with the aontu indented after it, and every line of output
476
- is held on a line of its own. A file with no marker line in it is
477
- another language's, and is refused.
447
+ A file whose extension is not .aon is a GENERATOR, as it is for
448
+ template: the aontu its marker lines carry is formatted, the marker
449
+ stands at the left margin with the aontu indented after it, and every
450
+ line of output is held on a line of its own. A file with no marker line
451
+ in it is another language's, and is refused.
478
452
 
479
453
  Fmt exit codes: 0 formatted or clean, 1 a --check file would change or
480
454
  a --strict finding, 2 usage, 4 a document does not parse.
@@ -512,32 +486,9 @@ function version(): string {
512
486
  }
513
487
 
514
488
 
515
- // The terminal colour escapes the parser puts in its message text. A
516
- // machine-readable report is no place for them, which is the rule
517
- // findingOf states in ts/src/vet.ts; the twin here rather than an
518
- // import because go/cmd/aontu carries its own for the same reason (the
519
- // engine's is not exported to its command).
520
489
  const EVAL_ANSI = new RegExp('\u001b\\[[0-9;]*m', 'g')
521
490
 
522
491
 
523
- // THE ENGINE'S DIAGNOSIS AS A FINDING (G11 phase 7). The bare command
524
- // was the one verb whose failure had no machine-readable form, so the
525
- // default entry point was the one an agent had to parse with a regular
526
- // expression.
527
- //
528
- // THE HEADLINE ONLY, and no `hint`. Both are parity decisions rather
529
- // than economies: the frames under the headline are drawn for a person
530
- // reading a terminal and only the first line is held to byte parity
531
- // between the ports (the rule findingOf states), and the hint TABLES
532
- // are deliberately not in parity while the code registry is -- so a
533
- // hint here would make the two ports answer differently for a code
534
- // only one of them explains. `aontu explain <code>` is where the hint
535
- // lives, which is what phase 3 built it for.
536
- //
537
- // The CLASS comes from the registry rather than from the nil, because
538
- // the registry is what both ports hold set-equal
539
- // (test/spec/errcodes.tsv). Mirrors evalFinding in
540
- // go/cmd/aontu/main.go.
541
492
  function evalFinding(code: string, text: string): VetFinding {
542
493
  return {
543
494
  class: codeClass(code),
@@ -545,10 +496,6 @@ function evalFinding(code: string, text: string): VetFinding {
545
496
  message: text.split('\n')[0].replace(EVAL_ANSI, ''),
546
497
  path: '$',
547
498
  severity: 'error',
548
- // NO SITE. The bare command's failure is the whole document not
549
- // standing up, and the two sites a conflict names are in the
550
- // frames the text form prints; naming one of them here would be a
551
- // choice the engine has not made.
552
499
  sites: [],
553
500
  }
554
501
  }
@@ -562,12 +509,6 @@ function evalSource(
562
509
  mode: Mode,
563
510
  ): { ok: boolean; text: string; findings: VetFinding[] } {
564
511
  try {
565
- // exactJSON, not JSON.stringify: a document using the `0d` exact
566
- // leaves generates bigints and Decimals, which JSON.stringify cannot
567
- // write (D9). The CLI prints INDENTED JSON and the shared suite's
568
- // `gens` mode prints COMPACT JSON, but both go through this one
569
- // emitter -- an indent argument rather than a second implementation,
570
- // so the two cannot drift from each other or from the Go port.
571
512
  const text = 'canon' === mode
572
513
  ? aontu.unify(src).canon
573
514
  : exactJSON(aontu.generate(src), 2)
@@ -577,13 +518,6 @@ function evalSource(
577
518
  const msg = (err instanceof AontuError || true === err?.aontu)
578
519
  ? err.message
579
520
  : String(err?.message ?? err)
580
- // WHAT THE ENGINE COLLECTED, when it collected anything: an
581
- // AontuError carries the NilVals the run failed on, already
582
- // materialised (handleErrors in ts/src/aontu.ts), and their first
583
- // is the diagnosis every other verb reports. An error raised
584
- // outside the engine's own collection -- exactJSON's circular
585
- // refusal, a foreign object claiming to be one -- carries none,
586
- // and answers with the text alone rather than an invented code.
587
521
  const errs: any[] = 'function' === typeof err?.errs ? err.errs() : []
588
522
  const first: any = errs[0]
589
523
  return {
@@ -619,23 +553,11 @@ function emitEval(
619
553
  }
620
554
 
621
555
 
622
- // The include capability the main verb runs with (G5, docs/trust.md).
623
- // `--trust` and `--include-root` set it explicitly; the default is
624
- // 'system' WITH the warning window: every resolution that escapes the
625
- // entry root or goes through package resolution prints a one-line
626
- // stderr warning naming the flag a future default will require
627
- // (phase 6, the staged flip).
628
556
  type TrustArg = (
629
557
  | { kind: 'system-warn' }
630
558
  | { kind: 'system' }
631
559
  | { kind: 'none' }
632
560
  | { kind: 'root', dir?: string }
633
- // EXTENSIONS READ AS TEXT ride with the capability rather than
634
- // beside it: both answer "what may an include read", both are
635
- // stripped by takeTrust before a verb parses its own tail, and a
636
- // verb that threads one and not the other is the G5 defect again --
637
- // `aontu vet` running under a flag the bare command honoured and it
638
- // did not.
639
561
  ) & { textExt: string[] }
640
562
 
641
563
 
@@ -679,16 +601,6 @@ function trustOpts(trust: TrustArg, entryRoot: string): any {
679
601
  }
680
602
 
681
603
 
682
- // EVERY VERB honours the include capability, not just the bare
683
- // command. G5 wired `--trust`/`--include-root` to `aontu <file>` alone,
684
- // so `aontu vet schema.aon data.json` -- the surface an agent actually
685
- // scripts -- ran the full system resolver with no flag to confine it
686
- // and no warning (use-cases/REVIEW.md finding G). The flags are
687
- // stripped here, before each verb parses its own tail, so a verb only
688
- // has to pass the profile on to its engine.
689
- //
690
- // Returns undefined when the spelling is wrong, with the message
691
- // already printed: the caller answers the usage class.
692
604
  function takeTrust(argv: string[]):
693
605
  { argv: string[], trust: TrustArg } | undefined {
694
606
  const rest: string[] = []
@@ -799,17 +711,6 @@ function runFile(
799
711
  src = readFileSync(file, 'utf8')
800
712
  }
801
713
  catch (err: any) {
802
- // A MISTYPED VERB READS AS A FILE NAME, and until G11 phase 2 that
803
- // was only said when there were TWO of them. The one-argument case
804
- // is the one an agent actually produces -- `aontu help`, `aontu
805
- // init`, `aontu ontology` -- and it answered `cannot read help:
806
- // ...`, which describes the symptom and hides the cause.
807
- //
808
- // The test is SHAPE, not existence: a bare word (no separator, no
809
- // extension) that cannot be read was meant as a verb, while
810
- // `./help`, `help.aon` and `/tmp/help` were meant as paths and keep
811
- // the file diagnosis and its exit 1. That is the same escape hatch
812
- // the subcommand dispatch documents. Mirrors go/cmd/aontu/main.go.
813
714
  if (looksLikeVerb(file)) {
814
715
  process.stderr.write(
815
716
  `aontu: \`${file}\` is not a file, and not a verb this port knows\n`)
@@ -826,12 +727,6 @@ function runFile(
826
727
  }
827
728
 
828
729
  const path = resolve(file)
829
- // `fs` IS WHAT MAKES A FRAME EXCERPT THE FILE IT NAMES. Without it,
830
- // err.ts's resolveSrc falls back to the ENTRY text, so a frame whose
831
- // arrow says `lib/types.aon:2:6` printed the entry's line 2 under it
832
- // -- a real file name over another file's line, which
833
- // docs/reference-api.md forbids in the same words it uses to require
834
- // the name.
835
730
  const aontu = new Aontu({
836
731
  path,
837
732
  errfs: { existsSync, readFileSync },
@@ -856,15 +751,6 @@ function runStdin(
856
751
  }
857
752
 
858
753
 
859
- // THE REPL AS AN INSPECTION TOOL (G7 phase 7): `:load` holds a
860
- // document, and `:get`, `:keys` and `:why` ask the query and
861
- // provenance surfaces about it, so the session is a place to
862
- // INTERROGATE a definition rather than only to evaluate snippets.
863
- //
864
- // The command handler is a PURE FUNCTION of (state, line): a readline
865
- // loop is untestable, and every answer this REPL gives has to be as
866
- // checkable as the CLI's. File reading is injected for the same
867
- // reason.
868
754
  export type ReplState = {
869
755
  // How a value renders: the `:canon` / `:json` toggle.
870
756
  mode: Mode
@@ -873,11 +759,6 @@ export type ReplState = {
873
759
  jsonl: boolean
874
760
  name?: string
875
761
  src?: string
876
- // The include capability the session evaluates under. `--trust` and
877
- // `--include-root` were parsed and then DROPPED on the way to the
878
- // REPL, so `--jsonl` -- the surface built to be driven by a harness
879
- // -- ran unconfined however it was invoked (use-cases/REVIEW.md
880
- // finding G). The state carries it, so every line honours it.
881
762
  trust?: TrustArg
882
763
  }
883
764
 
@@ -1032,14 +913,6 @@ function runRepl(initialMode: Mode, jsonl: boolean, trust: TrustArg): void {
1032
913
  })
1033
914
 
1034
915
  rl.on('close', () => {
1035
- // The closing newline is for a HUMAN, so it is written only for
1036
- // one: it moves the terminal off the prompt line that `rl` left
1037
- // hanging. In `--jsonl` there is no prompt, every answer already
1038
- // ends in its own newline, and this one appended a bare empty line
1039
- // to the stream -- a record that is not JSON, at the end of a
1040
- // protocol whose whole contract is one JSON object per line. A
1041
- // harness parsing every line it receives failed on it, after the
1042
- // commands had all succeeded. Mirrors go/cmd/aontu/repl.go.
1043
916
  if (!jsonl) {
1044
917
  process.stdout.write('\n')
1045
918
  }
@@ -1051,16 +924,6 @@ function runRepl(initialMode: Mode, jsonl: boolean, trust: TrustArg): void {
1051
924
  }
1052
925
 
1053
926
 
1054
-
1055
- // THE VET VERB (G2 phase 3).
1056
- //
1057
- // Exit codes are VERDICT CLASSES, not a pass/fail bit: an agent loop
1058
- // branches on "the data contradicts the truth" (1) differently from
1059
- // "the data has not supplied everything the truth requires" (3), and
1060
- // differently again from "the schema itself is broken" (4), which is
1061
- // never the data's fault. 2 stays what it already was for this CLI --
1062
- // the caller got the invocation wrong -- which is why an unreadable
1063
- // file is a 2 rather than a 4.
1064
927
  const VET_EXIT: Record<VetVerdict, number> = {
1065
928
  valid: 0,
1066
929
  invalid: 1,
@@ -1130,14 +993,6 @@ function parseVetArgs(argv: string[]): { args?: VetArgs; err?: string } {
1130
993
  format = f
1131
994
  }
1132
995
  else if ('--max-errors' === arg) {
1133
- // ONE GRAMMAR, spelled the same way in both ports: decimal
1134
- // digits, one to nine of them, at least 1. `Number()` alone
1135
- // accepted `1.0`, `1e2`, `0x10` and ` 3`, which Go's parser
1136
- // refuses -- so the same documented invocation meant different
1137
- // things in the two shipped commands. The nine-digit ceiling is
1138
- // where the ports would part company again: beyond it Go's
1139
- // integer conversion saturates, and a cap nobody can reach is
1140
- // not worth a divergence.
1141
996
  const raw = argv[++i]
1142
997
  if (!/^[0-9]{1,9}$/.test(raw ?? '') || 1 > Number(raw)) {
1143
998
  return { err: 'aontu: --max-errors needs a positive whole number' }
@@ -1220,10 +1075,6 @@ function renderFinding(f: VetFinding): string {
1220
1075
  out.push(` actual: ${f.actual}`)
1221
1076
  }
1222
1077
  for (const s of f.sites) {
1223
- // Every site carries the canon of the value it stands for: that is
1224
- // what makes the two sides of a conflict readable side by side. A
1225
- // site's file is always a string -- empty when the value belongs to
1226
- // neither document -- so there is nothing to coalesce here.
1227
1078
  out.push(` ${s.role}: ${s.file}:${s.row}:${s.col} (${s.value})`)
1228
1079
  }
1229
1080
 
@@ -1333,18 +1184,9 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
1333
1184
  return 2
1334
1185
  }
1335
1186
 
1336
- // Each data file is vetted on its own, because a parsed tree is
1337
- // single-use (docs/reference-api.md) -- and because two data files
1338
- // are two candidates for the same truth, not one merged candidate.
1339
1187
  let verdict: VetVerdict = 'valid'
1340
1188
  let truncated = false
1341
1189
  const findings: VetFinding[] = []
1342
- // COVERAGE ACROSS SEVERAL DATA FILES (G11 phase 5). Two data files
1343
- // are two candidates for one truth, so the schema side is the SAME
1344
- // for each: `declared` is taken once, and a declaration is unused
1345
- // only when NO file met it -- the intersection, because a
1346
- // declaration one file exercised is exercised. The data side adds
1347
- // up: leaves and checked leaves sum, and `unchecked` is the union.
1348
1190
  let cov: VetCoverage | undefined
1349
1191
  // Initialised rather than left undefined: it is filled in the same
1350
1192
  // block that sets `cov`, so a fallback at the read below would be an
@@ -1363,12 +1205,6 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
1363
1205
  maxErrors: args.maxErrors,
1364
1206
  schemaUrl: args.schema,
1365
1207
  dataUrl: source.file,
1366
- // The paths as well as the labels: a relative `@"file"` load
1367
- // inside either document resolves from ITS OWN directory, the
1368
- // way `aontu <file>` already resolves one (runFile above). The
1369
- // path is passed AS TYPED, not resolved: it doubles as the
1370
- // label above, and a report that mixed the typed path with an
1371
- // absolute one would name the same file two ways.
1372
1208
  schemaPath: args.schema,
1373
1209
  dataPath: source.file,
1374
1210
  coverage: args.coverage,
@@ -1400,27 +1236,11 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
1400
1236
  unusedSeen = true
1401
1237
  }
1402
1238
 
1403
- // A SCHEMA-SIDE FAULT IS THE SAME FAULT FOR EVERY DATA FILE, so it
1404
- // is reported ONCE. `error` means exactly that -- the run could not
1405
- // be set up from the truth's side, never the data's (the exit table
1406
- // in docs/reference-api.md) -- so the report the first file
1407
- // produced is the report every later file would produce, character
1408
- // for character. Concatenating them repeated one broken schema N
1409
- // times and, past the cap, marked the report `truncated` over a
1410
- // single underlying fault. It only became visible once the `error`
1411
- // verdict started carrying findings at all: while the list was
1412
- // empty there was nothing to duplicate.
1413
1239
  if ('error' === report.verdict) {
1414
1240
  break
1415
1241
  }
1416
1242
  }
1417
1243
 
1418
- // The cap is on the REPORT, not on each file. Capping every file's
1419
- // list and then concatenating them let `--max-errors 1` emit one
1420
- // finding PER FILE -- and leave `truncated` false while doing it,
1421
- // because no single file had been cut. The engine still caps each
1422
- // run, so a pathological file cannot flood the aggregate before it
1423
- // gets here; this is the second, honest cut.
1424
1244
  const cap = args.maxErrors ?? VET_MAX_ERRORS
1425
1245
  const kept = cap < findings.length ? findings.slice(0, cap) : findings
1426
1246
 
@@ -1442,12 +1262,6 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
1442
1262
 
1443
1263
  process.stdout.write(text + '\n')
1444
1264
 
1445
- // A VACUOUS CHECK IS A FAILED GATE UNDER `--strict-coverage`, and
1446
- // only under it: the verdict WORD is unchanged, so nothing that
1447
- // passes today starts failing, and a caller who wants the stronger
1448
- // gate asks for it. The reason goes to stderr, because stdout is a
1449
- // report contract -- a JSON consumer reads `coverage.vacuous` and a
1450
- // person reads this.
1451
1265
  if (true === args.strictCoverage && true === report.coverage?.vacuous) {
1452
1266
  process.stderr.write(
1453
1267
  'aontu: no data leaf was constrained by the schema:' +
@@ -1483,19 +1297,6 @@ function sleep(ms: number): Promise<void> {
1483
1297
  }
1484
1298
 
1485
1299
 
1486
- // Resolve true when any watched file's signature moves off `before`.
1487
- // This is the real waiter: it never resolves false, so a real watch
1488
- // runs until the process is interrupted; tests inject their own waiter
1489
- // to bound the loop, and pass a short pollMs when they drive this one
1490
- // directly. The interval is a required argument (the command passes
1491
- // WATCH_POLL_MS) so there is no defaulting branch a test could never
1492
- // take.
1493
- //
1494
- // The BASELINE is an argument, not a snapshot taken here: the loop
1495
- // records it BEFORE each vet run, so a save landing between the run's
1496
- // reads and the wait still compares as a change. A waiter that
1497
- // snapshotted on entry would adopt that unvetted save as its baseline
1498
- // and wait indefinitely on a stale report.
1499
1300
  async function watchChange(
1500
1301
  files: string[], before: string, pollMs: number): Promise<boolean> {
1501
1302
  for (;;) {
@@ -1535,9 +1336,6 @@ async function watchVet(
1535
1336
  }
1536
1337
 
1537
1338
 
1538
- // The vet verb. Non-watch runs are synchronous and return the exit
1539
- // class directly; `--watch` returns a promise that resolves only when
1540
- // the waiter says stop (never, for the real one).
1541
1339
  function runVet(argv: string[], wait?: VetWaiter): number | Promise<number> {
1542
1340
  const trusted = takeTrust(argv)
1543
1341
  if (null == trusted) {
@@ -1715,14 +1513,6 @@ type BreakingArgs = {
1715
1513
  file: string
1716
1514
  against: string[]
1717
1515
  mode?: BreakingMode
1718
- // `--at`: compare a SUBTREE of both versions. The gate's own
1719
- // sub-question, and the one a real repository needs -- a document's
1720
- // top level carries the module's version string and its policy
1721
- // block, which are supposed to change between releases and which
1722
- // make the whole-document comparison answer about them rather than
1723
- // about the contract (use-cases/REVIEW.md finding D). `subsume` has
1724
- // taken it since G3; `breaking` did not, so the only way to gate a
1725
- // subtree was to split the file.
1726
1516
  at?: string
1727
1517
  allowUndecided: boolean
1728
1518
  allowDeprecatedRemoval: boolean
@@ -1806,36 +1596,10 @@ function parseBreakingArgs(
1806
1596
  }
1807
1597
  }
1808
1598
 
1809
- // One resolved `--against` spelling: the old document's text, the path
1810
- // its own relative includes must resolve from, and (for a git spelling)
1811
- // the temporary tree to remove when the run is done.
1812
1599
  type OldVersion = { src: string, path: string, temp?: string }
1813
1600
 
1814
- // A source file the include resolver can actually load. `git#<rev>`
1815
- // materialises these and nothing else: an include names an Aontu
1816
- // document (`.aon`/`.aontu`, the two extensions `@"foo"` tries) or a
1817
- // JSON one, so the rest of a revision's tree cannot be part of any
1818
- // include closure and copying it would be pure cost.
1819
1601
  const INCLUDABLE = /\.(aon|aontu|jsonic|json)$/
1820
1602
 
1821
- // Resolve one --against spelling to an old version.
1822
- //
1823
- // A `git#<rev>` spelling is the old version of the WHOLE TREE, not of
1824
- // the entry file alone. It used to be `git show <rev>:./<file>`, whose
1825
- // text was then evaluated with `generalPath`/`specificPath` pointing at
1826
- // the WORKING file -- so every `@"..."` include in the old document
1827
- // resolved against the working tree, and the "old" side was old entry
1828
- // text meeting new includes. A breaking change inside an included file
1829
- // therefore compared against itself and answered `compatible`: the
1830
- // documented CI gate silently un-gated every non-entry file of the
1831
- // multi-file layout real models use (use-cases/BUGS.md §26). The old
1832
- // tree's includable sources are copied into a temporary directory and
1833
- // the old document is evaluated from THERE.
1834
- //
1835
- // Sources outside the revision -- package includes under node_modules,
1836
- // the bundled `std/system` -- still resolve as they do today: they are
1837
- // not in the tree, and their versions travel with the lockfile rather
1838
- // than with this comparison.
1839
1603
  function oldVersion(spec: string, file: string): OldVersion | undefined {
1840
1604
  if (!spec.startsWith('git#')) {
1841
1605
  try {
@@ -1867,24 +1631,10 @@ function oldVersion(spec: string, file: string): OldVersion | undefined {
1867
1631
  // that only some failures take.
1868
1632
  const temp = mkdtempSync(join(tmpdir(), 'aontu-against-'))
1869
1633
  try {
1870
- // THE REPO-RELATIVE PATH COMES FROM GIT, not from path arithmetic.
1871
- // Relativising `rev-parse --show-toplevel` against `resolve(file)`
1872
- // puts two DIFFERENT COORDINATE SYSTEMS on either side of the
1873
- // subtraction: git prints the real path, while the caller's is
1874
- // whatever they typed. On macOS a temp file under /var is
1875
- // /private/var to git, and on Windows a TMP short name
1876
- // (RUNNER~1) is the long form to git -- so the subtraction gave a
1877
- // `../..` climb, the entry was "not in that revision", and the
1878
- // documented CI spelling failed on both platforms while passing on
1879
- // Linux (this PR's own CI). `--show-prefix` is the same question
1880
- // asked in git's coordinates: the repo-relative directory of the
1881
- // cwd, already slash-separated and already normalised.
1882
1634
  const prefix = git(['rev-parse', '--show-prefix'], dir).trim()
1883
1635
  const entryRel = prefix + basename(file)
1884
1636
  const top = git(['rev-parse', '--show-toplevel'], dir).trim()
1885
1637
 
1886
- // `-z` so a path with a newline or a quote cannot be mistaken for
1887
- // two paths (git otherwise quotes such names).
1888
1638
  const listed = git(['ls-tree', '-r', '-z', '--name-only', rev], top)
1889
1639
  .split('\0').filter((p) => '' !== p)
1890
1640
  if (!listed.includes(entryRel)) {
@@ -1919,13 +1669,6 @@ function policyCompat(
1919
1669
  ): BreakingMode | undefined {
1920
1670
  const aontu = new Aontu()
1921
1671
  const ctx = aontu.ctx({ collect: true })
1922
- // The declaration is read by EVALUATING the document, so this leg
1923
- // runs the include resolver too and has to run it under BOTH of the
1924
- // verb's include options -- a `breaking --trust none` that read its
1925
- // own mode through an unconfined resolver would confine the
1926
- // comparison and not the question (use-cases/REVIEW.md finding G),
1927
- // and one that took the capability alone read no mode at all when
1928
- // the declaration arrived through a `--text-ext` include.
1929
1672
  const v: any = aontu.unify(newSrc, { path, ...includeOpts(include) }, ctx)
1930
1673
  if (0 < ctx.err.length || true === v?.isNil) {
1931
1674
  return undefined
@@ -1945,11 +1688,6 @@ function policyCompat(
1945
1688
  ? m : undefined
1946
1689
  }
1947
1690
 
1948
- // Is the evaluated old version's value at the finding path deprecated?
1949
- // The --allow-deprecated-removal downgrade (G3 phase 4): removing (or
1950
- // otherwise changing) a value the old version already deprecated warns
1951
- // instead of breaking. The Go port exports the same reader as
1952
- // aontu.DeprecatedAt.
1953
1691
  function deprecatedAt(oldSrc: string, path: string, filePath: string): boolean {
1954
1692
  const aontu = new Aontu()
1955
1693
  const ctx = aontu.ctx({ collect: true })
@@ -2024,9 +1762,6 @@ function runBreaking(argv: string[]): number {
2024
1762
  return 2
2025
1763
  }
2026
1764
 
2027
- // The declared mode: --mode overrides the document's own policy;
2028
- // neither means backward, the index's framing (v1-valid documents
2029
- // stay valid).
2030
1765
  const mode: BreakingMode =
2031
1766
  args.mode ??
2032
1767
  policyCompat(newSrc, args.file,
@@ -2066,8 +1801,6 @@ function runBreaking(argv: string[]): number {
2066
1801
  temps.push(old.temp)
2067
1802
  }
2068
1803
 
2069
- // backward: the NEW document is the general side — every old
2070
- // instance must still be admitted. forward: the old one is.
2071
1804
  const checks: Array<{ general: [string, string], specific: [string, string] }> = []
2072
1805
  if ('backward' === mode || 'full' === mode) {
2073
1806
  checks.push({ general: [newSrc, args.file], specific: [oldSrc, spec] })
@@ -2084,18 +1817,10 @@ function runBreaking(argv: string[]): number {
2084
1817
  at: args.at,
2085
1818
  generalUrl: check.general[1],
2086
1819
  specificUrl: check.specific[1],
2087
- // The old side's relative loads resolve from ITS own tree --
2088
- // the materialised revision for a git spelling, the named
2089
- // file's directory otherwise -- so an included file's change
2090
- // is part of the comparison rather than invisible to it.
2091
1820
  generalPath: check.general[1] === spec ? oldPath : args.file,
2092
1821
  specificPath: check.specific[1] === spec ? oldPath : args.file,
2093
1822
  })
2094
1823
 
2095
- // The deprecated-removal downgrade: a finding about a value the
2096
- // OLD version already deprecated becomes a warning, and warnings
2097
- // do not move the verdict. Deprecate-then-remove is the
2098
- // supported rename path (the design's own sequencing).
2099
1824
  let verdict = report.verdict
2100
1825
  if (args.allowDeprecatedRemoval) {
2101
1826
  let liveFindings = 0
@@ -2153,13 +1878,6 @@ function renderBreakingJson(report: SubsumeReport, mode: string): string {
2153
1878
  }
2154
1879
 
2155
1880
 
2156
- // ---------------------------------------------------------------------
2157
- // The trim reporter (G3 phase 6): report redundant entries as paths.
2158
- // Report-only — REWRITING needs G7's format-preserving patch surface —
2159
- // which is why --check is REQUIRED rather than defaulted: `aontu trim
2160
- // f.aon` reads as "trim this file", and doing something else silently
2161
- // is worse than saying so.
2162
-
2163
1881
  const TRIM_HELP = 'aontu trim --check <file> (try --help)'
2164
1882
 
2165
1883
  const TRIM_EXIT: Record<TrimVerdict, number> = {
@@ -2237,8 +1955,6 @@ function runTrim(argv: string[]): number {
2237
1955
 
2238
1956
  function renderTrimText(report: TrimReport): string {
2239
1957
  const head = `verdict: ${report.verdict}`
2240
- // WHY, when the document could not be evaluated at all: rendered as
2241
- // vet renders a finding, because it IS one (the review's finding F).
2242
1958
  const errors = report.errors ?? []
2243
1959
  if (0 < errors.length) {
2244
1960
  return [head, ''].concat(errors.map(renderFinding)).join('\n')
@@ -2303,29 +2019,12 @@ const VIEW_EDGES: ViewEdges[] = ['upward', 'all', 'none']
2303
2019
  // the same division err.ts already draws for the error frames.
2304
2020
  const VIEW_STYLES = ['auto', 'none', 'ansi', 'css']
2305
2021
 
2306
- // `--style auto` resolved, which only the CLI can do. The mechanism is
2307
- // the PROFILE's and the library knows it -- an SVG carries its
2308
- // stylesheet unless told not to, which is what makes a figure stand
2309
- // alone. What the library cannot know is whether the DESTINATION is a
2310
- // terminal, so that is the only thing decided here: escapes on the
2311
- // text profile when stdout is a terminal and NO_COLOR is unset, the
2312
- // same two conditions the error frames use. `undefined` leaves the
2313
- // profile's own default in place.
2314
2022
  function viewStyleOf(
2315
2023
  asked: string | undefined, as: ViewProfile | undefined
2316
2024
  ): ViewStyle | undefined {
2317
2025
  if (undefined !== asked && 'auto' !== asked) {
2318
2026
  return asked as ViewStyle
2319
2027
  }
2320
- // STDOUT'S OWN TERMINAL-NESS, and NO_COLOR read here rather than
2321
- // through colorActive(). The figure goes to STDOUT and the error
2322
- // frames go to STDERR, and they are not the same destination: main()
2323
- // has already called setColor for stderr, so asking colorActive()
2324
- // would answer the wrong question twice --- no escapes for
2325
- // `aontu view tree m.aon 2>/dev/null` at a terminal, and escapes
2326
- // into the pipe for `aontu view tree m.aon | less`. The NO_COLOR
2327
- // rule is the one no-color.org states and err.ts implements:
2328
- // set, to anything but empty, means no colour.
2329
2028
  const no = process.env.NO_COLOR
2330
2029
  return 'text' === as && true === process.stdout.isTTY
2331
2030
  && (null == no || '' === no) ? 'ansi' : undefined
@@ -2351,25 +2050,13 @@ const VIEW_USAGE_CODES = [
2351
2050
 
2352
2051
  const MOD_HELP = 'aontu mod tidy|verify|vendor|manifest [dir] (try --help)'
2353
2052
 
2354
- // The module tooling (G6 phase 3, ts/src/mod-tool.ts). All LOCAL:
2355
- // `tidy` resolves the closure from what is in the stores and rewrites
2356
- // the lockfile, `verify` asks whether the stores still mean what the
2357
- // lockfile pins and changes nothing, `vendor` materialises the locked
2358
- // closure into the project, `manifest` prints what a publish would
2359
- // push.
2360
- //
2361
- // TIDY AND VERIFY ARE DIFFERENT QUESTIONS, and that is why both exist.
2362
- // Tidy recomputes and rewrites by design -- a pin is what a module
2363
- // means NOW -- so it makes the lockfile agree with whatever the store
2364
- // holds, tampering included. Verify is the gate: a CI job runs it
2365
- // BEFORE tidy, or instead of it.
2366
- //
2367
- // `get` and `publish` are the NETWORK half of the design and are not in
2368
- // this build. They are named here rather than left to fall out as an
2369
- // unknown subcommand, because a reader of the design will type them and
2370
- // deserves to be told which half is missing rather than that the word
2371
- // is wrong.
2372
2053
  function runMod(argv: string[]): number {
2054
+ const trusted = takeTrust(argv)
2055
+ if (null == trusted) {
2056
+ return 2
2057
+ }
2058
+ argv = trusted.argv
2059
+ const trust = trusted.trust
2373
2060
  const rest: string[] = []
2374
2061
  let format: SubsumeFormat = 'text'
2375
2062
  let against: string | undefined
@@ -2422,10 +2109,6 @@ function runMod(argv: string[]): number {
2422
2109
  return 2
2423
2110
  }
2424
2111
 
2425
- // THE OLD LAYOUT IS NAMED, NOT READ. The lockfile and the vendored
2426
- // closure moved under aontu_meta/; a project that still carries them
2427
- // at its root would otherwise look untouched by any of these verbs,
2428
- // which is the one silence worth breaking.
2429
2112
  if (existsSync(join(dir, 'aon_vendor')) || existsSync(join(dir, 'mod-lock.aon'))) {
2430
2113
  process.stderr.write(
2431
2114
  'aontu: aon_vendor/ and mod-lock.aon now live under aontu_meta/: ' +
@@ -2439,11 +2122,13 @@ function runMod(argv: string[]): number {
2439
2122
  return 2
2440
2123
  }
2441
2124
 
2125
+ const modopts = modToolOptions(trust, resolve(dir))
2126
+
2442
2127
  const report =
2443
- 'tidy' === sub ? modTidy(dir, modToolOptions()) :
2444
- 'verify' === sub ? modVerify(dir, modToolOptions()) :
2445
- 'vendor' === sub ? modVendor(dir, modToolOptions()) :
2446
- modManifest(dir, modToolOptions(), against)
2128
+ 'tidy' === sub ? modTidy(dir, modopts) :
2129
+ 'verify' === sub ? modVerify(dir, modopts) :
2130
+ 'vendor' === sub ? modVendor(dir, modopts) :
2131
+ modManifest(dir, modopts, against)
2447
2132
 
2448
2133
  process.stdout.write(('json' === format ?
2449
2134
  exactJSON({ aontu: { version: version(), verb: 'mod ' + sub }, ...report },
@@ -2469,9 +2154,6 @@ type ModVerdict =
2469
2154
  const MOD_EXIT: Record<ModVerdict, number> = {
2470
2155
  ok: 0,
2471
2156
  missing: 1,
2472
- // A REFUSED GATE, with `breaking`: a store that no longer means what
2473
- // the lockfile pins is the integrity check saying no, and a CI job
2474
- // reading exit codes should not have to learn a third class for it.
2475
2157
  mismatch: 1,
2476
2158
  // Likewise a lockfile that does not cover the project: the gate has
2477
2159
  // nothing to check, which is a refusal and not a pass.
@@ -2485,11 +2167,16 @@ const MOD_EXIT: Record<ModVerdict, number> = {
2485
2167
  // The tooling's evaluator: the same standalone evaluation the module
2486
2168
  // resolver verifies with (ts/src/mod.ts), and for the same reason —
2487
2169
  // only the engine can say what a module MEANS.
2488
- function modToolOptions() {
2170
+ function modToolOptions(trust: TrustArg, entryRoot: string) {
2171
+ const opts = verbOpts(trust, entryRoot)
2172
+ // The user cache lives outside any confinement root, so a confined
2173
+ // run reads the vendor tree only -- as the evaluator's own module
2174
+ // leg already does when a root is set.
2175
+ const rooted = null != (opts.trust as any)?.include?.root
2489
2176
  return {
2490
- cache: modCacheDir(),
2177
+ ...(rooted ? {} : { cache: modCacheDir() }),
2491
2178
  eval: (src: string, path: string) => {
2492
- const a0 = new Aontu()
2179
+ const a0 = new Aontu(opts)
2493
2180
  const ctx = a0.ctx({ collect: true })
2494
2181
  const val: any = a0.unify(src, { path }, ctx)
2495
2182
  return {
@@ -2522,11 +2209,6 @@ function modText(sub: string, report: any): string {
2522
2209
  for (const f of report.findings) {
2523
2210
  lines.push(f.path + ': ' + f.message)
2524
2211
  }
2525
- // What a manifest lacks is a declaration the module does not make
2526
- // or an entry file that is not there, and neither is something a
2527
- // fetch would supply -- so this is not the tail the other two
2528
- // subcommands share. The name says which kind it is: `mod.version`
2529
- // is a declaration, `service.aon` is a file.
2530
2212
  for (const miss of report.missing) {
2531
2213
  lines.push(miss + ': missing')
2532
2214
  }
@@ -2537,8 +2219,6 @@ function modText(sub: string, report: any): string {
2537
2219
  for (const mod of report.verified) {
2538
2220
  lines.push(mod + ': verified')
2539
2221
  }
2540
- // BOTH HASHES, because the useful question is which way it moved:
2541
- // an empty `got` is a module that no longer stands up at all.
2542
2222
  for (const m of report.mismatched) {
2543
2223
  lines.push(m.mod + ': pinned ' + m.want + ' but the store means ' +
2544
2224
  ('' === m.got ? 'nothing (it does not evaluate)' : m.got))
@@ -2574,26 +2254,6 @@ function modText(sub: string, report: any): string {
2574
2254
  }
2575
2255
 
2576
2256
 
2577
- // VACUITY SIGNALS (G11 phase 4,
2578
- // docs/capability-review/g11-agent-onramp.md).
2579
- //
2580
- // The same principle phase 5 applied to `vet`: a verb that did NOTHING
2581
- // and a verb that did its job answer the same. `aontu view tree` over a
2582
- // document declaring no relations printed one newline and exited 0;
2583
- // `aontu render` with no profile printed nothing and exited 0; `aontu
2584
- // relations` over a document declaring none answered `verdict: pass`.
2585
- // For a person at a terminal that is a shrug. For an unattended agent
2586
- // it is a green check mark on an empty box.
2587
- //
2588
- // ON STDERR, ALWAYS. stdout is a report contract -- a `--format json`
2589
- // consumer parses it -- and the exit code is a verdict class that
2590
- // callers already branch on. Neither changes here: what changes is
2591
- // that the caller is TOLD. A caller who wants it to be fatal has
2592
- // `vet --strict-coverage`, and the same argument would give the other
2593
- // verbs a flag of their own if one is ever asked for.
2594
- //
2595
- // The repository already ruled this for one verb, in G8 phase 6 on
2596
- // `trim`: "doing something else silently is worse than refusing".
2597
2257
  function vacuous(what: string, why: string): void {
2598
2258
  process.stderr.write(`aontu: ${what}: ${why}\n`)
2599
2259
  }
@@ -2665,6 +2325,120 @@ function runRelations(argv: string[]): number {
2665
2325
  return RELATIONS_EXIT[report.verdict]
2666
2326
  }
2667
2327
 
2328
+ const TRACE_HELP =
2329
+ 'aontu trace [--at <path>] [--format json] [--marker <token>] ' +
2330
+ '[--profile <file>] <file>'
2331
+
2332
+
2333
+ // WHAT WROTE THIS LINE. Every piece a rule stamped, under the
2334
+ // component tree, with the file it reached, the rule set that wrote it
2335
+ // and the model node the dispatch matched.
2336
+ function runTrace(argv: string[]): number {
2337
+ const trusted = takeTrust(argv)
2338
+ if (null == trusted) {
2339
+ return 2
2340
+ }
2341
+ argv = trusted.argv
2342
+ const trust = trusted.trust
2343
+ const rest: string[] = []
2344
+ const profileFiles: string[] = []
2345
+ let format: 'text' | 'json' = 'text'
2346
+ let at: string | undefined = undefined
2347
+ let marker: string | undefined = undefined
2348
+
2349
+ for (let i = 0; i < argv.length; i++) {
2350
+ const arg = argv[i]
2351
+ if ('-h' === arg || '--help' === arg) {
2352
+ process.stdout.write(HELP)
2353
+ return 0
2354
+ }
2355
+ if ('--format' === arg) {
2356
+ const f = argv[++i]
2357
+ if ('text' !== f && 'json' !== f) {
2358
+ process.stderr.write('aontu: --format needs text or json\n')
2359
+ return 2
2360
+ }
2361
+ format = f
2362
+ }
2363
+ else if ('--at' === arg) {
2364
+ at = argv[++i]
2365
+ if (null == at) {
2366
+ process.stderr.write('aontu: --at needs a path\n')
2367
+ return 2
2368
+ }
2369
+ }
2370
+ else if ('--marker' === arg) {
2371
+ marker = argv[++i]
2372
+ if (null == marker) {
2373
+ process.stderr.write('aontu: --marker needs a token\n')
2374
+ return 2
2375
+ }
2376
+ }
2377
+ else if ('--profile' === arg) {
2378
+ const pf = argv[++i]
2379
+ if (null == pf) {
2380
+ process.stderr.write('aontu: --profile needs a file\n')
2381
+ return 2
2382
+ }
2383
+ profileFiles.push(pf)
2384
+ }
2385
+ else if (arg.startsWith('-')) {
2386
+ process.stderr.write(`aontu: unknown trace option ${arg} (try --help)\n`)
2387
+ return 2
2388
+ }
2389
+ else {
2390
+ rest.push(arg)
2391
+ }
2392
+ }
2393
+
2394
+ if (1 !== rest.length) {
2395
+ process.stderr.write(`aontu: trace needs one file\n${TRACE_HELP}\n`)
2396
+ return 2
2397
+ }
2398
+
2399
+ let src: string
2400
+ try {
2401
+ src = readFileSync(rest[0], 'utf8')
2402
+ }
2403
+ catch (err: any) {
2404
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
2405
+ return 2
2406
+ }
2407
+
2408
+ const declared = loadProfiles(profileFiles, trust)
2409
+ if ('number' === typeof declared) {
2410
+ return declared
2411
+ }
2412
+
2413
+ // A GENERATOR IS AN ENTRY, not a preprocessing step: the file whose
2414
+ // provenance is asked for is the one the author edits.
2415
+ if (!rest[0].endsWith('.aon')) {
2416
+ src = desugarTemplate(src, marker ??
2417
+ markerFromProfiles(declared, rest[0]) ?? markerFor(rest[0]))
2418
+ }
2419
+
2420
+ const report = traceRun(src, {
2421
+ path: rest[0], at,
2422
+ ...verbOpts(trust, entryRootOf(rest[0])),
2423
+ })
2424
+ if ('error' === report.verdict) {
2425
+ // An error report always carries its findings.
2426
+ const errors = report.errors as VetFinding[]
2427
+ process.stderr.write(errors.map(renderFinding).join('\n') + '\n')
2428
+ return 4
2429
+ }
2430
+ if ('json' === format) {
2431
+ process.stdout.write(JSON.stringify({ trace: report.trace }) + '\n')
2432
+ return 0
2433
+ }
2434
+ for (const e of report.trace) {
2435
+ process.stdout.write(
2436
+ [e.file, e.at, e.node, e.rule].join('\t') + '\n')
2437
+ }
2438
+ return 0
2439
+ }
2440
+
2441
+
2668
2442
  function runReaches(argv: string[]): number {
2669
2443
  const trusted = takeTrust(argv)
2670
2444
  if (null == trusted) {
@@ -2881,12 +2655,6 @@ function runView(argv: string[]): number {
2881
2655
  }
2882
2656
  }
2883
2657
 
2884
- // ESCAPES NEVER GO INTO A FILE. A pinned golden holding terminal
2885
- // control codes is not a golden anybody can read, and a byte
2886
- // comparison against one would fail on the reader's terminal
2887
- // settings. `auto` resolves to `none` there on its own; asking for
2888
- // `ansi` explicitly is a usage error rather than a silent downgrade,
2889
- // so a script that wanted colour is told where it went.
2890
2658
  if ('ansi' === style && (undefined !== out || undefined !== opts.views)) {
2891
2659
  process.stderr.write(
2892
2660
  'aontu: --style ansi writes to a terminal, not to a file\n')
@@ -2896,10 +2664,6 @@ function runView(argv: string[]): number {
2896
2664
  // THE VIEW DOCUMENT draws every figure a document declares, so it
2897
2665
  // names no kind: the declarations do, one each.
2898
2666
  if (undefined !== opts.views) {
2899
- // A declaration names its own profile, so the style is left to
2900
- // each figure's own default; `--style none` still reaches every
2901
- // one of them, which is how a host page that binds the CSS
2902
- // variables asks for eight figures without eight stylesheets.
2903
2667
  opts.style = viewStyleOf(style, undefined)
2904
2668
  return runViewSet(rest, opts, trust, { format, check, strict, out })
2905
2669
  }
@@ -2975,17 +2739,6 @@ function runView(argv: string[]): number {
2975
2739
  }
2976
2740
  const report = view(srcs[0], viewOpts)
2977
2741
 
2978
- // AN EMPTY FIGURE IS THE SAME BYTES AS A DRAWN ONE MINUS ITS
2979
- // CONTENT, and every profile spells "empty" differently: text draws
2980
- // nothing at all, mermaid still draws its `flowchart LR` header, the
2981
- // matrix still prints its count line. Rather than teach this one
2982
- // place each of those spellings -- a list that goes stale the first
2983
- // time a profile gains a header -- ASK THE SAME KIND TO DRAW AN
2984
- // EMPTY DOCUMENT and compare. Equal texts mean this document
2985
- // contributed nothing to the figure, whatever the profile.
2986
- //
2987
- // It costs one drawing of `{}`, which is the cheapest document
2988
- // there is, and only on a run that produced a figure at all.
2989
2742
  if ('error' !== report.verdict && null != report.text) {
2990
2743
  const bare = view('{}', viewOpts)
2991
2744
  if ('error' !== bare.verdict && bare.text === report.text) {
@@ -3039,13 +2792,6 @@ function runView(argv: string[]): number {
3039
2792
  return strict && 'lossy' === report.verdict ? 1 : VIEW_EXIT[report.verdict]
3040
2793
  }
3041
2794
 
3042
- // `aontu view --views <path> <file>`: every figure the document
3043
- // declares, from one evaluation, all or nothing.
3044
- //
3045
- // A declared `out` is resolved against the DOCUMENT's own directory,
3046
- // not the caller's: a view document is committed beside the figures it
3047
- // gates, and a gate that only passes from one working directory is not
3048
- // a gate.
3049
2795
  function runViewSet(
3050
2796
  rest: string[], opts: ViewOptions, trust: TrustArg,
3051
2797
  how: { format: SubsumeFormat, check: boolean, strict: boolean, out?: string }
@@ -3186,8 +2932,6 @@ function renderViewJson(report: ViewReport): string {
3186
2932
 
3187
2933
  function renderRelationsText(report: RelationReport): string {
3188
2934
  const head = `verdict: ${report.verdict}`
3189
- // WHY, when the document could not be evaluated at all: rendered as
3190
- // vet renders a finding, because it IS one (the review's finding F).
3191
2935
  const errors = report.errors ?? []
3192
2936
  if (0 < errors.length) {
3193
2937
  return [head, ''].concat(errors.map(renderFinding)).join('\n')
@@ -3213,21 +2957,6 @@ function renderRelationsJson(report: RelationReport): string {
3213
2957
  }
3214
2958
 
3215
2959
 
3216
-
3217
- // ---------------------------------------------------------------------
3218
- // JSON SCHEMA EXPORT (SUPPORT.md act 2, the review's finding I): the
3219
- // bridge to every structured-output API, which constrains generation to
3220
- // JSON Schema and nothing else. Export the model, let the provider
3221
- // generate under it, then `vet` the result against the model itself --
3222
- // the hybrid an enterprise actually deploys, and impossible without
3223
- // this verb.
3224
- //
3225
- // THE SCHEMA GOES TO STDOUT AND THE LOSSES TO STDERR, so `aontu
3226
- // jsonschema x.aon > schema.json` writes a schema and still tells the
3227
- // reader what it could not carry. `--strict` makes a loss a refusal,
3228
- // for the CI job that would rather fail than ship a schema weaker than
3229
- // its model.
3230
-
3231
2960
  const JSONSCHEMA_HELP =
3232
2961
  'aontu jsonschema [--at <path>] [--strict] <file> (try --help)'
3233
2962
 
@@ -3323,358 +3052,22 @@ function runJsonSchema(argv: string[]): number {
3323
3052
  strict && 'lossy' === report.verdict ? 1 : 0
3324
3053
  }
3325
3054
 
3326
- // ---------------------------------------------------------------------
3327
- // THE RENDER VERB (docs/design/RENDER.0.md D8): evaluate a document,
3328
- // vet the value at --at against aontu:code, fold code.units into bytes,
3329
- // and put them where the flag says -- one unit on stdout, every unit
3330
- // below --out (all or nothing), or compared against --check. Exit codes
3331
- // mirror jsonschema's: 0 ok; 1 lossy under --strict or drift under
3332
- // --check; 2 usage or I/O, a refused unit path included; 4 the
3333
- // document does not stand up or the instance is not aontu:code.
3334
-
3335
- const RENDER_HELP =
3336
- 'aontu render [--at <path>] [--profile <file>]... [--unit <path>] ' +
3337
- '[--stdout | --out <dir> | --check <dir> | --coverage] ' +
3338
- '[--coverage-at <path>] [--strict] [--marker <token>] <file> (try --help)'
3339
-
3340
- function runRender(argv: string[]): number {
3055
+
3056
+ const TEMPLATE_HELP =
3057
+ 'aontu template [--resugar] [--check] [--marker <token>] <file> (try --help)'
3058
+
3059
+ function runTemplate(argv: string[]): number {
3341
3060
  const trusted = takeTrust(argv)
3342
3061
  if (null == trusted) {
3343
3062
  return 2
3344
3063
  }
3345
3064
  argv = trusted.argv
3346
3065
  const trust = trusted.trust
3347
- const files: string[] = []
3348
- const profileFiles: string[] = []
3349
- let format: SubsumeFormat = 'text'
3350
- let at: string | undefined = undefined
3351
- let unit: string | undefined = undefined
3352
- let out: string | undefined = undefined
3353
- let check: string | undefined = undefined
3354
- let toStdout = false
3355
- let strict = false
3356
- let coverage = false
3357
- let coverageAt: string | undefined = undefined
3358
- let marker: string | undefined = undefined
3359
-
3360
- for (let i = 0; i < argv.length; i++) {
3361
- const arg = argv[i]
3362
- if ('-h' === arg || '--help' === arg) {
3363
- process.stdout.write(HELP)
3364
- return 0
3365
- }
3366
- if ('--format' === arg) {
3367
- const f = argv[++i]
3368
- if ('text' !== f && 'json' !== f) {
3369
- process.stderr.write('aontu: --format needs text or json\n')
3370
- return 2
3371
- }
3372
- format = f
3373
- }
3374
- else if ('--at' === arg) {
3375
- at = argv[++i]
3376
- if (null == at) {
3377
- process.stderr.write('aontu: --at needs a path\n')
3378
- return 2
3379
- }
3380
- }
3381
- else if ('--unit' === arg) {
3382
- unit = argv[++i]
3383
- if (null == unit) {
3384
- process.stderr.write('aontu: --unit needs a unit path\n')
3385
- return 2
3386
- }
3387
- }
3388
- else if ('--profile' === arg) {
3389
- const pf = argv[++i]
3390
- if (null == pf) {
3391
- process.stderr.write('aontu: --profile needs a file\n')
3392
- return 2
3393
- }
3394
- profileFiles.push(pf)
3395
- }
3396
- else if ('--out' === arg) {
3397
- out = argv[++i]
3398
- if (null == out) {
3399
- process.stderr.write('aontu: --out needs a directory\n')
3400
- return 2
3401
- }
3402
- }
3403
- else if ('--check' === arg) {
3404
- check = argv[++i]
3405
- if (null == check) {
3406
- process.stderr.write('aontu: --check needs a directory\n')
3407
- return 2
3408
- }
3409
- }
3410
- else if ('--stdout' === arg) {
3411
- toStdout = true
3412
- }
3413
- else if ('--coverage' === arg) {
3414
- coverage = true
3415
- }
3416
- else if ('--marker' === arg) {
3417
- marker = argv[++i]
3418
- if (null == marker) {
3419
- process.stderr.write('aontu: --marker needs a token\n')
3420
- return 2
3421
- }
3422
- }
3423
- else if ('--coverage-at' === arg) {
3424
- coverageAt = argv[++i]
3425
- if (null == coverageAt) {
3426
- process.stderr.write('aontu: --coverage-at needs a path\n')
3427
- return 2
3428
- }
3429
- }
3430
- else if ('--strict' === arg) {
3431
- strict = true
3432
- }
3433
- else if (arg.startsWith('-')) {
3434
- process.stderr.write(
3435
- `aontu: unknown render option ${arg} (try --help)\n`)
3436
- return 2
3437
- }
3438
- else {
3439
- files.push(arg)
3440
- }
3441
- }
3442
-
3443
- if (1 !== files.length) {
3444
- process.stderr.write(`aontu: render needs one file\n${RENDER_HELP}\n`)
3445
- return 2
3446
- }
3447
- const modes = [toStdout, undefined !== out, undefined !== check, coverage]
3448
- .filter((on) => on).length
3449
- if (1 < modes) {
3450
- process.stderr.write(
3451
- 'aontu: render takes one of --stdout, --out, --check or --coverage\n')
3452
- return 2
3453
- }
3454
- // A NARROWER MEASURE NEEDS SOMETHING TO NARROW. `--coverage-at`
3455
- // without `--coverage` asks for a report the run does not compute,
3456
- // and answering silently would be the wrong half of the request.
3457
- if (undefined !== coverageAt && !coverage) {
3458
- process.stderr.write('aontu: --coverage-at needs --coverage\n')
3459
- return 2
3460
- }
3461
-
3462
- let src: string
3463
- try {
3464
- src = readFileSync(files[0], 'utf8')
3465
- }
3466
- catch (err: any) {
3467
- process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
3468
- return 2
3469
- }
3470
-
3471
- // THE ENTRY MAY BE A TEMPLATE (TEMPLATE.0.md; P8), and its EXTENSION
3472
- // decides, as an include's extension decides what the include is
3473
- // (ADR-012): a generator is a file in the target's own syntax, so it
3474
- // carries the target's extension and never `.aon`. Desugared here
3475
- // rather than anywhere deeper, because a template is an entry
3476
- // spelling and not a value: an include is still aontu.
3477
- if (!files[0].endsWith('.aon')) {
3478
- src = desugarTemplate(src, marker ?? markerFor(files[0]))
3479
- }
3480
-
3481
- // THE PROFILES (D5): each --profile file is a document whose root is
3482
- // `profile: {lang, ...}`, evaluated under the verb's trust and vetted
3483
- // against aontu:profile as a settled value before the fold reads it
3484
- // (renderProfile, which also fills the defaults). Two files claiming
3485
- // one lang is a usage error: the fold could not choose.
3486
- const profiles: any[] = []
3487
- const langs = new Map<string, string>()
3488
- for (const pf of profileFiles) {
3489
- let text: string
3490
- try {
3491
- text = readFileSync(pf, 'utf8')
3492
- }
3493
- catch (err: any) {
3494
- process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
3495
- return 2
3496
- }
3497
- const loaded = renderProfile(text,
3498
- { path: resolve(pf), ...verbOpts(trust, entryRootOf(pf)) })
3499
- if (undefined !== loaded.errors) {
3500
- process.stderr.write(loaded.errors.map(renderFinding).join('\n') + '\n')
3501
- return 4
3502
- }
3503
- const profile = loaded.profile
3504
- const prev = langs.get(profile.lang)
3505
- if (undefined !== prev) {
3506
- process.stderr.write(
3507
- `aontu: two profiles claim ${profile.lang}: ${prev} and ${pf}\n`)
3508
- return 2
3509
- }
3510
- langs.set(profile.lang, pf)
3511
- profiles.push(profile)
3512
- }
3513
-
3514
- // A RENDER WITH NO PROFILE PRODUCES NO UNITS, and said so with zero
3515
- // bytes and exit 0. The profile is what maps a model onto a
3516
- // language, so without one there is nothing for the renderer to
3517
- // write -- which is a usable answer only if the caller is told.
3518
- const noProfiles = 0 === profiles.length
3519
- const report = render(src, {
3520
- at, unit, strict, profiles, path: files[0],
3521
- coverage, coverageAt,
3522
- // THE JSON REPORT CARRIES THE TRACE (D9), which is what the shape
3523
- // there has always said; a text run computes it only when the
3524
- // coverage report needs it.
3525
- trace: 'json' === format,
3526
- ...verbOpts(trust, entryRootOf(files[0])),
3527
- })
3528
-
3529
- // Said once, whatever the format: stdout stays the report.
3530
- if ('error' !== report.verdict && 0 === report.units.length) {
3531
- vacuous('nothing was rendered',
3532
- noProfiles
3533
- ? 'no profile was given, and the document declares none' +
3534
- ' (see aontu help tasks)'
3535
- : 'the document produced no units under this profile')
3536
- }
3537
-
3538
- if ('json' === format) {
3539
- process.stdout.write(exactJSON({
3540
- aontu: { version: version(), verb: 'render' },
3541
- verdict: report.verdict,
3542
- units: report.units,
3543
- lossy: report.lossy,
3544
- ...(null == report.errors ? {} : { errors: report.errors }),
3545
- ...(null == report.trace ? {} : { trace: report.trace }),
3546
- ...(null == report.coverage ? {} : { coverage: report.coverage }),
3547
- }, 2) + '\n')
3548
- return renderExit(report, 0)
3549
- }
3550
- if ('error' === report.verdict) {
3551
- process.stderr.write(
3552
- (report.errors as VetFinding[]).map(renderFinding).join('\n') + '\n')
3553
- return renderExit(report, 0)
3554
- }
3555
-
3556
- let drift = 0
3557
- if (toStdout) {
3558
- // ONE UNIT'S BYTES AND NOTHING ELSE, so the output can be piped
3559
- // into a formatter or a file.
3560
- if (1 !== report.units.length) {
3561
- process.stderr.write(
3562
- 'aontu: --stdout needs exactly one unit, and the instance has ' +
3563
- `${report.units.length}; --unit names one\n`)
3564
- return 2
3565
- }
3566
- process.stdout.write(report.units[0].text)
3567
- }
3568
- else if (undefined !== out) {
3569
- // EVERY UNIT BELOW <dir>, OR NOTHING: every unit rendered first
3570
- // (the report above), and no file touched unless all did. The
3571
- // directory is realpath-confined; a unit path is already a relative
3572
- // descent (render_path refuses the rest), and the check here is
3573
- // against the symlink inside it. render never deletes.
3574
- for (const u of report.units) {
3575
- if (outsideRoot(out, resolve(out, u.path))) {
3576
- process.stderr.write(`aontu: ${u.path} escapes ${out}\n`)
3577
- return 2
3578
- }
3579
- }
3580
- for (const u of report.units) {
3581
- const full = resolve(out, u.path)
3582
- try {
3583
- mkdirSync(dirname(full), { recursive: true })
3584
- writeFileSync(full, u.text, 'utf8')
3585
- }
3586
- catch (err: any) {
3587
- process.stderr.write(`aontu: cannot write ${u.path}: ${err.message}\n`)
3588
- return 2
3589
- }
3590
- process.stderr.write(`wrote ${u.path}\n`)
3591
- }
3592
- }
3593
- else if (undefined !== check) {
3594
- // RENDER AND COMPARE: a unit whose bytes differ from the file at
3595
- // <dir>/<path>, or whose file is absent, is drift, listed by path.
3596
- // The CI form.
3597
- for (const u of report.units) {
3598
- let have: string | undefined = undefined
3599
- try {
3600
- have = readFileSync(resolve(check, u.path), 'utf8')
3601
- }
3602
- catch {
3603
- // Absent is drift, reported below.
3604
- }
3605
- if (undefined === have) {
3606
- drift++
3607
- process.stderr.write(`aontu: ${u.path} is missing from ${check}\n`)
3608
- }
3609
- else if (have !== u.text) {
3610
- drift++
3611
- process.stderr.write(`aontu: ${u.path} differs from the rendered unit\n`)
3612
- }
3613
- }
3614
- }
3615
- else if (coverage) {
3616
- // THE COVERAGE REPORT (P7), one line per finding and a count at
3617
- // the end: dead model first, then the declarations no rule
3618
- // produced. A clean report is the count line alone.
3619
- const cov = report.coverage as RenderCoverage
3620
- for (const d of cov.dead) {
3621
- process.stdout.write(`dead: ${d}\n`)
3622
- }
3623
- for (const u of cov.unruled) {
3624
- process.stdout.write(`unruled: ${u.unit} ${u.path}\n`)
3625
- }
3626
- process.stdout.write(
3627
- `coverage: ${cov.read.length} path(s) read, ${cov.dead.length} ` +
3628
- `no output consumed, ${cov.unruled.length} declaration(s) ` +
3629
- 'no rule produced\n')
3630
- }
3631
- else {
3632
- // THE SUMMARY: one line per unit -- its path, its language and its
3633
- // size -- since several units have no one text to print.
3634
- for (const u of report.units) {
3635
- process.stdout.write(`${u.path}\t${u.lang}\t${u.text.length} bytes\n`)
3636
- }
3637
- }
3638
- for (const l of report.lossy) {
3639
- process.stderr.write(
3640
- `lossy: ${l.unit} ${l.path} tier ${l.tier} ${l.construct}: ${l.reason}\n`)
3641
- }
3642
- return renderExit(report, drift)
3643
- }
3644
-
3645
- // D8's exit table over a report: a refused unit path is usage (2), a
3646
- // strict refusal is lossy (1), any other error is the document's (4);
3647
- // drift under --check is 1.
3648
- function renderExit(report: RenderReport, drift: number): number {
3649
- if ('error' === report.verdict) {
3650
- const errors = report.errors as VetFinding[]
3651
- if (errors.every((f) => 'render_path' === f.code)) {
3652
- return 2
3653
- }
3654
- if (errors.every((f) => 'render_strict' === f.code)) {
3655
- return 1
3656
- }
3657
- return 4
3658
- }
3659
- return 0 < drift ? 1 : 0
3660
- }
3661
-
3662
-
3663
- // ---------------------------------------------------------------------
3664
- // THE TEMPLATE SURFACE (docs/design/TEMPLATE.0.md; RENDER.0.md P8): the
3665
- // two transforms and the round trip between them. `render` reads a
3666
- // template directly, by its extension; this verb is for seeing the
3667
- // canonical form, for writing one by hand and sugaring it, and for the
3668
- // check that keeps a committed template and its meaning in agreement.
3669
-
3670
- const TEMPLATE_HELP =
3671
- 'aontu template [--resugar] [--check] [--marker <token>] <file> (try --help)'
3672
-
3673
- function runTemplate(argv: string[]): number {
3674
3066
  const files: string[] = []
3675
3067
  let resugar = false
3676
3068
  let check = false
3677
3069
  let marker: string | undefined = undefined
3070
+ const profileFiles: string[] = []
3678
3071
 
3679
3072
  for (let i = 0; i < argv.length; i++) {
3680
3073
  const arg = argv[i]
@@ -3695,6 +3088,14 @@ function runTemplate(argv: string[]): number {
3695
3088
  return 2
3696
3089
  }
3697
3090
  }
3091
+ else if ('--profile' === arg) {
3092
+ const pf = argv[++i]
3093
+ if (null == pf) {
3094
+ process.stderr.write('aontu: --profile needs a file\n')
3095
+ return 2
3096
+ }
3097
+ profileFiles.push(pf)
3098
+ }
3698
3099
  else if (arg.startsWith('-')) {
3699
3100
  process.stderr.write(
3700
3101
  `aontu: unknown template option ${arg} (try --help)\n`)
@@ -3709,10 +3110,6 @@ function runTemplate(argv: string[]): number {
3709
3110
  process.stderr.write(`aontu: template needs one file\n${TEMPLATE_HELP}\n`)
3710
3111
  return 2
3711
3112
  }
3712
- // THE TWO ARE DIRECTIONS, NOT MODES THAT COMPOSE: `--check` reads a
3713
- // template and asks whether the round trip answers it back, and
3714
- // `--resugar` reads the canonical form instead. A run cannot be both
3715
- // at once, because the file is one thing or the other.
3716
3113
  if (resugar && check) {
3717
3114
  process.stderr.write(
3718
3115
  'aontu: template takes one of --resugar or --check\n')
@@ -3728,19 +3125,15 @@ function runTemplate(argv: string[]): number {
3728
3125
  return 2
3729
3126
  }
3730
3127
 
3731
- const mark = marker ?? markerFor(files[0])
3128
+ const declared = loadProfiles(profileFiles, trust)
3129
+ if ('number' === typeof declared) {
3130
+ return declared
3131
+ }
3132
+
3133
+ const mark = marker ?? markerFromProfiles(declared, files[0]) ??
3134
+ markerFor(files[0])
3732
3135
 
3733
3136
  if (check) {
3734
- // THE ROUND TRIP IS THE CHECK (D6): the file held to the spelling
3735
- // the two transforms answer. What that names is a marker line the
3736
- // transform would not have written -- one without its space, or one
3737
- // whose aontu is indented after the marker rather than before it,
3738
- // since the marker keeps its own indentation. It does NOT name a
3739
- // changed body line: a template's whitespace is output, so a
3740
- // trimmed trailing space is still a valid template and it is
3741
- // `render --check` against the committed files that catches it.
3742
- // The first line that differs is the report, since a whole diff of
3743
- // a generator is the file again.
3744
3137
  const back = resugarTemplate(desugarTemplate(src, mark), mark)
3745
3138
  if (back === src) {
3746
3139
  return 0
@@ -3751,12 +3144,6 @@ function runTemplate(argv: string[]): number {
3751
3144
  while (n < want.length && n < have.length && want[n] === have[n]) {
3752
3145
  n++
3753
3146
  }
3754
- // THE TWO ARE THE SAME LENGTH, always: each transform maps one
3755
- // line to one line and applies the same trailing-newline rule, so
3756
- // `back` has as many lines as `src`. The loop above therefore stops
3757
- // at a real difference rather than by running out of either -- an
3758
- // equal prefix all the way to the end IS `back === src`, which
3759
- // returned above. So both indexes are in range here.
3760
3147
  process.stderr.write(
3761
3148
  `aontu: ${files[0]}:${n + 1} is not what the round trip answers\n` +
3762
3149
  ` have: ${JSON.stringify(have[n])}\n` +
@@ -3770,13 +3157,43 @@ function runTemplate(argv: string[]): number {
3770
3157
  }
3771
3158
 
3772
3159
 
3773
- // ---------------------------------------------------------------------
3774
- // The canon-hash (G6 phase 1): the pin an agent, a lockfile or a
3775
- // registry stores for "this module, this meaning". The hash covers the
3776
- // module evaluated STANDALONE -- its own include closure resolved and
3777
- // unified at its own root, before any consumer context -- which is what
3778
- // makes the pin transitive: an edit two includes deep changes the
3779
- // unified root, hence the hash.
3160
+ // The profiles named by --profile, vetted, or the exit code that says
3161
+ // why not. A profile is a language declared as data: `template` and
3162
+ // `fmt` match one to a file by the extensions its `template.ext`
3163
+ // names.
3164
+ function loadProfiles(
3165
+ profileFiles: string[], trust: TrustArg
3166
+ ): any[] | number {
3167
+ const profiles: any[] = []
3168
+ const langs = new Map<string, string>()
3169
+ for (const pf of profileFiles) {
3170
+ let text: string
3171
+ try {
3172
+ text = readFileSync(pf, 'utf8')
3173
+ }
3174
+ catch (err: any) {
3175
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
3176
+ return 2
3177
+ }
3178
+ const loaded = loadProfile(text,
3179
+ { path: resolve(pf), ...verbOpts(trust, entryRootOf(pf)) })
3180
+ if (undefined !== loaded.errors) {
3181
+ process.stderr.write(loaded.errors.map(renderFinding).join('\n') + '\n')
3182
+ return 4
3183
+ }
3184
+ const profile = loaded.profile
3185
+ const prev = langs.get(profile.lang)
3186
+ if (undefined !== prev) {
3187
+ process.stderr.write(
3188
+ `aontu: two profiles claim ${profile.lang}: ${prev} and ${pf}\n`)
3189
+ return 2
3190
+ }
3191
+ langs.set(profile.lang, pf)
3192
+ profiles.push(profile)
3193
+ }
3194
+ return profiles
3195
+ }
3196
+
3780
3197
 
3781
3198
  const HASH_HELP = 'aontu hash <file> (try --help)'
3782
3199
 
@@ -3837,14 +3254,6 @@ function runHash(argv: string[]): number {
3837
3254
  const ctx = aontu.ctx({ collect: true })
3838
3255
  const v: any = aontu.unify(src, { path: files[0] }, ctx)
3839
3256
  if (0 < ctx.err.length || true === v?.isNil) {
3840
- // A document that does not stand up on its own has no meaning to
3841
- // pin, and a hash of a broken evaluation would be a pin that
3842
- // silently agrees with every other broken evaluation.
3843
- // WHY it does not stand up, not just that it does not: the same
3844
- // diagnosis `aontu <file>` prints (the review's finding F).
3845
- // evalFailure unconditionally, as every other call site does: it
3846
- // owns the "ctx.err is never empty here" contract, and a guard
3847
- // that pretends otherwise is a dead arm asserting nothing.
3848
3257
  process.stderr.write(
3849
3258
  `aontu: ${files[0]} does not evaluate on its own; nothing to hash\n` +
3850
3259
  renderFinding(evalFailure(ctx)) + '\n')
@@ -3863,13 +3272,6 @@ function runHash(argv: string[]): number {
3863
3272
  }
3864
3273
 
3865
3274
 
3866
- // ---------------------------------------------------------------------
3867
- // The query surface (G7 phase 1): one node of an evaluated document,
3868
- // selected by path and rendered. Evaluation is still GLOBAL -- what
3869
- // `get` buys is the size of the ANSWER, not the cost of producing it --
3870
- // and the projections are lattice abstractions, each a valid Aontu
3871
- // document that subsumes the truth it summarises.
3872
-
3873
3275
  const GET_HELP = 'aontu get <path> <file> (try --help)'
3874
3276
 
3875
3277
  function runGet(argv: string[]): number {
@@ -4057,11 +3459,6 @@ function runWhy(argv: string[]): number {
4057
3459
  }
4058
3460
 
4059
3461
 
4060
- // One contribution per line, numbered in source order, each with what
4061
- // was written, where, and how it got here. A siteless contribution
4062
- // prints no location rather than a `-1:-1` that means nothing —
4063
- // exported for the direct test, because the site SHAPE allows one
4064
- // while no document has yet produced one (ADR-002).
4065
3462
  function renderWhyText(record: WhyRecord): string {
4066
3463
  const head = `${record.path} = ${record.value}`
4067
3464
  if (0 === record.conjuncts.length) {
@@ -4079,13 +3476,6 @@ function renderWhyText(record: WhyRecord): string {
4079
3476
  }
4080
3477
 
4081
3478
 
4082
- // ---------------------------------------------------------------------
4083
- // The overlay patch verb (G7 phase 5): change a document by APPENDING
4084
- // to an overlay, not by rewriting it. An overlay entry is just another
4085
- // conjunct and unification is order-independent, so this needs no
4086
- // rewriter — the format-preserving in-place edit is stage 2, and needs
4087
- // a comment-preserving CST the parser stack does not have.
4088
-
4089
3479
  const SET_HELP =
4090
3480
  'aontu set <path>=<value> --entry <file> --overlay <file> (try --help)'
4091
3481
 
@@ -4202,29 +3592,12 @@ function runSet(argv: string[]): number {
4202
3592
  }, 2) + '\n')
4203
3593
  }
4204
3594
  else {
4205
- // A replacement is REPORTED as the edit it is, not left for the
4206
- // reader to infer from a changed file: `where: what -> what`, in
4207
- // source spelling, because the spelling is what changed.
4208
- //
4209
- // PAST TENSE ONLY WHERE IT HAPPENED. A refused write leaves the
4210
- // file exactly as it was, and one assignment can be replaceable
4211
- // while another makes the whole run invalid — so `replaced:` there
4212
- // tells an operator the pin was changed when it was not, and unlike
4213
- // `--dry-run` there is nothing else on the line to say otherwise.
4214
3595
  const verb = wrote ? 'replaced' : 'would replace'
4215
3596
  const edits = report.replaced.map((r) =>
4216
3597
  `${verb}: ${r.file}:${r.row}:${r.col} ${r.from} -> ${r.to}`)
4217
3598
  const head = [`verdict: ${report.verdict}`].concat(edits).join('\n') +
4218
3599
  (wrote ? `\nwrote: ${overlayFile}` : dryRun ? '\n(dry run)' : '')
4219
3600
 
4220
- // A SUCCESSFUL COMMAND WRITES ITS STATUS TO STDOUT, findings or
4221
- // not. Routing on `findings.length` was right while every finding
4222
- // this verb could produce was an ERROR; `--in-place` made a WARNING
4223
- // possible, and a run that held, wrote the file and exited 0 then
4224
- // sent its whole report to stderr — leaving stdout empty, so
4225
- // `$(aontu set ...)` captured nothing and only the JSON form
4226
- // behaved like a success. The verdict decides the stream; warnings
4227
- // are diagnostics and go to stderr beside it.
4228
3601
  const failed = 'invalid' === report.verdict || 'error' === report.verdict
4229
3602
  const findingText = report.findings.map(renderFinding)
4230
3603
  if (failed) {
@@ -4246,15 +3619,6 @@ function runSet(argv: string[]): number {
4246
3619
  }
4247
3620
 
4248
3621
 
4249
- // ---------------------------------------------------------------------
4250
- // The role gate (docs/design/ALLOW.0.md): may the role the caller is
4251
- // operating under modify these subtrees? Asked before `set`, by an
4252
- // agent whose skill names its role, and answered from a role model
4253
- // that is itself an aontu document. The verdict is the exit code, as
4254
- // it is for every gate here: 0 is yes, 1 is no, 4 is "the model that
4255
- // was to decide does not stand up", and an agent branches on nothing
4256
- // else.
4257
-
4258
3622
  const ALLOW_HELP =
4259
3623
  'aontu allow --role <role> <roles-file> <path> [more-paths...] (try --help)'
4260
3624
 
@@ -4372,14 +3736,6 @@ function runAllow(argv: string[]): number {
4372
3736
  return 2
4373
3737
  }
4374
3738
 
4375
- // A path may arrive in `set`'s spelling, `$.a.b=1`, so a skill can
4376
- // hand the gate the very arguments the write will get. The text up
4377
- // to the first `=` is the path, and it starts with `$`: an empty
4378
- // argument, or a second file name, would otherwise read as a path
4379
- // and be answered. The VALUE is checked to be one value. `set`
4380
- // appends it as source after the flattened path, so a value carrying
4381
- // a second pair -- `3 secrets: key: "x"` -- writes a sibling of the
4382
- // overlay root, a subtree the gate was never asked about.
4383
3739
  const paths: string[] = []
4384
3740
  for (const arg of asked) {
4385
3741
  const eq = arg.indexOf('=')
@@ -4535,35 +3891,23 @@ function runAgentsMd(argv: string[]): number {
4535
3891
  }
4536
3892
 
4537
3893
 
4538
- // Exit without truncating output.
4539
- //
4540
- // process.exit() terminates immediately, discarding anything still
4541
- // queued on stdout. A write to a PIPE is asynchronous once it exceeds
4542
- // the pipe buffer, so `write(big); exit(0)` silently truncated output at
4543
- // 65536 bytes — while a write to a TTY or a file, being synchronous,
4544
- // looked fine. Setting exitCode instead lets the process end naturally,
4545
- // after the queue drains.
4546
- //
4547
- // This predates the exact leaves but they make it trivially reachable
4548
- // (one long biginteger canon exceeds the buffer), and it lands squarely
4549
- // on the parity-probe discipline in AGENTS.md, which derives expected
4550
- // spec values by piping BOTH CLIs and comparing. A truncated pipe there
4551
- // reads as a port divergence.
4552
- // ---------------------------------------------------------------------
4553
- // The source formatter (docs/design/FMT.0.md): one agreed form, in the
4554
- // tradition of gofmt. The verb prints, lists, checks, diffs or rewrites;
4555
- // the form itself is the library's (ts/src/format.ts), and the two
4556
- // ports agree on it row by row in test/spec/fmt.tsv.
4557
-
4558
3894
  const FMT_HELP =
4559
- 'aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>] <file>... (try --help)'
3895
+ 'aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>] ' +
3896
+ '[--profile <file>] <file>... (try --help)'
4560
3897
 
4561
3898
  type FmtFlags = {
4562
3899
  write: boolean, list: boolean, check: boolean, diff: boolean, lint: boolean, strict: boolean,
4563
3900
  }
4564
3901
 
4565
3902
  function runFmt(argv: string[]): number | Promise<number> {
3903
+ const trusted = takeTrust(argv)
3904
+ if (null == trusted) {
3905
+ return 2
3906
+ }
3907
+ argv = trusted.argv
3908
+ const trust = trusted.trust
4566
3909
  const files: string[] = []
3910
+ const profileFiles: string[] = []
4567
3911
  let marker: string | undefined = undefined
4568
3912
  const flags: FmtFlags = {
4569
3913
  write: false, list: false, check: false, diff: false, lint: false, strict: false,
@@ -4596,14 +3940,22 @@ function runFmt(argv: string[]): number | Promise<number> {
4596
3940
  }
4597
3941
  else if ('--marker' === arg) {
4598
3942
  // THE MARKER SAYS THE FILE IS A GENERATOR, whatever its
4599
- // extension: `render` and `template` take the same option for
4600
- // the same reason, a language the table has never seen.
3943
+ // extension: `fmt` and `template` take the same option for the
3944
+ // same reason, a language the table has never seen.
4601
3945
  marker = argv[++i]
4602
3946
  if (null == marker) {
4603
3947
  process.stderr.write('aontu: --marker needs a token\n')
4604
3948
  return 2
4605
3949
  }
4606
3950
  }
3951
+ else if ('--profile' === arg) {
3952
+ const pf = argv[++i]
3953
+ if (null == pf) {
3954
+ process.stderr.write('aontu: --profile needs a file\n')
3955
+ return 2
3956
+ }
3957
+ profileFiles.push(pf)
3958
+ }
4607
3959
  else if (arg.startsWith('-')) {
4608
3960
  process.stderr.write(`aontu: unknown fmt option ${arg} (try --help)\n`)
4609
3961
  return 2
@@ -4629,6 +3981,11 @@ function runFmt(argv: string[]): number | Promise<number> {
4629
3981
  })
4630
3982
  }
4631
3983
 
3984
+ const declared = loadProfiles(profileFiles, trust)
3985
+ if ('number' === typeof declared) {
3986
+ return declared
3987
+ }
3988
+
4632
3989
  // Several files onto standard output would be one stream nobody can
4633
3990
  // split again (the note's X-6): the verb refuses unless an option
4634
3991
  // says what to do with each.
@@ -4649,13 +4006,14 @@ function runFmt(argv: string[]): number | Promise<number> {
4649
4006
  process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
4650
4007
  return 2
4651
4008
  }
4652
- const mark = fmtMarker(file, src, marker)
4009
+ const mark = fmtMarker(file, src,
4010
+ marker ?? markerFromProfiles(declared, file))
4653
4011
  if (false === mark) {
4654
4012
  process.stderr.write(
4655
4013
  `aontu: ${file} is not aontu source (.aon, .aontu) and carries no ` +
4656
4014
  `${markerFor(file)} marker line, so there is no aontu in it to ` +
4657
- 'format; --marker names the marker for a language the table does ' +
4658
- 'not know\n')
4015
+ 'format; --marker names the marker for a language the table ' +
4016
+ 'does not know, and --profile reads one that declares it\n')
4659
4017
  return 2
4660
4018
  }
4661
4019
  worst = Math.max(worst, fmtOne(file, src, flags, mark))
@@ -4663,20 +4021,6 @@ function runFmt(argv: string[]): number | Promise<number> {
4663
4021
  return worst
4664
4022
  }
4665
4023
 
4666
- // WHAT A FILE IS, BY ITS EXTENSION (ADR-012's rule, and the one
4667
- // `render` reads an entry by): `.aon` and `.aontu` are aontu source,
4668
- // and anything else is a GENERATOR written in the target's own syntax
4669
- // (docs/design/TEMPLATE.0.md), whose marker lines carry the document
4670
- // this formats and whose other lines are output. `undefined` is aontu,
4671
- // a string is the generator's marker, and `false` is neither.
4672
- //
4673
- // A FILE WITH NO MARKER LINE IN IT IS NEITHER, and that is what keeps
4674
- // FMT.0.md §9's boundary where it stood: a `.json`, `.yaml` or `.toml`
4675
- // include is another language's file, and reading one as a generator
4676
- // would answer it back unchanged having understood none of it. The
4677
- // marker is the evidence that a file was written to carry aontu at
4678
- // all. `--marker` says so outright, and then the file is a generator
4679
- // whatever it is called.
4680
4024
  function fmtMarker(
4681
4025
  file: string, src: string, marker: string | undefined): string | undefined | false {
4682
4026
  if (undefined !== marker) {
@@ -4738,26 +4082,13 @@ function fmtOne(
4738
4082
  }
4739
4083
 
4740
4084
 
4741
- // ---------------------------------------------------------------------
4742
- // The servers as verbs: `aontu lsp` runs the language server and
4743
- // `aontu mcp` the MCP server over the CLI's own streams, so the one
4744
- // command on PATH is the editor's and the agent's server too, and a
4745
- // version manager (docs/design/ENV.0.md) has one thing to resolve. The
4746
- // standalone bins (aontu-lsp, aontu-mcp) run the same functions. Each
4747
- // server owns its exit; the CLI only dispatches. The pair is a
4748
- // parameter of main so that a test can see the dispatch without a
4749
- // server taking the test's own stdin.
4750
-
4751
4085
  type Servers = {
4752
4086
  lsp: () => void
4753
4087
  mcp: (argv: string[]) => void
4754
4088
  }
4755
4089
 
4756
- // The real pair takes the process's own stdin and stdout, which no
4757
- // in-process test can lend it; the executable-entry tests in
4758
- // cli.test.ts run each through a child process instead, so these two
4759
- // lines are excluded from the in-process count, as the stdio wiring
4760
- // of lsp-server.ts is.
4090
+ // Excluded: the real pair takes the process stdio, so ts/test/cli.test.ts
4091
+ // drives each server through a child process instead.
4761
4092
  /* node:coverage ignore next 4 */
4762
4093
  const SERVERS: Servers = {
4763
4094
  lsp: () => void lspMain(),
@@ -4804,20 +4135,6 @@ function parseTrustArg(value: string): TrustArg | undefined {
4804
4135
  }
4805
4136
 
4806
4137
 
4807
- // THE TEACHING PACK, SERVED FROM THE COMMAND (G11 phase 1,
4808
- // docs/capability-review/g11-agent-onramp.md; mirrors
4809
- // go/cmd/aontu/help.go).
4810
- //
4811
- // HELP documents the TOOLCHAIN and says nothing about the LANGUAGE:
4812
- // `&`, the map template and the one construct an ontology cannot be
4813
- // written without, occurs zero times in it, while `template` occurs
4814
- // fourteen times and names an unrelated verb every time. docs/skill/
4815
- // was already the right content and already gated; the gap was
4816
- // DELIVERY, since it reached an installation as
4817
- // node_modules/aontu/skill/ where nothing looks. ts/src/helpdoc.ts is
4818
- // generated from those sources by ts/scripts/helpdoc.cjs and asserted
4819
- // byte-identical with them by ts/test/helpdoc.test.ts.
4820
-
4821
4138
  const HELP_VERB_HELP = 'aontu help [topic] (try `aontu help` for the topics)'
4822
4139
  const EXPLAIN_HELP = 'aontu explain <code> (try `aontu explain --list`)'
4823
4140
 
@@ -4903,23 +4220,6 @@ function runHelp(argv: string[]): number {
4903
4220
  }
4904
4221
 
4905
4222
 
4906
- // `aontu explain <code>` (G11 phase 3; mirrors
4907
- // go/cmd/aontu/explain.go).
4908
- //
4909
- // THE REGISTRY IS THE LIST, NOT THE HINT TABLE. test/spec/errcodes.tsv
4910
- // registers 157 codes and the spec suite asserts set equality between
4911
- // the file and codeClasses IN BOTH PORTS, so listing from codeClasses
4912
- // is listing the shared contract. The hint tables are smaller and are
4913
- // NOT in parity -- 130 entries here against 131 in Go, the extra being
4914
- // decimal_syntax, which this port never raises -- so listing from them
4915
- // would make `aontu explain --list` differ between ports over a
4916
- // difference that is not about what either port can report.
4917
- //
4918
- // A REGISTERED CODE WITH NO HINT ANSWERS WITH ITS CLASS AND SAYS SO.
4919
- // Twenty-seven registered codes carry no explanation text here; before
4920
- // this verb their absence was invisible, because a hint is only ever
4921
- // seen beside the error that raises it.
4922
-
4923
4223
  // The dynamic prefixes a generated code extends (`func:upper`,
4924
4224
  // `op[+]`). Mirrors CODE_PREFIXES in ts/src/hints.ts, which is not
4925
4225
  // exported; a code that extends one is registered through its prefix
@@ -5062,21 +4362,6 @@ function runExplain(argv: string[]): number {
5062
4362
  }
5063
4363
 
5064
4364
 
5065
- // `aontu init` (G11 phase 6,
5066
- // docs/capability-review/g11-agent-onramp.md).
5067
- //
5068
- // NOT SCAFFOLDING CONVENIENCE. The agent's most expensive failure is
5069
- // writing a FIRST document at all: the measurement that opened G11
5070
- // found one reaching for the wildcard its neighbours use and getting
5071
- // `verdict: valid` over data that violates it. A known-good starting
5072
- // document turns generation into editing, which is the operation a
5073
- // model is reliably good at.
5074
- //
5075
- // The trio is real, runnable and tested where it lives
5076
- // (docs/skill/init/, run by ts/test/helpdoc.test.ts), and staged into
5077
- // both ports by the same generator that stages the teaching pack, so
5078
- // the two write the same bytes.
5079
-
5080
4365
  const INIT_HELP = 'aontu init [dir] (try --help)'
5081
4366
 
5082
4367
 
@@ -5101,10 +4386,6 @@ function runInit(argv: string[]): number {
5101
4386
  }
5102
4387
  const dir = dirs[0] ?? '.'
5103
4388
 
5104
- // REFUSES TO OVERWRITE, and checks every member BEFORE writing any of
5105
- // them: a scaffold that wrote two files and then refused the third
5106
- // would leave a directory in a state neither the caller nor a re-run
5107
- // can reason about.
5108
4389
  const standing = INITDOC.filter((f) => existsSync(join(dir, f.name)))
5109
4390
  if (0 < standing.length) {
5110
4391
  process.stderr.write(
@@ -5134,17 +4415,11 @@ function runInit(argv: string[]): number {
5134
4415
  }
5135
4416
 
5136
4417
 
5137
- // EVERY VERB THIS PORT DISPATCHES, for the nearest-verb suggestion
5138
- // G11 phase 2 prints. A separate list from the if-chain in main()
5139
- // because the chain's arms have three different shapes and cannot be
5140
- // a table; ts/test/cli-help.test.ts keeps the two from drifting by
5141
- // running each name and requiring it not to fall through to the bare
5142
- // command.
5143
4418
  const KNOWN_VERBS = [
5144
4419
  'agentsmd', 'allow', 'breaking', 'explain', 'fmt', 'get', 'hash',
5145
4420
  'help', 'init', 'jsonschema', 'lsp', 'mcp', 'mod', 'reaches',
5146
- 'relations', 'render', 'set', 'subsume', 'template', 'trim', 'vet',
5147
- 'view', 'why',
4421
+ 'relations', 'set', 'subsume', 'template', 'trace', 'trim',
4422
+ 'vet', 'view', 'why',
5148
4423
  ]
5149
4424
 
5150
4425
 
@@ -5159,13 +4434,6 @@ function looksLikeVerb(arg: string): boolean {
5159
4434
  }
5160
4435
 
5161
4436
 
5162
- // NEAREST-VERB SUGGESTION (G11 phase 2). Restricted
5163
- // Damerau-Levenshtein with a cap that grows with the word and stops at
5164
- // three: one edit is a convincing suggestion on any length, three is
5165
- // the most that can be believed on a long one, and an UNCAPPED
5166
- // nearest match on a three-letter typo names something unrelated with
5167
- // confidence. Mirrors go/cmd/aontu/help.go, including the sort, so
5168
- // the two ports suggest the same verb on a tie.
5169
4437
  function nearestVerb(word: string, verbs: string[]): string {
5170
4438
  let best = ''
5171
4439
  let bestDist = Infinity
@@ -5181,8 +4449,6 @@ function nearestVerb(word: string, verbs: string[]): string {
5181
4449
  }
5182
4450
 
5183
4451
 
5184
- // Levenshtein with a transposition, iterative over two rows. Mirrors
5185
- // editDistance in go/cmd/aontu/help.go exactly.
5186
4452
  function editDistance(a: string, b: string): number {
5187
4453
  const ar = [...a]
5188
4454
  const br = [...b]
@@ -5209,12 +4475,6 @@ function editDistance(a: string, b: string): number {
5209
4475
 
5210
4476
 
5211
4477
  function main(argv: string[], servers: Servers = SERVERS): void {
5212
- // COLOUR OFF WHEN THE DESTINATION IS NOT A TERMINAL. Error frames
5213
- // hardcoded their ANSI escapes, so a piped report and a `--jsonl`
5214
- // answer carried terminal control codes into whatever read them (the
5215
- // review's finding F). `NO_COLOR` is honoured by the library itself;
5216
- // only the CLI can see whether its stderr is a terminal, so only the
5217
- // CLI can make this call. `undefined` means "leave it to NO_COLOR".
5218
4478
  setColor(true === process.stderr.isTTY ? undefined : false)
5219
4479
 
5220
4480
  let mode: Mode = 'json'
@@ -5222,13 +4482,6 @@ function main(argv: string[], servers: Servers = SERVERS): void {
5222
4482
  // reads exactly what it always read, and a caller that asks for json
5223
4483
  // gets the failure in the finding shape every other verb reports.
5224
4484
  let format: EvalFormat = 'text'
5225
- // A LIST, though the bare command evaluates exactly one document.
5226
- // It used to be one variable and the last argument won, which made a
5227
- // MISTYPED VERB a silent success: `aontu vet2 schema.aon good.json`
5228
- // printed good.json and exited 0, because `vet2` matched no
5229
- // subcommand, fell through to this loop as a file name, and was
5230
- // overwritten twice. In a tool loop that reads as a passing
5231
- // validation. Counting them is what lets the refusal below happen.
5232
4485
  const files: string[] = []
5233
4486
  let trust: TrustArg = { kind: 'system-warn', textExt: [] }
5234
4487
  let textExt: string[] = []
@@ -5238,15 +4491,6 @@ function main(argv: string[], servers: Servers = SERVERS): void {
5238
4491
  // mode the REPL already has.
5239
4492
  let jsonl = false
5240
4493
 
5241
- // Subcommand dispatch, and deliberately only for a FIRST argument:
5242
- // `aontu vet` is the verb, while `aontu somefile vet` keeps meaning
5243
- // what it always did. A file named `vet` is still reachable as
5244
- // `aontu ./vet`.
5245
- //
5246
- // Promise.resolve either way: a non-watch run returns its exit class
5247
- // synchronously (and has already written its report), while `--watch`
5248
- // resolves only when the watch ends — so one await-shaped line serves
5249
- // both without a branch to keep covered.
5250
4494
  if ('vet' === argv[2]) {
5251
4495
  return void Promise.resolve(runVet(argv.slice(3))).then(finish)
5252
4496
  }
@@ -5316,14 +4560,15 @@ function main(argv: string[], servers: Servers = SERVERS): void {
5316
4560
  return finish(runJsonSchema(argv.slice(3)))
5317
4561
  }
5318
4562
 
5319
- if ('render' === argv[2]) {
5320
- return finish(runRender(argv.slice(3)))
5321
- }
5322
4563
 
5323
4564
  if ('template' === argv[2]) {
5324
4565
  return finish(runTemplate(argv.slice(3)))
5325
4566
  }
5326
4567
 
4568
+ if ('trace' === argv[2]) {
4569
+ return finish(runTrace(argv.slice(3)))
4570
+ }
4571
+
5327
4572
  if ('reaches' === argv[2]) {
5328
4573
  return finish(runReaches(argv.slice(3)))
5329
4574
  }
@@ -5403,14 +4648,6 @@ function main(argv: string[], servers: Servers = SERVERS): void {
5403
4648
  }
5404
4649
  }
5405
4650
 
5406
- // ONE DOCUMENT. The bare form has always been `aontu [options]
5407
- // [file]`, singular, and anything past the first was silently
5408
- // discarded rather than refused -- so every way of getting the verb
5409
- // wrong (a typo, a verb this port does not have, a verb spelled for
5410
- // another tool) ended in a plausible answer about the wrong file.
5411
- // Exit 2, the usage class, and the message names the cause rather
5412
- // than the symptom: nothing here can tell a mistyped verb from a
5413
- // second file, but the reader can.
5414
4651
  if (1 < files.length) {
5415
4652
  process.stderr.write(
5416
4653
  `aontu: the bare command evaluates one document, and ${files.length}` +
@@ -5419,9 +4656,6 @@ function main(argv: string[], servers: Servers = SERVERS): void {
5419
4656
  return finish(2)
5420
4657
  }
5421
4658
 
5422
- // The extensions ride with the capability from here on, so the three
5423
- // entry shapes below (file, REPL, stdin) each get them by threading
5424
- // the one value they already thread.
5425
4659
  trust = { ...trust, textExt }
5426
4660
 
5427
4661
  const file = files[0]
@@ -5438,7 +4672,7 @@ function main(argv: string[], servers: Servers = SERVERS): void {
5438
4672
  else {
5439
4673
  runStdin(mode, format, trust).then((code) => finish(code))
5440
4674
  }
5441
- } /* node:coverage ignore next 20 */
4675
+ } /* node:coverage ignore next 21 */
5442
4676
 
5443
4677
 
5444
4678
  // No require.main guard here: bin/aontu.js is the executable entry and
@@ -5450,8 +4684,8 @@ export {
5450
4684
  runReaches,
5451
4685
  runView,
5452
4686
  runJsonSchema,
5453
- runRender,
5454
4687
  runTemplate,
4688
+ runTrace,
5455
4689
  runMod,
5456
4690
  runHash, runGet, runHelp, runExplain, runInit, nearestVerb,
5457
4691
  looksLikeVerb,