diff --git a/AGENTS.md b/AGENTS.md index aeaa8ba..d617a06 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,6 @@ When working in this repository: - Create a branch before making changes unless the user explicitly asks to work on the current branch. -- Use branch names with the `codex/` prefix, for example `codex/fix-analyzer`. - Keep changes scoped to the user request. - Do not revert unrelated user changes. - Prefer opening a pull request instead of pushing directly to `main`, unless the user explicitly asks to push to `main`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d468ef1..0c66192 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,197 +1,195 @@ -# Contributing to DevMap - -Thanks for your interest in contributing. This document covers everything -you need to get started. - ---- - -## Project Structure - -``` -devmap/ -├── apps/ -│ └── web/ ← landing page (post-MVP, not active yet) -├── packages/ -│ └── cli/ ← core CLI — this is where you'll work -│ ├── src/ -│ │ ├── commands/ ← one file per CLI command -│ │ ├── analyzers/ ← static analysis logic -│ │ ├── ai/ ← AI provider abstraction -│ │ ├── cache/ ← file hashing + snapshot -│ │ └── utils/ ← output, config, helpers -│ └── test/ -│ └── fixtures/ ← dummy projects for testing -├── docs/ ← PRD, architecture, commands, roadmap -└── README.md -``` - -Most contributions will be inside `packages/cli/src/`. - ---- - -## Setup - -**Requirements:** Node.js 18+, pnpm - -```bash -# Clone the repo +# Contributing to DevMap + +Thanks for your interest in contributing. This document covers everything +you need to get started. + +--- + +## Project Structure + +``` +devmap/ +├── apps/ +│ └── web/ ← landing page (post-MVP, not active yet) +├── packages/ +│ └── cli/ ← core CLI — this is where you'll work +│ ├── src/ +│ │ ├── commands/ ← one file per CLI command +│ │ ├── analyzers/ ← static analysis logic +│ │ ├── ai/ ← AI provider abstraction +│ │ ├── cache/ ← file hashing + snapshot +│ │ └── utils/ ← output, config, helpers +│ └── test/ +│ └── fixtures/ ← dummy projects for testing +├── docs/ ← PRD, architecture, commands, roadmap +└── README.md +``` + +Most contributions will be inside `packages/cli/src/`. + +--- + +## Setup + +**Requirements:** Node.js 18+, pnpm + +```bash +# Clone the repo git clone https://github.com/itsflaid/devmap -cd devmap - -# Install dependencies -pnpm install - -# Link CLI globally so you can test it like a real user -cd packages/cli -npm link - -# Verify it works -devmap --version -``` - ---- - -## Development Workflow - -```bash -# Run CLI in development (no build needed) -cd packages/cli -pnpm dev - -# Or run a specific command directly -npx tsx src/index.ts analyze -npx tsx src/index.ts ask "how does auth work" - -# Build for production -pnpm build - -# Run tests -pnpm test -``` - ---- - -## Testing Your Changes - -Always test against real projects, not just the fixtures. - -```bash -# Go to any real project on your machine -cd ~/projects/some-nextjs-app - -# Run devmap against it -devmap analyze -devmap ask "how does auth work" -devmap doctor -``` - -The fixture projects in `test/fixtures/` are for automated tests. -Manual testing against real projects catches things fixtures miss. - -**Before submitting a PR, test against at least:** -- A Next.js project -- An Express project -- A project with many files (100+) - ---- - -## Adding a New Command - -1. Create `packages/cli/src/commands/yourcommand.ts` -2. Implement the command logic -3. Register it in `packages/cli/src/index.ts` -4. Add documentation to `docs/COMMANDS.md` -5. Add test fixtures if needed - -Follow the pattern of existing commands — use `output.ts` utilities -for all terminal output, never `console.log` directly. - ---- - -## Adding a New AI Provider - -1. Create `packages/cli/src/ai/yourprovider.ts` -2. Implement the provider interface: - -```ts -export async function complete(options: CompleteOptions): Promise -export async function isAvailable(): Promise -export function getModels(): string[] -``` - -3. Register the provider in `packages/cli/src/ai/provider.ts` -4. Add the provider to `devmap init` options in `packages/cli/src/commands/init.ts` -5. Update the provider table in `README.md` - ---- - -## Adding Framework Support - -Framework detection lives in `packages/cli/src/analyzers/frameworkDetector.ts`. - -Each framework needs: -- Detection logic (from `package.json` + file patterns) -- Entry point patterns specific to that framework -- Test fixture in `test/fixtures/` - -Before adding a new framework, open an issue first to discuss. -Framework support affects output quality significantly — -better to do one framework well than many frameworks poorly. - ---- - -## Code Style - -- TypeScript strict mode is enabled — no `any` without a comment explaining why -- Use `output.ts` utilities for all terminal output -- Keep command files thin — business logic belongs in `analyzers/` or `ai/` -- Prompts belong in `ai/prompts.ts`, never inline in command files -- One responsibility per file - ---- - -## Pull Request Guidelines - -**Small PRs are easier to review.** If you're adding a big feature, -open an issue first to discuss the approach before writing code. - -PR checklist: -- [ ] Tested against a real Next.js project -- [ ] Tested against a real Express project -- [ ] No raw `console.log` in command files -- [ ] New commands documented in `docs/COMMANDS.md` -- [ ] `devmap doctor` still passes after your changes - ---- - -## Reporting Bugs - -Run `devmap doctor` first and include the output in your bug report. -This gives all the context needed to reproduce the issue. - -Open an issue with: -1. `devmap doctor` output -2. What command you ran -3. What you expected to happen -4. What actually happened - ---- - -## Roadmap & Feature Requests - -Check `docs/ROADMAP.md` before requesting a feature — -it might already be planned. - -For features not in the roadmap, open an issue with: -- The problem you're trying to solve -- Why existing commands don't solve it -- What the command/output would look like - -Features that solve real problems with clear use cases -get prioritized over features that are technically interesting. - ---- - -## License - +cd devmap + +# Install dependencies +pnpm install + +# Link CLI globally so you can test it like a real user +cd packages/cli +npm link + +# Verify it works +devmap --version +``` + +--- + +## Development Workflow + +```bash +# Run CLI in development (no build needed) +cd packages/cli +pnpm dev + +# Or run a specific command directly +npx tsx src/index.ts analyze + +# Build for production +pnpm build + +# Run tests +pnpm test +``` + +--- + +## Testing Your Changes + +Always test against real projects, not just the fixtures. + +```bash +# Go to any real project on your machine +cd ~/projects/some-nextjs-app + +# Run devmap against it +devmap analyze +devmap doctor +``` + +The fixture projects in `test/fixtures/` are for automated tests. +Manual testing against real projects catches things fixtures miss. + +**Before submitting a PR, test against at least:** +- A Next.js project +- An Express project +- A project with many files (100+) + +--- + +## Adding a New Command + +1. Create `packages/cli/src/commands/yourcommand.ts` +2. Implement the command logic +3. Register it in `packages/cli/src/index.ts` +4. Add documentation to `docs/COMMANDS.md` +5. Add test fixtures if needed + +Follow the pattern of existing commands — use `output.ts` utilities +for all terminal output, never `console.log` directly. + +--- + +## Adding a New AI Provider + +1. Create `packages/cli/src/ai/yourprovider.ts` +2. Implement the provider interface: + +```ts +export async function complete(options: CompleteOptions): Promise +export async function isAvailable(): Promise +export function getModels(): string[] +``` + +3. Register the provider in `packages/cli/src/ai/provider.ts` +4. Add the provider to `devmap init` options in `packages/cli/src/commands/init.ts` +5. Update the provider table in `README.md` + +--- + +## Adding Framework Support + +Framework detection lives in `packages/cli/src/analyzers/frameworkDetector.ts`. + +Each framework needs: +- Detection logic (from `package.json` + file patterns) +- Entry point patterns specific to that framework +- Test fixture in `test/fixtures/` + +Before adding a new framework, open an issue first to discuss. +Framework support affects output quality significantly — +better to do one framework well than many frameworks poorly. + +--- + +## Code Style + +- TypeScript strict mode is enabled — no `any` without a comment explaining why +- Use `output.ts` utilities for all terminal output +- Keep command files thin — business logic belongs in `analyzers/` or `ai/` +- Prompts belong in `ai/prompts.ts`, never inline in command files +- One responsibility per file + +--- + +## Pull Request Guidelines + +**Small PRs are easier to review.** If you're adding a big feature, +open an issue first to discuss the approach before writing code. + +PR checklist: +- [ ] Tested against a real Next.js project +- [ ] Tested against a real Express project +- [ ] No raw `console.log` in command files +- [ ] New commands documented in `docs/COMMANDS.md` +- [ ] `devmap doctor` still passes after your changes + +--- + +## Reporting Bugs + +Run `devmap doctor` first and include the output in your bug report. +This gives all the context needed to reproduce the issue. + +Open an issue with: +1. `devmap doctor` output +2. What command you ran +3. What you expected to happen +4. What actually happened + +--- + +## Roadmap & Feature Requests + +Check `docs/ROADMAP.md` before requesting a feature — +it might already be planned. + +For features not in the roadmap, open an issue with: +- The problem you're trying to solve +- Why existing commands don't solve it +- What the command/output would look like + +Features that solve real problems with clear use cases +get prioritized over features that are technically interesting. + +--- + +## License + By contributing, you agree your contributions will be licensed under MIT. diff --git a/PRD.md b/PRD.md index 02c3642..5f33edb 100644 --- a/PRD.md +++ b/PRD.md @@ -348,47 +348,6 @@ devmap analyze --deep --- -### `devmap ask "[question]"` - -Answer natural-language questions about the codebase using the existing snapshot. - -- Does not re-analyze unless no snapshot exists -- If no snapshot exists, runs a quick analyze first -- Responds in the same language as the question - -**Usage:** - -```bash -devmap ask "how does authentication work?" -devmap ask "where is payment logic handled?" -devmap ask "explain the booking flow" -devmap ask "which files handle AI integration?" -devmap ask "where is a new user created?" -``` - -**Internal flow:** - -```txt -Question - → Detect language - → Read snapshot - → Find relevant files using path + keyword + dependency heuristics - → Build compact context - → Send relevant data to AI - → Return answer in user's language -``` - -**Rules:** - -- Never send the entire project to AI -- Prefer 3–5 most relevant files -- Include related files in output -- Keep technical labels readable and consistent -- Stream human-readable AI responses progressively -- Keep `--json` buffered so stdout remains exactly one valid JSON document - ---- - ### `devmap doctor` Diagnostics command. Helps users debug setup and provides copy-pasteable output for filing issues. @@ -450,7 +409,6 @@ integrations: ```bash devmap init --json devmap analyze --json -devmap ask "how does authentication work?" --json devmap doctor --json devmap config model auto --json ``` @@ -538,7 +496,6 @@ Generated by `devmap analyze`. **Purpose:** - Fresh project analysis data -- Source of truth for `devmap ask` - Reusable context for AI tools - Regenerated every time the project is re-analyzed - Full analysis archive and backward-compatible source for DevMap commands @@ -618,10 +575,8 @@ ai/ | Command | Model | Reason | |---|---|---| -| `ask` | `llama-3.1-8b-instant` | Fast model for focused codebase questions | | `analyze` | `openai/gpt-oss-20b` | Balanced architecture interpretation | | `analyze --deep` | `openai/gpt-oss-120b` | Heavy cross-module reasoning | -| `ask` fallbacks | `qwen/qwen3.6-27b` -> `llama-3.3-70b-versatile` -> `openai/gpt-oss-20b` | Preserve responsiveness while increasing reasoning capacity only when needed | | `analyze` fallbacks | `qwen/qwen3.6-27b` -> `llama-3.3-70b-versatile` -> `llama-3.1-8b-instant` | Keep snapshot enrichment available across model-specific limits | | `analyze --deep` fallbacks | `llama-3.3-70b-versatile` -> `qwen/qwen3.6-27b` -> `openai/gpt-oss-20b` | Degrade heavy reasoning gradually instead of failing immediately | @@ -670,7 +625,6 @@ DevMap automatically detects the language used by the user. ### Applies To -- `devmap ask` - `devmap explain` *(future)* - `devmap onboarding` - `devmap docs` *(future)* @@ -824,7 +778,7 @@ interface DevMapSnapshot { - File index entries should include compact navigation metadata such as purpose, responsibility scope, exported symbols, top functions/code symbols, feature references, search terms, and importance. These fields help - `devmap ask`, future onboarding output, and future flow generation without + future onboarding output and future flow generation without storing full raw source. - Flow metadata should include compact high-confidence feature flows and request/API flows derived from routes and local dependency edges. @@ -897,18 +851,14 @@ entire project ### Relevance Confidence And Query Expansion -`devmap ask` treats retrieval quality as part of the answer contract. - - Direct query keywords are extracted separately from generic intent words such as add, change, explain, or find. -- When AI configuration is available, Ask may run a lightweight retrieval-only +- When AI configuration is available, the context builder may run a lightweight retrieval-only model call that returns up to 10 generic technical terms for better recall. - Expanded terms improve ranking, but direct keyword matches remain stronger than inferred matches. - Files below the minimum relevance score of 25 are excluded. - Confidence is `high` at 70+, `medium` at 40+, and `low` below 40. -- Low-confidence questions should produce an honest local response instead of - asking AI to guess from unrelated context. ### Goals @@ -988,7 +938,6 @@ Before that, use careful wording: - Do static analysis first - Never send raw full project to AI - Send compact JSON summary where possible -- For `ask`, send only relevant files - Cache snapshots and reuse them ### Expected Behavior @@ -1366,7 +1315,6 @@ DevMap is ready to publish when all of these are true: - [x] `devmap init` runs without crashing - [x] `devmap analyze` runs without crashing on a Next.js project - [x] `devmap analyze` runs without crashing on an Express project -- [x] `devmap ask` works using existing snapshot - [x] `devmap doctor` returns useful diagnostic output ### Quality diff --git a/README.md b/README.md index 4b838dc..5cc5e57 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,6 @@ Node.js 18+ - [x] `devmap init` - [x] `devmap analyze` -- [x] `devmap ask` - [x] `devmap onboarding` - [x] `devmap doctor` diff --git a/docs/architecture.md b/docs/architecture.md index 2e4e708..8f9f66b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -203,7 +203,7 @@ app/page.tsx * Critical file detection * Entry point detection * Context expansion -* Better answers in `devmap ask` +* Better retrieval context --- @@ -343,7 +343,7 @@ Snapshot is the reusable project context generated by DevMap. ### Purpose * Store current project analysis -* Act as source of truth for `devmap ask` +* Act as source of truth for AI context * Provide reusable context for AI agents * Avoid repeated project exploration @@ -408,7 +408,7 @@ Each `fileIndex` entry stores compact navigation metadata: | `scope` | Responsibility classification: API, UI, database, config, service, CLI, test, docs, or unknown | | `topFunctions` | Compact list of important functions or exported code symbols with line numbers | | `featureRefs` | Feature names that reference this file | -| `searchTerms` | Retrieval-focused terms used by `devmap ask` | +| `searchTerms` | Retrieval-focused terms used by context builder | | `importance` | Static importance score from references, entry point status, critical-file score, and feature ownership | The scope classifier is responsibility-based. Framework conventions may provide @@ -569,7 +569,7 @@ Files below score 25 are excluded before context is read. This prevents Ask from selecting unrelated files only because they scored slightly above other unrelated files. -Before scoring, `devmap ask` can make a small Groq request that returns generic +Before scoring, the context builder can make a small Groq request that returns generic retrieval terms as a JSON array. This call is not allowed to choose files or invent project-specific paths. It only improves recall for deterministic ranking. @@ -642,7 +642,6 @@ MVP default model routing: | Command | Primary | Ordered fallbacks | | ---------------- | -------------------------- | ----------------- | -| `ask` | `llama-3.1-8b-instant` | `qwen/qwen3.6-27b` -> `llama-3.3-70b-versatile` -> `openai/gpt-oss-20b` | | `analyze` | `openai/gpt-oss-20b` | `qwen/qwen3.6-27b` -> `llama-3.3-70b-versatile` -> `llama-3.1-8b-instant` | | `analyze --deep` | `openai/gpt-oss-120b` | `llama-3.3-70b-versatile` -> `qwen/qwen3.6-27b` -> `openai/gpt-oss-20b` | @@ -671,7 +670,7 @@ Raw provider errors should not be shown directly to users. ## Streaming AI Output Groq and OpenRouter chat completions use server-sent events for human-readable -`analyze` and `ask` output. Each provider adapter reconstructs the response while +`analyze` output. Each provider adapter reconstructs the response while emitting incremental deltas to the output layer. Terminal Markdown is buffered to paragraph boundaries before rendering. This @@ -714,7 +713,6 @@ However, token-efficiency claims must be benchmarked before being used in public * Static analysis first * Never send the full raw project * Send compact snapshot data -* For `ask`, send only relevant context * Cache and reuse snapshot --- @@ -736,8 +734,8 @@ Future cache source: ### MVP Behavior * `devmap analyze` generates snapshot -* `devmap ask` reuses snapshot -* If no snapshot exists, `ask` may run quick analysis first +* `devmap analyze` reuses snapshot +* If no snapshot exists, `analyze` runs fresh analysis * If project changes, user may be prompted to re-analyze ### Future Optimization @@ -849,8 +847,7 @@ CLI output should be: ### Agent Output Every MVP command supports `--json`. JSON mode is implemented at the output -context layer so nested operations, such as `ask` triggering quick analysis, -do not leak human progress text into stdout. +context layer so nested operations do not leak human progress text into stdout. Rules: diff --git a/docs/commands.md b/docs/commands.md index 303b799..efe8e63 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -10,7 +10,6 @@ DevMap MVP provides five core project commands and one configuration command: * `devmap init` * `devmap analyze` -* `devmap ask` * `devmap onboarding` * `devmap doctor` * `devmap config model` @@ -218,10 +217,9 @@ server-side. Database access is centralized through the data layer. Snapshot saved: .devmap/snapshot.json -Next: -devmap ask "how does authentication work?" ``` + ### Deep Output When using: @@ -263,169 +261,6 @@ Shared utilities, database access, authentication logic, and helpers. --- -## `devmap ask` - -Ask questions about the current project. - -### Purpose - -`devmap ask` answers natural-language questions about the codebase using the existing snapshot and selected relevant files. - -### Usage - -```bash -devmap ask "how does authentication work?" -devmap ask "where is payment logic handled?" -devmap ask "explain the booking flow" -devmap ask "which files handle AI integration?" -devmap ask "where is a new user created?" -``` - -### Responsibilities - -* Read `.devmap/snapshot.json` -* Run quick analysis if no snapshot exists -* Detect question language -* Extract generic intent and relevant keywords -* Optionally expand retrieval terms with a lightweight AI call -* Select relevant files -* Calculate retrieval confidence -* Build compact context -* Send only relevant context to AI when confidence is sufficient -* Return answer in the same language as the question - -### Internal Flow - -```txt -Question - ↓ -Detect Language - ↓ -Read Snapshot - ↓ -Find Relevant Files - ↓ -Build Context - ↓ -Send To AI - ↓ -Stream Answer -``` - -### Context Selection Rules - -DevMap should select relevant files using: - -* File path matching -* Keyword matching -* Expanded retrieval-term matching -* Import/export matching -* Dependency matching -* Known framework conventions - -Example question: - -```txt -how does authentication work? -``` - -Likely relevant files: - -```txt -middleware.ts -lib/auth.ts -lib/session.ts -app/api/auth/* -``` - -### Hard Rules - -* Never send the entire project to AI -* Prefer 3–5 most relevant files -* Exclude files below the minimum relevance score of 25 -* Report retrieval confidence as `high`, `medium`, or `low` -* Use low-confidence local answers instead of asking AI to guess from weak or - missing evidence -* Include related files in output -* Keep answer readable -* Respond in the same language as the question -* Technical labels can remain in English -* Stream new AI answers progressively in human-readable mode -* Do not stream `--json`; emit one complete JSON document instead - -### Output Example - -```txt -Authentication Flow - -Authentication is handled using NextAuth. - -Flow: - -1. Request enters middleware.ts -2. Session validation occurs -3. Invalid session redirects to login -4. Valid session continues to protected routes - -Key Files - -→ middleware.ts -→ lib/auth.ts -→ app/api/auth/* -``` - -### Low-Confidence Behavior - -If no strong matches are found, Ask does not pretend unrelated files are -relevant. It returns an honest local answer: - -```txt -No strong file matches found in the current snapshot. - -No strong matching files found for "login". - -The current snapshot does not contain strong evidence for that concept, so -DevMap will not guess an existing implementation. -``` - -This protects users from hallucinated navigation and avoids spending answer -tokens on weak context. - -### Query Expansion - -When Groq is configured, Ask may first request up to 10 generic retrieval terms -as JSON. These expanded terms improve recall but do not choose files directly. -Deterministic scoring still ranks files, and direct keyword matches outweigh -expanded-term matches. If expansion fails, Ask falls back to keyword-only -retrieval. - -### Missing Snapshot Behavior - -If no snapshot exists: - -```txt -No snapshot found. - -Running quick analysis first... -``` - -Then continue answering the question. - -### Stale Snapshot Behavior - -If project files changed after last analyze: - -```txt -Project changed since last analyze. - -Use existing snapshot or re-analyze first? - -[1] Use existing snapshot -[2] Re-analyze now -``` - ---- - ## `devmap onboarding` Generate a project onboarding guide from the current snapshot. @@ -553,7 +388,7 @@ Issues found: Run devmap init again and enter a valid API key. ⚠ Snapshot is missing - Run devmap analyze before using devmap ask. + Run devmap analyze before using devmap onboarding. ``` ### Rules @@ -579,7 +414,6 @@ devmap config model auto `auto` restores command-based routing: -* `ask` uses `llama-3.1-8b-instant` * `analyze` uses `openai/gpt-oss-20b` * `analyze --deep` uses `openai/gpt-oss-120b` @@ -595,7 +429,6 @@ The typed model is stored as the primary choice and is not silently replaced. Automatic routing also uses ordered fallback chains: -* `ask`: `qwen/qwen3.6-27b`, `llama-3.3-70b-versatile`, then `openai/gpt-oss-20b` * `analyze`: `qwen/qwen3.6-27b`, `llama-3.3-70b-versatile`, then `llama-3.1-8b-instant` * `analyze --deep`: `llama-3.3-70b-versatile`, `qwen/qwen3.6-27b`, then `openai/gpt-oss-20b` @@ -644,7 +477,6 @@ integration. devmap init --json devmap analyze --json devmap analyze --deep --json -devmap ask "where is authentication handled?" --json devmap onboarding --json devmap doctor --json devmap config model auto --json @@ -662,9 +494,8 @@ Contract: * package-manager wrapper warnings may appear on stderr and are not part of the DevMap JSON document -`analyze --json` returns the project snapshot. `ask --json` returns the answer, -selected files, model, and token usage. `onboarding --json` returns guide -metadata and Markdown. `doctor --json` returns diagnostics and issues as +`analyze --json` returns the project snapshot. `onboarding --json` returns guide +metadata and Markdown. `doctor --json` returns diagnostics and issues as structured fields. --- diff --git a/docs/design.md b/docs/design.md index d7425bf..153cbe4 100644 --- a/docs/design.md +++ b/docs/design.md @@ -173,7 +173,6 @@ Start with: Popular commands: devmap analyze scan current project - devmap ask "..." ask your codebase devmap onboarding generate reading guide ``` diff --git a/docs/for-me-personal/DEBUG.md b/docs/for-me-personal/DEBUG.md index 274d5af..92f43fd 100644 --- a/docs/for-me-personal/DEBUG.md +++ b/docs/for-me-personal/DEBUG.md @@ -494,7 +494,7 @@ dengan pemberitahuan singkat. ### Solusi -- Gunakan `openai/gpt-oss-20b` untuk `ask` dan standard `analyze`. +- Gunakan `openai/gpt-oss-20b` untuk standard `analyze`. - Gunakan `llama-3.3-70b-versatile` untuk `analyze --deep`. - Gunakan `llama-3.3-70b-versatile` sebagai fallback. - Perbarui PRD dan architecture docs. @@ -614,7 +614,7 @@ test satu pertanyaan. ### Gejala -Jawaban `devmap ask` menampilkan marker seperti `**bold**`, backtick, dan table +Output `devmap ask` (sebelum dihapus) menampilkan marker seperti `**bold**`, backtick, dan table pipe secara literal. Tabel lebar terpotong oleh terminal dan sulit dipindai. ### Akar Masalah @@ -628,14 +628,14 @@ source preview, tetapi tidak memahami struktur Markdown yang dihasilkan model. - Render heading, prose, list, fenced code, dan inline formatting. - Ubah Markdown table menjadi record vertikal. - Bungkus text berdasarkan lebar terminal. -- Gunakan renderer hanya untuk jawaban AI `ask` dan interpretation `analyze`. +- Gunakan renderer hanya untuk jawaban AI `analyze`. - Pertahankan `codeBlock()` untuk static source context. ### Verifikasi - Unit test mencakup heading, inline marker, list, table, wrapping, dan code fence. -- Integration test memastikan output `ask` dan cached `analyze` tidak +- Integration test memastikan output `analyze` tidak menampilkan marker Markdown mentah. - Preview manual dengan contoh database menghasilkan blok `users` dan `rooms` yang terbaca tanpa table pipe. @@ -745,14 +745,14 @@ TypeScript. Retry rate limit memakai satu cabang `if`, bukan loop berbatas. TypeScript type assertion tidak memvalidasi data runtime. Semua data persisted harus melewati boundary validation sebelum dipakai oleh command lain. -## 14. Ask Output Terlalu Ramai Dan Jawaban Berulang +## 14. Ask Output Terlalu Ramai Dan Jawaban Berulang (removed) **Tanggal:** 2026-06-16 **Status:** Selesai ### Gejala -`devmap ask` menampilkan `Relevant Files` dengan alasan scoring yang panjang +Output `devmap ask` (sebelum dihapus) menampilkan `Relevant Files` dengan alasan scoring yang panjang dan jawaban AI dapat mengulang kalimat, memberi high-level outline terlalu panjang, atau menampilkan contoh kode padahal user hanya butuh arah file. @@ -784,7 +784,7 @@ tidak relevan. Prompt `ask` belum memberi kontrak format yang cukup tegas. ### Verifikasi -- `pnpm --filter devmap exec tsx --test test/context-builder.test.ts test/ask-command.test.ts test/ai-client.test.ts` +- `pnpm --filter devmap exec tsx --test test/context-builder.test.ts test/ai-client.test.ts` ### Pelajaran @@ -830,7 +830,7 @@ direct match atau membuat file lemah terlihat seolah relevan. - Focused suite: ```powershell -pnpm --filter devmap exec tsx --test test/context-builder.test.ts test/ask-command.test.ts test/ai-client.test.ts +pnpm --filter devmap exec tsx --test test/context-builder.test.ts test/ai-client.test.ts ``` ### Pelajaran diff --git a/docs/for-me-personal/PROGRESS.md b/docs/for-me-personal/PROGRESS.md index e13ceed..ed008f7 100644 --- a/docs/for-me-personal/PROGRESS.md +++ b/docs/for-me-personal/PROGRESS.md @@ -1,6 +1,32 @@ # Progress DevMap -Terakhir diperbarui: 2026-06-22 +Terakhir diperbarui: 2026-06-23 + +## Update 2026-06-23 + +### Ask Command — Complete Removal + +- **Seluruh fitur `devmap ask` dihapus permanen.** +- File dihapus: `src/commands/ask.ts`, `test/ask-command.test.ts`. +- **Source code clean-up:** + - `index.ts` — hapus import dan registrasi command `ask`. + - `provider.ts` — hapus `"ask"` dari `AiTask` union type. + - `groq.ts` — hapus `DEFAULT_AI_MODELS.ask` dan `DEFAULT_AI_FALLBACKS.ask`. + - `prompts.ts` — hapus `buildAskMessages`, `buildQueryExpansionMessages`, type `AskProjectSummary`. + - `doctor.ts` — ganti `resolveAiRouting(config, "ask")` → `"analyze"`. + - `featureDetector.ts` — hapus `"ask"` dari terms CLI Commands. +- **Test files clean-up:** + - Hapus `test/ask-command.test.ts`. + - `json-output.test.ts` — hapus test `ask --json`. + - `ai-client.test.ts` — hapus test `ask prompt`, ganti model constants jadi `deepAnalyze`. + - `openrouter-client.test.ts` — ganti `"ask"` jadi `"analyze"`. + - `doctor.test.ts` — update expected model regex. +- **Dokumentasi:** hapus semua referensi `devmap ask` dari `PRD.md`, `docs/commands.md`, `docs/architecture.md`, `README.md`, `CONTRIBUTING.md`, `packages/cli/README.md`, `docs/design.md`, `docs/roadmap.md`, `docs/releasing.md`, `docs/generated-files.md`. +- **Personal notes:** update `TEST.md`, `PROGRESS.md`, `DEBUG.md` (tandai entri ask sebagai removed). +- **Test suite:** 112 pass, 2 fail (keduanya pre-existing — `analyzers.test.ts` routes params bug dan `context-builder.test.ts` confidence threshold). +- **Type check:** lulus tanpa error. + +--- ## Update 2026-06-22 @@ -197,11 +223,11 @@ Terakhir diperbarui: 2026-06-22 - Snapshot reader memberi default aman untuk snapshot lama agar `ask` tetap berjalan sambil user bisa regenerate snapshot. -### Ask Retrieval Strengthening +### Ask Retrieval Strengthening (removed) - `QuestionContext` sekarang menyimpan `expandedTerms` selain `intent`, `keywords`, `confidence`, `relevantFiles`, dan `topScore`. -- `devmap ask` dapat menjalankan query-expansion Groq ringan sebelum scoring. +- Context Builder dapat menjalankan query-expansion Groq ringan sebelum scoring. Respons harus berupa JSON array dan hanya dipakai sebagai retrieval terms. - Expanded terms ikut ranking dengan bobot lebih rendah dari keyword langsung, sehingga direct match tetap mengalahkan inferred match. @@ -219,9 +245,9 @@ Terakhir diperbarui: 2026-06-22 ## Update 2026-06-16 -### Ask Output Polish +### Ask Output (removed) -- Human-readable `devmap ask` sekarang hanya menampilkan path pada bagian +- Human-readable `devmap ask` (sebelum dihapus) hanya menampilkan path pada bagian `Relevant Files`; alasan scoring tetap tersedia di `--json`. - Query understanding memisahkan intent umum (`add_feature`, `change`, `debug`, `explain`, `navigate`, `general`) dari keyword pencarian agar @@ -279,7 +305,7 @@ Terakhir diperbarui: 2026-06-22 ### AI Response Streaming -- Human-readable `devmap ask` dan AI interpretation pada `devmap analyze` +- AI interpretation pada `devmap analyze` sekarang memakai Groq server-sent events. - Delta response direkonstruksi menjadi hasil lengkap untuk token metadata, snapshot persistence, dan cache. @@ -310,7 +336,6 @@ Terakhir diperbarui: 2026-06-22 ### Model Routing And Config -- Default `devmap ask` memakai `llama-3.1-8b-instant`. - Standard `devmap analyze` tetap memakai `openai/gpt-oss-20b`. - `devmap analyze --deep` memakai `openai/gpt-oss-120b`. - Fallback model memakai `openai/gpt-oss-20b`. @@ -406,7 +431,7 @@ Terakhir diperbarui: 2026-06-22 ### Terminal Markdown Rendering -- Jawaban AI dari `devmap ask` dan architecture interpretation dari +- Architecture interpretation dari `devmap analyze` sekarang dirender sebagai output terminal yang terstruktur. - Heading memakai accent aqua dan separator. - Marker Markdown inline seperti bold, italic, strikethrough, link, dan @@ -472,7 +497,7 @@ Terakhir diperbarui: 2026-06-22 - Batas context adalah maksimal 5 file dan 200 baris per file. - File besar memakai relevant line window. - Path traversal dan symlink escape di luar project root ditolak. -- `devmap ask` sudah memakai Context Builder secara lokal. +- `devmap ask` (sebelum dihapus) sudah memakai Context Builder secara lokal. - Benchmark 20 pertanyaan mencakup auth, database, route session, halaman, layout, payment, dan entry point dalam Bahasa Indonesia dan English. - Hasil benchmark saat ini: top-1 accuracy 20/20 dan top-3 recall 20/20. @@ -491,9 +516,9 @@ Terakhir diperbarui: 2026-06-22 header `retry-after`. - Invalid API key, rate limit, provider failure, empty response, dan response yang tidak valid diterjemahkan menjadi error actionable. -- Prompt `ask` hanya memakai context terpilih dan meminta jawaban dalam bahasa +- Prompt `ask` (sebelum dihapus) hanya memakai context terpilih dan meminta jawaban dalam bahasa yang sama dengan pertanyaan. -- `devmap ask` menampilkan token usage agar benchmarking dapat dilakukan. +- `devmap ask` (sebelum dihapus) menampilkan token usage agar benchmarking dapat dilakukan. - Jika AI belum dikonfigurasi atau gagal, selected static context tetap ditampilkan. - Automated test AI memakai fake provider sehingga tidak menggunakan quota. diff --git a/docs/for-me-personal/TEST.md b/docs/for-me-personal/TEST.md index 93345af..c6da792 100644 --- a/docs/for-me-personal/TEST.md +++ b/docs/for-me-personal/TEST.md @@ -38,7 +38,7 @@ Expected interactive flow: atau ketik model ID gratis/berbayar yang ingin diuji. 4. Pastikan output menjelaskan command `devmap config model ` untuk mengganti model nanti. -5. Jalankan `devmap doctor`, `devmap analyze`, dan `devmap ask` lalu pastikan +5. Jalankan `devmap doctor` dan `devmap analyze` lalu pastikan provider serta model yang tampil sesuai config. Non-interactive setup dapat memakai: @@ -185,11 +185,7 @@ Jalankan focused test ranking dan evaluation: pnpm --filter devmap exec tsx --test test/context-builder.test.ts test/context-builder-eval.test.ts ``` -Untuk polish output `ask`, jalankan focused contract test: -```powershell -pnpm --filter devmap exec tsx --test test/context-builder.test.ts test/ask-command.test.ts test/ai-client.test.ts -``` Expected result: @@ -240,11 +236,7 @@ Expected result: Manual source-mode check: -```powershell -pnpm dev:cli ask "where scanner" -pnpm dev:cli ask "which tests cover the scanner?" -pnpm dev:cli ask "where is the web UI dashboard component?" -``` + Periksa `Relevant Files` dan prompt token usage. Query pertama seharusnya memprioritaskan production CLI source dan memakai context jauh lebih kecil @@ -261,7 +253,7 @@ pasti. Focused automated test: ```powershell -pnpm --filter devmap exec tsx --test test/config-command.test.ts test/analyze-ai.test.ts test/ask-command.test.ts +pnpm --filter devmap exec tsx --test test/config-command.test.ts test/analyze-ai.test.ts ``` Expected automatic routing: @@ -330,14 +322,14 @@ Expected: Focused automated test: ```powershell -pnpm --filter devmap exec tsx --test test/ai-client.test.ts test/ask-command.test.ts test/analyze-ai.test.ts test/json-output.test.ts +pnpm --filter devmap exec tsx --test test/ai-client.test.ts test/analyze-ai.test.ts test/json-output.test.ts ``` Coverage penting: - SSE event tetap terbaca ketika JSON event terpecah pada network chunk; - delta dikirim berurutan dan hasil lengkap dikembalikan provider; -- `ask` dan fresh AI interpretation `analyze` memakai streaming jika tersedia; +- fresh AI interpretation `analyze` memakai streaming jika tersedia; - hasil lengkap `analyze` tetap disimpan ke snapshot; - `--json` memakai completion penuh dan tidak memanggil streaming. @@ -346,8 +338,6 @@ Manual live check: ```powershell $env:GROQ_API_KEY="gsk_your_key" pnpm dev:cli -- analyze --fresh -pnpm dev:cli -- ask "explain the main architecture" -pnpm dev:cli -- ask "explain the main architecture" --json | ConvertFrom-Json Remove-Item Env:GROQ_API_KEY ``` @@ -767,7 +757,6 @@ Jalankan: ```powershell npx devmap init npx devmap analyze --fresh -npx devmap ask "jelaskan struktur dan alur utama project ini" npx devmap doctor ``` @@ -827,7 +816,6 @@ Kemudian jalankan kembali: ```powershell npx devmap analyze --fresh -npx devmap ask "pertanyaan pengujian" ``` ### F. Cleanup Project Uji @@ -889,7 +877,6 @@ Sekarang command dapat dijalankan dari project mana pun: ```powershell devmap --version devmap analyze --fresh -devmap ask "jelaskan project ini" devmap doctor ``` @@ -924,8 +911,6 @@ Flow minimum: devmap init devmap analyze --fresh devmap analyze -devmap ask "Bagaimana autentikasi bekerja?" -devmap ask "Jelaskan struktur database dalam tabel" devmap doctor ``` diff --git a/docs/generated-files.md b/docs/generated-files.md index 874fb02..74f0152 100644 --- a/docs/generated-files.md +++ b/docs/generated-files.md @@ -25,8 +25,8 @@ Purpose: Direct AI agents to DevMap. Generated `DEVMAP.md` tells AI agents to use command-level `--json` output -instead of parsing decorated terminal text. This applies to `analyze`, `ask`, -and `doctor`, while `init --json` is intended for non-interactive setup with an +instead of parsing decorated terminal text. This applies to `analyze` and +`doctor`, while `init --json` is intended for non-interactive setup with an environment API key. Its navigation contract uses this order: @@ -100,8 +100,7 @@ devmap analyze Purpose: -- Project snapshot -- Source of truth for ask +- Project snapshot - Full reusable AI context archive and debugging data Regenerated when project files change or when `devmap analyze --fresh` is used. diff --git a/docs/releasing.md b/docs/releasing.md index 3876beb..4a12795 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -31,7 +31,7 @@ git diff --check Confirm: -- live Groq `init`, `analyze`, `ask`, streaming, and `doctor` were tested; +- live Groq `init`, `analyze`, streaming, and `doctor` were tested; - GitHub Actions is green on Windows, macOS, and Linux; - `packages/cli/package.json` and CLI `--version` both report `0.1.0`; - no API key, `.env`, `.devmap`, source test fixture, or local artifact is in diff --git a/docs/roadmap.md b/docs/roadmap.md index 088ea6a..20cdee6 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -44,7 +44,7 @@ adding AI on top. If the foundation is wrong, AI output will be wrong too. - All error scenarios handled (no raw stack traces) **Deliverable:** `devmap analyze` with AI interpretation, -`devmap ask` with context-aware answers, and `devmap onboarding` for a +`devmap onboarding` for a snapshot-based reading guide when the output is stable enough for `0.1.0`. --- @@ -119,7 +119,7 @@ Not planned. Not scheduled. Revisit when Phase 5 ships. |---|---|---| | 0.1.0 | 2 | Early beta with static analysis, Groq AI, JSON output, and streaming | | 0.2.0 | 2 | Feedback-driven reliability and analyzer improvements | -| 1.0.0 | 2 | Stable `devmap analyze` + `devmap ask` release | +| 1.0.0 | 2 | Stable `devmap analyze` release | | 1.1.0 | 2 | Performance improvements, cache optimization | | 1.2.0 | 2 | Express support solidified | | 2.0.0 | 3 | `devmap docs` + expanded onboarding | diff --git a/packages/cli/README.md b/packages/cli/README.md index ddb0854..14d2c02 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -4,7 +4,7 @@ Understand any codebase in minutes, not days. DevMap is a CLI that combines static analysis with optional AI-powered interpretation. It maps project structure, generates reusable context, and -answers focused questions without sending an entire repository to an AI model. +maps project structure and generates reusable context without sending an entire repository to an AI model. Version `0.1.0` is an early beta focused on Next.js and Express projects. @@ -34,7 +34,6 @@ Run these commands from the root of the project you want to understand: ```bash devmap init devmap analyze -devmap ask "How does authentication work?" devmap onboarding devmap doctor ``` @@ -93,7 +92,6 @@ devmap init devmap analyze devmap analyze --deep devmap analyze --fresh -devmap ask "Where is payment logic handled?" devmap onboarding --write devmap onboarding --write --language id devmap doctor @@ -132,7 +130,6 @@ Use `--json` for scripts, editors, CI, or AI agents: ```bash devmap analyze --json -devmap ask "Where is authentication handled?" --json devmap onboarding --json devmap doctor --json ``` @@ -152,7 +149,6 @@ support promise. - Project analysis runs locally before AI interpretation. - Full repository source is not sent to the selected provider. -- `ask` selects a small set of relevant files. - `.env` files and common generated directories are ignored. - API keys are stored locally and should never be committed. diff --git a/packages/cli/src/ai/groq.ts b/packages/cli/src/ai/groq.ts index a5f57ed..a7f749f 100644 --- a/packages/cli/src/ai/groq.ts +++ b/packages/cli/src/ai/groq.ts @@ -14,18 +14,12 @@ const MAX_RATE_LIMIT_DELAY_MS = 10_000; const MAX_RATE_LIMIT_RETRIES = 3; export const DEFAULT_AI_MODELS = { - ask: "llama-3.1-8b-instant", analyze: "openai/gpt-oss-20b", deepAnalyze: "openai/gpt-oss-120b", fallback: "openai/gpt-oss-20b" } as const; export const DEFAULT_AI_FALLBACKS = { - ask: [ - "qwen/qwen3.6-27b", - "llama-3.3-70b-versatile", - "openai/gpt-oss-20b" - ], analyze: [ "qwen/qwen3.6-27b", "llama-3.3-70b-versatile", diff --git a/packages/cli/src/ai/prompts.ts b/packages/cli/src/ai/prompts.ts index 2de989a..0f1153d 100644 --- a/packages/cli/src/ai/prompts.ts +++ b/packages/cli/src/ai/prompts.ts @@ -1,40 +1,6 @@ import type { ProjectMap } from "../analyzers/projectMap.js"; -import type { QuestionContext } from "./contextBuilder.js"; import type { AiMessage } from "./types.js"; -export type AskProjectSummary = Pick - & { frameworks?: ProjectMap["project"]["frameworks"] }; - -export function buildQueryExpansionMessages(query: string): AiMessage[] { - return [ - { - role: "system", - content: [ - "You expand developer questions into retrieval terms for a codebase navigator.", - "Return a JSON array only.", - "Max 10 terms.", - "Each term must be 1-3 words.", - "Prefer concrete code concepts, file name fragments, function name fragments, and implementation patterns.", - "Avoid vague terms such as data, logic, handler, service, feature, app, or page unless directly relevant.", - "Do not include framework-specific guesses unless the query explicitly mentions that framework.", - "Do not invent project-specific files.", - "Keep terms generic enough to work across repositories.", - "Include original important query terms when useful." - ].join(" ") - }, - { - role: "user", - content: [ - "Given this developer query:", - "", - JSON.stringify(query), - "", - "List technical terms, patterns, file name fragments, function name fragments, or code concepts that a developer would likely use to implement or locate this in a codebase." - ].join("\n") - } - ]; -} - export function buildAnalyzeMessages( snapshot: ProjectMap, deep = false @@ -75,70 +41,3 @@ export function buildAnalyzeMessages( } ]; } - -export function buildAskMessages( - context: QuestionContext, - project: AskProjectSummary -): AiMessage[] { - const fileContext = context.files - .map((file) => [ - `FILE: ${file.path}`, - `LINES: ${file.startLine}-${file.endLine}`, - `EXPORTS: ${file.exports.length > 0 ? file.exports.join(", ") : "none detected"}`, - `TOP_FUNCTIONS: ${file.topFunctions.length > 0 ? JSON.stringify(file.topFunctions) : "not extracted yet"}`, - `PURPOSE: ${file.purpose ?? "not inferred yet"}`, - `RELEVANCE: ${file.reasons.join("; ")}`, - "CONTENT:", - file.content - ].join("\n")) - .join("\n\n---\n\n") || "No files passed the minimum relevance threshold."; - - return [ - { - role: "system", - content: [ - "You are DevMap, a codebase understanding assistant.", - "Answer using only the supplied DevMap context.", - "Do not invent files, functions, flows, or behavior.", - "Only mention files as existing files when they appear as FILE entries in SELECTED CONTEXT.", - "If you infer a path that is not listed in SELECTED CONTEXT, label it as a suggested new or possible file, not an existing file.", - "If the context is insufficient, say what is missing.", - "Use RETRIEVAL_CONFIDENCE and TOP_SCORE to judge how strongly the selected files match the question.", - "Use EXPANDED_TERMS as inferred retrieval hints, not as confirmed project facts.", - "If matches came primarily through expanded terms, say these files appear related based on inferred concepts although the exact term was not found.", - "If RETRIEVAL_CONFIDENCE is low, do not claim the selected files are correct.", - "For low confidence, explicitly say no strong matches were found, explain that the requested concept may not exist in the current snapshot, and offer investigation paths or likely architectural entry points.", - "If RETRIEVAL_CONFIDENCE is high, be direct and mention exact files and exported functions when available.", - "Answer in the same language as the user's question.", - "Cite relevant file paths and explain relationships clearly.", - "Do not restate the question.", - "Do not repeat the same sentence, section, or file list.", - "Keep the answer concise and practical.", - "Start with the direct answer in one short paragraph.", - "Use a Key Files section with `path` - role bullets when files matter.", - "Use an Evidence section only when relationships or flow need explanation.", - "Use a Limits section only when the supplied context is insufficient.", - "Do not include long code examples unless the user explicitly asks for code.", - "For implementation guidance, describe the smallest next change and the existing file or function to inspect first.", - "Prefer existing supplied files over inventing new files; propose a new file only when the context clearly supports it." - ].join(" ") - }, - { - role: "user", - content: [ - `PROJECT: ${project.name}`, - `FRAMEWORK: ${project.framework}`, - `WORKSPACE_FRAMEWORKS: ${project.frameworks?.join(", ") || "none"}`, - `INTENT: ${context.intent}`, - `KEYWORDS: ${context.keywords.length > 0 ? context.keywords.join(", ") : "none"}`, - `EXPANDED_TERMS: ${context.expandedTerms.length > 0 ? context.expandedTerms.join(", ") : "none"}`, - `RETRIEVAL_CONFIDENCE: ${context.confidence}`, - `TOP_SCORE: ${context.topScore}`, - `QUESTION: ${context.question}`, - "", - "SELECTED CONTEXT:", - fileContext - ].join("\n") - } - ]; -} diff --git a/packages/cli/src/ai/provider.ts b/packages/cli/src/ai/provider.ts index 050cca5..9caba3d 100644 --- a/packages/cli/src/ai/provider.ts +++ b/packages/cli/src/ai/provider.ts @@ -12,7 +12,7 @@ import { OPENROUTER_FREE_MODEL } from "./openrouter.js"; -export type AiTask = "ask" | "analyze" | "deepAnalyze"; +export type AiTask = "analyze" | "deepAnalyze"; export type ProviderInspection = { reachable: true; modelAvailable: boolean }; export function createAiClient(config: DevmapConfig): AiClient { diff --git a/packages/cli/src/analyzers/featureDetector.ts b/packages/cli/src/analyzers/featureDetector.ts index 77a0759..0c949ee 100644 --- a/packages/cli/src/analyzers/featureDetector.ts +++ b/packages/cli/src/analyzers/featureDetector.ts @@ -78,7 +78,7 @@ const ROLE_FEATURES: Array<{ role: "cli-command", name: "CLI Commands", purpose: "Contains command entry points that orchestrate DevMap behavior.", - terms: ["cli", "command", "analyze", "ask", "init", "doctor"] + terms: ["cli", "command", "analyze", "init", "doctor"] }, { role: "snapshot-engine", diff --git a/packages/cli/src/commands/ask.ts b/packages/cli/src/commands/ask.ts deleted file mode 100644 index 3925646..0000000 --- a/packages/cli/src/commands/ask.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { buildQuestionContext } from "../ai/contextBuilder.js"; -import { completeWithOptionalStreaming } from "../ai/completion.js"; -import { - createAiClient as createDefaultAiClient, - providerDisplayName, - resolveAiRouting -} from "../ai/provider.js"; -import { buildAskMessages, buildQueryExpansionMessages } from "../ai/prompts.js"; -import type { AiClient } from "../ai/types.js"; -import { inspectSnapshot, isSnapshotStale } from "../cache/snapshot.js"; -import { readConfig, type DevmapConfig } from "../utils/config.js"; -import { DevmapError } from "../utils/errors.js"; -import { analyzeCommand } from "./analyze.js"; -import { output, withJsonOutput } from "../utils/output.js"; - -const MEDIUM_RELEVANCE_SCORE = 40; - -export type AskDependencies = { - json?: boolean; - projectRoot?: string; - loadConfig?: () => Promise; - createAiClient?: (config: DevmapConfig) => AiClient; -}; - -export async function askCommand( - questionParts: string[], - dependencies: AskDependencies = {} -): Promise { - if (dependencies.json) { - await withJsonOutput(async () => { - output.json(await runAsk(questionParts, dependencies)); - }); - return; - } - - await runAsk(questionParts, dependencies); -} - -async function runAsk( - questionParts: string[], - dependencies: AskDependencies -): Promise> { - const question = questionParts.join(" ").trim(); - if (!question) { - output.error("Please include a question."); - return { status: "error", error: "Please include a question." }; - } - - const projectRoot = dependencies.projectRoot ?? process.cwd(); - let snapshotResult = await inspectSnapshot(projectRoot); - - if (snapshotResult.status === "corrupt" || snapshotResult.status === "unsupported") { - output.warning("The existing snapshot cannot be used. Running quick analyze first."); - await analyzeCommand("."); - snapshotResult = await inspectSnapshot(projectRoot); - } else if (snapshotResult.status === "missing") { - output.warning("No snapshot found. Running quick analyze first."); - await analyzeCommand("."); - snapshotResult = await inspectSnapshot(projectRoot); - } - - if (snapshotResult.status !== "valid") { - output.error("Could not create snapshot."); - return { status: "error", error: "Could not create snapshot." }; - } - - const snapshot = snapshotResult.snapshot; - if (await isSnapshotStale(projectRoot, snapshot)) { - output.warning("Snapshot is stale: this answer may use outdated project structure."); - output.note("Run devmap analyze --fresh, then repeat devmap ask for the latest result."); - } - - const loadConfig = dependencies.loadConfig ?? readConfig; - const config = await loadConfig(); - const createAiClient = dependencies.createAiClient - ?? createDefaultAiClient; - const routing = resolveAiRouting(config ?? { - provider: "groq", - model: "auto" - }, "ask"); - const model = routing.model; - const client = config?.apiKey ? createAiClient(config) : null; - let context = await buildQuestionContext( - projectRoot, - snapshot, - question - ); - if (client && context.topScore < MEDIUM_RELEVANCE_SCORE) { - const expandedTerms = await expandQuestionTerms( - client, - question, - model, - routing.fallbackModels - ); - if (expandedTerms.length > 0) { - context = await buildQuestionContext( - projectRoot, - snapshot, - question, - { expandedTerms } - ); - } - } - - output.section("Relevant Files"); - if (context.confidence === "low") { - output.warning("No strong file matches found in the current snapshot."); - for (const file of context.files) { - output.item(`${file.path} (weak match)`); - } - printLowConfidenceAnswer(context); - return { - status: "low_confidence", - question, - intent: context.intent, - keywords: context.keywords, - expandedTerms: context.expandedTerms, - confidence: context.confidence, - topScore: context.topScore, - relevantFiles: serializeContextFiles(context.files), - answer: buildLowConfidenceAnswer(context), - model: null, - usage: null - }; - } else { - for (const file of context.files) { - output.item(file.path); - } - } - - if (!config?.apiKey || !client) { - output.warning("AI answering is not configured yet."); - output.note("Run devmap init to configure an AI provider API key."); - printStaticContext(context.files); - return { - status: "static", - question, - intent: context.intent, - keywords: context.keywords, - expandedTerms: context.expandedTerms, - confidence: context.confidence, - topScore: context.topScore, - relevantFiles: serializeContextFiles(context.files), - answer: null, - model: null, - usage: null - }; - } - - output.step(`Asking ${providerDisplayName(config.provider)} with ${model}`); - - try { - const execution = await completeWithOptionalStreaming(client, { - messages: buildAskMessages(context, snapshot.project), - model, - fallbackModels: routing.fallbackModels, - maxCompletionTokens: 1200, - temperature: 0.2 - }, !dependencies.json, () => output.section("Answer")); - const answer = execution.result; - - if (!execution.streamed) { - output.section("Answer"); - output.markdown(answer.content); - } - output.note(formatUsage(answer.model, answer.usage)); - return { - status: "ok", - question, - intent: context.intent, - keywords: context.keywords, - expandedTerms: context.expandedTerms, - confidence: context.confidence, - topScore: context.topScore, - relevantFiles: serializeContextFiles(context.files), - answer: answer.content, - model: answer.model, - usage: answer.usage ?? null - }; - } catch (error) { - if (!(error instanceof DevmapError)) { - throw error; - } - - output.warning(error.message); - if (error.hint) { - output.note(`Tip: ${error.hint}`); - } - output.note("Showing selected source context instead."); - printStaticContext(context.files); - return { - status: "fallback", - question, - intent: context.intent, - keywords: context.keywords, - expandedTerms: context.expandedTerms, - confidence: context.confidence, - topScore: context.topScore, - relevantFiles: serializeContextFiles(context.files), - answer: null, - model, - usage: null, - error: error.message, - hint: error.hint ?? null - }; - } -} - -async function expandQuestionTerms( - client: AiClient, - question: string, - model: string, - fallbackModels: readonly string[] -): Promise { - try { - const result = await client.complete({ - messages: buildQueryExpansionMessages(question), - model, - fallbackModels, - maxCompletionTokens: 180, - temperature: 0 - }); - - return parseExpandedTerms(result.content); - } catch { - return []; - } -} - -function parseExpandedTerms(content: string): string[] { - let parsed: unknown; - - try { - parsed = JSON.parse(content); - } catch { - return []; - } - - if (!Array.isArray(parsed)) { - return []; - } - - return parsed - .filter((item): item is string => typeof item === "string") - .slice(0, 10); -} - -function printLowConfidenceAnswer( - context: Awaited> -): void { - output.section("Answer"); - output.markdown(buildLowConfidenceAnswer(context)); -} - -function buildLowConfidenceAnswer( - context: Awaited> -): string { - const target = context.keywords.length > 0 - ? context.keywords.slice(0, 4).join(", ") - : context.question; - - return [ - `No strong matching files found for "${target}".`, - "", - "The current snapshot does not contain strong evidence for that concept, so DevMap will not guess an existing implementation.", - "The behavior may not exist yet, or the snapshot may be stale.", - "Next investigation paths:", - "- Run `devmap analyze --fresh` if the project changed.", - "- Try a more specific code term, route name, package name, or folder name.", - "- If this is a new feature, start from the closest existing entry point, route, command, or UI area after confirming it exists in the project." - ].join("\n"); -} - -function serializeContextFiles( - files: Awaited>["files"] -): Array> { - return files.map((file) => ({ - path: file.path, - score: file.score, - reasons: file.reasons, - exports: file.exports, - topFunctions: file.topFunctions, - purpose: file.purpose ?? null, - startLine: file.startLine, - endLine: file.endLine, - truncated: file.truncated - })); -} - -function printStaticContext( - files: Awaited>["files"] -): void { - output.section("Static Context"); - - for (const file of files.slice(0, 3)) { - const previewLines = file.content.split(/\r?\n/).slice(0, 24); - const previewEndLine = file.startLine + previewLines.length - 1; - const lineRange = file.startLine === previewEndLine - ? `line ${file.startLine}` - : `lines ${file.startLine}-${previewEndLine}`; - output.section(`${file.path} (${lineRange})`); - output.codeBlock(previewLines.join("\n")); - } -} - -function formatUsage( - model: string, - usage: Awaited>["usage"] -): string { - if (!usage) { - return `Model: ${model}`; - } - - return [ - `Model: ${model}`, - `Prompt tokens: ${usage.promptTokens}`, - `Completion tokens: ${usage.completionTokens}`, - `Total tokens: ${usage.totalTokens}` - ].join(" | "); -} diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index bc875d6..6df27aa 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -61,7 +61,7 @@ async function runDoctor( const frameworks = detectFrameworks(files); const project = detectProjectMetadata(projectRoot, framework, files, frameworks); const selectedModel = config - ? resolveAiRouting(config, "ask").model + ? resolveAiRouting(config, "analyze").model : undefined; const issues: string[] = []; const nodeSupported = readNodeMajor(process.version) >= MINIMUM_NODE_MAJOR; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 8f6222a..56fbd58 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,7 +1,6 @@ #!/usr/bin/env node import { Command } from "commander"; import { analyzeCommand } from "./commands/analyze.js"; -import { askCommand } from "./commands/ask.js"; import { configModelCommand } from "./commands/config.js"; import { doctorCommand } from "./commands/doctor.js"; import { initCommand } from "./commands/init.js"; @@ -33,13 +32,6 @@ program .option("--json", "output machine-readable JSON") .action((target, options) => analyzeCommand(target, options)); -program - .command("ask") - .description("Find files relevant to a codebase question") - .argument("", "question to ask") - .option("--json", "output machine-readable JSON") - .action((question, options) => askCommand(question, { json: options.json })); - program .command("onboarding") .alias("onboard") diff --git a/packages/cli/test/ai-client.test.ts b/packages/cli/test/ai-client.test.ts index f834232..c9c5e44 100644 --- a/packages/cli/test/ai-client.test.ts +++ b/packages/cli/test/ai-client.test.ts @@ -8,8 +8,7 @@ import { GroqClient, type GroqClientDependencies } from "../src/ai/groq.js"; -import { buildAnalyzeMessages, buildAskMessages } from "../src/ai/prompts.js"; -import type { QuestionContext } from "../src/ai/contextBuilder.js"; +import { buildAnalyzeMessages } from "../src/ai/prompts.js"; import { createProjectMap } from "../src/analyzers/projectMap.js"; import { DevmapError } from "../src/utils/errors.js"; @@ -19,7 +18,7 @@ test("Groq client returns normalized content and token usage", async () => { fetch: async (url, init) => { requests.push({ url: String(url), init }); return jsonResponse({ - model: DEFAULT_AI_MODELS.ask, + model: DEFAULT_AI_MODELS.deepAnalyze, choices: [{ message: { content: "Authentication uses a session handler." } }], usage: { prompt_tokens: 120, @@ -32,13 +31,13 @@ test("Groq client returns normalized content and token usage", async () => { const result = await client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.ask + model: DEFAULT_AI_MODELS.deepAnalyze }); assert.equal(requests.length, 1); assert.equal(requests[0]?.url, "https://api.groq.com/openai/v1/chat/completions"); assert.equal(result.content, "Authentication uses a session handler."); - assert.equal(result.model, DEFAULT_AI_MODELS.ask); + assert.equal(result.model, DEFAULT_AI_MODELS.deepAnalyze); assert.deepEqual(result.usage, { promptTokens: 120, completionTokens: 18, @@ -61,10 +60,10 @@ test("Groq client streams split SSE deltas and returns the complete result", asy return new Response(new ReadableStream({ start(controller) { controller.enqueue(encoder.encode( - 'data: {"model":"llama-3.1-8b-instant","choices":[{"delta":{"content":"Auth"}}]}\n' + 'data: {"model":"openai/gpt-oss-120b","choices":[{"delta":{"content":"Auth"}}]}\n' )); controller.enqueue(encoder.encode( - '\ndata: {"model":"llama-3.1-8b-instant","choices":[{"delta":{"content":" works."}}],"usage":{"prompt_tokens":10,"completion_tokens":2,"total_tokens":12}}\n\n' + '\ndata: {"model":"openai/gpt-oss-120b","choices":[{"delta":{"content":" works."}}],"usage":{"prompt_tokens":10,"completion_tokens":2,"total_tokens":12}}\n\n' )); controller.enqueue(encoder.encode("data: [DONE]\n\n")); controller.close(); @@ -77,14 +76,14 @@ test("Groq client streams split SSE deltas and returns the complete result", asy const result = await client.stream({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.ask + model: DEFAULT_AI_MODELS.deepAnalyze }, (delta) => { deltas.push(delta); }); assert.deepEqual(deltas, ["Auth", " works."]); assert.equal(result.content, "Auth works."); - assert.equal(result.model, DEFAULT_AI_MODELS.ask); + assert.equal(result.model, DEFAULT_AI_MODELS.deepAnalyze); assert.deepEqual(result.usage, { promptTokens: 10, completionTokens: 2, @@ -107,7 +106,7 @@ test("Groq client retries rate limits with exponential backoff", async () => { } return jsonResponse({ - model: DEFAULT_AI_MODELS.ask, + model: DEFAULT_AI_MODELS.deepAnalyze, choices: [{ message: { content: "Recovered." } }] }); }, @@ -119,7 +118,7 @@ test("Groq client retries rate limits with exponential backoff", async () => { const result = await client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.ask + model: DEFAULT_AI_MODELS.deepAnalyze }); assert.equal(requestCount, 4); @@ -146,7 +145,7 @@ test("Groq client stops after three rate-limit retries", async () => { await assert.rejects( client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.ask + model: DEFAULT_AI_MODELS.deepAnalyze }), (error: unknown) => error instanceof DevmapError && /rate limit reached after retrying/i.test(error.message) @@ -163,7 +162,7 @@ test("Groq client falls back when the primary model is unavailable", async () => const body = JSON.parse(String(init?.body)) as { model: string }; requestedModels.push(body.model); - if (body.model === DEFAULT_AI_MODELS.ask) { + if (body.model === DEFAULT_AI_MODELS.deepAnalyze) { return jsonResponse( { error: { message: "The model is not available." } }, 404 @@ -179,12 +178,12 @@ test("Groq client falls back when the primary model is unavailable", async () => const result = await client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.ask, + model: DEFAULT_AI_MODELS.deepAnalyze, fallbackModel: DEFAULT_AI_MODELS.fallback }); assert.deepEqual(requestedModels, [ - DEFAULT_AI_MODELS.ask, + DEFAULT_AI_MODELS.deepAnalyze, DEFAULT_AI_MODELS.fallback ]); assert.equal(result.content, "Fallback answer."); @@ -194,13 +193,13 @@ test("Groq client falls back when the primary model is unavailable", async () => test("Groq client follows an ordered fallback chain after unavailable and rate-limited models", async () => { const requestedModels: string[] = []; const delays: number[] = []; - const [qwenModel, versatileModel] = DEFAULT_AI_FALLBACKS.ask; + const [qwenModel, versatileModel] = DEFAULT_AI_FALLBACKS.deepAnalyze; const client = new GroqClient("gsk_test", { fetch: async (_url, init) => { const body = JSON.parse(String(init?.body)) as { model: string }; requestedModels.push(body.model); - if (body.model === DEFAULT_AI_MODELS.ask) { + if (body.model === DEFAULT_AI_MODELS.deepAnalyze) { return jsonResponse( { error: { message: "The model is not available." } }, 404 @@ -226,12 +225,12 @@ test("Groq client follows an ordered fallback chain after unavailable and rate-l const result = await client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.ask, - fallbackModels: DEFAULT_AI_FALLBACKS.ask + model: DEFAULT_AI_MODELS.deepAnalyze, + fallbackModels: DEFAULT_AI_FALLBACKS.deepAnalyze }); assert.deepEqual(requestedModels, [ - DEFAULT_AI_MODELS.ask, + DEFAULT_AI_MODELS.deepAnalyze, qwenModel, qwenModel, qwenModel, @@ -250,7 +249,7 @@ test("Groq client removes duplicate models from the fallback chain", async () => const body = JSON.parse(String(init?.body)) as { model: string }; requestedModels.push(body.model); - if (body.model === DEFAULT_AI_MODELS.ask) { + if (body.model === DEFAULT_AI_MODELS.deepAnalyze) { return jsonResponse( { error: { message: "The model is not available." } }, 404 @@ -266,9 +265,9 @@ test("Groq client removes duplicate models from the fallback chain", async () => await client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.ask, + model: DEFAULT_AI_MODELS.deepAnalyze, fallbackModels: [ - DEFAULT_AI_MODELS.ask, + DEFAULT_AI_MODELS.deepAnalyze, DEFAULT_AI_MODELS.fallback, DEFAULT_AI_MODELS.fallback ], @@ -276,7 +275,7 @@ test("Groq client removes duplicate models from the fallback chain", async () => }); assert.deepEqual(requestedModels, [ - DEFAULT_AI_MODELS.ask, + DEFAULT_AI_MODELS.deepAnalyze, DEFAULT_AI_MODELS.fallback ]); }); @@ -284,7 +283,7 @@ test("Groq client removes duplicate models from the fallback chain", async () => test("Groq streaming follows the fallback chain before emitting deltas", async () => { const requestedModels: string[] = []; const deltas: string[] = []; - const fallbackModel = DEFAULT_AI_FALLBACKS.ask[0]; + const fallbackModel = DEFAULT_AI_FALLBACKS.deepAnalyze[0]; const encoder = new TextEncoder(); const client = new GroqClient("gsk_test", { fetch: async (_url, init) => { @@ -295,7 +294,7 @@ test("Groq streaming follows the fallback chain before emitting deltas", async ( requestedModels.push(body.model); assert.equal(body.stream, true); - if (body.model === DEFAULT_AI_MODELS.ask) { + if (body.model === DEFAULT_AI_MODELS.deepAnalyze) { return jsonResponse( { error: { message: "The model is not available." } }, 404 @@ -318,13 +317,13 @@ test("Groq streaming follows the fallback chain before emitting deltas", async ( const result = await client.stream({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.ask, - fallbackModels: DEFAULT_AI_FALLBACKS.ask + model: DEFAULT_AI_MODELS.deepAnalyze, + fallbackModels: DEFAULT_AI_FALLBACKS.deepAnalyze }, (delta) => { deltas.push(delta); }); - assert.deepEqual(requestedModels, [DEFAULT_AI_MODELS.ask, fallbackModel]); + assert.deepEqual(requestedModels, [DEFAULT_AI_MODELS.deepAnalyze, fallbackModel]); assert.deepEqual(deltas, ["Fallback stream."]); assert.equal(result.model, fallbackModel); }); @@ -344,8 +343,8 @@ test("Groq client does not fall back after authentication errors", async () => { await assert.rejects( client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.ask, - fallbackModels: DEFAULT_AI_FALLBACKS.ask + model: DEFAULT_AI_MODELS.deepAnalyze, + fallbackModels: DEFAULT_AI_FALLBACKS.deepAnalyze }), (error: unknown) => error instanceof DevmapError && /API key is invalid/i.test(error.message) @@ -365,7 +364,7 @@ test("Groq client maps invalid credentials to an actionable error", async () => await assert.rejects( client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.ask + model: DEFAULT_AI_MODELS.deepAnalyze }), (error: unknown) => error instanceof DevmapError && /API key is invalid/i.test(error.message) @@ -373,54 +372,6 @@ test("Groq client maps invalid credentials to an actionable error", async () => ); }); -test("ask prompt grounds the answer in snapshot context and preserves language", () => { - const context: QuestionContext = { - question: "Bagaimana autentikasi bekerja?", - intent: "explain", - keywords: ["auth", "session"], - expandedTerms: ["middleware"], - confidence: "high", - topScore: 72, - relevantFiles: [], - files: [ - { - path: "lib/auth.ts", - score: 20, - reasons: ["evidence for Authentication"], - exports: ["getSession"], - topFunctions: [], - startLine: 1, - endLine: 4, - truncated: false, - content: "export async function getSession() {\n return auth();\n}" - } - ] - }; - - const messages = buildAskMessages(context, { - projectName: "fixture", - framework: "nextjs" - }); - - assert.match(messages[0]?.content ?? "", /only the supplied DevMap context/i); - assert.match(messages[0]?.content ?? "", /same language/i); - assert.match(messages[0]?.content ?? "", /Do not restate the question/i); - assert.match(messages[0]?.content ?? "", /Do not repeat/i); - assert.match(messages[0]?.content ?? "", /Only mention files as existing files/i); - assert.match(messages[0]?.content ?? "", /suggested new or possible file/i); - assert.match(messages[0]?.content ?? "", /EXPANDED_TERMS/i); - assert.match(messages[0]?.content ?? "", /Key Files/); - assert.match(messages[0]?.content ?? "", /Limits/); - assert.match(messages[0]?.content ?? "", /existing supplied files/i); - assert.match(messages[1]?.content ?? "", /INTENT: explain/); - assert.match(messages[1]?.content ?? "", /EXPANDED_TERMS: middleware/); - assert.match(messages[1]?.content ?? "", /RETRIEVAL_CONFIDENCE: high/); - assert.match(messages[1]?.content ?? "", /EXPORTS: getSession/); - assert.match(messages[1]?.content ?? "", /Bagaimana autentikasi bekerja/); - assert.match(messages[1]?.content ?? "", /lib\/auth\.ts/); - assert.match(messages[1]?.content ?? "", /getSession/); -}); - test("analyze prompt contains structured snapshot facts without raw source", async () => { const testDirectory = dirname(fileURLToPath(import.meta.url)); const snapshot = await createProjectMap(join(testDirectory, "fixtures", "nextjs-project")); diff --git a/packages/cli/test/ask-command.test.ts b/packages/cli/test/ask-command.test.ts deleted file mode 100644 index 9633e9a..0000000 --- a/packages/cli/test/ask-command.test.ts +++ /dev/null @@ -1,323 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import test from "node:test"; -import { - DEFAULT_AI_FALLBACKS, - DEFAULT_AI_MODELS -} from "../src/ai/groq.js"; -import type { - AiClient, - AiCompletionRequest, - AiCompletionResult -} from "../src/ai/types.js"; -import { createProjectMap } from "../src/analyzers/projectMap.js"; -import { saveSnapshot } from "../src/cache/snapshot.js"; -import { askCommand } from "../src/commands/ask.js"; -import { DevmapError } from "../src/utils/errors.js"; - -test("ask command uses configured AI client and prints token usage", async () => { - const projectRoot = await createAskProject(); - const requests: AiCompletionRequest[] = []; - const client: AiClient = { - async complete(request): Promise { - requests.push(request); - if (request.messages[0]?.content.includes("Return a JSON array only")) { - return { - content: "[\"auth\", \"session\"]", - model: request.model - }; - } - - return { - content: "## Authentication\n\nAuthentication is handled in **`auth.ts`**.", - model: request.model, - usage: { - promptTokens: 100, - completionTokens: 12, - totalTokens: 112 - } - }; - } - }; - - try { - const logs = await captureOutput(() => askCommand( - ["Bagaimana", "autentikasi", "bekerja?"], - { - projectRoot, - loadConfig: async () => ({ - provider: "groq", - apiKey: "gsk_fixture", - model: "auto" - }), - createAiClient: () => client - } - )); - - assert.equal(requests.length, 1); - assert.equal(requests[0]?.model, DEFAULT_AI_MODELS.ask); - assert.deepEqual(requests[0]?.fallbackModels, DEFAULT_AI_FALLBACKS.ask); - assert.match(requests[0]?.messages[1]?.content ?? "", /EXPANDED_TERMS: none/); - assert.match(requests[0]?.messages[1]?.content ?? "", /auth\.ts/); - const plainLogs = stripAnsi(logs); - assert.equal(countMatches(plainLogs, /Relevant Files/g), 1); - assert.equal(countMatches(plainLogs, /Asking Groq/g), 1); - assert.equal(countMatches(plainLogs, /^Answer$/gm), 1); - assert.match(plainLogs, /[•*]\s+auth\.ts/); - assert.doesNotMatch(plainLogs, /path matches|export matches|dependency matches/); - assert.match(plainLogs, /Authentication\n-+/); - assert.match(plainLogs, /Authentication is handled in auth\.ts/); - assert.doesNotMatch(plainLogs, /\*\*|`/); - assert.match(logs, /Total tokens: 112/); - } finally { - await rm(projectRoot, { recursive: true, force: true }); - } -}); - -test("ask command streams AI paragraphs when the client supports streaming", async () => { - const projectRoot = await createAskProject(); - let completeCalls = 0; - let streamCalls = 0; - const client: AiClient = { - async complete(request): Promise { - completeCalls += 1; - if (request.messages[0]?.content.includes("Return a JSON array only")) { - return { - content: "[\"auth\"]", - model: request.model - }; - } - - throw new Error("complete should only be used for query expansion"); - }, - async stream(request, onDelta): Promise { - streamCalls += 1; - onDelta("## Authentication\n\n"); - onDelta("Authentication uses `auth.ts`."); - return { - content: "## Authentication\n\nAuthentication uses `auth.ts`.", - model: request.model - }; - } - }; - - try { - const logs = stripAnsi(await captureOutput(() => askCommand( - ["where", "is", "auth"], - { - projectRoot, - loadConfig: async () => ({ - provider: "groq", - apiKey: "gsk_fixture", - model: "auto" - }), - createAiClient: () => client - } - ))); - - assert.equal(streamCalls, 1); - assert.equal(completeCalls, 0); - assert.match(logs, /Authentication\n-+/); - assert.match(logs, /Authentication uses auth\.ts\./); - } finally { - await rm(projectRoot, { recursive: true, force: true }); - } -}); - -test("ask command clearly warns when the snapshot is stale", async () => { - const projectRoot = await createAskProject(); - const client: AiClient = { - async complete(request): Promise { - return { - content: "Authentication uses `auth.ts`.", - model: request.model - }; - } - }; - - try { - await writeFile( - join(projectRoot, "auth.ts"), - "export async function getSession() { return { user: 'changed' }; }\n", - "utf8" - ); - - const logs = stripAnsi(await captureOutput(() => askCommand( - ["where", "is", "auth"], - { - projectRoot, - loadConfig: async () => ({ - provider: "groq", - apiKey: "gsk_fixture", - model: "auto" - }), - createAiClient: () => client - } - ))); - - assert.match(logs, /Snapshot is stale/i); - assert.match(logs, /devmap analyze --fresh/i); - } finally { - await rm(projectRoot, { recursive: true, force: true }); - } -}); - -test("ask command answers low-confidence questions locally without calling AI", async () => { - const projectRoot = await createAskProject(); - const requests: AiCompletionRequest[] = []; - const client: AiClient = { - async complete(request): Promise { - requests.push(request); - return { - content: "[\"payments\"]", - model: request.model - }; - } - }; - - try { - const logs = stripAnsi(await captureOutput(() => askCommand( - ["If", "I", "want", "to", "add", "payments,", "where", "should", "I", "start?"], - { - projectRoot, - loadConfig: async () => ({ - provider: "groq", - apiKey: "gsk_fixture", - model: "auto" - }), - createAiClient: () => client - } - ))); - - assert.equal(requests.length, 1); - assert.match(requests[0]?.messages[0]?.content ?? "", /Return a JSON array only/); - assert.match(logs, /No strong file matches found/i); - assert.match(logs, /No strong matching files found for ".*payments/i); - assert.doesNotMatch(logs, /auth\.ts/); - assert.doesNotMatch(logs, /Asking Groq/); - } finally { - await rm(projectRoot, { recursive: true, force: true }); - } -}); - -test("ask command falls back safely when weak query expansion returns invalid JSON", async () => { - const projectRoot = await createAskProject(); - const requests: AiCompletionRequest[] = []; - const client: AiClient = { - async complete(request): Promise { - requests.push(request); - if (request.messages[0]?.content.includes("Return a JSON array only")) { - return { - content: "auth, session", - model: request.model - }; - } - - return { - content: "Authentication uses `auth.ts`.", - model: request.model - }; - } - }; - - try { - const logs = stripAnsi(await captureOutput(() => askCommand( - ["where", "is", "revenue"], - { - projectRoot, - loadConfig: async () => ({ - provider: "groq", - apiKey: "gsk_fixture", - model: "auto" - }), - createAiClient: () => client - } - ))); - - assert.equal(requests.length, 1); - assert.match(requests[0]?.messages[0]?.content ?? "", /Return a JSON array only/); - assert.match(logs, /No strong file matches found/i); - assert.doesNotMatch(logs, /Asking Groq/); - } finally { - await rm(projectRoot, { recursive: true, force: true }); - } -}); - -test("ask command falls back to static context after actionable AI errors", async () => { - const projectRoot = await createAskProject(); - const client: AiClient = { - async complete(): Promise { - throw new DevmapError( - "Groq rate limit reached after retrying.", - "Try again later." - ); - } - }; - - try { - const logs = await captureOutput(() => askCommand( - ["where", "is", "auth"], - { - projectRoot, - loadConfig: async () => ({ - provider: "groq", - apiKey: "gsk_fixture", - model: "auto" - }), - createAiClient: () => client - } - )); - - assert.match(logs, /rate limit reached/i); - assert.match(logs, /Static Context/); - assert.match(logs, /auth\.ts/); - assert.doesNotMatch(logs, /\sat\s.*\(/); - } finally { - await rm(projectRoot, { recursive: true, force: true }); - } -}); - -async function createAskProject(): Promise { - const projectRoot = await mkdtemp(join(tmpdir(), "devmap-ask-test-")); - await writeFile( - join(projectRoot, "package.json"), - JSON.stringify({ name: "ask-fixture" }), - "utf8" - ); - await writeFile( - join(projectRoot, "auth.ts"), - "export async function getSession() { return { user: 'fixture' }; }\n", - "utf8" - ); - - const snapshot = await createProjectMap(projectRoot); - await saveSnapshot(projectRoot, snapshot); - return projectRoot; -} - -function stripAnsi(value: string): string { - return value.replace(/\u001B\[[0-9;]*m/g, ""); -} - -function countMatches(value: string, pattern: RegExp): number { - return value.match(pattern)?.length ?? 0; -} - -async function captureOutput(action: () => Promise): Promise { - const logs: string[] = []; - const originalLog = console.log; - const originalError = console.error; - - console.log = (...values: unknown[]) => logs.push(values.join(" ")); - console.error = (...values: unknown[]) => logs.push(values.join(" ")); - - try { - await action(); - return logs.join("\n"); - } finally { - console.log = originalLog; - console.error = originalError; - } -} diff --git a/packages/cli/test/doctor.test.ts b/packages/cli/test/doctor.test.ts index cb681a2..1537605 100644 --- a/packages/cli/test/doctor.test.ts +++ b/packages/cli/test/doctor.test.ts @@ -41,7 +41,7 @@ test("doctor reports project, provider, model, and snapshot diagnostics", async assert.match(logs, /Framework\s+express/); assert.match(logs, /Provider\s+groq/); assert.match(logs, /API key\s+valid/); - assert.match(logs, /Model\s+llama-3\.1-8b-instant/); + assert.match(logs, /Model\s+openai\/gpt-oss-20b/); assert.match(logs, /Snapshot\s+valid/); assert.match(logs, /No issues found/); assert.doesNotMatch(logs, /gsk_fixture/); diff --git a/packages/cli/test/json-output.test.ts b/packages/cli/test/json-output.test.ts index d7c5759..344b7ba 100644 --- a/packages/cli/test/json-output.test.ts +++ b/packages/cli/test/json-output.test.ts @@ -3,11 +3,9 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import type { AiClient } from "../src/ai/types.js"; import { createProjectMap } from "../src/analyzers/projectMap.js"; import { saveSnapshot } from "../src/cache/snapshot.js"; import { analyzeCommand } from "../src/commands/analyze.js"; -import { askCommand } from "../src/commands/ask.js"; import { configModelCommand } from "../src/commands/config.js"; import { doctorCommand } from "../src/commands/doctor.js"; import { initCommand } from "../src/commands/init.js"; @@ -31,67 +29,6 @@ test("analyze --json emits one parseable snapshot document", async () => { } }); -test("ask --json emits answer, relevant files, model, and usage", async () => { - const projectRoot = await createProject("json-ask"); - const snapshot = await createProjectMap(projectRoot); - await saveSnapshot(projectRoot, snapshot); - let completeCalls = 0; - let streamCalls = 0; - const client: AiClient = { - async complete(request) { - completeCalls += 1; - if (request.messages[0]?.content.includes("Return a JSON array only")) { - return { - content: "[\"startup\"]", - model: request.model - }; - } - - return { - content: "The entry point is index.ts.", - model: request.model, - usage: { - promptTokens: 20, - completionTokens: 8, - totalTokens: 28 - } - }; - }, - async stream() { - streamCalls += 1; - throw new Error("JSON output must not use streaming"); - } - }; - - try { - const output = await captureStdout(() => askCommand( - ["where", "is", "the", "entry", "point"], - { - json: true, - projectRoot, - loadConfig: async () => ({ - provider: "groq", - apiKey: "gsk_fixture", - model: "auto" - }), - createAiClient: () => client - } - )); - const payload = parseSingleJson(output); - - assert.equal(payload.status, "ok"); - assert.equal(payload.answer, "The entry point is index.ts."); - assert.equal(payload.model, "llama-3.1-8b-instant"); - assert.equal(payload.usage.totalTokens, 28); - assert.deepEqual(payload.expandedTerms, []); - assert.ok(Array.isArray(payload.relevantFiles)); - assert.equal(completeCalls, 1); - assert.equal(streamCalls, 0); - } finally { - await rm(projectRoot, { recursive: true, force: true }); - } -}); - test("doctor and config JSON outputs contain no formatting noise", async () => { const projectRoot = await createProject("json-doctor"); let savedModel = ""; diff --git a/packages/cli/test/openrouter-client.test.ts b/packages/cli/test/openrouter-client.test.ts index 3888060..67c44f6 100644 --- a/packages/cli/test/openrouter-client.test.ts +++ b/packages/cli/test/openrouter-client.test.ts @@ -95,7 +95,7 @@ test("OpenRouter automatic routing defaults safely to the free router", () => { provider: "openrouter", apiKey: "sk-or-fixture", model: "auto" - }, "ask"), { + }, "analyze"), { model: OPENROUTER_FREE_MODEL, fallbackModels: [] });