dsh-hashline-edit 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +72 -70
  2. package/cordis.patch.yml +1 -1
  3. package/lib/index.js +4494 -4813
  4. package/lib/types/dsh/contracts.d.ts +69 -6
  5. package/lib/types/dsh/fs.d.ts +38 -3
  6. package/lib/types/dsh/guidance.d.ts +5 -5
  7. package/lib/types/dsh/hide.d.ts +78 -0
  8. package/lib/types/dsh/host-compat.d.ts +7 -78
  9. package/lib/types/dsh/index.d.ts +17 -7
  10. package/lib/types/dsh/metrics/events.d.ts +8 -1
  11. package/lib/types/dsh/metrics/instrument.d.ts +20 -14
  12. package/lib/types/dsh/metrics/observer.d.ts +28 -0
  13. package/lib/types/dsh/sandbox.d.ts +2 -2
  14. package/lib/types/dsh/shell-hook.d.ts +17 -8
  15. package/lib/types/dsh/shell-view.d.ts +65 -3
  16. package/lib/types/dsh/tools/apply-patch.d.ts +38 -0
  17. package/lib/types/dsh/tools/context.d.ts +19 -1
  18. package/lib/types/dsh/tools/output.d.ts +32 -4
  19. package/lib/types/dsh/tools/read.d.ts +12 -15
  20. package/lib/types/dsh/tools/undo.d.ts +5 -0
  21. package/lib/types/dsh/write-hook.d.ts +8 -22
  22. package/lib/types/src/apply-patch/commit.d.ts +49 -0
  23. package/lib/types/src/apply-patch/grammar.d.ts +29 -0
  24. package/lib/types/src/apply-patch/lifecycle.d.ts +32 -0
  25. package/lib/types/src/apply-patch/plan.d.ts +50 -0
  26. package/lib/types/src/apply-patch/render.d.ts +51 -0
  27. package/lib/types/src/apply-patch/resolve.d.ts +129 -0
  28. package/lib/types/src/apply-patch/tool.d.ts +54 -0
  29. package/lib/types/src/apply-patch/types.d.ts +362 -0
  30. package/lib/types/src/bash-view-banner.d.ts +20 -0
  31. package/lib/types/src/domain-errors.d.ts +16 -2
  32. package/lib/types/src/edit-undo.d.ts +11 -2
  33. package/lib/types/src/file-content/detection.d.ts +17 -0
  34. package/lib/types/src/file-content/index.d.ts +2 -2
  35. package/lib/types/src/file-content/preview.d.ts +1 -1
  36. package/lib/types/src/fs-lifecycle.d.ts +44 -0
  37. package/lib/types/src/hashline/resolve.d.ts +6 -0
  38. package/lib/types/src/host-utils.d.ts +63 -0
  39. package/lib/types/src/mutation-engine/batch-span-gate.d.ts +6 -0
  40. package/lib/types/src/mutation-engine/edit-request.d.ts +34 -0
  41. package/lib/types/src/mutation-engine/pipeline.d.ts +18 -3
  42. package/lib/types/src/mutation-engine/types.d.ts +50 -58
  43. package/lib/types/src/read-expansion/grammar-manifest.d.ts +4 -2
  44. package/lib/types/src/read-expansion/java-decoration.d.ts +21 -0
  45. package/lib/types/src/read-expansion/span-expansion.d.ts +6 -2
  46. package/lib/types/src/read-expansion/span-kind.d.ts +3 -2
  47. package/lib/types/src/read-expansion/vue-decoration.d.ts +14 -0
  48. package/lib/types/src/served-session/serve-render.d.ts +9 -3
  49. package/lib/types/src/undo-store.d.ts +17 -1
  50. package/package.json +10 -22
  51. package/prompts/apply-patch-guidelines.md +10 -0
  52. package/prompts/apply-patch.md +1 -0
  53. package/prompts/read-guidelines.md +5 -5
  54. package/prompts/read.md +1 -1
  55. package/prompts/undo-last-edit-guidelines.md +2 -2
  56. package/prompts/undo-last-edit-snippet.md +1 -1
  57. package/prompts/undo-last-edit.md +1 -1
  58. package/src/apply-patch/commit.ts +190 -0
  59. package/src/apply-patch/grammar.ts +620 -0
  60. package/src/apply-patch/index.ts +17 -0
  61. package/src/apply-patch/lifecycle.ts +195 -0
  62. package/src/apply-patch/plan.ts +800 -0
  63. package/src/apply-patch/render.ts +137 -0
  64. package/src/apply-patch/resolve.ts +470 -0
  65. package/src/apply-patch/tool.ts +257 -0
  66. package/src/apply-patch/types.ts +387 -0
  67. package/src/cut-repair.ts +77 -11
  68. package/src/domain-errors.ts +58 -10
  69. package/src/edit-undo.ts +379 -25
  70. package/src/file-content/detection.ts +34 -0
  71. package/src/file-content/index.ts +2 -2
  72. package/src/file-content/preview.ts +1 -1
  73. package/src/file-content/served-header.ts +1 -1
  74. package/src/fs-lifecycle.ts +201 -0
  75. package/src/hash-store.ts +18 -1
  76. package/src/hashline/lease-resolve.ts +14 -8
  77. package/src/hashline/resolve.ts +30 -27
  78. package/src/host-utils.ts +224 -0
  79. package/src/mutation-engine/batch-span-gate.ts +17 -4
  80. package/src/mutation-engine/edit-request.ts +39 -0
  81. package/src/mutation-engine/pipeline.ts +535 -63
  82. package/src/mutation-engine/types.ts +49 -65
  83. package/src/mutation-queue.ts +1 -1
  84. package/src/prompts.ts +0 -28
  85. package/src/read-expansion/ast-provider.ts +4 -0
  86. package/src/read-expansion/grammar-manifest.ts +21 -2
  87. package/src/read-expansion/java-decoration.ts +34 -0
  88. package/src/read-expansion/span-expansion.ts +6 -2
  89. package/src/read-expansion/span-kind.ts +3 -1
  90. package/src/read-expansion/vue-decoration.ts +43 -0
  91. package/src/served-session/serve-render.ts +5 -5
  92. package/src/undo-store.ts +74 -5
  93. package/lib/types/dsh/encoding-bridge.d.ts +0 -25
  94. package/lib/types/dsh/encoding.d.ts +0 -144
  95. package/lib/types/dsh/tools/edit.d.ts +0 -10
  96. package/lib/types/src/edit-tool.d.ts +0 -41
  97. package/lib/types/src/mutation-engine/engine.d.ts +0 -31
  98. package/lib/types/src/payload-contract.d.ts +0 -124
  99. package/prompts/edit-guidelines.md +0 -10
  100. package/prompts/edit-snippet.md +0 -1
  101. package/prompts/edit.md +0 -1
  102. package/src/bash-classifier.ts +0 -1626
  103. package/src/bash-conversion/decide.ts +0 -812
  104. package/src/bash-display-transform.ts +0 -50
  105. package/src/bash-multifile-config.ts +0 -239
  106. package/src/bash-view-config.ts +0 -158
  107. package/src/edit-normalize.ts +0 -8
  108. package/src/edit-render.ts +0 -161
  109. package/src/edit-tool.ts +0 -116
  110. package/src/edit.ts +0 -136
  111. package/src/integrations/pi-lens/command.ts +0 -712
  112. package/src/integrations/pi-lens/config.ts +0 -148
  113. package/src/integrations/pi-lens/io-bridge-adapter.ts +0 -234
  114. package/src/integrations/pi-lens/io-bridge.ts +0 -112
  115. package/src/integrations/pi-lens/settings-overlay.ts +0 -272
  116. package/src/lifecycle-hooks/index.ts +0 -1992
  117. package/src/lifecycle-hooks/types.ts +0 -64
  118. package/src/mutated-files.ts +0 -75
  119. package/src/mutation-engine/engine.ts +0 -173
  120. package/src/mutation-engine/index.ts +0 -17
  121. package/src/payload-contract.ts +0 -637
  122. package/src/preview-controller.ts +0 -84
  123. package/src/read.ts +0 -865
  124. package/src/search-hook.ts +0 -41
  125. package/src/store-config.ts +0 -73
  126. package/src/store-janitor-warning.ts +0 -169
  127. package/src/store-janitor.ts +0 -539
  128. package/src/store-kernel/settings.ts +0 -233
  129. package/src/store.ts +0 -154
  130. package/src/tui-presenter.ts +0 -134
  131. package/src/write-hook.ts +0 -130
