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.
Files changed (61) hide show
  1. package/README.md +6 -6
  2. package/dist/aontu.d.ts +3 -3
  3. package/dist/aontu.js +4 -6
  4. package/dist/aontu.js.map +1 -1
  5. package/dist/aontumodel.js +6 -12
  6. package/dist/aontumodel.js.map +1 -1
  7. package/dist/casing.d.ts +5 -0
  8. package/dist/casing.js +90 -0
  9. package/dist/casing.js.map +1 -0
  10. package/dist/cli.d.ts +2 -2
  11. package/dist/cli.js +147 -335
  12. package/dist/cli.js.map +1 -1
  13. package/dist/helpdoc.js +1 -1
  14. package/dist/helpdoc.js.map +1 -1
  15. package/dist/hints.js +3 -0
  16. package/dist/hints.js.map +1 -1
  17. package/dist/lsp.d.ts +1 -1
  18. package/dist/mcp.js +0 -36
  19. package/dist/mcp.js.map +1 -1
  20. package/dist/mod-tool.js +16 -1
  21. package/dist/mod-tool.js.map +1 -1
  22. package/dist/profile.d.ts +9 -0
  23. package/dist/profile.js +28 -0
  24. package/dist/profile.js.map +1 -0
  25. package/dist/trace.d.ts +21 -0
  26. package/dist/trace.js +107 -0
  27. package/dist/trace.js.map +1 -0
  28. package/dist/tsconfig.tsbuildinfo +1 -1
  29. package/dist/unify.js +1 -6
  30. package/dist/unify.js.map +1 -1
  31. package/dist/val/CmpFuncVal.d.ts +2 -0
  32. package/dist/val/CmpFuncVal.js +47 -13
  33. package/dist/val/CmpFuncVal.js.map +1 -1
  34. package/dist/val/NomFuncVal.js +11 -13
  35. package/dist/val/NomFuncVal.js.map +1 -1
  36. package/dist/val/ReferFuncVal.js +3 -0
  37. package/dist/val/ReferFuncVal.js.map +1 -1
  38. package/package.json +1 -1
  39. package/skill/tasks.md +9 -7
  40. package/src/aontu.ts +3 -8
  41. package/src/aontumodel.ts +6 -12
  42. package/src/casing.ts +95 -0
  43. package/src/cli.ts +162 -359
  44. package/src/helpdoc.ts +1 -1
  45. package/src/hints.ts +3 -0
  46. package/src/mcp.ts +0 -39
  47. package/src/mod-tool.ts +18 -1
  48. package/src/profile.ts +42 -0
  49. package/src/trace.ts +157 -0
  50. package/src/unify.ts +1 -6
  51. package/src/val/CmpFuncVal.ts +66 -20
  52. package/src/val/NomFuncVal.ts +3 -5
  53. package/src/val/ReferFuncVal.ts +4 -0
  54. package/dist/lower.d.ts +0 -23
  55. package/dist/lower.js +0 -528
  56. package/dist/lower.js.map +0 -1
  57. package/dist/render.d.ts +0 -53
  58. package/dist/render.js +0 -432
  59. package/dist/render.js.map +0 -1
  60. package/src/lower.ts +0 -586
  61. package/src/render.ts +0 -580
