aontu 0.53.0 → 0.55.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 (197) hide show
  1. package/dist/aontu.d.ts +3 -2
  2. package/dist/aontu.js +36 -7
  3. package/dist/aontu.js.map +1 -1
  4. package/dist/cli.d.ts +2 -1
  5. package/dist/cli.js +500 -8
  6. package/dist/cli.js.map +1 -1
  7. package/dist/ctx.d.ts +5 -0
  8. package/dist/ctx.js +1 -0
  9. package/dist/ctx.js.map +1 -1
  10. package/dist/diff.js.map +1 -1
  11. package/dist/err.js +7 -1
  12. package/dist/err.js.map +1 -1
  13. package/dist/graph.d.ts +2 -5
  14. package/dist/graph.js +83 -46
  15. package/dist/graph.js.map +1 -1
  16. package/dist/hcanon.js +9 -10
  17. package/dist/hcanon.js.map +1 -1
  18. package/dist/hints.js +94 -26
  19. package/dist/hints.js.map +1 -1
  20. package/dist/jsonschema.js +34 -0
  21. package/dist/jsonschema.js.map +1 -1
  22. package/dist/lang.js +593 -191
  23. package/dist/lang.js.map +1 -1
  24. package/dist/lsp.d.ts +1 -1
  25. package/dist/lsp.js +86 -6
  26. package/dist/lsp.js.map +1 -1
  27. package/dist/mcp.js +153 -6
  28. package/dist/mcp.js.map +1 -1
  29. package/dist/mod-tool.js +42 -8
  30. package/dist/mod-tool.js.map +1 -1
  31. package/dist/mod.d.ts +4 -0
  32. package/dist/mod.js +97 -2
  33. package/dist/mod.js.map +1 -1
  34. package/dist/patch.d.ts +5 -0
  35. package/dist/patch.js +25 -25
  36. package/dist/patch.js.map +1 -1
  37. package/dist/provenance.d.ts +1 -0
  38. package/dist/provenance.js +2 -1
  39. package/dist/provenance.js.map +1 -1
  40. package/dist/query.js.map +1 -1
  41. package/dist/reach.d.ts +1 -0
  42. package/dist/reach.js +49 -21
  43. package/dist/reach.js.map +1 -1
  44. package/dist/relation.d.ts +4 -0
  45. package/dist/relation.js +125 -200
  46. package/dist/relation.js.map +1 -1
  47. package/dist/sig.d.ts +25 -0
  48. package/dist/sig.js +277 -0
  49. package/dist/sig.js.map +1 -0
  50. package/dist/sigdecl.d.ts +2 -0
  51. package/dist/sigdecl.js +11 -0
  52. package/dist/sigdecl.js.map +1 -0
  53. package/dist/siggate.d.ts +4 -0
  54. package/dist/siggate.js +90 -0
  55. package/dist/siggate.js.map +1 -0
  56. package/dist/std.js +75 -16
  57. package/dist/std.js.map +1 -1
  58. package/dist/subsume.js +57 -10
  59. package/dist/subsume.js.map +1 -1
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/dist/unify.d.ts +2 -2
  62. package/dist/unify.js +93 -114
  63. package/dist/unify.js.map +1 -1
  64. package/dist/utility.d.ts +1 -2
  65. package/dist/utility.js +7 -61
  66. package/dist/utility.js.map +1 -1
  67. package/dist/val/AggFuncVal.d.ts +12 -1
  68. package/dist/val/AggFuncVal.js +165 -3
  69. package/dist/val/AggFuncVal.js.map +1 -1
  70. package/dist/val/BagVal.d.ts +2 -0
  71. package/dist/val/BagVal.js +56 -8
  72. package/dist/val/BagVal.js.map +1 -1
  73. package/dist/val/ConstraintVal.js +33 -5
  74. package/dist/val/ConstraintVal.js.map +1 -1
  75. package/dist/val/ContainerKindVal.d.ts +35 -0
  76. package/dist/val/ContainerKindVal.js +99 -0
  77. package/dist/val/ContainerKindVal.js.map +1 -0
  78. package/dist/val/CopyFuncVal.js +0 -7
  79. package/dist/val/CopyFuncVal.js.map +1 -1
  80. package/dist/val/DisjunctVal.d.ts +1 -2
  81. package/dist/val/DisjunctVal.js +146 -39
  82. package/dist/val/DisjunctVal.js.map +1 -1
  83. package/dist/val/ExpectVal.js +37 -2
  84. package/dist/val/ExpectVal.js.map +1 -1
  85. package/dist/val/FuncBaseVal.d.ts +1 -0
  86. package/dist/val/FuncBaseVal.js +19 -8
  87. package/dist/val/FuncBaseVal.js.map +1 -1
  88. package/dist/val/GraphAtomVal.d.ts +39 -0
  89. package/dist/val/GraphAtomVal.js +184 -0
  90. package/dist/val/GraphAtomVal.js.map +1 -0
  91. package/dist/val/JunctionVal.js +22 -5
  92. package/dist/val/JunctionVal.js.map +1 -1
  93. package/dist/val/ListVal.js +27 -34
  94. package/dist/val/ListVal.js.map +1 -1
  95. package/dist/val/MapVal.d.ts +1 -0
  96. package/dist/val/MapVal.js +82 -35
  97. package/dist/val/MapVal.js.map +1 -1
  98. package/dist/val/PathFuncVal.d.ts +2 -2
  99. package/dist/val/PathFuncVal.js +75 -16
  100. package/dist/val/PathFuncVal.js.map +1 -1
  101. package/dist/val/PathVal.d.ts +25 -0
  102. package/dist/val/PathVal.js +150 -0
  103. package/dist/val/PathVal.js.map +1 -0
  104. package/dist/val/PlusOpVal.d.ts +2 -1
  105. package/dist/val/PlusOpVal.js +50 -35
  106. package/dist/val/PlusOpVal.js.map +1 -1
  107. package/dist/val/PrefVal.d.ts +2 -0
  108. package/dist/val/PrefVal.js +159 -32
  109. package/dist/val/PrefVal.js.map +1 -1
  110. package/dist/val/RecurseVal.d.ts +19 -0
  111. package/dist/val/RecurseVal.js +217 -0
  112. package/dist/val/RecurseVal.js.map +1 -0
  113. package/dist/val/RefVal.d.ts +2 -1
  114. package/dist/val/RefVal.js +105 -72
  115. package/dist/val/RefVal.js.map +1 -1
  116. package/dist/val/ReferFuncVal.d.ts +30 -7
  117. package/dist/val/ReferFuncVal.js +395 -94
  118. package/dist/val/ReferFuncVal.js.map +1 -1
  119. package/dist/val/ScalarKindVal.d.ts +4 -2
  120. package/dist/val/ScalarKindVal.js +12 -1
  121. package/dist/val/ScalarKindVal.js.map +1 -1
  122. package/dist/val/SuperFuncVal.d.ts +4 -2
  123. package/dist/val/SuperFuncVal.js +118 -14
  124. package/dist/val/SuperFuncVal.js.map +1 -1
  125. package/dist/val/TopVal.d.ts +1 -1
  126. package/dist/val/Val.d.ts +2 -3
  127. package/dist/val/Val.js +39 -20
  128. package/dist/val/Val.js.map +1 -1
  129. package/dist/val/arith.js +4 -1
  130. package/dist/val/arith.js.map +1 -1
  131. package/dist/vet.d.ts +1 -0
  132. package/dist/vet.js +32 -1
  133. package/dist/vet.js.map +1 -1
  134. package/dist/view.d.ts +90 -0
  135. package/dist/view.js +2168 -0
  136. package/dist/view.js.map +1 -0
  137. package/grammar/aontu.gbnf +18 -10
  138. package/grammar/aontu.lark +15 -10
  139. package/grammar/aontu.tmLanguage.json +184 -0
  140. package/package.json +10 -3
  141. package/skill/grammar-card.md +1 -2
  142. package/src/aontu.ts +37 -7
  143. package/src/cli.ts +553 -8
  144. package/src/ctx.ts +20 -0
  145. package/src/diff.ts +4 -2
  146. package/src/err.ts +8 -1
  147. package/src/graph.ts +125 -75
  148. package/src/hcanon.ts +9 -11
  149. package/src/hints.ts +112 -29
  150. package/src/jsonschema.ts +41 -0
  151. package/src/lang.ts +642 -203
  152. package/src/lsp.ts +75 -6
  153. package/src/mcp.ts +164 -6
  154. package/src/mod-tool.ts +48 -9
  155. package/src/mod.ts +110 -1
  156. package/src/patch.ts +31 -27
  157. package/src/provenance.ts +8 -1
  158. package/src/query.ts +4 -2
  159. package/src/reach.ts +52 -23
  160. package/src/relation.ts +139 -236
  161. package/src/sig.ts +345 -0
  162. package/src/sigdecl.ts +11 -0
  163. package/src/siggate.ts +144 -0
  164. package/src/std.ts +77 -16
  165. package/src/subsume.ts +59 -10
  166. package/src/unify.ts +102 -125
  167. package/src/utility.ts +7 -67
  168. package/src/val/AggFuncVal.ts +231 -4
  169. package/src/val/BagVal.ts +58 -9
  170. package/src/val/ConstraintVal.ts +34 -5
  171. package/src/val/ContainerKindVal.ts +158 -0
  172. package/src/val/CopyFuncVal.ts +0 -7
  173. package/src/val/DisjunctVal.ts +152 -40
  174. package/src/val/ExpectVal.ts +39 -4
  175. package/src/val/FuncBaseVal.ts +21 -8
  176. package/src/val/GraphAtomVal.ts +264 -0
  177. package/src/val/JunctionVal.ts +23 -6
  178. package/src/val/ListVal.ts +30 -37
  179. package/src/val/MapVal.ts +87 -39
  180. package/src/val/PathFuncVal.ts +107 -19
  181. package/src/val/PathVal.ts +221 -0
  182. package/src/val/PlusOpVal.ts +56 -37
  183. package/src/val/PrefVal.ts +186 -38
  184. package/src/val/RecurseVal.ts +285 -0
  185. package/src/val/RefVal.ts +105 -83
  186. package/src/val/ReferFuncVal.ts +445 -100
  187. package/src/val/ScalarKindVal.ts +12 -0
  188. package/src/val/SuperFuncVal.ts +137 -13
  189. package/src/val/TopVal.ts +1 -1
  190. package/src/val/Val.ts +44 -35
  191. package/src/val/arith.ts +4 -1
  192. package/src/vet.ts +41 -4
  193. package/src/view.ts +2882 -0
  194. package/dist/val/IdFuncVal.d.ts +0 -13
  195. package/dist/val/IdFuncVal.js +0 -54
  196. package/dist/val/IdFuncVal.js.map +0 -1
  197. package/src/val/IdFuncVal.ts +0 -91
