Skip to content

feat(cli): migrate update + search + archive + stats and add markdown-only guards (#267)#297

Merged
tikazyq merged 2 commits into
mainfrom
claude/implement-issue-267-npWZw
May 19, 2026
Merged

feat(cli): migrate update + search + archive + stats and add markdown-only guards (#267)#297
tikazyq merged 2 commits into
mainfrom
claude/implement-issue-267-npWZw

Conversation

@tikazyq

@tikazyq tikazyq commented May 19, 2026

Copy link
Copy Markdown
Contributor

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 through adapter.update(). Status / priority / tags / assignee resolve via the schema's semantic hints; body flags (--replace, --section, --check, --append, --content) apply pure-string utilities to the content field and work on any adapter that exposes one. Draft-skip and completion-verification guards stay markdown-scoped.
  • search — delegates to adapter.search(), resolves each hit's title via adapter.get() for a list-style render.
  • archive — calls adapter.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.rs exposes require_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, validate

A non-markdown project running any of these gets:

`gantt` requires a markdown adapter.
Active adapter: github

Run `leanspec capabilities` to see what operations are available.

Markdown adapter

SpecWriter::update_metadata now records status transitions and stamps completed_at on 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 --workspace clean
  • cargo clippy --workspace --no-deps clean
  • cargo test -p leanspec-core — 222 pass
  • cargo test -p leanspec-cli --test {archive,search,stats,update,list,board,view,create,create_template,init} — all pass
  • New tests/markdown_only_guards.rs — 11 tests pass, one per guarded command (split's --output flag clashes with the global -o/--output, so it's covered manually only)
  • Manual smoke test on a fresh markdown project: createupdate --status in-progress --add-tags backendviewsearchstatsarchive all show expected output
  • Manual smoke test against a GitHub-configured project (no token): the 12 markdown-only commands return the guard error before hitting auth

Pre-existing baseline failures (unchanged by this PR): regression::test_regression_special_characters_in_name, spec_lifecycle::* tests that exercise the still-stubbed rel, plus the still-stubbed tokens, validate, tui, tui_e2e suites — those remain on the migration roadmap and are explicitly outside this spec's scope.

Notes

  • The dead commands/tui/{app,board,detail,theme,…}.rs submodule files still import legacy SpecLoader / SpecInfo / SpecStatus / SpecPriority types but are not declared in tui/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

…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.
Copilot AI review requested due to automatic review settings May 19, 2026 03:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/stats against the Adapter API, using schema semantic hints (status/priority/tags/assignee) and the content field for body edits
  • Add commands/shared.rs with resolve_adapter, require_markdown_project, require_markdown_adapter; wire the guard into 12 markdown-only commands
  • Extend SpecWriter::update_metadata to push a StatusTransition and stamp completed_at on 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 thread rust/leanspec-cli/src/commands/stats.rs Outdated
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.
@tikazyq tikazyq merged commit 63ba262 into main May 19, 2026
2 of 3 checks passed
@tikazyq tikazyq deleted the claude/implement-issue-267-npWZw branch May 19, 2026 04:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(cli): update + search + archive + stats Migration + Markdown-Only Guards

3 participants