@@ -8,32 +8,49 @@
8
8
  import type { LineEnding } from "../edit-diff.js";
9
9
  import type { ResolvedRange } from "../served-session/serve-render.js";
10
10
  import type { HashStore } from "../hash-store.js";
11
- import type { EditDetails, RMetrics } from "../edit-response.js";
12
- import type { DomainErrorCode, RangeCause } from "../domain-errors.js";
13
- import type { ServedRow } from "../domain-errors.js";
14
11
 
15
12
  // WHY: Re-export pipeline-facing options — validated once at admission (edit.ts),
16
13
  // WHY: trusted inside the engine. No `any`.
17
14
  export interface PipelineOptions {
15
+ /** Workspace root used to re-assert lifecycle path boundaries at commit time. */
16
+ workspaceRoot?: string;
18
17
  accessMode?: number;
19
18
  signal?: AbortSignal;
20
19
  store?: HashStore;
21
20
  noPersist?: boolean;
22
21
  sessionKey?: string;
23
22
  /**
24
- * TEST-ONLY fault-injection seam (ticket-04b §3): invoked inside a foreign-source cut transaction
25
- * AFTER the target insert is durably committed and BEFORE the source retirement rename. A
26
- * throw here lands inside the two-rename window, which is exactly the half-applied state the
27
- * intent record and next-run repair must resolve.
23
+ * TEST-ONLY fault-injection seam (ticket-04b §3): invoked inside a correlated write transaction
24
+ * AFTER the first file's rename is durably committed and BEFORE the next file's rename. A throw
25
+ * here lands inside the rename window — the cut path's target-then-sources order, or a multi-file
26
+ * edit's plan order — which is exactly the half-applied state the intent record and next-run
27
+ * repair must resolve.
28
28
  */
29
29
  onCutBetweenWrites?: (committedAbsolutePath: string) => void | Promise<void>;
30
30
  /**
31
- * TEST-ONLY observation seam (ticket-04b remediation-2 R2): invoked inside a foreign-source cut
32
- * transaction BEFORE the first rename, while no byte of the transaction has landed. The rule
33
- * under test is write-AHEAD: the intent record must already be durable at the moment of the
34
- * first mutation — an existence check taken inside the window cannot show that.
31
+ * TEST-ONLY observation seam (ticket-04b remediation-2 R2): invoked inside a correlated write
32
+ * transaction (a foreign-source cut, or a multi-file edit) BEFORE the first rename, while no byte
33
+ * of the transaction has landed. The rule under test is write-AHEAD: the intent record must
34
+ * already be durable at the moment of the first mutation — an existence check taken inside the
35
+ * window cannot show that.
35
36
  */
36
37
  onBeforeFirstCutWrite?: () => void | Promise<void>;
38
+ /**
39
+ * TEST-ONLY mid-member fault seam: called after a move's rename is durable and before its
40
+ * destination content is rewritten, so the rollback of that in-flight member is exercised.
41
+ */
42
+ onAfterMoveRename?: (toAbsolutePath: string) => void | Promise<void>;
43
+ }
44
+
45
+ /**
46
+ * The honest outcome of a failed multi-member commit's rollback attempt. `restored` members are
47
+ * proven back at their pre-images; `remaining` members were not rolled back; `uncertain` members
48
+ * are those whose rollback attempt itself threw, so neither state can be claimed for them.
49
+ */
50
+ export interface TransactionRecoveryReport {
51
+ restored: string[];
52
+ remaining: string[];
53
+ uncertain: string[];
37
54
  }
