commit 24739cb270f14e855e910f956ba461b3034d3525 Author: teernisse Date: Thu Feb 12 12:33:05 2026 -0500 bd-a7e: Bootstrap Rust project and directory structure diff --git a/.beads/.gitignore b/.beads/.gitignore new file mode 100644 index 0000000..f32e807 --- /dev/null +++ b/.beads/.gitignore @@ -0,0 +1,11 @@ +# Database +*.db +*.db-shm +*.db-wal + +# Lock files +*.lock + +# Temporary +last-touched +*.tmp diff --git a/.beads/config.yaml b/.beads/config.yaml new file mode 100644 index 0000000..4e54a22 --- /dev/null +++ b/.beads/config.yaml @@ -0,0 +1,4 @@ +# Beads Project Configuration +# issue_prefix: bd +# default_priority: 2 +# default_type: task diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl new file mode 100644 index 0000000..a8d02f1 --- /dev/null +++ b/.beads/issues.jsonl @@ -0,0 +1,50 @@ +{"id":"bd-132","title":"Add semantic search with embeddings","description":"## What\nAdd semantic/fuzzy search as alternative to text search. Compute embeddings for endpoint descriptions and schema names during index building. Use cosine similarity for ranking.\n\n## Acceptance Criteria\n- [ ] --semantic flag enables embedding-based search\n- [ ] Results ranked by cosine similarity\n- [ ] Fallback to text search when embeddings unavailable\n\n## Files\n- CREATE: src/core/embeddings.rs\n- MODIFY: src/core/search.rs (add semantic search mode)","status":"open","priority":4,"issue_type":"task","created_at":"2026-02-12T16:31:57.268115Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:42:45.253877Z","compaction_level":0,"original_size":0,"labels":["future","phase3"],"dependencies":[{"issue_id":"bd-132","depends_on_id":"bd-2e4","type":"blocks","created_at":"2026-02-12T16:42:45.253860Z","created_by":"tayloreernisse"},{"issue_id":"bd-132","depends_on_id":"bd-3aq","type":"parent-child","created_at":"2026-02-12T16:31:57.269278Z","created_by":"tayloreernisse"}]} +{"id":"bd-161","title":"Epic: Sync and Updates","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-12T16:22:23.251895Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:22:23.253943Z","compaction_level":0,"original_size":0,"labels":["epic"]} +{"id":"bd-16o","title":"Wire fetch command with local file, stdin, and remote URL support","description":"## Background\nThe fetch command is the primary entry point for getting specs into swagger-cli. It orchestrates: HTTP download (or local file/stdin read), format detection, YAML normalization, index building, pointer validation, and crash-consistent cache write. It supports auth headers, bearer tokens, auth profiles, --force overwrite, and robot output.\n\n## Approach\nImplement src/cli/fetch.rs with FetchArgs struct and async execute() function:\n\n**FetchArgs (clap derive):**\n- url: String (positional, required — can be URL, file path, or \"-\" for stdin)\n- alias: String (--alias, required)\n- header: Vec (--header, repeatable)\n- auth_header: Vec (--auth-header, alias for --header)\n- bearer: Option (--bearer)\n- auth_profile: Option (--auth-profile)\n- force: bool (--force)\n- timeout_ms: u64 (--timeout-ms, default 10000)\n- max_bytes: u64 (--max-bytes, default 26214400)\n- retries: u32 (--retries, default 2)\n- input_format: Option (--input-format, values: auto/json/yaml)\n- resolve_external_refs: bool (--resolve-external-refs)\n- ref_allow_host: Vec (--ref-allow-host, repeatable)\n- ref_max_depth: u32 (--ref-max-depth, default 3)\n- ref_max_bytes: u64 (--ref-max-bytes, default 10MB)\n- allow_private_host: Vec (--allow-private-host, repeatable)\n- allow_insecure_http: bool (--allow-insecure-http)\n\n**Execute flow:**\n1. Validate alias format (validate_alias)\n2. Check if alias exists — error unless --force\n3. Resolve auth (merge --auth-profile with explicit headers, --bearer)\n4. Determine source: URL (http/https), local file (file:// or path), or stdin (-)\n5. For URL: use AsyncHttpClient.fetch_spec() with SSRF/HTTPS policy\n6. For local file: canonicalize path, read bytes directly (no network policy)\n7. For stdin: read all bytes from stdin\n8. Detect format, normalize to JSON\n9. Parse as serde_json::Value\n10. Build index (build_index)\n11. Write cache (write_cache with all artifacts)\n12. Output robot JSON or human success message\n\n## Acceptance Criteria\n- [ ] `swagger-cli fetch ./petstore.json --alias pet` succeeds (local file)\n- [ ] `swagger-cli fetch https://... --alias pet --robot` outputs JSON with ok:true, data.alias, data.endpoint_count\n- [ ] `swagger-cli fetch - --alias stdin-api` reads from stdin\n- [ ] Alias validation rejects \"../bad\" before any network call\n- [ ] Existing alias without --force returns ALIAS_EXISTS (exit 6)\n- [ ] --force overwrites existing alias\n- [ ] --bearer TOKEN adds Authorization: Bearer TOKEN header\n- [ ] --auth-profile loads from config.toml\n- [ ] Robot output includes: alias, url, version, title, endpoint_count, schema_count, cached_at, source_format, cache_dir, files, content_hash\n- [ ] Auth header values never appear in output or error messages\n\n## Files\n- MODIFY: src/cli/fetch.rs (FetchArgs, execute, auth resolution, source routing)\n- MODIFY: src/output/robot.rs (add output_fetch function)\n- MODIFY: src/output/human.rs (add output_fetch function)\n\n## TDD Anchor\nRED: Write integration test `test_fetch_local_file` — use assert_cmd to run `swagger-cli fetch tests/fixtures/petstore.json --alias test-pet --robot` with SWAGGER_CLI_HOME set to tempdir. Assert exit 0, stdout JSON has ok:true.\nGREEN: Implement full fetch pipeline.\nVERIFY: `cargo test test_fetch_local_file`\n\nAdditional tests:\n- test_fetch_alias_exists_error\n- test_fetch_force_overwrites\n- test_fetch_stdin\n- test_fetch_bearer_auth\n- test_fetch_yaml_file (if YAML fixture exists)\n\n## Edge Cases\n- stdin (\"-\") is not a URL — don't try to HTTP-fetch it\n- Local file paths must be canonicalized (resolve symlinks, relative paths) before reading\n- file:// URLs must be converted to local paths (strip scheme)\n- If auth-profile references an EnvVar source, resolve the env var at fetch time\n- Robot output: redact --bearer and --auth-header values even in success output\n\n## Dependency Context\nUses AsyncHttpClient from bd-3b6 (async HTTP client with SSRF protection). Uses build_index, detect_format, normalize_to_json from bd-189 (spec format detection and index building). Uses CacheManager.write_cache and validate_alias from bd-1ie (cache write path). Uses Config for auth profiles from bd-1sb (configuration system).","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:26:35.220966Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:55:46.975549Z","compaction_level":0,"original_size":0,"labels":["fetch","phase1"],"dependencies":[{"issue_id":"bd-16o","depends_on_id":"bd-189","type":"blocks","created_at":"2026-02-12T16:34:06.059316Z","created_by":"tayloreernisse"},{"issue_id":"bd-16o","depends_on_id":"bd-1ie","type":"blocks","created_at":"2026-02-12T16:34:06.154074Z","created_by":"tayloreernisse"},{"issue_id":"bd-16o","depends_on_id":"bd-1sb","type":"blocks","created_at":"2026-02-12T16:34:06.248426Z","created_by":"tayloreernisse"},{"issue_id":"bd-16o","depends_on_id":"bd-3b6","type":"blocks","created_at":"2026-02-12T16:34:06.005417Z","created_by":"tayloreernisse"},{"issue_id":"bd-16o","depends_on_id":"bd-3d2","type":"blocks","created_at":"2026-02-12T16:26:35.222663Z","created_by":"tayloreernisse"},{"issue_id":"bd-16o","depends_on_id":"bd-3ny","type":"parent-child","created_at":"2026-02-12T16:26:35.222248Z","created_by":"tayloreernisse"}]} +{"id":"bd-189","title":"Implement spec format detection, YAML normalization, and index building","description":"## Background\nAfter downloading raw spec bytes, swagger-cli must detect the input format (JSON vs YAML), normalize YAML to JSON, parse the spec as serde_json::Value, and build a SpecIndex from it. The index building is the heart of the query performance -- it pre-extracts endpoints, schemas, and tags into a compact, sorted structure with JSON pointers back to raw.json. All pointers must be validated.\n\n## Approach\nCreate src/core/indexer.rs with:\n\n**Format detection:** `detect_format(bytes, filename_hint, content_type_hint) -> Format` where Format is Json or Yaml. Priority: explicit --input-format flag > Content-Type header > file extension > content sniffing (try JSON parse first, fall back to YAML).\n\n**YAML normalization:** `normalize_to_json(bytes, format) -> Result>` -- if YAML, parse with serde_yaml then serialize to serde_json. If JSON, pass through (validate it parses).\n\n**Index building:** `build_index(raw_json: &serde_json::Value, content_hash: &str, generation: u64) -> Result`:\n1. Extract info.title, info.version, openapi version\n2. Iterate paths.* -> methods -> build IndexedEndpoint with: path, method (uppercased), summary, description, operation_id, tags, deprecated, parameters (name/location/required/desc), request_body_required, request_body_content_types, security_schemes (effective: operation-level overrides root-level), security_required, operation_ptr (JSON pointer format: /paths/~1pet~1{petId}/get)\n3. Iterate components.schemas.* -> build IndexedSchema with name, schema_ptr\n4. Compute tags from endpoints + root-level tags -> IndexedTag with endpoint_count\n5. Sort deterministically: endpoints by (path ASC, method_rank ASC), schemas by (name ASC), tags by (name ASC)\n6. Validate ALL operation_ptr and schema_ptr resolve in the raw Value\n7. Set index_version to current version constant (1)\n\n**Canonical method ranking:** GET=0, POST=1, PUT=2, PATCH=3, DELETE=4, OPTIONS=5, HEAD=6, TRACE=7, unknown=99.\n\n**JSON pointer encoding:** Path segments use RFC 6901: `~` -> `~0`, `/` -> `~1`. So `/pet/{petId}` becomes `/paths/~1pet~1{petId}`.\n\n## Acceptance Criteria\n- [ ] JSON input detected and passed through correctly\n- [ ] YAML input detected and normalized to equivalent JSON\n- [ ] build_index extracts correct endpoint count from petstore spec (19 endpoints)\n- [ ] Endpoints sorted by path ASC, method_rank ASC (deterministic)\n- [ ] Schemas sorted by name ASC\n- [ ] Tags have correct endpoint_count\n- [ ] All operation_ptr values resolve in raw Value (validated during build_index)\n- [ ] Invalid pointer causes fetch failure (not silent corruption)\n- [ ] JSON pointer encoding handles /pet/{petId} correctly (escapes /)\n- [ ] Security inheritance: operation without security inherits root; operation with empty [] means no auth\n\n## Files\n- CREATE: src/core/indexer.rs (detect_format, normalize_to_json, build_index, method_rank, json_pointer_encode, validate_pointers)\n- MODIFY: src/core/mod.rs (pub mod indexer;)\n\n## TDD Anchor\nRED: Write `test_build_index_petstore` -- load tests/fixtures/petstore.json as Value, call build_index, assert endpoint_count == 19 and endpoints are sorted.\nGREEN: Implement full index building.\nVERIFY: `cargo test test_build_index_petstore`\n\nAdditional tests:\n- test_detect_format_json, test_detect_format_yaml\n- test_yaml_normalization_roundtrip\n- test_json_pointer_encoding\n- test_method_rank_ordering\n- test_security_inheritance\n- test_invalid_pointer_rejected\n\n## Edge Cases\n- Some specs have paths with special chars in operation IDs -- don't assume alphanumeric\n- serde_yaml may produce different JSON than direct JSON parse (number types, null handling) -- normalize consistently\n- Large specs (8MB+ GitHub) should still build index in <1s\n- OpenAPI 3.1 may use `webhooks` key -- ignore for MVP (only extract from `paths`)\n- Tags defined at root level but not used by any operation should still appear with endpoint_count=0\n\n## Dependency Context\nUses SpecIndex, IndexedEndpoint, IndexedSchema, IndexedTag, IndexedParam types from bd-ilo (error types + core models bead).","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:26:35.194671Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:45:39.762612Z","compaction_level":0,"original_size":0,"labels":["fetch","phase1"],"dependencies":[{"issue_id":"bd-189","depends_on_id":"bd-3ny","type":"parent-child","created_at":"2026-02-12T16:26:35.195659Z","created_by":"tayloreernisse"},{"issue_id":"bd-189","depends_on_id":"bd-ilo","type":"blocks","created_at":"2026-02-12T16:26:35.196142Z","created_by":"tayloreernisse"}]} +{"id":"bd-1bp","title":"Implement fetch-time external ref bundling","description":"## Background\nOptional fetch-time external ref bundling. When --resolve-external-refs is passed during fetch, external $ref targets are fetched and inlined into raw.json. Requires explicit --ref-allow-host allowlist. Bounded by --ref-max-depth and --ref-max-bytes.\n\n## Approach\nAfter fetching the main spec and parsing as Value:\n1. Walk the Value tree looking for $ref values that don't start with \"#/\"\n2. For each external ref: parse URL, check host against --ref-allow-host allowlist (reject if not listed)\n3. Fetch external ref content (uses AsyncHttpClient, respects SSRF policy)\n4. Track total bytes fetched (abort at --ref-max-bytes)\n5. Track resolution depth (abort at --ref-max-depth for ref chains)\n6. Replace the $ref object with the fetched content\n7. Store bundled result as raw.json; original (with $ref pointers) as raw.source\n\n## Acceptance Criteria\n- [ ] --resolve-external-refs without --ref-allow-host returns USAGE_ERROR\n- [ ] External refs to allowed hosts are fetched and inlined\n- [ ] External refs to disallowed hosts are rejected with PolicyBlocked\n- [ ] --ref-max-depth limits chain resolution\n- [ ] --ref-max-bytes limits total fetched content\n- [ ] Bundled raw.json has all external refs resolved; raw.source preserves originals\n\n## Edge Cases\n- **Circular external refs:** A.yaml $refs B.yaml which $refs A.yaml. Must detect and break cycles (return error or annotate).\n- **Relative refs:** `$ref: \"./schemas/Pet.yaml\"` must resolve relative to the base spec URL, not CWD.\n- **Mixed internal + external refs:** Only resolve external refs (not starting with #/). Internal refs stay as-is.\n- **External ref returns non-JSON/YAML:** Return InvalidSpec error for that ref, don't fail the entire fetch.\n- **--ref-max-bytes exceeded:** Abort cleanly and report how many refs were resolved before the limit.\n\n## Files\n- CREATE: src/core/external_refs.rs (resolve_external_refs, walk_refs, fetch_ref)\n- MODIFY: src/cli/fetch.rs (integrate external ref resolution after main fetch)\n- MODIFY: src/core/mod.rs (pub mod external_refs;)\n\n## TDD Anchor\nRED: Write `test_external_ref_resolution` — create a spec with external $ref, mock the external URL, run fetch --resolve-external-refs --ref-allow-host localhost, verify ref is inlined.\nGREEN: Implement ref walking and fetching.\nVERIFY: `cargo test test_external_ref_resolution`\n\n## Dependency Context\nUses AsyncHttpClient from bd-3b6. Extends fetch pipeline from bd-16o.","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-12T16:29:50.213098Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:58:12.383898Z","compaction_level":0,"original_size":0,"labels":["global","phase2"],"dependencies":[{"issue_id":"bd-1bp","depends_on_id":"bd-16o","type":"blocks","created_at":"2026-02-12T16:29:50.214577Z","created_by":"tayloreernisse"},{"issue_id":"bd-1bp","depends_on_id":"bd-3ll","type":"parent-child","created_at":"2026-02-12T16:29:50.214143Z","created_by":"tayloreernisse"}]} +{"id":"bd-1ck","title":"Implement diff command with structural comparison and CI gates","description":"## Background\nThe diff command compares two spec states structurally. It reports added, removed, and modified endpoints and schemas by comparing normalized indexes. Supports alias-vs-alias, alias-vs-URL, and --fail-on breaking for CI gates.\n\n## Approach\nImplement src/cli/diff.rs with DiffArgs and async execute():\n\n**DiffArgs:** left (String, positional), right (String, positional), fail_on (Option — \"breaking\"), details (bool).\n\n**Source resolution:** LEFT and RIGHT can be alias names or URLs. If URL, fetch into a temp location, build index, use for comparison (don't persist to cache).\n\n**Diff computation (reuse from sync):**\n- Compare endpoint sets by (path, method): added = in right not left, removed = in left not right, modified = in both but different (compare summary, tags, deprecated, parameters, security)\n- Compare schema sets by name: added, removed, modified (compare schema_ptr target content hashes? or just name-based for Phase 2)\n- Summary: total_changes, has_breaking (Phase 2: just structural; Phase 3: semantic classification)\n\n**--fail-on breaking:** If breaking changes detected, exit non-zero (exit code 17). Phase 2 heuristic: removed endpoint = breaking, removed required parameter = breaking. Added endpoint/schema = non-breaking.\n\n## Acceptance Criteria\n- [ ] `diff alias1 alias2 --robot` reports structural changes\n- [ ] Added/removed/modified endpoints correctly identified\n- [ ] --fail-on breaking exits non-zero when breaking changes exist\n- [ ] URL as right-side source works (temp fetch, not persisted)\n- [ ] Robot output: left, right, changes.endpoints, changes.schemas, summary\n\n## Edge Cases\n- **Same spec on both sides:** Return ok:true with changed:false, empty change lists. Not an error.\n- **URL-as-source fails to fetch:** Return Network error, not a diff-specific error.\n- **Breaking change heuristic false positives:** Endpoint renamed (removed old + added new) looks like a breaking removal. Phase 2 is heuristic-only — document this limitation.\n- **Very large diff (thousands of changes):** Apply same 200-item cap as sync --details with truncated flag.\n- **Schema comparison:** Phase 2 is name-based only (added/removed by name). Modified schema detection is Phase 3 (requires deep structural comparison).\n\n## Files\n- MODIFY: src/cli/diff.rs (DiffArgs, execute, compute_diff, classify_breaking)\n- MODIFY: src/output/robot.rs (add output_diff)\n- MODIFY: src/output/human.rs (add output_diff)\n\n## TDD Anchor\nRED: Write `test_diff_detects_added_endpoint` — fetch petstore twice (one modified), run diff, assert added endpoint appears.\nGREEN: Implement index comparison.\nVERIFY: `cargo test test_diff_detects_added`\n\n## Dependency Context\nReuses index diff logic from sync command (bd-3f4). Uses AsyncHttpClient for URL-as-source (bd-3b6). Uses indexer for building temp index from URL (bd-189).","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-12T16:30:58.972480Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:58:11.727380Z","compaction_level":0,"original_size":0,"labels":["diff","phase2"],"dependencies":[{"issue_id":"bd-1ck","depends_on_id":"bd-189","type":"blocks","created_at":"2026-02-12T16:30:58.976840Z","created_by":"tayloreernisse"},{"issue_id":"bd-1ck","depends_on_id":"bd-21m","type":"parent-child","created_at":"2026-02-12T16:30:58.975608Z","created_by":"tayloreernisse"},{"issue_id":"bd-1ck","depends_on_id":"bd-3f4","type":"blocks","created_at":"2026-02-12T16:30:58.976270Z","created_by":"tayloreernisse"}]} +{"id":"bd-1cv","title":"Implement global network policy (offline/auto/online-only)","description":"## Background\nThe --network global flag controls whether swagger-cli makes network calls. \"auto\" (default) allows network when needed. \"offline\" blocks all network calls (fetch/sync fail with OFFLINE_MODE). \"online-only\" flags when network would be needed but is not available. This is critical for CI reproducibility and agent sandboxing.\n\n## Approach\n\n### NetworkPolicy Enum (src/core/network.rs)\nCreate `NetworkPolicy` enum with three variants:\n- **Auto** (default): Allow network calls when needed, no restrictions. Standard behavior.\n- **Offline**: Block ALL outbound network calls preemptively. Any command that would make a network request (fetch, sync) returns `SwaggerCliError::OfflineMode` immediately without attempting the call. Commands that are index-only (list, search, show, tags) work normally.\n- **OnlineOnly**: Allow network calls but surface a clear, distinct error if a network call would be needed AND DNS resolution fails or the host is unreachable. The error is different from OfflineMode — it indicates \"you requested online-only mode but network is unavailable\" rather than \"network calls are blocked by policy.\"\n\n### Implementation\n1. Parse --network flag in CLI (already in Cli struct from skeleton bead)\n2. Create `NetworkPolicy` enum and `check_network_policy()` function in `src/core/network.rs`\n3. Check policy before any HTTP call in AsyncHttpClient — if Offline, return `SwaggerCliError::OfflineMode` without making the request\n4. For OnlineOnly: attempt the request, but if it fails due to network issues, return a specific `SwaggerCliError::NetworkUnavailable` (different exit code or error code than OfflineMode)\n5. `SWAGGER_CLI_NETWORK` env var as alternative to --network flag (flag takes precedence)\n\n### Scope boundaries\n- **Local file sources (file:// paths, stdin):** Network policy does NOT apply. These are local I/O operations. `fetch ./local-spec.yaml --network offline` should succeed because no network call is made.\n- **Stdin sources:** Same as local files — no network needed, policy irrelevant.\n- **sync --dry-run in offline mode:** ALLOWED. Dry-run compares cached state without actually fetching, so no network call is needed. Returns cached state comparison only.\n\n## Acceptance Criteria\n- [ ] NetworkPolicy enum with Auto, Offline, OnlineOnly variants in src/core/network.rs\n- [ ] --network offline causes fetch (remote URL) to fail with OFFLINE_MODE error (exit 15)\n- [ ] --network offline allows fetch from local file path (no network needed)\n- [ ] --network offline allows fetch from stdin (no network needed)\n- [ ] --network offline allows list/search/show/tags (index-only, no network needed)\n- [ ] --network offline allows sync --dry-run (cached comparison only, no network)\n- [ ] --network online-only surfaces clear error when network is unavailable (distinct from OFFLINE_MODE)\n- [ ] --network auto allows all commands (default behavior, no restrictions)\n- [ ] SWAGGER_CLI_NETWORK=offline env var works same as --network offline flag\n- [ ] Flag takes precedence over env var when both are set\n- [ ] Robot error for offline mode has code OFFLINE_MODE with suggestion to remove --network flag or unset env var\n- [ ] Robot error for online-only network failure has code NETWORK_UNAVAILABLE with distinct suggestion\n\n## Edge Cases\n- **sync --dry-run in offline mode:** Allowed — returns cached state comparison only, no actual fetch happens. This is a read-only operation on cached data.\n- **Local file + offline mode:** Allowed — `fetch ./spec.yaml --network offline` succeeds because it is local I/O, not a network call.\n- **OnlineOnly vs Offline distinction:** Offline blocks proactively (never attempts the call). OnlineOnly attempts the call and fails with a specific error if the network is down. This matters for agents that want to know \"was this blocked by policy or by actual network unavailability?\"\n- **Mixed sources:** If a future version supports specs that reference remote $ref URLs but the base spec is local, the network policy should apply to the remote $ref resolution (not the local file read).\n\n## Files\n- CREATE: src/core/network.rs (NetworkPolicy enum, check_network_policy function)\n- MODIFY: src/core/http.rs (check policy before fetch)\n- MODIFY: src/cli/fetch.rs (check policy at start, skip for local/stdin sources)\n- MODIFY: src/cli/sync.rs (check policy at start, allow --dry-run in offline)\n\n## TDD Anchor\nRED: Write `test_offline_blocks_fetch` — set SWAGGER_CLI_NETWORK=offline, run fetch with remote URL, assert exit 15 and OFFLINE_MODE error.\nRED: Write `test_offline_allows_local_file` — set offline, fetch local file, assert success.\nRED: Write `test_offline_allows_list` — set offline, run list on cached alias, assert success.\nRED: Write `test_online_only_network_unavailable` — set online-only, mock DNS failure, assert NETWORK_UNAVAILABLE error.\nGREEN: Implement policy check.\nVERIFY: `cargo test test_offline_blocks_fetch`\n\n## Dependency Context\nModifies AsyncHttpClient in bd-3b6 (async HTTP client) to check network policy before requests. Uses SwaggerCliError variants (OfflineMode) from bd-ilo (error types and core data models).","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-12T16:29:50.156478Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:56:01.445917Z","compaction_level":0,"original_size":0,"labels":["global","phase2"],"dependencies":[{"issue_id":"bd-1cv","depends_on_id":"bd-16o","type":"blocks","created_at":"2026-02-12T16:29:50.158591Z","created_by":"tayloreernisse"},{"issue_id":"bd-1cv","depends_on_id":"bd-3b6","type":"blocks","created_at":"2026-02-12T16:29:50.158232Z","created_by":"tayloreernisse"},{"issue_id":"bd-1cv","depends_on_id":"bd-3ll","type":"parent-child","created_at":"2026-02-12T16:29:50.157784Z","created_by":"tayloreernisse"}]} +{"id":"bd-1d4","title":"Implement cache lifecycle command with stats, prune, and LRU eviction","description":"## Background\nThe cache command manages cache growth with stats, pruning, and LRU eviction. Separate from doctor (which validates health). Uses coalesced last_accessed timestamps for LRU ordering.\n\n## Approach\nImplement src/cli/cache.rs with CacheArgs and execute():\n\n**CacheArgs:** stats (bool, default action), prune_stale (bool), prune_threshold (u32, default 90 days), max_total_mb (Option), dry_run (bool).\n\n**Operations:**\n- Stats: list_aliases with size computation, show per-alias and total bytes\n- Prune: find aliases older than threshold, delete (or dry-run report)\n- LRU eviction: sort by last_accessed ASC, delete oldest until total < max_total_mb (or dry-run)\n\n## Acceptance Criteria\n- [ ] cache --stats shows per-alias sizes and total\n- [ ] cache --prune-stale deletes aliases >90 days old\n- [ ] cache --prune-threshold 30 overrides default\n- [ ] cache --max-total-mb 500 evicts oldest-accessed aliases\n- [ ] --dry-run shows what would be pruned without deleting\n- [ ] Robot output: aliases[], total_bytes, pruned[], evicted[]\n\n## Edge Cases\n- **No aliases cached:** cache --stats returns ok:true with total_bytes:0, empty aliases array.\n- **Concurrent prune + fetch:** If a fetch writes a new alias while prune is deleting, the new alias should not be affected. Prune operates on snapshot of alias list taken at start.\n- **last_accessed coalescing:** LRU ordering uses coalesced last_accessed (10-min granularity). Hot-read aliases within the same 10-min window have identical last_accessed — tie-break by fetched_at.\n- **--max-total-mb smaller than single largest alias:** Evict everything except the largest, then warn that target cannot be reached.\n- **Alias being synced during prune:** Skip locked aliases with warning.\n\n## Files\n- MODIFY: src/cli/cache.rs (CacheArgs, execute, prune, evict)\n- MODIFY: src/output/robot.rs (add output_cache)\n- MODIFY: src/output/human.rs (add output_cache)\n\n## TDD Anchor\nRED: Write `test_cache_prune_stale` — create alias with old fetched_at, run cache --prune-stale --robot, assert alias appears in pruned[].\nGREEN: Implement stale detection and pruning.\nVERIFY: `cargo test test_cache_prune_stale`\n\n## Dependency Context\nUses CacheManager (list_aliases, delete_alias) from bd-3ea. Uses is_stale from CacheMetadata.","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-12T16:29:50.122830Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:58:11.481108Z","compaction_level":0,"original_size":0,"labels":["health","phase2"],"dependencies":[{"issue_id":"bd-1d4","depends_on_id":"bd-1y0","type":"parent-child","created_at":"2026-02-12T16:29:50.123937Z","created_by":"tayloreernisse"},{"issue_id":"bd-1d4","depends_on_id":"bd-3d2","type":"blocks","created_at":"2026-02-12T16:29:50.124722Z","created_by":"tayloreernisse"},{"issue_id":"bd-1d4","depends_on_id":"bd-3ea","type":"blocks","created_at":"2026-02-12T16:29:50.124351Z","created_by":"tayloreernisse"}]} +{"id":"bd-1dj","title":"Implement show command with ref expansion and pointer navigation","description":"## Background\nThe show command displays full details for a specific endpoint. Unlike list/search, show MUST load raw.json to extract the complete operation subtree (parameters with schemas, request body, response schemas, etc.). It uses the operation_ptr from the index to locate the exact JSON node in raw.json, avoiding full-spec parsing. Optional --expand-refs resolves internal $ref pointers with cycle detection.\n\n## Approach\nImplement src/cli/show.rs with ShowArgs and execute():\n\n**ShowArgs:** alias (Option), path (String, positional), method (Option), format (String, default \"pretty\"), expand_refs (bool), max_depth (u32, default 3).\n\n**Execute flow:**\n1. Resolve alias, load_index\n2. Find matching endpoint(s) by path in index\n3. If multiple methods and --method not specified → USAGE_ERROR listing available methods\n4. Get operation_ptr from matched IndexedEndpoint\n5. CacheManager::load_raw(alias, &meta) — loads raw.json as Value, validates raw_hash\n6. Navigate to operation subtree using JSON pointer (operation_ptr)\n7. If --expand-refs: recursively resolve internal $ref pointers (starting with #/) with cycle detection (max_depth). External refs get annotated as {\"$external_ref\": \"...\"}. Circular refs get {\"$circular_ref\": \"...\"}. Add warnings to meta.warnings[].\n8. Output robot JSON or human formatted details\n\n**JSON Pointer navigation:** Parse `/paths/~1pet~1{petId}/get` → navigate Value tree. `~1` → `/`, `~0` → `~`.\n\n**Ref expansion:** Walk the Value tree. When encountering `{\"$ref\": \"#/components/schemas/Pet\"}`, resolve by navigating the pointer in raw Value. Track visited refs for cycle detection. Stop at max_depth.\n\n## Acceptance Criteria\n- [ ] `swagger-cli show petstore \"/pet/{petId}\" --robot` returns full operation details\n- [ ] Multiple methods without --method returns USAGE_ERROR with available methods\n- [ ] --method POST selects specific method\n- [ ] operation_ptr correctly navigates to the right subtree in raw.json\n- [ ] --expand-refs resolves internal refs up to max_depth\n- [ ] Circular refs produce $circular_ref annotation (not infinite loop)\n- [ ] External refs produce $external_ref annotation + warning\n- [ ] raw_hash mismatch returns CacheIntegrity error\n- [ ] Robot output includes: path, method, summary, description, tags, operation_id, parameters, request_body, responses, security\n\n## Files\n- MODIFY: src/cli/show.rs (ShowArgs, execute, pointer navigation, ref expansion)\n- CREATE: src/core/refs.rs (expand_refs, resolve_pointer, cycle detection)\n- MODIFY: src/core/mod.rs (pub mod refs;)\n- MODIFY: src/output/robot.rs (add output_show)\n- MODIFY: src/output/human.rs (add output_show)\n\n## TDD Anchor\nRED: Write `test_show_endpoint_details` — fetch petstore fixture, run show \"/pet/{petId}\" --method GET --robot, parse JSON, assert data.path == \"/pet/{petId}\" and data.method == \"GET\".\nGREEN: Implement pointer navigation and output.\nVERIFY: `cargo test test_show_endpoint_details`\n\nAdditional tests:\n- test_show_multiple_methods_error\n- test_expand_refs_basic\n- test_expand_refs_circular_detection\n- test_expand_refs_external_annotation\n\n## Edge Cases\n- JSON pointer decoding: `~1` → `/`, `~0` → `~` (order matters: decode ~1 first, then ~0)\n- Path matching should be exact (not regex) — \"/pet/{petId}\" must match literally\n- Some operations may have no request body, no parameters, or no security — handle None gracefully\n- ref expansion must handle refs-to-refs (transitive resolution)\n\n## Dependency Context\nUses CacheManager.load_index and load_raw from bd-3ea (cache read). Uses index types from bd-ilo. Requires a fetched spec in cache.","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:27:27.091022Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:27:27.093228Z","compaction_level":0,"original_size":0,"labels":["phase1","query"],"dependencies":[{"issue_id":"bd-1dj","depends_on_id":"bd-3d2","type":"blocks","created_at":"2026-02-12T16:27:27.093222Z","created_by":"tayloreernisse"},{"issue_id":"bd-1dj","depends_on_id":"bd-3ea","type":"blocks","created_at":"2026-02-12T16:27:27.092774Z","created_by":"tayloreernisse"},{"issue_id":"bd-1dj","depends_on_id":"bd-epk","type":"parent-child","created_at":"2026-02-12T16:27:27.092375Z","created_by":"tayloreernisse"}]} +{"id":"bd-1ie","title":"Implement cache write path with crash-consistent protocol and alias validation","description":"## Background\nThe cache write path is the most safety-critical code in swagger-cli. It implements a crash-consistent multi-file commit protocol: raw.source -> raw.json -> index.json are written as .tmp files with fsync, renamed atomically, then meta.json is written LAST as the commit marker. Per-alias file locking prevents concurrent write corruption. Alias names are validated against a strict regex to prevent path traversal.\n\n## Approach\n**Alias validation:** Implement `validate_alias()` that checks against `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`, rejects path separators (/ \\), `..`, reserved device names (CON, PRN, NUL, AUX, COM1-9, LPT1-9 case-insensitive), and leading dots. Return SwaggerCliError::Usage on failure.\n\n**Cache directory management:** Implement `CacheManager` struct with `new()` (uses Config::cache_dir()), `new_with_path()` (for tests), `alias_dir()`, `ensure_dirs()`. Cache layout: `{cache_dir}/aliases/{alias}/`.\n\n**File locking:** Use `fs2::FileExt::try_lock_exclusive()` on `{alias_dir}/.lock` with a bounded timeout (default 1000ms, poll every 50ms). Return SwaggerCliError::CacheLocked on timeout.\n\n**Hash computation:** Implement `compute_hash(bytes: &[u8]) -> String` returning `\"sha256:{hex}\"` using sha2 crate.\n\n**Crash-consistent write:** Implement `CacheManager::write_cache()` that:\n1. Acquires exclusive lock on .lock (bounded timeout)\n2. Computes content_hash, raw_hash, next generation, index_hash\n3. Writes raw.source.tmp, raw.json.tmp, index.json.tmp (each with sync_all before rename)\n4. Renames each .tmp to final name (sync_all after each rename)\n5. Writes meta.json.tmp LAST (commit marker)\n6. Renames meta.json.tmp -> meta.json (sync_all)\n7. Best-effort fsync on alias directory fd (Unix only)\n8. Releases lock\n\n**Parameters for write_cache:** Takes alias, raw_source_bytes, raw_json_bytes, index (SpecIndex), url, spec_version, spec_title, source_format, etag, last_modified, previous_generation (Option).\n\n## Acceptance Criteria\n- [ ] validate_alias(\"petstore\") -> Ok\n- [ ] validate_alias(\"../etc/passwd\") -> Err(Usage)\n- [ ] validate_alias(\"CON\") -> Err(Usage) (case-insensitive)\n- [ ] validate_alias(\".hidden\") -> Err(Usage)\n- [ ] validate_alias(\"a\".repeat(65)) -> Err(Usage)\n- [ ] write_cache creates all 4 files + .lock in correct directory\n- [ ] meta.json is the last file written (verified by checking file mtimes or write order)\n- [ ] compute_hash produces deterministic sha256:hex output\n- [ ] Lock timeout returns CacheLocked error (not hang)\n- [ ] Hash values are deterministic (same input -> same sha256 output)\n- [ ] Files survive process kill between steps (no partial meta.json)\n\n## Files\n- CREATE: src/core/cache.rs (CacheManager, validate_alias, compute_hash, write_cache, lock helpers)\n- MODIFY: src/core/mod.rs (pub mod cache;)\n\n## TDD Anchor\nRED: Write `test_validate_alias_rejects_traversal` -- assert validate_alias(\"../etc\") returns Err with Usage variant.\nGREEN: Implement regex validation + blocklist checks.\nVERIFY: `cargo test test_validate_alias`\n\nAdditional tests:\n- test_validate_alias_accepts_valid (petstore, my-api, v1.0, API_2)\n- test_validate_alias_rejects_reserved (CON, con, NUL, COM1)\n- test_write_cache_creates_all_files\n- test_compute_hash_deterministic\n\n## Edge Cases\n- On macOS, fsync on directory fd may not be supported -- handle gracefully (best-effort)\n- Lock file must be created if it doesn't exist (open with create flag)\n- If alias directory doesn't exist, create it before acquiring lock\n- sync_all() is critical -- without it, data may be in OS page cache but not on disk after rename\n- Generation starts at 1 for new aliases, increments from previous for updates\n\n## Dependency Context\nUses SpecIndex and CacheMetadata types from bd-ilo (error types and core data models). Uses SwaggerCliError variants (CacheLocked, Cache, Usage) from bd-ilo.","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:25:15.503359Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:55:51.728315Z","compaction_level":0,"original_size":0,"labels":["infrastructure","phase1"],"dependencies":[{"issue_id":"bd-1ie","depends_on_id":"bd-hcb","type":"parent-child","created_at":"2026-02-12T16:25:15.504778Z","created_by":"tayloreernisse"},{"issue_id":"bd-1ie","depends_on_id":"bd-ilo","type":"blocks","created_at":"2026-02-12T16:25:15.505170Z","created_by":"tayloreernisse"}]} +{"id":"bd-1ky","title":"Implement sync --all with async concurrency and resumable execution","description":"## Background\nsync --all synchronizes all aliases concurrently. It uses bounded async concurrency (--jobs, default 4) with per-host throttling (--per-host, default 2) to avoid abusive request patterns. Supports resumable execution via checkpoint files and a failure budget (--max-failures) to limit blast radius.\n\n## Approach\nBuild on the single-alias sync from the previous bead:\n\n**Async concurrency:** Use tokio::sync::Semaphore for global concurrency (--jobs). Use a per-host semaphore map (HashMap) for --per-host throttling. Process aliases via futures::stream::StreamExt::buffer_unordered.\n\n**Resumable sync (--resume):** Write a checkpoint file ({cache_dir}/sync-checkpoint.json) after each alias completes. Contains: aliases_completed, aliases_failed, started_at. On --resume, skip already-completed aliases.\n\n**Failure budget (--max-failures):** Track failure count. When exceeded, abort remaining aliases and report partial results.\n\n**Retry-After:** Honor response header (seconds or HTTP-date format). Use exponential backoff + jitter when header absent.\n\n**Per-alias output:** Collect results from all aliases. Report per-alias success/failure in robot output. Don't abort on single-alias failure (unless failure budget exceeded).\n\n## Acceptance Criteria\n- [ ] sync --all processes all aliases concurrently\n- [ ] --jobs limits concurrent syncs (verified: never more than N in-flight)\n- [ ] --per-host limits requests to same host\n- [ ] --resume skips already-completed aliases\n- [ ] --max-failures aborts after N failures\n- [ ] Per-alias success/failure reported in robot output\n- [ ] Retry-After header honored\n\n## Edge Cases\n- **Empty aliases (no specs cached):** sync --all with no aliases should succeed with empty results, not error.\n- **All aliases fail:** If every alias fails sync, the overall command should still exit 0 with per-alias failure details (unless --max-failures triggered, then exit non-zero).\n- **Checkpoint file corruption:** If sync-checkpoint.json is malformed, delete it and start fresh (don't error).\n- **Per-host semaphore with many aliases to same host:** 10 aliases all pointing to api.example.com with --per-host 2 means only 2 concurrent to that host, even if --jobs allows more.\n- **Retry-After as HTTP-date:** Parse both formats: seconds (e.g., \"120\") and HTTP-date (e.g., \"Thu, 01 Jan 2026 00:00:00 GMT\").\n\n## Files\n- MODIFY: src/cli/sync.rs (add sync_all, concurrency control, checkpoint, failure budget)\n\n## TDD Anchor\nRED: Write `test_sync_all_concurrent` — set up 4 aliases, run sync --all --jobs 2 --robot, verify all 4 synced and output includes per-alias results.\nGREEN: Implement concurrent sync with semaphore.\nVERIFY: `cargo test test_sync_all_concurrent`\n\n## Dependency Context\nExtends single-alias sync from bd-3f4 (sync command). Uses tokio Semaphore for concurrency control. Uses CacheManager.list_aliases from bd-3ea (cache read path).","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-12T16:28:47.465114Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:58:09.820419Z","compaction_level":0,"original_size":0,"labels":["phase2","sync"],"dependencies":[{"issue_id":"bd-1ky","depends_on_id":"bd-161","type":"parent-child","created_at":"2026-02-12T16:28:47.466140Z","created_by":"tayloreernisse"},{"issue_id":"bd-1ky","depends_on_id":"bd-3f4","type":"blocks","created_at":"2026-02-12T16:34:06.315322Z","created_by":"tayloreernisse"}]} +{"id":"bd-1lj","title":"Create CI/CD pipeline and cargo-deny configuration","description":"## Background\nThe CI/CD pipeline runs tests, linting, security audits, and multi-platform builds on GitLab CI. It also configures cargo-deny for license/advisory policy enforcement.\n\n## Approach\n\n### .gitlab-ci.yml Structure\n\n**Stages:** test, build, release\n\n**Test stage jobs:**\n- `test:unit` — `cargo test --lib` (unit tests only, no integration tests)\n- `test:integration` — `cargo test --test '*'` (integration tests in tests/ directory)\n- `lint` — `cargo fmt --check` + `cargo clippy -- -D warnings` (warnings are errors)\n- `security:deps` — `cargo-deny check` (license + advisory) + `cargo-audit` (RUSTSEC advisory DB)\n\n**Build stage:**\n- Build template (YAML anchor or extends) shared across 4 target jobs:\n - `build:aarch64-apple-darwin` — Apple Silicon macOS\n - `build:x86_64-apple-darwin` — Intel macOS\n - `build:x86_64-unknown-linux-gnu` — x86 Linux\n - `build:aarch64-unknown-linux-gnu` — ARM Linux\n- Each job: `cargo build --release --locked --target $TARGET`, upload binary as artifact\n\n**Release stage:**\n- `release` job (runs on tagged commits only):\n - Collect all 4 binaries from build artifacts\n - Generate `SHA256SUMS` file: `sha256sum swagger-cli-* > SHA256SUMS`\n - Sign with minisign: `minisign -S -m SHA256SUMS` (produces SHA256SUMS.minisig)\n - Upload all files to GitLab Package Registry via `curl` to Package Registry API\n- `docker` job (runs on tagged commits):\n - `docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG .`\n - `docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG`\n - Also tag as `:latest`\n\n### deny.toml Structure\n\n**[licenses]:**\n- `unlicensed = \"deny\"` — all deps must have a license\n- `allow = [\"MIT\", \"Apache-2.0\", \"BSD-2-Clause\", \"BSD-3-Clause\", \"ISC\", \"Zlib\", \"Unicode-3.0\", \"Unicode-DFS-2016\", \"OpenSSL\"]` — common OSS license allow-list\n- `copyleft = \"deny\"` — no copyleft licenses without explicit exception\n\n**[advisories]:**\n- `vulnerability = \"deny\"` — fail on known vulnerabilities\n- `unmaintained = \"warn\"` — warn but don't fail on unmaintained crates\n- `unsound = \"warn\"` — warn on unsound crates\n- `yanked = \"deny\"` — fail on yanked versions\n- `notice = \"warn\"` — warn on advisories with notices\n\n**[bans]:**\n- `multiple-versions = \"warn\"` — warn if same crate appears in multiple versions\n- `wildcards = \"deny\"` — no wildcard version specs\n- Specific ban list empty initially (add problematic crates as discovered)\n\n## Acceptance Criteria\n- [ ] .gitlab-ci.yml has test, build, release stages in correct order\n- [ ] test:unit runs `cargo test --lib`\n- [ ] test:integration runs `cargo test --test '*'`\n- [ ] lint job runs both fmt --check and clippy -D warnings\n- [ ] security:deps runs both cargo-deny check and cargo-audit\n- [ ] All 4 platform targets defined as separate build jobs\n- [ ] Build jobs use shared template/anchor to avoid duplication\n- [ ] Release job generates SHA256SUMS from all binaries\n- [ ] Release job signs SHA256SUMS with minisign\n- [ ] Release job uploads to GitLab Package Registry\n- [ ] Docker job builds and pushes to CI_REGISTRY\n- [ ] deny.toml license allow-list covers common OSS licenses\n- [ ] deny.toml advisory policy denies known vulnerabilities\n- [ ] `cargo deny check` passes locally after creation\n\n## Files\n- CREATE: .gitlab-ci.yml\n- CREATE: deny.toml\n\n## TDD Anchor\nValidate: `cargo deny check` passes locally (after deny.toml is created).\nVERIFY: `cargo deny check 2>&1 | head -5`\n\n## Edge Cases\n- **Cross-compilation toolchains:** macOS targets require macOS runners or cross-compilation tools. Verify CI has the right runners or use `cross` tool.\n- **cargo-deny not installed:** The security:deps job must install cargo-deny and cargo-audit before running. Use `cargo install` with version pinning.\n- **Minisign key management:** The release job needs the minisign private key as a CI secret variable. Document the required CI variables.\n- **Tag-only release trigger:** Release jobs must have `rules: - if: $CI_COMMIT_TAG` to avoid running on every push.\n- **Large binary artifacts:** Rust release binaries can be 10MB+. Verify CI artifact storage limits.","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-12T16:31:32.392078Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:56:20.412480Z","compaction_level":0,"original_size":0,"labels":["ci","phase2"],"dependencies":[{"issue_id":"bd-1lj","depends_on_id":"bd-1lo","type":"parent-child","created_at":"2026-02-12T16:31:32.394604Z","created_by":"tayloreernisse"},{"issue_id":"bd-1lj","depends_on_id":"bd-a7e","type":"blocks","created_at":"2026-02-12T16:31:32.395053Z","created_by":"tayloreernisse"}]} +{"id":"bd-1lo","title":"Epic: Distribution and CI","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-12T16:22:28.089610Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:22:28.090122Z","compaction_level":0,"original_size":0,"labels":["epic"]} +{"id":"bd-1rk","title":"Implement cross-alias discovery (--all-aliases)","description":"## Background\n--all-aliases on list and search queries across every cached alias, returning results with an additional \"alias\" field per result. Useful for agents managing multiple APIs.\n\n## Approach\n1. In list execute: if --all-aliases, iterate all aliases via list_aliases(), load each index, filter/sort per-alias, merge results with \"alias\" field added to each endpoint item\n2. In search execute: same pattern — search each alias's index, merge results, re-sort by score\n3. Robot output: each endpoint/result includes \"alias\" string field identifying which spec it came from\n\n**Sorting rules:**\n- For `list --all-aliases`: merged endpoints sorted by (alias ASC, path ASC, method_rank ASC) where method_rank follows standard HTTP method ordering (GET=0, POST=1, PUT=2, PATCH=3, DELETE=4, etc.)\n- For `search --all-aliases`: merged results sorted by (score DESC, then normal tie-breaking within same score) — the alias field is just additional metadata, it does not affect ranking\n\n**Robot output envelope:**\n- `data.endpoints[]` or `data.results[]` — each item includes `\"alias\": \"petstore\"` field\n- `data.aliases_searched: string[]` — lists which aliases were queried (e.g., `[\"petstore\", \"github-api\", \"stripe\"]`)\n- This allows agents to detect if an alias was missing/skipped vs simply had no matching results\n\n**Error handling for individual alias failures:**\n- If one alias fails to load (corrupted index, missing files), do NOT abort the entire operation\n- Skip the failed alias and continue with remaining aliases\n- Add warning to `meta.warnings[]` array: `{\"alias\": \"broken-api\", \"code\": \"INDEX_LOAD_FAILED\", \"message\": \"...\"}`\n- Robot output still returns ok:true with partial results (this is a degraded success, not an error)\n- If ALL aliases fail, then return an error\n\n## Acceptance Criteria\n- [ ] `list --all-aliases --robot` returns endpoints from all aliases with alias field on each item\n- [ ] `search \"pet\" --all-aliases --robot` searches across all aliases with alias field on each result\n- [ ] List results sorted by alias ASC, then path ASC, then method_rank ASC\n- [ ] Search results sorted by score DESC (alias does not affect ranking)\n- [ ] Robot output includes data.aliases_searched listing all queried aliases\n- [ ] If one alias fails to load, operation continues with remaining aliases and meta.warnings populated\n- [ ] If all aliases fail, returns error (not partial success)\n- [ ] Human output groups or labels results by alias for readability\n\n## Edge Cases\n- **Single alias cached:** --all-aliases works but is equivalent to normal query (aliases_searched has one entry)\n- **No aliases cached:** Return error — no specs available\n- **One alias fails to load:** Skip with warning in meta.warnings, return results from healthy aliases\n- **All aliases fail:** Return error with details about each failure\n- **Duplicate endpoint paths across aliases:** Both appear in results, distinguished by alias field\n\n## Files\n- MODIFY: src/cli/list.rs (add --all-aliases logic)\n- MODIFY: src/cli/search.rs (add --all-aliases logic)\n\n## TDD Anchor\nRED: Write `test_list_all_aliases` — fetch 2 different specs, run list --all-aliases --robot, assert results contain endpoints from both with alias field.\nRED: Write `test_search_all_aliases` — fetch 2 specs, run search --all-aliases --robot, assert results from both with correct score-based sorting.\nRED: Write `test_all_aliases_partial_failure` — corrupt one alias index, run list --all-aliases, assert partial results returned with warning.\nGREEN: Implement cross-alias iteration.\nVERIFY: `cargo test test_list_all_aliases`\n\n## Dependency Context\nRequires list command (bd-3km) and search command (bd-acf) to be implemented first.","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-12T16:29:50.184624Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:45:28.367812Z","compaction_level":0,"original_size":0,"labels":["global","phase2"],"dependencies":[{"issue_id":"bd-1rk","depends_on_id":"bd-3km","type":"blocks","created_at":"2026-02-12T16:29:50.187908Z","created_by":"tayloreernisse"},{"issue_id":"bd-1rk","depends_on_id":"bd-3ll","type":"parent-child","created_at":"2026-02-12T16:29:50.187260Z","created_by":"tayloreernisse"},{"issue_id":"bd-1rk","depends_on_id":"bd-acf","type":"blocks","created_at":"2026-02-12T16:29:50.188740Z","created_by":"tayloreernisse"}]} +{"id":"bd-1sb","title":"Implement configuration system with path resolution and auth profiles","description":"## Background\nswagger-cli uses TOML config files for user preferences and auth profiles, stored in XDG config dir. The config system handles path resolution with a specific precedence: --config > SWAGGER_CLI_CONFIG > SWAGGER_CLI_HOME > XDG defaults. Auth profiles allow loading credentials from config.toml instead of passing raw tokens on the command line.\n\n## Approach\nImplement Config, AuthConfig, CredentialSource, AuthType, DisplayConfig structs in src/core/config.rs per the PRD. Config::load() reads from config_path(), Config::save() writes with toml::to_string_pretty(). Both config_path() and cache_dir() implement the D7 override precedence using env vars SWAGGER_CLI_HOME, SWAGGER_CLI_CONFIG, SWAGGER_CLI_CACHE, and directories::ProjectDirs. CredentialSource is a tagged enum (Literal, EnvVar, Keyring) with serde tag=\"source\". AuthType is an enum (Bearer, ApiKey { header }). Default stale_threshold_days is 30.\n\n## Acceptance Criteria\n- [ ] Config::load() returns default Config when no file exists\n- [ ] Config::save() writes valid TOML that round-trips through load()\n- [ ] config_path() respects: --config flag > SWAGGER_CLI_CONFIG env > SWAGGER_CLI_HOME/config/config.toml > XDG\n- [ ] cache_dir() respects: SWAGGER_CLI_CACHE > SWAGGER_CLI_HOME/cache > XDG cache dir\n- [ ] AuthConfig with CredentialSource::EnvVar serializes/deserializes correctly\n- [ ] Default config has stale_threshold_days=30, empty auth_profiles, no default_alias\n- [ ] All tests hermetic (use SWAGGER_CLI_HOME with tempdir)\n\n## Files\n- CREATE: src/core/config.rs (Config, AuthConfig, CredentialSource, AuthType, DisplayConfig, Default impl, load/save, path resolution)\n- MODIFY: src/core/mod.rs (pub mod config;)\n\n## TDD Anchor\nRED: Write `test_config_path_precedence` — set SWAGGER_CLI_HOME to a tempdir, verify config_path() returns tempdir/config/config.toml. Then set SWAGGER_CLI_CONFIG to a specific path, verify it takes precedence.\nGREEN: Implement config_path() with env var checks.\nVERIFY: `cargo test test_config_path_precedence`\n\n## Edge Cases\n- CredentialSource::Keyring is Phase 2 — include the enum variant but document it as unimplemented\n- Config file might not exist (return default, don't error)\n- SWAGGER_CLI_HOME dir might not exist yet (create parent dirs on save)\n- toml crate is named `toml`, not `serde_toml`","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:25:15.475935Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:34:06.198329Z","compaction_level":0,"original_size":0,"labels":["infrastructure","phase1"],"dependencies":[{"issue_id":"bd-1sb","depends_on_id":"bd-hcb","type":"parent-child","created_at":"2026-02-12T16:25:15.477635Z","created_by":"tayloreernisse"},{"issue_id":"bd-1sb","depends_on_id":"bd-ilo","type":"blocks","created_at":"2026-02-12T16:25:15.478018Z","created_by":"tayloreernisse"}]} +{"id":"bd-1x5","title":"Implement reliability stress tests and performance benchmarks","description":"## Background\nReliability tests validate the crash-consistency, concurrency safety, and determinism claims. These are the hardest tests but most important for production confidence. Includes fault injection (simulated crash at each write step), multi-process lock contention (32 concurrent processes), and property-based tests (proptest for deterministic ordering).\n\n## Approach\n**Crash consistency (tests/reliability/crash_consistency_test.rs):**\n- test_crash_before_meta_rename: write raw+index but not meta → read protocol detects → doctor --fix repairs\n- test_crash_after_raw_before_index: write raw but not index → doctor --fix rebuilds\n\n**Lock contention (tests/reliability/lock_contention_test.rs):**\n- test_concurrent_fetch_32_processes: spawn 32 threads, all fetch same alias with --force → verify all exit 0 or 9 (CACHE_LOCKED), no panics, final state passes doctor\n\n**Property-based (tests/reliability/property_test.rs with proptest):**\n- index_ordering_deterministic: random endpoints → build_index → build_index from shuffled → same JSON\n- hash_deterministic: same bytes → same hash\n\nAdd proptest = '1.0' to [dev-dependencies] in Cargo.toml alongside existing test deps.\n\n**Benchmarks (benches/perf.rs with Criterion):**\n- bench_load_index, bench_list_endpoints, bench_search_query on 500+ endpoint spec\n\n## Acceptance Criteria\n- [ ] Crash consistency tests verify doctor can repair all simulated crash states\n- [ ] 32-process contention test passes without deadlocks or corruption\n- [ ] Property-based tests verify deterministic ordering\n- [ ] Benchmarks establish baseline performance numbers\n- [ ] All reliability tests pass on CI\n\n## Files\n- CREATE: tests/reliability/crash_consistency_test.rs\n- CREATE: tests/reliability/lock_contention_test.rs\n- CREATE: tests/reliability/property_test.rs\n- CREATE: benches/perf.rs\n- MODIFY: Cargo.toml (add [[bench]] for criterion, add proptest dev-dep)\n\n## TDD Anchor\nRun: `cargo test --test crash_consistency && cargo test --test lock_contention && cargo test --test property`\nVERIFY: `cargo bench -- --test`\n\n## Dependency Context\nUses CacheManager write/read protocol from cache beads. Uses build_index from indexer bead. Uses compute_hash. Tests the claims made in the cache architecture.\n\n## Edge Cases\n- **Crash test cleanup:** Crash simulation tests must clean up temp files even on test failure. Use Drop guards or panic hooks.\n- **32-process test may exceed file descriptor limits:** On CI, ulimit may be low. Test should check and skip with a clear message if fd limit < 256.\n- **Property test shrinking:** proptest shrinking can be slow for complex inputs. Set max shrink iterations to avoid CI timeouts.\n- **Benchmark stability:** Criterion requires multiple iterations. First run creates baseline. Tests should not fail on benchmark regression — only warn.\n- **Lock contention test timing:** 32 threads racing for a lock is timing-dependent. Use a barrier to ensure all threads start simultaneously.","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-12T16:30:59.112199Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:56:19.044503Z","compaction_level":0,"original_size":0,"labels":["phase2","testing"],"dependencies":[{"issue_id":"bd-1x5","depends_on_id":"bd-189","type":"blocks","created_at":"2026-02-12T16:30:59.114670Z","created_by":"tayloreernisse"},{"issue_id":"bd-1x5","depends_on_id":"bd-1ie","type":"blocks","created_at":"2026-02-12T16:30:59.113671Z","created_by":"tayloreernisse"},{"issue_id":"bd-1x5","depends_on_id":"bd-3ea","type":"blocks","created_at":"2026-02-12T16:30:59.114152Z","created_by":"tayloreernisse"},{"issue_id":"bd-1x5","depends_on_id":"bd-p7g","type":"parent-child","created_at":"2026-02-12T16:30:59.113195Z","created_by":"tayloreernisse"}]} +{"id":"bd-1y0","title":"Epic: Health and Cache Lifecycle","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-12T16:22:24.038779Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:22:24.039351Z","compaction_level":0,"original_size":0,"labels":["epic"]} +{"id":"bd-21m","title":"Epic: Diff Command","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-12T16:22:26.500265Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:22:26.501933Z","compaction_level":0,"original_size":0,"labels":["epic"]} +{"id":"bd-2e4","title":"Flesh out Phase 3 scope and requirements","description":"## Background\nPhase 3 beads (SBOM, keyring, curl gen, breaking-change classification, semantic search, YAML output) are currently stubs without enough detail for implementation. Before any Phase 3 work begins, the requirements and scope for each feature must be fleshed out with proper acceptance criteria, approach, and file lists.\n\n## What Needs to Happen\n1. Review each Phase 3 bead against the PRD's Phase 3 section\n2. Research any dependencies or design decisions not yet documented\n3. Write full bead descriptions (Background, Approach, Acceptance Criteria, Files, TDD Anchor, Edge Cases) for each Phase 3 bead\n4. Validate scope boundaries — what's in vs out for each feature\n\n## Acceptance Criteria\n- [ ] All Phase 3 beads have full descriptions (score 4+/5 on agent-readiness)\n- [ ] Each bead has concrete file lists and TDD anchors\n- [ ] Scope boundaries are documented (what's explicitly out of scope)\n- [ ] No unresolved ambiguities — all genuinely ambiguous decisions are resolved\n\n## Phase 3 Beads to Flesh Out\n- bd-37c: SBOM generation and cosign attestation\n- bd-3pz: OS keychain credential backend\n- bd-60k: Generate curl commands from endpoints\n- bd-j23: Breaking-change classification for diff\n- bd-132: Semantic search with embeddings\n- bd-b8h: YAML output format","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-12T16:42:40.196904Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:42:40.200279Z","compaction_level":0,"original_size":0,"labels":["phase3","planning"],"dependencies":[{"issue_id":"bd-2e4","depends_on_id":"bd-3aq","type":"parent-child","created_at":"2026-02-12T16:42:40.200266Z","created_by":"tayloreernisse"}]} +{"id":"bd-2gp","title":"Implement golden robot output tests and index-only invariant tests","description":"## Background\nGolden tests are the #1 defense against robot JSON shape regressions. They verify structural invariants (ok, data, meta.schema_version, meta.tool_version, meta.command, meta.duration_ms) and snapshot-compare against golden files. Index-only invariant tests verify that list/search work without raw.json (core performance promise).\n\n## Approach\n**Golden tests (tests/integration/golden_test.rs):**\n1. For each command (list, show, search, schemas, tags, aliases), run with --robot\n2. Parse output as JSON, verify structural invariants (ok is bool, data is object, meta has required fields)\n3. Compare against golden snapshot files in tests/integration/golden/\n4. Fail CI if shape changes unless schema_version is incremented\n\n**Index-only invariant tests:**\n- test_list_does_not_read_raw_json: fetch spec, delete raw.json, run list — must succeed\n- test_search_does_not_read_raw_json: same pattern with search\n- test_tags_does_not_read_raw_json: same\n- test_schemas_list_does_not_read_raw_json: same (list mode only; show mode needs raw)\n\n**JSON Schema validation (optional enhancement):**\n- Create docs/robot-schema/v1/success.schema.json and error.schema.json\n- Validate robot output against these schemas in golden tests\n\n## Acceptance Criteria\n- [ ] Golden test verifies all 6 command outputs have correct structure\n- [ ] Index-only tests pass (list/search/tags/schemas-list work without raw.json)\n- [ ] Golden files exist in tests/integration/golden/\n- [ ] JSON Schema files exist in docs/robot-schema/v1/\n\n## Edge Cases\n- **duration_ms non-determinism:** Golden files must NOT include duration_ms in snapshot comparison (it changes every run). Strip or mask it before comparing.\n- **tool_version changes:** Updating Cargo.toml version breaks golden files. Golden comparison should either mask tool_version or tests should update golden files via an env var flag.\n- **Platform-specific key ordering:** serde_json with BTreeMap ensures deterministic ordering. Verify golden files use sorted keys.\n- **Index-only tests must verify raw.json is actually deleted:** Don't just skip loading it — physically remove the file and prove the command works without it.\n\n## Files\n- CREATE: tests/integration/golden_test.rs\n- CREATE: tests/integration/golden/*.json (golden snapshot files)\n- CREATE: docs/robot-schema/v1/success.schema.json\n- CREATE: docs/robot-schema/v1/error.schema.json\n\n## TDD Anchor\nRun all golden and invariant tests.\nVERIFY: `cargo test golden && cargo test does_not_read_raw`\n\n## Dependency Context\nRequires all query commands to be implemented. Uses test helpers from bd-lx6 (Create test fixtures and integration test helpers).","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:30:59.080993Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:58:14.664977Z","compaction_level":0,"original_size":0,"labels":["phase2","testing"],"dependencies":[{"issue_id":"bd-2gp","depends_on_id":"bd-3bl","type":"blocks","created_at":"2026-02-12T16:30:59.083124Z","created_by":"tayloreernisse"},{"issue_id":"bd-2gp","depends_on_id":"bd-acf","type":"blocks","created_at":"2026-02-12T16:30:59.082691Z","created_by":"tayloreernisse"},{"issue_id":"bd-2gp","depends_on_id":"bd-lx6","type":"blocks","created_at":"2026-02-12T16:34:06.420878Z","created_by":"tayloreernisse"},{"issue_id":"bd-2gp","depends_on_id":"bd-p7g","type":"parent-child","created_at":"2026-02-12T16:30:59.082181Z","created_by":"tayloreernisse"},{"issue_id":"bd-2gp","depends_on_id":"bd-x15","type":"blocks","created_at":"2026-02-12T16:30:59.083539Z","created_by":"tayloreernisse"}]} +{"id":"bd-2mr","title":"Add supply chain hardening and robot JSON Schema artifacts","description":"## Background\nSupply chain hardening: release artifacts include SHA256SUMS + minisign signatures. Robot output JSON Schemas published as build artifacts for agent validation.\n\n## Approach\n\n### Supply Chain Artifacts\n1. Update release job in .gitlab-ci.yml to generate SHA256SUMS and sign with minisign\n2. Upload SHA256SUMS + SHA256SUMS.minisig alongside binaries to GitLab Package Registry\n3. Update install.sh to verify signature when minisign is available\n\n### JSON Schema Files\n\nCreate `docs/robot-schema/v1/success.schema.json`:\n```json\n{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"type\": \"object\",\n \"required\": [\"ok\", \"data\", \"meta\"],\n \"properties\": {\n \"ok\": { \"type\": \"boolean\", \"const\": true },\n \"data\": { \"type\": \"object\" },\n \"meta\": {\n \"type\": \"object\",\n \"required\": [\"schema_version\", \"tool_version\", \"command\", \"duration_ms\"],\n \"properties\": {\n \"schema_version\": { \"type\": \"string\", \"pattern\": \"^\\\\d+\\\\.\\\\d+$\" },\n \"tool_version\": { \"type\": \"string\" },\n \"command\": { \"type\": \"string\" },\n \"command_version\": { \"type\": \"string\", \"description\": \"Per-command payload version for independent evolution\" },\n \"duration_ms\": { \"type\": \"integer\", \"minimum\": 0 }\n }\n }\n },\n \"additionalProperties\": false\n}\n```\n\nCreate `docs/robot-schema/v1/error.schema.json`:\n```json\n{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"type\": \"object\",\n \"required\": [\"ok\", \"error\", \"meta\"],\n \"properties\": {\n \"ok\": { \"type\": \"boolean\", \"const\": false },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\"code\", \"message\"],\n \"properties\": {\n \"code\": { \"type\": \"string\" },\n \"message\": { \"type\": \"string\" },\n \"suggestion\": { \"type\": \"string\" }\n }\n },\n \"meta\": {\n \"type\": \"object\",\n \"properties\": {\n \"schema_version\": { \"type\": \"string\" },\n \"tool_version\": { \"type\": \"string\" },\n \"command\": { \"type\": \"string\" },\n \"duration_ms\": { \"type\": \"integer\", \"minimum\": 0 }\n }\n }\n },\n \"additionalProperties\": false\n}\n```\n\n### Compatibility Policy\n- **No version bump:** Adding new optional fields to data or meta (additive changes)\n- **MUST bump schema_version:** Removing fields, renaming fields, changing field types, changing required status\n- **meta.command_version:** Each command can independently evolve its data payload structure. When a command's data shape changes in a breaking way, bump command_version without bumping the global schema_version. This allows agents to pin to specific command output shapes.\n\n## Acceptance Criteria\n- [ ] Release pipeline generates SHA256SUMS from all binary artifacts\n- [ ] minisign signature generated for SHA256SUMS (when key available in CI)\n- [ ] SHA256SUMS and SHA256SUMS.minisig uploaded alongside binaries\n- [ ] install.sh attempts signature verification when minisign is on PATH\n- [ ] docs/robot-schema/v1/success.schema.json matches the structure above\n- [ ] docs/robot-schema/v1/error.schema.json matches the structure above\n- [ ] success.schema.json meta requires: schema_version, tool_version, command, duration_ms\n- [ ] success.schema.json meta includes optional command_version field\n- [ ] error.schema.json error requires: code, message (suggestion is optional)\n- [ ] Both schemas have additionalProperties: false at top level\n- [ ] Schema files are valid JSON Schema (validate with a JSON Schema validator)\n\n## Files\n- MODIFY: .gitlab-ci.yml (add checksum + signing to release)\n- MODIFY: install.sh (add signature verification)\n- CREATE: docs/robot-schema/v1/success.schema.json\n- CREATE: docs/robot-schema/v1/error.schema.json\n\n## Dependency Context\nExtends CI pipeline from bd-1lj and install script from bd-gvr.\n\n## Edge Cases\n- **Schema evolution testing:** When robot output changes, both the code AND the JSON Schema must be updated together. Tests should validate all command outputs against the schemas.\n- **Minisign not available in CI:** If minisign is not installed, the release should still succeed but skip signing with a clear warning (don't fail the release).\n- **JSON Schema draft version:** Use 2020-12 draft. Older validators may not support it — document the minimum validator version.\n- **additionalProperties:false on nested objects:** The top-level schemas have it, but nested objects (data, meta) may need it too if strict validation is desired. Decision: only enforce at top level for now.","status":"open","priority":4,"issue_type":"task","created_at":"2026-02-12T16:31:32.482765Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:56:21.036013Z","compaction_level":0,"original_size":0,"labels":["ci","phase2"],"dependencies":[{"issue_id":"bd-2mr","depends_on_id":"bd-1lj","type":"blocks","created_at":"2026-02-12T16:34:06.478112Z","created_by":"tayloreernisse"},{"issue_id":"bd-2mr","depends_on_id":"bd-1lo","type":"parent-child","created_at":"2026-02-12T16:31:32.485330Z","created_by":"tayloreernisse"},{"issue_id":"bd-2mr","depends_on_id":"bd-gvr","type":"blocks","created_at":"2026-02-12T16:34:06.528568Z","created_by":"tayloreernisse"}]} +{"id":"bd-2pl","title":"Epic: Alias Management","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-12T16:22:22.527514Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:22:22.528316Z","compaction_level":0,"original_size":0,"labels":["epic"]} +{"id":"bd-2s6","title":"Implement doctor command with integrity validation and --fix repair","description":"## Background\nThe doctor command validates installation health and cache integrity. It checks config/cache directories exist, validates each alias's cache files (meta, index, raw), detects integrity issues (generation/hash mismatches, stale index versions, missing files), warns on insecure config permissions, and validates all index pointers against raw.json. Optional --fix repairs recoverable issues.\n\n## Approach\nImplement src/cli/doctor.rs with DoctorArgs and execute():\n\n**Checks performed:**\n1. Config directory exists and is readable/writable\n2. Cache directory exists and is readable/writable\n3. For each alias: check meta.json exists, validate generation/index_hash/index_version against index.json, validate raw_hash against raw.json, validate all operation_ptr/schema_ptr resolve in raw.json\n4. Detect stale caches (>30 days, configurable via config)\n5. Check config.toml permissions -- warn if group/world readable when auth tokens present\n6. Report disk usage (per-alias and total)\n\n**--fix repair modes (per alias, after acquiring lock):**\n1. If raw exists but index missing/invalid or index_version mismatched -> rebuild index from raw\n2. If raw + index valid but meta missing -> reconstruct meta from raw + index\n3. If raw unreadable/unparseable -> delete alias (last resort)\n\n**Health status:** HEALTHY (no issues), WARNING (stale caches, permission issues), DEGRADED (some aliases have integrity issues but are fixable), UNHEALTHY (unfixable corruption).\n\n## Acceptance Criteria\n- [ ] doctor reports HEALTHY for a valid cache\n- [ ] doctor detects missing meta.json as integrity issue\n- [ ] doctor detects generation mismatch between meta and index\n- [ ] doctor detects invalid operation_ptr (pointer doesn't resolve)\n- [ ] doctor warns on stale caches (>30 days)\n- [ ] doctor warns on insecure config permissions\n- [ ] --fix rebuilds index from raw when index is invalid\n- [ ] --fix reconstructs meta when meta is missing but raw+index exist\n- [ ] --fix deletes alias only when raw is unreadable\n- [ ] Robot output: health status, per-alias status, warnings[], disk_usage\n\n## Edge Cases\n- **Concurrent doctor + fetch:** Doctor reads while fetch writes. Doctor should acquire read lock or tolerate mid-write state gracefully (report as integrity issue, not crash).\n- **Very large cache (hundreds of aliases):** Doctor must not OOM — process aliases one at a time, not load all into memory.\n- **Permission denied on cache directory:** Report as WARNING, not crash. Doctor should be resilient to partial access.\n- **Empty alias directory (no files):** Skip with warning, don't crash. This can happen from interrupted deletes.\n- **--fix on locked alias:** If another process holds the lock, skip that alias with warning (don't block).\n\n## Files\n- MODIFY: src/cli/doctor.rs (DoctorArgs, execute, check_alias, fix_alias, permission_check)\n- MODIFY: src/output/robot.rs (add output_doctor)\n- MODIFY: src/output/human.rs (add output_doctor)\n\n## TDD Anchor\nRED: Write `test_doctor_detects_missing_meta` -- create cache with raw+index but no meta, run doctor --robot, assert alias status is \"integrity_error\".\nGREEN: Implement per-alias integrity checking.\nVERIFY: `cargo test test_doctor_detects_missing_meta`\n\n## Dependency Context\nUses CacheManager (load_index, load_raw, list_aliases) from bd-3ea (cache read path). Uses SpecIndex and CacheMetadata types from bd-ilo (error types and core data models). Uses build_index from bd-189 (indexer) for --fix index rebuild.","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:29:50.084259Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:58:11.000975Z","compaction_level":0,"original_size":0,"labels":["health","phase2"],"dependencies":[{"issue_id":"bd-2s6","depends_on_id":"bd-189","type":"blocks","created_at":"2026-02-12T16:29:50.088686Z","created_by":"tayloreernisse"},{"issue_id":"bd-2s6","depends_on_id":"bd-1y0","type":"parent-child","created_at":"2026-02-12T16:29:50.087511Z","created_by":"tayloreernisse"},{"issue_id":"bd-2s6","depends_on_id":"bd-3d2","type":"blocks","created_at":"2026-02-12T16:29:50.089158Z","created_by":"tayloreernisse"},{"issue_id":"bd-2s6","depends_on_id":"bd-3ea","type":"blocks","created_at":"2026-02-12T16:29:50.088266Z","created_by":"tayloreernisse"}]} +{"id":"bd-30a","title":"Implement aliases command with list, rename, delete, set-default","description":"## Background\nThe aliases command manages multiple API specs. List all aliases with stats, show details, rename, delete, set default. All operations except delete are metadata-only (fast). Delete removes the entire alias directory after acquiring lock.\n\n## Approach\nImplement src/cli/aliases.rs with AliasesArgs and execute():\n\n**AliasesArgs:** list (bool, default), show (Option), rename (Option> — [old, new], requires 2 values), delete (Option), set_default (Option).\n\n**Operations:**\n- **List:** CacheManager.list_aliases() -> display name, url, version, is_default, cached_at, size, endpoint/schema counts\n- **Show:** load meta for specific alias, display full details\n- **Rename:** validate new alias name format (same rules as alias creation — alphanumeric, hyphens, underscores), check new name does not already exist, rename directory atomically using `std::fs::rename()` syscall (atomic on same filesystem), if renamed alias was the default alias in config, update config.default_alias to the new name and save\n- **Delete:** acquire lock on alias directory, remove entire alias directory (`std::fs::remove_dir_all`), if deleted alias was the default alias, clear default_alias in config (set to None), save config. No confirmation prompt — CLI is non-interactive for agent compatibility. PRD says \"explicit delete required\" meaning the user must explicitly pass --delete, but no interactive Y/N prompt.\n- **Set-default:** verify alias exists in cache before setting, update config.default_alias, save config. If alias does not exist, return error with suggestion listing available aliases.\n\n## Error Handling Details\n\n**Rename errors:**\n- New name fails format validation -> error with INVALID_ALIAS_NAME code and suggestion showing valid format\n- New name already exists -> error with ALIAS_EXISTS code\n- Rename to same name -> no-op, return success (idempotent, do not error)\n- Old alias does not exist -> error with ALIAS_NOT_FOUND code\n- Filesystem rename fails -> error with IO_ERROR code\n\n**Delete errors:**\n- Alias does not exist -> error with ALIAS_NOT_FOUND code\n- Lock contention (e.g., sync running) -> error with LOCK_CONTENTION code and suggestion to retry\n- Deleting the only alias -> allowed (leaves empty aliases state, no special handling)\n\n**Set-default errors:**\n- Alias does not exist -> error with ALIAS_NOT_FOUND code and suggestion listing available aliases\n\n## Acceptance Criteria\n- [ ] `aliases --robot` lists all aliases with correct metadata (name, url, version, is_default, cached_at, size, endpoint_count, schema_count)\n- [ ] `aliases --show petstore` shows full details for one alias\n- [ ] `aliases --rename old new` renames directory atomically and updates config if renamed alias was default\n- [ ] `aliases --rename old old` (same name) is a no-op, returns success\n- [ ] `aliases --delete old-api` removes alias directory and clears default if it was default\n- [ ] Delete does NOT prompt for confirmation (non-interactive CLI)\n- [ ] `aliases --set-default petstore` updates config, errors if alias does not exist\n- [ ] Rename validates new alias format (alphanumeric, hyphens, underscores)\n- [ ] Rename checks new name does not already exist\n- [ ] Delete of default alias clears default_alias in config\n- [ ] Robot output for each operation is well-structured with ok/data/meta envelope\n- [ ] Error responses include appropriate error codes and suggestions\n\n## Edge Cases\n- **Rename to same name:** No-op, return success (idempotent behavior).\n- **Delete the only alias:** Allowed. Leaves cache in empty state. Subsequent commands that need an alias will error with ALIAS_NOT_FOUND suggesting the user fetch a spec.\n- **Delete while sync is running:** Lock contention. Return LOCK_CONTENTION error with suggestion to wait or retry. Do not force-delete.\n- **Set-default to non-existent alias:** Error with ALIAS_NOT_FOUND and suggestion listing available aliases from cache.\n- **Rename when target name has invalid characters:** Error with INVALID_ALIAS_NAME showing the format rules.\n\n## Files\n- MODIFY: src/cli/aliases.rs (AliasesArgs, execute, rename/delete/set-default logic)\n- MODIFY: src/output/robot.rs (add output_aliases)\n- MODIFY: src/output/human.rs (add output_aliases)\n\n## TDD Anchor\nRED: Write `test_aliases_list` — fetch two specs, run aliases --robot, assert data.aliases has length 2.\nRED: Write `test_aliases_rename` — rename an alias, verify directory moved and config updated.\nRED: Write `test_aliases_rename_same_name` — rename to same name, verify no-op success.\nRED: Write `test_aliases_delete` — delete alias, verify directory removed and config cleared.\nRED: Write `test_aliases_delete_lock_contention` — hold lock on alias, attempt delete, assert LOCK_CONTENTION error.\nGREEN: Implement list_aliases and output.\nVERIFY: `cargo test test_aliases_list`\n\n## Dependency Context\nUses CacheManager (list_aliases, delete_alias) from bd-3ea. Uses Config (default_alias, save) from bd-1sb.","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:28:47.390765Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:46:14.418127Z","compaction_level":0,"original_size":0,"labels":["management","phase2"],"dependencies":[{"issue_id":"bd-30a","depends_on_id":"bd-1sb","type":"blocks","created_at":"2026-02-12T16:28:47.395669Z","created_by":"tayloreernisse"},{"issue_id":"bd-30a","depends_on_id":"bd-2pl","type":"parent-child","created_at":"2026-02-12T16:28:47.394226Z","created_by":"tayloreernisse"},{"issue_id":"bd-30a","depends_on_id":"bd-3d2","type":"blocks","created_at":"2026-02-12T16:28:47.396077Z","created_by":"tayloreernisse"},{"issue_id":"bd-30a","depends_on_id":"bd-3ea","type":"blocks","created_at":"2026-02-12T16:28:47.394978Z","created_by":"tayloreernisse"}]} +{"id":"bd-37c","title":"Add SBOM generation and cosign attestation","description":"## What\nGenerate SBOM (CycloneDX or SPDX) during CI build. Sign release artifacts with cosign for provenance attestation.\n\n## Acceptance Criteria\n- [ ] SBOM generated in CI pipeline\n- [ ] cosign attestation attached to release artifacts\n- [ ] Verifiable with cosign verify\n\n## Files\n- MODIFY: .gitlab-ci.yml (add SBOM + cosign steps)","status":"open","priority":4,"issue_type":"task","created_at":"2026-02-12T16:31:57.365996Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:42:45.149708Z","compaction_level":0,"original_size":0,"labels":["future","phase3"],"dependencies":[{"issue_id":"bd-37c","depends_on_id":"bd-2e4","type":"blocks","created_at":"2026-02-12T16:42:45.149692Z","created_by":"tayloreernisse"},{"issue_id":"bd-37c","depends_on_id":"bd-3aq","type":"parent-child","created_at":"2026-02-12T16:31:57.367010Z","created_by":"tayloreernisse"}]} +{"id":"bd-3aq","title":"Epic: Phase 3 Future","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-12T16:22:29.339564Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:22:29.340289Z","compaction_level":0,"original_size":0,"labels":["epic"]} +{"id":"bd-3b6","title":"Build async HTTP client with SSRF protection and streaming download","description":"## Background\nswagger-cli fetches OpenAPI specs over HTTPS with strict security controls. The HTTP client must enforce SSRF protection (blocking private/loopback/link-local/multicast IPs), require HTTPS by default, support streaming downloads with max-bytes enforcement, and handle retries with backoff. This is async (tokio + reqwest).\n\n## Approach\nCreate src/core/http.rs with:\n\n**AsyncHttpClient struct:** Wraps reqwest::Client configured with rustls-tls, connect timeout (5s), overall timeout (configurable, default 10s), redirect policy (max 5). Provides `fetch_spec()` async method.\n\n**SSRF Protection:** Before connecting, resolve the hostname and check the IP against blocked CIDR ranges: 127.0.0.0/8, ::1, 169.254.0.0/16, fe80::/10, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, multicast (224.0.0.0/4, ff00::/8). Also check resolved IP AFTER redirects (DNS rebinding defense). Return PolicyBlocked error for violations. Accept --allow-private-host exceptions.\n\n**HTTPS Enforcement:** Reject http:// URLs unless --allow-insecure-http is set. Return PolicyBlocked.\n\n**Streaming Download:** Use response.chunk() in a loop, counting bytes. Abort when exceeding max_bytes (default 25MB). This prevents OOM on huge specs.\n\n**Retries:** Retry on 5xx and network errors up to N times (default 2) with exponential backoff + jitter. Honor Retry-After header. Do NOT retry 4xx (except 429 rate limit).\n\n**Auth Headers:** Accept Vec of (name, value) header pairs. Redact auth values in any error messages.\n\n## Acceptance Criteria\n- [ ] fetch_spec(\"https://...\") returns body bytes on success\n- [ ] Loopback IP (127.0.0.1) is blocked with PolicyBlocked error\n- [ ] Private IP (10.0.0.1) is blocked with PolicyBlocked error\n- [ ] Link-local (169.254.169.254) is blocked with PolicyBlocked error\n- [ ] http:// URL without --allow-insecure-http returns PolicyBlocked\n- [ ] Download exceeding max_bytes aborts with InvalidSpec error\n- [ ] 401/403 returns Auth error (not retried)\n- [ ] 500 is retried up to retry count\n- [ ] Auth header values are not included in error messages\n\n## Files\n- CREATE: src/core/http.rs (AsyncHttpClient, SSRF checks, streaming download, retries)\n- MODIFY: src/core/mod.rs (pub mod http;)\n\n## TDD Anchor\nRED: Write `test_ssrf_blocks_loopback` — call the IP validation function with 127.0.0.1, assert it returns Err(PolicyBlocked).\nGREEN: Implement CIDR range checking.\nVERIFY: `cargo test test_ssrf_blocks`\n\nAdditional tests (use mockito for HTTP):\n- test_fetch_success_https\n- test_fetch_rejects_http\n- test_fetch_max_bytes_abort\n- test_fetch_retries_on_500\n- test_fetch_no_retry_on_401\n- test_auth_header_redacted_in_errors\n\n## Edge Cases\n- DNS resolution must happen BEFORE connecting — use `tokio::net::lookup_host()` or reqwest's resolve API\n- DNS rebinding: a hostname might resolve to public IP initially, then private IP on redirect. Check IP at EACH hop.\n- IPv6 mapped IPv4 addresses (::ffff:127.0.0.1) must also be caught\n- Retry-After header may be seconds or HTTP-date — parse both formats\n- connect_timeout (5s) is separate from overall timeout (10s)\n\n## Dependency Context\nUses SwaggerCliError variants (Network, Auth, PolicyBlocked, InvalidSpec) from bd-ilo (error types and core data models).","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:26:35.163338Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:55:53.972326Z","compaction_level":0,"original_size":0,"labels":["fetch","phase1","security"],"dependencies":[{"issue_id":"bd-3b6","depends_on_id":"bd-3d2","type":"blocks","created_at":"2026-02-12T16:26:35.167736Z","created_by":"tayloreernisse"},{"issue_id":"bd-3b6","depends_on_id":"bd-3ny","type":"parent-child","created_at":"2026-02-12T16:26:35.167093Z","created_by":"tayloreernisse"}]} +{"id":"bd-3bl","title":"Implement tags command","description":"## Background\nThe tags command is simple — it lists OpenAPI tags with their endpoint counts and descriptions. Pure index-backed, fast.\n\n## Approach\nImplement src/cli/tags.rs with TagsArgs (alias only) and execute(). Load index, output tags from index.tags (already sorted and counted during index building). Robot: data.tags[] with name, description, endpoint_count. Human: formatted list with \"X total\" in header.\n\n## Acceptance Criteria\n- [ ] `tags petstore --robot` returns JSON with data.tags array\n- [ ] Each tag has name (string), description (string|null), endpoint_count (integer)\n- [ ] Tags sorted by name ASC (pre-sorted in index)\n- [ ] Human output shows tag name, count, and description\n- [ ] Human output shows \"X total\" count in header line\n- [ ] Tags with no description show null in robot output, empty/omitted in human output\n- [ ] Empty tags list (spec with no tags defined) returns ok:true with data.tags as empty array\n- [ ] Robot meta includes standard fields (schema_version, tool_version, command, duration_ms)\n\n## Edge Cases\n- **Spec with no tags:** Return ok:true, data.tags: [], meta.total: 0. Human output: \"0 total\" header, no rows.\n- **Tags with empty descriptions:** Tag defined in spec with `description: \"\"` — treat as null/empty in output (same as missing description).\n- **Orphaned tags:** Tags defined at root level in the OpenAPI spec but not referenced by any operation. These should still appear in output with endpoint_count: 0 (they exist in the spec, the command reports what the spec declares).\n\n## Files\n- MODIFY: src/cli/tags.rs (TagsArgs, execute)\n- MODIFY: src/output/robot.rs (add output_tags)\n- MODIFY: src/output/human.rs (add output_tags)\n\n## TDD Anchor\nRED: Write `test_tags_list` — fetch petstore, run tags --robot, assert data.tags has expected tag count.\nRED: Write `test_tags_empty` — use a spec with no tags, assert data.tags is empty array.\nRED: Write `test_tags_no_description` — use a spec with a tag that has no description, assert description is null in robot output.\nGREEN: Implement tags command.\nVERIFY: `cargo test test_tags_list`\n\n## Dependency Context\nUses SpecIndex and IndexedTag types from bd-ilo (error types and core data models). Uses CacheManager.load_index from bd-3ea (cache read path).","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-12T16:28:05.366529Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:55:58.303891Z","compaction_level":0,"original_size":0,"labels":["phase2","query"],"dependencies":[{"issue_id":"bd-3bl","depends_on_id":"bd-3d2","type":"blocks","created_at":"2026-02-12T16:28:05.368603Z","created_by":"tayloreernisse"},{"issue_id":"bd-3bl","depends_on_id":"bd-3ea","type":"blocks","created_at":"2026-02-12T16:28:05.368039Z","created_by":"tayloreernisse"},{"issue_id":"bd-3bl","depends_on_id":"bd-jek","type":"parent-child","created_at":"2026-02-12T16:28:05.367634Z","created_by":"tayloreernisse"}]} +{"id":"bd-3d2","title":"Build CLI skeleton with clap and output formatting framework","description":"## Background\nswagger-cli needs a CLI parser (clap) that routes to subcommands and an output framework that handles both human-readable and robot JSON formatting. The CLI skeleton defines the top-level Cli struct with global flags (--robot, --pretty, --network, --config) and the Commands enum with all 11 subcommands. The output framework provides consistent formatting for all commands.\n\n## Approach\n**CLI (src/cli/mod.rs):**\n- Define `Cli` struct with `#[derive(Parser)]`: command (Commands subcommand), robot (bool, global), pretty (bool, global), network (String, global, default \"auto\"), config (Option, global, env SWAGGER_CLI_CONFIG)\n- Define `Commands` enum with all 11 variants: Fetch, List, Show, Search, Schemas, Tags, Aliases, Sync, Doctor, Cache, Diff\n- Create stub modules for each command (src/cli/fetch.rs, list.rs, etc.) with empty `FetchArgs` structs and `pub async fn execute()` signatures returning `Result<(), SwaggerCliError>`\n- Note: ALL execute functions use `async fn` signatures (we use tokio runtime throughout). Fetch, sync, and diff perform actual async I/O; query commands (list, show, search, etc.) are async in signature but may not await internally.\n\n**Main (src/main.rs):**\n- Pre-scan argv for `--robot` before clap parsing (handles parse errors in robot JSON)\n- `#[tokio::main] async fn main()` that tries Cli::try_parse_from, routes to command execute, handles errors\n- `output_robot_error()` and `output_human_error()` functions per PRD\n\n**Output (src/output/):**\n- `mod.rs`: Common traits/helpers, `RobotEnvelope` struct with ok, data, meta fields\n- `robot.rs`: Generic `robot_success()` and `robot_error()` functions that build the envelope with schema_version=1, tool_version from CARGO_PKG_VERSION, command name, duration_ms\n- `human.rs`: Stub formatters, TTY detection for color/unicode\n- `table.rs`: Table formatting helpers using `tabled` crate\n\n## Acceptance Criteria\n- [ ] `cargo build` succeeds with the full CLI skeleton\n- [ ] `swagger-cli --help` shows all 11 subcommands\n- [ ] `swagger-cli --version` prints version from Cargo.toml\n- [ ] `swagger-cli nonexistent --robot` outputs JSON error to stderr with code USAGE_ERROR and exits 2\n- [ ] `swagger-cli fetch --robot` (missing required args) outputs JSON error to stderr\n- [ ] RobotEnvelope serializes with ok, data, meta.schema_version, meta.tool_version, meta.command, meta.duration_ms\n- [ ] All command stubs return `Ok(())` (do nothing yet)\n\n## Files\n- CREATE: src/cli/mod.rs (Cli, Commands, pub mods)\n- CREATE: src/cli/fetch.rs, list.rs, show.rs, search.rs, schemas.rs, tags.rs, aliases.rs, sync.rs, doctor.rs, cache.rs, diff.rs (stub args + execute)\n- CREATE: src/output/mod.rs, src/output/robot.rs, src/output/human.rs, src/output/table.rs\n- MODIFY: src/main.rs (full entry point with error handling)\n- MODIFY: src/lib.rs (pub mod cli; pub mod output;)\n\n## TDD Anchor\nRED: Write test `test_robot_error_on_bad_command` using `assert_cmd` -- run `swagger-cli nonexistent --robot`, parse stderr as JSON, assert the JSON contains code=\"USAGE_ERROR\" and the process exits with code 2.\nGREEN: Implement main.rs pre-scan and robot error output.\nVERIFY: `cargo test test_robot_error_on_bad_command`\n\n## Edge Cases\n- Pre-scan argv for --robot MUST happen before clap parsing, otherwise clap's error output is plaintext even when agent expects JSON\n- Global --robot flag must be accessible from all subcommands (use `#[arg(long, global = true)]`)\n- duration_ms in robot envelope: use `std::time::Instant::now()` at start, elapsed at output\n- BTreeMap (not HashMap) for any constructed JSON objects to ensure deterministic key ordering\n\n## Dependency Context\nUses SwaggerCliError (exit_code, code, suggestion) from bd-ilo (error types and core data models).","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:24:12.604507Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:55:49.272227Z","compaction_level":0,"original_size":0,"labels":["foundation","phase1"],"dependencies":[{"issue_id":"bd-3d2","depends_on_id":"bd-3e0","type":"parent-child","created_at":"2026-02-12T16:24:12.606348Z","created_by":"tayloreernisse"},{"issue_id":"bd-3d2","depends_on_id":"bd-ilo","type":"blocks","created_at":"2026-02-12T16:24:12.607227Z","created_by":"tayloreernisse"}]} +{"id":"bd-3e0","title":"Epic: Project Foundation","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-12T16:22:16.954888Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:22:16.956168Z","compaction_level":0,"original_size":0,"labels":["epic"]} +{"id":"bd-3ea","title":"Implement cache read path with integrity validation","description":"## Background\nThe cache read path is used by every query command (list, search, tags, schemas, aliases, doctor). It loads index.json and meta.json, validates their integrity (generation match, hash match, index_version match), and provides the data to commands. The `show` command additionally loads raw.json. Coalesced last_accessed updates reduce write amplification for hot-read bursts.\n\n## Approach\nImplement on CacheManager:\n- `load_index(alias) -> Result<(SpecIndex, CacheMetadata)>`: Read meta.json first (commit marker). If missing -> AliasNotFound or CacheIntegrity. Read index.json. Validate: meta.index_version == index.index_version, meta.generation == index.generation, meta.index_hash == sha256(index.json bytes). Mismatch -> CacheIntegrity error. Update last_accessed if stale >10min (best-effort, no lock required).\n- `load_raw(alias, meta: &CacheMetadata) -> Result`: Read raw.json, parse as Value. Validate meta.raw_hash == sha256(raw.json bytes). Return Value.\n- `list_aliases() -> Result>`: Iterate alias directories, read meta.json from each. Skip broken/partial aliases (log warning).\n- `delete_alias(alias) -> Result<()>`: Remove alias directory after acquiring lock.\n- `default_alias() -> Option`: Load config, return default_alias.\n- `alias_exists(alias) -> bool`: Check if meta.json exists in alias dir.\n\nCoalesced last_accessed: When load_index reads meta, check if meta.last_accessed is >10min old. If so, update only the last_accessed field in meta.json (best-effort write, no lock, ignore errors).\n\n## Acceptance Criteria\n- [ ] load_index succeeds for a valid cache (all 4 files present, hashes match)\n- [ ] load_index returns CacheIntegrity when generation mismatches\n- [ ] load_index returns CacheIntegrity when index_hash mismatches\n- [ ] load_index returns AliasNotFound when alias directory doesn't exist\n- [ ] load_raw validates raw_hash and returns parsed Value\n- [ ] list_aliases returns metadata for all valid aliases, skips broken ones\n- [ ] delete_alias removes the entire alias directory\n- [ ] last_accessed is updated at most once per 10 minutes\n\n## Files\n- MODIFY: src/core/cache.rs (add load_index, load_raw, list_aliases, delete_alias, default_alias, alias_exists, coalesced last_accessed)\n\n## TDD Anchor\nRED: Write `test_load_index_integrity_check` -- create a valid cache, then tamper with index.json (change a byte). Assert load_index returns CacheIntegrity error.\nGREEN: Implement hash validation in load_index().\nVERIFY: `cargo test test_load_index_integrity_check`\n\nAdditional tests:\n- test_load_index_success\n- test_load_index_missing_meta\n- test_load_raw_validates_hash\n- test_list_aliases_skips_broken\n- test_coalesced_last_accessed (verify not written within 10min window)\n\n## Edge Cases\n- list_aliases must not panic if an alias directory has no meta.json -- skip with warning\n- Coalesced write is best-effort: if it fails (permissions, concurrent write), silently ignore\n- load_raw is only called by show and schemas --show -- never by list/search/tags\n- Empty cache directory (no aliases) should return empty Vec, not error\n\n## Dependency Context\nUses CacheManager and write_cache() from bd-1ie (cache write path). Uses CacheMetadata, SpecIndex, and SwaggerCliError from bd-ilo (error types and core data models).","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:25:15.526245Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:55:44.766573Z","compaction_level":0,"original_size":0,"labels":["infrastructure","phase1"],"dependencies":[{"issue_id":"bd-3ea","depends_on_id":"bd-1ie","type":"blocks","created_at":"2026-02-12T16:34:05.956814Z","created_by":"tayloreernisse"},{"issue_id":"bd-3ea","depends_on_id":"bd-hcb","type":"parent-child","created_at":"2026-02-12T16:25:15.528127Z","created_by":"tayloreernisse"},{"issue_id":"bd-3ea","depends_on_id":"bd-ilo","type":"blocks","created_at":"2026-02-12T16:25:15.528786Z","created_by":"tayloreernisse"}]} +{"id":"bd-3f4","title":"Implement sync command with change detection and index-based diffs","description":"## Background\nThe sync command checks if a remote spec has changed and re-fetches if needed. Change detection uses ETag/Last-Modified headers and content hash comparison. Index-based diffs compute added/removed/modified endpoints and schemas by comparing old vs new indexes. This is async (network calls).\n\n## Approach\nImplement src/cli/sync.rs with SyncArgs and async execute():\n\n**Single alias sync flow:**\n1. Load existing meta (get ETag, Last-Modified, content_hash)\n2. Fetch remote spec with conditional headers (If-None-Match, If-Modified-Since)\n3. If 304 Not Modified → report \"no changes\"\n4. If 200: compute new content_hash, compare with stored\n5. If hash matches → report \"no changes\" (content identical despite no 304)\n6. If changed: normalize, build new index, compare old vs new index\n7. Compute diff: added/removed/modified endpoints and schemas (compare by path+method / name)\n8. If not --dry-run: write new cache\n9. Output change summary\n\n**SyncArgs:** alias (Option), all (bool), dry_run (bool), force (bool — re-fetch regardless), details (bool — include change lists), jobs (usize, default 4), per_host (usize, default 2), resume (bool), max_failures (Option).\n\n**--details output:** Capped at 200 items per category. Include truncated:bool flag.\n\n## Acceptance Criteria\n- [ ] Sync detects \"no changes\" via 304 response\n- [ ] Sync detects changes via content hash mismatch\n- [ ] --dry-run checks without writing\n- [ ] --force re-fetches regardless\n- [ ] --details includes added/removed/modified endpoint/schema lists (capped at 200)\n- [ ] Robot output: changed (bool), local_version, remote_version, changes.endpoints/schemas counts\n- [ ] --force with --dry-run: fetches and computes diff but doesn't write\n\n## Edge Cases\n- **304 Not Modified but content actually changed:** Server returns 304 incorrectly. Content hash comparison catches this (hash mismatch despite 304 = treat as changed).\n- **Huge index diff:** If thousands of endpoints changed, --details output must respect the 200-item cap with truncated:true flag.\n- **Server returns different Content-Type:** e.g., returns HTML error page instead of JSON. Format detection catches this — invalid spec error.\n- **ETag/Last-Modified missing on first fetch:** meta.etag and meta.last_modified will be None. Sync without conditional headers — always downloads full content.\n- **--force with --dry-run:** Fetch the content but don't write. Report what would change. This combo must be supported.\n\n## Files\n- MODIFY: src/cli/sync.rs (SyncArgs, execute, single_alias_sync, compute_index_diff)\n- MODIFY: src/output/robot.rs (add output_sync)\n- MODIFY: src/output/human.rs (add output_sync)\n\n## TDD Anchor\nRED: Write `test_sync_no_changes` — fetch petstore fixture locally, run sync --robot (mock server returns same content), assert changed==false.\nGREEN: Implement hash-based change detection.\nVERIFY: `cargo test test_sync_no_changes`\n\n## Dependency Context\nUses AsyncHttpClient from bd-3b6 for fetching. Uses indexer from bd-189 for rebuilding. Uses CacheManager from bd-3ea/bd-1ie for read/write. Requires a fetched spec in cache.","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:28:47.430949Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:58:08.755348Z","compaction_level":0,"original_size":0,"labels":["phase2","sync"],"dependencies":[{"issue_id":"bd-3f4","depends_on_id":"bd-161","type":"parent-child","created_at":"2026-02-12T16:28:47.432443Z","created_by":"tayloreernisse"},{"issue_id":"bd-3f4","depends_on_id":"bd-16o","type":"blocks","created_at":"2026-02-12T16:28:47.432950Z","created_by":"tayloreernisse"},{"issue_id":"bd-3f4","depends_on_id":"bd-189","type":"blocks","created_at":"2026-02-12T16:28:47.433412Z","created_by":"tayloreernisse"}]} +{"id":"bd-3km","title":"Implement list command with index-backed filtering and sorting","description":"## Background\nThe list command is the most-used query command. It loads only index.json (never raw.json) and filters/sorts endpoints by method, tag, and path regex. This is the command that must hit <50ms for cached queries, even on large specs like Stripe (312 endpoints) and GitHub (800+ endpoints).\n\n## Approach\nImplement src/cli/list.rs with ListArgs and execute():\n\n**ListArgs:** alias (Option), method (Option, value_parser GET/POST/PUT/DELETE/PATCH), tag (Option), path (Option — regex), sort (String, default \"path\", values: path/method/tag), limit (usize, default 50), all (bool — show all, no limit), all_aliases (bool — cross-alias search, Phase 2 bead).\n\n**Execute flow:**\n1. Resolve alias (use default if not specified)\n2. CacheManager::load_index(alias) — loads index.json + meta.json only\n3. Build regex from --path (fail fast with USAGE_ERROR on invalid regex)\n4. Filter endpoints: method match (case-insensitive), tag contains, path regex match\n5. Sort using canonical method ranking: GET=0, POST=1, PUT=2, PATCH=3, DELETE=4, OPTIONS=5, HEAD=6, TRACE=7\n6. Apply limit (unless --all)\n7. Output robot JSON or human table\n\n**Robot output format:** Per PRD — ok:true, data.endpoints[], data.total, data.filtered, data.applied_filters, meta with alias/spec_version/cached_at/duration_ms.\n\n**Human output:** Tabular format with METHOD PATH SUMMARY, header with API title and total count, footer with \"Showing X of Y endpoints (filtered)\".\n\n## Acceptance Criteria\n- [ ] `swagger-cli list petstore --robot` returns all endpoints as JSON\n- [ ] --method POST filters to POST-only endpoints\n- [ ] --tag pet filters to pet-tagged endpoints\n- [ ] --path \"store.*\" filters by regex\n- [ ] Invalid regex returns USAGE_ERROR (exit 2)\n- [ ] Combined filters work (--method POST --tag pet)\n- [ ] Default limit is 50; --all shows all\n- [ ] Endpoints sorted by path ASC, method_rank ASC by default\n- [ ] --sort method sorts by method rank first\n- [ ] No alias + no default → CONFIG_ERROR with suggestion\n- [ ] Command completes in <50ms for 500+ endpoint index\n\n## Files\n- MODIFY: src/cli/list.rs (ListArgs, execute, filter_endpoints, sort_endpoints, method_rank)\n- MODIFY: src/output/robot.rs (add output_list)\n- MODIFY: src/output/human.rs (add output_list)\n\n## TDD Anchor\nRED: Write `test_list_filter_by_method` — set up test cache with petstore, run list --method POST --robot, parse JSON, assert all endpoints have method==POST.\nGREEN: Implement filter_endpoints with method check.\nVERIFY: `cargo test test_list_filter_by_method`\n\n## Edge Cases\n- Empty result set (no matches) should return ok:true with empty endpoints array, not error\n- Path regex with special chars (e.g., `/pet/{petId}` — the braces are regex special) — users must escape\n- Method comparison must be case-insensitive (user passes \"post\", spec has \"POST\")\n\n## Dependency Context\nUses CacheManager.load_index from bd-3ea (cache read). Uses SpecIndex/IndexedEndpoint types from bd-ilo. Uses CLI skeleton from bd-3d2. Requires a fetched spec in cache to work (tested via test helper that calls fetch with local fixture).","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:27:27.054531Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:27:27.058996Z","compaction_level":0,"original_size":0,"labels":["phase1","query"],"dependencies":[{"issue_id":"bd-3km","depends_on_id":"bd-3d2","type":"blocks","created_at":"2026-02-12T16:27:27.058985Z","created_by":"tayloreernisse"},{"issue_id":"bd-3km","depends_on_id":"bd-3ea","type":"blocks","created_at":"2026-02-12T16:27:27.058492Z","created_by":"tayloreernisse"},{"issue_id":"bd-3km","depends_on_id":"bd-epk","type":"parent-child","created_at":"2026-02-12T16:27:27.057878Z","created_by":"tayloreernisse"}]} +{"id":"bd-3ll","title":"Epic: Global Features","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-12T16:22:25.182608Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:22:25.183512Z","compaction_level":0,"original_size":0,"labels":["epic"]} +{"id":"bd-3ny","title":"Epic: Fetch Pipeline","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-12T16:22:18.835110Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:22:18.835697Z","compaction_level":0,"original_size":0,"labels":["epic"]} +{"id":"bd-3pz","title":"Add OS keychain credential backend","description":"## What\nImplement CredentialSource::Keyring variant — resolve auth tokens from macOS Keychain or Linux Secret Service at runtime.\n\n## Acceptance Criteria\n- [ ] macOS Keychain lookup works\n- [ ] Linux Secret Service lookup works\n- [ ] Graceful fallback when keychain unavailable\n\n## Files\n- CREATE: src/core/keyring.rs\n- MODIFY: src/core/config.rs (implement Keyring resolution)","status":"open","priority":4,"issue_type":"task","created_at":"2026-02-12T16:31:57.341889Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:42:45.178091Z","compaction_level":0,"original_size":0,"labels":["future","phase3"],"dependencies":[{"issue_id":"bd-3pz","depends_on_id":"bd-2e4","type":"blocks","created_at":"2026-02-12T16:42:45.178074Z","created_by":"tayloreernisse"},{"issue_id":"bd-3pz","depends_on_id":"bd-3aq","type":"parent-child","created_at":"2026-02-12T16:31:57.342970Z","created_by":"tayloreernisse"}]} +{"id":"bd-60k","title":"Generate curl commands from endpoints","description":"## What\nNew command: swagger-cli curl [--method METHOD] that generates a ready-to-run curl command with correct URL, headers, auth, and example request body.\n\n## Acceptance Criteria\n- [ ] Generates valid curl command for endpoint\n- [ ] Includes auth headers from profile\n- [ ] Includes example request body from schema\n\n## Files\n- CREATE: src/cli/curl.rs","status":"open","priority":4,"issue_type":"task","created_at":"2026-02-12T16:31:57.318701Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:42:45.203709Z","compaction_level":0,"original_size":0,"labels":["future","phase3"],"dependencies":[{"issue_id":"bd-60k","depends_on_id":"bd-2e4","type":"blocks","created_at":"2026-02-12T16:42:45.203691Z","created_by":"tayloreernisse"},{"issue_id":"bd-60k","depends_on_id":"bd-3aq","type":"parent-child","created_at":"2026-02-12T16:31:57.319605Z","created_by":"tayloreernisse"}]} +{"id":"bd-a7e","title":"Bootstrap Rust project and directory structure","description":"## Background\nswagger-cli is a greenfield Rust CLI tool for querying OpenAPI specifications. Nothing exists yet — no Cargo.toml, no src/, no VCS. This bead creates the complete project skeleton that all other beads build on.\n\nNote: The PRD code examples show sync patterns (blocking reqwest), but we've made a conscious decision to implement async from the start (tokio + non-blocking reqwest), following feedback-2 recommendations. The PRD code examples are stale in this regard.\n\n## Approach\n1. Run `cargo init --name swagger-cli` to create the Rust project\n2. Run `jj init --git` to initialize jj version control (colocated with git)\n3. Populate Cargo.toml with all dependencies (async tokio instead of blocking reqwest):\n - `reqwest = { version = \"0.13\", default-features = false, features = [\"json\", \"rustls-tls\"] }`\n - `tokio = { version = \"1\", features = [\"full\"] }`\n - `serde`, `serde_json`, `serde_yaml`, `clap`, `anyhow`, `thiserror`, `toml`, `directories`, `colored`, `tabled`, `chrono`, `regex`, `sha2`, `fs2`\n - dev-deps: `assert_cmd`, `predicates`, `tempfile`, `mockito`, `criterion`, `tokio` (macros+rt)\n4. Create full directory structure: src/{main.rs, lib.rs, errors.rs, utils.rs}, src/cli/, src/core/, src/output/, tests/integration/, tests/fixtures/, benches/, docs/robot-schema/v1/\n5. Add .gitignore, run `cargo check`\n\n## Acceptance Criteria\n- [ ] `cargo check` succeeds with zero errors\n- [ ] Cargo.toml has all dependencies with correct features (async reqwest, tokio)\n- [ ] src/main.rs has `#[tokio::main] async fn main()` that exits 0\n- [ ] All directories from project structure exist\n- [ ] src/lib.rs declares pub mods for cli, core, output, errors, utils\n- [ ] jj initialized (`.jj/` exists)\n- [ ] .gitignore present\n\n## Files\n- CREATE: Cargo.toml, src/main.rs, src/lib.rs, src/errors.rs, src/utils.rs, src/cli/mod.rs, src/core/mod.rs, src/output/mod.rs, .gitignore\n\n## TDD Anchor\nRED: `cargo check` fails (no modules). GREEN: Create placeholders. VERIFY: `cargo check && echo OK`\n\n## Edge Cases\n- Use `edition = \"2021\"` (safer for dep compat)\n- Do NOT add `[[bench]]` entries yet\n- main.rs should NOT do any real work yet","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-12T16:23:21.100423Z","created_by":"tayloreernisse","updated_at":"2026-02-12T17:33:03.858953Z","closed_at":"2026-02-12T17:33:03.858784Z","close_reason":"Completed: project bootstrapped with edition 2024, all deps, async tokio, directory structure, jj init","compaction_level":0,"original_size":0,"labels":["foundation","phase1"],"dependencies":[{"issue_id":"bd-a7e","depends_on_id":"bd-3e0","type":"parent-child","created_at":"2026-02-12T16:23:21.102893Z","created_by":"tayloreernisse"}]} +{"id":"bd-acf","title":"Implement search engine with tokenized scoring and search command","description":"## Background\nThe search engine is a core piece of swagger-cli. It provides tokenized multi-term text search across endpoint paths, summaries, descriptions, and schema names. All search is index-backed (never loads raw.json). Scores are quantized to integer basis points for cross-platform determinism. Results include ranked ordering with stable tie-breaking.\n\n## Approach\nImplement src/core/search.rs with SearchEngine struct and src/cli/search.rs with SearchArgs:\n\n**SearchEngine:** Takes a SpecIndex reference. search() method tokenizes query (whitespace-split unless --exact), scores each endpoint and schema against terms with field weights: path=10, summary=5, description=2, schema_name=8. Coverage boost: matching more terms increases score (score *= 1.0 + coverage_ratio). Quantize: (raw_float * 100.0).round() as u32.\n\n**Tie-breaking (deterministic):** Primary: score DESC. Secondary: type ordinal (endpoint=0 before schema=1). Tertiary: path/name ASC, method_rank ASC. Assign 1-based rank after sorting.\n\n**Unicode-safe snippets:** safe_snippet() uses char_indices() to find char-boundary-safe positions. Context window of 50 chars before/after match. Prefix/suffix with \"...\" when truncated.\n\n**SearchArgs:** alias (Option), query (String, positional), case_sensitive (bool), exact (bool), in_fields (Option -- comma-separated: all/paths/descriptions/schemas, invalid -> USAGE_ERROR), limit (usize, default 20), all_aliases (bool -- Phase 2 cross-alias).\n\n**SearchOptions:** search_paths, search_descriptions, search_schemas (parsed from --in), case_sensitive, exact, limit.\n\n## Acceptance Criteria\n- [ ] search(\"pet status\") finds endpoints with \"pet\" and \"status\" in path/summary\n- [ ] Scores quantized to integer (u32), deterministic across platforms\n- [ ] --exact treats query as single phrase\n- [ ] --case-sensitive respects case\n- [ ] --in paths only searches path field\n- [ ] --in invalid_field returns USAGE_ERROR\n- [ ] Results sorted by score DESC with stable tie-breaking\n- [ ] Unicode-safe snippets don't panic on multi-byte chars\n- [ ] Robot output: results[] with type, path/name, method, summary, rank, score, matches[]\n- [ ] Search never loads raw.json (index-only)\n\n## Files\n- CREATE: src/core/search.rs (SearchEngine, SearchResult, SearchResultType, Match, SearchOptions, tokenize, safe_snippet)\n- MODIFY: src/cli/search.rs (SearchArgs, execute)\n- MODIFY: src/output/robot.rs (add output_search)\n- MODIFY: src/output/human.rs (add output_search)\n\n## TDD Anchor\nRED: Write `test_search_basic` -- create a SpecIndex with petstore endpoints, search for \"pet status\", assert results include /pet/findByStatus.\nGREEN: Implement SearchEngine::search().\nVERIFY: `cargo test test_search_basic`\n\nRED: Write `test_search_scores_deterministic` -- search for \"pet\" on petstore index, record scores, search again, assert identical scores and ordering.\nGREEN: Implement quantized scoring with stable sort.\nVERIFY: `cargo test test_search_scores_deterministic`\n\n## Edge Cases\n- Empty query should return empty results, not error\n- Single-char queries should work (common for agents: \"/\" to find all paths)\n- Coverage boost division by zero: if terms.len() == 0, skip boost (shouldn't happen with non-empty query)\n- safe_snippet on very short text: don't add \"...\" if text fits entirely within context\n\n## Dependency Context\nUses SpecIndex, IndexedEndpoint, and IndexedSchema types from bd-ilo (error types and core data models). Uses CacheManager.load_index from bd-3ea (cache read path). Uses CLI skeleton from bd-3d2.","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:28:05.308043Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:55:56.194230Z","compaction_level":0,"original_size":0,"labels":["phase2","query"],"dependencies":[{"issue_id":"bd-acf","depends_on_id":"bd-3d2","type":"blocks","created_at":"2026-02-12T16:28:05.310709Z","created_by":"tayloreernisse"},{"issue_id":"bd-acf","depends_on_id":"bd-3ea","type":"blocks","created_at":"2026-02-12T16:28:05.310222Z","created_by":"tayloreernisse"},{"issue_id":"bd-acf","depends_on_id":"bd-jek","type":"parent-child","created_at":"2026-02-12T16:28:05.309771Z","created_by":"tayloreernisse"}]} +{"id":"bd-b8h","title":"Add YAML output format (--format yaml)","description":"## What\nAdd --format yaml option to list/show/search/schemas commands. Use serde_yaml for serialization with deterministic key ordering. Create YAML golden fixtures.\n\n## Acceptance Criteria\n- [ ] --format yaml outputs valid YAML for all query commands\n- [ ] YAML output is deterministic (sorted keys)\n- [ ] Golden YAML fixtures exist\n\n## Files\n- MODIFY: src/output/mod.rs (add yaml output mode)\n- CREATE: src/output/yaml.rs","status":"open","priority":4,"issue_type":"task","created_at":"2026-02-12T16:31:57.239297Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:42:45.279099Z","compaction_level":0,"original_size":0,"labels":["future","phase3"],"dependencies":[{"issue_id":"bd-b8h","depends_on_id":"bd-2e4","type":"blocks","created_at":"2026-02-12T16:42:45.279082Z","created_by":"tayloreernisse"},{"issue_id":"bd-b8h","depends_on_id":"bd-3aq","type":"parent-child","created_at":"2026-02-12T16:31:57.241719Z","created_by":"tayloreernisse"}]} +{"id":"bd-epk","title":"Epic: Query Commands Phase 1","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-12T16:22:20.420042Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:22:20.420513Z","compaction_level":0,"original_size":0,"labels":["epic"]} +{"id":"bd-gvr","title":"Create Dockerfile and installation script","description":"## Background\nDockerfile for minimal Alpine-based image and install.sh for curl-based binary installation with checksum/signature verification.\n\n## Approach\n\n**Dockerfile (multi-stage build):**\n- Builder stage: `FROM rust:1.93-alpine` as builder, `apk add musl-dev`, `COPY . .`, `cargo build --release --locked --target x86_64-unknown-linux-musl`\n- Runtime stage: `FROM alpine:latest`, `apk add --no-cache ca-certificates`, `COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/swagger-cli /usr/local/bin/swagger-cli`\n- Pre-create XDG dirs: `mkdir -p /root/.config/swagger-cli /root/.cache/swagger-cli/aliases`\n- `ENTRYPOINT [\"swagger-cli\"]` (no CMD — user passes subcommands directly)\n\n**install.sh:**\n- Header: `#!/usr/bin/env bash`, `set -euo pipefail`\n- Secure temp directory: `mktemp -d` with cleanup trap (`trap \"rm -rf $TMPDIR\" EXIT`)\n- OS detection: `uname -s` → Darwin or Linux (reject others with clear error)\n- Arch detection: `uname -m` → arm64/aarch64 maps to aarch64, x86_64 stays x86_64 (reject others)\n- Download URL: GitLab Package Registry URL pattern, constructed from OS+arch variables\n- Download: `curl -fsSL` binary + SHA256SUMS file\n- Checksum verification (portable): Linux uses `sha256sum --check`, macOS uses `shasum -a 256 --check` — detect which is available\n- Optional minisign verification: if `minisign` is on PATH, download `.minisig` file and verify signature; if not on PATH, print info message and skip (not an error)\n- Install: `chmod +x`, move to `/usr/local/bin/` (or `~/.local/bin/` if no write access to /usr/local/bin)\n- PATH check: verify install dir is on PATH, print warning if not with suggested export command\n\n## Acceptance Criteria\n- [ ] Dockerfile builds successfully with `docker build .`\n- [ ] Container runs `swagger-cli --version` and exits 0\n- [ ] Docker image is minimal (Alpine-based runtime, no build tools in final image)\n- [ ] install.sh starts with `set -euo pipefail` and creates secure temp dir with cleanup trap\n- [ ] install.sh detects OS (Darwin/Linux) and architecture (arm64/x86_64) correctly\n- [ ] install.sh rejects unsupported OS/arch with clear error message\n- [ ] Checksum verification works on Linux (sha256sum) and macOS (shasum -a 256)\n- [ ] Checksum failure aborts install with non-zero exit\n- [ ] Optional minisign verification runs when minisign is available, skips gracefully when not\n- [ ] Binary installed to /usr/local/bin or ~/.local/bin with executable permissions\n- [ ] PATH warning printed if install directory not on PATH\n\n## Files\n- CREATE: Dockerfile\n- CREATE: install.sh\n\n## TDD Anchor\nVERIFY: `docker build -t swagger-cli-test . && docker run swagger-cli-test --version`\nVERIFY: `bash -n install.sh` (syntax check)\n\n## Edge Cases\n- **musl vs glibc:** Alpine uses musl. The Dockerfile must use the musl target, not the gnu target. Mixing causes runtime failures.\n- **Rootless Docker:** ENTRYPOINT should work regardless of UID. Don't assume /root/ — use $HOME or a configurable path.\n- **install.sh on minimal systems:** Some minimal Docker images don't have `curl`. The script should check for curl and error with a clear message.\n- **Interrupted install:** The trap ensures temp dir cleanup on any exit (EXIT, not just specific signals). Verify install.sh doesn't leave artifacts on Ctrl+C.\n- **Apple Silicon detection:** `uname -m` returns \"arm64\" on macOS but \"aarch64\" on Linux. Both must map to the aarch64 binary.","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-12T16:31:32.440225Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:56:19.763349Z","compaction_level":0,"original_size":0,"labels":["ci","phase2"],"dependencies":[{"issue_id":"bd-gvr","depends_on_id":"bd-1lo","type":"parent-child","created_at":"2026-02-12T16:31:32.444746Z","created_by":"tayloreernisse"},{"issue_id":"bd-gvr","depends_on_id":"bd-a7e","type":"blocks","created_at":"2026-02-12T16:31:32.445590Z","created_by":"tayloreernisse"}]} +{"id":"bd-hcb","title":"Epic: Config and Cache Infrastructure","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-12T16:22:17.707241Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:22:17.707865Z","compaction_level":0,"original_size":0,"labels":["epic"]} +{"id":"bd-ilo","title":"Implement error types and core data models","description":"## Background\nEvery command in swagger-cli returns typed errors that map to specific exit codes and robot JSON error codes. This bead defines the complete error type system and the core data models (SpecIndex, CacheMetadata, IndexedEndpoint, etc.) that all commands operate on. These types are the backbone of the entire application.\n\n## Approach\nImplement `SwaggerCliError` enum in `src/errors.rs` with variants: Usage, CacheLocked, Network, InvalidSpec, AliasNotFound, AliasExists, Cache, CacheIntegrity, Config, Auth, Io, Json, OfflineMode, PolicyBlocked. Each variant maps to an exit code (2-16), a string error code (USAGE_ERROR, CACHE_LOCKED, etc.), and an optional suggestion string. Use `thiserror::Error` for Display derivation.\n\nImplement core data models in `src/core/spec.rs`: SpecIndex (with index_version, generation, content_hash, openapi, info, endpoints, schemas, tags), IndexInfo, IndexedEndpoint (with path, method, summary, description, operation_id, tags, deprecated, parameters, request_body_required, request_body_content_types, security_schemes, security_required, operation_ptr), IndexedParam (name, location, required, description), IndexedSchema (name, schema_ptr), IndexedTag (name, description, endpoint_count). All derive Serialize, Deserialize, Debug, Clone.\n\nImplement CacheMetadata in `src/core/cache.rs` types section (or a separate types file): alias, url, fetched_at, last_accessed, content_hash, raw_hash, etag, last_modified, spec_version, spec_title, endpoint_count, schema_count, raw_size_bytes, source_format, index_version, generation, index_hash. Include `is_stale()` method.\n\n## Acceptance Criteria\n- [ ] SwaggerCliError has all 14 variants with correct exit_code(), code(), and suggestion() methods\n- [ ] SpecIndex and all index types compile and derive Serialize + Deserialize\n- [ ] CacheMetadata compiles with all fields and is_stale() works correctly\n- [ ] `cargo test --lib` passes for error mapping tests\n- [ ] No use of `any` type -- all fields have concrete types with explicit Rust type annotations\n\n## Files\n- CREATE: src/errors.rs (SwaggerCliError enum, exit_code/code/suggestion impls)\n- CREATE: src/core/spec.rs (SpecIndex, IndexInfo, IndexedEndpoint, IndexedParam, IndexedSchema, IndexedTag)\n- CREATE: src/core/cache.rs (CacheMetadata struct and related types — CacheManager is added later by bd-1ie)\n- CREATE: src/core/mod.rs (pub mod spec; pub mod cache; pub mod config; pub mod search;)\n- MODIFY: src/lib.rs (add pub mod errors; ensure pub mod core;)\n\n## TDD Anchor\nRED: Write `test_error_exit_codes` that asserts each SwaggerCliError variant returns the correct exit code (Usage->2, Network->4, InvalidSpec->5, AliasExists->6, Auth->7, AliasNotFound->8, CacheLocked->9, Cache->10, Config->11, Io->12, Json->13, CacheIntegrity->14, OfflineMode->15, PolicyBlocked->16).\nGREEN: Implement all variants and methods.\nVERIFY: `cargo test test_error_exit_codes`\n\n## Edge Cases\n- AliasNotFound suggestion references `swagger-cli aliases --list` (not just the alias name)\n- suggestion() returns Option -- some variants (Io, Json) have no suggestion\n- Network variant wraps reqwest::Error via #[from] -- but since we use async reqwest, ensure the Error type is the async one","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:23:46.561151Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:56:18.328851Z","compaction_level":0,"original_size":0,"labels":["foundation","phase1"],"dependencies":[{"issue_id":"bd-ilo","depends_on_id":"bd-3e0","type":"parent-child","created_at":"2026-02-12T16:23:46.566627Z","created_by":"tayloreernisse"},{"issue_id":"bd-ilo","depends_on_id":"bd-a7e","type":"blocks","created_at":"2026-02-12T16:23:46.566979Z","created_by":"tayloreernisse"}]} +{"id":"bd-j23","title":"Add breaking-change classification for diff command","description":"## What\nExtend diff command with heuristic-based breaking-change classification. Removed endpoint = breaking. Removed required parameter = breaking. Added optional field = non-breaking. Changed type = breaking.\n\n## Acceptance Criteria\n- [ ] Each change classified as breaking/non-breaking/unknown\n- [ ] --fail-on breaking uses classification (not just structural)\n- [ ] classification field in robot output\n\n## Files\n- MODIFY: src/cli/diff.rs (add classification logic)","status":"open","priority":4,"issue_type":"task","created_at":"2026-02-12T16:31:57.292836Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:42:45.229543Z","compaction_level":0,"original_size":0,"labels":["future","phase3"],"dependencies":[{"issue_id":"bd-j23","depends_on_id":"bd-1ck","type":"blocks","created_at":"2026-02-12T16:31:57.294318Z","created_by":"tayloreernisse"},{"issue_id":"bd-j23","depends_on_id":"bd-2e4","type":"blocks","created_at":"2026-02-12T16:42:45.229527Z","created_by":"tayloreernisse"},{"issue_id":"bd-j23","depends_on_id":"bd-3aq","type":"parent-child","created_at":"2026-02-12T16:31:57.293815Z","created_by":"tayloreernisse"}]} +{"id":"bd-jek","title":"Epic: Query Commands Phase 2","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-12T16:22:21.465792Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:22:21.466699Z","compaction_level":0,"original_size":0,"labels":["epic"]} +{"id":"bd-lx6","title":"Create test fixtures and integration test helpers","description":"## Background\nAll integration tests need test fixtures (OpenAPI spec files) and helper functions to set up hermetic test environments. This bead creates the fixtures and test infrastructure that all other test beads depend on.\n\n## Approach\n1. Create tests/fixtures/petstore.json — download the standard Petstore v3 spec (JSON format, ~50KB, 19 endpoints)\n2. Create tests/fixtures/petstore.yaml — same spec in YAML format (for format normalization tests)\n3. Create tests/fixtures/minimal.json — minimal valid OpenAPI 3.0 spec (3 endpoints, for fast tests)\n4. Create tests/helpers/mod.rs — shared test utilities:\n - setup_test_env() → creates tempdir, sets SWAGGER_CLI_HOME, returns TestEnv struct with paths\n - fetch_fixture(env, fixture_name, alias) → runs swagger-cli fetch with local fixture file\n - run_cmd(args) → creates assert_cmd Command with SWAGGER_CLI_HOME set\n - parse_robot_json(output) → parses stdout as serde_json::Value\n\n## Acceptance Criteria\n- [ ] petstore.json is a valid OpenAPI 3.0 spec with 19+ endpoints\n- [ ] petstore.yaml is equivalent YAML version\n- [ ] minimal.json is valid OpenAPI 3.0 with 3 endpoints\n- [ ] setup_test_env() creates isolated tempdir with SWAGGER_CLI_HOME\n- [ ] fetch_fixture() successfully caches a fixture spec\n- [ ] All test helpers compile and can be used from integration tests\n\n## Files\n- CREATE: tests/fixtures/petstore.json (Petstore v3 spec)\n- CREATE: tests/fixtures/petstore.yaml (same in YAML)\n- CREATE: tests/fixtures/minimal.json (minimal 3-endpoint spec)\n- CREATE: tests/helpers/mod.rs (TestEnv, setup_test_env, fetch_fixture, run_cmd, parse_robot_json)\n\n## TDD Anchor\nRED: Write `test_fixture_is_valid_json` — parse petstore.json, assert it has \"openapi\" and \"paths\" keys.\nGREEN: Create the fixture file.\nVERIFY: `cargo test test_fixture_is_valid`\n\n## Edge Cases\n- Fixtures must use absolute paths (canonicalize) for file:// URLs\n- petstore.json should be a real Petstore spec, not a minimal stub\n- SWAGGER_CLI_HOME must be set BEFORE any command runs (use env() on assert_cmd)\n- Test helpers should clean up tempdirs (use TempDir which auto-cleans on drop)","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:30:59.014337Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:34:06.397049Z","compaction_level":0,"original_size":0,"labels":["phase1","testing"],"dependencies":[{"issue_id":"bd-lx6","depends_on_id":"bd-16o","type":"blocks","created_at":"2026-02-12T16:30:59.016051Z","created_by":"tayloreernisse"},{"issue_id":"bd-lx6","depends_on_id":"bd-p7g","type":"parent-child","created_at":"2026-02-12T16:30:59.015600Z","created_by":"tayloreernisse"}]} +{"id":"bd-p7g","title":"Epic: Testing","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-12T16:22:27.310201Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:22:27.311058Z","compaction_level":0,"original_size":0,"labels":["epic"]} +{"id":"bd-rex","title":"Write integration tests for fetch and query commands","description":"## Background\nIntegration tests validate the full command pipeline end-to-end: fetch → list → show → search → schemas → tags. These tests use assert_cmd to run the binary, mockito for HTTP mocking, and the test fixtures/helpers from the previous bead.\n\n## Approach\nCreate tests/integration/ test files:\n\n**fetch_test.rs:** test_fetch_success (local file), test_fetch_invalid_json, test_fetch_network_error, test_fetch_alias_exists, test_fetch_force_overwrites, test_fetch_yaml_file, test_fetch_stdin\n\n**list_test.rs:** test_list_all_endpoints, test_list_filter_by_method, test_list_filter_by_tag, test_list_path_regex, test_list_invalid_regex_error, test_list_combined_filters, test_list_default_limit, test_list_all_flag\n\n**show_test.rs:** test_show_endpoint_details, test_show_multiple_methods_error, test_show_expand_refs\n\n**search_test.rs:** test_search_basic, test_search_exact, test_search_case_sensitive, test_search_in_paths_only, test_search_invalid_field_error\n\n**schemas_test.rs:** test_schemas_list, test_schemas_show, test_schemas_name_filter\n\n**aliases_test.rs:** test_aliases_list, test_aliases_rename, test_aliases_delete, test_aliases_set_default\n\n## Acceptance Criteria\n- [ ] All fetch tests pass (success, errors, force, YAML, stdin)\n- [ ] All list tests pass (filters, sorting, limits)\n- [ ] All show tests pass (details, ref expansion, method disambiguation)\n- [ ] All search tests pass (basic, exact, case-sensitive, field scoping)\n- [ ] Schema and alias tests pass\n- [ ] All tests are hermetic (SWAGGER_CLI_HOME in tempdir)\n- [ ] No real network calls (local fixtures or mockito)\n\n## Edge Cases\n- **Test isolation:** Each test MUST use its own tempdir for SWAGGER_CLI_HOME. Tests running in parallel must not share cache state.\n- **CI binary not built:** Tests use assert_cmd which builds the binary. Ensure Cargo.toml has the right [[bin]] target.\n- **Mockito port conflicts:** Each test needing a mock server should use mockito::Server::new() which picks a random port, not a hardcoded port.\n- **Fixture file paths:** Use canonicalized absolute paths for local file fetch tests. Relative paths may break depending on test runner CWD.\n\n## Files\n- CREATE: tests/integration/fetch_test.rs\n- CREATE: tests/integration/list_test.rs\n- CREATE: tests/integration/show_test.rs\n- CREATE: tests/integration/search_test.rs\n- CREATE: tests/integration/schemas_test.rs\n- CREATE: tests/integration/aliases_test.rs\n\n## TDD Anchor\nWrite all tests listed above. Many will already exist as unit tests in individual command beads — this bead consolidates them as proper integration tests using assert_cmd binary invocation.\nVERIFY: `cargo test --test '*'`\n\n## Dependency Context\nUses test fixtures and helpers from bd-lx6 (Create test fixtures and integration test helpers). Tests the commands implemented in all query/management beads.","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:30:59.050732Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:58:13.670195Z","compaction_level":0,"original_size":0,"labels":["phase2","testing"],"dependencies":[{"issue_id":"bd-rex","depends_on_id":"bd-30a","type":"blocks","created_at":"2026-02-12T16:30:59.053106Z","created_by":"tayloreernisse"},{"issue_id":"bd-rex","depends_on_id":"bd-acf","type":"blocks","created_at":"2026-02-12T16:30:59.052620Z","created_by":"tayloreernisse"},{"issue_id":"bd-rex","depends_on_id":"bd-lx6","type":"blocks","created_at":"2026-02-12T16:34:06.369443Z","created_by":"tayloreernisse"},{"issue_id":"bd-rex","depends_on_id":"bd-p7g","type":"parent-child","created_at":"2026-02-12T16:30:59.051904Z","created_by":"tayloreernisse"}]} +{"id":"bd-x15","title":"Implement schemas command with list and show modes","description":"## Background\nThe schemas command lets users browse and inspect OpenAPI component schemas. Listing is index-backed (fast). Showing a specific schema loads raw.json via schema_ptr (same pattern as show command). Supports --name regex filter and --expand-refs with cycle detection.\n\n## Approach\nImplement src/cli/schemas.rs with SchemasArgs and execute():\n\n**SchemasArgs:** alias (Option), name (Option — regex filter, invalid → USAGE_ERROR), list (bool, default action), show (Option — schema name), expand_refs (bool), max_depth (u32, default 3).\n\n**List mode (default):** Load index, filter schemas by --name regex, output names. Robot: data.schemas[] with name. Human: bulleted list with title.\n\n**Show mode (--show Name):** Find schema in index by exact name match. Load raw via schema_ptr. If --expand-refs, use the same ref expansion from show command (src/core/refs.rs). Output full schema JSON.\n\n## Acceptance Criteria\n- [ ] `schemas petstore` lists all schema names (sorted by name)\n- [ ] `schemas petstore --name \".*Pet.*\"` filters by regex\n- [ ] Invalid --name regex returns USAGE_ERROR\n- [ ] `schemas petstore --show Pet --robot` returns full Pet schema JSON\n- [ ] --expand-refs works on schema details\n- [ ] Schema not found returns clear error\n- [ ] List mode never loads raw.json\n- [ ] Show mode validates raw_hash\n\n## Edge Cases\n- **Empty schemas (spec with no components/schemas):** Return ok:true with data.schemas as empty array.\n- **Schema name with special characters:** Some schema names contain dots or hyphens (e.g., \"Pet.Response\"). Regex --name filter must handle these.\n- **--show with non-existent schema name:** Return clear error with suggestion listing available schema names.\n- **schema_ptr doesn't resolve in raw.json:** Return CacheIntegrity error (corrupted index). Should not happen if index was built correctly, but defend against it.\n\n## Files\n- MODIFY: src/cli/schemas.rs (SchemasArgs, execute)\n- MODIFY: src/output/robot.rs (add output_schemas_list, output_schemas_show)\n- MODIFY: src/output/human.rs (add output_schemas_list, output_schemas_show)\n\n## TDD Anchor\nRED: Write `test_schemas_list` — fetch petstore, run schemas --robot, assert data.schemas is array with correct count.\nGREEN: Implement list mode.\nVERIFY: `cargo test test_schemas_list`\n\n## Dependency Context\nUses ref expansion from bd-1dj (show command, src/core/refs.rs). Uses CacheManager from bd-3ea.","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-12T16:28:05.337939Z","created_by":"tayloreernisse","updated_at":"2026-02-12T16:58:13.215435Z","compaction_level":0,"original_size":0,"labels":["phase2","query"],"dependencies":[{"issue_id":"bd-x15","depends_on_id":"bd-1dj","type":"blocks","created_at":"2026-02-12T16:28:05.340569Z","created_by":"tayloreernisse"},{"issue_id":"bd-x15","depends_on_id":"bd-3ea","type":"blocks","created_at":"2026-02-12T16:28:05.339868Z","created_by":"tayloreernisse"},{"issue_id":"bd-x15","depends_on_id":"bd-jek","type":"parent-child","created_at":"2026-02-12T16:28:05.339431Z","created_by":"tayloreernisse"}]} diff --git a/.beads/metadata.json b/.beads/metadata.json new file mode 100644 index 0000000..c787975 --- /dev/null +++ b/.beads/metadata.json @@ -0,0 +1,4 @@ +{ + "database": "beads.db", + "jsonl_export": "issues.jsonl" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c7f360 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Build artifacts +/target + +# bv (beads viewer) local config and caches +.bv/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..851e03c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,652 @@ +# AGENTS.md + +## RULE 0 - THE FUNDAMENTAL OVERRIDE PEROGATIVE + +If I tell you to do something, even if it goes against what follows below, YOU MUST LISTEN TO ME. I AM IN CHARGE, NOT YOU. + +--- + +## RULE NUMBER 1: NO FILE DELETION + +**YOU ARE NEVER ALLOWED TO DELETE A FILE WITHOUT EXPRESS PERMISSION.** Even a new file that you yourself created, such as a test code file. You have a horrible track record of deleting critically important files or otherwise throwing away tons of expensive work. As a result, you have permanently lost any and all rights to determine that a file or folder should be deleted. + +**YOU MUST ALWAYS ASK AND RECEIVE CLEAR, WRITTEN PERMISSION BEFORE EVER DELETING A FILE OR FOLDER OF ANY KIND.** + +--- + +## Version Control: jj-First (CRITICAL) + +**ALWAYS prefer jj (Jujutsu) over git for all VCS operations.** This is a colocated repo with both `.jj/` and `.git/`. When instructed to use git by anything — even later in this file — use the best jj replacement commands instead. Only fall back to raw `git` for things jj cannot do (hooks, LFS, submodules, `gh` CLI interop). + +See `~/.claude/rules/jj-vcs/` for the full command reference, translation table, revsets, patterns, and recovery recipes. + +--- + +## Irreversible Git & Filesystem Actions — DO NOT EVER BREAK GLASS + +> **Note:** Treat destructive commands as break-glass. If there's any doubt, stop and ask. + +1. **Absolutely forbidden commands:** `git reset --hard`, `git clean -fd`, `rm -rf`, or any command that can delete or overwrite code/data must never be run unless the user explicitly provides the exact command and states, in the same message, that they understand and want the irreversible consequences. +2. **No guessing:** If there is any uncertainty about what a command might delete or overwrite, stop immediately and ask the user for specific approval. "I think it's safe" is never acceptable. +3. **Safer alternatives first:** When cleanup or rollbacks are needed, request permission to use non-destructive options (`git status`, `git diff`, `git stash`, copying to backups) before ever considering a destructive command. +4. **Mandatory explicit plan:** Even after explicit user authorization, restate the command verbatim, list exactly what will be affected, and wait for a confirmation that your understanding is correct. Only then may you execute it—if anything remains ambiguous, refuse and escalate. +5. **Document the confirmation:** When running any approved destructive command, record (in the session notes / final response) the exact user text that authorized it, the command actually run, and the execution time. If that record is absent, the operation did not happen. + +--- + +## Toolchain: Rust & Cargo + +We only use **Cargo** in this project, NEVER any other package manager. + +- **Edition/toolchain:** Follow `rust-toolchain.toml` (if present). Do not assume stable vs nightly. +- **Dependencies:** Explicit versions for stability; keep the set minimal. +- **Configuration:** Cargo.toml only +- **Unsafe code:** Forbidden (`#![forbid(unsafe_code)]`) + +When writing Rust code, reference RUST_CLI_TOOLS_BEST_PRACTICES.md + +### Release Profile + +Use the release profile defined in `Cargo.toml`. If you need to change it, justify the +performance/size tradeoff and how it impacts determinism and cancellation behavior. + +--- + +## Code Editing Discipline + +### No Script-Based Changes + +**NEVER** run a script that processes/changes code files in this repo. Brittle regex-based transformations create far more problems than they solve. + +- **Always make code changes manually**, even when there are many instances +- For many simple changes: use parallel subagents +- For subtle/complex changes: do them methodically yourself + +### No File Proliferation + +If you want to change something or add a feature, **revise existing code files in place**. + +**NEVER** create variations like: +- `mainV2.rs` +- `main_improved.rs` +- `main_enhanced.rs` + +New files are reserved for **genuinely new functionality** that makes zero sense to include in any existing file. The bar for creating new files is **incredibly high**. + +--- + +## Backwards Compatibility + +We do not care about backwards compatibility—we're in early development with no users. We want to do things the **RIGHT** way with **NO TECH DEBT**. + +- Never create "compatibility shims" +- Never create wrapper functions for deprecated APIs +- Just fix the code directly + +--- + +## Compiler Checks (CRITICAL) + +**After any substantive code changes, you MUST verify no errors were introduced:** + +```bash +# Check for compiler errors and warnings +cargo check --all-targets + +# Check for clippy lints (pedantic + nursery are enabled) +cargo clippy --all-targets -- -D warnings + +# Verify formatting +cargo fmt --check +``` + +If you see errors, **carefully understand and resolve each issue**. Read sufficient context to fix them the RIGHT way. + +--- + +## Testing + +### Unit & Property Tests + +```bash +# Run all tests +cargo test + +# Run with output +cargo test -- --nocapture +``` + +When adding or changing primitives, add tests that assert the core invariants: + +- no task leaks +- no obligation leaks +- losers are drained after races +- region close implies quiescence + +Prefer deterministic lab-runtime tests for concurrency-sensitive behavior. + +--- + +## MCP Agent Mail — Multi-Agent Coordination + +A mail-like layer that lets coding agents coordinate asynchronously via MCP tools and resources. Provides identities, inbox/outbox, searchable threads, and advisory file reservations with human-auditable artifacts in Git. + +### Why It's Useful + +- **Prevents conflicts:** Explicit file reservations (leases) for files/globs +- **Token-efficient:** Messages stored in per-project archive, not in context +- **Quick reads:** `resource://inbox/...`, `resource://thread/...` + +### Same Repository Workflow + +1. **Register identity:** + ``` + ensure_project(project_key=) + register_agent(project_key, program, model) + ``` + +2. **Reserve files before editing:** + ``` + file_reservation_paths(project_key, agent_name, ["src/**"], ttl_seconds=3600, exclusive=true) + ``` + +3. **Communicate with threads:** + ``` + send_message(..., thread_id="FEAT-123") + fetch_inbox(project_key, agent_name) + acknowledge_message(project_key, agent_name, message_id) + ``` + +4. **Quick reads:** + ``` + resource://inbox/{Agent}?project=&limit=20 + resource://thread/{id}?project=&include_bodies=true + ``` + +### Macros vs Granular Tools + +- **Prefer macros for speed:** `macro_start_session`, `macro_prepare_thread`, `macro_file_reservation_cycle`, `macro_contact_handshake` +- **Use granular tools for control:** `register_agent`, `file_reservation_paths`, `send_message`, `fetch_inbox`, `acknowledge_message` + +### Common Pitfalls + +- `"from_agent not registered"`: Always `register_agent` in the correct `project_key` first +- `"FILE_RESERVATION_CONFLICT"`: Adjust patterns, wait for expiry, or use non-exclusive reservation +- **Auth errors:** If JWT+JWKS enabled, include bearer token with matching `kid` + +--- + +## Beads (br) — Dependency-Aware Issue Tracking + +Beads provides a lightweight, dependency-aware issue database and CLI (`br` / beads_rust) for selecting "ready work," setting priorities, and tracking status. It complements MCP Agent Mail's messaging and file reservations. + +**Note:** `br` is non-invasive—it never executes git commands directly. You must run git commands manually after `br sync --flush-only`. + +### Conventions + +- **Single source of truth:** Beads for task status/priority/dependencies; Agent Mail for conversation and audit +- **Shared identifiers:** Use Beads issue ID (e.g., `br-123`) as Mail `thread_id` and prefix subjects with `[br-123]` +- **Reservations:** When starting a task, call `file_reservation_paths()` with the issue ID in `reason` + +### Typical Agent Flow + +1. **Pick ready work (Beads):** + ```bash + br ready --json # Choose highest priority, no blockers + ``` + +2. **Reserve edit surface (Mail):** + ``` + file_reservation_paths(project_key, agent_name, ["src/**"], ttl_seconds=3600, exclusive=true, reason="br-123") + ``` + +3. **Announce start (Mail):** + ``` + send_message(..., thread_id="br-123", subject="[br-123] Start: ", ack_required=true) + ``` + +4. **Work and update:** Reply in-thread with progress + +5. **Complete and release:** + ```bash + br close br-123 --reason "Completed" + ``` + ``` + release_file_reservations(project_key, agent_name, paths=["src/**"]) + ``` + Final Mail reply: `[br-123] Completed` with summary + +### Mapping Cheat Sheet + +| Concept | Value | +|---------|-------| +| Mail `thread_id` | `br-###` | +| Mail subject | `[br-###] ...` | +| File reservation `reason` | `br-###` | +| Commit messages | Include `br-###` for traceability | + +--- + +## bv — Graph-Aware Triage Engine + +bv is a graph-aware triage engine for Beads projects (`.beads/beads.jsonl`). It computes PageRank, betweenness, critical path, cycles, HITS, eigenvector, and k-core metrics deterministically. + +**Scope boundary:** bv handles *what to work on* (triage, priority, planning). For agent-to-agent coordination (messaging, work claiming, file reservations), use MCP Agent Mail. + +**CRITICAL: Use ONLY `--robot-*` flags. Bare `bv` launches an interactive TUI that blocks your session.** + +### The Workflow: Start With Triage + +**`bv --robot-triage` is your single entry point.** It returns: +- `quick_ref`: at-a-glance counts + top 3 picks +- `recommendations`: ranked actionable items with scores, reasons, unblock info +- `quick_wins`: low-effort high-impact items +- `blockers_to_clear`: items that unblock the most downstream work +- `project_health`: status/type/priority distributions, graph metrics +- `commands`: copy-paste shell commands for next steps + +```bash +bv --robot-triage # THE MEGA-COMMAND: start here +bv --robot-next # Minimal: just the single top pick + claim command +``` + +### Command Reference + +**Planning:** +| Command | Returns | +|---------|---------| +| `--robot-plan` | Parallel execution tracks with `unblocks` lists | +| `--robot-priority` | Priority misalignment detection with confidence | + +**Graph Analysis:** +| Command | Returns | +|---------|---------| +| `--robot-insights` | Full metrics: PageRank, betweenness, HITS, eigenvector, critical path, cycles, k-core, articulation points, slack | +| `--robot-label-health` | Per-label health: `health_level`, `velocity_score`, `staleness`, `blocked_count` | +| `--robot-label-flow` | Cross-label dependency: `flow_matrix`, `dependencies`, `bottleneck_labels` | +| `--robot-label-attention [--attention-limit=N]` | Attention-ranked labels | + +**History & Change Tracking:** +| Command | Returns | +|---------|---------| +| `--robot-history` | Bead-to-commit correlations | +| `--robot-diff --diff-since <ref>` | Changes since ref: new/closed/modified issues, cycles | + +**Other:** +| Command | Returns | +|---------|---------| +| `--robot-burndown <sprint>` | Sprint burndown, scope changes, at-risk items | +| `--robot-forecast <id\|all>` | ETA predictions with dependency-aware scheduling | +| `--robot-alerts` | Stale issues, blocking cascades, priority mismatches | +| `--robot-suggest` | Hygiene: duplicates, missing deps, label suggestions | +| `--robot-graph [--graph-format=json\|dot\|mermaid]` | Dependency graph export | +| `--export-graph <file.html>` | Interactive HTML visualization | + +### Scoping & Filtering + +```bash +bv --robot-plan --label backend # Scope to label's subgraph +bv --robot-insights --as-of HEAD~30 # Historical point-in-time +bv --recipe actionable --robot-plan # Pre-filter: ready to work +bv --recipe high-impact --robot-triage # Pre-filter: top PageRank +bv --robot-triage --robot-triage-by-track # Group by parallel work streams +bv --robot-triage --robot-triage-by-label # Group by domain +``` + +### Understanding Robot Output + +**All robot JSON includes:** +- `data_hash` — Fingerprint of source beads.jsonl +- `status` — Per-metric state: `computed|approx|timeout|skipped` + elapsed ms +- `as_of` / `as_of_commit` — Present when using `--as-of` + +**Two-phase analysis:** +- **Phase 1 (instant):** degree, topo sort, density +- **Phase 2 (async, 500ms timeout):** PageRank, betweenness, HITS, eigenvector, cycles + +### jq Quick Reference + +```bash +bv --robot-triage | jq '.quick_ref' # At-a-glance summary +bv --robot-triage | jq '.recommendations[0]' # Top recommendation +bv --robot-plan | jq '.plan.summary.highest_impact' # Best unblock target +bv --robot-insights | jq '.status' # Check metric readiness +bv --robot-insights | jq '.Cycles' # Circular deps (must fix!) +``` + +--- + +## UBS — Ultimate Bug Scanner + +**Golden Rule:** `ubs <changed-files>` before every commit. Exit 0 = safe. Exit >0 = fix & re-run. + +### Commands + +```bash +ubs file.rs file2.rs # Specific files (< 1s) — USE THIS +ubs $(jj diff --name-only) # Changed files — before commit +ubs --only=rust,toml src/ # Language filter (3-5x faster) +ubs --ci --fail-on-warning . # CI mode — before PR +ubs . # Whole project (ignores target/, Cargo.lock) +``` + +### Output Format + +``` +⚠️ Category (N errors) + file.rs:42:5 – Issue description + 💡 Suggested fix +Exit code: 1 +``` + +Parse: `file:line:col` → location | 💡 → how to fix | Exit 0/1 → pass/fail + +### Fix Workflow + +1. Read finding → category + fix suggestion +2. Navigate `file:line:col` → view context +3. Verify real issue (not false positive) +4. Fix root cause (not symptom) +5. Re-run `ubs <file>` → exit 0 +6. Commit + +### Bug Severity + +- **Critical (always fix):** Memory safety, use-after-free, data races, SQL injection +- **Important (production):** Unwrap panics, resource leaks, overflow checks +- **Contextual (judgment):** TODO/FIXME, println! debugging + +--- + +## ast-grep vs ripgrep + +**Use `ast-grep` when structure matters.** It parses code and matches AST nodes, ignoring comments/strings, and can **safely rewrite** code. + +- Refactors/codemods: rename APIs, change import forms +- Policy checks: enforce patterns across a repo +- Editor/automation: LSP mode, `--json` output + +**Use `ripgrep` when text is enough.** Fastest way to grep literals/regex. + +- Recon: find strings, TODOs, log lines, config values +- Pre-filter: narrow candidate files before ast-grep + +### Rule of Thumb + +- Need correctness or **applying changes** → `ast-grep` +- Need raw speed or **hunting text** → `rg` +- Often combine: `rg` to shortlist files, then `ast-grep` to match/modify + +### Rust Examples + +```bash +# Find structured code (ignores comments) +ast-grep run -l Rust -p 'fn $NAME($$$ARGS) -> $RET { $$$BODY }' + +# Find all unwrap() calls +ast-grep run -l Rust -p '$EXPR.unwrap()' + +# Quick textual hunt +rg -n 'println!' -t rust + +# Combine speed + precision +rg -l -t rust 'unwrap\(' | xargs ast-grep run -l Rust -p '$X.unwrap()' --json +``` + +--- + +## Morph Warp Grep — AI-Powered Code Search + +**Use `mcp__morph-mcp__warp_grep` for exploratory "how does X work?" questions.** An AI agent expands your query, greps the codebase, reads relevant files, and returns precise line ranges with full context. + +**Use `ripgrep` for targeted searches.** When you know exactly what you're looking for. + +**Use `ast-grep` for structural patterns.** When you need AST precision for matching/rewriting. + +### When to Use What + +| Scenario | Tool | Why | +|----------|------|-----| +| "How is pattern matching implemented?" | `warp_grep` | Exploratory; don't know where to start | +| "Where is the quick reject filter?" | `warp_grep` | Need to understand architecture | +| "Find all uses of `Regex::new`" | `ripgrep` | Targeted literal search | +| "Find files with `println!`" | `ripgrep` | Simple pattern | +| "Replace all `unwrap()` with `expect()`" | `ast-grep` | Structural refactor | + +### warp_grep Usage + +``` +mcp__morph-mcp__warp_grep( + repoPath: "/path/to/dcg", + query: "How does the safe pattern whitelist work?" +) +``` + +Returns structured results with file paths, line ranges, and extracted code snippets. + +### Anti-Patterns + +- **Don't** use `warp_grep` to find a specific function name → use `ripgrep` +- **Don't** use `ripgrep` to understand "how does X work" → wastes time with manual reads +- **Don't** use `ripgrep` for codemods → risks collateral edits + +<!-- bv-agent-instructions-v1 --> + +--- + +## Beads Workflow Integration + +This project uses [beads_viewer](https://github.com/Dicklesworthstone/beads_viewer) for issue tracking. Issues are stored in `.beads/` and tracked in version control. + +**Note:** `br` is non-invasive—it never executes VCS commands directly. You must commit manually after `br sync --flush-only`. + +### Essential Commands + +```bash +# View issues (launches TUI - avoid in automated sessions) +bv + +# CLI commands for agents (use these instead) +br ready # Show issues ready to work (no blockers) +br list --status=open # All open issues +br show <id> # Full issue details with dependencies +br create --title="..." --type=task --priority=2 +br update <id> --status=in_progress +br close <id> --reason="Completed" +br close <id1> <id2> # Close multiple issues at once +br sync --flush-only # Export to JSONL (then: jj commit -m "Update beads") +``` + +### Workflow Pattern + +1. **Start**: Run `br ready` to find actionable work +2. **Claim**: Use `br update <id> --status=in_progress` +3. **Work**: Implement the task +4. **Complete**: Use `br close <id>` +5. **Sync**: Run `br sync --flush-only`, then `git add .beads/ && git commit -m "Update beads"` + +### Key Concepts + +- **Dependencies**: Issues can block other issues. `br ready` shows only unblocked work. +- **Priority**: P0=critical, P1=high, P2=medium, P3=low, P4=backlog (use numbers, not words) +- **Types**: task, bug, feature, epic, question, docs +- **Blocking**: `br dep add <issue> <depends-on>` to add dependencies + +### Session Protocol + +**Before ending any session, run this checklist (solo/lead only — workers skip VCS):** + +```bash +jj status # Check what changed +br sync --flush-only # Export beads to JSONL +jj commit -m "..." # Commit code and beads (jj auto-tracks all changes) +jj bookmark set <name> -r @- # Point bookmark at committed work +jj git push -b <name> # Push to remote +``` + +### Best Practices + +- Check `br ready` at session start to find available work +- Update status as you work (in_progress → closed) +- Create new issues with `br create` when you discover tasks +- Use descriptive titles and set appropriate priority/type +- Always run `br sync --flush-only` then commit before ending session (jj auto-tracks .beads/) + +<!-- end-bv-agent-instructions --> + +## Landing the Plane (Session Completion) + +**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until push succeeds. + +**WHO RUNS THIS:** Solo agents run it themselves. In multi-agent sessions, ONLY the team lead runs this. Workers skip VCS entirely. + +**MANDATORY WORKFLOW:** + +1. **File issues for remaining work** - Create issues for anything that needs follow-up +2. **Run quality gates** (if code changed) - Tests, linters, builds +3. **Update issue status** - Close finished work, update in-progress items +4. **PUSH TO REMOTE** - This is MANDATORY: + ```bash + jj git fetch # Get latest remote state + jj rebase -d trunk() # Rebase onto latest trunk if needed + br sync --flush-only # Export beads to JSONL + jj commit -m "Update beads" # Commit (jj auto-tracks .beads/ changes) + jj bookmark set <name> -r @- # Point bookmark at committed work + jj git push -b <name> # Push to remote + jj log -r '<name>' # Verify bookmark position + ``` +5. **Clean up** - Abandon empty orphan changes if any (`jj abandon <rev>`) +6. **Verify** - All changes committed AND pushed +7. **Hand off** - Provide context for next session + +**CRITICAL RULES:** +- Work is NOT complete until `jj git push` succeeds +- NEVER stop before pushing - that leaves work stranded locally +- NEVER say "ready to push when you are" - YOU must push +- If push fails, resolve and retry until it succeeds + +--- + +## cass — Cross-Agent Session Search + +`cass` indexes prior agent conversations (Claude Code, Codex, Cursor, Gemini, ChatGPT, etc.) so we can reuse solved problems. + +**Rules:** Never run bare `cass` (TUI). Always use `--robot` or `--json`. + +### Examples + +```bash +cass health +cass search "async runtime" --robot --limit 5 +cass view /path/to/session.jsonl -n 42 --json +cass expand /path/to/session.jsonl -n 42 -C 3 --json +cass capabilities --json +cass robot-docs guide +``` + +### Tips + +- Use `--fields minimal` for lean output +- Filter by agent with `--agent` +- Use `--days N` to limit to recent history + +stdout is data-only, stderr is diagnostics; exit code 0 means success. + +Treat cass as a way to avoid re-solving problems other agents already handled. + +--- + +## Note for Codex/GPT-5.2 + +You constantly bother me and stop working with concerned questions that look similar to this: + +``` +Unexpected changes (need guidance) + +- Working tree still shows edits I did not make in Cargo.toml, Cargo.lock, src/runtime.rs, src/scope.rs. Please advise whether to keep/commit/revert these before any further work. I did not touch them. + +Next steps (pick one) + +1. Decide how to handle the unrelated modified files above so we can resume cleanly. +``` + +NEVER EVER DO THAT AGAIN. The answer is literally ALWAYS the same: those are changes created by the potentially dozen of other agents working on the project at the same time. This is not only a common occurence, it happens multiple times PER MINUTE. The way to deal with it is simple: you NEVER, under ANY CIRCUMSTANCE, stash, revert, overwrite, or otherwise disturb in ANY way the work of other agents. Just treat those changes identically to changes that you yourself made. Just fool yourself into thinking YOU made the changes and simply don't recall it for some reason. + +--- + +## Note on Built-in TODO Functionality + +Also, if I ask you to explicitly use your built-in TODO functionality, don't complain about this and say you need to use beads. You can use built-in TODOs if I tell you specifically to do so. Always comply with such orders. + +## TDD Requirements + +Test-first development is mandatory: +1. **RED** - Write failing test first +2. **GREEN** - Minimal implementation to pass +3. **REFACTOR** - Clean up while green + +## Key Patterns + +Find the simplest solution that meets all acceptance criteria. +Use third party libraries whenever there's a well-maintained, active, and widely adopted solution (for example, date-fns for TS date math) +Build extensible pieces of logic that can easily be integrated with other pieces. +DRY principles should be loosely held. +Architecture MUST be clear and well thought-out. Ask the user for clarification whenever ambiguity is discovered around architecture, or you think a better approach than planned exists. + +--- + +## Third-Party Library Usage + +If you aren't 100% sure how to use a third-party library, **SEARCH ONLINE** to find the latest documentation and mid-2025 best practices. + +--- + +````markdown +## UBS Quick Reference for AI Agents + +UBS stands for "Ultimate Bug Scanner": **The AI Coding Agent's Secret Weapon: Flagging Likely Bugs for Fixing Early On** + +**Install:** `curl -sSL https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/master/install.sh | bash` + +**Golden Rule:** `ubs <changed-files>` before every commit. Exit 0 = safe. Exit >0 = fix & re-run. + +**Commands:** +```bash +ubs file.ts file2.py # Specific files (< 1s) — USE THIS +ubs $(git diff --name-only --cached) # Staged files — before commit +ubs --only=js,python src/ # Language filter (3-5x faster) +ubs --ci --fail-on-warning . # CI mode — before PR +ubs --help # Full command reference +ubs sessions --entries 1 # Tail the latest install session log +ubs . # Whole project (ignores things like .venv and node_modules automatically) +``` + +**Output Format:** +``` +⚠️ Category (N errors) + file.ts:42:5 – Issue description + 💡 Suggested fix +Exit code: 1 +``` +Parse: `file:line:col` → location | 💡 → how to fix | Exit 0/1 → pass/fail + +**Fix Workflow:** +1. Read finding → category + fix suggestion +2. Navigate `file:line:col` → view context +3. Verify real issue (not false positive) +4. Fix root cause (not symptom) +5. Re-run `ubs <file>` → exit 0 +6. Commit + +**Speed Critical:** Scope to changed files. `ubs src/file.ts` (< 1s) vs `ubs .` (30s). Never full scan for small edits. + +**Bug Severity:** +- **Critical** (always fix): Null safety, XSS/injection, async/await, memory leaks +- **Important** (production): Type narrowing, division-by-zero, resource leaks +- **Contextual** (judgment): TODO/FIXME, console logs + +**Anti-Patterns:** +- ❌ Ignore findings → ✅ Investigate each +- ❌ Full scan per edit → ✅ Scope to file +- ❌ Fix symptom (`if (x) { x.y }`) → ✅ Root cause (`x?.y`) +```` diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..09d632f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2759 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "assert_cmd" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.181" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mockito" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0" +dependencies = [ + "assert-json-diff", + "bytes", + "colored", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "log", + "pin-project-lite", + "rand", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "papergrid" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b0f8def1f117e13c895f3eda65a7b5650688da29d6ad04635f61bc7b92eebd" +dependencies = [ + "bytecount", + "fnv", + "unicode-width", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.115", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swagger-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "assert_cmd", + "chrono", + "clap", + "colored", + "criterion", + "directories", + "fs2", + "mockito", + "predicates", + "regex", + "reqwest", + "serde", + "serde_json", + "serde_yaml", + "sha2", + "tabled", + "tempfile", + "thiserror", + "tokio", + "toml", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "tabled" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6709222f3973137427ce50559cd564dc187a95b9cfe01613d2f4e93610e510a" +dependencies = [ + "papergrid", + "tabled_derive", +] + +[[package]] +name = "tabled_derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "931be476627d4c54070a1f3a9739ccbfec9b36b39815106a20cce2243bbcefe1" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.115", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.115", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.115", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b5f416f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "swagger-cli" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1" +chrono = { version = "0.4", features = ["serde"] } +clap = { version = "4", features = ["derive"] } +colored = "3" +directories = "6" +fs2 = "0.4" +regex = "1" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_yaml = "0.9" +sha2 = "0.10" +tabled = "0.17" +thiserror = "2" +tokio = { version = "1", features = ["full"] } +toml = "0.8" + +[dev-dependencies] +assert_cmd = "2" +criterion = "0.5" +mockito = "1" +predicates = "3" +tempfile = "3" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +strip = true diff --git a/prd-swagger-cli.feedback-1.md b/prd-swagger-cli.feedback-1.md new file mode 100644 index 0000000..661cb29 --- /dev/null +++ b/prd-swagger-cli.feedback-1.md @@ -0,0 +1,171 @@ +No `## Rejected Recommendations` section exists in `prd-swagger-cli.md`, so all suggestions below are net-new. + +**1. Add a canonical ingest pipeline (JSON + YAML + gzip) with streaming limits** +Current plan is effectively JSON-first and buffer-oriented. In practice, many OpenAPI specs are YAML and/or compressed; forcing JSON-only ingestion creates adoption friction and unnecessary memory pressure on large specs. A canonical ingest stage (detect format, decode, parse, normalize) gives one robust path for URL/file/stdin and makes future behaviors (ref resolution, provenance) cleaner. + +```diff +diff --git a/prd-swagger-cli.md b/prd-swagger-cli.md +@@ FR-1: Spec Fetching and Caching +-- ✓ Validate JSON is parseable before caching (no full OpenAPI structural validation) ++- ✓ Validate spec is parseable JSON or YAML before caching (no full OpenAPI structural validation) ++- ✓ Support compressed inputs (`.json.gz`, `.yaml.gz`, `Content-Encoding: gzip`) ++- ✓ Enforce `--max-bytes` during streaming download (fail before full buffering) +@@ OPTIONS: ++ --format <FORMAT> Input format hint: auto (default), json, yaml +@@ Cache directory layout +- ├── raw.json # Exact upstream bytes (lossless) ++ ├── raw.source # Exact upstream bytes (json|yaml|gz as fetched) ++ ├── raw.json # Canonical normalized JSON for pointers/show +@@ Core dependencies ++serde_yaml = "0.9" ++flate2 = "1.0" +``` + +**2. Add explicit fetch-time external-ref bundling (opt-in)** +You already correctly avoid external network fetches during `show --expand-refs`. The missing piece is specs that rely on external refs for core operations. Add an explicit fetch-time bundling mode with strict allowlists/limits. Default remains offline-safe and unchanged. + +```diff +diff --git a/prd-swagger-cli.md b/prd-swagger-cli.md +@@ FR-1 OPTIONS: ++ --resolve-external-refs Resolve and bundle external $ref targets at fetch-time (opt-in) ++ --ref-allow-host <HOST> Allowlist host for external ref resolution (repeatable) ++ --ref-max-depth <N> Max external ref chain depth (default: 3) ++ --ref-max-bytes <N> Total bytes cap for all external ref fetches +@@ FR-3 Decision rationale +-- External refs are NOT fetched (no network). ++- Query-time external refs are NOT fetched (no network). ++- Optional fetch-time bundling can resolve external refs under explicit policy flags. ++- Bundled snapshots preserve offline guarantees for all later commands. +``` + +**3. Add a global network policy switch for deterministic agent runs** +Manual `sync` is good, but a global network policy is better for CI/agent reproducibility. This prevents accidental network behavior in restricted environments and makes failure mode explicit. + +```diff +diff --git a/prd-swagger-cli.md b/prd-swagger-cli.md +@@ src/cli/mod.rs ++ /// Network policy: auto (default), offline, online-only ++ #[arg(long, global = true, default_value = "auto", value_parser = ["auto","offline","online-only"])] ++ pub network: String, +@@ Goals ++6. **Determinism:** Global network policy control for reproducible offline/CI execution +@@ Appendix B: Exit Code Reference ++| 15 | Offline mode blocked network operation | `OFFLINE_MODE` | No | Retry without `--network offline` | +``` + +**4. Strengthen integrity checks with raw-hash verification + pointer validation + safe auto-rebuild** +Current generation/index-hash integrity is good but incomplete for raw corruption and stale pointers. Add raw hash verification when raw is used, and validate every pointer at fetch/sync/doctor. Also allow safe index auto-rebuild from valid raw under lock to reduce operational toil. + +```diff +diff --git a/prd-swagger-cli.md b/prd-swagger-cli.md +@@ Read protocol: +- - Read index.json. Validate ALL THREE match meta.json: ++ - Read index.json. Validate ALL FOUR match meta.json: + 1. meta.index_version == index.index_version + 2. meta.generation == index.generation + 3. meta.index_hash == sha256(index.json bytes) ++ 4. meta.content_hash == sha256(raw.json bytes) (commands that require raw) ++ - Validate every `operation_ptr` / `schema_ptr` resolves during fetch/sync; doctor re-checks all pointers. ++ - If index integrity fails but raw is valid: auto-rebuild index under alias lock (unless `--strict-integrity`). +@@ FR-9 doctor ++- ✓ Verify index pointers resolve to existing JSON nodes ++- ✓ Repair path prefers deterministic index rebuild before surfacing CACHE_INTEGRITY +``` + +**5. Make `sync --all` scalable and polite (bounded concurrency + host throttling + Retry-After)** +As alias count grows, sequential sync is slow; unconstrained parallel sync is abusive and unreliable. Add bounded concurrency plus per-host caps and Retry-After handling for robust large-team usage. + +```diff +diff --git a/prd-swagger-cli.md b/prd-swagger-cli.md +@@ FR-7 OPTIONS: ++ --jobs <N> Parallel aliases to sync (default: 4, bounded) ++ --per-host <N> Max concurrent requests per host (default: 2) +@@ Decision rationale: ++- `sync --all` uses bounded concurrency with per-host throttling. ++- Retries honor `Retry-After` when present; otherwise exponential backoff + jitter. ++- Robot output reports partial failures per alias without aborting the entire run. +``` + +**6. Upgrade search to a precomputed token index + deterministic fuzzy fallback** +Current contains-based scoring will degrade on larger specs and misses common misspellings. A small postings index in `index.json` keeps search fast and makes ranking better without adding a heavy FTS dependency. + +```diff +diff --git a/prd-swagger-cli.md b/prd-swagger-cli.md +@@ FR-5 Acceptance Criteria ++- ✓ Use precomputed token postings for O(query_terms + matches) lookup ++- ✓ Optional typo-tolerant matching (`--fuzzy`) with bounded edit distance ++- ✓ Deterministic fixed-point scoring (integer), stable tie-breaking retained +@@ Command: swagger-cli search ++ --fuzzy Enable bounded typo-tolerant token matching ++ --min-score <N> Filter low-relevance matches +@@ Data Models: SpecIndex ++ pub search_lexicon_version: u32, ++ pub search_postings: Vec<SearchPosting>, +``` + +**7. Add cross-alias discovery mode for `list` and `search`** +Single-alias operation is clean, but discovery across many APIs is a common real-world workflow. `--all-aliases` gives immediate utility to both humans and agents while preserving existing default behavior. + +```diff +diff --git a/prd-swagger-cli.md b/prd-swagger-cli.md +@@ FR-2 Command: swagger-cli list ++ --all-aliases Run query across all aliases; include `alias` per result +@@ FR-5 Command: swagger-cli search ++ --all-aliases Search across all aliases; include `alias` per result +@@ Robot output (list/search) ++ "alias": "petstore", +@@ Open Questions Q3 +-Decision: Single alias per query in MVP; revisit if requested ++Decision: Default remains single alias; `--all-aliases` added for explicit federated discovery. +``` + +**8. Add cache lifecycle management (`cache` command)** +You report disk usage but don’t provide lifecycle controls. Add prune/compact/stats to avoid long-term cache bloat and improve operational hygiene. + +```diff +diff --git a/prd-swagger-cli.md b/prd-swagger-cli.md +@@ Functional Requirements ++### FR-10: Cache Lifecycle Management ++**Description:** Manage cache growth and retention. ++**Command:** swagger-cli cache [OPTIONS] ++OPTIONS: ++ --stats Show per-alias and total cache usage ++ --prune-stale Delete aliases older than stale threshold ++ --max-total-mb <N> Enforce global cache cap via LRU eviction ++ --robot Machine-readable output +``` + +**9. Harden release/install supply chain (checksums + signatures)** +Current installer downloads and executes binaries without verification. Add checksum/signature artifacts and enforce verification in installer by default. + +```diff +diff --git a/prd-swagger-cli.md b/prd-swagger-cli.md +@@ Release stage ++ - sha256sum swagger-cli-* > SHA256SUMS ++ - minisign -Sm SHA256SUMS ++ - upload SHA256SUMS and SHA256SUMS.minisig with binaries +@@ install.sh +-# Download binary ++# Download binary + checksum manifest + signature ++# Verify signature + checksum before chmod +x ++VERIFY="${VERIFY:-true}" ++if [ "$VERIFY" = "true" ]; then ++ # fail closed on verification mismatch ++fi +``` + +**10. Add adversarial reliability tests (fault injection + concurrency stress + property tests)** +The plan has good tests, but not enough proof for crash consistency and lock behavior under contention. Add targeted destructive tests to validate the core reliability claims. + +```diff +diff --git a/prd-swagger-cli.md b/prd-swagger-cli.md +@@ Testing Strategy ++### Reliability Stress Tests ++- Fault-injection tests at each write step (before/after fsync, before/after rename) to prove recoverability. ++- Multi-process lock contention tests (N>=32) validating bounded lock timeout and no deadlocks. ++- Property-based tests for deterministic ordering, stable tie-breaking, and pointer validity. +@@ Success Metrics Phase 1 ++- Crash-consistency claim is validated by automated fault-injection test suite (not only unit tests). +``` + +If you want, I can now consolidate these into a single full unified patch for `prd-swagger-cli.md` with section-by-section wording ready to paste. \ No newline at end of file diff --git a/prd-swagger-cli.feedback-2.md b/prd-swagger-cli.feedback-2.md new file mode 100644 index 0000000..a7a5b88 --- /dev/null +++ b/prd-swagger-cli.feedback-2.md @@ -0,0 +1,158 @@ +Proposed revisions below are all new and avoid anything in your `## Rejected Recommendations` list. + +1. **Add fetch-time policy enforcement (SSRF + insecure transport controls)** +Analysis: Right now external ref allowlisting is good, but primary `fetch` still permits risky network targets. Adding policy gates prevents accidental access to metadata endpoints/internal services and makes CI behavior more deterministic. This is a high-impact security and reliability improvement with low implementation risk. +```diff +@@ FR-1: Spec Fetching and Caching / Acceptance Criteria ++- ✓ Enforce fetch network policy by default: block loopback/link-local/RFC1918/multicast targets for remote URL fetches ++- ✓ Require HTTPS for remote URLs by default; plain HTTP requires explicit opt-in ++- ✓ Resolve and validate final connected IP after redirects to mitigate DNS-rebinding bypasses + +@@ Command: swagger-cli fetch <url> [OPTIONS] ++ --allow-private-host <HOST> Allow specific private/internal hosts (repeatable) ++ --allow-insecure-http Permit http:// URLs (default: reject) + +@@ Error cases ++| Policy blocked | 16 | `POLICY_BLOCKED` | +``` + +2. **Harden alias and filesystem path safety** +Analysis: Alias names currently appear to map directly to directory names, which creates path traversal and portability risk if not constrained. Strict alias validation plus canonical path handling for local files removes a whole class of corruption/security bugs. +```diff +@@ FR-1: Spec Fetching and Caching / Acceptance Criteria ++- ✓ Validate alias format: `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$` ++- ✓ Reject aliases containing path separators, `..`, or reserved device names ++- ✓ Canonicalize local file paths before ingest; fail fast on unreadable targets +``` + +3. **Promote `diff` to Phase 2 with breaking-change classification** +Analysis: You already compute sync deltas; exposing this directly as a first-class command is highly compelling for CI, release checks, and agent workflows. It turns passive metadata into an actionable compatibility contract. +```diff +@@ Functional Requirements ++### FR-11: Spec Diff and Compatibility ++**Description:** Compare two spec states and classify changes as breaking/non-breaking. ++**Acceptance Criteria:** ++- ✓ Compare alias vs alias, alias vs URL, or alias generation vs generation ++- ✓ Classify endpoint and schema changes (`breaking`, `non_breaking`, `unknown`) ++- ✓ Robot mode emits machine-actionable compatibility summary ++**Command:** ++`swagger-cli diff <LEFT> <RIGHT> [--fail-on breaking] [--details] [--robot]` + +@@ Phase 2 (Polish) - Week 2 ++- ✓ Diff command with compatibility classification for CI gates +``` + +4. **Formalize robot contract with JSON Schema artifacts and compatibility policy** +Analysis: Golden tests are good, but schema files give stronger guarantees and external tooling compatibility. This materially improves long-term API stability for agent consumers. +```diff +@@ Appendix C: Robot Mode Output Schemas ++Canonical schemas are versioned and published: ++- docs/robot-schema/v1/success.schema.json ++- docs/robot-schema/v1/error.schema.json ++ ++Compatibility policy: ++- Additive fields: no schema_version bump ++- Removed/renamed/type-changed fields: MUST bump schema_version ++- `meta.command_version` added for command-specific payload evolution +``` + +5. **Reduce write amplification from `last_accessed` updates** +Analysis: Updating metadata on every query can create unnecessary I/O, contention, and SSD churn, especially with frequent agent calls. Coalescing updates preserves LRU usefulness while improving performance/reliability under concurrency. +```diff +@@ FR-10 Decision rationale +-- **LRU eviction:** Uses `last_accessed` timestamp (updated on every query command) for fair eviction ordering. ++- **LRU eviction:** Uses coalesced `last_accessed` writes (e.g., write only when older than 10 minutes) to reduce lock contention and write amplification. + +@@ src/core/cache.rs / Read protocol +-- Update meta.last_accessed timestamp on successful read (best-effort, no lock required). ++- Coalesce last_accessed updates with minimum write interval; skip metadata rewrite for hot-read bursts. +``` + +6. **Shift to async HTTP + explicit service layering** +Analysis: `sync --all` and retry/per-host controls are cleaner and more robust with async primitives. Separating CLI parsing from application services and infra adapters improves testability and lowers long-term maintenance cost. +```diff +@@ Technology Stack +-reqwest = { version = "0.13", default-features = false, features = ["json", "blocking", "rustls-tls"] } ++reqwest = { version = "0.13", default-features = false, features = ["json", "rustls-tls", "http2"] } ++tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "sync"] } ++tracing = "0.1" ++tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +@@ Project Structure ++├── src/app/ # Use-case orchestration (fetch/list/show/sync services) ++├── src/infra/ # HTTP client, fs/cache, lock, clock abstractions +``` + +7. **Add resumable sync and failure budget controls** +Analysis: Large multi-alias sync runs can fail mid-way and currently require full reruns. Resume checkpoints and configurable failure budgets improve reliability for CI and large fleet use. +```diff +@@ FR-7: Sync and Updates / OPTIONS ++ --resume Resume from last interrupted --all sync checkpoint ++ --max-failures <N> Abort run after N alias failures (default: unlimited) + +@@ FR-7 Acceptance Criteria ++- ✓ `sync --all` persists per-alias progress checkpoint for resumable execution ++- ✓ Supports controlled abort via failure budget to limit noisy upstream incidents +``` + +8. **Upgrade secret handling model beyond plaintext token field** +Analysis: You already warn on file perms, but adding env/keyring sources avoids persisting sensitive tokens where possible. This is a meaningful security upgrade with backward-compatible defaults. +```diff +@@ src/core/config.rs +-pub struct AuthConfig { +- pub url_pattern: String, +- pub auth_type: AuthType, +- pub token: String, +-} ++pub struct AuthConfig { ++ pub url_pattern: String, ++ pub auth_type: AuthType, ++ pub credential: CredentialSource, ++} ++ ++pub enum CredentialSource { ++ Literal(String), ++ EnvVar(String), ++ Keyring { service: String, account: String }, ++} +``` + +9. **Expand supply-chain controls (dependency policy + SBOM + provenance)** +Analysis: Checksums/signatures help artifact integrity, but you still need dependency and provenance gates in CI for stronger trust. This is especially valuable for an agent-facing CLI that may run in privileged automation. +```diff +@@ .gitlab-ci.yml ++security:deps: ++ stage: test ++ image: rust:1.93 ++ script: ++ - cargo install cargo-deny cargo-audit ++ - cargo deny check ++ - cargo audit ++ ++release:sbom: ++ stage: release ++ script: ++ - cargo install cargo-cyclonedx ++ - cargo cyclonedx --format json --output-file sbom.cdx.json ++ - cosign attest --predicate sbom.cdx.json --type cyclonedx $RELEASE_ARTIFACT +``` + +10. **Fix installer portability and cleanup behavior** +Analysis: `sha256sum` is not guaranteed on macOS, and current temp-file handling is not robust on interrupted runs. These fixes reduce installation friction and avoid leaving sensitive artifacts in `/tmp`. +```diff +@@ install.sh +-set -e ++set -euo pipefail ++TMP_DIR="$(mktemp -d)" ++trap 'rm -rf "$TMP_DIR"' EXIT + +@@ checksum verification +-ACTUAL=$(sha256sum "$INSTALL_DIR/swagger-cli" | awk '{print $1}') ++if command -v sha256sum >/dev/null 2>&1; then ++ ACTUAL=$(sha256sum "$INSTALL_DIR/swagger-cli" | awk '{print $1}') ++else ++ ACTUAL=$(shasum -a 256 "$INSTALL_DIR/swagger-cli" | awk '{print $1}') ++fi +``` + +If you want, I can produce one consolidated full-PRD patch (single unified diff) applying all 10 changes in-place so you can drop it directly into the next iteration. \ No newline at end of file diff --git a/prd-swagger-cli.md b/prd-swagger-cli.md new file mode 100644 index 0000000..17a12fe --- /dev/null +++ b/prd-swagger-cli.md @@ -0,0 +1,4154 @@ +--- +plan: true +title: "" +status: iterating +iteration: 2 +target_iterations: 8 +beads_revision: 0 +related_plans: [] +created: 2026-02-12 +updated: 2026-02-12 +--- + +# PRD: swagger-cli - OpenAPI Specification CLI Tool + +**Version:** 1.6 +**Status:** Draft (revised: integrates SSRF/transport policy, alias format validation, robot JSON Schema artifacts, coalesced LRU writes, resumable sync, credential source abstraction, installer portability, diff command, dependency audit CI) +**Created:** 2026-02-02 +**Target Audience:** Engineering teams, AI agents + +## Executive Summary + +Build a fast, agent-optimized CLI tool for querying OpenAPI specifications. Enables instant endpoint discovery, schema browsing, and API exploration without repeated web fetches. + +**Key Metrics:** +- Query latency: <50ms for cached specs (index-backed, no raw spec parsing) +- First query: <2s including fetch + index build +- Robot mode JSON: 100% structured output with versioned schema contract and published JSON Schema artifacts +- Exit codes: Consistent, actionable, concurrency-safe + +--- + +## Problem Statement + +### Current State + +**Agents querying APIs today:** +1. Use `WebFetch` with OpenAPI URLs +2. Requires prompt engineering to extract specific data +3. Multiple round-trips to find information +4. Re-fetches on every query (no caching) +5. Inconsistent output format + +**Example pain point (observed):** +```bash +# Took 3 attempts to get endpoint info +WebFetch(url1) → config code only +WebFetch(url2) → 404 error +WebFetch(url3) → finally got spec +# Total: ~8-10 seconds, 3 tool calls +``` + +### Desired State + +**With swagger-cli:** +```bash +# First query (with fetch) +swagger-cli fetch https://petstore3.swagger.io/api/v3/openapi.json --alias petstore +# ~2 seconds + +# All subsequent queries +swagger-cli list petstore --tag "pet" --robot +# <50ms, structured JSON +``` + +--- + +## Goals and Non-Goals + +### Goals + +1. **Performance:** <50ms cached queries, <2s first fetch +2. **Agent ergonomics:** Robot mode with structured JSON, meaningful exit codes +3. **Reliability:** Offline-capable after initial fetch +4. **Flexibility:** Support multiple APIs with aliases +5. **Maintainability:** Clear code, comprehensive tests, documented decisions +6. **Determinism:** Global network policy control for reproducible offline/CI execution +7. **Operational hygiene:** Cache lifecycle management (prune, stats, size caps) + +### Non-Goals + +1. **API execution:** Not a replacement for curl/httpie (no actual HTTP requests) +2. **Code generation:** Not generating client SDKs (maybe v2) +3. **Spec validation:** Not validating OpenAPI correctness (trust upstream) +4. **UI/TUI:** CLI only, no interactive interfaces + +--- + +## User Personas + +### Primary: AI Agents + +**Needs:** +- Instant endpoint lookup +- Structured JSON output +- Predictable behavior +- Meaningful errors +- No interactivity + +**Usage patterns:** +```bash +swagger-cli fetch $URL --alias api --robot +swagger-cli search api "create user" --robot | jq '.data[0].path' +swagger-cli show api "/users" --robot +``` + +### Secondary: Human Developers + +**Needs:** +- Quick API reference +- Readable output +- Easy setup +- Multiple API management + +**Usage patterns:** +```bash +swagger-cli list petstore --method POST +swagger-cli show petstore "/pets/{petId}" +swagger-cli search petstore "order" --limit 5 +``` + +--- + +## Functional Requirements + +### FR-1: Spec Fetching and Caching + +**Description:** Download and cache OpenAPI specs locally. + +**Acceptance Criteria:** +- ✓ Fetch from URL with optional auth headers +- ✓ Store with user-defined alias +- ✓ Validate alias format: `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$` — rejects path separators, `..`, reserved device names (CON, PRN, NUL, etc.), and leading dots +- ✓ Canonicalize local file paths before ingest; fail fast on unreadable targets +- ✓ Cache in XDG cache dir (default `~/.cache/swagger-cli/`) with per-alias directories +- ✓ Store four files per alias: original upstream bytes (`raw.source`), canonical JSON (`raw.json`), derived index (`index.json`), metadata (`meta.json`) +- ✓ Validate spec is parseable JSON or YAML before caching (no full OpenAPI structural validation) +- ✓ YAML input is normalized to JSON during ingestion (raw.json always stores canonical JSON; original bytes preserved in raw.source) +- ✓ Enforce `--max-bytes` during streaming download (fail before full buffering; do not buffer entire response in memory) +- ✓ All query commands (list/search/tags/schemas/aliases/doctor) read index only -- never parse raw spec +- ✓ Handle redirects (follow up to 5) +- ✓ Default timeout is an overall request timeout (10s). Connect timeout is capped at 5s. +- ✓ Max spec size guardrail (default 25MB, configurable) +- ✓ Enforce fetch network policy by default: block loopback (127.0.0.0/8, ::1), link-local (169.254.0.0/16, fe80::/10), RFC1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), and multicast targets for remote URL fetches — mitigates SSRF +- ✓ Require HTTPS for remote URLs by default; plain HTTP requires explicit `--allow-insecure-http` opt-in +- ✓ Resolve and validate final connected IP after redirects to mitigate DNS-rebinding bypasses (check resolved IP against blocked ranges before sending request body) +- ✓ Support local specs: `file://...` and `./openapi.json` paths (network policy does not apply to local files) +- ✓ Support stdin: `swagger-cli fetch - --alias x` reads spec from stdin (useful for piping from other tools) +- ✓ Support OpenAPI 3.0.x and 3.1.x +- ✓ Cache writes are crash-consistent (meta.json written LAST as commit marker; generation + index_hash validated on read) +- ✓ Cache is safe under concurrent processes (per-alias file lock) +- ✓ Readers detect torn/partial cache state (meta missing or generation/hash mismatch → CACHE_INTEGRITY error) +- ✓ Never print auth tokens in output; `--auth-header` values are redacted in logs/errors +- ✓ Support `--auth-profile <NAME>` to load auth config from `config.toml` (preferred over raw tokens in shell history) +- ✓ Optional fetch-time external ref bundling (`--resolve-external-refs`) with host allowlist and depth/size limits +- ✓ Validate all index pointers (operation_ptr, schema_ptr) resolve against raw.json during fetch/sync; invalid pointers fail the fetch + +**Command:** +```bash +swagger-cli fetch <url> [OPTIONS] + +OPTIONS: + --alias <NAME> Set alias for this spec (required; must match ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$) + --header <HEADER> Add request header (repeatable; e.g., "X-API-Key: token", "Accept: application/json") + --auth-header <HEADER> Alias for --header (kept for discoverability; same behavior) + --bearer <TOKEN> Shorthand for Bearer auth + --auth-profile <NAME> Load auth header/token from config.toml profile (preferred over raw tokens) + --force Overwrite existing alias + --timeout-ms <N> Request timeout in ms (default: 10000) + --max-bytes <N> Max download size in bytes (default: 26214400 / 25MB) + --retries <N> Retry transient failures (default: 2) + --input-format <FORMAT> Input format hint: auto (default), json, yaml + --resolve-external-refs Resolve and bundle external $ref targets at fetch-time (opt-in) + --ref-allow-host <HOST> Allowlist host for external ref resolution (repeatable; required with --resolve-external-refs) + --ref-max-depth <N> Max external ref chain depth (default: 3) + --ref-max-bytes <N> Total bytes cap for all external ref fetches (default: 10MB) + --allow-private-host <HOST> Allow specific private/internal host for SSRF policy bypass (repeatable) + --allow-insecure-http Permit http:// URLs (default: reject remote URLs without HTTPS) + --robot Machine-readable output +``` + +**Examples:** +```bash +# Basic fetch +swagger-cli fetch https://api.example.com/openapi.json --alias example + +# With authentication +swagger-cli fetch https://internal.api/spec.json --alias internal \ + --auth-header "X-API-Key: secret123" + +# Bearer token shorthand +swagger-cli fetch https://api.com/spec --alias api --bearer $TOKEN + +# Preferred: auth profile (avoids shell history leakage) +swagger-cli fetch https://internal.api/spec.json --alias internal \ + --auth-profile corp-internal + +# Force overwrite +swagger-cli fetch $URL --alias petstore --force + +# Local file (useful for testing and offline workflows) +swagger-cli fetch ./openapi.json --alias local-api +swagger-cli fetch file:///absolute/path/to/spec.json --alias local-api + +# Stdin (useful for piping from other tools or CI) +curl -s https://api.example.com/spec.json | swagger-cli fetch - --alias piped-api + +# YAML spec (auto-detected or hinted) +swagger-cli fetch https://api.example.com/openapi.yaml --alias yaml-api +swagger-cli fetch ./spec.yaml --alias local-yaml --input-format yaml + +# External ref bundling (opt-in, with host allowlist) +swagger-cli fetch https://api.example.com/openapi.json --alias bundled \ + --resolve-external-refs \ + --ref-allow-host api.example.com \ + --ref-allow-host schemas.example.com +``` + +**Robot output:** +```json +{ + "ok": true, + "data": { + "alias": "petstore", + "url": "https://petstore3.swagger.io/api/v3/openapi.json", + "version": "1.0.17", + "title": "Swagger Petstore", + "endpoint_count": 19, + "schema_count": 8, + "cached_at": "2026-02-02T20:45:00Z", + "source_format": "json", + "cache_dir": "/Users/user/.cache/swagger-cli/aliases/petstore/", + "files": { + "raw_source": "/Users/user/.cache/swagger-cli/aliases/petstore/raw.source", + "raw": "/Users/user/.cache/swagger-cli/aliases/petstore/raw.json", + "index": "/Users/user/.cache/swagger-cli/aliases/petstore/index.json", + "meta": "/Users/user/.cache/swagger-cli/aliases/petstore/meta.json" + }, + "content_hash": "sha256:abc123..." + }, + "meta": { + "schema_version": 1, + "tool_version": "1.0.0", + "command": "fetch", + "duration_ms": 1847 + } +} +``` + +**Error cases:** +| Error | Exit Code | JSON Error Code | +|-------|-----------|-----------------| +| Network failure | 4 | `NETWORK_ERROR` | +| Invalid JSON | 5 | `INVALID_SPEC` | +| Alias exists | 6 | `ALIAS_EXISTS` | +| Auth failed | 7 | `AUTH_FAILED` | +| Policy blocked (SSRF, insecure transport) | 16 | `POLICY_BLOCKED` | + +**HTTP error classification (normative):** +- `401/403` → `AUTH_FAILED` (exit 7) +- `404` → `INVALID_SPEC` (exit 5) — spec not found at URL +- Other `4xx` → `NETWORK_ERROR` (exit 4, not retryable) +- `5xx` → `NETWORK_ERROR` (exit 4, retryable with backoff) + +**Decision rationale:** +- **Cache location:** XDG cache dir (`~/.cache/`) for ephemeral data; config in XDG config dir (`~/.config/`). Proper separation per XDG spec. +- **Four-file cache:** `raw.source` preserves exact upstream bytes (lossless provenance, may be JSON or YAML); `raw.json` stores canonical normalized JSON (all query/show logic operates on this); `index.json` is a precomputed, small, fast-loading structure for query commands; `meta.json` stores fetch metadata. The raw.source/raw.json split enables YAML input support without any impact on query performance or internal logic. +- **Alias format validation:** Aliases map directly to directory names, so path traversal (`../`), reserved device names (`CON`, `NUL`), and shell-hostile characters must be rejected at parse time. The regex `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$` covers all portable filesystem-safe names. 64 chars is generous but prevents absurdly long directory names. +- **SSRF protection:** Agent-facing CLI tools may run in privileged environments with access to cloud metadata endpoints (169.254.169.254) and internal services. Blocking private/loopback/link-local/multicast ranges by default prevents accidental SSRF. `--allow-private-host` permits explicit exceptions for legitimate internal API specs. DNS rebinding check validates the resolved IP after redirects, not just the hostname. +- **HTTPS-by-default:** Agents downloading API specs over plaintext HTTP risk MITM injection of malicious spec content. Requiring HTTPS by default with explicit `--allow-insecure-http` opt-in makes the secure path the default. +- **Alias requirement:** Forces explicit naming, prevents confusion with multiple specs +- **No auto-sync:** Manual sync required (`swagger-cli sync`) to prevent unexpected network calls +- **Content hash:** SHA256 for change detection, stored in metadata +- **Crash-consistent writes:** Cache writes use a multi-file commit protocol: raw.source, raw.json, and index.json are written and renamed first, then meta.json is written LAST as the commit marker. Readers validate meta.generation + meta.index_hash against index.json (and meta.raw_hash against raw.json for show commands) to detect torn state. Per-alias file locks prevent concurrent write corruption (justifies exit code 9, `CACHE_LOCKED`). +- **Auth redaction:** Auth tokens are never printed in output to prevent accidental secret leakage in logs or piped output. +- **Auth profiles:** `--auth-profile <NAME>` loads auth config from `config.toml`, avoiding raw tokens in shell history, process lists, and CI logs. Explicit `--header`/`--bearer` flags merge with (and override) profile headers for one-off use cases. +- **Repeatable headers:** `--header` is repeatable for multi-header fetches (common in corporate environments). `--auth-header` is kept as an alias for discoverability. +- **YAML input support:** Many real-world OpenAPI specs are authored in YAML. Input format is auto-detected (file extension, Content-Type header) or hinted via `--input-format`. YAML is normalized to JSON during ingestion; `raw.source` preserves original bytes, `raw.json` stores canonical JSON. This keeps all query/index logic JSON-only while accepting YAML at the boundary. +- **Streaming max-bytes:** `--max-bytes` is enforced during download via streaming byte counting, not post-download file size check. Prevents OOM on malicious/huge specs. +- **External ref bundling:** Opt-in at fetch time only. Requires explicit `--ref-allow-host` allowlist to prevent fetching from arbitrary hosts. Bundled result is stored in `raw.json` (all refs inlined). Preserves offline guarantee for all query commands. Default behavior (no `--resolve-external-refs`) is unchanged. +- **Pointer validation:** All `operation_ptr` and `schema_ptr` values in the index are validated against `raw.json` at fetch/sync time. Invalid pointers fail the fetch rather than silently producing broken `show` commands later. `doctor` re-validates all pointers. + +### FR-2: Endpoint Listing + +**Description:** List all endpoints with filtering and sorting. + +**Acceptance Criteria:** +- ✓ Display path, method, summary +- ✓ Filter by HTTP method +- ✓ Filter by tag/category +- ✓ Filter by path pattern (regex) +- ✓ Sort by path (default) or method +- ✓ Limit results +- ✓ Robot mode JSON + +**Command:** +```bash +swagger-cli list [ALIAS] [OPTIONS] + +OPTIONS: + --method <METHOD> Filter by HTTP method (GET, POST, PUT, DELETE, PATCH) + --tag <TAG> Filter by OpenAPI tag + --path <PATTERN> Filter by path regex (invalid regex → USAGE_ERROR; no silent fallback) + --sort <FIELD> Sort by: path (default), method, tag + --limit <N> Limit results (default: 50) + --all Show all results (no limit) + --all-aliases Run query across all aliases; include `alias` field per result in output + --robot Machine-readable output +``` + +**Examples:** +```bash +# All endpoints +swagger-cli list petstore + +# POST endpoints only +swagger-cli list petstore --method POST + +# Tagged endpoints +swagger-cli list petstore --tag "pet" + +# Path pattern +swagger-cli list petstore --path "store.*" + +# Combined filters +swagger-cli list petstore --method POST --tag pet --limit 10 + +# All results +swagger-cli list petstore --all +``` + +**Human output:** +``` +Petstore API (Swagger Petstore v1.0.17) - 19 endpoints + +GET /pet/{petId} Find pet by ID +GET /pet/findByStatus Finds pets by status +GET /pet/findByTags Finds pets by tags +POST /pet Add a new pet to the store +PUT /pet Update an existing pet +DELETE /pet/{petId} Deletes a pet +GET /store/inventory Returns pet inventories +POST /store/order Place an order for a pet + +Showing 8 of 19 endpoints (filtered) +``` + +**Robot output:** +```json +{ + "ok": true, + "data": { + "endpoints": [ + { + "path": "/pet", + "method": "POST", + "summary": "Add a new pet to the store", + "description": "Add a new pet to the store", + "tags": ["pet"], + "operation_id": "addPet", + "deprecated": false, + "parameters": [], + "request_body_required": true, + "security": ["petstore_auth"] + } + ], + "total": 19, + "filtered": 3, + "applied_filters": { + "method": "POST", + "tag": "pet" + } + }, + "meta": { + "schema_version": 1, + "tool_version": "1.0.0", + "command": "list", + "alias": "petstore", + "spec_version": "1.0.17", + "cached_at": "2026-02-02T20:45:00Z", + "duration_ms": 12 + } +} +``` + +**Decision rationale:** +- **Default limit:** 50 prevents overwhelming output +- **Regex patterns:** More flexible than glob patterns for paths +- **Tag filtering:** Maps to OpenAPI native tags, not custom grouping +- **Index-backed:** All filtering and sorting operates on `index.json`, never parses raw spec. Index contains *effective* security scheme names (root + operation override semantics) so agents can accurately detect auth requirements without loading raw.json. +- **Deterministic ordering:** Index arrays are pre-sorted (endpoints by path+method, schemas by name, tags by name). Robot output preserves this ordering for stable diffs and predictable agent parsing. +- **Strict validation:** Invalid regex in `--path` or `--name` fails with `USAGE_ERROR` immediately. No silent fallback to "no filter." Invalid `--in` field selectors similarly fail fast. +- **Color/unicode:** Controlled by TTY detection independently of `--robot`. Piping to `| less` or `> file` disables color but does NOT switch to JSON output. + +### FR-3: Endpoint Details + +**Description:** Show complete details for a specific endpoint. + +**Acceptance Criteria:** +- ✓ Display path, method, summary, description +- ✓ Show all parameters (path, query, header, cookie) +- ✓ Show request body schema (if applicable) +- ✓ Show response schemas with status codes +- ✓ Show security requirements +- ✓ Pretty-print JSON schemas +- ✓ Robot mode with full structured data + +**Command:** +```bash +swagger-cli show [ALIAS] <path> [OPTIONS] + +OPTIONS: + --method <METHOD> Required if path has multiple methods; otherwise optional + --format <FORMAT> Output format: pretty (default), json + --expand-refs Expand $ref pointers inline + --max-depth <N> Max expansion depth (default: 3; only with --expand-refs) + --robot Machine-readable output (implies --format json) +``` + +**Examples:** +```bash +# Show endpoint +swagger-cli show petstore "/pet/{petId}" + +# Specific method +swagger-cli show petstore "/pet" --method POST + +# With ref expansion (bounded) +swagger-cli show petstore "/store/order" --expand-refs --max-depth 5 + +# Robot mode +swagger-cli show petstore "/pet" --method POST --robot +``` + +**Human output:** +``` +POST /pet +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Summary: Add a new pet to the store +Description: Add a new pet to the store + +Tags: pet +Operation ID: addPet +Security: petstore_auth (required) + +━━ Request Body (application/json) ━━━━━━━━━━━━━━━━━━━━━━━━━ + +Schema: Pet (required) + +{ + "id": 10, + "name": "doggie", + "category": { "id": 1, "name": "Dogs" }, + "photoUrls": ["string"], + "tags": [{ "id": 0, "name": "string" }], + "status": "available" +} + +━━ Responses ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +200 OK Successful operation + Schema: Pet + +405 Invalid Input Invalid input + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +**Robot output:** +```json +{ + "ok": true, + "data": { + "path": "/pet", + "method": "POST", + "summary": "Add a new pet to the store", + "description": "Add a new pet to the store", + "tags": ["pet"], + "operation_id": "addPet", + "deprecated": false, + "parameters": [], + "request_body": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": ["write:pets", "read:pets"] + } + ] + }, + "meta": { + "schema_version": 1, + "tool_version": "1.0.0", + "command": "show", + "alias": "petstore", + "spec_version": "1.0.17", + "duration_ms": 23 + } +} +``` + +**Decision rationale:** +- **Default no-expand:** Refs are useful for understanding schema reuse; expand optional +- **Bounded expand:** `--expand-refs` MUST be bounded by `--max-depth` (default 3) and MUST detect cycles. Cycles are annotated in output with `{"$circular_ref": "#/components/schemas/Foo"}` rather than causing infinite recursion. +- **External refs are NOT fetched (no network).** If `--expand-refs` encounters a non-internal `$ref` (anything not starting with `#/`), it MUST leave it unexpanded and annotate with `{"$external_ref": "<ref>"}` plus a warning in `meta.warnings[]` (robot) or a warning line (human). This preserves the offline guarantee. +- **Multiple methods:** If multiple methods exist and `--method` is not provided, return `USAGE_ERROR` with available methods listed in the suggestion field. This avoids nondeterminism and makes agent retries straightforward +- **Full schema inclusion:** Agents need complete data for code generation potential +- **Index-then-raw:** `show` first loads `index.json` to find the operation's JSON pointer, then loads `raw.json` as `serde_json::Value` and extracts the operation subtree. This is the only command that reads `raw.json`. + +### FR-4: Schema Browser + +**Description:** Browse and inspect component schemas. + +**Acceptance Criteria:** +- ✓ List all schemas +- ✓ Filter by name pattern +- ✓ Show schema details +- ✓ Expand nested refs recursively +- ✓ Detect circular references +- ✓ Robot mode JSON + +**Command:** +```bash +swagger-cli schemas [ALIAS] [OPTIONS] + +OPTIONS: + --name <PATTERN> Filter by schema name (regex; invalid → USAGE_ERROR) + --list List all schema names (default) + --show <NAME> Show specific schema details + --expand-refs Recursively expand $ref pointers + --max-depth <N> Max recursion depth (default: 3) + --robot Machine-readable output +``` + +**Examples:** +```bash +# List all schemas +swagger-cli schemas petstore + +# Filter schemas +swagger-cli schemas petstore --name ".*Pet.*" + +# Show specific schema +swagger-cli schemas petstore --show Pet + +# Show with expansion +swagger-cli schemas petstore --show Order --expand-refs + +# Robot mode +swagger-cli schemas petstore --show Pet --robot +``` + +**Human output:** +``` +Petstore API Schemas (8 total) + +Pet +Category +Tag +Order +User +Address +Customer +ApiResponse + +Showing 8 schemas +``` + +**Schema detail output:** +``` +Schema: Pet +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Type: object +Required: name, photoUrls + +Properties: + id (integer) Pet ID + name (string) Pet name + category ($ref) Reference to Category + photoUrls (array) Array of image URLs + tags (array) Array of Tag references + status (string) Pet status (enum: available, pending, sold) + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +**Robot output:** +```json +{ + "ok": true, + "data": { + "name": "Pet", + "type": "object", + "required": ["name", "photoUrls"], + "properties": { + "id": { + "type": "integer", + "format": "int64", + "description": "Pet ID" + }, + "name": { + "type": "string", + "description": "Pet name" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "photoUrls": { + "type": "array", + "items": { "type": "string" } + }, + "tags": { + "type": "array", + "items": { "$ref": "#/components/schemas/Tag" } + }, + "status": { + "type": "string", + "enum": ["available", "pending", "sold"], + "description": "Pet status in the store" + } + } + }, + "meta": { + "schema_version": 1, + "tool_version": "1.0.0", + "command": "schemas", + "alias": "petstore", + "circular_refs": false, + "duration_ms": 8 + } +} +``` + +**Decision rationale:** +- **Max depth:** Prevents infinite recursion with circular refs +- **Circular detection:** Warns rather than errors (valid OpenAPI pattern) +- **Regex filter:** More powerful than substring matching +- **Index-backed listing:** Schema names and pointers come from `index.json`. Schema details (`--show`) load the relevant subtree from `raw.json` via JSON pointer. +- **Parameter details intentionally shallow:** name/location/required/desc in index preserves <50ms queries while improving agent usefulness for planning. + +### FR-5: Text Search + +**Description:** Search across endpoint paths, summaries, descriptions, and schema names. + +**Acceptance Criteria:** +- ✓ Full-text search across all searchable fields +- ✓ Case-insensitive by default +- ✓ Rank results by relevance +- ✓ Show context snippets +- ✓ Limit results +- ✓ Robot mode JSON + +**Command:** +```bash +swagger-cli search [ALIAS] <query> [OPTIONS] + +OPTIONS: + --case-sensitive Case-sensitive matching + --exact Exact phrase matching + --in <FIELDS> Search in: all (default), paths, descriptions, schemas (invalid field → USAGE_ERROR) + --limit <N> Limit results (default: 20) + --all-aliases Search across all aliases; include `alias` field per result in output + --robot Machine-readable output +``` + +**Examples:** +```bash +# Basic search +swagger-cli search petstore "pet status" + +# Case-sensitive +swagger-cli search petstore "ID" --case-sensitive + +# Exact phrase +swagger-cli search petstore "find pet" --exact + +# Scoped search +swagger-cli search petstore "order" --in paths,descriptions + +# Robot mode +swagger-cli search petstore "store inventory" --robot --limit 5 +``` + +**Human output:** +``` +Petstore API Search: "pet status" (4 matches) + +━━ Endpoints ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +GET /pet/findByStatus + Finds pets by status + Multiple status values can be provided with comma separated strings... + +PUT /pet + Update an existing pet + Update an existing pet by Id... + +━━ Schemas ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Pet + Schema for pet objects with status field + +Order + Schema for store orders with status tracking +``` + +**Robot output:** +```json +{ + "ok": true, + "data": { + "query": "pet status", + "results": [ + { + "type": "endpoint", + "path": "/pet/findByStatus", + "method": "GET", + "summary": "Finds pets by status", + "description": "Multiple status values can be provided...", + "rank": 1, + "score": 9500, + "matches": [ + { + "field": "summary", + "snippet": "Finds pets by status" + }, + { + "field": "path", + "snippet": "/pet/findByStatus" + } + ] + }, + { + "type": "schema", + "name": "Pet", + "description": "Schema for pet objects", + "rank": 2, + "score": 8700, + "matches": [ + { + "field": "name", + "snippet": "Pet" + } + ] + } + ], + "total": 4, + "query_time_ms": 12 + }, + "meta": { + "schema_version": 1, + "tool_version": "1.0.0", + "command": "search", + "alias": "petstore", + "duration_ms": 12 + } +} +``` + +**Decision rationale:** +- **Tokenized scoring:** Query is split into terms; score combines field weights (path > summary > description) with term coverage boost. `--exact` treats query as single phrase. Scores are quantized to integer (basis points) for cross-platform determinism and stable golden tests. Results include `rank` (1..N) for easy agent filtering. +- **Index-backed:** Search runs over `index.json` for speed and consistency; never loads `raw.json` +- **No semantic search:** Adds complexity; text search sufficient for MVP +- **Context snippets:** 50 chars before/after match, using char-boundary-safe slicing (Unicode-safe) +- **Options honored:** `--case-sensitive` and `--exact` flags affect matching behavior as expected + +### FR-6: Alias Management + +**Description:** Manage multiple API aliases. + +**Acceptance Criteria:** +- ✓ List all configured aliases +- ✓ Show alias details (URL, version, stats) +- ✓ Rename aliases +- ✓ Delete aliases +- ✓ Set default alias +- ✓ Robot mode JSON + +**Command:** +```bash +swagger-cli aliases [OPTIONS] + +OPTIONS: + --list List all aliases (default) + --show <ALIAS> Show alias details + --rename <OLD> <NEW> Rename alias + --delete <ALIAS> Delete alias + --set-default <ALIAS> Set default alias + --robot Machine-readable output +``` + +**Examples:** +```bash +# List all +swagger-cli aliases + +# Details +swagger-cli aliases --show petstore + +# Rename +swagger-cli aliases --rename petstore pets + +# Delete +swagger-cli aliases --delete old-api + +# Set default +swagger-cli aliases --set-default petstore +``` + +**Human output:** +``` +Configured APIs (3 total) + +petstore * (default) + URL: https://petstore3.swagger.io/api/v3/openapi.json + Version: 1.0.17 + Cached: 2026-02-02 15:45 + Size: 45 KB + +stripe + URL: https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json + Version: 2023-10-16 + Cached: 2026-01-28 10:22 + Size: 2.4 MB + +github + URL: https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json + Version: 1.1.4 + Cached: 2026-02-01 09:15 + Size: 8.2 MB +``` + +**Robot output:** +```json +{ + "ok": true, + "data": { + "aliases": [ + { + "name": "petstore", + "url": "https://petstore3.swagger.io/api/v3/openapi.json", + "version": "1.0.17", + "is_default": true, + "cached_at": "2026-02-02T20:45:00Z", + "cache_size_bytes": 46080, + "endpoint_count": 19, + "schema_count": 8 + }, + { + "name": "stripe", + "url": "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json", + "version": "2023-10-16", + "is_default": false, + "cached_at": "2026-01-28T15:22:00Z", + "cache_size_bytes": 2516582, + "endpoint_count": 312, + "schema_count": 456 + } + ], + "default_alias": "petstore" + }, + "meta": { + "schema_version": 1, + "tool_version": "1.0.0", + "command": "aliases", + "duration_ms": 5 + } +} +``` + +**Decision rationale:** +- **Default alias:** Allows `swagger-cli list` without specifying alias +- **Size tracking:** Useful for cache management +- **No auto-delete:** Explicit delete required to prevent accidents + +### FR-7: Sync and Updates + +**Description:** Check for and apply spec updates. + +**Acceptance Criteria:** +- ✓ Check if remote spec changed +- ✓ Re-fetch if changed +- ✓ Preserve alias and config +- ✓ Report differences +- ✓ Support --all flag +- ✓ `sync --all` persists per-alias progress checkpoint for resumable execution (`--resume`) +- ✓ Supports controlled abort via failure budget (`--max-failures`) to limit blast radius from noisy upstream incidents +- ✓ Robot mode JSON + +**Command:** +```bash +swagger-cli sync [ALIAS] [OPTIONS] + +OPTIONS: + --all Sync all aliases + --dry-run Check without updating + --force Force re-fetch regardless of changes + --details Include capped lists of added/removed/modified items in robot output (max 200 items; truncated flag) + --jobs <N> Parallel aliases to sync with --all (default: 4, bounded) + --per-host <N> Max concurrent requests per host (default: 2) + --resume Resume from last interrupted --all sync checkpoint + --max-failures <N> Abort run after N alias failures (default: unlimited) + --robot Machine-readable output +``` + +**Examples:** +```bash +# Sync one +swagger-cli sync petstore + +# Check without updating +swagger-cli sync petstore --dry-run + +# Sync all +swagger-cli sync --all + +# Force re-fetch +swagger-cli sync petstore --force +``` + +**Human output:** +``` +Syncing petstore... + +Remote: https://petstore3.swagger.io/api/v3/openapi.json +Local: 1.0.17 (cached 2026-02-02 15:45) +Remote: 1.0.17 (checked 2026-02-02 16:14) + +✓ No changes detected + +Cache is up to date. +``` + +**Change detected output:** +``` +Syncing petstore... + +Remote: https://petstore3.swagger.io/api/v3/openapi.json +Local: 1.0.17 (cached 2026-02-02 15:45) +Remote: 1.0.18 (checked 2026-02-02 16:14) + +⚠ Changes detected + Version: 1.0.17 → 1.0.18 + Endpoints: 19 → 21 (+2) + Schemas: 8 → 9 (+1) + +✓ Updated cache + +Use `swagger-cli sync petstore --dry-run --robot` to retrieve the summary change set. +``` + +**Robot output:** +```json +{ + "ok": true, + "data": { + "alias": "petstore", + "changed": true, + "local_version": "1.0.17", + "remote_version": "1.0.18", + "changes": { + "version_changed": true, + "endpoints": { + "before": 19, + "after": 21, + "added": 2, + "removed": 0, + "modified": 1 + }, + "endpoint_details": { + "added": [["POST", "/pets/batch"], ["GET", "/inventory/summary"]], + "removed": [], + "modified": [["PUT", "/pet"]], + "truncated": false + }, + "schemas": { + "before": 8, + "after": 9, + "added": 1, + "removed": 0 + }, + "schema_details": { + "added": ["BatchRequest"], + "removed": [], + "truncated": false + } + }, + "updated_at": "2026-02-02T21:14:00Z" + }, + "meta": { + "schema_version": 1, + "tool_version": "1.0.0", + "command": "sync", + "duration_ms": 1523 + } +} +``` + +**Decision rationale:** +- **ETag + Last-Modified support:** Prefer `If-None-Match` (ETag); fallback to `If-Modified-Since` (Last-Modified) when ETag absent. Covers more real-world servers. +- **Default behavior:** `sync` checks and applies updates when changed. `--dry-run` performs check-only without writing. +- **Index-based diffs:** Change detection (added/removed endpoints, schemas) computed by comparing old vs new `index.json`, not raw spec text. Fast and deterministic. +- **Actionable details:** `--details` includes capped (max 200) lists of added/removed/modified endpoints and schemas with `truncated` flag. Makes `sync --dry-run --robot --details` immediately useful to agents for understanding what changed. +- **Concurrency-safe:** Sync acquires per-alias lock before writing +- **Bounded parallelism:** `sync --all` uses bounded concurrency (default 4 aliases) with per-host throttling (default 2 per host). Prevents abusive request patterns against upstream servers. Retries honor `Retry-After` header when present; otherwise exponential backoff + jitter. +- **Partial failure reporting:** `sync --all --robot` reports per-alias success/failure without aborting the entire run. Agents can parse which aliases failed and retry selectively. + +### FR-8: Tags Browser + +**Description:** Browse OpenAPI tags/categories. + +**Acceptance Criteria:** +- ✓ List all tags +- ✓ Show endpoint count per tag +- ✓ Show tag descriptions +- ✓ Robot mode JSON + +**Command:** +```bash +swagger-cli tags [ALIAS] [OPTIONS] + +OPTIONS: + --robot Machine-readable output +``` + +**Examples:** +```bash +# List all tags +swagger-cli tags petstore + +# Robot mode +swagger-cli tags petstore --robot +``` + +**Human output:** +``` +Petstore API Tags (3 total) + +pet (8 endpoints) + Everything about your Pets + +store (4 endpoints) + Access to Petstore orders + +user (7 endpoints) + Operations about user +``` + +**Robot output:** +```json +{ + "ok": true, + "data": { + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets", + "endpoint_count": 8 + }, + { + "name": "store", + "description": "Access to Petstore orders", + "endpoint_count": 4 + }, + { + "name": "user", + "description": "Operations about user", + "endpoint_count": 7 + } + ] + }, + "meta": { + "schema_version": 1, + "tool_version": "1.0.0", + "command": "tags", + "alias": "petstore", + "total_tags": 3, + "duration_ms": 3 + } +} +``` + +### FR-9: Health Check and Doctor + +**Description:** Validate installation and cache health. + +**Acceptance Criteria:** +- ✓ Check config directory exists +- ✓ Validate cached specs +- ✓ Detect corrupted files +- ✓ Detect partial/in-progress caches (e.g., raw/index present but meta missing; generation/hash mismatch between meta and index) +- ✓ Warn if config file permissions are insecure (auth tokens present + file is group/world readable) +- ✓ Verify all index pointers (operation_ptr, schema_ptr) resolve to existing JSON nodes in raw.json +- ✓ If raw exists but index is missing/invalid or index_version mismatched, rebuild index (when `--fix` enabled) +- ✓ Check for stale caches +- ✓ Report disk usage +- ✓ Robot mode JSON + +**Command:** +```bash +swagger-cli doctor [OPTIONS] + +OPTIONS: + --fix Auto-fix issues after acquiring per-alias lock: + rebuild index.json from raw if possible (preferred); + remove alias only if raw is unreadable/unparseable + --robot Machine-readable output +``` + +**Examples:** +```bash +# Check health +swagger-cli doctor + +# Auto-fix +swagger-cli doctor --fix + +# Robot mode +swagger-cli doctor --robot +``` + +**Human output:** +``` +swagger-cli Health Check +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +✓ Config directory: /Users/user/.config/swagger-cli +✓ Cache directory: /Users/user/.cache/swagger-cli/aliases +✓ Permissions: Read/write OK + +Cached APIs (3): + ✓ petstore 45 KB Fetched 29 minutes ago + ✓ stripe 2.4 MB Fetched 5 days ago + ⚠ github 8.2 MB Fetched 45 days ago (stale) + +Disk Usage: + Total cache: 10.6 MB + +Health: HEALTHY +Warnings: 1 stale cache (github) +``` + +**Robot output:** +```json +{ + "ok": true, + "data": { + "health": "healthy", + "config_dir": "/Users/user/.config/swagger-cli", + "cache_dir": "/Users/user/.cache/swagger-cli/aliases", + "permissions": { + "config_readable": true, + "config_writable": true, + "cache_readable": true, + "cache_writable": true + }, + "aliases": [ + { + "name": "petstore", + "status": "healthy", + "size_bytes": 46080, + "age_days": 0.02, + "is_stale": false + }, + { + "name": "github", + "status": "stale", + "size_bytes": 8601600, + "age_days": 45, + "is_stale": true + } + ], + "disk_usage": { + "total_bytes": 11141120, + "total_mb": 10.6 + }, + "warnings": [ + { + "code": "STALE_CACHE", + "alias": "github", + "message": "Cache is 45 days old", + "suggestion": "Run: swagger-cli sync github" + } + ] + } +} +``` + +**Decision rationale:** +- **Stale threshold:** 30 days (warning only, not error) +- **Auto-fix scope:** Prefer repair over deletion; delete alias only as last resort when raw is unreadable. Never deletes stale caches. Repair modes: + 1. If raw exists but index is missing/invalid or `index_version` mismatched → rebuild index from raw. + 2. If raw + index are valid but meta is missing → reconstruct meta from raw + index (compute content_hash, set generation=1, compute index_hash). + 3. If raw is unreadable/unparseable → delete alias (last resort). +- **Integrity validation:** Uses generation + index_hash from meta.json to detect torn/partial cache state. If meta is missing but raw/index exist, treat as incomplete (fixable via repair mode 2). +- **Security hygiene:** If auth tokens exist in config, doctor warns on insecure config permissions (group/world readable). Suggests `chmod 600`. +- **Disk usage tracking:** Useful for large deployments + +### FR-10: Cache Lifecycle Management + +**Description:** Manage cache growth, retention, and disk usage. + +**Acceptance Criteria:** +- ✓ Show per-alias and total cache usage statistics +- ✓ Prune stale aliases older than configurable threshold +- ✓ Enforce global cache size cap via LRU eviction (optional) +- ✓ Robot mode JSON + +**Command:** +```bash +swagger-cli cache [OPTIONS] + +OPTIONS: + --stats Show per-alias and total cache usage (default action) + --prune-stale Delete aliases older than stale threshold (default: 90 days) + --prune-threshold <DAYS> Override stale threshold for pruning (default: 90) + --max-total-mb <N> Enforce global cache cap via LRU eviction (evicts oldest-accessed aliases first) + --dry-run Show what would be pruned/evicted without deleting + --robot Machine-readable output +``` + +**Examples:** +```bash +# Show cache stats +swagger-cli cache --stats + +# Prune stale caches +swagger-cli cache --prune-stale + +# Enforce 500MB cap +swagger-cli cache --max-total-mb 500 + +# Preview what would be pruned +swagger-cli cache --prune-stale --dry-run --robot +``` + +**Robot output:** +```json +{ + "ok": true, + "data": { + "aliases": [ + { + "name": "petstore", + "size_bytes": 46080, + "last_accessed": "2026-02-12T10:00:00Z", + "age_days": 10 + } + ], + "total_bytes": 11141120, + "total_mb": 10.6, + "pruned": [], + "evicted": [] + }, + "meta": { + "schema_version": 1, + "tool_version": "1.0.0", + "command": "cache", + "duration_ms": 5 + } +} +``` + +**Decision rationale:** +- **Separate from doctor:** Doctor validates health; cache manages lifecycle. Different concerns. +- **LRU eviction:** Uses coalesced `last_accessed` writes (write only when older than 10 minutes) to reduce lock contention and write amplification under frequent agent queries. Preserves LRU ordering accuracy while avoiding per-query metadata rewrites. +- **Conservative defaults:** Prune threshold is 90 days (not 30 — that's doctor's stale warning). No automatic eviction; `--max-total-mb` must be explicitly requested. +- **Dry-run support:** Critical for agents to preview impact before destructive operations. + +### FR-11: Spec Diff (Phase 2) + +**Description:** Compare two spec states and report structural changes. + +**Acceptance Criteria:** +- ✓ Compare alias vs alias, alias vs URL, or alias generation vs generation +- ✓ Report added, removed, and modified endpoints and schemas +- ✓ Robot mode emits machine-actionable diff summary +- ✓ `--fail-on breaking` exits non-zero when breaking changes detected (useful for CI gates) + +**Command:** +```bash +swagger-cli diff <LEFT> <RIGHT> [OPTIONS] + +OPTIONS: + --fail-on <LEVEL> Exit non-zero if changes at this level: breaking (default: none) + --details Include per-item change descriptions + --robot Machine-readable output +``` + +**Examples:** +```bash +# Compare two aliases +swagger-cli diff petstore-v1 petstore-v2 --robot + +# Compare alias against remote URL (fetches RIGHT as temp) +swagger-cli diff petstore https://api.example.com/openapi.json --robot + +# CI gate: fail if breaking changes +swagger-cli diff petstore-prod petstore-staging --fail-on breaking --robot +``` + +**Robot output:** +```json +{ + "ok": true, + "data": { + "left": "petstore-v1", + "right": "petstore-v2", + "changes": { + "endpoints": { + "added": [["POST", "/pets/batch"]], + "removed": [], + "modified": [["PUT", "/pet"]] + }, + "schemas": { + "added": ["BatchRequest"], + "removed": [], + "modified": ["Pet"] + }, + "summary": { + "total_changes": 3, + "has_breaking": false + } + } + }, + "meta": { + "schema_version": 1, + "tool_version": "1.0.0", + "command": "diff", + "duration_ms": 45 + } +} +``` + +**Decision rationale:** +- **Structural diff, not text diff:** Compares normalized index structures (added/removed/modified endpoints and schemas), not raw JSON text. Produces actionable output for agents and CI. +- **Breaking-change classification (Phase 3 enhancement):** Full breaking/non-breaking/unknown classification requires heuristics (e.g., removed required field = breaking, added optional field = non-breaking, changed type = breaking). Phase 2 reports structural changes; Phase 3 adds semantic classification. +- **Leverages sync infrastructure:** Uses the same index-comparison logic already built for `sync --details`. + +--- + +## Technical Architecture + +### Technology Stack + +**Language:** Rust 1.93+ (stable baseline, Jan 2026) -- all CI/Docker builds must pin to the same toolchain + +**Core dependencies:** +```toml +[dependencies] +# HTTP client — rustls for portable musl/alpine builds (no OpenSSL toolchain needed) +reqwest = { version = "0.13", default-features = false, features = ["json", "blocking", "rustls-tls"] } + +# JSON + YAML processing +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_yaml = "0.9" + +# CLI framework +clap = { version = "4.5", features = ["derive", "env"] } + +# Error handling +anyhow = "1.0" +thiserror = "1.0" + +# Config management +toml = "0.8" +directories = "5.0" + +# Output formatting +colored = "2.0" +tabled = "0.15" + +# Date/time +chrono = { version = "0.4", features = ["serde"] } + +# Regex +regex = "1.10" + +# Hashing +sha2 = "0.10" + +# File locking (for concurrent cache safety) +fs2 = "0.4" + +[dev-dependencies] +assert_cmd = "2.0" +predicates = "3.0" +tempfile = "3.8" +mockito = "1.2" +criterion = "0.5" +``` + +**Rationale:** +- **Rust 1.93+:** Current stable; enables modern std features for terminal detection (no `atty` dependency needed) +- **reqwest 0.13 + rustls:** Current release line with `default-features = false` + `rustls-tls` to avoid OpenSSL dependency. This is critical for the Alpine/musl Dockerfile and simplifies cross-compilation. +- **clap v4:** Modern CLI framework with derive macros +- **serde:** De facto JSON standard in Rust ecosystem +- **toml:** Config serialization (note: crate name is `toml`, not `serde_toml`) +- **fs2:** Cross-platform file locking for concurrent cache safety + +### Project Structure + +``` +swagger-cli/ +├── Cargo.toml +├── Cargo.lock +├── README.md +├── LICENSE +├── .gitlab-ci.yml +│ +├── src/ +│ ├── main.rs # Entry point, CLI setup +│ ├── lib.rs # Public library interface +│ │ +│ ├── cli/ +│ │ ├── mod.rs # CLI command definitions +│ │ ├── fetch.rs # Fetch command +│ │ ├── list.rs # List command +│ │ ├── show.rs # Show command +│ │ ├── search.rs # Search command +│ │ ├── schemas.rs # Schema browsing +│ │ ├── tags.rs # Tag browsing +│ │ ├── aliases.rs # Alias management +│ │ ├── sync.rs # Sync command +│ │ ├── doctor.rs # Health check +│ │ ├── cache.rs # Cache lifecycle management +│ │ └── diff.rs # Spec diff (Phase 2) +│ │ +│ ├── core/ +│ │ ├── mod.rs # Core types and traits +│ │ ├── spec.rs # OpenAPI spec parsing +│ │ ├── cache.rs # Cache management +│ │ ├── config.rs # Configuration +│ │ └── search.rs # Search engine +│ │ +│ ├── output/ +│ │ ├── mod.rs # Output formatting +│ │ ├── human.rs # Human-readable output +│ │ ├── robot.rs # Robot mode JSON +│ │ └── table.rs # Table formatting +│ │ +│ ├── errors.rs # Error types and codes +│ └── utils.rs # Shared utilities +│ +├── benches/ +│ └── perf.rs # Criterion benchmarks +│ +├── tests/ +│ ├── integration/ +│ │ ├── fetch_test.rs +│ │ ├── list_test.rs +│ │ ├── show_test.rs +│ │ ├── search_test.rs +│ │ ├── aliases_test.rs +│ │ └── golden/ # Golden robot output snapshots +│ │ ├── fetch_success.json +│ │ ├── list_success.json +│ │ ├── show_success.json +│ │ └── error_alias_not_found.json +│ │ +│ └── fixtures/ +│ ├── petstore.json # Sample OpenAPI spec (JSON) +│ ├── petstore.yaml # Same spec in YAML (for format normalization tests) +│ ├── github.json # Large spec (8MB+) for perf testing +│ ├── external-refs.json # Spec with external $ref pointers (for bundling tests) +│ └── fastapi.json +│ +├── docs/ +│ ├── architecture.md # This file +│ ├── contributing.md +│ ├── examples.md +│ └── robot-schema/ +│ └── v1/ +│ ├── success.schema.json # JSON Schema for robot success responses +│ └── error.schema.json # JSON Schema for robot error responses +│ +└── deny.toml # cargo-deny configuration (license + advisory policies) +``` + +### Data Models + +**Design principle:** The raw OpenAPI spec is stored losslessly as JSON bytes (`raw.json`). Commands operate primarily on a normalized index (`index.json`). This avoids the fragility of a full typed OpenAPI model (which would break across 3.0/3.1 differences, extensions, and spec variations) while keeping query commands fast by avoiding multi-MB JSON deserialization. + +**Index types (what query commands load):** + +```rust +// src/core/spec.rs + +use serde::{Deserialize, Serialize}; + +/// Precomputed index derived from the raw spec on fetch. +/// This is what list/search/tags/schemas/aliases/doctor load. +/// Typically 10-50KB even for large specs. +/// +/// Determinism requirements (normative): +/// - endpoints MUST be sorted by (path ASC, method_rank ASC, method ASC) +/// - schemas MUST be sorted by (name ASC) +/// - tags MUST be sorted by (name ASC) +/// This guarantees stable robot output ordering, stable sync diffs, and meaningful golden tests. +/// +/// Canonical HTTP method order (normative): +/// GET=0, POST=1, PUT=2, PATCH=3, DELETE=4, OPTIONS=5, HEAD=6, TRACE=7 +/// Unknown methods sort last (rank=99), then lexicographically. +/// +/// Tie-breaking for search results (normative): +/// - Primary: score DESC +/// - Secondary: type (endpoint before schema) +/// - Tertiary: path/name ASC, method_rank ASC +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SpecIndex { + /// Bump when index format changes (forces re-index on load) + pub index_version: u32, + /// Mirrors CacheMetadata.generation for torn-write detection. + /// Read protocol validates: meta.generation == index.generation. + pub generation: u64, + /// Mirrors CacheMetadata.content_hash for change detection / sanity checks. + pub content_hash: String, + pub openapi: String, + pub info: IndexInfo, + pub endpoints: Vec<IndexedEndpoint>, + pub schemas: Vec<IndexedSchema>, + pub tags: Vec<IndexedTag>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndexInfo { + pub title: String, + pub version: String, + pub description: Option<String>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndexedEndpoint { + pub path: String, + pub method: String, + pub summary: Option<String>, + pub description: Option<String>, + pub operation_id: Option<String>, + pub tags: Vec<String>, + pub deprecated: bool, + /// Minimal parameter descriptors for agent planning (no schema expansion here). + pub parameters: Vec<IndexedParam>, + /// True if requestBody exists and is required. + pub request_body_required: bool, + /// Media types present under requestBody.content (e.g. ["application/json"]). + pub request_body_content_types: Vec<String>, + /// Effective security for this operation, applying OpenAPI override semantics: + /// - if operation.security is absent → inherit root-level security + /// - if operation.security == [] → explicitly no auth + /// Flattened to scheme names for compactness. + pub security_schemes: Vec<String>, + /// True if auth is required (i.e., effective security is non-empty). + pub security_required: bool, + /// JSON pointer into raw.json (e.g. "/paths/~1pet/get") + /// Used by `show` command to extract full operation details + pub operation_ptr: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndexedParam { + pub name: String, + /// "path" | "query" | "header" | "cookie" + pub location: String, + pub required: bool, + /// Optional short description (truncated during indexing to keep index small) + pub description: Option<String>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndexedSchema { + pub name: String, + /// JSON pointer into raw.json (e.g. "/components/schemas/Pet") + pub schema_ptr: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndexedTag { + pub name: String, + pub description: Option<String>, + pub endpoint_count: usize, +} +``` + +**Why not a full typed OpenAPI model?** + +A typed `OpenApiSpec` struct (even a complete one) will either: +- Reject real-world specs when deserialization hits unexpected fields or 3.1 JSON Schema constructs +- Silently drop fields via `#[serde(flatten)]` catch-alls, losing data agents might need + +Instead: +- **Raw spec** is stored as exact bytes, parsed as `serde_json::Value` only when `show` or `schemas --show` needs full operation/schema details +- **Index** extracts only the fields needed for listing, filtering, searching, and linking back to raw via JSON pointers +- This makes the tool compatible with any valid OpenAPI 3.0.x or 3.1.x spec, including those with custom extensions + +**Cache metadata and management:** + +```rust +// src/core/cache.rs + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// Stored as meta.json in each alias directory +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CacheMetadata { + pub alias: String, + pub url: String, + pub fetched_at: DateTime<Utc>, + /// Updated on query commands for LRU eviction ordering. + /// Coalesced: written only when >10min stale to reduce write amplification. + pub last_accessed: DateTime<Utc>, + pub content_hash: String, + /// SHA256 hash of raw.json bytes. Commands that load raw.json (show, schemas --show) + /// validate this to detect raw file corruption. + pub raw_hash: String, + pub etag: Option<String>, + pub last_modified: Option<String>, + pub spec_version: String, + pub spec_title: String, + pub endpoint_count: usize, + pub schema_count: usize, + pub raw_size_bytes: u64, + /// Original input format: "json" | "yaml" + pub source_format: String, + pub index_version: u32, + /// Monotonically increasing; bumped on every successful fetch/sync commit. + pub generation: u64, + /// SHA256 hash of index.json bytes for integrity checking. + /// Readers validate this against the actual index.json to detect torn state. + pub index_hash: String, +} + +impl CacheMetadata { + pub fn is_stale(&self, threshold_days: u32) -> bool { + let age = Utc::now() - self.fetched_at; + age.num_days() > threshold_days as i64 + } +} + +/// Cache directory layout: +/// ~/.cache/swagger-cli/aliases/<alias>/ +/// ├── raw.source # Original upstream bytes as fetched (json, yaml, or gz — lossless provenance) +/// ├── raw.json # Canonical normalized JSON (always JSON; YAML normalized at ingest) +/// ├── index.json # Precomputed SpecIndex (small, fast) +/// ├── meta.json # CacheMetadata +/// └── .lock # File lock (held during writes) +/// +/// Write protocol (crash-consistent, multi-file): +/// 1. Acquire exclusive lock on .lock +/// - Lock acquisition is bounded (default <= 1000ms). On timeout: CACHE_LOCKED (exit 9). +/// - Prevents permanent stalls from dead processes holding locks. +/// 2. Compute content_hash + raw_hash + next generation number + index_hash +/// 3. Write raw.source.tmp + raw.json.tmp + index.json.tmp +/// - raw.source.tmp: original bytes as fetched (JSON or YAML) +/// - raw.json.tmp: canonical normalized JSON (YAML normalized at this step) +/// - MUST call sync_all() on each tmp file before rename (ensures durability) +/// 4. Rename raw.source.tmp → raw.source; raw.json.tmp → raw.json; index.json.tmp → index.json +/// - MUST call sync_all() on each file after rename +/// 5. Write meta.json.tmp LAST (acts as commit marker; includes generation + hashes) +/// - MUST call sync_all() on meta.json.tmp before rename +/// 6. Rename meta.json.tmp → meta.json +/// - MUST call sync_all() on meta.json after rename +/// 6b. Best-effort: fsync the alias directory fd on Unix after renames +/// (ensures directory entries are durable; no-op on platforms that don't support it) +/// 7. Release lock +/// +/// Read protocol: +/// - Read meta.json first (commit marker). If missing → alias incomplete/partial. +/// - Read index.json. Validate ALL THREE match meta.json: +/// 1. meta.index_version == index.index_version +/// 2. meta.generation == index.generation +/// 3. meta.index_hash == sha256(index.json bytes) +/// - If any mismatch → surface CACHE_INTEGRITY error (doctor --fix can rebuild from raw). +/// - `show` command: additionally reads raw.json as serde_json::Value +/// and validates meta.raw_hash == sha256(raw.json bytes) to detect raw corruption. +/// - Coalesce last_accessed updates: write only when current timestamp is >10min older than stored value. +/// This reduces write amplification and lock contention for hot-read bursts (best-effort, no lock required). +/// +/// Hash computation: +pub fn compute_hash(raw_bytes: &[u8]) -> String { + use sha2::{Sha256, Digest}; + let mut hasher = Sha256::new(); + hasher.update(raw_bytes); + format!("sha256:{:x}", hasher.finalize()) +} +``` + +**Configuration:** + +```rust +// src/core/config.rs + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub default_alias: Option<String>, + pub stale_threshold_days: u32, + pub auth_profiles: HashMap<String, AuthConfig>, + #[serde(default)] + pub display: DisplayConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthConfig { + pub url_pattern: String, + pub auth_type: AuthType, + pub credential: CredentialSource, +} + +/// Credential resolution: Literal for backward compat, EnvVar for CI/agent use, +/// Keyring for desktop environments (Phase 2). +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "source", rename_all = "snake_case")] +pub enum CredentialSource { + /// Inline token (backward-compatible; doctor warns on insecure perms) + Literal { token: String }, + /// Read token from environment variable at runtime + EnvVar { var_name: String }, + /// OS keychain lookup (Phase 2 — macOS Keychain, Linux Secret Service) + Keyring { service: String, account: String }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AuthType { + Bearer, + ApiKey { header: String }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct DisplayConfig { + pub color: bool, + pub unicode: bool, + pub max_width: Option<usize>, +} + +impl Config { + pub fn load() -> anyhow::Result<Self> { + let config_path = Self::config_path()?; + if config_path.exists() { + let content = std::fs::read_to_string(&config_path)?; + Ok(toml::from_str(&content)?) + } else { + Ok(Self::default()) + } + } + + pub fn save(&self) -> anyhow::Result<()> { + let config_path = Self::config_path()?; + let content = toml::to_string_pretty(self)?; + std::fs::write(&config_path, content)?; + Ok(()) + } + + /// Implements D7 override precedence: + /// 1. --config <path> / SWAGGER_CLI_CONFIG (handled at call site) + /// 2. SWAGGER_CLI_HOME (base dir; implies config/cache under it) + /// 3. XDG defaults via directories::ProjectDirs + pub fn config_path() -> anyhow::Result<PathBuf> { + if let Ok(home) = std::env::var("SWAGGER_CLI_HOME") { + return Ok(PathBuf::from(home).join("config").join("config.toml")); + } + let dirs = directories::ProjectDirs::from("", "", "swagger-cli") + .ok_or_else(|| anyhow::anyhow!("Cannot determine config directory"))?; + Ok(dirs.config_dir().join("config.toml")) + } + + /// Implements D7 override precedence: + /// 1. SWAGGER_CLI_CACHE (cache dir only; highest for cache) + /// 2. SWAGGER_CLI_HOME (base dir; implies cache under it) + /// 3. XDG defaults via directories::ProjectDirs + pub fn cache_dir() -> anyhow::Result<PathBuf> { + if let Ok(cache) = std::env::var("SWAGGER_CLI_CACHE") { + return Ok(PathBuf::from(cache)); + } + if let Ok(home) = std::env::var("SWAGGER_CLI_HOME") { + return Ok(PathBuf::from(home).join("cache")); + } + let dirs = directories::ProjectDirs::from("", "", "swagger-cli") + .ok_or_else(|| anyhow::anyhow!("Cannot determine cache directory"))?; + Ok(dirs.cache_dir().to_path_buf()) + } +} + +impl Default for Config { + fn default() -> Self { + Self { + default_alias: None, + stale_threshold_days: 30, + auth_profiles: HashMap::new(), + display: DisplayConfig::default(), + } + } +} +``` + +**Error types:** + +```rust +// src/errors.rs + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum SwaggerCliError { + #[error("Usage error: {0}")] + Usage(String), + + #[error("Cache locked for alias '{0}'")] + CacheLocked(String), + + #[error("Network error: {0}")] + Network(#[from] reqwest::Error), + + #[error("Invalid OpenAPI specification: {0}")] + InvalidSpec(String), + + #[error("Alias '{0}' not found")] + AliasNotFound(String), + + #[error("Alias '{0}' already exists")] + AliasExists(String), + + #[error("Cache error: {0}")] + Cache(String), + + #[error("Cache integrity error: {0}")] + CacheIntegrity(String), + + #[error("Config error: {0}")] + Config(String), + + #[error("Authentication failed: {0}")] + Auth(String), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + + #[error("Offline mode: {0}")] + OfflineMode(String), + + #[error("Policy blocked: {0}")] + PolicyBlocked(String), +} + +impl SwaggerCliError { + /// Convert to exit code + pub fn exit_code(&self) -> i32 { + match self { + Self::Usage(_) => 2, + Self::CacheLocked(_) => 9, + Self::Network(_) => 4, + Self::InvalidSpec(_) => 5, + Self::AliasExists(_) => 6, + Self::Auth(_) => 7, + Self::AliasNotFound(_) => 8, + Self::Cache(_) => 10, + Self::CacheIntegrity(_) => 14, + Self::Config(_) => 11, + Self::Io(_) => 12, + Self::Json(_) => 13, + Self::OfflineMode(_) => 15, + Self::PolicyBlocked(_) => 16, + } + } + + /// Error code for robot mode + pub fn code(&self) -> &'static str { + match self { + Self::Usage(_) => "USAGE_ERROR", + Self::CacheLocked(_) => "CACHE_LOCKED", + Self::Network(_) => "NETWORK_ERROR", + Self::InvalidSpec(_) => "INVALID_SPEC", + Self::AliasExists(_) => "ALIAS_EXISTS", + Self::Auth(_) => "AUTH_FAILED", + Self::AliasNotFound(_) => "ALIAS_NOT_FOUND", + Self::Cache(_) => "CACHE_ERROR", + Self::CacheIntegrity(_) => "CACHE_INTEGRITY", + Self::Config(_) => "CONFIG_ERROR", + Self::Io(_) => "IO_ERROR", + Self::Json(_) => "JSON_ERROR", + Self::OfflineMode(_) => "OFFLINE_MODE", + Self::PolicyBlocked(_) => "POLICY_BLOCKED", + } + } + + /// Suggestion for robot mode + pub fn suggestion(&self) -> Option<String> { + match self { + Self::Usage(_) => { + Some("Run: swagger-cli --help".to_string()) + } + Self::CacheLocked(alias) => { + Some(format!("Retry later; another process is updating '{alias}'")) + } + Self::AliasNotFound(alias) => { + Some(format!("Run: swagger-cli aliases --list")) + } + Self::AliasExists(alias) => { + Some(format!("Use --force to overwrite")) + } + Self::CacheIntegrity(_) => { + Some("Run: swagger-cli doctor --fix".to_string()) + } + Self::Auth(_) => { + Some("Check authentication credentials".to_string()) + } + Self::OfflineMode(_) => { + Some("Remove --network offline or set SWAGGER_CLI_NETWORK=auto".to_string()) + } + Self::PolicyBlocked(_) => { + Some("Use --allow-private-host <HOST> or --allow-insecure-http to bypass".to_string()) + } + _ => None, + } + } +} +``` + +### Search Engine + +**Index-backed text search with multi-term scoring:** + +```rust +// src/core/search.rs + +use crate::core::spec::{SpecIndex, IndexedEndpoint, IndexedSchema}; + +pub struct SearchEngine { + index: SpecIndex, +} + +#[derive(Debug, Clone)] +pub struct SearchResult { + pub result_type: SearchResultType, + /// Relevance score quantized to integer (basis points) for cross-platform determinism. + /// Computed internally as float, then quantized at output: (raw_score * 100.0).round() as u32 + pub score: u32, + /// 1-based rank in the result set (assigned after sorting). + pub rank: usize, + pub matches: Vec<Match>, +} + +#[derive(Debug, Clone)] +pub enum SearchResultType { + Endpoint { + path: String, + method: String, + summary: Option<String>, + description: Option<String>, + tags: Vec<String>, + operation_id: Option<String>, + }, + Schema { + name: String, + }, +} + +#[derive(Debug, Clone)] +pub struct Match { + pub field: String, + pub snippet: String, +} + +impl SearchEngine { + /// Search engine operates on the precomputed index, never on raw.json. + pub fn new(index: SpecIndex) -> Self { + Self { index } + } + + pub fn search(&self, query: &str, options: &SearchOptions) -> Vec<SearchResult> { + // Normalize query based on options + let normalized = if options.case_sensitive { + query.to_string() + } else { + query.to_lowercase() + }; + + // Tokenize unless exact phrase matching is requested + let terms = if options.exact { + vec![normalized.clone()] + } else { + tokenize(&normalized) + }; + + let mut results = Vec::new(); + + // Search endpoints + if options.search_paths || options.search_descriptions { + for endpoint in &self.index.endpoints { + if let Some(result) = self.match_endpoint(endpoint, &terms, options) { + results.push(result); + } + } + } + + // Search schemas + if options.search_schemas { + for schema in &self.index.schemas { + if let Some(result) = self.match_schema(&schema.name, &terms, options) { + results.push(result); + } + } + } + + // Sort by score descending with deterministic tie-breaking: + // (score DESC, type ordinal ASC, path/name ASC, method_rank ASC) + results.sort_by(|a, b| { + b.score.cmp(&a.score) + .then_with(|| a.result_type.type_ordinal().cmp(&b.result_type.type_ordinal())) + .then_with(|| a.result_type.sort_key().cmp(&b.result_type.sort_key())) + }); + + // Assign 1-based ranks after sorting + for (i, result) in results.iter_mut().enumerate() { + result.rank = i + 1; + } + + // Apply limit + if let Some(limit) = options.limit { + results.truncate(limit); + } + + results + } + + fn match_endpoint( + &self, + endpoint: &IndexedEndpoint, + terms: &[String], + options: &SearchOptions, + ) -> Option<SearchResult> { + let mut total_score = 0.0; + let mut matches = Vec::new(); + let mut terms_matched = 0; + + for term in terms { + let mut term_matched = false; + + // Match path (highest weight: 10) + let path_norm = self.normalize(&endpoint.path, options.case_sensitive); + if path_norm.contains(term.as_str()) { + total_score += 10.0; + term_matched = true; + matches.push(Match { + field: "path".to_string(), + snippet: safe_snippet(&endpoint.path, term, 50), + }); + } + + // Match summary (weight: 5) + if let Some(summary) = &endpoint.summary { + let summary_norm = self.normalize(summary, options.case_sensitive); + if summary_norm.contains(term.as_str()) { + total_score += 5.0; + term_matched = true; + matches.push(Match { + field: "summary".to_string(), + snippet: safe_snippet(summary, term, 50), + }); + } + } + + // Match description (weight: 2) + if let Some(description) = &endpoint.description { + let desc_norm = self.normalize(description, options.case_sensitive); + if desc_norm.contains(term.as_str()) { + total_score += 2.0; + term_matched = true; + matches.push(Match { + field: "description".to_string(), + snippet: safe_snippet(description, term, 50), + }); + } + } + + if term_matched { + terms_matched += 1; + } + } + + if total_score > 0.0 { + // Coverage boost: matching all terms scores higher + let coverage = terms_matched as f32 / terms.len() as f32; + total_score *= 1.0 + coverage; + + Some(SearchResult { + result_type: SearchResultType::Endpoint { + path: endpoint.path.clone(), + method: endpoint.method.clone(), + summary: endpoint.summary.clone(), + description: endpoint.description.clone(), + tags: endpoint.tags.clone(), + operation_id: endpoint.operation_id.clone(), + }, + score: total_score, + matches, + }) + } else { + None + } + } + + fn normalize(&self, text: &str, case_sensitive: bool) -> String { + if case_sensitive { text.to_string() } else { text.to_lowercase() } + } +} + +/// Tokenize query into whitespace-separated terms +fn tokenize(query: &str) -> Vec<String> { + query.split_whitespace() + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect() +} + +/// Create snippet with char-boundary-safe slicing (Unicode-safe). +/// Uses char_indices to find safe boundaries instead of byte offsets. +fn safe_snippet(text: &str, query: &str, context_chars: usize) -> String { + let text_lower = text.to_lowercase(); + let query_lower = query.to_lowercase(); + + if let Some(byte_pos) = text_lower.find(&query_lower) { + // Find char-safe boundaries + let char_positions: Vec<(usize, char)> = text.char_indices().collect(); + let match_char_idx = char_positions.iter() + .position(|(b, _)| *b >= byte_pos) + .unwrap_or(0); + + let start_char = match_char_idx.saturating_sub(context_chars); + let end_char = (match_char_idx + query.chars().count() + context_chars) + .min(char_positions.len()); + + let start_byte = char_positions[start_char].0; + let end_byte = if end_char >= char_positions.len() { + text.len() + } else { + char_positions[end_char].0 + }; + + let mut snippet = text[start_byte..end_byte].to_string(); + + if start_char > 0 { + snippet = format!("...{}", snippet); + } + if end_char < char_positions.len() { + snippet = format!("{}...", snippet); + } + + snippet + } else { + text.chars().take(100).collect() + } +} + +pub struct SearchOptions { + pub search_paths: bool, + pub search_descriptions: bool, + pub search_schemas: bool, + pub case_sensitive: bool, + pub exact: bool, + pub limit: Option<usize>, +} + +impl Default for SearchOptions { + fn default() -> Self { + Self { + search_paths: true, + search_descriptions: true, + search_schemas: true, + case_sensitive: false, + exact: false, + limit: Some(20), + } + } +} +``` + +### CLI Implementation + +**Main entry point:** + +```rust +// src/main.rs + +use clap::Parser; +use swagger_cli::{cli, errors::SwaggerCliError}; + +fn main() { + // Pre-scan argv so parse/usage errors can still honor --robot. + // This prevents "sometimes JSON, sometimes clap text" output for agents. + let argv: Vec<String> = std::env::args().collect(); + let robot_requested = argv.iter().any(|a| a == "--robot"); + + let cli = match cli::Cli::try_parse_from(&argv) { + Ok(v) => v, + Err(e) => { + if robot_requested { + let json = serde_json::json!({ + "ok": false, + "error": { + "code": "USAGE_ERROR", + "message": e.to_string(), + "suggestion": "Run: swagger-cli --help" + }, + "meta": { "schema_version": 1, "command": "parse" } + }); + eprintln!("{}", serde_json::to_string(&json).unwrap()); + std::process::exit(2); + } + e.exit(); + } + }; + + // Robot mode is explicit only -- never auto-enabled by TTY detection. + // TTY detection is used only for color/unicode formatting decisions. + let robot_mode = cli.robot; + + // Execute command + let result = match cli.command { + cli::Commands::Fetch(args) => cli::fetch::execute(args, robot_mode), + cli::Commands::List(args) => cli::list::execute(args, robot_mode), + cli::Commands::Show(args) => cli::show::execute(args, robot_mode), + cli::Commands::Search(args) => cli::search::execute(args, robot_mode), + cli::Commands::Schemas(args) => cli::schemas::execute(args, robot_mode), + cli::Commands::Tags(args) => cli::tags::execute(args, robot_mode), + cli::Commands::Aliases(args) => cli::aliases::execute(args, robot_mode), + cli::Commands::Sync(args) => cli::sync::execute(args, robot_mode), + cli::Commands::Doctor(args) => cli::doctor::execute(args, robot_mode), + cli::Commands::Cache(args) => cli::cache::execute(args, robot_mode), + cli::Commands::Diff(args) => cli::diff::execute(args, robot_mode), + }; + + // Handle result + match result { + Ok(()) => std::process::exit(0), + Err(e) => { + if robot_mode { + output_robot_error(&e); + } else { + output_human_error(&e); + } + std::process::exit(e.exit_code()); + } + } +} + +fn output_robot_error(error: &SwaggerCliError) { + let json = serde_json::json!({ + "ok": false, + "error": { + "code": error.code(), + "message": error.to_string(), + "suggestion": error.suggestion(), + }, + "meta": { + "schema_version": 1, + "tool_version": env!("CARGO_PKG_VERSION"), + "command": "error", + "duration_ms": 0 + } + }); + eprintln!("{}", serde_json::to_string(&json).unwrap()); +} + +fn output_human_error(error: &SwaggerCliError) { + use colored::Colorize; + eprintln!("{} {}", "Error:".red().bold(), error); + if let Some(suggestion) = error.suggestion() { + eprintln!("{} {}", "Suggestion:".yellow(), suggestion); + } +} +``` + +**CLI structure:** + +```rust +// src/cli/mod.rs + +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +#[command(name = "swagger-cli")] +#[command(about = "Fast OpenAPI specification CLI tool", long_about = None)] +#[command(version)] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, + + /// Output JSON for machine parsing + #[arg(long, global = true)] + pub robot: bool, + + /// Pretty-print output (human debugging). If combined with --robot, prints pretty JSON. + #[arg(long, global = true)] + pub pretty: bool, + + /// Network policy: auto (default), offline, online-only + #[arg(long, global = true, default_value = "auto", value_parser = ["auto", "offline", "online-only"])] + pub network: String, + + /// Path to config file + #[arg(long, global = true, env = "SWAGGER_CLI_CONFIG")] + pub config: Option<std::path::PathBuf>, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Fetch and cache OpenAPI spec + Fetch(fetch::FetchArgs), + + /// List endpoints + List(list::ListArgs), + + /// Show endpoint details + Show(show::ShowArgs), + + /// Search across spec + Search(search::SearchArgs), + + /// Browse schemas + Schemas(schemas::SchemasArgs), + + /// List tags + Tags(tags::TagsArgs), + + /// Manage aliases + Aliases(aliases::AliasesArgs), + + /// Sync specs + Sync(sync::SyncArgs), + + /// Health check + Doctor(doctor::DoctorArgs), + + /// Cache lifecycle management + Cache(cache::CacheArgs), + + /// Compare two spec states (Phase 2) + Diff(diff::DiffArgs), +} + +pub mod fetch; +pub mod list; +pub mod show; +pub mod search; +pub mod schemas; +pub mod tags; +pub mod aliases; +pub mod sync; +pub mod doctor; +pub mod cache; +pub mod diff; +``` + +**Example command implementation (index-backed):** + +```rust +// src/cli/list.rs + +use clap::Args; +use crate::{ + core::{cache::CacheManager, spec::{SpecIndex, IndexedEndpoint}}, + errors::SwaggerCliError, + output::{human, robot}, +}; + +#[derive(Args)] +pub struct ListArgs { + /// Alias to query (uses default if not specified) + alias: Option<String>, + + /// Filter by HTTP method + #[arg(long, value_parser = ["GET", "POST", "PUT", "DELETE", "PATCH"])] + method: Option<String>, + + /// Filter by tag + #[arg(long)] + tag: Option<String>, + + /// Filter by path pattern (regex) + #[arg(long)] + path: Option<String>, + + /// Sort by field + #[arg(long, default_value = "path", value_parser = ["path", "method", "tag"])] + sort: String, + + /// Limit results + #[arg(long, default_value = "50")] + limit: usize, + + /// Show all results (no limit) + #[arg(long)] + all: bool, +} + +pub fn execute(args: ListArgs, robot_mode: bool) -> Result<(), SwaggerCliError> { + let cache = CacheManager::new()?; + let alias = args.alias.or_else(|| cache.default_alias()) + .ok_or_else(|| SwaggerCliError::Config("No alias specified and no default set".into()))?; + + // Load index only -- never touches raw.json + let (index, meta) = cache.load_index(&alias)?; + + // Build filters + let filters = Filters { + method: args.method, + tag: args.tag, + path_pattern: args.path, + }; + + // Filter directly from index endpoints (already structured; fails fast on invalid regex) + let mut endpoints = filter_endpoints(&index.endpoints, &filters)?; + + // Sort + sort_endpoints(&mut endpoints, &args.sort); + + // Apply limit + let total = endpoints.len(); + if !args.all { + endpoints.truncate(args.limit); + } + + // Output + if robot_mode { + robot::output_list(&alias, &meta, &endpoints, total)?; + } else { + human::output_list(&alias, &meta, &endpoints, total)?; + } + + Ok(()) +} + +struct Filters { + method: Option<String>, + tag: Option<String>, + path_pattern: Option<String>, +} + +fn filter_endpoints(endpoints: &[IndexedEndpoint], filters: &Filters) -> Result<Vec<IndexedEndpoint>, SwaggerCliError> { + let path_regex = match &filters.path_pattern { + Some(p) => Some(regex::Regex::new(p).map_err(|e| { + SwaggerCliError::Usage(format!("Invalid --path regex '{}': {}", p, e)) + })?), + None => None, + }; + + endpoints.iter() + .filter(|ep| { + // Filter by path pattern + if let Some(regex) = &path_regex { + if !regex.is_match(&ep.path) { + return false; + } + } + + // Filter by method + if let Some(filter_method) = &filters.method { + if ep.method.to_uppercase() != filter_method.to_uppercase() { + return false; + } + } + + // Filter by tag + if let Some(filter_tag) = &filters.tag { + if !ep.tags.contains(filter_tag) { + return false; + } + } + + true + }) + .cloned() + .collect()) +} + +/// Canonical HTTP method ranking for deterministic ordering. +/// GET=0, POST=1, PUT=2, PATCH=3, DELETE=4, OPTIONS=5, HEAD=6, TRACE=7. +/// Unknown methods sort last (99), then lexicographically. +fn method_rank(method: &str) -> u8 { + match method { + "GET" => 0, "POST" => 1, "PUT" => 2, "PATCH" => 3, + "DELETE" => 4, "OPTIONS" => 5, "HEAD" => 6, "TRACE" => 7, + _ => 99, + } +} + +fn sort_endpoints(endpoints: &mut [IndexedEndpoint], sort_by: &str) { + match sort_by { + "method" => endpoints.sort_by(|a, b| { + method_rank(&a.method).cmp(&method_rank(&b.method)) + .then_with(|| a.path.cmp(&b.path)) + .then_with(|| a.operation_id.cmp(&b.operation_id)) + }), + "tag" => endpoints.sort_by(|a, b| { + let a_tag = a.tags.first().map(|s| s.as_str()).unwrap_or(""); + let b_tag = b.tags.first().map(|s| s.as_str()).unwrap_or(""); + a_tag.cmp(b_tag) + .then_with(|| a.path.cmp(&b.path)) + .then_with(|| method_rank(&a.method).cmp(&method_rank(&b.method))) + }), + _ => endpoints.sort_by(|a, b| { + a.path.cmp(&b.path) + .then_with(|| method_rank(&a.method).cmp(&method_rank(&b.method))) + }), + } +} +``` + +--- + +## Testing Strategy + +### Unit Tests + +**Coverage targets:** +- Core logic: 90%+ +- CLI commands: 80%+ +- Error handling: 100% + +**Example tests:** + +```rust +// tests/core/cache_test.rs + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_cache_save_and_load() { + let temp = TempDir::new().unwrap(); + let cache = CacheManager::new_with_path(temp.path()).unwrap(); + + let spec_value = create_test_spec_value(); + let index = build_index_from_value(&spec_value).unwrap(); + let cached = CachedSpec { + alias: "test".to_string(), + url: "https://example.com/spec.json".to_string(), + spec, + metadata: CacheMetadata { + fetched_at: Utc::now(), + content_hash: "abc123".to_string(), + etag: None, + spec_version: "1.0.0".to_string(), + endpoint_count: 5, + schema_count: 3, + }, + }; + + cache.save(&cached).unwrap(); + + let loaded = cache.load("test").unwrap(); + assert_eq!(loaded.alias, "test"); + assert_eq!(loaded.url, "https://example.com/spec.json"); + } + + #[test] + fn test_cache_not_found() { + let temp = TempDir::new().unwrap(); + let cache = CacheManager::new_with_path(temp.path()).unwrap(); + + let result = cache.load("nonexistent"); + assert!(matches!(result, Err(SwaggerCliError::AliasNotFound(_)))); + } + + #[test] + fn test_is_stale() { + let mut cached = create_test_cached_spec(); + + // Fresh + assert!(!cached.is_stale(30)); + + // Old + cached.metadata.fetched_at = Utc::now() - chrono::Duration::days(45); + assert!(cached.is_stale(30)); + } + + fn create_test_spec_value() -> serde_json::Value { + // Minimal valid OpenAPI as Value (tolerant parsing architecture) + serde_json::json!({ + "openapi": "3.0.0", + "info": { "title": "Test API", "version": "1.0.0" }, + "paths": {} + }) + } +} +``` + +### Integration Tests + +**Test scenarios:** + +```rust +// tests/integration/fetch_test.rs + +use assert_cmd::Command; +use predicates::prelude::*; +use mockito::mock; + +#[test] +fn test_fetch_success() { + let _m = mock("GET", "/openapi.json") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(include_str!("../fixtures/petstore.json")) + .create(); + + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["fetch", &format!("{}/openapi.json", mockito::server_url()), "--alias", "test", "--robot"]) + .assert() + .success() + .stdout(predicate::str::contains(r#""ok":true"#)); +} + +#[test] +fn test_fetch_invalid_json() { + let _m = mock("GET", "/openapi.json") + .with_status(200) + .with_body("not json") + .create(); + + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["fetch", &mockito::server_url(), "--alias", "test", "--robot"]) + .assert() + .failure() + .code(5) // INVALID_SPEC + .stderr(predicate::str::contains("INVALID_SPEC")); +} + +#[test] +fn test_fetch_network_error() { + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["fetch", "http://localhost:1", "--alias", "test", "--robot"]) + .assert() + .failure() + .code(4) // NETWORK_ERROR + .stderr(predicate::str::contains("NETWORK_ERROR")); +} + +#[test] +fn test_alias_exists() { + let temp = TempDir::new().unwrap(); + std::env::set_var("SWAGGER_CLI_HOME", temp.path()); + + let fixture = std::fs::canonicalize("tests/fixtures/petstore.json").unwrap(); + let fixture_str = fixture.to_str().unwrap(); + + // First fetch (uses local fixture — no network) + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["fetch", fixture_str, "--alias", "test"]) + .assert() + .success(); + + // Second fetch without --force + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["fetch", fixture_str, "--alias", "test", "--robot"]) + .assert() + .failure() + .code(6) // ALIAS_EXISTS + .stderr(predicate::str::contains("ALIAS_EXISTS")) + .stderr(predicate::str::contains("--force")); +} +``` + +```rust +// tests/integration/list_test.rs + +#[test] +fn test_list_all_endpoints() { + setup_test_cache("petstore"); + + let output = Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["list", "petstore", "--robot"]) + .output() + .unwrap(); + + assert!(output.status.success()); + // Parse stdout as JSON rather than string predicates (more robust, catches shape issues) + let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(json["ok"], true); + assert!(json["data"]["endpoints"].is_array()); + assert!(json["meta"]["schema_version"].is_number()); +} + +#[test] +fn test_list_filter_by_method() { + setup_test_cache("petstore"); + + let output = Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["list", "petstore", "--method", "POST", "--robot"]) + .output() + .unwrap(); + + let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + let endpoints = json["data"]["endpoints"].as_array().unwrap(); + + for endpoint in endpoints { + assert_eq!(endpoint["method"], "POST"); + } +} + +#[test] +fn test_list_filter_by_tag() { + setup_test_cache("petstore"); + + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["list", "petstore", "--tag", "pets", "--robot"]) + .assert() + .success() + .stdout(predicate::str::contains(r#""tags":["pets"]"#)); +} + +fn setup_test_cache(alias: &str) { + // Helper to set up test cache with fixtures. + // All tests MUST run under SWAGGER_CLI_HOME for hermetic behavior. + // Uses canonicalize() for absolute paths — file:// requires absolute per RFC 8089. + let path = std::fs::canonicalize(format!("tests/fixtures/{alias}.json")) + .expect("fixture file must exist"); + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["fetch", path.to_str().unwrap(), "--alias", alias]) + .assert() + .success(); +} + +/// Invariant test: list and search MUST NOT read raw.json. +/// This validates the core performance promise of index-backed queries. +#[test] +fn test_list_does_not_read_raw_json() { + let temp = TempDir::new().unwrap(); + std::env::set_var("SWAGGER_CLI_HOME", temp.path()); + setup_test_cache("petstore"); + + // Remove raw.json after cache setup -- list should still work from index alone + let raw_path = temp.path().join("cache/aliases/petstore/raw.json"); + std::fs::remove_file(&raw_path).unwrap(); + + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["list", "petstore", "--robot"]) + .assert() + .success(); // Must succeed without raw.json +} + +#[test] +fn test_search_does_not_read_raw_json() { + let temp = TempDir::new().unwrap(); + std::env::set_var("SWAGGER_CLI_HOME", temp.path()); + setup_test_cache("petstore"); + + let raw_path = temp.path().join("cache/aliases/petstore/raw.json"); + std::fs::remove_file(&raw_path).unwrap(); + + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["search", "petstore", "pet", "--robot"]) + .assert() + .success(); // Must succeed without raw.json +} +``` + +### End-to-End Tests + +**Workflow tests:** + +```rust +// tests/integration/workflow_test.rs + +#[test] +fn test_complete_workflow() { + let temp = TempDir::new().unwrap(); + std::env::set_var("SWAGGER_CLI_HOME", temp.path()); + + // 1. Fetch spec (uses absolute path — no network, no invalid file:// URL) + let fixture = std::fs::canonicalize("tests/fixtures/petstore.json").unwrap(); + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["fetch", fixture.to_str().unwrap(), "--alias", "pet"]) + .assert() + .success(); + + // 2. List endpoints + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["list", "pet"]) + .assert() + .success() + .stdout(predicate::str::contains("/pets")); + + // 3. Show specific endpoint + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["show", "pet", "/pets", "--method", "GET"]) + .assert() + .success() + .stdout(predicate::str::contains("List all pets")); + + // 4. Search + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["search", "pet", "create"]) + .assert() + .success(); + + // 5. Check aliases + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["aliases"]) + .assert() + .success() + .stdout(predicate::str::contains("pet")); + + // 6. Health check + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["doctor"]) + .assert() + .success() + .stdout(predicate::str::contains("HEALTHY")); +} +``` + +### Reliability Stress Tests + +**Purpose:** Validate crash-consistency, lock behavior, and determinism claims under adversarial conditions. + +**Fault injection tests:** +```rust +// tests/reliability/crash_consistency_test.rs + +/// Simulate crash at each write step to prove recoverability. +/// For each step in the write protocol (before/after fsync, before/after rename), +/// interrupt the write and verify: +/// - Partial state is detected by the read protocol +/// - doctor --fix can recover from any interrupted state +/// - No data corruption or silent degradation +#[test] +fn test_crash_before_meta_rename() { + // Write raw.json + index.json successfully, but don't write meta.json + // Verify: read protocol detects missing meta → CACHE_INTEGRITY + // Verify: doctor --fix rebuilds from raw +} + +#[test] +fn test_crash_after_raw_before_index() { + // Write raw.json but crash before index.json + // Verify: meta.json (from previous generation) still valid + // Verify: doctor --fix detects stale index and rebuilds +} +``` + +**Multi-process lock contention tests:** +```rust +// tests/reliability/lock_contention_test.rs + +/// Spawn N concurrent processes (N>=32) all attempting to fetch/sync the same alias. +/// Verify: +/// - Lock timeout is bounded (no deadlocks) +/// - Exactly one process succeeds per generation +/// - Failed processes get CACHE_LOCKED with actionable suggestion +/// - Final cache state is consistent (generation matches, hashes valid) +#[test] +fn test_concurrent_fetch_32_processes() { + let temp = TempDir::new().unwrap(); + let handles: Vec<_> = (0..32).map(|_| { + std::thread::spawn(move || { + Command::cargo_bin("swagger-cli") + .unwrap() + .args(&["fetch", fixture_path, "--alias", "contended", "--force", "--robot"]) + .env("SWAGGER_CLI_HOME", temp.path()) + .output() + .unwrap() + }) + }).collect(); + + // Verify: all exit 0 or 9 (CACHE_LOCKED), no panics, no corruption + // Verify: final cache state passes doctor validation +} +``` + +**Property-based tests:** +```rust +// tests/reliability/property_test.rs + +/// Use proptest to verify: +/// - Index ordering is deterministic regardless of input order +/// - Search tie-breaking is stable across runs +/// - All generated pointers resolve in raw.json +/// - Content hash is deterministic for same input bytes +#[cfg(test)] +mod prop_tests { + use proptest::prelude::*; + + proptest! { + #[test] + fn index_ordering_deterministic(endpoints in arb_endpoints(1..100)) { + let index1 = build_index(endpoints.clone()); + let index2 = build_index(endpoints.into_iter().rev().collect()); + assert_eq!( + serde_json::to_string(&index1).unwrap(), + serde_json::to_string(&index2).unwrap() + ); + } + } +} +``` + +### Performance Tests + +```rust +// benches/perf.rs (Criterion convention: benches/ directory) + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +fn bench_load_index(c: &mut Criterion) { + let index_path = setup_large_spec_index(); // 500+ endpoints + + c.bench_function("load_index_json", |b| { + b.iter(|| { + let bytes = std::fs::read(black_box(&index_path)).unwrap(); + let _index: SpecIndex = serde_json::from_slice(&bytes).unwrap(); + }); + }); +} + +fn bench_list_endpoints(c: &mut Criterion) { + let index = load_large_spec_index(); // 500+ endpoints + + c.bench_function("list_all_endpoints", |b| { + b.iter(|| { + filter_endpoints(black_box(&index.endpoints), &Filters::default()) + }); + }); +} + +fn bench_search(c: &mut Criterion) { + let index = load_large_spec_index(); + let search_engine = SearchEngine::new(index); + + c.bench_function("search_query", |b| { + b.iter(|| { + search_engine.search(black_box("create user"), &Default::default()) + }); + }); +} + +criterion_group!(benches, bench_load_index, bench_list_endpoints, bench_search); +criterion_main!(benches); +``` + +**Performance targets:** +- Index load: <5ms (index.json is typically 10-50KB) +- List 500 endpoints: <50ms (filter + sort on in-memory index) +- Search 500 endpoints: <100ms (tokenized multi-term scoring) +- Fetch + cache 2MB spec: <2s (includes index build) +- `show` command: <100ms (loads raw.json as Value, extracts subtree via JSON pointer) + +### Golden Robot Output Tests + +**Purpose:** The #1 regression risk for agent tooling is "robot JSON changed shape". Golden tests prevent this. + +```rust +// tests/integration/golden_test.rs + +#[test] +fn test_robot_output_schema_stability() { + // For each command, verify robot JSON matches golden snapshot + let commands = vec![ + ("list", vec!["list", "petstore", "--robot"]), + ("show", vec!["show", "petstore", "/pet/{petId}", "--robot"]), + ("search", vec!["search", "petstore", "pet", "--robot"]), + ("schemas", vec!["schemas", "petstore", "--robot"]), + ("tags", vec!["tags", "petstore", "--robot"]), + ("aliases", vec!["aliases", "--robot"]), + ]; + + for (name, args) in commands { + let output = run_command(&args); + let json: serde_json::Value = serde_json::from_str(&output).unwrap(); + + // Verify structural invariants + assert!(json["ok"].is_boolean(), "{name}: missing ok field"); + assert!(json["data"].is_object() || json["data"].is_null(), "{name}: bad data"); + assert!(json["meta"]["schema_version"].is_number(), "{name}: missing schema_version"); + assert!(json["meta"]["tool_version"].is_string(), "{name}: missing tool_version"); + assert!(json["meta"]["command"].is_string(), "{name}: missing command"); + assert!(json["meta"]["duration_ms"].is_number(), "{name}: missing duration_ms"); + + // Snapshot test: compare against golden file + // Fail CI on breaking changes unless schema_version is incremented + let golden_path = format!("tests/integration/golden/{name}_success.json"); + assert_schema_compatible(&json, &golden_path); + } +} +``` + +--- + +## Distribution and Deployment + +### GitLab CI/CD + +```yaml +# .gitlab-ci.yml + +stages: + - test + - build + - release + +variables: + CARGO_HOME: $CI_PROJECT_DIR/.cargo + +cache: + key: $CI_COMMIT_REF_SLUG + paths: + - .cargo/ + - target/ + +# Test stage +test:unit: + stage: test + image: rust:1.93 + script: + - cargo test --lib + - cargo test --doc + coverage: '/^\d+\.\d+% coverage/' + +test:integration: + stage: test + image: rust:1.93 + script: + - cargo test --test '*' + +lint: + stage: test + image: rust:1.93 + script: + - rustup component add clippy rustfmt + - cargo fmt -- --check + - cargo clippy -- -D warnings + +security:deps: + stage: test + image: rust:1.93 + script: + - cargo install cargo-deny cargo-audit + - cargo deny check + - cargo audit + allow_failure: false + +# Build stage +.build_template: &build_template + stage: build + image: rust:1.93 + script: + - cargo build --release --locked --target $TARGET + - cp target/$TARGET/release/swagger-cli swagger-cli-$TARGET + artifacts: + paths: + - swagger-cli-$TARGET + expire_in: 30 days + +build:macos-arm64: + <<: *build_template + tags: + - macos + - arm64 + variables: + TARGET: aarch64-apple-darwin + +build:macos-x86_64: + <<: *build_template + tags: + - macos + - x86_64 + variables: + TARGET: x86_64-apple-darwin + +build:linux-x86_64: + <<: *build_template + image: rust:1.93 + variables: + TARGET: x86_64-unknown-linux-gnu + +build:linux-arm64: + <<: *build_template + image: rust:1.93 + before_script: + - apt-get update && apt-get install -y gcc-aarch64-linux-gnu + - rustup target add aarch64-unknown-linux-gnu + variables: + TARGET: aarch64-unknown-linux-gnu + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + +# Release stage (only on tags) +release: + stage: release + image: curlimages/curl:latest + only: + - tags + dependencies: + - build:macos-arm64 + - build:macos-x86_64 + - build:linux-x86_64 + - build:linux-arm64 + script: + - | + # Generate checksums and sign + sha256sum swagger-cli-* > SHA256SUMS + minisign -Sm SHA256SUMS -s /run/secrets/minisign_key + # Upload binaries + integrity artifacts + for file in swagger-cli-* SHA256SUMS SHA256SUMS.minisig; do + curl --header "JOB-TOKEN: $CI_JOB_TOKEN" \ + --upload-file $file \ + "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/swagger-cli/${CI_COMMIT_TAG}/$file" + done + +# Docker image +docker: + stage: release + image: docker:latest + services: + - docker:dind + only: + - tags + script: + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG . + - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG $CI_REGISTRY_IMAGE:latest + - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG + - docker push $CI_REGISTRY_IMAGE:latest +``` + +### Dockerfile + +```dockerfile +# Multi-stage build for minimal image +FROM rust:1.93-alpine as builder + +WORKDIR /build + +# Install build dependencies +RUN apk add --no-cache musl-dev + +# Copy source +COPY Cargo.toml Cargo.lock ./ +COPY src ./src + +# Build release +RUN cargo build --release --locked --target x86_64-unknown-linux-musl + +# Runtime image +FROM alpine:latest + +RUN apk add --no-cache ca-certificates + +COPY --from=builder /build/target/x86_64-unknown-linux-musl/release/swagger-cli /usr/local/bin/ + +# Create XDG-compliant directories +RUN mkdir -p /root/.config/swagger-cli /root/.cache/swagger-cli/aliases + +ENTRYPOINT ["swagger-cli"] +``` + +### Installation Script + +```bash +#!/bin/bash +# install.sh - Universal installer + +set -euo pipefail + +# Secure temp directory with cleanup trap +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +# Detect OS and architecture +OS="$(uname -s)" +ARCH="$(uname -m)" + +case "$OS" in + Darwin) + OS_LOWER="macos" + ;; + Linux) + OS_LOWER="linux" + ;; + *) + echo "Unsupported OS: $OS" + exit 1 + ;; +esac + +case "$ARCH" in + arm64|aarch64) + ARCH_LOWER="arm64" + TARGET="aarch64" + ;; + x86_64|amd64) + ARCH_LOWER="x86_64" + TARGET="x86_64" + ;; + *) + echo "Unsupported architecture: $ARCH" + exit 1 + ;; +esac + +VERSION="${VERSION:-latest}" +INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" + +BINARY_NAME="swagger-cli-${TARGET}-${OS_LOWER}" +# TODO: Replace with actual GitLab Package Registry URL (matches .gitlab-ci.yml release target) +DOWNLOAD_URL="https://<your-gitlab-host>/api/v4/projects/<id>/packages/generic/swagger-cli/${VERSION}/${BINARY_NAME}" + +echo "Installing swagger-cli..." +echo " Version: $VERSION" +echo " Platform: $OS_LOWER-$ARCH_LOWER" +echo " Install dir: $INSTALL_DIR" + +# Create install directory +mkdir -p "$INSTALL_DIR" + +# Download binary + integrity artifacts +echo "Downloading..." +curl -sSL "$DOWNLOAD_URL" -o "$INSTALL_DIR/swagger-cli" + +VERIFY="${VERIFY:-true}" +if [ "$VERIFY" = "true" ]; then + CHECKSUMS_URL="${DOWNLOAD_URL%/*}/SHA256SUMS" + SIGNATURE_URL="${DOWNLOAD_URL%/*}/SHA256SUMS.minisig" + curl -sSL "$CHECKSUMS_URL" -o "$TMP_DIR/SHA256SUMS" + curl -sSL "$SIGNATURE_URL" -o "$TMP_DIR/SHA256SUMS.minisig" + + # Verify signature if minisign is available + if command -v minisign >/dev/null 2>&1; then + echo "Verifying signature..." + minisign -Vm "$TMP_DIR/SHA256SUMS" -p "$MINISIGN_PUBKEY" || { + echo "ERROR: Signature verification failed. Aborting." + rm -f "$INSTALL_DIR/swagger-cli" + exit 1 + } + else + echo "Note: minisign not found, skipping signature verification (checksum only)" + fi + + # Verify checksum (portable: sha256sum on Linux, shasum on macOS) + echo "Verifying checksum..." + EXPECTED=$(grep "$BINARY_NAME" "$TMP_DIR/SHA256SUMS" | awk '{print $1}') + if command -v sha256sum >/dev/null 2>&1; then + ACTUAL=$(sha256sum "$INSTALL_DIR/swagger-cli" | awk '{print $1}') + else + ACTUAL=$(shasum -a 256 "$INSTALL_DIR/swagger-cli" | awk '{print $1}') + fi + if [ "$EXPECTED" != "$ACTUAL" ]; then + echo "ERROR: Checksum mismatch. Expected $EXPECTED, got $ACTUAL" + rm -f "$INSTALL_DIR/swagger-cli" + exit 1 + fi + echo "Integrity verified." +fi + +# Make executable +chmod +x "$INSTALL_DIR/swagger-cli" + +echo "✓ Installed to $INSTALL_DIR/swagger-cli" + +# Check if in PATH +if ! echo "$PATH" | grep -q "$INSTALL_DIR"; then + echo "" + echo "Warning: $INSTALL_DIR is not in your PATH" + echo "Add this to your shell profile:" + echo " export PATH=\"\$PATH:$INSTALL_DIR\"" +fi + +echo "" +echo "Get started:" +echo " swagger-cli fetch https://api.example.com/openapi.json --alias myapi" +echo " swagger-cli list myapi" +``` + +--- + +## Success Metrics + +### Phase 1 (MVP) - Week 1 (CP0-CP2) + +**Must have:** +- ✓ Fetch and cache specs (four-file format: raw.source/raw.json/index/meta, with YAML normalization) +- ✓ True crash-consistent writes with fsync, per-alias file locking, bounded lock timeout (meta.json as commit marker) +- ✓ List endpoints with basic filtering (index-backed) +- ✓ Show endpoint details (index pointer + raw Value extraction) +- ✓ Robot mode JSON output with versioned schema contract (unified envelope for success AND errors) +- ✓ Exit codes and error handling (with Usage and CacheIntegrity variants) +- ✓ Strict option validation (invalid regex/options fail with USAGE_ERROR, no silent fallbacks) +- ✓ 80%+ test coverage + +**Success criteria:** +- Query latency <50ms (cached, index-backed) -- validated against 8MB+ spec +- Fetch time <2s (2MB spec) +- Works with 3 real-world specs (Petstore, Stripe, GitHub) +- `list` and `search` never load raw.json (verified by removal test: delete raw.json, commands still work) +- Robot JSON includes schema_version, tool_version, command, duration_ms (on BOTH success and error) +- Index build is deterministic (sorted endpoints/schemas/tags with canonical method ordering; verified by golden tests) +- Cache writes are crash-consistent (fsync before renames, generation embedded in index.json, validated on read) +- All integration tests hermetic (SWAGGER_CLI_HOME set, no real network calls, absolute fixture paths) +- Crash-consistency claim validated by automated fault-injection test suite (not only unit tests) +- YAML input accepted and normalized to JSON during fetch (validated with YAML fixture) +- Alias format validated (rejects path traversal, reserved names, shell-hostile characters) +- SSRF policy enforced by default (loopback/private/link-local blocked; HTTPS required for remote URLs) + +### Phase 2 (Polish) - Week 2 (CP3-CP5) + +**Should have:** +- ✓ Text search (tokenized multi-term, Unicode-safe) +- ✓ Schema browsing (index-backed listing, raw-backed details) +- ✓ Tag browser (index-backed) +- ✓ Alias management +- ✓ Cross-alias discovery (`--all-aliases` on list and search) +- ✓ Sync with change detection (index-based diffs) + optional `--details` with capped change lists +- ✓ Sync concurrency (`--jobs`, `--per-host`, Retry-After handling) +- ✓ Health check (integrity validation via generation/hash, pointer validation, permission checks, `--fix` with locking) +- ✓ Cache lifecycle management (stats, prune, size caps) +- ✓ Global network policy (`--network offline/auto/online-only`) +- ✓ CI/CD pipeline with supply chain hardening (SHA256SUMS + minisign + cargo-deny/cargo-audit) +- ✓ Multi-platform binaries +- ✓ Golden robot output tests validated against published JSON Schema artifacts +- ✓ Reliability stress tests (fault injection, lock contention, property-based) +- ✓ Diff command with structural comparison for CI gates (FR-11) + +**Success criteria:** +- Search 500 endpoints <100ms +- All commands have robot mode with stable schema +- Installable via curl script from correct hosting URL +- Golden tests prevent robot JSON shape regressions +- Sync `--details --robot` provides actionable added/removed/modified lists (capped, with truncated flag) +- Doctor detects partial caches + warns on insecure config permissions +- `--all-aliases` works for list and search (verified with 3+ aliases) +- `cache --stats` and `cache --prune-stale` operational +- `sync --all --jobs 4` completes faster than sequential (verified with 4+ aliases) +- Release artifacts include SHA256SUMS + minisign signatures +- `cargo deny check` and `cargo audit` pass in CI (dependency policy enforcement) +- `--network offline` blocks all network commands with OFFLINE_MODE error +- SSRF policy blocks loopback/private ranges by default; `--allow-private-host` permits exceptions +- Lock contention tests pass with 32 concurrent processes +- `diff` command reports structural changes between two alias states + +### Phase 3 (Advanced) - Future + +**Nice to have:** +- YAML output format (`--format yaml` with serde_yaml, determinism rules, golden fixtures) +- Semantic search with embeddings +- Diff breaking-change classification (heuristic-based: removed field = breaking, added optional = non-breaking, type change = breaking) +- Generate curl commands from endpoints +- Schema validation (validate JSON against schema) +- Import/export aliases +- Compressed cache storage +- OS keychain credential backend (`CredentialSource::Keyring` — macOS Keychain, Linux Secret Service) +- SBOM generation + cosign attestation for release artifacts +- Web UI (local server) + +--- + +## Decision Log + +### D1: Rust vs Go vs TypeScript + +**Decision:** Rust + +**Rationale:** +- **Performance:** Faster than Go/TS for parsing large specs +- **Type safety:** Stronger than TS, excellent serde ecosystem +- **Distribution:** Single binary, no runtime required +- **Ecosystem:** Rich CLI tooling (clap, colored, tabled) + +**Alternatives considered:** +- Go: Easier concurrency, but slower JSON parsing +- TypeScript: Easier for team, but requires Node.js runtime + +### D2: SQLite vs JSON Cache + +**Decision:** JSON files + +**Rationale:** +- **Simplicity:** No schema migrations, easy debugging +- **Portability:** No binary database files +- **Performance:** Adequate for <100 specs (target: 3-10) +- **Atomicity:** Atomic writes with temp files + +**Alternatives considered:** +- SQLite: Better for complex queries, but overkill for MVP +- Could add later if search becomes bottleneck + +### D3: Text Search vs Semantic Search + +**Decision:** Text search (MVP), semantic later + +**Rationale:** +- **Complexity:** Text search: 100 LOC; semantic: 1000+ LOC + embeddings +- **Dependencies:** No ML dependencies, smaller binary +- **Accuracy:** 90% of queries answered by text search +- **Upgrade path:** Can add semantic in v2 without breaking API + +### D4: Sync Strategy (Manual vs Auto) + +**Decision:** Manual sync with `swagger-cli sync` + +**Rationale:** +- **Predictability:** Agents know when network calls happen +- **Offline:** Works offline after first fetch +- **Control:** User chooses when to check for updates + +**Alternatives considered:** +- Auto-sync on first query: Unpredictable latency +- TTL-based: Still unpredictable, cache stampede risk + +### D5: Ref Expansion (Inline vs Separate) + +**Decision:** Keep refs by default, expand with `--expand-refs` + +**Rationale:** +- **Understanding:** Refs show reuse patterns +- **Size:** Inline expansion = 3-5x larger output +- **Performance:** Expansion = recursive resolution = slower +- **Flexibility:** Opt-in when needed + +### D6: Error Handling (Panic vs Result) + +**Decision:** All public APIs return `Result`, no panics + +**Rationale:** +- **Robustness:** Agents can handle errors programmatically +- **Robot mode:** All errors serializable to JSON +- **Recovery:** Network errors shouldn't crash CLI + +**Implementation:** +- Use `anyhow::Result` internally +- Convert to typed `SwaggerCliError` at boundaries +- Map errors to exit codes + +### D7: Config and Cache Location + +**Decision:** Config in XDG config dir; cache in XDG cache dir (separate) + +**Rationale:** +- **Standard:** Follows XDG Base Directory spec correctly -- config is durable settings, cache is regenerable data +- **Override precedence (highest to lowest):** + 1. `--config <path>` (config file path) + 2. `SWAGGER_CLI_CONFIG` (config file path) + 3. `SWAGGER_CLI_HOME` (base dir; implies config/cache under it for hermetic runs, CI, and tests) + 4. `SWAGGER_CLI_CACHE` (cache dir only; highest precedence for cache location) + 5. XDG defaults via `directories::ProjectDirs` +- **Normative clarification:** + - Config path resolution ignores `SWAGGER_CLI_CACHE`. + - Cache dir resolution order is: `SWAGGER_CLI_CACHE` > `SWAGGER_CLI_HOME` > XDG. + - Config path resolution order is: `--config` > `SWAGGER_CLI_CONFIG` > `SWAGGER_CLI_HOME` > XDG. +- **Operations:** Cache can be safely deleted without losing config. Backup tools can exclude cache. Container volume mounts are cleaner. +- **Rationale:** This keeps tests and CI hermetic (via `SWAGGER_CLI_HOME`) while still allowing direct overrides in production. +- **Implementation note (normative):** A single PathResolver function applies this precedence. `Config::config_path()` and `Config::cache_dir()` implement the env var checks. All tests MUST run under `SWAGGER_CLI_HOME` to ensure hermetic behavior and avoid contaminating real user caches. + +**Structure:** +``` +~/.config/swagger-cli/ +└── config.toml # User config (aliases, default, auth) + +~/.cache/swagger-cli/ +└── aliases/ + ├── petstore/ + │ ├── meta.json # Fetch metadata + integrity hashes + │ ├── raw.source # Original upstream bytes (json/yaml as fetched) + │ ├── raw.json # Canonical normalized JSON + │ ├── index.json # Precomputed query index + │ └── .lock # File lock for writes + └── stripe/ + ├── meta.json + ├── raw.source + ├── raw.json + ├── index.json + └── .lock +``` + +### D8: Index-Backed Cache (Source + Raw + Index + Meta) + +**Decision:** Four-file cache per alias instead of single combined file + +**Rationale:** +- **Performance:** Query commands (list, search, tags, schemas) load only `index.json` (10-50KB), avoiding deserialization of multi-MB raw specs. This is the key design choice that makes <50ms realistic for large specs like Stripe (2.4MB) and GitHub (8.2MB). +- **Correctness:** Raw spec is stored as exact upstream bytes, no lossy struct parsing. `show` and `schemas --show` use JSON pointers from the index to extract subtrees from the raw Value. +- **Compatibility:** No typed OpenAPI model to break across 3.0/3.1 differences, extensions, or spec variations. +- **Simplicity:** Index is rebuilt on every fetch/sync (cheap operation). If index format changes, bump `index_version` and re-index. + +**Alternatives considered:** +- Single `CachedSpec` file with embedded spec: Simpler code, but loads entire spec for every command. Fails latency target for large specs. +- SQLite with FTS: Better for very large specs, but overkill for MVP (typically 3-10 specs). + +### D9: Tolerant Parsing (serde_json::Value vs Typed Model) + +**Decision:** Parse raw spec as `serde_json::Value`, extract normalized index + +**Rationale:** +- OpenAPI 3.1 aligns with JSON Schema Draft 2020-12, introducing many new schema constructs +- Real-world specs contain custom `x-*` extensions, vendor-specific fields, and structural variations +- A typed model either rejects valid specs (deserialization failure) or silently drops fields +- `serde_json::Value` accepts any valid JSON; index extraction pulls only what's needed +- `show` command returns raw JSON subtrees, preserving all upstream data + +**Alternatives considered:** +- Full typed `OpenApiSpec` struct: Clean code, but fragile against real-world spec diversity +- `openapiv3` crate: Good coverage but adds dependency weight and still may lag spec evolution + +### D10: Robot Mode Contract + +**Decision:** `--robot` is the only switch for JSON output; TTY detection controls only color/unicode + +**Rationale:** +- Auto-enabling robot mode when stdout is not a TTY (the original design) is hostile to humans piping output (`| less`, `> file`) +- Makes behavior environment-dependent and unpredictable +- Agents should explicitly request robot mode; humans piping should get human-readable text without color + +**Implementation:** +- `--robot` flag: switches output to JSON +- TTY detection: controls color and unicode characters only +- Every robot payload includes: `meta.schema_version`, `meta.tool_version`, `meta.command`, `meta.duration_ms` +- `schema_version` is bumped when JSON shape changes, enabling agents to detect breaking changes +- Robot JSON must be deterministic: + - Use `BTreeMap` (not `HashMap`) for any constructed objects/maps to ensure canonical key ordering + - Index arrays are emitted in their pre-sorted order (endpoints by path+method_rank, schemas by name, tags by name) + - Compact output by default; pretty-print only with `--pretty` + - This guarantees: stable golden tests, meaningful sync diffs, predictable agent parsing + +### D11: Distribution Strategy + +**Decision:** GitLab Releases + Docker (hybrid) + +**Rationale:** +- **Humans:** Prefer native binaries (faster) +- **Agents:** Prefer Docker (reproducible) +- **CI/CD:** Multi-platform builds in GitLab +- **Updates:** Manual for now (future: auto-update) + +**Release checklist:** +1. Tag version: `git tag v1.0.0` +2. Push: `git push --tags` +3. CI builds binaries for all platforms +4. Upload to GitLab Package Registry +5. Build and push Docker image + +### D12: YAML Output De-scoped from MVP (Input Supported) + +**Decision:** YAML *input* is accepted and normalized to JSON at ingest time. YAML *output* format deferred to v1.1+. + +**Rationale:** +- `serde_yaml` is now a dependency for input normalization (see D16) +- YAML *output* determinism is harder to snapshot-test (key ordering, multiline strings, anchor handling) +- Agents overwhelmingly want JSON output; humans want the pretty formatter +- YAML output format (`--format yaml`) requires its own golden fixtures and normalization rules +- Input support covers the primary adoption friction; output format is a separate concern + +### D13: External Refs — Annotate at Query Time, Bundle at Fetch Time (Opt-in) + +**Decision:** Query-time `--expand-refs` expands internal refs only (`#/...`). External refs are annotated, never fetched at query time. Optional `--resolve-external-refs` at fetch time bundles external refs with explicit host allowlist. + +**Rationale:** +- Query-time external ref fetching would cause unexpected network calls, violating the offline guarantee +- Fetch-time bundling is opt-in with explicit `--ref-allow-host` allowlist (see D19) +- Annotation (`{"$external_ref": "..."}`) at query time is transparent and agents can follow up if needed +- Warning in `meta.warnings[]` (robot) or stderr (human) makes the behavior discoverable +- Bundled specs (from fetch-time resolution) have all refs internalized, so `--expand-refs` at query time works fully + +### D14: Auth Profiles — Preferred Over Raw Tokens + +**Decision:** `--auth-profile <NAME>` loads auth config from `config.toml` profiles. Raw `--bearer`/`--header` flags kept for one-offs. + +**Rationale:** +- Shell history, process lists, and CI logs can leak raw tokens +- Config already has `auth_profiles` structure — connecting fetch to it is low effort +- Explicit flags merge with and override profile headers (explicit wins) +- Doctor already checks config permissions — auth profiles integrate naturally + +### D15: Portable Builds — rustls Over native-tls + +**Decision:** Use `reqwest` with `rustls-tls` feature, not default native-tls (OpenSSL). + +**Rationale:** +- Dockerfile uses `rust:1.93-alpine` with musl target — OpenSSL is a known pain point +- rustls is pure Rust, no system library dependency, simpler cross-compilation +- Smaller binary, no runtime linking issues in containers +- Industry trend: most Rust CLI tools have moved to rustls + +### D16: YAML Input Support (Ingest, Not Output) + +**Decision:** Accept YAML as input format during fetch; normalize to JSON internally. YAML *output* remains de-scoped. + +**Rationale:** +- Many real-world OpenAPI specs are authored in YAML (especially GitLab, Kubernetes ecosystem) +- Rejecting YAML input creates unnecessary adoption friction +- Normalization to JSON at ingest means all internal logic (indexing, querying, show) remains JSON-only +- `raw.source` preserves original bytes for provenance; `raw.json` stores normalized JSON +- YAML output is a different concern (serialization determinism, golden test complexity) and stays in Phase 3 + +### D17: SSRF Protection and Transport Policy + +**Decision:** Block loopback/RFC1918/link-local/multicast targets by default; require HTTPS for remote URLs; validate resolved IP after redirects + +**Rationale:** +- Agent-facing CLI tools may run in privileged environments (cloud VMs, CI runners) with access to metadata endpoints (e.g., `169.254.169.254`) and internal services +- Default-deny for private ranges eliminates SSRF as an attack vector without impacting legitimate use +- `--allow-private-host <HOST>` permits explicit exceptions for internal API specs +- HTTPS-by-default prevents MITM injection of malicious spec content into the cache +- DNS rebinding check (validating resolved IP post-redirect) prevents DNS-rebinding bypass where a hostname resolves to a public IP initially then to a private IP on redirect +- Low implementation cost: resolve DNS before connecting, compare against blocked CIDR ranges + +### D18: Global Network Policy + +**Decision:** `--network auto|offline|online-only` global flag + +**Rationale:** +- Manual `sync` is good, but agents/CI need a hard guarantee of no network calls +- `--network offline` makes any network-requiring command fail with `OFFLINE_MODE` (exit 15) instead of silently attempting a connection +- `--network online-only` is the inverse: useful for environments where offline behavior should be flagged +- Default `auto` preserves all existing behavior +- Low implementation cost (check flag before any HTTP call) + +### D19: External Ref Bundling (Fetch-Time Only) + +**Decision:** Opt-in `--resolve-external-refs` at fetch time with explicit host allowlist + +**Rationale:** +- The original D13 correctly avoids query-time network calls for external refs +- But many production specs depend on external refs for core operations (especially microservice architectures) +- Fetch-time bundling with host allowlist preserves the offline guarantee for all query commands +- Explicit `--ref-allow-host` prevents fetching from arbitrary hosts (security) +- Depth and size limits prevent unbounded resolution chains +- Bundled result stored in `raw.json`; original (with $ref pointers) in `raw.source` + +### D20: Supply Chain Hardening (Checksums + Signatures) + +**Decision:** SHA256SUMS + minisign signatures for release artifacts + +**Rationale:** +- Current installer downloads and executes binaries without verification +- SHA256SUMS provides integrity checking; minisign provides provenance verification +- minisign is lightweight, fast, and commonly used in Rust ecosystem (unlike GPG) +- Installer verifies by default (`VERIFY=true`); can be skipped with `VERIFY=false` for trusted environments +- Low implementation cost (one CI step + installer enhancement) + +### D21: Alias Format Validation + +**Decision:** Strict regex validation `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$` for alias names + +**Rationale:** +- Alias names map directly to directory names under `~/.cache/swagger-cli/aliases/` +- Without validation, aliases like `../../../etc/passwd`, `CON`, or names with shell metacharacters could cause path traversal, filesystem issues on Windows, or injection in scripts +- The regex allows alphanumeric starts, permits dots/hyphens/underscores (common in API naming), and caps at 64 chars +- Validation happens at parse time (before any filesystem operation), failing with `USAGE_ERROR` + +### D22: Credential Source Abstraction + +**Decision:** `CredentialSource` enum (Literal, EnvVar, Keyring) for auth profile tokens + +**Rationale:** +- Plaintext tokens in config files are a security risk, especially in shared or version-controlled environments +- `EnvVar` source avoids persisting tokens on disk entirely — preferred for CI and agent environments +- `Keyring` source (Phase 2) delegates to OS-native secure storage +- Backward-compatible: `Literal` still works for simple setups +- `doctor` already warns on insecure config permissions, so the Literal path is covered + +### D23: Coalesced LRU Writes + +**Decision:** `last_accessed` metadata writes coalesced with 10-minute minimum interval + +**Rationale:** +- Updating metadata on every query creates unnecessary I/O, file lock contention, and SSD write amplification +- With frequent agent queries (potentially multiple per second), per-query writes add measurable latency +- 10-minute coalescing preserves LRU ordering accuracy for eviction while reducing writes by orders of magnitude +- Best-effort, no lock required — acceptable if a coalesced write is lost + +### D24: Robot JSON Schema Artifacts + +**Decision:** Publish versioned JSON Schema files alongside the binary + +**Rationale:** +- Golden tests catch regressions during development, but external consumers (agent frameworks, CI pipelines) need a machine-readable contract +- JSON Schema files (`docs/robot-schema/v1/`) serve as the authoritative specification for robot mode output +- Integration tests validate golden fixtures against these schemas, ensuring the published contract matches reality +- Enables external tooling to validate swagger-cli output without custom parsers + +### D25: Structural Diff Command (Phase 2) + +**Decision:** `swagger-cli diff` compares two spec states using index-based structural comparison + +**Rationale:** +- Sync already computes index diffs (added/removed/modified endpoints and schemas) — diff reuses this logic +- Agents and CI need actionable change reports, not raw text diffs +- `--fail-on breaking` enables CI gate usage (non-zero exit on breaking changes) +- Full breaking-change classification (heuristic semantic analysis) deferred to Phase 3 — Phase 2 reports structural changes only + +--- + +## Open Questions + +### Q1: Should we support OpenAPI 2.0 (Swagger)? + +**Status:** Deferred to v2 + +**Rationale:** +- OpenAPI 3.0+ is industry standard (2017+) +- Legacy support adds complexity (different schema) +- Can convert 2.0 → 3.0 with external tools if needed + +**Decision:** MVP targets OpenAPI 3.0.x and 3.1.x only + +### Q2: How to handle huge specs (10MB+, 1000+ endpoints)? + +**Status:** Largely addressed by index-backed architecture + +**Current mitigations:** +- Index-backed queries: list/search/tags/schemas load only index.json (10-50KB even for huge specs) +- Raw spec parsed only by `show` command, and only the relevant subtree via JSON pointer +- Max download size guardrail (default 25MB) prevents accidental huge fetches + +**Remaining potential issues:** +- `show` on a spec with deeply nested schemas + `--expand-refs` could be slow +- Cache size: 10MB × 10 specs = 100MB (acceptable) + +**Optimization if needed:** +- SQLite FTS for search across very large specs +- Lazy raw.json loading with mmap +- Pagination for list output + +**Decision:** Index architecture handles the common case; monitor `show` performance on large specs + +### Q3: Should aliases support wildcards/regex? + +**Status:** Partially addressed — `--all-aliases` flag added + +**Use case:** +```bash +swagger-cli list --all-aliases --tag users # Query all aliased APIs +swagger-cli search --all-aliases "create user" # Search across all specs +``` + +**Resolution:** +- Default remains single alias per query (unchanged behavior) +- `--all-aliases` flag added to `list` and `search` for explicit cross-alias discovery +- Each result includes `alias` field so agents can disambiguate origin +- Wildcard/regex alias patterns (e.g., `prod-*`) deferred to future — `--all-aliases` covers the common case + +**Decision:** Single alias by default; `--all-aliases` for federated discovery across all specs + +### Q4: Authentication token storage security? + +**Status:** Multi-source credential resolution (Literal, EnvVar, Keyring) + +**Security model:** +- `CredentialSource::Literal` — inline token in config.toml (backward-compatible; doctor warns on insecure perms) +- `CredentialSource::EnvVar` — reads token from environment variable at runtime (preferred for CI/agents; no token persisted on disk) +- `CredentialSource::Keyring` — OS keychain lookup (Phase 2; macOS Keychain, Linux Secret Service via `keyring` crate) +- File permissions: 0600 (user-only read/write); doctor warns if weaker +- No encryption of config file itself (adds complexity; EnvVar/Keyring avoid the need) + +**Config example:** +```toml +[auth_profiles.corp-internal] +url_pattern = "https://internal.api/*" +auth_type = "bearer" + +[auth_profiles.corp-internal.credential] +source = "env_var" +var_name = "CORP_API_TOKEN" +``` + +**Decision:** CredentialSource enum supports Literal (MVP backward compat), EnvVar (MVP, preferred for agents), Keyring (Phase 2) + +--- + +## Appendix A: Sample OpenAPI Spec + +```json +{ + "openapi": "3.0.0", + "info": { + "title": "Pet Store API", + "version": "1.0.0" + }, + "paths": { + "/pets": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": ["pets"], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "A list of pets", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + } + } + } + }, + "post": { + "summary": "Create a pet", + "operationId": "createPet", + "tags": ["pets"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": ["id", "name"], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "Pets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "securitySchemes": { + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + } + } + } +} +``` + +--- + +## Appendix B: Exit Code Reference + +| Code | Meaning | JSON Code | Retry? | Agent Action | +|------|---------|-----------|--------|--------------| +| 0 | Success | N/A | N/A | Process data | +| 1 | Generic error | `INTERNAL_ERROR` | Maybe | Report bug | +| 2 | Usage error (bad args, invalid regex) | `USAGE_ERROR` | No | Fix command syntax | +| 4 | Network error | `NETWORK_ERROR` | Yes | Retry with backoff (5xx only; 4xx not retryable) | +| 5 | Invalid spec | `INVALID_SPEC` | No | Check spec URL | +| 6 | Alias exists | `ALIAS_EXISTS` | No | Use --force | +| 7 | Auth failed | `AUTH_FAILED` | No | Check credentials | +| 8 | Not found | `ALIAS_NOT_FOUND` | No | Run aliases --list | +| 9 | Cache locked (file lock busy) | `CACHE_LOCKED` | Yes | Retry after delay | +| 10 | Cache error | `CACHE_ERROR` | Maybe | Run doctor --fix | +| 11 | Config error | `CONFIG_ERROR` | No | Check config file | +| 12 | IO error | `IO_ERROR` | Maybe | Check permissions | +| 13 | JSON error | `JSON_ERROR` | No | Check input data | +| 14 | Cache integrity (torn/partial state) | `CACHE_INTEGRITY` | Yes | Run doctor --fix | +| 15 | Offline mode blocked network operation | `OFFLINE_MODE` | No | Retry without `--network offline` | +| 16 | Policy blocked (SSRF, insecure transport) | `POLICY_BLOCKED` | No | Use `--allow-private-host` or `--allow-insecure-http` | + +--- + +## Appendix C: Robot Mode Output Schemas + +**Success response:** +```json +{ + "ok": true, + "data": { ... }, + "meta": { + "schema_version": 1, + "tool_version": "semver", + "command": "string", + "alias": "string", + "spec_version": "string", + "cached_at": "ISO-8601", + "duration_ms": 0 + } +} +``` + +**Error response (stderr):** +```json +{ + "ok": false, + "error": { + "code": "ERROR_CODE", + "message": "Human-readable error message", + "suggestion": "Actionable suggestion (optional)" + }, + "meta": { + "schema_version": 1, + "tool_version": "semver", + "command": "string", + "duration_ms": 0 + } +} +``` + +**Contract guarantee:** Both success and error envelopes ALWAYS include `meta`. Agents need only one JSON parser, not two. + +**JSON Schema artifacts (normative):** + +Canonical JSON Schema files are versioned and published alongside the binary: +- `docs/robot-schema/v1/success.schema.json` — validates all success responses +- `docs/robot-schema/v1/error.schema.json` — validates all error responses + +These schemas are the authoritative contract for robot mode output. Integration tests validate all golden fixtures against these schemas. External agent frameworks can fetch and use them for input validation. + +**Compatibility policy:** +- **Additive fields** (new optional fields in `data` or `meta`): no `schema_version` bump required +- **Removed, renamed, or type-changed fields**: MUST bump `schema_version` +- `meta.command_version` (optional) added for command-specific payload evolution when a single command's data shape changes independently of others + +--- + +## Implementation Phases + +### Week 1: Core Infrastructure (CP0-CP2) + +**CP0: Repo + CLI skeleton** +- Initialize Cargo project with correct dependencies +- Create directory structure +- Wire global flags (`--robot`, `--config`) +- Set up CI skeleton (fmt, clippy, test gates) +- **Exit criteria:** `swagger-cli --help` works; CI runs fmt/clippy/tests; all global flags parsed + +**CP1: Cache + Index format** +- Implement XDG-compliant cache directory layout (`~/.cache/swagger-cli/aliases/<alias>/`) +- Implement config management (`~/.config/swagger-cli/config.toml`) +- Implement PathResolver applying D7 precedence (SWAGGER_CLI_HOME, SWAGGER_CLI_CACHE, XDG) +- Build index extraction from raw JSON spec (parse as `serde_json::Value`, extract `SpecIndex`) +- Index build MUST be deterministic (sorted endpoints/schemas/tags) +- Compute effective security per operation (root + operation-level override semantics) +- Implement crash-consistent writes (raw+index first, meta LAST as commit marker, generation + index_hash) +- Implement per-alias file locking +- Implement canonical ingest pipeline: detect format (JSON/YAML via extension + Content-Type), parse, normalize to JSON +- Store original bytes as `raw.source`, normalized JSON as `raw.json` +- Enforce `--max-bytes` via streaming byte count during download (fail before full buffering) +- Validate all index pointers (operation_ptr, schema_ptr) resolve against raw.json after index build +- Implement alias format validation (regex check, reserved name rejection, path traversal prevention) +- Implement SSRF protection (block private/loopback/link-local/multicast ranges, HTTPS-by-default, DNS rebinding check, `--allow-private-host`, `--allow-insecure-http`) +- Implement `fetch` command (URL + file:// + local path + stdin, repeatable --header, --bearer, --auth-profile, --input-format, --resolve-external-refs, timeouts, size caps, retries) +- Write error types with exit codes (including Usage and CacheIntegrity variants) +- **Exit criteria:** + - `fetch` writes raw.source + raw.json + index/meta crash-consistently with fsync; meta.json is commit marker; generation/index_hash validated on read + - Lock acquisition bounded (<=1s default), fails fast with CACHE_LOCKED on timeout + - Index build is deterministic (sorted by path+method / name) + - All index pointers (operation_ptr, schema_ptr) validated against raw.json at build time + - Index contains all fields required by FR-2 robot output (parameters, request_body_required, effective security_schemes + security_required) + - Alias format validation rejects path traversal, reserved names, empty, and overlong inputs + - SSRF policy enforced: loopback/RFC1918/link-local/multicast blocked by default; `--allow-private-host` permits exceptions; `--allow-insecure-http` required for HTTP URLs + - DNS rebinding check validates resolved IP after redirects + - `file://` and local paths work; YAML input accepted and normalized + - Streaming max-bytes enforcement (does not buffer entire response) + - All tests run under SWAGGER_CLI_HOME for hermetic behavior + - Unit tests validate index extraction against fixtures (petstore JSON + petstore YAML + large fixture) + +**CP2: Core queries** +- Implement `list` command (filters, sort, limit -- reads index only) +- Implement `show` command (loads index for pointer, then raw.json as Value for full details) + - `show` returns raw subtrees deterministically (no ref expansion in CP2 — deferred to CP3 shared utility) +- Implement robot mode output with `schema_version`, `tool_version`, `command`, `duration_ms` +- Human output formatting (color/unicode controlled by TTY, independent of `--robot`) +- Implement global `--network` policy enforcement (offline blocks fetch/sync with OFFLINE_MODE error) +- **Exit criteria:** + - `list` + `show` + robot output stable with schema_version=1 + - Robot error envelope includes meta (schema_version, tool_version, command, duration_ms) -- unified with success envelope + - Invalid regex/options fail with USAGE_ERROR (no silent fallbacks) + - `show` is deterministic: ambiguous path (multiple methods) requires `--method` and returns `USAGE_ERROR` in robot mode with available methods in suggestion + - Latency target validated against GitHub spec fixture (8MB+) + - Golden output test for both commands + - Raw.json removal test: `list` succeeds without raw.json present + +**Deliverable:** Working fetch, list, show with index-backed performance and stable robot contract + +### Week 2: Polish and Distribution (CP3-CP5) + +**CP3: Discovery commands** +- Implement `search` command (tokenized multi-term scoring, Unicode-safe snippets, honors `--case-sensitive` and `--exact`) +- Implement `schemas` command (list from index, `--show` loads raw via JSON pointer) +- Implement `tags` command (reads index only) +- Implement shared `$ref` expansion utility used by: + - `show --expand-refs` + - `schemas --show --expand-refs` + - Bounded depth + cycle detection (`$circular_ref`) + external-ref annotation (`$external_ref`) +- Add golden robot output tests for all commands +- **Exit criteria:** All three commands index-backed; options honored; golden tests added and passing; ref expansion works with cycles and external refs annotated + +**CP4: Alias + Sync + Doctor + Cache** +- Implement `aliases` command (list, show, rename, delete, set-default) +- Implement `sync` command (change detection via index comparison, atomic cache update) + - Bounded concurrency for `--all` (`--jobs`, `--per-host`, Retry-After handling) + - Per-alias partial failure reporting in robot output +- Implement `doctor` command (validate cache integrity, pointer validation, stale detection, disk usage, `--fix` with locking) +- Implement `cache` command (stats, prune-stale, max-total-mb with LRU eviction, dry-run) +- Add `--all-aliases` support to `list` and `search` commands +- Implement `diff` command (structural comparison of two spec states, leveraging sync index-diff logic) +- **Exit criteria:** + - All commands concurrency-safe; sync diffs computed from old vs new index + - Sync supports `--details` with capped change lists (agent-actionable added/removed/modified arrays) + - Sync `--all --jobs 4` faster than sequential (verified with 4+ aliases) + - Sync `--resume` resumes interrupted `--all` runs from checkpoint; `--max-failures` aborts after N failures + - Doctor detects partial caches (meta missing, generation/hash mismatch) + validates pointers + warns on insecure config permissions (tokens present) + - Cache stats/prune/eviction operational + - `--all-aliases` returns results with `alias` field from multiple specs + - `diff` command reports added/removed/modified endpoints and schemas + - Integration tests pass + +**CP5: Release artifacts + Reliability** +- GitLab CI/CD pipeline (multi-platform builds) +- Supply chain hardening: SHA256SUMS + minisign signatures in release pipeline; cargo-deny + cargo-audit in CI +- Publish robot JSON Schema artifacts (`docs/robot-schema/v1/`); golden tests validate against schemas +- Docker image (correct XDG paths: `/root/.cache/swagger-cli/aliases` + `/root/.config/swagger-cli`) +- Installation script with checksum/signature verification (URLs match chosen hosting -- GitLab, not GitHub placeholder) +- Performance benchmarks in `benches/perf.rs` with Criterion +- Reliability stress tests: + - Fault injection at each write protocol step (verify recoverability) + - Multi-process lock contention (N>=32, verify bounded timeout + no deadlocks) + - Property-based tests for deterministic ordering and pointer validity +- Final golden test validation +- **Exit criteria:** Multi-platform binaries build in CI; Docker image runs correctly; install script downloads from correct URL and verifies integrity (portable sha256sum/shasum); cargo-deny + cargo-audit pass; all benchmarks meet targets; fault injection + lock contention + property tests pass; robot JSON Schema artifacts published and golden tests validate against them + +**Deliverable:** Production-ready v1.0.0 + +### Future (v2+) + +**Advanced features:** +- Semantic search +- Diff breaking-change classification (heuristic-based semantic analysis) +- Schema validation +- curl command generation +- Import/export +- OS keychain credential backend +- SBOM/provenance attestation +- Auto-update + +--- + +## Conclusion + +This PRD provides a complete blueprint for building swagger-cli, a fast agent-optimized CLI tool for querying OpenAPI specifications. The design prioritizes: + +1. **Performance:** <50ms cached queries via index-backed architecture (query commands never parse raw specs) +2. **Reliability:** True crash-consistent cache writes (fsync + bounded lock acquisition), strict option validation, meaningful exit codes, concurrent-safe locking, validated by fault-injection and lock-contention stress tests +3. **Determinism:** Sorted indexes with canonical method ordering, deterministic tie-breaking in search, integer scores, stable golden tests, predictable sync diffs, global network policy for reproducible CI/agent runs +4. **Contract stability:** Unified JSON envelope (meta on both success AND error), versioned schema, strict error taxonomy aligned between code and docs, HTTP error classification +5. **Security:** SSRF protection (default-deny for private ranges, HTTPS required, DNS rebinding check), credential source abstraction (Literal/EnvVar/Keyring), alias format validation, auth profiles, token redaction, insecure config permission warnings, supply chain hardening (SHA256SUMS + minisign + cargo-deny/cargo-audit) +6. **Compatibility:** Tolerant JSON/YAML parsing handles any valid OpenAPI 3.0/3.1 spec, including extensions; effective security semantics; external refs annotated (never fetched) or bundled at fetch time (opt-in) +7. **Portability:** Rustls TLS for Alpine/musl builds, hermetic test suite, no OpenSSL dependency +8. **Simplicity:** Single binary, no runtime dependencies +9. **Extensibility:** Clean architecture for future features (semantic search, diff, validation, YAML output) +10. **Operational hygiene:** Cache lifecycle management (prune, stats, size caps), cross-alias discovery + +**Next steps:** +1. Review and approve PRD +2. Create repository +3. Begin CP0 (repo + CLI skeleton) +4. Checkpoint reviews at CP0, CP1, CP2 (Week 1) and CP3, CP4, CP5 (Week 2) + +--- + +**Document Status:** Ready for review +**Target Start Date:** TBD +**Target Release:** v1.0.0 in 2 weeks + +--- + +## Rejected Recommendations + +- **Precomputed token postings index + fuzzy search in MVP (ChatGPT #6)** — rejected because the original plan's tokenized multi-term scoring over the SpecIndex is already fast enough for the target use case (3-10 specs, <500 endpoints). Precomputed postings add index format complexity and churn. Fuzzy/typo-tolerant matching adds edge-case complexity (edit distance thresholds, false positives). Both are valid Phase 3 enhancements if search performance degrades with larger specs, but not justified for MVP. +- **Gzip/compressed input support (ChatGPT #1 partial)** — rejected because OpenAPI specs are rarely served gzip-compressed at the application layer (HTTP transport handles this transparently via `Accept-Encoding`). Adding explicit `.json.gz` / `.yaml.gz` handling increases ingestion complexity for a rare case. If needed, users can decompress before piping to stdin. +- **Silent auto-rebuild of index from raw during reads (ChatGPT #4 partial)** — rejected because transparent repair during reads masks bugs and creates unpredictable latency spikes. The current design surfaces CACHE_INTEGRITY errors explicitly and relies on `doctor --fix` for repair. Explicit > implicit for agent reliability. Pointer validation at fetch/sync time and doctor re-validation are accepted (see D19 and FR-9 updates). +- **Async HTTP + explicit service layering with tokio/tracing/src/app/src/infra structure (ChatGPT feedback-2 #6)** — rejected because the MVP scope (10 commands, 3-10 specs, `sync --all` with `--jobs` bounded thread pool) does not justify the complexity of an async runtime (tokio), structured logging framework (tracing), and three-layer architecture (app/infra/cli). `sync --all` already uses bounded thread-based concurrency. The flat `cli/` + `core/` structure is appropriate at this scale. If async becomes needed for Phase 3 (e.g., streaming, WebSocket-based live sync), it can be introduced then without breaking the CLI interface. +- **SBOM generation + cosign provenance attestation in v1 release pipeline (ChatGPT feedback-2 #9 partial)** — rejected for v1 because it adds CI complexity (cargo-cyclonedx, cosign toolchain, attestation workflow) for marginal benefit at initial release. cargo-deny + cargo-audit dependency auditing is accepted and provides the primary supply-chain benefit. SBOM/provenance is tracked in Phase 3 for when the tool has external consumers who need formal software bill of materials. +- **Full breaking-change classification in Phase 2 diff command (ChatGPT feedback-2 #3 partial)** — rejected for Phase 2 because heuristic breaking-change analysis (field removal = breaking, type change = breaking, new optional field = non-breaking) requires substantial domain logic and edge-case handling. Phase 2 diff reports structural changes (added/removed/modified); Phase 3 adds semantic classification. This avoids shipping unreliable breaking-change verdicts. diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..a3127f6 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1 @@ +// CLI command definitions diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..126a4c1 --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1 @@ +// Core business logic modules diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..a695f12 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1 @@ +// Error types - implemented by bd-ilo diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..eaf8488 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,7 @@ +#![forbid(unsafe_code)] + +pub mod cli; +pub mod core; +pub mod errors; +pub mod output; +pub mod utils; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c782c11 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,8 @@ +#![forbid(unsafe_code)] + +use std::process::ExitCode; + +#[tokio::main] +async fn main() -> ExitCode { + ExitCode::SUCCESS +} diff --git a/src/output/mod.rs b/src/output/mod.rs new file mode 100644 index 0000000..34403c4 --- /dev/null +++ b/src/output/mod.rs @@ -0,0 +1 @@ +// Output formatting diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..638e9b7 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1 @@ +// Utility functions