dsh-hashline-edit 0.2.2 → 0.2.3

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.
Files changed (108) hide show
  1. package/README.md +57 -45
  2. package/cordis.patch.yml +3 -3
  3. package/lib/index.js +3175 -3943
  4. package/lib/types/dsh/contracts.d.ts +12 -3
  5. package/lib/types/dsh/fs.d.ts +3 -0
  6. package/lib/types/dsh/guidance.d.ts +5 -5
  7. package/lib/types/dsh/hide.d.ts +79 -0
  8. package/lib/types/dsh/host-compat.d.ts +7 -78
  9. package/lib/types/dsh/index.d.ts +21 -9
  10. package/lib/types/dsh/metrics/events.d.ts +1 -1
  11. package/lib/types/dsh/metrics/observer.d.ts +1 -1
  12. package/lib/types/dsh/sandbox.d.ts +2 -2
  13. package/lib/types/dsh/shell-hook.d.ts +1 -1
  14. package/lib/types/dsh/tools/apply-patch.d.ts +38 -0
  15. package/lib/types/dsh/tools/read.d.ts +3 -3
  16. package/lib/types/src/apply-patch/commit.d.ts +49 -0
  17. package/lib/types/src/apply-patch/grammar.d.ts +29 -0
  18. package/lib/types/src/apply-patch/lifecycle.d.ts +32 -0
  19. package/lib/types/src/apply-patch/plan.d.ts +50 -0
  20. package/lib/types/src/apply-patch/render.d.ts +51 -0
  21. package/lib/types/src/apply-patch/resolve.d.ts +129 -0
  22. package/lib/types/src/apply-patch/tool.d.ts +54 -0
  23. package/lib/types/src/apply-patch/types.d.ts +362 -0
  24. package/lib/types/src/domain-errors.d.ts +16 -2
  25. package/lib/types/src/edit-undo.d.ts +11 -2
  26. package/lib/types/src/file-content/index.d.ts +1 -1
  27. package/lib/types/src/file-content/preview.d.ts +1 -1
  28. package/lib/types/src/fs-lifecycle.d.ts +44 -0
  29. package/lib/types/src/hashline/alphabet.d.ts +4 -3
  30. package/lib/types/src/hashline/hash-identity.d.ts +12 -7
  31. package/lib/types/src/host-utils.d.ts +63 -0
  32. package/lib/types/src/mutation-engine/edit-request.d.ts +34 -0
  33. package/lib/types/src/mutation-engine/pipeline.d.ts +8 -1
  34. package/lib/types/src/mutation-engine/types.d.ts +40 -50
  35. package/lib/types/src/served-session/serve-render.d.ts +6 -0
  36. package/lib/types/src/undo-store.d.ts +17 -1
  37. package/package.json +18 -21
  38. package/prompts/apply-patch-guidelines.md +10 -0
  39. package/prompts/apply-patch.md +1 -0
  40. package/prompts/read-guidelines.md +4 -4
  41. package/prompts/read.md +1 -1
  42. package/prompts/undo-last-edit-guidelines.md +2 -2
  43. package/prompts/undo-last-edit-snippet.md +1 -1
  44. package/prompts/undo-last-edit.md +1 -1
  45. package/src/apply-patch/commit.ts +190 -0
  46. package/src/apply-patch/grammar.ts +620 -0
  47. package/src/apply-patch/index.ts +17 -0
  48. package/src/apply-patch/lifecycle.ts +195 -0
  49. package/src/apply-patch/plan.ts +800 -0
  50. package/src/apply-patch/render.ts +137 -0
  51. package/src/apply-patch/resolve.ts +470 -0
  52. package/src/apply-patch/tool.ts +257 -0
  53. package/src/apply-patch/types.ts +387 -0
  54. package/src/cut-repair.ts +77 -11
  55. package/src/domain-errors.ts +35 -3
  56. package/src/edit-undo.ts +379 -25
  57. package/src/file-content/index.ts +1 -1
  58. package/src/file-content/preview.ts +1 -1
  59. package/src/file-content/served-header.ts +1 -1
  60. package/src/fs-lifecycle.ts +201 -0
  61. package/src/hash-store.ts +18 -1
  62. package/src/hashline/alphabet.ts +7 -7
  63. package/src/hashline/hash-identity.ts +12 -7
  64. package/src/host-utils.ts +224 -0
  65. package/src/mutation-engine/edit-request.ts +39 -0
  66. package/src/mutation-engine/pipeline.ts +198 -27
  67. package/src/mutation-engine/types.ts +39 -57
  68. package/src/mutation-queue.ts +1 -1
  69. package/src/prompts.ts +3 -30
  70. package/src/served-session/serve-render.ts +2 -2
  71. package/src/undo-store.ts +74 -5
  72. package/lib/types/dsh/tools/edit.d.ts +0 -10
  73. package/lib/types/src/edit-tool.d.ts +0 -41
  74. package/lib/types/src/mutation-engine/engine.d.ts +0 -31
  75. package/lib/types/src/payload-contract.d.ts +0 -135
  76. package/prompts/edit-guidelines.md +0 -10
  77. package/prompts/edit-snippet.md +0 -1
  78. package/prompts/edit.md +0 -1
  79. package/src/bash-classifier.ts +0 -1626
  80. package/src/bash-conversion/decide.ts +0 -812
  81. package/src/bash-display-transform.ts +0 -50
  82. package/src/bash-multifile-config.ts +0 -239
  83. package/src/bash-view-config.ts +0 -158
  84. package/src/edit-normalize.ts +0 -8
  85. package/src/edit-render.ts +0 -161
  86. package/src/edit-tool.ts +0 -118
  87. package/src/edit.ts +0 -136
  88. package/src/integrations/pi-lens/command.ts +0 -712
  89. package/src/integrations/pi-lens/config.ts +0 -148
  90. package/src/integrations/pi-lens/io-bridge-adapter.ts +0 -234
  91. package/src/integrations/pi-lens/io-bridge.ts +0 -112
  92. package/src/integrations/pi-lens/settings-overlay.ts +0 -272
  93. package/src/lifecycle-hooks/index.ts +0 -1992
  94. package/src/lifecycle-hooks/types.ts +0 -64
  95. package/src/mutated-files.ts +0 -75
  96. package/src/mutation-engine/engine.ts +0 -239
  97. package/src/mutation-engine/index.ts +0 -17
  98. package/src/payload-contract.ts +0 -685
  99. package/src/preview-controller.ts +0 -84
  100. package/src/read.ts +0 -865
  101. package/src/search-hook.ts +0 -41
  102. package/src/store-config.ts +0 -73
  103. package/src/store-janitor-warning.ts +0 -169
  104. package/src/store-janitor.ts +0 -539
  105. package/src/store-kernel/settings.ts +0 -233
  106. package/src/store.ts +0 -154
  107. package/src/tui-presenter.ts +0 -134
  108. package/src/write-hook.ts +0 -130
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * WHY an environment variable rather than a settings-file key: `alphabet.ts` is a zero-import leaf
5
5
  * and every shape, regex, bitset and capacity constant below is derived at module load, so the
