pi-supernova 0.10.2 → 0.11.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 +365 -90
- package/docs/CHANGELOG.md +89 -5
- package/docs/TOKEN_COSTS.md +149 -324
- package/index.js +40 -11
- package/package.json +4 -3
- package/src/adapters/bash.js +40 -10
- package/src/adapters/edit.js +77 -23
- package/src/adapters/errors.js +4 -0
- package/src/adapters/index.js +1 -0
- package/src/adapters/list.js +6 -3
- package/src/adapters/read-focus.js +6 -0
- package/src/adapters/read-image.js +17 -9
- package/src/adapters/read-indexed.js +72 -0
- package/src/adapters/read-text.js +19 -2
- package/src/adapters/read.js +57 -13
- package/src/adapters/refs.js +17 -7
- package/src/adapters/write.js +14 -21
- package/src/bridge/host-bridge.js +152 -39
- package/src/bridge/invoke.js +3 -1
- package/src/bridge/tool-registry.js +132 -16
- package/src/context/evidence-graph.js +1 -0
- package/src/context/evidence-rank.js +1 -0
- package/src/context/evidence.js +4 -2
- package/src/context/fuzzy.js +31 -8
- package/src/context/outline.js +33 -9
- package/src/context/query.js +7 -2
- package/src/context/repo-index.js +8 -3
- package/src/context/search-files.js +20 -0
- package/src/context/search.js +25 -10
- package/src/context/snap-search.js +28 -6
- package/src/context/snap.js +8 -3
- package/src/context/source-entry.js +64 -28
- package/src/context/spans.js +1 -0
- package/src/contract/bash.js +16 -7
- package/src/contract/edit.js +26 -7
- package/src/contract/program.js +2 -1
- package/src/contract/read.js +46 -46
- package/src/fs/background.js +10 -2
- package/src/fs/check.js +844 -46
- package/src/fs/commit.js +89 -15
- package/src/fs/diff.js +12 -9
- package/src/fs/directory.js +14 -4
- package/src/fs/file-io.js +28 -7
- package/src/fs/glob.js +93 -11
- package/src/fs/json-read.js +3 -2
- package/src/fs/json-size.js +2 -1
- package/src/fs/lines.js +49 -2
- package/src/fs/patch.js +26 -10
- package/src/fs/process-tree.js +19 -1
- package/src/fs/read-window.js +36 -20
- package/src/fs/source-window.js +5 -10
- package/src/fs/text-ops.js +38 -18
- package/src/fs/vfs.js +296 -41
- package/src/fs/workspace.js +24 -6
- package/src/output/bottleneck.js +50 -25
- package/src/output/final.js +20 -0
- package/src/output/format.js +97 -36
- package/src/output/outcome.js +16 -4
- package/src/runtime/batch-input.js +17 -3
- package/src/runtime/guest-api.js +32 -7
- package/src/runtime/guest-worker.js +22 -4
- package/src/runtime/parallel.js +9 -49
- package/src/runtime/program-batch.js +53 -28
- package/src/runtime/program-file.js +2 -1
- package/src/runtime/program.js +94 -41
- package/src/runtime/reference.js +10 -9
- package/src/runtime/runtime.js +69 -18
- package/src/runtime/worker-pool.js +10 -1
- package/src/shared/decode.js +9 -8
- package/src/shared/image-worker.js +7 -0
- package/src/shared/image.js +58 -15
- package/src/shared/png.js +18 -0
- package/src/shared/result.js +30 -9
- package/src/shared/syntax-context.js +7 -1
- package/src/shared/utf8.js +11 -13
- package/src/ui/host-render.js +4 -0
- package/src/ui/omp-frame.js +2 -0
- package/src/ui/progress.js +2 -1
- package/src/ui/render-measure.js +11 -0
- package/src/ui/render.js +92 -31
- package/src/ui/trace.js +8 -5
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import {isString} from '../shared/decode.js';
|
|
4
|
+
|
|
5
|
+
function contained(root, target) {
|
|
6
|
+
const rel = path.relative(root,target);
|
|
7
|
+
|
|
8
|
+
return rel !== '..' && !rel.startsWith('..'+path.sep) && !path.isAbsolute(rel);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function validHintPath(hint) {
|
|
12
|
+
return isString(hint?.path) && !!hint.path && hint.path.isWellFormed() && !hint.path.includes('\0');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function validHintRange(hint) {
|
|
16
|
+
return Number.isSafeInteger(hint.startLine) && Number.isSafeInteger(hint.endLine) &&
|
|
17
|
+
hint.startLine >= 1 && hint.endLine >= hint.startLine && hint.endLine-hint.startLine < 40;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function assertScope(root, scope, target, message) {
|
|
21
|
+
if (!contained(root,target) || !contained(scope,target)) throw new Error(message);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function resolveHint(hint, cwd, scope, root, directory) {
|
|
25
|
+
if (!validHintPath(hint) || !validHintRange(hint)) throw new Error('invalid indexed context range');
|
|
26
|
+
let target = path.resolve(cwd,hint.path);
|
|
27
|
+
|
|
28
|
+
if (!contained(cwd,target) || !contained(scope,target)) {
|
|
29
|
+
// Providers may spell the same workspace with its realpath. Admit only
|
|
30
|
+
// that canonical scope, then restore the caller's logical path for reads.
|
|
31
|
+
assertScope(root,directory,target,'indexed context is outside the requested workspace scope');
|
|
32
|
+
target = path.resolve(scope,path.relative(directory,target));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const real = await fs.realpath(target);
|
|
36
|
+
assertScope(root,directory,real,'indexed context escapes workspace scope');
|
|
37
|
+
|
|
38
|
+
return {path:target,line:hint.startLine,endLine:hint.endLine};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function contextHints(response) {
|
|
42
|
+
const details = response?.details;
|
|
43
|
+
|
|
44
|
+
if (response?.isError || details?.code !== 'OK' || details.timedOut) throw new Error('indexed search incomplete or unavailable; retry or omit indexed:true');
|
|
45
|
+
const hints = details.contexts;
|
|
46
|
+
|
|
47
|
+
if (!Array.isArray(hints) || hints.length > 8) throw new Error('invalid indexed context metadata');
|
|
48
|
+
|
|
49
|
+
return {details,hints};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sourceSelection(candidates, details, cwd) {
|
|
53
|
+
if (candidates.length === 1 && (!details.truncated || details.contextSelectionComplete === true)) return {status:'found',...candidates[0]};
|
|
54
|
+
|
|
55
|
+
return {status:candidates.length ? 'ambiguous' : 'not_found',path:null,line:null,context:candidates.map(c=>({...c,path:path.relative(cwd,c.path)})),message:'Narrow the indexed query or use an explicit path.'};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Provider metadata is only a locator. The owned reader captures bytes and its
|
|
59
|
+
// CAS baseline itself; rendered text and sourceVersion are never trusted.
|
|
60
|
+
export async function indexedSource(hooks, query, cwd, scope, signal) {
|
|
61
|
+
if (!hooks.indexedSearch) throw new Error('indexed read requires a callable isearch');
|
|
62
|
+
signal?.throwIfAborted();
|
|
63
|
+
const response = await hooks.indexedSearch({query,path:path.relative(cwd,scope)||'.',withContext:true,limit:2,budgetChars:8000});
|
|
64
|
+
signal?.throwIfAborted();
|
|
65
|
+
const {details,hints} = contextHints(response);
|
|
66
|
+
const root = await fs.realpath(cwd), directory = await fs.realpath(scope);
|
|
67
|
+
const candidates = [];
|
|
68
|
+
|
|
69
|
+
for (const hint of hints) candidates.push(await resolveHint(hint,cwd,scope,root,directory));
|
|
70
|
+
|
|
71
|
+
return sourceSelection(candidates,details,cwd);
|
|
72
|
+
}
|
|
@@ -18,38 +18,51 @@ export function createTextReader(ctx, readWindow) {
|
|
|
18
18
|
async function loadText(targetPath, params, query, signal) {
|
|
19
19
|
const explicit = isNumber(params.offset) || isNumber(params.limit);
|
|
20
20
|
const windowed = explicit && params.complete !== true && !isString(params.about) && !isString(query);
|
|
21
|
+
|
|
21
22
|
if (!windowed) {
|
|
22
23
|
// Whole reads hash the bytes already loaded. Do not read once for a window
|
|
23
24
|
// and open/hash the entire file a second time just to return the same text.
|
|
24
25
|
return {text:await vfs.read(targetPath,{maxBytes:TEXT_MAX_BYTES}),windowed:false};
|
|
25
26
|
}
|
|
27
|
+
|
|
26
28
|
const start = isNumber(params.offset) ? Math.max(1,Math.floor(params.offset)) : 1;
|
|
27
29
|
const count = isNumber(params.limit) ? Math.max(0,Math.floor(params.limit)) : undefined;
|
|
28
30
|
const window = await readWindow(targetPath,start,count,TEXT_MAX_BYTES,signal);
|
|
31
|
+
|
|
29
32
|
if (!window.satisfied) throw new Error("read window exceeds " + TEXT_MAX_BYTES + " bytes: " + targetPath + "; request fewer lines or use a bounded parser through bash");
|
|
33
|
+
|
|
30
34
|
return {text:window.text,windowed:true,windowWhole:window.whole};
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
async function maybeOutline(cwd, rel, targetPath, text, params, entry) {
|
|
34
38
|
if (!isString(params.about)) return null;
|
|
35
|
-
|
|
39
|
+
// Focused text has no declarations and cannot use lexical caller hints.
|
|
40
|
+
// Do not make this file-local read wait for unrelated workspace discovery.
|
|
41
|
+
const references = entry && WorkspaceIndex.spansOf(entry).length ? await referenceFinder(cwd,targetPath) : null;
|
|
42
|
+
const outline = entry && outlineFile(entry,rel,params.about,outlineOptions(params,references,config));
|
|
43
|
+
|
|
36
44
|
if (!outline) return null;
|
|
37
45
|
recordOutlineOrigins(ledger,rel,outline.text);
|
|
46
|
+
|
|
38
47
|
return readResult(outline.text,{path:targetPath,outline:true,expanded:outline.expanded,declarations:outline.declarations});
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
function resolveSpan(entry, sourceLine, query, params) {
|
|
42
51
|
if (!params.resolve || !isString(query) || !entry) return {offset:params.offset,limit:params.limit,viewComplete:undefined};
|
|
43
52
|
const span=pickSpan(WorkspaceIndex.spansOf(entry),{line:sourceLine,name:query});
|
|
53
|
+
|
|
44
54
|
if (!span) return {offset:params.offset,limit:params.limit,viewComplete:undefined};
|
|
45
55
|
const spanLines=span.end-span.start+1;
|
|
46
56
|
const limit=isNumber(params.limit) ? Math.min(params.limit,spanLines) : spanLines;
|
|
57
|
+
|
|
47
58
|
return {offset:span.start,limit,viewComplete:limit>=spanLines};
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
function textFileResult(rel,targetPath,loaded,span,sliced,firstLine,explicit) {
|
|
51
|
-
const lines=sliced.length<=LARGE_FILE_BYTES ? sourceLines(sliced) : null;
|
|
62
|
+
const lines=ledger.window!==0 && sliced.length<=LARGE_FILE_BYTES ? sourceLines(sliced) : null;
|
|
63
|
+
|
|
52
64
|
if (lines) ledger.recordOrigin(rel,firstLine,lines,explicit);
|
|
65
|
+
|
|
53
66
|
return readResult(sliced,{path:targetPath,firstLine,lastLine:firstLine+(lines?.length ?? contentLineInfo(sliced).count)-1,
|
|
54
67
|
sourceChars:sliced.length,complete:loaded.windowed ? loaded.windowWhole : sliced===loaded.text,viewComplete:span.viewComplete});
|
|
55
68
|
}
|
|
@@ -60,12 +73,16 @@ export function createTextReader(ctx, readWindow) {
|
|
|
60
73
|
index.touch(rel);
|
|
61
74
|
const entry=needsSourceIndex(params,query) ? WorkspaceIndex.fromText(targetPath,loaded.text) : null;
|
|
62
75
|
const outlined=await maybeOutline(getCwd(),rel,targetPath,loaded.text,params,entry);
|
|
76
|
+
|
|
63
77
|
if (outlined) return outlined;
|
|
64
78
|
const span=resolveSpan(entry,sourceLine,query,params);
|
|
65
79
|
const firstLine=isNumber(span.offset) ? Math.max(1,Math.floor(span.offset)) : 1;
|
|
66
80
|
const sliced=loaded.windowed ? loaded.text : sliceLinesRaw(loaded.text,span.offset,span.limit);
|
|
81
|
+
|
|
67
82
|
if (params.complete===true && sliced!==loaded.text) throw new Error("complete:true requires the whole file; remove offset/limit for " + rel);
|
|
83
|
+
|
|
68
84
|
return textFileResult(rel,targetPath,loaded,span,sliced,firstLine,explicit);
|
|
69
85
|
}
|
|
86
|
+
|
|
70
87
|
return readTextFile;
|
|
71
88
|
}
|
package/src/adapters/read.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { isString } from "../shared/decode.js";
|
|
4
|
-
import { readResult, asReadResult, READ_VALUE, READ_BYTES, MAX_READ_VALUE_BYTES } from "../shared/result.js";
|
|
4
|
+
import { readResult, asReadResult, READ_VALUE, READ_BYTES, READ_FILES, MAX_READ_VALUE_BYTES } from "../shared/result.js";
|
|
5
5
|
import { extractStructuralSurface } from "../context/surface.js";
|
|
6
6
|
import { executeSnap, tokenizeQuery } from "../context/snap.js";
|
|
7
7
|
import { selectEvidence } from "../context/evidence.js";
|
|
@@ -16,6 +16,7 @@ import { projectJson } from "./read-json.js";
|
|
|
16
16
|
import { createImageReader } from "./read-image.js";
|
|
17
17
|
import { createTextReader } from "./read-text.js";
|
|
18
18
|
import { createFocusedReader } from "./read-focus.js";
|
|
19
|
+
import { indexedSource } from "./read-indexed.js";
|
|
19
20
|
|
|
20
21
|
export function createRead(ctx) {
|
|
21
22
|
const { getCwd, vfs, config, index, ledger, hooks, reads } = ctx;
|
|
@@ -29,19 +30,30 @@ export function createRead(ctx) {
|
|
|
29
30
|
params = { ...params, resolve: params.resolve !== false };
|
|
30
31
|
const cwd = getCwd();
|
|
31
32
|
|
|
33
|
+
const pendingPaths = await vfs.getOverlayPaths(searchDir);
|
|
34
|
+
|
|
35
|
+
if (params.indexed && pendingPaths.length === 0) {
|
|
36
|
+
const hint = await indexedSource(hooks, query, cwd, searchDir, signal);
|
|
37
|
+
|
|
38
|
+
if (hint.status !== "found") return readResult(hint, {isSnap:true});
|
|
39
|
+
const opened = await readFile(hint.path, {resolve:true,offset:hint.line,limit:hint.endLine-hint.line+1}, hint.line, relativeSlash(cwd,hint.path), undefined, signal);
|
|
40
|
+
|
|
41
|
+
return openedSource({...hint,path:relativeSlash(cwd,hint.path)}, params, opened.content[0], opened.details);
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
const includeHidden = path.relative(cwd, searchDir).split(path.sep)
|
|
33
45
|
.some(segment => segment.startsWith(".") && segment.length > 1);
|
|
34
46
|
|
|
35
47
|
const result = await executeSnap({ query, searchDir, root: cwd, includeHidden,
|
|
36
48
|
pathContext: { frecency: index.frecency, currentFile: index.lastTouched },
|
|
37
|
-
overlayText: p => vfs.getOverlay(p), pendingPaths
|
|
49
|
+
overlayText: p => vfs.getOverlay(p), pendingPaths, signal });
|
|
38
50
|
|
|
39
51
|
return openSource(result, params, signal, undefined, query);
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
async function sourceIsBounded(target, query, params) {
|
|
43
55
|
if (!isString(query) || params.complete === true) return false;
|
|
44
|
-
const overlay = vfs.getOverlay(target);
|
|
56
|
+
const overlay = await vfs.getOverlay(target);
|
|
45
57
|
|
|
46
58
|
if (overlay !== undefined) return Buffer.byteLength(overlay, "utf8") > 512 * 1024;
|
|
47
59
|
|
|
@@ -111,12 +123,13 @@ export function createRead(ctx) {
|
|
|
111
123
|
raw.isError = true;
|
|
112
124
|
}
|
|
113
125
|
|
|
126
|
+
for (const file of raw[READ_FILES] ?? []) state.files.add(file);
|
|
114
127
|
state.sourcePaths[index] = raw.details?.sourcePath;
|
|
115
128
|
await addBatchItem(state,index,raw,onItem);
|
|
116
129
|
}
|
|
117
130
|
|
|
118
131
|
async function readBatch(params, signal, onItem) {
|
|
119
|
-
const state = {items:[],sourcePaths:[],errors:Array(params.path.length).fill(null),bytes:0,streamed:false};
|
|
132
|
+
const state = {items:[],sourcePaths:[],files:new Set(),errors:Array(params.path.length).fill(null),bytes:0,streamed:false};
|
|
120
133
|
|
|
121
134
|
// Delivery/acknowledgement stays inside the same eight-operation scheduler
|
|
122
135
|
// slot as I/O. A busy guest cannot cause unbounded host/message-queue buffering.
|
|
@@ -136,6 +149,8 @@ export function createRead(ctx) {
|
|
|
136
149
|
|
|
137
150
|
response.isError = params._independent!==true && state.errors.some(Boolean);
|
|
138
151
|
|
|
152
|
+
if (!state.streamed) response[READ_FILES] = [...state.files];
|
|
153
|
+
|
|
139
154
|
return response;
|
|
140
155
|
}
|
|
141
156
|
|
|
@@ -152,19 +167,47 @@ export function createRead(ctx) {
|
|
|
152
167
|
params = normalizeRead({ ...params, path: targetParam });
|
|
153
168
|
const existing = needsProbe(params) ? await probeExistingPath(cwd, params.path, vfs) : null;
|
|
154
169
|
const cls = classifyRead(params, existing);
|
|
170
|
+
const scope = await scopeForRead(params, cwd, cls, signal);
|
|
171
|
+
const staged = await vfs.getOverlay(scope) !== undefined;
|
|
172
|
+
const revision = await vfs.readRevision(scope);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const value = await readSingleUnchecked(params, cwd, cls, scope, signal);
|
|
176
|
+
|
|
177
|
+
if (!staged) await vfs.assertReadCommitted(revision);
|
|
178
|
+
const source = value.details;
|
|
179
|
+
|
|
180
|
+
// Only the native text reader supplies these fields. JSON documents,
|
|
181
|
+
// directories, outlines and empty windows are not text-open receipts.
|
|
182
|
+
if (source?.sourceChars > 0 && isString(source.path) && path.isAbsolute(source.path) && await vfs.getOverlay(source.path) === undefined) value[READ_FILES] = [source.path];
|
|
183
|
+
|
|
184
|
+
return value;
|
|
185
|
+
} finally { vfs.releaseRead(revision); }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Classify once so the observation guard follows the same path/scope as dispatch.
|
|
189
|
+
async function scopeForRead(params, cwd, cls, signal) {
|
|
190
|
+
if (cls.kind === "session") return resolveSessionResource(params.path, signal, hooks);
|
|
191
|
+
|
|
192
|
+
if (cls.kind === "snap") return cls.existing?.directory ? cls.existing.path : cls.scoped ? resolveReadPath(cwd, params.path) : cwd;
|
|
193
|
+
|
|
194
|
+
if (cls.kind === "evidence") return cls.scope ? resolveReadPath(cwd, cls.scope) : cwd;
|
|
195
|
+
|
|
196
|
+
return cls.existing?.path ?? (isString(params.path) ? resolveReadPath(cwd, params.path) : cwd);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function readSingleUnchecked(params, cwd, cls, scope, signal) {
|
|
200
|
+
if (params.indexed && cls.kind !== "snap") throw new Error("indexed:true requires a source query");
|
|
155
201
|
const relOf = hit => relativeSlash(cwd, hit.path);
|
|
156
|
-
const snapScope = (scoped, hit) => hit?.directory ? hit.path : scoped ? resolveReadPath(cwd, params.path) : cwd;
|
|
157
202
|
|
|
158
203
|
const kinds = {
|
|
159
204
|
session: async () => {
|
|
160
|
-
const target = await resolveSessionResource(params.path, signal, hooks);
|
|
161
|
-
|
|
162
205
|
return params.resolve
|
|
163
|
-
? openSource({ status: "found", path: params.path, line: params.offset ?? 1 }, params, signal,
|
|
164
|
-
: readFile(
|
|
206
|
+
? openSource({ status: "found", path: params.path, line: params.offset ?? 1 }, params, signal, scope)
|
|
207
|
+
: readFile(scope, params, undefined, params.path, undefined, signal);
|
|
165
208
|
},
|
|
166
209
|
evidence: () => evidence({ ...params, query: cls.query, path: cls.scope }, signal),
|
|
167
|
-
snap: () => sourceRead(cls.query,
|
|
210
|
+
snap: () => sourceRead(cls.query, scope, signal, params),
|
|
168
211
|
outline: () => surface({ path: params.path }, signal),
|
|
169
212
|
focus: () => focusAbout({ rel: relOf(cls.existing), about: cls.about, overlay: cls.existing.overlay, targetPath: cls.existing.path, signal }),
|
|
170
213
|
open: () => openSource({ status: "found", path: relOf(cls.existing), line: params.offset ?? 1 }, params, signal),
|
|
@@ -223,7 +266,7 @@ export function createRead(ctx) {
|
|
|
223
266
|
root: cwd,
|
|
224
267
|
includeHidden,
|
|
225
268
|
overlayText: (p) => vfs.getOverlay(p),
|
|
226
|
-
pendingPaths: vfs.getOverlayPaths(),
|
|
269
|
+
pendingPaths: await vfs.getOverlayPaths(cwd),
|
|
227
270
|
signal,
|
|
228
271
|
});
|
|
229
272
|
|
|
@@ -249,7 +292,7 @@ export function createRead(ctx) {
|
|
|
249
292
|
|
|
250
293
|
if (signal?.aborted) throw new Error("aborted");
|
|
251
294
|
const searchDir = params?.path ? await resolveWorkspacePath(cwd, params.path, "evidence", true) : cwd;
|
|
252
|
-
const res = await selectEvidence({ query: params.query, root: cwd, searchDir, index, overlayText: (p) => vfs.getOverlay(p), pendingPaths: vfs.getOverlayPaths(), options: evidenceOptions(params) });
|
|
295
|
+
const res = await selectEvidence({ query: params.query, root: cwd, searchDir, index, overlayText: (p) => vfs.getOverlay(p), pendingPaths: await vfs.getOverlayPaths(searchDir), options: evidenceOptions(params) });
|
|
253
296
|
|
|
254
297
|
for (const span of res.spans) ledger.recordOrigin(span.path, span.lines[0], span.text.split("\n"));
|
|
255
298
|
|
|
@@ -258,7 +301,8 @@ export function createRead(ctx) {
|
|
|
258
301
|
|
|
259
302
|
async function surface(params, signal) {
|
|
260
303
|
const cwd = getCwd();
|
|
261
|
-
|
|
304
|
+
// Outlines are read-only, like text reads; mutations still require workspace admission.
|
|
305
|
+
const target = resolveReadPath(cwd, params?.path);
|
|
262
306
|
|
|
263
307
|
if (signal?.aborted) throw new Error("aborted");
|
|
264
308
|
const text = await vfs.read(target, { maxBytes: 2 * 1024 * 1024 });
|
package/src/adapters/refs.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { relativeSlash } from "../fs/workspace.js";
|
|
2
|
+
import { overlaySnapshot } from "../context/search-files.js";
|
|
2
3
|
|
|
3
4
|
export function outlineOptions(params, references, config) {
|
|
4
5
|
const options = { references };
|
|
@@ -17,24 +18,33 @@ export function recordOutlineOrigins(ledger, rel, outlineText) {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
/** Where else
|
|
21
|
+
/** Where else an expanded outline name appears, excluding its own declaration. */
|
|
21
22
|
export function createReferenceFinder(index, vfs) {
|
|
22
23
|
return async function referenceFinder(cwd, targetPath) {
|
|
23
24
|
let files;
|
|
24
25
|
|
|
25
|
-
try { files = [...new Set([...await index.files(cwd), ...vfs.getOverlayPaths()])]; }
|
|
26
|
+
try { files = [...new Set([...await index.files(cwd), ...await vfs.getOverlayPaths(cwd)])]; }
|
|
26
27
|
catch { return () => []; }
|
|
27
28
|
|
|
28
29
|
if (!index.canScan(files)) return () => [];
|
|
30
|
+
const overlayText = await overlaySnapshot(files, file => vfs.getOverlay(file));
|
|
31
|
+
const targetRel = relativeSlash(await vfs.resolvePath(cwd), await vfs.resolvePath(targetPath));
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
// Each outline supplies its expanded names together. The row snapshot is
|
|
34
|
+
// call-local; the existing index still owns file validation and body caching.
|
|
35
|
+
let rows;
|
|
36
|
+
|
|
37
|
+
return (name, excludeLine, names) => {
|
|
31
38
|
if (!name || name.length < 3) return [];
|
|
32
39
|
const escaped = name.replace(/[$]/g, (c) => "\\" + c);
|
|
33
|
-
|
|
40
|
+
// JavaScript identifiers include $, which is not a regex word character.
|
|
41
|
+
const regex = new RegExp("(?<![\\w$])" + escaped + "(?![\\w$])");
|
|
42
|
+
|
|
43
|
+
rows ??= index.grepRows(files, new RegExp("(?<![\\w$])(?:" + names.filter(name => name.length >= 3)
|
|
44
|
+
.map(name => name.replaceAll("$", "\\$")).join("|") + ")(?![\\w$])"), cwd, overlayText);
|
|
34
45
|
|
|
35
|
-
return
|
|
36
|
-
.
|
|
37
|
-
.filter((r) => !(r.line === excludeLine && r.rel === relativeSlash(cwd, targetPath)))
|
|
46
|
+
return rows
|
|
47
|
+
.filter((r) => regex.test(r.text) && !(r.line === excludeLine && r.rel === targetRel))
|
|
38
48
|
.map((r) => r.rel + ":" + r.line);
|
|
39
49
|
};
|
|
40
50
|
};
|
package/src/adapters/write.js
CHANGED
|
@@ -50,6 +50,7 @@ export function createWrite(ctx) {
|
|
|
50
50
|
const unknown = Object.keys(params ?? {}).filter(key => !WRITE_OPTION_KEYS.includes(key));
|
|
51
51
|
|
|
52
52
|
if (unknown.length) throw new Error("write does not accept option " + unknown.map(key => JSON.stringify(key)).join(", ") + "; supported options are " + WRITE_OPTION_KEYS.join(", "));
|
|
53
|
+
|
|
53
54
|
if (!isString(params?.content)) throw new Error("write requires string content");
|
|
54
55
|
assertWriteAppendFlag(params.append);
|
|
55
56
|
assertWriteArtifactsFlag(params.allowReadArtifacts);
|
|
@@ -62,21 +63,15 @@ export function createWrite(ctx) {
|
|
|
62
63
|
return content;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
async function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Append needs the real content: the diff snapshot may be a lossy decode, and
|
|
70
|
-
// concatenating that would silently corrupt a non-UTF-8 file.
|
|
71
|
-
let prevText;
|
|
72
|
-
|
|
73
|
-
try { prevText = overlay !== undefined ? overlay : await vfs.read(target, { maxBytes: WRITE_APPEND_MAX_READ_BYTES, preserveRead: true }); }
|
|
66
|
+
async function readAppendBase(target) {
|
|
67
|
+
// Append bytes and receipt text share one strict, bounded read. The VFS
|
|
68
|
+
// supplies staged overlays and preserves any earlier read's CAS baseline.
|
|
69
|
+
try { return await vfs.read(target, { maxBytes: WRITE_APPEND_MAX_READ_BYTES, label: "append input", preserveRead: true }); }
|
|
74
70
|
catch (error) {
|
|
75
71
|
if (error.code !== "ENOENT") throw error;
|
|
76
|
-
prevText = "";
|
|
77
|
-
}
|
|
78
72
|
|
|
79
|
-
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
80
75
|
}
|
|
81
76
|
|
|
82
77
|
async function write(params, signal) {
|
|
@@ -85,15 +80,13 @@ export function createWrite(ctx) {
|
|
|
85
80
|
|
|
86
81
|
if (signal?.aborted) throw new Error("aborted");
|
|
87
82
|
let content = assertWriteParams(params);
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
removedLines = appended.removedLines;
|
|
96
|
-
}
|
|
83
|
+
const appending = params.append === true;
|
|
84
|
+
|
|
85
|
+
const { previous: prevText, removedLines } = appending
|
|
86
|
+
? { previous: await readAppendBase(target) }
|
|
87
|
+
: await writeSnapshot(vfs, target, signal);
|
|
88
|
+
|
|
89
|
+
if (appending) content = prevText + content;
|
|
97
90
|
|
|
98
91
|
const { speculative } = await vfs.write(target, content);
|
|
99
92
|
index.touch(relativeSlash(cwd, target));
|