aontu 0.67.0 → 0.69.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 (86) hide show
  1. package/dist/alias.d.ts +12 -1
  2. package/dist/alias.js +176 -6
  3. package/dist/alias.js.map +1 -1
  4. package/dist/aliasname.d.ts +19 -0
  5. package/dist/aliasname.js +65 -0
  6. package/dist/aliasname.js.map +1 -0
  7. package/dist/aontu.d.ts +1 -1
  8. package/dist/aontu.js +21 -8
  9. package/dist/aontu.js.map +1 -1
  10. package/dist/aontumodel.js +1 -1
  11. package/dist/aontumodel.js.map +1 -1
  12. package/dist/cli.d.ts +4 -1
  13. package/dist/cli.js +29 -13
  14. package/dist/cli.js.map +1 -1
  15. package/dist/ctx.d.ts +1 -0
  16. package/dist/ctx.js +1 -0
  17. package/dist/ctx.js.map +1 -1
  18. package/dist/err.js +18 -1
  19. package/dist/err.js.map +1 -1
  20. package/dist/format.js +14 -0
  21. package/dist/format.js.map +1 -1
  22. package/dist/helpdoc.js +1 -1
  23. package/dist/helpdoc.js.map +1 -1
  24. package/dist/hints.js +171 -0
  25. package/dist/hints.js.map +1 -1
  26. package/dist/lang.js +325 -38
  27. package/dist/lang.js.map +1 -1
  28. package/dist/lsp.d.ts +10 -4
  29. package/dist/lsp.js +116 -7
  30. package/dist/lsp.js.map +1 -1
  31. package/dist/site.d.ts +9 -0
  32. package/dist/site.js +2 -1
  33. package/dist/site.js.map +1 -1
  34. package/dist/subsume.js +6 -1
  35. package/dist/subsume.js.map +1 -1
  36. package/dist/val/BagVal.d.ts +1 -0
  37. package/dist/val/BagVal.js +3 -0
  38. package/dist/val/BagVal.js.map +1 -1
  39. package/dist/val/EmitFuncVal.js +2 -1
  40. package/dist/val/EmitFuncVal.js.map +1 -1
  41. package/dist/val/FilterFuncVal.js +1 -1
  42. package/dist/val/FilterFuncVal.js.map +1 -1
  43. package/dist/val/FuncBaseVal.d.ts +2 -1
  44. package/dist/val/FuncBaseVal.js +25 -1
  45. package/dist/val/FuncBaseVal.js.map +1 -1
  46. package/dist/val/MapVal.js +10 -4
  47. package/dist/val/MapVal.js.map +1 -1
  48. package/dist/val/MatchFuncVal.js +2 -1
  49. package/dist/val/MatchFuncVal.js.map +1 -1
  50. package/dist/val/NilVal.js +3 -0
  51. package/dist/val/NilVal.js.map +1 -1
  52. package/dist/val/RecurseVal.d.ts +1 -0
  53. package/dist/val/RecurseVal.js +7 -3
  54. package/dist/val/RecurseVal.js.map +1 -1
  55. package/dist/val/RefVal.d.ts +1 -0
  56. package/dist/val/RefVal.js +47 -18
  57. package/dist/val/RefVal.js.map +1 -1
  58. package/dist/vet.d.ts +2 -1
  59. package/dist/vet.js +51 -6
  60. package/dist/vet.js.map +1 -1
  61. package/package.json +1 -1
  62. package/skill/error-codes.md +25 -1
  63. package/src/alias.ts +214 -6
  64. package/src/aliasname.ts +95 -0
  65. package/src/aontu.ts +23 -9
  66. package/src/aontumodel.ts +1 -1
  67. package/src/cli.ts +35 -13
  68. package/src/ctx.ts +4 -1
  69. package/src/err.ts +19 -1
  70. package/src/format.ts +14 -0
  71. package/src/helpdoc.ts +1 -1
  72. package/src/hints.ts +228 -0
  73. package/src/lang.ts +370 -33
  74. package/src/lsp.ts +136 -6
  75. package/src/site.ts +15 -1
  76. package/src/subsume.ts +9 -2
  77. package/src/val/BagVal.ts +4 -0
  78. package/src/val/EmitFuncVal.ts +3 -2
  79. package/src/val/FilterFuncVal.ts +2 -2
  80. package/src/val/FuncBaseVal.ts +30 -1
  81. package/src/val/MapVal.ts +10 -4
  82. package/src/val/MatchFuncVal.ts +3 -2
  83. package/src/val/NilVal.ts +3 -0
  84. package/src/val/RecurseVal.ts +8 -3
  85. package/src/val/RefVal.ts +49 -19
  86. package/src/vet.ts +55 -6