6
- * width can only be a process-wide, load-time value. Switching it changes every served anchor; the
7
- * persisted store stays safe because the anchor generation (`hash-identity.ts`) folds the width in,
8
- * so a switch is a hard generation miss (re-read), never a cross-width hit.
6
+ * width can only be a process-wide, load-time value. The build bakes it via esbuild `define`
7
+ * (`__ANCHOR_WIDTH__`), so a shipped bundle carries a literal and never reads the environment; the
8
+ * anchor generation (`hash-identity.ts`) folds the width in, so a width switch is a hard generation
9
+ * miss (re-read), never a cross-width hit.
9
10
  */
10
11
  export declare const ANCHOR_WIDTH_ENV_VAR = "PI_BETTER_EDIT_ANCHOR_WIDTH";
11
12
  /**
@@ -61,23 +61,28 @@ export declare const HASH_PROBE_STRIDE: number;
61
61
  */
62
62
  export declare const CANON_VERSION = 3;
63
63
  /**
64
- * The file-scoped anchor-derivation generation (issue #20, ADR-0031; ADR-0020 for the width axis).
64
+ * ANCHOR_GENERATION 1 (issue #20, ADR-0031): the file-scoped anchor-derivation generation.
65
65
  * It covers exactly: (1) the width (`HASH_LEN`, `ALPHA`, `HASH_SPACE`/`USABLE_HASH_SPACE`);
66
66
  * (2) the (path, content) seeding (`fileBaseIndex` seeds xxh32 with the canonical absolute path,
67
67
  * `contentBaseIndex` is the explicit content-only fallback); (3) the all-digit reservation
68
68
  * (`DIGIT_CHARS`/`RESERVED_HASH_SPELLINGS`/`RESERVED_BITS` pre-marked in allocation); (4) the probe
69
69
  * stride (`HASH_PROBE_STRIDE`, coprime with both spaces). Bump it whenever any of those change.
70
70
  *
71
- * WHY it is a function of the width now: the width is a runtime switch (default 4, optional 3), so a
72
- * width change MUST flip the generation or a store written at one width could be read at another.
73
- * Width 4 stays generation 1 — the original persisted generation — so existing stores keep their
74
- * anchors across this change; width 3 gets generation 2, and any future width gets `100 + width`.
75
- *
76
- * WHY: single owner of the anchor-generation value — anchor-bearing artifacts (the undo/snapshot
71
+ * WHY: single owner of the anchor-generation literal — anchor-bearing artifacts (the undo/snapshot
77
72
  * generation gate and the open-time sweep) read this axis only, never `CANON_VERSION`; the snapshot
78
73
  * cache key carries both (`${CANON_VERSION}:${ANCHOR_GENERATION}:${checksum}`), so a canon-only change
79
74
  * misses the cache without invalidating anchor state while a generation change invalidates both.
80
75
  */
76
+ /**
77
+ * The file-scoped anchor-derivation generation. It covers the width (`HASH_LEN`, `ALPHA`,
78
+ * `HASH_SPACE`/`USABLE_HASH_SPACE`), the (path, content) seeding, the all-digit reservation and the
79
+ * probe stride. Bump it whenever any of those change.
80
+ *
81
+ * WHY it is a function of the width: the width is a build-time switch (ADR-0020: default 4, optional
82
+ * 3), so a width change MUST flip the generation or a store written at one width could be read at
83
+ * another. Width 4 stays generation 1 — the original persisted generation — so existing stores keep
84
+ * their anchors; width 3 gets generation 2, and any future width gets `100 + width`.
85
+ */
81
86
  export declare function anchorGenerationFor(width: number): number;
