plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


889886fPeter John 2026-07-19T17:15:37+05:30
docs: add CLI formatter implementation plan
docs/superpowers/plans/2026-07-19-cli-formatter.md ADDED
@@ -0,0 +1,625 @@
1
+ # CLI & Formatter Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Restructure the project as a Cargo workspace, extract a `plum-core` library, and add a `plum-cli` binary with a `format` subcommand backed by Topiary.
6
+
7
+ **Architecture:** Current `src/` code splits into `plum-core` (library: ast, parser, formatter) and `plum-cli` (thin binary). `formatter.rs` wraps `topiary-core`, embedding `format.scm` at compile time via `include_str!`. The CLI uses `clap` for argument parsing and delegates all logic to `plum-core`.
8
+
9
+ **Tech Stack:** Rust 2021 edition, Cargo workspaces, topiary-core 0.7.3, topiary-tree-sitter-facade 0.7.3, tree-sitter 0.26 (required by topiary), tree-sitter-plum 0.1.0, clap 4, anyhow 1.
10
+
11
+ ## Global Constraints
12
+
13
+ - topiary-core version: `"0.7.3"` — exact, do not upgrade
14
+ - topiary-tree-sitter-facade version: `"0.7.3"` — exact, matches topiary-core
15
+ - tree-sitter version: `"0.26"` — topiary-core 0.7.3 resolves tree-sitter 0.26.11; use `"0.26"` (not `"0.24.5"`) in plum-core
16
+ - tree-sitter-plum version: `"0.1.0"` — local crate at `tooling/tree-sitter-plum`; reference via `path`
17
+ - clap version: `"4"` with `features = ["derive"]`
18
+ - anyhow version: `"1"`
19
+ - format.scm lives at `tooling/tree-sitter-plum/queries/plum/format.scm`
20
+ - `plum format <file>` edits in place silently on success; `--check` exits 1 if file would change; `--stdin` reads stdin, writes stdout
21
+ - `tolerate_parsing_errors = false` — refuse to format files with syntax errors
22
+
23
+ ---
24
+
25
+ ## File Map
26
+
27
+ | Path | Action | Responsibility |
28
+ |---|---|---|
29
+ | `Cargo.toml` | Replace | Workspace root — lists members, no `[package]` |
30
+ | `plum-core/Cargo.toml` | Create | Library dependencies (tree-sitter 0.26, topiary, etc.) |
31
+ | `plum-core/src/lib.rs` | Create | `pub mod` declarations + re-exports |
32
+ | `plum-core/src/ast.rs` | Move from `src/ast.rs` | Typed AST types (unchanged) |
33
+ | `plum-core/src/parser.rs` | Move from `src/parser.rs` | CST → AST walking (unchanged) |
34
+ | `plum-core/src/formatter.rs` | Create | Topiary wrapper — `format_source` |
35
+ | `plum-core/tests/formatter_test.rs` | Create | Integration tests for `format_source` |
36
+ | `plum-cli/Cargo.toml` | Create | Binary dependencies (plum-core, clap, anyhow) |
37
+ | `plum-cli/src/main.rs` | Create | clap CLI, file I/O, calls `plum_core::format_source` |
38
+ | `tooling/tree-sitter-plum/queries/plum/format.scm` | Create | Topiary formatting rules |
39
+
40
+ ---
41
+
42
+ ### Task 1: Restructure as Cargo Workspace
43
+
44
+ **Files:**
45
+ - Replace: `Cargo.toml`
46
+ - Create: `plum-core/Cargo.toml`
47
+ - Create: `plum-core/src/lib.rs`
48
+ - Move: `src/ast.rs` → `plum-core/src/ast.rs`
49
+ - Move: `src/parser.rs` → `plum-core/src/parser.rs`
50
+
51
+ **Interfaces:**
52
+ - Produces: `plum-core` crate compilable with `cargo build -p plum-core`
53
+
54
+ - [ ] **Step 1: Replace root Cargo.toml with workspace definition**
55
+
56
+ ```toml
57
+ # Cargo.toml
58
+ [workspace]
59
+ members = ["plum-core", "plum-cli"]
60
+ resolver = "2"
61
+ ```
62
+
63
+ - [ ] **Step 2: Create `plum-core/Cargo.toml`**
64
+
65
+ ```toml
66
+ [package]
67
+ name = "plum-core"
68
+ version = "0.1.0"
69
+ edition = "2021"
70
+
71
+ [dependencies]
72
+ tree-sitter = "0.26"
73
+ tree-sitter-plum = { path = "../tooling/tree-sitter-plum" }
74
+ topiary-core = "0.7.3"
75
+ topiary-tree-sitter-facade = "0.7.3"
76
+ ```
77
+
78
+ - [ ] **Step 3: Move source files into plum-core**
79
+
80
+ ```bash
81
+ mkdir -p plum-core/src
82
+ cp src/ast.rs plum-core/src/ast.rs
83
+ cp src/parser.rs plum-core/src/parser.rs
84
+ ```
85
+
86
+ - [ ] **Step 4: Create `plum-core/src/lib.rs`**
87
+
88
+ ```rust
89
+ pub mod ast;
90
+ pub mod parser;
91
+ pub mod formatter;
92
+
93
+ pub use formatter::{format_source, FormatterError};
94
+ pub use parser::AstParser;
95
+ ```
96
+
97
+ - [ ] **Step 5: Verify plum-core compiles (formatter module is absent — add stub)**
98
+
99
+ Add an empty `plum-core/src/formatter.rs` so lib.rs compiles:
100
+
101
+ ```rust
102
+ // plum-core/src/formatter.rs
103
+ #[derive(Debug)]
104
+ pub enum FormatterError {
105
+ TopiaryCoreError(String),
106
+ }
107
+
108
+ impl std::fmt::Display for FormatterError {
109
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110
+ match self {
111
+ FormatterError::TopiaryCoreError(msg) => write!(f, "Topiary error: {msg}"),
112
+ }
113
+ }
114
+ }
115
+
116
+ impl std::error::Error for FormatterError {}
117
+
118
+ pub fn format_source(_source: &str) -> Result<String, FormatterError> {
119
+ unimplemented!()
120
+ }
121
+ ```
122
+
123
+ Run: `cargo build -p plum-core`
124
+ Expected: compiles (formatter fn is `unimplemented!()` — no test calls it yet)
125
+
126
+ - [ ] **Step 6: Commit**
127
+
128
+ ```bash
129
+ git add Cargo.toml plum-core/
130
+ git commit -m "refactor: restructure as Cargo workspace, add plum-core skeleton"
131
+ ```
132
+
133
+ ---
134
+
135
+ ### Task 2: Write `format.scm` Topiary Formatting Rules
136
+
137
+ **Files:**
138
+ - Create: `tooling/tree-sitter-plum/queries/plum/format.scm`
139
+
140
+ **Interfaces:**
141
+ - Produces: formatting query embedded at compile time via `include_str!("../../../../tooling/tree-sitter-plum/queries/plum/format.scm")` from `plum-core/src/formatter.rs`
142
+
143
+ Topiary annotation reference:
144
+ - `@append_space` — insert a space after the matched node
145
+ - `@prepend_space` — insert a space before the matched node
146
+ - `@prepend_hardline` — start a new line before the matched node
147
+ - `@append_hardline` — start a new line after the matched node
148
+ - `@append_indent_start` — increase indent after this node
149
+ - `@append_indent_end` — decrease indent after this node
150
+ - `@append_empty_softline` — newline if multiline context, nothing if single-line
151
+ - `@delete` — remove this node from output (used to strip indent/dedent tokens)
152
+ - `@allow_blank_line_before` — permit blank lines before this node
153
+
154
+ - [ ] **Step 1: Create `tooling/tree-sitter-plum/queries/plum/format.scm`**
155
+
156
+ ```scheme
157
+ ; ============================================================
158
+ ; Top-level items — blank line between each
159
+ ; ============================================================
160
+
161
+ (source
162
+ (fn) @allow_blank_line_before)
163
+
164
+ (source
165
+ (class) @allow_blank_line_before)
166
+
167
+ (source
168
+ (enum) @allow_blank_line_before)
169
+
170
+ (source
171
+ (trait) @allow_blank_line_before)
172
+
173
+ (source
174
+ (const) @allow_blank_line_before)
175
+
176
+ ; ============================================================
177
+ ; Binary operators — space before and after
178
+ ; ============================================================
179
+
180
+ [
181
+ "+"
182
+ "-"
183
+ "*"
184
+ "/"
185
+ "%"
186
+ "^"
187
+ "|"
188
+ "&"
189
+ "<<"
190
+ ">>"
191
+ ".."
192
+ ] @prepend_space @append_space
193
+
194
+ [
195
+ "=="
196
+ "!="
197
+ "<"
198
+ "<="
199
+ ">"
200
+ ">="
201
+ "<>"
202
+ "&&"
203
+ "||"
204
+ ] @prepend_space @append_space
205
+
206
+ ; ============================================================
207
+ ; Assignment and arrows
208
+ ; ============================================================
209
+
210
+ "=" @prepend_space @append_space
211
+
212
+ "->" @prepend_space @append_space
213
+
214
+ "=>" @prepend_space @append_space
215
+
216
+ ; ============================================================
217
+ ; Separators — no space before, one space after
218
+ ; ============================================================
219
+
220
+ "," @append_space
221
+
222
+ ":" @append_space
223
+
224
+ ; ============================================================
225
+ ; Enum variant separators — new line before each |
226
+ ; ============================================================
227
+
228
+ (enum_variant
229
+ "|" @prepend_hardline)
230
+
231
+ ; ============================================================
232
+ ; Function body — each statement on its own line
233
+ ; ============================================================
234
+
235
+ (body
236
+ (_) @prepend_hardline)
237
+
238
+ ; ============================================================
239
+ ; Indent / dedent tokens — strip them (Topiary regenerates indentation)
240
+ ; ============================================================
241
+
242
+ (indent) @delete
243
+
244
+ (dedent) @delete
245
+
246
+ (newline) @delete
247
+ ```
248
+
249
+ - [ ] **Step 2: Commit**
250
+
251
+ ```bash
252
+ git add tooling/tree-sitter-plum/queries/plum/format.scm
253
+ git commit -m "feat: add Topiary format.scm formatting rules for Plum"
254
+ ```
255
+
256
+ ---
257
+
258
+ ### Task 3: Implement `formatter.rs`
259
+
260
+ **Files:**
261
+ - Modify: `plum-core/src/formatter.rs`
262
+
263
+ **Interfaces:**
264
+ - Consumes: `topiary-core`, `topiary-tree-sitter-facade`, `tree-sitter-plum::LANGUAGE`
265
+ - Produces: `pub fn format_source(source: &str) -> Result<String, FormatterError>`
266
+
267
+ The Topiary 0.7.3 API:
268
+ ```rust
269
+ // topiary_core::formatter
270
+ pub fn formatter(
271
+ input: &mut impl std::io::Read,
272
+ output: &mut impl std::io::Write,
273
+ query: &topiary_core::TopiaryQuery,
274
+ grammar: topiary_tree_sitter_facade::Language,
275
+ operation: topiary_core::Operation,
276
+ ) -> Result<(), topiary_core::FormatterError>
277
+ ```
278
+
279
+ `topiary_core::Operation::Format { skip_idempotence, tolerate_parsing_errors }` is the format variant.
280
+
281
+ `topiary_core::TopiaryQuery::new(grammar, query_text)` constructs the query.
282
+
283
+ `tree_sitter_plum::LANGUAGE` is `LanguageFn`; convert to `tree_sitter::Language` via `.into()`, then to `topiary_tree_sitter_facade::Language` via `.into()`.
284
+
285
+ - [ ] **Step 1: Replace the stub in `plum-core/src/formatter.rs`**
286
+
287
+ ```rust
288
+ use std::io::Cursor;
289
+
290
+ use topiary_core::{formatter, Operation, TopiaryQuery};
291
+ use topiary_tree_sitter_facade::Language;
292
+
293
+ const FORMAT_QUERY: &str =
294
+ include_str!("../../../../tooling/tree-sitter-plum/queries/plum/format.scm");
295
+
296
+ #[derive(Debug)]
297
+ pub enum FormatterError {
298
+ TopiaryCoreError(topiary_core::FormatterError),
299
+ }
300
+
301
+ impl std::fmt::Display for FormatterError {
302
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303
+ match self {
304
+ FormatterError::TopiaryCoreError(e) => write!(f, "Topiary error: {e}"),
305
+ }
306
+ }
307
+ }
308
+
309
+ impl std::error::Error for FormatterError {
310
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
311
+ match self {
312
+ FormatterError::TopiaryCoreError(e) => Some(e),
313
+ }
314
+ }
315
+ }
316
+
317
+ impl From<topiary_core::FormatterError> for FormatterError {
318
+ fn from(e: topiary_core::FormatterError) -> Self {
319
+ FormatterError::TopiaryCoreError(e)
320
+ }
321
+ }
322
+
323
+ pub fn format_source(source: &str) -> Result<String, FormatterError> {
324
+ let ts_language: tree_sitter::Language = tree_sitter_plum::LANGUAGE.into();
325
+ let grammar: Language = ts_language.into();
326
+ let query = TopiaryQuery::new(&grammar, FORMAT_QUERY)?;
327
+ let mut input = Cursor::new(source.as_bytes());
328
+ let mut output = Vec::new();
329
+ formatter(
330
+ &mut input,
331
+ &mut output,
332
+ &query,
333
+ grammar,
334
+ Operation::Format {
335
+ skip_idempotence: false,
336
+ tolerate_parsing_errors: false,
337
+ },
338
+ )?;
339
+ Ok(String::from_utf8(output).expect("Topiary output is valid UTF-8"))
340
+ }
341
+ ```
342
+
343
+ - [ ] **Step 2: Verify it compiles**
344
+
345
+ Run: `cargo build -p plum-core`
346
+ Expected: compiles without errors
347
+
348
+ If the `TopiaryQuery::new` signature or `formatter` signature differs, check `cargo doc --open -p topiary-core` and adjust accordingly. The 0.7.3 API may use a builder; the key types are `TopiaryQuery`, `Language` (facade), and `Operation`.
349
+
350
+ - [ ] **Step 3: Commit**
351
+
352
+ ```bash
353
+ git add plum-core/src/formatter.rs
354
+ git commit -m "feat: implement format_source wrapping topiary-core"
355
+ ```
356
+
357
+ ---
358
+
359
+ ### Task 4: Integration Tests for `format_source`
360
+
361
+ **Files:**
362
+ - Create: `plum-core/tests/formatter_test.rs`
363
+
364
+ **Interfaces:**
365
+ - Consumes: `plum_core::format_source`
366
+
367
+ - [ ] **Step 1: Create `plum-core/tests/formatter_test.rs`**
368
+
369
+ ```rust
370
+ use plum_core::format_source;
371
+
372
+ #[test]
373
+ fn formats_simple_function() {
374
+ let input = "main() =\n x = 1\n";
375
+ let result = format_source(input).expect("format_source should succeed");
376
+ // formatted output has the same content structure (exact spacing may vary)
377
+ assert!(result.contains("main()"));
378
+ assert!(result.contains("x ="));
379
+ }
380
+
381
+ #[test]
382
+ fn formats_binary_operator_spacing() {
383
+ let input = "add<Int>(a: Int, b: Int) -> Int =\n a+b\n";
384
+ let result = format_source(input).expect("format_source should succeed");
385
+ // binary operator gets spaces around it
386
+ assert!(result.contains("a + b"));
387
+ }
388
+
389
+ #[test]
390
+ fn rejects_syntax_error() {
391
+ let input = "fn @@invalid@@\n";
392
+ let result = format_source(input);
393
+ assert!(result.is_err());
394
+ }
395
+
396
+ #[test]
397
+ fn idempotent_on_already_formatted() {
398
+ let input = "main() =\n x = 1 + 2\n";
399
+ let first = format_source(input).expect("first pass");
400
+ let second = format_source(&first).expect("second pass");
401
+ assert_eq!(first, second, "formatting should be idempotent");
402
+ }
403
+ ```
404
+
405
+ - [ ] **Step 2: Run the tests**
406
+
407
+ Run: `cargo test -p plum-core`
408
+ Expected: all 4 tests pass (the idempotence test verifies Topiary's double-format check)
409
+
410
+ If `rejects_syntax_error` fails because Topiary tolerates the error anyway, change that test to verify it returns an `Err` with a descriptive message.
411
+
412
+ - [ ] **Step 3: Commit**
413
+
414
+ ```bash
415
+ git add plum-core/tests/formatter_test.rs
416
+ git commit -m "test: add integration tests for format_source"
417
+ ```
418
+
419
+ ---
420
+
421
+ ### Task 5: Create `plum-cli` Binary
422
+
423
+ **Files:**
424
+ - Create: `plum-cli/Cargo.toml`
425
+ - Create: `plum-cli/src/main.rs`
426
+
427
+ **Interfaces:**
428
+ - Consumes: `plum_core::format_source`, `plum_core::FormatterError`
429
+ - Produces: `plum format <file>`, `plum format --check <file>`, `plum format --stdin`
430
+
431
+ - [ ] **Step 1: Create `plum-cli/Cargo.toml`**
432
+
433
+ ```toml
434
+ [package]
435
+ name = "plum-cli"
436
+ version = "0.1.0"
437
+ edition = "2021"
438
+
439
+ [[bin]]
440
+ name = "plum"
441
+ path = "src/main.rs"
442
+
443
+ [dependencies]
444
+ plum-core = { path = "../plum-core" }
445
+ clap = { version = "4", features = ["derive"] }
446
+ anyhow = "1"
447
+ ```
448
+
449
+ - [ ] **Step 2: Create `plum-cli/src/main.rs`**
450
+
451
+ ```rust
452
+ use std::fs;
453
+ use std::io::{self, Read};
454
+ use std::process;
455
+
456
+ use anyhow::{Context, Result};
457
+ use clap::{Parser, Subcommand};
458
+
459
+ use plum_core::format_source;
460
+
461
+ #[derive(Parser)]
462
+ #[command(name = "plum", about = "The Plum language toolchain")]
463
+ struct Cli {
464
+ #[command(subcommand)]
465
+ command: Command,
466
+ }
467
+
468
+ #[derive(Subcommand)]
469
+ enum Command {
470
+ /// Format a Plum source file
471
+ Format {
472
+ /// File to format (omit to use --stdin)
473
+ file: Option<std::path::PathBuf>,
474
+ /// Check if file is formatted; exit 1 if it would change
475
+ #[arg(long)]
476
+ check: bool,
477
+ /// Read from stdin and write formatted source to stdout
478
+ #[arg(long)]
479
+ stdin: bool,
480
+ },
481
+ }
482
+
483
+ fn main() {
484
+ if let Err(e) = run() {
485
+ eprintln!("error: {e:#}");
486
+ process::exit(1);
487
+ }
488
+ }
489
+
490
+ fn run() -> Result<()> {
491
+ let cli = Cli::parse();
492
+ match cli.command {
493
+ Command::Format { file, check, stdin } => cmd_format(file, check, stdin),
494
+ }
495
+ }
496
+
497
+ fn cmd_format(
498
+ file: Option<std::path::PathBuf>,
499
+ check: bool,
500
+ use_stdin: bool,
501
+ ) -> Result<()> {
502
+ if use_stdin {
503
+ let mut source = String::new();
504
+ io::stdin()
505
+ .read_to_string(&mut source)
506
+ .context("failed to read stdin")?;
507
+ let formatted = format_source(&source).map_err(|e| anyhow::anyhow!("{e}"))?;
508
+ print!("{formatted}");
509
+ return Ok(());
510
+ }
511
+
512
+ let path = file.ok_or_else(|| anyhow::anyhow!("provide a file path or --stdin"))?;
513
+ let source =
514
+ fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?;
515
+ let formatted = format_source(&source).map_err(|e| anyhow::anyhow!("{e}"))?;
516
+
517
+ if check {
518
+ if source != formatted {
519
+ eprintln!("{}: would reformat", path.display());
520
+ process::exit(1);
521
+ }
522
+ return Ok(());
523
+ }
524
+
525
+ if source != formatted {
526
+ fs::write(&path, formatted.as_bytes())
527
+ .with_context(|| format!("failed to write {}", path.display()))?;
528
+ }
529
+ Ok(())
530
+ }
531
+ ```
532
+
533
+ - [ ] **Step 3: Verify build**
534
+
535
+ Run: `cargo build -p plum-cli`
536
+ Expected: produces `target/debug/plum` binary
537
+
538
+ - [ ] **Step 4: Smoke-test the binary**
539
+
540
+ ```bash
541
+ echo 'main() =\n x=1+2\n' > /tmp/smoke.plum
542
+ cargo run -p plum-cli -- format /tmp/smoke.plum
543
+ cat /tmp/smoke.plum
544
+ ```
545
+
546
+ Expected: `x = 1 + 2` (spaces around `=` and `+`)
547
+
548
+ ```bash
549
+ cargo run -p plum-cli -- format --check /tmp/smoke.plum
550
+ echo "exit: $?"
551
+ ```
552
+
553
+ Expected: exits 0 (file is already formatted after the previous run)
554
+
555
+ ```bash
556
+ echo 'main()=\n x=1\n' | cargo run -p plum-cli -- format --stdin
557
+ ```
558
+
559
+ Expected: formatted source printed to stdout
560
+
561
+ - [ ] **Step 5: Commit**
562
+
563
+ ```bash
564
+ git add plum-cli/
565
+ git commit -m "feat: add plum-cli binary with format subcommand"
566
+ ```
567
+
568
+ ---
569
+
570
+ ### Task 6: Clean Up Old Root Crate
571
+
572
+ **Files:**
573
+ - Delete: `src/` directory (the old root `src/ast.rs`, `src/parser.rs`, `src/main.rs`)
574
+
575
+ **Interfaces:**
576
+ - Consumes: nothing (cleanup only)
577
+ - Produces: clean workspace with no dangling `src/`
578
+
579
+ - [ ] **Step 1: Verify `src/` is no longer referenced**
580
+
581
+ Run: `cargo build` from workspace root
582
+ Expected: builds both `plum-core` and `plum-cli` with no errors
583
+
584
+ - [ ] **Step 2: Remove old `src/` directory**
585
+
586
+ ```bash
587
+ rm -rf src/
588
+ ```
589
+
590
+ - [ ] **Step 3: Run full workspace check**
591
+
592
+ Run: `cargo test`
593
+ Expected: all tests pass; `src/` is gone
594
+
595
+ - [ ] **Step 4: Commit**
596
+
597
+ ```bash
598
+ git add -A
599
+ git commit -m "chore: remove old root src/ after workspace migration"
600
+ ```
601
+
602
+ ---
603
+
604
+ ## Self-Review
605
+
606
+ **Spec coverage check:**
607
+
608
+ | Spec requirement | Covered by |
609
+ |---|---|
610
+ | Workspace restructure | Task 1 |
611
+ | `plum-core` with ast, parser, formatter | Tasks 1, 3 |
612
+ | `format.scm` Topiary rules | Task 2 |
613
+ | `format_source` wrapping topiary-core | Task 3 |
614
+ | Formatter integration tests | Task 4 |
615
+ | `plum format <file>` in-place | Task 5 |
616
+ | `plum format --check <file>` | Task 5 |
617
+ | `plum format --stdin` | Task 5 |
618
+ | `tolerate_parsing_errors = false` | Task 3 (`Operation::Format` field) |
619
+ | Errors to stderr, exit 1 | Task 5 (`run()` + `process::exit(1)`) |
620
+ | tree-sitter upgrade to 0.26 | Task 1 (`plum-core/Cargo.toml`) |
621
+ | Old `src/` removed | Task 6 |
622
+
623
+ **tree-sitter version note:** `topiary-core = "0.7.3"` resolves `tree-sitter = "0.26.11"`. `plum-core/Cargo.toml` must declare `tree-sitter = "0.26"` (not `"0.24.5"`) so Cargo uses a single copy. The `tree-sitter-plum` grammar crate uses `tree-sitter-language = "0.1"` at runtime (no direct tree-sitter dependency), so no patch is needed.
624
+
625
+ **`include_str!` path:** `format.scm` is embedded via a path relative to `plum-core/src/formatter.rs`, which is `../../../../tooling/tree-sitter-plum/queries/plum/format.scm`. Verify this resolves correctly after the workspace move. If it doesn't, an alternative is to add a build script that copies the file into `OUT_DIR`.