package/src/cli.ts CHANGED
@@ -5039,6 +5039,17 @@ function runHelp(argv: string[]): number {
5039
5039
  // and carries that prefix's hint.
5040
5040
  const EXPLAIN_PREFIXES = ['func:', 'op:', 'op[', 'var[', 'ref[']
5041
5041
 
5042
+ // A report prints the namespaced spelling in its brackets, which is the
5043
+ // span a reader copies. The answer names the registered one.
5044
+ const EXPLAIN_NAMESPACE = 'aontu/'
5045
+
5046
+
5047
+ function canonExplainCode(code: string): string {
5048
+ return code.startsWith(EXPLAIN_NAMESPACE)
5049
+ ? code.slice(EXPLAIN_NAMESPACE.length)
5050
+ : code
5051
+ }
5052
+
5042
5053
 
5043
5054
  function explainCode(
5044
5055
  code: string): { cls: string, hint: string, registered: boolean } {
@@ -5078,9 +5089,10 @@ function explainListText(format: SubsumeFormat): string {
5078
5089
  codes: codes.map((code) => ({
5079
5090
  code,
5080
5091
  class: codeClass(code),
5081
- // Whether this port carries explanation text for the code. The
5082
- // registry is in parity; the hint tables are not, so a consumer
5083
- // that wants only explained codes can filter rather than guess.
5092
+ // Whether this port carries explanation text for the code. A
5093
+ // gate holds every registered code explained, so this reads
5094
+ // true throughout; it stays because the registry is
5095
+ // append-only and a consumer should filter rather than guess.
5084
5096
  explained: '' !== explainCode(code).hint,
5085
5097
  })),
5086
5098
  }, 2)
@@ -5088,7 +5100,21 @@ function explainListText(format: SubsumeFormat): string {
5088
5100
  const width = codes.reduce((w, c) => Math.max(w, c.length), 0)
5089
5101
  return codes.map((c) =>
5090
5102
  c.padEnd(width) + ' ' + codeClass(c) +
5091
- ('' === explainCode(c).hint ? ' (no text)' : '')).join('\n')
5103
+ noTextMark(explainCode(c).hint)).join('\n')
5104
+ }
5105
+
5106
+
5107
+ // The registry is append-only, so a code can be registered before its
5108
+ // text is written; saying so beats printing an empty block.
5109
+ function explainBody(hint: string): string {
5110
+ return '' === hint
5111
+ ? '(no explanation text is registered for this code)'
5112
+ : hint
5113
+ }
5114
+
5115
+
5116
+ function noTextMark(hint: string): string {
5117
+ return '' === hint ? ' (no text)' : ''
5092
5118
  }
5093
5119
 
5094
5120
 
@@ -5138,7 +5164,7 @@ function runExplain(argv: string[]): number {
5138
5164
  return 2
5139
5165
  }
5140
5166
 
5141
- const code = codes[0]
5167
+ const code = canonExplainCode(codes[0])
5142
5168
  const { cls, hint, registered } = explainCode(code)
5143
5169
  if (!registered) {
5144
5170
  // AN UNKNOWN CODE IS A USAGE ERROR AND NAMES NEAR MATCHES. A
@@ -5164,13 +5190,8 @@ function runExplain(argv: string[]): number {
5164
5190
  }, 2) + '\n')
5165
5191
  return 0
5166
5192
  }
5167
- // A REGISTERED CODE WITH NO HINT SAYS SO rather than printing an
5168
- // empty block, which would read as an explanation that happened to
5169
- // be blank.
5170
- const body = '' === hint
5171
- ? '(no explanation text is registered for this code)'
5172
- : hint
5173
- process.stdout.write(`code: ${code}\nclass: ${cls}\n\n${body}\n`)
5193
+ process.stdout.write(
5194
+ `code: ${code}\nclass: ${cls}\n\n${explainBody(hint)}\n`)
5174
5195
  return 0
5175
5196
  }
5176
5197
 
@@ -5487,7 +5508,7 @@ function main(argv: string[], servers: Servers = SERVERS): void {
5487
5508
  else {
5488
5509
  runStdin(mode, format, trust).then((code) => finish(code))
5489
5510
  }
5490
- } /* node:coverage ignore next 21 */
5511
+ } /* node:coverage ignore next 22 */
5491
5512
 
5492
5513
 
5493
5514
  // No require.main guard here: bin/aontu.js is the executable entry and