82
87
  export declare const ANCHOR_GENERATION: number;
83
88
  export declare function canon(line: string): string;
@@ -0,0 +1,63 @@
1
+ /** Retained line budget, matching pi's `DEFAULT_MAX_LINES`. */
2
+ export declare const DEFAULT_MAX_LINES = 2000;
3
+ /** Retained byte budget, matching pi's `DEFAULT_MAX_BYTES` (50KB). */
4
+ export declare const DEFAULT_MAX_BYTES: number;
5
+ /** The truncation report the core's read page renders. */
6
+ export interface TruncationResult {
7
+ /** The truncated content. */
8
+ content: string;
9
+ /** Whether truncation occurred. */
10
+ truncated: boolean;
11
+ /** Which limit was hit: `"lines"`, `"bytes"`, or `null` when nothing was cut. */
12
+ truncatedBy: "lines" | "bytes" | null;
13
+ /** Total number of lines in the original content. */
14
+ totalLines: number;
15
+ /** Total number of bytes in the original content. */
16
+ totalBytes: number;
17
+ /** Number of complete lines in the truncated output. */
18
+ outputLines: number;
19
+ /** Number of bytes in the truncated output. */
20
+ outputBytes: number;
21
+ /** Whether the last line was partially truncated (tail truncation only). */
22
+ lastLinePartial: boolean;
23
+ /** Whether the first line exceeded the byte limit (head truncation only). */
24
+ firstLineExceedsLimit: boolean;
25
+ /** The line limit that was applied. */
26
+ maxLines: number;
27
+ /** The byte limit that was applied. */
28
+ maxBytes: number;
29
+ }
30
+ /** The two independent limits a truncation call applies. */
31
+ export interface TruncationOptions {
32
+ /** Maximum number of lines; defaults to {@link DEFAULT_MAX_LINES}. */
33
+ maxLines?: number;
34
+ /** Maximum number of bytes; defaults to {@link DEFAULT_MAX_BYTES}. */
35
+ maxBytes?: number;
36
+ }
37
+ /**
38
+ * Format bytes as a human-readable size (`512B`, `1.5KB`, `2.0MB`).
39
+ * @param bytes - the byte count to render.
40
+ * @returns the pi-compatible size label.
41
+ */
42
+ export declare function formatSize(bytes: number): string;
43
+ /**
44
+ * Truncate content from the head (keep the first N lines / bytes).
45
+ *
46
+ * WHY: the read page's oversize and truncation warnings must agree with the pi
47
+ * implementation the rest of the contract was written against, so this is a
48
+ * faithful port including its two edge rules: complete lines only, and an
49
+ * over-long first line yields empty content with `firstLineExceedsLimit`.
50
+ * @param content - the text to truncate.
51
+ * @param options - the line and byte limits.
52
+ * @returns the truncation report.
53
+ */
54
+ export declare function truncateHead(content: string, options?: TruncationOptions): TruncationResult;
55
+ /**
56
+ * Serialize file mutation operations that target the same file; different files still run
57
+ * in parallel. A faithful port of pi's queue so the core's cut transaction keeps its
58
+ * lock ordering guarantees under the dsh host.
59
+ * @param filePath - the file the mutation targets.
60
+ * @param run - the critical section to run once the path is free.
61
+ * @returns the critical section's result.
62
+ */
63
+ export declare function withFileMutationQueue<T>(filePath: string, run: () => Promise<T>): Promise<T>;
@@ -0,0 +1,34 @@
1
+ /** The internal payload vocabulary the mutation engine consumes; no wire schema lives here. */
2
+ type SpanRef = {
3
+ anchor_from: string;
4
+ anchor_to: string;
5
+ file?: string;
6
+ mode: "copy" | "cut";
7
+ };
8
+ type EditMode = "general" | "literal";
9
+ type Placement = "in-place" | "before" | "after";
10
+ export type DesiredContent = {
11
+ kind: "literal";
12
+ text: string;
13
+ } | {
14
+ kind: "reference";
15
+ span: SpanRef;
16
+ mode: "copy" | "cut";
17
+ } | {
18
+ kind: "empty";
19
+ };
20
+ export type NormalizedEditItem = {
21
+ target: {
22
+ anchor_from: string;
23
+ anchor_to: string;
24
+ };
25
+ at: Placement;
26
+ payload: DesiredContent;
27
+ file?: string;
28
+ };
29
+ export type NormalizedEditRequest = {
30
+ file: string;
31
+ edits: NormalizedEditItem[];
32
+ mode?: EditMode;
33
+ };
34
+ export {};
@@ -1,8 +1,9 @@
1
- import type { NormalizedEditRequest } from "../payload-contract.js";
1
+ import type { NormalizedEditRequest } from "./edit-request.js";
2
2
  import { buildBatchResult } from "../edit-response.js";
3
3
  import type { PipelineOptions, ProcessedEditFile } from "./types.js";
4
4
  export type { PipelineOptions, ProcessedEditFile };
