aontu 0.52.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/README.md +88 -0
  2. package/bin/aontu-mcp.js +4 -0
  3. package/dist/agentsmd.d.ts +16 -0
  4. package/dist/agentsmd.js +107 -0
  5. package/dist/agentsmd.js.map +1 -0
  6. package/dist/aontu.d.ts +14 -3
  7. package/dist/aontu.js +145 -4
  8. package/dist/aontu.js.map +1 -1
  9. package/dist/cli.d.ts +44 -1
  10. package/dist/cli.js +2401 -44
  11. package/dist/cli.js.map +1 -1
  12. package/dist/ctx.d.ts +16 -0
  13. package/dist/ctx.js +44 -0
  14. package/dist/ctx.js.map +1 -1
  15. package/dist/diff.d.ts +22 -0
  16. package/dist/diff.js +141 -0
  17. package/dist/diff.js.map +1 -0
  18. package/dist/err.d.ts +3 -1
  19. package/dist/err.js +48 -8
  20. package/dist/err.js.map +1 -1
  21. package/dist/graph.d.ts +16 -0
  22. package/dist/graph.js +73 -0
  23. package/dist/graph.js.map +1 -0
  24. package/dist/hcanon.d.ts +3 -0
  25. package/dist/hcanon.js +146 -0
  26. package/dist/hcanon.js.map +1 -0
  27. package/dist/hints.js +223 -5
  28. package/dist/hints.js.map +1 -1
  29. package/dist/jsonschema.d.ts +20 -0
  30. package/dist/jsonschema.js +391 -0
  31. package/dist/jsonschema.js.map +1 -0
  32. package/dist/lang.js +698 -35
  33. package/dist/lang.js.map +1 -1
  34. package/dist/lsp.d.ts +9 -2
  35. package/dist/lsp.js +262 -46
  36. package/dist/lsp.js.map +1 -1
  37. package/dist/mcp-server.d.ts +20 -0
  38. package/dist/mcp-server.js +147 -0
  39. package/dist/mcp-server.js.map +1 -0
  40. package/dist/mcp.d.ts +42 -0
  41. package/dist/mcp.js +814 -0
  42. package/dist/mcp.js.map +1 -0
  43. package/dist/mod-tool.d.ts +58 -0
  44. package/dist/mod-tool.js +498 -0
  45. package/dist/mod-tool.js.map +1 -0
  46. package/dist/mod.d.ts +31 -0
  47. package/dist/mod.js +250 -0
  48. package/dist/mod.js.map +1 -0
  49. package/dist/patch.d.ts +44 -0
  50. package/dist/patch.js +506 -0
  51. package/dist/patch.js.map +1 -0
  52. package/dist/provenance.d.ts +40 -0
  53. package/dist/provenance.js +335 -0
  54. package/dist/provenance.js.map +1 -0
  55. package/dist/query.d.ts +27 -0
  56. package/dist/query.js +294 -0
  57. package/dist/query.js.map +1 -0
  58. package/dist/reach.d.ts +14 -0
  59. package/dist/reach.js +140 -0
  60. package/dist/reach.js.map +1 -0
  61. package/dist/relation.d.ts +19 -0
  62. package/dist/relation.js +305 -0
  63. package/dist/relation.js.map +1 -0
  64. package/dist/report-sarif.d.ts +14 -0
  65. package/dist/report-sarif.js +102 -0
  66. package/dist/report-sarif.js.map +1 -0
  67. package/dist/site.d.ts +4 -0
  68. package/dist/site.js +31 -0
  69. package/dist/site.js.map +1 -1
  70. package/dist/std.d.ts +1 -0
  71. package/dist/std.js +73 -0
  72. package/dist/std.js.map +1 -0
  73. package/dist/subsume.d.ts +39 -0
  74. package/dist/subsume.js +526 -0
  75. package/dist/subsume.js.map +1 -0
  76. package/dist/trim.d.ts +19 -0
  77. package/dist/trim.js +155 -0
  78. package/dist/trim.js.map +1 -0
  79. package/dist/tsconfig.tsbuildinfo +1 -1
  80. package/dist/type.d.ts +17 -1
  81. package/dist/type.js.map +1 -1
  82. package/dist/unify.d.ts +3 -1
  83. package/dist/unify.js +287 -18
  84. package/dist/unify.js.map +1 -1
  85. package/dist/utility.d.ts +9 -1
  86. package/dist/utility.js +122 -1
  87. package/dist/utility.js.map +1 -1
  88. package/dist/val/AggFuncVal.d.ts +33 -0
  89. package/dist/val/AggFuncVal.js +202 -0
  90. package/dist/val/AggFuncVal.js.map +1 -0
  91. package/dist/val/ArithFuncVal.d.ts +31 -0
  92. package/dist/val/ArithFuncVal.js +62 -0
  93. package/dist/val/ArithFuncVal.js.map +1 -0
  94. package/dist/val/BagVal.d.ts +5 -0
  95. package/dist/val/BagVal.js +96 -5
  96. package/dist/val/BagVal.js.map +1 -1
  97. package/dist/val/CloseFuncVal.js +9 -1
  98. package/dist/val/CloseFuncVal.js.map +1 -1
  99. package/dist/val/ConjunctVal.d.ts +1 -1
  100. package/dist/val/ConjunctVal.js +19 -0
  101. package/dist/val/ConjunctVal.js.map +1 -1
  102. package/dist/val/ConstraintVal.d.ts +48 -1
  103. package/dist/val/ConstraintVal.js +1501 -110
  104. package/dist/val/ConstraintVal.js.map +1 -1
  105. package/dist/val/CopyFuncVal.d.ts +1 -2
  106. package/dist/val/CopyFuncVal.js +7 -0
  107. package/dist/val/CopyFuncVal.js.map +1 -1
  108. package/dist/val/Decimal.d.ts +1 -0
  109. package/dist/val/Decimal.js +13 -0
  110. package/dist/val/Decimal.js.map +1 -1
  111. package/dist/val/DeprecateFuncVal.d.ts +11 -0
  112. package/dist/val/DeprecateFuncVal.js +47 -0
  113. package/dist/val/DeprecateFuncVal.js.map +1 -0
  114. package/dist/val/DisjunctVal.js +130 -21
  115. package/dist/val/DisjunctVal.js.map +1 -1
  116. package/dist/val/EachFuncVal.d.ts +15 -0
  117. package/dist/val/EachFuncVal.js +75 -0
  118. package/dist/val/EachFuncVal.js.map +1 -0
  119. package/dist/val/ExpectVal.d.ts +1 -0
  120. package/dist/val/ExpectVal.js +41 -4
  121. package/dist/val/ExpectVal.js.map +1 -1
  122. package/dist/val/FeatureVal.js +1 -1
  123. package/dist/val/FeatureVal.js.map +1 -1
  124. package/dist/val/FilterFuncVal.d.ts +15 -0
  125. package/dist/val/FilterFuncVal.js +91 -0
  126. package/dist/val/FilterFuncVal.js.map +1 -0
  127. package/dist/val/FuncBaseVal.d.ts +6 -1
  128. package/dist/val/FuncBaseVal.js +178 -2
  129. package/dist/val/FuncBaseVal.js.map +1 -1
  130. package/dist/val/HideFuncVal.js.map +1 -1
  131. package/dist/val/IdFuncVal.d.ts +13 -0
  132. package/dist/val/IdFuncVal.js +54 -0
  133. package/dist/val/IdFuncVal.js.map +1 -0
  134. package/dist/val/JunctionVal.js +7 -1
  135. package/dist/val/JunctionVal.js.map +1 -1
  136. package/dist/val/KeyFuncVal.d.ts +1 -1
  137. package/dist/val/KeyFuncVal.js +38 -30
  138. package/dist/val/KeyFuncVal.js.map +1 -1
  139. package/dist/val/ListVal.js +117 -17
  140. package/dist/val/ListVal.js.map +1 -1
  141. package/dist/val/LowerFuncVal.js.map +1 -1
  142. package/dist/val/MapVal.js +102 -8
  143. package/dist/val/MapVal.js.map +1 -1
  144. package/dist/val/MatchFuncVal.d.ts +15 -0
  145. package/dist/val/MatchFuncVal.js +107 -0
  146. package/dist/val/MatchFuncVal.js.map +1 -0
  147. package/dist/val/MoveFuncVal.js.map +1 -1
  148. package/dist/val/NilVal.js +24 -0
  149. package/dist/val/NilVal.js.map +1 -1
  150. package/dist/val/OpBaseVal.d.ts +1 -1
  151. package/dist/val/OpBaseVal.js +24 -2
  152. package/dist/val/OpBaseVal.js.map +1 -1
  153. package/dist/val/OpenFuncVal.js +4 -1
  154. package/dist/val/OpenFuncVal.js.map +1 -1
  155. package/dist/val/PackFuncVal.d.ts +15 -0
  156. package/dist/val/PackFuncVal.js +108 -0
  157. package/dist/val/PackFuncVal.js.map +1 -0
  158. package/dist/val/PathFuncVal.js.map +1 -1
  159. package/dist/val/PlaceVal.d.ts +13 -0
  160. package/dist/val/PlaceVal.js +131 -0
  161. package/dist/val/PlaceVal.js.map +1 -0
  162. package/dist/val/PlusOpVal.js +11 -2
  163. package/dist/val/PlusOpVal.js.map +1 -1
  164. package/dist/val/PrefFuncVal.js.map +1 -1
  165. package/dist/val/PrefVal.d.ts +2 -2
  166. package/dist/val/PrefVal.js +78 -23
  167. package/dist/val/PrefVal.js.map +1 -1
  168. package/dist/val/RefVal.d.ts +1 -1
  169. package/dist/val/RefVal.js +158 -30
  170. package/dist/val/RefVal.js.map +1 -1
  171. package/dist/val/ReferFuncVal.d.ts +36 -0
  172. package/dist/val/ReferFuncVal.js +303 -0
  173. package/dist/val/ReferFuncVal.js.map +1 -0
  174. package/dist/val/ScalarKindVal.d.ts +1 -2
  175. package/dist/val/ScalarKindVal.js +0 -11
  176. package/dist/val/ScalarKindVal.js.map +1 -1
  177. package/dist/val/TopVal.js.map +1 -1
  178. package/dist/val/TypeFuncVal.js.map +1 -1
  179. package/dist/val/UpperFuncVal.js.map +1 -1
  180. package/dist/val/Val.d.ts +9 -2
  181. package/dist/val/Val.js +150 -4
  182. package/dist/val/Val.js.map +1 -1
  183. package/dist/val/VarVal.js.map +1 -1
  184. package/dist/val/arith.d.ts +6 -0
  185. package/dist/val/arith.js +170 -0
  186. package/dist/val/arith.js.map +1 -0
  187. package/dist/vet.d.ts +45 -0
  188. package/dist/vet.js +776 -0
  189. package/dist/vet.js.map +1 -0
  190. package/dist/walk.d.ts +2 -0
  191. package/dist/walk.js +91 -0
  192. package/dist/walk.js.map +1 -0
  193. package/grammar/aontu.gbnf +130 -0
  194. package/grammar/aontu.lark +113 -0
  195. package/package.json +30 -15
  196. package/skill/SKILL.md +37 -0
  197. package/skill/error-codes.md +62 -0
  198. package/skill/examples.md +99 -0
  199. package/skill/grammar-card.md +57 -0
  200. package/src/agentsmd.ts +135 -0
  201. package/src/aontu.ts +192 -4
  202. package/src/cli.ts +2858 -71
  203. package/src/ctx.ts +81 -0
  204. package/src/diff.ts +196 -0
  205. package/src/err.ts +52 -8
  206. package/src/graph.ts +135 -0
  207. package/src/hcanon.ts +169 -0
  208. package/src/hints.ts +271 -5
  209. package/src/jsonschema.ts +511 -0
  210. package/src/lang.ts +779 -37
  211. package/src/lsp.ts +281 -47
  212. package/src/mcp-server.ts +187 -0
  213. package/src/mcp.ts +993 -0
  214. package/src/mod-tool.ts +679 -0
  215. package/src/mod.ts +344 -0
  216. package/src/patch.ts +624 -0
  217. package/src/provenance.ts +430 -0
  218. package/src/query.ts +379 -0
  219. package/src/reach.ts +184 -0
  220. package/src/relation.ts +395 -0
  221. package/src/report-sarif.ts +137 -0
  222. package/src/site.ts +36 -1
  223. package/src/std.ts +73 -0
  224. package/src/subsume.ts +690 -0
  225. package/src/trim.ts +195 -0
  226. package/src/tsconfig.json +10 -4
  227. package/src/type.ts +51 -2
  228. package/src/unify.ts +311 -16
  229. package/src/utility.ts +139 -1
  230. package/src/val/AggFuncVal.ts +319 -0
  231. package/src/val/ArithFuncVal.ts +108 -0
  232. package/src/val/BagVal.ts +101 -4
  233. package/src/val/CloseFuncVal.ts +9 -1
  234. package/src/val/ConjunctVal.ts +20 -0
  235. package/src/val/ConstraintVal.ts +1699 -116
  236. package/src/val/CopyFuncVal.ts +7 -1
  237. package/src/val/Decimal.ts +15 -0
  238. package/src/val/DeprecateFuncVal.ts +84 -0
  239. package/src/val/DisjunctVal.ts +139 -28
  240. package/src/val/EachFuncVal.ts +133 -0
  241. package/src/val/ExpectVal.ts +43 -6
  242. package/src/val/FeatureVal.ts +1 -1
  243. package/src/val/FilterFuncVal.ts +154 -0
  244. package/src/val/FuncBaseVal.ts +200 -3
  245. package/src/val/HideFuncVal.ts +0 -2
  246. package/src/val/IdFuncVal.ts +91 -0
  247. package/src/val/JunctionVal.ts +7 -1
  248. package/src/val/KeyFuncVal.ts +39 -35
  249. package/src/val/ListVal.ts +125 -18
  250. package/src/val/LowerFuncVal.ts +0 -1
  251. package/src/val/MapVal.ts +110 -8
  252. package/src/val/MatchFuncVal.ts +176 -0
  253. package/src/val/MoveFuncVal.ts +0 -2
  254. package/src/val/NilVal.ts +25 -0
  255. package/src/val/OpBaseVal.ts +26 -3
  256. package/src/val/OpenFuncVal.ts +4 -2
  257. package/src/val/PackFuncVal.ts +175 -0
  258. package/src/val/PathFuncVal.ts +0 -1
  259. package/src/val/PlaceVal.ts +193 -0
  260. package/src/val/PlusOpVal.ts +11 -2
  261. package/src/val/PrefFuncVal.ts +0 -1
  262. package/src/val/PrefVal.ts +79 -36
  263. package/src/val/RefVal.ts +163 -30
  264. package/src/val/ReferFuncVal.ts +387 -0
  265. package/src/val/ScalarKindVal.ts +0 -13
  266. package/src/val/TopVal.ts +0 -1
  267. package/src/val/TypeFuncVal.ts +0 -2
  268. package/src/val/UpperFuncVal.ts +0 -1
  269. package/src/val/Val.ts +213 -3
  270. package/src/val/VarVal.ts +0 -1
  271. package/src/val/arith.ts +316 -0
  272. package/src/vet.ts +992 -0
  273. package/src/walk.ts +99 -0
