aontu 0.63.0 → 0.64.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/README.md +6 -6
- package/dist/aontu.d.ts +3 -3
- package/dist/aontu.js +4 -6
- package/dist/aontu.js.map +1 -1
- package/dist/aontumodel.js +6 -12
- package/dist/aontumodel.js.map +1 -1
- package/dist/casing.d.ts +5 -0
- package/dist/casing.js +90 -0
- package/dist/casing.js.map +1 -0
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +147 -335
- package/dist/cli.js.map +1 -1
- package/dist/helpdoc.js +1 -1
- package/dist/helpdoc.js.map +1 -1
- package/dist/hints.js +3 -0
- package/dist/hints.js.map +1 -1
- package/dist/lsp.d.ts +1 -1
- package/dist/mcp.js +0 -36
- package/dist/mcp.js.map +1 -1
- package/dist/mod-tool.js +16 -1
- package/dist/mod-tool.js.map +1 -1
- package/dist/profile.d.ts +9 -0
- package/dist/profile.js +28 -0
- package/dist/profile.js.map +1 -0
- package/dist/trace.d.ts +21 -0
- package/dist/trace.js +107 -0
- package/dist/trace.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/unify.js +1 -6
- package/dist/unify.js.map +1 -1
- package/dist/val/CmpFuncVal.d.ts +2 -0
- package/dist/val/CmpFuncVal.js +47 -13
- package/dist/val/CmpFuncVal.js.map +1 -1
- package/dist/val/NomFuncVal.js +11 -13
- package/dist/val/NomFuncVal.js.map +1 -1
- package/dist/val/ReferFuncVal.js +3 -0
- package/dist/val/ReferFuncVal.js.map +1 -1
- package/package.json +1 -1
- package/skill/tasks.md +9 -7
- package/src/aontu.ts +3 -8
- package/src/aontumodel.ts +6 -12
- package/src/casing.ts +95 -0
- package/src/cli.ts +162 -359
- package/src/helpdoc.ts +1 -1
- package/src/hints.ts +3 -0
- package/src/mcp.ts +0 -39
- package/src/mod-tool.ts +18 -1
- package/src/profile.ts +42 -0
- package/src/trace.ts +157 -0
- package/src/unify.ts +1 -6
- package/src/val/CmpFuncVal.ts +66 -20
- package/src/val/NomFuncVal.ts +3 -5
- package/src/val/ReferFuncVal.ts +4 -0
- package/dist/lower.d.ts +0 -23
- package/dist/lower.js +0 -528
- package/dist/lower.js.map +0 -1
- package/dist/render.d.ts +0 -53
- package/dist/render.js +0 -432
- package/dist/render.js.map +0 -1
- package/src/lower.ts +0 -586
- package/src/render.ts +0 -580
package/src/cli.ts
CHANGED
|
@@ -17,16 +17,14 @@ import {
|
|
|
17
17
|
hcanon, canonHash,
|
|
18
18
|
get, why, patch, agentsMd,
|
|
19
19
|
allow,
|
|
20
|
-
|
|
21
|
-
renderProfile,
|
|
20
|
+
loadProfile,
|
|
22
21
|
} from './aontu'
|
|
23
22
|
import type { AllowDecision, AllowReport, AllowVerdict } from './allow'
|
|
24
|
-
import
|
|
23
|
+
import { traceRun } from './trace'
|
|
25
24
|
import {
|
|
26
25
|
desugarTemplate, resugarTemplate, templateOutputs, markerFor,
|
|
27
26
|
markerFromProfiles,
|
|
28
27
|
} from './template'
|
|
29
|
-
import { outsideRoot } from './mcp'
|
|
30
28
|
import { sarifReport } from './report-sarif'
|
|
31
29
|
import { main as lspMain } from './lsp-server'
|
|
32
30
|
import { main as mcpMain } from './mcp-server'
|
|
@@ -83,11 +81,10 @@ const HELP = `Usage: aontu [options] [file]
|
|
|
83
81
|
aontu view <kind> [options] <file>...
|
|
84
82
|
aontu view --views <path> [--check] [options] <file>
|
|
85
83
|
aontu jsonschema [--at <path>] [--strict] [options] <file>
|
|
86
|
-
aontu render [--at <path>] [--profile <file>]... [--unit <path>]
|
|
87
|
-
[--stdout | --out <dir> | --check <dir> | --coverage]
|
|
88
|
-
[--coverage-at <path>] [--strict] <file>
|
|
89
84
|
aontu template [--resugar] [--check] [--marker <token>]
|
|
90
85
|
[--profile <file>] <file>
|
|
86
|
+
aontu trace [--at <path>] [--format json] [--marker <token>]
|
|
87
|
+
[--profile <file>] <file>
|
|
91
88
|
aontu hash [options] <file>
|
|
92
89
|
aontu mod tidy|verify|vendor|manifest [options] [dir]
|
|
93
90
|
aontu get <path> [options] <file>
|
|
@@ -333,37 +330,11 @@ View exit codes: 0 rendered, 1 --check mismatch or lossy under
|
|
|
333
330
|
--strict, 2 usage or --max-rows exceeded, 4 the document does not stand
|
|
334
331
|
up on its own, or a relation, root or path that names nothing.
|
|
335
332
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
one per language
|
|
342
|
-
--unit <path> Render only the unit with this path
|
|
343
|
-
--stdout One unit's bytes and nothing else (with --unit when
|
|
344
|
-
the instance has several)
|
|
345
|
-
--out <dir> Write every unit below dir, or nothing; never deletes
|
|
346
|
-
--check <dir> Compare every unit with dir/<path>; drift is listed
|
|
347
|
-
--coverage Report what the render read and what it did not:
|
|
348
|
-
model paths no output consumed, and rendered
|
|
349
|
-
declarations no rule produced. Writes nothing
|
|
350
|
-
--coverage-at <p> Measure coverage under this path only, instead of
|
|
351
|
-
the document root
|
|
352
|
-
--strict Refuse the opaque escapes (a text declaration, a raw
|
|
353
|
-
block)
|
|
354
|
-
--format <f> text (default) or json, the whole report; json
|
|
355
|
-
carries the dispatch trace, one entry per emitted
|
|
356
|
-
piece
|
|
357
|
-
|
|
358
|
-
Render exit codes: 0 rendered, 1 lossy under --strict or drift under
|
|
359
|
-
--check, 2 usage or I/O (a refused unit path included), 4 the document
|
|
360
|
-
does not stand up or the instance is not aontu:code.
|
|
361
|
-
|
|
362
|
-
A render entry file whose extension is not .aon is a TEMPLATE: a
|
|
363
|
-
generator in the target's own syntax, whose marker lines carry aontu
|
|
364
|
-
and whose other lines are output. It is desugared before it is
|
|
365
|
-
evaluated, and a language the table does not know names its marker with
|
|
366
|
-
--marker, or declares it once in a profile file that --profile reads.
|
|
333
|
+
A template entry file whose extension is not .aon is a GENERATOR: a
|
|
334
|
+
document in the target's own syntax, whose marker lines carry aontu and
|
|
335
|
+
whose other lines are output. It is desugared before it is evaluated,
|
|
336
|
+
and a language the table does not know names its marker with --marker,
|
|
337
|
+
or declares it once in a profile file that --profile reads.
|
|
367
338
|
|
|
368
339
|
Template options:
|
|
369
340
|
--resugar The file is the canonical aontu; print the template
|
|
@@ -473,11 +444,11 @@ Fmt options:
|
|
|
473
444
|
The fmt verb prints one document in the agreed form; with no file it
|
|
474
445
|
reads standard input. Several files need one of the options above.
|
|
475
446
|
|
|
476
|
-
A file whose extension is not .aon is a GENERATOR, as it is for
|
|
477
|
-
the aontu its marker lines carry is formatted, the marker
|
|
478
|
-
left margin with the aontu indented after it, and every
|
|
479
|
-
is held on a line of its own. A file with no marker line
|
|
480
|
-
another language's, and is refused.
|
|
447
|
+
A file whose extension is not .aon is a GENERATOR, as it is for
|
|
448
|
+
template: the aontu its marker lines carry is formatted, the marker
|
|
449
|
+
stands at the left margin with the aontu indented after it, and every
|
|
450
|
+
line of output is held on a line of its own. A file with no marker line
|
|
451
|
+
in it is another language's, and is refused.
|
|
481
452
|
|
|
482
453
|
Fmt exit codes: 0 formatted or clean, 1 a --check file would change or
|
|
483
454
|
a --strict finding, 2 usage, 4 a document does not parse.
|
|
@@ -2080,6 +2051,12 @@ const VIEW_USAGE_CODES = [
|
|
|
2080
2051
|
const MOD_HELP = 'aontu mod tidy|verify|vendor|manifest [dir] (try --help)'
|
|
2081
2052
|
|
|
2082
2053
|
function runMod(argv: string[]): number {
|
|
2054
|
+
const trusted = takeTrust(argv)
|
|
2055
|
+
if (null == trusted) {
|
|
2056
|
+
return 2
|
|
2057
|
+
}
|
|
2058
|
+
argv = trusted.argv
|
|
2059
|
+
const trust = trusted.trust
|
|
2083
2060
|
const rest: string[] = []
|
|
2084
2061
|
let format: SubsumeFormat = 'text'
|
|
2085
2062
|
let against: string | undefined
|
|
@@ -2145,11 +2122,13 @@ function runMod(argv: string[]): number {
|
|
|
2145
2122
|
return 2
|
|
2146
2123
|
}
|
|
2147
2124
|
|
|
2125
|
+
const modopts = modToolOptions(trust, resolve(dir))
|
|
2126
|
+
|
|
2148
2127
|
const report =
|
|
2149
|
-
'tidy' === sub ? modTidy(dir,
|
|
2150
|
-
'verify' === sub ? modVerify(dir,
|
|
2151
|
-
'vendor' === sub ? modVendor(dir,
|
|
2152
|
-
modManifest(dir,
|
|
2128
|
+
'tidy' === sub ? modTidy(dir, modopts) :
|
|
2129
|
+
'verify' === sub ? modVerify(dir, modopts) :
|
|
2130
|
+
'vendor' === sub ? modVendor(dir, modopts) :
|
|
2131
|
+
modManifest(dir, modopts, against)
|
|
2153
2132
|
|
|
2154
2133
|
process.stdout.write(('json' === format ?
|
|
2155
2134
|
exactJSON({ aontu: { version: version(), verb: 'mod ' + sub }, ...report },
|
|
@@ -2188,11 +2167,16 @@ const MOD_EXIT: Record<ModVerdict, number> = {
|
|
|
2188
2167
|
// The tooling's evaluator: the same standalone evaluation the module
|
|
2189
2168
|
// resolver verifies with (ts/src/mod.ts), and for the same reason —
|
|
2190
2169
|
// only the engine can say what a module MEANS.
|
|
2191
|
-
function modToolOptions() {
|
|
2170
|
+
function modToolOptions(trust: TrustArg, entryRoot: string) {
|
|
2171
|
+
const opts = verbOpts(trust, entryRoot)
|
|
2172
|
+
// The user cache lives outside any confinement root, so a confined
|
|
2173
|
+
// run reads the vendor tree only -- as the evaluator's own module
|
|
2174
|
+
// leg already does when a root is set.
|
|
2175
|
+
const rooted = null != (opts.trust as any)?.include?.root
|
|
2192
2176
|
return {
|
|
2193
|
-
cache: modCacheDir(),
|
|
2177
|
+
...(rooted ? {} : { cache: modCacheDir() }),
|
|
2194
2178
|
eval: (src: string, path: string) => {
|
|
2195
|
-
const a0 = new Aontu()
|
|
2179
|
+
const a0 = new Aontu(opts)
|
|
2196
2180
|
const ctx = a0.ctx({ collect: true })
|
|
2197
2181
|
const val: any = a0.unify(src, { path }, ctx)
|
|
2198
2182
|
return {
|
|
@@ -2341,6 +2325,120 @@ function runRelations(argv: string[]): number {
|
|
|
2341
2325
|
return RELATIONS_EXIT[report.verdict]
|
|
2342
2326
|
}
|
|
2343
2327
|
|
|
2328
|
+
const TRACE_HELP =
|
|
2329
|
+
'aontu trace [--at <path>] [--format json] [--marker <token>] ' +
|
|
2330
|
+
'[--profile <file>] <file>'
|
|
2331
|
+
|
|
2332
|
+
|
|
2333
|
+
// WHAT WROTE THIS LINE. Every piece a rule stamped, under the
|
|
2334
|
+
// component tree, with the file it reached, the rule set that wrote it
|
|
2335
|
+
// and the model node the dispatch matched.
|
|
2336
|
+
function runTrace(argv: string[]): number {
|
|
2337
|
+
const trusted = takeTrust(argv)
|
|
2338
|
+
if (null == trusted) {
|
|
2339
|
+
return 2
|
|
2340
|
+
}
|
|
2341
|
+
argv = trusted.argv
|
|
2342
|
+
const trust = trusted.trust
|
|
2343
|
+
const rest: string[] = []
|
|
2344
|
+
const profileFiles: string[] = []
|
|
2345
|
+
let format: 'text' | 'json' = 'text'
|
|
2346
|
+
let at: string | undefined = undefined
|
|
2347
|
+
let marker: string | undefined = undefined
|
|
2348
|
+
|
|
2349
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2350
|
+
const arg = argv[i]
|
|
2351
|
+
if ('-h' === arg || '--help' === arg) {
|
|
2352
|
+
process.stdout.write(HELP)
|
|
2353
|
+
return 0
|
|
2354
|
+
}
|
|
2355
|
+
if ('--format' === arg) {
|
|
2356
|
+
const f = argv[++i]
|
|
2357
|
+
if ('text' !== f && 'json' !== f) {
|
|
2358
|
+
process.stderr.write('aontu: --format needs text or json\n')
|
|
2359
|
+
return 2
|
|
2360
|
+
}
|
|
2361
|
+
format = f
|
|
2362
|
+
}
|
|
2363
|
+
else if ('--at' === arg) {
|
|
2364
|
+
at = argv[++i]
|
|
2365
|
+
if (null == at) {
|
|
2366
|
+
process.stderr.write('aontu: --at needs a path\n')
|
|
2367
|
+
return 2
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
else if ('--marker' === arg) {
|
|
2371
|
+
marker = argv[++i]
|
|
2372
|
+
if (null == marker) {
|
|
2373
|
+
process.stderr.write('aontu: --marker needs a token\n')
|
|
2374
|
+
return 2
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
else if ('--profile' === arg) {
|
|
2378
|
+
const pf = argv[++i]
|
|
2379
|
+
if (null == pf) {
|
|
2380
|
+
process.stderr.write('aontu: --profile needs a file\n')
|
|
2381
|
+
return 2
|
|
2382
|
+
}
|
|
2383
|
+
profileFiles.push(pf)
|
|
2384
|
+
}
|
|
2385
|
+
else if (arg.startsWith('-')) {
|
|
2386
|
+
process.stderr.write(`aontu: unknown trace option ${arg} (try --help)\n`)
|
|
2387
|
+
return 2
|
|
2388
|
+
}
|
|
2389
|
+
else {
|
|
2390
|
+
rest.push(arg)
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
if (1 !== rest.length) {
|
|
2395
|
+
process.stderr.write(`aontu: trace needs one file\n${TRACE_HELP}\n`)
|
|
2396
|
+
return 2
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
let src: string
|
|
2400
|
+
try {
|
|
2401
|
+
src = readFileSync(rest[0], 'utf8')
|
|
2402
|
+
}
|
|
2403
|
+
catch (err: any) {
|
|
2404
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
|
|
2405
|
+
return 2
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
const declared = loadProfiles(profileFiles, trust)
|
|
2409
|
+
if ('number' === typeof declared) {
|
|
2410
|
+
return declared
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// A GENERATOR IS AN ENTRY, not a preprocessing step: the file whose
|
|
2414
|
+
// provenance is asked for is the one the author edits.
|
|
2415
|
+
if (!rest[0].endsWith('.aon')) {
|
|
2416
|
+
src = desugarTemplate(src, marker ??
|
|
2417
|
+
markerFromProfiles(declared, rest[0]) ?? markerFor(rest[0]))
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
const report = traceRun(src, {
|
|
2421
|
+
path: rest[0], at,
|
|
2422
|
+
...verbOpts(trust, entryRootOf(rest[0])),
|
|
2423
|
+
})
|
|
2424
|
+
if ('error' === report.verdict) {
|
|
2425
|
+
// An error report always carries its findings.
|
|
2426
|
+
const errors = report.errors as VetFinding[]
|
|
2427
|
+
process.stderr.write(errors.map(renderFinding).join('\n') + '\n')
|
|
2428
|
+
return 4
|
|
2429
|
+
}
|
|
2430
|
+
if ('json' === format) {
|
|
2431
|
+
process.stdout.write(JSON.stringify({ trace: report.trace }) + '\n')
|
|
2432
|
+
return 0
|
|
2433
|
+
}
|
|
2434
|
+
for (const e of report.trace) {
|
|
2435
|
+
process.stdout.write(
|
|
2436
|
+
[e.file, e.at, e.node, e.rule].join('\t') + '\n')
|
|
2437
|
+
}
|
|
2438
|
+
return 0
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
|
|
2344
2442
|
function runReaches(argv: string[]): number {
|
|
2345
2443
|
const trusted = takeTrust(argv)
|
|
2346
2444
|
if (null == trusted) {
|
|
@@ -2955,302 +3053,6 @@ function runJsonSchema(argv: string[]): number {
|
|
|
2955
3053
|
}
|
|
2956
3054
|
|
|
2957
3055
|
|
|
2958
|
-
const RENDER_HELP =
|
|
2959
|
-
'aontu render [--at <path>] [--profile <file>]... [--unit <path>] ' +
|
|
2960
|
-
'[--stdout | --out <dir> | --check <dir> | --coverage] ' +
|
|
2961
|
-
'[--coverage-at <path>] [--strict] [--marker <token>] <file> (try --help)'
|
|
2962
|
-
|
|
2963
|
-
function runRender(argv: string[]): number {
|
|
2964
|
-
const trusted = takeTrust(argv)
|
|
2965
|
-
if (null == trusted) {
|
|
2966
|
-
return 2
|
|
2967
|
-
}
|
|
2968
|
-
argv = trusted.argv
|
|
2969
|
-
const trust = trusted.trust
|
|
2970
|
-
const files: string[] = []
|
|
2971
|
-
const profileFiles: string[] = []
|
|
2972
|
-
let format: SubsumeFormat = 'text'
|
|
2973
|
-
let at: string | undefined = undefined
|
|
2974
|
-
let unit: string | undefined = undefined
|
|
2975
|
-
let out: string | undefined = undefined
|
|
2976
|
-
let check: string | undefined = undefined
|
|
2977
|
-
let toStdout = false
|
|
2978
|
-
let strict = false
|
|
2979
|
-
let coverage = false
|
|
2980
|
-
let coverageAt: string | undefined = undefined
|
|
2981
|
-
let marker: string | undefined = undefined
|
|
2982
|
-
|
|
2983
|
-
for (let i = 0; i < argv.length; i++) {
|
|
2984
|
-
const arg = argv[i]
|
|
2985
|
-
if ('-h' === arg || '--help' === arg) {
|
|
2986
|
-
process.stdout.write(HELP)
|
|
2987
|
-
return 0
|
|
2988
|
-
}
|
|
2989
|
-
if ('--format' === arg) {
|
|
2990
|
-
const f = argv[++i]
|
|
2991
|
-
if ('text' !== f && 'json' !== f) {
|
|
2992
|
-
process.stderr.write('aontu: --format needs text or json\n')
|
|
2993
|
-
return 2
|
|
2994
|
-
}
|
|
2995
|
-
format = f
|
|
2996
|
-
}
|
|
2997
|
-
else if ('--at' === arg) {
|
|
2998
|
-
at = argv[++i]
|
|
2999
|
-
if (null == at) {
|
|
3000
|
-
process.stderr.write('aontu: --at needs a path\n')
|
|
3001
|
-
return 2
|
|
3002
|
-
}
|
|
3003
|
-
}
|
|
3004
|
-
else if ('--unit' === arg) {
|
|
3005
|
-
unit = argv[++i]
|
|
3006
|
-
if (null == unit) {
|
|
3007
|
-
process.stderr.write('aontu: --unit needs a unit path\n')
|
|
3008
|
-
return 2
|
|
3009
|
-
}
|
|
3010
|
-
}
|
|
3011
|
-
else if ('--profile' === arg) {
|
|
3012
|
-
const pf = argv[++i]
|
|
3013
|
-
if (null == pf) {
|
|
3014
|
-
process.stderr.write('aontu: --profile needs a file\n')
|
|
3015
|
-
return 2
|
|
3016
|
-
}
|
|
3017
|
-
profileFiles.push(pf)
|
|
3018
|
-
}
|
|
3019
|
-
else if ('--out' === arg) {
|
|
3020
|
-
out = argv[++i]
|
|
3021
|
-
if (null == out) {
|
|
3022
|
-
process.stderr.write('aontu: --out needs a directory\n')
|
|
3023
|
-
return 2
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
else if ('--check' === arg) {
|
|
3027
|
-
check = argv[++i]
|
|
3028
|
-
if (null == check) {
|
|
3029
|
-
process.stderr.write('aontu: --check needs a directory\n')
|
|
3030
|
-
return 2
|
|
3031
|
-
}
|
|
3032
|
-
}
|
|
3033
|
-
else if ('--stdout' === arg) {
|
|
3034
|
-
toStdout = true
|
|
3035
|
-
}
|
|
3036
|
-
else if ('--coverage' === arg) {
|
|
3037
|
-
coverage = true
|
|
3038
|
-
}
|
|
3039
|
-
else if ('--marker' === arg) {
|
|
3040
|
-
marker = argv[++i]
|
|
3041
|
-
if (null == marker) {
|
|
3042
|
-
process.stderr.write('aontu: --marker needs a token\n')
|
|
3043
|
-
return 2
|
|
3044
|
-
}
|
|
3045
|
-
}
|
|
3046
|
-
else if ('--coverage-at' === arg) {
|
|
3047
|
-
coverageAt = argv[++i]
|
|
3048
|
-
if (null == coverageAt) {
|
|
3049
|
-
process.stderr.write('aontu: --coverage-at needs a path\n')
|
|
3050
|
-
return 2
|
|
3051
|
-
}
|
|
3052
|
-
}
|
|
3053
|
-
else if ('--strict' === arg) {
|
|
3054
|
-
strict = true
|
|
3055
|
-
}
|
|
3056
|
-
else if (arg.startsWith('-')) {
|
|
3057
|
-
process.stderr.write(
|
|
3058
|
-
`aontu: unknown render option ${arg} (try --help)\n`)
|
|
3059
|
-
return 2
|
|
3060
|
-
}
|
|
3061
|
-
else {
|
|
3062
|
-
files.push(arg)
|
|
3063
|
-
}
|
|
3064
|
-
}
|
|
3065
|
-
|
|
3066
|
-
if (1 !== files.length) {
|
|
3067
|
-
process.stderr.write(`aontu: render needs one file\n${RENDER_HELP}\n`)
|
|
3068
|
-
return 2
|
|
3069
|
-
}
|
|
3070
|
-
const modes = [toStdout, undefined !== out, undefined !== check, coverage]
|
|
3071
|
-
.filter((on) => on).length
|
|
3072
|
-
if (1 < modes) {
|
|
3073
|
-
process.stderr.write(
|
|
3074
|
-
'aontu: render takes one of --stdout, --out, --check or --coverage\n')
|
|
3075
|
-
return 2
|
|
3076
|
-
}
|
|
3077
|
-
// A NARROWER MEASURE NEEDS SOMETHING TO NARROW. `--coverage-at`
|
|
3078
|
-
// without `--coverage` asks for a report the run does not compute,
|
|
3079
|
-
// and answering silently would be the wrong half of the request.
|
|
3080
|
-
if (undefined !== coverageAt && !coverage) {
|
|
3081
|
-
process.stderr.write('aontu: --coverage-at needs --coverage\n')
|
|
3082
|
-
return 2
|
|
3083
|
-
}
|
|
3084
|
-
|
|
3085
|
-
let src: string
|
|
3086
|
-
try {
|
|
3087
|
-
src = readFileSync(files[0], 'utf8')
|
|
3088
|
-
}
|
|
3089
|
-
catch (err: any) {
|
|
3090
|
-
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
|
|
3091
|
-
return 2
|
|
3092
|
-
}
|
|
3093
|
-
|
|
3094
|
-
const loadedProfiles = loadProfiles(profileFiles, trust)
|
|
3095
|
-
if ('number' === typeof loadedProfiles) {
|
|
3096
|
-
return loadedProfiles
|
|
3097
|
-
}
|
|
3098
|
-
const profiles = loadedProfiles
|
|
3099
|
-
|
|
3100
|
-
if (!files[0].endsWith('.aon')) {
|
|
3101
|
-
src = desugarTemplate(src, marker ??
|
|
3102
|
-
markerFromProfiles(profiles, files[0]) ?? markerFor(files[0]))
|
|
3103
|
-
}
|
|
3104
|
-
|
|
3105
|
-
// A RENDER WITH NO PROFILE PRODUCES NO UNITS, and said so with zero
|
|
3106
|
-
// bytes and exit 0. The profile is what maps a model onto a
|
|
3107
|
-
// language, so without one there is nothing for the renderer to
|
|
3108
|
-
// write -- which is a usable answer only if the caller is told.
|
|
3109
|
-
const noProfiles = 0 === profiles.length
|
|
3110
|
-
const report = render(src, {
|
|
3111
|
-
at, unit, strict, profiles, path: files[0],
|
|
3112
|
-
coverage, coverageAt,
|
|
3113
|
-
// THE JSON REPORT CARRIES THE TRACE (D9), which is what the shape
|
|
3114
|
-
// there has always said; a text run computes it only when the
|
|
3115
|
-
// coverage report needs it.
|
|
3116
|
-
trace: 'json' === format,
|
|
3117
|
-
...verbOpts(trust, entryRootOf(files[0])),
|
|
3118
|
-
})
|
|
3119
|
-
|
|
3120
|
-
// Said once, whatever the format: stdout stays the report.
|
|
3121
|
-
if ('error' !== report.verdict && 0 === report.units.length) {
|
|
3122
|
-
vacuous('nothing was rendered',
|
|
3123
|
-
noProfiles
|
|
3124
|
-
? 'no profile was given, and the document declares none' +
|
|
3125
|
-
' (see aontu help tasks)'
|
|
3126
|
-
: 'the document produced no units under this profile')
|
|
3127
|
-
}
|
|
3128
|
-
|
|
3129
|
-
if ('json' === format) {
|
|
3130
|
-
process.stdout.write(exactJSON({
|
|
3131
|
-
aontu: { version: version(), verb: 'render' },
|
|
3132
|
-
verdict: report.verdict,
|
|
3133
|
-
units: report.units,
|
|
3134
|
-
lossy: report.lossy,
|
|
3135
|
-
...(null == report.errors ? {} : { errors: report.errors }),
|
|
3136
|
-
...(null == report.trace ? {} : { trace: report.trace }),
|
|
3137
|
-
...(null == report.coverage ? {} : { coverage: report.coverage }),
|
|
3138
|
-
}, 2) + '\n')
|
|
3139
|
-
return renderExit(report, 0)
|
|
3140
|
-
}
|
|
3141
|
-
if ('error' === report.verdict) {
|
|
3142
|
-
process.stderr.write(
|
|
3143
|
-
(report.errors as VetFinding[]).map(renderFinding).join('\n') + '\n')
|
|
3144
|
-
return renderExit(report, 0)
|
|
3145
|
-
}
|
|
3146
|
-
|
|
3147
|
-
let drift = 0
|
|
3148
|
-
if (toStdout) {
|
|
3149
|
-
// ONE UNIT'S BYTES AND NOTHING ELSE, so the output can be piped
|
|
3150
|
-
// into a formatter or a file.
|
|
3151
|
-
if (1 !== report.units.length) {
|
|
3152
|
-
process.stderr.write(
|
|
3153
|
-
'aontu: --stdout needs exactly one unit, and the instance has ' +
|
|
3154
|
-
`${report.units.length}; --unit names one\n`)
|
|
3155
|
-
return 2
|
|
3156
|
-
}
|
|
3157
|
-
process.stdout.write(report.units[0].text)
|
|
3158
|
-
}
|
|
3159
|
-
else if (undefined !== out) {
|
|
3160
|
-
// EVERY UNIT BELOW <dir>, OR NOTHING: every unit rendered first
|
|
3161
|
-
// (the report above), and no file touched unless all did. The
|
|
3162
|
-
// directory is realpath-confined; a unit path is already a relative
|
|
3163
|
-
// descent (render_path refuses the rest), and the check here is
|
|
3164
|
-
// against the symlink inside it. render never deletes.
|
|
3165
|
-
for (const u of report.units) {
|
|
3166
|
-
if (outsideRoot(out, resolve(out, u.path))) {
|
|
3167
|
-
process.stderr.write(`aontu: ${u.path} escapes ${out}\n`)
|
|
3168
|
-
return 2
|
|
3169
|
-
}
|
|
3170
|
-
}
|
|
3171
|
-
for (const u of report.units) {
|
|
3172
|
-
const full = resolve(out, u.path)
|
|
3173
|
-
try {
|
|
3174
|
-
mkdirSync(dirname(full), { recursive: true })
|
|
3175
|
-
writeFileSync(full, u.text, 'utf8')
|
|
3176
|
-
}
|
|
3177
|
-
catch (err: any) {
|
|
3178
|
-
process.stderr.write(`aontu: cannot write ${u.path}: ${err.message}\n`)
|
|
3179
|
-
return 2
|
|
3180
|
-
}
|
|
3181
|
-
process.stderr.write(`wrote ${u.path}\n`)
|
|
3182
|
-
}
|
|
3183
|
-
}
|
|
3184
|
-
else if (undefined !== check) {
|
|
3185
|
-
// RENDER AND COMPARE: a unit whose bytes differ from the file at
|
|
3186
|
-
// <dir>/<path>, or whose file is absent, is drift, listed by path.
|
|
3187
|
-
// The CI form.
|
|
3188
|
-
for (const u of report.units) {
|
|
3189
|
-
let have: string | undefined = undefined
|
|
3190
|
-
try {
|
|
3191
|
-
have = readFileSync(resolve(check, u.path), 'utf8')
|
|
3192
|
-
}
|
|
3193
|
-
catch {
|
|
3194
|
-
// Absent is drift, reported below.
|
|
3195
|
-
}
|
|
3196
|
-
if (undefined === have) {
|
|
3197
|
-
drift++
|
|
3198
|
-
process.stderr.write(`aontu: ${u.path} is missing from ${check}\n`)
|
|
3199
|
-
}
|
|
3200
|
-
else if (have !== u.text) {
|
|
3201
|
-
drift++
|
|
3202
|
-
process.stderr.write(`aontu: ${u.path} differs from the rendered unit\n`)
|
|
3203
|
-
}
|
|
3204
|
-
}
|
|
3205
|
-
}
|
|
3206
|
-
else if (coverage) {
|
|
3207
|
-
// THE COVERAGE REPORT (P7), one line per finding and a count at
|
|
3208
|
-
// the end: dead model first, then the declarations no rule
|
|
3209
|
-
// produced. A clean report is the count line alone.
|
|
3210
|
-
const cov = report.coverage as RenderCoverage
|
|
3211
|
-
for (const d of cov.dead) {
|
|
3212
|
-
process.stdout.write(`dead: ${d}\n`)
|
|
3213
|
-
}
|
|
3214
|
-
for (const u of cov.unruled) {
|
|
3215
|
-
process.stdout.write(`unruled: ${u.unit} ${u.path}\n`)
|
|
3216
|
-
}
|
|
3217
|
-
process.stdout.write(
|
|
3218
|
-
`coverage: ${cov.read.length} path(s) read, ${cov.dead.length} ` +
|
|
3219
|
-
`no output consumed, ${cov.unruled.length} declaration(s) ` +
|
|
3220
|
-
'no rule produced\n')
|
|
3221
|
-
}
|
|
3222
|
-
else {
|
|
3223
|
-
// THE SUMMARY: one line per unit -- its path, its language and its
|
|
3224
|
-
// size -- since several units have no one text to print.
|
|
3225
|
-
for (const u of report.units) {
|
|
3226
|
-
process.stdout.write(`${u.path}\t${u.lang}\t${u.text.length} bytes\n`)
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
3229
|
-
for (const l of report.lossy) {
|
|
3230
|
-
process.stderr.write(
|
|
3231
|
-
`lossy: ${l.unit} ${l.path} tier ${l.tier} ${l.construct}: ${l.reason}\n`)
|
|
3232
|
-
}
|
|
3233
|
-
return renderExit(report, drift)
|
|
3234
|
-
}
|
|
3235
|
-
|
|
3236
|
-
// D8's exit table over a report: a refused unit path is usage (2), a
|
|
3237
|
-
// strict refusal is lossy (1), any other error is the document's (4);
|
|
3238
|
-
// drift under --check is 1.
|
|
3239
|
-
function renderExit(report: RenderReport, drift: number): number {
|
|
3240
|
-
if ('error' === report.verdict) {
|
|
3241
|
-
const errors = report.errors as VetFinding[]
|
|
3242
|
-
if (errors.every((f) => 'render_path' === f.code)) {
|
|
3243
|
-
return 2
|
|
3244
|
-
}
|
|
3245
|
-
if (errors.every((f) => 'render_strict' === f.code)) {
|
|
3246
|
-
return 1
|
|
3247
|
-
}
|
|
3248
|
-
return 4
|
|
3249
|
-
}
|
|
3250
|
-
return 0 < drift ? 1 : 0
|
|
3251
|
-
}
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
3056
|
const TEMPLATE_HELP =
|
|
3255
3057
|
'aontu template [--resugar] [--check] [--marker <token>] <file> (try --help)'
|
|
3256
3058
|
|
|
@@ -3356,9 +3158,9 @@ function runTemplate(argv: string[]): number {
|
|
|
3356
3158
|
|
|
3357
3159
|
|
|
3358
3160
|
// The profiles named by --profile, vetted, or the exit code that says
|
|
3359
|
-
// why not. A profile is a language declared as data: `
|
|
3360
|
-
// one to a
|
|
3361
|
-
//
|
|
3161
|
+
// why not. A profile is a language declared as data: `template` and
|
|
3162
|
+
// `fmt` match one to a file by the extensions its `template.ext`
|
|
3163
|
+
// names.
|
|
3362
3164
|
function loadProfiles(
|
|
3363
3165
|
profileFiles: string[], trust: TrustArg
|
|
3364
3166
|
): any[] | number {
|
|
@@ -3373,7 +3175,7 @@ function loadProfiles(
|
|
|
3373
3175
|
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
|
|
3374
3176
|
return 2
|
|
3375
3177
|
}
|
|
3376
|
-
const loaded =
|
|
3178
|
+
const loaded = loadProfile(text,
|
|
3377
3179
|
{ path: resolve(pf), ...verbOpts(trust, entryRootOf(pf)) })
|
|
3378
3180
|
if (undefined !== loaded.errors) {
|
|
3379
3181
|
process.stderr.write(loaded.errors.map(renderFinding).join('\n') + '\n')
|
|
@@ -4138,8 +3940,8 @@ function runFmt(argv: string[]): number | Promise<number> {
|
|
|
4138
3940
|
}
|
|
4139
3941
|
else if ('--marker' === arg) {
|
|
4140
3942
|
// THE MARKER SAYS THE FILE IS A GENERATOR, whatever its
|
|
4141
|
-
// extension: `
|
|
4142
|
-
//
|
|
3943
|
+
// extension: `fmt` and `template` take the same option for the
|
|
3944
|
+
// same reason, a language the table has never seen.
|
|
4143
3945
|
marker = argv[++i]
|
|
4144
3946
|
if (null == marker) {
|
|
4145
3947
|
process.stderr.write('aontu: --marker needs a token\n')
|
|
@@ -4616,8 +4418,8 @@ function runInit(argv: string[]): number {
|
|
|
4616
4418
|
const KNOWN_VERBS = [
|
|
4617
4419
|
'agentsmd', 'allow', 'breaking', 'explain', 'fmt', 'get', 'hash',
|
|
4618
4420
|
'help', 'init', 'jsonschema', 'lsp', 'mcp', 'mod', 'reaches',
|
|
4619
|
-
'relations', '
|
|
4620
|
-
'view', 'why',
|
|
4421
|
+
'relations', 'set', 'subsume', 'template', 'trace', 'trim',
|
|
4422
|
+
'vet', 'view', 'why',
|
|
4621
4423
|
]
|
|
4622
4424
|
|
|
4623
4425
|
|
|
@@ -4758,14 +4560,15 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
4758
4560
|
return finish(runJsonSchema(argv.slice(3)))
|
|
4759
4561
|
}
|
|
4760
4562
|
|
|
4761
|
-
if ('render' === argv[2]) {
|
|
4762
|
-
return finish(runRender(argv.slice(3)))
|
|
4763
|
-
}
|
|
4764
4563
|
|
|
4765
4564
|
if ('template' === argv[2]) {
|
|
4766
4565
|
return finish(runTemplate(argv.slice(3)))
|
|
4767
4566
|
}
|
|
4768
4567
|
|
|
4568
|
+
if ('trace' === argv[2]) {
|
|
4569
|
+
return finish(runTrace(argv.slice(3)))
|
|
4570
|
+
}
|
|
4571
|
+
|
|
4769
4572
|
if ('reaches' === argv[2]) {
|
|
4770
4573
|
return finish(runReaches(argv.slice(3)))
|
|
4771
4574
|
}
|
|
@@ -4869,7 +4672,7 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
4869
4672
|
else {
|
|
4870
4673
|
runStdin(mode, format, trust).then((code) => finish(code))
|
|
4871
4674
|
}
|
|
4872
|
-
} /* node:coverage ignore next
|
|
4675
|
+
} /* node:coverage ignore next 21 */
|
|
4873
4676
|
|
|
4874
4677
|
|
|
4875
4678
|
// No require.main guard here: bin/aontu.js is the executable entry and
|
|
@@ -4881,8 +4684,8 @@ export {
|
|
|
4881
4684
|
runReaches,
|
|
4882
4685
|
runView,
|
|
4883
4686
|
runJsonSchema,
|
|
4884
|
-
runRender,
|
|
4885
4687
|
runTemplate,
|
|
4688
|
+
runTrace,
|
|
4886
4689
|
runMod,
|
|
4887
4690
|
runHash, runGet, runHelp, runExplain, runInit, nearestVerb,
|
|
4888
4691
|
looksLikeVerb,
|
package/src/helpdoc.ts
CHANGED
|
@@ -17,7 +17,7 @@ const HELPDOC: HelpTopic[] = [
|
|
|
17
17
|
"topic": "tasks",
|
|
18
18
|
"summary": "what do you want to do? — the verb for a job",
|
|
19
19
|
"source": "docs/skill/tasks.md",
|
|
20
|
-
"text": "# What do you want to do?\n\nThe verb for a job, found by the word you arrived with. aontu's own\nvocabulary is on the right; yours is probably on the left.\n\n## Start from a document that works\n\nNothing written yet? Do not invent the first document, edit one:\n\n```\naontu init # into the working directory\naontu init model/ # or into a named one\nsh check.sh # the four questions, on what was just written\n```\n\n`init` writes `model.aon` (an entity map constrained with `&:`),\n`data.aon` (an instance of it that holds) and `check.sh` (the four\nchecks to run after every edit). It never overwrites: if any\nof the three already stands there, it refuses and writes none of them.\n\n## Describe a domain\n\nAn **ontology**, a **schema**, a **data model**, a **contract** — in\naontu these are all one thing: a document. Write the entities as a\n**map keyed by name**, and say what every entry must satisfy with the\n`&:` template:\n\n```\nentity: {\n &: {\n table: string\n fields: { &: { type: string, required: *false | boolean } }\n }\n planet: { table: \"planets\", fields: { name: { type: \"string\" } } }\n moon: { table: \"moons\", fields: { name: { type: \"string\" } } }\n}\n```\n\n`&:` is the construct to reach for first, and the one most easily\nmissed. It meets **every** key of the map it sits in. A quoted `\"*\"`\nis not a wildcard — it is a key named `*`, and a schema written that\nway constrains nothing while still reporting `valid`.\n\nPrefer a named map to a list unless the order is a fact (a migration\nsequence, a rule table tried in order). A key is an address, a name\nother parts can refer to, and a diff that shows one insertion instead\nof every following element renumbered.\n\n## Check data against a model\n\n```\naontu vet model.aon data.aon\n```\n\nExit `0` valid, `1` a contradiction, `3` incomplete (nothing\ncontradicts, but the model is not yet satisfied), `4` the model does\nnot stand up on its own. `--format json` for the machine-readable\nreport, `--closed` to refuse keys the model does not declare,\n`--max-errors <n>` to cap the list.\n\n**Make the check prove it checked something.** A check that examined\nnothing answers exactly like one that passed:\n\n```\naontu vet --coverage model.aon data.aon # what did it examine?\naontu vet --strict-coverage model.aon data.aon # exit 1 if nothing\n```\n\n`--coverage` reports how many data leaves a declaration constrained,\nthe data paths none did, and the declarations no data met.\n`--strict-coverage` exits 1 when the answer is nothing. The usual\ncause is the `\"*\"` mistake above: reach for `&:`.\n\n## Check the model is coherent with itself\n\n```\naontu relations model.aon # declared entity edges: targets resolve, no cycles\naontu reaches planet moon model.aon # does one entity reach another, at any remove?\naontu trim --check model.aon # entries whose removal changes nothing\n```\n\n##
|
|
20
|
+
"text": "# What do you want to do?\n\nThe verb for a job, found by the word you arrived with. aontu's own\nvocabulary is on the right; yours is probably on the left.\n\n## Start from a document that works\n\nNothing written yet? Do not invent the first document, edit one:\n\n```\naontu init # into the working directory\naontu init model/ # or into a named one\nsh check.sh # the four questions, on what was just written\n```\n\n`init` writes `model.aon` (an entity map constrained with `&:`),\n`data.aon` (an instance of it that holds) and `check.sh` (the four\nchecks to run after every edit). It never overwrites: if any\nof the three already stands there, it refuses and writes none of them.\n\n## Describe a domain\n\nAn **ontology**, a **schema**, a **data model**, a **contract** — in\naontu these are all one thing: a document. Write the entities as a\n**map keyed by name**, and say what every entry must satisfy with the\n`&:` template:\n\n```\nentity: {\n &: {\n table: string\n fields: { &: { type: string, required: *false | boolean } }\n }\n planet: { table: \"planets\", fields: { name: { type: \"string\" } } }\n moon: { table: \"moons\", fields: { name: { type: \"string\" } } }\n}\n```\n\n`&:` is the construct to reach for first, and the one most easily\nmissed. It meets **every** key of the map it sits in. A quoted `\"*\"`\nis not a wildcard — it is a key named `*`, and a schema written that\nway constrains nothing while still reporting `valid`.\n\nPrefer a named map to a list unless the order is a fact (a migration\nsequence, a rule table tried in order). A key is an address, a name\nother parts can refer to, and a diff that shows one insertion instead\nof every following element renumbered.\n\n## Check data against a model\n\n```\naontu vet model.aon data.aon\n```\n\nExit `0` valid, `1` a contradiction, `3` incomplete (nothing\ncontradicts, but the model is not yet satisfied), `4` the model does\nnot stand up on its own. `--format json` for the machine-readable\nreport, `--closed` to refuse keys the model does not declare,\n`--max-errors <n>` to cap the list.\n\n**Make the check prove it checked something.** A check that examined\nnothing answers exactly like one that passed:\n\n```\naontu vet --coverage model.aon data.aon # what did it examine?\naontu vet --strict-coverage model.aon data.aon # exit 1 if nothing\n```\n\n`--coverage` reports how many data leaves a declaration constrained,\nthe data paths none did, and the declarations no data met.\n`--strict-coverage` exits 1 when the answer is nothing. The usual\ncause is the `\"*\"` mistake above: reach for `&:`.\n\n## Check the model is coherent with itself\n\n```\naontu relations model.aon # declared entity edges: targets resolve, no cycles\naontu reaches planet moon model.aon # does one entity reach another, at any remove?\naontu trim --check model.aon # entries whose removal changes nothing\n```\n\n## Generate code from a model, and hold it\n\n```\naontu get out gen.aon # the component tree: files, and the lines in them\naontu trace gen.aon # what rule wrote each line\n```\n\nA generator answers a **component tree** -- `file(name, [lines])` and\nits neighbours -- which a generator runtime writes to disk. Hand the\ntree to one and compare with what is committed: the model is the truth,\nthe code is the claim, and drift is a finding. `aontu trace` answers\nthe other direction: for a line of the output, the model node and the\nrule behind it.\n\n## Ask what a model says, and why\n\n```\naontu get $.entity.planet.table model.aon\naontu why $.entity.planet.table model.aon\naontu get $.entity --keys model.aon\naontu get $.entity --types model.aon\n```\n\n`why` names **every** contribution to a value, with the file and line\neach was written on. It is the first thing to run when a value is not\nwhat you expected, and the second thing to run when `vet` refuses.\n\n## Change a value without editing the file\n\n```\naontu set '$.entity.planet.table=planet_v2' --entry model.aon --overlay local.aon\n```\n\nThe change is checked before it is written; a change that contradicts\na pinned value is refused, and the file is left alone.\n\n## Gate a change to the model itself\n\n```\naontu subsume old.aon new.aon # does the general admit every specific?\naontu breaking --against git#HEAD~1 model.aon\naontu hash model.aon # a pin that survives reformatting\n```\n\n## Hand a model to another agent\n\n```\naontu agentsmd --write AGENTS.md model.aon\n```\n\nSplices a derived stanza — the pin, the root keys, the shape, and the\ncommands spelled with paths that exist — between two markers, and\nleaves the rest of the file alone. Re-run it in the commit that\nchanges the model.\n\n## When something refuses\n\nRead the code in the brackets, then look it up:\n\n```\naontu explain no_scalar_unify\n```\n\nEvery finding carries a `code`, a `class`, a `path` and the **two\nsites** that disagree — the value and the constraint it failed, each\nwith its file and line. The class says what kind of answer it is:\n`conflict` (two things cannot both hold), `incomplete` (nothing is\nwrong yet, something is missing), `parse`, `reference`, `budget`.\n\nFull index: [`error-codes.md`](error-codes.md). The language on one\npage: [`grammar-card.md`](grammar-card.md). The worked ladder from\nplain JSON upward: [`examples.md`](examples.md).\n"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"topic": "language",
|
package/src/hints.ts
CHANGED
|
@@ -211,6 +211,9 @@ const hints: Record<string, string> = {
|
|
|
211
211
|
|
|
212
212
|
view_kind_unknown: 'The figure kind is not one the verb draws. The kinds are tree, matrix,\ngraph, layer, sets, layers, ladder and poset; the note lists them.',
|
|
213
213
|
|
|
214
|
+
// RETIRED with the `render` verb (ADR-038). Registered still,
|
|
215
|
+
// errcodes.tsv being append-only, so a code a released engine raised
|
|
216
|
+
// keeps its meaning.
|
|
214
217
|
render_path: 'A unit path is written below the output directory: relative, with no\n`..` segment, and no two units the same. Rename the unit.',
|
|
215
218
|
render_lang: 'A text escape carries verbatim syntax of ONE language, and it must be\nthe unit\'s: a text of another language in this unit would be written\nas if it were this one. Move it to a unit of its own language.',
|
|
216
219
|
render_profile: 'Only fragments and text escapes render without a lowering, under the\ntext profile. A record, enum, alias, const or func needs a profile\nwhose language has one (typescript, go): set the unit\'s lang, or\nwrite the declaration as a fragment.',
|