aontu 0.52.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/README.md +88 -0
  2. package/bin/aontu-mcp.js +4 -0
  3. package/dist/agentsmd.d.ts +16 -0
  4. package/dist/agentsmd.js +107 -0
  5. package/dist/agentsmd.js.map +1 -0
  6. package/dist/aontu.d.ts +14 -3
  7. package/dist/aontu.js +145 -4
  8. package/dist/aontu.js.map +1 -1
  9. package/dist/cli.d.ts +44 -1
  10. package/dist/cli.js +2401 -44
  11. package/dist/cli.js.map +1 -1
  12. package/dist/ctx.d.ts +16 -0
  13. package/dist/ctx.js +44 -0
  14. package/dist/ctx.js.map +1 -1
  15. package/dist/diff.d.ts +22 -0
  16. package/dist/diff.js +141 -0
  17. package/dist/diff.js.map +1 -0
  18. package/dist/err.d.ts +3 -1
  19. package/dist/err.js +48 -8
  20. package/dist/err.js.map +1 -1
  21. package/dist/graph.d.ts +16 -0
  22. package/dist/graph.js +73 -0
  23. package/dist/graph.js.map +1 -0
  24. package/dist/hcanon.d.ts +3 -0
  25. package/dist/hcanon.js +146 -0
  26. package/dist/hcanon.js.map +1 -0
  27. package/dist/hints.js +223 -5
  28. package/dist/hints.js.map +1 -1
  29. package/dist/jsonschema.d.ts +20 -0
  30. package/dist/jsonschema.js +391 -0
  31. package/dist/jsonschema.js.map +1 -0
  32. package/dist/lang.js +698 -35
  33. package/dist/lang.js.map +1 -1
  34. package/dist/lsp.d.ts +9 -2
  35. package/dist/lsp.js +262 -46
  36. package/dist/lsp.js.map +1 -1
  37. package/dist/mcp-server.d.ts +20 -0
  38. package/dist/mcp-server.js +147 -0
  39. package/dist/mcp-server.js.map +1 -0
  40. package/dist/mcp.d.ts +42 -0
  41. package/dist/mcp.js +814 -0
  42. package/dist/mcp.js.map +1 -0
  43. package/dist/mod-tool.d.ts +58 -0
  44. package/dist/mod-tool.js +498 -0
  45. package/dist/mod-tool.js.map +1 -0
  46. package/dist/mod.d.ts +31 -0
  47. package/dist/mod.js +250 -0
  48. package/dist/mod.js.map +1 -0
  49. package/dist/patch.d.ts +44 -0
  50. package/dist/patch.js +506 -0
  51. package/dist/patch.js.map +1 -0
  52. package/dist/provenance.d.ts +40 -0
  53. package/dist/provenance.js +335 -0
  54. package/dist/provenance.js.map +1 -0
  55. package/dist/query.d.ts +27 -0
  56. package/dist/query.js +294 -0
  57. package/dist/query.js.map +1 -0
  58. package/dist/reach.d.ts +14 -0
  59. package/dist/reach.js +140 -0
  60. package/dist/reach.js.map +1 -0
  61. package/dist/relation.d.ts +19 -0
  62. package/dist/relation.js +305 -0
  63. package/dist/relation.js.map +1 -0
  64. package/dist/report-sarif.d.ts +14 -0
  65. package/dist/report-sarif.js +102 -0
  66. package/dist/report-sarif.js.map +1 -0
  67. package/dist/site.d.ts +4 -0
  68. package/dist/site.js +31 -0
  69. package/dist/site.js.map +1 -1
  70. package/dist/std.d.ts +1 -0
  71. package/dist/std.js +73 -0
  72. package/dist/std.js.map +1 -0
  73. package/dist/subsume.d.ts +39 -0
  74. package/dist/subsume.js +526 -0
  75. package/dist/subsume.js.map +1 -0
  76. package/dist/trim.d.ts +19 -0
  77. package/dist/trim.js +155 -0
  78. package/dist/trim.js.map +1 -0
  79. package/dist/tsconfig.tsbuildinfo +1 -1
  80. package/dist/type.d.ts +17 -1
  81. package/dist/type.js.map +1 -1
  82. package/dist/unify.d.ts +3 -1
  83. package/dist/unify.js +287 -18
  84. package/dist/unify.js.map +1 -1
  85. package/dist/utility.d.ts +9 -1
  86. package/dist/utility.js +122 -1
  87. package/dist/utility.js.map +1 -1
  88. package/dist/val/AggFuncVal.d.ts +33 -0
  89. package/dist/val/AggFuncVal.js +202 -0
  90. package/dist/val/AggFuncVal.js.map +1 -0
  91. package/dist/val/ArithFuncVal.d.ts +31 -0
  92. package/dist/val/ArithFuncVal.js +62 -0
  93. package/dist/val/ArithFuncVal.js.map +1 -0
  94. package/dist/val/BagVal.d.ts +5 -0
  95. package/dist/val/BagVal.js +96 -5
  96. package/dist/val/BagVal.js.map +1 -1
  97. package/dist/val/CloseFuncVal.js +9 -1
  98. package/dist/val/CloseFuncVal.js.map +1 -1
  99. package/dist/val/ConjunctVal.d.ts +1 -1
  100. package/dist/val/ConjunctVal.js +19 -0
  101. package/dist/val/ConjunctVal.js.map +1 -1
  102. package/dist/val/ConstraintVal.d.ts +48 -1
  103. package/dist/val/ConstraintVal.js +1501 -110
  104. package/dist/val/ConstraintVal.js.map +1 -1
  105. package/dist/val/CopyFuncVal.d.ts +1 -2
  106. package/dist/val/CopyFuncVal.js +7 -0
  107. package/dist/val/CopyFuncVal.js.map +1 -1
  108. package/dist/val/Decimal.d.ts +1 -0
  109. package/dist/val/Decimal.js +13 -0
  110. package/dist/val/Decimal.js.map +1 -1
  111. package/dist/val/DeprecateFuncVal.d.ts +11 -0
  112. package/dist/val/DeprecateFuncVal.js +47 -0
  113. package/dist/val/DeprecateFuncVal.js.map +1 -0
  114. package/dist/val/DisjunctVal.js +130 -21
  115. package/dist/val/DisjunctVal.js.map +1 -1
  116. package/dist/val/EachFuncVal.d.ts +15 -0
  117. package/dist/val/EachFuncVal.js +75 -0
  118. package/dist/val/EachFuncVal.js.map +1 -0
  119. package/dist/val/ExpectVal.d.ts +1 -0
  120. package/dist/val/ExpectVal.js +41 -4
  121. package/dist/val/ExpectVal.js.map +1 -1
  122. package/dist/val/FeatureVal.js +1 -1
  123. package/dist/val/FeatureVal.js.map +1 -1
  124. package/dist/val/FilterFuncVal.d.ts +15 -0
  125. package/dist/val/FilterFuncVal.js +91 -0
  126. package/dist/val/FilterFuncVal.js.map +1 -0
  127. package/dist/val/FuncBaseVal.d.ts +6 -1
  128. package/dist/val/FuncBaseVal.js +178 -2
  129. package/dist/val/FuncBaseVal.js.map +1 -1
  130. package/dist/val/HideFuncVal.js.map +1 -1
  131. package/dist/val/IdFuncVal.d.ts +13 -0
  132. package/dist/val/IdFuncVal.js +54 -0
  133. package/dist/val/IdFuncVal.js.map +1 -0
  134. package/dist/val/JunctionVal.js +7 -1
  135. package/dist/val/JunctionVal.js.map +1 -1
  136. package/dist/val/KeyFuncVal.d.ts +1 -1
  137. package/dist/val/KeyFuncVal.js +38 -30
  138. package/dist/val/KeyFuncVal.js.map +1 -1
  139. package/dist/val/ListVal.js +117 -17
  140. package/dist/val/ListVal.js.map +1 -1
  141. package/dist/val/LowerFuncVal.js.map +1 -1
  142. package/dist/val/MapVal.js +102 -8
  143. package/dist/val/MapVal.js.map +1 -1
  144. package/dist/val/MatchFuncVal.d.ts +15 -0
  145. package/dist/val/MatchFuncVal.js +107 -0
  146. package/dist/val/MatchFuncVal.js.map +1 -0
  147. package/dist/val/MoveFuncVal.js.map +1 -1
  148. package/dist/val/NilVal.js +24 -0
  149. package/dist/val/NilVal.js.map +1 -1
  150. package/dist/val/OpBaseVal.d.ts +1 -1
  151. package/dist/val/OpBaseVal.js +24 -2
  152. package/dist/val/OpBaseVal.js.map +1 -1
  153. package/dist/val/OpenFuncVal.js +4 -1
  154. package/dist/val/OpenFuncVal.js.map +1 -1
  155. package/dist/val/PackFuncVal.d.ts +15 -0
  156. package/dist/val/PackFuncVal.js +108 -0
  157. package/dist/val/PackFuncVal.js.map +1 -0
  158. package/dist/val/PathFuncVal.js.map +1 -1
  159. package/dist/val/PlaceVal.d.ts +13 -0
  160. package/dist/val/PlaceVal.js +131 -0
  161. package/dist/val/PlaceVal.js.map +1 -0
  162. package/dist/val/PlusOpVal.js +11 -2
  163. package/dist/val/PlusOpVal.js.map +1 -1
  164. package/dist/val/PrefFuncVal.js.map +1 -1
  165. package/dist/val/PrefVal.d.ts +2 -2
  166. package/dist/val/PrefVal.js +78 -23
  167. package/dist/val/PrefVal.js.map +1 -1
  168. package/dist/val/RefVal.d.ts +1 -1
  169. package/dist/val/RefVal.js +158 -30
  170. package/dist/val/RefVal.js.map +1 -1
  171. package/dist/val/ReferFuncVal.d.ts +36 -0
  172. package/dist/val/ReferFuncVal.js +303 -0
  173. package/dist/val/ReferFuncVal.js.map +1 -0
  174. package/dist/val/ScalarKindVal.d.ts +1 -2
  175. package/dist/val/ScalarKindVal.js +0 -11
  176. package/dist/val/ScalarKindVal.js.map +1 -1
  177. package/dist/val/TopVal.js.map +1 -1
  178. package/dist/val/TypeFuncVal.js.map +1 -1
  179. package/dist/val/UpperFuncVal.js.map +1 -1
  180. package/dist/val/Val.d.ts +9 -2
  181. package/dist/val/Val.js +150 -4
  182. package/dist/val/Val.js.map +1 -1
  183. package/dist/val/VarVal.js.map +1 -1
  184. package/dist/val/arith.d.ts +6 -0
  185. package/dist/val/arith.js +170 -0
  186. package/dist/val/arith.js.map +1 -0
  187. package/dist/vet.d.ts +45 -0
  188. package/dist/vet.js +776 -0
  189. package/dist/vet.js.map +1 -0
  190. package/dist/walk.d.ts +2 -0
  191. package/dist/walk.js +91 -0
  192. package/dist/walk.js.map +1 -0
  193. package/grammar/aontu.gbnf +130 -0
  194. package/grammar/aontu.lark +113 -0
  195. package/package.json +30 -15
  196. package/skill/SKILL.md +37 -0
  197. package/skill/error-codes.md +62 -0
  198. package/skill/examples.md +99 -0
  199. package/skill/grammar-card.md +57 -0
  200. package/src/agentsmd.ts +135 -0
  201. package/src/aontu.ts +192 -4
  202. package/src/cli.ts +2858 -71
  203. package/src/ctx.ts +81 -0
  204. package/src/diff.ts +196 -0
  205. package/src/err.ts +52 -8
  206. package/src/graph.ts +135 -0
  207. package/src/hcanon.ts +169 -0
  208. package/src/hints.ts +271 -5
  209. package/src/jsonschema.ts +511 -0
  210. package/src/lang.ts +779 -37
  211. package/src/lsp.ts +281 -47
  212. package/src/mcp-server.ts +187 -0
  213. package/src/mcp.ts +993 -0
  214. package/src/mod-tool.ts +679 -0
  215. package/src/mod.ts +344 -0
  216. package/src/patch.ts +624 -0
  217. package/src/provenance.ts +430 -0
  218. package/src/query.ts +379 -0
  219. package/src/reach.ts +184 -0
  220. package/src/relation.ts +395 -0
  221. package/src/report-sarif.ts +137 -0
  222. package/src/site.ts +36 -1
  223. package/src/std.ts +73 -0
  224. package/src/subsume.ts +690 -0
  225. package/src/trim.ts +195 -0
  226. package/src/tsconfig.json +10 -4
  227. package/src/type.ts +51 -2
  228. package/src/unify.ts +311 -16
  229. package/src/utility.ts +139 -1
  230. package/src/val/AggFuncVal.ts +319 -0
  231. package/src/val/ArithFuncVal.ts +108 -0
  232. package/src/val/BagVal.ts +101 -4
  233. package/src/val/CloseFuncVal.ts +9 -1
  234. package/src/val/ConjunctVal.ts +20 -0
  235. package/src/val/ConstraintVal.ts +1699 -116
  236. package/src/val/CopyFuncVal.ts +7 -1
  237. package/src/val/Decimal.ts +15 -0
  238. package/src/val/DeprecateFuncVal.ts +84 -0
  239. package/src/val/DisjunctVal.ts +139 -28
  240. package/src/val/EachFuncVal.ts +133 -0
  241. package/src/val/ExpectVal.ts +43 -6
  242. package/src/val/FeatureVal.ts +1 -1
  243. package/src/val/FilterFuncVal.ts +154 -0
  244. package/src/val/FuncBaseVal.ts +200 -3
  245. package/src/val/HideFuncVal.ts +0 -2
  246. package/src/val/IdFuncVal.ts +91 -0
  247. package/src/val/JunctionVal.ts +7 -1
  248. package/src/val/KeyFuncVal.ts +39 -35
  249. package/src/val/ListVal.ts +125 -18
  250. package/src/val/LowerFuncVal.ts +0 -1
  251. package/src/val/MapVal.ts +110 -8
  252. package/src/val/MatchFuncVal.ts +176 -0
  253. package/src/val/MoveFuncVal.ts +0 -2
  254. package/src/val/NilVal.ts +25 -0
  255. package/src/val/OpBaseVal.ts +26 -3
  256. package/src/val/OpenFuncVal.ts +4 -2
  257. package/src/val/PackFuncVal.ts +175 -0
  258. package/src/val/PathFuncVal.ts +0 -1
  259. package/src/val/PlaceVal.ts +193 -0
  260. package/src/val/PlusOpVal.ts +11 -2
  261. package/src/val/PrefFuncVal.ts +0 -1
  262. package/src/val/PrefVal.ts +79 -36
  263. package/src/val/RefVal.ts +163 -30
  264. package/src/val/ReferFuncVal.ts +387 -0
  265. package/src/val/ScalarKindVal.ts +0 -13
  266. package/src/val/TopVal.ts +0 -1
  267. package/src/val/TypeFuncVal.ts +0 -2
  268. package/src/val/UpperFuncVal.ts +0 -1
  269. package/src/val/Val.ts +213 -3
  270. package/src/val/VarVal.ts +0 -1
  271. package/src/val/arith.ts +316 -0
  272. package/src/vet.ts +992 -0
  273. package/src/walk.ts +99 -0