38
55
 
39
56
  /**
@@ -49,6 +66,24 @@ export interface ForeignCutRecord {
49
66
  spanTo: string;
50
67
  }
51
68
 
69
+ /**
70
+ * The file-lifecycle action one transaction member carries.
71
+ *
72
+ * This is the engine-local vocabulary: apply_patch's plan IR maps onto it in
73
+ * `src/apply-patch/commit.ts` so the mutation engine never imports the apply-patch layer.
74
+ * Absent is an ordinary in-place edit.
75
+ */
76
+ export type MemberLifecycle =
77
+ | { kind: "keep" }
78
+ | { kind: "add"; createdDirs: readonly string[] }
79
+ | { kind: "delete" }
80
+ | {
81
+ kind: "move";
82
+ fromAbsolutePath: string;
83
+ toAbsolutePath: string;
84
+ createdDirs: readonly string[];
85
+ };
86
+
52
87
  // WHY: Internal: the engine's view of one file's mutation outcome.
53
88
  // WHY: Mirrors `ProcessedEditFile` from the old pipeline — kept here as the engine's owned fact.
54
89
  export interface ProcessedEditFile {
@@ -94,60 +129,9 @@ export interface ProcessedEditFile {
94
129
  * correlated two-file transaction; the in-memory buffer itself never touches these files.
95
130
  */
96
131
  foreignCuts: ForeignCutRecord[];
97
- }
98
-
99
- // WHY: Discriminated success/failure for the deep seam.
100
- // WHY: Callers use exhaustive switch on `ok` — no `isError` flag checks,
101
- // WHY: no `any` threading.
102
- export interface MutationSuccess {
103
- ok: true;
104
- /** SAFETY: Normalized result content (LF). */
105
- result: string;
106
- /** SAFETY: Unified diff (hash-anchored) for model consumption. */
107
- diff: string;
108
- /** SAFETY: User-facing drift notice, if any (details only, not model content). */
109
- drift: string | undefined;
110
- /** SAFETY: Metrics for telemetry. */
111
- metrics: RMetrics;
112
- raw: ProcessedEditFile;
113
- /** SAFETY: Full tool result (content + details) for pi's tool_result hook. */
114
- toolResult: {
115
- content: Array<{ type: "text"; text: string }>;
116
- details: EditDetails;
117
- };
118
- }
119
-
120
- export interface MutationFailure {
121
- ok: false;
122
- /**
123
- * SAFETY: Machine code — a registry member (`DomainErrorCode`). `toFailure` validates every
124
- * caught code through the envelope reader (`src/error-envelope.ts`): non-registry strings
125
- * (errno pass-through included) route to `E_UNKNOWN`, so the union is honest.
126
- */
127
- code: DomainErrorCode;
128
- /** SAFETY: Human message — model-facing signal when applicable. */
129
- message: string;
130
- /** SAFETY: Fresh served block for retry when available (reject-and-serve). */
131
- servedBlock?: string;
132
- servedRows?: ServedRow[];
133
132
  /**
134
- * SAFETY: the RAW `E_BAD_PAYLOAD` message behind a rendered refusal, when the route is one, so
135
- * SAFETY: the tool seam can name the invoking tool instead of falling back to the neutral
136
- * SAFETY: wording. Present only for `code: "E_BAD_PAYLOAD"`.
133
+ * apply_patch lifecycle action for this member; absent is an ordinary edit. The correlated
134
+ * transaction reads it for pre-image capture, commit ordering, rollback and undo-row shape.
137
135
  */
138
- payloadMessage?: string;
139
- /** SAFETY: User-facing diagnosis, never a model remedy — a GLOSSARY.md glossary term. */
140
- cause?: RangeCause;
141
- details?: { code: DomainErrorCode; cause: RangeCause };
142
- }
143
-
144
- export type MutationResult = MutationSuccess | MutationFailure;
145
-
146
- // WHY: Narrowing helpers — keep call sites exhaustive.
147
- export function isMutationSuccess(r: MutationResult): r is MutationSuccess {
148
- return r.ok === true;
149
- }
150
-
151
- export function isMutationFailure(r: MutationResult): r is MutationFailure {
152
- return r.ok === false;
136
+ lifecycle?: MemberLifecycle;
153
137
  }
@@ -1,4 +1,4 @@
1
- import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
1
+ import { withFileMutationQueue } from "./host-utils.js";
2
2
 
3
3
  /**
4
4
  * Enter pi's per-path mutation queues for SEVERAL paths in one deterministic, sorted nesting.
package/src/prompts.ts CHANGED
@@ -1,25 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { EDIT_DESCRIPTION, EDIT_SNIPPET, EDIT_GUIDELINES } from "./payload-contract.js";
3
-
4
- const EDIT_PROMPT_FILES = new Set([
5
- "../prompts/edit.md",
6
- "prompts/edit.md",
7
- "../prompts/edit-snippet.md",
8
- "prompts/edit-snippet.md",
9
- ]);
10
-
11
- const EDIT_GUIDE_FILES = new Set(["../prompts/edit-guidelines.md", "prompts/edit-guidelines.md"]);
12
2
 
13
3
  export function loadP(relativePath: string, replacements?: Record<string, string>): string {
14
- if (EDIT_PROMPT_FILES.has(relativePath)) {
15
- let content = relativePath.includes("snippet") ? EDIT_SNIPPET : EDIT_DESCRIPTION;
16
- if (replacements) {
17
- for (const [key, value] of Object.entries(replacements)) {
18
- content = content.split(`{{${key}}}`).join(value);
19
- }
20
- }
21
- return content;
22
- }
23
4
  let content = readFileSync(new URL(relativePath, import.meta.url), "utf-8").trim();
24
5
  if (replacements) {
25
6
  for (const [key, value] of Object.entries(replacements)) {
@@ -30,15 +11,6 @@ export function loadP(relativePath: string, replacements?: Record<string, string
30
11
  }
31
12
 
32
13
  export function loadGuide(relativePath: string, replacements?: Record<string, string>): string[] {
33
- if (EDIT_GUIDE_FILES.has(relativePath)) {
34
- let guidelines = [...EDIT_GUIDELINES];
35
- if (replacements) {
36
- for (const [key, value] of Object.entries(replacements)) {
37
- guidelines = guidelines.map((line) => line.split(`{{${key}}}`).join(value));
38
- }
39
- }
40
- return guidelines;
41
- }
42
14
  let content = readFileSync(new URL(relativePath, import.meta.url), "utf-8");
43
15
  if (replacements) {
44
16
  for (const [key, value] of Object.entries(replacements)) {
@@ -24,6 +24,8 @@ import { TYPESCRIPT_DECORATION } from "./typescript-decoration.js";
24
24
  import { PYTHON_DECORATION } from "./python-decoration.js";
25
25
  import { GO_DECORATION } from "./go-decoration.js";
26
26
  import { RUST_DECORATION } from "./rust-decoration.js";
27
+ import { JAVA_DECORATION } from "./java-decoration.js";
28
+ import { VUE_DECORATION } from "./vue-decoration.js";
27
29
 
28
30
  interface LanguageScope {
29
31
  readonly decorate: ScopeDecorator;
@@ -45,6 +47,8 @@ export const LANGUAGES: Readonly<Record<string, LanguageScope>> = {
45
47
  },
46
48
  go: { decorate: createScopeDecorator(GO_DECORATION), kinds: GO_DECORATION.kinds },
47
49
  rust: { decorate: createScopeDecorator(RUST_DECORATION), kinds: RUST_DECORATION.kinds },
50
+ java: { decorate: createScopeDecorator(JAVA_DECORATION), kinds: JAVA_DECORATION.kinds },
51
+ vue: { decorate: createScopeDecorator(VUE_DECORATION), kinds: VUE_DECORATION.kinds },
48
52
  };
49
53
 
50
54
  export interface AstSpanProviderOptions extends GrammarCacheOptions {
@@ -19,8 +19,10 @@ export interface GrammarManifestEntry {
19
19
  export type GrammarManifest = Readonly<Record<string, GrammarManifestEntry>>;
20
20
 
21
21
  /**
22
- * TypeScript/TSX grammars from the pinned tree-sitter-typescript release. Both are fetched from a
23
- * versioned URL and verified against the committed digest before they reach the parser.
22
+ * Pinned grammars: TypeScript/TSX from the tree-sitter-typescript release, Python/Go/Rust from their
23
+ * own releases, Java from the tree-sitter-java release and Vue from the tree-sitter-vue-wasm npm
24
+ * package. Every entry is fetched from a versioned URL and verified against the committed digest
25
+ * before it reaches the parser.
24
26
  */
25
27
  export const GRAMMAR_MANIFEST: GrammarManifest = {
26
28
  typescript: {
@@ -53,6 +55,18 @@ export const GRAMMAR_MANIFEST: GrammarManifest = {
53
55
  url: "https://github.com/tree-sitter/tree-sitter-rust/releases/download/v0.24.2/tree-sitter-rust.wasm",
54
56
  sha256: "24c89bd9252255e4aebbcbd7d2d308bd92c86dd95a130fdc80efa49577b8d738",
55
57
  },
58
+ java: {
59
+ language: "java",
60
+ version: "0.23.5",
61
+ url: "https://github.com/tree-sitter/tree-sitter-java/releases/download/v0.23.5/tree-sitter-java.wasm",
62
+ sha256: "4fdeac4ca6ca089f06c6f7e562abcac1733cd465728cc7031ebb73c2019122c4",
63
+ },
64
+ vue: {
65
+ language: "vue",
66
+ version: "0.1.0",
67
+ url: "https://unpkg.com/tree-sitter-vue-wasm@0.1.0/tree-sitter-vue.wasm",
68
+ sha256: "193c01119229352f1a772952e590b2ad59a75ee21eb7244433abdafdaacaea71",
69
+ },
56
70
  };
57
71
 
58
72
  const LANGUAGE_BY_EXTENSION = new Map([
@@ -63,6 +77,11 @@ const LANGUAGE_BY_EXTENSION = new Map([
63
77
  [".py", "python"],
64
78
  [".go", "go"],
65
79
  [".rs", "rust"],
80
+ [".java", "java"],
81
+ [".vue", "vue"],
82
+ // WHY: no .sh/.bash here — bash-provider.ts already serves shell functions through the existing
83
+ // WHY: `unbash` dependency (ADR-0008 decision 10); mapping them here would prefetch a second,
84
+ // WHY: unused parser and contradict the no-new-download decision.
66
85
  ]);
67
86
 
68
87
  /** The manifest entry for a source path, or undefined when no grammar is supported yet. */
@@ -0,0 +1,34 @@
1
+ /** Java vocabulary for the shared scope-decoration walk. */
2
+ import type { DecorationSpec } from "./scope-decoration.js";
3
+ import type { SpanKindTable } from "./span-kind.js";
4
+
5
+ /**
6
+ * Java declaration nodes and their note kinds. A record is a class-like declaration, so it shares
7
+ * the `class` label; a constructor is its own protocol kind and is never relabelled as a method.
8
+ */
9
+ export const JAVA_SPAN_KINDS = {
10
+ class_declaration: "class",
11
+ record_declaration: "class",
12
+ interface_declaration: "interface",
13
+ enum_declaration: "enum",
14
+ method_declaration: "method",
15
+ constructor_declaration: "constructor",
16
+ compact_constructor_declaration: "constructor",
17
+ } as const satisfies SpanKindTable;
18
+
19
+ /**
20
+ * A Java declaration carries its modifiers (annotations included) inside its own node, so the
21
+ * span already starts at the first annotation. Only a directly preceding JavaDoc block needs the
22
+ * leading-sibling walk; ordinary `//` and `/*` comments are not documentation and stay outside.
23
+ */
24
+ export const JAVA_DECORATION: DecorationSpec = {
25
+ kinds: JAVA_SPAN_KINDS,
26
+ docCommentTypes: new Set(["block_comment"]),
27
+ isDocComment: (node) => node.text?.trimStart().startsWith("/**") ?? false,
28
+ wrapperTypes: new Set(),
29
+ assignmentNodeTypes: new Set(),
30
+ assignmentWrapperTypes: new Set(),
31
+ transparentTypes: new Set(),
32
+ decorationTypes: new Set(),
33
+ nameField: "name",
34
+ };
@@ -62,9 +62,13 @@ export interface SpanProvider {
62
62
 
63
63
  /**
64
64
  * Only a folded requested span at or below this size triggers expansion. Folding happens first, so
65
- * sixteen adjacent 24-line windows are one 384-line span and refuse the trigger.
65
+ * sixteen adjacent windows are one span and refuse the trigger.
66
+ *
67
+ * WHY 61: measured against the working corpus — 25-40 line reads occurred 66 times, 41-60 38 times,
68
+ * 61-100 38 times and >200 once, with a median refused read of 61 lines. At the previous 24-line
69
+ * trigger expansion engaged on only 9.6% of reads; the cap, not this trigger, still bounds the cost.
66
70
  */
67
- export const TRIGGER_MAX = 24;
71
+ export const TRIGGER_MAX = 61;
68
72
 
69
73
  /** Code scopes may widen to a function or method; this ceiling is refused, never clamped. */
70
74
  export const SPAN_CAP_CODE = 80;
@@ -5,7 +5,8 @@
5
5
  * `SPAN_KIND_TO_LSP` carries the canonical PascalCase name next to it. Six extensions cover
6
6
  * concepts the protocol cannot express honestly: `arrow` (an anonymous function assigned to a
7
7
  * name), `typeAlias`, `impl` and `trait` (Rust items; mapping them to `Object` would be a lie),
8
- * and the non-AST prose units `section` and `paragraph`.
8
+ * and the non-symbol document units `section` (Markdown sections and Vue SFC blocks) and
9
+ * `paragraph` (plain-text paragraphs).
9
10
  *
10
11
  * Display stays lowercase because the note is model-facing prose, not a protocol symbol. The two
11
12
  * camel-case members `enumMember` and `typeParameter` are rendered as `enum member` and
@@ -187,6 +188,7 @@ export const PRODUCED_KINDS = [
187
188
  "typeAlias",
188
189
  "section",
189
190
  "paragraph",
191
+ "constructor",
190
192
  ] as const satisfies readonly SpanKind[];
191
193
 
192
194
  /** One language's grammar-node-type to note-kind data table. */
@@ -0,0 +1,43 @@
1
+ /** Vue single-file-component vocabulary for the shared scope-decoration walk. */
2
+ import type { SyntaxNodeLike } from "./ast-span.js";
3
+ import type { DecorationSpec } from "./scope-decoration.js";
4
+ import type { SpanKindTable } from "./span-kind.js";
5
+
6
+ /**
7
+ * A `.vue` file is one component made of top-level document sections. The note vocabulary has no
8
+ * template/style symbol and `component` is the file root (never a widening target), so the three
9
+ * SFC block nodes share the existing `section` extension and are distinguished by their names.
10
+ * Embedded JavaScript/TypeScript is raw text in this grammar, so a script section is never parsed
11
+ * or widened beyond its own block.
12
+ */
13
+ export const VUE_SPAN_KINDS = {
14
+ template_element: "section",
15
+ script_element: "section",
16
+ style_element: "section",
17
+ } as const satisfies SpanKindTable;
18
+
19
+ /** The block name a reader sees after the `section` label: template, script or style. */
20
+ function vueSectionName(node: SyntaxNodeLike): string | undefined {
21
+ switch (node.type) {
22
+ case "template_element":
23
+ return "template";
24
+ case "script_element":
25
+ return "script";
26
+ case "style_element":
27
+ return "style";
28
+ default:
29
+ return undefined;
30
+ }
31
+ }
32
+
33
+ export const VUE_DECORATION: DecorationSpec = {
34
+ kinds: VUE_SPAN_KINDS,
35
+ docCommentTypes: new Set(),
36
+ wrapperTypes: new Set(),
37
+ assignmentNodeTypes: new Set(),
38
+ assignmentWrapperTypes: new Set(),
39
+ transparentTypes: new Set(),
40
+ decorationTypes: new Set(),
41
+ nameField: "name",
42
+ nameOf: vueSectionName,
43
+ };
@@ -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
- function paginationHint(nextOffset: number, more: number): string {
93
+ export 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
- function buildRangeServeBlock(
110
+ export function buildRangeServeBlock(
111
111
  startLine: number,
112
112
  endLine: number,
113
113
  fileHashes: string[],
@@ -221,9 +221,9 @@ export function makeServedRejection(opts: {
221
221
 
222
222
  /**
223
223
  * Builds the `[E_STALE_ANCHOR]` reject-and-serve rejection for a boundary anchor the session holds
224
- * no lease for. It serves the rows under `Current range:` with the retry hint (spec §5.3): the
225
- * served rows are themselves serves, so the retry needs no `read`. Distinct from the range-family
226
- * codes, which serve a fresh read with no mandate (issue #149).
224
+ * no lease for. It serves the rows under `Current range:` as evidence with no retry mandate: the
225
+ * served rows are themselves serves, so deciding from them needs no `read` (same fresh-read rule as
226
+ * the range-family codes, issue #149). The registry renders the block at most once.
227
227
  */
228
228
  export function makeStaleAnchorRejection(opts: {
229
229
  headline: string;
package/src/undo-store.ts CHANGED
@@ -29,8 +29,25 @@ export interface UndoRecord {
29
29
  * `null` is a row from before this column, or an ordinary single-file edit.
30
30
  */
31
31
  rawPre?: string | null;
32
+ /**
33
+ * The lifecycle action this row reverses. Absent/null is an ordinary edit (back-compatible).
34
+ * add = the file did not exist before; delete = the file does not exist after; move = a pure
35
+ * rename with an optional content edit.
36
+ */
37
+ lifecycle?: UndoLifecycle | null;
38
+ /** Move: the absolute path the file had before the transaction. */
39
+ fromPath?: string | null;
40
+ /** Move: the absolute path the file has after the transaction. */
41
+ toPath?: string | null;
42
+ /** The RAW post-transaction text when the commit wrote non-canonical bytes (pure move). */
43
+ rawPost?: string | null;
44
+ /** Add: the directories the add created, outermost first, JSON-encoded in the column. */
45
+ createdDirs?: readonly string[] | null;
32
46
  }
33
47
 
48
+ /** The lifecycle action one undo row reverses. */
49
+ export type UndoLifecycle = "edit" | "add" | "delete" | "move";
50
+
34
51
  export type CutIntentDirection = "revert" | null;
35
52
 
36
53
  export interface CutIntent {
@@ -50,6 +67,11 @@ export interface UndoStmts {
50
67
  snapshotHash: string | null,
51
68
  transactionId: string | null,
52
69
  rawPre: string | null,
70
+ lifecycle: string | null,
71
+ fromPath: string | null,
72
+ toPath: string | null,
73
+ rawPost: string | null,
74
+ createdDirs: string | null,
53
75
  anchorGeneration: number,
54
76
  updatedAt: number,
55
77
  ) => void;
@@ -75,15 +97,15 @@ export function undoStmts(db: DatabaseSync): UndoStmts {
75
97
 
76
98
  function buildStmts(db: DatabaseSync): UndoStmts {
77
99
  const undoUpsertStmt = db.prepare(
78
- "INSERT INTO file_undo (path, content, bom, ending, hashes, result_content, snapshot_hash, transaction_id, raw_pre, anchor_generation, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " +
79
- "ON CONFLICT(path) DO UPDATE SET content = excluded.content, bom = excluded.bom, ending = excluded.ending, hashes = excluded.hashes, result_content = excluded.result_content, snapshot_hash = excluded.snapshot_hash, transaction_id = excluded.transaction_id, raw_pre = excluded.raw_pre, anchor_generation = excluded.anchor_generation, updated_at = excluded.updated_at",
100
+ "INSERT INTO file_undo (path, content, bom, ending, hashes, result_content, snapshot_hash, transaction_id, raw_pre, lifecycle, from_path, to_path, raw_post, created_dirs, anchor_generation, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " +
101
+ "ON CONFLICT(path) DO UPDATE SET content = excluded.content, bom = excluded.bom, ending = excluded.ending, hashes = excluded.hashes, result_content = excluded.result_content, snapshot_hash = excluded.snapshot_hash, transaction_id = excluded.transaction_id, raw_pre = excluded.raw_pre, lifecycle = excluded.lifecycle, from_path = excluded.from_path, to_path = excluded.to_path, raw_post = excluded.raw_post, created_dirs = excluded.created_dirs, anchor_generation = excluded.anchor_generation, updated_at = excluded.updated_at",
80
102
  );
81
103
  const undoGetStmt = db.prepare(
82
- "SELECT content, bom, ending, hashes, result_content, snapshot_hash, transaction_id, raw_pre, anchor_generation FROM file_undo WHERE path = ?",
104
+ "SELECT content, bom, ending, hashes, result_content, snapshot_hash, transaction_id, raw_pre, lifecycle, from_path, to_path, raw_post, created_dirs, anchor_generation FROM file_undo WHERE path = ?",
83
105
  );
84
106
  const undoDelStmt = db.prepare("DELETE FROM file_undo WHERE path = ?");
85
107
  const undoGetTransactionStmt = db.prepare(
86
- "SELECT path, content, bom, ending, hashes, result_content, snapshot_hash, transaction_id, raw_pre, anchor_generation FROM file_undo WHERE transaction_id = ? ORDER BY path",
108
+ "SELECT path, content, bom, ending, hashes, result_content, snapshot_hash, transaction_id, raw_pre, lifecycle, from_path, to_path, raw_post, created_dirs, anchor_generation FROM file_undo WHERE transaction_id = ? ORDER BY path",
87
109
  );
88
110
  const undoDelTransactionStmt = db.prepare("DELETE FROM file_undo WHERE transaction_id = ?");
89
111
  const intentUpsertStmt = db.prepare(
@@ -105,6 +127,11 @@ function buildStmts(db: DatabaseSync): UndoStmts {
105
127
  snapshotHash,
106
128
  transactionId,
107
129
  rawPre,
130
+ lifecycle,
131
+ fromPath,
132
+ toPath,
133
+ rawPost,
134
+ createdDirs,
108
135
  anchorGeneration,
109
136
  updatedAt,
110
137
  ) => {
@@ -119,6 +146,11 @@ function buildStmts(db: DatabaseSync): UndoStmts {
119
146
  snapshotHash,
120
147
  transactionId,
121
148
  rawPre,
149
+ lifecycle,
150
+ fromPath,
151
+ toPath,
152
+ rawPost,
153
+ createdDirs,
122
154
  anchorGeneration,
123
155
  updatedAt,
124
156
  );
@@ -164,12 +196,25 @@ onStoreOpen((db) => {
164
196
  ensureFileUndoSchema(db);
165
197
  });
166
198
 
199
+ /** Parse the JSON-encoded created-directory list; a corrupt value degrades to an empty list. */
200
+ function parseCreatedDirs(value: unknown): string[] | null {
201
+ if (typeof value !== "string" || value.length === 0) return null;
202
+ try {
203
+ const parsed: unknown = JSON.parse(value);
204
+ return Array.isArray(parsed) && parsed.every((entry) => typeof entry === "string")
205
+ ? (parsed as string[])
206
+ : null;
207
+ } catch {
208
+ return null;
209
+ }
210
+ }
211
+
167
212
  function parseUndoRow(row: Record<string, unknown> | undefined): UndoRecord | undefined {
168
213
  if (!row) return undefined;
169
214
  try {
170
215
  const parsed = JSON.parse(row.hashes as string);
171
216
  if (!isValidHashList(parsed)) return undefined;
172
- return {
217
+ const record: UndoRecord = {
173
218
  content: row.content as string,
174
219
  bom: row.bom as string,
175
220
  ending: row.ending as string,
@@ -180,6 +225,23 @@ function parseUndoRow(row: Record<string, unknown> | undefined): UndoRecord | un
180
225
  rawPre: (row.raw_pre as string | null) ?? null,
181
226
  anchorGeneration: (row.anchor_generation as number | null) ?? 0,
182
227
  };
228
+ // WHY: the lifecycle fields are additive; an ordinary row omits them entirely so its
229
+ // WHY: pre-extension shape stays byte-for-byte what existing consumers assert.
230
+ if (row.lifecycle !== null && row.lifecycle !== undefined) {
231
+ record.lifecycle = row.lifecycle as UndoLifecycle;
232
+ }
233
+ if (row.from_path !== null && row.from_path !== undefined) {
234
+ record.fromPath = row.from_path as string;
235
+ }
236
+ if (row.to_path !== null && row.to_path !== undefined) {
237
+ record.toPath = row.to_path as string;
238
+ }
239
+ if (row.raw_post !== null && row.raw_post !== undefined) {
240
+ record.rawPost = row.raw_post as string;
241
+ }
242
+ const createdDirs = parseCreatedDirs(row.created_dirs);
243
+ if (createdDirs !== null) record.createdDirs = createdDirs;
244
+ return record;
183
245
  } catch {
184
246
  return undefined;
185
247
  }
@@ -196,6 +258,13 @@ export function upsertUndo(store: HashStore, path: string, entry: UndoRecord): v
196
258
  entry.snapshotHash ?? null,
197
259
  entry.transactionId ?? null,
198
260
  entry.rawPre ?? null,
261
+ entry.lifecycle ?? null,
262
+ entry.fromPath ?? null,
263
+ entry.toPath ?? null,
264
+ entry.rawPost ?? null,
265
+ entry.createdDirs === null || entry.createdDirs === undefined
266
+ ? null
267
+ : JSON.stringify(entry.createdDirs),
199
268
  // WHY: ADR-0031 §4 (refuse, do not launder) — the stamp comes from the payload, never
200
269
  // WHY: hard-coded: a replay of a pre-generation row preserves its generation so the
201
270
  // WHY: restore gate re-derives, while a fresh write (absent generation) defaults current.
@@ -1,25 +0,0 @@
1
- import type { FileIO } from "./contracts.js";
2
- import { type EncodingState } from "./encoding.js";
3
- /** One staged UTF-8 bridge: the bytes to restore on failure, the state to invert on success. */
4
- export interface EncodingBridge {
5
- readonly absolutePath: string;
6
- readonly rawBytes: Uint8Array;
7
- readonly state: EncodingState;
8
- }
9
- /**
10
- * Capture every member of one mutation (a correlated cut's target and source
11
- * files), staging each recorded legacy/UTF-16 member as UTF-8. Members without a
12
- * recorded state or with a UTF-8-native state are skipped, so they keep the
13
- * core's UTF-8 behavior. Staging is all-or-nothing: a mid-list failure restores
14
- * the members already staged before rethrowing.
15
- * @param files - the mutation's member paths, in commit order.
16
- * @param label - operation name for diagnostics and rollback messages.
17
- */
18
- export declare function captureEncodingBridges(io: FileIO, files: readonly string[], cwd: string, signal: AbortSignal | undefined, label: string): Promise<EncodingBridge[]>;
19
- /**
20
- * Invert the recorded encoding over each member's core-written UTF-8 result and
21
- * collect the model-facing warning of every lossy degradation.
22
- */
23
- export declare function finishEncodingBridges(io: FileIO, bridges: readonly EncodingBridge[], label: string): Promise<string[]>;
24
- /** Restore every captured member's pre-mutation bytes after a refusal (best-effort). */
25
- export declare function restoreEncodingBridges(io: FileIO, bridges: readonly EncodingBridge[], label: string): Promise<void>;