5
5
  export declare function previewEdits(request: NormalizedEditRequest, cwd: string, options?: Omit<PipelineOptions, "noPersist">): Promise<ProcessedEditFile>;
6
+ declare function runCorrelatedTransaction(writePlans: ProcessedEditFile[], sectionPlans: ProcessedEditFile[], options: PipelineOptions | undefined, sessionKey: string): Promise<BatchOutcome>;
6
7
  /** What one plan or committed batch reports: the primary file leads, every section rides the result. */
7
8
  interface BatchOutcome {
8
9
  result: string;
@@ -20,3 +21,9 @@ interface BatchOutcome {
20
21
  export declare function planEditTargets(request: NormalizedEditRequest, cwd: string, options?: PipelineOptions): Promise<ProcessedEditFile[] | undefined>;
21
22
  export declare function apply(request: NormalizedEditRequest, cwd: string, options?: PipelineOptions): Promise<BatchOutcome>;
22
23
  export declare function execEdits(request: NormalizedEditRequest, cwd: string, options?: PipelineOptions): Promise<ProcessedEditFile>;
24
+ /**
25
+ * Visibility-only export of the correlated transaction for apply_patch's commit seam. The
26
+ * function body is the one the cut path already uses; apply_patch members carry an optional
27
+ * lifecycle action read by that body. No engine semantics change.
28
+ */
29
+ export { runCorrelatedTransaction as commitTransaction };
@@ -7,10 +7,9 @@
7
7
  import type { LineEnding } from "../edit-diff.js";
8
8
  import type { ResolvedRange } from "../served-session/serve-render.js";
9
9
  import type { HashStore } from "../hash-store.js";
10
- import type { EditDetails, RMetrics } from "../edit-response.js";
11
- import type { DomainErrorCode, RangeCause } from "../domain-errors.js";
12
- import type { ServedRow } from "../domain-errors.js";
13
10
  export interface PipelineOptions {
11
+ /** Workspace root used to re-assert lifecycle path boundaries at commit time. */
12
+ workspaceRoot?: string;
14
13
  accessMode?: number;
15
14
  signal?: AbortSignal;
16
15
  store?: HashStore;
@@ -32,6 +31,21 @@ export interface PipelineOptions {
32
31
  * window cannot show that.
33
32
  */
34
33
  onBeforeFirstCutWrite?: () => void | Promise<void>;
34
+ /**
35
+ * TEST-ONLY mid-member fault seam: called after a move's rename is durable and before its
36
+ * destination content is rewritten, so the rollback of that in-flight member is exercised.
37
+ */
38
+ onAfterMoveRename?: (toAbsolutePath: string) => void | Promise<void>;
39
+ }
40
+ /**
41
+ * The honest outcome of a failed multi-member commit's rollback attempt. `restored` members are
42
+ * proven back at their pre-images; `remaining` members were not rolled back; `uncertain` members
43
+ * are those whose rollback attempt itself threw, so neither state can be claimed for them.
44
+ */
45
+ export interface TransactionRecoveryReport {
46
+ restored: string[];
47
+ remaining: string[];
48
+ uncertain: string[];
35
49
  }
36
50
  /**
37
51
  * (ticket-04b) One foreign `mode: "cut"` item as seen by the pre-pass: the submitted span anchors
@@ -45,6 +59,26 @@ export interface ForeignCutRecord {
45
59
  spanFrom: string;
46
60
  spanTo: string;
47
61
  }
62
+ /**
63
+ * The file-lifecycle action one transaction member carries.
64
+ *
65
+ * This is the engine-local vocabulary: apply_patch's plan IR maps onto it in
66
+ * `src/apply-patch/commit.ts` so the mutation engine never imports the apply-patch layer.
67
+ * Absent is an ordinary in-place edit.
68
+ */
69
+ export type MemberLifecycle = {
70
+ kind: "keep";
71
+ } | {
72
+ kind: "add";
73
+ createdDirs: readonly string[];
74
+ } | {
75
+ kind: "delete";
76
+ } | {
77
+ kind: "move";
78
+ fromAbsolutePath: string;
79
+ toAbsolutePath: string;
80
+ createdDirs: readonly string[];
81
+ };
48
82
  export interface ProcessedEditFile {
49
83
  path: string;
50
84
  absolutePath: string;
@@ -88,53 +122,9 @@ export interface ProcessedEditFile {
88
122
  * correlated two-file transaction; the in-memory buffer itself never touches these files.
89
123
  */
90
124
  foreignCuts: ForeignCutRecord[];
91
- }
92
- export interface MutationSuccess {
93
- ok: true;
94
- /** SAFETY: Normalized result content (LF). */
95
- result: string;
96
- /** SAFETY: Unified diff (hash-anchored) for model consumption. */
97
- diff: string;
98
- /** SAFETY: User-facing drift notice, if any (details only, not model content). */
99
- drift: string | undefined;
100
- /** SAFETY: Metrics for telemetry. */
101
- metrics: RMetrics;
102
- raw: ProcessedEditFile;
103
- /** SAFETY: Full tool result (content + details) for pi's tool_result hook. */
104
- toolResult: {
105
- content: Array<{
106
- type: "text";
107
- text: string;
108
- }>;
109
- details: EditDetails;
110
- };
111
- }
112
- export interface MutationFailure {
113
- ok: false;
114
- /**
115
- * SAFETY: Machine code — a registry member (`DomainErrorCode`). `toFailure` validates every
116
- * caught code through the envelope reader (`src/error-envelope.ts`): non-registry strings
117
- * (errno pass-through included) route to `E_UNKNOWN`, so the union is honest.
118
- */
119
- code: DomainErrorCode;
120
- /** SAFETY: Human message — model-facing signal when applicable. */
121
- message: string;
122
- /** SAFETY: Fresh served block for retry when available (reject-and-serve). */
123
- servedBlock?: string;
124
- servedRows?: ServedRow[];
125
125
  /**
126
- * SAFETY: the RAW `E_BAD_PAYLOAD` message behind a rendered refusal, when the route is one, so
127
- * SAFETY: the tool seam can name the invoking tool instead of falling back to the neutral
128
- * SAFETY: wording. Present only for `code: "E_BAD_PAYLOAD"`.
126
+ * apply_patch lifecycle action for this member; absent is an ordinary edit. The correlated
127
+ * transaction reads it for pre-image capture, commit ordering, rollback and undo-row shape.
129
128
  */
130
- payloadMessage?: string;
131
- /** SAFETY: User-facing diagnosis, never a model remedy — a GLOSSARY.md glossary term. */
132
- cause?: RangeCause;
133
- details?: {
134
- code: DomainErrorCode;
135
- cause: RangeCause;
136
- };
129
+ lifecycle?: MemberLifecycle;
137
130
  }
138
- export type MutationResult = MutationSuccess | MutationFailure;
139
- export declare function isMutationSuccess(r: MutationResult): r is MutationSuccess;
140
- export declare function isMutationFailure(r: MutationResult): r is MutationFailure;
@@ -14,7 +14,13 @@ export interface FileSnapshotContext {
14
14
  export declare function buildRangeServeRows(startLine: number, endLine: number, fileHashes: string[]): ServedRow[];
15
15
  export declare function denseServeRows(hashes: readonly string[]): ServedRow[];
16
16
  export declare function fmtServedRows(rows: ServedRow[], fileLines: string[]): string;
17
+ export declare function paginationHint(nextOffset: number, more: number): string;
17
18
  export declare function servedPositionsOf(served: (string | null)[], hash: string): number[];
19
+ /** Builds the reject-and-serve served rows + rendered block for a current on-disk range. */
20
+ export declare function buildRangeServeBlock(startLine: number, endLine: number, fileHashes: string[], fileLines: string[]): {
21
+ servedRows: ServedRow[];
22
+ rendered: string;
23
+ };
18
24
  /**
19
25
  * Builds an `[E_TARGET_LOST]` rejection for a retired leased identity whose range cannot be
20
26
  * identified (spec stale-identity-reject-and-serve D1/D6, ADR-0018 decisions 1-2). The payload
@@ -25,7 +25,23 @@ export interface UndoRecord {
25
25
  * `null` is a row from before this column, or an ordinary single-file edit.
26
26
  */
27
27
  rawPre?: string | null;
28
+ /**
29
+ * The lifecycle action this row reverses. Absent/null is an ordinary edit (back-compatible).
30
+ * add = the file did not exist before; delete = the file does not exist after; move = a pure
31
+ * rename with an optional content edit.
32
+ */
33
+ lifecycle?: UndoLifecycle | null;
34
+ /** Move: the absolute path the file had before the transaction. */
35
+ fromPath?: string | null;
36
+ /** Move: the absolute path the file has after the transaction. */
37
+ toPath?: string | null;
38
+ /** The RAW post-transaction text when the commit wrote non-canonical bytes (pure move). */
39
+ rawPost?: string | null;
40
+ /** Add: the directories the add created, outermost first, JSON-encoded in the column. */
41
+ createdDirs?: readonly string[] | null;
28
42
  }
43
+ /** The lifecycle action one undo row reverses. */
44
+ export type UndoLifecycle = "edit" | "add" | "delete" | "move";
29
45
  export type CutIntentDirection = "revert" | null;
30
46
  export interface CutIntent {
31
47
  txnId: string;
@@ -33,7 +49,7 @@ export interface CutIntent {
33
49
  direction: CutIntentDirection;
34
50
  }
35
51
  export interface UndoStmts {
36
- undoUpsert: (path: string, content: string, bom: string, ending: string, hashes: string, resultContent: string, snapshotHash: string | null, transactionId: string | null, rawPre: string | null, anchorGeneration: number, updatedAt: number) => void;
52
+ undoUpsert: (path: string, content: string, bom: string, ending: string, hashes: string, resultContent: string, snapshotHash: string | null, transactionId: string | null, rawPre: string | null, lifecycle: string | null, fromPath: string | null, toPath: string | null, rawPost: string | null, createdDirs: string | null, anchorGeneration: number, updatedAt: number) => void;
37
53
  undoGet: (path: string) => Record<string, unknown> | undefined;
38
54
  undoDelete: (path: string) => void;
39
55
  undoGetTransaction: (transactionId: string) => Record<string, unknown>[];
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "dsh-hashline-edit",
3
- "version": "0.2.2",
4
- "description": "Hash-anchored read/edit/undo_last_edit tools for DeepSeek Harness (dsh). Every line gets a unique 3- or 4-char hash (A-Za-z0-9, configurable) that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
3
+ "version": "0.2.3",
4
+ "description": "Hash-anchored read/apply_patch tools for DeepSeek Harness (dsh). Every line gets a unique 3- or 4-char hash (A-Za-z0-9, configurable) that stays stable across edits; apply_patch is the only file-mutation entry point and commits one patch batch per transaction. Undo persists across restarts.",
5
5
  "keywords": [
6
6
  "anchors",
7
+ "apply-patch",
7
8
  "coding-agent",
8
9
  "deepseek-harness",
9
10
  "dsh",
10
11
  "dsh-plugin",
11
- "edit",
12
12
  "file-editing",
13
13
  "hash",
14
14
  "hash-anchored",
@@ -56,17 +56,8 @@
56
56
  "format:fix": "oxfmt .",
57
57
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.dsh.json",
58
58
  "build": "node scripts/build-dsh.mjs",
59
- "build:pi": "node scripts/build-dist.mjs",
60
59
  "prepublishOnly": "node scripts/build-dsh.mjs && pnpm run typecheck && pnpm run lint && pnpm run test:win",
61
60
  "canon:audit": "node scripts/canon-churn-audit.mjs",
62
- "prompts:show": "node scripts/served-prompts.mjs",
63
- "replay:read-expansion": "node scripts/read-expansion-replay.mjs",
64
- "test:runtime": "node scripts/runtime-edge-test.mjs",
65
- "verify:dist": "node scripts/verify-dist.mjs",
66
- "verify:dist:mutations": "node scripts/verify-dist-mutations.mjs",
67
- "measure:import": "node scripts/measure-import.mjs --max-ratio 0.75",
68
- "smoke:install": "node scripts/install-smoke.mjs",
69
- "prepare": "node scripts/prepare.mjs",
70
61
  "prepack": "node scripts/build-dsh.mjs",
71
62
  "audit": "pnpm audit --audit-level high --registry https://registry.npmjs.org/",
72
63
  "verify": "pnpm run lint && pnpm run format && pnpm run typecheck && pnpm run test:win:coverage"
@@ -74,7 +65,6 @@
74
65
  "dependencies": {
75
66
  "diff": "^8.0.2",
76
67
  "file-type": "^21.3.0",
77
- "typebox": "^1.3.27",
78
68
  "unbash": "5.0.0",
79
69
  "xxhash-wasm": "^1.1.0"
80
70
  },
@@ -82,14 +72,12 @@
82
72
  "@babel/parser": "7.29.8",
83
73
  "@commitlint/cli": "^21.2.2",
84
74
  "@commitlint/config-conventional": "^21.2.2",
85
- "@deepseek-ai/cordis": "^4.0.5",
86
- "@deepseek-ai/dsh-agent": "0.2.1-rc.2",
87
- "@deepseek-ai/dsh-fs": "0.2.1-rc.2",
88
- "@deepseek-ai/dsh-home-paths": "0.2.1-rc.2",
89
- "@deepseek-ai/dsh-sandbox": "0.2.1-rc.2",
90
- "@deepseek-ai/dsh-tools": "0.2.1-rc.2",
91
- "@earendil-works/pi-coding-agent": "^0.84.0",
92
- "@earendil-works/pi-tui": "*",
75
+ "@deepseek-ai/cordis": "4.1.0",
76
+ "@deepseek-ai/dsh-agent": "0.2.2-rc.1",
77
+ "@deepseek-ai/dsh-fs": "0.2.2-rc.1",
78
+ "@deepseek-ai/dsh-home-paths": "0.2.2-rc.1",
79
+ "@deepseek-ai/dsh-sandbox": "0.2.2-rc.1",
80
+ "@deepseek-ai/dsh-tools": "0.2.2-rc.1",
93
81
  "@semantic-release/git": "^11.0.1",
94
82
  "@semantic-release/github": "^12.0.9",
95
83
  "@semantic-release/npm": "^13.1.5",
@@ -105,10 +93,19 @@
105
93
  "vite": "^8.3.5",
106
94
  "vitest": "^4.1.8"
107
95
  },
96
+ "peerDependencies": {
97
+ "@deepseek-ai/cordis": "~4.1.0",
98
+ "@deepseek-ai/dsh-agent": ">=0.2.2-rc.1 <0.2.3-0",
99
+ "@deepseek-ai/dsh-fs": ">=0.2.2-rc.1 <0.2.3-0",
100
+ "@deepseek-ai/dsh-home-paths": ">=0.2.2-rc.1 <0.2.3-0",
101
+ "@deepseek-ai/dsh-sandbox": ">=0.2.2-rc.1 <0.2.3-0",
102
+ "@deepseek-ai/dsh-tools": ">=0.2.2-rc.1 <0.2.3-0"
103
+ },
108
104
  "optionalDependencies": {
109
105
  "web-tree-sitter": "0.27.1"
110
106
  },
111
107
  "engines": {
108
+ "dsh": ">=0.2.2-rc.1 <0.2.3-0",
112
109
  "node": ">=24.0.0"
113
110
  },
114
111
  "packageManager": "pnpm@12.0.0",
@@ -0,0 +1,10 @@
1
+ - apply_patch: one call = one transaction. Collect every related file change (code, tests, docs, config) into one patch; split only when an earlier RESULT must decide a later edit.
2
+ - apply_patch: two addressing modes. Fragment mode (`@@` or `@@ <context>`) locates by exact content and needs no served state. Identity mode (`@@<a>` / `@@<a>-<b>`) targets rows this session served and never writes the old text back.
3
+ - apply_patch: the character after `@@` selects the mode. `@@` followed immediately by exactly {{HASH_LEN}} anchor characters is identity mode; `@@` followed by a space is Codex context mode (`@@ <line>`); a bare `@@` starts the search at the file start. Any other token is a syntax rejection.
4
+ - apply_patch: fragment mode must match exactly once. Zero or several hits reject and serve the candidate rows; add surrounding context to disambiguate, never guess.
5
+ - apply_patch: identity mode accepts only `+` lines; an empty body deletes the span. A `-` or context line in an identity hunk, or body lines in a Copy/Cut hunk, rejects at admission with its patch line number.
6
+ - apply_patch: every hunk resolves against the file as it was before the call; later windows begin after the previous hunk's end. Overlapping target ranges in one file reject the whole batch.
7
+ - apply_patch: lifecycle joins the same transaction and undo. `*** Add File:` content lines all start with `+`; `*** Delete File:` removes the file; `*** Move to:` must immediately follow `*** Update File:` and renames before its hunks apply; an existing move target rejects.
8
+ - apply_patch: a rejection writes nothing. Read the `- patch line N ([E_*], path): ...` lines, then use the immediately following `HASH│content` block — those are the current rows and anchors; retry directly without issuing a separate `read`.
9
+ - apply_patch: a success starts with `Updated N files:` and ends with the changed lines' fresh anchors per file; use those anchors directly in the next identity hunk. `[W_*]` lines are informational.
10
+ - apply_patch: paths are workspace-relative or workspace-absolute; `..`, `.git`, and symlink targets reject. Whole-file replacement is one `*** Update File:` identity hunk spanning every served line with `+` lines for the new content.
@@ -0,0 +1 @@
1
+ Apply one Codex-format patch to one or more files in a single transaction: `{ input: string }`. `input` is the complete patch text: `*** Begin Patch` ... `*** End Patch`, with `*** Add File:`, `*** Delete File:`, `*** Update File:` (plus optional `*** Move to:`) and hunks addressed either by content (`@@` or `@@ <context>` with ` `/`-`/`+` lines) or by served anchors (`@@<a>` / `@@<a>-<b>` with `+` lines only). `*** Copy From:` / `*** Cut From:` copy or move a served span from another file. Every target validates before anything is written; a rejection changes nothing and returns the current rows with anchors. On success the result lists the changed files and the new anchors for the changed lines, so a follow-up call needs no `read`. To replace a whole file, use `*** Update File:` with one identity hunk spanning every served line and `+` lines for the new content.
@@ -1,6 +1,6 @@
1
- - read: call it only for information the tool never served you, such as content past the auto-read diff. A bash view or an auto-read already served the file, so edit from those rows.
2
- - read: when you need several blocks or both boundaries of one replacement, put every range in ONE call. Use `{ file, windows: [{ offset, limit }] }`. Each window is served in that turn, so its anchors work in the same edit. A window can also be `{ around_anchor, radius }` to centre on a line this session already served. An anchor outside this session's served rows is omitted with an inline `[Window warning: ...]`.
1
+ - read: call it only for information the tool never served you, such as content past the auto-read diff. A bash view or an auto-read already served the file, so apply_patch from those rows.
2
+ - read: when you need several blocks or both boundaries of one replacement, put every range in ONE call. Use `{ file, windows: [{ offset, limit }] }`. Each window is served in that turn, so its anchors work in the same apply_patch call. A window can also be `{ around_anchor, radius }` to centre on a line this session already served. An anchor outside this session's served rows is omitted with an inline `[Window warning: ...]`.
3
3
  - read: a requested window of 61 lines or fewer can widen to its enclosing block; overlapping or adjacent windows fold before this test. Caps are 80 lines for code and Markdown, 40 for plain text. Widened rows begin with `[widened <requested> -> <served>, <label>]`; above three requested windows the note is `[widened N windows: <ranges>]`. An over-cap Markdown section clamps and names `(section <extent>)`; over-cap code or plain text refuses with no note. `mode: "verbatim"` widens too, with no anchors.
4
- - read: when you will not edit the file, pass `mode: "verbatim"` for plain content with no anchors. Examples are a config value, docs, a README, or skill content (a SKILL.md or any file in its directory). Omit `mode` when you intend to edit, so the rows carry anchors.
5
- - read: send one file per call as `{ file, windows?, mode? }`, the same way `edit` does. The single top-level `mode` applies to the file. To read several files, make parallel `read` calls.
4
+ - read: when you will not apply_patch the file, pass `mode: "verbatim"` for plain content with no anchors. Examples are a config value, docs, a README, or skill content (a SKILL.md or any file in its directory). Omit `mode` when you intend to use the anchors with apply_patch, so the rows carry anchors.
5
+ - read: send one file per call as `{ file, windows?, mode? }`, the same way `apply_patch` does. The single top-level `mode` applies to the file. To read several files, make parallel `read` calls.
6
6
  - read: PNG/JPG images return as image attachments for multimodal models. Request one with `file` like any other file.
package/prompts/read.md CHANGED
@@ -1 +1 @@
1
- Read one text file via `file`, with optional `windows` and top-level `mode`. To read several files, make parallel `read` calls. Served mode (default) returns each line as HASH│content with a {{HASH_LEN}}-char alphanumeric hash. That hash is the `edit` anchor, so copy only the {{HASH_LEN}} chars before `│`. Pass `mode: "verbatim"` for plain text with no anchors when you will not edit. `mode: "verbatim"` is not capped by line budget. Name ranges with `windows`: `[{ offset, limit }]` (offset is 1-indexed, limit is a line count, maximum 16 windows) or `[{ around_anchor: "<anchor>", radius: 10 }]` for a line this session served. When a page stops short, continue from the next unshown line with `windows: [{ offset, limit }]`. A requested window of 61 lines or fewer can widen to its enclosing block before serving; overlapping or adjacent windows fold before this test. Caps are 80 lines for code and Markdown, 40 for plain text. A widened page begins with `[widened <requested> -> <served>, <label>]`; more than three requested windows collapse to `[widened N windows: <ranges>]`. An over-cap Markdown section clamps to a cap-sized window and names `(section <extent>)`; an over-cap code or plain-text block refuses, serving the requested window. `mode: "verbatim"` widens the same way but adds no anchors. Inline `[...]` notices describe the served result, not file content. Empty files return `HASH│` (edit to insert). PNG/JPG/GIF/WebP return as image attachments for multimodal models. Binary and directory files are rejected. Hashes are file-scoped: an anchor is bound to the file it was served from.
1
+ Read one text file via `file`, with optional `windows` and top-level `mode`. To read several files, make parallel `read` calls. Served mode (default) returns each line as HASH│content with a {{HASH_LEN}}-char alphanumeric hash. That hash is the `apply_patch` identity anchor, so copy only the {{HASH_LEN}} chars before `│`. Pass `mode: "verbatim"` for plain text with no anchors when you will not apply_patch. `mode: "verbatim"` is not capped by line budget. Name ranges with `windows`: `[{ offset, limit }]` (offset is 1-indexed, limit is a line count, maximum 16 windows) or `[{ around_anchor: "<anchor>", radius: 10 }]` for a line this session served. When a page stops short, continue from the next unshown line with `windows: [{ offset, limit }]`. A requested window of 61 lines or fewer can widen to its enclosing block before serving; overlapping or adjacent windows fold before this test. Caps are 80 lines for code and Markdown, 40 for plain text. A widened page begins with `[widened <requested> -> <served>, <label>]`; more than three requested windows collapse to `[widened N windows: <ranges>]`. An over-cap Markdown section clamps to a cap-sized window and names `(section <extent>)`; an over-cap code or plain-text block refuses, serving the requested window. `mode: "verbatim"` widens the same way but adds no anchors. Inline `[...]` notices describe the served result, not file content. Empty files return `HASH│` (use an apply_patch identity hunk to insert). PNG/JPG/GIF/WebP return as image attachments for multimodal models. Binary and directory files are rejected. Hashes are file-scoped: an anchor is bound to the file it was served from.
@@ -1,2 +1,2 @@
1
- - undo_last_edit: reverts the most recent `edit` call on the file at `path`, and every item of its `edits`. Its scope is the edited file. The exception is an edit that was a foreign `cut`: undo then restores the source file too. The file must still hold the edited bytes. An out-of-band write makes the call fail with `E_UNDO_STALE`, and the file stays byte-identical. Call it as soon as a result is bad.
2
- - undo_last_edit: a `write` to the file, or a later `edit`, replaces the undo entry. A read, a bash view or an out-of-band write leaves the entry in place. An edit is bad when its post-edit diff shows `-HASH│` rows for lines you meant to keep. Examples are a closing brace, an import, or a declaration.
1
+ - undo_last_edit: reverts the most recent `apply_patch` batch that touched the file at `path`, and every member of that batch: text hunks plus file lifecycle actions (add/delete/move). The exception is a foreign `cut`: undo then restores the source file too. The file must still hold the edited bytes. An out-of-band write makes the call fail with `E_UNDO_STALE`, and the file stays byte-identical. Call it as soon as a result is bad.
2
+ - undo_last_edit: a `write` to the file, or a later `apply_patch` batch, replaces the undo entry. A read, a bash view or an out-of-band write leaves the entry in place. An apply_patch result is bad when its diff shows `-HASH│` rows for lines you meant to keep. Examples are a closing brace, an import, or a declaration.
@@ -1 +1 @@
1
- Undo the last edit on the file at `path`
1
+ Undo the last `apply_patch` batch on the file at `path`
@@ -1 +1 @@
1
- Undo the last edit on the file at `path`, reverting it to its previous state. Use when an edit produced incorrect results (e.g., wrong content, duplicated lines, broken syntax). The file must be unchanged since that edit.
1
+ Undo the last `apply_patch` batch that touched the file at `path`, reverting the whole batch — every text hunk plus file lifecycle actions (add/delete/move) — to its previous state. Use when an `apply_patch` produced incorrect results (e.g., wrong content, duplicated lines, broken syntax). The file must be unchanged since that batch.