feat(cli): migrate update + search + archive + stats and add markdown-only guards (#267)#297
Merged
Merged
Conversation
…markdown-only guards Closes #267 Routes the last four legacy commands through the adapter layer: - `update` builds an `UpdateRequest` via the fetch-transform-push pattern, resolving status / priority / tags / assignee via the schema's semantic hints and applying body utilities (replace, section, checklist toggles) to the `content` field for any adapter that exposes one. Markdown-only guards (draft skip, completion verification) stay scoped to the markdown adapter. - `search` delegates to `adapter.search()` and resolves each hit's title via `adapter.get()` so output still reads like a spec list. - `archive` calls `adapter.delete()` (status flip on markdown, issue close elsewhere) and rejects fuzzy substring matches on markdown to preserve the legacy "exact id / numeric prefix only" safety net. - `stats` groups counts by the schema's status / priority / tags semantic fields, surfacing adapter-declared enum order. Adds `require_markdown_project` to `commands/shared.rs` and wires it into the 12 commands with no non-markdown equivalent (analyze, backfill, check, compact, deps, gantt, rel, split, templates, timeline, tokens, validate) so they exit with a clear "markdown adapter required" message before any backend authentication runs. Markdown adapter writer now records status transitions and stamps `completed_at` the first time a spec reaches Complete, matching the parser-level behaviour the legacy CLI relied on.
There was a problem hiding this comment.
Pull request overview
Final batch of adapter-aware CLI migrations: update, search, archive, and stats now route through the Adapter trait instead of directly touching markdown files, while a new require_markdown_project guard provides a clear, pre-auth error for the 12 commands that have no non-markdown equivalent. The markdown writer also records status transitions and stamps completed_at so adapter-mediated updates preserve the velocity history the legacy CLI used to maintain.
Changes:
- Rewrite
update/search/archive/statsagainst theAdapterAPI, using schema semantic hints (status/priority/tags/assignee) and thecontentfield for body edits - Add
commands/shared.rswithresolve_adapter,require_markdown_project,require_markdown_adapter; wire the guard into 12 markdown-only commands - Extend
SpecWriter::update_metadatato push aStatusTransitionand stampcompleted_aton first transition into Complete
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| rust/leanspec-cli/src/commands/shared.rs | New helper module with adapter resolution + markdown-only guards |
| rust/leanspec-cli/src/commands/mod.rs | Declares the new shared submodule |
| rust/leanspec-cli/src/commands/update.rs | Adapter-aware metadata + body edits with fetch-transform-push |
| rust/leanspec-cli/src/commands/search.rs | Delegates to adapter.search; renders title via adapter.get per hit |
| rust/leanspec-cli/src/commands/archive.rs | Adapter-aware delete with exact-id requirement on markdown |
| rust/leanspec-cli/src/commands/stats.rs | Schema-driven grouping by status/priority/tags + token totals |
| rust/leanspec-cli/src/commands/{analyze,backfill,check,compact,deps,gantt,rel,split,templates,timeline,tokens,validate}.rs | Add require_markdown_project guard at top of run |
| rust/leanspec-core/src/adapters/markdown/writer.rs | Record status transitions and stamp completed_at on first Complete |
| rust/leanspec-cli/tests/markdown_only_guards.rs | New E2E tests covering the markdown-only guard for 11 commands |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+52
to
+75
| for hit in &hits { | ||
| // Title isn't part of the hit struct; pull it from the adapter so the | ||
| // user sees something more meaningful than a bare id. If the doc | ||
| // disappeared between search and read, fall back to the id. | ||
| let (title, tags) = match adapter.get(&hit.id) { | ||
| Ok(doc) => { | ||
| let title = doc.title.clone(); | ||
| let tags = doc | ||
| .fields | ||
| .get( | ||
| adapter | ||
| .schema() | ||
| .key_for_semantic(semantic::TAGS) | ||
| .unwrap_or("tags"), | ||
| ) | ||
| .and_then(|v| match v { | ||
| FieldValue::Strings(s) => Some(s.clone()), | ||
| _ => None, | ||
| }) | ||
| .unwrap_or_default(); | ||
| (title, tags) | ||
| } | ||
| Err(_) => (hit.id.clone(), Vec::new()), | ||
| }; |
Comment on lines
+112
to
+124
| // Track status transitions so the velocity history persists across | ||
| // adapter-mediated updates the same way it did on the legacy CLI path. | ||
| if frontmatter.status != previous_status { | ||
| frontmatter.transitions.push(StatusTransition { | ||
| status: frontmatter.status, | ||
| at: now, | ||
| }); | ||
| } | ||
|
|
||
| // Stamp `completed_at` the first time a spec lands in Complete so | ||
| // downstream consumers (board cards, velocity charts) don't have to | ||
| // reconstruct it from the transition log. | ||
| if frontmatter.status == SpecStatus::Complete && frontmatter.completed_at.is_none() { |
| pending_fields: &HashMap<String, FieldValue>, | ||
| ) -> Result<(), Box<dyn Error>> { | ||
| Err("`update` is not yet migrated to the adapter API".into()) | ||
| let current_status = doc.fields.get("status").and_then(|v| v.as_str()); |
Comment on lines
+149
to
+154
| let completion = stats | ||
| .by_status | ||
| .iter() | ||
| .filter(|(k, _)| matches!(k.as_str(), "complete" | "closed" | "done" | "resolved")) | ||
| .map(|(_, v)| *v) | ||
| .sum::<usize>(); |
- `cargo fmt`: format on `search.rs` and `update.rs` to make CI happy. - `search`: skip the per-hit `adapter.get` round-trip when the adapter already populated `hit.snippet`. Gives remote adapters an escape hatch from N+1 once their `search` packs enough context into the response. Doc comment now spells out the cost honestly. - `stats`: derive the "terminal" status set from the schema's declared enum options (case-insensitive match against known done-state names) instead of a hardcoded CLI list. Skips the Progress line entirely when no declared status looks terminal, rather than silently lying. - `update`: look up the markdown completion guard's current-status key via `schema.key_for_semantic(STATUS)` so the guard isn't tied to the literal `"status"` key the way the rest of `update_single` already avoids. - `writer`: gate `completed_at` stamping on an actual status transition so an unrelated metadata edit (tags, assignee) on an already-Complete spec doesn't silently back-fill a synthetic completion timestamp.
This was referenced May 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #267 — the final batch of adapter-aware CLI migrations plus markdown-only guards for commands with no non-markdown equivalent.
Migrated commands
update— fetch-transform-push throughadapter.update(). Status / priority / tags / assignee resolve via the schema's semantic hints; body flags (--replace,--section,--check,--append,--content) apply pure-string utilities to thecontentfield and work on any adapter that exposes one. Draft-skip and completion-verification guards stay markdown-scoped.search— delegates toadapter.search(), resolves each hit's title viaadapter.get()for a list-style render.archive— callsadapter.delete()(status flip on markdown, issue close on remote backends) with an adapter-aware confirmation label. Rejects fuzzy substring matches on markdown so destructive ops still need an exact id or numeric prefix.stats— groups by the schema's status / priority / tags semantic fields, preserves enum-declared display order, and includes a token total + relationship summary in--detailed.Markdown-only guards
New
commands/shared.rsexposesrequire_markdown_project(command), which inspects the configured adapter name without instantiating it — so the guard fires before any remote-backend authentication. Wired into:analyze,backfill,check,compact,deps,gantt,rel,split,templates,timeline,tokens,validateA non-markdown project running any of these gets:
Markdown adapter
SpecWriter::update_metadatanow records status transitions and stampscompleted_aton first transition to Complete, matching the parser-level behaviour the legacy CLI path relied on so adapter-mediated updates pick it up too.Test plan
cargo check --workspacecleancargo clippy --workspace --no-depscleancargo test -p leanspec-core— 222 passcargo test -p leanspec-cli --test {archive,search,stats,update,list,board,view,create,create_template,init}— all passtests/markdown_only_guards.rs— 11 tests pass, one per guarded command (split's--outputflag clashes with the global-o/--output, so it's covered manually only)create→update --status in-progress --add-tags backend→view→search→stats→archiveall show expected outputPre-existing baseline failures (unchanged by this PR):
regression::test_regression_special_characters_in_name,spec_lifecycle::*tests that exercise the still-stubbedrel, plus the still-stubbedtokens,validate,tui,tui_e2esuites — those remain on the migration roadmap and are explicitly outside this spec's scope.Notes
commands/tui/{app,board,detail,theme,…}.rssubmodule files still import legacySpecLoader/SpecInfo/SpecStatus/SpecPrioritytypes but are not declared intui/mod.rs(which is a stub), so they're not compiled. They survive as scaffolding for the eventual TUI migration; the spec's grep acceptance criterion is satisfied for all compiled CLI code.https://claude.ai/code/session_012dmoyRXqkJ2Bf384ccAU5H
Generated by Claude Code