dsh-hashline-edit 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -56
- package/cordis.patch.yml +3 -3
- package/lib/index.js +7224 -6454
- package/lib/types/dsh/contracts.d.ts +3 -12
- package/lib/types/dsh/fs.d.ts +0 -3
- package/lib/types/dsh/guidance.d.ts +5 -5
- package/lib/types/dsh/host-compat.d.ts +78 -7
- package/lib/types/dsh/index.d.ts +9 -21
- package/lib/types/dsh/metrics/events.d.ts +1 -1
- package/lib/types/dsh/metrics/observer.d.ts +1 -1
- package/lib/types/dsh/sandbox.d.ts +2 -2
- package/lib/types/dsh/shell-hook.d.ts +1 -1
- package/lib/types/dsh/tools/edit.d.ts +10 -0
- package/lib/types/dsh/tools/read.d.ts +3 -3
- package/lib/types/src/domain-errors.d.ts +2 -16
- package/lib/types/src/edit-tool.d.ts +41 -0
- package/lib/types/src/edit-undo.d.ts +2 -11
- package/lib/types/src/file-content/index.d.ts +1 -1
- package/lib/types/src/file-content/preview.d.ts +1 -1
- package/lib/types/src/hash-store.d.ts +2 -2
- package/lib/types/src/hashline/alphabet.d.ts +16 -1
- package/lib/types/src/hashline/hash-identity.d.ts +10 -4
- package/lib/types/src/mutation-engine/engine.d.ts +31 -0
- package/lib/types/src/mutation-engine/pipeline.d.ts +1 -8
- package/lib/types/src/mutation-engine/types.d.ts +50 -40
- package/lib/types/src/payload-contract.d.ts +135 -0
- package/lib/types/src/served-session/serve-render.d.ts +0 -6
- package/lib/types/src/undo-store.d.ts +1 -17
- package/package.json +21 -18
- package/prompts/edit-guidelines.md +10 -0
- package/prompts/edit-snippet.md +1 -0
- package/prompts/edit.md +1 -0
- package/prompts/read-guidelines.md +4 -4
- package/prompts/read.md +1 -1
- package/prompts/undo-last-edit-guidelines.md +2 -2
- package/prompts/undo-last-edit-snippet.md +1 -1
- package/prompts/undo-last-edit.md +1 -1
- package/src/bash-classifier.ts +1626 -0
- package/src/bash-conversion/decide.ts +812 -0
- package/src/bash-display-transform.ts +50 -0
- package/src/bash-multifile-config.ts +239 -0
- package/src/bash-view-config.ts +158 -0
- package/src/cut-repair.ts +11 -77
- package/src/domain-errors.ts +3 -35
- package/src/edit-normalize.ts +8 -0
- package/src/edit-render.ts +161 -0
- package/src/edit-tool.ts +118 -0
- package/src/edit-undo.ts +25 -379
- package/src/edit.ts +136 -0
- package/src/file-content/index.ts +1 -1
- package/src/file-content/preview.ts +1 -1
- package/src/file-content/served-header.ts +1 -1
- package/src/hash-store.ts +1 -18
- package/src/hashline/alphabet.ts +42 -1
- package/src/hashline/hash-identity.ts +12 -3
- package/src/integrations/pi-lens/command.ts +712 -0
- package/src/integrations/pi-lens/config.ts +148 -0
- package/src/integrations/pi-lens/io-bridge-adapter.ts +234 -0
- package/src/integrations/pi-lens/io-bridge.ts +112 -0
- package/src/integrations/pi-lens/settings-overlay.ts +272 -0
- package/src/lifecycle-hooks/index.ts +1992 -0
- package/src/lifecycle-hooks/types.ts +64 -0
- package/src/mutated-files.ts +75 -0
- package/src/mutation-engine/engine.ts +239 -0
- package/src/mutation-engine/index.ts +17 -0
- package/src/mutation-engine/pipeline.ts +27 -198
- package/src/mutation-engine/types.ts +57 -39
- package/src/mutation-queue.ts +1 -1
- package/src/payload-contract.ts +685 -0
- package/src/preview-controller.ts +84 -0
- package/src/prompts.ts +43 -10
- package/src/read.ts +865 -0
- package/src/search-hook.ts +41 -0
- package/src/served-session/serve-render.ts +2 -2
- package/src/store-config.ts +73 -0
- package/src/store-janitor-warning.ts +169 -0
- package/src/store-janitor.ts +539 -0
- package/src/store-kernel/settings.ts +233 -0
- package/src/store.ts +154 -0
- package/src/tui-presenter.ts +134 -0
- package/src/undo-store.ts +5 -74
- package/src/write-hook.ts +130 -0
- package/lib/types/dsh/hide.d.ts +0 -79
- package/lib/types/dsh/tools/apply-patch.d.ts +0 -38
- package/lib/types/src/apply-patch/commit.d.ts +0 -49
- package/lib/types/src/apply-patch/grammar.d.ts +0 -29
- package/lib/types/src/apply-patch/lifecycle.d.ts +0 -32
- package/lib/types/src/apply-patch/plan.d.ts +0 -50
- package/lib/types/src/apply-patch/render.d.ts +0 -51
- package/lib/types/src/apply-patch/resolve.d.ts +0 -129
- package/lib/types/src/apply-patch/tool.d.ts +0 -54
- package/lib/types/src/apply-patch/types.d.ts +0 -362
- package/lib/types/src/fs-lifecycle.d.ts +0 -44
- package/lib/types/src/host-utils.d.ts +0 -63
- package/lib/types/src/mutation-engine/edit-request.d.ts +0 -34
- package/prompts/apply-patch-guidelines.md +0 -10
- package/prompts/apply-patch.md +0 -1
- package/src/apply-patch/commit.ts +0 -190
- package/src/apply-patch/grammar.ts +0 -620
- package/src/apply-patch/index.ts +0 -17
- package/src/apply-patch/lifecycle.ts +0 -195
- package/src/apply-patch/plan.ts +0 -800
- package/src/apply-patch/render.ts +0 -137
- package/src/apply-patch/resolve.ts +0 -470
- package/src/apply-patch/tool.ts +0 -257
- package/src/apply-patch/types.ts +0 -387
- package/src/fs-lifecycle.ts +0 -201
- package/src/host-utils.ts +0 -224
- package/src/mutation-engine/edit-request.ts +0 -39
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SAFETY: Pre-execution bash rewrite for intercepted search commands (spec §3.1/§3.3, I1a).
|
|
3
|
+
*
|
|
4
|
+
* WHY a `tool_call` hook and not `tool_result`: native `grep`/`rg` only print `LINE:content`
|
|
5
|
+
* rows when asked for line numbers, and patching the command before dispatch is the only seam
|
|
6
|
+
* that makes the real binary emit them — no shell re-run, no duplicated regex engine (JS
|
|
7
|
+
* `RegExp` and BRE/ERE/Rust regex diverge, so the system binary must execute natively).
|
|
8
|
+
*
|
|
9
|
+
* WHY no shared state with the `tool_result` side: the rewrite is a pure function of the
|
|
10
|
+
* command text, and `handleBash` re-parses the mutated `event.input.command` on its own. A
|
|
11
|
+
* session- or call-keyed map would leak across concurrent bash calls and could pin a lease to
|
|
12
|
+
* a command some other call rewrote.
|
|
13
|
+
*
|
|
14
|
+
* The hook fails open by construction: it returns without touching `input` unless the pure
|
|
15
|
+
* rewrite actually changed the command, and every throw is swallowed so interception can never
|
|
16
|
+
* break a bash call.
|
|
17
|
+
*/
|
|
18
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { withSearchLineNumbers } from "./bash-classifier.js";
|
|
20
|
+
|
|
21
|
+
export function registerSearchHook(pi: ExtensionAPI): void {
|
|
22
|
+
pi.on("tool_call", async (event) => {
|
|
23
|
+
try {
|
|
24
|
+
if (event.toolName !== "bash") return;
|
|
25
|
+
const input = event.input as Record<string, unknown> | undefined;
|
|
26
|
+
if (input === undefined) return;
|
|
27
|
+
const command = input.command;
|
|
28
|
+
if (typeof command !== "string" || command.trim() === "") return;
|
|
29
|
+
const rewritten = withSearchLineNumbers(command);
|
|
30
|
+
// WHY: a denied or non-search command must reach bash byte-identical, so the only
|
|
31
|
+
// WHY: mutation this hook can ever make is the documented line-number splice — `-n` for
|
|
32
|
+
// WHY: grep, `--line-number` for rg, and only for a bare single-file, multi-file or
|
|
33
|
+
// WHY: recursive search (never a pipeline, never `rg --files`).
|
|
34
|
+
if (rewritten === command) return;
|
|
35
|
+
input.command = rewritten;
|
|
36
|
+
} catch {
|
|
37
|
+
// SAFETY: interception is an optimisation, never a gate — a classifier fault leaves the
|
|
38
|
+
// SAFETY: original command and the original raw bash output untouched.
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -90,7 +90,7 @@ export function fmtServedRows(rows: ServedRow[], fileLines: string[]): string {
|
|
|
90
90
|
|
|
91
91
|
// WHY: the one continuation a rejected edit offers (read-chrome): the `windows` offset, not a bare
|
|
92
92
|
// WHY: `offset`, because `offset` alone no longer continues anything a page footer used to name.
|
|
93
|
-
|
|
93
|
+
function paginationHint(nextOffset: number, more: number): string {
|
|
94
94
|
return `[... ${more} more — use windows: [{ offset: ${nextOffset} }]]`;
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -107,7 +107,7 @@ export function servedPositionsOf(served: (string | null)[], hash: string): numb
|
|
|
107
107
|
// WHY: ---------------------------------------------------------------------------
|
|
108
108
|
|
|
109
109
|
/** Builds the reject-and-serve served rows + rendered block for a current on-disk range. */
|
|
110
|
-
|
|
110
|
+
function buildRangeServeBlock(
|
|
111
111
|
startLine: number,
|
|
112
112
|
endLine: number,
|
|
113
113
|
fileHashes: string[],
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store janitor settings — the WRITE half of the `store.*` keys of the shared config file.
|
|
3
|
+
*
|
|
4
|
+
* WHY: the read/resolve ladder lives in `store-kernel/settings.ts` (the kernel owns the maintenance
|
|
5
|
+
* WHY: policy the values feed); what belongs here is the config file's own shape and the atomic,
|
|
6
|
+
* WHY: preserving write. `saveStoreJanitorConfig` is the only writer of these keys.
|
|
7
|
+
*/
|
|
8
|
+
import { DomainError } from "./domain-errors.js";
|
|
9
|
+
import {
|
|
10
|
+
configFilePath,
|
|
11
|
+
isConfigScope,
|
|
12
|
+
isRecord,
|
|
13
|
+
updateConfigFile,
|
|
14
|
+
type ConfigScope,
|
|
15
|
+
} from "./json-config.js";
|
|
16
|
+
import {
|
|
17
|
+
STORE_JANITOR_KEY_NAMES,
|
|
18
|
+
isIntervalMs,
|
|
19
|
+
isBatchSize,
|
|
20
|
+
isStoreJanitorMode,
|
|
21
|
+
type StoreJanitorScopeSettings,
|
|
22
|
+
} from "./store-kernel/settings.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Persist the keys in `settings` for `scope`, preserving every other top-level key, other `store.*`
|
|
26
|
+
* keys and other owners' keys already in the file. Creates the directory as needed and replaces the
|
|
27
|
+
* file atomically. Throws `E_INTERNAL_INVARIANT` — and writes nothing — for a value outside its
|
|
28
|
+
* accepted shape or a scope outside the enum: a defensive guard, unreachable from the command
|
|
29
|
+
* and the overlay, because a silently dropped value would read back as a default.
|
|
30
|
+
*/
|
|
31
|
+
export function saveStoreJanitorConfig(
|
|
32
|
+
settings: StoreJanitorScopeSettings,
|
|
33
|
+
scope: ConfigScope,
|
|
34
|
+
cwd: string,
|
|
35
|
+
): { path: string } {
|
|
36
|
+
if (!isConfigScope(scope)) {
|
|
37
|
+
throw new DomainError("E_INTERNAL_INVARIANT", {
|
|
38
|
+
message: `Unknown config scope "${String(scope)}".`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (settings.mode !== undefined && !isStoreJanitorMode(settings.mode)) {
|
|
42
|
+
throw new DomainError("E_INTERNAL_INVARIANT", {
|
|
43
|
+
message: `Unknown store janitor mode "${String(settings.mode)}".`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (settings.intervalMs !== undefined && !isIntervalMs(settings.intervalMs)) {
|
|
47
|
+
throw new DomainError("E_INTERNAL_INVARIANT", {
|
|
48
|
+
message: `Unknown store janitor interval "${String(settings.intervalMs)}".`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (settings.batchSize !== undefined && !isBatchSize(settings.batchSize)) {
|
|
52
|
+
throw new DomainError("E_INTERNAL_INVARIANT", {
|
|
53
|
+
message: `Unknown store janitor batch size "${String(settings.batchSize)}".`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const patch: Record<string, unknown> = {};
|
|
58
|
+
if (settings.mode !== undefined) patch[STORE_JANITOR_KEY_NAMES.mode] = settings.mode;
|
|
59
|
+
if (settings.intervalMs !== undefined)
|
|
60
|
+
patch[STORE_JANITOR_KEY_NAMES.intervalMs] = settings.intervalMs;
|
|
61
|
+
if (settings.batchSize !== undefined)
|
|
62
|
+
patch[STORE_JANITOR_KEY_NAMES.batchSize] = settings.batchSize;
|
|
63
|
+
|
|
64
|
+
const path = configFilePath(scope, cwd);
|
|
65
|
+
// WHY: only the keys named in `patch` are ours to set — the rest of `store` and every other
|
|
66
|
+
// WHY: top-level key belong to the user, so all of them survive the spread.
|
|
67
|
+
updateConfigFile(path, (existing) => ({
|
|
68
|
+
...existing,
|
|
69
|
+
store: { ...(isRecord(existing.store) ? existing.store : {}), ...patch },
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
return { path };
|
|
73
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #197 — the STORE JANITOR's user-visible warning: a pure rendering of one `JanitorOutcome`.
|
|
3
|
+
*
|
|
4
|
+
* WHY the trigger lives in the formatter rather than in a caller: "did anything observable happen"
|
|
5
|
+
* is a property of the outcome, and a caller that re-derived it would be a second owner of the same
|
|
6
|
+
* rule (three counts, the compaction, the deep check, the rebuild). Returning `null` here makes the
|
|
7
|
+
* quiet case unrepresentable downstream, so #198's wiring has one decision — render what it is given
|
|
8
|
+
* — and a run that removed nothing can never become a warning by accident.
|
|
9
|
+
*
|
|
10
|
+
* WHY two levels and not three (#199): the operator's question is "does this need my attention".
|
|
11
|
+
* A routine cleanup and a DEFERRED compaction both answer no (`info`); damage and a compaction that
|
|
12
|
+
* FAILED answer yes (`warn`). The outcome already carries that answer, so the wiring never decides a
|
|
13
|
+
* severity and every surface publishes the same level for the same run.
|
|
14
|
+
*
|
|
15
|
+
* WHY nothing here performs I/O: the only dependency is the log's PATH (`diagnosticsLogPath`, a
|
|
16
|
+
* computation over the state root), never the file. A formatter that opened the log would fail on the
|
|
17
|
+
* failure path it exists to describe, and a test could no longer assert the text without a store.
|
|
18
|
+
*
|
|
19
|
+
* WHY one em-dash clause and no emoji: this text is rendered inside Pi's differential renderer, where
|
|
20
|
+
* width changes are expensive and emoji width is not stable. The bracket prefix is the greppable
|
|
21
|
+
* handle (`grep -F '[pi-better-edit store-janitor]'`), so every line carries it — including the
|
|
22
|
+
* multi-line damaged-store shape, whose lines are read out of order in a log.
|
|
23
|
+
*/
|
|
24
|
+
import { diagnosticsLogPath } from "./diagnostics.js";
|
|
25
|
+
import type { JanitorOutcome } from "./store-janitor.js";
|
|
26
|
+
|
|
27
|
+
/** Stable, greppable handle on every line this module emits. Never localize, never shorten. */
|
|
28
|
+
export const JANITOR_WARNING_PREFIX = "[pi-better-edit store-janitor]";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One rendered warning. `info` is a single cleanup sentence; `warn` is a degraded-store shape (damage,
|
|
32
|
+
* or a compaction that failed), whose lines are separate because a reader must be able to quote "your
|
|
33
|
+
* files were not touched" alone.
|
|
34
|
+
*/
|
|
35
|
+
export interface JanitorWarning {
|
|
36
|
+
readonly level: "info" | "warn";
|
|
37
|
+
readonly lines: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Injection seam for the log pointer, so a test never has to resolve a real state root. */
|
|
41
|
+
export interface JanitorWarningOptions {
|
|
42
|
+
readonly logPath?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const WHY = "keeps the local undo/history store small and fast";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The one entry point: `null` when no warning is warranted (the mode or the throttle skipped the run,
|
|
49
|
+
* or the run observed nothing), an `info` cleanup sentence, or the `warn` degraded-store shape.
|
|
50
|
+
*/
|
|
51
|
+
export function formatJanitorWarning(
|
|
52
|
+
outcome: JanitorOutcome,
|
|
53
|
+
options: JanitorWarningOptions = {},
|
|
54
|
+
): JanitorWarning | null {
|
|
55
|
+
if (!outcome.ran) return null;
|
|
56
|
+
if (outcome.deepCheckFailed || outcome.rebuilt) {
|
|
57
|
+
// WHY: damage wins over a failed compaction — a rebuild REPLACED the store, so pointing the
|
|
58
|
+
// WHY: operator at the old file's failed VACUUM would name the wrong event.
|
|
59
|
+
return {
|
|
60
|
+
level: "warn",
|
|
61
|
+
lines: damageLines(outcome.rebuilt, options.logPath ?? diagnosticsLogPath()),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (outcome.compacted.reason === "failed") {
|
|
65
|
+
return {
|
|
66
|
+
level: "warn",
|
|
67
|
+
lines: compactionFailureLines(options.logPath ?? diagnosticsLogPath()),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const sentence = cleanupSentence(outcome);
|
|
71
|
+
return sentence === undefined
|
|
72
|
+
? null
|
|
73
|
+
: { level: "info", lines: [`${JANITOR_WARNING_PREFIX} ${sentence}`] };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* One sentence: the non-zero families, the reclaimed space, then the why-clause. A deferral replaces
|
|
78
|
+
* the why-clause because it has nothing to reclaim yet and one thing to promise instead (the retry).
|
|
79
|
+
*/
|
|
80
|
+
function cleanupSentence(outcome: JanitorOutcome): string | undefined {
|
|
81
|
+
const removed = removedClause(outcome.deleted);
|
|
82
|
+
const reclaimed = reclaimedClause(outcome.compacted);
|
|
83
|
+
if (removed === undefined) {
|
|
84
|
+
if (reclaimed === undefined) return deferredClause(outcome.compacted);
|
|
85
|
+
const joined = outcome.compacted.reclaimedBytes > 0 ? `and ${reclaimed}` : `but ${reclaimed}`;
|
|
86
|
+
return `Compacted the store ${joined} — ${WHY}.`;
|
|
87
|
+
}
|
|
88
|
+
if (reclaimed !== undefined) return `${removed}, ${reclaimed} — ${WHY}.`;
|
|
89
|
+
const deferred = deferredClause(outcome.compacted);
|
|
90
|
+
return deferred === undefined ? `${removed} — ${WHY}.` : `${removed}. ${deferred}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** "Removed 2 snapshots and 1 lineage row" — zero families are absent, never rendered as 0. */
|
|
94
|
+
function removedClause(deleted: JanitorOutcome["deleted"]): string | undefined {
|
|
95
|
+
const phrases = [
|
|
96
|
+
countPhrase(deleted.expiredLeases, "expired session lease"),
|
|
97
|
+
countPhrase(deleted.leases, "stale lease"),
|
|
98
|
+
countPhrase(deleted.snapshots, "snapshot"),
|
|
99
|
+
countPhrase(deleted.lineage, "lineage row"),
|
|
100
|
+
].filter((phrase) => phrase !== undefined);
|
|
101
|
+
if (phrases.length === 0) return undefined;
|
|
102
|
+
const last = phrases[phrases.length - 1];
|
|
103
|
+
const head = phrases.slice(0, -1).join(", ");
|
|
104
|
+
return `Removed ${head === "" ? last : `${head} and ${last}`}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function countPhrase(count: number, noun: string): string | undefined {
|
|
108
|
+
return count > 0 ? `${count} ${count === 1 ? noun : `${noun}s`}` : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** "reclaimed 1.5 MB" only when the VACUUM actually ran; a deferred one says nothing. */
|
|
112
|
+
function reclaimedClause(compacted: JanitorOutcome["compacted"]): string | undefined {
|
|
113
|
+
if (!compacted.ran) return undefined;
|
|
114
|
+
return compacted.reclaimedBytes > 0
|
|
115
|
+
? `reclaimed ${humanizeBytes(compacted.reclaimedBytes)}`
|
|
116
|
+
: "reclaimed no space";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* "Compaction deferred: the store was busy — it retries at the next maintenance pass." WHY a whole
|
|
121
|
+
* sentence and not a clause: it reads correctly both alone and after a removal, and it names its own
|
|
122
|
+
* retry because the deferral is a fact about the NEXT pass, not about this one.
|
|
123
|
+
*/
|
|
124
|
+
function deferredClause(compacted: JanitorOutcome["compacted"]): string | undefined {
|
|
125
|
+
return !compacted.ran && compacted.reason === "busy"
|
|
126
|
+
? "Compaction deferred: the store was busy — it retries at the next maintenance pass."
|
|
127
|
+
: undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The FAILED-compaction shape. WHY it is a warning and carries the log pointer: the reclaim did not
|
|
132
|
+
* happen, so the store keeps those pages until a later pass wins, and the exception that explains it
|
|
133
|
+
* lives in the log. WHY "your files were not touched" still leads: it is the same first question the
|
|
134
|
+
* damaged-store shape answers, and a failed VACUUM has never written to a user's file.
|
|
135
|
+
*/
|
|
136
|
+
function compactionFailureLines(logPath: string): readonly string[] {
|
|
137
|
+
return [
|
|
138
|
+
"The local undo/history store could not be compacted; your files were not touched. " +
|
|
139
|
+
"The next maintenance pass retries.",
|
|
140
|
+
`Details: ${logPath}`,
|
|
141
|
+
].map((line) => `${JANITOR_WARNING_PREFIX} ${line}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** KB below a megabyte, MB above it, one decimal in both: the operator reads a magnitude, not bytes. */
|
|
145
|
+
function humanizeBytes(bytes: number): string {
|
|
146
|
+
return bytes >= 1024 * 1024
|
|
147
|
+
? `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
148
|
+
: `${(bytes / 1024).toFixed(1)} KB`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The damaged/rebuilt shape. WHY separate lines and separate wording: the operator's first question is
|
|
153
|
+
* "did it touch my files", so that answer leads; the second is "is my history gone", which is
|
|
154
|
+
* "may be incomplete", never "lost"; the third is where the evidence is, which is the log.
|
|
155
|
+
*/
|
|
156
|
+
function damageLines(rebuilt: boolean, logPath: string): readonly string[] {
|
|
157
|
+
const lines = rebuilt
|
|
158
|
+
? [
|
|
159
|
+
"The local undo/history store was damaged and has been rebuilt; your files were not touched.",
|
|
160
|
+
"Undo/history recorded before the rebuild may be incomplete.",
|
|
161
|
+
`Details: ${logPath}`,
|
|
162
|
+
]
|
|
163
|
+
: [
|
|
164
|
+
"The local undo/history store looks damaged; your files were not touched.",
|
|
165
|
+
"Undo/history may be incomplete until the store is rebuilt.",
|
|
166
|
+
`Details: ${logPath}`,
|
|
167
|
+
];
|
|
168
|
+
return lines.map((line) => `${JANITOR_WARNING_PREFIX} ${line}`);
|
|
169
|
+
}
|