@pugi/cli 0.1.0-beta.4 → 0.1.0-beta.41

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 (250) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/assets/pugi-mascot.ansi +15 -25
  3. package/bin/run.js +33 -1
  4. package/dist/commands/jobs-watch.js +201 -0
  5. package/dist/commands/jobs.js +15 -0
  6. package/dist/commands/smoke.js +133 -0
  7. package/dist/core/agent-progress/cleanup.js +134 -0
  8. package/dist/core/agent-progress/schema.js +144 -0
  9. package/dist/core/agent-progress/writer.js +101 -0
  10. package/dist/core/artifact-chain/dispatcher.js +148 -0
  11. package/dist/core/artifact-chain/exporter.js +164 -0
  12. package/dist/core/artifact-chain/state.js +243 -0
  13. package/dist/core/artifact-chain/steps.js +169 -0
  14. package/dist/core/auth/ensure-authenticated.js +129 -0
  15. package/dist/core/auth/env-provider.js +238 -0
  16. package/dist/core/auto-update/channels.js +122 -0
  17. package/dist/core/auto-update/checker.js +241 -0
  18. package/dist/core/auto-update/state.js +235 -0
  19. package/dist/core/bare-mode/index.js +107 -0
  20. package/dist/core/bash-classifier.js +108 -1
  21. package/dist/core/checkpoint/resumer.js +149 -0
  22. package/dist/core/checkpoint/rewinder.js +291 -0
  23. package/dist/core/codegraph/decision-store.js +248 -0
  24. package/dist/core/codegraph/detect-repo.js +459 -0
  25. package/dist/core/codegraph/install.js +134 -0
  26. package/dist/core/codegraph/offer-hook.js +220 -0
  27. package/dist/core/compact/auto-trigger.js +96 -0
  28. package/dist/core/compact/buffer-rewriter.js +115 -0
  29. package/dist/core/compact/summarizer.js +208 -0
  30. package/dist/core/compact/token-counter.js +108 -0
  31. package/dist/core/consensus/diff-capture.js +73 -0
  32. package/dist/core/context/index.js +7 -0
  33. package/dist/core/context/markdown-traverse.js +255 -0
  34. package/dist/core/cost/rate-card.js +129 -0
  35. package/dist/core/cost/tracker.js +221 -0
  36. package/dist/core/denial-tracking/index.js +8 -0
  37. package/dist/core/denial-tracking/state.js +264 -0
  38. package/dist/core/diagnostics/probe-runner.js +93 -0
  39. package/dist/core/diagnostics/probes/api.js +46 -0
  40. package/dist/core/diagnostics/probes/auth.js +86 -0
  41. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  42. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  43. package/dist/core/diagnostics/probes/config.js +72 -0
  44. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  45. package/dist/core/diagnostics/probes/disk.js +81 -0
  46. package/dist/core/diagnostics/probes/git.js +65 -0
  47. package/dist/core/diagnostics/probes/mcp.js +75 -0
  48. package/dist/core/diagnostics/probes/node.js +59 -0
  49. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  50. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  51. package/dist/core/diagnostics/probes/session.js +74 -0
  52. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  53. package/dist/core/diagnostics/probes/workspace.js +63 -0
  54. package/dist/core/diagnostics/types.js +70 -0
  55. package/dist/core/dispatch/cache-cleanup.js +197 -0
  56. package/dist/core/dispatch/cache-handoff.js +295 -0
  57. package/dist/core/edits/dispatch.js +218 -2
  58. package/dist/core/edits/journal.js +199 -0
  59. package/dist/core/edits/layer-d-ast.js +557 -14
  60. package/dist/core/edits/verify-hook.js +273 -0
  61. package/dist/core/edits/worktree.js +322 -0
  62. package/dist/core/engine/anvil-client.js +115 -5
  63. package/dist/core/engine/budgets.js +98 -0
  64. package/dist/core/engine/context-prefix.js +155 -0
  65. package/dist/core/engine/intent.js +260 -0
  66. package/dist/core/engine/native-pugi.js +860 -211
  67. package/dist/core/engine/prompts.js +88 -2
  68. package/dist/core/engine/strip-internal-fields.js +124 -0
  69. package/dist/core/engine/tool-bridge.js +1045 -36
  70. package/dist/core/feedback/queue.js +177 -0
  71. package/dist/core/feedback/submitter.js +145 -0
  72. package/dist/core/file-cache.js +113 -1
  73. package/dist/core/hooks/events.js +44 -0
  74. package/dist/core/hooks/index.js +15 -0
  75. package/dist/core/hooks/registry.js +213 -0
  76. package/dist/core/hooks/runner.js +236 -0
  77. package/dist/core/hooks/v2/event-emitter.js +115 -0
  78. package/dist/core/hooks/v2/executor.js +282 -0
  79. package/dist/core/hooks/v2/index.js +25 -0
  80. package/dist/core/hooks/v2/lifecycle.js +104 -0
  81. package/dist/core/hooks/v2/loader.js +216 -0
  82. package/dist/core/hooks/v2/matcher.js +125 -0
  83. package/dist/core/hooks/v2/trust.js +143 -0
  84. package/dist/core/hooks/v2/types.js +86 -0
  85. package/dist/core/lsp/cache.js +105 -0
  86. package/dist/core/lsp/client.js +776 -0
  87. package/dist/core/lsp/language-detect.js +66 -0
  88. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  89. package/dist/core/mcp/client.js +75 -6
  90. package/dist/core/mcp/http-server.js +553 -0
  91. package/dist/core/mcp/orchestrator-tools.js +662 -0
  92. package/dist/core/mcp/permission.js +190 -0
  93. package/dist/core/mcp/registry.js +24 -2
  94. package/dist/core/mcp/server-tools.js +219 -0
  95. package/dist/core/mcp/server.js +397 -0
  96. package/dist/core/memory/dual-write.js +416 -0
  97. package/dist/core/memory/phase1-kinds.js +20 -0
  98. package/dist/core/memory-sync/queue.js +158 -0
  99. package/dist/core/onboarding/ensure-initialized.js +133 -0
  100. package/dist/core/onboarding/marker.js +111 -0
  101. package/dist/core/onboarding/telemetry-state.js +108 -0
  102. package/dist/core/output-style/presets.js +176 -0
  103. package/dist/core/output-style/state.js +185 -0
  104. package/dist/core/permissions/auto-classifier.js +124 -0
  105. package/dist/core/permissions/circuit-breaker.js +83 -0
  106. package/dist/core/permissions/gate.js +278 -0
  107. package/dist/core/permissions/index.js +20 -0
  108. package/dist/core/permissions/mode.js +174 -0
  109. package/dist/core/permissions/state.js +241 -0
  110. package/dist/core/permissions/tool-class.js +93 -0
  111. package/dist/core/prd-check/parser.js +215 -0
  112. package/dist/core/prd-check/reporter.js +127 -0
  113. package/dist/core/prd-check/session-review.js +557 -0
  114. package/dist/core/prd-check/verifiers.js +223 -0
  115. package/dist/core/pugi-md/context-injector.js +76 -0
  116. package/dist/core/pugi-md/walk-up.js +207 -0
  117. package/dist/core/release-notes/parser.js +241 -0
  118. package/dist/core/release-notes/state.js +116 -0
  119. package/dist/core/repl/history.js +11 -1
  120. package/dist/core/repl/model-pricing.js +135 -0
  121. package/dist/core/repl/session.js +1899 -38
  122. package/dist/core/repl/slash-commands.js +406 -21
  123. package/dist/core/repl/store/session-store.js +31 -2
  124. package/dist/core/repl/workspace-context.js +22 -0
  125. package/dist/core/repo-map/build.js +125 -0
  126. package/dist/core/repo-map/cache.js +185 -0
  127. package/dist/core/repo-map/extractor.js +254 -0
  128. package/dist/core/repo-map/formatter.js +145 -0
  129. package/dist/core/repo-map/scanner.js +211 -0
  130. package/dist/core/retry-budget/budget.js +284 -0
  131. package/dist/core/retry-budget/index.js +5 -0
  132. package/dist/core/session.js +92 -0
  133. package/dist/core/settings.js +80 -0
  134. package/dist/core/share/formatter.js +271 -0
  135. package/dist/core/share/redactor.js +221 -0
  136. package/dist/core/share/uploader.js +267 -0
  137. package/dist/core/skills/defaults.js +457 -0
  138. package/dist/core/smoke/headless-driver.js +174 -0
  139. package/dist/core/smoke/orchestrator.js +194 -0
  140. package/dist/core/smoke/runner.js +238 -0
  141. package/dist/core/smoke/scenario-parser.js +316 -0
  142. package/dist/core/subagents/dispatcher-real.js +600 -0
  143. package/dist/core/subagents/dispatcher.js +113 -24
  144. package/dist/core/subagents/index.js +18 -5
  145. package/dist/core/subagents/isolation-matrix.js +213 -0
  146. package/dist/core/subagents/spawn.js +19 -4
  147. package/dist/core/telemetry/emitter.js +229 -0
  148. package/dist/core/telemetry/queue.js +251 -0
  149. package/dist/core/theme/context.js +91 -0
  150. package/dist/core/theme/presets.js +228 -0
  151. package/dist/core/theme/state.js +181 -0
  152. package/dist/core/todos/invariant.js +10 -0
  153. package/dist/core/todos/state.js +177 -0
  154. package/dist/core/transport/version-interceptor.js +166 -0
  155. package/dist/core/vim/keymap.js +288 -0
  156. package/dist/core/vim/state.js +92 -0
  157. package/dist/index.js +28 -0
  158. package/dist/runtime/bootstrap.js +190 -0
  159. package/dist/runtime/cli.js +3073 -321
  160. package/dist/runtime/commands/cancel.js +231 -0
  161. package/dist/runtime/commands/chain.js +489 -0
  162. package/dist/runtime/commands/codegraph-status.js +227 -0
  163. package/dist/runtime/commands/compact.js +297 -0
  164. package/dist/runtime/commands/cost.js +199 -0
  165. package/dist/runtime/commands/delegate.js +242 -11
  166. package/dist/runtime/commands/dispatch.js +126 -0
  167. package/dist/runtime/commands/doctor.js +390 -0
  168. package/dist/runtime/commands/feedback.js +184 -0
  169. package/dist/runtime/commands/hooks.js +184 -0
  170. package/dist/runtime/commands/lsp.js +368 -0
  171. package/dist/runtime/commands/mcp.js +879 -0
  172. package/dist/runtime/commands/memory.js +508 -0
  173. package/dist/runtime/commands/model.js +237 -0
  174. package/dist/runtime/commands/onboarding.js +275 -0
  175. package/dist/runtime/commands/patch.js +128 -0
  176. package/dist/runtime/commands/permissions.js +112 -0
  177. package/dist/runtime/commands/plan.js +143 -0
  178. package/dist/runtime/commands/prd-check.js +285 -0
  179. package/dist/runtime/commands/redo-blob-store.js +92 -0
  180. package/dist/runtime/commands/redo.js +361 -0
  181. package/dist/runtime/commands/release-notes.js +229 -0
  182. package/dist/runtime/commands/repo-map.js +95 -0
  183. package/dist/runtime/commands/report.js +299 -0
  184. package/dist/runtime/commands/resume.js +118 -0
  185. package/dist/runtime/commands/review-consensus.js +17 -2
  186. package/dist/runtime/commands/rewind.js +333 -0
  187. package/dist/runtime/commands/sessions.js +163 -0
  188. package/dist/runtime/commands/share.js +316 -0
  189. package/dist/runtime/commands/status.js +186 -0
  190. package/dist/runtime/commands/stickers.js +82 -0
  191. package/dist/runtime/commands/style.js +194 -0
  192. package/dist/runtime/commands/theme.js +196 -0
  193. package/dist/runtime/commands/undo.js +32 -0
  194. package/dist/runtime/commands/update.js +289 -0
  195. package/dist/runtime/commands/vim.js +140 -0
  196. package/dist/runtime/commands/worktree.js +177 -0
  197. package/dist/runtime/headless-repl.js +195 -0
  198. package/dist/runtime/headless.js +543 -0
  199. package/dist/runtime/load-hooks-or-exit.js +71 -0
  200. package/dist/runtime/plan-decompose.js +531 -0
  201. package/dist/runtime/version.js +65 -0
  202. package/dist/tools/agent-tool.js +229 -0
  203. package/dist/tools/apply-patch.js +556 -0
  204. package/dist/tools/ask-user-question.js +213 -0
  205. package/dist/tools/ask-user.js +115 -0
  206. package/dist/tools/file-tools.js +85 -14
  207. package/dist/tools/lsp-tools.js +189 -0
  208. package/dist/tools/mcp-tool.js +260 -0
  209. package/dist/tools/multi-edit.js +361 -0
  210. package/dist/tools/powershell.js +156 -0
  211. package/dist/tools/registry.js +51 -0
  212. package/dist/tools/skill-tool.js +96 -0
  213. package/dist/tools/tasks.js +208 -0
  214. package/dist/tools/todo-write.js +184 -0
  215. package/dist/tools/web-fetch.js +147 -2
  216. package/dist/tools/web-search.js +458 -0
  217. package/dist/tui/agent-progress-card.js +111 -0
  218. package/dist/tui/agent-tree.js +10 -0
  219. package/dist/tui/ask-modal.js +2 -2
  220. package/dist/tui/ask-user-question-prompt.js +192 -0
  221. package/dist/tui/compact-banner.js +81 -0
  222. package/dist/tui/conversation-pane.js +82 -8
  223. package/dist/tui/cost-table.js +111 -0
  224. package/dist/tui/doctor-table.js +46 -0
  225. package/dist/tui/feedback-prompt.js +156 -0
  226. package/dist/tui/input-box.js +69 -2
  227. package/dist/tui/markdown-render.js +4 -4
  228. package/dist/tui/onboarding-wizard.js +240 -0
  229. package/dist/tui/permissions-picker.js +86 -0
  230. package/dist/tui/render.js +35 -0
  231. package/dist/tui/repl-render.js +303 -13
  232. package/dist/tui/repl-splash.js +2 -2
  233. package/dist/tui/repl.js +72 -14
  234. package/dist/tui/splash.js +1 -1
  235. package/dist/tui/status-bar.js +94 -16
  236. package/dist/tui/status-table.js +7 -0
  237. package/dist/tui/stickers-art.js +136 -0
  238. package/dist/tui/style-table.js +28 -0
  239. package/dist/tui/theme-table.js +29 -0
  240. package/dist/tui/tool-stream-pane.js +52 -3
  241. package/dist/tui/update-banner.js +20 -2
  242. package/dist/tui/vim-input.js +267 -0
  243. package/docs/examples/codegraph.mcp.json +10 -0
  244. package/package.json +12 -6
  245. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  246. package/test/scenarios/compact-force.scenario.txt +11 -0
  247. package/test/scenarios/identity.scenario.txt +11 -0
  248. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  249. package/test/scenarios/walkback.scenario.txt +12 -0
  250. package/dist/core/engine/compaction-hook.js +0 -154
