aontu 0.66.0 → 0.68.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.
package/src/cli.ts CHANGED
@@ -166,22 +166,30 @@ query between a document and its own earlier versions.
166
166
 
167
167
  Options:
168
168
  -c, --canon Print the canonical form instead of generated JSON
169
- --format <f> text (default) or json. The json form wraps the
170
- answer as {aontu, findings, ok, out}, so a failure
171
- here reads like every other verb's
169
+ (the bare command's, as --jsonl is; model get has
170
+ its own)
171
+ --format <f> text (default) or json, on every verb that answers a
172
+ report. The json form is one object opening with an
173
+ aontu block; the bare command's carries findings, ok
174
+ and out
172
175
  -h, --help Show this help and exit (the verbs and their flags);
173
176
  aontu help is the LANGUAGE, and lists its own topics
174
177
  --jsonl REPL: answer every command as one JSON line
175
178
  -v, --version Print the version and exit
176
- --trust <t> Include capability: system (default), none, or
177
- root[:dir] to confine @"..." below a directory.
178
- Every verb takes it too, and a bare root means the
179
- document's own directory
179
+ --trust <t> Include capability: system, none, or root[:dir] to
180
+ confine @"..." below a directory. A bare root means
181
+ the entry root: the document's own directory, or the
182
+ project's for the package verbs. Every verb takes it
183
+ but help, explain, init and lsp, which read no
184
+ document, and mcp, which is the npm build's server
185
+ and confines with --root. Unset, a run behaves as
186
+ system, and the bare command warns once for an
187
+ include that leaves the entry root
180
188
  --include-root <dir> Shorthand for --trust root:<dir>
181
- --text-ext <e> Read these extensions as text too, comma-separated
182
- and without dots (md,sql). .txt needs no flag; a
189
+ --text-ext <e> Read these extensions as text too, comma-separated,
190
+ with or without dots (md,sql). .txt needs no flag; a
183
191
  named format keeps its meaning, and .js stays
184
- refused. Every verb takes it
192
+ refused. The verbs that take --trust take it too
185
193
 
186
194
  Package verbs (a module is imported; a package is published):
187
195
  sync Make the project correct: resolve by minimum version
@@ -708,7 +716,7 @@ function takeTrust(argv: string[], io: Io = PROCESS_IO):
708
716
  }
