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,395 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ // RELATION GRAPH CHECKS (G4 phase 5,
4
+ // docs/capability-review/g4-identity-relations.md): acyclicity and
5
+ // inverse consistency over the edge set, checked AFTER unification and
6
+ // never by it.
7
+ //
8
+ // Why not in the lattice. Both properties are GLOBAL and NON-MONOTONE:
9
+ // an acyclic graph becomes cyclic when one more edge unifies in, and an
10
+ // inverse that is present becomes absent when the far side is narrowed.
11
+ // The lattice guarantee is that more information never falsifies what
12
+ // has been observed, so a constraint that could be true and then false
13
+ // is not a constraint the lattice may hold. These are facts about the
14
+ // finished model, and the verb that reports facts about a finished
15
+ // model is where they belong.
16
+ //
17
+ // A relation is DECLARED as data, under the `relations` key of the
18
+ // document root, which is the `std/system` vocabulary's convention:
19
+ //
20
+ // relations: dependsOn: $.std.Relation & {
21
+ // target: $.std.Service, inverse: dependedOnBy, acyclic: true
22
+ // }
23
+ //
24
+ // Nothing in the engine knows the name `relations`; this pass does,
25
+ // and says so.
26
+
27
+ import { Aontu } from './aontu'
28
+ import { failureFinding } from './vet'
29
+ import type { VetFinding } from './vet'
30
+ import type { TrustOptions, Val } from './type'
31
+ import { graphOf } from './graph'
32
+ import type { Edge, EntityEntry, Graph } from './graph'
33
+ import { cmpCodePoint } from './keyorder'
34
+ import { unite } from './unify'
35
+
36
+
37
+ export type RelationVerdict = 'pass' | 'fail' | 'error'
38
+
39
+ export type RelationFinding = {
40
+ code: string
41
+ // The relation the finding is about.
42
+ relation: string
43
+ // Where the offending edge is written, as a `$.dotted.path`.
44
+ at: string
45
+ // For a cycle, the entities it runs through, in the order the walk
46
+ // found them, closing back on the first. For a missing inverse, the
47
+ // two ends and the relation that should have mirrored it.
48
+ detail: string[]
49
+ }
50
+
51
+ export type RelationReport = {
52
+ verdict: RelationVerdict
53
+ findings: RelationFinding[]
54
+
55
+ // WHY the graph could not be looked at, in the same finding shape
56
+ // vet reports in (the review's finding F). `findings` is about the
57
+ // GRAPH and stays that way; a document that does not stand up has no
58
+ // graph to have findings about, and an `error` verdict used to
59
+ // arrive with an empty list -- something is wrong, and nothing about
60
+ // what. Present ONLY on an `error` verdict.
61
+ errors?: VetFinding[]
62
+ }
63
+
64
+ export type RelationOptions = {
65
+ // Where the document CAME FROM, so a relative `@"file"` load inside
66
+ // it resolves from its own directory (trimCheck's precedent).
67
+ path?: string
68
+ // The include capability this document evaluates under (G5,
69
+ // docs/trust.md). vet's precedent: the verb passes the profile the
70
+ // caller asked for, and an absent option means today's default.
71
+ trust?: TrustOptions
72
+ }
73
+
74
+
75
+ // One declared relation, as the document spells it.
76
+ type Declared = {
77
+ name: string
78
+ inverse?: string
79
+ acyclic: boolean
80
+ // What the FAR END must satisfy, if the relation says. Absent when
81
+ // the relation declares none, and when it declares `top` -- which
82
+ // constrains nothing and would report nothing, so reading it as a
83
+ // declaration would only cost a meet per edge.
84
+ target?: Val
85
+ }
86
+
87
+
88
+ // The entity an address names — everything before the first dot. An
89
+ // edge into `svc/auth.ports.http` is an edge to `svc/auth`: a relation
90
+ // holds between ENTITIES, and the path inside one says which part of it
91
+ // the link reaches.
92
+ function entityOf(addr: string): string {
93
+ const dot = addr.indexOf('.')
94
+ return dot < 0 ? addr : addr.slice(0, dot)
95
+ }
96
+
97
+
98
+ // The node an address names, or undefined. The entity's own position
99
+ // comes from the graph (a merged entity sits at every position that
100
+ // declared it, and they hold the same value, so the FIRST in the
101
+ // graph's sorted list is as good as any and is the same one in both
102
+ // ports); the rest of the address walks into it, exactly as the
103
+ // address's own grammar says.
104
+ // NO GUARD ON THE LOOKUP OR THE WALK. An edge exists only because
105
+ // `refer()` RESOLVED its full address, so `find` cannot miss and no
106
+ // segment can fall off: an address that does not walk is
107
+ // `refer_unresolved` at unification and the document never reaches the
108
+ // graph (probed for a missing key, a scalar mid-path and an
109
+ // out-of-range index, in both ports). An unreachable `if` is a branch
110
+ // arm the ADR-002 gate counts and no marker suppresses, so the Go twin
111
+ // keeps its guards — where a nil would PANIC rather than propagate,
112
+ // and where the marker mechanism can carry them — and this one relies
113
+ // on optional chaining instead.
114
+ function addressed(
115
+ root: any, graph: Graph, addr: string
116
+ ): any {
117
+ const dot = addr.indexOf('.')
118
+ const name = dot < 0 ? addr : addr.slice(0, dot)
119
+ const entry = graph.entities.find((e) => e.id === name) as EntityEntry
120
+ const segs = entry.paths[0].slice(2).split('.')
121
+ .concat(dot < 0 ? [] : addr.slice(dot + 1).split('.'))
122
+ let node: any = root
123
+ for (const seg of segs) {
124
+ node = node?.peg?.[seg]
125
+ }
126
+ return node
127
+ }
128
+
129
+
130
+ // Does the far end satisfy the declared target? A TEST, never a flow:
131
+ // the check reports on a finished model and writing into it would be
132
+ // generation, which `relations` does not do (the same rule that keeps
133
+ // it from writing an author's inverse for them). So both sides are
134
+ // CLONED into a throwaway context and the meet is taken there; what
135
+ // the document holds is untouched either way.
136
+ //
137
+ // `refer(t)` is the other half of this and does flow, at the site. The
138
+ // two agree on what "satisfies" means -- a meet that is not a nil --
139
+ // which is what lets a relation declare once what every site would
140
+ // otherwise repeat.
141
+ // `root` is the DOCUMENT, not the node: a target lifted out of the
142
+ // model (`target: $.std.Service`) can still hold a reference that
143
+ // resolves against the document, and a probe rooted at the far end
144
+ // answers `no_path` for it -- which the worked example in
145
+ // test/spec/relation.tsv caught the moment this check existed.
146
+ function meets(
147
+ aontu: any, root: any, node: any, target: any
148
+ ): string | undefined {
149
+ const ctx = aontu.ctx({ collect: true })
150
+ ctx.root = root
151
+ const out: any = unite(ctx, node.clone(ctx), target.clone(ctx), 'relation-target')
152
+ if (true === out?.isNil) {
153
+ return out.why as string
154
+ }
155
+ if (0 < ctx.err.length) {
156
+ return ctx.err[0].why as string
157
+ }
158
+
159
+ // A MEET THAT LEAVES A HOLE IS NOT SATISFACTION. `target:
160
+ // {kind: service, port: integer}` against a far end with no `port`
161
+ // does not CONFLICT -- the meet simply carries `integer` into a key
162
+ // that had none -- and a check that stopped at "no conflict" would
163
+ // pass a far end that is missing half of what the relation demands.
164
+ //
165
+ // What `refer(t)` does at the site is the yardstick: it flows `t` in,
166
+ // and the document then fails to generate, because `integer` is not a
167
+ // value. So the same question is asked here -- can the far end still
168
+ // generate once the target is met? -- and the answer is compared with
169
+ // the far end ALONE, so a node that was already incomplete for its
170
+ // own reasons is not blamed on the relation that points at it.
171
+ // The REASON reported is the engine's own -- the code `refer(t)` at
172
+ // the site would have raised -- rather than a name invented here.
173
+ const probe = (v: any): string | undefined => {
174
+ const gctx = aontu.ctx({ collect: true })
175
+ gctx.root = root
176
+ v.gen(gctx)
177
+ return 0 === gctx.err.length ? undefined : (gctx.err[0].why as string)
178
+ }
179
+ const alone = probe(node.clone(aontu.ctx({ collect: true })))
180
+ const met = probe(out)
181
+ return undefined === alone && undefined !== met ? met : undefined
182
+ }
183
+
184
+
185
+ function declaredRelations(root: any): Declared[] {
186
+ const rels = root?.peg?.relations
187
+ if (true !== rels?.isMap) {
188
+ return []
189
+ }
190
+ const out: Declared[] = []
191
+ for (const name of Object.keys(rels.peg).sort(cmpCodePoint)) {
192
+ const r: any = rels.peg[name]
193
+ if (true !== r?.isMap) {
194
+ continue
195
+ }
196
+ const inv: any = r.peg.inverse
197
+ const acy: any = r.peg.acyclic
198
+ const tgt: any = r.peg.target
199
+ out.push({
200
+ name,
201
+ inverse: true === inv?.isScalar && 'string' === typeof inv.peg
202
+ ? inv.peg : undefined,
203
+ acyclic: true === acy?.isScalar && true === acy.peg,
204
+ target: true === tgt?.isVal && true !== tgt.isTop ? tgt : undefined,
205
+ })
206
+ }
207
+ return out
208
+ }
209
+
210
+
211
+ // The first cycle reachable from `start`, as the entities it runs
212
+ // through, or undefined. Depth-first with the path as the stack, and
213
+ // the successors visited in sorted order, so the cycle a report names
214
+ // is the same one in both ports.
215
+ function findCycle(
216
+ start: string,
217
+ succ: Map<string, string[]>,
218
+ done: Set<string>,
219
+ ): string[] | undefined {
220
+ const stack: string[] = []
221
+ const onStack = new Set<string>()
222
+
223
+ const walk = (node: string): string[] | undefined => {
224
+ if (onStack.has(node)) {
225
+ return [...stack.slice(stack.indexOf(node)), node]
226
+ }
227
+ if (done.has(node)) {
228
+ return undefined
229
+ }
230
+ done.add(node)
231
+ stack.push(node)
232
+ onStack.add(node)
233
+ for (const next of succ.get(node) ?? []) {
234
+ const found = walk(next)
235
+ if (undefined !== found) {
236
+ return found
237
+ }
238
+ }
239
+ stack.pop()
240
+ onStack.delete(node)
241
+ return undefined
242
+ }
243
+
244
+ return walk(start)
245
+ }
246
+
247
+
248
+ // The relation checks for one document.
249
+ export function relationCheck(
250
+ src: string, opts?: RelationOptions): RelationReport {
251
+ const options = opts ?? {}
252
+ const aontu = new Aontu(
253
+ null == options.trust ? undefined : { trust: options.trust })
254
+ const ctx = aontu.ctx({ collect: true })
255
+ const parseOpts = null == options.path ? undefined : { path: options.path }
256
+ const root: any = aontu.unify(src, parseOpts, ctx)
257
+
258
+ // A document that does not stand up is not a document with a bad
259
+ // graph: the errors it already has are the answer, and blaming its
260
+ // relations on top would be noise.
261
+ if (0 < ctx.err.length || true === root?.isNil) {
262
+ return {
263
+ verdict: 'error',
264
+ findings: [],
265
+ errors: [failureFinding(ctx, options.path, root)],
266
+ }
267
+ }
268
+
269
+ const declared = declaredRelations(root)
270
+ if (0 === declared.length) {
271
+ return { verdict: 'pass', findings: [] }
272
+ }
273
+
274
+ const graph = graphOf(root)
275
+ const edges = graph.edges
276
+ const findings: RelationFinding[] = []
277
+
278
+ // The edge set, indexed the two ways the checks read it.
279
+ const byRelation = new Map<string, Edge[]>()
280
+ const pairs = new Set<string>()
281
+ for (const e of edges) {
282
+ if ('' === e.from) {
283
+ // An edge outside every entity has no source to be a relation OF.
284
+ continue
285
+ }
286
+ const list = byRelation.get(e.key)
287
+ if (undefined === list) {
288
+ byRelation.set(e.key, [e])
289
+ }
290
+ else {
291
+ list.push(e)
292
+ }
293
+ pairs.add(e.key + ' ' + e.from + ' ' + entityOf(e.to))
294
+ }
295
+
296
+ for (const rel of declared) {
297
+ const mine = byRelation.get(rel.name) ?? []
298
+
299
+ if (rel.acyclic) {
300
+ const succ = new Map<string, string[]>()
301
+ for (const e of mine) {
302
+ const list = succ.get(e.from)
303
+ const to = entityOf(e.to)
304
+ if (undefined === list) {
305
+ succ.set(e.from, [to])
306
+ }
307
+ else {
308
+ list.push(to)
309
+ }
310
+ }
311
+ for (const list of succ.values()) {
312
+ list.sort(cmpCodePoint)
313
+ }
314
+
315
+ // The roots are visited in sorted order, and a node already
316
+ // settled is not revisited, so one cycle is reported once and the
317
+ // SAME one in both ports.
318
+ const done = new Set<string>()
319
+ const roots = [...succ.keys()].sort(cmpCodePoint)
320
+ for (const from of roots) {
321
+ const cycle = findCycle(from, succ, done)
322
+ if (undefined !== cycle) {
323
+ // The cycle's first node is a key of `succ`, and every key of
324
+ // `succ` came from an edge's `from`, so the edge is there.
325
+ const at = mine.find((e) => e.from === cycle[0]) as Edge
326
+ findings.push({
327
+ code: 'relation_cycle',
328
+ relation: rel.name,
329
+ at: at.at,
330
+ detail: cycle,
331
+ })
332
+ break
333
+ }
334
+ }
335
+ }
336
+
337
+ // TARGET: the far end IS what the relation says it is (the review's
338
+ // finding J). The declaration used to be inert -- `target` was read
339
+ // by nothing, on the reasoning that `refer(t)` already flows the
340
+ // type in at the site. It does, and that is exactly why the
341
+ // declaration was worth nothing: the site has to REPEAT it, and the
342
+ // idiom that avoids repeating it (`refer($.std.Service)`) tripped
343
+ // the fixpoint until §19 was fixed, so every real model wrote bare
344
+ // `refer()` and a typed-endpoint declaration checked nothing.
345
+ //
346
+ // Reported per EDGE, not per entity: an entity reached by two
347
+ // relations must satisfy both, and the report points at the link
348
+ // that made the demand.
349
+ if (undefined !== rel.target) {
350
+ for (const e of mine) {
351
+ // No guard on the node either, and for the same reason
352
+ // `addressed` needs none: an unresolved link is a nil in the
353
+ // tree, so this document would not have reached the graph.
354
+ const why = meets(aontu, root, addressed(root, graph, e.to),
355
+ rel.target)
356
+ if (undefined !== why) {
357
+ findings.push({
358
+ code: 'relation_target_unmet',
359
+ relation: rel.name,
360
+ at: e.at,
361
+ detail: [e.from, e.to, why],
362
+ })
363
+ }
364
+ }
365
+ }
366
+
367
+ if (undefined !== rel.inverse) {
368
+ for (const e of mine) {
369
+ const to = entityOf(e.to)
370
+ if (!pairs.has(rel.inverse + ' ' + to + ' ' + e.from)) {
371
+ findings.push({
372
+ code: 'relation_inverse_missing',
373
+ relation: rel.name,
374
+ at: e.at,
375
+ detail: [e.from, to, rel.inverse],
376
+ })
377
+ }
378
+ }
379
+ }
380
+ }
381
+
382
+ // SORTED, because a report is read by a machine that diffs it: by the
383
+ // position the offending edge is written at, then by code. No third
384
+ // key: one edge sits under one key and one key is one relation, so
385
+ // two findings can share (at, code) only by being the same finding.
386
+ // The sort is STABLE, and the relations were iterated in sorted
387
+ // order, so what order remains is fixed anyway.
388
+ findings.sort((a, b) =>
389
+ cmpCodePoint(a.at, b.at) || cmpCodePoint(a.code, b.code))
390
+
391
+ return {
392
+ verdict: 0 === findings.length ? 'pass' : 'fail',
393
+ findings,
394
+ }
395
+ }
@@ -0,0 +1,137 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ // SARIF rendering for a vet report (G2 phase 5,
4
+ // docs/capability-review/g2-validation-verb.md).
5
+ //
6
+ // A MINIMAL SARIF 2.1.0 profile, and deliberately nothing more: one
7
+ // run, one `result` per finding, the finding's first site as the
8
+ // primary location, its remaining sites under `relatedLocations`, and
9
+ // the whole finding object embedded in `properties` so a SARIF consumer
10
+ // still holds the native contract. No fixes, no code flows, no
11
+ // baselines — the JSON report is the native contract, SARIF is the
12
+ // interchange skin CI systems already ingest (GitHub code scanning
13
+ // uploads, PR annotation).
14
+ //
15
+ // This is library API, not CLI plumbing, for the same reason the vet
16
+ // engine is: an embedder (and G7's MCP server later) must be able to
17
+ // emit the interchange form without shelling out. The Go twin is
18
+ // go/report_sarif.go, and the two are held to byte parity over the
19
+ // shared fixture pair in test/spec/files/vet-sarif/ — with the message
20
+ // text and producer version redacted, exactly as test/spec/vet.tsv
21
+ // carves the message out of its goldens, because prose and the two
22
+ // independent version series are deliberately not in cross-port parity.
23
+
24
+ import type { VetReport, VetFinding, VetSite } from './vet'
25
+
26
+ import { exactJSON } from './exactjson'
27
+
28
+
29
+ // SARIF levels are error/warning/note; the report's severities are
30
+ // error/warning/info. Only `info` needs translating, but the map spells
31
+ // out all three so a new severity fails loudly here rather than
32
+ // silently emitting itself.
33
+ const SARIF_LEVEL: Record<string, string> = {
34
+ error: 'error',
35
+ warning: 'warning',
36
+ info: 'note',
37
+ }
38
+
39
+
40
+ // A site's file is a filesystem path, and a SARIF artifactLocation.uri
41
+ // is a URI reference: `#`, `%`, spaces and every other URI-significant
42
+ // character must be percent-encoded or a consumer parses the path as
43
+ // something else (text after `#` becomes a fragment). Encoded BY BYTE
44
+ // over UTF-8, with RFC 3986's unreserved and path characters kept
45
+ // literal, so the Go twin produces identical bytes from an identical
46
+ // loop (go/report_sarif.go sarifURI).
47
+ function sarifUri(path: string): string {
48
+ const bytes = Buffer.from(path, 'utf8')
49
+ let out = ''
50
+ for (const b of bytes) {
51
+ const c = String.fromCharCode(b)
52
+ if (b < 128 && /[A-Za-z0-9\-._~/!$&'()*+,;=:@]/.test(c)) {
53
+ out += c
54
+ }
55
+ else {
56
+ out += '%' + b.toString(16).toUpperCase().padStart(2, '0')
57
+ }
58
+ }
59
+ return out
60
+ }
61
+
62
+
63
+ function sarifLocation(site: VetSite): any {
64
+ const physical: any = {
65
+ artifactLocation: { uri: sarifUri(site.file) },
66
+ }
67
+ // SARIF regions are 1-based. A finding with no position — a parse
68
+ // failure reports at -1:-1 — gets a location with no region rather
69
+ // than an invented one.
70
+ if (1 <= site.row) {
71
+ physical.region = { startColumn: site.col, startLine: site.row }
72
+ }
73
+ return { physicalLocation: physical }
74
+ }
75
+
76
+
77
+ function sarifResult(finding: VetFinding): any {
78
+ // The engine orders sites data-first (the thing to fix), so the first
79
+ // site is the primary location and the rest are related — which for a
80
+ // two-site conflict puts the schema's declaration under
81
+ // `relatedLocations`, exactly where a code-scanning UI shows "the
82
+ // other side".
83
+ const result: any = {
84
+ level: SARIF_LEVEL[finding.severity],
85
+ locations: [sarifLocation(finding.sites[0])],
86
+ message: { text: finding.message },
87
+ properties: finding,
88
+ ruleId: 'aontu/' + finding.code,
89
+ }
90
+ const related = finding.sites.slice(1)
91
+ if (0 < related.length) {
92
+ result.relatedLocations = related.map(sarifLocation)
93
+ }
94
+ return result
95
+ }
96
+
97
+
98
+ /**
99
+ * Render a vet report as SARIF 2.1.0 text (a minimal profile: one run,
100
+ * one result per finding, the finding embedded in `properties`).
101
+ *
102
+ * @param report A report from `vet()`.
103
+ * @param version The producer version for `tool.driver.version` —
104
+ * the CLI passes its package version; the two ports'
105
+ * version series are independent by design.
106
+ * @returns The SARIF JSON text, indented two spaces, keys in
107
+ * the canonical emitter's sorted order.
108
+ */
109
+ function sarifReport(report: VetReport, version: string): string {
110
+ return exactJSON({
111
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
112
+ runs: [{
113
+ // An `error` verdict means the run could not be set up (an
114
+ // unusable schema): zero findings from a FAILED run must not
115
+ // read like zero findings from a clean one, so the failure is
116
+ // carried in SARIF's own invocation metadata rather than by an
117
+ // indistinguishable empty result list.
118
+ invocations: [{
119
+ executionSuccessful: 'error' !== report.verdict,
120
+ }],
121
+ results: report.findings.map(sarifResult),
122
+ tool: {
123
+ driver: {
124
+ informationUri: 'https://github.com/rjrodger/aontu',
125
+ name: 'aontu',
126
+ version,
127
+ },
128
+ },
129
+ }],
130
+ version: '2.1.0',
131
+ }, 2)
132
+ } /* node:coverage ignore next 6 */
133
+
134
+
135
+ export {
136
+ sarifReport,
137
+ }
package/src/site.ts CHANGED
@@ -6,12 +6,45 @@ import type {
6
6
  } from './type'
7
7
 
8
8
 
9
- type SiteSpec = { row?: number, col?: number, url?: string }
9
+ type SiteSpec = {
10
+ row?: number, col?: number, url?: string, len?: number, src?: string,
11
+ }
10
12
 
13
+ // Site locates a value in the text that produced it: where it starts
14
+ // (row, col), HOW FAR IT RUNS (len), and the source text itself (src).
15
+ //
16
+ // THE EXTENT IS NOT OPTIONAL DETAIL — it is what makes a site safe to
17
+ // edit. Without it the only length available is the CANON, and canon is
18
+ // not source text: `port: 0x1F` has canon `31`, so replacing
19
+ // `(col, canon.length)` writes `port: 5x1F` and corrupts the document.
20
+ // The parser knew the extent all along — the token carries `len` and
21
+ // `src` beside the `rI`/`cI` this already read — and dropping it was
22
+ // the whole defect (status report 2026-08-21, §5).
23
+ //
24
+ // WHAT THE EXTENT COVERS is the TOKEN THE SITE POINTS AT, which is the
25
+ // same thing row and col have always pointed at:
26
+ //
27
+ // 0x1F a scalar literal len 4 the whole literal
28
+ // "hi there" a string len 10 quotes included
29
+ // {x:1} a map len 1 the opening brace
30
+ // min(3) a constraint len 3 the name, not the call
31
+ //
32
+ // So for a SCALAR — which is what a repair edits, and what `set` and
33
+ // the manual fallback rewrite — `(col, len)` is exactly the span to
34
+ // replace. For a container it under-reaches to the opening delimiter,
35
+ // which is safe in the direction that matters: a highlight that is too
36
+ // short is a cosmetic flaw, while canon.length on a map over-reaches
37
+ // across lines into text the value never occupied.
38
+ //
39
+ // -1 and '' mean UNKNOWN, the same convention row and col already use:
40
+ // a value minted by unification rather than written by a document has
41
+ // no site, and must not be edited as though it had one.
11
42
  class Site {
12
43
  row: number
13
44
  col: number
14
45
  url: string
46
+ len: number
47
+ src: string
15
48
 
16
49
  constructor(val?: Val | SiteSpec) {
17
50
  const site = ((val as any)?.site ?? val) as SiteSpec
@@ -19,6 +52,8 @@ class Site {
19
52
  this.row = site?.row ?? -1
20
53
  this.col = site?.col ?? -1
21
54
  this.url = site?.url ?? ''
55
+ this.len = site?.len ?? -1
56
+ this.src = site?.src ?? ''
22
57
  }
23
58
  } /* node:coverage ignore next 6 */
24
59
 
package/src/std.ts ADDED
@@ -0,0 +1,73 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ // THE BUNDLED VOCABULARY (G4 phase 4,
4
+ // docs/capability-review/g4-identity-relations.md): `@"std/system"` is
5
+ // served from the engine itself — no filesystem, no package resolution
6
+ // — so a document may use it under every include capability except
7
+ // `none`, and the hermeticity posture is not widened by a source that
8
+ // never leaves the process.
9
+ //
10
+ // The TEXT is the shared artifact: go/std.go carries the same bytes,
11
+ // and test/spec/std-system.tsv pins its canon and its canon-hash in
12
+ // both engines, so the two copies cannot drift without a red suite.
13
+ // It carries no backtick for that reason: one string literal per port,
14
+ // and Go's raw string has no escape.
15
+
16
+ const STD_SYSTEM = `# std/system --- the SYSTEM VOCABULARY (G4 phase 4). Ports, components
17
+ # and relations need no syntax: they are schemas. Everything here is
18
+ # ordinary unification --- conjunction, spreads, marks, defaults ---
19
+ # so the vocabulary costs the language nothing, and an author who wants
20
+ # a different one writes it the same way.
21
+ #
22
+ # EXPERIMENTAL until the distribution layer can version it by
23
+ # canon-hash. Entity ids deliberately do NOT embed versions: fusing
24
+ # identity and version makes "v1 and v2 describe the same entity"
25
+ # inexpressible.
26
+
27
+ std: {
28
+
29
+ # One end of a connection.
30
+ Port: type({
31
+ direction: *in | out | inout
32
+ protocol?: string
33
+ })
34
+
35
+ # A node with ports. A Component that is not itself an entity is a
36
+ # component OF its nearest identified ancestor, which is the
37
+ # entity/component distinction and needs no mark of its own.
38
+ Component: type({
39
+ ports?: {&: $.std.Port}
40
+ })
41
+
42
+ # A component that is a service. Written out rather than as
43
+ # $.std.Component & {kind: service}: a reference from one member of
44
+ # this file to another does not survive being INCLUDED into a
45
+ # document (the marks the include carries make the referring member
46
+ # unusable), so the vocabulary states each schema on its own.
47
+ Service: type({
48
+ kind: service
49
+ ports?: {&: $.std.Port}
50
+ })
51
+
52
+ # A DECLARED RELATION. "target" is what the far end must satisfy,
53
+ # "inverse" names the relation that must mirror it, and "acyclic"
54
+ # asks that the edge set have no cycle. The last two are checked
55
+ # AFTER unification, not by it: they are global and non-monotone ---
56
+ # an acyclic graph becomes cyclic when one more edge unifies in ---
57
+ # and no lattice citizen may be falsified by more information.
58
+ Relation: type({
59
+ target?: top
60
+ inverse?: string
61
+ acyclic?: *false | boolean
62
+ })
63
+ }
64
+ `
65
+
66
+
67
+ // The bundled sources, by the name a document writes. Both the bare
68
+ // name and the `.aon` spelling resolve, because both are what an
69
+ // author reaches for.
70
+ export const STD_SOURCES: Record<string, string> = {
71
+ 'std/system': STD_SYSTEM,
72
+ 'std/system.aon': STD_SYSTEM,
73
+ }