Skip to main content

agentstatedeveloper_cli/
lib.rs

1//! agentstatedeveloper-cli — library surface.
2//!
3//! The `asd` binary is a thin wrapper over this crate. `asd-pro`
4//! (commercial) imports this as a library and extends it: it reuses
5//! [`run_oss_command`] for OSS subcommands and provides its own
6//! handlers for the ratify / audit-verify commands that OSS stubs out.
7
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::sync::OnceLock;
11
12use anyhow::Result;
13use clap::{Parser, Subcommand};
14
15use agentstatedeveloper_core::{AuditSink, RatifyOps};
16
17pub mod commands;
18pub mod config;
19// Vendored agent-skillgen; the CLI uses only part of its surface.
20#[allow(dead_code, unused_imports)]
21pub(crate) mod skillgen;
22
23pub use config::Config;
24
25/// Process-wide audit sink override. `asd-pro` sets this at startup
26/// to swap in `JsonlFileSink` when `--audit-log` / `ASD_AUDIT_LOG` is
27/// configured. OSS `asd` leaves it unset, so subcommands fall back to
28/// the default `NullSink` (with a warning if a log path was configured).
29static AUDIT_SINK_OVERRIDE: OnceLock<Arc<dyn AuditSink>> = OnceLock::new();
30
31pub fn set_audit_sink_override(sink: Arc<dyn AuditSink>) {
32    // First-write-wins: OnceLock::set returns Err if already initialized.
33    // `asd-pro` installs once at startup; later calls (test re-init, etc.)
34    // are intentional no-ops — the first sink remains authoritative.
35    let _ = AUDIT_SINK_OVERRIDE.set(sink);
36}
37
38pub(crate) fn audit_sink_override() -> Option<Arc<dyn AuditSink>> {
39    AUDIT_SINK_OVERRIDE.get().cloned()
40}
41
42/// Process-wide ratify ops override. `asd-pro` installs `RatifyOpsImpl`
43/// here before dispatch. `open_engine` wires it into every Engine instance.
44static RATIFY_OVERRIDE: OnceLock<Arc<dyn RatifyOps>> = OnceLock::new();
45
46pub fn set_ratify_ops_override(ratify: Arc<dyn RatifyOps>) {
47    // First-write-wins (see set_audit_sink_override): the OSS->pro
48    // installer runs once; double-set is a no-op by design.
49    let _ = RATIFY_OVERRIDE.set(ratify);
50}
51
52pub(crate) fn ratify_ops_override() -> Option<Arc<dyn RatifyOps>> {
53    RATIFY_OVERRIDE.get().cloned()
54}
55
56/// AgentStateDeveloper — code-level context and audit overlay.
57#[derive(Debug, Parser)]
58#[command(
59    name = "asd",
60    version,
61    about = "AgentStateDeveloper CLI",
62    // We ship our own `help` subcommand (on-demand asd/ctx feature docs), so the
63    // auto-generated clap `help` subcommand is disabled. `-h`/`--help` still work.
64    disable_help_subcommand = true,
65    long_about = "AgentStateDeveloper CLI — code-level context and audit overlay.
66
67Bootstrap a NEW repo (no sidecar yet) — manual sequence:
68  asd init                  # install git hooks, create .asd/ directory
69  asd index <path>          # walk source files; build FTS + ASG state
70  asd sync                  # write .asd/v1/ sidecar (commit this)
71  asd status                # verify (expect state: fresh, sidecar: hydrated)
72
73Bootstrap a FRESH CLONE (sidecar already in repo) — manual sequence:
74  asd init                  # install git hooks
75  asd hydrate --verify      # restore ASG from sidecar; --verify catches drift
76  asd status                # verify (index_consistency.consistent == true)
77
78One-shot alternative (recommended): `asd onboard` runs the right sequence
79automatically for either case above. Idempotent — safe to re-run.
80
81Daily loop:
82  asd prepare-change \"<task>\"   # one-call context package (use BEFORE editing)
83  asd search \"<query>\" --agent   # ranked semantic search
84  asd impact <symbol>             # blast radius for a planned change
85  asd ledger append ...           # record a decision / invariant / hazard
86
87Which command when (they overlap — pick by intent):
88  Cold repo, don't know the layout      -> asd architecture     (languages, layers, hotspots)
89  \"Where is the code that does X?\"       -> asd search \"X\"       (ranked symbols)
90  \"Explain feature X end-to-end\"         -> asd investigate X    (search + call chains + hazards)
91  About to edit — give me everything    -> asd prepare-change \"<task>\"  (one-call package)
92  I know the symbol; what breaks?       -> asd impact <symbol>           (blast radius)
93  Pre-edit safety checklist             -> asd checklist \"<task>\"
94  Reviewing a diff / PR                 -> asd since <base-sha>
95  Pull full context on named symbols    -> asd context-for a,b,c
96
97When source changes outside ASD's commit hooks, re-run `asd index <path>`
98to refresh the FTS cache. `asd status` will flag staleness if the index
99is more than an hour behind."
100)]
101pub struct Cli {
102    /// Path to the ASD SQLite database. Overrides `ASD_DB` env var.
103    #[arg(long, global = true)]
104    pub db: Option<PathBuf>,
105
106    /// Path to a JSON policy file evaluated against ledger/effect writes.
107    /// Overrides `ASD_POLICY` env var. When absent, solo-dev default is
108    /// permissive (everything Allow).
109    #[arg(long, global = true)]
110    pub policy: Option<PathBuf>,
111
112    /// Path to an append-only JSONL audit log. Every ledger/effect
113    /// mutation emits one event. Overrides `ASD_AUDIT_LOG` env var.
114    /// When absent, the audit sink discards events.
115    #[arg(long, global = true)]
116    pub audit_log: Option<PathBuf>,
117
118    /// Plan D t-001: emit compact output projecting each command down to
119    /// load-bearing fields (qname, file:line, signature, first doc line).
120    /// 60-80% token reduction vs default. Also honors `ASD_FORMAT=brief`.
121    /// `--json` (default) keeps the structured payload.
122    #[arg(long, global = true)]
123    pub brief: bool,
124
125    #[command(subcommand)]
126    pub cmd: Command,
127}
128
129#[derive(Debug, Subcommand)]
130pub enum Command {
131    /// On-demand feature docs: syntax, examples, gotchas. `asd help` lists the
132    /// catalog; `asd help <feature>` details one; `--publish` writes the shared
133    /// cross-tool help index.
134    Help(commands::help::HelpArgs),
135
136    /// Initialize an ASD repository and install git hooks.
137    Init(commands::init::InitArgs),
138
139    /// Plan K t-005: one-shot post-clone setup. Runs `init → index →
140    /// conclusions import` in the right order so a new developer (or
141    /// their agent) gets a fully usable ASD project in one command.
142    /// Idempotent — re-runs are safe.
143    Onboard(commands::onboard::OnboardArgs),
144
145    /// Show installed ASD git hooks and their current status.
146    Hooks(commands::hooks::HooksArgs),
147
148    /// Walk source files under a directory and build the FTS + ASG
149    /// index. Re-runnable; idempotent. (Aliased as `reindex` to
150    /// match the MCP `mcp__asd__reindex` tool name.)
151    #[command(alias = "reindex")]
152    Index(commands::index::IndexArgs),
153
154    /// Read a symbol, its effects, and recent ledger entries.
155    /// Plan D t-003: also accepts `code_read` (MCP-era alias).
156    #[command(alias = "code_read")]
157    Read(commands::read::ReadArgs),
158
159    /// Ledger operations.
160    #[command(subcommand)]
161    Ledger(commands::ledger::LedgerCmd),
162
163    /// Policy introspection (requires --policy / ASD_POLICY).
164    #[command(subcommand)]
165    Policy(commands::policy::PolicyCmd),
166
167    /// Verify a symbol's declared effects against its source. Reports each as
168    /// verified or unverified (unverified = source unreadable or no adapter for
169    /// that language). `--write` persists results. Use before trusting
170    /// `impact`'s effect set on a symbol you're about to edit.
171    VerifyEffects(commands::verify_effects::VerifyEffectsArgs),
172
173    /// Run a program under the ASD runtime tracer and ingest observed effects
174    /// (Python only — uses sys.settrace via tools/asd_tracer.py).
175    Trace(commands::trace::TraceArgs),
176
177    /// Mirror ASG state into the `.asd/v1/` on-disk sidecar so it can
178    /// travel with `git commit`.
179    Sync(commands::sync::SyncArgs),
180
181    /// Read the `.asd/v1/` sidecar back into ASG. Inverse of `sync`.
182    /// Used to restore state after a fresh `git clone`.
183    Hydrate(commands::hydrate::HydrateArgs),
184
185    /// Read back audit events (ledger mutations, policy evaluations,
186    /// effect declarations) emitted to the JSONL audit log.
187    #[command(subcommand)]
188    Audit(commands::audit::AuditCmd),
189
190    /// Install, uninstall, or check asd-mcp registration in agent tools.
191    #[command(subcommand)]
192    Mcp(commands::mcp::McpCmd),
193
194    /// Install ASD's agent Skill (SKILL.md) into detected agent skill dirs.
195    Skill(commands::skill::SkillArgs),
196
197    /// Print a paste-into-your-agent block that installs + connects ASD (+ CTX).
198    Bootstrap(commands::bootstrap::BootstrapArgs),
199
200    /// Watch the repo and re-index automatically on source changes.
201    Watch(commands::watch::WatchArgs),
202
203    /// List indexed symbols, effects, or ledger entries.
204    List(commands::list::ListArgs),
205
206    /// Show symbols that call the given symbol (direct or transitive).
207    /// Plan D t-003: also accepts `callers_of` (MCP-era alias).
208    #[command(alias = "callers_of")]
209    Callers(commands::graph::CallersArgs),
210
211    /// Show symbols called by the given symbol (direct or transitive).
212    /// Plan D t-003: also accepts `callees_of` (MCP-era alias).
213    #[command(alias = "callees_of")]
214    Callees(commands::graph::CalleesArgs),
215
216    /// Assemble agent query context for one or more symbols.
217    /// Returns signature, callers/callees, effects, invariants, hazards, and ledger.
218    #[command(name = "context-for")]
219    ContextFor(commands::context_for::ContextForArgs),
220
221    /// Scan the ASG for integrity issues (orphaned refs, malformed blobs, stale
222    /// call graph edges) and optionally apply safe auto-corrections.
223    /// By default runs read-only (dry-run); pass `--fix` to apply corrections.
224    Repair(commands::repair::RepairArgs),
225
226    /// Garbage-collect the ASG store: preview by default, reporting what a
227    /// sweep would reclaim; `--sweep` deletes, `--vacuum` shrinks the file, and
228    /// `--unpin-legacy` releases milestones distilled before AgentStateGraph
229    /// v1.2.2. Takes the store's write lock while sweeping, so other ASD
230    /// writers wait rather than lose data.
231    Gc(commands::gc::GcArgs),
232
233    /// Working notes scoped to a symbol or investigation, with a
234    /// promote-to-ledger path. Local-only; not synced by `asd sync`.
235    #[command(subcommand)]
236    Scratch(commands::scratch::ScratchCmd),
237
238    /// Plan-scoped git worktrees: isolated files + HEAD per unit of work so
239    /// parallel agents can't clobber each other. `start`, work, then `finish`
240    /// to merge back + tear down.
241    #[command(subcommand)]
242    Worktree(commands::worktree::WorktreeCmd),
243
244    /// Ranked concept search over indexed symbols.
245    /// Plan D t-003: also accepts `code_search` and `code_query` (MCP-era aliases).
246    #[command(aliases = ["code_search", "code_query"])]
247    Search(commands::search::SearchArgs),
248
249    /// Exact-symbol references via literal text scan + index definition lookup.
250    /// Use this when you want rg-style completeness on a concrete identifier
251    /// (no tokenization, no BM25). Requires `rg` on PATH.
252    References(commands::references::ReferencesArgs),
253
254    /// List named scope aliases defined in `.asd/scopes.toml`. Discoverability
255    /// for the `--scope` and `--paths` flags supported by search and friends.
256    #[command(subcommand)]
257    Scopes(commands::scopes::ScopesCmd),
258
259    /// View ledger entries bucketed by the six Plan B conclusion classes
260    /// (decisions, classifications, mappings, hazards, recipes, followups).
261    #[command(subcommand)]
262    Conclusions(commands::conclusions::ConclusionsCmd),
263
264    /// Plan C t-004: structured change-intent recipes. Returns per-file
265    /// action plans (Delete / Gate / Run / KeepAsCovered / Review) for
266    /// known task families.
267    #[command(subcommand)]
268    Recipe(commands::recipe::RecipeCmd),
269
270    /// Plan C t-007: initial-read project summary. Walks the indexed
271    /// project, identifies package boundaries, and tags test files
272    /// (fast-test / diagnostic-test). Writes Ownership ledger entries
273    /// with role tags so the next session inherits the mental model.
274    Map(commands::map::MapArgs),
275
276    /// Plan G t-003: capture agent thinking. Subcommands: speculate
277    /// (Hypothesis), model (MentalModel), failed (FailedAttempt),
278    /// question (OpenQuestion), list. See docs/initial-read-prompt.md.
279    #[command(subcommand)]
280    Think(commands::think::ThinkCmd),
281
282    /// Sidecar utilities. `migrate` flips a repo from the legacy `.asd/v1/`
283    /// layout (Plan A) to the compact `.asd/conclusions/` layout (Plan B).
284    #[command(subcommand)]
285    Sidecar(commands::sidecar::SidecarCmd),
286
287    /// Broad feature archaeology: search → expand call chains, invariants,
288    /// hazards, and effects for the top matching entry points in one pass.
289    Investigate(commands::investigate::InvestigateArgs),
290
291    /// Show index health: age, symbol count, and optionally dirty source files.
292    Status(commands::status::StatusArgs),
293
294    /// Blast-radius analysis before editing a symbol: transitive callers,
295    /// aggregated effects, invariants/hazards, affected tests, and recent git touches.
296    Impact(commands::impact::ImpactArgs),
297
298    /// Structured pre-edit checklist: files to inspect, invariants to preserve,
299    /// tests to run, known hazards, and effects to verify. Markdown or JSON output.
300    Checklist(commands::checklist::ChecklistArgs),
301
302    /// Record, list, and remove invariants attached to symbols.
303    /// Shortcut for `asd ledger {append,list,withdraw} --kind invariant`.
304    #[command(subcommand)]
305    Invariant(commands::invariant::InvariantCmd),
306
307    /// One-call agent-ready context package for a planned change: design invariants,
308    /// layer-grouped entry points, likely edit files, affected tests, effects, and
309    /// recent git touches — all composed in a single JSON response.
310    #[command(name = "prepare-change")]
311    PrepareChange(commands::prepare_change::PrepareChangeArgs),
312
313    /// Symbols in files changed since a commit + combined blast radius.
314    /// PR-review workflow: pass the base SHA to get full impact without knowing
315    /// any symbol names upfront. Supports --agent, --intent, --depth.
316    Since(commands::since::SinceArgs),
317
318    /// List cross-service endpoints (HTTP routes/clients, pub-sub) and match
319    /// in-repo client calls to the routes that serve them. A client call with NO
320    /// matching route is a candidate contract-drift signal (a caller reaching an
321    /// endpoint the server no longer serves). `--export` emits a service manifest
322    /// for cross-repo matching.
323    Endpoints(commands::endpoints::EndpointsArgs),
324
325    /// One-call "orient me" overview: languages, packages, layers, routes, and
326    /// call-graph hotspots for a cold agent. Supports --agent.
327    Architecture(commands::architecture::ArchitectureArgs),
328
329    /// Functions/methods with no inbound call edges (CANDIDATE dead code).
330    /// Excludes route handlers, tests, and main/dunder methods. Candidates only —
331    /// a symbol may still be reached via reflection, a framework entrypoint, or a
332    /// cross-language boundary the call graph doesn't span; confirm with
333    /// `asd references <name>` before deleting. Supports --agent.
334    DeadCode(commands::dead_code::DeadCodeArgs),
335
336    /// Read test-runner output on stdin; emit a compact failures-only summary
337    /// (cargo/pytest parsed precisely, others via generic scan). Supports --agent.
338    TestSummary(commands::test_summary::TestSummaryArgs),
339
340    /// Record and list search-quality feedback verdicts for (query, symbol) pairs.
341    #[command(subcommand)]
342    Feedback(commands::feedback::FeedbackCmd),
343
344    /// Derive ledger annotations from a git commit and optionally write them.
345    /// Reads changed files and the commit message, resolves touched symbols,
346    /// and suggests (or records) decisions, invariants, proofs, and hazards.
347    #[command(name = "annotate-commit")]
348    AnnotateCommit(commands::annotate_commit::AnnotateCommitArgs),
349
350    /// Close an active task: write proof and optional validation entries to the
351    /// ledger for all symbols affected by HEAD, tagged with CTX plan/task provenance.
352    #[command(name = "task-close")]
353    TaskClose(commands::task_close::TaskCloseArgs),
354
355    /// Benchmark scorecard across the five ASD dimensions:
356    /// truth, feedback, change, uncertainty, and workflow.
357    Scorecard(commands::scorecard::ScorecardArgs),
358
359    /// Golden benchmark harness: run structural assertions against ASD
360    /// command output to catch ranking/classification regressions.
361    Probe(commands::probe::ProbeCmd),
362
363    /// State Trust Score: machine-readable rollup of index freshness, sidecar
364    /// status, ledger density, dirty files, and concept gaps. Answers: "can I
365    /// rely on ASD for the current task?" in a single call.
366    Trust(commands::trust::TrustArgs),
367
368    /// Task workflow session history: evidence quality, workflow type, and
369    /// missing steps across recent `asd task-close` invocations.
370    Workflow(commands::workflow::WorkflowArgs),
371
372    /// Manage the shared ASD repo registry at ~/.config/asd/repos.toml.
373    /// Subcommands: add, list, use, rm, show.
374    #[command(subcommand)]
375    Repo(commands::repo::RepoCmd),
376}
377
378/// Resolve [`Config`] from the parsed CLI flags.
379pub fn config_from_cli(cli: &Cli) -> Config {
380    // init/onboard/index/hydrate create or (re)populate a db in the current
381    // location, so they must resolve to `./.asd-state.db` (or --db/ASD_DB) only —
382    // never the walk-up parent or the registry's active repo (Plan Q t-002
383    // scoping). hydrate was missing from this list: run in a directory holding
384    // only a sidecar, it fell through to the registry's active repo and loaded
385    // one project's symbols and ledger into another project's store.
386    let local_only = matches!(
387        cli.cmd,
388        Command::Init(_) | Command::Onboard(_) | Command::Index(_) | Command::Hydrate(_)
389    );
390    // `hydrate --dir <d>` fills the store beside the sidecar it reads, and
391    // `index <dir>` uses the store of the directory it indexes — not one in
392    // whatever directory `asd` ran from. An explicit --db or ASD_DB still wins.
393    let explicit_db = cli.db.clone().or_else(|| match &cli.cmd {
394        Command::Hydrate(args) if std::env::var_os("ASD_DB").is_none() => {
395            args.dir.as_ref().map(|d| d.join(".asd-state.db"))
396        }
397        Command::Index(args) if std::env::var_os("ASD_DB").is_none() => std::env::current_dir()
398            .ok()
399            .map(|cwd| index_store(&args.path, &cwd)),
400        _ => None,
401    });
402    Config::resolve_with_brief(
403        explicit_db,
404        cli.policy.clone(),
405        cli.audit_log.clone(),
406        cli.brief,
407        local_only,
408    )
409}
410
411/// The store `asd index <path>` uses when neither `--db` nor `ASD_DB` names
412/// one. It was always `./.asd-state.db`, so indexing a project from its parent
413/// directory (`asd index SessionDrift-ios` from `Apps/`) built and registered a
414/// new store in the parent instead of using the one the project has.
415///
416/// - the current directory, or a file: `./.asd-state.db`, as before;
417/// - a directory with a store of its own: that store;
418/// - a directory inside the current project (the current directory has a
419///   store): `./.asd-state.db`, as before;
420/// - any other directory: a new store in it, beside what it indexes.
421fn index_store(path: &Path, cwd: &Path) -> PathBuf {
422    let here = PathBuf::from("./.asd-state.db");
423    let (Ok(dir), Ok(cwd)) = (cwd.join(path).canonicalize(), cwd.canonicalize()) else {
424        return here;
425    };
426    if !dir.is_dir() || dir == cwd {
427        return here;
428    }
429    let own = dir.join(".asd-state.db");
430    if own.exists() {
431        return own;
432    }
433    if cwd.join(".asd-state.db").exists() && dir.starts_with(&cwd) {
434        return here;
435    }
436    own
437}
438
439/// Dispatch the OSS command set. `asd-pro` can call this for any
440/// subcommand that does not require a commercial override.
441pub fn run(cli: Cli) -> Result<()> {
442    let cfg = config_from_cli(&cli);
443    run_with_config(&cfg, cli.cmd)
444}
445
446/// Same as [`run`] but uses an already-resolved [`Config`]. `asd-pro`
447/// uses this after customizing the audit sink / ledger store.
448pub fn run_with_config(cfg: &Config, cmd: Command) -> Result<()> {
449    use commands::*;
450    match cmd {
451        Command::Help(args) => help::run(cfg, args),
452        Command::Init(args) => init::run(cfg, args),
453        Command::Onboard(args) => onboard::run(cfg, args),
454        Command::Hooks(args) => hooks::run(cfg, args),
455        Command::Index(args) => index::run(cfg, args),
456        Command::Read(args) => read::run(cfg, args),
457        Command::Ledger(sub) => ledger::run(cfg, sub),
458        Command::Policy(sub) => policy::run(cfg, sub),
459        Command::VerifyEffects(args) => verify_effects::run(cfg, args),
460        Command::Trace(args) => trace::run(cfg, args),
461        Command::Sync(args) => sync::run(cfg, args),
462        Command::Gc(args) => gc::run(cfg, args),
463        Command::Hydrate(args) => hydrate::run(cfg, args),
464        Command::Audit(sub) => audit::run(cfg, sub),
465        Command::Mcp(sub) => mcp::run(cfg, sub),
466        Command::Skill(args) => skill::run(args),
467        Command::Bootstrap(args) => bootstrap::run(args),
468        Command::Watch(args) => watch::run(cfg, args),
469        Command::List(args) => list::run(cfg, args),
470        Command::Callers(args) => graph::run_callers(cfg, args),
471        Command::Callees(args) => graph::run_callees(cfg, args),
472        Command::ContextFor(args) => context_for::run(cfg, args),
473        Command::Repair(args) => repair::run(cfg, args),
474        Command::Scratch(cmd) => scratch::run(cfg, cmd),
475        Command::Worktree(cmd) => worktree::run(cmd),
476        Command::Search(args) => search::run(cfg, args),
477        Command::References(args) => references::run(cfg, args),
478        Command::Scopes(cmd) => scopes::run(cfg, cmd),
479        Command::Conclusions(cmd) => conclusions::run(cfg, cmd),
480        Command::Recipe(cmd) => recipe::run(cfg, cmd),
481        Command::Map(args) => map::run(cfg, args),
482        Command::Think(cmd) => think::run(cfg, cmd),
483        Command::Sidecar(cmd) => sidecar::run(cfg, cmd),
484        Command::Investigate(args) => investigate::run(cfg, args),
485        Command::Status(args) => status::run(cfg, args),
486        Command::Impact(args) => impact::run(cfg, args),
487        Command::Checklist(args) => checklist::run(cfg, args),
488        Command::Invariant(sub) => invariant::run(cfg, sub),
489        Command::PrepareChange(args) => prepare_change::run(cfg, args),
490        Command::Since(args) => since::run(cfg, args),
491        Command::Endpoints(args) => endpoints::run(cfg, args),
492        Command::Architecture(args) => architecture::run(cfg, args),
493        Command::DeadCode(args) => dead_code::run(cfg, args),
494        Command::TestSummary(args) => test_summary::run(cfg, args),
495        Command::Feedback(sub) => feedback::run(cfg, sub),
496        Command::AnnotateCommit(args) => annotate_commit::run(cfg, args),
497        Command::TaskClose(args) => task_close::run(cfg, args),
498        Command::Scorecard(args) => scorecard::run(cfg, args),
499        Command::Probe(cmd) => probe::run(cfg, cmd),
500        Command::Trust(args) => trust::run(cfg, args),
501        Command::Workflow(args) => workflow::run(cfg, args),
502        Command::Repo(cmd) => repo::run(cfg, cmd),
503    }
504}
505
506#[cfg(test)]
507mod index_store_tests {
508    use super::index_store;
509    use std::path::PathBuf;
510
511    fn touch_store(dir: &std::path::Path) {
512        std::fs::create_dir_all(dir).unwrap();
513        std::fs::write(dir.join(".asd-state.db"), b"").unwrap();
514    }
515
516    #[test]
517    fn the_current_directory_and_files_keep_the_local_store() {
518        let tmp = tempfile::tempdir().unwrap();
519        let cwd = tmp.path().canonicalize().unwrap();
520        std::fs::write(cwd.join("a.py"), b"").unwrap();
521        let here = PathBuf::from("./.asd-state.db");
522        assert_eq!(index_store(&PathBuf::from("."), &cwd), here);
523        assert_eq!(index_store(&cwd, &cwd), here);
524        assert_eq!(index_store(&PathBuf::from("a.py"), &cwd), here);
525    }
526
527    #[test]
528    fn a_project_indexed_from_its_parent_uses_its_own_store() {
529        let tmp = tempfile::tempdir().unwrap();
530        let apps = tmp.path().canonicalize().unwrap();
531        touch_store(&apps.join("proj"));
532        assert_eq!(
533            index_store(&PathBuf::from("proj"), &apps),
534            apps.join("proj/.asd-state.db")
535        );
536        // From anywhere, by absolute path, even with a store of your own here.
537        touch_store(&apps.join("other"));
538        assert_eq!(
539            index_store(&apps.join("proj"), &apps.join("other")),
540            apps.join("proj/.asd-state.db")
541        );
542    }
543
544    #[test]
545    fn a_subdirectory_of_the_current_project_uses_its_store() {
546        let tmp = tempfile::tempdir().unwrap();
547        let proj = tmp.path().canonicalize().unwrap();
548        touch_store(&proj);
549        std::fs::create_dir_all(proj.join("src")).unwrap();
550        assert_eq!(
551            index_store(&PathBuf::from("src"), &proj),
552            PathBuf::from("./.asd-state.db")
553        );
554    }
555
556    #[test]
557    fn a_directory_with_no_store_gets_one_of_its_own() {
558        let tmp = tempfile::tempdir().unwrap();
559        let apps = tmp.path().canonicalize().unwrap();
560        std::fs::create_dir_all(apps.join("fresh")).unwrap();
561        assert_eq!(
562            index_store(&PathBuf::from("fresh"), &apps),
563            apps.join("fresh/.asd-state.db"),
564            "would have built a store in the parent"
565        );
566    }
567}