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/lang.ts CHANGED
@@ -106,6 +106,8 @@ import { DisjunctVal } from './val/DisjunctVal'
106
106
  import { IntegerVal } from './val/IntegerVal'
107
107
  import { ListVal } from './val/ListVal'
108
108
  import { MapVal } from './val/MapVal'
109
+ import { repathInstance } from './val/Val'
110
+ import { AontuContext } from './ctx'
109
111
  import { NilVal } from './val/NilVal'
110
112
  import { NullVal } from './val/NullVal'
111
113
  import { NumberVal } from './val/NumberVal'
@@ -188,7 +190,8 @@ const CC_d = 100
188
190
  const CC_D = 68
189
191
 
190
192
  const CC_PCT = 37
191
- const ALIAS_RE = /^%[A-Za-z_][A-Za-z0-9_]*/
193
+ // Hyphen separates segments; not leading or trailing, as `-` prefixes negation.
194
+ const ALIAS_RE = /^%[A-Za-z_][A-Za-z0-9_]*(?:-[A-Za-z0-9_]+)*/
192
195
 
193
196
  const CC_EQ = 61
194
197
  const CC_SP = 32
@@ -940,6 +943,16 @@ help isolate the syntax error.`,
940
943
  jsonic.rule('val', (rs: RuleSpec) => {
941
944
 
942
945
  rs
946
+ // Before the core implicit-map alts: a value prefix, not a key.
947
+ .open([
948
+ {
949
+ s: [VL, CL],
950
+ c: (r: Rule) => 0 !== r.d && isAliasDecl(r.o0, r.o1),
951
+ p: 'val',
952
+ a: (r: Rule) => { r.u.aontu_alias_val = '' + r.o0.src },
953
+ g: 'aontu-alias-val',
954
+ },
955
+ ], { append: false })
943
956
  .open([
944
957
  {
945
958
  s: [CJ, CL], p: 'map', b: 2, n: { pk: 1 },
@@ -1017,6 +1030,12 @@ help isolate the syntax error.`,
1017
1030
 
1018
1031
  r.node = valnode
1019
1032
 
1033
+ const aname = r.u.aontu_alias_val
1034
+ if (null != aname && null != valnode) {
1035
+ const hoist = ((ctx as any).aontu_alias_hoist ||= [])
1036
+ hoist.push({ name: '' + aname, val: valnode })
1037
+ }
1038
+
1020
1039
  return undefined
1021
1040
  })
1022
1041
 
@@ -1093,6 +1112,19 @@ help isolate the syntax error.`,
1093
1112
  delete mo.___alias
1094
1113
  }
1095
1114
 
1115
+ // A value-prefix declaration lands here, at the document root
1116
+ // (ALIASES.0.md), as a copy pathed at its name.
1117
+ if (1 === r.d) {
1118
+ for (const { name, val } of ((ctx as any).aontu_alias_hoist ?? [])) {
1119
+ const acx = new AontuContext({ path: [name] })
1120
+ const copy: any = (val as any).clone(acx, { path: [name] })
1121
+ repathInstance(copy, [name])
1122
+ mo[name] = null == mo[name] ? copy :
1123
+ addsite(new ConjunctVal({ peg: [mo[name], copy] }), r, ctx)
1124
+ if (!aliasKeys.includes(name)) { aliasKeys.push(name) }
1125
+ }
1126
+ }
1127
+
1096
1128
  // Handle defered conjuncts, e.g. `{x:1 @"foo"}`
1097
1129
  if (mo.___merge) {
1098
1130
  let mop = { ...mo }
@@ -1360,6 +1392,13 @@ help isolate the syntax error.`,
1360
1392
  v.path = [...(rule.k?.path ?? []),
1361
1393
  '' + rule.node.length, key]
1362
1394
  }
1395
+ // An element is a value position, so the prefix form applies.
1396
+ if (null == kr && isAliasDecl(ktkn, rule.o1)) {
1397
+ ; ((ctx as any).aontu_alias_hoist ||= []).push({ name: key, val: v })
1398
+ rule.node.push(v)
1399
+ return undefined
1400
+ }
1401
+
1363
1402
  const mv: any = addsite(
1364
1403
  new MapVal({ peg: { [key]: v } }), rule, ctx)
1365
1404
  // The element's path is the list's plus its index, as any