package/src/render.ts DELETED
@@ -1,580 +0,0 @@
1
- /* Copyright (c) 2025 Richard Rodger, MIT License */
2
-
3
-
4
- import { Aontu } from './aontu'
5
- import { vet, failureFinding, anchorAt } from './vet'
6
- import type { VetFinding } from './vet'
7
- import { hcanon } from './hcanon'
8
- import { makeNilErr } from './err'
9
- import { cmpCodePoint } from './keyorder'
10
- import { includeOpts } from './utility'
11
- import type { IncludeOptions } from './utility'
12
- import { lowerDecl, lowerHeader, ident } from './lower'
13
- import type { LowerCtx } from './lower'
14
-
15
-
16
- export type RenderVerdict = 'ok' | 'lossy' | 'error'
17
-
18
- // One rendered unit: the path the instance gave it, its language, and
19
- // its bytes, as one string.
20
- export type RenderUnit = {
21
- path: string
22
- lang: string
23
- text: string
24
- }
25
-
26
- export type RenderLoss = {
27
- unit: string
28
- path: string
29
- tier: 1 | 2 | 3
30
- construct: string
31
- reason: string
32
- }
33
-
34
- export type RenderTrace = {
35
- unit: string
36
- piece: string
37
- node: string
38
- rule: string
39
- }
40
-
41
- export type RenderCoverage = {
42
- // Every model path a reference resolved to, in walk order: what the
43
- // render READ.
44
- read: string[]
45
- // Dead model: the SHALLOWEST model paths no read reached. A path
46
- // whose subtree holds a read is not named; its unread children are.
47
- dead: string[]
48
- unruled: { unit: string, path: string }[]
49
- }
50
-
51
- export type RenderReport = {
52
- verdict: RenderVerdict
53
- units: RenderUnit[]
54
- // Per unit and path. Empty on `error`.
55
- lossy: RenderLoss[]
56
- errors?: VetFinding[]
57
- trace?: RenderTrace[]
58
- // The coverage report, under `coverage` (P7).
59
- coverage?: RenderCoverage
60
- }
61
-
62
- export type RenderOptions = IncludeOptions & {
63
- // The value to render, a path into the document. Absent means the
64
- // root.
65
- at?: string
66
- // Where the document CAME FROM, so a relative `@"file"` load inside
67
- // it resolves from its own directory.
68
- path?: string
69
- profiles?: any[]
70
- // Render only the unit at this path.
71
- unit?: string
72
- strict?: boolean
73
- trace?: boolean
74
- // Compute the coverage report, which needs the trace and turns it on.
75
- coverage?: boolean
76
- coverageAt?: string
77
- }
78
-
79
-
80
- const VOCABULARY = '@"aontu:code"'
81
- // The bundled profiles, by lang: aontu:render/lang/<lang>.
82
- const BUNDLED_LANGS = ['go', 'markdown', 'text', 'typescript']
83
- const PROFILE_VOCABULARY = '@"aontu:render"'
84
-
85
-
86
- function finding(
87
- code: string, cls: string, path: string, message: string
88
- ): VetFinding {
89
- return { code, class: cls as any, severity: 'error', path, message, sites: [] }
90
- }
91
-
92
-
93
- function errorReport(errors: VetFinding[]): RenderReport {
94
- return { verdict: 'error', units: [], lossy: [], errors }
95
- }
96
-
97
-
98
- export function render(src: string, options?: RenderOptions): RenderReport {
99
- const opts = options ?? {}
100
- const aontu = new Aontu(includeOpts(opts))
101
-
102
- const rec = true === opts.trace || true === opts.coverage
103
- const reads = rec ? new Set<string>() : undefined
104
-
105
- // COLLECT MODE, so a syntax error arrives on the context rather than
106
- // as a throw (the jsonschema and vet precedent; no try/catch, for
107
- // the reason those verbs have none).
108
- const actx = aontu.ctx({ collect: true, reads })
109
- const root: any = aontu.unify(src, { path: opts.path, collect: true }, actx)
110
- if (0 < actx.err.length || true === root?.isNil) {
111
- return errorReport([failureFinding(actx, opts.path, root)])
112
- }
113
-
114
- let node: any = root
115
- if (null != opts.at && '' !== opts.at) {
116
- const found: any = anchorAt(root, opts.at)
117
- if (null == found) {
118
- // The anchor names nothing: a `no_path` nil through the finding
119
- // shape every other refusal here uses.
120
- const nil: any = makeNilErr(actx, 'no_path', root, undefined, 'at')
121
- actx.err.push(nil)
122
- return errorReport([failureFinding(actx, opts.path, root)])
123
- }
124
- node = found
125
- }
126
-
127
- const value = hcanon(node)
128
- const report = vet(VOCABULARY, value)
129
- if ('valid' !== report.verdict) {
130
- return errorReport(report.findings)
131
- }
132
-
133
- const codeVal: any = node.peg.aontu?.peg?.Code
134
- const instance = new Aontu().generate(
135
- VOCABULARY + (undefined === codeVal ? '' : '\naontu: Code: ' + hcanon(codeVal)))
136
- const folded = renderValue(instance, opts)
137
-
138
- if (rec && 'error' !== folded.verdict) {
139
- const marks = emitted(node)
140
- const trace = traceOf(marks, instance, folded.units)
141
- if (0 < trace.length) {
142
- folded.trace = trace
143
- }
144
- if (true === opts.coverage) {
145
- const cov = coverOf(root, node, reads as Set<string>, opts, marks,
146
- instance, folded.units)
147
- if (undefined === cov) {
148
- const nil: any = makeNilErr(actx, 'no_path', root, undefined,
149
- 'coverageAt')
150
- actx.err.push(nil)
151
- return errorReport([failureFinding(actx, opts.path, root)])
152
- }
153
- folded.coverage = cov
154
- }
155
- }
156
- return folded
157
- }
158
-
159
-
160
- // ---------------------------------------------------------------------
161
- // THE TRACE AND THE COVERAGE REPORT (RENDER.0.md D11, P7).
162
-
163
- // The one walk order both ports walk in: a list by index, a map by key
164
- // in code-point order. `fn` answers whether to descend.
165
- function walkVals(root: any, fn: (v: any, path: string[]) => boolean): void {
166
- const walk = (v: any, path: string[]): void => {
167
- if (null == v || true !== v.isVal || !fn(v, path)) {
168
- return
169
- }
170
- if (true === v.isList && null != v.peg) {
171
- for (let i = 0; i < v.peg.length; i++) {
172
- walk(v.peg[i], [...path, String(i)])
173
- }
174
- }
175
- else if (true === v.isMap && null != v.peg) {
176
- for (const k of Object.keys(v.peg).sort(cmpCodePoint)) {
177
- if (!v.aliasKeys.includes(k)) {
178
- walk(v.peg[k], [...path, k])
179
- }
180
- }
181
- }
182
- }
183
- walk(root, [])
184
- }
185
-
186
-
187
- // A path as an address: `$`, then a dot before every segment.
188
- function addr(path: string[]): string {
189
- return '$' + path.map((seg) => '.' + seg).join('')
190
- }
191
-
192
-
193
- // Every piece a dispatch stamped, under the anchored value, in
194
- // document order. The path is relative to the anchor, which is where
195
- // the instance the fold reads is rooted too.
196
- function emitted(node: any): { path: string, mark: any }[] {
197
- const out: { path: string, mark: any }[] = []
198
- walkVals(node, (v: any, path: string[]) => {
199
- if (null != v.emitted) {
200
- out.push({ path: addr(path), mark: v.emitted })
201
- }
202
- return true
203
- })
204
- return out
205
- }
206
-
207
-
208
- function traceOf(marks: { path: string, mark: any }[], instance: any,
209
- units: RenderUnit[]): RenderTrace[] {
210
- const pre: { at: string, path: string }[] = []
211
- unitList(instance).forEach((u: any, i: number) => {
212
- if (units.some((r) => r.path === u.path)) {
213
- pre.push({ at: '$.aontu.Code.units.' + i, path: u.path })
214
- }
215
- })
216
- const out: RenderTrace[] = []
217
- for (const m of marks) {
218
- const hit = pre.find((p) => m.path === p.at || m.path.startsWith(p.at + '.'))
219
- if (undefined === hit) {
220
- continue
221
- }
222
- out.push({ unit: hit.path, piece: m.path, node: m.mark.node, rule: m.mark.rule })
223
- }
224
- return out
225
- }
226
-
227
-
228
- // Every proper ancestor of an address, `$` included.
229
- function ancestors(a: string): string[] {
230
- const parts = a.split('.')
231
- const out: string[] = []
232
- for (let i = 1; i < parts.length; i++) {
233
- out.push(parts.slice(0, i).join('.'))
234
- }
235
- return out
236
- }
237
-
238
-
239
- function covered(set: Set<string>, a: string): boolean {
240
- if (set.has(a)) {
241
- return true
242
- }
243
- for (const up of ancestors(a)) {
244
- if (set.has(up)) {
245
- return true
246
- }
247
- }
248
- return false
249
- }
250
-
251
-
252
- function coverOf(root: any, node: any, reads: Set<string>,
253
- opts: RenderOptions, marks: { path: string, mark: any }[], instance: any,
254
- units: RenderUnit[]): RenderCoverage | undefined {
255
- // THE ANCHOR IS THE ONE render() ALREADY FOUND, so the namespace
256
- // under it is named without asking a second time: `--at` is resolved
257
- // before the vet, and an anchor that named nothing never reached here.
258
- const codeAddr = addr(node.path.concat('aontu'))
259
-
260
- let from: any = root
261
- let base: string[] = []
262
- if (null != opts.coverageAt && '' !== opts.coverageAt) {
263
- const found: any = anchorAt(root, opts.coverageAt)
264
- if (null == found) {
265
- return undefined
266
- }
267
- from = found
268
- base = found.path
269
- }
270
-
271
- const above = new Set<string>(reads)
272
- const below = new Set<string>()
273
- for (const r of reads) {
274
- for (const up of ancestors(r)) {
275
- below.add(up)
276
- }
277
- }
278
-
279
- const dead: string[] = []
280
- walkVals(from, (_v: any, path: string[]) => {
281
- const a = addr(base.concat(path))
282
- if (a === codeAddr || covered(above, a)) {
283
- return false
284
- }
285
- if (below.has(a) || 0 === path.length) {
286
- return true
287
- }
288
- dead.push(a)
289
- return false
290
- })
291
-
292
- // A SILENT HOLE: a declaration of a rendered unit that no stamp
293
- // touches -- neither its own, nor one on the unit above it, nor one
294
- // on a piece inside it.
295
- const stamped = new Set<string>(marks.map((m) => m.path))
296
- const inside = new Set<string>()
297
- for (const m of marks) {
298
- for (const up of ancestors(m.path)) {
299
- inside.add(up)
300
- }
301
- }
302
- const unruled: { unit: string, path: string }[] = []
303
- unitList(instance).forEach((unit: any, i: number) => {
304
- if (!units.some((u) => u.path === unit.path)) {
305
- return
306
- }
307
- const decls: any[] = unit.decls
308
- decls.forEach((_d: any, j: number) => {
309
- const a = '$.aontu.Code.units.' + i + '.decls.' + j
310
- if (!covered(stamped, a) && !inside.has(a)) {
311
- unruled.push({ unit: unit.path, path: a })
312
- }
313
- })
314
- })
315
-
316
- return { read: [...reads].sort(cmpCodePoint), dead, unruled }
317
- }
318
-
319
-
320
- // The bundled text profile, evaluated once: the profile of a unit
321
- // whose declarations are fragments and text escapes only.
322
- const bundled: Record<string, any> = {}
323
-
324
- // A bundled profile, evaluated once: the meet of aontu:render/lang/<lang>
325
- // with the vocabulary, so its defaults are in it.
326
- function bundledProfile(lang: string): any {
327
- if (!BUNDLED_LANGS.includes(lang)) {
328
- return undefined
329
- }
330
- if (undefined === bundled[lang]) {
331
- bundled[lang] = new Aontu().generate('@"aontu:render/lang/' + lang + '"').aontu.render.Lang
332
- }
333
- return bundled[lang]
334
- }
335
-
336
-
337
- function profileFor(lang: string, given: any[] | undefined, fragOnly: boolean): any {
338
- const supplied = (given ?? []).find((p: any) => p?.lang === lang)
339
- if (undefined !== supplied) {
340
- return supplied
341
- }
342
- const own = bundledProfile(lang)
343
- if (undefined !== own) {
344
- return own
345
- }
346
- return fragOnly ? bundledProfile('text') : undefined
347
- }
348
-
349
-
350
- function isMap(v: any): boolean {
351
- return null != v && 'object' === typeof v && !Array.isArray(v)
352
- }
353
-
354
-
355
- // An inline profile merged over its base, map by map, the inline
356
- // value winning at a leaf; keys in code-point order, so the merge is
357
- // the same in both ports.
358
- function mergeProfile(base: any, over: any): any {
359
- const out: any = { ...base }
360
- for (const k of Object.keys(over).sort(cmpCodePoint)) {
361
- out[k] = isMap(base[k]) && isMap(over[k]) ?
362
- mergeProfile(base[k], over[k]) : over[k]
363
- }
364
- return out
365
- }
366
-
367
-
368
- function pad(profile: any, at: number): string {
369
- const indent = profile.indent ?? { unit: ' ', width: 2 }
370
- return (indent.unit ?? ' ').repeat((indent.width ?? 2) * at)
371
- }
372
-
373
- function line(profile: any, at: number, text: string): string {
374
- return ('' === text ? '' : pad(profile, at)) + text + '\n'
375
- }
376
-
377
- // A reference inline is its name: through the profile's identifier
378
- // rules under a lowering, verbatim under text.
379
- function inline(piece: any, ctx: LowerCtx | undefined): string {
380
- if ('string' === typeof piece) {
381
- return piece
382
- }
383
- return undefined === ctx ? piece.name : ident(piece.name, 'record', ctx, '', false)
384
- }
385
-
386
- function foldPiece(
387
- piece: any, profile: any, unit: string, path: string, lossy: RenderLoss[],
388
- ctx?: LowerCtx
389
- ): string {
390
- if ('string' === typeof piece) {
391
- return line(profile, 0, piece)
392
- }
393
- if ('line' === piece.k) {
394
- return line(profile, piece.at ?? 0, piece.n.map((p: any) => inline(p, ctx)).join(''))
395
- }
396
- if ('blank' === piece.k) {
397
- return '\n'.repeat(piece.n ?? 1)
398
- }
399
- lossy.push({
400
- unit, path, tier: 3, construct: 'raw',
401
- reason: 'verbatim text: the renderer re-indents it and checks nothing else',
402
- })
403
- const at: number = piece.at ?? 0
404
- const reindent: boolean = piece.reindent ?? true
405
- const lines: string[] = piece.text.split('\n')
406
- if ('' === lines[lines.length - 1]) {
407
- lines.pop()
408
- }
409
- return lines.map((l: string) => reindent ? line(profile, at, l) : l + '\n').join('')
410
- }
411
-
412
-
413
- export function renderProfile(src: string, options?: RenderOptions):
414
- { profile?: any, errors?: VetFinding[] } {
415
- const opts = options ?? {}
416
- const aontu = new Aontu(includeOpts(opts))
417
- const actx = aontu.ctx({ collect: true })
418
- const root: any = aontu.unify(src, { path: opts.path, collect: true }, actx)
419
- if (0 < actx.err.length || true === root?.isNil) {
420
- return { errors: [failureFinding(actx, opts.path, root)] }
421
- }
422
- const report = vet(PROFILE_VOCABULARY, hcanon(root))
423
- if ('valid' !== report.verdict) {
424
- return { errors: report.findings }
425
- }
426
- // The meet, keyed as render's is: the vocabulary requires `profile`,
427
- // so a value the vet admitted has one.
428
- const instance = new Aontu().generate(
429
- PROFILE_VOCABULARY + '\naontu: render: Lang: ' +
430
- hcanon(root.peg.aontu.peg.render.peg.Lang))
431
- return { profile: instance.aontu.render.Lang }
432
- }
433
-
434
-
435
- // The instance's unit list, or none: `code` is the vocabulary's own
436
- // key and is always there, `units` is not. One reader, so the fold,
437
- // the trace and the coverage report all see the same list.
438
- function unitList(instance: any): any[] {
439
- return Array.isArray(instance?.aontu?.Code?.units) ?
440
- instance.aontu.Code.units : []
441
- }
442
-
443
-
444
- export function renderValue(instance: any, options?: RenderOptions): RenderReport {
445
- const opts = options ?? {}
446
- const errors: VetFinding[] = []
447
- const lossy: RenderLoss[] = []
448
- const units: RenderUnit[] = []
449
- const list: any[] = unitList(instance)
450
-
451
- const seen: string[] = []
452
- let selected = 0
453
- list.forEach((unit: any, i: number) => {
454
- const upath = '$.aontu.Code.units.' + i
455
- const path: string = unit.path
456
- const lang: string = unit.lang
457
-
458
- // A UNIT PATH IS RELATIVE, DESCENDS, AND IS ITS OWN (RENDER.0.md
459
- // D8): an absolute path, a `..` segment or a repeat of another
460
- // unit's path is refused before anything is written.
461
- if (path.startsWith('/')) {
462
- errors.push(finding('render_path', 'parse', upath + '.path',
463
- 'the unit path ' + path + ' is absolute.'))
464
- return
465
- }
466
- if (path.split('/').includes('..')) {
467
- errors.push(finding('render_path', 'parse', upath + '.path',
468
- 'the unit path ' + path + ' climbs out of the output directory.'))
469
- return
470
- }
471
- if (seen.includes(path)) {
472
- errors.push(finding('render_path', 'parse', upath + '.path',
473
- 'the unit path ' + path + ' repeats another unit\'s.'))
474
- return
475
- }
476
- seen.push(path)
477
-
478
- if (undefined !== opts.unit && opts.unit !== path) {
479
- return
480
- }
481
- selected++
482
-
483
- const decls: any[] = unit.decls
484
- const fragOnly = decls.every((d: any) => 'frag' === d.k || 'text' === d.k)
485
- const base = profileFor(lang, opts.profiles, fragOnly)
486
- if (undefined === base) {
487
- errors.push(finding('render_profile', 'parse', upath + '.lang',
488
- 'no profile renders ' + lang + ': a declaration needs a lowering, and ' +
489
- 'only fragments and text escapes render under aontu:render/lang/text.'))
490
- return
491
- }
492
- const profile = null == unit.profile ? base : mergeProfile(base, unit.profile)
493
-
494
- const family: string | undefined = profile.lowering
495
- const ctx: LowerCtx | undefined = undefined === family ? undefined
496
- : { profile, family, unit: path, lossy }
497
-
498
- let text = ''
499
- if (undefined !== ctx) {
500
- const header = lowerHeader(unit, instance?.aontu?.Code?.source, ctx)
501
- for (const piece of header) {
502
- text += foldPiece(piece, profile, path, upath, lossy, ctx)
503
- }
504
- if (0 < header.length && 0 < decls.length) {
505
- text += '\n'
506
- }
507
- }
508
- let lowered = false
509
- decls.forEach((decl: any, j: number) => {
510
- const dpath = upath + '.decls.' + j
511
- if ('frag' === decl.k) {
512
- lowered = false
513
- // Lossy only against a language whose DECLARATIONS could have
514
- // been lowered instead: a fragment is all the others have.
515
- if (null != profile.lowering) {
516
- lossy.push({
517
- unit: path, path: dpath, tier: 2, construct: 'frag',
518
- reason: 'a fragment says nothing about ' + lang + ' syntax',
519
- })
520
- }
521
- decl.n.forEach((piece: any, n: number) => {
522
- text += foldPiece(piece, profile, path, dpath + '.n.' + n, lossy, ctx)
523
- })
524
- }
525
- else if ('text' === decl.k) {
526
- lowered = false
527
- if (decl.lang !== lang) {
528
- errors.push(finding('render_lang', 'conflict', dpath + '.lang',
529
- 'the text escape is ' + decl.lang + ' in a ' + lang + ' unit.'))
530
- return
531
- }
532
- lossy.push({
533
- unit: path, path: dpath, tier: 3, construct: 'text',
534
- reason: 'verbatim ' + lang + ': the renderer checks nothing in it',
535
- })
536
- text += decl.text
537
- }
538
- else if (undefined !== ctx) {
539
- if (lowered) {
540
- text += '\n'
541
- }
542
- for (const piece of lowerDecl(decl, dpath, ctx)) {
543
- text += foldPiece(piece, profile, path, dpath, lossy, ctx)
544
- }
545
- lowered = true
546
- }
547
- else {
548
- errors.push(finding('render_profile', 'parse', dpath + '.k',
549
- 'a ' + decl.k + ' declaration has no lowering under the ' +
550
- profile.lang + ' profile.'))
551
- }
552
- })
553
-
554
- units.push({ path, lang, text })
555
- })
556
-
557
- if (undefined !== opts.unit && 0 === selected) {
558
- errors.push(finding('render_unit', 'reference', '$.aontu.Code.units',
559
- 'no unit has the path ' + opts.unit + '.'))
560
- }
561
-
562
- if (true === opts.strict) {
563
- for (const loss of lossy) {
564
- if (3 === loss.tier) {
565
- errors.push(finding('render_strict', 'conflict', loss.path,
566
- 'the ' + loss.construct + ' in ' + loss.unit +
567
- ' is an opaque escape, refused under strict.'))
568
- }
569
- }
570
- }
571
-
572
- if (0 < errors.length) {
573
- return errorReport(errors)
574
- }
575
- return {
576
- verdict: 0 < lossy.length ? 'lossy' : 'ok',
577
- units,
578
- lossy,
579
- }
580
- }