709
717
  else if ('--include-root' === arg) {
710
718
  const dir = argv[++i]
711
- if (null == dir) {
719
+ if (null == dir || '' === dir) {
712
720
  io.err('aontu: --include-root needs a directory\n')
713
721
  return undefined
714
722
  }
@@ -2704,20 +2712,42 @@ function pkgNetText(verb: string, report: any): string {
2704
2712
  const MODEL_HELP = 'aontu model get|why|set ... (try --help)'
2705
2713
 
2706
2714
  // One document, interrogated or edited (ADR-039 part 5).
2715
+ // Where the subcommand is, past any global flag that precedes it.
2716
+ // `takeTrust` strips those anywhere in a tail, so the subcommand has
2717
+ // to be found past a flag AND past its value: in `--text-ext get` the
2718
+ // `get` is the extension list, not the subcommand.
2719
+ const MODEL_SUBS = ['get', 'why', 'set']
2720
+ const GLOBAL_VALUED = ['--trust', '--include-root', '--text-ext']
2721
+
2722
+
2723
+ function modelSubAt(argv: string[]): number {
2724
+ for (let i = 0; i < argv.length; i++) {
2725
+ if (GLOBAL_VALUED.includes(argv[i])) {
2726
+ i++
2727
+ continue
2728
+ }
2729
+ return MODEL_SUBS.includes(argv[i]) ? i : -1
2730
+ }
2731
+ return -1
2732
+ }
2733
+
2734
+
2707
2735
  function runModel(argv: string[]): number {
2708
2736
  const sub = argv[0]
2709
2737
  if ('-h' === sub || '--help' === sub) {
2710
2738
  process.stdout.write(HELP)
2711
2739
  return 0
2712
2740
  }
2713
- if ('get' === sub) {
2714
- return runGet(argv.slice(1))
2715
- }
2716
- if ('why' === sub) {
2717
- return runWhy(argv.slice(1))
2718
- }
2719
- if ('set' === sub) {
2720
- return runSet(argv.slice(1))
2741
+ const at = modelSubAt(argv)
2742
+ if (0 <= at) {
2743
+ const tail = argv.slice(0, at).concat(argv.slice(at + 1))
2744
+ if ('get' === argv[at]) {
2745
+ return runGet(tail)
2746
+ }
2747
+ if ('why' === argv[at]) {
2748
+ return runWhy(tail)
2749
+ }
2750
+ return runSet(tail)
2721
2751
  }
2722
2752
  process.stderr.write(`aontu: model needs get, why or set\n${MODEL_HELP}\n`)
2723
2753
  return 2
@@ -2931,6 +2961,70 @@ function isDirectory(path: string): boolean {
2931
2961
  }
2932
2962
 
2933
2963
 
2964
+ // A `File` the write path skips, RENAMED rather than removed: dropping
2965
+ // the node would take its children's claims with it and hide drift the
2966
+ // write path does make. The skip is the File's own save alone, so what
2967
+ // separates the runs is its output path, composed by the runtime rather
2968
+ // than here.
2969
+ const EXCLUDED_NAME = '.aontu-check-excluded-'
2970
+
2971
+
2972
+ // `exclude` as the runtime reads it: `true`, or a string or list member
2973
+ // equal to the node's COMPONENT path, the chain of `name` props above
2974
+ // it, which a Project's `folder` is not part of. The Go runtime honours
2975
+ // the boolean alone, so the ports' `--check` answers differ for the
2976
+ // path forms (test/spec/divergent.tsv).
2977
+ function excludedFile(exclude: any, at: string[]): boolean {
2978
+ if (true === exclude) {
2979
+ return true
2980
+ }
2981
+ const path = at.join('/')
2982
+ if ('string' === typeof exclude) {
2983
+ return exclude === path
2984
+ }
2985
+ return Array.isArray(exclude) && exclude.includes(path)
2986
+ }
2987
+
2988
+
2989
+ function renameExcluded(node: any, at: string[], cut: number[]): any {
2990
+ if (Array.isArray(node)) {
2991
+ return node.map((child) => renameExcluded(child, at, cut))
2992
+ }
2993
+ // A hand-written tree carries nodes with no `props` and nodes with
2994
+ // no `children`, and neither needs an arm of its own.
2995
+ const props: any = node.props ?? {}
2996
+ const below = 'string' === typeof props.name ? at.concat(props.name) : at
2997
+ if ('File' === node.cmp && excludedFile(props.exclude, below)) {
2998
+ cut.push(1)
2999
+ return { ...node, props: { ...props, name: EXCLUDED_NAME + cut.length } }
3000
+ }
3001
+ if (!Array.isArray(node.children)) {
3002
+ return node
3003
+ }
3004
+ return { ...node, children: renameExcluded(node.children, below, cut) }
3005
+ }
3006
+
3007
+
3008
+ // A file the runtime declined to touch, its copy on disk carrying the
3009
+ // protect marker, is in none of the lists it answers with: they hold
3010
+ // what was DONE to a file, and nothing was. The run's own record names
3011
+ // it. `since` drops what earlier runs left.
3012
+ function renderSkipped(folder: string, since: number): string[] {
3013
+ const at = join(folder, '.jostraca', 'jostraca.meta.log')
3014
+ let meta: any
3015
+ try {
3016
+ meta = JSON.parse(readFileSync(at, 'utf8'))
3017
+ }
3018
+ catch (err: any) {
3019
+ return []
3020
+ }
3021
+ return Object.entries(meta?.files ?? {})
3022
+ .filter(([_, f]: [string, any]) => 'skip' === f?.action && since <= f?.when)
3023
+ .map(([path]) => path)
3024
+ .sort(cmpCodePoint)
3025
+ }
3026
+
3027
+
2934
3028
  async function runRender(argv: string[]): Promise<number> {
2935
3029
  const trusted = takeTrust(argv)
2936
3030
  if (null == trusted) {
@@ -3081,7 +3175,22 @@ async function runRender(argv: string[]): Promise<number> {
3081
3175
  try {
3082
3176
  if (check) {
3083
3177
  const res = await runtime.check({ folder }, root)
3084
- const drift = res.drift.map((d: any) => ({ kind: d.kind, path: d.path }))
3178
+ let drift = res.drift.map((d: any) => ({ kind: d.kind, path: d.path }))
3179
+ // `--check` answers "would `render` change anything", so a file
3180
+ // the write path leaves alone is not held to the generator's
3181
+ // bytes. The skip is gated on the target BEING there -- `render`
3182
+ // writes an absent one -- so drift at a path with nothing at it
3183
+ // survives, which is the `missing` a deleted file reports.
3184
+ const cut: number[] = []
3185
+ const renamed = renameExcluded(tree, [], cut)
3186
+ if (0 < cut.length) {
3187
+ const kept = new Set<string>((await Jostraca().check(
3188
+ { folder }, cmpTree(renamed, { raw: true }))).checked)
3189
+ const skipped = new Set<string>(
3190
+ res.checked.filter((p: string) => !kept.has(p)))
3191
+ drift = drift.filter((d: any) => !skipped.has(d.path) ||
3192
+ !existsSync(join(folder, d.path)))
3193
+ }
3085
3194
  if ('json' === format) {
3086
3195
  process.stdout.write(exactJSON({
3087
3196
  aontu: { version: version(), verb: 'render' },
@@ -3098,14 +3207,21 @@ async function runRender(argv: string[]): Promise<number> {
3098
3207
  return 0 === drift.length ? 0 : 1
3099
3208
  }
3100
3209
 
3210
+ const since = Date.now()
3101
3211
  const res = await runtime.generate({ folder }, root)
3212
+ const skipped = renderSkipped(folder, since)
3102
3213
  if ('json' === format) {
3103
3214
  process.stdout.write(exactJSON({
3104
3215
  aontu: { version: version(), verb: 'render' },
3105
3216
  verdict: 'ok',
3106
- files: res.files,
3217
+ files: { ...res.files, skipped },
3107
3218
  }, 2) + '\n')
3108
3219
  }
3220
+ else {
3221
+ for (const path of skipped) {
3222
+ process.stdout.write(`skipped: ${path}\n`)
3223
+ }
3224
+ }
3109
3225
  return 0
3110
3226
  }
3111
3227
  catch (err: any) {
@@ -3932,7 +4048,7 @@ function runHash(argv: string[]): number {
3932
4048
  if (0 < ctx.err.length || true === v?.isNil) {
3933
4049
  process.stderr.write(
3934
4050
  `aontu: ${files[0]} does not evaluate on its own; nothing to hash\n` +
3935
- renderFinding(evalFailure(ctx)) + '\n')
4051
+ renderFinding(evalFailure(ctx, v)) + '\n')
3936
4052
  return 4
3937
4053
  }
3938
4054
 
@@ -4923,6 +5039,17 @@ function runHelp(argv: string[]): number {
4923
5039
  // and carries that prefix's hint.
4924
5040
  const EXPLAIN_PREFIXES = ['func:', 'op:', 'op[', 'var[', 'ref[']
4925
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
+
4926
5053
 
4927
5054
  function explainCode(
4928
5055
  code: string): { cls: string, hint: string, registered: boolean } {
@@ -4962,9 +5089,10 @@ function explainListText(format: SubsumeFormat): string {
4962
5089
  codes: codes.map((code) => ({
4963
5090
  code,
4964
5091
  class: codeClass(code),
4965
- // Whether this port carries explanation text for the code. The
4966
- // registry is in parity; the hint tables are not, so a consumer
4967
- // 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.
4968
5096
  explained: '' !== explainCode(code).hint,
4969
5097
  })),
4970
5098
  }, 2)
@@ -4972,7 +5100,21 @@ function explainListText(format: SubsumeFormat): string {
4972
5100
  const width = codes.reduce((w, c) => Math.max(w, c.length), 0)
4973
5101
  return codes.map((c) =>
4974
5102
  c.padEnd(width) + ' ' + codeClass(c) +
4975
- ('' === 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)' : ''
4976
5118
  }
4977
5119
 
4978
5120
 
@@ -5022,7 +5164,7 @@ function runExplain(argv: string[]): number {
5022
5164
  return 2
5023
5165
  }
5024
5166
 
5025
- const code = codes[0]
5167
+ const code = canonExplainCode(codes[0])
5026
5168
  const { cls, hint, registered } = explainCode(code)
5027
5169
  if (!registered) {
5028
5170
  // AN UNKNOWN CODE IS A USAGE ERROR AND NAMES NEAR MATCHES. A
@@ -5048,13 +5190,8 @@ function runExplain(argv: string[]): number {
5048
5190
  }, 2) + '\n')
5049
5191
  return 0
5050
5192
  }
5051
- // A REGISTERED CODE WITH NO HINT SAYS SO rather than printing an
5052
- // empty block, which would read as an explanation that happened to
5053
- // be blank.
5054
- const body = '' === hint
5055
- ? '(no explanation text is registered for this code)'
5056
- : hint
5057
- 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`)
5058
5195
  return 0
5059
5196
  }
5060
5197
 
@@ -5322,7 +5459,7 @@ function main(argv: string[], servers: Servers = SERVERS): void {
5322
5459
  }
5323
5460
  else if ('--include-root' === arg) {
5324
5461
  const dir = args[++i]
5325
- if (null == dir) {
5462
+ if (null == dir || '' === dir) {
5326
5463
  process.stderr.write('aontu: --include-root needs a directory\n')
5327
5464
  return finish(2)
5328
5465
  }
@@ -5371,7 +5508,7 @@ function main(argv: string[], servers: Servers = SERVERS): void {
5371
5508
  else {
5372
5509
  runStdin(mode, format, trust).then((code) => finish(code))
5373
5510
  }
5374
- } /* node:coverage ignore next 21 */
5511
+ } /* node:coverage ignore next 22 */
5375
5512
 
5376
5513
 
5377
5514
  // No require.main guard here: bin/aontu.js is the executable entry and
@@ -5385,9 +5522,10 @@ export {
5385
5522
  runJsonSchema,
5386
5523
  runTemplate,
5387
5524
  runTrace,
5388
- runRender,
5525
+ runRender, renderSkipped,
5389
5526
  runPkg, runModel, runPackageVerb, pkgToolOptions, serveUntilInterrupted,
5390
5527
  runHash, runGet, runHelp, runExplain, runInit, nearestVerb,
5528
+ explainBody, noTextMark, canonExplainCode,
5391
5529
  looksLikeVerb,
5392
5530
  KNOWN_VERBS,
5393
5531
  runWhy, renderWhyText, runSet, runAllow, runAgentsMd, runFmt,
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
@@ -181,7 +181,7 @@ const hints: Record<string, string> = {
181
181
 
182
182
  merge_conflict: 'A version-control conflict marker was found in the source. The\nfile still holds an unresolved merge: resolve it and remove the\n`<<<<<<<`, `=======` and `>>>>>>>` lines before unifying.\n \nExamples:\n <<<<<<< HEAD -> nil # A conflict marker, not a `<` operation;\n ======= -> nil # ... nor a chain of `=` characters;\n >>>>>>> other -> nil # ... nor a `>` operation.',
183
183
 
184
- include_denied: 'An @"..." include was refused by the active trust profile\n(docs/trust.md). The document asked to read a source the evaluation\'s\ninclude capability does not allow: widen the capability if the read is\nintended, or remove the include if it is not.\n \nExamples:\n a:@"./in-root.aon" -> {..} # Inside the confinement root: allowed;\n a:@"../secret.aon" -> nil # ... but escaping the root is denied;\n a:@"/etc/hostname" -> nil # ... and so is an absolute path outside it.',
184
+ include_denied: 'An @"..." include was refused by the active trust profile\n(docs/trust.md). The document asked to read a source the evaluation\'s\ninclude capability does not allow: widen the capability if the read is\nintended, or remove the include if it is not.\n \nExamples:\n a:@"./in-root.aon" -> {..} # Inside the confinement root: allowed;\n a:@"../secret.aon" -> nil # ... but escaping the root is denied;\n a:@"/etc/hostname" -> nil # ... and so is an absolute path outside it.',
185
185
  include_extension: 'An @\"...\" include named a file the engine does not read. THE\nEXTENSION DECIDES which of two things a file is: `.aon` and\n`.aontu` are aontu source, with the whole language in them, and\n`.json`, `.jsonld`, `.jsonc`, `.json5`, `.jsonic`, `.jsc`, `.toml`,\n`.yaml`, `.yml` and `.ini` are configuration DATA, read by that\nformat\'s own parser. Anything else -- and a name with no extension\n-- is refused rather than guessed at: a guess produces a document\nthat looks right and is not.\n \nExamples:\n a:@\"model.aon\" -> {..} # aontu source;\n a:@\"server.toml\" -> {..} # ... config data, read as TOML;\n a:@\"notes.txt\" -> nil # ... but text is not a document;\n a:@\"data\" -> nil # ... and neither is an unnamed kind.',
186
186
 
187
187
  func_arity: 'This function was called with the wrong number of arguments:\n{func} takes {want}, but was given {got}.\n \nExamples:\n upper(\"a\") -> \"A\" # One argument, which is what upper takes;\n upper(\"a\",\"b\") -> nil # ... so two is a mistake in the source;\n key() -> \"\" # key takes none, or one level count;\n neq(1,2,3) -> neq # ... and neq takes one or more exclusions.',
@@ -542,6 +542,201 @@ const hints: Record<string, string> = {
542
542
  // Close operation
543
543
  'close': 'Failed to close structure. The structure could not be closed.',
544
544
 
545
+ // Parse: the source text is malformed or unusable
546
+ 'parse': 'The document could not be turned into a value. This wraps the\n' +
547
+ 'failure that stopped it -- a syntax error, or a source the include\n' +
548
+ 'machinery refused -- and that inner code and its frame say which,\n' +
549
+ 'so a report names the inner code rather than this one.\n' +
550
+ ' \nNote that `parse` is also the name of a CLASS, and the class is\n' +
551
+ 'what a refusal shows in the second bracket: `syntax [parse]` is\n' +
552
+ 'the code `syntax` in the class `parse`.',
553
+
554
+ 'syntax': 'The text is not valid Aontu syntax. The frame points at the\n' +
555
+ 'character the parser stopped on; the fault is usually just before\n' +
556
+ 'it, and commenting out the suspect lines with # isolates which.',
557
+
558
+ 'parse_unknown':
559
+ 'A parsed value arrived in a kind the language has no value for.\n' +
560
+ 'Aontu builds from maps, lists, strings, numbers, booleans and nil,\n' +
561
+ 'so a value outside that set is a defect in whatever produced it\n' +
562
+ 'rather than something a document can write.',
563
+
564
+ 'negative': 'Only a number can be negated. A `-` in front of a\n' +
565
+ 'non-numeric value is an error rather than a missing value.\n' +
566
+ ' \nA `-` is a PREFIX here and never an operator between two values,\n' +
567
+ 'so subtraction is sub(a, b).' +
568
+ '\n \nExamples:\n' +
569
+ ' a: -1 -> -1 # A number negates;\n' +
570
+ ' a: -x -> nil # ... a bare word does not;\n' +
571
+ ' a: k-x -> "k-x" # ... and this is one word, not a minus;\n' +
572
+ ' a: sub(5,3) -> 2 # Subtraction is a function.',
573
+
574
+ 'not_number': 'This numeric literal is not a finite number. A literal\n' +
575
+ 'past the range a double can hold (1e999) overflows to infinity\n' +
576
+ 'while lexing, which is an error value and not a number. Write it\n' +
577
+ 'within range, or as a decimal literal with the 0d escape.',
578
+
579
+ 'incomplete_expression':
580
+ 'The expression has no terms. An operator or a pair of parentheses\n' +
581
+ 'was written with nothing for it to work on -- `a:()` is the bare\n' +
582
+ 'case. Supply the operand, or delete the construct.',
583
+
584
+ 'alias_in_path':
585
+ 'An alias is not a path segment. The alias namespace and the path\n' +
586
+ 'namespace are disjoint, so `$.%foo` is refused at any depth: an\n' +
587
+ 'alias is reached by writing `%foo` and only that.' +
588
+ '\n \nExamples:\n' +
589
+ ' %port = min(1) -> declared;\n' +
590
+ ' listen: %port -> the alias, reached by its own name;\n' +
591
+ ' listen: $.%port -> nil # Not a path segment.',
592
+
593
+ 'alias_not_toplevel':
594
+ 'An alias declaration sits at the root of the document. A nested\n' +
595
+ '`x: { %a = 1 }` is refused because `%a` resolves from the root: the\n' +
596
+ 'declaration would be erased from the output, being a declaration,\n' +
597
+ 'and still unreachable by any reference, not being at the root.\n' +
598
+ 'Where the declaration LANDS decides this, not where it was written,\n' +
599
+ 'so an include spliced at the root may declare one and an include\n' +
600
+ 'taken as a value may not.',
601
+
602
+ 'patch_assignment':
603
+ 'This is not a <path>=<value> assignment. The path is what stands\n' +
604
+ 'before the first `=` and the value is what follows it, so an\n' +
605
+ 'argument carrying only one of them cannot be applied.',
606
+
607
+ // Reference: a name or path resolves to nothing editable
608
+ 'multisource_not_found':
609
+ 'The source named here was not found. For a file include, check the\n' +
610
+ 'path as written, which is resolved against the document that writes\n' +
611
+ 'it; for an `aontu:` name the message lists the models the language\n' +
612
+ 'supplies, and a name outside that set is never looked for on disk.',
613
+
614
+ 'patch_ambiguous':
615
+ 'More than one statement pins this path, so there is no single\n' +
616
+ 'literal to rewrite in place. The sites on the finding are all of\n' +
617
+ 'them: edit the one that should change, or narrow the overlay so\n' +
618
+ 'that only one pins the path.',
619
+
620
+ 'patch_not_editable':
621
+ 'There is no literal at this path to rewrite in place. The value is\n' +
622
+ 'reached through a reference, arrives only once the overlay loads\n' +
623
+ 'another document, or is produced rather than written -- the finding\n' +
624
+ 'names which, and the sites say where it does come from. Edit there,\n' +
625
+ 'or let set append instead of asking for --in-place.',
626
+
627
+ 'var': 'This variable has no value, and an unresolved variable cannot\n' +
628
+ 'be generated. A `$name` variable is supplied by the CALLER, never\n' +
629
+ 'by the document: bind it through `ctx.vars` in TypeScript or pass\n' +
630
+ 'it to `UnifyVars` in Go. If the value was meant to come from the\n' +
631
+ 'document, a path reference ($.a.b) or an alias (%name) is what\n' +
632
+ 'reads one.',
633
+
634
+ // Internal: the engine surprised itself
635
+ 'patch_span_mismatch':
636
+ 'The overlay does not hold the text the site says is there, so the\n' +
637
+ 'span cannot be verified and the edit is refused rather than\n' +
638
+ 'written: splicing without verifying the span corrupts the file.\n' +
639
+ 'The span is checked against the very text it was derived from, so\n' +
640
+ 'a document changing underneath cannot produce this. It is an\n' +
641
+ 'engine provenance defect -- the one refusal classed internal for\n' +
642
+ 'that reason -- and worth reporting.',
643
+
644
+ 'unify_failed':
645
+ 'The document does not evaluate, and the failure carried no more\n' +
646
+ 'specific code. This stands in where a nil reaches the report\n' +
647
+ 'without one, so the other findings in the same run are what say\n' +
648
+ 'what actually went wrong.',
649
+
650
+ 'unknown_op':
651
+ 'The parser produced an operator form the value builder does not\n' +
652
+ 'recognise. Reaching this is an engine defect rather than a fault\n' +
653
+ 'in the document.',
654
+
655
+ // Compat: the subsumption and outcome vocabulary
656
+ 'compat_narrowed':
657
+ 'The general value does not admit the specific one: the specific\n' +
658
+ 'side allows something the general side refuses -- a wider kind, a\n' +
659
+ 'wider residual, or a value the general residual excludes. A\n' +
660
+ 'document the specific side accepts can therefore fail against the\n' +
661
+ 'general one. Which document is on which side is the comparison\'s:\n' +
662
+ 'subsume takes them in the order given, and breaking decides by its\n' +
663
+ 'mode.',
664
+
665
+ 'compat_required_added':
666
+ 'The general value requires this key and the specific value does\n' +
667
+ 'not, either because the key is absent there or because it is\n' +
668
+ 'optional there. Instances without the key are admitted where the\n' +
669
+ 'general side refuses them.',
670
+
671
+ 'compat_default_changed':
672
+ 'The general and specific values have different effective defaults,\n' +
673
+ 'so a document resolving to one of them against one side resolves\n' +
674
+ 'to the other -- or stops resolving -- against the other, with\n' +
675
+ 'nothing in the document itself changing. Which document is on\n' +
676
+ 'which side is the comparison\'s own.',
677
+
678
+ 'compat_marks_changed':
679
+ 'The marks on this value differ between the general and specific\n' +
680
+ 'sides: one carries type() or hide() where the other does not. A\n' +
681
+ 'mark changes what the value contributes to a generated document,\n' +
682
+ 'so the two are not the same declaration even where what they admit\n' +
683
+ 'agrees. Reported under the gen profile, which is the one that\n' +
684
+ 'compares them.',
685
+
686
+ 'compat_outcome_changed':
687
+ 'This path resolved to one value before and resolves to a different\n' +
688
+ 'one now. Nothing refuses, so the change is silent: a consumer\n' +
689
+ 'reading this path gets a new answer with no error to say so.',
690
+
691
+ 'compat_undetermined':
692
+ 'This path resolved to a value before and nothing resolves it now.\n' +
693
+ 'The declaration became incomplete rather than wrong, so a consumer\n' +
694
+ 'that read a value here reads nothing.',
695
+
696
+ 'deprecated':
697
+ 'This value is marked deprecated. The record carries the author\'s\n' +
698
+ 'message, and where they supplied them, what to use instead and the\n' +
699
+ 'version it was deprecated in. Nothing refuses: a deprecation is a\n' +
700
+ 'warning and never changes a verdict.',
701
+
702
+ 'pref_not_instance':
703
+ 'The preferred value is not repeated among the remaining\n' +
704
+ 'alternatives of its disjunction. Nothing is refused and the\n' +
705
+ 'preference still holds -- the default stays admitted, and\n' +
706
+ 'generation selects it -- so this is ADVISORY. It catches the typo\n' +
707
+ 'where a default was meant to name one of the alternatives beside\n' +
708
+ 'it and names something else instead.',
709
+
710
+ 'sub_unresolved':
711
+ 'An unresolved value has no admitted set to compare. Either a\n' +
712
+ 'residue is still standing here, or the two sides are value formers\n' +
713
+ 'no comparison rule covers, so the answer is undecided rather than\n' +
714
+ 'yes or no.',
715
+
716
+ 'sub_evaluate_only':
717
+ 'An evaluate-only check makes the admitted set opaque. must() is\n' +
718
+ 'checked by running it and never by reasoning about what it admits,\n' +
719
+ 'so a value carrying one cannot be compared and the answer is\n' +
720
+ 'undecided rather than yes or no.',
721
+
722
+ 'sub_disjunct_distribution':
723
+ 'An alternative is not admitted member by member, and no concrete\n' +
724
+ 'value settles it either way. Comparing a disjunction member-wise\n' +
725
+ 'is sound when it answers yes; a no needs a counterexample, and\n' +
726
+ 'there is none here, so the answer is undecided.',
727
+
728
+ 'sub_default_indeterminate':
729
+ 'The effective default is not a single value, because preferences\n' +
730
+ 'of equal rank disagree. Nothing can be decided about the default\n' +
731
+ 'until one of them is ranked (`**x`) or they are made to agree, so\n' +
732
+ 'the answer is undecided rather than yes or no.',
733
+
734
+ 'sub_path_dependent_spread':
735
+ 'A spread template that depends on where it lands cannot be\n' +
736
+ 'compared structurally. What it produces is known only once it is\n' +
737
+ 'applied to a path, so the general and specific sides cannot be\n' +
738
+ 'held against each other and the answer is undecided.',
739
+
545
740
  // Dynamic patterns (these serve as prefixes)
546
741
  'func:': 'Function error: ',
547
742
  'op:': 'Operator error: ',
@@ -780,6 +975,7 @@ const codeClasses: Record<string, string> = {
780
975
 
781
976
  // internal -- the engine reached a state it should not reach
782
977
  internal: 'internal',
978
+ unify_failed: 'internal',
783
979
  unify_no_res: 'internal',
784
980
  unknown_op: 'internal',
785
981
  }