@@ -5504,6 +5525,7 @@ export {
5504
5525
  runRender, renderSkipped,
5505
5526
  runPkg, runModel, runPackageVerb, pkgToolOptions, serveUntilInterrupted,
5506
5527
  runHash, runGet, runHelp, runExplain, runInit, nearestVerb,
5528
+ explainBody, noTextMark, canonExplainCode,
5507
5529
  looksLikeVerb,
5508
5530
  KNOWN_VERBS,
5509
5531
  runWhy, renderWhyText, runSet, runAllow, runAgentsMd, runFmt,
package/src/ctx.ts CHANGED
@@ -93,7 +93,9 @@ class AontuContext {
93
93
 
94
94
  _fixroot: any
95
95
 
96
- budget: { passes: number, revisits: number, depth: number }
96
+ budget: {
97
+ passes: number, revisits: number, depth: number, alias: number
98
+ }
97
99
 
98
100
  // The include manifest sink (G5, docs/trust.md): every include the
99
101
  // resolver reads is recorded here as { path, capability }, and
@@ -153,6 +155,7 @@ class AontuContext {
153
155
  passes: budget.passes ?? 9,
154
156
  revisits: 999,
155
157
  depth: budget.depth ?? 1000,
158
+ alias: budget.alias ?? 1000000,
156
159
  }
157
160
  }
158
161
 
package/src/err.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /* Copyright (c) 2021-2025 Richard Rodger, MIT License */
2
2
 
3
3
 
4
+ import { aliasPathSegment } from './aliasname'
4
5
  import { sep } from 'node:path'
5
6
 
6
7
  import { util } from '@tabnas/jsonic'
@@ -71,7 +72,10 @@ function descErr<NILS extends NilVal | NilVal[]>(
71
72
  let v1src = resolveSrc(v1, errctx)
72
73
  let v2src = resolveSrc(v2, errctx)
73
74
 
74
- let path = ['$', ...err.path].filter((p: any) => null != p && '' !== p)
75
+ // A list index is a number here, and only a key can be an alias.
76
+ let path = ['$', ...err.path.map((p: any) =>
77
+ 'string' === typeof p ? aliasPathSegment(p) : p)]
78
+ .filter((p: any) => null != p && '' !== p)
75
79
 
76
80
  // '$' is neither null nor '', so the filter always leaves it.
77
81
  let valpath = path.join('.')
@@ -125,6 +129,20 @@ function descErr<NILS extends NilVal | NilVal[]>(
125
129
  col: v2.site.col,
126
130
  })),
127
131
 
132
+ // A NAME IS WHERE THE VALUE ENTERED THIS PATH, which is not
133
+ // where it is written (ALIASES.0.md A-1).
134
+ ...[v1, v2].map((v: any) => null != v?.site?.via && errmsg({
135
+ color: { active: colorActive(), line: '\x1b[34m' },
136
+ txts: {
137
+ msg: 'Value arrived through ' + v.site.via.name,
138
+ site: ''
139
+ },
140
+ smsg: 'used ' + v.site.via.name + ' here',
141
+ file: resolveFile(v.site.via.url),
142
+ src: resolveSrc({ site: v.site.via } as any, errctx),
143
+ row: v.site.via.row,
144
+ col: v.site.via.col,
145
+ })),
128
146
 
129
147
  ]
130
148
  .filter((n: any) => null != n && false !== n)
package/src/format.ts CHANGED
@@ -6,6 +6,7 @@ import { failureFinding } from './vet'
6
6
  import type { VetFinding } from './vet'
7
7
  import type { Resolver } from './type'
8
8
  import { desugarTemplate, resugarTemplate, templateOutputs } from './template'
9
+ import { ALIAS_RE, EXPORT_DECL_NAME, isExportHoldKey } from './aliasname'
9
10
 
10
11
 
11
12
  const BUDGET = 80
@@ -303,6 +304,19 @@ class Reader {
303
304
  this.i += 2
304
305
  return { t: 'spread', value: this.value(), at }
305
306
  }