@@ -1370,12 +1409,6 @@ help isolate the syntax error.`,
1370
1409
  if (true === rule.u.aontu_optional_elem) {
1371
1410
  mv.optionalKeys = [key]
1372
1411
  }
1373
- // ... and a declaration is a declaration IN the element, which
1374
- // is where MapVal.unify refuses it: a list element is not the
1375
- // top level.
1376
- if (isAliasDecl(ktkn, rule.o1)) {
1377
- mv.aliasKeys = [key]
1378
- }
1379
1412
  rule.node.push(mv)
1380
1413
  }
1381
1414
 
@@ -1544,9 +1577,13 @@ function makeModelResolver(options: any) {
1544
1577
 
1545
1578
  const memCapability =
1546
1579
  'object' === typeof capability && null != (capability as any).mem
1580
+ // An EMPTY root denies: resolving it yields the process directory.
1581
+ const emptyRoot =
1582
+ 'object' === typeof capability && '' === (capability as any).root
1547
1583
  const rootDir: string | undefined =
1548
1584
  'object' === typeof capability &&
1549
- 'string' === typeof (capability as any).root
1585
+ 'string' === typeof (capability as any).root &&
1586
+ '' !== (capability as any).root
1550
1587
  ? pathResolve((capability as any).root) : undefined
1551
1588
 
1552
1589
  let memResolver = makeMemResolver(memCapability
@@ -1584,7 +1621,7 @@ function makeModelResolver(options: any) {
1584
1621
  const deny = (path: string): never => {
1585
1622
  // Only 'none' and 'root' can deny: the mem capability's misses are
1586
1623
  // not-found (its set is the whole world), so there is no third arm.
1587
- const capname = 'none' === capability ? 'none' : 'root:' + rootDir
1624
+ const capname = null == rootDir ? 'none' : 'root:' + rootDir
1588
1625
  const err: any = new Error(
1589
1626
  'include denied: ' + path + ' (capability: ' + capname + ')')
1590
1627
  err.code = 'include_denied'
@@ -1679,7 +1716,7 @@ function makeModelResolver(options: any) {
1679
1716
  return { found: false, path: '' + (path ?? ''), search: [] }
1680
1717
  }
1681
1718
 
1682
- if ('none' === capability) {
1719
+ if ('none' === capability || emptyRoot) {
1683
1720
  deny(path)
1684
1721
  }
1685
1722
 
package/src/mcp-server.ts CHANGED
@@ -33,7 +33,7 @@ export function parseArgs(argv: string[]): ServerArgs {
33
33
  }
34
34
  if ('--root' === arg) {
35
35
  const dir = argv[++i]
36
- if (null == dir) {
36
+ if (null == dir || '' === dir) {
37
37
  return { err: 'aontu-mcp: --root needs a directory' }
38
38
  }
39
39
  root = dir
package/src/mcp.ts CHANGED
@@ -653,7 +653,7 @@ function hashOf(
653
653
  const v: any = aontu.unify(
654
654
  src, null == path ? undefined : { path }, ctx)
655
655
  if (0 < ctx.err.length || null == v || true === v.isNil) {
656
- return { ok: false, hash: '', findings: [evalFailure(ctx)] }
656
+ return { ok: false, hash: '', findings: [evalFailure(ctx, v)] }
657
657
  }
658
658
  return {
659
659
  ok: true,
package/src/query.ts CHANGED
@@ -5,7 +5,7 @@ import { includeOpts } from './utility'
5
5
  import { Aontu } from './aontu'
6
6
  import type { TrustOptions } from './type'
7
7
  import { exactJSON } from './exactjson'
8
- import { anchorAt } from './vet'
8
+ import { anchorAt, engineFinding } from './vet'
9
9
  import type { VetFinding } from './vet'
10
10
  import { cmpCodePoint } from './keyorder'
11
11
  import { Provenance } from './provenance'
@@ -87,9 +87,6 @@ export function pathParts(path: string): string[] {
87
87
  }
88
88
 
89
89
 
90
- // The queried path, normalised the way anchorAt reads it — so a
91
- // finding names `$.a.b` whether the caller wrote that, `a.b` or
92
- // `$.a.b.` — and `$` for the root.
93
90
  function pathText(path: string): string {
94
91
  const parts = pathParts(path)
95
92
  return '$' + (0 < parts.length ? '.' + parts.join('.') : '')
@@ -170,9 +167,10 @@ function finding(
170
167
  }
171
168
 
172
169
 
173
- export function evalFailure(ctx: any): VetFinding {
174
- const err: any = ctx.err[0]
175
- return finding(err.why, '$', err.msg)
170
+ // A document that does not stand up, reported at the root: a literal
171
+ // nil adds nothing to `ctx.err`, so `failed` carries the only code.
172
+ export function evalFailure(ctx: any, failed?: any): VetFinding {
173
+ return engineFinding(ctx.err[0] ?? failed, ctx, '$')
176
174
  }
177
175
 
178
176
 
@@ -216,7 +214,7 @@ export function get(
216
214
  const root: any = aontu.unify(src, parseOpts, ctx)
217
215
 
218
216
  if (0 < ctx.err.length || null == root || true === root.isNil) {
219
- return { ok: false, out: '', findings: [evalFailure(ctx)] }
217
+ return { ok: false, out: '', findings: [evalFailure(ctx, root)] }
220
218
  }
221
219
 
222
220
  const node: any = anchorAt(root, path)
@@ -232,10 +230,7 @@ export function get(
232
230
  return {
233
231
  ok: false,
234
232
  out: '',
235
- findings: [finding(
236
- err?.why ?? 'no_gen',
237
- pathText(path),
238
- err?.msg ?? 'The value at this path is not concrete.')],
233
+ findings: [engineFinding(err, ctx, pathText(path))],
239
234
  }
240
235
  }
241
236
  return { ok: true, out: exactJSON(gen, 2), findings: [] }
@@ -274,7 +269,7 @@ export function why(
274
269
 
275
270
  const root: any = aontu.unify(parsed, parseOpts, ctx)
276
271
  if (0 < ctx.err.length || null == root || true === root.isNil) {
277
- return { ok: false, findings: [evalFailure(ctx)] }
272
+ return { ok: false, findings: [evalFailure(ctx, root)] }
278
273
  }
279
274
 
280
275
  const node: any = anchorAt(root, path)
package/src/tsconfig.json CHANGED
@@ -18,6 +18,7 @@
18
18
  "types": [
19
19
  "node"
20
20
  ],
21
- "composite": true
21
+ "composite": true,
22
+ "tsBuildInfoFile": "../.tsbuild/src.tsbuildinfo"
22
23
  }
23
24
  }
package/src/val/RefVal.ts CHANGED
@@ -316,16 +316,27 @@ class RefVal extends FeatureVal {
316
316
  refpath = this.path.slice(0, -1).concat(parts)
317
317
  }
318
318
 
319
- let sep = '.'
320
- refpath = refpath
321
- .reduce(((a: string[], p: string) =>
322
- (p === sep ? a.length = a.length - 1 : a.push(p), a)), [])
319
+ // A PARENT STEP OFF THE TOP names nothing, and underflows.
320
+ let offtop = false
321
+ const reduced: string[] = []
322
+ for (const part of refpath) {
323
+ if ('.' === part) {
324
+ if (0 === reduced.length) {
325
+ offtop = true
326
+ break
327
+ }
328
+ reduced.length = reduced.length - 1
329
+ continue
330
+ }
331
+ reduced.push(part)
332
+ }
333
+ refpath = reduced
323
334
 
324
335
  let node = ctx.root as Val
325
336
 
326
- let nopath = false
337
+ let nopath = offtop
327
338
 
328
- if (null != node) {
339
+ if (!offtop && null != node) {
329
340
  for (; pI < refpath.length; pI++) {
330
341
  let part = refpath[pI]
331
342
 
@@ -378,7 +389,7 @@ class RefVal extends FeatureVal {
378
389
  }
379
390
 
380
391
  const fixroot: any = (ctx as any)._fixroot
381
- if (this.absolute && null != fixroot
392
+ if (this.absolute && !offtop && null != fixroot
382
393
  && (nopath || pI !== refpath.length)) {
383
394
  nopath = false
384
395
  pI = 0
package/src/vet.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  } from 'node:path'
11
11
 
12
12
  import { descErr, getHint } from './err'
13
+ import { codeClass } from './hints'
13
14
  import { ConjunctVal } from './val/ConjunctVal'
14
15
  import { walkVals, collectNils } from './walk'
15
16
  import { sizingResidue } from './val/BagVal'
@@ -204,6 +205,9 @@ function materialise(nil: any, ctx: any): void {
204
205
  }
205
206
 
206
207
 
208
+ const UNSHAPED = 'unify_failed'
209
+
210
+
207
211
  // The terminal colour escapes the parser puts in its message text. A
208
212
  // RegExp built from a string, not a literal: the escape is a control
209
213
  // character, and spelling it `\u001b` keeps the source readable.
@@ -214,16 +218,10 @@ function stripAnsi(s: string): string {
214
218
  }
215
219
 
216
220
 
217
- function findingOf(nil: any, prov: Prov): VetFinding {
221
+ // The registry's hint and a code's own `details` fields: the same set
222
+ // on every finding reporting an ENGINE code.
223
+ function fromRegistry(finding: VetFinding, nil: any): VetFinding {
218
224
  const details = nil.details ?? {}
219
- const finding: VetFinding = {
220
- code: nil.why,
221
- class: nil.class,
222
- severity: 'error',
223
- path: pathText(nil.path),
224
- message: stripAnsi(nil.msg.split('\n')[0]),
225
- sites: sitesOf(nil, prov),
226
- }
227
225
 
228
226
  const hint = getHint(nil.why, nil.details)
229
227
  if (null != hint && '' !== hint) {
@@ -244,6 +242,18 @@ function findingOf(nil: any, prov: Prov): VetFinding {
244
242
  }
245
243
 
246
244
 
245
+ function findingOf(nil: any, prov: Prov): VetFinding {
246
+ return fromRegistry({
247
+ code: nil.why,
248
+ class: nil.class,
249
+ severity: 'error',
250
+ path: pathText(nil.path),
251
+ message: stripAnsi(nil.msg.split('\n')[0]),
252
+ sites: sitesOf(nil, prov),
253
+ }, nil)
254
+ }
255
+
256
+
247
257
  const ORDER_PAD = 9
248
258
 
249
259
 
@@ -265,6 +275,34 @@ function orderKey(f: VetFinding, index: number): string {
265
275
  }
266
276
 
267
277
 
278
+ // An ENGINE code at a path the caller names: class from the registry
279
+ // through NilVal.class, message materialised first (a `gen` nil has none).
280
+ export function engineFinding(
281
+ nil: any, ctx: any, path: string): VetFinding {
282
+ if (null == nil) {
283
+ return fromRegistry({
284
+ code: UNSHAPED,
285
+ class: codeClass(UNSHAPED),
286
+ severity: 'error',
287
+ path,
288
+ message: 'The document does not evaluate.',
289
+ sites: [],
290
+ }, { why: UNSHAPED })
291
+ }
292
+
293
+ materialise(nil, ctx)
294
+
295
+ return fromRegistry({
296
+ code: nil.why,
297
+ class: nil.class,
298
+ severity: 'error',
299
+ path,
300
+ message: stripAnsi(nil.msg.split('\n')[0]),
301
+ sites: [],
302
+ }, nil)
303
+ }
304
+
305
+
268
306
  export function failureFinding(
269
307
  ctx: any, url?: string, failed?: any): VetFinding {
270
308
  const nil: any = ctx.err[0] ?? failed
@@ -385,14 +423,62 @@ function coverDataPaths(
385
423
  }
386
424
 
387
425
 
426
+ // A spread or a key can carry a shape-preserving call or a reference,
427
+ // and coverage wants the bag under it; the seen set settles a cycle.
428
+ export function coverThrough(v: any, root: any): any {
429
+ let at = v
430
+ const seen = new Set<any>()
431
+ while (null != at && !seen.has(at)) {
432
+ if (true === at.isMap || true === at.isList) {
433
+ return at
434
+ }
435
+ seen.add(at)
436
+ if ((true === at.isCloseFunc || true === at.isTypeFunc) &&
437
+ Array.isArray(at.peg)) {
438
+ at = at.peg[0]
439
+ continue
440
+ }
441
+ // A relative reference reads from where it was WRITTEN, not where
442
+ // the template lands, so the engine's own resolution is the rule.
443
+ if (true === at.isRef) {
444
+ at = coverRefTarget(root, at.plainRefPath())
445
+ continue
446
+ }
447
+ if (true === at.isRecurse) {
448
+ at = coverRefTarget(root, at.target)
449
+ continue
450
+ }
451
+ return undefined
452
+ }
453
+ return undefined
454
+ }
455
+
456
+
457
+ // A reference descends a list by the segment AS WRITTEN: `$.Defs.0`.
458
+ function coverRefTarget(root: any, segs: string[] | undefined): any {
459
+ if (null == segs) {
460
+ return undefined
461
+ }
462
+ let at = root
463
+ for (const seg of segs) {
464
+ at = true === at?.isMap || true === at?.isList ? at.peg?.[seg] : undefined
465
+ }
466
+ return at
467
+ }
468
+
469
+
388
470
  // Walk one data path down the schema, naming the declaration that
389
471
  // constrains it -- the exact key where the schema has one, else the
390
472
  // covering template. Undefined when the schema declares nothing there.
391
473
  function coverMatch(
392
- anchor: any, segs: string[]): string | undefined {
474
+ anchor: any, segs: string[], root: any): string | undefined {
393
475
  let at: any = anchor
394
476
  let decl = ''
395
477
  for (const seg of segs) {
478
+ at = coverThrough(at, root)
479
+ if (null == at) {
480
+ return undefined
481
+ }
396
482
  const named = true === at.isMap && null != at.peg ? at.peg[seg]
397
483
  : true === at.isList && Array.isArray(at.peg) ? at.peg[Number(seg)]
398
484
  : undefined
@@ -432,7 +518,8 @@ function coverShallowest(paths: string[]): string[] {
432
518
  // The accounting itself: what the schema declared, what the data holds,
433
519
  // and which of each the other met.
434
520
  export function vetCoverage(
435
- anchor: any, dataVal: any, coverageAt?: string): VetCoverage {
521
+ anchor: any, dataVal: any, coverageAt: string | undefined,
522
+ root: any): VetCoverage {
436
523
  const declarations = new Map<string, any>()
437
524
  coverDeclare(anchor, [], declarations)
438
525
 
@@ -461,7 +548,7 @@ export function vetCoverage(
461
548
  continue
462
549
  }
463
550
  const segs = path.replace(/^\$\.?/, '').split('.').filter((x) => '' !== x)
464
- const decl = coverMatch(anchor, segs)
551
+ const decl = coverMatch(anchor, segs, root)
465
552
  if (leaf) {
466
553
  leaves++
467
554
  }
@@ -579,7 +666,7 @@ export function vet(
579
666
  // the same paths minus whatever an include would have added.
580
667
  const measured = 0 === coverCtx.err.length && true !== settledData?.isNil
581
668
  ? settledData : dataVal
582
- coverage = vetCoverage(anchor, measured, options.coverageAt)
669
+ coverage = vetCoverage(anchor, measured, options.coverageAt, schemaVal)
583
670
  }
584
671
 
585
672
  const lintFindings: VetFinding[] = []
package/src/view.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  import { basename, dirname, isAbsolute, relative, resolve } from 'node:path'
5
5
 
6
6
  import { Aontu } from './aontu'
7
- import { failureFinding, anchorAt, throughResidue } from './vet'
7
+ import { failureFinding, engineFinding, anchorAt, throughResidue } from './vet'
8
8
  import type { VetFinding } from './vet'
9
9
  import type { TrustOptions } from './type'
10
10
  import { graphOf } from './graph'
@@ -2544,10 +2544,8 @@ function drawLoaded(
2544
2544
  const before = ctx.err.length
2545
2545
  value = root.gen(ctx)
2546
2546
  if (before < ctx.err.length) {
2547
- const err: any = ctx.err[before]
2548
2547
  return {
2549
- errors: [finding(err?.why ?? 'unify_failed', 'reference', '$',
2550
- err?.msg ?? 'The document does not generate.')],
2548
+ errors: [engineFinding(ctx.err[before], ctx, '$')],
2551
2549
  }
2552
2550
  }
2553
2551
  }
@@ -2749,11 +2747,9 @@ export function viewSet(
2749
2747
  const before = ctx.err.length
2750
2748
  const value = root.gen(ctx)
2751
2749
  if (before < ctx.err.length) {
2752
- const err: any = ctx.err[before]
2753
2750
  return {
2754
2751
  verdict: 'error', views: [],
2755
- errors: [finding(err?.why ?? 'unify_failed', 'reference', '$',
2756
- err?.msg ?? 'The document does not generate.')],
2752
+ errors: [engineFinding(ctx.err[before], ctx, '$')],
2757
2753
  }
2758
2754
  }
2759
2755
  const declared = genAt(value, at)