@chances-ai/engine 28.0.0 → 30.0.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/dist/agents/index.js +1 -1
- package/dist/ai/adapters/ai-sdk-stream.js +15 -0
- package/dist/ai/adapters/ai-sdk-stream.js.map +1 -1
- package/dist/ai/adapters/ai-sdk.js +1 -1
- package/dist/ai/adapters/ai-sdk.js.map +1 -1
- package/dist/ai/setup.d.ts +1 -1
- package/dist/ai/setup.js +1 -1
- package/dist/ai/types.d.ts +13 -1
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/core/engine.d.ts +17 -3
- package/dist/core/engine.d.ts.map +1 -1
- package/dist/core/engine.js +49 -3
- package/dist/core/engine.js.map +1 -1
- package/dist/core/task-tool.d.ts +1 -1
- package/dist/core/task-tool.js +1 -1
- package/dist/core/workspace-query.d.ts +1 -1
- package/dist/core/workspace-query.js +1 -1
- package/dist/hashline/apply.d.ts +9 -0
- package/dist/hashline/apply.d.ts.map +1 -0
- package/dist/hashline/apply.js +523 -0
- package/dist/hashline/apply.js.map +1 -0
- package/dist/hashline/block.d.ts +25 -0
- package/dist/hashline/block.d.ts.map +1 -0
- package/dist/hashline/block.js +71 -0
- package/dist/hashline/block.js.map +1 -0
- package/dist/hashline/format.d.ts +106 -0
- package/dist/hashline/format.d.ts.map +1 -0
- package/dist/hashline/format.js +191 -0
- package/dist/hashline/format.js.map +1 -0
- package/dist/hashline/fs.d.ts +87 -0
- package/dist/hashline/fs.d.ts.map +1 -0
- package/dist/hashline/fs.js +123 -0
- package/dist/hashline/fs.js.map +1 -0
- package/dist/hashline/index.d.ts +27 -0
- package/dist/hashline/index.d.ts.map +1 -0
- package/dist/hashline/index.js +27 -0
- package/dist/hashline/index.js.map +1 -0
- package/dist/hashline/input.d.ts +101 -0
- package/dist/hashline/input.d.ts.map +1 -0
- package/dist/hashline/input.js +378 -0
- package/dist/hashline/input.js.map +1 -0
- package/dist/hashline/messages.d.ts +87 -0
- package/dist/hashline/messages.d.ts.map +1 -0
- package/dist/hashline/messages.js +94 -0
- package/dist/hashline/messages.js.map +1 -0
- package/dist/hashline/mismatch.d.ts +45 -0
- package/dist/hashline/mismatch.d.ts.map +1 -0
- package/dist/hashline/mismatch.js +118 -0
- package/dist/hashline/mismatch.js.map +1 -0
- package/dist/hashline/normalize.d.ts +22 -0
- package/dist/hashline/normalize.d.ts.map +1 -0
- package/dist/hashline/normalize.js +31 -0
- package/dist/hashline/normalize.js.map +1 -0
- package/dist/hashline/parser.d.ts +24 -0
- package/dist/hashline/parser.d.ts.map +1 -0
- package/dist/hashline/parser.js +295 -0
- package/dist/hashline/parser.js.map +1 -0
- package/dist/hashline/patcher.d.ts +111 -0
- package/dist/hashline/patcher.d.ts.map +1 -0
- package/dist/hashline/patcher.js +332 -0
- package/dist/hashline/patcher.js.map +1 -0
- package/dist/hashline/recovery.d.ts +41 -0
- package/dist/hashline/recovery.d.ts.map +1 -0
- package/dist/hashline/recovery.js +175 -0
- package/dist/hashline/recovery.js.map +1 -0
- package/dist/hashline/snapshots.d.ts +62 -0
- package/dist/hashline/snapshots.d.ts.map +1 -0
- package/dist/hashline/snapshots.js +127 -0
- package/dist/hashline/snapshots.js.map +1 -0
- package/dist/hashline/tokenizer.d.ts +66 -0
- package/dist/hashline/tokenizer.d.ts.map +1 -0
- package/dist/hashline/tokenizer.js +408 -0
- package/dist/hashline/tokenizer.js.map +1 -0
- package/dist/hashline/types.d.ts +117 -0
- package/dist/hashline/types.d.ts.map +1 -0
- package/dist/hashline/types.js +13 -0
- package/dist/hashline/types.js.map +1 -0
- package/dist/local-vault/index.d.ts +1 -1
- package/dist/local-vault/index.js +1 -1
- package/dist/lsp/index.d.ts +1 -1
- package/dist/lsp/index.js +1 -1
- package/dist/lsp/types.d.ts +2 -2
- package/dist/mcp/host.d.ts +3 -3
- package/dist/mcp/load-mcp-host.d.ts +4 -4
- package/dist/mcp/load-mcp-host.js +4 -4
- package/dist/mcp/oauth/provider.d.ts.map +1 -1
- package/dist/mcp/oauth/provider.js +10 -1
- package/dist/mcp/oauth/provider.js.map +1 -1
- package/dist/plugin-api/index.d.ts +10 -2
- package/dist/plugin-api/index.d.ts.map +1 -1
- package/dist/plugin-api/index.js +15 -0
- package/dist/plugin-api/index.js.map +1 -1
- package/dist/tools/approval.d.ts +1 -1
- package/dist/tools/approval.js +1 -1
- package/dist/tools/builtins/_hashline-fs.d.ts +16 -0
- package/dist/tools/builtins/_hashline-fs.d.ts.map +1 -0
- package/dist/tools/builtins/_hashline-fs.js +62 -0
- package/dist/tools/builtins/_hashline-fs.js.map +1 -0
- package/dist/tools/builtins/_image.d.ts +26 -0
- package/dist/tools/builtins/_image.d.ts.map +1 -0
- package/dist/tools/builtins/_image.js +76 -0
- package/dist/tools/builtins/_image.js.map +1 -0
- package/dist/tools/builtins/_login-shell.d.ts +17 -0
- package/dist/tools/builtins/_login-shell.d.ts.map +1 -0
- package/dist/tools/builtins/_login-shell.js +66 -0
- package/dist/tools/builtins/_login-shell.js.map +1 -0
- package/dist/tools/builtins/_notebook.d.ts +15 -0
- package/dist/tools/builtins/_notebook.d.ts.map +1 -0
- package/dist/tools/builtins/_notebook.js +81 -0
- package/dist/tools/builtins/_notebook.js.map +1 -0
- package/dist/tools/builtins/_pdf.d.ts +19 -0
- package/dist/tools/builtins/_pdf.d.ts.map +1 -0
- package/dist/tools/builtins/_pdf.js +42 -0
- package/dist/tools/builtins/_pdf.js.map +1 -0
- package/dist/tools/builtins/_shared.d.ts +11 -0
- package/dist/tools/builtins/_shared.d.ts.map +1 -1
- package/dist/tools/builtins/_shared.js +40 -1
- package/dist/tools/builtins/_shared.js.map +1 -1
- package/dist/tools/builtins/ast-edit.d.ts +18 -0
- package/dist/tools/builtins/ast-edit.d.ts.map +1 -0
- package/dist/tools/builtins/ast-edit.js +109 -0
- package/dist/tools/builtins/ast-edit.js.map +1 -0
- package/dist/tools/builtins/ast-grep.d.ts +6 -0
- package/dist/tools/builtins/ast-grep.d.ts.map +1 -0
- package/dist/tools/builtins/ast-grep.js +67 -0
- package/dist/tools/builtins/ast-grep.js.map +1 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +13 -2
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +112 -31
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/lsp.js +1 -1
- package/dist/tools/builtins/pty.d.ts +1 -1
- package/dist/tools/builtins/read.d.ts.map +1 -1
- package/dist/tools/builtins/read.js +187 -11
- package/dist/tools/builtins/read.js.map +1 -1
- package/dist/tools/builtins.d.ts.map +1 -1
- package/dist/tools/builtins.js +4 -0
- package/dist/tools/builtins.js.map +1 -1
- package/dist/tools/file-lock.d.ts +8 -0
- package/dist/tools/file-lock.d.ts.map +1 -1
- package/dist/tools/file-lock.js +22 -0
- package/dist/tools/file-lock.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/types.d.ts +27 -1
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js.map +1 -1
- package/package.json +8 -3
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level patch orchestrator. Reads each section's target file via the
|
|
3
|
+
* configured {@link Filesystem}, strips BOM and normalizes line endings,
|
|
4
|
+
* validates the section snapshot tag (with {@link Recovery}), applies the
|
|
5
|
+
* result back through the same {@link Filesystem}. Ported from oh-my-pi
|
|
6
|
+
* `packages/hashline/src/patcher.ts`.
|
|
7
|
+
*
|
|
8
|
+
* **chances-cli change (7.9 R1-M3):** the live-match integrity check is exact
|
|
9
|
+
* snapshot-text equality (`byHash(path, tag)?.text === live`), NOT hash
|
|
10
|
+
* equality. The 4-hex tag is only a fast index; comparing the stored full text
|
|
11
|
+
* means a 16-bit collision can never silently apply stale line numbers to
|
|
12
|
+
* different live content — it degrades to recovery → forced re-read. The cost
|
|
13
|
+
* is that an edit to a file whose snapshot was evicted from the bounded store
|
|
14
|
+
* (or carried from a prior session) re-reads instead of fast-applying; that is
|
|
15
|
+
* the safe direction.
|
|
16
|
+
*
|
|
17
|
+
* Two layers:
|
|
18
|
+
*
|
|
19
|
+
* - {@link Patcher.apply} — high-level, all-or-nothing. Preflights every
|
|
20
|
+
* section in memory before any write hits disk, then commits in order.
|
|
21
|
+
* - {@link Patcher.prepare} / {@link Patcher.commit} — granular primitives
|
|
22
|
+
* for callers that need per-section control (e.g. the engine `edit` tool,
|
|
23
|
+
* which holds the per-file mutation lock across the whole prepare+commit
|
|
24
|
+
* span — 7.9 R1-M1).
|
|
25
|
+
*
|
|
26
|
+
* Because `prepare` already runs the full apply, a multi-section batch is
|
|
27
|
+
* naturally all-or-nothing: by the time any `commit` runs, every section
|
|
28
|
+
* has been validated.
|
|
29
|
+
*
|
|
30
|
+
* The patcher itself is stateless across calls; reuse one instance per
|
|
31
|
+
* filesystem configuration.
|
|
32
|
+
*/
|
|
33
|
+
import { applyEdits } from "./apply.js";
|
|
34
|
+
import { hasBlockEdit, resolveBlockEdits } from "./block.js";
|
|
35
|
+
import { computeFileHash, formatHashlineHeader, hashTrimsTrailing } from "./format.js";
|
|
36
|
+
import { isNotFound } from "./fs.js";
|
|
37
|
+
import { HEADTAIL_DRIFT_WARNING, missingSnapshotTagMessage } from "./messages.js";
|
|
38
|
+
import { MismatchError } from "./mismatch.js";
|
|
39
|
+
import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize.js";
|
|
40
|
+
import { Recovery } from "./recovery.js";
|
|
41
|
+
/**
|
|
42
|
+
* Opaque token returned by {@link Patcher.prepare}. Carries the section, the
|
|
43
|
+
* raw file content read off disk, and the in-memory apply result.
|
|
44
|
+
* {@link Patcher.commit} just writes the {@link PreparedSection.applyResult}.
|
|
45
|
+
*/
|
|
46
|
+
export class PreparedSection {
|
|
47
|
+
section;
|
|
48
|
+
canonicalPath;
|
|
49
|
+
exists;
|
|
50
|
+
rawContent;
|
|
51
|
+
bom;
|
|
52
|
+
lineEnding;
|
|
53
|
+
normalized;
|
|
54
|
+
applyResult;
|
|
55
|
+
parseWarnings;
|
|
56
|
+
/** @internal */
|
|
57
|
+
constructor(section, canonicalPath, exists, rawContent, bom, lineEnding, normalized, applyResult, parseWarnings) {
|
|
58
|
+
this.section = section;
|
|
59
|
+
this.canonicalPath = canonicalPath;
|
|
60
|
+
this.exists = exists;
|
|
61
|
+
this.rawContent = rawContent;
|
|
62
|
+
this.bom = bom;
|
|
63
|
+
this.lineEnding = lineEnding;
|
|
64
|
+
this.normalized = normalized;
|
|
65
|
+
this.applyResult = applyResult;
|
|
66
|
+
this.parseWarnings = parseWarnings;
|
|
67
|
+
}
|
|
68
|
+
/** Convenience: returns true when the apply produced no change. */
|
|
69
|
+
get isNoop() {
|
|
70
|
+
return this.applyResult.text === this.normalized;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function hasAnchorScopedEdit(edits) {
|
|
74
|
+
return edits.some((edit) => {
|
|
75
|
+
if (edit.kind === "delete")
|
|
76
|
+
return true;
|
|
77
|
+
// A `replace block N:` edit anchors to concrete content on line N.
|
|
78
|
+
if (edit.kind === "block")
|
|
79
|
+
return true;
|
|
80
|
+
return edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor";
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function assertSectionHashPresent(sectionPath, fileHash) {
|
|
84
|
+
if (fileHash !== undefined)
|
|
85
|
+
return;
|
|
86
|
+
throw new Error(missingSnapshotTagMessage(sectionPath));
|
|
87
|
+
}
|
|
88
|
+
function recoveryToApplyResult(result) {
|
|
89
|
+
return {
|
|
90
|
+
text: result.text,
|
|
91
|
+
firstChangedLine: result.firstChangedLine,
|
|
92
|
+
warnings: result.warnings,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function mergeWarnings(...sources) {
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const source of sources) {
|
|
98
|
+
if (!source)
|
|
99
|
+
continue;
|
|
100
|
+
for (const warning of source)
|
|
101
|
+
out.push(warning);
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
function assertUniqueCanonicalPaths(prepared) {
|
|
106
|
+
const seen = new Map();
|
|
107
|
+
for (const entry of prepared) {
|
|
108
|
+
const previous = seen.get(entry.canonicalPath);
|
|
109
|
+
if (previous !== undefined) {
|
|
110
|
+
throw new Error(`Multiple hashline sections resolve to the same file (${previous} and ${entry.section.path}). Merge their ops under one header before applying.`);
|
|
111
|
+
}
|
|
112
|
+
seen.set(entry.canonicalPath, entry.section.path);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* High-level patcher. Wires a {@link Filesystem} and a required
|
|
117
|
+
* {@link SnapshotStore} together with the parsing + applying core.
|
|
118
|
+
*
|
|
119
|
+
* Construct once per FS configuration; reuse across patches.
|
|
120
|
+
*/
|
|
121
|
+
export class Patcher {
|
|
122
|
+
fs;
|
|
123
|
+
snapshots;
|
|
124
|
+
recovery;
|
|
125
|
+
blockResolver;
|
|
126
|
+
constructor(options) {
|
|
127
|
+
if (!options.snapshots) {
|
|
128
|
+
throw new Error("Hashline Patcher requires a SnapshotStore; section tags are opaque store pointers.");
|
|
129
|
+
}
|
|
130
|
+
this.fs = options.fs;
|
|
131
|
+
this.snapshots = options.snapshots;
|
|
132
|
+
this.recovery = new Recovery(options.snapshots);
|
|
133
|
+
this.blockResolver = options.blockResolver;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Apply every section in `patch`. `prepare` runs the full apply for each
|
|
137
|
+
* section in memory before any write hits the filesystem, so a
|
|
138
|
+
* multi-section batch is naturally all-or-nothing. Returns one
|
|
139
|
+
* {@link PatchSectionResult} per section in the original patch order.
|
|
140
|
+
*/
|
|
141
|
+
async apply(patch) {
|
|
142
|
+
// Single-section fast path.
|
|
143
|
+
if (patch.sections.length === 1) {
|
|
144
|
+
const prepared = await this.prepare(patch.sections[0]);
|
|
145
|
+
return { sections: [await this.commit(prepared)] };
|
|
146
|
+
}
|
|
147
|
+
// Prepare every section first so any failure (stale hash, missing
|
|
148
|
+
// file, parse error, in-memory no-op) surfaces before any write.
|
|
149
|
+
const prepared = [];
|
|
150
|
+
for (const section of patch.sections)
|
|
151
|
+
prepared.push(await this.prepare(section));
|
|
152
|
+
assertUniqueCanonicalPaths(prepared);
|
|
153
|
+
for (const entry of prepared) {
|
|
154
|
+
if (entry.isNoop) {
|
|
155
|
+
throw new Error(`Edits to ${entry.section.path} resulted in no changes being made.`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const results = [];
|
|
159
|
+
for (const entry of prepared)
|
|
160
|
+
results.push(await this.commit(entry));
|
|
161
|
+
return { sections: results };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Run the preflight pass only: read, parse, validate, apply-in-memory.
|
|
165
|
+
* No writes hit the filesystem. Use for CI checks and dry runs.
|
|
166
|
+
*/
|
|
167
|
+
async preflight(patch) {
|
|
168
|
+
const prepared = [];
|
|
169
|
+
for (const section of patch.sections)
|
|
170
|
+
prepared.push(await this.prepare(section));
|
|
171
|
+
assertUniqueCanonicalPaths(prepared);
|
|
172
|
+
for (const entry of prepared) {
|
|
173
|
+
if (entry.isNoop) {
|
|
174
|
+
throw new Error(`Edits to ${entry.section.path} resulted in no changes being made.`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Read a section's target file, parse the section, validate the snapshot
|
|
180
|
+
* tag (with recovery), and apply the edits in memory. Returns a
|
|
181
|
+
* {@link PreparedSection} which can be fed to {@link commit} to land
|
|
182
|
+
* the result on the filesystem.
|
|
183
|
+
*
|
|
184
|
+
* Throws on parse error, missing-file-for-anchored-edit, or unrecovered
|
|
185
|
+
* tag mismatch ({@link MismatchError}).
|
|
186
|
+
*/
|
|
187
|
+
async prepare(section) {
|
|
188
|
+
const { edits, warnings: parseWarnings } = section.parse();
|
|
189
|
+
assertSectionHashPresent(section.path, section.fileHash);
|
|
190
|
+
const canonicalPath = this.fs.canonicalPath(section.path);
|
|
191
|
+
await this.fs.preflightWrite(section.path);
|
|
192
|
+
const { exists, rawContent } = await this.#tryRead(section.path);
|
|
193
|
+
if (!exists) {
|
|
194
|
+
throw new Error(`File not found: ${section.path}. Use the write tool to create new files.`);
|
|
195
|
+
}
|
|
196
|
+
const { bom, text } = stripBom(rawContent);
|
|
197
|
+
const lineEnding = detectLineEnding(text);
|
|
198
|
+
const normalized = normalizeToLF(text);
|
|
199
|
+
const applyResult = this.#applyWithRecovery({
|
|
200
|
+
section,
|
|
201
|
+
canonicalPath,
|
|
202
|
+
exists,
|
|
203
|
+
normalized,
|
|
204
|
+
edits,
|
|
205
|
+
});
|
|
206
|
+
return new PreparedSection(section, canonicalPath, exists, rawContent, bom, lineEnding, normalized, applyResult, parseWarnings);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Commit a previously {@link prepare}d section to the filesystem.
|
|
210
|
+
* Restores line endings and BOM, writes via the {@link Filesystem}, and
|
|
211
|
+
* records a fresh snapshot in the {@link SnapshotStore} keyed by the
|
|
212
|
+
* filesystem-canonical path.
|
|
213
|
+
*/
|
|
214
|
+
async commit(prepared) {
|
|
215
|
+
const { section, normalized, bom, lineEnding, parseWarnings, exists, applyResult, canonicalPath } = prepared;
|
|
216
|
+
const after = applyResult.text;
|
|
217
|
+
const warnings = mergeWarnings(parseWarnings, applyResult.warnings);
|
|
218
|
+
if (after === normalized) {
|
|
219
|
+
const hash = this.#recordFullSnapshot(canonicalPath, normalized);
|
|
220
|
+
return {
|
|
221
|
+
path: section.path,
|
|
222
|
+
canonicalPath,
|
|
223
|
+
op: "noop",
|
|
224
|
+
before: normalized,
|
|
225
|
+
after: normalized,
|
|
226
|
+
persisted: prepared.rawContent,
|
|
227
|
+
written: prepared.rawContent,
|
|
228
|
+
fileHash: hash,
|
|
229
|
+
header: formatHashlineHeader(section.path, hash),
|
|
230
|
+
warnings,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const persisted = bom + restoreLineEndings(after, lineEnding);
|
|
234
|
+
const write = await this.fs.writeText(section.path, persisted);
|
|
235
|
+
const fileHash = this.#recordFullSnapshot(canonicalPath, after);
|
|
236
|
+
const op = exists ? "update" : "create";
|
|
237
|
+
return {
|
|
238
|
+
path: section.path,
|
|
239
|
+
canonicalPath,
|
|
240
|
+
op,
|
|
241
|
+
before: normalized,
|
|
242
|
+
after,
|
|
243
|
+
persisted,
|
|
244
|
+
written: write.text,
|
|
245
|
+
fileHash,
|
|
246
|
+
header: formatHashlineHeader(section.path, fileHash),
|
|
247
|
+
firstChangedLine: applyResult.firstChangedLine,
|
|
248
|
+
warnings,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async #tryRead(path) {
|
|
252
|
+
try {
|
|
253
|
+
const content = await this.fs.readText(path);
|
|
254
|
+
return { exists: true, rawContent: content };
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (isNotFound(error))
|
|
258
|
+
return { exists: false, rawContent: "" };
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
#recordFullSnapshot(canonicalPath, normalized) {
|
|
263
|
+
return this.snapshots.record(canonicalPath, normalized);
|
|
264
|
+
}
|
|
265
|
+
#mismatchError(section, canonicalPath, normalized, expected, hashRecognized) {
|
|
266
|
+
// Display the live content's tag (path-aware) without disturbing the
|
|
267
|
+
// version history's head/recency: compute directly rather than record.
|
|
268
|
+
const actualFileHash = computeFileHash(normalized, hashTrimsTrailing(section.path));
|
|
269
|
+
return new MismatchError({
|
|
270
|
+
path: section.path,
|
|
271
|
+
expectedFileHash: expected,
|
|
272
|
+
actualFileHash,
|
|
273
|
+
fileLines: normalized.split("\n"),
|
|
274
|
+
anchorLines: section.collectAnchorLines(),
|
|
275
|
+
hashRecognized,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
#applyWithRecovery(args) {
|
|
279
|
+
const { section, canonicalPath, exists, normalized, edits } = args;
|
|
280
|
+
const expected = exists ? section.fileHash : undefined;
|
|
281
|
+
// R1-M3: the tag is a fast INDEX; the integrity check is exact stored-text
|
|
282
|
+
// equality. A 16-bit collision can therefore never silently apply stale
|
|
283
|
+
// line numbers — it falls through to recovery / re-read.
|
|
284
|
+
const liveMatches = expected !== undefined && this.snapshots.byHash(canonicalPath, expected)?.text === normalized;
|
|
285
|
+
// Resolve `replace block N:` edits to concrete ranges before recovery
|
|
286
|
+
// runs. Block anchors are expressed against the snapshot the section tag
|
|
287
|
+
// names, so resolve against that exact text:
|
|
288
|
+
// - live content matches the tag (or there is no tag) → resolve against
|
|
289
|
+
// the live, normalized content;
|
|
290
|
+
// - the file drifted → resolve against the tagged snapshot's text so the
|
|
291
|
+
// resulting ranges flow through the 3-way-merge recovery below.
|
|
292
|
+
// When a block edit needs the tagged snapshot but it is unavailable, the
|
|
293
|
+
// range cannot be placed safely — reject with a MismatchError (re-read).
|
|
294
|
+
let resolved = edits;
|
|
295
|
+
if (hasBlockEdit(edits)) {
|
|
296
|
+
const baseText = expected === undefined || liveMatches ? normalized : this.snapshots.byHash(canonicalPath, expected)?.text;
|
|
297
|
+
if (baseText === undefined) {
|
|
298
|
+
throw this.#mismatchError(section, canonicalPath, normalized, expected ?? "", false);
|
|
299
|
+
}
|
|
300
|
+
resolved = resolveBlockEdits(edits, baseText, section.path, this.blockResolver, { onUnresolved: "throw" });
|
|
301
|
+
}
|
|
302
|
+
if (expected === undefined)
|
|
303
|
+
return applyEdits(normalized, resolved);
|
|
304
|
+
// Whole-file unchanged → the tag still names the live content, so an
|
|
305
|
+
// edit anchored at ANY line (displayed or not) is safe to apply.
|
|
306
|
+
if (liveMatches)
|
|
307
|
+
return applyEdits(normalized, resolved);
|
|
308
|
+
// Head/tail-only inserts are position-stable: "start"/"end" cannot move
|
|
309
|
+
// with content drift, so a stale tag is non-fatal — BUT only when the tag
|
|
310
|
+
// was actually minted this session (R2-M1). An unrecognized tag (`byHash`
|
|
311
|
+
// null — fabricated or carried from a prior session) must still force a
|
|
312
|
+
// re-read; otherwise the model could append to a file it never read,
|
|
313
|
+
// breaking the "tag comes from read" contract (the M3 safety boundary).
|
|
314
|
+
if (!hasAnchorScopedEdit(resolved) && this.snapshots.byHash(canonicalPath, expected) !== null) {
|
|
315
|
+
const result = applyEdits(normalized, resolved);
|
|
316
|
+
return { ...result, warnings: [HEADTAIL_DRIFT_WARNING, ...(result.warnings ?? [])] };
|
|
317
|
+
}
|
|
318
|
+
// File drifted: try to replay the edit against the version the tag
|
|
319
|
+
// names and 3-way-merge it onto the live content.
|
|
320
|
+
const recovered = this.recovery.tryRecover({
|
|
321
|
+
path: canonicalPath,
|
|
322
|
+
currentText: normalized,
|
|
323
|
+
fileHash: expected,
|
|
324
|
+
edits: resolved,
|
|
325
|
+
});
|
|
326
|
+
if (recovered)
|
|
327
|
+
return recoveryToApplyResult(recovered);
|
|
328
|
+
const hashRecognized = this.snapshots.byHash(canonicalPath, expected) !== null;
|
|
329
|
+
throw this.#mismatchError(section, canonicalPath, normalized, expected, hashRecognized);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
//# sourceMappingURL=patcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patcher.js","sourceRoot":"","sources":["../../src/hashline/patcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { SnapshotStore } from "./snapshots.js";
|
|
2
|
+
import type { Edit } from "./types.js";
|
|
3
|
+
export interface RecoveryArgs {
|
|
4
|
+
path: string;
|
|
5
|
+
currentText: string;
|
|
6
|
+
fileHash: string;
|
|
7
|
+
edits: readonly Edit[];
|
|
8
|
+
}
|
|
9
|
+
export interface RecoveryResult {
|
|
10
|
+
/** Post-recovery text. */
|
|
11
|
+
text: string;
|
|
12
|
+
/** First changed line (1-indexed) relative to the live `currentText`, or `undefined`. */
|
|
13
|
+
firstChangedLine: number | undefined;
|
|
14
|
+
/** Warnings collected during recovery, including the user-facing recovery banner. */
|
|
15
|
+
warnings: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Stateless recovery driver over a {@link SnapshotStore}. Construct once and
|
|
19
|
+
* call {@link Recovery.tryRecover} per stale-tag incident. The default
|
|
20
|
+
* implementation tries two strategies in order:
|
|
21
|
+
*
|
|
22
|
+
* 1. Apply the edits on the full-file version the tag names, then 3-way-merge
|
|
23
|
+
* the resulting patch onto the live content (handles external writes).
|
|
24
|
+
* 2. (Session chain) If that version wasn't the head, replay the edits onto
|
|
25
|
+
* the live content directly when line counts match AND every edit's anchor
|
|
26
|
+
* line content is unchanged between version and current — a prior in-session
|
|
27
|
+
* edit advanced the tag and the model's anchors still name the same logical
|
|
28
|
+
* rows. Emits a dedicated {@link RECOVERY_SESSION_REPLAY_WARNING} because
|
|
29
|
+
* even with both guards a coincidental insert+delete pair on duplicate rows
|
|
30
|
+
* can still land the edit on the wrong row; see {@link replaySessionChainOnCurrent}.
|
|
31
|
+
*/
|
|
32
|
+
export declare class Recovery {
|
|
33
|
+
readonly store: SnapshotStore;
|
|
34
|
+
constructor(store: SnapshotStore);
|
|
35
|
+
/**
|
|
36
|
+
* Attempt recovery. Returns `null` when no path forward is found — the
|
|
37
|
+
* caller should then surface a {@link MismatchError}.
|
|
38
|
+
*/
|
|
39
|
+
tryRecover(args: RecoveryArgs): RecoveryResult | null;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=recovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../../src/hashline/recovery.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAY,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,KAAK,EAAuB,IAAI,EAAE,MAAM,YAAY,CAAC;AAO5D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,qFAAqF;IACrF,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAkHD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,QAAQ;IACP,QAAQ,CAAC,KAAK,EAAE,aAAa;gBAApB,KAAK,EAAE,aAAa;IACzC;;;OAGG;IACH,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,cAAc,GAAG,IAAI;CAetD"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recover from a stale section snapshot tag by replaying the would-be edit
|
|
3
|
+
* against a cached pre-edit snapshot of the file and 3-way-merging the
|
|
4
|
+
* result onto the current on-disk content. Ported from oh-my-pi
|
|
5
|
+
* `packages/hashline/src/recovery.ts`.
|
|
6
|
+
*
|
|
7
|
+
* This is the synthesis the 7.9 decision [5.1] calls for: claude-code's
|
|
8
|
+
* normalization cascade survives here as the WITHIN-block content fallback —
|
|
9
|
+
* when the live file drifted, we replay the edit on the version the tag names
|
|
10
|
+
* and 3-way-merge onto live, instead of hard-rejecting.
|
|
11
|
+
*
|
|
12
|
+
* The patcher consults this when a section tag resolves to a snapshot that no
|
|
13
|
+
* longer matches the live file content. The recovery class is stateless apart
|
|
14
|
+
* from the {@link SnapshotStore} it queries; the snapshot store is the seam
|
|
15
|
+
* that lets you plug in your own caching strategy.
|
|
16
|
+
*/
|
|
17
|
+
import * as Diff from "diff";
|
|
18
|
+
import { applyEdits } from "./apply.js";
|
|
19
|
+
import { RECOVERY_EXTERNAL_WARNING, RECOVERY_SESSION_CHAIN_WARNING, RECOVERY_SESSION_REPLAY_WARNING, } from "./messages.js";
|
|
20
|
+
// Section tags are line-precise; never let Diff.applyPatch slide a hunk
|
|
21
|
+
// onto a duplicate closer 100+ lines away. If snapshot replay does not
|
|
22
|
+
// align exactly, refuse and let the caller re-read.
|
|
23
|
+
const RECOVERY_FUZZ_FACTOR = 0;
|
|
24
|
+
function applyEditsToSnapshot(previousText, currentText, edits, recoveryWarning) {
|
|
25
|
+
let applied;
|
|
26
|
+
try {
|
|
27
|
+
applied = applyEdits(previousText, [...edits]);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (applied.text === previousText)
|
|
33
|
+
return null;
|
|
34
|
+
const patch = Diff.structuredPatch("file", "file", previousText, applied.text, "", "", { context: 3 });
|
|
35
|
+
const merged = Diff.applyPatch(currentText, patch, { fuzzFactor: RECOVERY_FUZZ_FACTOR });
|
|
36
|
+
if (typeof merged !== "string" || merged === currentText)
|
|
37
|
+
return null;
|
|
38
|
+
const firstChangedLine = findFirstChangedLine(currentText, merged) ?? applied.firstChangedLine;
|
|
39
|
+
const hasNetChange = firstChangedLine !== undefined;
|
|
40
|
+
const warnings = hasNetChange ? [recoveryWarning, ...(applied.warnings ?? [])] : [...(applied.warnings ?? [])];
|
|
41
|
+
return { text: merged, firstChangedLine, warnings };
|
|
42
|
+
}
|
|
43
|
+
function collectAnchorLines(edits) {
|
|
44
|
+
const lines = [];
|
|
45
|
+
for (const edit of edits) {
|
|
46
|
+
for (const anchor of getEditAnchors(edit))
|
|
47
|
+
lines.push(anchor.line);
|
|
48
|
+
}
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
51
|
+
function getEditAnchors(edit) {
|
|
52
|
+
if (edit.kind === "delete")
|
|
53
|
+
return [edit.anchor];
|
|
54
|
+
// Recovery only ever receives already-resolved edits (no `block`); this arm
|
|
55
|
+
// exists for type-exhaustiveness over the full `Edit` union.
|
|
56
|
+
if (edit.kind === "block")
|
|
57
|
+
return [edit.anchor];
|
|
58
|
+
return edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor" ? [edit.cursor.anchor] : [];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Returns true when every anchor line in `edits` has identical content in
|
|
62
|
+
* `previousText` and `currentText`. The session-chain replay fast-path
|
|
63
|
+
* requires this: if the prior in-session edit rewrote the line the model is
|
|
64
|
+
* now re-targeting with a stale hash, replaying onto current would silently
|
|
65
|
+
* overwrite the new content with whatever the model authored against the
|
|
66
|
+
* old content — a corruption window, not a recovery.
|
|
67
|
+
*/
|
|
68
|
+
function verifyAnchorContent(previousText, currentText, edits) {
|
|
69
|
+
const lines = collectAnchorLines(edits);
|
|
70
|
+
if (lines.length === 0)
|
|
71
|
+
return true;
|
|
72
|
+
const prev = previousText.split("\n");
|
|
73
|
+
const curr = currentText.split("\n");
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
const idx = line - 1;
|
|
76
|
+
if (idx < 0 || idx >= prev.length || idx >= curr.length)
|
|
77
|
+
return false;
|
|
78
|
+
if (prev[idx] !== curr[idx])
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
function replaySessionChainOnCurrent(previousText, currentText, edits) {
|
|
84
|
+
// Two guards narrow the corruption window. Neither alone is sufficient,
|
|
85
|
+
// and even together they don't fully prove correctness — replay is the
|
|
86
|
+
// less-certain recovery mode and emits RECOVERY_SESSION_REPLAY_WARNING
|
|
87
|
+
// so the caller can verify the diff.
|
|
88
|
+
// - Equal line counts: every line number in `edits` still resolves to
|
|
89
|
+
// SOME logical row (no net shift across the prior chain). A
|
|
90
|
+
// coincidental insert+delete pair can still leave indices pointing
|
|
91
|
+
// at different logical rows than the model anchored against.
|
|
92
|
+
// - Anchor-content alignment: the row at each anchor's line index has
|
|
93
|
+
// identical content in previous and current. Catches the common
|
|
94
|
+
// case of a prior edit rewriting the targeted line; can still be
|
|
95
|
+
// coincidentally satisfied by a duplicated row at the shifted
|
|
96
|
+
// index.
|
|
97
|
+
if (previousText.split("\n").length !== currentText.split("\n").length)
|
|
98
|
+
return null;
|
|
99
|
+
if (!verifyAnchorContent(previousText, currentText, edits))
|
|
100
|
+
return null;
|
|
101
|
+
let applied;
|
|
102
|
+
try {
|
|
103
|
+
applied = applyEdits(currentText, [...edits]);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (applied.text === currentText)
|
|
109
|
+
return null;
|
|
110
|
+
return {
|
|
111
|
+
text: applied.text,
|
|
112
|
+
firstChangedLine: applied.firstChangedLine,
|
|
113
|
+
warnings: [RECOVERY_SESSION_REPLAY_WARNING, ...(applied.warnings ?? [])],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** First 1-indexed line at which `a` and `b` diverge, or `undefined` if equal. */
|
|
117
|
+
function findFirstChangedLine(a, b) {
|
|
118
|
+
if (a === b)
|
|
119
|
+
return undefined;
|
|
120
|
+
const aLines = a.split("\n");
|
|
121
|
+
const bLines = b.split("\n");
|
|
122
|
+
const max = Math.max(aLines.length, bLines.length);
|
|
123
|
+
for (let i = 0; i < max; i++) {
|
|
124
|
+
if (aLines[i] !== bLines[i])
|
|
125
|
+
return i + 1;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
function isHeadSnapshot(head, snapshot) {
|
|
130
|
+
return head === snapshot;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Stateless recovery driver over a {@link SnapshotStore}. Construct once and
|
|
134
|
+
* call {@link Recovery.tryRecover} per stale-tag incident. The default
|
|
135
|
+
* implementation tries two strategies in order:
|
|
136
|
+
*
|
|
137
|
+
* 1. Apply the edits on the full-file version the tag names, then 3-way-merge
|
|
138
|
+
* the resulting patch onto the live content (handles external writes).
|
|
139
|
+
* 2. (Session chain) If that version wasn't the head, replay the edits onto
|
|
140
|
+
* the live content directly when line counts match AND every edit's anchor
|
|
141
|
+
* line content is unchanged between version and current — a prior in-session
|
|
142
|
+
* edit advanced the tag and the model's anchors still name the same logical
|
|
143
|
+
* rows. Emits a dedicated {@link RECOVERY_SESSION_REPLAY_WARNING} because
|
|
144
|
+
* even with both guards a coincidental insert+delete pair on duplicate rows
|
|
145
|
+
* can still land the edit on the wrong row; see {@link replaySessionChainOnCurrent}.
|
|
146
|
+
*/
|
|
147
|
+
export class Recovery {
|
|
148
|
+
store;
|
|
149
|
+
constructor(store) {
|
|
150
|
+
this.store = store;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Attempt recovery. Returns `null` when no path forward is found — the
|
|
154
|
+
* caller should then surface a {@link MismatchError}.
|
|
155
|
+
*/
|
|
156
|
+
tryRecover(args) {
|
|
157
|
+
const { path, currentText, fileHash, edits } = args;
|
|
158
|
+
const snapshot = this.store.byHash(path, fileHash);
|
|
159
|
+
if (!snapshot)
|
|
160
|
+
return null;
|
|
161
|
+
const isHead = isHeadSnapshot(this.store.head(path), snapshot);
|
|
162
|
+
const recoveryWarning = isHead ? RECOVERY_EXTERNAL_WARNING : RECOVERY_SESSION_CHAIN_WARNING;
|
|
163
|
+
const merged = applyEditsToSnapshot(snapshot.text, currentText, edits, recoveryWarning);
|
|
164
|
+
if (merged !== null)
|
|
165
|
+
return merged;
|
|
166
|
+
// Session-chain fallback: the 3-way merge on the version refused.
|
|
167
|
+
// Replay onto current is gated by line-count equality AND
|
|
168
|
+
// anchor-content alignment — see `replaySessionChainOnCurrent`
|
|
169
|
+
// for why both guards together still don't fully prove correctness.
|
|
170
|
+
if (!isHead)
|
|
171
|
+
return replaySessionChainOnCurrent(snapshot.text, currentText, edits);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=recovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recovery.js","sourceRoot":"","sources":["../../src/hashline/recovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One full-file version observed at a point in time. The tag the model sees is
|
|
3
|
+
* {@link Snapshot.hash}; recovery replays edits against {@link Snapshot.text}.
|
|
4
|
+
*/
|
|
5
|
+
export interface Snapshot {
|
|
6
|
+
/** Canonical path this version belongs to. */
|
|
7
|
+
readonly path: string;
|
|
8
|
+
/** Full normalized (LF, no BOM) file text as observed. */
|
|
9
|
+
readonly text: string;
|
|
10
|
+
/** Content-derived tag for {@link Snapshot.text} (see {@link computeFileHash}). */
|
|
11
|
+
readonly hash: string;
|
|
12
|
+
/** Timestamp (ms since epoch) the version was recorded. */
|
|
13
|
+
recordedAt: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Storage seam for full-file version snapshots. The patcher calls {@link head}
|
|
17
|
+
* for the latest version of a path and {@link byHash} when it needs the
|
|
18
|
+
* specific historical version a section's stale tag names.
|
|
19
|
+
*/
|
|
20
|
+
export declare abstract class SnapshotStore {
|
|
21
|
+
/** Most-recently recorded version for `path`, or `null` if none. */
|
|
22
|
+
abstract head(path: string): Snapshot | null;
|
|
23
|
+
/** Recorded version for `path` whose tag equals `hash`, or `null`. */
|
|
24
|
+
abstract byHash(path: string, hash: string): Snapshot | null;
|
|
25
|
+
/** Record the full normalized text of `path` and return its content tag. */
|
|
26
|
+
abstract record(path: string, fullText: string): string;
|
|
27
|
+
/** Drop the version history for a single path. */
|
|
28
|
+
abstract invalidate(path: string): void;
|
|
29
|
+
/** Drop every version history. */
|
|
30
|
+
abstract clear(): void;
|
|
31
|
+
}
|
|
32
|
+
export interface InMemorySnapshotStoreOptions {
|
|
33
|
+
/** Maximum number of distinct paths tracked at once (default 30). LRU eviction. */
|
|
34
|
+
maxPaths?: number;
|
|
35
|
+
/** Maximum full-file versions retained per path (default 4). Oldest dropped first. */
|
|
36
|
+
maxVersionsPerPath?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Clock used to stamp {@link Snapshot.recordedAt}. Defaults to `Date.now`;
|
|
39
|
+
* injectable so tests stay deterministic. (recordedAt is advisory only —
|
|
40
|
+
* recency ordering uses the version array order, not the timestamp.)
|
|
41
|
+
*/
|
|
42
|
+
now?: () => number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* In-memory {@link SnapshotStore} backed by {@link LruMap}. Per-path history is
|
|
46
|
+
* a short ring of full-file versions (oldest dropped first); per-session path
|
|
47
|
+
* tracking is LRU-bounded so cold paths age out automatically.
|
|
48
|
+
*
|
|
49
|
+
* Recording byte-identical content again refreshes recency and reuses the
|
|
50
|
+
* existing tag (read fusion); recording new content unshifts a fresh version
|
|
51
|
+
* onto the front of the path history.
|
|
52
|
+
*/
|
|
53
|
+
export declare class InMemorySnapshotStore extends SnapshotStore {
|
|
54
|
+
#private;
|
|
55
|
+
constructor(options?: InMemorySnapshotStoreOptions);
|
|
56
|
+
head(path: string): Snapshot | null;
|
|
57
|
+
byHash(path: string, hash: string): Snapshot | null;
|
|
58
|
+
record(path: string, fullText: string): string;
|
|
59
|
+
invalidate(path: string): void;
|
|
60
|
+
clear(): void;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=snapshots.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshots.d.ts","sourceRoot":"","sources":["../../src/hashline/snapshots.ts"],"names":[],"mappings":"AA6BA;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,8BAAsB,aAAa;IACjC,oEAAoE;IACpE,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IAE5C,sEAAsE;IACtE,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IAE5D,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAEvD,kDAAkD;IAClD,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAEvC,kCAAkC;IAClC,QAAQ,CAAC,KAAK,IAAI,IAAI;CACvB;AA8CD,MAAM,WAAW,4BAA4B;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,qBAAa,qBAAsB,SAAQ,aAAa;;gBAK1C,OAAO,GAAE,4BAAiC;IAOtD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IAInC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IAKnD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAoB9C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI9B,KAAK,IAAI,IAAI;CAGd"}
|