package/src/patch.ts ADDED
@@ -0,0 +1,624 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ // OVERLAY PATCH (G7 phase 5,
4
+ // docs/capability-review/g7-machine-access.md): change a document by
5
+ // APPENDING to an overlay, not by rewriting the file.
6
+ //
7
+ // This is the stage that needs no rewriter. An overlay entry is just
8
+ // another conjunct, and unification is order-independent, so appending
9
+ // `services: auth: owner: "identity-2"` to a second file and
10
+ // evaluating both is exactly the same value as writing it into the
11
+ // first — with no parsing of the target, no comment or layout damage,
12
+ // and nothing to preserve. The spec pins that equivalence rather than
13
+ // asserting it.
14
+ //
15
+ // What an overlay CANNOT do is change a PINNED value: the lattice
16
+ // refuses 5 against 3, and the report says so with the pinning site,
17
+ // which `why` then locates. That left the loop "set → conflict → why →
18
+ // edit the pinning site" with its last step manual — and since the
19
+ // commonest vet failure of all is "the data pins the wrong value",
20
+ // `set` was unable to repair the very case it existed for.
21
+ //
22
+ // IN-PLACE REPLACE (`--in-place`) closes that. The G7 design deferred
23
+ // it behind two prerequisites: an evaluated-path → contributing-span
24
+ // map, and a comment-and-layout-preserving CST. The first now exists —
25
+ // `why` is that map, and sites carry `len` and `src` since a site was
26
+ // given an extent. The second turns out NOT to be needed for the case
27
+ // that matters, and the reason is worth stating: a CST is what you need
28
+ // to RE-SERIALISE a document, and a targeted span splice serialises
29
+ // nothing. It replaces `len` code units at one offset and leaves every
30
+ // other byte — every comment, every blank line, every alignment space —
31
+ // exactly as the author left it, because it never looks at them.
32
+ //
33
+ // What makes the splice safe rather than merely plausible is that the
34
+ // site carries `src`, the text it claims to cover. The span is VERIFIED
35
+ // against it before a byte is written, so the corrupting arithmetic
36
+ // this repository has already shipped once — `port: 0x1F` reporting
37
+ // canon `"31"` at column 7, and `(col, canon.length)` writing
38
+ // `port: 5x1F` — cannot be reached: `0x1F` is four code units and says
39
+ // so, and if the text at the span is anything else the edit is refused
40
+ // rather than guessed.
41
+ //
42
+ // Replace is never WORSE than append. Where the value is not a single
43
+ // editable literal in this overlay — a spread template governing other
44
+ // keys, a reference whose site is the `$` and not the target, two
45
+ // statements pinning the same path, a literal in an included file — the
46
+ // splice is refused and the assignment is APPENDED exactly as it would
47
+ // have been without the flag, plus one `warning` finding naming the
48
+ // case and the site it came from. Warnings never move a verdict, so
49
+ // `--in-place` cannot turn a run that would have succeeded into one
50
+ // that fails; it can only rewrite where rewriting is safe, and explain
51
+ // itself where it is not.
52
+ //
53
+ // The verdict is G2's, unchanged: `vet(entry, overlay)` already asks
54
+ // exactly the right question — does this document hold against that
55
+ // truth, and if not, where — so `set` adds a writer, not a report.
56
+
57
+ import { vet } from './vet'
58
+ import type { TrustOptions } from './type'
59
+ import type { VetFinding, VetReport, VetVerdict } from './vet'
60
+ import { pathParts, why } from './query'
61
+ import type { WhyConjunct } from './provenance'
62
+ import { Aontu } from './aontu'
63
+
64
+
65
+ export type PatchOptions = {
66
+ // Where each document CAME FROM, so relative `@"file"` loads inside
67
+ // them resolve from their own directories (vet's precedent).
68
+ entryPath?: string
69
+ overlayPath?: string
70
+ // Rewrite a pinned literal where the author wrote it, instead of
71
+ // appending a line that contradicts it. Opt-in: appending is
72
+ // non-destructive and in-place editing is not, so the caller says
73
+ // which one they meant.
74
+ inPlace?: boolean
75
+ // The include capability this document evaluates under
76
+ // (G5, docs/trust.md); vet's precedent.
77
+ trust?: TrustOptions
78
+ }
79
+
80
+
81
+ // One literal rewritten where it was written. `from` and `to` are
82
+ // SOURCE TEXT, not values: replacing `0x1F` with `31` is a different
83
+ // edit from replacing it with `0x1F`, and only the spelling says which.
84
+ export type PatchReplacement = {
85
+ col: number
86
+ file: string
87
+ from: string
88
+ path: string
89
+ row: number
90
+ to: string
91
+ }
92
+
93
+ export type PatchReport = {
94
+ // The overlay text as it would stand after the assignments: the
95
+ // existing text, with any in-place replacements applied, plus one
96
+ // appended line for each assignment that was not replaced. The caller
97
+ // writes it — an engine that touched the filesystem could not be used
98
+ // by a server, and the CLI is the one place that knows about files.
99
+ overlay: string
100
+ // The appended lines alone, in order.
101
+ appended: string[]
102
+ // The in-place replacements made, in the order the assignments were
103
+ // given (NOT the order they were applied to the text, which is
104
+ // back-to-front so that earlier offsets stay valid). Empty unless
105
+ // `inPlace` was asked for.
106
+ replaced: PatchReplacement[]
107
+ verdict: VetVerdict
108
+ findings: VetFinding[]
109
+ }
110
+
111
+
112
+ // An assignment is `<path>=<value>`, split at the FIRST `=`: a path
113
+ // segment is a name, and the value is arbitrary Aontu source, which
114
+ // may itself contain `=` (`a: min(1)` does not, but a string can).
115
+ export function parseAssignment(
116
+ text: string): { path: string, value: string } | undefined {
117
+ const eq = text.indexOf('=')
118
+ if (eq < 1) {
119
+ return undefined
120
+ }
121
+ const path = text.slice(0, eq).trim()
122
+ const value = text.slice(eq + 1).trim()
123
+ if ('' === value || 0 === pathParts(path).length) {
124
+ return undefined
125
+ }
126
+ return { path, value }
127
+ }
128
+
129
+
130
+ // The path-flattened conjunct one assignment becomes:
131
+ // `$.a.b = 1` is `"a": "b": 1`. Keys are QUOTED — a segment may be a
132
+ // word the grammar spells otherwise (`if`), a number, or a name with
133
+ // a space in it, and quoting one key is the same value as writing it
134
+ // bare.
135
+ export function overlayLine(path: string, value: string): string {
136
+ return pathParts(path).map((p) => JSON.stringify(p)).join(': ') +
137
+ ': ' + value
138
+ }
139
+
140
+
141
+ // The character offset of a 1-based (row, col) in `src`, or -1 when the
142
+ // text has no such position. Columns are UTF-16 code units, which is
143
+ // what a site carries and what a JavaScript string index already is —
144
+ // so this is the inverse of the site arithmetic, not a reinterpretation
145
+ // of it (go/patch.go converts to a byte offset, because Go strings are
146
+ // bytes; both address the same character).
147
+ export function offsetAt(src: string, row: number, col: number): number {
148
+ if (row < 1 || col < 1) {
149
+ return -1
150
+ }
151
+ let off = 0
152
+ for (let r = 1; r < row; r++) {
153
+ const nl = src.indexOf('\n', off)
154
+ if (nl < 0) {
155
+ return -1
156
+ }
157
+ off = nl + 1
158
+ }
159
+ const at = off + (col - 1)
160
+ return at <= src.length ? at : -1
161
+ }
162
+
163
+
164
+ // The text a site covers, or undefined when the site does not describe
165
+ // a position in this text at all.
166
+ export function spanAt(
167
+ src: string, site: { row: number, col: number, len: number }
168
+ ): string | undefined {
169
+ const off = offsetAt(src, site.row, site.col)
170
+ return off < 0 ? undefined : src.slice(off, off + site.len)
171
+ }
172
+
173
+
174
+ // DOES THE TEXT AT THIS SITE SAY WHAT THE SITE CLAIMS IT SAYS?
175
+ //
176
+ // The last check before a splice, and the one that makes the write
177
+ // PROVABLE rather than argued. Exported so it can be exercised with a
178
+ // site the engine would never produce — an out-of-range position, a
179
+ // span over different text — which is the only way to test a guard whose
180
+ // whole purpose is to catch a state the rest of the code says cannot
181
+ // happen. (go/patch.go has the twin, tested the same way.)
182
+ export function spanHolds(
183
+ src: string, site: { row: number, col: number, len: number }, expect: string
184
+ ): boolean {
185
+ // THE SITE'S OWN LENGTH IS PART OF ITS CLAIM, and is checked before
186
+ // the text is. A site whose `len` disagrees with the text it says it
187
+ // covers CONTRADICTS ITSELF, which is exactly the state this guard
188
+ // exists to catch — and a zero-length span would otherwise compare
189
+ // equal against nothing and then splice nothing, INSERTING the new
190
+ // value rather than replacing anything.
191
+ //
192
+ // Both ports compare in UTF-16 code units, which is what a site's
193
+ // `len` counts. That is free here and is not in Go, where a string is
194
+ // bytes (go/patch.go converts).
195
+ if ('' === expect || site.len !== expect.length) {
196
+ return false
197
+ }
198
+ return spanAt(src, site) === expect
199
+ }
200
+
201
+
202
+ // WHY IS THE VALUE AT THIS PATH WHAT IT IS, and is exactly one of the
203
+ // answers a literal this overlay can edit in place?
204
+ //
205
+ // The four refusals below are not defensive padding; each is a real
206
+ // document shape that the probe corpus produced, and each would corrupt
207
+ // something different if the splice ran anyway:
208
+ //
209
+ // - a SPREAD contribution's site is inside the template, which
210
+ // governs every other key too, so rewriting it there changes keys
211
+ // the author did not name;
212
+ // - a REFERENCE's site is the `$` that starts the path and has length
213
+ // 1, so splicing over it writes the new value INTO the path
214
+ // expression (`$.base` becomes `5.base`) — and the value the author
215
+ // wants changed lives at the target anyway;
216
+ // - TWO literals at one path (a duplicate key, two files merged) give
217
+ // no single place to edit, and picking either silently is picking
218
+ // for the author;
219
+ // - a literal in an INCLUDED file is editable, but not by
220
+ // `--overlay <this file>`: the write would land in a document the
221
+ // caller did not name.
222
+ //
223
+ // A PREFERENCE is not refused here for the same reason it is not
224
+ // replaced: appending already overrides a default correctly, so the
225
+ // caller loses nothing by falling through to it.
226
+ function editableLiteral(
227
+ overlaySrc: string,
228
+ path: string,
229
+ overlayPath: string | undefined,
230
+ ): { site: PatchReplacement | undefined, finding: VetFinding | undefined } {
231
+ // THE AUTHORITY IS THE OVERLAY TEXT ALONE, WITH INCLUDES DENIED.
232
+ //
233
+ // The splice happens in the text this function was handed, so what it
234
+ // has to establish is that the contribution is IN that text — and the
235
+ // site's `file` cannot establish it. Two ways it fails: a caller of
236
+ // the library API need not pass `overlayPath`, leaving nothing to
237
+ // compare against; and the Go port names the ENTRY document for an
238
+ // included value anyway (issue #66), so the comparison is the overlay
239
+ // against itself. Either way an included literal's (row, col, len,
240
+ // src) can COINCIDE with different text at the same coordinates here
241
+ // — an include holding `a: 42` at 1:4 and an overlay holding `x: 42`
242
+ // at 1:4 — and the span verification cannot tell them apart, because
243
+ // the text really does match. The splice then rewrites `x` while
244
+ // reporting a replacement of `$.a`, in both ports.
245
+ //
246
+ // Denying includes removes the ambiguity at its source rather than
247
+ // detecting it: what resolves is what this text says by itself. An
248
+ // overlay that loads other documents therefore cannot be edited in
249
+ // place at all — the conservative answer, and the assignment still
250
+ // appends. It costs nothing in the shape `set` is for, an overlay it
251
+ // owns and appends to, and it does not depend on file attribution, so
252
+ // both ports agree without waiting on #66.
253
+ const alone = why(overlaySrc, path, {
254
+ trust: { include: 'none' },
255
+ ...(null == overlayPath ? {} : { path: overlayPath }),
256
+ })
257
+
258
+ if (true !== alone.ok || null == alone.record) {
259
+ // Nothing here BY ITSELF. Two very different reasons, and they earn
260
+ // different answers: the path may simply not be in this overlay, in
261
+ // which case appending is the whole of the answer and nothing has
262
+ // gone wrong — or it may be here only because something was loaded,
263
+ // which is the case above and has to say so.
264
+ const withLoads = why(overlaySrc, path,
265
+ null == overlayPath ? undefined : { path: overlayPath })
266
+ if (true !== withLoads.ok || null == withLoads.record) {
267
+ return { site: undefined, finding: undefined }
268
+ }
269
+ return {
270
+ site: undefined,
271
+ finding: notEditable('patch_not_editable', path,
272
+ 'this path resolves only once the overlay loads another ' +
273
+ 'document, so no literal here can be shown to be the one to ' +
274
+ 'edit; run set with the document that writes it as the overlay',
275
+ withLoads.record.conjuncts),
276
+ }
277
+ }
278
+
279
+ const record = alone.record
280
+
281
+ // No `?? []`: WhyRecord.conjuncts is a non-optional array and the
282
+ // record's own presence was just established, so a fallback here
283
+ // would claim a possibility the type does not have — and the
284
+ // coverage gate says so, an arm nothing can take.
285
+ const conjuncts: WhyConjunct[] = record.conjuncts
286
+
287
+ // A VALUE REACHED THROUGH A REFERENCE IS NOT THIS PATH'S TO EDIT.
288
+ // Provenance travels through clones now, so `n: $.base` against
289
+ // `base: 7` reports the literal `7` -- correctly, and at the site
290
+ // where it was written, which is `base`'s line and not `n`'s. A
291
+ // splice there would rewrite the REFERENT: every other reader of
292
+ // `$.base` changes with it, and the path the caller named does not
293
+ // move at all. The reference is what stands here, so the reference
294
+ // is what has to be edited, wherever it points.
295
+ const refs = conjuncts.filter((c) => 'ref' === c.role)
296
+ if (0 < refs.length) {
297
+ return {
298
+ site: undefined,
299
+ finding: notEditable('patch_not_editable', path,
300
+ 'the value here is reached through a reference (ref), so the ' +
301
+ 'literal below belongs to the path it points at; edit where it ' +
302
+ 'comes from',
303
+ refs),
304
+ }
305
+ }
306
+
307
+ const literals = conjuncts.filter((c) => 'literal' === c.role)
308
+
309
+ if (1 < literals.length) {
310
+ return {
311
+ site: undefined,
312
+ finding: notEditable('patch_ambiguous', path,
313
+ 'two or more statements pin this path, so there is no single ' +
314
+ 'place to edit; the sites below are all of them',
315
+ literals),
316
+ }
317
+ }
318
+
319
+ // Only indirect contributions. A pref is the benign case — append
320
+ // overrides a default — so it earns no finding; the others do.
321
+ if (0 === literals.length) {
322
+ const indirect = conjuncts.filter((c) => 'pref' !== c.role)
323
+ if (0 === indirect.length) {
324
+ return { site: undefined, finding: undefined }
325
+ }
326
+ return {
327
+ site: undefined,
328
+ finding: notEditable('patch_not_editable', path,
329
+ 'the value here is not written as a literal (' +
330
+ indirect.map((c) => c.role).join(', ') +
331
+ '), so there is no literal to rewrite; edit where it comes from',
332
+ indirect),
333
+ }
334
+ }
335
+
336
+ const one = literals[0]
337
+
338
+ // THE SPAN MUST CHECK OUT, and this is one condition rather than two.
339
+ //
340
+ // A first draft tested "no extent" separately from "the text
341
+ // disagrees", which read as two guards and was really one question
342
+ // asked twice — with the second half unreachable, since denying
343
+ // includes means the site comes from evaluating THIS TEXT with
344
+ // nothing loaded, so its coordinates describe this text by
345
+ // construction. Merged, the question is reachable through the case
346
+ // that has no extent at all (`x: hello |> upper` synthesises a call
347
+ // the parser never sited), so the check is exercised rather than
348
+ // argued for — and `spanHolds` is exported and tested against sites
349
+ // the engine would never produce, which is the only way to reach the
350
+ // half that remains theoretical.
351
+ //
352
+ // It is load-bearing either way: a contribution with no `src` would
353
+ // otherwise splice ZERO characters, INSERTING the new value into the
354
+ // middle of a line instead of replacing anything.
355
+ if (!spanHolds(overlaySrc, one.site, one.src)) {
356
+ return {
357
+ site: undefined,
358
+ finding: notEditable('patch_span_mismatch', path,
359
+ 'the overlay does not hold ' + JSON.stringify(one.src) + ' at ' +
360
+ one.site.row + ':' + one.site.col + ' (len ' + one.site.len +
361
+ '), so the span cannot be verified before writing',
362
+ [one]),
363
+ }
364
+ }
365
+
366
+ // DOES THE SPAN MEAN THE WHOLE CONTRIBUTION?
367
+ //
368
+ // This is the check that `role === 'literal'` looks like it makes and
369
+ // does not. A site names the TOKEN it points at, so a COMPOUND value
370
+ // reports its OPENING token while its canon is the whole thing:
371
+ // `min(1)` is a literal-role contribution whose src is `min`, `1+2`
372
+ // reports `1`, `$.k+1` reports `$`, `{b:1}` reports `{` and `[1,2]`
373
+ // reports `[`. Splicing over any of those writes the new value INTO
374
+ // the expression — `a: 5(1)`, `a: 5+2`, `a: 5.k+1` — which is the
375
+ // same class of corruption as the canon-length arithmetic, reached by
376
+ // a different route.
377
+ //
378
+ // Rather than enumerate the shapes (a list is a thing to be
379
+ // incomplete about), ASK THE ENGINE: parse `src` on its own and
380
+ // require the value it means to be the value the contribution
381
+ // contributed. That is exactly the property a splice needs — this
382
+ // text, alone, is this value — and it is decided by the same unifier
383
+ // that produced the contribution, so it cannot drift from it.
384
+ //
385
+ // It also gets the interesting case right without special-casing it:
386
+ // `0x1F` canons to `31`, which is not its own spelling, but IS the
387
+ // contribution's canon, so a hex literal is editable while `min` is
388
+ // not.
389
+ const span = spanValue(one.src)
390
+ if (null == span || span.canon !== one.canon) {
391
+ return {
392
+ site: undefined,
393
+ finding: notEditable('patch_not_editable', path,
394
+ 'the site names ' + JSON.stringify(one.src) + ', which is the ' +
395
+ 'opening token of ' + one.canon + ' rather than the whole of ' +
396
+ 'it; rewriting that span would edit the expression, not the ' +
397
+ 'value',
398
+ [one]),
399
+ }
400
+ }
401
+
402
+ // AN ABSTRACT CONTRIBUTION IS NOT A PIN. `a: integer` and
403
+ // `a: above(0)` state a constraint, and appending already narrows
404
+ // them — that is the one case the status report notes `set` could
405
+ // always repair. Replacing them would silently DISCARD a constraint
406
+ // the author wrote, to no benefit, so this falls through to append.
407
+ if (true !== span.concrete) {
408
+ return {
409
+ site: undefined,
410
+ finding: notEditable('patch_not_editable', path,
411
+ one.canon + ' is a constraint here, not a pinned value; ' +
412
+ 'appending narrows it without discarding what it says',
413
+ [one]),
414
+ }
415
+ }
416
+
417
+ return {
418
+ site: {
419
+ col: one.site.col,
420
+ file: one.site.file,
421
+ from: one.src,
422
+ path,
423
+ row: one.site.row,
424
+ to: '',
425
+ },
426
+ finding: undefined,
427
+ }
428
+ }
429
+
430
+
431
+ // What does this source text mean ON ITS OWN, and is it a value rather
432
+ // than a constraint? Undefined when it does not stand alone at all
433
+ // (`$` from a path, an unbalanced `{`).
434
+ //
435
+ // The wrapper key is arbitrary and the document it makes is thrown
436
+ // away; what is wanted is the unifier's own reading of the fragment.
437
+ export function spanValue(
438
+ src: string): { canon: string, concrete: boolean } | undefined {
439
+ // NO COLLECTING CONTEXT: `unify` THROWS on a source it cannot read,
440
+ // so a ctx.err check here is a branch nothing can reach — the catch
441
+ // below is the only path a bad fragment takes. (A first draft had
442
+ // both, and the coverage gate called the pair what it was.) What the
443
+ // nil test still earns is the fragment that PARSES and means nothing:
444
+ // `$` is a path with no target, and answers a nil rather than
445
+ // throwing.
446
+ let canon: string
447
+ try {
448
+ const root: any = new Aontu().unify('v: ' + src)
449
+ const node: any = root?.peg?.['v']
450
+ if (null == node || true === node.isNil) {
451
+ return undefined
452
+ }
453
+ canon = node.canon
454
+ }
455
+ catch (e) {
456
+ return undefined
457
+ }
458
+
459
+ // Generability is the concreteness test, and it is the engine's own:
460
+ // a kind, a constraint and an unresolved disjunction all refuse to
461
+ // generate, which is precisely the line this needs drawn.
462
+ try {
463
+ new Aontu().generate('v: ' + src)
464
+ }
465
+ catch (e) {
466
+ return { canon, concrete: false }
467
+ }
468
+ return { canon, concrete: true }
469
+ }
470
+
471
+
472
+ // A refusal to replace, as a WARNING: the assignment still appends, so
473
+ // nothing about the run got worse and the verdict must not move
474
+ // (ts/src/vet.ts, "warnings never touch the verdict"). What the finding
475
+ // adds is the reason, which is the whole value of asking for --in-place
476
+ // over plain set.
477
+ function notEditable(
478
+ code: string, path: string, why: string, from: WhyConjunct[]
479
+ ): VetFinding {
480
+ return {
481
+ code,
482
+ class: 'patch_span_mismatch' === code ? 'internal' : 'reference',
483
+ severity: 'warning',
484
+ path,
485
+ // No separate `note`: the renderer prints both, and a note that
486
+ // restates its own message is noise wearing a second label.
487
+ message: 'cannot rewrite ' + path + ' in place: ' + why,
488
+ sites: from.map((c) => ({
489
+ file: c.site.file,
490
+ row: c.site.row,
491
+ col: c.site.col,
492
+ len: c.site.len,
493
+ src: c.src,
494
+ role: 'data',
495
+ value: c.canon,
496
+ })) as any,
497
+ }
498
+ }
499
+
500
+
501
+ // Append the assignments to the overlay and answer what the result
502
+ // holds. The report's verdict is the vet verdict of the ENTRY against
503
+ // the new overlay: `valid` when it holds and is concrete, `incomplete`
504
+ // when nothing contradicts but the truth is not yet satisfied,
505
+ // `invalid` when the overlay contradicts a pinned value, `error` when
506
+ // the entry itself does not stand up.
507
+ export function patch(
508
+ entrySrc: string,
509
+ overlaySrc: string,
510
+ assignments: string[],
511
+ opts?: PatchOptions,
512
+ ): PatchReport {
513
+ const options = opts ?? {}
514
+
515
+ const appended: string[] = []
516
+ const replaced: PatchReplacement[] = []
517
+ const notes: VetFinding[] = []
518
+ // Each pending edit as (offset, length, text). Collected first and
519
+ // applied last, back to front: a splice shifts every offset after it,
520
+ // and recomputing them per edit is a way to be subtly wrong for free.
521
+ const edits: { at: number, len: number, to: string }[] = []
522
+
523
+ for (const text of assignments) {
524
+ const a = parseAssignment(text)
525
+ if (null == a) {
526
+ return {
527
+ overlay: overlaySrc,
528
+ appended: [],
529
+ replaced: [],
530
+ verdict: 'error',
531
+ findings: [{
532
+ code: 'patch_assignment',
533
+ class: 'parse',
534
+ severity: 'error',
535
+ path: '$',
536
+ message: `Not a <path>=<value> assignment: ${text}`,
537
+ sites: [],
538
+ }],
539
+ }
540
+ }
541
+
542
+ if (true === options.inPlace) {
543
+ const found = editableLiteral(overlaySrc, a.path, options.overlayPath)
544
+ if (null != found.finding) {
545
+ notes.push(found.finding)
546
+ }
547
+ if (null != found.site) {
548
+ // Two assignments naming the same path would splice the same
549
+ // span twice. The second is the one the author wrote last, so
550
+ // it wins — and the first is dropped rather than layered.
551
+ const at = offsetAt(overlaySrc, found.site.row, found.site.col)
552
+ const dup = edits.findIndex((e) => e.at === at)
553
+ const edit = { at, len: found.site.from.length, to: a.value }
554
+ if (dup < 0) {
555
+ edits.push(edit)
556
+ replaced.push({ ...found.site, to: a.value })
557
+ }
558
+ else {
559
+ edits[dup] = edit
560
+ replaced[dup] = { ...found.site, to: a.value }
561
+ }
562
+ continue
563
+ }
564
+ }
565
+
566
+ appended.push(overlayLine(a.path, a.value))
567
+ }
568
+
569
+ const overlay = joinOverlay(applyEdits(overlaySrc, edits), appended)
570
+
571
+ // The file names ride as URLs as well as base paths, so a finding
572
+ // names the entry and the overlay rather than vet's generic
573
+ // `schema`/`data` labels — with two documents that both belong to
574
+ // the caller, "which file" is the whole question.
575
+ const report: VetReport = vet(entrySrc, overlay, {
576
+ trust: options.trust,
577
+ schemaPath: options.entryPath,
578
+ dataPath: options.overlayPath,
579
+ schemaUrl: options.entryPath,
580
+ dataUrl: options.overlayPath,
581
+ } as any)
582
+
583
+ return {
584
+ overlay,
585
+ appended,
586
+ replaced,
587
+ verdict: report.verdict,
588
+ // The refusals come FIRST: they explain why the run took the shape
589
+ // it did, and a reader who stops after the first finding should
590
+ // read that rather than a conflict it predicted.
591
+ findings: notes.concat(report.findings),
592
+ }
593
+ }
594
+
595
+
596
+ // Apply the collected splices back to front, so an earlier edit's
597
+ // offset is never invalidated by a later one having already run.
598
+ function applyEdits(
599
+ src: string, edits: { at: number, len: number, to: string }[]
600
+ ): string {
601
+ if (0 === edits.length) {
602
+ return src
603
+ }
604
+ let out = src
605
+ for (const e of [...edits].sort((x, y) => y.at - x.at)) {
606
+ out = out.slice(0, e.at) + e.to + out.slice(e.at + e.len)
607
+ }
608
+ return out
609
+ }
610
+
611
+
612
+ // One line per assignment, after whatever the overlay already said. A
613
+ // trailing newline is kept when the file had one and added when it
614
+ // did not: a file that does not end in a newline is still a file, and
615
+ // appending to it must not join two entries into one line.
616
+ function joinOverlay(overlaySrc: string, appended: string[]): string {
617
+ if (0 === appended.length) {
618
+ return overlaySrc
619
+ }
620
+ const head = '' === overlaySrc || overlaySrc.endsWith('\n')
621
+ ? overlaySrc
622
+ : overlaySrc + '\n'
623
+ return head + appended.join('\n') + '\n'
624
+ }