plum

#treesitter#compiler#wasm

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

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


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