better-dsh 0.2.2-c → 0.2.3-b
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/docs/50_test-reports/2026-09-06-write/345/267/245/345/205/267sandbox/345/215/207/347/272/247/351/200/217/344/274/240bug/345/244/215/345/217/221/345/217/212/346/214/202/350/265/267-/344/272/213/344/273/266/346/212/245/345/221/212.md +133 -0
- package/docs/50_test-reports/2026-09-08-hashline-edit-E_RANGE_UNVERIFIED/350/267/250/350/275/256/344/274/232/350/257/235/351/224/256/345/244/261/346/225/210-/350/257/212/346/226/255/346/212/245/345/221/212.md +226 -0
- package/docs/50_test-reports/upstream-dsh-0.1.3-alpha.2-local-test-report.md +44 -0
- package/docs/50_test-reports/upstream-dsh-0.1.3-alpha.2-report.md +110 -0
- package/docs/50_test-reports/v0.2.3b-hashline-content-locator/345/256/236/346/265/213/346/212/245/345/221/212.md +73 -0
- package/docs/60_exploration-and-research/01-cordis-runtime/cordis-engineering-feasibility.md +155 -0
- package/docs/60_exploration-and-research/01-cordis-runtime/cordis-research.md +385 -152
- package/docs/60_exploration-and-research/01-cordis-runtime/js-ts-language-fundamentals.md +628 -0
- package/docs/60_exploration-and-research/02-dsh/dash-research.md +592 -0
- package/docs/60_exploration-and-research/02-dsh/dsh-web-profile-package-map.md +186 -0
- package/docs/60_exploration-and-research/02-dsh/dsh-web-ui-slot-system-research.md +310 -0
- package/docs/60_exploration-and-research/02-dsh/dsh-webui-backend-data-inventory.md +633 -0
- package/docs/60_exploration-and-research/02-dsh/dsh-webui-strip-boundary-research.md +300 -0
- package/docs/60_exploration-and-research/02-dsh/dsh-webui-wire-appendix.md +3729 -0
- package/docs/60_exploration-and-research/02-dsh/web-frontend-composability-research.md +191 -0
- package/docs/60_exploration-and-research/02-dsh/webui-wire-data/capture-live-turn.json +1 -0
- package/docs/60_exploration-and-research/02-dsh/webui-wire-data/observed-endpoints.json +224 -0
- package/docs/60_exploration-and-research/02-dsh/webui-wire-data/remote-inventory.json +110954 -0
- package/docs/60_exploration-and-research/02-dsh/webui-wire-data/served-index-sample.html +47 -0
- package/docs/60_exploration-and-research/02-dsh/webui-wire-data/session-events.json +9411 -0
- package/docs/60_exploration-and-research/02-dsh/webui-wire-data/ws-frame-examples.json +20 -0
- package/docs/superd/00-blueprint.md +331 -0
- package/docs/superd/01-component-boundaries.md +261 -0
- package/docs/superd/02-dsh-embed-modes.md +151 -0
- package/docs/superd/image.png +0 -0
- package/lib/index.js +1214 -1293
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -2556,15 +2556,15 @@ const here = path.dirname(fileURLToPath(import.meta.url));
|
|
|
2556
2556
|
/** `require` anchored at this module — used to dlopen absolute `.node` paths. */
|
|
2557
2557
|
const requireHere = createRequire(import.meta.url);
|
|
2558
2558
|
/**
|
|
2559
|
-
* Locate the leaf package directory
|
|
2560
|
-
* `fromDir`
|
|
2561
|
-
*
|
|
2559
|
+
* Locate the leaf package directory with an explicit node_modules-walk upward
|
|
2560
|
+
* from `fromDir` — the ONLY mechanism. `require.resolve` is deliberately not
|
|
2561
|
+
* used here: its fallback through the process global paths (which include the
|
|
2562
|
+
* pnpm virtual-store hoist root `.pnpm/node_modules`) can resolve the leaf
|
|
2563
|
+
* package from ANY anchor, defeating the anchor-bounded contract this loader
|
|
2564
|
+
* guarantees for its degradation tests and unsupported-platform probes.
|
|
2565
|
+
* Returns `undefined` when absent.
|
|
2562
2566
|
*/
|
|
2563
2567
|
function findPackageDir(packageName, fromDir) {
|
|
2564
|
-
try {
|
|
2565
|
-
const manifest = createRequire(path.join(fromDir, "noop.js")).resolve(`${packageName}/package.json`);
|
|
2566
|
-
return path.dirname(manifest);
|
|
2567
|
-
} catch {}
|
|
2568
2568
|
let dir = fromDir;
|
|
2569
2569
|
for (;;) {
|
|
2570
2570
|
const candidate = path.join(dir, "node_modules", packageName);
|
|
@@ -5166,7 +5166,7 @@ async function renderRoster(family, descendants, agents) {
|
|
|
5166
5166
|
/** One child's last activity: its live log's last event, else its persisted one. */
|
|
5167
5167
|
async function lastActivityOf(rawId, family) {
|
|
5168
5168
|
const live = family.sessions.get(SessionId(rawId));
|
|
5169
|
-
if (live !== void 0) return lastEventTime(live.
|
|
5169
|
+
if (live !== void 0) return lastEventTime(live.snapshotEvents());
|
|
5170
5170
|
const inspection = await family.sessionPersistence.inspect(SessionId(rawId));
|
|
5171
5171
|
return inspection === void 0 ? "-" : lastEventTime(inspection.events);
|
|
5172
5172
|
}
|
|
@@ -5941,7 +5941,7 @@ function createGrepTool(deps) {
|
|
|
5941
5941
|
const MAX_BYTES = 100 * 1024 * 1024;
|
|
5942
5942
|
const MAX_READ_LINE_BYTES = 200 * 1024;
|
|
5943
5943
|
const HASH_STORE_BUSY_TIMEOUT = 1e3;
|
|
5944
|
-
const HASH_STORE_VERSION =
|
|
5944
|
+
const HASH_STORE_VERSION = 7;
|
|
5945
5945
|
const EDITS_MAX_ITEMS = 32;
|
|
5946
5946
|
const SERVED_TTL_MS = 10080 * 60 * 1e3;
|
|
5947
5947
|
const SERVED_ECHO_CAP = 150;
|
|
@@ -6095,20 +6095,18 @@ function assertUndoRequest(request) {
|
|
|
6095
6095
|
//#endregion
|
|
6096
6096
|
//#region src/url-schema/vendored/hashline/paths.js
|
|
6097
6097
|
/**
|
|
6098
|
-
* On-disk home for dsh-better-edit state
|
|
6099
|
-
*
|
|
6100
|
-
*
|
|
6101
|
-
* `
|
|
6102
|
-
*
|
|
6103
|
-
*
|
|
6104
|
-
* so a caller without a workspace never writes into an arbitrary cwd.
|
|
6105
|
-
* @param cwd - the workspace root, or undefined for the shared-home fallback.
|
|
6098
|
+
* On-disk home for dsh-better-edit state: ONE centralized store per harness
|
|
6099
|
+
* home — `$DSH_HOME/storages/dsh-better-edit` (default
|
|
6100
|
+
* `~/.dsh/storages/dsh-better-edit`), resolved through the harness home
|
|
6101
|
+
* resolver so `DSH_HOME` isolates deployments (test homes never touch prod).
|
|
6102
|
+
* There is no per-workspace state directory: the store keys everything by
|
|
6103
|
+
* canonical absolute path.
|
|
6106
6104
|
*/
|
|
6107
|
-
function configDir(
|
|
6108
|
-
return
|
|
6105
|
+
function configDir() {
|
|
6106
|
+
return join(resolveDshHome(), "storages", "dsh-better-edit");
|
|
6109
6107
|
}
|
|
6110
|
-
function hashStorePath(
|
|
6111
|
-
return join(configDir(
|
|
6108
|
+
function hashStorePath() {
|
|
6109
|
+
return join(configDir(), "hash-store.sqlite");
|
|
6112
6110
|
}
|
|
6113
6111
|
|
|
6114
6112
|
//#endregion
|
|
@@ -6330,769 +6328,655 @@ function mapStableHashes(oldContent, oldHashes, newContent, removedHashes) {
|
|
|
6330
6328
|
}
|
|
6331
6329
|
|
|
6332
6330
|
//#endregion
|
|
6333
|
-
//#region src/url-schema/vendored/hashline/
|
|
6334
|
-
function
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
if (trimmed.includes("│")) return `[E_BAD_REF] Invalid anchor "${trimmed}". remove_from and remove_to must contain the 3-char hash only — remove everything from "│" onward.`;
|
|
6339
|
-
return `[E_BAD_REF] Invalid anchor "${trimmed}". Expected a 3-char alphanumeric anchor (e.g. "aB3").`;
|
|
6340
|
-
}
|
|
6341
|
-
function parseRef(ref) {
|
|
6342
|
-
const trimmed = ref.trim();
|
|
6343
|
-
if (trimmed.length === ANCHOR_LEN && ALPH_RE.test(trimmed)) return { hash: trimmed };
|
|
6344
|
-
throw new Error(diagRef(ref));
|
|
6331
|
+
//#region src/url-schema/vendored/hashline/hash-store.js
|
|
6332
|
+
function isValidHashList(value) {
|
|
6333
|
+
if (!Array.isArray(value)) return false;
|
|
6334
|
+
for (const hash of value) if (typeof hash !== "string" || !HASH_RE.test(hash)) return false;
|
|
6335
|
+
return true;
|
|
6345
6336
|
}
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
if (/^\n+$/.test(normalized)) return new Array(normalized.length).fill("");
|
|
6352
|
-
return normalized.split("\n");
|
|
6337
|
+
function isValidSnapshot(value) {
|
|
6338
|
+
if (typeof value !== "object" || value === null) return false;
|
|
6339
|
+
const v = value;
|
|
6340
|
+
if (typeof v.content !== "string") return false;
|
|
6341
|
+
return isValidHashList(v.hashes);
|
|
6353
6342
|
}
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
if (!
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
hash: ref.hash,
|
|
6363
|
-
hashMatched: true
|
|
6364
|
-
};
|
|
6365
|
-
return {
|
|
6366
|
-
ref,
|
|
6367
|
-
kind: "ambiguous",
|
|
6368
|
-
candidates: hashMatches
|
|
6369
|
-
};
|
|
6343
|
+
/** A served-row array: per-position hash, or null for never-served slots. */
|
|
6344
|
+
function isValidServedList(value) {
|
|
6345
|
+
if (!Array.isArray(value)) return false;
|
|
6346
|
+
for (const entry of value) {
|
|
6347
|
+
if (entry === null) continue;
|
|
6348
|
+
if (typeof entry !== "string" || !HASH_RE.test(entry)) return false;
|
|
6349
|
+
}
|
|
6350
|
+
return true;
|
|
6370
6351
|
}
|
|
6371
|
-
function
|
|
6372
|
-
|
|
6352
|
+
function cacheKey(checksum) {
|
|
6353
|
+
return `${CANON_VERSION}:${checksum}`;
|
|
6373
6354
|
}
|
|
6374
|
-
function
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
if (ln < 1 || ln > fileLines.length) return;
|
|
6381
|
-
const position = ln - 1;
|
|
6382
|
-
if (seen.has(position)) return;
|
|
6383
|
-
seen.add(position);
|
|
6384
|
-
servedRows.push({
|
|
6385
|
-
position,
|
|
6386
|
-
hash: fileHashes[ln - 1]
|
|
6387
|
-
});
|
|
6388
|
-
};
|
|
6389
|
-
const notFound = mismatches.filter((m) => m.kind === "not_found");
|
|
6390
|
-
const ambiguous = mismatches.filter((m) => m.kind === "ambiguous");
|
|
6391
|
-
const refList = notFound.map((m) => `"${m.ref.hash}"`).join(", ");
|
|
6392
|
-
if (notFound.length > 0) {
|
|
6393
|
-
out.push(`[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}: ${refList}. Re-read for fresh anchors.`);
|
|
6394
|
-
for (const m of notFound) {
|
|
6395
|
-
const ctx = m.context;
|
|
6396
|
-
if (!ctx) continue;
|
|
6397
|
-
const from = Math.max(1, ctx.line - 1);
|
|
6398
|
-
const to = Math.min(fileLines.length, ctx.line + 1);
|
|
6399
|
-
const rows = [];
|
|
6400
|
-
for (let ln = from; ln <= to; ln++) {
|
|
6401
|
-
rows.push(` ${ln}: ${fileHashes[ln - 1]}│${clipLine(fileLines[ln - 1] ?? "")}`);
|
|
6402
|
-
pushRow(ln);
|
|
6403
|
-
}
|
|
6404
|
-
out.push("");
|
|
6405
|
-
out.push(` Current context around resolved anchor "${ctx.hash}" (line ${ctx.line}):\n${rows.join("\n")}`);
|
|
6406
|
-
}
|
|
6355
|
+
function isCorruptionError(error) {
|
|
6356
|
+
if (error && typeof error === "object") {
|
|
6357
|
+
const errcode = error.errcode;
|
|
6358
|
+
if (typeof errcode === "number") return errcode === 11 || errcode === 24 || errcode === 26;
|
|
6359
|
+
const code = error.code;
|
|
6360
|
+
if (typeof code === "string" && /NOTADB|CORRUPT/.test(code)) return true;
|
|
6407
6361
|
}
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
const lines = sample.map((line) => {
|
|
6415
|
-
const content = clipLine(fileLines[line - 1] ?? "");
|
|
6416
|
-
pushRow(line);
|
|
6417
|
-
return ` ${line}: ${fileHashes[line - 1]}│${content}`;
|
|
6418
|
-
}).join("\n");
|
|
6419
|
-
out.push(` Hash "${m.ref.hash}" matches lines ${sample.join(", ")}${more}.\n${lines}`);
|
|
6420
|
-
}
|
|
6362
|
+
return error instanceof Error && /corrupt|not a database|malformed|database disk image/i.test(error.message);
|
|
6363
|
+
}
|
|
6364
|
+
function isBusyError(error) {
|
|
6365
|
+
if (error && typeof error === "object") {
|
|
6366
|
+
const errcode = error.errcode;
|
|
6367
|
+
if (typeof errcode === "number") return errcode === 5 || errcode === 6;
|
|
6421
6368
|
}
|
|
6422
|
-
return
|
|
6423
|
-
message: out.join("\n"),
|
|
6424
|
-
servedRows
|
|
6425
|
-
};
|
|
6369
|
+
return error instanceof Error && /busy|locked/i.test(error.message);
|
|
6426
6370
|
}
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
"remove_to"
|
|
6431
|
-
]);
|
|
6432
|
-
function assertItem(edit) {
|
|
6433
|
-
rejectUnknownFields(edit, ITEM_KS, "Edit", "The edit takes only { replacement_text, remove_from, remove_to }.");
|
|
6434
|
-
if ("remove_from" in edit && typeof edit.remove_from !== "string") throw new Error(`[E_BAD_SHAPE] Field "remove_from" must be an anchor string (3-char hash).`);
|
|
6435
|
-
if ("remove_to" in edit && typeof edit.remove_to !== "string") throw new Error(`[E_BAD_SHAPE] Field "remove_to" must be an anchor string (3-char hash).`);
|
|
6436
|
-
if (!("replacement_text" in edit)) throw new Error(`[E_BAD_SHAPE] The edit requires a "replacement_text" field. Provide the replacement text (use "" to delete).`);
|
|
6437
|
-
if (typeof edit.replacement_text !== "string") throw new Error(NEW_CONTENT_NOT_STRING_MSG);
|
|
6438
|
-
if (typeof edit.remove_from !== "string" || typeof edit.remove_to !== "string") throw new Error(`[E_BAD_SHAPE] The edit requires "remove_from" and "remove_to" anchor strings (3-char hashes from read output).`);
|
|
6371
|
+
function sleepSync(ms) {
|
|
6372
|
+
const sab = new Int32Array(new SharedArrayBuffer(4));
|
|
6373
|
+
Atomics.wait(sab, 0, 0, ms);
|
|
6439
6374
|
}
|
|
6440
|
-
const
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
warnings?.push(message);
|
|
6453
|
-
return match[2];
|
|
6454
|
-
}
|
|
6455
|
-
return ref;
|
|
6456
|
-
});
|
|
6457
|
-
return {
|
|
6458
|
-
content_lines: editLines,
|
|
6459
|
-
hash_bounds: [parseHashRef(bounds[0]), parseHashRef(bounds[1])]
|
|
6460
|
-
};
|
|
6375
|
+
const BUSY_RETRIES = 3;
|
|
6376
|
+
const BUSY_RETRY_DELAY_MS = 100;
|
|
6377
|
+
function withBusyRetry(fn) {
|
|
6378
|
+
let lastError;
|
|
6379
|
+
for (let attempt = 0; attempt <= BUSY_RETRIES; attempt++) try {
|
|
6380
|
+
return fn();
|
|
6381
|
+
} catch (error) {
|
|
6382
|
+
lastError = error;
|
|
6383
|
+
if (!isBusyError(error) || attempt === BUSY_RETRIES) throw error;
|
|
6384
|
+
sleepSync(BUSY_RETRY_DELAY_MS);
|
|
6385
|
+
}
|
|
6386
|
+
throw lastError;
|
|
6461
6387
|
}
|
|
6462
|
-
function
|
|
6463
|
-
|
|
6388
|
+
function openDbWithBusyRetry(storePath) {
|
|
6389
|
+
return withBusyRetry(() => openDb(storePath));
|
|
6464
6390
|
}
|
|
6465
|
-
/**
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
const locations = stripped.map((s) => `replacement_text line ${s.lineIndex + 1}`).join(", ");
|
|
6480
|
-
const matchedCount = stripped.filter((s) => s.matched).length;
|
|
6481
|
-
const evidence = matchedCount === 0 ? "0 matched — verify literal 'HASH│' content" : `${matchedCount}/${stripped.length} matched`;
|
|
6482
|
-
warnings.push(`[E_BARE_HASH_PREFIX] stripped "HASH│" prefix from ${locations} (${evidence}).`);
|
|
6483
|
-
return {
|
|
6484
|
-
...edit,
|
|
6485
|
-
content_lines: contentLines
|
|
6486
|
-
};
|
|
6391
|
+
/** One open store per store path (per workspace); parallel sessions share per-workspace dbs. */
|
|
6392
|
+
const stores = /* @__PURE__ */ new Map();
|
|
6393
|
+
const openings = /* @__PURE__ */ new Map();
|
|
6394
|
+
let exitHandlerRegistered = false;
|
|
6395
|
+
function openDb(storePath) {
|
|
6396
|
+
const db = new DatabaseSync(storePath, { timeout: HASH_STORE_BUSY_TIMEOUT });
|
|
6397
|
+
try {
|
|
6398
|
+
return buildStore(db);
|
|
6399
|
+
} catch (error) {
|
|
6400
|
+
try {
|
|
6401
|
+
db.close();
|
|
6402
|
+
} catch {}
|
|
6403
|
+
throw error;
|
|
6404
|
+
}
|
|
6487
6405
|
}
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
const
|
|
6506
|
-
|
|
6406
|
+
function buildStore(db) {
|
|
6407
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
6408
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
6409
|
+
db.exec("CREATE TABLE IF NOT EXISTS snapshots (path TEXT PRIMARY KEY, checksum TEXT NOT NULL, line_count INTEGER NOT NULL, hashes TEXT NOT NULL, updated_at INTEGER NOT NULL)");
|
|
6410
|
+
db.exec("CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
|
|
6411
|
+
db.exec("CREATE TABLE IF NOT EXISTS undo (path TEXT PRIMARY KEY, content TEXT NOT NULL, bom TEXT NOT NULL, ending TEXT NOT NULL, hashes TEXT NOT NULL, result_content TEXT NOT NULL, updated_at INTEGER NOT NULL)");
|
|
6412
|
+
const versionRow = db.prepare("SELECT value FROM meta WHERE key = 'version'").get();
|
|
6413
|
+
const versionChanged = versionRow !== void 0 && versionRow.value !== String(HASH_STORE_VERSION);
|
|
6414
|
+
if (versionChanged) {
|
|
6415
|
+
db.exec("DELETE FROM snapshots");
|
|
6416
|
+
db.exec("DELETE FROM undo");
|
|
6417
|
+
}
|
|
6418
|
+
const servedColumns = db.prepare("PRAGMA table_info(served)").all();
|
|
6419
|
+
if (versionChanged || servedColumns.some((column) => column.name === "session_id")) db.exec("DROP TABLE IF EXISTS served");
|
|
6420
|
+
db.exec("CREATE TABLE IF NOT EXISTS served (path TEXT NOT NULL PRIMARY KEY, hashes TEXT NOT NULL, reported TEXT, updated_at INTEGER NOT NULL)");
|
|
6421
|
+
db.prepare("INSERT INTO meta (key, value) VALUES ('version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(String(HASH_STORE_VERSION));
|
|
6422
|
+
const getStmt = db.prepare("SELECT hashes FROM snapshots WHERE path = ? AND checksum = ? AND line_count = ?");
|
|
6423
|
+
const allStmt = db.prepare("SELECT path FROM snapshots UNION SELECT path FROM undo UNION SELECT path FROM served");
|
|
6424
|
+
const allHashesStmt = db.prepare("SELECT path, hashes FROM snapshots");
|
|
6425
|
+
const delStmt = db.prepare("DELETE FROM snapshots WHERE path = ?");
|
|
6426
|
+
const upsertStmt = db.prepare("INSERT INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET checksum = excluded.checksum, line_count = excluded.line_count, hashes = excluded.hashes, updated_at = excluded.updated_at");
|
|
6427
|
+
const undoUpsertStmt = db.prepare("INSERT INTO undo (path, content, bom, ending, hashes, result_content, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET content = excluded.content, bom = excluded.bom, ending = excluded.ending, hashes = excluded.hashes, result_content = excluded.result_content, updated_at = excluded.updated_at");
|
|
6428
|
+
const undoGetStmt = db.prepare("SELECT content, bom, ending, hashes, result_content FROM undo WHERE path = ?");
|
|
6429
|
+
const undoDelStmt = db.prepare("DELETE FROM undo WHERE path = ?");
|
|
6430
|
+
const servedGetStmt = db.prepare("SELECT hashes, reported FROM served WHERE path = ?");
|
|
6431
|
+
const servedUpsertStmt = db.prepare("INSERT INTO served (path, hashes, updated_at) VALUES (?, ?, ?) ON CONFLICT(path) DO UPDATE SET hashes = excluded.hashes, updated_at = excluded.updated_at");
|
|
6432
|
+
const servedReportedUpsertStmt = db.prepare("INSERT INTO served (path, hashes, reported, updated_at) VALUES (?, '[]', ?, ?) ON CONFLICT(path) DO UPDATE SET reported = excluded.reported, updated_at = excluded.updated_at");
|
|
6433
|
+
const servedReportedClearStmt = db.prepare("UPDATE served SET reported = NULL, updated_at = ? WHERE path = ?");
|
|
6434
|
+
const servedDeleteStmt = db.prepare("DELETE FROM served WHERE path = ?");
|
|
6435
|
+
const servedDeletePathStmt = db.prepare("DELETE FROM served WHERE path = ?");
|
|
6436
|
+
const servedPruneOlderThanStmt = db.prepare("DELETE FROM served WHERE updated_at < ?");
|
|
6507
6437
|
return {
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6438
|
+
db,
|
|
6439
|
+
stmts: {
|
|
6440
|
+
get: (...params) => getStmt.get(...params),
|
|
6441
|
+
allPaths: (...params) => allStmt.all(...params),
|
|
6442
|
+
allHashes: (...params) => allHashesStmt.all(...params),
|
|
6443
|
+
deleteOne: (...params) => {
|
|
6444
|
+
withBusyRetry(() => {
|
|
6445
|
+
delStmt.run(...params);
|
|
6446
|
+
});
|
|
6447
|
+
},
|
|
6448
|
+
upsert: (...params) => {
|
|
6449
|
+
withBusyRetry(() => {
|
|
6450
|
+
upsertStmt.run(...params);
|
|
6451
|
+
});
|
|
6452
|
+
},
|
|
6453
|
+
undoUpsert: (...params) => {
|
|
6454
|
+
withBusyRetry(() => {
|
|
6455
|
+
undoUpsertStmt.run(...params);
|
|
6456
|
+
});
|
|
6457
|
+
},
|
|
6458
|
+
undoGet: (...params) => undoGetStmt.get(...params),
|
|
6459
|
+
undoDelete: (...params) => {
|
|
6460
|
+
withBusyRetry(() => {
|
|
6461
|
+
undoDelStmt.run(...params);
|
|
6462
|
+
});
|
|
6463
|
+
},
|
|
6464
|
+
servedGet: (...params) => servedGetStmt.get(...params),
|
|
6465
|
+
servedUpsert: (...params) => {
|
|
6466
|
+
withBusyRetry(() => {
|
|
6467
|
+
servedUpsertStmt.run(...params);
|
|
6468
|
+
});
|
|
6469
|
+
},
|
|
6470
|
+
servedReportedUpsert: (...params) => {
|
|
6471
|
+
withBusyRetry(() => {
|
|
6472
|
+
servedReportedUpsertStmt.run(...params);
|
|
6473
|
+
});
|
|
6474
|
+
},
|
|
6475
|
+
servedReportedClear: (path$1) => {
|
|
6476
|
+
withBusyRetry(() => {
|
|
6477
|
+
servedReportedClearStmt.run(Date.now(), path$1);
|
|
6478
|
+
});
|
|
6479
|
+
},
|
|
6480
|
+
servedDelete: (...params) => {
|
|
6481
|
+
withBusyRetry(() => {
|
|
6482
|
+
servedDeleteStmt.run(...params);
|
|
6483
|
+
});
|
|
6484
|
+
},
|
|
6485
|
+
servedDeletePath: (...params) => {
|
|
6486
|
+
withBusyRetry(() => {
|
|
6487
|
+
servedDeletePathStmt.run(...params);
|
|
6488
|
+
});
|
|
6489
|
+
},
|
|
6490
|
+
servedPruneOlderThan: (...params) => {
|
|
6491
|
+
withBusyRetry(() => {
|
|
6492
|
+
servedPruneOlderThanStmt.run(...params);
|
|
6493
|
+
});
|
|
6494
|
+
}
|
|
6495
|
+
}
|
|
6496
|
+
};
|
|
6511
6497
|
}
|
|
6512
|
-
/**
|
|
6513
|
-
function
|
|
6514
|
-
const lineByHash = /* @__PURE__ */ new Map();
|
|
6515
|
-
for (let i = 0; i < fileHashes.length; i++) lineByHash.set(fileHashes[i], i + 1);
|
|
6516
|
-
const [startRef, endRef] = edit.hash_bounds;
|
|
6517
|
-
const startLine = lineByHash.get(startRef.hash);
|
|
6518
|
-
const endLine = lineByHash.get(endRef.hash);
|
|
6519
|
-
if (startLine === void 0 || endLine === void 0 || startLine <= endLine) return edit;
|
|
6520
|
-
warnings.push(`[E_BAD_OP] reversed remove_from/remove_to (${startRef.hash} after ${endRef.hash}); swapped.`);
|
|
6498
|
+
/** Wire the domain methods over the prepared statements. */
|
|
6499
|
+
function makeDomainStore(stmts) {
|
|
6521
6500
|
return {
|
|
6522
|
-
|
|
6523
|
-
|
|
6501
|
+
engine: "node:sqlite",
|
|
6502
|
+
getSnapshot(path$1, content, deleteCorrupt = true) {
|
|
6503
|
+
const checksum = cacheKey(contentChecksum(content));
|
|
6504
|
+
const lineCount = splitLines(content).length;
|
|
6505
|
+
const row = stmts.get(path$1, checksum, lineCount);
|
|
6506
|
+
if (!row) return void 0;
|
|
6507
|
+
try {
|
|
6508
|
+
const parsed = JSON.parse(row.hashes);
|
|
6509
|
+
if (isValidHashList(parsed)) return parsed;
|
|
6510
|
+
if (deleteCorrupt) stmts.deleteOne(path$1);
|
|
6511
|
+
return;
|
|
6512
|
+
} catch {
|
|
6513
|
+
if (deleteCorrupt) stmts.deleteOne(path$1);
|
|
6514
|
+
return;
|
|
6515
|
+
}
|
|
6516
|
+
},
|
|
6517
|
+
upsertSnapshot(path$1, checksum, lineCount, hashes) {
|
|
6518
|
+
stmts.upsert(path$1, cacheKey(checksum), lineCount, JSON.stringify(hashes), Date.now());
|
|
6519
|
+
},
|
|
6520
|
+
allKnownPaths() {
|
|
6521
|
+
return stmts.allPaths();
|
|
6522
|
+
},
|
|
6523
|
+
allSnapshotHashes() {
|
|
6524
|
+
return stmts.allHashes();
|
|
6525
|
+
},
|
|
6526
|
+
deleteSnapshot(path$1) {
|
|
6527
|
+
stmts.deleteOne(path$1);
|
|
6528
|
+
},
|
|
6529
|
+
findSnapshotPaths(hashes) {
|
|
6530
|
+
const rows = stmts.allHashes();
|
|
6531
|
+
const matches = [];
|
|
6532
|
+
for (const row of rows) try {
|
|
6533
|
+
const parsed = JSON.parse(row.hashes);
|
|
6534
|
+
if (!isValidHashList(parsed)) continue;
|
|
6535
|
+
if (hashes.every((h) => parsed.includes(h))) matches.push(row.path);
|
|
6536
|
+
} catch {}
|
|
6537
|
+
return matches;
|
|
6538
|
+
},
|
|
6539
|
+
getUndo(path$1) {
|
|
6540
|
+
const row = stmts.undoGet(path$1);
|
|
6541
|
+
if (!row) return void 0;
|
|
6542
|
+
try {
|
|
6543
|
+
const parsed = JSON.parse(row.hashes);
|
|
6544
|
+
if (!isValidHashList(parsed)) {
|
|
6545
|
+
stmts.undoDelete(path$1);
|
|
6546
|
+
return;
|
|
6547
|
+
}
|
|
6548
|
+
return {
|
|
6549
|
+
content: row.content,
|
|
6550
|
+
bom: row.bom,
|
|
6551
|
+
ending: row.ending,
|
|
6552
|
+
hashes: parsed,
|
|
6553
|
+
resultContent: row.result_content
|
|
6554
|
+
};
|
|
6555
|
+
} catch {
|
|
6556
|
+
stmts.undoDelete(path$1);
|
|
6557
|
+
return;
|
|
6558
|
+
}
|
|
6559
|
+
},
|
|
6560
|
+
upsertUndo(path$1, entry) {
|
|
6561
|
+
stmts.undoUpsert(path$1, entry.content, entry.bom, entry.ending, JSON.stringify(entry.hashes), entry.resultContent, Date.now());
|
|
6562
|
+
},
|
|
6563
|
+
deleteUndo(path$1) {
|
|
6564
|
+
stmts.undoDelete(path$1);
|
|
6565
|
+
},
|
|
6566
|
+
getServed(path$1) {
|
|
6567
|
+
const row = stmts.servedGet(path$1);
|
|
6568
|
+
if (!row) return [];
|
|
6569
|
+
try {
|
|
6570
|
+
const parsed = JSON.parse(row.hashes);
|
|
6571
|
+
if (isValidServedList(parsed)) return parsed;
|
|
6572
|
+
stmts.servedDelete(path$1);
|
|
6573
|
+
return [];
|
|
6574
|
+
} catch {
|
|
6575
|
+
stmts.servedDelete(path$1);
|
|
6576
|
+
return [];
|
|
6577
|
+
}
|
|
6578
|
+
},
|
|
6579
|
+
getServedReported(path$1) {
|
|
6580
|
+
const row = stmts.servedGet(path$1);
|
|
6581
|
+
if (!row) return /* @__PURE__ */ new Set();
|
|
6582
|
+
const raw = row.reported;
|
|
6583
|
+
if (typeof raw !== "string" || raw.length === 0) return /* @__PURE__ */ new Set();
|
|
6584
|
+
try {
|
|
6585
|
+
const parsed = JSON.parse(raw);
|
|
6586
|
+
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
6587
|
+
return new Set(parsed.filter((h) => typeof h === "string" && HASH_RE.test(h)));
|
|
6588
|
+
} catch {
|
|
6589
|
+
return /* @__PURE__ */ new Set();
|
|
6590
|
+
}
|
|
6591
|
+
},
|
|
6592
|
+
upsertServed(path$1, hashesJson) {
|
|
6593
|
+
stmts.servedUpsert(path$1, hashesJson, Date.now());
|
|
6594
|
+
},
|
|
6595
|
+
upsertServedReported(path$1, reportedJson) {
|
|
6596
|
+
stmts.servedReportedUpsert(path$1, reportedJson, Date.now());
|
|
6597
|
+
},
|
|
6598
|
+
clearServedReported(path$1) {
|
|
6599
|
+
stmts.servedReportedClear(path$1);
|
|
6600
|
+
},
|
|
6601
|
+
deleteServed(path$1) {
|
|
6602
|
+
stmts.servedDelete(path$1);
|
|
6603
|
+
},
|
|
6604
|
+
deleteServedByPath(path$1) {
|
|
6605
|
+
stmts.servedDeletePath(path$1);
|
|
6606
|
+
},
|
|
6607
|
+
pruneServedOlderThan(ts) {
|
|
6608
|
+
stmts.servedPruneOlderThan(ts);
|
|
6609
|
+
},
|
|
6610
|
+
async pruneMissing() {
|
|
6611
|
+
const missing = await statMissing(stmts.allPaths());
|
|
6612
|
+
if (missing.length === 0) return;
|
|
6613
|
+
withStore(() => {
|
|
6614
|
+
for (const path$1 of missing) {
|
|
6615
|
+
stmts.deleteOne(path$1);
|
|
6616
|
+
stmts.undoDelete(path$1);
|
|
6617
|
+
stmts.servedDeletePath(path$1);
|
|
6618
|
+
}
|
|
6619
|
+
});
|
|
6620
|
+
}
|
|
6524
6621
|
};
|
|
6525
6622
|
}
|
|
6526
|
-
function
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
if (contentLines[start - k] !== fileLines[endLine + k]) break;
|
|
6533
|
-
dups.push({
|
|
6534
|
-
kind: "trailing",
|
|
6535
|
-
replacementLineIndex: start - k
|
|
6536
|
-
});
|
|
6623
|
+
function isHealthy(db) {
|
|
6624
|
+
try {
|
|
6625
|
+
return db.prepare("PRAGMA quick_check").get()?.quick_check === "ok";
|
|
6626
|
+
} catch (error) {
|
|
6627
|
+
if (isCorruptionError(error)) return false;
|
|
6628
|
+
return true;
|
|
6537
6629
|
}
|
|
6538
|
-
return dups;
|
|
6539
6630
|
}
|
|
6540
|
-
function
|
|
6541
|
-
const
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
});
|
|
6631
|
+
async function quarantineStore(storePath) {
|
|
6632
|
+
const suffix = `.corrupt-${Date.now()}`;
|
|
6633
|
+
for (const candidate of [
|
|
6634
|
+
storePath,
|
|
6635
|
+
`${storePath}-wal`,
|
|
6636
|
+
`${storePath}-shm`
|
|
6637
|
+
]) try {
|
|
6638
|
+
await rename(candidate, `${candidate}${suffix}`);
|
|
6639
|
+
} catch (error) {
|
|
6640
|
+
if (errCode(error) !== "ENOENT") console.error("Failed to quarantine corrupt hash store file:", error);
|
|
6551
6641
|
}
|
|
6552
|
-
return dups;
|
|
6553
6642
|
}
|
|
6554
|
-
function
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6643
|
+
function shutdownDb(db) {
|
|
6644
|
+
try {
|
|
6645
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
6646
|
+
} catch {}
|
|
6647
|
+
db.close();
|
|
6648
|
+
}
|
|
6649
|
+
const STAT_BATCH = 64;
|
|
6650
|
+
async function statMissing(rows) {
|
|
6651
|
+
const missing = [];
|
|
6652
|
+
for (let i = 0; i < rows.length; i += STAT_BATCH) {
|
|
6653
|
+
const batch = rows.slice(i, i + STAT_BATCH);
|
|
6654
|
+
const results = await Promise.all(batch.map(async (row) => {
|
|
6655
|
+
try {
|
|
6656
|
+
await stat(row.path);
|
|
6657
|
+
return;
|
|
6658
|
+
} catch {
|
|
6659
|
+
return row.path;
|
|
6660
|
+
}
|
|
6661
|
+
}));
|
|
6662
|
+
for (const path$1 of results) if (path$1 !== void 0) missing.push(path$1);
|
|
6562
6663
|
}
|
|
6563
|
-
return
|
|
6664
|
+
return missing;
|
|
6564
6665
|
}
|
|
6565
|
-
function
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
let
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6666
|
+
async function openStore(storePath) {
|
|
6667
|
+
await initHasher();
|
|
6668
|
+
await mkdir(dirname(storePath), { recursive: true });
|
|
6669
|
+
let existed = existsSync(storePath);
|
|
6670
|
+
let opened;
|
|
6671
|
+
try {
|
|
6672
|
+
opened = openDbWithBusyRetry(storePath);
|
|
6673
|
+
} catch (error) {
|
|
6674
|
+
if (!isCorruptionError(error)) throw error;
|
|
6675
|
+
console.error("Hash store failed to open, rebuilding:", error);
|
|
6676
|
+
await quarantineStore(storePath);
|
|
6677
|
+
existed = false;
|
|
6678
|
+
opened = openDbWithBusyRetry(storePath);
|
|
6679
|
+
}
|
|
6680
|
+
if (!isHealthy(opened.db)) {
|
|
6681
|
+
shutdownDb(opened.db);
|
|
6682
|
+
await quarantineStore(storePath);
|
|
6683
|
+
existed = false;
|
|
6684
|
+
opened = openDbWithBusyRetry(storePath);
|
|
6685
|
+
}
|
|
6686
|
+
const { db, stmts } = opened;
|
|
6687
|
+
if (!existed) await migrateLegacy(db, storePath);
|
|
6688
|
+
withBusyRetry(() => {
|
|
6689
|
+
stmts.servedPruneOlderThan(Date.now() - SERVED_TTL_MS);
|
|
6576
6690
|
});
|
|
6577
|
-
|
|
6691
|
+
const store = makeDomainStore(stmts);
|
|
6692
|
+
stores.set(storePath, {
|
|
6693
|
+
path: storePath,
|
|
6694
|
+
db,
|
|
6695
|
+
stmts,
|
|
6696
|
+
store
|
|
6697
|
+
});
|
|
6698
|
+
if (!exitHandlerRegistered) {
|
|
6699
|
+
exitHandlerRegistered = true;
|
|
6700
|
+
process.once("exit", () => shutdownHashStore());
|
|
6701
|
+
for (const sig of ["SIGINT", "SIGTERM"]) process.once(sig, () => {
|
|
6702
|
+
shutdownHashStore();
|
|
6703
|
+
process.kill(process.pid, sig);
|
|
6704
|
+
});
|
|
6705
|
+
}
|
|
6706
|
+
return store;
|
|
6578
6707
|
}
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6708
|
+
/** Resolve the store path: the single centralized store under `$DSH_HOME/storages`. */
|
|
6709
|
+
function storePathFor() {
|
|
6710
|
+
return hashStorePath();
|
|
6711
|
+
}
|
|
6712
|
+
/**
|
|
6713
|
+
* Load (and cache) the centralized hash store. There is exactly one store per
|
|
6714
|
+
* harness home; `DSH_HOME` isolates deployments (test homes never touch prod).
|
|
6715
|
+
*/
|
|
6716
|
+
function loadHashStore() {
|
|
6717
|
+
const storePath = storePathFor();
|
|
6718
|
+
const cached = stores.get(storePath);
|
|
6719
|
+
if (cached && cached.db.isOpen) return Promise.resolve(cached.store);
|
|
6720
|
+
const existing = openings.get(storePath);
|
|
6721
|
+
if (existing) return existing;
|
|
6722
|
+
const promise = openStore(storePath).finally(() => {
|
|
6723
|
+
openings.delete(storePath);
|
|
6591
6724
|
});
|
|
6592
|
-
|
|
6725
|
+
openings.set(storePath, promise);
|
|
6726
|
+
return promise;
|
|
6593
6727
|
}
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
6599
|
-
}
|
|
6600
|
-
return counts;
|
|
6728
|
+
/** The cached store entry for the active workspace (or the shared-home fallback), if open. */
|
|
6729
|
+
function currentStore() {
|
|
6730
|
+
const entry = stores.get(storePathFor());
|
|
6731
|
+
return entry?.db.isOpen ? entry : void 0;
|
|
6601
6732
|
}
|
|
6602
|
-
/**
|
|
6603
|
-
function
|
|
6604
|
-
const
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
for (let i = start; i >= 0 && i < contentLines.length; i += step) {
|
|
6608
|
-
const line = contentLines[i];
|
|
6609
|
-
if (line.length === 0) continue;
|
|
6610
|
-
const key = canon(line);
|
|
6611
|
-
const count = multiset.get(key) ?? 0;
|
|
6612
|
-
if (count > 0) multiset.set(key, count - 1);
|
|
6613
|
-
else return {
|
|
6614
|
-
index: i,
|
|
6615
|
-
line
|
|
6616
|
-
};
|
|
6617
|
-
}
|
|
6733
|
+
/** Close every open store (process exit, HMR, tests). */
|
|
6734
|
+
function shutdownHashStore() {
|
|
6735
|
+
for (const [, entry] of stores) shutdownDb(entry.db);
|
|
6736
|
+
stores.clear();
|
|
6737
|
+
openings.clear();
|
|
6618
6738
|
}
|
|
6619
|
-
/**
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
}
|
|
6637
|
-
return result;
|
|
6638
|
-
};
|
|
6639
|
-
abortIf(signal);
|
|
6640
|
-
const startResolved = tryResolve(edit.hash_bounds[0]);
|
|
6641
|
-
const endResolved = tryResolve(edit.hash_bounds[1]);
|
|
6642
|
-
if (!startResolved || !endResolved) {
|
|
6643
|
-
if (!startResolved && endResolved) {
|
|
6644
|
-
const startMismatch = mismatches.findLast((m) => m.ref === edit.hash_bounds[0]);
|
|
6645
|
-
if (startMismatch && startMismatch.kind === "not_found") startMismatch.context = endResolved;
|
|
6646
|
-
} else if (startResolved && !endResolved) {
|
|
6647
|
-
const endMismatch = mismatches.findLast((m) => m.ref === edit.hash_bounds[1]);
|
|
6648
|
-
if (endMismatch && endMismatch.kind === "not_found") endMismatch.context = startResolved;
|
|
6739
|
+
/**
|
|
6740
|
+
* Run `fn` inside one BEGIN IMMEDIATE transaction on the active workspace's
|
|
6741
|
+
* store. Without an open store for this context the call runs bare (the
|
|
6742
|
+
* caller has already loaded the store in every in-process path).
|
|
6743
|
+
*/
|
|
6744
|
+
function withStore(fn) {
|
|
6745
|
+
const store = currentStore();
|
|
6746
|
+
if (store) withBusyRetry(() => {
|
|
6747
|
+
store.db.exec("BEGIN IMMEDIATE");
|
|
6748
|
+
try {
|
|
6749
|
+
fn();
|
|
6750
|
+
store.db.exec("COMMIT");
|
|
6751
|
+
} catch (e) {
|
|
6752
|
+
try {
|
|
6753
|
+
store.db.exec("ROLLBACK");
|
|
6754
|
+
} catch {}
|
|
6755
|
+
throw e;
|
|
6649
6756
|
}
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
mismatches,
|
|
6653
|
-
boundaryDups
|
|
6654
|
-
};
|
|
6655
|
-
}
|
|
6656
|
-
if (startResolved.line > endResolved.line) throw new Error(`[E_BAD_OP] Range start line ${startResolved.line} must be <= end line ${endResolved.line} (anchors ${edit.hash_bounds[0].hash} and ${edit.hash_bounds[1].hash}).`);
|
|
6657
|
-
const endLine = endResolved.line;
|
|
6658
|
-
const rangeLines = fileLines.slice(startResolved.line - 1, endLine);
|
|
6659
|
-
const canonLines = fileLines.map((line) => canon(line));
|
|
6660
|
-
boundaryDups.push(...trailingDups(edit.content_lines, fileLines, endLine), ...leadingDups(edit.content_lines, fileLines, startResolved.line), ...firstNewAfterDups(edit.content_lines, rangeLines, canonLines, endLine), ...lastNewBeforeDups(edit.content_lines, rangeLines, canonLines, startResolved.line));
|
|
6661
|
-
return {
|
|
6662
|
-
resolved: {
|
|
6663
|
-
content_lines: edit.content_lines,
|
|
6664
|
-
hash_bounds: [startResolved, endResolved]
|
|
6665
|
-
},
|
|
6666
|
-
mismatches,
|
|
6667
|
-
boundaryDups
|
|
6668
|
-
};
|
|
6757
|
+
});
|
|
6758
|
+
else fn();
|
|
6669
6759
|
}
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
this.servedRows = opts.servedRows;
|
|
6760
|
+
async function migrateLegacy(db, storePath) {
|
|
6761
|
+
const legacyPath = join(dirname(storePath), "hash-store.json");
|
|
6762
|
+
let content;
|
|
6763
|
+
try {
|
|
6764
|
+
content = await readFile(legacyPath, "utf-8");
|
|
6765
|
+
} catch (error) {
|
|
6766
|
+
if (errCode(error) === "ENOENT") return;
|
|
6767
|
+
console.error("Failed to read legacy hash store for migration:", error);
|
|
6768
|
+
return;
|
|
6680
6769
|
}
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
this.servedRows = servedRows;
|
|
6770
|
+
let parsed;
|
|
6771
|
+
try {
|
|
6772
|
+
parsed = JSON.parse(content);
|
|
6773
|
+
} catch (error) {
|
|
6774
|
+
console.error("Failed to parse legacy hash store, skipping migration:", error);
|
|
6775
|
+
return;
|
|
6688
6776
|
}
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
const total = endLine - startLine + 1;
|
|
6692
|
-
const shown = Math.min(total, SERVED_ECHO_CAP);
|
|
6777
|
+
const raw = parsed.snapshots;
|
|
6778
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
6693
6779
|
const rows = [];
|
|
6694
|
-
for (
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
}
|
|
6700
|
-
|
|
6701
|
-
|
|
6780
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
6781
|
+
if (!isValidSnapshot(value)) continue;
|
|
6782
|
+
if (new Set(value.hashes).size !== value.hashes.length) {
|
|
6783
|
+
console.warn(`Skipped legacy snapshot with duplicate hashes for ${key}; it will be re-hashed on next read.`);
|
|
6784
|
+
continue;
|
|
6785
|
+
}
|
|
6786
|
+
rows.push([
|
|
6787
|
+
key,
|
|
6788
|
+
contentChecksum(value.content),
|
|
6789
|
+
splitLines(value.content).length,
|
|
6790
|
+
JSON.stringify(value.hashes),
|
|
6791
|
+
Date.now()
|
|
6792
|
+
]);
|
|
6793
|
+
}
|
|
6794
|
+
if (rows.length > 0) {
|
|
6795
|
+
db.exec("BEGIN IMMEDIATE");
|
|
6796
|
+
try {
|
|
6797
|
+
const stmt = db.prepare("INSERT OR REPLACE INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?)");
|
|
6798
|
+
for (const row of rows) stmt.run(...row);
|
|
6799
|
+
db.exec("COMMIT");
|
|
6800
|
+
} catch (e) {
|
|
6801
|
+
db.exec("ROLLBACK");
|
|
6802
|
+
throw e;
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
try {
|
|
6806
|
+
await rename(legacyPath, `${legacyPath}.bak`);
|
|
6807
|
+
} catch (error) {
|
|
6808
|
+
console.error("Failed to rename legacy hash store after migration:", error);
|
|
6809
|
+
}
|
|
6702
6810
|
}
|
|
6703
|
-
|
|
6704
|
-
|
|
6811
|
+
/** Find files whose stored snapshot hashes contain every given anchor. */
|
|
6812
|
+
async function findSnapshotPathsByHashes(hashes) {
|
|
6813
|
+
return (await loadHashStore()).findSnapshotPaths(hashes);
|
|
6705
6814
|
}
|
|
6706
|
-
|
|
6707
|
-
|
|
6815
|
+
/** Persist a hash snapshot for one path (async over the active store). */
|
|
6816
|
+
async function upsertSnapshotFor(path$1, checksum, lineCount, hashes) {
|
|
6817
|
+
(await loadHashStore()).upsertSnapshot(path$1, checksum, lineCount, hashes);
|
|
6708
6818
|
}
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
let from;
|
|
6720
|
-
let to;
|
|
6721
|
-
if (startPositions.length === 1 && endPositions.length === 1) {
|
|
6722
|
-
from = Math.min(startPositions[0], endPositions[0]);
|
|
6723
|
-
to = Math.max(startPositions[0], endPositions[0]);
|
|
6724
|
-
} else {
|
|
6725
|
-
const candidates = [];
|
|
6726
|
-
for (const s of startPositions) for (const e of endPositions) {
|
|
6727
|
-
const candFrom = Math.min(s, e);
|
|
6728
|
-
const candTo = Math.max(s, e);
|
|
6729
|
-
if (candTo - candFrom + 1 !== currentLen) continue;
|
|
6730
|
-
let ok = true;
|
|
6731
|
-
for (let k = 0; k < currentLen; k++) if (served[candFrom + k] !== fileHashes[startLine - 1 + k]) {
|
|
6732
|
-
ok = false;
|
|
6733
|
-
break;
|
|
6734
|
-
}
|
|
6735
|
-
if (ok) candidates.push({
|
|
6736
|
-
from: candFrom,
|
|
6737
|
-
to: candTo
|
|
6738
|
-
});
|
|
6819
|
+
|
|
6820
|
+
//#endregion
|
|
6821
|
+
//#region src/url-schema/vendored/hashline/hashline/hash.js
|
|
6822
|
+
let defaultHashSnapshotIO;
|
|
6823
|
+
function snapshotIOFor(store) {
|
|
6824
|
+
if (store) return {
|
|
6825
|
+
get: (path$1, content, deleteCorrupt) => Promise.resolve(store.getSnapshot(path$1, content, deleteCorrupt)),
|
|
6826
|
+
upsert: (path$1, checksum, lineCount, hashes) => {
|
|
6827
|
+
store.upsertSnapshot(path$1, checksum, lineCount, hashes);
|
|
6828
|
+
return Promise.resolve();
|
|
6739
6829
|
}
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6830
|
+
};
|
|
6831
|
+
return defaultHashSnapshotIO ?? {
|
|
6832
|
+
get: async (path$1, content, deleteCorrupt) => {
|
|
6833
|
+
return (await loadHashStore()).getSnapshot(path$1, content, deleteCorrupt);
|
|
6834
|
+
},
|
|
6835
|
+
upsert: async (path$1, checksum, lineCount, hashes) => {
|
|
6836
|
+
(await loadHashStore()).upsertSnapshot(path$1, checksum, lineCount, hashes);
|
|
6837
|
+
}
|
|
6838
|
+
};
|
|
6839
|
+
}
|
|
6840
|
+
async function lineHashes(content, path$1, previous, ioOrStore, persist) {
|
|
6841
|
+
await initHasher();
|
|
6842
|
+
if (!path$1) return lineHashesPure(content);
|
|
6843
|
+
const io = ioOrStore && "getSnapshot" in ioOrStore ? snapshotIOFor(ioOrStore) : ioOrStore ?? snapshotIOFor(void 0);
|
|
6844
|
+
if (previous) {
|
|
6845
|
+
const newHashes$1 = mapStableHashes(previous.content, previous.hashes, content, previous.removedHashes);
|
|
6846
|
+
if (persist !== false && io) try {
|
|
6847
|
+
await io.upsert(path$1, contentChecksum(content), splitLines(content).length, newHashes$1);
|
|
6848
|
+
} catch (e) {
|
|
6849
|
+
console.error("Failed to persist hash snapshot:", e);
|
|
6747
6850
|
}
|
|
6851
|
+
return newHashes$1;
|
|
6748
6852
|
}
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
else if (endPositions.length > 1) problems.push(`remove_to "${endHash}" was served at ${endPositions.length} positions`);
|
|
6755
|
-
throw new ServedRejectionError({
|
|
6756
|
-
code: "E_RANGE_UNVERIFIED",
|
|
6757
|
-
message: `[E_RANGE_UNVERIFIED] cannot verify range against served state${where}: ${problems.join("; ")}. Current range:
|
|
6758
|
-
${echo}
|
|
6759
|
-
${retryHint()}`,
|
|
6760
|
-
servedRows: echoRows
|
|
6761
|
-
});
|
|
6853
|
+
let cached;
|
|
6854
|
+
if (io) try {
|
|
6855
|
+
cached = await io.get(path$1, content, persist !== false);
|
|
6856
|
+
} catch (e) {
|
|
6857
|
+
console.error("Failed to read hash store snapshot:", e);
|
|
6762
6858
|
}
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
const servedLen = to - from + 1;
|
|
6770
|
-
if (servedLen !== currentLen) throw new ServedRejectionError({
|
|
6771
|
-
code: "E_RANGE_STALE",
|
|
6772
|
-
message: `[E_RANGE_STALE] served span (${servedLen} lines) no longer matches current range (${currentLen} lines)${where}.\nCurrent range:\n${echo}\n${retryHint()}`,
|
|
6773
|
-
firstOffendingLine: startLine,
|
|
6774
|
-
servedRows: echoRows
|
|
6775
|
-
});
|
|
6776
|
-
for (let k = 0; k < servedLen; k++) if (served[from + k] !== fileHashes[startLine - 1 + k]) {
|
|
6777
|
-
const offendingLine = startLine + k;
|
|
6778
|
-
throw new ServedRejectionError({
|
|
6779
|
-
code: "E_RANGE_STALE",
|
|
6780
|
-
message: `[E_RANGE_STALE] Line ${offendingLine}${where} differs from what you were served — the file changed on disk since it was read. Current range:\n${echo}\n${retryHint()}`,
|
|
6781
|
-
firstOffendingLine: offendingLine,
|
|
6782
|
-
servedRows: echoRows
|
|
6783
|
-
});
|
|
6859
|
+
if (cached) return cached;
|
|
6860
|
+
const newHashes = lineHashesPure(content);
|
|
6861
|
+
if (persist !== false && io) try {
|
|
6862
|
+
await io.upsert(path$1, contentChecksum(content), splitLines(content).length, newHashes);
|
|
6863
|
+
} catch (e) {
|
|
6864
|
+
console.error("Failed to persist hash snapshot:", e);
|
|
6784
6865
|
}
|
|
6866
|
+
return newHashes;
|
|
6785
6867
|
}
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6868
|
+
|
|
6869
|
+
//#endregion
|
|
6870
|
+
//#region src/url-schema/vendored/hashline/session-view.js
|
|
6871
|
+
const current = new AsyncLocalStorage();
|
|
6872
|
+
function withWorkspace(cwd, fn) {
|
|
6873
|
+
return current.run(cwd, fn);
|
|
6789
6874
|
}
|
|
6790
|
-
function
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6875
|
+
function execCwd(exec) {
|
|
6876
|
+
return exec.agent?.session.header.cwd ?? process.cwd();
|
|
6877
|
+
}
|
|
6878
|
+
/**
|
|
6879
|
+
* Merge served rows into a copy of the stored array. This single helper owns
|
|
6880
|
+
* the served-merge invariant shared by recordServed and recordServedTruncated.
|
|
6881
|
+
* Eagerly heals orphaned serves: if the same hash is written at a new position
|
|
6882
|
+
* the old position is nulled (O(n) scan, no extra I/O). This prevents a
|
|
6883
|
+
* partial re-serve from leaving a stale duplicate behind (ADR-0008).
|
|
6884
|
+
*/
|
|
6885
|
+
function _mergeServedRows(current$1, rows, options) {
|
|
6886
|
+
const updated = current$1.slice();
|
|
6887
|
+
if (options?.truncateTo !== void 0 && updated.length > options.truncateTo) updated.length = options.truncateTo;
|
|
6888
|
+
if (options?.clearFrom !== void 0) for (let i = options.clearFrom; i < updated.length; i++) updated[i] = null;
|
|
6889
|
+
const index = /* @__PURE__ */ new Map();
|
|
6890
|
+
for (let i = 0; i < updated.length; i++) {
|
|
6891
|
+
const h = updated[i];
|
|
6892
|
+
if (h === null) continue;
|
|
6893
|
+
const prev = index.get(h);
|
|
6894
|
+
if (prev !== void 0) updated[prev] = null;
|
|
6895
|
+
index.set(h, i);
|
|
6798
6896
|
}
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6897
|
+
for (const entry of rows) {
|
|
6898
|
+
if (!Number.isInteger(entry.position) || entry.position < 0) throw new TypeError(`Invalid served position: ${entry.position}`);
|
|
6899
|
+
if (entry.hash !== null && (typeof entry.hash !== "string" || !HASH_RE.test(entry.hash))) throw new TypeError(`Invalid served hash: ${String(entry.hash)}`);
|
|
6900
|
+
while (updated.length <= entry.position) updated.push(null);
|
|
6901
|
+
if (entry.hash !== null) {
|
|
6902
|
+
const existing = index.get(entry.hash);
|
|
6903
|
+
if (existing !== void 0 && existing !== entry.position) {
|
|
6904
|
+
updated[existing] = null;
|
|
6905
|
+
index.delete(entry.hash);
|
|
6906
|
+
}
|
|
6907
|
+
const oldAtPos = updated[entry.position];
|
|
6908
|
+
if (oldAtPos !== null && oldAtPos !== entry.hash) index.delete(oldAtPos);
|
|
6909
|
+
index.set(entry.hash, entry.position);
|
|
6910
|
+
} else {
|
|
6911
|
+
const oldAtPos = updated[entry.position];
|
|
6912
|
+
if (oldAtPos !== null) index.delete(oldAtPos);
|
|
6913
|
+
}
|
|
6914
|
+
updated[entry.position] = entry.hash;
|
|
6915
|
+
}
|
|
6916
|
+
while (updated.length > 0 && updated[updated.length - 1] === null) updated.pop();
|
|
6917
|
+
return updated;
|
|
6803
6918
|
}
|
|
6804
|
-
function
|
|
6805
|
-
|
|
6919
|
+
async function loadServed(path$1) {
|
|
6920
|
+
return (await loadHashStore()).getServed(path$1);
|
|
6806
6921
|
}
|
|
6807
|
-
function
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
end: lineStarts[endLine - 1] + fileLines[endLine - 1].length,
|
|
6821
|
-
replacement: edit.content_lines.join("\n")
|
|
6822
|
-
};
|
|
6823
|
-
if (startLine === 1 && endLine === fileLines.length) return {
|
|
6824
|
-
kind: "replace",
|
|
6825
|
-
start: 0,
|
|
6826
|
-
end: content.length,
|
|
6827
|
-
replacement: ""
|
|
6828
|
-
};
|
|
6829
|
-
if (endLine < fileLines.length) return {
|
|
6830
|
-
kind: "replace",
|
|
6831
|
-
start: lineStarts[startLine - 1],
|
|
6832
|
-
end: lineStarts[endLine],
|
|
6833
|
-
replacement: ""
|
|
6834
|
-
};
|
|
6835
|
-
if (content.endsWith("\n")) return {
|
|
6836
|
-
kind: "replace",
|
|
6837
|
-
start: lineStarts[startLine - 1],
|
|
6838
|
-
end: content.length,
|
|
6839
|
-
replacement: ""
|
|
6840
|
-
};
|
|
6841
|
-
const prevLine = startLine >= 2 ? fileLines[startLine - 2] : void 0;
|
|
6842
|
-
return {
|
|
6843
|
-
kind: "replace",
|
|
6844
|
-
start: prevLine !== void 0 && prevLine.length === 0 ? lineStarts[startLine - 1] : Math.max(0, lineStarts[startLine - 1] - 1),
|
|
6845
|
-
end: content.length,
|
|
6846
|
-
replacement: ""
|
|
6847
|
-
};
|
|
6922
|
+
async function recordServed(path$1, rows, lineCount) {
|
|
6923
|
+
if (rows.length === 0) return;
|
|
6924
|
+
try {
|
|
6925
|
+
const store = await loadHashStore();
|
|
6926
|
+
withStore(() => {
|
|
6927
|
+
const current$1 = store.getServed(path$1);
|
|
6928
|
+
const updated = _mergeServedRows(current$1, rows, lineCount === void 0 ? void 0 : { truncateTo: lineCount });
|
|
6929
|
+
if (current$1.length === updated.length && current$1.every((v, i) => v === updated[i])) return;
|
|
6930
|
+
store.upsertServed(path$1, JSON.stringify(updated));
|
|
6931
|
+
});
|
|
6932
|
+
} catch (error) {
|
|
6933
|
+
console.error("Failed to record served rows:", error);
|
|
6934
|
+
}
|
|
6848
6935
|
}
|
|
6849
|
-
function
|
|
6850
|
-
|
|
6851
|
-
|
|
6936
|
+
async function recordServedTruncated(path$1, rows, lineCount, clearFrom = 0) {
|
|
6937
|
+
if (rows.length === 0) return;
|
|
6938
|
+
try {
|
|
6939
|
+
const store = await loadHashStore();
|
|
6940
|
+
withStore(() => {
|
|
6941
|
+
const current$1 = store.getServed(path$1);
|
|
6942
|
+
const updated = _mergeServedRows(current$1, rows, {
|
|
6943
|
+
truncateTo: lineCount,
|
|
6944
|
+
clearFrom
|
|
6945
|
+
});
|
|
6946
|
+
if (current$1.length === updated.length && current$1.every((v, i) => v === updated[i])) return;
|
|
6947
|
+
store.upsertServed(path$1, JSON.stringify(updated));
|
|
6948
|
+
});
|
|
6949
|
+
} catch (error) {
|
|
6950
|
+
console.error("Failed to record truncated served rows:", error);
|
|
6951
|
+
}
|
|
6852
6952
|
}
|
|
6853
|
-
function
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
const { resolved: initialResolved, mismatches, boundaryDups } = valEdit(prefixFixed, lineIndex.fileLines, fileHashes, warnings, signal);
|
|
6860
|
-
if (mismatches.length || !initialResolved) {
|
|
6861
|
-
const { message, servedRows } = fmtMismatchWithServes(mismatches, lineIndex.fileLines, fileHashes, filePath);
|
|
6862
|
-
throw new AnchorMismatchError(message, servedRows);
|
|
6953
|
+
async function driftReported(path$1) {
|
|
6954
|
+
try {
|
|
6955
|
+
return (await loadHashStore()).getServedReported(path$1);
|
|
6956
|
+
} catch (error) {
|
|
6957
|
+
console.error("Failed to load reported drift set:", error);
|
|
6958
|
+
return /* @__PURE__ */ new Set();
|
|
6863
6959
|
}
|
|
6864
|
-
warnUnicodeEsc(prefixFixed, warnings);
|
|
6865
|
-
let resolved = initialResolved;
|
|
6866
|
-
let autoFixes;
|
|
6867
|
-
if (boundaryDups.length > 0) {
|
|
6868
|
-
autoFixes = [];
|
|
6869
|
-
const correctedEdit = {
|
|
6870
|
-
...prefixFixed,
|
|
6871
|
-
content_lines: [...prefixFixed.content_lines]
|
|
6872
|
-
};
|
|
6873
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6874
|
-
const uniqueDups = [];
|
|
6875
|
-
for (const dup of boundaryDups) {
|
|
6876
|
-
if (seen.has(dup.replacementLineIndex)) continue;
|
|
6877
|
-
seen.add(dup.replacementLineIndex);
|
|
6878
|
-
uniqueDups.push(dup);
|
|
6879
|
-
}
|
|
6880
|
-
const dupsByIndex = uniqueDups.sort((a, b) => b.replacementLineIndex - a.replacementLineIndex);
|
|
6881
|
-
for (const dup of dupsByIndex) {
|
|
6882
|
-
const idx = dup.replacementLineIndex;
|
|
6883
|
-
if (idx < 0 || idx >= correctedEdit.content_lines.length) continue;
|
|
6884
|
-
const removed = correctedEdit.content_lines.splice(idx, 1)[0];
|
|
6885
|
-
autoFixes.push({
|
|
6886
|
-
kind: dup.kind,
|
|
6887
|
-
removedLine: removed,
|
|
6888
|
-
removedLineIndex: idx
|
|
6889
|
-
});
|
|
6890
|
-
}
|
|
6891
|
-
const correctedResult = valEdit(correctedEdit, lineIndex.fileLines, fileHashes, warnings, signal);
|
|
6892
|
-
if (correctedResult.mismatches.length || !correctedResult.resolved) {
|
|
6893
|
-
const { message, servedRows } = fmtMismatchWithServes(correctedResult.mismatches, lineIndex.fileLines, fileHashes, filePath);
|
|
6894
|
-
throw new AnchorMismatchError(message, servedRows);
|
|
6895
|
-
}
|
|
6896
|
-
resolved = correctedResult.resolved;
|
|
6897
|
-
}
|
|
6898
|
-
if (served) {
|
|
6899
|
-
const startAnchor = resolved.hash_bounds[0];
|
|
6900
|
-
const endAnchor = resolved.hash_bounds[1];
|
|
6901
|
-
verifyServedRange({
|
|
6902
|
-
served,
|
|
6903
|
-
startHash: startAnchor.hash,
|
|
6904
|
-
endHash: endAnchor.hash,
|
|
6905
|
-
startLine: startAnchor.line,
|
|
6906
|
-
endLine: endAnchor.line,
|
|
6907
|
-
fileHashes,
|
|
6908
|
-
fileLines: lineIndex.fileLines,
|
|
6909
|
-
filePath
|
|
6910
|
-
});
|
|
6911
|
-
}
|
|
6912
|
-
const spanResult = resToSpan(resolved, content, lineIndex);
|
|
6913
|
-
if (spanResult.kind === "noop") return {
|
|
6914
|
-
content,
|
|
6915
|
-
firstChangedLine: void 0,
|
|
6916
|
-
lastChangedLine: void 0,
|
|
6917
|
-
range: resolvedRange(resolved),
|
|
6918
|
-
...warnings.length ? { warnings } : {},
|
|
6919
|
-
noopEdit: {
|
|
6920
|
-
loc: spanResult.loc,
|
|
6921
|
-
currentContent: spanResult.currentContent
|
|
6922
|
-
}
|
|
6923
|
-
};
|
|
6924
|
-
const result = assemble(content, spanResult, signal);
|
|
6925
|
-
assertNotEmpty(content, result);
|
|
6926
|
-
const changed = changedRange(content, result);
|
|
6927
|
-
return {
|
|
6928
|
-
content: result,
|
|
6929
|
-
firstChangedLine: changed?.firstChangedLine,
|
|
6930
|
-
lastChangedLine: changed?.lastChangedLine,
|
|
6931
|
-
range: resolvedRange(resolved),
|
|
6932
|
-
...warnings.length ? { warnings } : {},
|
|
6933
|
-
...autoFixes ? { autoFixes } : {}
|
|
6934
|
-
};
|
|
6935
|
-
}
|
|
6936
|
-
function resolvedRange(resolved) {
|
|
6937
|
-
const [start, end] = resolved.hash_bounds;
|
|
6938
|
-
return {
|
|
6939
|
-
startLine: start.line,
|
|
6940
|
-
endLine: end.line,
|
|
6941
|
-
startHash: start.hash,
|
|
6942
|
-
endHash: end.hash,
|
|
6943
|
-
delta: resolved.content_lines.length - (Math.abs(end.line - start.line) + 1)
|
|
6944
|
-
};
|
|
6945
|
-
}
|
|
6946
|
-
function fmtRegion(hashes, lines) {
|
|
6947
|
-
if (hashes.length !== lines.length) throw new Error(`fmtRegion: hashes.length (${hashes.length}) must match lines.length (${lines.length}).`);
|
|
6948
|
-
return lines.map((line, index) => `${hashes[index]}${HASH_SEP}${line}`).join("\n");
|
|
6949
|
-
}
|
|
6950
|
-
function changedRange(original, result) {
|
|
6951
|
-
if (original === result) return null;
|
|
6952
|
-
if (original.length === 0) return {
|
|
6953
|
-
firstChangedLine: 1,
|
|
6954
|
-
lastChangedLine: splitLines(result).length
|
|
6955
|
-
};
|
|
6956
|
-
const originalLines = splitLines(original);
|
|
6957
|
-
const resultLines = splitLines(result);
|
|
6958
|
-
if (originalLines.length === resultLines.length && originalLines.every((line, index) => line === resultLines[index])) return null;
|
|
6959
|
-
const minLen = Math.min(originalLines.length, resultLines.length);
|
|
6960
|
-
let first = 0;
|
|
6961
|
-
while (first < minLen && originalLines[first] === resultLines[first]) first++;
|
|
6962
|
-
let lastOrig = originalLines.length - 1;
|
|
6963
|
-
let lastRes = resultLines.length - 1;
|
|
6964
|
-
while (lastOrig >= first && lastRes >= first && originalLines[lastOrig] === resultLines[lastRes]) {
|
|
6965
|
-
lastOrig--;
|
|
6966
|
-
lastRes--;
|
|
6967
|
-
}
|
|
6968
|
-
return {
|
|
6969
|
-
firstChangedLine: first + 1,
|
|
6970
|
-
lastChangedLine: Math.max(first, lastRes) + 1
|
|
6971
|
-
};
|
|
6972
|
-
}
|
|
6973
|
-
|
|
6974
|
-
//#endregion
|
|
6975
|
-
//#region src/url-schema/vendored/hashline/session-view.js
|
|
6976
|
-
const current = new AsyncLocalStorage();
|
|
6977
|
-
function withWorkspace(cwd, fn) {
|
|
6978
|
-
return current.run(cwd, fn);
|
|
6979
|
-
}
|
|
6980
|
-
function workspaceCwd() {
|
|
6981
|
-
return current.getStore();
|
|
6982
|
-
}
|
|
6983
|
-
let fallbackSessionKey;
|
|
6984
|
-
function sessionKeyFor(sessionId) {
|
|
6985
|
-
if (sessionId && sessionId.length > 0) return sessionId;
|
|
6986
|
-
return fallbackSessionKey ??= randomUUID();
|
|
6987
|
-
}
|
|
6988
|
-
function execCwd(exec) {
|
|
6989
|
-
return exec.agent?.session.header.cwd ?? process.cwd();
|
|
6990
|
-
}
|
|
6991
|
-
function execSessionKey(exec) {
|
|
6992
|
-
return sessionKeyFor(exec.agent?.session.id);
|
|
6993
|
-
}
|
|
6994
|
-
/**
|
|
6995
|
-
* Merge served rows into a copy of the stored array. This single helper owns
|
|
6996
|
-
* the served-merge invariant shared by recordServed and recordServedTruncated.
|
|
6997
|
-
* Eagerly heals orphaned serves: if the same hash is written at a new position
|
|
6998
|
-
* the old position is nulled (O(n) scan, no extra I/O). This prevents a
|
|
6999
|
-
* partial re-serve from leaving a stale duplicate behind (ADR-0008).
|
|
7000
|
-
*/
|
|
7001
|
-
function _mergeServedRows(current$1, rows, options) {
|
|
7002
|
-
const updated = current$1.slice();
|
|
7003
|
-
if (options?.truncateTo !== void 0 && updated.length > options.truncateTo) updated.length = options.truncateTo;
|
|
7004
|
-
if (options?.clearFrom !== void 0) for (let i = options.clearFrom; i < updated.length; i++) updated[i] = null;
|
|
7005
|
-
const index = /* @__PURE__ */ new Map();
|
|
7006
|
-
for (let i = 0; i < updated.length; i++) {
|
|
7007
|
-
const h = updated[i];
|
|
7008
|
-
if (h === null) continue;
|
|
7009
|
-
const prev = index.get(h);
|
|
7010
|
-
if (prev !== void 0) updated[prev] = null;
|
|
7011
|
-
index.set(h, i);
|
|
7012
|
-
}
|
|
7013
|
-
for (const entry of rows) {
|
|
7014
|
-
if (!Number.isInteger(entry.position) || entry.position < 0) throw new TypeError(`Invalid served position: ${entry.position}`);
|
|
7015
|
-
if (entry.hash !== null && (typeof entry.hash !== "string" || !HASH_RE.test(entry.hash))) throw new TypeError(`Invalid served hash: ${String(entry.hash)}`);
|
|
7016
|
-
while (updated.length <= entry.position) updated.push(null);
|
|
7017
|
-
if (entry.hash !== null) {
|
|
7018
|
-
const existing = index.get(entry.hash);
|
|
7019
|
-
if (existing !== void 0 && existing !== entry.position) {
|
|
7020
|
-
updated[existing] = null;
|
|
7021
|
-
index.delete(entry.hash);
|
|
7022
|
-
}
|
|
7023
|
-
const oldAtPos = updated[entry.position];
|
|
7024
|
-
if (oldAtPos !== null && oldAtPos !== entry.hash) index.delete(oldAtPos);
|
|
7025
|
-
index.set(entry.hash, entry.position);
|
|
7026
|
-
} else {
|
|
7027
|
-
const oldAtPos = updated[entry.position];
|
|
7028
|
-
if (oldAtPos !== null) index.delete(oldAtPos);
|
|
7029
|
-
}
|
|
7030
|
-
updated[entry.position] = entry.hash;
|
|
7031
|
-
}
|
|
7032
|
-
while (updated.length > 0 && updated[updated.length - 1] === null) updated.pop();
|
|
7033
|
-
return updated;
|
|
7034
|
-
}
|
|
7035
|
-
async function loadServed(sessionKey, path$1) {
|
|
7036
|
-
return (await loadHashStore()).getServed(sessionKey, path$1);
|
|
7037
6960
|
}
|
|
7038
|
-
async function
|
|
7039
|
-
if (rows.length === 0) return;
|
|
7040
|
-
try {
|
|
7041
|
-
const store = await loadHashStore();
|
|
7042
|
-
withStore(() => {
|
|
7043
|
-
const current$1 = store.getServed(sessionKey, path$1);
|
|
7044
|
-
const updated = _mergeServedRows(current$1, rows, lineCount === void 0 ? void 0 : { truncateTo: lineCount });
|
|
7045
|
-
if (current$1.length === updated.length && current$1.every((v, i) => v === updated[i])) return;
|
|
7046
|
-
store.upsertServed(sessionKey, path$1, JSON.stringify(updated));
|
|
7047
|
-
});
|
|
7048
|
-
} catch (error) {
|
|
7049
|
-
console.error("Failed to record served rows:", error);
|
|
7050
|
-
}
|
|
7051
|
-
}
|
|
7052
|
-
async function recordServedTruncated(sessionKey, path$1, rows, lineCount, clearFrom = 0) {
|
|
7053
|
-
if (rows.length === 0) return;
|
|
7054
|
-
try {
|
|
7055
|
-
const store = await loadHashStore();
|
|
7056
|
-
withStore(() => {
|
|
7057
|
-
const current$1 = store.getServed(sessionKey, path$1);
|
|
7058
|
-
const updated = _mergeServedRows(current$1, rows, {
|
|
7059
|
-
truncateTo: lineCount,
|
|
7060
|
-
clearFrom
|
|
7061
|
-
});
|
|
7062
|
-
if (current$1.length === updated.length && current$1.every((v, i) => v === updated[i])) return;
|
|
7063
|
-
store.upsertServed(sessionKey, path$1, JSON.stringify(updated));
|
|
7064
|
-
});
|
|
7065
|
-
} catch (error) {
|
|
7066
|
-
console.error("Failed to record truncated served rows:", error);
|
|
7067
|
-
}
|
|
7068
|
-
}
|
|
7069
|
-
async function driftReported(sessionKey, path$1) {
|
|
7070
|
-
try {
|
|
7071
|
-
return (await loadHashStore()).getServedReported(sessionKey, path$1);
|
|
7072
|
-
} catch (error) {
|
|
7073
|
-
console.error("Failed to load reported drift set:", error);
|
|
7074
|
-
return /* @__PURE__ */ new Set();
|
|
7075
|
-
}
|
|
7076
|
-
}
|
|
7077
|
-
async function markDriftReported(sessionKey, path$1, hashes) {
|
|
6961
|
+
async function markDriftReported(path$1, hashes) {
|
|
7078
6962
|
try {
|
|
7079
6963
|
const valid = hashes.filter((hash) => HASH_RE.test(hash));
|
|
7080
6964
|
if (valid.length === 0) return;
|
|
7081
6965
|
const store = await loadHashStore();
|
|
7082
6966
|
withStore(() => {
|
|
7083
|
-
const current$1 = store.getServedReported(
|
|
6967
|
+
const current$1 = store.getServedReported(path$1);
|
|
7084
6968
|
for (const hash of valid) current$1.add(hash);
|
|
7085
|
-
store.upsertServedReported(
|
|
6969
|
+
store.upsertServedReported(path$1, JSON.stringify([...current$1]));
|
|
7086
6970
|
});
|
|
7087
6971
|
} catch (error) {
|
|
7088
6972
|
console.error("Failed to record reported drift set:", error);
|
|
7089
6973
|
}
|
|
7090
6974
|
}
|
|
7091
|
-
async function clearDriftReported(
|
|
6975
|
+
async function clearDriftReported(path$1) {
|
|
7092
6976
|
try {
|
|
7093
6977
|
const store = await loadHashStore();
|
|
7094
6978
|
withStore(() => {
|
|
7095
|
-
store.clearServedReported(
|
|
6979
|
+
store.clearServedReported(path$1);
|
|
7096
6980
|
});
|
|
7097
6981
|
} catch (error) {
|
|
7098
6982
|
console.error("Failed to clear reported drift set:", error);
|
|
@@ -7189,568 +7073,661 @@ function computeDrift(input) {
|
|
|
7189
7073
|
};
|
|
7190
7074
|
}
|
|
7191
7075
|
async function scanDrift(input) {
|
|
7192
|
-
const reported = await driftReported(input.
|
|
7076
|
+
const reported = await driftReported(input.path);
|
|
7193
7077
|
const result = computeDrift({
|
|
7194
7078
|
...input,
|
|
7195
7079
|
reported
|
|
7196
7080
|
});
|
|
7197
7081
|
if (!result || result.allAlreadyReported) return result?.text;
|
|
7198
|
-
await recordServed(input.
|
|
7082
|
+
await recordServed(input.path, result.rows.map((row) => ({
|
|
7199
7083
|
position: row.position,
|
|
7200
7084
|
hash: row.hash
|
|
7201
7085
|
})), input.resultLines.length);
|
|
7202
|
-
await markDriftReported(input.
|
|
7086
|
+
await markDriftReported(input.path, result.rows.filter((row) => row.drifted).map((row) => row.hash));
|
|
7203
7087
|
return result.text;
|
|
7204
7088
|
}
|
|
7205
7089
|
|
|
7206
7090
|
//#endregion
|
|
7207
|
-
//#region src/url-schema/vendored/hashline/
|
|
7208
|
-
function
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
return
|
|
7091
|
+
//#region src/url-schema/vendored/hashline/hashline/anchor-pipeline.js
|
|
7092
|
+
function diagRef(ref) {
|
|
7093
|
+
const trimmed = ref.trim();
|
|
7094
|
+
if (!trimmed.length) return `[E_BAD_REF] Invalid anchor. Expected a 3-char alphanumeric anchor (e.g. "aB3").`;
|
|
7095
|
+
if (/^\d+/.test(trimmed)) return `[E_BAD_REF] Invalid anchor. Use the hash alone (e.g. "aB3") — no line numbers or trailing content.`;
|
|
7096
|
+
if (trimmed.includes("│")) return `[E_BAD_REF] Invalid anchor "${trimmed}". remove_from and remove_to must contain the 3-char hash only — remove everything from "│" onward.`;
|
|
7097
|
+
return `[E_BAD_REF] Invalid anchor "${trimmed}". Expected a 3-char alphanumeric anchor (e.g. "aB3").`;
|
|
7212
7098
|
}
|
|
7213
|
-
function
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
return isValidHashList(v.hashes);
|
|
7099
|
+
function parseRef(ref) {
|
|
7100
|
+
const trimmed = ref.trim();
|
|
7101
|
+
if (trimmed.length === ANCHOR_LEN && ALPH_RE.test(trimmed)) return { hash: trimmed };
|
|
7102
|
+
throw new Error(diagRef(ref));
|
|
7218
7103
|
}
|
|
7219
|
-
|
|
7220
|
-
function
|
|
7221
|
-
if (
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
return true;
|
|
7104
|
+
const parseHashRef = parseRef;
|
|
7105
|
+
function parseText(edit) {
|
|
7106
|
+
if (typeof edit !== "string") throw new Error(NEW_CONTENT_NOT_STRING_MSG);
|
|
7107
|
+
const normalized = edit.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
7108
|
+
if (normalized === "") return [];
|
|
7109
|
+
if (/^\n+$/.test(normalized)) return new Array(normalized.length).fill("");
|
|
7110
|
+
return normalized.split("\n");
|
|
7227
7111
|
}
|
|
7228
|
-
function
|
|
7229
|
-
|
|
7112
|
+
function resAnchorFromMap(ref, hashIndex) {
|
|
7113
|
+
const hashMatches = hashIndex.get(ref.hash);
|
|
7114
|
+
if (!hashMatches || hashMatches.length === 0) return {
|
|
7115
|
+
ref,
|
|
7116
|
+
kind: "not_found"
|
|
7117
|
+
};
|
|
7118
|
+
if (hashMatches.length === 1) return {
|
|
7119
|
+
line: hashMatches[0],
|
|
7120
|
+
hash: ref.hash,
|
|
7121
|
+
hashMatched: true
|
|
7122
|
+
};
|
|
7123
|
+
return {
|
|
7124
|
+
ref,
|
|
7125
|
+
kind: "ambiguous",
|
|
7126
|
+
candidates: hashMatches
|
|
7127
|
+
};
|
|
7230
7128
|
}
|
|
7231
|
-
function
|
|
7232
|
-
if (
|
|
7233
|
-
const errcode = error.errcode;
|
|
7234
|
-
if (typeof errcode === "number") return errcode === 11 || errcode === 24 || errcode === 26;
|
|
7235
|
-
const code = error.code;
|
|
7236
|
-
if (typeof code === "string" && /NOTADB|CORRUPT/.test(code)) return true;
|
|
7237
|
-
}
|
|
7238
|
-
return error instanceof Error && /corrupt|not a database|malformed|database disk image/i.test(error.message);
|
|
7129
|
+
function assertAligned(fileLines, fileHashes, ctx) {
|
|
7130
|
+
if (fileHashes.length !== fileLines.length) throw new Error(`${ctx}: fileHashes.length (${fileHashes.length}) must match fileLines.length (${fileLines.length}).`);
|
|
7239
7131
|
}
|
|
7240
|
-
function
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7132
|
+
function fmtMismatchWithServes(mismatches, fileLines, fileHashes, filePath) {
|
|
7133
|
+
assertAligned(fileLines, fileHashes, "fmtMismatch");
|
|
7134
|
+
const out = [];
|
|
7135
|
+
const servedRows = [];
|
|
7136
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7137
|
+
const pushRow = (ln) => {
|
|
7138
|
+
if (ln < 1 || ln > fileLines.length) return;
|
|
7139
|
+
const position = ln - 1;
|
|
7140
|
+
if (seen.has(position)) return;
|
|
7141
|
+
seen.add(position);
|
|
7142
|
+
servedRows.push({
|
|
7143
|
+
position,
|
|
7144
|
+
hash: fileHashes[ln - 1]
|
|
7145
|
+
});
|
|
7146
|
+
};
|
|
7147
|
+
const notFound = mismatches.filter((m) => m.kind === "not_found");
|
|
7148
|
+
const ambiguous = mismatches.filter((m) => m.kind === "ambiguous");
|
|
7149
|
+
const refList = notFound.map((m) => `"${m.ref.hash}"`).join(", ");
|
|
7150
|
+
if (notFound.length > 0) {
|
|
7151
|
+
out.push(`[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}: ${refList}. Re-read for fresh anchors.`);
|
|
7152
|
+
for (const m of notFound) {
|
|
7153
|
+
const ctx = m.context;
|
|
7154
|
+
if (!ctx) continue;
|
|
7155
|
+
const from = Math.max(1, ctx.line - 1);
|
|
7156
|
+
const to = Math.min(fileLines.length, ctx.line + 1);
|
|
7157
|
+
const rows = [];
|
|
7158
|
+
for (let ln = from; ln <= to; ln++) {
|
|
7159
|
+
rows.push(` ${ln}: ${fileHashes[ln - 1]}│${clipLine(fileLines[ln - 1] ?? "")}`);
|
|
7160
|
+
pushRow(ln);
|
|
7161
|
+
}
|
|
7162
|
+
out.push("");
|
|
7163
|
+
out.push(` Current context around resolved anchor "${ctx.hash}" (line ${ctx.line}):\n${rows.join("\n")}`);
|
|
7164
|
+
}
|
|
7244
7165
|
}
|
|
7245
|
-
|
|
7166
|
+
if (ambiguous.length > 0) {
|
|
7167
|
+
if (out.length > 0) out.push("");
|
|
7168
|
+
out.push(`[E_AMBIGUOUS_ANCHOR] ${ambiguous.length} ambiguous anchor${ambiguous.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}. Re-read for fresh anchors.`);
|
|
7169
|
+
for (const m of ambiguous) {
|
|
7170
|
+
const sample = (m.candidates ?? []).slice(0, 5);
|
|
7171
|
+
const more = (m.candidates?.length ?? 0) > sample.length ? `, ... (+${(m.candidates?.length ?? 0) - sample.length} more)` : "";
|
|
7172
|
+
const lines = sample.map((line) => {
|
|
7173
|
+
const content = clipLine(fileLines[line - 1] ?? "");
|
|
7174
|
+
pushRow(line);
|
|
7175
|
+
return ` ${line}: ${fileHashes[line - 1]}│${content}`;
|
|
7176
|
+
}).join("\n");
|
|
7177
|
+
out.push(` Hash "${m.ref.hash}" matches lines ${sample.join(", ")}${more}.\n${lines}`);
|
|
7178
|
+
}
|
|
7179
|
+
}
|
|
7180
|
+
return {
|
|
7181
|
+
message: out.join("\n"),
|
|
7182
|
+
servedRows
|
|
7183
|
+
};
|
|
7246
7184
|
}
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7185
|
+
const ITEM_KS = new Set([
|
|
7186
|
+
"replacement_text",
|
|
7187
|
+
"remove_from",
|
|
7188
|
+
"remove_to"
|
|
7189
|
+
]);
|
|
7190
|
+
function assertItem(edit) {
|
|
7191
|
+
rejectUnknownFields(edit, ITEM_KS, "Edit", "The edit takes only { replacement_text, remove_from, remove_to }.");
|
|
7192
|
+
if ("remove_from" in edit && typeof edit.remove_from !== "string") throw new Error(`[E_BAD_SHAPE] Field "remove_from" must be an anchor string (3-char hash).`);
|
|
7193
|
+
if ("remove_to" in edit && typeof edit.remove_to !== "string") throw new Error(`[E_BAD_SHAPE] Field "remove_to" must be an anchor string (3-char hash).`);
|
|
7194
|
+
if (!("replacement_text" in edit)) throw new Error(`[E_BAD_SHAPE] The edit requires a "replacement_text" field. Provide the replacement text (use "" to delete).`);
|
|
7195
|
+
if (typeof edit.replacement_text !== "string") throw new Error(NEW_CONTENT_NOT_STRING_MSG);
|
|
7196
|
+
if (typeof edit.remove_from !== "string" || typeof edit.remove_to !== "string") throw new Error(`[E_BAD_SHAPE] The edit requires "remove_from" and "remove_to" anchor strings (3-char hashes from read output).`);
|
|
7250
7197
|
}
|
|
7251
|
-
const
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7198
|
+
const ANCHOR_ROW_RE = /* @__PURE__ */ new RegExp(`^([+-]?)(${HASH_CLASS})│`);
|
|
7199
|
+
function resEdit(edit, warnings) {
|
|
7200
|
+
assertItem(edit);
|
|
7201
|
+
const editLines = parseText(edit.replacement_text);
|
|
7202
|
+
const bounds = [edit.remove_from, edit.remove_to].map((ref) => {
|
|
7203
|
+
const trimmed = ref.trim();
|
|
7204
|
+
const match = trimmed.match(ANCHOR_ROW_RE);
|
|
7205
|
+
if (match) {
|
|
7206
|
+
let message;
|
|
7207
|
+
if (match[1] === "+") message = `[E_BAD_REF] stripped diff-preview marker from remove_from/remove_to "${trimmed}".`;
|
|
7208
|
+
else if (match[1] === "-") message = `[E_BAD_REF] stripped leading "-" marker from remove_from/remove_to "${trimmed}".`;
|
|
7209
|
+
else message = `[E_BAD_REF] stripped "HASH│" prefix from remove_from/remove_to "${trimmed}".`;
|
|
7210
|
+
warnings?.push(message);
|
|
7211
|
+
return match[2];
|
|
7212
|
+
}
|
|
7213
|
+
return ref;
|
|
7214
|
+
});
|
|
7215
|
+
return {
|
|
7216
|
+
content_lines: editLines,
|
|
7217
|
+
hash_bounds: [parseHashRef(bounds[0]), parseHashRef(bounds[1])]
|
|
7218
|
+
};
|
|
7219
|
+
}
|
|
7220
|
+
function warnUnicodeEsc(edit, warnings) {
|
|
7221
|
+
if (edit.content_lines.some((line) => /\\uDDDD/i.test(line))) warnings.push("Detected literal \\uDDDD in edit content; no autocorrection applied. Verify whether this should be a real Unicode escape or plain text.");
|
|
7222
|
+
}
|
|
7223
|
+
/** @internal — private to anchor-pipeline seam; do not import directly, use anchor-pipeline.ts */
|
|
7224
|
+
function stripBarePrefixes(edit, fileHashes, warnings) {
|
|
7225
|
+
const fileHashSet = new Set(fileHashes);
|
|
7226
|
+
const stripped = [];
|
|
7227
|
+
const contentLines = edit.content_lines.map((line, lineIndex) => {
|
|
7228
|
+
const match = line.match(HL_BARE_PREFIX_RE);
|
|
7229
|
+
if (!match) return line;
|
|
7230
|
+
stripped.push({
|
|
7231
|
+
lineIndex,
|
|
7232
|
+
matched: fileHashSet.has(match[1])
|
|
7233
|
+
});
|
|
7234
|
+
return line.slice(match[0].length);
|
|
7235
|
+
});
|
|
7236
|
+
if (stripped.length === 0) return edit;
|
|
7237
|
+
const locations = stripped.map((s) => `replacement_text line ${s.lineIndex + 1}`).join(", ");
|
|
7238
|
+
const matchedCount = stripped.filter((s) => s.matched).length;
|
|
7239
|
+
const evidence = matchedCount === 0 ? "0 matched — verify literal 'HASH│' content" : `${matchedCount}/${stripped.length} matched`;
|
|
7240
|
+
warnings.push(`[E_BARE_HASH_PREFIX] stripped "HASH│" prefix from ${locations} (${evidence}).`);
|
|
7241
|
+
return {
|
|
7242
|
+
...edit,
|
|
7243
|
+
content_lines: contentLines
|
|
7244
|
+
};
|
|
7245
|
+
}
|
|
7246
|
+
/** @internal — private to anchor-pipeline seam */
|
|
7247
|
+
function stripDiffPrefixes(edit, warnings) {
|
|
7248
|
+
const stripped = [];
|
|
7249
|
+
const contentLines = edit.content_lines.map((line, lineIndex) => {
|
|
7250
|
+
const plus = line.match(HL_PREFIX_PLUS_RE);
|
|
7251
|
+
if (plus) {
|
|
7252
|
+
stripped.push(lineIndex);
|
|
7253
|
+
return line.slice(plus[0].length);
|
|
7254
|
+
}
|
|
7255
|
+
const minus = line.match(HL_PREFIX_MINUS_RE);
|
|
7256
|
+
if (minus) {
|
|
7257
|
+
stripped.push(lineIndex);
|
|
7258
|
+
return line.slice(minus[0].length);
|
|
7259
|
+
}
|
|
7260
|
+
return line;
|
|
7261
|
+
});
|
|
7262
|
+
if (stripped.length === 0) return edit;
|
|
7263
|
+
const locations = stripped.map((i) => `replacement_text line ${i + 1}`).join(", ");
|
|
7264
|
+
warnings.push(`[E_INVALID_PATCH] stripped diff-preview marker from ${locations}.`);
|
|
7265
|
+
return {
|
|
7266
|
+
...edit,
|
|
7267
|
+
content_lines: contentLines
|
|
7268
|
+
};
|
|
7269
|
+
}
|
|
7270
|
+
/** @internal — private to anchor-pipeline seam */
|
|
7271
|
+
function swapReversedRanges(edit, fileHashes, warnings) {
|
|
7272
|
+
const lineByHash = /* @__PURE__ */ new Map();
|
|
7273
|
+
for (let i = 0; i < fileHashes.length; i++) lineByHash.set(fileHashes[i], i + 1);
|
|
7274
|
+
const [startRef, endRef] = edit.hash_bounds;
|
|
7275
|
+
const startLine = lineByHash.get(startRef.hash);
|
|
7276
|
+
const endLine = lineByHash.get(endRef.hash);
|
|
7277
|
+
if (startLine === void 0 || endLine === void 0 || startLine <= endLine) return edit;
|
|
7278
|
+
warnings.push(`[E_BAD_OP] reversed remove_from/remove_to (${startRef.hash} after ${endRef.hash}); swapped.`);
|
|
7279
|
+
return {
|
|
7280
|
+
...edit,
|
|
7281
|
+
hash_bounds: [endRef, startRef]
|
|
7282
|
+
};
|
|
7283
|
+
}
|
|
7284
|
+
function trailingDups(contentLines, fileLines, endLine) {
|
|
7285
|
+
const start = lastNonEmptyIndex(contentLines);
|
|
7286
|
+
if (start < 0) return [];
|
|
7287
|
+
const dups = [];
|
|
7288
|
+
const maxK = Math.min(start + 1, fileLines.length - endLine);
|
|
7289
|
+
for (let k = 0; k < maxK; k++) {
|
|
7290
|
+
if (contentLines[start - k] !== fileLines[endLine + k]) break;
|
|
7291
|
+
dups.push({
|
|
7292
|
+
kind: "trailing",
|
|
7293
|
+
replacementLineIndex: start - k
|
|
7294
|
+
});
|
|
7261
7295
|
}
|
|
7262
|
-
|
|
7296
|
+
return dups;
|
|
7263
7297
|
}
|
|
7264
|
-
function
|
|
7265
|
-
|
|
7298
|
+
function leadingDups(contentLines, fileLines, startLine) {
|
|
7299
|
+
const start = firstNonEmptyIndex(contentLines);
|
|
7300
|
+
if (start < 0) return [];
|
|
7301
|
+
const dups = [];
|
|
7302
|
+
const maxK = Math.min(contentLines.length - start, startLine - 1);
|
|
7303
|
+
for (let k = 0; k < maxK; k++) {
|
|
7304
|
+
if (contentLines[start + k] !== fileLines[startLine - 2 - k]) break;
|
|
7305
|
+
dups.push({
|
|
7306
|
+
kind: "leading",
|
|
7307
|
+
replacementLineIndex: start + k
|
|
7308
|
+
});
|
|
7309
|
+
}
|
|
7310
|
+
return dups;
|
|
7266
7311
|
}
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
let
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
} catch (error) {
|
|
7276
|
-
try {
|
|
7277
|
-
db.close();
|
|
7278
|
-
} catch {}
|
|
7279
|
-
throw error;
|
|
7312
|
+
function sectionIsUnique(canonLines, start, length) {
|
|
7313
|
+
let count = 0;
|
|
7314
|
+
for (let i = 0; i + length <= canonLines.length; i++) {
|
|
7315
|
+
let k = 0;
|
|
7316
|
+
while (k < length && canonLines[i + k] === canonLines[start + k]) k++;
|
|
7317
|
+
if (k < length) continue;
|
|
7318
|
+
count++;
|
|
7319
|
+
if (count > 1) return false;
|
|
7280
7320
|
}
|
|
7321
|
+
return true;
|
|
7281
7322
|
}
|
|
7282
|
-
function
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
const
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7323
|
+
function firstNewAfterDups(contentLines, rangeLines, canonLines, endLine) {
|
|
7324
|
+
const firstNew = findNewEdge(contentLines, rangeLines, false);
|
|
7325
|
+
if (!firstNew) return [];
|
|
7326
|
+
const maxK = Math.min(contentLines.length - firstNew.index, canonLines.length - endLine);
|
|
7327
|
+
let runLen = 0;
|
|
7328
|
+
while (runLen < maxK && canon(contentLines[firstNew.index + runLen]) === canonLines[endLine + runLen]) runLen++;
|
|
7329
|
+
if (runLen === 0 || !sectionIsUnique(canonLines, endLine, runLen)) return [];
|
|
7330
|
+
const dups = [];
|
|
7331
|
+
for (let k = 0; k < runLen; k++) dups.push({
|
|
7332
|
+
kind: "first-new-after",
|
|
7333
|
+
replacementLineIndex: firstNew.index + k
|
|
7334
|
+
});
|
|
7335
|
+
return dups;
|
|
7336
|
+
}
|
|
7337
|
+
function lastNewBeforeDups(contentLines, rangeLines, canonLines, startLine) {
|
|
7338
|
+
const lastNew = findNewEdge(contentLines, rangeLines, true);
|
|
7339
|
+
if (!lastNew) return [];
|
|
7340
|
+
const maxK = Math.min(lastNew.index + 1, startLine - 1);
|
|
7341
|
+
let runLen = 0;
|
|
7342
|
+
while (runLen < maxK && canon(contentLines[lastNew.index - runLen]) === canonLines[startLine - 2 - runLen]) runLen++;
|
|
7343
|
+
if (runLen === 0) return [];
|
|
7344
|
+
if (!sectionIsUnique(canonLines, startLine - 1 - runLen, runLen)) return [];
|
|
7345
|
+
const dups = [];
|
|
7346
|
+
for (let k = 0; k < runLen; k++) dups.push({
|
|
7347
|
+
kind: "last-new-before",
|
|
7348
|
+
replacementLineIndex: lastNew.index - k
|
|
7349
|
+
});
|
|
7350
|
+
return dups;
|
|
7351
|
+
}
|
|
7352
|
+
function canonCounts(lines) {
|
|
7353
|
+
const counts = /* @__PURE__ */ new Map();
|
|
7354
|
+
for (const line of lines) {
|
|
7355
|
+
const key = canon(line);
|
|
7356
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
7293
7357
|
}
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
const
|
|
7299
|
-
const
|
|
7300
|
-
const
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
undoUpsert: (...params) => {
|
|
7331
|
-
withBusyRetry(() => {
|
|
7332
|
-
undoUpsertStmt.run(...params);
|
|
7333
|
-
});
|
|
7334
|
-
},
|
|
7335
|
-
undoGet: (...params) => undoGetStmt.get(...params),
|
|
7336
|
-
undoDelete: (...params) => {
|
|
7337
|
-
withBusyRetry(() => {
|
|
7338
|
-
undoDelStmt.run(...params);
|
|
7339
|
-
});
|
|
7340
|
-
},
|
|
7341
|
-
servedGet: (...params) => servedGetStmt.get(...params),
|
|
7342
|
-
servedUpsert: (...params) => {
|
|
7343
|
-
withBusyRetry(() => {
|
|
7344
|
-
servedUpsertStmt.run(...params);
|
|
7345
|
-
});
|
|
7346
|
-
},
|
|
7347
|
-
servedReportedUpsert: (...params) => {
|
|
7348
|
-
withBusyRetry(() => {
|
|
7349
|
-
servedReportedUpsertStmt.run(...params);
|
|
7350
|
-
});
|
|
7351
|
-
},
|
|
7352
|
-
servedReportedClear: (...params) => {
|
|
7353
|
-
withBusyRetry(() => {
|
|
7354
|
-
servedReportedClearStmt.run(params[1], params[0], params[2]);
|
|
7355
|
-
});
|
|
7356
|
-
},
|
|
7357
|
-
servedDelete: (...params) => {
|
|
7358
|
-
withBusyRetry(() => {
|
|
7359
|
-
servedDeleteStmt.run(...params);
|
|
7360
|
-
});
|
|
7361
|
-
},
|
|
7362
|
-
servedDeletePath: (...params) => {
|
|
7363
|
-
withBusyRetry(() => {
|
|
7364
|
-
servedDeletePathStmt.run(...params);
|
|
7365
|
-
});
|
|
7366
|
-
},
|
|
7367
|
-
servedWipe: (...params) => {
|
|
7368
|
-
withBusyRetry(() => {
|
|
7369
|
-
servedWipeStmt.run(...params);
|
|
7370
|
-
});
|
|
7371
|
-
},
|
|
7372
|
-
servedPruneOlderThan: (...params) => {
|
|
7373
|
-
withBusyRetry(() => {
|
|
7374
|
-
servedPruneOlderThanStmt.run(...params);
|
|
7375
|
-
});
|
|
7376
|
-
}
|
|
7358
|
+
return counts;
|
|
7359
|
+
}
|
|
7360
|
+
/** @internal — private to anchor-pipeline seam */
|
|
7361
|
+
function findNewEdge(contentLines, rangeLines, fromEnd) {
|
|
7362
|
+
const multiset = canonCounts(rangeLines);
|
|
7363
|
+
const step = fromEnd ? -1 : 1;
|
|
7364
|
+
const start = fromEnd ? contentLines.length - 1 : 0;
|
|
7365
|
+
for (let i = start; i >= 0 && i < contentLines.length; i += step) {
|
|
7366
|
+
const line = contentLines[i];
|
|
7367
|
+
if (line.length === 0) continue;
|
|
7368
|
+
const key = canon(line);
|
|
7369
|
+
const count = multiset.get(key) ?? 0;
|
|
7370
|
+
if (count > 0) multiset.set(key, count - 1);
|
|
7371
|
+
else return {
|
|
7372
|
+
index: i,
|
|
7373
|
+
line
|
|
7374
|
+
};
|
|
7375
|
+
}
|
|
7376
|
+
}
|
|
7377
|
+
/** @internal — private to anchor-pipeline seam: detection + boundaryDups belongs to AnchorPipeline ordering */
|
|
7378
|
+
function valEdit(edit, fileLines, fileHashes, warnings, signal) {
|
|
7379
|
+
assertAligned(fileLines, fileHashes, "valEdit");
|
|
7380
|
+
const mismatches = [];
|
|
7381
|
+
const boundaryDups = [];
|
|
7382
|
+
const hashIndex = /* @__PURE__ */ new Map();
|
|
7383
|
+
for (let i = 0; i < fileHashes.length; i++) {
|
|
7384
|
+
const h = fileHashes[i];
|
|
7385
|
+
const list = hashIndex.get(h) ?? [];
|
|
7386
|
+
list.push(i + 1);
|
|
7387
|
+
hashIndex.set(h, list);
|
|
7388
|
+
}
|
|
7389
|
+
const tryResolve = (ref) => {
|
|
7390
|
+
const result = resAnchorFromMap(ref, hashIndex);
|
|
7391
|
+
if ("kind" in result) {
|
|
7392
|
+
mismatches.push(result);
|
|
7393
|
+
return;
|
|
7377
7394
|
}
|
|
7395
|
+
return result;
|
|
7378
7396
|
};
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
const
|
|
7388
|
-
if (
|
|
7389
|
-
try {
|
|
7390
|
-
const parsed = JSON.parse(row.hashes);
|
|
7391
|
-
if (isValidHashList(parsed)) return parsed;
|
|
7392
|
-
if (deleteCorrupt) stmts.deleteOne(path$1);
|
|
7393
|
-
return;
|
|
7394
|
-
} catch {
|
|
7395
|
-
if (deleteCorrupt) stmts.deleteOne(path$1);
|
|
7396
|
-
return;
|
|
7397
|
-
}
|
|
7398
|
-
},
|
|
7399
|
-
upsertSnapshot(path$1, checksum, lineCount, hashes) {
|
|
7400
|
-
stmts.upsert(path$1, cacheKey(checksum), lineCount, JSON.stringify(hashes), Date.now());
|
|
7401
|
-
},
|
|
7402
|
-
allKnownPaths() {
|
|
7403
|
-
return stmts.allPaths();
|
|
7404
|
-
},
|
|
7405
|
-
allSnapshotHashes() {
|
|
7406
|
-
return stmts.allHashes();
|
|
7407
|
-
},
|
|
7408
|
-
deleteSnapshot(path$1) {
|
|
7409
|
-
stmts.deleteOne(path$1);
|
|
7410
|
-
},
|
|
7411
|
-
findSnapshotPaths(hashes) {
|
|
7412
|
-
const rows = stmts.allHashes();
|
|
7413
|
-
const matches = [];
|
|
7414
|
-
for (const row of rows) try {
|
|
7415
|
-
const parsed = JSON.parse(row.hashes);
|
|
7416
|
-
if (!isValidHashList(parsed)) continue;
|
|
7417
|
-
if (hashes.every((h) => parsed.includes(h))) matches.push(row.path);
|
|
7418
|
-
} catch {}
|
|
7419
|
-
return matches;
|
|
7420
|
-
},
|
|
7421
|
-
getUndo(path$1) {
|
|
7422
|
-
const row = stmts.undoGet(path$1);
|
|
7423
|
-
if (!row) return void 0;
|
|
7424
|
-
try {
|
|
7425
|
-
const parsed = JSON.parse(row.hashes);
|
|
7426
|
-
if (!isValidHashList(parsed)) {
|
|
7427
|
-
stmts.undoDelete(path$1);
|
|
7428
|
-
return;
|
|
7429
|
-
}
|
|
7430
|
-
return {
|
|
7431
|
-
content: row.content,
|
|
7432
|
-
bom: row.bom,
|
|
7433
|
-
ending: row.ending,
|
|
7434
|
-
hashes: parsed,
|
|
7435
|
-
resultContent: row.result_content
|
|
7436
|
-
};
|
|
7437
|
-
} catch {
|
|
7438
|
-
stmts.undoDelete(path$1);
|
|
7439
|
-
return;
|
|
7440
|
-
}
|
|
7441
|
-
},
|
|
7442
|
-
upsertUndo(path$1, entry) {
|
|
7443
|
-
stmts.undoUpsert(path$1, entry.content, entry.bom, entry.ending, JSON.stringify(entry.hashes), entry.resultContent, Date.now());
|
|
7444
|
-
},
|
|
7445
|
-
deleteUndo(path$1) {
|
|
7446
|
-
stmts.undoDelete(path$1);
|
|
7447
|
-
},
|
|
7448
|
-
getServed(sessionKey, path$1) {
|
|
7449
|
-
const row = stmts.servedGet(sessionKey, path$1);
|
|
7450
|
-
if (!row) return [];
|
|
7451
|
-
try {
|
|
7452
|
-
const parsed = JSON.parse(row.hashes);
|
|
7453
|
-
if (isValidServedList(parsed)) return parsed;
|
|
7454
|
-
stmts.servedDelete(sessionKey, path$1);
|
|
7455
|
-
return [];
|
|
7456
|
-
} catch {
|
|
7457
|
-
stmts.servedDelete(sessionKey, path$1);
|
|
7458
|
-
return [];
|
|
7459
|
-
}
|
|
7460
|
-
},
|
|
7461
|
-
getServedReported(sessionKey, path$1) {
|
|
7462
|
-
const row = stmts.servedGet(sessionKey, path$1);
|
|
7463
|
-
if (!row) return /* @__PURE__ */ new Set();
|
|
7464
|
-
const raw = row.reported;
|
|
7465
|
-
if (typeof raw !== "string" || raw.length === 0) return /* @__PURE__ */ new Set();
|
|
7466
|
-
try {
|
|
7467
|
-
const parsed = JSON.parse(raw);
|
|
7468
|
-
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
7469
|
-
return new Set(parsed.filter((h) => typeof h === "string" && HASH_RE.test(h)));
|
|
7470
|
-
} catch {
|
|
7471
|
-
return /* @__PURE__ */ new Set();
|
|
7472
|
-
}
|
|
7473
|
-
},
|
|
7474
|
-
upsertServed(sessionKey, path$1, hashesJson) {
|
|
7475
|
-
stmts.servedUpsert(sessionKey, path$1, hashesJson, Date.now());
|
|
7476
|
-
},
|
|
7477
|
-
upsertServedReported(sessionKey, path$1, reportedJson) {
|
|
7478
|
-
stmts.servedReportedUpsert(sessionKey, path$1, reportedJson, Date.now());
|
|
7479
|
-
},
|
|
7480
|
-
clearServedReported(sessionKey, path$1) {
|
|
7481
|
-
stmts.servedReportedClear(sessionKey, Date.now(), path$1);
|
|
7482
|
-
},
|
|
7483
|
-
deleteServed(sessionKey, path$1) {
|
|
7484
|
-
stmts.servedDelete(sessionKey, path$1);
|
|
7485
|
-
},
|
|
7486
|
-
deleteServedByPath(path$1) {
|
|
7487
|
-
stmts.servedDeletePath(path$1);
|
|
7488
|
-
},
|
|
7489
|
-
wipeServed(sessionKey) {
|
|
7490
|
-
stmts.servedWipe(sessionKey);
|
|
7491
|
-
},
|
|
7492
|
-
pruneServedOlderThan(ts) {
|
|
7493
|
-
stmts.servedPruneOlderThan(ts);
|
|
7494
|
-
},
|
|
7495
|
-
async pruneMissing() {
|
|
7496
|
-
const missing = await statMissing(stmts.allPaths());
|
|
7497
|
-
if (missing.length === 0) return;
|
|
7498
|
-
withStore(() => {
|
|
7499
|
-
for (const path$1 of missing) {
|
|
7500
|
-
stmts.deleteOne(path$1);
|
|
7501
|
-
stmts.undoDelete(path$1);
|
|
7502
|
-
stmts.servedDeletePath(path$1);
|
|
7503
|
-
}
|
|
7504
|
-
});
|
|
7397
|
+
abortIf(signal);
|
|
7398
|
+
const startResolved = tryResolve(edit.hash_bounds[0]);
|
|
7399
|
+
const endResolved = tryResolve(edit.hash_bounds[1]);
|
|
7400
|
+
if (!startResolved || !endResolved) {
|
|
7401
|
+
if (!startResolved && endResolved) {
|
|
7402
|
+
const startMismatch = mismatches.findLast((m) => m.ref === edit.hash_bounds[0]);
|
|
7403
|
+
if (startMismatch && startMismatch.kind === "not_found") startMismatch.context = endResolved;
|
|
7404
|
+
} else if (startResolved && !endResolved) {
|
|
7405
|
+
const endMismatch = mismatches.findLast((m) => m.ref === edit.hash_bounds[1]);
|
|
7406
|
+
if (endMismatch && endMismatch.kind === "not_found") endMismatch.context = startResolved;
|
|
7505
7407
|
}
|
|
7408
|
+
return {
|
|
7409
|
+
resolved: void 0,
|
|
7410
|
+
mismatches,
|
|
7411
|
+
boundaryDups
|
|
7412
|
+
};
|
|
7413
|
+
}
|
|
7414
|
+
if (startResolved.line > endResolved.line) throw new Error(`[E_BAD_OP] Range start line ${startResolved.line} must be <= end line ${endResolved.line} (anchors ${edit.hash_bounds[0].hash} and ${edit.hash_bounds[1].hash}).`);
|
|
7415
|
+
const endLine = endResolved.line;
|
|
7416
|
+
const rangeLines = fileLines.slice(startResolved.line - 1, endLine);
|
|
7417
|
+
const canonLines = fileLines.map((line) => canon(line));
|
|
7418
|
+
boundaryDups.push(...trailingDups(edit.content_lines, fileLines, endLine), ...leadingDups(edit.content_lines, fileLines, startResolved.line), ...firstNewAfterDups(edit.content_lines, rangeLines, canonLines, endLine), ...lastNewBeforeDups(edit.content_lines, rangeLines, canonLines, startResolved.line));
|
|
7419
|
+
return {
|
|
7420
|
+
resolved: {
|
|
7421
|
+
content_lines: edit.content_lines,
|
|
7422
|
+
hash_bounds: [startResolved, endResolved]
|
|
7423
|
+
},
|
|
7424
|
+
mismatches,
|
|
7425
|
+
boundaryDups
|
|
7506
7426
|
};
|
|
7507
7427
|
}
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7428
|
+
var ServedRejectionError = class extends Error {
|
|
7429
|
+
code;
|
|
7430
|
+
firstOffendingLine;
|
|
7431
|
+
servedRows;
|
|
7432
|
+
constructor(opts) {
|
|
7433
|
+
super(opts.message);
|
|
7434
|
+
this.name = "ServedRejectionError";
|
|
7435
|
+
this.code = opts.code;
|
|
7436
|
+
this.firstOffendingLine = opts.firstOffendingLine;
|
|
7437
|
+
this.servedRows = opts.servedRows;
|
|
7514
7438
|
}
|
|
7515
|
-
}
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
]) try {
|
|
7523
|
-
await rename(candidate, `${candidate}${suffix}`);
|
|
7524
|
-
} catch (error) {
|
|
7525
|
-
if (errCode(error) !== "ENOENT") console.error("Failed to quarantine corrupt hash store file:", error);
|
|
7439
|
+
};
|
|
7440
|
+
var AnchorMismatchError = class extends Error {
|
|
7441
|
+
servedRows;
|
|
7442
|
+
constructor(message, servedRows) {
|
|
7443
|
+
super(message);
|
|
7444
|
+
this.name = "AnchorMismatchError";
|
|
7445
|
+
this.servedRows = servedRows;
|
|
7526
7446
|
}
|
|
7447
|
+
};
|
|
7448
|
+
function buildRangeEcho(startLine, endLine, fileHashes) {
|
|
7449
|
+
const total = endLine - startLine + 1;
|
|
7450
|
+
const shown = Math.min(total, SERVED_ECHO_CAP);
|
|
7451
|
+
const rows = [];
|
|
7452
|
+
for (let ln = startLine; ln < startLine + shown; ln++) rows.push({
|
|
7453
|
+
position: ln - 1,
|
|
7454
|
+
hash: fileHashes[ln - 1]
|
|
7455
|
+
});
|
|
7456
|
+
return rows;
|
|
7527
7457
|
}
|
|
7528
|
-
function
|
|
7529
|
-
|
|
7530
|
-
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
7531
|
-
} catch {}
|
|
7532
|
-
db.close();
|
|
7458
|
+
function fmtServedRows(rows, fileLines) {
|
|
7459
|
+
return rows.map((row) => `${row.hash}${HASH_SEP}${fileLines[row.position] ?? ""}`).join("\n");
|
|
7533
7460
|
}
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
const missing = [];
|
|
7537
|
-
for (let i = 0; i < rows.length; i += STAT_BATCH) {
|
|
7538
|
-
const batch = rows.slice(i, i + STAT_BATCH);
|
|
7539
|
-
const results = await Promise.all(batch.map(async (row) => {
|
|
7540
|
-
try {
|
|
7541
|
-
await stat(row.path);
|
|
7542
|
-
return;
|
|
7543
|
-
} catch {
|
|
7544
|
-
return row.path;
|
|
7545
|
-
}
|
|
7546
|
-
}));
|
|
7547
|
-
for (const path$1 of results) if (path$1 !== void 0) missing.push(path$1);
|
|
7548
|
-
}
|
|
7549
|
-
return missing;
|
|
7461
|
+
function retryHint() {
|
|
7462
|
+
return "Read the file once to refresh anchors, then resubmit the whole batch.";
|
|
7550
7463
|
}
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7464
|
+
function paginationHint(nextOffset, more) {
|
|
7465
|
+
return `[... ${more} more — read offset=${nextOffset}]`;
|
|
7466
|
+
}
|
|
7467
|
+
function verifyServedRange(args) {
|
|
7468
|
+
const { served, startHash, endHash, startLine, endLine, fileHashes, fileLines, filePath } = args;
|
|
7469
|
+
const where = filePath ? ` in ${filePath}` : "";
|
|
7470
|
+
const echoRows = buildRangeEcho(startLine, endLine, fileHashes);
|
|
7471
|
+
const totalLen = endLine - startLine + 1;
|
|
7472
|
+
const tail = echoRows.length < totalLen ? `\n${paginationHint(startLine + echoRows.length, totalLen - echoRows.length)}` : "";
|
|
7473
|
+
const echo = fmtServedRows(echoRows, fileLines) + tail;
|
|
7474
|
+
const startPositions = servedPositionsOf(served, startHash);
|
|
7475
|
+
const endPositions = servedPositionsOf(served, endHash);
|
|
7476
|
+
if (startPositions.length === 0 && endPositions.length === 0) return;
|
|
7477
|
+
const currentLen = endLine - startLine + 1;
|
|
7478
|
+
let from;
|
|
7479
|
+
let to;
|
|
7480
|
+
if (startPositions.length === 1 && endPositions.length === 1) {
|
|
7481
|
+
from = Math.min(startPositions[0], endPositions[0]);
|
|
7482
|
+
to = Math.max(startPositions[0], endPositions[0]);
|
|
7483
|
+
} else {
|
|
7484
|
+
const candidates = [];
|
|
7485
|
+
for (const s of startPositions) for (const e of endPositions) {
|
|
7486
|
+
const candFrom = Math.min(s, e);
|
|
7487
|
+
const candTo = Math.max(s, e);
|
|
7488
|
+
if (candTo - candFrom + 1 !== currentLen) continue;
|
|
7489
|
+
let ok = true;
|
|
7490
|
+
for (let k = 0; k < currentLen; k++) if (served[candFrom + k] !== fileHashes[startLine - 1 + k]) {
|
|
7491
|
+
ok = false;
|
|
7492
|
+
break;
|
|
7493
|
+
}
|
|
7494
|
+
if (ok) candidates.push({
|
|
7495
|
+
from: candFrom,
|
|
7496
|
+
to: candTo
|
|
7497
|
+
});
|
|
7498
|
+
}
|
|
7499
|
+
if (candidates.length === 1) {
|
|
7500
|
+
from = candidates[0].from;
|
|
7501
|
+
to = candidates[0].to;
|
|
7502
|
+
} else if (candidates.length > 1) {
|
|
7503
|
+
candidates.sort((a, b) => Math.abs(a.from - (startLine - 1)) - Math.abs(b.from - (startLine - 1)));
|
|
7504
|
+
from = candidates[0].from;
|
|
7505
|
+
to = candidates[0].to;
|
|
7506
|
+
}
|
|
7564
7507
|
}
|
|
7565
|
-
if (
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7508
|
+
if (from === void 0 || to === void 0) {
|
|
7509
|
+
const problems = [];
|
|
7510
|
+
if (startPositions.length === 0) problems.push(`remove_from "${startHash}" has no served position`);
|
|
7511
|
+
else if (startPositions.length > 1) problems.push(`remove_from "${startHash}" was served at ${startPositions.length} positions`);
|
|
7512
|
+
if (endPositions.length === 0) problems.push(`remove_to "${endHash}" has no served position`);
|
|
7513
|
+
else if (endPositions.length > 1) problems.push(`remove_to "${endHash}" was served at ${endPositions.length} positions`);
|
|
7514
|
+
throw new ServedRejectionError({
|
|
7515
|
+
code: "E_RANGE_UNVERIFIED",
|
|
7516
|
+
message: `[E_RANGE_UNVERIFIED] cannot verify range against served state${where}: ${problems.join("; ")}. Current range:
|
|
7517
|
+
${echo}
|
|
7518
|
+
${retryHint()}`,
|
|
7519
|
+
servedRows: echoRows
|
|
7520
|
+
});
|
|
7570
7521
|
}
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7522
|
+
for (let i = from; i <= to; i++) if (served[i] === null) throw new ServedRejectionError({
|
|
7523
|
+
code: "E_RANGE_UNSERVED",
|
|
7524
|
+
message: `[E_RANGE_UNSERVED] line ${i + 1}${where} was never served.\nCurrent range:\n${echo}\n${retryHint()}`,
|
|
7525
|
+
firstOffendingLine: i + 1,
|
|
7526
|
+
servedRows: echoRows
|
|
7575
7527
|
});
|
|
7576
|
-
const
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7528
|
+
const servedLen = to - from + 1;
|
|
7529
|
+
if (servedLen !== currentLen) throw new ServedRejectionError({
|
|
7530
|
+
code: "E_RANGE_STALE",
|
|
7531
|
+
message: `[E_RANGE_STALE] served span (${servedLen} lines) no longer matches current range (${currentLen} lines)${where}.\nCurrent range:\n${echo}\n${retryHint()}`,
|
|
7532
|
+
firstOffendingLine: startLine,
|
|
7533
|
+
servedRows: echoRows
|
|
7582
7534
|
});
|
|
7583
|
-
if (
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7535
|
+
for (let k = 0; k < servedLen; k++) if (served[from + k] !== fileHashes[startLine - 1 + k]) {
|
|
7536
|
+
const offendingLine = startLine + k;
|
|
7537
|
+
throw new ServedRejectionError({
|
|
7538
|
+
code: "E_RANGE_STALE",
|
|
7539
|
+
message: `[E_RANGE_STALE] Line ${offendingLine}${where} differs from what you were served — the file changed on disk since it was read. Current range:\n${echo}\n${retryHint()}`,
|
|
7540
|
+
firstOffendingLine: offendingLine,
|
|
7541
|
+
servedRows: echoRows
|
|
7589
7542
|
});
|
|
7590
7543
|
}
|
|
7591
|
-
return store;
|
|
7592
7544
|
}
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7545
|
+
async function recordEchoServes(path$1, rows, policy, lineCount) {
|
|
7546
|
+
if (policy !== "live") return;
|
|
7547
|
+
await recordServed(path$1, rows, lineCount);
|
|
7596
7548
|
}
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
openings.delete(storePath);
|
|
7611
|
-
});
|
|
7612
|
-
openings.set(storePath, promise);
|
|
7613
|
-
return promise;
|
|
7549
|
+
function buildIdx(content) {
|
|
7550
|
+
const fileLines = splitLines(content);
|
|
7551
|
+
const lineStarts = [];
|
|
7552
|
+
let offset = 0;
|
|
7553
|
+
for (let index = 0; index < fileLines.length; index++) {
|
|
7554
|
+
lineStarts.push(offset);
|
|
7555
|
+
offset += fileLines[index].length;
|
|
7556
|
+
if (index < fileLines.length - 1) offset += 1;
|
|
7557
|
+
}
|
|
7558
|
+
return {
|
|
7559
|
+
fileLines,
|
|
7560
|
+
lineStarts
|
|
7561
|
+
};
|
|
7614
7562
|
}
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
const entry = stores.get(storePathFor());
|
|
7618
|
-
return entry?.db.isOpen ? entry : void 0;
|
|
7563
|
+
function assertNotEmpty(originalContent, result) {
|
|
7564
|
+
if (originalContent.length > 0 && result.length === 0) throw new Error("[E_WOULD_EMPTY] Cannot empty a non-empty file via edit. Use `write` if you need to clear the file.");
|
|
7619
7565
|
}
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7566
|
+
function resToSpan(edit, content, lineIndex) {
|
|
7567
|
+
const { fileLines, lineStarts } = lineIndex;
|
|
7568
|
+
const startLine = edit.hash_bounds[0].line;
|
|
7569
|
+
const endLine = edit.hash_bounds[1].line;
|
|
7570
|
+
const originalLines = fileLines.slice(startLine - 1, endLine);
|
|
7571
|
+
if (originalLines.length === edit.content_lines.length && originalLines.every((line, lineIndex$1) => line === edit.content_lines[lineIndex$1])) return {
|
|
7572
|
+
kind: "noop",
|
|
7573
|
+
loc: edit.hash_bounds[0].hash,
|
|
7574
|
+
currentContent: originalLines.join("\n")
|
|
7575
|
+
};
|
|
7576
|
+
if (edit.content_lines.length > 0) return {
|
|
7577
|
+
kind: "replace",
|
|
7578
|
+
start: lineStarts[startLine - 1],
|
|
7579
|
+
end: lineStarts[endLine - 1] + fileLines[endLine - 1].length,
|
|
7580
|
+
replacement: edit.content_lines.join("\n")
|
|
7581
|
+
};
|
|
7582
|
+
if (startLine === 1 && endLine === fileLines.length) return {
|
|
7583
|
+
kind: "replace",
|
|
7584
|
+
start: 0,
|
|
7585
|
+
end: content.length,
|
|
7586
|
+
replacement: ""
|
|
7587
|
+
};
|
|
7588
|
+
if (endLine < fileLines.length) return {
|
|
7589
|
+
kind: "replace",
|
|
7590
|
+
start: lineStarts[startLine - 1],
|
|
7591
|
+
end: lineStarts[endLine],
|
|
7592
|
+
replacement: ""
|
|
7593
|
+
};
|
|
7594
|
+
if (content.endsWith("\n")) return {
|
|
7595
|
+
kind: "replace",
|
|
7596
|
+
start: lineStarts[startLine - 1],
|
|
7597
|
+
end: content.length,
|
|
7598
|
+
replacement: ""
|
|
7599
|
+
};
|
|
7600
|
+
const prevLine = startLine >= 2 ? fileLines[startLine - 2] : void 0;
|
|
7601
|
+
return {
|
|
7602
|
+
kind: "replace",
|
|
7603
|
+
start: prevLine !== void 0 && prevLine.length === 0 ? lineStarts[startLine - 1] : Math.max(0, lineStarts[startLine - 1] - 1),
|
|
7604
|
+
end: content.length,
|
|
7605
|
+
replacement: ""
|
|
7606
|
+
};
|
|
7625
7607
|
}
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
* caller has already loaded the store in every in-process path).
|
|
7630
|
-
*/
|
|
7631
|
-
function withStore(fn) {
|
|
7632
|
-
const store = currentStore();
|
|
7633
|
-
if (store) withBusyRetry(() => {
|
|
7634
|
-
store.db.exec("BEGIN IMMEDIATE");
|
|
7635
|
-
try {
|
|
7636
|
-
fn();
|
|
7637
|
-
store.db.exec("COMMIT");
|
|
7638
|
-
} catch (e) {
|
|
7639
|
-
try {
|
|
7640
|
-
store.db.exec("ROLLBACK");
|
|
7641
|
-
} catch {}
|
|
7642
|
-
throw e;
|
|
7643
|
-
}
|
|
7644
|
-
});
|
|
7645
|
-
else fn();
|
|
7608
|
+
function assemble(content, span, signal) {
|
|
7609
|
+
abortIf(signal);
|
|
7610
|
+
return content.slice(0, span.start) + span.replacement + content.slice(span.end);
|
|
7646
7611
|
}
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
let parsed;
|
|
7658
|
-
try {
|
|
7659
|
-
parsed = JSON.parse(content);
|
|
7660
|
-
} catch (error) {
|
|
7661
|
-
console.error("Failed to parse legacy hash store, skipping migration:", error);
|
|
7662
|
-
return;
|
|
7612
|
+
function applyEdit(content, edit, signal, precomputedHashes, filePath, served) {
|
|
7613
|
+
abortIf(signal);
|
|
7614
|
+
const lineIndex = buildIdx(content);
|
|
7615
|
+
const fileHashes = precomputedHashes ?? lineHashesPure(content);
|
|
7616
|
+
const warnings = [];
|
|
7617
|
+
const prefixFixed = stripDiffPrefixes(stripBarePrefixes(swapReversedRanges(edit, fileHashes, warnings), fileHashes, warnings), warnings);
|
|
7618
|
+
const { resolved: initialResolved, mismatches, boundaryDups } = valEdit(prefixFixed, lineIndex.fileLines, fileHashes, warnings, signal);
|
|
7619
|
+
if (mismatches.length || !initialResolved) {
|
|
7620
|
+
const { message, servedRows } = fmtMismatchWithServes(mismatches, lineIndex.fileLines, fileHashes, filePath);
|
|
7621
|
+
throw new AnchorMismatchError(message, servedRows);
|
|
7663
7622
|
}
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7623
|
+
warnUnicodeEsc(prefixFixed, warnings);
|
|
7624
|
+
let resolved = initialResolved;
|
|
7625
|
+
let autoFixes;
|
|
7626
|
+
if (boundaryDups.length > 0) {
|
|
7627
|
+
autoFixes = [];
|
|
7628
|
+
const correctedEdit = {
|
|
7629
|
+
...prefixFixed,
|
|
7630
|
+
content_lines: [...prefixFixed.content_lines]
|
|
7631
|
+
};
|
|
7632
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7633
|
+
const uniqueDups = [];
|
|
7634
|
+
for (const dup of boundaryDups) {
|
|
7635
|
+
if (seen.has(dup.replacementLineIndex)) continue;
|
|
7636
|
+
seen.add(dup.replacementLineIndex);
|
|
7637
|
+
uniqueDups.push(dup);
|
|
7672
7638
|
}
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
db.exec("ROLLBACK");
|
|
7689
|
-
throw e;
|
|
7639
|
+
const dupsByIndex = uniqueDups.sort((a, b) => b.replacementLineIndex - a.replacementLineIndex);
|
|
7640
|
+
for (const dup of dupsByIndex) {
|
|
7641
|
+
const idx = dup.replacementLineIndex;
|
|
7642
|
+
if (idx < 0 || idx >= correctedEdit.content_lines.length) continue;
|
|
7643
|
+
const removed = correctedEdit.content_lines.splice(idx, 1)[0];
|
|
7644
|
+
autoFixes.push({
|
|
7645
|
+
kind: dup.kind,
|
|
7646
|
+
removedLine: removed,
|
|
7647
|
+
removedLineIndex: idx
|
|
7648
|
+
});
|
|
7649
|
+
}
|
|
7650
|
+
const correctedResult = valEdit(correctedEdit, lineIndex.fileLines, fileHashes, warnings, signal);
|
|
7651
|
+
if (correctedResult.mismatches.length || !correctedResult.resolved) {
|
|
7652
|
+
const { message, servedRows } = fmtMismatchWithServes(correctedResult.mismatches, lineIndex.fileLines, fileHashes, filePath);
|
|
7653
|
+
throw new AnchorMismatchError(message, servedRows);
|
|
7690
7654
|
}
|
|
7655
|
+
resolved = correctedResult.resolved;
|
|
7691
7656
|
}
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7657
|
+
if (served) {
|
|
7658
|
+
const startAnchor = resolved.hash_bounds[0];
|
|
7659
|
+
const endAnchor = resolved.hash_bounds[1];
|
|
7660
|
+
verifyServedRange({
|
|
7661
|
+
served,
|
|
7662
|
+
startHash: startAnchor.hash,
|
|
7663
|
+
endHash: endAnchor.hash,
|
|
7664
|
+
startLine: startAnchor.line,
|
|
7665
|
+
endLine: endAnchor.line,
|
|
7666
|
+
fileHashes,
|
|
7667
|
+
fileLines: lineIndex.fileLines,
|
|
7668
|
+
filePath
|
|
7669
|
+
});
|
|
7696
7670
|
}
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
//#endregion
|
|
7708
|
-
//#region src/url-schema/vendored/hashline/hashline/hash.js
|
|
7709
|
-
let defaultHashSnapshotIO;
|
|
7710
|
-
function snapshotIOFor(store) {
|
|
7711
|
-
if (store) return {
|
|
7712
|
-
get: (path$1, content, deleteCorrupt) => Promise.resolve(store.getSnapshot(path$1, content, deleteCorrupt)),
|
|
7713
|
-
upsert: (path$1, checksum, lineCount, hashes) => {
|
|
7714
|
-
store.upsertSnapshot(path$1, checksum, lineCount, hashes);
|
|
7715
|
-
return Promise.resolve();
|
|
7671
|
+
const spanResult = resToSpan(resolved, content, lineIndex);
|
|
7672
|
+
if (spanResult.kind === "noop") return {
|
|
7673
|
+
content,
|
|
7674
|
+
firstChangedLine: void 0,
|
|
7675
|
+
lastChangedLine: void 0,
|
|
7676
|
+
range: resolvedRange(resolved),
|
|
7677
|
+
...warnings.length ? { warnings } : {},
|
|
7678
|
+
noopEdit: {
|
|
7679
|
+
loc: spanResult.loc,
|
|
7680
|
+
currentContent: spanResult.currentContent
|
|
7716
7681
|
}
|
|
7717
7682
|
};
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7683
|
+
const result = assemble(content, spanResult, signal);
|
|
7684
|
+
assertNotEmpty(content, result);
|
|
7685
|
+
const changed = changedRange(content, result);
|
|
7686
|
+
return {
|
|
7687
|
+
content: result,
|
|
7688
|
+
firstChangedLine: changed?.firstChangedLine,
|
|
7689
|
+
lastChangedLine: changed?.lastChangedLine,
|
|
7690
|
+
range: resolvedRange(resolved),
|
|
7691
|
+
...warnings.length ? { warnings } : {},
|
|
7692
|
+
...autoFixes ? { autoFixes } : {}
|
|
7725
7693
|
};
|
|
7726
7694
|
}
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
}
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
const
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7695
|
+
function resolvedRange(resolved) {
|
|
7696
|
+
const [start, end] = resolved.hash_bounds;
|
|
7697
|
+
return {
|
|
7698
|
+
startLine: start.line,
|
|
7699
|
+
endLine: end.line,
|
|
7700
|
+
startHash: start.hash,
|
|
7701
|
+
endHash: end.hash,
|
|
7702
|
+
delta: resolved.content_lines.length - (Math.abs(end.line - start.line) + 1)
|
|
7703
|
+
};
|
|
7704
|
+
}
|
|
7705
|
+
function fmtRegion(hashes, lines) {
|
|
7706
|
+
if (hashes.length !== lines.length) throw new Error(`fmtRegion: hashes.length (${hashes.length}) must match lines.length (${lines.length}).`);
|
|
7707
|
+
return lines.map((line, index) => `${hashes[index]}${HASH_SEP}${line}`).join("\n");
|
|
7708
|
+
}
|
|
7709
|
+
function changedRange(original, result) {
|
|
7710
|
+
if (original === result) return null;
|
|
7711
|
+
if (original.length === 0) return {
|
|
7712
|
+
firstChangedLine: 1,
|
|
7713
|
+
lastChangedLine: splitLines(result).length
|
|
7714
|
+
};
|
|
7715
|
+
const originalLines = splitLines(original);
|
|
7716
|
+
const resultLines = splitLines(result);
|
|
7717
|
+
if (originalLines.length === resultLines.length && originalLines.every((line, index) => line === resultLines[index])) return null;
|
|
7718
|
+
const minLen = Math.min(originalLines.length, resultLines.length);
|
|
7719
|
+
let first = 0;
|
|
7720
|
+
while (first < minLen && originalLines[first] === resultLines[first]) first++;
|
|
7721
|
+
let lastOrig = originalLines.length - 1;
|
|
7722
|
+
let lastRes = resultLines.length - 1;
|
|
7723
|
+
while (lastOrig >= first && lastRes >= first && originalLines[lastOrig] === resultLines[lastRes]) {
|
|
7724
|
+
lastOrig--;
|
|
7725
|
+
lastRes--;
|
|
7752
7726
|
}
|
|
7753
|
-
return
|
|
7727
|
+
return {
|
|
7728
|
+
firstChangedLine: first + 1,
|
|
7729
|
+
lastChangedLine: Math.max(first, lastRes) + 1
|
|
7730
|
+
};
|
|
7754
7731
|
}
|
|
7755
7732
|
|
|
7756
7733
|
//#endregion
|
|
@@ -8182,15 +8159,15 @@ const UTF8_REWRITE_NOTE = "[Non-UTF-8 bytes shown as U+FFFD; editing rewrites th
|
|
|
8182
8159
|
* context.
|
|
8183
8160
|
*/
|
|
8184
8161
|
async function readAndServe(io, rawPath, cwd, options) {
|
|
8185
|
-
const {
|
|
8162
|
+
const { signal } = options;
|
|
8186
8163
|
abortIf(signal);
|
|
8187
8164
|
const view = await readView(io, rawPath, cwd, {
|
|
8188
8165
|
offset: options.offset,
|
|
8189
8166
|
limit: options.limit,
|
|
8190
8167
|
signal
|
|
8191
8168
|
});
|
|
8192
|
-
if (view.served.length > 0) await recordServed(
|
|
8193
|
-
await clearDriftReported(
|
|
8169
|
+
if (view.served.length > 0) await recordServed(view.absolutePath, view.served, view.hashes.length);
|
|
8170
|
+
await clearDriftReported(view.absolutePath);
|
|
8194
8171
|
return {
|
|
8195
8172
|
text: view.hadUtf8DecodeErrors ? `${view.text}\n\n${UTF8_REWRITE_NOTE}` : view.text,
|
|
8196
8173
|
served: view.served,
|
|
@@ -8254,10 +8231,8 @@ function createReadTool(deps) {
|
|
|
8254
8231
|
}
|
|
8255
8232
|
return withWorkspace(execCwd(exec), async () => {
|
|
8256
8233
|
const cwd = execCwd(exec);
|
|
8257
|
-
const sessionKey = execSessionKey(exec);
|
|
8258
8234
|
const signal = exec.signal;
|
|
8259
8235
|
const { text, absolutePath } = await readAndServe(io, rawPath, cwd, {
|
|
8260
|
-
sessionKey,
|
|
8261
8236
|
signal,
|
|
8262
8237
|
offset: canonical.offset,
|
|
8263
8238
|
limit: canonical.limit
|
|
@@ -8684,12 +8659,12 @@ async function applyOne(input, onReject) {
|
|
|
8684
8659
|
* with no change. Messages are byte-identical to the pre-engine tools.
|
|
8685
8660
|
*/
|
|
8686
8661
|
async function enforceNoopLoop(opts) {
|
|
8687
|
-
const { absolutePath, removeFrom, removeTo, displayPath, index, count,
|
|
8662
|
+
const { absolutePath, removeFrom, removeTo, displayPath, index, count, originalHashes } = opts;
|
|
8688
8663
|
if (index === void 0) {
|
|
8689
8664
|
if (count >= NOOP_LOOP_THRESHOLD) {
|
|
8690
8665
|
const echoRows = buildRangeEcho(opts.range.startLine, opts.range.endLine, originalHashes);
|
|
8691
8666
|
const echo = fmtServedRows(echoRows, splitLines(opts.originalNormalized));
|
|
8692
|
-
await recordEchoServes(
|
|
8667
|
+
await recordEchoServes(absolutePath, echoRows, "live", originalHashes.length);
|
|
8693
8668
|
throw new Error(`[E_NOOP_LOOP] identical edit (${removeFrom} → ${removeTo} in ${displayPath}) submitted ${count}×, no changes each time. Range already contains this text; resend will reject. Current range:\n${echo}`);
|
|
8694
8669
|
}
|
|
8695
8670
|
if (count === 2) return `[E_NOOP_LOOP] Notice: identical edit (${removeFrom} → ${removeTo} in ${displayPath}) no-op'd twice; range already has this text. Resend will reject.`;
|
|
@@ -8698,7 +8673,7 @@ async function enforceNoopLoop(opts) {
|
|
|
8698
8673
|
if (count >= NOOP_LOOP_THRESHOLD) {
|
|
8699
8674
|
const originalLines = splitLines(opts.originalNormalized);
|
|
8700
8675
|
const echoRows = opts.echoRows;
|
|
8701
|
-
if (echoRows) await recordEchoServes(
|
|
8676
|
+
if (echoRows) await recordEchoServes(absolutePath, echoRows, "live", originalHashes.length);
|
|
8702
8677
|
throw new Error(`[E_NOOP_LOOP] edits[${index}] (${displayPath}): identical edit (${removeFrom} → ${removeTo}) submitted ${count}×, no changes each time. Range already has this text; resend will reject the batch.` + (echoRows ? ` Current on-disk range:\n${fmtServedRows(echoRows, originalLines)}` : ""));
|
|
8703
8678
|
}
|
|
8704
8679
|
if (count === 2) return `[E_NOOP_LOOP] Notice: edits[${index}] (${displayPath}) — identical edit no-op'd twice; range already has this text. Resend will reject the batch.`;
|
|
@@ -8728,7 +8703,7 @@ async function runFileEdits(io, items, opts) {
|
|
|
8728
8703
|
signal: opts.signal,
|
|
8729
8704
|
maxLines: MAX_HASH_LINES
|
|
8730
8705
|
});
|
|
8731
|
-
const served = await loadServed(
|
|
8706
|
+
const served = await loadServed(absolutePath);
|
|
8732
8707
|
const warnings = [];
|
|
8733
8708
|
let currentContent = originalNormalized;
|
|
8734
8709
|
let currentHashes = originalHashes;
|
|
@@ -8760,7 +8735,7 @@ async function runFileEdits(io, items, opts) {
|
|
|
8760
8735
|
if (error instanceof AnchorMismatchError || error instanceof ServedRejectionError) {
|
|
8761
8736
|
const originalLines = splitLines(originalNormalized);
|
|
8762
8737
|
const echoRows = error.servedRows.length > 0 ? error.servedRows : edit ? echoRowsForItem(edit, originalHashes) : void 0;
|
|
8763
|
-
if (echoRows) await recordEchoServes(
|
|
8738
|
+
if (echoRows) await recordEchoServes(absolutePath, echoRows, "live", originalHashes.length);
|
|
8764
8739
|
const echoBlock = echoRows ? ` Current on-disk range for edits[${item.index}] (unchanged — nothing was written):\n${fmtServedRows(echoRows, originalLines)}` : " Call read() to get fresh anchors.";
|
|
8765
8740
|
throw new Error(`[E_BATCH_ABORT] edits[${item.index}] (${item.path}) failed: ${error.message}${echoBlock}\nThe whole batch was rejected and NOTHING was written — no file changed and earlier items in the batch were NOT applied. Fix the failing edit (and any later edit that depends on it), then resubmit the batch.`);
|
|
8766
8741
|
}
|
|
@@ -8787,7 +8762,6 @@ async function runFileEdits(io, items, opts) {
|
|
|
8787
8762
|
displayPath: item.path,
|
|
8788
8763
|
index: item.index,
|
|
8789
8764
|
count,
|
|
8790
|
-
sessionKey: opts.sessionKey,
|
|
8791
8765
|
originalHashes,
|
|
8792
8766
|
originalNormalized,
|
|
8793
8767
|
echoRows: echoRowsForItem(applied.edit, originalHashes)
|
|
@@ -8826,7 +8800,6 @@ async function runFileEdits(io, items, opts) {
|
|
|
8826
8800
|
const originalLines = splitLines(originalNormalized);
|
|
8827
8801
|
try {
|
|
8828
8802
|
driftNotice = await scanDrift({
|
|
8829
|
-
sessionKey: opts.sessionKey,
|
|
8830
8803
|
served,
|
|
8831
8804
|
resultHashes,
|
|
8832
8805
|
resultLines,
|
|
@@ -9036,11 +9009,8 @@ function buildBatchResult(sections) {
|
|
|
9036
9009
|
* across every tool — it concentrates (deep).
|
|
9037
9010
|
*/
|
|
9038
9011
|
async function execute(opts) {
|
|
9039
|
-
const { io, items,
|
|
9040
|
-
const fileResult = await applySequence(io, items, {
|
|
9041
|
-
signal,
|
|
9042
|
-
sessionKey
|
|
9043
|
-
});
|
|
9012
|
+
const { io, items, signal, exec, sandbox, sandboxPolicy } = opts;
|
|
9013
|
+
const fileResult = await applySequence(io, items, { signal });
|
|
9044
9014
|
const toSection = () => ({
|
|
9045
9015
|
path: fileResult.displayPath,
|
|
9046
9016
|
originalNormalized: fileResult.originalNormalized,
|
|
@@ -9057,7 +9027,7 @@ async function execute(opts) {
|
|
|
9057
9027
|
const recordIfNeeded = async (built$1) => {
|
|
9058
9028
|
if (built$1.details.servedRows && built$1.details.servedRows.length > 0) {
|
|
9059
9029
|
const entry = built$1.details.servedByPath?.[0];
|
|
9060
|
-
if (entry) await recordServedTruncated(
|
|
9030
|
+
if (entry) await recordServedTruncated(fileResult.absolutePath, entry.servedRows, splitLines(fileResult.result).length, fileResult.range.startLine - 1);
|
|
9061
9031
|
}
|
|
9062
9032
|
};
|
|
9063
9033
|
if (items.length === 1 && fileResult.appliedCount + fileResult.noopCount === 1) {
|
|
@@ -9214,7 +9184,6 @@ function buildEditTool(io, sandbox) {
|
|
|
9214
9184
|
async execute(args, exec) {
|
|
9215
9185
|
return withWorkspace(execCwd(exec), async () => {
|
|
9216
9186
|
const cwd = execCwd(exec);
|
|
9217
|
-
const sessionKey = execSessionKey(exec);
|
|
9218
9187
|
const signal = exec.signal;
|
|
9219
9188
|
const canonical = normalizeRequest(args);
|
|
9220
9189
|
assertEditRequest(canonical);
|
|
@@ -9251,7 +9220,6 @@ function buildEditTool(io, sandbox) {
|
|
|
9251
9220
|
return execute({
|
|
9252
9221
|
io,
|
|
9253
9222
|
items,
|
|
9254
|
-
sessionKey,
|
|
9255
9223
|
signal,
|
|
9256
9224
|
exec,
|
|
9257
9225
|
sandbox,
|
|
@@ -9296,7 +9264,6 @@ function buildUndoTool(io, sandbox) {
|
|
|
9296
9264
|
async execute(args, exec) {
|
|
9297
9265
|
return withWorkspace(execCwd(exec), async () => {
|
|
9298
9266
|
const cwd = execCwd(exec);
|
|
9299
|
-
const sessionKey = execSessionKey(exec);
|
|
9300
9267
|
const signal = exec.signal;
|
|
9301
9268
|
const canonical = normalizeRequest(args);
|
|
9302
9269
|
assertUndoRequest(canonical);
|
|
@@ -9347,7 +9314,7 @@ function buildUndoTool(io, sandbox) {
|
|
|
9347
9314
|
const parts = [`Undone last edit on ${path$1}.`];
|
|
9348
9315
|
if (linesAddedByEdit > 0 || linesRemovedByEdit > 0) parts.push(`Removed ${linesAddedByEdit} line(s) that were added and restored ${linesRemovedByEdit} line(s) that were removed.`);
|
|
9349
9316
|
parts.push("File reverted to previous state. The post-edit diff rows carry the restored file’s fresh anchors for follow-up edits.");
|
|
9350
|
-
if (undoDenseRows.length > 0) await recordServedTruncated(
|
|
9317
|
+
if (undoDenseRows.length > 0) await recordServedTruncated(absolutePath, undoDenseRows, splitLines(undo.content).length, restoredRange?.firstChangedLine ?? undoDiffResult.firstChangedLine ?? 0);
|
|
9351
9318
|
return [
|
|
9352
9319
|
parts.join("\n"),
|
|
9353
9320
|
"",
|
|
@@ -9366,51 +9333,6 @@ function registerUndoTool(_rootCtx, agentCtx, io, sandbox) {
|
|
|
9366
9333
|
return agentCtx.tools.register(buildUndoTool(io, sandbox));
|
|
9367
9334
|
}
|
|
9368
9335
|
|
|
9369
|
-
//#endregion
|
|
9370
|
-
//#region src/url-schema/vendored/hashline/write-hook.js
|
|
9371
|
-
const AUTO_READ_HEADING = "--- Auto-read (hashline anchors) ---";
|
|
9372
|
-
/**
|
|
9373
|
-
* Register the post-write auto-read listener on the calling agent's scope.
|
|
9374
|
-
* Replaces the result content (never the canonical value) with the original
|
|
9375
|
-
* content plus the hashline preview; any failure falls back to the untouched
|
|
9376
|
-
* decision so a broken auto-read never breaks the write.
|
|
9377
|
-
* @param rootCtx - host context for diagnostics.
|
|
9378
|
-
* @param agentCtx - the agent's scoped context; the listener receives only
|
|
9379
|
-
* this agent's tool results.
|
|
9380
|
-
* @param io - the filesystem bridge.
|
|
9381
|
-
* @returns the exact disposer that unregisters the listener.
|
|
9382
|
-
*/
|
|
9383
|
-
function registerWriteHook(rootCtx, agentCtx, io) {
|
|
9384
|
-
return agentCtx.on("tools/post-execute", async (exec, result, next) => {
|
|
9385
|
-
return withWorkspace(execCwd(exec), async () => {
|
|
9386
|
-
const decision = await next();
|
|
9387
|
-
if (exec.name !== "write" || result.isError || decision.kind !== "accept") return decision;
|
|
9388
|
-
const decisionContent = decision.content ?? result.content;
|
|
9389
|
-
const rawPath = exec.arguments?.file_path ?? exec.arguments?.path;
|
|
9390
|
-
if (typeof rawPath !== "string") return decision;
|
|
9391
|
-
try {
|
|
9392
|
-
const cwd = execCwd(exec);
|
|
9393
|
-
const sessionKey = execSessionKey(exec);
|
|
9394
|
-
const signal = exec.signal;
|
|
9395
|
-
const { text } = await readAndServe(io, rawPath, cwd, {
|
|
9396
|
-
sessionKey,
|
|
9397
|
-
signal
|
|
9398
|
-
});
|
|
9399
|
-
return {
|
|
9400
|
-
kind: "accept",
|
|
9401
|
-
content: [...decisionContent, {
|
|
9402
|
-
type: "text",
|
|
9403
|
-
text: `\n\n${AUTO_READ_HEADING}\n${text}`
|
|
9404
|
-
}]
|
|
9405
|
-
};
|
|
9406
|
-
} catch (error) {
|
|
9407
|
-
rootCtx.logger.warn(`dsh-better-edit: auto-read after write failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
9408
|
-
return decision;
|
|
9409
|
-
}
|
|
9410
|
-
});
|
|
9411
|
-
});
|
|
9412
|
-
}
|
|
9413
|
-
|
|
9414
9336
|
//#endregion
|
|
9415
9337
|
//#region src/url-schema/vendored/hashline/guidance/parse.js
|
|
9416
9338
|
/**
|
|
@@ -9847,7 +9769,6 @@ function installAgentTools(rootCtx, agent, resolver) {
|
|
|
9847
9769
|
const hashlineIo = ctxFsIO(rootCtx.fs, rootCtx);
|
|
9848
9770
|
disposers.push(registerEditTool(rootCtx, agent.ctx, hashlineIo, hashlineSandbox));
|
|
9849
9771
|
disposers.push(registerUndoTool(rootCtx, agent.ctx, hashlineIo, hashlineSandbox));
|
|
9850
|
-
disposers.push(registerWriteHook(rootCtx, agent.ctx, hashlineIo));
|
|
9851
9772
|
{
|
|
9852
9773
|
const diagnosticsHook = buildLspWriteFeedback().postWrite;
|
|
9853
9774
|
disposers.push(agent.ctx.on("tools/post-execute", async (exec, result, next) => {
|
|
@@ -11754,7 +11675,7 @@ function apply(ctx, config) {
|
|
|
11754
11675
|
if (session === void 0) return "";
|
|
11755
11676
|
const sandboxPolicy = runtimeCtx.get("sandboxPolicy");
|
|
11756
11677
|
if (!sandboxPolicy) return "";
|
|
11757
|
-
return sandboxPolicy.resolve({ session }).mode === "workspace-write" ? "
|
|
11678
|
+
return sandboxPolicy.resolve({ session }).mode === "workspace-write" ? "A sandbox-deniable/denied call may be escalated with `sandbox_permissions=\"danger-full-access\"` and a one-line `justification`; the runtime will prompt for user approval. Escalation and its approval/denial are per-call." : "";
|
|
11758
11679
|
}
|
|
11759
11680
|
});
|
|
11760
11681
|
});
|