package/src/provenance.ts CHANGED
@@ -55,6 +55,12 @@ export type WhyConjunct = {
55
55
  // actually written. Empty when the contribution occupies no source —
56
56
  // a value unification minted rather than a document wrote.
57
57
  src: string
58
+ // The PREFERENCE RANK, 0-based, when the contribution is one: `*x`
59
+ // is 0, `**x` is 1. Absent for anything else. The engine's own
60
+ // number (PrefVal.rank), so a reader arbitrating between ranked
61
+ // contributions -- the meet ladder -- need not count stars in a
62
+ // canon string.
63
+ rank?: number
58
64
  }
59
65
 
60
66
  export type WhyRecord = {
@@ -76,7 +82,7 @@ export const FROM_SPREAD = '_fromSpread'
76
82
  // which is true of the parsed tree and of nothing derived from it. So
77
83
  // every value that reached a path through a CLONE was dark: a default
78
84
  // flowing into a `pack()`-generated child, a shape carried by a `$ref`,
79
- // one side of an id()-merge. `why` answered "(no contributions:
85
+ // a value a spread template stamped. `why` answered "(no contributions:
80
86
  // nothing met at this path)" over a value it had just printed, with
81
87
  // exit 0 -- a false statement, and the one an audit surface may not
82
88
  // make (use-cases/BUGS.md §23, §24).
@@ -338,6 +344,7 @@ export class Provenance {
338
344
  row: v.site.row,
339
345
  },
340
346
  src: v.site.src,
347
+ ...(true === v.isPref ? { rank: v.rank } : {}),
341
348
  })