@@ -1,29 +1,572 @@
1
1
  /**
2
- * Sentinel error type the dispatcher recognises. Distinct from the
3
- * generic `Error` so the dispatcher can render a friendly
4
- * "AST edits land in α6.6b" message rather than a stack trace.
2
+ * Layer D AST-aware diff applicator β7 (2026-05-26).
5
3
  *
6
- * Caller convention: catch `LayerDDeferredError` explicitly; rethrow
7
- * any other error as before.
4
+ * Replaces the α6.6 stub. Implements structural edits that cannot be
5
+ * expressed safely as text-level Layer A/B/C diffs:
6
+ *
7
+ * - `rename_symbol` — rename every reference to a symbol inside a
8
+ * single file, respecting language identifier
9
+ * boundaries. Cross-file rename is handled by
10
+ * the engine loop dispatching one Layer D edit
11
+ * per affected file (see multi_edit tool).
12
+ * - `add_import` — add an `import`/`use`/`from` statement at
13
+ * the canonical location for the file's
14
+ * language. Idempotent: re-adding an existing
15
+ * import is a no-op success.
16
+ * - `remove_import` — remove an `import`/`use`/`from` statement
17
+ * when present; no-op success when absent.
18
+ * - `extract_function` — currently surfaces `not_supported` (real
19
+ * tree-sitter integration deferred to β8).
20
+ * - `inline_variable` — same; deferred to β8.
21
+ *
22
+ * Why not tree-sitter (yet):
23
+ *
24
+ * The α6.6+ posture for `apps/pugi-cli` keeps the dep tree intentionally
25
+ * lean (zod + ink + react + undici + tar). Pulling
26
+ * `tree-sitter` + per-language grammars (5 native binding packages,
27
+ * each ~5-15 MiB compiled) would balloon install size from ~80 MiB to
28
+ * ~250 MiB and require native rebuild on every Node major bump. We
29
+ * ship the operations we can do CORRECTLY with a language-aware
30
+ * regex+lexer approach today (rename/add_import/remove_import on
31
+ * identifier boundaries — see `identifierBoundaryReplace` below), and
32
+ * keep the deferred operations behind a structured `not_supported`
33
+ * reason instead of a bogus tree-sitter dep.
34
+ *
35
+ * The grammar of identifiers + import statements is small enough across
36
+ * TS/JS/Python/Go/Rust that the regex approach (with language-aware
37
+ * boundary characters) catches every realistic in-file rename target
38
+ * without false positives — string literals and comments are skipped
39
+ * by a tiny per-language tokenizer. The approach matches what most
40
+ * "find-and-replace symbol" IDEs do for the same operation when no
41
+ * LSP server is available.
42
+ *
43
+ * Security: every operation routes through `applySecurityGate`. Atomic
44
+ * writes via `atomicWrite` (same pattern as Layer A).
45
+ *
46
+ * Spec source: `docs/research/2026-05-21-pugi-cli-architecture.md` §4.4
47
+ * and `docs/research/2026-05-26-pugi-cli-consolidated-sprint-plan.md` β7.
48
+ */
49
+ import { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from 'node:fs';
50
+ import { extname } from 'node:path';
51
+ import { applySecurityGate } from './security-gate.js';
52
+ /**
53
+ * Sentinel error type the dispatcher recognises. Kept for back-compat
54
+ * with α6.6 callers — never thrown by the new implementation; deferred
55
+ * operations now surface as structured `not_supported` results.
8
56
  */
9
57
  export class LayerDDeferredError extends Error {
10
58
  code = 'LAYER_D_DEFERRED';
11
59
  operation;
12
60
  constructor(operation) {
13
- super(`Layer D (AST) edits land in α6.6b — attempted operation: ${operation}. ` +
14
- 'Use Layer A / B / C until the AST engine ships.');
61
+ super(`Layer D operation ${operation} is not yet supported.`);
15
62
  this.name = 'LayerDDeferredError';
16
63
  this.operation = operation;
17
64
  }
18
65
  }
66
+ export function detectLanguage(file) {
67
+ const ext = extname(file).toLowerCase();
68
+ if (ext === '.ts' || ext === '.tsx')
69
+ return 'ts';
70
+ if (ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs')
71
+ return 'js';
72
+ if (ext === '.py' || ext === '.pyi')
73
+ return 'py';
74
+ if (ext === '.go')
75
+ return 'go';
76
+ if (ext === '.rs')
77
+ return 'rust';
78
+ return 'other';
79
+ }
80
+ /**
81
+ * Apply a Layer D structural edit. Returns the standard `ApplyResult`
82
+ * shape so the dispatcher can treat all four layers uniformly.
83
+ */
84
+ export async function applyLayerD(edit, opts) {
85
+ // SECURITY GATE — fail-fast before ANY filesystem read/write. Same
86
+ // chokepoint Layer A/B/C goes through.
87
+ let gateResult;
88
+ try {
89
+ gateResult = applySecurityGate(edit.file, { cwd: opts.cwd, toolName: 'layer-c' });
90
+ }
91
+ catch (error) {
92
+ return {
93
+ ok: false,
94
+ bytesWritten: 0,
95
+ reason: 'write_error',
96
+ absPath: edit.file,
97
+ detail: error instanceof Error ? error.message : String(error),
98
+ };
99
+ }
100
+ if (!gateResult.ok) {
101
+ return {
102
+ ok: false,
103
+ bytesWritten: 0,
104
+ reason: gateResult.reason,
105
+ absPath: edit.file,
106
+ detail: gateResult.detail,
107
+ };
108
+ }
109
+ const absPath = gateResult.absPath;
110
+ if (!existsSync(absPath)) {
111
+ return {
112
+ ok: false,
113
+ bytesWritten: 0,
114
+ reason: 'file_missing',
115
+ absPath,
116
+ detail: `file does not exist: ${edit.file}`,
117
+ };
118
+ }
119
+ let body;
120
+ try {
121
+ body = readFileSync(absPath, 'utf8');
122
+ }
123
+ catch (error) {
124
+ return {
125
+ ok: false,
126
+ bytesWritten: 0,
127
+ reason: 'write_error',
128
+ absPath,
129
+ detail: error instanceof Error ? error.message : String(error),
130
+ };
131
+ }
132
+ const lang = detectLanguage(edit.file);
133
+ switch (edit.operation) {
134
+ case 'rename_symbol':
135
+ return await renameSymbol(edit, body, absPath, lang, opts);
136
+ case 'add_import':
137
+ return await addImport(edit, body, absPath, lang, opts);
138
+ case 'remove_import':
139
+ return await removeImport(edit, body, absPath, lang, opts);
140
+ case 'extract_function':
141
+ case 'inline_variable':
142
+ return {
143
+ ok: false,
144
+ bytesWritten: 0,
145
+ reason: 'write_error',
146
+ absPath,
147
+ detail: `Layer D operation ${edit.operation} is not yet supported. ` +
148
+ `Use Layer A/B/C for the equivalent text edit, or wait for the ` +
149
+ `β8 tree-sitter integration.`,
150
+ };
151
+ }
152
+ }
153
+ /* ------------------------------------------------------------------ */
154
+ /* rename_symbol */
155
+ /* ------------------------------------------------------------------ */
156
+ async function renameSymbol(edit, body, absPath, lang, opts) {
157
+ const params = parseRenameParams(edit.params);
158
+ if (!params) {
159
+ return {
160
+ ok: false,
161
+ bytesWritten: 0,
162
+ reason: 'write_error',
163
+ absPath,
164
+ detail: 'rename_symbol: params must be { from: string, to: string }',
165
+ };
166
+ }
167
+ if (!isValidIdentifier(params.from, lang) || !isValidIdentifier(params.to, lang)) {
168
+ return {
169
+ ok: false,
170
+ bytesWritten: 0,
171
+ reason: 'write_error',
172
+ absPath,
173
+ detail: `rename_symbol: identifier validation failed for language ${lang}. ` +
174
+ `Both 'from' and 'to' must be valid identifiers.`,
175
+ };
176
+ }
177
+ if (params.from === params.to) {
178
+ return {
179
+ ok: false,
180
+ bytesWritten: 0,
181
+ reason: 'identical_replacement',
182
+ absPath,
183
+ detail: 'rename_symbol: from and to are identical',
184
+ };
185
+ }
186
+ const { result, count } = identifierBoundaryReplace(body, params.from, params.to, lang);
187
+ if (count === 0) {
188
+ return {
189
+ ok: false,
190
+ bytesWritten: 0,
191
+ reason: 'no_match',
192
+ matchCount: 0,
193
+ absPath,
194
+ detail: `rename_symbol: no references to '${params.from}' found in ${edit.file}`,
195
+ };
196
+ }
197
+ if (opts.dryRun) {
198
+ return { ok: true, bytesWritten: 0, matchCount: count, absPath };
199
+ }
200
+ try {
201
+ atomicWrite(absPath, result);
202
+ }
203
+ catch (error) {
204
+ return {
205
+ ok: false,
206
+ bytesWritten: 0,
207
+ reason: 'write_error',
208
+ absPath,
209
+ detail: error instanceof Error ? error.message : String(error),
210
+ };
211
+ }
212
+ return {
213
+ ok: true,
214
+ bytesWritten: Buffer.byteLength(result, 'utf8'),
215
+ matchCount: count,
216
+ absPath,
217
+ };
218
+ }
219
+ function parseRenameParams(params) {
220
+ const from = params['from'];
221
+ const to = params['to'];
222
+ if (typeof from !== 'string' || typeof to !== 'string')
223
+ return null;
224
+ if (from.length === 0 || to.length === 0)
225
+ return null;
226
+ return { from, to };
227
+ }
228
+ /**
229
+ * Replace every standalone identifier occurrence of `from` with `to`.
230
+ * Identifier boundaries are language-aware: in Python `foo.bar` and
231
+ * `foo_bar` are distinct identifiers; in Rust `foo::bar` likewise.
232
+ * Tokens inside string literals and comments are skipped.
233
+ *
234
+ * Returns both the rewritten body and the replacement count so the
235
+ * dispatcher can report `matchCount`.
236
+ */
237
+ export function identifierBoundaryReplace(body, from, to, lang) {
238
+ const tokens = tokenize(body, lang);
239
+ const parts = [];
240
+ let count = 0;
241
+ for (const tok of tokens) {
242
+ if (tok.kind === 'ident' && tok.text === from) {
243
+ parts.push(to);
244
+ count += 1;
245
+ }
246
+ else {
247
+ parts.push(tok.text);
248
+ }
249
+ }
250
+ return { result: parts.join(''), count };
251
+ }
252
+ /* ------------------------------------------------------------------ */
253
+ /* add_import / remove_import */
254
+ /* ------------------------------------------------------------------ */
255
+ async function addImport(edit, body, absPath, lang, opts) {
256
+ const statement = parseImportStatement(edit.params);
257
+ if (statement === null) {
258
+ return {
259
+ ok: false,
260
+ bytesWritten: 0,
261
+ reason: 'write_error',
262
+ absPath,
263
+ detail: 'add_import: params must be { statement: string } (single-line)',
264
+ };
265
+ }
266
+ // Idempotency — a verbatim line match counts as already-applied.
267
+ const lines = body.split('\n');
268
+ if (lines.some((line) => line === statement)) {
269
+ if (opts.dryRun) {
270
+ return { ok: true, bytesWritten: 0, absPath, matchCount: 1 };
271
+ }
272
+ return { ok: true, bytesWritten: 0, absPath, matchCount: 1 };
273
+ }
274
+ const insertAt = findImportInsertIndex(lines, lang);
275
+ const next = [...lines.slice(0, insertAt), statement, ...lines.slice(insertAt)];
276
+ const result = next.join('\n');
277
+ if (opts.dryRun) {
278
+ return { ok: true, bytesWritten: 0, absPath, matchCount: 1 };
279
+ }
280
+ try {
281
+ atomicWrite(absPath, result);
282
+ }
283
+ catch (error) {
284
+ return {
285
+ ok: false,
286
+ bytesWritten: 0,
287
+ reason: 'write_error',
288
+ absPath,
289
+ detail: error instanceof Error ? error.message : String(error),
290
+ };
291
+ }
292
+ return {
293
+ ok: true,
294
+ bytesWritten: Buffer.byteLength(result, 'utf8'),
295
+ absPath,
296
+ matchCount: 1,
297
+ };
298
+ }
299
+ async function removeImport(edit, body, absPath, _lang, opts) {
300
+ const statement = parseImportStatement(edit.params);
301
+ if (statement === null) {
302
+ return {
303
+ ok: false,
304
+ bytesWritten: 0,
305
+ reason: 'write_error',
306
+ absPath,
307
+ detail: 'remove_import: params must be { statement: string } (single-line)',
308
+ };
309
+ }
310
+ const lines = body.split('\n');
311
+ const filtered = lines.filter((line) => line !== statement);
312
+ if (filtered.length === lines.length) {
313
+ // No-op success — idempotent removal.
314
+ return { ok: true, bytesWritten: 0, absPath, matchCount: 0 };
315
+ }
316
+ const result = filtered.join('\n');
317
+ if (opts.dryRun) {
318
+ return {
319
+ ok: true,
320
+ bytesWritten: 0,
321
+ absPath,
322
+ matchCount: lines.length - filtered.length,
323
+ };
324
+ }
325
+ try {
326
+ atomicWrite(absPath, result);
327
+ }
328
+ catch (error) {
329
+ return {
330
+ ok: false,
331
+ bytesWritten: 0,
332
+ reason: 'write_error',
333
+ absPath,
334
+ detail: error instanceof Error ? error.message : String(error),
335
+ };
336
+ }
337
+ return {
338
+ ok: true,
339
+ bytesWritten: Buffer.byteLength(result, 'utf8'),
340
+ absPath,
341
+ matchCount: lines.length - filtered.length,
342
+ };
343
+ }
344
+ function parseImportStatement(params) {
345
+ const statement = params['statement'];
346
+ if (typeof statement !== 'string')
347
+ return null;
348
+ if (statement.length === 0)
349
+ return null;
350
+ // Single-line invariant: imports SHOULD fit one line. Multi-line
351
+ // import payloads are out of scope (use Layer A/B with explicit
352
+ // anchors instead).
353
+ if (statement.includes('\n'))
354
+ return null;
355
+ return statement;
356
+ }
19
357
  /**
20
- * Stub applicator. Always rejects with `LayerDDeferredError`. Returns
21
- * the standard `ApplyResult` shape on the (impossible) success path
22
- * so the dispatcher can treat all four layers uniformly once Layer D
23
- * lights up.
358
+ * Pick the line index at which a new import should land. The heuristic
359
+ * is conservative: after every leading shebang / module-doc comment /
360
+ * existing import line, before the first non-import non-comment line.
361
+ *
362
+ * Per-language nuance is intentionally light — every supported language
363
+ * accepts an import anywhere at file scope; we aim for "next to the
364
+ * other imports" which is what every formatter expects.
24
365
  */
25
- // eslint-disable-next-line @typescript-eslint/require-await
26
- export async function applyLayerD(edit, _opts) {
27
- throw new LayerDDeferredError(edit.operation);
366
+ export function findImportInsertIndex(lines, _lang) {
367
+ // Skip leading shebang.
368
+ let i = 0;
369
+ if (i < lines.length && lines[i].startsWith('#!'))
370
+ i += 1;
371
+ // Skip leading single-line comments / blank lines / module docstring
372
+ // openers. Conservative — never crosses an `import` block boundary
373
+ // because the import check below catches them.
374
+ while (i < lines.length) {
375
+ const line = lines[i];
376
+ const trimmed = line.trim();
377
+ if (trimmed === '') {
378
+ i += 1;
379
+ continue;
380
+ }
381
+ if (isCommentLine(trimmed)) {
382
+ i += 1;
383
+ continue;
384
+ }
385
+ break;
386
+ }
387
+ // Walk past existing import block. We stop at the first non-import
388
+ // non-comment line so the new statement lands tail-of-imports.
389
+ while (i < lines.length) {
390
+ const line = lines[i];
391
+ const trimmed = line.trim();
392
+ if (trimmed === '' || isCommentLine(trimmed) || isImportLine(trimmed)) {
393
+ i += 1;
394
+ continue;
395
+ }
396
+ break;
397
+ }
398
+ // Trim trailing blank lines from the insertion index so the new
399
+ // import lands directly under the existing imports rather than after
400
+ // a stray gap.
401
+ while (i > 0 && lines[i - 1].trim() === '')
402
+ i -= 1;
403
+ return i;
404
+ }
405
+ function isCommentLine(trimmed) {
406
+ return (trimmed.startsWith('//') ||
407
+ trimmed.startsWith('#') ||
408
+ trimmed.startsWith('/*') ||
409
+ trimmed.startsWith('*') ||
410
+ trimmed.startsWith('"""') ||
411
+ trimmed.startsWith("'''"));
412
+ }
413
+ function isImportLine(trimmed) {
414
+ return (trimmed.startsWith('import ') ||
415
+ trimmed.startsWith('from ') ||
416
+ trimmed.startsWith('use ') ||
417
+ trimmed.startsWith('extern crate ') ||
418
+ trimmed.startsWith('const ') && trimmed.includes('require(') ||
419
+ trimmed.startsWith('require ') ||
420
+ trimmed.startsWith('package '));
421
+ }
422
+ /**
423
+ * Tiny tokenizer that splits a source body into identifier tokens vs.
424
+ * everything else. Skips string-literal interiors and comments so the
425
+ * rename pass never rewrites a string mention of the symbol.
426
+ *
427
+ * Per-language config:
428
+ * - line comment prefix
429
+ * - block comment delimiters
430
+ * - string literal openers (with backslash-escape awareness)
431
+ * - triple-quoted strings (Python)
432
+ * - identifier character set
433
+ *
434
+ * The tokenizer is character-stream linear; complexity O(n).
435
+ */
436
+ export function tokenize(src, lang) {
437
+ const out = [];
438
+ let buf = '';
439
+ let i = 0;
440
+ const flushOther = () => {
441
+ if (buf.length > 0) {
442
+ out.push({ kind: 'other', text: buf });
443
+ buf = '';
444
+ }
445
+ };
446
+ const lineComment = lang === 'py' ? '#' : '//';
447
+ const tripleQuote = lang === 'py';
448
+ while (i < src.length) {
449
+ const ch = src[i];
450
+ const next = src[i + 1] ?? '';
451
+ // Line comment.
452
+ if (ch === lineComment[0] && (lineComment.length === 1 || next === lineComment[1])) {
453
+ flushOther();
454
+ const start = i;
455
+ while (i < src.length && src[i] !== '\n')
456
+ i += 1;
457
+ out.push({ kind: 'other', text: src.slice(start, i) });
458
+ continue;
459
+ }
460
+ // C-style block comment (TS/JS/Go/Rust). Python uses """ instead.
461
+ if (lang !== 'py' && ch === '/' && next === '*') {
462
+ flushOther();
463
+ const start = i;
464
+ i += 2;
465
+ while (i < src.length - 1 && !(src[i] === '*' && src[i + 1] === '/'))
466
+ i += 1;
467
+ i = Math.min(i + 2, src.length);
468
+ out.push({ kind: 'other', text: src.slice(start, i) });
469
+ continue;
470
+ }
471
+ // Triple-quoted string (Python).
472
+ if (tripleQuote && (ch === '"' || ch === "'") && next === ch && src[i + 2] === ch) {
473
+ flushOther();
474
+ const quote = ch;
475
+ const start = i;
476
+ i += 3;
477
+ while (i < src.length - 2 &&
478
+ !(src[i] === quote && src[i + 1] === quote && src[i + 2] === quote)) {
479
+ i += 1;
480
+ }
481
+ i = Math.min(i + 3, src.length);
482
+ out.push({ kind: 'other', text: src.slice(start, i) });
483
+ continue;
484
+ }
485
+ // Regular string literal.
486
+ if (ch === '"' || ch === "'" || (lang !== 'py' && ch === '`')) {
487
+ flushOther();
488
+ const quote = ch;
489
+ const start = i;
490
+ i += 1;
491
+ while (i < src.length) {
492
+ if (src[i] === '\\') {
493
+ i += 2;
494
+ continue;
495
+ }
496
+ if (src[i] === quote) {
497
+ i += 1;
498
+ break;
499
+ }
500
+ i += 1;
501
+ }
502
+ out.push({ kind: 'other', text: src.slice(start, i) });
503
+ continue;
504
+ }
505
+ // Identifier — sequence of identifier characters.
506
+ if (isIdentStart(ch)) {
507
+ flushOther();
508
+ const start = i;
509
+ while (i < src.length && isIdentCont(src[i]))
510
+ i += 1;
511
+ out.push({ kind: 'ident', text: src.slice(start, i) });
512
+ continue;
513
+ }
514
+ buf += ch;
515
+ i += 1;
516
+ }
517
+ flushOther();
518
+ return out;
519
+ }
520
+ function isIdentStart(ch) {
521
+ return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$';
522
+ }
523
+ function isIdentCont(ch) {
524
+ return isIdentStart(ch) || (ch >= '0' && ch <= '9');
525
+ }
526
+ export function isValidIdentifier(name, lang) {
527
+ if (name.length === 0)
528
+ return false;
529
+ if (!isIdentStart(name[0]))
530
+ return false;
531
+ for (let i = 1; i < name.length; i += 1) {
532
+ if (!isIdentCont(name[i]))
533
+ return false;
534
+ }
535
+ // Per-language reserved-word check is intentionally light — the dispatcher
536
+ // is the trust boundary; the operator (or model) supplies the new name
537
+ // and is responsible for it not clashing with a keyword. Catching the
538
+ // obvious cases here would only mask a bigger upstream bug.
539
+ // R7 P2 (Codex 2026-05-26): a future refinement could load a per-language
540
+ // reserved-word list from `framework_*` RAG; tracked under β8.
541
+ return name.length <= 128;
542
+ }
543
+ /**
544
+ * Atomic write helper — mirrors Layer A's pattern (writeFileSync to
545
+ * tmp + rename). See `layer-a-apply.ts::atomicWrite` for the rationale.
546
+ */
547
+ function atomicWrite(absPath, contents) {
548
+ const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
549
+ const tmp = `${absPath}.pugi-tmp-${suffix}`;
550
+ try {
551
+ writeFileSync(tmp, contents, { encoding: 'utf8', mode: 0o600 });
552
+ renameSync(tmp, absPath);
553
+ }
554
+ catch (error) {
555
+ try {
556
+ unlinkSync(tmp);
557
+ }
558
+ catch {
559
+ // tmp file may not exist if writeFileSync itself failed.
560
+ }
561
+ throw error;
562
+ }
28
563
  }
564
+ /** Test-only surface for the spec suite. */
565
+ export const __test__ = {
566
+ tokenize,
567
+ identifierBoundaryReplace,
568
+ findImportInsertIndex,
569
+ isValidIdentifier,
570
+ detectLanguage,
571
+ };
29
572
  //# sourceMappingURL=layer-d-ast.js.map