307
+ // `export({ %a })` is ONE declaration lexed as a pair.
308
+ if (isExportHoldKey(this.T[this.i].val) &&
309
+ EXPORT_DECL_NAME === this.T[this.i].src && this.atKey()) {
310
+ const text = EXPORT_DECL_NAME + '(' + this.T[this.i + 2].src + ')'
311
+ this.i += 3
312
+ return { t: 'atom', text, at }
313
+ }
314
+ if (this.atKey() && ALIAS_RE.test('' + this.T[this.i].src) &&
315
+ this.T[this.i].src === this.T[this.i + 2]?.src) {
316
+ const text = '' + this.T[this.i].src
317
+ this.i += 3
318
+ return { t: 'atom', text, at }
319
+ }
306
320
  if (this.atKey()) {
307
321
  const tok = this.T[this.i]
308
322
  const opt = '#QM' === this.name(1)
package/src/helpdoc.ts CHANGED
@@ -35,7 +35,7 @@ const HELPDOC: HelpTopic[] = [
35
35
  "topic": "codes",
36
36
  "summary": "what a refusal means, and what to do about it",
37
37
  "source": "docs/skill/error-codes.md",
38
- "text": "# What a refusal means\n\nEvery aontu error carries a **code**, and every code has a **class**.\nThe class says what kind of thing went wrong, which is what decides\nyour next move. The full registry — every code, its class, and the\nversion it appeared in — is\n[`test/spec/errcodes.tsv`](../../test/spec/errcodes.tsv), which the\ntest suite holds to the engine in both implementations.\n\n| Class | Meaning | What to do |\n|-------|---------|------------|\n| `parse` | the text is not a document | fix the syntax at the site the frame points at |\n| `conflict` | two values cannot both hold | one of them is wrong: `aontu model why <path>` names both and where they were written |\n| `incomplete` | nothing contradicts, but the value is not concrete | supply what is missing, or accept it with `--partial` |\n| `reference` | a path names nothing | check the spelling; `aontu model get $ --keys` lists what is there |\n| `compat` | a change breaks an earlier version | that is `aontu subsume` / `aontu breaking` talking: widen the change or version it |\n| `budget` | evaluation hit a deterministic limit | usually a cycle; simplify, or raise the budget deliberately |\n| `internal` | the engine surprised itself | a bug worth reporting |\n\n## The repair loop\n\n```\naontu vet schema.aon mine.aon --format json\n```\n\nThe exit code branches for you: `0` valid, `1` invalid (fix the\ndata), `3` incomplete (supply more), `4` the schema itself is\nunusable (fix the truth, not the data). Every finding carries\n`path`, `code`, and the sites on both sides — the data site first,\nbecause that is the one to edit. A site names **the file whose text\nit excerpts**, so its row and column are safe to edit at even when\nthe document loads others.\n\nRead `hint` before guessing. `message` is the one-line headline;\n`hint` is the engine's own explanation of the failure class with the\noffending values filled in, and for several codes it names the fix\noutright — `lossy_integer_literal` tells you to write the literal as\n`0d…`. It is absent for codes that have no hint text.\n\nFor a conflict, `aontu model why <path> mine.aon` lists every contribution\nto that path with its role and source line, which turns \"these\ndisagree\" into \"these two lines disagree\".\n\nThen fix it:\n\n```\naontu model set '$.replicas=5' --entry schema.aon --overlay mine.aon --in-place\n```\n\n`--in-place` rewrites the pinned literal **where it was written**, so\ncomments and layout survive. Without it, `set` APPENDS — which is the\nright thing when the document left a hole, and cannot work when it\npinned the wrong value, because unification only narrows.\n\nThe edit is verified before a byte is written: a site carries the\nsource text it covers, and the text at the span must match it. Where\nthat cannot be established — the value comes from a `&:` template or a\n`$ref`, two statements pin the path, the site names the opening token\nof a compound like `min(1)`, or the overlay `@\"includes\"` another\ndocument — the assignment is appended as usual and a **warning** says\nwhich case it hit. A warning never changes the verdict, so asking for\n`--in-place` cannot make a run fail that would have succeeded.\n"
38
+ "text": "# What a refusal means\n\nEvery aontu error carries a **code**, and every code has a **class**.\nThe class says what kind of thing went wrong, which is what decides\nyour next move. The full registry — every code, its class, and the\nversion it appeared in — is\n[`test/spec/errcodes.tsv`](../../test/spec/errcodes.tsv), which the\ntest suite holds to the engine in both implementations.\n\n| Class | Meaning | What to do |\n|-------|---------|------------|\n| `parse` | the text is not a document | fix the syntax at the site the frame points at |\n| `conflict` | two values cannot both hold | one of them is wrong: `aontu model why <path>` names both and where they were written |\n| `incomplete` | nothing contradicts, but the value is not concrete | supply what is missing, or accept it with `--partial` |\n| `reference` | a path names nothing | check the spelling; `aontu model get $ --keys` lists what is there |\n| `compat` | a change breaks an earlier version | that is `aontu subsume` / `aontu breaking` talking: widen the change or version it |\n| `budget` | evaluation hit a deterministic limit | usually a cycle; simplify, or raise the budget deliberately |\n| `internal` | the engine surprised itself | a bug worth reporting |\n\n## Looking a code up\n\n```\naontu explain constraint # what one code means, and its class\naontu explain --list # every registered code, with its class\n```\n\nA report prints two bracketed spans, and only one of them is a code:\n\n```\n$.note.n1.id: constraint [conflict]\n [aontu/constraint]: Cannot unify values at path $.note.n1.id\n```\n\n`[conflict]` is the **class**. `[aontu/constraint]` is the **code**,\ncarrying the `aontu/` prefix. `explain` takes either spelling —\n`constraint` or `aontu/constraint` — and answers under the registered\none. The headline carries the bare code as well, and so does the `code`\nfield of a `--format json` report.\n\n## The repair loop\n\n```\naontu vet schema.aon mine.aon --format json\n```\n\nThe exit code branches for you: `0` valid, `1` invalid (fix the\ndata), `3` incomplete (supply more), `4` the schema itself is\nunusable (fix the truth, not the data). Every finding carries\n`path`, `code`, and the sites on both sides — the data site first,\nbecause that is the one to edit. A site names **the file whose text\nit excerpts**, so its row and column are safe to edit at even when\nthe document loads others.\n\nRead `hint` before guessing. `message` is the one-line headline;\n`hint` is the engine's own explanation of the failure class with the\noffending values filled in, and for several codes it names the fix\noutright — `lossy_integer_literal` tells you to write the literal as\n`0d…`. Every code in the registry has hint text, which is what\n`aontu explain` prints. A finding carries it inline for an exact\nregistry code; a **dynamic** code (`func:upper`, `op[+]`) is registered\nthrough its prefix, and only `explain` falls back to the prefix's text,\nso such a finding carries no `hint`.\n\nFor a conflict, `aontu model why <path> mine.aon` lists every contribution\nto that path with its role and source line, which turns \"these\ndisagree\" into \"these two lines disagree\".\n\nThen fix it:\n\n```\naontu model set '$.replicas=5' --entry schema.aon --overlay mine.aon --in-place\n```\n\n`--in-place` rewrites the pinned literal **where it was written**, so\ncomments and layout survive. Without it, `set` APPENDS — which is the\nright thing when the document left a hole, and cannot work when it\npinned the wrong value, because unification only narrows.\n\nThe edit is verified before a byte is written: a site carries the\nsource text it covers, and the text at the span must match it. Where\nthat cannot be established — the value comes from a `&:` template or a\n`$ref`, two statements pin the path, the site names the opening token\nof a compound like `min(1)`, or the overlay `@\"includes\"` another\ndocument — the assignment is appended as usual and a **warning** says\nwhich case it hit. A warning never changes the verdict, so asking for\n`--in-place` cannot make a run fail that would have succeeded.\n"
39
39
  },
40
40
  {
41
41
  "topic": "grammar",
package/src/hints.ts CHANGED
@@ -189,6 +189,8 @@ const hints: Record<string, string> = {
189
189
 
190
190
  elided_value: 'A key or element was written with no value after the colon. An\nelided value is a mistake in the source rather than a null: write\n`null` if that is what was meant, or supply the value.\n \nExamples:\n a:null -> null # An explicit null, which is a value;\n a: -> nil # ... but nothing at all is not;\n a: b:1 -> {..} # A colon chain is not an elision;\n [1,] -> [1] # ... nor is a trailing comma.',
191
191
 
192
+ alias_budget: 'Alias expansion is bounded but not small: a name that names names\nexpands to the product of what they hold, so twenty shallow\ndeclarations reach a million nodes. The expanded size is counted\nbefore evaluation and refused over the budget ({budget} nodes), so\nthe document is turned away rather than run until memory is gone.\n \nExpansion terminates whatever the budget: an alias takes no\nparameters, a cycle is refused, and a file declares finitely many\nnames. The budget is about SIZE, not about termination.\n \nRaise it with trust.budget.alias where the document is trusted and\nthe machine can hold the result.',
193
+ reserved_key: 'The \\u0000aontu_ key prefix belongs to the engine. A document\'s key\norder, its spreads and its alias declarations are held under keys in\nthat namespace, so a source key written there would land on one of\nthem. It is refused where it stands rather than silently replacing\nwhat the parser put there. No ordinary key needs the prefix: it\nbegins with a NUL, which only an escape can spell.\n \nExamples:\n "\\u0000aontu_order": 1 -> nil # The namespace is the engine\'s;\n "aontu_order": 1 -> {..} # ... without the NUL it is a key;\n "___merge": 1 -> {..} # ... and so is this.',
192
194
  alias_colon: 'An alias is declared with `=`: write `%name = value`. Until 0.57.0\nthe declaration was spelled with a colon, `%name: value`, and that\nform is refused rather than read as an ordinary key -- a document\nwritten for the old spelling fails here, at the declaration, instead\nof gaining a key named `%name` and a use that resolves to nothing.\n \nExamples:\n %u8 = integer & min(0) -> {..} # Declares %u8, which a: %u8 uses;\n %u8: integer -> nil # The form before 0.58.0, refused;\n "%u8": 1 -> {..} # A quoted key is an ordinary key.',
193
195
 
194
196
  bare_punct: 'A bare string holds letters, digits, `-` and `_`, and nothing else.\nThis one holds `{char}`, in `{text}`. Every other punctuation\ncharacter is either syntax or an error, never silently part of a\nstring: a value that needs one is written quoted, and a `>` or `<`\nthat was meant as a bound is written as min(x), max(x), above(x) or\nbelow(x).\n \nExamples:\n a: team-payments -> "team-payments" # `-` and `_` are text;\n a: 2026-09-05 -> "2026-09-05" # ... digits included;\n a: x=y -> nil # `=` is not;\n a: "x=y" -> "x=y" # ... so quote it;\n a: >10 -> nil # Not an operator: write above(10).',
@@ -542,6 +544,228 @@ const hints: Record<string, string> = {
542
544
  // Close operation
543
545
  'close': 'Failed to close structure. The structure could not be closed.',
544
546
 
547
+ // Parse: the source text is malformed or unusable
548
+ 'parse': 'The document could not be turned into a value. This wraps the\n' +
549
+ 'failure that stopped it -- a syntax error, or a source the include\n' +
550
+ 'machinery refused -- and that inner code and its frame say which,\n' +
551
+ 'so a report names the inner code rather than this one.\n' +
552
+ ' \nNote that `parse` is also the name of a CLASS, and the class is\n' +
553
+ 'what a refusal shows in the second bracket: `syntax [parse]` is\n' +
554
+ 'the code `syntax` in the class `parse`.',
555
+
556
+ 'syntax': 'The text is not valid Aontu syntax. The frame points at the\n' +
557
+ 'character the parser stopped on; the fault is usually just before\n' +
558
+ 'it, and commenting out the suspect lines with # isolates which.',
559
+
560
+ 'parse_unknown':
561
+ 'A parsed value arrived in a kind the language has no value for.\n' +
562
+ 'Aontu builds from maps, lists, strings, numbers, booleans and nil,\n' +
563
+ 'so a value outside that set is a defect in whatever produced it\n' +
564
+ 'rather than something a document can write.',
565
+
566
+ 'negative': 'Only a number can be negated. A `-` in front of a\n' +
567
+ 'non-numeric value is an error rather than a missing value.\n' +
568
+ ' \nA `-` is a PREFIX here and never an operator between two values,\n' +
569
+ 'so subtraction is sub(a, b).' +
570
+ '\n \nExamples:\n' +
571
+ ' a: -1 -> -1 # A number negates;\n' +
572
+ ' a: -x -> nil # ... a bare word does not;\n' +
573
+ ' a: k-x -> "k-x" # ... and this is one word, not a minus;\n' +
574
+ ' a: sub(5,3) -> 2 # Subtraction is a function.',
575
+
576
+ 'not_number': 'This numeric literal is not a finite number. A literal\n' +
577
+ 'past the range a double can hold (1e999) overflows to infinity\n' +
578
+ 'while lexing, which is an error value and not a number. Write it\n' +
579
+ 'within range, or as a decimal literal with the 0d escape.',
580
+
581
+ 'incomplete_expression':
582
+ 'The expression has no terms. An operator or a pair of parentheses\n' +
583
+ 'was written with nothing for it to work on -- `a:()` is the bare\n' +
584
+ 'case. Supply the operand, or delete the construct.',
585
+
586
+ 'alias_in_path':
587
+ 'An alias is not a path segment. The alias namespace and the path\n' +
588
+ 'namespace are disjoint, so `$.%foo` is refused at any depth: an\n' +
589
+ 'alias is reached by writing `%foo` and only that.' +
590
+ '\n \nExamples:\n' +
591
+ ' %port = min(1) -> declared;\n' +
592
+ ' listen: %port -> the alias, reached by its own name;\n' +
593
+ ' listen: $.%port -> nil # Not a path segment.',
594
+
595
+ 'alias_not_toplevel':
596
+ 'An alias declaration written as a KEY sits at the root of the\n' +
597
+ 'document. A nested `x: { %a = 1 }` is refused because `%a` resolves\n' +
598
+ 'from the root: the declaration would be erased from the output,\n' +
599
+ 'being a declaration, and still unreachable by any reference, not\n' +
600
+ 'being at the root. Where the declaration LANDS decides this, not\n' +
601
+ 'where it was written, so an include spliced at the root may declare\n' +
602
+ 'one and an include taken as a value may not.\n' +
603
+ 'To name a shape where it is used, write the declaration as a VALUE\n' +
604
+ 'prefix instead: `x: %a = 1` is accepted at any depth, leaves the\n' +
605
+ 'value alone, and declares `%a` for the document.',
606
+
607
+ 'export_arg':
608
+ '`export` takes a SET OF ALIAS NAMES and nothing else: write\n' +
609
+ '`export({ %a, %b })`. A key already crosses a file boundary as a\n' +
610
+ 'value, so a bare word names nothing `export` could publish, and a\n' +
611
+ 'single name still stands in a set. The `{%}` wildcard is the\n' +
612
+ 'importing side\'s: the publishing file chooses what it publishes.\n' +
613
+ ' \nExamples:\n' +
614
+ ' %u8 = integer\n' +
615
+ ' export({ %u8 }) -> published;\n' +
616
+ ' export({ u8 }) -> nil # A key, not an alias;\n' +
617
+ ' export(%u8) -> nil # A set, even of one;\n' +
618
+ ' export({%}) -> nil # The wildcard is the importer\'s.',
619
+
620
+ 'import_not_exported':
621
+ 'The file this name was asked of does not publish `{name}`. A name\n' +
622
+ 'belongs to the file that declares it and crosses only where that\n' +
623
+ 'file says so, which is what `export` is for: add the name to the\n' +
624
+ 'other file\'s `export({ ... })`, or write the value in this one.\n' +
625
+ 'The include still placed the file\'s values -- it is the NAME that\n' +
626
+ 'did not cross.\n' +
627
+ ' \nExamples:\n' +
628
+ ' { %u8 } = @"types.aon" -> bound, if types.aon exports %u8;\n' +
629
+ ' { %secret } = @"types.aon" -> nil # ... and refused if not.',
630
+
631
+ 'patch_assignment':
632
+ 'This is not a <path>=<value> assignment. The path is what stands\n' +
633
+ 'before the first `=` and the value is what follows it, so an\n' +
634
+ 'argument carrying only one of them cannot be applied.',
635
+
636
+ // Reference: a name or path resolves to nothing editable
637
+ 'multisource_not_found':
638
+ 'The source named here was not found. For a file include, check the\n' +
639
+ 'path as written, which is resolved against the document that writes\n' +
640
+ 'it; for an `aontu:` name the message lists the models the language\n' +
641
+ 'supplies, and a name outside that set is never looked for on disk.',
642
+
643
+ 'patch_ambiguous':
644
+ 'More than one statement pins this path, so there is no single\n' +
645
+ 'literal to rewrite in place. The sites on the finding are all of\n' +
646
+ 'them: edit the one that should change, or narrow the overlay so\n' +
647
+ 'that only one pins the path.',
648
+
649
+ 'patch_not_editable':
650
+ 'There is no literal at this path to rewrite in place. The value is\n' +
651
+ 'reached through a reference, arrives only once the overlay loads\n' +
652
+ 'another document, or is produced rather than written -- the finding\n' +
653
+ 'names which, and the sites say where it does come from. Edit there,\n' +
654
+ 'or let set append instead of asking for --in-place.',
655
+
656
+ 'var': 'This variable has no value, and an unresolved variable cannot\n' +
657
+ 'be generated. A `$name` variable is supplied by the CALLER, never\n' +
658
+ 'by the document: bind it through `ctx.vars` in TypeScript or pass\n' +
659
+ 'it to `UnifyVars` in Go. If the value was meant to come from the\n' +
660
+ 'document, a path reference ($.a.b) or an alias (%name) is what\n' +
661
+ 'reads one.',
662
+
663
+ // Internal: the engine surprised itself
664
+ 'patch_span_mismatch':
665
+ 'The overlay does not hold the text the site says is there, so the\n' +
666
+ 'span cannot be verified and the edit is refused rather than\n' +
667
+ 'written: splicing without verifying the span corrupts the file.\n' +
668
+ 'The span is checked against the very text it was derived from, so\n' +
669
+ 'a document changing underneath cannot produce this. It is an\n' +
670
+ 'engine provenance defect -- the one refusal classed internal for\n' +
671
+ 'that reason -- and worth reporting.',
672
+
673
+ 'unify_failed':
674
+ 'The document does not evaluate, and the failure carried no more\n' +
675
+ 'specific code. This stands in where a nil reaches the report\n' +
676
+ 'without one, so the other findings in the same run are what say\n' +
677
+ 'what actually went wrong.',
678
+
679
+ 'unknown_op':
680
+ 'The parser produced an operator form the value builder does not\n' +
681
+ 'recognise. Reaching this is an engine defect rather than a fault\n' +
682
+ 'in the document.',
683
+
684
+ // Compat: the subsumption and outcome vocabulary
685
+ 'compat_narrowed':
686
+ 'The general value does not admit the specific one: the specific\n' +
687
+ 'side allows something the general side refuses -- a wider kind, a\n' +
688
+ 'wider residual, or a value the general residual excludes. A\n' +
689
+ 'document the specific side accepts can therefore fail against the\n' +
690
+ 'general one. Which document is on which side is the comparison\'s:\n' +
691
+ 'subsume takes them in the order given, and breaking decides by its\n' +
692
+ 'mode.',
693
+
694
+ 'compat_required_added':
695
+ 'The general value requires this key and the specific value does\n' +
696
+ 'not, either because the key is absent there or because it is\n' +
697
+ 'optional there. Instances without the key are admitted where the\n' +
698
+ 'general side refuses them.',
699
+
700
+ 'compat_default_changed':
701
+ 'The general and specific values have different effective defaults,\n' +
702
+ 'so a document resolving to one of them against one side resolves\n' +
703
+ 'to the other -- or stops resolving -- against the other, with\n' +
704
+ 'nothing in the document itself changing. Which document is on\n' +
705
+ 'which side is the comparison\'s own.',
706
+
707
+ 'compat_marks_changed':
708
+ 'The marks on this value differ between the general and specific\n' +
709
+ 'sides: one carries type() or hide() where the other does not. A\n' +
710
+ 'mark changes what the value contributes to a generated document,\n' +
711
+ 'so the two are not the same declaration even where what they admit\n' +
712
+ 'agrees. Reported under the gen profile, which is the one that\n' +
713
+ 'compares them.',
714
+
715
+ 'compat_outcome_changed':
716
+ 'This path resolved to one value before and resolves to a different\n' +
717
+ 'one now. Nothing refuses, so the change is silent: a consumer\n' +
718
+ 'reading this path gets a new answer with no error to say so.',
719
+
720
+ 'compat_undetermined':
721
+ 'This path resolved to a value before and nothing resolves it now.\n' +
722
+ 'The declaration became incomplete rather than wrong, so a consumer\n' +
723
+ 'that read a value here reads nothing.',
724
+
725
+ 'deprecated':
726
+ 'This value is marked deprecated. The record carries the author\'s\n' +
727
+ 'message, and where they supplied them, what to use instead and the\n' +
728
+ 'version it was deprecated in. Nothing refuses: a deprecation is a\n' +
729
+ 'warning and never changes a verdict.',
730
+
731
+ 'pref_not_instance':
732
+ 'The preferred value is not repeated among the remaining\n' +
733
+ 'alternatives of its disjunction. Nothing is refused and the\n' +
734
+ 'preference still holds -- the default stays admitted, and\n' +
735
+ 'generation selects it -- so this is ADVISORY. It catches the typo\n' +
736
+ 'where a default was meant to name one of the alternatives beside\n' +
737
+ 'it and names something else instead.',
738
+
739
+ 'sub_unresolved':
740
+ 'An unresolved value has no admitted set to compare. Either a\n' +
741
+ 'residue is still standing here, or the two sides are value formers\n' +
742
+ 'no comparison rule covers, so the answer is undecided rather than\n' +
743
+ 'yes or no.',
744
+
745
+ 'sub_evaluate_only':
746
+ 'An evaluate-only check makes the admitted set opaque. must() is\n' +
747
+ 'checked by running it and never by reasoning about what it admits,\n' +
748
+ 'so a value carrying one cannot be compared and the answer is\n' +
749
+ 'undecided rather than yes or no.',
750
+
751
+ 'sub_disjunct_distribution':
752
+ 'An alternative is not admitted member by member, and no concrete\n' +
753
+ 'value settles it either way. Comparing a disjunction member-wise\n' +
754
+ 'is sound when it answers yes; a no needs a counterexample, and\n' +
755
+ 'there is none here, so the answer is undecided.',
756
+
757
+ 'sub_default_indeterminate':
758
+ 'The effective default is not a single value, because preferences\n' +
759
+ 'of equal rank disagree. Nothing can be decided about the default\n' +
760
+ 'until one of them is ranked (`**x`) or they are made to agree, so\n' +
761
+ 'the answer is undecided rather than yes or no.',
762
+
763
+ 'sub_path_dependent_spread':
764
+ 'A spread template that depends on where it lands cannot be\n' +
765
+ 'compared structurally. What it produces is known only once it is\n' +
766
+ 'applied to a path, so the general and specific sides cannot be\n' +
767
+ 'held against each other and the answer is undecided.',
768
+
545
769
  // Dynamic patterns (these serve as prefixes)
546
770
  'func:': 'Function error: ',
547
771
  'op:': 'Operator error: ',
@@ -687,7 +911,11 @@ const codeClasses: Record<string, string> = {
687
911
  pref_implicit_bag: 'parse',
688
912
  alias_not_toplevel: 'parse',
689
913
  alias_in_path: 'parse',
914
+ alias_budget: 'budget',
915
+ reserved_key: 'parse',
690
916
  alias_colon: 'parse',
917
+ export_arg: 'parse',
918
+ import_not_exported: 'reference',
691
919
  bare_punct: 'parse',
692
920
  not_number: 'parse',
693
921
  negative: 'parse',