342
349
  }
343
350
 
package/src/query.ts CHANGED
@@ -51,8 +51,10 @@ export type QueryOptions = {
51
51
  // The trust profile this run evaluates under (G5, docs/trust.md).
52
52
  // The source arrives from a caller, so the caller must be able to
53
53
  // say what it may reach: without this the include chain is the
54
- // default one, and `@"x.js"` is arbitrary code execution in the
55
- // evaluating process. A server passes `{include:'none'}`.
54
+ // default one, and `@"../../etc/passwd.aon"` reads whatever the
55
+ // process can. (`@"x.js"` no longer executes -- ADR-012 refuses the
56
+ // extension -- but reading is enough.) A server passes
57
+ // `{include:'none'}`.
56
58
  trust?: TrustOptions
57
59
  }
58
60
 
package/src/reach.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /* Copyright (c) 2025 Richard Rodger, MIT License */
2
2
 
3
- // REACHABILITY OVER THE ENTITY GRAPH (the review's finding J,
3
+ // REACHABILITY OVER THE LINK GRAPH (the review's finding J,
4
4
  // use-cases/REVIEW.md): "ship a transitive `reaches(a, b)` check verb".
5
5
  //
6
6
  // `relations` answers questions about the edge set as a whole --- is it
@@ -41,7 +41,7 @@ export type ReachVerdict = 'reaches' | 'unreachable' | 'error'
41
41
  export type ReachReport = {
42
42
  verdict: ReachVerdict
43
43
 
44
- // The path found, as entity names from the source to the
44
+ // The path found, as `$.dotted` node paths from the source to the
45
45
  // destination, both included. Present ONLY on `reaches`: a path is
46
46
  // the evidence for the answer, and there is no evidence for a
47
47
  // negative one.
@@ -49,7 +49,7 @@ export type ReachReport = {
49
49
 
50
50
  // WHY the graph could not be looked at, in vet's finding shape (the
51
51
  // review's finding F): a document that does not stand up, or an
52
- // endpoint that names no entity. Present ONLY on `error`.
52
+ // endpoint that names no node. Present ONLY on `error`.
53
53
  errors?: VetFinding[]
54
54
  }
55
55
 
@@ -66,14 +66,37 @@ export type ReachOptions = {
66
66
  }
67
67
 
68
68
 
69
- // The entity an address names --- everything before the first dot. A
70
- // link into `svc/auth.ports.http` reaches `svc/auth`: reachability is
71
- // between ENTITIES, and the path inside one says which part of it the
72
- // link arrives at. Same rule as relation.ts's entityOf, and it has to
73
- // be, or the two verbs would disagree about what an edge connects.
74
- function entityOf(addr: string): string {
75
- const dot = addr.indexOf('.')
76
- return dot < 0 ? addr : addr.slice(0, dot)
69
+ // The segments a `$.dotted` endpoint spells, or undefined when it is
70
+ // not one. Reachability is between TREE POSITIONS (ADR-014), so an
71
+ // endpoint is a path and nothing else --- the same spelling the report
72
+ // prints back.
73
+ export function parseNodePath(s: string): string[] | undefined {
74
+ if ('$' === s) {
75
+ return []
76
+ }
77
+ if (!s.startsWith('$.')) {
78
+ return undefined
79
+ }
80
+ const parts = s.slice(2).split('.')
81
+ return parts.every((p) => /^[A-Za-z0-9_-]+$/.test(p)) ? parts : undefined
82
+ }
83
+
84
+
85
+ // Whether a path names a node of the evaluated tree. An endpoint that
86
+ // exists but has no edges is a perfectly good question with the answer
87
+ // `unreachable`; only one that names NOTHING is an error.
88
+ function nodeAt(root: any, path: string[]): boolean {
89
+ let node: any = root
90
+ for (const seg of path) {
91
+ if (true !== node?.isMap && true !== node?.isList) {
92
+ return false
93
+ }
94
+ node = node.peg[seg]
95
+ if (null == node) {
96
+ return false
97
+ }
98
+ }
99
+ return null != node
77
100
  }
78
101
 
79
102
 
@@ -83,14 +106,14 @@ function endpointFinding(name: string, known: string[]): VetFinding {
83
106
  class: 'reference',
84
107
  severity: 'error',
85
108
  path: '$',
86
- // NOT "unreachable". An endpoint that names no entity is a
87
- // question the document cannot answer, and answering it `no` would
88
- // report a typo as a fact about the model --- the fail-open shape
89
- // this review exists to retire.
90
- message: `${name} names no entity in this document.`,
109
+ // NOT "unreachable". An endpoint that names no node is a question
110
+ // the document cannot answer, and answering it `no` would report a
111
+ // typo as a fact about the model --- the fail-open shape this
112
+ // review exists to retire.
113
+ message: `${name} names no node in this document.`,
91
114
  sites: [],
92
115
  ...(0 === known.length ? {} : {
93
- note: 'known entities: ' + known.join(', '),
116
+ note: 'nodes with links: ' + known.join(', '),
94
117
  }),
95
118
  }
96
119
  }
@@ -117,12 +140,19 @@ export function reachCheck(
117
140
  }
118
141
 
119
142
  const graph = graphOf(root)
120
- const known = graph.entities.map((e) => e.id).sort(cmpCodePoint)
121
- const missing = [from, to].filter((n) => !known.includes(n))
143
+ // The nodes the graph actually touches, for the error note: a
144
+ // document has every path in it, and listing them all would drown the
145
+ // one fact a mistyped endpoint needs.
146
+ const linked = [...new Set(graph.edges
147
+ .flatMap((e) => [e.from, e.to]))].sort(cmpCodePoint)
148
+ const missing = [from, to].filter((n) => {
149
+ const parts = parseNodePath(n)
150
+ return undefined === parts || !nodeAt(root, parts)
151
+ })
122
152
  if (0 < missing.length) {
123
153
  return {
124
154
  verdict: 'error',
125
- errors: missing.map((n) => endpointFinding(n, known)),
155
+ errors: missing.map((n) => endpointFinding(n, linked)),
126
156
  }
127
157
  }
128
158
 
@@ -131,12 +161,11 @@ export function reachCheck(
131
161
  // both ports.
132
162
  const succ = new Map<string, string[]>()
133
163
  for (const e of graph.edges) {
134
- if ('' === e.from ||
135
- (null != options.relation && options.relation !== e.key)) {
164
+ if (null != options.relation && options.relation !== e.key) {
136
165
  continue
137
166
  }
138
167
  const list = succ.get(e.from)
139
- const dest = entityOf(e.to)
168
+ const dest = e.to
140
169
  if (undefined === list) {
141
170
  succ.set(e.from, [dest])
142
171
  }
package/src/relation.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /* Copyright (c) 2025 Richard Rodger, MIT License */
2
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.
3
+ // RELATION GRAPH VERDICTS (RELATIONS.0.md §3.3, replacing the G4
4
+ // phase 5 magic-key pass): acyclicity and inverse consistency over
5
+ // the edge set, DECLARED by the graph atoms -- `acyclic()` and
6
+ // `inverse(name)` conjoined at the field whose key is the predicate
7
+ // -- and decided AFTER unification, never by it.
7
8
  //
8
9
  // Why not in the lattice. Both properties are GLOBAL and NON-MONOTONE:
9
10
  // an acyclic graph becomes cyclic when one more edge unifies in, and an
@@ -11,27 +12,27 @@
11
12
  // The lattice guarantee is that more information never falsifies what
12
13
  // has been observed, so a constraint that could be true and then false
13
14
  // 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.
15
+ // finished model, so the atoms only REGISTER during unification
16
+ // (GraphAtomVal.register, onto ctx._reldecls), and the verdict lands
17
+ // at GENERATION -- the sizing atoms' model -- where no more
18
+ // information can arrive. The `relations` verb reports the same
19
+ // verdict from the same declarations: one decision, two surfaces.
16
20
  //
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.
21
+ // The old declaration channel -- a `relations:` key at the document
22
+ // root, read by name -- is GONE, discharging ADR-010's grandfather
23
+ // clause: the engine no longer knows any spellable key. The target
24
+ // half of the old declaration is `rel(t)`'s flow at the site, which
25
+ // checks by unifying rather than by a report-layer probe.
26
26
 
27
27
  import { Aontu } from './aontu'
28
28
  import { failureFinding } from './vet'
29
29
  import type { VetFinding } from './vet'
30
- import type { TrustOptions, Val } from './type'
30
+ import type { TrustOptions } from './type'
31
31
  import { graphOf } from './graph'
32
- import type { Edge, EntityEntry, Graph } from './graph'
32
+ import type { Edge, Graph } from './graph'
33
33
  import { cmpCodePoint } from './keyorder'
34
- import { unite } from './unify'
34
+ import { makeNilErr } from './err'
35
+ import type { RelDecl } from './val/GraphAtomVal'
35
36
 
36
37
 
37
38
  export type RelationVerdict = 'pass' | 'fail' | 'error'
@@ -42,7 +43,7 @@ export type RelationFinding = {
42
43
  relation: string
43
44
  // Where the offending edge is written, as a `$.dotted.path`.
44
45
  at: string
45
- // For a cycle, the entities it runs through, in the order the walk
46
+ // For a cycle, the node paths it runs through, in the order the walk
46
47
  // found them, closing back on the first. For a missing inverse, the
47
48
  // two ends and the relation that should have mirrored it.
48
49
  detail: string[]
@@ -72,143 +73,7 @@ export type RelationOptions = {
72
73
  }
73
74
 
74
75
 
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
76
+ // The first cycle reachable from `start`, as the node paths it runs
212
77
  // through, or undefined. Depth-first with the path as the stack, and
213
78
  // the successors visited in sorted order, so the cycle a report names
214
79
  // is the same one in both ports.
@@ -245,44 +110,19 @@ function findCycle(
245
110
  }
246
111
 
247
112
 
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
113
+ // The verdict itself, pure over what the evaluation produced: the
114
+ // registered declarations and the edge set. Shared by the generation
115
+ // hook (relationErrors) and the `relations` verb, so the two surfaces
116
+ // cannot disagree.
117
+ export function relationFindings(
118
+ decls: Map<string, RelDecl>, graph: Graph
119
+ ): RelationFinding[] {
276
120
  const findings: RelationFinding[] = []
277
121
 
278
122
  // The edge set, indexed the two ways the checks read it.
279
123
  const byRelation = new Map<string, Edge[]>()
280
124
  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
- }
125
+ for (const e of graph.edges) {
286
126
  const list = byRelation.get(e.key)
287
127
  if (undefined === list) {
288
128
  byRelation.set(e.key, [e])
@@ -290,17 +130,22 @@ export function relationCheck(
290
130
  else {
291
131
  list.push(e)
292
132
  }
293
- pairs.add(e.key + ' ' + e.from + ' ' + entityOf(e.to))
133
+ pairs.add(e.key + ' ' + e.from + ' ' + e.to)
294
134
  }
295
135
 
296
- for (const rel of declared) {
297
- const mine = byRelation.get(rel.name) ?? []
136
+ // Predicates in sorted order, so the findings arrive the same way
137
+ // in both ports (the registry is insertion-ordered here, random in
138
+ // Go).
139
+ const names = [...decls.keys()].sort(cmpCodePoint)
140
+ for (const name of names) {
141
+ const decl = decls.get(name) as RelDecl
142
+ const mine = byRelation.get(name) ?? []
298
143
 
299
- if (rel.acyclic) {
144
+ if (true === decl.acyclic) {
300
145
  const succ = new Map<string, string[]>()
301
146
  for (const e of mine) {
302
147
  const list = succ.get(e.from)
303
- const to = entityOf(e.to)
148
+ const to = e.to
304
149
  if (undefined === list) {
305
150
  succ.set(e.from, [to])
306
151
  }
@@ -325,7 +170,7 @@ export function relationCheck(
325
170
  const at = mine.find((e) => e.from === cycle[0]) as Edge
326
171
  findings.push({
327
172
  code: 'relation_cycle',
328
- relation: rel.name,
173
+ relation: name,
329
174
  at: at.at,
330
175
  detail: cycle,
331
176
  })
@@ -334,45 +179,16 @@ export function relationCheck(
334
179
  }
335
180
  }
336
181
 
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) {
182
+ const inverses = [...decl.inverses].sort(cmpCodePoint)
183
+ for (const inv of inverses) {
350
184
  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)) {
185
+ const to = e.to
186
+ if (!pairs.has(inv + ' ' + to + ' ' + e.from)) {
371
187
  findings.push({
372
188
  code: 'relation_inverse_missing',
373
- relation: rel.name,
189
+ relation: name,
374
190
  at: e.at,
375
- detail: [e.from, to, rel.inverse],
191
+ detail: [e.from, to, inv],
376
192
  })
377
193
  }
378
194
  }
@@ -380,14 +196,101 @@ export function relationCheck(
380
196
  }
381
197
 
382
198
  // 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.
199
+ // position the offending edge is written at, then by code, then by
200
+ // the detail (two inverse declarations on one predicate can flag one
201
+ // edge twice). The sort is STABLE and the predicates were iterated
202
+ // in sorted order, so what order remains is fixed anyway.
388
203
  findings.sort((a, b) =>
389
- cmpCodePoint(a.at, b.at) || cmpCodePoint(a.code, b.code))
204
+ cmpCodePoint(a.at, b.at) || cmpCodePoint(a.code, b.code)
205
+ || cmpCodePoint(a.detail.join(' '), b.detail.join(' ')))
206
+
207
+ return findings
208
+ }
209
+
210
+
211
+ // The generation hook (Aontu.generate, between unification success
212
+ // and value generation): each finding becomes a LOCATED evaluation
213
+ // error at the offending edge, exactly as an unmet sizing atom
214
+ // refuses at generation. Findings name node paths and positions the
215
+ // document spelled, so the walk to the site cannot miss.
216
+ export function relationErrors(ctx: any, root: any): void {
217
+ const decls: Map<string, RelDecl> = ctx._reldecls
218
+ if (0 === decls.size) {
219
+ return
220
+ }
221
+ const findings = relationFindings(decls, graphOf(root))
222
+ for (const f of findings) {
223
+ let node: any = root
224
+ for (const seg of f.at.slice(2).split('.')) {
225
+ // Graph atoms hold the field's value -- possibly nested, one
226
+ // atom carrying another -- and the path steps through them
227
+ // exactly as the graph walk does.
228
+ while (true === node?.isGraphAtom) {
229
+ node = node.held
230
+ }
231
+ node = node?.peg?.[seg]
232
+ }
233
+ // No unwrap AFTER the walk: a finding's `at` names an edge
234
+ // element, and an edge's element is a string -- an atom-wrapped
235
+ // element mints no edge in the first place (the graph visit
236
+ // descends atoms only at field values), so the walk cannot end on
237
+ // an atom.
238
+ ctx.adderr(makeNilErr(ctx, f.code, node, undefined, 'relate', {
239
+ relation: f.relation,
240
+ detail: f.detail.join(' -> '),
241
+ }))
242
+ }
243
+ }
244
+
245
+
246
+ // The relation checks for one document: evaluate, then report the
247
+ // same verdict generation enforces.
248
+ export function relationCheck(
249
+ src: string, opts?: RelationOptions): RelationReport {
250
+ const options = opts ?? {}
251
+ const aontu = new Aontu(
252
+ null == options.trust ? undefined : { trust: options.trust })
253
+ const ctx = aontu.ctx({ collect: true })
254
+ const parseOpts = null == options.path ? undefined : { path: options.path }
255
+ const root: any = aontu.unify(src, parseOpts, ctx)
256
+
257
+ // A document that does not stand up is not a document with a bad
258
+ // graph: the errors it already has are the answer, and blaming its
259
+ // relations on top would be noise.
260
+ if (0 < ctx.err.length || true === root?.isNil) {
261
+ return {
262
+ verdict: 'error',
263
+ findings: [],
264
+ errors: [failureFinding(ctx, options.path, root)],
265
+ }
266
+ }
267
+
268
+ const decls: Map<string, RelDecl> = (ctx as any)._reldecls
269
+ if (0 === decls.size) {
270
+ return { verdict: 'pass', findings: [] }
271
+ }
272
+
273
+ // NOR IS A DOCUMENT THAT CANNOT BE GENERATED. Unification can
274
+ // succeed over a tree that still holds an unsettled disjunction --
275
+ // more than one alternative admitted, which `disjunct_no_gen`
276
+ // refuses at generation -- and the graph walk cannot read inside
277
+ // one: the links in every alternative are invisible, so a mirror
278
+ // the document plainly writes is missing from the edge set and
279
+ // `inverse(n)` calls it missing. Generation is therefore asked
280
+ // FIRST, on a collecting context of its own, and what it says is
281
+ // the answer. Generation is what the atoms' verdict lands at, so
282
+ // this is the same order the engine itself now runs in.
283
+ const gctx = ctx.clone({ err: [], collect: true })
284
+ root.gen(gctx)
285
+ if (0 < gctx.err.length) {
286
+ return {
287
+ verdict: 'error',
288
+ findings: [],
289
+ errors: [failureFinding(gctx, options.path, root)],
290
+ }
291
+ }
390
292
 
293
+ const findings = relationFindings(decls, graphOf(root))
391
294
  return {
392
295
  verdict: 0 === findings.length ? 'pass' : 'fail',
393
296
  findings,