@@ -0,0 +1,679 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ // MODULE TOOLING (G6 phase 3, docs/capability-review/g6-distribution.md):
4
+ // the LOCAL half — `aontu mod tidy`, `verify`, `vendor` and
5
+ // `manifest`.
6
+ //
7
+ // Evaluation never touches the network, and neither does this: `tidy`
8
+ // resolves versions and rewrites the lockfile from what is already in
9
+ // the local stores, `verify` asks whether the stores still MEAN what
10
+ // the lockfile pins and changes nothing, and `vendor` materialises the
11
+ // locked closure into the project. Fetching and publishing are the
12
+ // network half, and are not in this build (see the register).
13
+ //
14
+ // MINIMUM VERSION SELECTION, not a solver: each module declares the
15
+ // MINIMUM version of each dependency it needs, and the selected version
16
+ // is the maximum of those minima over the closure. Deterministic, and
17
+ // deterministic without backtracking — the lockfile CONFIRMS the
18
+ // resolution rather than determining it, which is why a tidy run can be
19
+ // re-run to the same bytes.
20
+
21
+ import {
22
+ readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync,
23
+ copyFileSync,
24
+ } from 'node:fs'
25
+ import { join as pathJoin, dirname as pathDirname } from 'node:path'
26
+
27
+ import { parseModuleRef, moduleDir, lockJson } from './mod'
28
+ import { subsume } from './subsume'
29
+ import type { ModuleRef } from './mod'
30
+
31
+
32
+ // One entry of the lockfile, and of a tidy report.
33
+ export type ModLock = {
34
+ // The module path and major, as an import spells it.
35
+ mod: string
36
+ // The selected version.
37
+ v: string
38
+ // The canon-hash of the module as it is in the local store.
39
+ canon: string
40
+ // The registry digest, carried over from a previous lockfile. Empty
41
+ // when nothing has ever fetched this module: the OCI pin is the
42
+ // registry's word, and only a fetch can hear it.
43
+ oci: string
44
+ }
45
+
46
+ export type ModTidyReport = {
47
+ verdict: 'ok' | 'missing' | 'error'
48
+ // The resolved closure, sorted by module.
49
+ lock: ModLock[]
50
+ // Modules named by a dependency but present in no local store, sorted.
51
+ missing: string[]
52
+ // Modules present in a store but which DO NOT EVALUATE standalone,
53
+ // sorted. A pin is what a module MEANS, so there is nothing to pin
54
+ // here and the lockfile is left alone.
55
+ unevaluable: string[]
56
+ }
57
+
58
+ export type ModVerifyReport = {
59
+ // `ok` the lockfile covers what the project declares and every
60
+ // locked module still means what it pins; `mismatch` at least one
61
+ // does not; `unlocked` the lockfile does not cover the project's own
62
+ // dependencies; `missing` at least one locked module is not in any
63
+ // store. In that order of precedence, most specific first.
64
+ verdict: 'ok' | 'mismatch' | 'unlocked' | 'missing'
65
+ // The locked modules that verified, sorted.
66
+ verified: string[]
67
+ // What the lockfile pins against what the store now means, for each
68
+ // module that does not match, sorted by module.
69
+ mismatched: { mod: string, want: string, got: string }[]
70
+ // Dependencies the project declares that the lockfile does not name,
71
+ // sorted. A tidy is what fills them in.
72
+ unlocked: string[]
73
+ // Locked modules present in no store, sorted.
74
+ missing: string[]
75
+ }
76
+
77
+ export type ModVendorReport = {
78
+ verdict: 'ok' | 'missing'
79
+ // The modules materialised into `aon_vendor/`, sorted.
80
+ vendored: string[]
81
+ // Locked modules present in no store, sorted.
82
+ missing: string[]
83
+ }
84
+
85
+
86
+ // What the tooling needs from the engine, injected for the reason
87
+ // ts/src/mod.ts's ModuleEval is: evaluating a module is what the
88
+ // evaluator does, and the tooling is a caller of it rather than a
89
+ // second implementation.
90
+ export type ModToolEval = (src: string, path: string) =>
91
+ {
92
+ gen: any, hash: string, canon: string,
93
+ // DID IT STAND UP ON ITS OWN? A module that does not evaluate has
94
+ // no meaning to pin, and `canonHash` of the nil it collapses to is
95
+ // the SAME string for every such module -- so a lockfile written
96
+ // from one carries no information while looking exactly like one
97
+ // that does (use-cases/BUGS.md §31). `aontu hash` already refuses
98
+ // that file; `tidy` refuses it too, and this is what tells it.
99
+ ok: boolean,
100
+ }
101
+
102
+
103
+ export type ModToolOptions = {
104
+ // The content-addressed user cache. Empty means no cache is
105
+ // consulted, which is a store that misses rather than an error.
106
+ cache?: string
107
+ eval: ModToolEval
108
+ }
109
+
110
+
111
+ // The `dep` block a module file declares: import string -> version.
112
+ function declaredDeps(file: string, options: ModToolOptions):
113
+ Record<string, string> {
114
+ if (!existsSync(file)) {
115
+ return {}
116
+ }
117
+
118
+ const gen: any = options.eval(readFileSync(file, 'utf8'), file).gen
119
+ const dep = gen?.dep
120
+ if (null == dep || 'object' !== typeof dep) {
121
+ return {}
122
+ }
123
+
124
+ const out: Record<string, string> = {}
125
+ for (const key of Object.keys(dep)) {
126
+ const v = dep[key]?.v
127
+ if ('string' === typeof v && '' !== v) {
128
+ out[key] = v
129
+ }
130
+ }
131
+ return out
132
+ }
133
+
134
+
135
+ // Numeric-dotted version order: `1.10.0` is above `1.9.0`, which
136
+ // STRING order gets wrong, and that is the whole reason this is not a
137
+ // `<` on the text. A part that is not a number compares as text, after
138
+ // every number — a pre-release tag is below no version and above none.
139
+ export function versionCompare(a: string, b: string): number {
140
+ const ap = a.split('.')
141
+ const bp = b.split('.')
142
+ for (let i = 0; i < Math.max(ap.length, bp.length); i++) {
143
+ // A part the shorter version does not have is ZERO, so `1.2` and
144
+ // `1.2.0` are the same version -- which is what everyone means by
145
+ // them, and what a lockfile rewritten from either must agree on.
146
+ const x = ap[i] ?? '0'
147
+ const y = bp[i] ?? '0'
148
+ if (x === y) {
149
+ continue
150
+ }
151
+ const xn = /^\d+$/.test(x)
152
+ const yn = /^\d+$/.test(y)
153
+ if (xn && yn) {
154
+ return +x < +y ? -1 : 1
155
+ }
156
+ if (xn !== yn) {
157
+ return xn ? -1 : 1
158
+ }
159
+ return x < y ? -1 : 1
160
+ }
161
+ return 0
162
+ }
163
+
164
+
165
+ // The directory a module is in, in the local stores: the project's
166
+ // vendor tree first, then the cache under the hash the lockfile pins.
167
+ function storeDir(
168
+ root: string, ref: ModuleRef, hash: string, options: ModToolOptions,
169
+ ): string | undefined {
170
+ const stores = [moduleDir(pathJoin(root, 'aon_vendor'), ref)]
171
+ if (null != options.cache && '' !== hash) {
172
+ stores.push(pathJoin(options.cache, hash))
173
+ }
174
+ return stores.find((d) => existsSync(pathJoin(d, 'mod.aon')))
175
+ }
176
+
177
+
178
+ // The lockfile's entries, as written.
179
+ function readLock(root: string): Record<string, ModLock> {
180
+ const file = pathJoin(root, 'mod-lock.aon')
181
+ if (!existsSync(file)) {
182
+ return {}
183
+ }
184
+
185
+ let lock: any
186
+ try {
187
+ lock = JSON.parse(lockJson(readFileSync(file, 'utf8')))
188
+ }
189
+ catch {
190
+ return {}
191
+ }
192
+
193
+ const out: Record<string, ModLock> = {}
194
+ for (const mod of Object.keys(lock?.lock ?? {})) {
195
+ const e = lock.lock[mod]
196
+ out[mod] = {
197
+ mod,
198
+ v: 'string' === typeof e?.v ? e.v : '',
199
+ canon: 'string' === typeof e?.canon ? e.canon : '',
200
+ oci: 'string' === typeof e?.oci ? e.oci : '',
201
+ }
202
+ }
203
+ return out
204
+ }
205
+
206
+
207
+ // The lockfile TEXT: canonical Aontu, one line, keys sorted. Built as
208
+ // source and canonicalised by the engine rather than printed by hand,
209
+ // so "canonical form" means what the language means by it and cannot
210
+ // drift from it.
211
+ export function lockText(entries: ModLock[], options: ModToolOptions): string {
212
+ const parts = entries.map((e) =>
213
+ JSON.stringify(e.mod) + ':{' +
214
+ '"canon":' + JSON.stringify(e.canon) + ',' +
215
+ '"oci":' + JSON.stringify(e.oci) + ',' +
216
+ '"v":' + JSON.stringify(e.v) + '}')
217
+ // Canonicalised by the ENGINE rather than printed by hand, so
218
+ // "canonical form" means what the language means by it and cannot
219
+ // drift from it. For a map of scalars that canon is also JSON, which
220
+ // is what lets the resolver read a pin back without an evaluator
221
+ // (ts/src/mod.ts lockHash).
222
+ return options.eval('{"lock":{' + parts.join(',') + '}}', 'mod-lock.aon').canon
223
+ }
224
+
225
+
226
+ // `aontu mod tidy`: resolve the closure by MVS and rewrite the lockfile.
227
+ export function modTidy(root: string, options: ModToolOptions): ModTidyReport {
228
+ const previous = readLock(root)
229
+ const selected: Record<string, string> = {}
230
+ const missing: string[] = []
231
+
232
+ // The closure, breadth-first from the project's own declarations. A
233
+ // module already selected at a version at least as high contributes
234
+ // nothing new, which is what makes this terminate without a cycle
235
+ // check: the selected version only ever rises.
236
+ let frontier = declaredDeps(pathJoin(root, 'mod.aon'), options)
237
+ for (; 0 < Object.keys(frontier).length;) {
238
+ const next: Record<string, string> = {}
239
+
240
+ for (const mod of Object.keys(frontier)) {
241
+ const want = frontier[mod]
242
+ const have = selected[mod]
243
+ if (null != have && 0 <= versionCompare(have, want)) {
244
+ continue
245
+ }
246
+ selected[mod] = want
247
+
248
+ const ref = parseModuleRef(mod)
249
+ if (undefined === ref) {
250
+ // A dependency key that is not a module path names nothing this
251
+ // resolver can find, which is the same answer as a module that
252
+ // is not there.
253
+ missing.push(mod)
254
+ continue
255
+ }
256
+
257
+ const dir = storeDir(root, ref, previous[mod]?.canon ?? '', options)
258
+ if (undefined === dir) {
259
+ missing.push(mod)
260
+ continue
261
+ }
262
+
263
+ const deps = declaredDeps(pathJoin(dir, 'mod.aon'), options)
264
+ for (const key of Object.keys(deps)) {
265
+ const bid = next[key]
266
+ if (null == bid || 0 > versionCompare(bid, deps[key])) {
267
+ next[key] = deps[key]
268
+ }
269
+ }
270
+ }
271
+
272
+ frontier = next
273
+ }
274
+
275
+ const lock: ModLock[] = []
276
+ const unevaluable: string[] = []
277
+ for (const mod of Object.keys(selected).sort()) {
278
+ if (missing.includes(mod)) {
279
+ continue
280
+ }
281
+ const ref = parseModuleRef(mod) as ModuleRef
282
+ const dir = storeDir(root, ref, previous[mod]?.canon ?? '', options) as string
283
+ const main = pathJoin(dir, mainOf(dir, options))
284
+ // RECOMPUTED, never carried over: the pin is what the module in
285
+ // this store MEANS, and a tidy that copied the old hash forward
286
+ // would pin what it used to mean.
287
+ const got = existsSync(main) ?
288
+ options.eval(readFileSync(main, 'utf8'), main) : undefined
289
+ // A NIL PIN IS WORSE THAN NO PIN. A module that does not stand up
290
+ // hashes to canonHash(nil) -- the same string for every broken
291
+ // module -- so writing it would put a plausible, uninformative
292
+ // pin in the lockfile and silently void the "breaks on any
293
+ // semantic change in the closure" contract (BUGS.md §31).
294
+ if (null != got && !got.ok) {
295
+ unevaluable.push(mod)
296
+ continue
297
+ }
298
+ lock.push({
299
+ mod,
300
+ v: selected[mod],
301
+ canon: null == got ? '' : got.hash,
302
+ // Carried over: the OCI digest is the registry's word about the
303
+ // bytes it served, and nothing local can hear it.
304
+ oci: previous[mod]?.oci ?? '',
305
+ })
306
+ }
307
+
308
+ const uniqueMissing = [...new Set(missing)].sort()
309
+ const uniqueUnevaluable = [...new Set(unevaluable)].sort()
310
+ const held = 0 === uniqueMissing.length && 0 === uniqueUnevaluable.length
311
+ if (held) {
312
+ writeFileSync(pathJoin(root, 'mod-lock.aon'),
313
+ LOCK_HEADER + lockText(lock, options) + '\n')
314
+ }
315
+
316
+ return {
317
+ verdict: held ? 'ok' :
318
+ 0 < uniqueUnevaluable.length ? 'error' : 'missing',
319
+ lock,
320
+ missing: uniqueMissing,
321
+ unevaluable: uniqueUnevaluable,
322
+ }
323
+ }
324
+
325
+
326
+ // The generated-file header. A lockfile is machine-written, and the
327
+ // file says so where an editor will see it.
328
+ const LOCK_HEADER =
329
+ '# mod-lock.aon (generated by `aontu mod tidy`; do not edit)\n'
330
+
331
+
332
+ // The entry file a module declares, or the default. `dir` is always a
333
+ // STORE directory, and `storeDir` only answers one that holds a
334
+ // `mod.aon` -- so there is no missing-file arm to take here.
335
+ function mainOf(dir: string, options: ModToolOptions): string {
336
+ const file = pathJoin(dir, 'mod.aon')
337
+ const gen: any = options.eval(readFileSync(file, 'utf8'), file).gen
338
+ const main = gen?.mod?.main
339
+ return 'string' === typeof main && '' !== main ? main : 'main.aon'
340
+ }
341
+
342
+
343
+ // `aontu mod verify`: does every locked module still MEAN what the
344
+ // lockfile pins? Recompute and compare, and CHANGE NOTHING.
345
+ //
346
+ // The verb exists because `tidy` cannot answer this question. Tidy
347
+ // recomputes and REWRITES by design -- a pin is what a module means
348
+ // now -- so tampering with a vendored module and running tidy makes
349
+ // the lockfile agree with the tampering, `verdict: ok`, and the next
350
+ // evaluation passes. That is correct for the job tidy does and useless
351
+ // as a gate, which left a CI job that tidies before evaluating with no
352
+ // integrity protection at all (use-cases/BUGS.md §32). Verification is
353
+ // a question; answering it must not be an edit.
354
+ export function modVerify(root: string, options: ModToolOptions):
355
+ ModVerifyReport {
356
+ const locked = readLock(root)
357
+ const verified: string[] = []
358
+ const mismatched: { mod: string, want: string, got: string }[] = []
359
+ const missing: string[] = []
360
+
361
+ // NOTHING TO CHECK IS NOT A PASS. A project with no lockfile at all
362
+ // -- or one whose lockfile predates a dependency someone added --
363
+ // would otherwise verify clean, because the loop below walks what is
364
+ // LOCKED and there is nothing locked to walk. That is the same shape
365
+ // as the defect this verb exists to close: absence reading as
366
+ // agreement. Every dependency the project itself declares must be in
367
+ // the lockfile before the pins mean anything, and the repair is a
368
+ // tidy rather than a fetch. Transitive dependencies need no separate
369
+ // check: a locked module's own imports are resolved when its pin is
370
+ // recomputed, so one that is unreachable makes its DEPENDANT fail to
371
+ // evaluate and lands in `mismatched` below.
372
+ const declared = declaredDeps(pathJoin(root, 'mod.aon'), options)
373
+ const unlocked = Object.keys(declared)
374
+ .filter((mod) => null == locked[mod]).sort()
375
+
376
+ for (const mod of Object.keys(locked).sort()) {
377
+ const ref = parseModuleRef(mod)
378
+ if (undefined === ref) {
379
+ missing.push(mod)
380
+ continue
381
+ }
382
+ const dir = storeDir(root, ref, locked[mod].canon, options)
383
+ if (undefined === dir) {
384
+ missing.push(mod)
385
+ continue
386
+ }
387
+ const main = pathJoin(dir, mainOf(dir, options))
388
+ if (!existsSync(main)) {
389
+ missing.push(mod)
390
+ continue
391
+ }
392
+
393
+ // A module that no longer stands up is not a match: it has no
394
+ // meaning to compare, and reporting `got: <hash of nil>` would
395
+ // print the same string for every broken module. The empty `got`
396
+ // says the store holds something that does not evaluate.
397
+ const got = options.eval(readFileSync(main, 'utf8'), main)
398
+ const want = locked[mod].canon
399
+ if (got.ok && want === got.hash) {
400
+ verified.push(mod)
401
+ continue
402
+ }
403
+ mismatched.push({ mod, want, got: got.ok ? got.hash : '' })
404
+ }
405
+
406
+ return {
407
+ verdict: 0 < mismatched.length ? 'mismatch' :
408
+ 0 < unlocked.length ? 'unlocked' :
409
+ 0 < missing.length ? 'missing' : 'ok',
410
+ verified,
411
+ mismatched,
412
+ unlocked,
413
+ missing: missing.sort(),
414
+ }
415
+ }
416
+
417
+
418
+ // `aontu mod vendor`: materialise the locked closure into `aon_vendor/`.
419
+ export function modVendor(root: string, options: ModToolOptions):
420
+ ModVendorReport {
421
+ const locked = readLock(root)
422
+ const vendored: string[] = []
423
+ const missing: string[] = []
424
+
425
+ for (const mod of Object.keys(locked).sort()) {
426
+ const ref = parseModuleRef(mod)
427
+ if (undefined === ref) {
428
+ missing.push(mod)
429
+ continue
430
+ }
431
+
432
+ const from = storeDir(root, ref, locked[mod].canon, options)
433
+ if (undefined === from) {
434
+ missing.push(mod)
435
+ continue
436
+ }
437
+
438
+ const to = moduleDir(pathJoin(root, 'aon_vendor'), ref)
439
+ if (from !== to) {
440
+ copyTree(from, to)
441
+ }
442
+ vendored.push(mod)
443
+ }
444
+
445
+ return {
446
+ verdict: 0 === missing.length ? 'ok' : 'missing',
447
+ vendored,
448
+ missing: missing.sort(),
449
+ }
450
+ }
451
+
452
+
453
+ // A whole module directory, copied. Modules are source trees — that is
454
+ // what an OCI layer holds — so this walks rather than reading one file.
455
+ function copyTree(from: string, to: string): void {
456
+ mkdirSync(to, { recursive: true })
457
+ for (const name of readdirSync(from).sort()) {
458
+ const src = pathJoin(from, name)
459
+ const dst = pathJoin(to, name)
460
+ if (statSync(src).isDirectory()) {
461
+ copyTree(src, dst)
462
+ }
463
+ else {
464
+ mkdirSync(pathDirname(dst), { recursive: true })
465
+ copyFileSync(src, dst)
466
+ }
467
+ }
468
+ }
469
+
470
+
471
+ // THE PUBLISH BOUNDARY (G6 phase 4,
472
+ // docs/capability-review/g6-distribution.md). A module is an OCI
473
+ // artifact, and what a publish PUSHES is a manifest: a config media
474
+ // type, one layer holding the module's source tree, and annotations
475
+ // carrying the module path, its version and its canon-hash.
476
+ //
477
+ // The push needs a registry, which this build does not have. Everything
478
+ // the push would ASSERT is local, and that is what `aontu mod manifest`
479
+ // answers: the exact artifact description, computed the way the
480
+ // registry would be told it, plus the gate that decides whether it may
481
+ // be minted at all.
482
+ //
483
+ // WHY THE ANNOTATION MATTERS MORE THAN THE BYTES. "Has the truth
484
+ // changed?" is one annotation read and a string compare -- no download,
485
+ // no parse -- because the canon-hash pins MEANING rather than text. A
486
+ // consumer holding `aon1-oQs6…` can ask a registry index whether the
487
+ // module still hashes to it, and a reformat, a comment or a file split
488
+ // will not move it.
489
+
490
+ // The config media type the design fixes: an Aontu module is not an
491
+ // image, and the type is what tells a registry so.
492
+ export const MODULE_CONFIG_MEDIA_TYPE = 'application/vnd.aontu.module.v1+json'
493
+
494
+ // The canon-hash annotation. OCI asks a custom key to be the reverse
495
+ // DNS of a domain its author controls, and the project's own home is
496
+ // the only domain it has -- inventing an `aontu.dev` would be a claim
497
+ // it cannot back. The two facts OCI already has keys for use those.
498
+ export const MODULE_ANNOTATION_CANON = 'com.github.rjrodger.aontu.canon'
499
+ export const MODULE_ANNOTATION_MAJOR = 'com.github.rjrodger.aontu.major'
500
+
501
+
502
+ export type ModManifestReport = {
503
+ // `ok` the artifact may be minted; `breaking`, `undecided` or `error`
504
+ // the gate refused, and no publish should follow.
505
+ verdict: 'ok' | 'breaking' | 'undecided' | 'error'
506
+ // The module path with its major, as an import spells it.
507
+ mod: string
508
+ version: string
509
+ // The canon-hash of the module's entry file, evaluated standalone.
510
+ canon: string
511
+ config: string
512
+ // The layer's contents: every file of the source tree, relative,
513
+ // forward-slashed and sorted, so two implementations on two platforms
514
+ // describe the same layer.
515
+ files: string[]
516
+ annotations: Record<string, string>
517
+ // What the module does not declare, sorted. A manifest cannot be
518
+ // minted without them.
519
+ missing: string[]
520
+ // The gate's findings, when a prior version was named.
521
+ findings: any[]
522
+ }
523
+
524
+
525
+ // What a module file says about ITSELF. Distinct from `declaredDeps`,
526
+ // which reads what it says about others.
527
+ type ModSelf = { path: string, version: string, main: string }
528
+
529
+ function modSelf(dir: string, options: ModToolOptions): ModSelf {
530
+ const file = pathJoin(dir, 'mod.aon')
531
+ if (!existsSync(file)) {
532
+ return { path: '', version: '', main: 'main.aon' }
533
+ }
534
+ const gen: any = options.eval(readFileSync(file, 'utf8'), file).gen
535
+ const mod = gen?.mod
536
+ const str = (k: string): string =>
537
+ 'string' === typeof mod?.[k] ? mod[k] : ''
538
+ return {
539
+ path: str('path'),
540
+ version: str('version'),
541
+ main: '' === str('main') ? 'main.aon' : str('main'),
542
+ }
543
+ }
544
+
545
+
546
+ // The leading numeric component of a version, which is the major an
547
+ // import spells. Empty when the version does not start with one: a
548
+ // version whose major cannot be read cannot be published under a
549
+ // module path, because the path is where the major lives.
550
+ function majorOf(version: string): string {
551
+ const m = /^(\d+)/.exec(version)
552
+ return null == m ? '' : m[1]
553
+ }
554
+
555
+
556
+ // Every file of a module's source tree, relative and forward-slashed.
557
+ // `aon_vendor/` is excluded: a published module carries its own
558
+ // sources, not a copy of everyone else's -- a consumer resolves the
559
+ // closure itself, and a nested vendor tree would publish the world.
560
+ function layerFiles(dir: string, prefix = ''): string[] {
561
+ const out: string[] = []
562
+ for (const name of readdirSync(dir).sort()) {
563
+ if ('aon_vendor' === name) {
564
+ continue
565
+ }
566
+ const full = pathJoin(dir, name)
567
+ const rel = '' === prefix ? name : prefix + '/' + name
568
+ if (statSync(full).isDirectory()) {
569
+ out.push(...layerFiles(full, rel))
570
+ }
571
+ else {
572
+ out.push(rel)
573
+ }
574
+ }
575
+ return out
576
+ }
577
+
578
+
579
+ // `aontu mod manifest`: the OCI artifact description a publish would
580
+ // push, and the gate that decides whether it may be.
581
+ export function modManifest(
582
+ root: string, options: ModToolOptions, against?: string,
583
+ ): ModManifestReport {
584
+ const self = modSelf(root, options)
585
+ const major = majorOf(self.version)
586
+
587
+ const missing: string[] = []
588
+ if ('' === self.path) {
589
+ missing.push('mod.path')
590
+ }
591
+ if ('' === major) {
592
+ missing.push('mod.version')
593
+ }
594
+ const main = pathJoin(root, self.main)
595
+ if (!existsSync(main)) {
596
+ missing.push(self.main)
597
+ }
598
+
599
+ const mod = '' === self.path || '' === major ?
600
+ '' : self.path + '@' + major
601
+
602
+ if (0 < missing.length) {
603
+ return {
604
+ verdict: 'error',
605
+ mod,
606
+ version: self.version,
607
+ canon: '',
608
+ config: MODULE_CONFIG_MEDIA_TYPE,
609
+ files: [],
610
+ annotations: {},
611
+ missing: missing.sort(),
612
+ findings: [],
613
+ }
614
+ }
615
+
616
+ const newSrc = readFileSync(main, 'utf8')
617
+ const canon = options.eval(newSrc, main).hash
618
+
619
+ const report: ModManifestReport = {
620
+ verdict: 'ok',
621
+ mod,
622
+ version: self.version,
623
+ canon,
624
+ config: MODULE_CONFIG_MEDIA_TYPE,
625
+ files: layerFiles(root),
626
+ annotations: {
627
+ [MODULE_ANNOTATION_CANON]: canon,
628
+ [MODULE_ANNOTATION_MAJOR]: major,
629
+ 'org.opencontainers.image.title': self.path,
630
+ 'org.opencontainers.image.version': self.version,
631
+ },
632
+ missing: [],
633
+ findings: [],
634
+ }
635
+
636
+ if (null == against) {
637
+ return report
638
+ }
639
+
640
+ // THE PUBLISH-TIME BREAKING GATE. The semantics of "breaking" belong
641
+ // wholly to G3 (ts/src/subsume.ts); this is the wiring, at the one
642
+ // place versions are minted.
643
+ const prior = modSelf(against, options)
644
+ const priorMain = pathJoin(against, prior.main)
645
+ if (!existsSync(priorMain)) {
646
+ report.verdict = 'error'
647
+ report.missing = [prior.main]
648
+ return report
649
+ }
650
+
651
+ // A MAJOR BUMP IS WHERE BREAKING IS ALLOWED. The major lives in the
652
+ // module path, so a consumer of `@1` never sees `@2` unless it asks:
653
+ // checking compatibility across majors would forbid the one change
654
+ // the version scheme exists to express.
655
+ if (majorOf(prior.version) !== major) {
656
+ return report
657
+ }
658
+
659
+ // Backward compatibility: the NEW version is the general side, so
660
+ // every instance the old one admitted must still be admitted.
661
+ const gate = subsume(newSrc, readFileSync(priorMain, 'utf8'), {
662
+ generalUrl: main,
663
+ specificUrl: priorMain,
664
+ generalPath: main,
665
+ specificPath: priorMain,
666
+ })
667
+
668
+ report.findings = gate.findings
669
+ report.verdict = MANIFEST_VERDICT[gate.verdict]
670
+ return report
671
+ }
672
+
673
+
674
+ const MANIFEST_VERDICT: Record<string, ModManifestReport['verdict']> = {
675
+ subsumes: 'ok',
676
+ does_not_subsume: 'breaking',
677
+ undecided: 'undecided',
678
+ error: 'error',
679
+ }