aontu 0.52.0 → 0.53.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 +88 -0
- package/bin/aontu-mcp.js +4 -0
- package/dist/agentsmd.d.ts +16 -0
- package/dist/agentsmd.js +107 -0
- package/dist/agentsmd.js.map +1 -0
- package/dist/aontu.d.ts +14 -3
- package/dist/aontu.js +145 -4
- package/dist/aontu.js.map +1 -1
- package/dist/cli.d.ts +44 -1
- package/dist/cli.js +2401 -44
- package/dist/cli.js.map +1 -1
- package/dist/ctx.d.ts +16 -0
- package/dist/ctx.js +44 -0
- package/dist/ctx.js.map +1 -1
- package/dist/diff.d.ts +22 -0
- package/dist/diff.js +141 -0
- package/dist/diff.js.map +1 -0
- package/dist/err.d.ts +3 -1
- package/dist/err.js +48 -8
- package/dist/err.js.map +1 -1
- package/dist/graph.d.ts +16 -0
- package/dist/graph.js +73 -0
- package/dist/graph.js.map +1 -0
- package/dist/hcanon.d.ts +3 -0
- package/dist/hcanon.js +146 -0
- package/dist/hcanon.js.map +1 -0
- package/dist/hints.js +223 -5
- package/dist/hints.js.map +1 -1
- package/dist/jsonschema.d.ts +20 -0
- package/dist/jsonschema.js +391 -0
- package/dist/jsonschema.js.map +1 -0
- package/dist/lang.js +698 -35
- package/dist/lang.js.map +1 -1
- package/dist/lsp.d.ts +9 -2
- package/dist/lsp.js +262 -46
- package/dist/lsp.js.map +1 -1
- package/dist/mcp-server.d.ts +20 -0
- package/dist/mcp-server.js +147 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/mcp.d.ts +42 -0
- package/dist/mcp.js +814 -0
- package/dist/mcp.js.map +1 -0
- package/dist/mod-tool.d.ts +58 -0
- package/dist/mod-tool.js +498 -0
- package/dist/mod-tool.js.map +1 -0
- package/dist/mod.d.ts +31 -0
- package/dist/mod.js +250 -0
- package/dist/mod.js.map +1 -0
- package/dist/patch.d.ts +44 -0
- package/dist/patch.js +506 -0
- package/dist/patch.js.map +1 -0
- package/dist/provenance.d.ts +40 -0
- package/dist/provenance.js +335 -0
- package/dist/provenance.js.map +1 -0
- package/dist/query.d.ts +27 -0
- package/dist/query.js +294 -0
- package/dist/query.js.map +1 -0
- package/dist/reach.d.ts +14 -0
- package/dist/reach.js +140 -0
- package/dist/reach.js.map +1 -0
- package/dist/relation.d.ts +19 -0
- package/dist/relation.js +305 -0
- package/dist/relation.js.map +1 -0
- package/dist/report-sarif.d.ts +14 -0
- package/dist/report-sarif.js +102 -0
- package/dist/report-sarif.js.map +1 -0
- package/dist/site.d.ts +4 -0
- package/dist/site.js +31 -0
- package/dist/site.js.map +1 -1
- package/dist/std.d.ts +1 -0
- package/dist/std.js +73 -0
- package/dist/std.js.map +1 -0
- package/dist/subsume.d.ts +39 -0
- package/dist/subsume.js +526 -0
- package/dist/subsume.js.map +1 -0
- package/dist/trim.d.ts +19 -0
- package/dist/trim.js +155 -0
- package/dist/trim.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/type.d.ts +17 -1
- package/dist/type.js.map +1 -1
- package/dist/unify.d.ts +3 -1
- package/dist/unify.js +287 -18
- package/dist/unify.js.map +1 -1
- package/dist/utility.d.ts +9 -1
- package/dist/utility.js +122 -1
- package/dist/utility.js.map +1 -1
- package/dist/val/AggFuncVal.d.ts +33 -0
- package/dist/val/AggFuncVal.js +202 -0
- package/dist/val/AggFuncVal.js.map +1 -0
- package/dist/val/ArithFuncVal.d.ts +31 -0
- package/dist/val/ArithFuncVal.js +62 -0
- package/dist/val/ArithFuncVal.js.map +1 -0
- package/dist/val/BagVal.d.ts +5 -0
- package/dist/val/BagVal.js +96 -5
- package/dist/val/BagVal.js.map +1 -1
- package/dist/val/CloseFuncVal.js +9 -1
- package/dist/val/CloseFuncVal.js.map +1 -1
- package/dist/val/ConjunctVal.d.ts +1 -1
- package/dist/val/ConjunctVal.js +19 -0
- package/dist/val/ConjunctVal.js.map +1 -1
- package/dist/val/ConstraintVal.d.ts +48 -1
- package/dist/val/ConstraintVal.js +1501 -110
- package/dist/val/ConstraintVal.js.map +1 -1
- package/dist/val/CopyFuncVal.d.ts +1 -2
- package/dist/val/CopyFuncVal.js +7 -0
- package/dist/val/CopyFuncVal.js.map +1 -1
- package/dist/val/Decimal.d.ts +1 -0
- package/dist/val/Decimal.js +13 -0
- package/dist/val/Decimal.js.map +1 -1
- package/dist/val/DeprecateFuncVal.d.ts +11 -0
- package/dist/val/DeprecateFuncVal.js +47 -0
- package/dist/val/DeprecateFuncVal.js.map +1 -0
- package/dist/val/DisjunctVal.js +130 -21
- package/dist/val/DisjunctVal.js.map +1 -1
- package/dist/val/EachFuncVal.d.ts +15 -0
- package/dist/val/EachFuncVal.js +75 -0
- package/dist/val/EachFuncVal.js.map +1 -0
- package/dist/val/ExpectVal.d.ts +1 -0
- package/dist/val/ExpectVal.js +41 -4
- package/dist/val/ExpectVal.js.map +1 -1
- package/dist/val/FeatureVal.js +1 -1
- package/dist/val/FeatureVal.js.map +1 -1
- package/dist/val/FilterFuncVal.d.ts +15 -0
- package/dist/val/FilterFuncVal.js +91 -0
- package/dist/val/FilterFuncVal.js.map +1 -0
- package/dist/val/FuncBaseVal.d.ts +6 -1
- package/dist/val/FuncBaseVal.js +178 -2
- package/dist/val/FuncBaseVal.js.map +1 -1
- package/dist/val/HideFuncVal.js.map +1 -1
- package/dist/val/IdFuncVal.d.ts +13 -0
- package/dist/val/IdFuncVal.js +54 -0
- package/dist/val/IdFuncVal.js.map +1 -0
- package/dist/val/JunctionVal.js +7 -1
- package/dist/val/JunctionVal.js.map +1 -1
- package/dist/val/KeyFuncVal.d.ts +1 -1
- package/dist/val/KeyFuncVal.js +38 -30
- package/dist/val/KeyFuncVal.js.map +1 -1
- package/dist/val/ListVal.js +117 -17
- package/dist/val/ListVal.js.map +1 -1
- package/dist/val/LowerFuncVal.js.map +1 -1
- package/dist/val/MapVal.js +102 -8
- package/dist/val/MapVal.js.map +1 -1
- package/dist/val/MatchFuncVal.d.ts +15 -0
- package/dist/val/MatchFuncVal.js +107 -0
- package/dist/val/MatchFuncVal.js.map +1 -0
- package/dist/val/MoveFuncVal.js.map +1 -1
- package/dist/val/NilVal.js +24 -0
- package/dist/val/NilVal.js.map +1 -1
- package/dist/val/OpBaseVal.d.ts +1 -1
- package/dist/val/OpBaseVal.js +24 -2
- package/dist/val/OpBaseVal.js.map +1 -1
- package/dist/val/OpenFuncVal.js +4 -1
- package/dist/val/OpenFuncVal.js.map +1 -1
- package/dist/val/PackFuncVal.d.ts +15 -0
- package/dist/val/PackFuncVal.js +108 -0
- package/dist/val/PackFuncVal.js.map +1 -0
- package/dist/val/PathFuncVal.js.map +1 -1
- package/dist/val/PlaceVal.d.ts +13 -0
- package/dist/val/PlaceVal.js +131 -0
- package/dist/val/PlaceVal.js.map +1 -0
- package/dist/val/PlusOpVal.js +11 -2
- package/dist/val/PlusOpVal.js.map +1 -1
- package/dist/val/PrefFuncVal.js.map +1 -1
- package/dist/val/PrefVal.d.ts +2 -2
- package/dist/val/PrefVal.js +78 -23
- package/dist/val/PrefVal.js.map +1 -1
- package/dist/val/RefVal.d.ts +1 -1
- package/dist/val/RefVal.js +158 -30
- package/dist/val/RefVal.js.map +1 -1
- package/dist/val/ReferFuncVal.d.ts +36 -0
- package/dist/val/ReferFuncVal.js +303 -0
- package/dist/val/ReferFuncVal.js.map +1 -0
- package/dist/val/ScalarKindVal.d.ts +1 -2
- package/dist/val/ScalarKindVal.js +0 -11
- package/dist/val/ScalarKindVal.js.map +1 -1
- package/dist/val/TopVal.js.map +1 -1
- package/dist/val/TypeFuncVal.js.map +1 -1
- package/dist/val/UpperFuncVal.js.map +1 -1
- package/dist/val/Val.d.ts +9 -2
- package/dist/val/Val.js +150 -4
- package/dist/val/Val.js.map +1 -1
- package/dist/val/VarVal.js.map +1 -1
- package/dist/val/arith.d.ts +6 -0
- package/dist/val/arith.js +170 -0
- package/dist/val/arith.js.map +1 -0
- package/dist/vet.d.ts +45 -0
- package/dist/vet.js +776 -0
- package/dist/vet.js.map +1 -0
- package/dist/walk.d.ts +2 -0
- package/dist/walk.js +91 -0
- package/dist/walk.js.map +1 -0
- package/grammar/aontu.gbnf +130 -0
- package/grammar/aontu.lark +113 -0
- package/package.json +30 -15
- package/skill/SKILL.md +37 -0
- package/skill/error-codes.md +62 -0
- package/skill/examples.md +99 -0
- package/skill/grammar-card.md +57 -0
- package/src/agentsmd.ts +135 -0
- package/src/aontu.ts +192 -4
- package/src/cli.ts +2858 -71
- package/src/ctx.ts +81 -0
- package/src/diff.ts +196 -0
- package/src/err.ts +52 -8
- package/src/graph.ts +135 -0
- package/src/hcanon.ts +169 -0
- package/src/hints.ts +271 -5
- package/src/jsonschema.ts +511 -0
- package/src/lang.ts +779 -37
- package/src/lsp.ts +281 -47
- package/src/mcp-server.ts +187 -0
- package/src/mcp.ts +993 -0
- package/src/mod-tool.ts +679 -0
- package/src/mod.ts +344 -0
- package/src/patch.ts +624 -0
- package/src/provenance.ts +430 -0
- package/src/query.ts +379 -0
- package/src/reach.ts +184 -0
- package/src/relation.ts +395 -0
- package/src/report-sarif.ts +137 -0
- package/src/site.ts +36 -1
- package/src/std.ts +73 -0
- package/src/subsume.ts +690 -0
- package/src/trim.ts +195 -0
- package/src/tsconfig.json +10 -4
- package/src/type.ts +51 -2
- package/src/unify.ts +311 -16
- package/src/utility.ts +139 -1
- package/src/val/AggFuncVal.ts +319 -0
- package/src/val/ArithFuncVal.ts +108 -0
- package/src/val/BagVal.ts +101 -4
- package/src/val/CloseFuncVal.ts +9 -1
- package/src/val/ConjunctVal.ts +20 -0
- package/src/val/ConstraintVal.ts +1699 -116
- package/src/val/CopyFuncVal.ts +7 -1
- package/src/val/Decimal.ts +15 -0
- package/src/val/DeprecateFuncVal.ts +84 -0
- package/src/val/DisjunctVal.ts +139 -28
- package/src/val/EachFuncVal.ts +133 -0
- package/src/val/ExpectVal.ts +43 -6
- package/src/val/FeatureVal.ts +1 -1
- package/src/val/FilterFuncVal.ts +154 -0
- package/src/val/FuncBaseVal.ts +200 -3
- package/src/val/HideFuncVal.ts +0 -2
- package/src/val/IdFuncVal.ts +91 -0
- package/src/val/JunctionVal.ts +7 -1
- package/src/val/KeyFuncVal.ts +39 -35
- package/src/val/ListVal.ts +125 -18
- package/src/val/LowerFuncVal.ts +0 -1
- package/src/val/MapVal.ts +110 -8
- package/src/val/MatchFuncVal.ts +176 -0
- package/src/val/MoveFuncVal.ts +0 -2
- package/src/val/NilVal.ts +25 -0
- package/src/val/OpBaseVal.ts +26 -3
- package/src/val/OpenFuncVal.ts +4 -2
- package/src/val/PackFuncVal.ts +175 -0
- package/src/val/PathFuncVal.ts +0 -1
- package/src/val/PlaceVal.ts +193 -0
- package/src/val/PlusOpVal.ts +11 -2
- package/src/val/PrefFuncVal.ts +0 -1
- package/src/val/PrefVal.ts +79 -36
- package/src/val/RefVal.ts +163 -30
- package/src/val/ReferFuncVal.ts +387 -0
- package/src/val/ScalarKindVal.ts +0 -13
- package/src/val/TopVal.ts +0 -1
- package/src/val/TypeFuncVal.ts +0 -2
- package/src/val/UpperFuncVal.ts +0 -1
- package/src/val/Val.ts +213 -3
- package/src/val/VarVal.ts +0 -1
- package/src/val/arith.ts +316 -0
- package/src/vet.ts +992 -0
- package/src/walk.ts +99 -0
package/src/patch.ts
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
/* Copyright (c) 2025 Richard Rodger, MIT License */
|
|
2
|
+
|
|
3
|
+
// OVERLAY PATCH (G7 phase 5,
|
|
4
|
+
// docs/capability-review/g7-machine-access.md): change a document by
|
|
5
|
+
// APPENDING to an overlay, not by rewriting the file.
|
|
6
|
+
//
|
|
7
|
+
// This is the stage that needs no rewriter. An overlay entry is just
|
|
8
|
+
// another conjunct, and unification is order-independent, so appending
|
|
9
|
+
// `services: auth: owner: "identity-2"` to a second file and
|
|
10
|
+
// evaluating both is exactly the same value as writing it into the
|
|
11
|
+
// first — with no parsing of the target, no comment or layout damage,
|
|
12
|
+
// and nothing to preserve. The spec pins that equivalence rather than
|
|
13
|
+
// asserting it.
|
|
14
|
+
//
|
|
15
|
+
// What an overlay CANNOT do is change a PINNED value: the lattice
|
|
16
|
+
// refuses 5 against 3, and the report says so with the pinning site,
|
|
17
|
+
// which `why` then locates. That left the loop "set → conflict → why →
|
|
18
|
+
// edit the pinning site" with its last step manual — and since the
|
|
19
|
+
// commonest vet failure of all is "the data pins the wrong value",
|
|
20
|
+
// `set` was unable to repair the very case it existed for.
|
|
21
|
+
//
|
|
22
|
+
// IN-PLACE REPLACE (`--in-place`) closes that. The G7 design deferred
|
|
23
|
+
// it behind two prerequisites: an evaluated-path → contributing-span
|
|
24
|
+
// map, and a comment-and-layout-preserving CST. The first now exists —
|
|
25
|
+
// `why` is that map, and sites carry `len` and `src` since a site was
|
|
26
|
+
// given an extent. The second turns out NOT to be needed for the case
|
|
27
|
+
// that matters, and the reason is worth stating: a CST is what you need
|
|
28
|
+
// to RE-SERIALISE a document, and a targeted span splice serialises
|
|
29
|
+
// nothing. It replaces `len` code units at one offset and leaves every
|
|
30
|
+
// other byte — every comment, every blank line, every alignment space —
|
|
31
|
+
// exactly as the author left it, because it never looks at them.
|
|
32
|
+
//
|
|
33
|
+
// What makes the splice safe rather than merely plausible is that the
|
|
34
|
+
// site carries `src`, the text it claims to cover. The span is VERIFIED
|
|
35
|
+
// against it before a byte is written, so the corrupting arithmetic
|
|
36
|
+
// this repository has already shipped once — `port: 0x1F` reporting
|
|
37
|
+
// canon `"31"` at column 7, and `(col, canon.length)` writing
|
|
38
|
+
// `port: 5x1F` — cannot be reached: `0x1F` is four code units and says
|
|
39
|
+
// so, and if the text at the span is anything else the edit is refused
|
|
40
|
+
// rather than guessed.
|
|
41
|
+
//
|
|
42
|
+
// Replace is never WORSE than append. Where the value is not a single
|
|
43
|
+
// editable literal in this overlay — a spread template governing other
|
|
44
|
+
// keys, a reference whose site is the `$` and not the target, two
|
|
45
|
+
// statements pinning the same path, a literal in an included file — the
|
|
46
|
+
// splice is refused and the assignment is APPENDED exactly as it would
|
|
47
|
+
// have been without the flag, plus one `warning` finding naming the
|
|
48
|
+
// case and the site it came from. Warnings never move a verdict, so
|
|
49
|
+
// `--in-place` cannot turn a run that would have succeeded into one
|
|
50
|
+
// that fails; it can only rewrite where rewriting is safe, and explain
|
|
51
|
+
// itself where it is not.
|
|
52
|
+
//
|
|
53
|
+
// The verdict is G2's, unchanged: `vet(entry, overlay)` already asks
|
|
54
|
+
// exactly the right question — does this document hold against that
|
|
55
|
+
// truth, and if not, where — so `set` adds a writer, not a report.
|
|
56
|
+
|
|
57
|
+
import { vet } from './vet'
|
|
58
|
+
import type { TrustOptions } from './type'
|
|
59
|
+
import type { VetFinding, VetReport, VetVerdict } from './vet'
|
|
60
|
+
import { pathParts, why } from './query'
|
|
61
|
+
import type { WhyConjunct } from './provenance'
|
|
62
|
+
import { Aontu } from './aontu'
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
export type PatchOptions = {
|
|
66
|
+
// Where each document CAME FROM, so relative `@"file"` loads inside
|
|
67
|
+
// them resolve from their own directories (vet's precedent).
|
|
68
|
+
entryPath?: string
|
|
69
|
+
overlayPath?: string
|
|
70
|
+
// Rewrite a pinned literal where the author wrote it, instead of
|
|
71
|
+
// appending a line that contradicts it. Opt-in: appending is
|
|
72
|
+
// non-destructive and in-place editing is not, so the caller says
|
|
73
|
+
// which one they meant.
|
|
74
|
+
inPlace?: boolean
|
|
75
|
+
// The include capability this document evaluates under
|
|
76
|
+
// (G5, docs/trust.md); vet's precedent.
|
|
77
|
+
trust?: TrustOptions
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
// One literal rewritten where it was written. `from` and `to` are
|
|
82
|
+
// SOURCE TEXT, not values: replacing `0x1F` with `31` is a different
|
|
83
|
+
// edit from replacing it with `0x1F`, and only the spelling says which.
|
|
84
|
+
export type PatchReplacement = {
|
|
85
|
+
col: number
|
|
86
|
+
file: string
|
|
87
|
+
from: string
|
|
88
|
+
path: string
|
|
89
|
+
row: number
|
|
90
|
+
to: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type PatchReport = {
|
|
94
|
+
// The overlay text as it would stand after the assignments: the
|
|
95
|
+
// existing text, with any in-place replacements applied, plus one
|
|
96
|
+
// appended line for each assignment that was not replaced. The caller
|
|
97
|
+
// writes it — an engine that touched the filesystem could not be used
|
|
98
|
+
// by a server, and the CLI is the one place that knows about files.
|
|
99
|
+
overlay: string
|
|
100
|
+
// The appended lines alone, in order.
|
|
101
|
+
appended: string[]
|
|
102
|
+
// The in-place replacements made, in the order the assignments were
|
|
103
|
+
// given (NOT the order they were applied to the text, which is
|
|
104
|
+
// back-to-front so that earlier offsets stay valid). Empty unless
|
|
105
|
+
// `inPlace` was asked for.
|
|
106
|
+
replaced: PatchReplacement[]
|
|
107
|
+
verdict: VetVerdict
|
|
108
|
+
findings: VetFinding[]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
// An assignment is `<path>=<value>`, split at the FIRST `=`: a path
|
|
113
|
+
// segment is a name, and the value is arbitrary Aontu source, which
|
|
114
|
+
// may itself contain `=` (`a: min(1)` does not, but a string can).
|
|
115
|
+
export function parseAssignment(
|
|
116
|
+
text: string): { path: string, value: string } | undefined {
|
|
117
|
+
const eq = text.indexOf('=')
|
|
118
|
+
if (eq < 1) {
|
|
119
|
+
return undefined
|
|
120
|
+
}
|
|
121
|
+
const path = text.slice(0, eq).trim()
|
|
122
|
+
const value = text.slice(eq + 1).trim()
|
|
123
|
+
if ('' === value || 0 === pathParts(path).length) {
|
|
124
|
+
return undefined
|
|
125
|
+
}
|
|
126
|
+
return { path, value }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
// The path-flattened conjunct one assignment becomes:
|
|
131
|
+
// `$.a.b = 1` is `"a": "b": 1`. Keys are QUOTED — a segment may be a
|
|
132
|
+
// word the grammar spells otherwise (`if`), a number, or a name with
|
|
133
|
+
// a space in it, and quoting one key is the same value as writing it
|
|
134
|
+
// bare.
|
|
135
|
+
export function overlayLine(path: string, value: string): string {
|
|
136
|
+
return pathParts(path).map((p) => JSON.stringify(p)).join(': ') +
|
|
137
|
+
': ' + value
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
// The character offset of a 1-based (row, col) in `src`, or -1 when the
|
|
142
|
+
// text has no such position. Columns are UTF-16 code units, which is
|
|
143
|
+
// what a site carries and what a JavaScript string index already is —
|
|
144
|
+
// so this is the inverse of the site arithmetic, not a reinterpretation
|
|
145
|
+
// of it (go/patch.go converts to a byte offset, because Go strings are
|
|
146
|
+
// bytes; both address the same character).
|
|
147
|
+
export function offsetAt(src: string, row: number, col: number): number {
|
|
148
|
+
if (row < 1 || col < 1) {
|
|
149
|
+
return -1
|
|
150
|
+
}
|
|
151
|
+
let off = 0
|
|
152
|
+
for (let r = 1; r < row; r++) {
|
|
153
|
+
const nl = src.indexOf('\n', off)
|
|
154
|
+
if (nl < 0) {
|
|
155
|
+
return -1
|
|
156
|
+
}
|
|
157
|
+
off = nl + 1
|
|
158
|
+
}
|
|
159
|
+
const at = off + (col - 1)
|
|
160
|
+
return at <= src.length ? at : -1
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
// The text a site covers, or undefined when the site does not describe
|
|
165
|
+
// a position in this text at all.
|
|
166
|
+
export function spanAt(
|
|
167
|
+
src: string, site: { row: number, col: number, len: number }
|
|
168
|
+
): string | undefined {
|
|
169
|
+
const off = offsetAt(src, site.row, site.col)
|
|
170
|
+
return off < 0 ? undefined : src.slice(off, off + site.len)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
// DOES THE TEXT AT THIS SITE SAY WHAT THE SITE CLAIMS IT SAYS?
|
|
175
|
+
//
|
|
176
|
+
// The last check before a splice, and the one that makes the write
|
|
177
|
+
// PROVABLE rather than argued. Exported so it can be exercised with a
|
|
178
|
+
// site the engine would never produce — an out-of-range position, a
|
|
179
|
+
// span over different text — which is the only way to test a guard whose
|
|
180
|
+
// whole purpose is to catch a state the rest of the code says cannot
|
|
181
|
+
// happen. (go/patch.go has the twin, tested the same way.)
|
|
182
|
+
export function spanHolds(
|
|
183
|
+
src: string, site: { row: number, col: number, len: number }, expect: string
|
|
184
|
+
): boolean {
|
|
185
|
+
// THE SITE'S OWN LENGTH IS PART OF ITS CLAIM, and is checked before
|
|
186
|
+
// the text is. A site whose `len` disagrees with the text it says it
|
|
187
|
+
// covers CONTRADICTS ITSELF, which is exactly the state this guard
|
|
188
|
+
// exists to catch — and a zero-length span would otherwise compare
|
|
189
|
+
// equal against nothing and then splice nothing, INSERTING the new
|
|
190
|
+
// value rather than replacing anything.
|
|
191
|
+
//
|
|
192
|
+
// Both ports compare in UTF-16 code units, which is what a site's
|
|
193
|
+
// `len` counts. That is free here and is not in Go, where a string is
|
|
194
|
+
// bytes (go/patch.go converts).
|
|
195
|
+
if ('' === expect || site.len !== expect.length) {
|
|
196
|
+
return false
|
|
197
|
+
}
|
|
198
|
+
return spanAt(src, site) === expect
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
// WHY IS THE VALUE AT THIS PATH WHAT IT IS, and is exactly one of the
|
|
203
|
+
// answers a literal this overlay can edit in place?
|
|
204
|
+
//
|
|
205
|
+
// The four refusals below are not defensive padding; each is a real
|
|
206
|
+
// document shape that the probe corpus produced, and each would corrupt
|
|
207
|
+
// something different if the splice ran anyway:
|
|
208
|
+
//
|
|
209
|
+
// - a SPREAD contribution's site is inside the template, which
|
|
210
|
+
// governs every other key too, so rewriting it there changes keys
|
|
211
|
+
// the author did not name;
|
|
212
|
+
// - a REFERENCE's site is the `$` that starts the path and has length
|
|
213
|
+
// 1, so splicing over it writes the new value INTO the path
|
|
214
|
+
// expression (`$.base` becomes `5.base`) — and the value the author
|
|
215
|
+
// wants changed lives at the target anyway;
|
|
216
|
+
// - TWO literals at one path (a duplicate key, two files merged) give
|
|
217
|
+
// no single place to edit, and picking either silently is picking
|
|
218
|
+
// for the author;
|
|
219
|
+
// - a literal in an INCLUDED file is editable, but not by
|
|
220
|
+
// `--overlay <this file>`: the write would land in a document the
|
|
221
|
+
// caller did not name.
|
|
222
|
+
//
|
|
223
|
+
// A PREFERENCE is not refused here for the same reason it is not
|
|
224
|
+
// replaced: appending already overrides a default correctly, so the
|
|
225
|
+
// caller loses nothing by falling through to it.
|
|
226
|
+
function editableLiteral(
|
|
227
|
+
overlaySrc: string,
|
|
228
|
+
path: string,
|
|
229
|
+
overlayPath: string | undefined,
|
|
230
|
+
): { site: PatchReplacement | undefined, finding: VetFinding | undefined } {
|
|
231
|
+
// THE AUTHORITY IS THE OVERLAY TEXT ALONE, WITH INCLUDES DENIED.
|
|
232
|
+
//
|
|
233
|
+
// The splice happens in the text this function was handed, so what it
|
|
234
|
+
// has to establish is that the contribution is IN that text — and the
|
|
235
|
+
// site's `file` cannot establish it. Two ways it fails: a caller of
|
|
236
|
+
// the library API need not pass `overlayPath`, leaving nothing to
|
|
237
|
+
// compare against; and the Go port names the ENTRY document for an
|
|
238
|
+
// included value anyway (issue #66), so the comparison is the overlay
|
|
239
|
+
// against itself. Either way an included literal's (row, col, len,
|
|
240
|
+
// src) can COINCIDE with different text at the same coordinates here
|
|
241
|
+
// — an include holding `a: 42` at 1:4 and an overlay holding `x: 42`
|
|
242
|
+
// at 1:4 — and the span verification cannot tell them apart, because
|
|
243
|
+
// the text really does match. The splice then rewrites `x` while
|
|
244
|
+
// reporting a replacement of `$.a`, in both ports.
|
|
245
|
+
//
|
|
246
|
+
// Denying includes removes the ambiguity at its source rather than
|
|
247
|
+
// detecting it: what resolves is what this text says by itself. An
|
|
248
|
+
// overlay that loads other documents therefore cannot be edited in
|
|
249
|
+
// place at all — the conservative answer, and the assignment still
|
|
250
|
+
// appends. It costs nothing in the shape `set` is for, an overlay it
|
|
251
|
+
// owns and appends to, and it does not depend on file attribution, so
|
|
252
|
+
// both ports agree without waiting on #66.
|
|
253
|
+
const alone = why(overlaySrc, path, {
|
|
254
|
+
trust: { include: 'none' },
|
|
255
|
+
...(null == overlayPath ? {} : { path: overlayPath }),
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
if (true !== alone.ok || null == alone.record) {
|
|
259
|
+
// Nothing here BY ITSELF. Two very different reasons, and they earn
|
|
260
|
+
// different answers: the path may simply not be in this overlay, in
|
|
261
|
+
// which case appending is the whole of the answer and nothing has
|
|
262
|
+
// gone wrong — or it may be here only because something was loaded,
|
|
263
|
+
// which is the case above and has to say so.
|
|
264
|
+
const withLoads = why(overlaySrc, path,
|
|
265
|
+
null == overlayPath ? undefined : { path: overlayPath })
|
|
266
|
+
if (true !== withLoads.ok || null == withLoads.record) {
|
|
267
|
+
return { site: undefined, finding: undefined }
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
site: undefined,
|
|
271
|
+
finding: notEditable('patch_not_editable', path,
|
|
272
|
+
'this path resolves only once the overlay loads another ' +
|
|
273
|
+
'document, so no literal here can be shown to be the one to ' +
|
|
274
|
+
'edit; run set with the document that writes it as the overlay',
|
|
275
|
+
withLoads.record.conjuncts),
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const record = alone.record
|
|
280
|
+
|
|
281
|
+
// No `?? []`: WhyRecord.conjuncts is a non-optional array and the
|
|
282
|
+
// record's own presence was just established, so a fallback here
|
|
283
|
+
// would claim a possibility the type does not have — and the
|
|
284
|
+
// coverage gate says so, an arm nothing can take.
|
|
285
|
+
const conjuncts: WhyConjunct[] = record.conjuncts
|
|
286
|
+
|
|
287
|
+
// A VALUE REACHED THROUGH A REFERENCE IS NOT THIS PATH'S TO EDIT.
|
|
288
|
+
// Provenance travels through clones now, so `n: $.base` against
|
|
289
|
+
// `base: 7` reports the literal `7` -- correctly, and at the site
|
|
290
|
+
// where it was written, which is `base`'s line and not `n`'s. A
|
|
291
|
+
// splice there would rewrite the REFERENT: every other reader of
|
|
292
|
+
// `$.base` changes with it, and the path the caller named does not
|
|
293
|
+
// move at all. The reference is what stands here, so the reference
|
|
294
|
+
// is what has to be edited, wherever it points.
|
|
295
|
+
const refs = conjuncts.filter((c) => 'ref' === c.role)
|
|
296
|
+
if (0 < refs.length) {
|
|
297
|
+
return {
|
|
298
|
+
site: undefined,
|
|
299
|
+
finding: notEditable('patch_not_editable', path,
|
|
300
|
+
'the value here is reached through a reference (ref), so the ' +
|
|
301
|
+
'literal below belongs to the path it points at; edit where it ' +
|
|
302
|
+
'comes from',
|
|
303
|
+
refs),
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const literals = conjuncts.filter((c) => 'literal' === c.role)
|
|
308
|
+
|
|
309
|
+
if (1 < literals.length) {
|
|
310
|
+
return {
|
|
311
|
+
site: undefined,
|
|
312
|
+
finding: notEditable('patch_ambiguous', path,
|
|
313
|
+
'two or more statements pin this path, so there is no single ' +
|
|
314
|
+
'place to edit; the sites below are all of them',
|
|
315
|
+
literals),
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Only indirect contributions. A pref is the benign case — append
|
|
320
|
+
// overrides a default — so it earns no finding; the others do.
|
|
321
|
+
if (0 === literals.length) {
|
|
322
|
+
const indirect = conjuncts.filter((c) => 'pref' !== c.role)
|
|
323
|
+
if (0 === indirect.length) {
|
|
324
|
+
return { site: undefined, finding: undefined }
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
site: undefined,
|
|
328
|
+
finding: notEditable('patch_not_editable', path,
|
|
329
|
+
'the value here is not written as a literal (' +
|
|
330
|
+
indirect.map((c) => c.role).join(', ') +
|
|
331
|
+
'), so there is no literal to rewrite; edit where it comes from',
|
|
332
|
+
indirect),
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const one = literals[0]
|
|
337
|
+
|
|
338
|
+
// THE SPAN MUST CHECK OUT, and this is one condition rather than two.
|
|
339
|
+
//
|
|
340
|
+
// A first draft tested "no extent" separately from "the text
|
|
341
|
+
// disagrees", which read as two guards and was really one question
|
|
342
|
+
// asked twice — with the second half unreachable, since denying
|
|
343
|
+
// includes means the site comes from evaluating THIS TEXT with
|
|
344
|
+
// nothing loaded, so its coordinates describe this text by
|
|
345
|
+
// construction. Merged, the question is reachable through the case
|
|
346
|
+
// that has no extent at all (`x: hello |> upper` synthesises a call
|
|
347
|
+
// the parser never sited), so the check is exercised rather than
|
|
348
|
+
// argued for — and `spanHolds` is exported and tested against sites
|
|
349
|
+
// the engine would never produce, which is the only way to reach the
|
|
350
|
+
// half that remains theoretical.
|
|
351
|
+
//
|
|
352
|
+
// It is load-bearing either way: a contribution with no `src` would
|
|
353
|
+
// otherwise splice ZERO characters, INSERTING the new value into the
|
|
354
|
+
// middle of a line instead of replacing anything.
|
|
355
|
+
if (!spanHolds(overlaySrc, one.site, one.src)) {
|
|
356
|
+
return {
|
|
357
|
+
site: undefined,
|
|
358
|
+
finding: notEditable('patch_span_mismatch', path,
|
|
359
|
+
'the overlay does not hold ' + JSON.stringify(one.src) + ' at ' +
|
|
360
|
+
one.site.row + ':' + one.site.col + ' (len ' + one.site.len +
|
|
361
|
+
'), so the span cannot be verified before writing',
|
|
362
|
+
[one]),
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// DOES THE SPAN MEAN THE WHOLE CONTRIBUTION?
|
|
367
|
+
//
|
|
368
|
+
// This is the check that `role === 'literal'` looks like it makes and
|
|
369
|
+
// does not. A site names the TOKEN it points at, so a COMPOUND value
|
|
370
|
+
// reports its OPENING token while its canon is the whole thing:
|
|
371
|
+
// `min(1)` is a literal-role contribution whose src is `min`, `1+2`
|
|
372
|
+
// reports `1`, `$.k+1` reports `$`, `{b:1}` reports `{` and `[1,2]`
|
|
373
|
+
// reports `[`. Splicing over any of those writes the new value INTO
|
|
374
|
+
// the expression — `a: 5(1)`, `a: 5+2`, `a: 5.k+1` — which is the
|
|
375
|
+
// same class of corruption as the canon-length arithmetic, reached by
|
|
376
|
+
// a different route.
|
|
377
|
+
//
|
|
378
|
+
// Rather than enumerate the shapes (a list is a thing to be
|
|
379
|
+
// incomplete about), ASK THE ENGINE: parse `src` on its own and
|
|
380
|
+
// require the value it means to be the value the contribution
|
|
381
|
+
// contributed. That is exactly the property a splice needs — this
|
|
382
|
+
// text, alone, is this value — and it is decided by the same unifier
|
|
383
|
+
// that produced the contribution, so it cannot drift from it.
|
|
384
|
+
//
|
|
385
|
+
// It also gets the interesting case right without special-casing it:
|
|
386
|
+
// `0x1F` canons to `31`, which is not its own spelling, but IS the
|
|
387
|
+
// contribution's canon, so a hex literal is editable while `min` is
|
|
388
|
+
// not.
|
|
389
|
+
const span = spanValue(one.src)
|
|
390
|
+
if (null == span || span.canon !== one.canon) {
|
|
391
|
+
return {
|
|
392
|
+
site: undefined,
|
|
393
|
+
finding: notEditable('patch_not_editable', path,
|
|
394
|
+
'the site names ' + JSON.stringify(one.src) + ', which is the ' +
|
|
395
|
+
'opening token of ' + one.canon + ' rather than the whole of ' +
|
|
396
|
+
'it; rewriting that span would edit the expression, not the ' +
|
|
397
|
+
'value',
|
|
398
|
+
[one]),
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// AN ABSTRACT CONTRIBUTION IS NOT A PIN. `a: integer` and
|
|
403
|
+
// `a: above(0)` state a constraint, and appending already narrows
|
|
404
|
+
// them — that is the one case the status report notes `set` could
|
|
405
|
+
// always repair. Replacing them would silently DISCARD a constraint
|
|
406
|
+
// the author wrote, to no benefit, so this falls through to append.
|
|
407
|
+
if (true !== span.concrete) {
|
|
408
|
+
return {
|
|
409
|
+
site: undefined,
|
|
410
|
+
finding: notEditable('patch_not_editable', path,
|
|
411
|
+
one.canon + ' is a constraint here, not a pinned value; ' +
|
|
412
|
+
'appending narrows it without discarding what it says',
|
|
413
|
+
[one]),
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
site: {
|
|
419
|
+
col: one.site.col,
|
|
420
|
+
file: one.site.file,
|
|
421
|
+
from: one.src,
|
|
422
|
+
path,
|
|
423
|
+
row: one.site.row,
|
|
424
|
+
to: '',
|
|
425
|
+
},
|
|
426
|
+
finding: undefined,
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
// What does this source text mean ON ITS OWN, and is it a value rather
|
|
432
|
+
// than a constraint? Undefined when it does not stand alone at all
|
|
433
|
+
// (`$` from a path, an unbalanced `{`).
|
|
434
|
+
//
|
|
435
|
+
// The wrapper key is arbitrary and the document it makes is thrown
|
|
436
|
+
// away; what is wanted is the unifier's own reading of the fragment.
|
|
437
|
+
export function spanValue(
|
|
438
|
+
src: string): { canon: string, concrete: boolean } | undefined {
|
|
439
|
+
// NO COLLECTING CONTEXT: `unify` THROWS on a source it cannot read,
|
|
440
|
+
// so a ctx.err check here is a branch nothing can reach — the catch
|
|
441
|
+
// below is the only path a bad fragment takes. (A first draft had
|
|
442
|
+
// both, and the coverage gate called the pair what it was.) What the
|
|
443
|
+
// nil test still earns is the fragment that PARSES and means nothing:
|
|
444
|
+
// `$` is a path with no target, and answers a nil rather than
|
|
445
|
+
// throwing.
|
|
446
|
+
let canon: string
|
|
447
|
+
try {
|
|
448
|
+
const root: any = new Aontu().unify('v: ' + src)
|
|
449
|
+
const node: any = root?.peg?.['v']
|
|
450
|
+
if (null == node || true === node.isNil) {
|
|
451
|
+
return undefined
|
|
452
|
+
}
|
|
453
|
+
canon = node.canon
|
|
454
|
+
}
|
|
455
|
+
catch (e) {
|
|
456
|
+
return undefined
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Generability is the concreteness test, and it is the engine's own:
|
|
460
|
+
// a kind, a constraint and an unresolved disjunction all refuse to
|
|
461
|
+
// generate, which is precisely the line this needs drawn.
|
|
462
|
+
try {
|
|
463
|
+
new Aontu().generate('v: ' + src)
|
|
464
|
+
}
|
|
465
|
+
catch (e) {
|
|
466
|
+
return { canon, concrete: false }
|
|
467
|
+
}
|
|
468
|
+
return { canon, concrete: true }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
// A refusal to replace, as a WARNING: the assignment still appends, so
|
|
473
|
+
// nothing about the run got worse and the verdict must not move
|
|
474
|
+
// (ts/src/vet.ts, "warnings never touch the verdict"). What the finding
|
|
475
|
+
// adds is the reason, which is the whole value of asking for --in-place
|
|
476
|
+
// over plain set.
|
|
477
|
+
function notEditable(
|
|
478
|
+
code: string, path: string, why: string, from: WhyConjunct[]
|
|
479
|
+
): VetFinding {
|
|
480
|
+
return {
|
|
481
|
+
code,
|
|
482
|
+
class: 'patch_span_mismatch' === code ? 'internal' : 'reference',
|
|
483
|
+
severity: 'warning',
|
|
484
|
+
path,
|
|
485
|
+
// No separate `note`: the renderer prints both, and a note that
|
|
486
|
+
// restates its own message is noise wearing a second label.
|
|
487
|
+
message: 'cannot rewrite ' + path + ' in place: ' + why,
|
|
488
|
+
sites: from.map((c) => ({
|
|
489
|
+
file: c.site.file,
|
|
490
|
+
row: c.site.row,
|
|
491
|
+
col: c.site.col,
|
|
492
|
+
len: c.site.len,
|
|
493
|
+
src: c.src,
|
|
494
|
+
role: 'data',
|
|
495
|
+
value: c.canon,
|
|
496
|
+
})) as any,
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
// Append the assignments to the overlay and answer what the result
|
|
502
|
+
// holds. The report's verdict is the vet verdict of the ENTRY against
|
|
503
|
+
// the new overlay: `valid` when it holds and is concrete, `incomplete`
|
|
504
|
+
// when nothing contradicts but the truth is not yet satisfied,
|
|
505
|
+
// `invalid` when the overlay contradicts a pinned value, `error` when
|
|
506
|
+
// the entry itself does not stand up.
|
|
507
|
+
export function patch(
|
|
508
|
+
entrySrc: string,
|
|
509
|
+
overlaySrc: string,
|
|
510
|
+
assignments: string[],
|
|
511
|
+
opts?: PatchOptions,
|
|
512
|
+
): PatchReport {
|
|
513
|
+
const options = opts ?? {}
|
|
514
|
+
|
|
515
|
+
const appended: string[] = []
|
|
516
|
+
const replaced: PatchReplacement[] = []
|
|
517
|
+
const notes: VetFinding[] = []
|
|
518
|
+
// Each pending edit as (offset, length, text). Collected first and
|
|
519
|
+
// applied last, back to front: a splice shifts every offset after it,
|
|
520
|
+
// and recomputing them per edit is a way to be subtly wrong for free.
|
|
521
|
+
const edits: { at: number, len: number, to: string }[] = []
|
|
522
|
+
|
|
523
|
+
for (const text of assignments) {
|
|
524
|
+
const a = parseAssignment(text)
|
|
525
|
+
if (null == a) {
|
|
526
|
+
return {
|
|
527
|
+
overlay: overlaySrc,
|
|
528
|
+
appended: [],
|
|
529
|
+
replaced: [],
|
|
530
|
+
verdict: 'error',
|
|
531
|
+
findings: [{
|
|
532
|
+
code: 'patch_assignment',
|
|
533
|
+
class: 'parse',
|
|
534
|
+
severity: 'error',
|
|
535
|
+
path: '$',
|
|
536
|
+
message: `Not a <path>=<value> assignment: ${text}`,
|
|
537
|
+
sites: [],
|
|
538
|
+
}],
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (true === options.inPlace) {
|
|
543
|
+
const found = editableLiteral(overlaySrc, a.path, options.overlayPath)
|
|
544
|
+
if (null != found.finding) {
|
|
545
|
+
notes.push(found.finding)
|
|
546
|
+
}
|
|
547
|
+
if (null != found.site) {
|
|
548
|
+
// Two assignments naming the same path would splice the same
|
|
549
|
+
// span twice. The second is the one the author wrote last, so
|
|
550
|
+
// it wins — and the first is dropped rather than layered.
|
|
551
|
+
const at = offsetAt(overlaySrc, found.site.row, found.site.col)
|
|
552
|
+
const dup = edits.findIndex((e) => e.at === at)
|
|
553
|
+
const edit = { at, len: found.site.from.length, to: a.value }
|
|
554
|
+
if (dup < 0) {
|
|
555
|
+
edits.push(edit)
|
|
556
|
+
replaced.push({ ...found.site, to: a.value })
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
edits[dup] = edit
|
|
560
|
+
replaced[dup] = { ...found.site, to: a.value }
|
|
561
|
+
}
|
|
562
|
+
continue
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
appended.push(overlayLine(a.path, a.value))
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const overlay = joinOverlay(applyEdits(overlaySrc, edits), appended)
|
|
570
|
+
|
|
571
|
+
// The file names ride as URLs as well as base paths, so a finding
|
|
572
|
+
// names the entry and the overlay rather than vet's generic
|
|
573
|
+
// `schema`/`data` labels — with two documents that both belong to
|
|
574
|
+
// the caller, "which file" is the whole question.
|
|
575
|
+
const report: VetReport = vet(entrySrc, overlay, {
|
|
576
|
+
trust: options.trust,
|
|
577
|
+
schemaPath: options.entryPath,
|
|
578
|
+
dataPath: options.overlayPath,
|
|
579
|
+
schemaUrl: options.entryPath,
|
|
580
|
+
dataUrl: options.overlayPath,
|
|
581
|
+
} as any)
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
overlay,
|
|
585
|
+
appended,
|
|
586
|
+
replaced,
|
|
587
|
+
verdict: report.verdict,
|
|
588
|
+
// The refusals come FIRST: they explain why the run took the shape
|
|
589
|
+
// it did, and a reader who stops after the first finding should
|
|
590
|
+
// read that rather than a conflict it predicted.
|
|
591
|
+
findings: notes.concat(report.findings),
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
// Apply the collected splices back to front, so an earlier edit's
|
|
597
|
+
// offset is never invalidated by a later one having already run.
|
|
598
|
+
function applyEdits(
|
|
599
|
+
src: string, edits: { at: number, len: number, to: string }[]
|
|
600
|
+
): string {
|
|
601
|
+
if (0 === edits.length) {
|
|
602
|
+
return src
|
|
603
|
+
}
|
|
604
|
+
let out = src
|
|
605
|
+
for (const e of [...edits].sort((x, y) => y.at - x.at)) {
|
|
606
|
+
out = out.slice(0, e.at) + e.to + out.slice(e.at + e.len)
|
|
607
|
+
}
|
|
608
|
+
return out
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
// One line per assignment, after whatever the overlay already said. A
|
|
613
|
+
// trailing newline is kept when the file had one and added when it
|
|
614
|
+
// did not: a file that does not end in a newline is still a file, and
|
|
615
|
+
// appending to it must not join two entries into one line.
|
|
616
|
+
function joinOverlay(overlaySrc: string, appended: string[]): string {
|
|
617
|
+
if (0 === appended.length) {
|
|
618
|
+
return overlaySrc
|
|
619
|
+
}
|
|
620
|
+
const head = '' === overlaySrc || overlaySrc.endsWith('\n')
|
|
621
|
+
? overlaySrc
|
|
622
|
+
: overlaySrc + '\n'
|
|
623
|
+
return head + appended.join('\n') + '\n'
|
|
624
|
+
}
|