From 5c44fcfa420103e106a12eb50ee78c67589d9801 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Sat, 2 May 2026 18:03:33 +0300 Subject: [PATCH 1/7] =?UTF-8?q?refactor(audit):=20lift=20resolveAuditBasel?= =?UTF-8?q?ines=20cmd-audit=20=E2=86=92=20audit-engine=20(Tracer=201=20of?= =?UTF-8?q?=205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure move — function logic unchanged. Closes one of the 5 layer-reversal imports application/mcp-server.ts had on cli/* (called out in PR #35 self-audit). Now MCP server imports resolveAuditBaselines from the engine alongside runAudit, like the proper cmd-* ↔ *-engine seam. cmd-audit.ts test imports updated; CLI handler still calls the function (now via engine import). No behavior change. --- src/application/audit-engine.ts | 28 +++++++++++++++++++++++ src/application/mcp-server.ts | 3 +-- src/cli/cmd-audit.test.ts | 3 ++- src/cli/cmd-audit.ts | 39 +++++---------------------------- 4 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/application/audit-engine.ts b/src/application/audit-engine.ts index ea5a9b84..23bdcc24 100644 --- a/src/application/audit-engine.ts +++ b/src/application/audit-engine.ts @@ -119,6 +119,34 @@ export const V1_DELTAS: readonly AuditDeltaSpec[] = [ */ export type AuditBaselineMap = Partial>; +/** + * Compose the `AuditBaselineMap` from CLI / MCP arg shapes. Per-delta + * explicit names override auto-resolved slots. Auto-resolved slots that + * don't exist in `query_baselines` are silently absent — the delta just + * doesn't run. Same shape both `cmd-audit.ts` and the MCP `audit` tool + * call before handing off to {@link runAudit}. + */ +export function resolveAuditBaselines(opts: { + db: CodemapDatabase; + baselinePrefix: string | undefined; + perDelta: Record; +}): AuditBaselineMap { + const map: AuditBaselineMap = {}; + for (const spec of V1_DELTAS) { + if (opts.baselinePrefix !== undefined) { + const candidate = `${opts.baselinePrefix}-${spec.key}`; + if (getQueryBaseline(opts.db, candidate) !== undefined) { + map[spec.key] = candidate; + } + } + } + // Per-delta flags override the auto-resolved slot for that key. + for (const [key, name] of Object.entries(opts.perDelta)) { + map[key] = name; + } + return map; +} + /** * Run an audit against the per-delta baseline mapping. Each requested delta * (key present in `baselines`) loads its baseline, validates column-set diff --git a/src/application/mcp-server.ts b/src/application/mcp-server.ts index 3f3d0fa3..035d77f5 100644 --- a/src/application/mcp-server.ts +++ b/src/application/mcp-server.ts @@ -15,7 +15,6 @@ import { resolveAgentsTemplateDir } from "../agents-init"; // them here as pure data / pure functions (no execution flow crosses // cli → application). A future refactor may lift them to `src/application/` // once a second consumer (HTTP API) needs them. -import { resolveAuditBaselines } from "../cli/cmd-audit"; import { buildContextEnvelope } from "../cli/cmd-context"; import { buildShowResult } from "../cli/cmd-show"; import { buildSnippetResult } from "../cli/cmd-snippet"; @@ -39,7 +38,7 @@ import { GROUP_BY_MODES } from "../group-by"; import type { GroupByMode } from "../group-by"; import { configureResolver } from "../resolver"; import { getProjectRoot, getTsconfigPath, initCodemap } from "../runtime"; -import { runAudit } from "./audit-engine"; +import { resolveAuditBaselines, runAudit } from "./audit-engine"; import { getCurrentCommit } from "./index-engine"; import { executeQuery } from "./query-engine"; import { runCodemapIndex } from "./run-index"; diff --git a/src/cli/cmd-audit.test.ts b/src/cli/cmd-audit.test.ts index 586958c0..52a31c02 100644 --- a/src/cli/cmd-audit.test.ts +++ b/src/cli/cmd-audit.test.ts @@ -1,9 +1,10 @@ import { describe, expect, it } from "bun:test"; +import { resolveAuditBaselines } from "../application/audit-engine"; import { createTables, upsertQueryBaseline } from "../db"; import type { CodemapDatabase } from "../db"; import { openCodemapDatabase } from "../sqlite-db"; -import { parseAuditRest, resolveAuditBaselines } from "./cmd-audit"; +import { parseAuditRest } from "./cmd-audit"; function freshDb(): CodemapDatabase { const db = openCodemapDatabase(":memory:"); diff --git a/src/cli/cmd-audit.ts b/src/cli/cmd-audit.ts index d29e7b24..1c701240 100644 --- a/src/cli/cmd-audit.ts +++ b/src/cli/cmd-audit.ts @@ -1,12 +1,12 @@ -import { runAudit, V1_DELTAS } from "../application/audit-engine"; -import type { - AuditBaselineMap, - AuditEnvelope, +import { + resolveAuditBaselines, + runAudit, + V1_DELTAS, } from "../application/audit-engine"; +import type { AuditEnvelope } from "../application/audit-engine"; import { runCodemapIndex } from "../application/run-index"; import { loadUserConfig, resolveCodemapConfig } from "../config"; -import { closeDb, getQueryBaseline, openDb } from "../db"; -import type { CodemapDatabase } from "../db"; +import { closeDb, openDb } from "../db"; import { configureResolver } from "../resolver"; import { getProjectRoot, getTsconfigPath, initCodemap } from "../runtime"; @@ -146,33 +146,6 @@ function consumeFlagValue( return { kind: "value", value: next, next: i + 2 }; } -/** - * Compose the `AuditBaselineMap` from a CLI parse result. Per-delta explicit - * flags override auto-resolved slots. Auto-resolved slots that don't exist in - * `query_baselines` are silently absent (the slot just has no baseline → the - * delta doesn't run). - */ -export function resolveAuditBaselines(opts: { - db: CodemapDatabase; - baselinePrefix: string | undefined; - perDelta: Record; -}): AuditBaselineMap { - const map: AuditBaselineMap = {}; - for (const spec of V1_DELTAS) { - if (opts.baselinePrefix !== undefined) { - const candidate = `${opts.baselinePrefix}-${spec.key}`; - if (getQueryBaseline(opts.db, candidate) !== undefined) { - map[spec.key] = candidate; - } - } - } - // Per-delta flags override the auto-resolved slot for that key. - for (const [key, name] of Object.entries(opts.perDelta)) { - map[key] = name; - } - return map; -} - /** * Print **`codemap audit`** usage + flags to stdout. */ From c24b6fe53be54ec6430b6bec6c2cf63c6117ae6e Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Sat, 2 May 2026 18:05:29 +0300 Subject: [PATCH 2/7] =?UTF-8?q?refactor(recipes):=20lift=20cli/query-recip?= =?UTF-8?q?es=20=E2=86=92=20application/query-recipes=20(Tracer=202a=20of?= =?UTF-8?q?=205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-req for lifting buildContextEnvelope in Tracer 2b — query-recipes is engine-shaped (no CLI args, just exports for both CLI and MCP), so its location in cli/ was a misfile. git mv preserves history; only changes are the import paths in 6 callers and the relative paths inside the moved files. --- scripts/query-golden.ts | 2 +- src/application/mcp-server.ts | 12 ++++++------ src/{cli => application}/query-recipes.test.ts | 0 src/{cli => application}/query-recipes.ts | 8 ++++---- src/benchmark-default-scenarios.ts | 2 +- src/cli/cmd-context.ts | 2 +- src/cli/cmd-query.test.ts | 4 ++-- src/cli/cmd-query.ts | 14 +++++++------- 8 files changed, 22 insertions(+), 22 deletions(-) rename src/{cli => application}/query-recipes.test.ts (100%) rename src/{cli => application}/query-recipes.ts (96%) diff --git a/scripts/query-golden.ts b/scripts/query-golden.ts index 4093a77f..b6e498da 100644 --- a/scripts/query-golden.ts +++ b/scripts/query-golden.ts @@ -4,7 +4,7 @@ import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { createCodemap } from "../src/api"; -import { getQueryRecipeSql } from "../src/cli/query-recipes"; +import { getQueryRecipeSql } from "../src/application/query-recipes"; import { parseScenariosJson } from "./query-golden/schema"; import type { GoldenMatch, GoldenScenario } from "./query-golden/schema"; diff --git a/src/application/mcp-server.ts b/src/application/mcp-server.ts index 035d77f5..9ded79f4 100644 --- a/src/application/mcp-server.ts +++ b/src/application/mcp-server.ts @@ -19,12 +19,6 @@ import { buildContextEnvelope } from "../cli/cmd-context"; import { buildShowResult } from "../cli/cmd-show"; import { buildSnippetResult } from "../cli/cmd-snippet"; import { computeValidateRows, toProjectRelative } from "../cli/cmd-validate"; -import { - getQueryRecipeActions, - getQueryRecipeCatalogEntry, - getQueryRecipeSql, - listQueryRecipeCatalog, -} from "../cli/query-recipes"; import { loadUserConfig, resolveCodemapConfig } from "../config"; import { closeDb, @@ -41,6 +35,12 @@ import { getProjectRoot, getTsconfigPath, initCodemap } from "../runtime"; import { resolveAuditBaselines, runAudit } from "./audit-engine"; import { getCurrentCommit } from "./index-engine"; import { executeQuery } from "./query-engine"; +import { + getQueryRecipeActions, + getQueryRecipeCatalogEntry, + getQueryRecipeSql, + listQueryRecipeCatalog, +} from "./query-recipes"; import { runCodemapIndex } from "./run-index"; import { findSymbolsByName } from "./show-engine"; diff --git a/src/cli/query-recipes.test.ts b/src/application/query-recipes.test.ts similarity index 100% rename from src/cli/query-recipes.test.ts rename to src/application/query-recipes.test.ts diff --git a/src/cli/query-recipes.ts b/src/application/query-recipes.ts similarity index 96% rename from src/cli/query-recipes.ts rename to src/application/query-recipes.ts index 4fdf87e6..e6a768ab 100644 --- a/src/cli/query-recipes.ts +++ b/src/application/query-recipes.ts @@ -2,12 +2,12 @@ import { existsSync, statSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { loadAllRecipes } from "../application/recipes-loader"; -import type { LoadedRecipe } from "../application/recipes-loader"; import { getProjectRoot } from "../runtime"; +import { loadAllRecipes } from "./recipes-loader"; +import type { LoadedRecipe } from "./recipes-loader"; -export type { RecipeAction } from "../application/recipes-loader"; -import type { RecipeAction } from "../application/recipes-loader"; +export type { RecipeAction } from "./recipes-loader"; +import type { RecipeAction } from "./recipes-loader"; /** * Catalog entry surfaced to `--recipes-json`, the `codemap://recipes` MCP diff --git a/src/benchmark-default-scenarios.ts b/src/benchmark-default-scenarios.ts index a81bcfda..4c535967 100644 --- a/src/benchmark-default-scenarios.ts +++ b/src/benchmark-default-scenarios.ts @@ -1,9 +1,9 @@ +import { getQueryRecipeSql } from "./application/query-recipes"; import { globFilesFiltered, readAll, traditionalFanoutImportLines, } from "./benchmark-common"; -import { getQueryRecipeSql } from "./cli/query-recipes"; import type { CodemapDatabase } from "./db"; import { getProjectRoot } from "./runtime"; diff --git a/src/cli/cmd-context.ts b/src/cli/cmd-context.ts index c75bcffd..02b2cabb 100644 --- a/src/cli/cmd-context.ts +++ b/src/cli/cmd-context.ts @@ -1,10 +1,10 @@ +import { QUERY_RECIPES } from "../application/query-recipes"; import { loadUserConfig, resolveCodemapConfig } from "../config"; import { closeDb, getMeta, openDb, SCHEMA_VERSION } from "../db"; import type { CodemapDatabase } from "../db"; import { configureResolver } from "../resolver"; import { getProjectRoot, getTsconfigPath, initCodemap } from "../runtime"; import { CODEMAP_VERSION } from "../version"; -import { QUERY_RECIPES } from "./query-recipes"; /** * Snapshot envelope emitted by `codemap context`. Stable JSON shape any agent diff --git a/src/cli/cmd-query.test.ts b/src/cli/cmd-query.test.ts index a2e644e4..6ac52a56 100644 --- a/src/cli/cmd-query.test.ts +++ b/src/cli/cmd-query.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from "bun:test"; -import { parseQueryRest } from "./cmd-query"; import { getQueryRecipeActions, getQueryRecipeSql, listQueryRecipeCatalog, -} from "./query-recipes"; +} from "../application/query-recipes"; +import { parseQueryRest } from "./cmd-query"; describe("parseQueryRest", () => { it("errors when only query", () => { diff --git a/src/cli/cmd-query.ts b/src/cli/cmd-query.ts index 59f62885..e47ede0b 100644 --- a/src/cli/cmd-query.ts +++ b/src/cli/cmd-query.ts @@ -3,6 +3,13 @@ import { printQueryResult, queryRows, } from "../application/index-engine"; +import { + getQueryRecipeActions, + getQueryRecipeSql, + listQueryRecipeCatalog, + listQueryRecipeIds, + QUERY_RECIPES, +} from "../application/query-recipes"; import { loadUserConfig, resolveCodemapConfig } from "../config"; import { closeDb, @@ -26,13 +33,6 @@ import { } from "../group-by"; import { configureResolver } from "../resolver"; import { getProjectRoot, getTsconfigPath, initCodemap } from "../runtime"; -import { - getQueryRecipeActions, - getQueryRecipeSql, - listQueryRecipeCatalog, - listQueryRecipeIds, - QUERY_RECIPES, -} from "./query-recipes"; /** * Parse `argv` after the global bootstrap: `rest[0]` must be `"query"`. From d187cc9583f23411f078141118b5b00f1a1064e4 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Sat, 2 May 2026 18:07:33 +0300 Subject: [PATCH 3/7] =?UTF-8?q?refactor(context):=20lift=20buildContextEnv?= =?UTF-8?q?elope=20cmd-context=20=E2=86=92=20context-engine=20(Tracer=202b?= =?UTF-8?q?=20of=205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildContextEnvelope, classifyIntent, ContextEnvelope, readScalarInt are pure (DB read + envelope build, no I/O / argv / printing) — engine-shaped. Same pattern as audit-engine in Tracer 1. cmd-context.ts now holds parse/help/run only. mcp-server imports from the new engine instead of the CLI shell. --- src/application/context-engine.ts | 163 +++++++++++++++++++++++++++++ src/application/mcp-server.ts | 14 +-- src/cli/cmd-context.test.ts | 3 +- src/cli/cmd-context.ts | 166 +----------------------------- 4 files changed, 175 insertions(+), 171 deletions(-) create mode 100644 src/application/context-engine.ts diff --git a/src/application/context-engine.ts b/src/application/context-engine.ts new file mode 100644 index 00000000..e4f471ed --- /dev/null +++ b/src/application/context-engine.ts @@ -0,0 +1,163 @@ +import { getMeta, SCHEMA_VERSION } from "../db"; +import type { CodemapDatabase } from "../db"; +import { CODEMAP_VERSION } from "../version"; +import { QUERY_RECIPES } from "./query-recipes"; + +/** + * Snapshot envelope emitted by `codemap context`. Stable JSON shape any agent + * or CLI can pipe into a prompt without parsing prose. + */ +export interface ContextEnvelope { + codemap: { + cli_version: string; + schema_version: number; + }; + project: { + root: string; + file_count: number; + last_indexed_commit: string | null; + languages: { language: string; files: number }[]; + }; + hubs?: { to_path: string; fan_in: number }[]; + /** + * A flavor sample of TODO/FIXME/HACK/NOTE markers — the alphabetically-first + * 20 across the repo, ordered by `(file_path, line_number)`. Not a recency + * signal; for time-ordered output query `markers` directly, joining + * `files.last_modified`. + */ + sample_markers?: { + file_path: string; + line_number: number; + kind: string; + content: string; + }[]; + recipes: { id: string; description: string }[]; + intent?: { + input: string; + classified_as: string; + matched_recipes: string[]; + hint: string; + }; +} + +/** + * Map a free-text intent into a coarse category and a list of recipe ids + * worth running first. Pure regex matching — agents can override or ignore it. + */ +export function classifyIntent(intent: string): { + classified_as: string; + matched_recipes: string[]; + hint: string; +} { + const t = intent.toLowerCase(); + if (/refactor|rename|restructur|extract|move\b/.test(t)) { + return { + classified_as: "refactor", + matched_recipes: [ + "fan-in", + "fan-out", + "barrel-files", + "deprecated-symbols", + ], + hint: "Inspect fan-in / fan-out before moving symbols; barrel-files surfaces public-API hubs; deprecated-symbols flags risky callers.", + }; + } + if (/bug|fix|debug|error|crash|broken|regress/.test(t)) { + return { + classified_as: "debug", + matched_recipes: ["markers-by-kind", "fan-in", "deprecated-symbols"], + hint: "Markers (TODO/FIXME) and deprecated-symbols often hint at known gotchas; fan-in shows the blast radius of a change.", + }; + } + if (/test|coverage|spec|mock/.test(t)) { + return { + classified_as: "test", + matched_recipes: ["files-largest", "fan-in", "components-by-hooks"], + hint: "files-largest and fan-in surface high-leverage code worth testing first.", + }; + } + if (/add|implement|create|new feature|introduce|build/.test(t)) { + return { + classified_as: "feature", + matched_recipes: ["barrel-files", "components-by-hooks", "fan-out"], + hint: "barrel-files shows where new exports usually land; fan-out shows the dependency reach of starting points.", + }; + } + if (/explore|understand|read|tour|map|overview/.test(t)) { + return { + classified_as: "explore", + matched_recipes: [ + "index-summary", + "fan-in", + "files-largest", + "barrel-files", + ], + hint: "Start with index-summary for shape, fan-in for hubs, then drill into files-largest.", + }; + } + return { + classified_as: "other", + matched_recipes: ["index-summary", "fan-in", "markers-by-kind"], + hint: "No specific category matched — the index-summary / fan-in / markers triple is a safe default.", + }; +} + +/** + * Build the envelope from an open DB. Pure-ish (reads from DB but takes no I/O + * outside of it) — covered by unit tests against a temp DB. + */ +export function buildContextEnvelope( + db: CodemapDatabase, + projectRoot: string, + opts: { compact: boolean; intent: string | null }, +): ContextEnvelope { + const fileCount = readScalarInt(db, "SELECT COUNT(*) AS n FROM files"); + const lastCommit = getMeta(db, "last_indexed_commit") ?? null; + const languages = ( + db + .query( + "SELECT language, COUNT(*) AS files FROM files GROUP BY language ORDER BY files DESC, language ASC", + ) + .all() as { language: string; files: number }[] + ).map((r) => ({ language: r.language, files: r.files })); + + const envelope: ContextEnvelope = { + codemap: { + cli_version: CODEMAP_VERSION, + schema_version: SCHEMA_VERSION, + }, + project: { + root: projectRoot, + file_count: fileCount, + last_indexed_commit: lastCommit, + languages, + }, + recipes: Object.entries(QUERY_RECIPES).map(([id, meta]) => ({ + id, + description: meta.description, + })), + }; + + if (!opts.compact) { + envelope.hubs = db + .query(QUERY_RECIPES["fan-in"]!.sql) + .all() as ContextEnvelope["hubs"]; + envelope.sample_markers = db + .query( + "SELECT file_path, line_number, kind, content FROM markers ORDER BY file_path ASC, line_number ASC LIMIT 20", + ) + .all() as ContextEnvelope["sample_markers"]; + } + + if (opts.intent !== null) { + const cls = classifyIntent(opts.intent); + envelope.intent = { input: opts.intent, ...cls }; + } + + return envelope; +} + +function readScalarInt(db: CodemapDatabase, sql: string): number { + const row = db.query(sql).get() as { n?: number } | undefined; + return row?.n ?? 0; +} diff --git a/src/application/mcp-server.ts b/src/application/mcp-server.ts index 9ded79f4..319d6d40 100644 --- a/src/application/mcp-server.ts +++ b/src/application/mcp-server.ts @@ -9,13 +9,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { z } from "zod"; import { resolveAgentsTemplateDir } from "../agents-init"; -// Layer note: several modules below live under `src/cli/` because their CLI -// verb owns them today (`query-recipes`, `cmd-audit`'s baseline resolver, -// `cmd-context`'s envelope builder, `cmd-validate`'s row computer). We import -// them here as pure data / pure functions (no execution flow crosses -// cli → application). A future refactor may lift them to `src/application/` -// once a second consumer (HTTP API) needs them. -import { buildContextEnvelope } from "../cli/cmd-context"; import { buildShowResult } from "../cli/cmd-show"; import { buildSnippetResult } from "../cli/cmd-snippet"; import { computeValidateRows, toProjectRelative } from "../cli/cmd-validate"; @@ -33,6 +26,13 @@ import type { GroupByMode } from "../group-by"; import { configureResolver } from "../resolver"; import { getProjectRoot, getTsconfigPath, initCodemap } from "../runtime"; import { resolveAuditBaselines, runAudit } from "./audit-engine"; +// Layer note: several modules below live under `src/cli/` because their CLI +// verb owns them today (`query-recipes`, `cmd-audit`'s baseline resolver, +// `cmd-context`'s envelope builder, `cmd-validate`'s row computer). We import +// them here as pure data / pure functions (no execution flow crosses +// cli → application). A future refactor may lift them to `src/application/` +// once a second consumer (HTTP API) needs them. +import { buildContextEnvelope } from "./context-engine"; import { getCurrentCommit } from "./index-engine"; import { executeQuery } from "./query-engine"; import { diff --git a/src/cli/cmd-context.test.ts b/src/cli/cmd-context.test.ts index b31a2526..601567df 100644 --- a/src/cli/cmd-context.test.ts +++ b/src/cli/cmd-context.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "bun:test"; -import { classifyIntent, parseContextRest } from "./cmd-context"; +import { classifyIntent } from "../application/context-engine"; +import { parseContextRest } from "./cmd-context"; describe("parseContextRest", () => { it("returns help for --help / -h", () => { diff --git a/src/cli/cmd-context.ts b/src/cli/cmd-context.ts index 02b2cabb..cf07282d 100644 --- a/src/cli/cmd-context.ts +++ b/src/cli/cmd-context.ts @@ -1,47 +1,9 @@ -import { QUERY_RECIPES } from "../application/query-recipes"; +import { buildContextEnvelope } from "../application/context-engine"; +import type { ContextEnvelope } from "../application/context-engine"; import { loadUserConfig, resolveCodemapConfig } from "../config"; -import { closeDb, getMeta, openDb, SCHEMA_VERSION } from "../db"; -import type { CodemapDatabase } from "../db"; +import { closeDb, openDb } from "../db"; import { configureResolver } from "../resolver"; import { getProjectRoot, getTsconfigPath, initCodemap } from "../runtime"; -import { CODEMAP_VERSION } from "../version"; - -/** - * Snapshot envelope emitted by `codemap context`. Stable JSON shape any agent - * or CLI can pipe into a prompt without parsing prose. - */ -export interface ContextEnvelope { - codemap: { - cli_version: string; - schema_version: number; - }; - project: { - root: string; - file_count: number; - last_indexed_commit: string | null; - languages: { language: string; files: number }[]; - }; - hubs?: { to_path: string; fan_in: number }[]; - /** - * A flavor sample of TODO/FIXME/HACK/NOTE markers — the alphabetically-first - * 20 across the repo, ordered by `(file_path, line_number)`. Not a recency - * signal; for time-ordered output query `markers` directly, joining - * `files.last_modified`. - */ - sample_markers?: { - file_path: string; - line_number: number; - kind: string; - content: string; - }[]; - recipes: { id: string; description: string }[]; - intent?: { - input: string; - classified_as: string; - matched_recipes: string[]; - hint: string; - }; -} interface ContextOpts { root: string; @@ -115,128 +77,6 @@ export function parseContextRest( return { kind: "run", compact, intent }; } -/** - * Map a free-text intent into a coarse category and a list of recipe ids - * worth running first. Pure regex matching — agents can override or ignore it. - */ -export function classifyIntent(intent: string): { - classified_as: string; - matched_recipes: string[]; - hint: string; -} { - const t = intent.toLowerCase(); - if (/refactor|rename|restructur|extract|move\b/.test(t)) { - return { - classified_as: "refactor", - matched_recipes: [ - "fan-in", - "fan-out", - "barrel-files", - "deprecated-symbols", - ], - hint: "Inspect fan-in / fan-out before moving symbols; barrel-files surfaces public-API hubs; deprecated-symbols flags risky callers.", - }; - } - if (/bug|fix|debug|error|crash|broken|regress/.test(t)) { - return { - classified_as: "debug", - matched_recipes: ["markers-by-kind", "fan-in", "deprecated-symbols"], - hint: "Markers (TODO/FIXME) and deprecated-symbols often hint at known gotchas; fan-in shows the blast radius of a change.", - }; - } - if (/test|coverage|spec|mock/.test(t)) { - return { - classified_as: "test", - matched_recipes: ["files-largest", "fan-in", "components-by-hooks"], - hint: "files-largest and fan-in surface high-leverage code worth testing first.", - }; - } - if (/add|implement|create|new feature|introduce|build/.test(t)) { - return { - classified_as: "feature", - matched_recipes: ["barrel-files", "components-by-hooks", "fan-out"], - hint: "barrel-files shows where new exports usually land; fan-out shows the dependency reach of starting points.", - }; - } - if (/explore|understand|read|tour|map|overview/.test(t)) { - return { - classified_as: "explore", - matched_recipes: [ - "index-summary", - "fan-in", - "files-largest", - "barrel-files", - ], - hint: "Start with index-summary for shape, fan-in for hubs, then drill into files-largest.", - }; - } - return { - classified_as: "other", - matched_recipes: ["index-summary", "fan-in", "markers-by-kind"], - hint: "No specific category matched — the index-summary / fan-in / markers triple is a safe default.", - }; -} - -/** - * Build the envelope from an open DB. Pure-ish (reads from DB but takes no I/O - * outside of it) — covered by unit tests against a temp DB. - */ -export function buildContextEnvelope( - db: CodemapDatabase, - projectRoot: string, - opts: { compact: boolean; intent: string | null }, -): ContextEnvelope { - const fileCount = readScalarInt(db, "SELECT COUNT(*) AS n FROM files"); - const lastCommit = getMeta(db, "last_indexed_commit") ?? null; - const languages = ( - db - .query( - "SELECT language, COUNT(*) AS files FROM files GROUP BY language ORDER BY files DESC, language ASC", - ) - .all() as { language: string; files: number }[] - ).map((r) => ({ language: r.language, files: r.files })); - - const envelope: ContextEnvelope = { - codemap: { - cli_version: CODEMAP_VERSION, - schema_version: SCHEMA_VERSION, - }, - project: { - root: projectRoot, - file_count: fileCount, - last_indexed_commit: lastCommit, - languages, - }, - recipes: Object.entries(QUERY_RECIPES).map(([id, meta]) => ({ - id, - description: meta.description, - })), - }; - - if (!opts.compact) { - envelope.hubs = db - .query(QUERY_RECIPES["fan-in"]!.sql) - .all() as ContextEnvelope["hubs"]; - envelope.sample_markers = db - .query( - "SELECT file_path, line_number, kind, content FROM markers ORDER BY file_path ASC, line_number ASC LIMIT 20", - ) - .all() as ContextEnvelope["sample_markers"]; - } - - if (opts.intent !== null) { - const cls = classifyIntent(opts.intent); - envelope.intent = { input: opts.intent, ...cls }; - } - - return envelope; -} - -function readScalarInt(db: CodemapDatabase, sql: string): number { - const row = db.query(sql).get() as { n?: number } | undefined; - return row?.n ?? 0; -} - /** * Initialize Codemap for `opts.root`, then print the context envelope as JSON. */ From 231195399732ad1aea33d6cd47542ecf33aaba06 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Sat, 2 May 2026 18:09:18 +0300 Subject: [PATCH 4/7] =?UTF-8?q?refactor(validate):=20lift=20computeValidat?= =?UTF-8?q?eRows=20+=20toProjectRelative=20cmd-validate=20=E2=86=92=20vali?= =?UTF-8?q?date-engine=20(Tracer=203=20of=205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both functions are pure (no argv, no printing — just DB rows + filesystem hash compare). toProjectRelative was already public-API for cmd-show / cmd-snippet (cross-CLI reuse) and the MCP show/snippet handlers; this lift removes the cli→cli import edge and the mcp→cli edge in one move. cmd-validate.ts now holds parse / help / run only. --- src/application/mcp-server.ts | 2 +- src/application/validate-engine.ts | 79 +++++++++++++++++++++++++++++ src/cli/cmd-show.ts | 2 +- src/cli/cmd-snippet.ts | 2 +- src/cli/cmd-validate.test.ts | 5 +- src/cli/cmd-validate.ts | 81 +----------------------------- 6 files changed, 87 insertions(+), 84 deletions(-) create mode 100644 src/application/validate-engine.ts diff --git a/src/application/mcp-server.ts b/src/application/mcp-server.ts index 319d6d40..6fd2dffb 100644 --- a/src/application/mcp-server.ts +++ b/src/application/mcp-server.ts @@ -11,7 +11,6 @@ import { z } from "zod"; import { resolveAgentsTemplateDir } from "../agents-init"; import { buildShowResult } from "../cli/cmd-show"; import { buildSnippetResult } from "../cli/cmd-snippet"; -import { computeValidateRows, toProjectRelative } from "../cli/cmd-validate"; import { loadUserConfig, resolveCodemapConfig } from "../config"; import { closeDb, @@ -43,6 +42,7 @@ import { } from "./query-recipes"; import { runCodemapIndex } from "./run-index"; import { findSymbolsByName } from "./show-engine"; +import { computeValidateRows, toProjectRelative } from "./validate-engine"; /** * MCP server engine — owns the tool / resource registry. CLI shell diff --git a/src/application/validate-engine.ts b/src/application/validate-engine.ts new file mode 100644 index 00000000..26b7f7fd --- /dev/null +++ b/src/application/validate-engine.ts @@ -0,0 +1,79 @@ +import { readFileSync } from "node:fs"; +import { isAbsolute, relative, resolve, sep } from "node:path"; + +import type { CodemapDatabase } from "../db"; +import { hashContent } from "../hash"; + +/** + * One row in the staleness report. `status` distinguishes the three cases an + * agent might want to act on differently. + */ +export interface ValidateRow { + path: string; + status: "stale" | "missing" | "unindexed"; +} + +/** + * Walk the indexed files (or the explicit `paths` set), comparing on-disk + * SHA-256 to `files.content_hash`. Returns rows that are out of sync. Pure + * function over an open DB and the project root — covered by unit tests. + */ +export function computeValidateRows( + db: CodemapDatabase, + projectRoot: string, + explicitPaths: string[], +): ValidateRow[] { + const indexed = db.query("SELECT path, content_hash FROM files").all() as { + path: string; + content_hash: string; + }[]; + + const indexByPath = new Map(); + for (const row of indexed) indexByPath.set(row.path, row.content_hash); + + const targets = + explicitPaths.length === 0 ? indexed.map((r) => r.path) : explicitPaths; + + const seen = new Set(); + const rows: ValidateRow[] = []; + for (const raw of targets) { + const rel = toProjectRelative(projectRoot, raw); + if (seen.has(rel)) continue; + seen.add(rel); + + const indexedHash = indexByPath.get(rel); + const abs = resolve(projectRoot, rel); + let source: string | undefined; + try { + source = readFileSync(abs, "utf8"); + } catch { + source = undefined; + } + + if (indexedHash === undefined) { + if (source !== undefined) rows.push({ path: rel, status: "unindexed" }); + continue; + } + if (source === undefined) { + rows.push({ path: rel, status: "missing" }); + continue; + } + if (hashContent(source) !== indexedHash) { + rows.push({ path: rel, status: "stale" }); + } + } + rows.sort((a, b) => a.path.localeCompare(b.path)); + return rows; +} + +/** + * Convert a CLI-supplied path to a project-relative POSIX-style key matching + * the `files.path` format stored in the index. `path.relative()` returns + * backslash-separated paths on Windows; the index always stores forward + * slashes (tinyglobby / Bun.Glob / git diff all emit POSIX), so we normalize + * here to make `indexByPath.get(rel)` succeed cross-platform. + */ +export function toProjectRelative(projectRoot: string, p: string): string { + const rel = isAbsolute(p) ? relative(projectRoot, p) : p; + return sep === "/" ? rel : rel.split(sep).join("/"); +} diff --git a/src/cli/cmd-show.ts b/src/cli/cmd-show.ts index 7c0b4bdc..b7719b09 100644 --- a/src/cli/cmd-show.ts +++ b/src/cli/cmd-show.ts @@ -1,10 +1,10 @@ import { findSymbolsByName } from "../application/show-engine"; import type { SymbolMatch } from "../application/show-engine"; +import { toProjectRelative } from "../application/validate-engine"; import { loadUserConfig, resolveCodemapConfig } from "../config"; import { closeDb, openDb } from "../db"; import { configureResolver } from "../resolver"; import { getProjectRoot, getTsconfigPath, initCodemap } from "../runtime"; -import { toProjectRelative } from "./cmd-validate"; /** * The catalog envelope returned by `show` — same shape both the CLI's diff --git a/src/cli/cmd-snippet.ts b/src/cli/cmd-snippet.ts index 3d3ec6bb..5167609e 100644 --- a/src/cli/cmd-snippet.ts +++ b/src/cli/cmd-snippet.ts @@ -4,12 +4,12 @@ import { readSymbolSource, } from "../application/show-engine"; import type { SymbolMatch } from "../application/show-engine"; +import { toProjectRelative } from "../application/validate-engine"; import { loadUserConfig, resolveCodemapConfig } from "../config"; import { closeDb, openDb } from "../db"; import type { CodemapDatabase } from "../db"; import { configureResolver } from "../resolver"; import { getProjectRoot, getTsconfigPath, initCodemap } from "../runtime"; -import { toProjectRelative } from "./cmd-validate"; /** * Per-match payload returned by `snippet` — extends the `show` row shape diff --git a/src/cli/cmd-validate.test.ts b/src/cli/cmd-validate.test.ts index 3adc3314..668e9ed5 100644 --- a/src/cli/cmd-validate.test.ts +++ b/src/cli/cmd-validate.test.ts @@ -3,12 +3,13 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { computeValidateRows } from "../application/validate-engine"; +import type { ValidateRow } from "../application/validate-engine"; import { resolveCodemapConfig } from "../config"; import { closeDb, openDb } from "../db"; import { hashContent } from "../hash"; import { initCodemap } from "../runtime"; -import { computeValidateRows, parseValidateRest } from "./cmd-validate"; -import type { ValidateRow } from "./cmd-validate"; +import { parseValidateRest } from "./cmd-validate"; let tmpRoot = ""; diff --git a/src/cli/cmd-validate.ts b/src/cli/cmd-validate.ts index c069afbc..6b99c890 100644 --- a/src/cli/cmd-validate.ts +++ b/src/cli/cmd-validate.ts @@ -1,22 +1,10 @@ -import { readFileSync } from "node:fs"; -import { isAbsolute, relative, resolve, sep } from "node:path"; - +import { computeValidateRows } from "../application/validate-engine"; +import type { ValidateRow } from "../application/validate-engine"; import { loadUserConfig, resolveCodemapConfig } from "../config"; import { closeDb, openDb } from "../db"; -import type { CodemapDatabase } from "../db"; -import { hashContent } from "../hash"; import { configureResolver } from "../resolver"; import { getProjectRoot, getTsconfigPath, initCodemap } from "../runtime"; -/** - * One row in the staleness report. `status` distinguishes the three cases an - * agent might want to act on differently. - */ -export interface ValidateRow { - path: string; - status: "stale" | "missing" | "unindexed"; -} - interface ValidateOpts { root: string; configFile: string | undefined; @@ -86,71 +74,6 @@ export function parseValidateRest( return { kind: "run", paths, json }; } -/** - * Walk the indexed files (or the explicit `paths` set), comparing on-disk - * SHA-256 to `files.content_hash`. Returns rows that are out of sync. Pure - * function over an open DB and the project root — covered by unit tests. - */ -export function computeValidateRows( - db: CodemapDatabase, - projectRoot: string, - explicitPaths: string[], -): ValidateRow[] { - const indexed = db.query("SELECT path, content_hash FROM files").all() as { - path: string; - content_hash: string; - }[]; - - const indexByPath = new Map(); - for (const row of indexed) indexByPath.set(row.path, row.content_hash); - - const targets = - explicitPaths.length === 0 ? indexed.map((r) => r.path) : explicitPaths; - - const seen = new Set(); - const rows: ValidateRow[] = []; - for (const raw of targets) { - const rel = toProjectRelative(projectRoot, raw); - if (seen.has(rel)) continue; - seen.add(rel); - - const indexedHash = indexByPath.get(rel); - const abs = resolve(projectRoot, rel); - let source: string | undefined; - try { - source = readFileSync(abs, "utf8"); - } catch { - source = undefined; - } - - if (indexedHash === undefined) { - if (source !== undefined) rows.push({ path: rel, status: "unindexed" }); - continue; - } - if (source === undefined) { - rows.push({ path: rel, status: "missing" }); - continue; - } - if (hashContent(source) !== indexedHash) { - rows.push({ path: rel, status: "stale" }); - } - } - rows.sort((a, b) => a.path.localeCompare(b.path)); - return rows; -} - -/** - * Convert a CLI-supplied path to a project-relative POSIX-style key matching - * the `files.path` format stored in the index. `path.relative()` returns - * backslash-separated paths on Windows; the index always stores forward - * slashes (tinyglobby / Bun.Glob / git diff all emit POSIX), so we normalize - * here to make `indexByPath.get(rel)` succeed cross-platform. - */ -export function toProjectRelative(projectRoot: string, p: string): string { - const rel = isAbsolute(p) ? relative(projectRoot, p) : p; - return sep === "/" ? rel : rel.split(sep).join("/"); -} - /** * Initialize Codemap for `opts.root`, then print the staleness report. * Sets **`process.exitCode`** to **1** if any rows are returned (mirrors From c663f259b8011a269bc7a3be3a5d9cfad8ad4df3 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Sat, 2 May 2026 18:11:28 +0300 Subject: [PATCH 5/7] =?UTF-8?q?refactor(show):=20lift=20buildShowResult=20?= =?UTF-8?q?+=20buildSnippetResult=20cmd-show/snippet=20=E2=86=92=20show-en?= =?UTF-8?q?gine=20(Tracer=204=20of=205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the last 2 layer-reversal imports application/mcp-server.ts had on cli/* (originally called out in PR #35 self-audit). Verified: `grep 'from "../cli/' src/application` is empty. Both envelope builders are pure (transform a SymbolMatch[] into a {matches, disambiguation?} shape; snippet additionally enriches via readSymbolSource). They sit naturally next to the engine functions that produce / read their inputs (findSymbolsByName, readSymbolSource, getIndexedContentHash). cmd-show.ts and cmd-snippet.ts now hold parse/help/run/render only — same cmd-* ↔ *-engine seam Tracers 1-3 established. --- src/application/mcp-server.ts | 8 ++- src/application/show-engine.ts | 110 +++++++++++++++++++++++++++++++++ src/cli/cmd-show.test.ts | 3 +- src/cli/cmd-show.ts | 42 +------------ src/cli/cmd-snippet.test.ts | 3 +- src/cli/cmd-snippet.ts | 78 +---------------------- 6 files changed, 123 insertions(+), 121 deletions(-) diff --git a/src/application/mcp-server.ts b/src/application/mcp-server.ts index 6fd2dffb..c508be12 100644 --- a/src/application/mcp-server.ts +++ b/src/application/mcp-server.ts @@ -9,8 +9,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { z } from "zod"; import { resolveAgentsTemplateDir } from "../agents-init"; -import { buildShowResult } from "../cli/cmd-show"; -import { buildSnippetResult } from "../cli/cmd-snippet"; import { loadUserConfig, resolveCodemapConfig } from "../config"; import { closeDb, @@ -41,7 +39,11 @@ import { listQueryRecipeCatalog, } from "./query-recipes"; import { runCodemapIndex } from "./run-index"; -import { findSymbolsByName } from "./show-engine"; +import { + buildShowResult, + buildSnippetResult, + findSymbolsByName, +} from "./show-engine"; import { computeValidateRows, toProjectRelative } from "./validate-engine"; /** diff --git a/src/application/show-engine.ts b/src/application/show-engine.ts index 798a5f95..d9263a24 100644 --- a/src/application/show-engine.ts +++ b/src/application/show-engine.ts @@ -172,3 +172,113 @@ export function getIndexedContentHash( .get(filePath) as { content_hash: string } | null; return row?.content_hash; } + +/** + * The catalog envelope returned by `show` — same shape both the CLI's + * `--json` mode and the MCP `show` tool surface (per plan §4 uniformity + * + Q-2 settled). Single match → `{matches: [{...}]}`; multi-match adds + * a structured `disambiguation` block so agents narrow without scanning + * every row. + */ +export interface ShowResult { + matches: SymbolMatch[]; + disambiguation?: { + n: number; + by_kind: Record; + files: string[]; + hint: string; + }; +} + +/** + * Build the `ShowResult` envelope from a list of matches. Single-match + * → `{matches}` only. Multi-match → adds a `disambiguation` block with + * structured aids so agents narrow without scanning every row. + */ +export function buildShowResult(matches: SymbolMatch[]): ShowResult { + if (matches.length <= 1) return { matches }; + const byKind: Record = {}; + for (const m of matches) byKind[m.kind] = (byKind[m.kind] ?? 0) + 1; + const files = Array.from(new Set(matches.map((m) => m.file_path))).sort(); + return { + matches, + disambiguation: { + n: matches.length, + by_kind: byKind, + files, + hint: "Multiple matches. Narrow with --kind or --in .", + }, + }; +} + +/** + * Per-match payload returned by `snippet` — extends the `show` row shape + * with the source text and stale-flag fields. Same row shape as + * `findSymbolsByName` returns plus three additive fields: + * `source` (the file lines from line_start..line_end), + * `stale` (true when the file's content_hash drifted since indexing), + * `missing` (true when the file no longer exists on disk). + */ +export interface SnippetMatch extends SymbolMatch { + source: string | undefined; + stale: boolean; + missing: boolean; +} + +/** + * The catalog envelope returned by `snippet` — same shape as `show`'s + * `ShowResult` (per Q-2 + Q-5: snippet adds source/stale/missing on each + * row but keeps the {matches, disambiguation?} envelope). Single match + * → `{matches: [{...}]}`; multi-match adds the structured disambiguation + * block. + */ +export interface SnippetResult { + matches: SnippetMatch[]; + disambiguation?: { + n: number; + by_kind: Record; + files: string[]; + hint: string; + }; +} + +/** + * Build the `SnippetResult` envelope from matches + per-match source reads. + * Mirrors `buildShowResult` but enriches each match with `source` / `stale` + * / `missing` fields read fresh from disk per plan §9 Q-6 (read + flag, + * no auto-reindex). + */ +export function buildSnippetResult(opts: { + db: CodemapDatabase; + matches: SymbolMatch[]; + projectRoot: string; +}): SnippetResult { + const enriched: SnippetMatch[] = opts.matches.map((m) => { + const indexedHash = getIndexedContentHash(opts.db, m.file_path); + const read = readSymbolSource({ + match: m, + projectRoot: opts.projectRoot, + indexedContentHash: indexedHash, + }); + return { + ...m, + source: read.source, + stale: read.stale, + missing: read.missing, + }; + }); + + if (enriched.length <= 1) return { matches: enriched }; + const byKind: Record = {}; + for (const m of enriched) byKind[m.kind] = (byKind[m.kind] ?? 0) + 1; + const files = Array.from(new Set(enriched.map((m) => m.file_path))).sort(); + return { + matches: enriched, + disambiguation: { + n: enriched.length, + by_kind: byKind, + files, + hint: "Multiple matches. Narrow with --kind or --in .", + }, + }; +} diff --git a/src/cli/cmd-show.test.ts b/src/cli/cmd-show.test.ts index 4b722654..29364ed8 100644 --- a/src/cli/cmd-show.test.ts +++ b/src/cli/cmd-show.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from "bun:test"; import type { SymbolMatch } from "../application/show-engine"; -import { buildShowResult, parseShowRest } from "./cmd-show"; +import { buildShowResult } from "../application/show-engine"; +import { parseShowRest } from "./cmd-show"; describe("parseShowRest", () => { it("returns help on --help / -h", () => { diff --git a/src/cli/cmd-show.ts b/src/cli/cmd-show.ts index b7719b09..53b13a5c 100644 --- a/src/cli/cmd-show.ts +++ b/src/cli/cmd-show.ts @@ -1,28 +1,11 @@ -import { findSymbolsByName } from "../application/show-engine"; -import type { SymbolMatch } from "../application/show-engine"; +import { buildShowResult, findSymbolsByName } from "../application/show-engine"; +import type { ShowResult, SymbolMatch } from "../application/show-engine"; import { toProjectRelative } from "../application/validate-engine"; import { loadUserConfig, resolveCodemapConfig } from "../config"; import { closeDb, openDb } from "../db"; import { configureResolver } from "../resolver"; import { getProjectRoot, getTsconfigPath, initCodemap } from "../runtime"; -/** - * The catalog envelope returned by `show` — same shape both the CLI's - * `--json` mode and the MCP `show` tool surface (per plan §4 uniformity - * + Q-2 settled). Single match → `{matches: [{...}]}`; multi-match adds - * a structured `disambiguation` block so agents narrow without scanning - * every row. - */ -export interface ShowResult { - matches: SymbolMatch[]; - disambiguation?: { - n: number; - by_kind: Record; - files: string[]; - hint: string; - }; -} - interface ShowOpts { root: string; configFile: string | undefined; @@ -143,27 +126,6 @@ export function parseShowRest(rest: string[]): return { kind: "run", name, kindFilter, inPath, json }; } -/** - * Build the `ShowResult` envelope from a list of matches. Single-match - * → `{matches}` only. Multi-match → adds a `disambiguation` block with - * structured aids so agents narrow without scanning every row. - */ -export function buildShowResult(matches: SymbolMatch[]): ShowResult { - if (matches.length <= 1) return { matches }; - const byKind: Record = {}; - for (const m of matches) byKind[m.kind] = (byKind[m.kind] ?? 0) + 1; - const files = Array.from(new Set(matches.map((m) => m.file_path))).sort(); - return { - matches, - disambiguation: { - n: matches.length, - by_kind: byKind, - files, - hint: "Multiple matches. Narrow with --kind or --in .", - }, - }; -} - /** * Run `codemap show `. Bootstraps codemap, opens db, looks up, * renders. Sets `process.exitCode` (no `process.exit`) so piped stdout diff --git a/src/cli/cmd-snippet.test.ts b/src/cli/cmd-snippet.test.ts index 48e7674d..d69d2bf0 100644 --- a/src/cli/cmd-snippet.test.ts +++ b/src/cli/cmd-snippet.test.ts @@ -3,11 +3,12 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { buildSnippetResult } from "../application/show-engine"; import { createTables } from "../db"; import type { CodemapDatabase } from "../db"; import { hashContent } from "../hash"; import { openCodemapDatabase } from "../sqlite-db"; -import { buildSnippetResult, parseSnippetRest } from "./cmd-snippet"; +import { parseSnippetRest } from "./cmd-snippet"; describe("parseSnippetRest", () => { it("returns help on --help / -h", () => { diff --git a/src/cli/cmd-snippet.ts b/src/cli/cmd-snippet.ts index 5167609e..7b4072a0 100644 --- a/src/cli/cmd-snippet.ts +++ b/src/cli/cmd-snippet.ts @@ -1,47 +1,14 @@ import { + buildSnippetResult, findSymbolsByName, - getIndexedContentHash, - readSymbolSource, } from "../application/show-engine"; -import type { SymbolMatch } from "../application/show-engine"; +import type { SnippetResult, SymbolMatch } from "../application/show-engine"; import { toProjectRelative } from "../application/validate-engine"; import { loadUserConfig, resolveCodemapConfig } from "../config"; import { closeDb, openDb } from "../db"; -import type { CodemapDatabase } from "../db"; import { configureResolver } from "../resolver"; import { getProjectRoot, getTsconfigPath, initCodemap } from "../runtime"; -/** - * Per-match payload returned by `snippet` — extends the `show` row shape - * with the source text and stale-flag fields. Same row shape as - * `findSymbolsByName` returns plus three additive fields: - * `source` (the file lines from line_start..line_end), - * `stale` (true when the file's content_hash drifted since indexing), - * `missing` (true when the file no longer exists on disk). - */ -export interface SnippetMatch extends SymbolMatch { - source: string | undefined; - stale: boolean; - missing: boolean; -} - -/** - * The catalog envelope returned by `snippet` — same shape as `show`'s - * `ShowResult` (per Q-2 + Q-5: snippet adds source/stale/missing on each - * row but keeps the {matches, disambiguation?} envelope). Single match - * → `{matches: [{...}]}`; multi-match adds the structured disambiguation - * block. - */ -export interface SnippetResult { - matches: SnippetMatch[]; - disambiguation?: { - n: number; - by_kind: Record; - files: string[]; - hint: string; - }; -} - interface SnippetOpts { root: string; configFile: string | undefined; @@ -170,47 +137,6 @@ export function parseSnippetRest(rest: string[]): return { kind: "run", name, kindFilter, inPath, json }; } -/** - * Build the `SnippetResult` envelope from matches + per-match source reads. - * Mirrors `buildShowResult` from `cmd-show.ts` but enriches each match with - * `source` / `stale` / `missing` fields read fresh from disk per Q-6 - * (read + flag, no auto-reindex). - */ -export function buildSnippetResult(opts: { - db: CodemapDatabase; - matches: SymbolMatch[]; - projectRoot: string; -}): SnippetResult { - const enriched: SnippetMatch[] = opts.matches.map((m) => { - const indexedHash = getIndexedContentHash(opts.db, m.file_path); - const read = readSymbolSource({ - match: m, - projectRoot: opts.projectRoot, - indexedContentHash: indexedHash, - }); - return { - ...m, - source: read.source, - stale: read.stale, - missing: read.missing, - }; - }); - - if (enriched.length <= 1) return { matches: enriched }; - const byKind: Record = {}; - for (const m of enriched) byKind[m.kind] = (byKind[m.kind] ?? 0) + 1; - const files = Array.from(new Set(enriched.map((m) => m.file_path))).sort(); - return { - matches: enriched, - disambiguation: { - n: enriched.length, - by_kind: byKind, - files, - hint: "Multiple matches. Narrow with --kind or --in .", - }, - }; -} - /** * Run `codemap snippet `. Mirrors `runShowCmd`'s shape — bootstrap, * lookup, render. JSON mode prints the envelope verbatim; terminal mode From f7cad292febcaa6fe3cbf89014585e81a9446968 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Sat, 2 May 2026 18:14:06 +0300 Subject: [PATCH 6/7] docs: sync architecture.md / research / benchmark to lifted application/* layering (Tracer 5 of 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - architecture.md table (line ~23) + 'Repository layout' (line ~100): expand application/ description to enumerate all engines (run-index, index-engine, query-engine, audit-engine, context-engine, validate-engine, show-engine, query-recipes, recipes-loader, mcp-server) + state the never-import-cli/ rule. - 'Validate / Audit / Context / Show-snippet / Recipes wiring' paragraphs: name the new engine files, point toProjectRelative at validate-engine, point QUERY_RECIPES at application/. - docs/research/competitive-scan-2026-04.md + fallow.md: dangling src/cli/query-recipes.ts links updated to src/application/query-recipes.ts; context/validate rows now name both shell + engine. - docs/benchmark.md: getQueryRecipeSql import path updated. - changeset: patch — internal refactor, no behavior / public API change. --- .changeset/lift-cli-to-application.md | 5 ++ docs/architecture.md | 64 +++++++++++------------ docs/benchmark.md | 2 +- docs/research/competitive-scan-2026-04.md | 12 ++--- docs/research/fallow.md | 12 ++--- 5 files changed, 50 insertions(+), 45 deletions(-) create mode 100644 .changeset/lift-cli-to-application.md diff --git a/.changeset/lift-cli-to-application.md b/.changeset/lift-cli-to-application.md new file mode 100644 index 00000000..10e41a47 --- /dev/null +++ b/.changeset/lift-cli-to-application.md @@ -0,0 +1,5 @@ +--- +"@stainless-code/codemap": patch +--- + +Internal refactor — lift `cli/*` envelope builders + path helpers into `application/*` engines so `application/mcp-server.ts` no longer reaches sideways into `cli/`. Affected modules: `audit-engine` (added `resolveAuditBaselines`), new `context-engine` (`buildContextEnvelope`, `classifyIntent`, `ContextEnvelope`), new `validate-engine` (`computeValidateRows`, `toProjectRelative`), `show-engine` (added `buildShowResult`, `buildSnippetResult`, `ShowResult`, `SnippetResult`, `SnippetMatch`), `query-recipes` moved from `cli/` to `application/`. CLI verbs stay shells (parse / help / run / render). No behavior change, no public API change — `cli/cmd-*` and `application/*` are internal modules; the published surface (`api.ts`, the `codemap` binary, the MCP server) is untouched. diff --git a/docs/architecture.md b/docs/architecture.md index 6d2965ef..fdb24cde 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -16,13 +16,13 @@ A local SQLite database (`.codemap.db`) indexes the project tree and stores stru ## Layering -| Layer | Role | -| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`cli/`** (`bootstrap`, `main`, `cmd-*`) | Parses argv; **dynamic `import()`** loads only the command chunk (`cmd-index`, `cmd-query`, `cmd-agents`) so `--help` / `version` / `agents init` avoid the indexer. | -| **`api.ts`** | Public programmatic surface: `createCodemap()`, `Codemap` (`query`, `index`), re-exports `runCodemapIndex` for advanced use. | -| **`application/`** | Use cases: `run-index.ts` (incremental / full / targeted orchestration), `index-engine.ts` (collect files, git diff, `indexFiles`, workers via `worker-pool.ts`). | -| **`adapters/`** | `LanguageAdapter` registry; built-ins call `parser.ts` / `css-parser.ts` / `markers.ts` from `parse-worker-core`. | -| **`runtime.ts` / `config.ts` / `db.ts` / …** | Config, SQLite, resolver, workers. | +| Layer | Role | +| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`cli/`** (`bootstrap`, `main`, `cmd-*`) | Parses argv; **dynamic `import()`** loads only the command chunk (`cmd-index`, `cmd-query`, `cmd-agents`) so `--help` / `version` / `agents init` avoid the indexer. | +| **`api.ts`** | Public programmatic surface: `createCodemap()`, `Codemap` (`query`, `index`), re-exports `runCodemapIndex` for advanced use. | +| **`application/`** | Pure transport-agnostic engines: `run-index.ts` / `index-engine.ts` (orchestration + indexing); `query-engine.ts` (`executeQuery` / `executeQueryBatch`); `audit-engine.ts` (`runAudit` + `resolveAuditBaselines`); `context-engine.ts` (`buildContextEnvelope`); `validate-engine.ts` (`computeValidateRows` + `toProjectRelative`); `show-engine.ts` (lookup + envelope builders); `query-recipes.ts` + `recipes-loader.ts` (recipe registry); `mcp-server.ts` (MCP tool/resource layer). Engines depend on `db.ts` / `runtime.ts`; **never** on `cli/`. | +| **`adapters/`** | `LanguageAdapter` registry; built-ins call `parser.ts` / `css-parser.ts` / `markers.ts` from `parse-worker-core`. | +| **`runtime.ts` / `config.ts` / `db.ts` / …** | Config, SQLite, resolver, workers. | `index.ts` is the package entry: re-exports the public API and runs `cli/main` only when executed as the main module (Node/Bun `codemap` binary). @@ -92,42 +92,42 @@ A local SQLite database (`.codemap.db`) indexes the project tree and stores stru ## Key Files -| File | Purpose | -| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `index.ts` | Package entry — re-exports `api` / `config`, runs CLI when main | -| `cli/` | CLI — bootstrap argv, lazy command modules, `query` / `validate` / `context` / `agents init` / index modes | -| `api.ts` | Programmatic API — `createCodemap`, `Codemap`, `runCodemapIndex` | -| `application/` | Indexing use cases and engine (`run-index`, `index-engine`, types) | -| `worker-pool.ts` | Parallel parse workers (Bun / Node) | -| `db.ts` | SQLite adapter — schema DDL, typed CRUD, connection management | -| `parser.ts` | TS/TSX/JS/JSX extraction via `oxc-parser` — symbols (with JSDoc + generics + return types), type members, imports, exports, components, markers | -| `css-parser.ts` | CSS extraction via `lightningcss` — custom properties, classes, keyframes, `@theme` blocks | -| `resolver.ts` | Import path resolution via `oxc-resolver` — respects `tsconfig` aliases, builds dependency graph | -| `constants.ts` | Shared constants — e.g. `LANG_MAP` | -| `glob-sync.ts` | Include globs — Bun `Glob` vs `tinyglobby` on Node ([packaging § Node vs Bun](./packaging.md#node-vs-bun)) | -| `markers.ts` | Shared marker extraction (`TODO`/`FIXME`/`HACK`/`NOTE`) — used by all parsers | -| `parse-worker.ts` | Worker thread entry point — reads, parses, and extracts file data in parallel | -| `adapters/` | `LanguageAdapter` types and built-in TS/CSS/text implementations | -| `parsed-types.ts` | Shared `ParsedFile` shape for workers and adapters | -| `agents-init.ts` / `agents-init-interactive.ts` | `codemap agents init` — see [agents.md](./agents.md) (granular template + IDE writes, pointer upsert, **`--interactive`**, `.gitignore`) | -| `benchmark.ts` (+ `benchmark-default-scenarios.ts`, `benchmark-config.ts`, `benchmark-common.ts`) | SQL vs traditional timing; optional **`CODEMAP_BENCHMARK_CONFIG`** JSON — [benchmark.md § Custom scenarios](./benchmark.md#custom-scenarios-codemap_benchmark_config) | -| `config.ts` | `codemap.config.*` load path, **Zod** user schema (`codemapUserConfigSchema`), `resolveCodemapConfig` | +| File | Purpose | +| ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `index.ts` | Package entry — re-exports `api` / `config`, runs CLI when main | +| `cli/` | CLI — bootstrap argv, lazy command modules, `query` / `validate` / `context` / `agents init` / index modes | +| `api.ts` | Programmatic API — `createCodemap`, `Codemap`, `runCodemapIndex` | +| `application/` | Pure transport-agnostic engines (`run-index`, `index-engine`, `query-engine`, `audit-engine`, `context-engine`, `validate-engine`, `show-engine`, `query-recipes`, `recipes-loader`, `mcp-server`) | +| `worker-pool.ts` | Parallel parse workers (Bun / Node) | +| `db.ts` | SQLite adapter — schema DDL, typed CRUD, connection management | +| `parser.ts` | TS/TSX/JS/JSX extraction via `oxc-parser` — symbols (with JSDoc + generics + return types), type members, imports, exports, components, markers | +| `css-parser.ts` | CSS extraction via `lightningcss` — custom properties, classes, keyframes, `@theme` blocks | +| `resolver.ts` | Import path resolution via `oxc-resolver` — respects `tsconfig` aliases, builds dependency graph | +| `constants.ts` | Shared constants — e.g. `LANG_MAP` | +| `glob-sync.ts` | Include globs — Bun `Glob` vs `tinyglobby` on Node ([packaging § Node vs Bun](./packaging.md#node-vs-bun)) | +| `markers.ts` | Shared marker extraction (`TODO`/`FIXME`/`HACK`/`NOTE`) — used by all parsers | +| `parse-worker.ts` | Worker thread entry point — reads, parses, and extracts file data in parallel | +| `adapters/` | `LanguageAdapter` types and built-in TS/CSS/text implementations | +| `parsed-types.ts` | Shared `ParsedFile` shape for workers and adapters | +| `agents-init.ts` / `agents-init-interactive.ts` | `codemap agents init` — see [agents.md](./agents.md) (granular template + IDE writes, pointer upsert, **`--interactive`**, `.gitignore`) | +| `benchmark.ts` (+ `benchmark-default-scenarios.ts`, `benchmark-config.ts`, `benchmark-common.ts`) | SQL vs traditional timing; optional **`CODEMAP_BENCHMARK_CONFIG`** JSON — [benchmark.md § Custom scenarios](./benchmark.md#custom-scenarios-codemap_benchmark_config) | +| `config.ts` | `codemap.config.*` load path, **Zod** user schema (`codemapUserConfigSchema`), `resolveCodemapConfig` | ## CLI usage **Commands and flags** (index, query, **`codemap agents init`**, **`--root`**, **`--config`**, environment): [../README.md § CLI](../README.md#cli) — **do not duplicate** flag lists here; this section only adds implementation notes. From this repository: **`bun run dev`** or **`bun src/index.ts`** (same flags). -**Query wiring:** **`src/cli/cmd-query.ts`** (argv, **`printQueryResult`**, `--recipe` / `-r` alias, **`--summary`**, **`--changed-since`**, **`--group-by`**, **`--save-baseline`** / **`--baseline`** / **`--baselines`** / **`--drop-baseline`**), **`src/cli/query-recipes.ts`** (**`QUERY_RECIPES`** — bundled SQL only source; optional **`actions: RecipeAction[]`** per recipe), **`src/cli/main.ts`** (**`--recipes-json`** / **`--print-sql`** exit before config/DB). With **`--json`**, errors use **`{"error":"…"}`** on stdout for SQL failures, DB open, and bootstrap (same shape); **`runQueryCmd`** sets **`process.exitCode`** instead of **`process.exit`**. Friendlier "no `.codemap.db`" — `no such table: ` and `no such column: ` errors are rewritten in **`enrichQueryError`** to point at `codemap` / `codemap --full`. **`--summary`** filters output only — the SQL still executes against the index; output collapses to `{"count": N}` (with `--json`) or `count: N`. **`--changed-since `** post-filters result rows by `path` / `file_path` / `from_path` / `to_path` / `resolved_path` against `git diff --name-only ...HEAD ∪ git status --porcelain` (helper: **`src/git-changed.ts`** — `getFilesChangedSince`, `filterRowsByChangedFiles`, `PATH_COLUMNS`); rows with no recognised path column pass through. **`--group-by `** (`owner` | `directory` | `package`) routes through **`runGroupedQuery`** in `cmd-query.ts` and emits `{"group_by": "", "groups": [{key, count, rows}]}` (or `[{key, count}]` with `--summary`); helpers in **`src/group-by.ts`** (`groupRowsBy`, `firstDirectory`, `loadCodeowners`, `discoverWorkspaceRoots`, `makePackageBucketizer`, `codeownersGlobToRegex`). CODEOWNERS lookup is last-match-wins (GitHub semantics); workspace discovery reads `package.json` `workspaces` and `pnpm-workspace.yaml` `packages:`. **`--save-baseline[=]`** snapshots the result to the **`query_baselines`** table inside `.codemap.db` (no parallel JSON files; survives `--full` / SCHEMA bumps because the table is intentionally absent from `dropAll()`); name defaults to `--recipe` id, ad-hoc SQL needs an explicit name. **`--baseline[=]`** replays the SQL, fetches the saved row set, and emits `{baseline:{...}, current_row_count, added: [...], removed: [...]}` (or `{baseline:{...}, current_row_count, added: N, removed: N}` with `--summary`); identity is per-row multiset equality (canonical `JSON.stringify` keyed frequency map — duplicate rows are tracked, not collapsed). No fuzzy "changed" category in v1. **`--group-by` is mutually exclusive** with both `--save-baseline` and `--baseline` (different output shapes). **`--baselines`** (read-only list) and **`--drop-baseline `** complete the surface; helpers in **`src/db.ts`** (`upsertQueryBaseline`, `getQueryBaseline`, `listQueryBaselines`, `deleteQueryBaseline`). **Per-row recipe `actions`** are appended only when the user runs **`--recipe `** with **`--json`** AND the recipe defines an `actions` template — programmatic `cm.query(sql)` and ad-hoc CLI SQL never carry actions; under `--baseline`, actions attach to `added` rows only (the rows the agent should act on). The **`components-by-hooks`** recipe ranks by hook count with a **comma-based tally** on **`hooks_used`** (no SQLite JSON1). Shipped **`templates/agents/`** documents **`codemap query --json`** as the primary agent example ([README § CLI](../README.md#cli)). +**Query wiring:** **`src/cli/cmd-query.ts`** (argv, **`printQueryResult`**, `--recipe` / `-r` alias, **`--summary`**, **`--changed-since`**, **`--group-by`**, **`--save-baseline`** / **`--baseline`** / **`--baselines`** / **`--drop-baseline`**), **`src/application/query-recipes.ts`** (**`QUERY_RECIPES`** — bundled SQL only source; optional **`actions: RecipeAction[]`** per recipe), **`src/cli/main.ts`** (**`--recipes-json`** / **`--print-sql`** exit before config/DB). With **`--json`**, errors use **`{"error":"…"}`** on stdout for SQL failures, DB open, and bootstrap (same shape); **`runQueryCmd`** sets **`process.exitCode`** instead of **`process.exit`**. Friendlier "no `.codemap.db`" — `no such table: ` and `no such column: ` errors are rewritten in **`enrichQueryError`** to point at `codemap` / `codemap --full`. **`--summary`** filters output only — the SQL still executes against the index; output collapses to `{"count": N}` (with `--json`) or `count: N`. **`--changed-since `** post-filters result rows by `path` / `file_path` / `from_path` / `to_path` / `resolved_path` against `git diff --name-only ...HEAD ∪ git status --porcelain` (helper: **`src/git-changed.ts`** — `getFilesChangedSince`, `filterRowsByChangedFiles`, `PATH_COLUMNS`); rows with no recognised path column pass through. **`--group-by `** (`owner` | `directory` | `package`) routes through **`runGroupedQuery`** in `cmd-query.ts` and emits `{"group_by": "", "groups": [{key, count, rows}]}` (or `[{key, count}]` with `--summary`); helpers in **`src/group-by.ts`** (`groupRowsBy`, `firstDirectory`, `loadCodeowners`, `discoverWorkspaceRoots`, `makePackageBucketizer`, `codeownersGlobToRegex`). CODEOWNERS lookup is last-match-wins (GitHub semantics); workspace discovery reads `package.json` `workspaces` and `pnpm-workspace.yaml` `packages:`. **`--save-baseline[=]`** snapshots the result to the **`query_baselines`** table inside `.codemap.db` (no parallel JSON files; survives `--full` / SCHEMA bumps because the table is intentionally absent from `dropAll()`); name defaults to `--recipe` id, ad-hoc SQL needs an explicit name. **`--baseline[=]`** replays the SQL, fetches the saved row set, and emits `{baseline:{...}, current_row_count, added: [...], removed: [...]}` (or `{baseline:{...}, current_row_count, added: N, removed: N}` with `--summary`); identity is per-row multiset equality (canonical `JSON.stringify` keyed frequency map — duplicate rows are tracked, not collapsed). No fuzzy "changed" category in v1. **`--group-by` is mutually exclusive** with both `--save-baseline` and `--baseline` (different output shapes). **`--baselines`** (read-only list) and **`--drop-baseline `** complete the surface; helpers in **`src/db.ts`** (`upsertQueryBaseline`, `getQueryBaseline`, `listQueryBaselines`, `deleteQueryBaseline`). **Per-row recipe `actions`** are appended only when the user runs **`--recipe `** with **`--json`** AND the recipe defines an `actions` template — programmatic `cm.query(sql)` and ad-hoc CLI SQL never carry actions; under `--baseline`, actions attach to `added` rows only (the rows the agent should act on). The **`components-by-hooks`** recipe ranks by hook count with a **comma-based tally** on **`hooks_used`** (no SQLite JSON1). Shipped **`templates/agents/`** documents **`codemap query --json`** as the primary agent example ([README § CLI](../README.md#cli)). -**Validate wiring:** **`src/cli/cmd-validate.ts`** — **`computeValidateRows`** is a pure function over `(db, projectRoot, paths)` returning `{path, status}` rows where `status ∈ stale | missing | unindexed`. CLI wraps it with read-once-and-print + exits **1** on any drift (git-status semantics). Path normalization: **`toProjectRelative`** converts CLI input to POSIX-style relative keys matching the `files.path` storage format (Windows backslash → forward slash); same convention as `lint-staged.config.js`. +**Validate wiring:** **`src/cli/cmd-validate.ts`** (argv + render) + **`src/application/validate-engine.ts`** (engine — **`computeValidateRows`** + **`toProjectRelative`**). `computeValidateRows` is a pure function over `(db, projectRoot, paths)` returning `{path, status}` rows where `status ∈ stale | missing | unindexed`. CLI wraps it with read-once-and-print + exits **1** on any drift (git-status semantics). Path normalization: **`toProjectRelative`** converts CLI input to POSIX-style relative keys matching the `files.path` storage format (Windows backslash → forward slash); same convention as `lint-staged.config.js`. Also reused by `cmd-show.ts` / `cmd-snippet.ts` and the MCP show/snippet handlers — single canonical implementation. **Audit wiring:** **`src/cli/cmd-audit.ts`** (argv, `--baseline ` auto-resolve sugar, `---baseline ` per-delta explicit overrides, `--json`, `--summary`, `--no-index`) + **`src/application/audit-engine.ts`** (delta registry + diff). Mirrors the `cmd-index.ts ↔ application/index-engine.ts` seam — CLI parses + dispatches; engine does the diff. **`runAudit({db, baselines})`** iterates the per-delta baseline map; deltas absent from the map don't run. Each entry in **`V1_DELTAS`** pins a canonical SQL projection (`files`: `SELECT path FROM files`; `dependencies`: `SELECT from_path, to_path FROM dependencies`; `deprecated`: `SELECT name, kind, file_path FROM symbols WHERE doc_comment LIKE '%@deprecated%'`) plus a `requiredColumns` list. **`computeDelta`** validates baseline column-set membership, projects baseline rows down to the canonical column subset (extras dropped — schema-drift-resilient), runs the canonical SQL via the caller's DB connection, and set-diffs via the existing **`src/diff-rows.ts`** multiset helper (shared with `query --baseline`). Each emitted delta carries its own **`base`** metadata so mixed-baseline audits (e.g. `--baseline base --dependencies-baseline override`) are first-class. **`runAuditCmd`** runs an auto-incremental-index prelude (`runCodemapIndex({mode: "incremental", quiet: true})`) before the diff so `head` reflects the current source — `--no-index` opts out for frozen-DB CI scenarios. **`resolveAuditBaselines({db, baselinePrefix, perDelta})`** composes the baseline map: auto-resolves `-` for slots that exist (silently absent otherwise) and lets per-delta flags override individual slots. v1 ships no `verdict` / threshold config / non-zero exit codes — consumers compose `--json` + `jq` for CI exit codes; v1.x adds `verdict` + `codemap.config.audit` thresholds + `--base ` (worktree+reindex snapshot strategy). -**Context wiring:** **`src/cli/cmd-context.ts`** — **`buildContextEnvelope`** composes the JSON envelope from existing recipes (`fan-in` for `hubs`, `markers` SELECT for `sample_markers`, `QUERY_RECIPES` map for the catalog). **`classifyIntent`** maps `--for ""` to one of `refactor | debug | test | feature | explore | other` via regex against the trimmed input; whitespace-only intents are rejected. `--compact` drops `hubs` + `sample_markers` and emits one-line JSON; otherwise pretty-prints with 2-space indent. +**Context wiring:** **`src/cli/cmd-context.ts`** (argv + render) + **`src/application/context-engine.ts`** (engine — **`buildContextEnvelope`**, **`classifyIntent`**, `ContextEnvelope` type). `buildContextEnvelope` composes the JSON envelope from existing recipes (`fan-in` for `hubs`, `markers` SELECT for `sample_markers`, `QUERY_RECIPES` map for the catalog). **`classifyIntent`** maps `--for ""` to one of `refactor | debug | test | feature | explore | other` via regex against the trimmed input; whitespace-only intents are rejected. `--compact` drops `hubs` + `sample_markers` and emits one-line JSON; otherwise pretty-prints with 2-space indent. -**Show / snippet wiring:** **`src/cli/cmd-show.ts`** + **`src/cli/cmd-snippet.ts`** — sibling CLI verbs sharing the same parser shape (`` + `--kind` + `--in ` + `--json`) and the pure engine **`src/application/show-engine.ts`** (`findSymbolsByName({db, name, kind?, inPath?})` for the lookup; `readSymbolSource({match, projectRoot, indexedContentHash?})` + `getIndexedContentHash(db, filePath)` for the snippet-side FS read). Both verbs return the same `{matches, disambiguation?}` envelope per plan § 4 uniformity — single match → `{matches: [{...}]}`; multi-match adds `{n, by_kind, files, hint}`. Snippet matches add `source` / `stale` / `missing` fields (additive — no shape divergence). **`--in `** is normalized through `toProjectRelative(projectRoot, p)` (exported from **`src/cli/cmd-validate.ts`**) so `--in ./src/cli/`, `--in src/cli`, and `--in src/cli/cmd-show.ts` all resolve identically. Stale-file behavior on `snippet`: `hashContent` (from **`src/hash.ts`** — same primitive `cmd-validate.ts` uses) compares the on-disk content_hash against `files.content_hash`; mismatch sets `stale: true` but the source IS still returned (read tool, no auto-reindex side-effects). MCP tools `show` and `snippet` register parallel to the CLI surface (see [§ MCP wiring](#cli-usage)). +**Show / snippet wiring:** **`src/cli/cmd-show.ts`** + **`src/cli/cmd-snippet.ts`** — sibling CLI verbs sharing the same parser shape (`` + `--kind` + `--in ` + `--json`) and the pure engine **`src/application/show-engine.ts`** (`findSymbolsByName({db, name, kind?, inPath?})` for the lookup; `readSymbolSource({match, projectRoot, indexedContentHash?})` + `getIndexedContentHash(db, filePath)` for the snippet-side FS read; **`buildShowResult`** + **`buildSnippetResult`** envelope builders — same engine the MCP show/snippet tools call). Both verbs return the same `{matches, disambiguation?}` envelope per plan § 4 uniformity — single match → `{matches: [{...}]}`; multi-match adds `{n, by_kind, files, hint}`. Snippet matches add `source` / `stale` / `missing` fields (additive — no shape divergence). **`--in `** is normalized through `toProjectRelative(projectRoot, p)` (from **`src/application/validate-engine.ts`**) so `--in ./src/cli/`, `--in src/cli`, and `--in src/cli/cmd-show.ts` all resolve identically. Stale-file behavior on `snippet`: `hashContent` (from **`src/hash.ts`** — same primitive `cmd-validate.ts` uses) compares the on-disk content_hash against `files.content_hash`; mismatch sets `stale: true` but the source IS still returned (read tool, no auto-reindex side-effects). MCP tools `show` and `snippet` register parallel to the CLI surface (see [§ MCP wiring](#cli-usage)). -**Recipes wiring:** **`src/application/recipes-loader.ts`** (pure transport-agnostic loader) + **`src/cli/query-recipes.ts`** (shim — caches the loader output, exposes `getQueryRecipeSql` / `getQueryRecipeActions` / `listQueryRecipeIds` / `listQueryRecipeCatalog` / `getQueryRecipeCatalogEntry`). Recipes live as file pairs: **`.sql`** + optional **`.md`**. The loader reads `templates/recipes/` (bundled, ships in npm package next to `templates/agents/`) and `/.codemap/recipes/` (project-local — root-only resolution per the registry plan, no walk-up). Project recipes win on id collision; entries that override a bundled id carry **`shadows: true`** in the catalog so agents reading `codemap://recipes` at session start see when a recipe behaves differently from the documented bundled version. Per-row **`actions`** templates (kebab-case verb + description) live in YAML frontmatter on each `.md` — uniform shape across bundled + project. Hand-rolled YAML parser scoped to `actions: [{type, auto_fixable?, description?}]` only (no `js-yaml` dep). Load-time validation rejects empty SQL and DML / DDL keywords (`INSERT` / `UPDATE` / `DELETE` / `DROP` / `CREATE` / `ALTER` / `ATTACH` / `DETACH` / `REPLACE` / `TRUNCATE` / `VACUUM` / `PRAGMA`) with recipe-aware error messages — defence in depth alongside the runtime `PRAGMA query_only=1` backstop in `query-engine.ts` (PR #35). `.codemap.db` is gitignored; `.codemap/recipes/` is NOT (verified via `git check-ignore`) — recipes are git-tracked source code authored for human review. +**Recipes wiring:** **`src/application/recipes-loader.ts`** (pure transport-agnostic loader) + **`src/application/query-recipes.ts`** (cache + public API — `getQueryRecipeSql` / `getQueryRecipeActions` / `listQueryRecipeIds` / `listQueryRecipeCatalog` / `getQueryRecipeCatalogEntry`, shared by CLI + MCP). Recipes live as file pairs: **`.sql`** + optional **`.md`**. The loader reads `templates/recipes/` (bundled, ships in npm package next to `templates/agents/`) and `/.codemap/recipes/` (project-local — root-only resolution per the registry plan, no walk-up). Project recipes win on id collision; entries that override a bundled id carry **`shadows: true`** in the catalog so agents reading `codemap://recipes` at session start see when a recipe behaves differently from the documented bundled version. Per-row **`actions`** templates (kebab-case verb + description) live in YAML frontmatter on each `.md` — uniform shape across bundled + project. Hand-rolled YAML parser scoped to `actions: [{type, auto_fixable?, description?}]` only (no `js-yaml` dep). Load-time validation rejects empty SQL and DML / DDL keywords (`INSERT` / `UPDATE` / `DELETE` / `DROP` / `CREATE` / `ALTER` / `ATTACH` / `DETACH` / `REPLACE` / `TRUNCATE` / `VACUUM` / `PRAGMA`) with recipe-aware error messages — defence in depth alongside the runtime `PRAGMA query_only=1` backstop in `query-engine.ts` (PR #35). `.codemap.db` is gitignored; `.codemap/recipes/` is NOT (verified via `git check-ignore`) — recipes are git-tracked source code authored for human review. **MCP wiring:** **`src/cli/cmd-mcp.ts`** (argv — `--help` only; bootstrap absorbs `--root`/`--config`) + **`src/application/mcp-server.ts`** (engine — tool registry, resource handlers, response composition). Mirrors the `cmd-audit.ts ↔ audit-engine.ts` seam — CLI parses + lifecycle; engine owns the SDK. **`runMcpServer`** bootstraps codemap once at server boot (config + resolver + DB access become module-level state), instantiates `McpServer` from **`@modelcontextprotocol/sdk`**, attaches a **`StdioServerTransport`**, and resolves when stdin closes (clean shutdown). Tool handlers reuse the existing engine entry-points: **`query`** + **`query_recipe`** call **`executeQuery`** in **`src/application/query-engine.ts`** (a pure transport-agnostic engine extracted from `printQueryResult`'s JSON branch — same `[...rows]` / `{count}` / `{group_by, groups}` envelope `--json` would print); **`query_batch`** loops via **`executeQueryBatch`** with batch-wide-defaults + per-statement-overrides (items are `string | {sql, summary?, changed_since?, group_by?}`); **`audit`** runs `resolveAuditBaselines` + `runAudit` from PR #33 unchanged; **`context`** / **`validate`** call `buildContextEnvelope` / `computeValidateRows` (pure functions in `src/cli/cmd-*.ts` — same layer-reversal allowance as `query-recipes`). **`save_baseline`** is one polymorphic tool (`{name, sql? | recipe?}`) with a runtime exclusivity check — mirrors the CLI's single `--save-baseline=` verb. **Tool naming**: snake_case throughout — Codemap convention matching the patterns in MCP spec examples and reference servers (GitHub MCP, Cursor built-ins); the spec itself doesn't mandate it. CLI stays kebab — translation lives at the MCP-arg layer. **Resources** (`codemap://recipes`, `codemap://recipes/{id}`, `codemap://schema`, `codemap://skill`) use **lazy memoisation** — first `read_resource` populates a per-server-instance cache; constant for the server-process lifetime so eager-vs-lazy produce identical observable behavior. `codemap://schema` queries `sqlite_schema` live; `codemap://skill` reads from `resolveAgentsTemplateDir() + skills/codemap/SKILL.md`. Output shape uniformity (plan § 4): every tool returns the JSON envelope its CLI counterpart's `--json` flag prints, surfaced via `content: [{type: "text", text: JSON.stringify(payload)}]`. `--changed-since` git lookups are memoised per `(root, ref)` pair across batch items so a `query_batch` of N items sharing the same ref does one git invocation, not N. Per-statement errors in `query_batch` are isolated — failed statements return `{error}` in their slot while siblings still execute. diff --git a/docs/benchmark.md b/docs/benchmark.md index bb3bae1f..39d7cdda 100644 --- a/docs/benchmark.md +++ b/docs/benchmark.md @@ -167,7 +167,7 @@ On a small repo, totals move with noise and thermal variance. On a large indexed The script’s **reindex** section averages **3 internal runs** per mode; full-rebuild wall time varies with disk and CPU load. -The indexed CSS scenario uses `ORDER BY name LIMIT 50`. The **fan-out** row’s indexed path uses **`getQueryRecipeSql("fan-out")`** from **`src/cli/query-recipes.ts`** (same text as **`codemap query --recipe fan-out`**). Other default scenarios’ SQL lives in **`src/benchmark-default-scenarios.ts`**; custom JSON is loaded in **`src/benchmark-config.ts`** (keep **`fixtures/benchmark/scenarios.example.json`** in sync when recipe SQL changes). +The indexed CSS scenario uses `ORDER BY name LIMIT 50`. The **fan-out** row’s indexed path uses **`getQueryRecipeSql("fan-out")`** from **`src/application/query-recipes.ts`** (same text as **`codemap query --recipe fan-out`**). Other default scenarios’ SQL lives in **`src/benchmark-default-scenarios.ts`**; custom JSON is loaded in **`src/benchmark-config.ts`** (keep **`fixtures/benchmark/scenarios.example.json`** in sync when recipe SQL changes). ### Key takeaways diff --git a/docs/research/competitive-scan-2026-04.md b/docs/research/competitive-scan-2026-04.md index ed086dcd..0d109219 100644 --- a/docs/research/competitive-scan-2026-04.md +++ b/docs/research/competitive-scan-2026-04.md @@ -44,13 +44,13 @@ Sources: | Idea (originally §3 / §4 / §5 of this scan) | Shipped where | Inspired by | | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `codemap context` JSON envelope (incl. `--for ""` thin classifier, `--compact`) | `src/cli/cmd-context.ts` | JordanCoin (`codemap context`) | -| `codemap validate` (hash-based staleness, no re-read) | `src/cli/cmd-validate.ts` | AZidan (`codemap validate`) | +| `codemap context` JSON envelope (incl. `--for ""` thin classifier, `--compact`) | `src/cli/cmd-context.ts` + `src/application/context-engine.ts` | JordanCoin (`codemap context`) | +| `codemap validate` (hash-based staleness, no re-read) | `src/cli/cmd-validate.ts` + `src/application/validate-engine.ts` | AZidan (`codemap validate`) | | `--performance` per-phase timing + top-10 slowest files | `src/application/index-engine.ts` | own roadmap, sharpened by JordanCoin's daemon framing | -| `deprecated-symbols` recipe | `src/cli/query-recipes.ts` | fallow JSDoc visibility tags | -| `visibility-tags` recipe (`@internal` / `@private` / `@alpha` / `@beta`) | `src/cli/query-recipes.ts` | fallow JSDoc visibility tags | -| `barrel-files` recipe (top files by export count) | `src/cli/query-recipes.ts` | own derivation from JordanCoin "hubs" framing | -| `files-hashes` recipe powering `validate` | `src/cli/query-recipes.ts` | AZidan | +| `deprecated-symbols` recipe | `src/application/query-recipes.ts` | fallow JSDoc visibility tags | +| `visibility-tags` recipe (`@internal` / `@private` / `@alpha` / `@beta`) | `src/application/query-recipes.ts` | fallow JSDoc visibility tags | +| `barrel-files` recipe (top files by export count) | `src/application/query-recipes.ts` | own derivation from JordanCoin "hubs" framing | +| `files-hashes` recipe powering `validate` | `src/application/query-recipes.ts` | AZidan | | `-r` short alias for `--recipe`, cleaner `--help` | `src/cli/cmd-query.ts` | own UX polish | | Friendlier "no `.codemap.db`" error | `src/application/index-engine.ts` | own UX polish | | Anti-pitch — "What Codemap is not" | [why-codemap.md § What Codemap is not](../why-codemap.md#what-codemap-is-not) | AZidan | diff --git a/docs/research/fallow.md b/docs/research/fallow.md index 4af372bd..ffa6869f 100644 --- a/docs/research/fallow.md +++ b/docs/research/fallow.md @@ -61,12 +61,12 @@ Action hints in this section are **proposals** — not commitments. Tier hints r ### Tier A — Ship now (≤1 week each, mostly mechanical) -| # | Candidate | Sketch | Why this tier | -| --- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A.1 | **Per-row `actions` array on common recipes** | Each row from `query --recipe ` optionally carries `actions: [{type, auto_fixable, description}]`. Examples: `zero-fan-in-files` row → `[{type:"delete-file", auto_fixable:false}]`; `deprecated-symbols` row → `[{type:"open-deprecation-issue"}]`; `barrel-files` row → `[{type:"split-barrel"}]`. Recipe authors define the action template; the SQL stays untouched. Future ad-hoc SQL doesn't get actions automatically — that's fine, it's a recipe-only feature. | Mirrors fallow's killer agent-facing detail. Pure recipe-layer change in [`src/cli/query-recipes.ts`](../../src/cli/query-recipes.ts) — no schema impact. | -| A.2 | **`--changed-since ` on `query`** | Filters every query result row to files whose path matches `git diff --name-only ...HEAD`. Implementation: pre-compute the changed-file set, append `AND .file_path IN (...)` (or `from_path` / `to_path` for `dependencies` queries) to the SQL. Same primitive fallow uses for `--changed-since main`. | Unlocks PR-scoped queries without `codemap audit` (which is bigger — see B.5). Cheap; no schema impact. | -| A.3 | **`--group-by owner\|directory\|package`** | Partition any `file_path`-bearing query result by CODEOWNERS first-owner / first directory component / workspace package. Implementation: post-process at JSON emit time; no SQL change required. | Layered on top of existing query output; no schema impact. CODEOWNERS reading is the only new bit. | -| A.4 | **`--summary` flag** | Counts only, no rows. Useful pair with `--recipe` for dashboards / agent context windows. | Trivial; aligns with `codemap context --compact`. | +| # | Candidate | Sketch | Why this tier | +| --- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A.1 | **Per-row `actions` array on common recipes** | Each row from `query --recipe ` optionally carries `actions: [{type, auto_fixable, description}]`. Examples: `zero-fan-in-files` row → `[{type:"delete-file", auto_fixable:false}]`; `deprecated-symbols` row → `[{type:"open-deprecation-issue"}]`; `barrel-files` row → `[{type:"split-barrel"}]`. Recipe authors define the action template; the SQL stays untouched. Future ad-hoc SQL doesn't get actions automatically — that's fine, it's a recipe-only feature. | Mirrors fallow's killer agent-facing detail. Pure recipe-layer change in [`src/application/query-recipes.ts`](../../src/application/query-recipes.ts) — no schema impact. | +| A.2 | **`--changed-since ` on `query`** | Filters every query result row to files whose path matches `git diff --name-only ...HEAD`. Implementation: pre-compute the changed-file set, append `AND .file_path IN (...)` (or `from_path` / `to_path` for `dependencies` queries) to the SQL. Same primitive fallow uses for `--changed-since main`. | Unlocks PR-scoped queries without `codemap audit` (which is bigger — see B.5). Cheap; no schema impact. | +| A.3 | **`--group-by owner\|directory\|package`** | Partition any `file_path`-bearing query result by CODEOWNERS first-owner / first directory component / workspace package. Implementation: post-process at JSON emit time; no SQL change required. | Layered on top of existing query output; no schema impact. CODEOWNERS reading is the only new bit. | +| A.4 | **`--summary` flag** | Counts only, no rows. Useful pair with `--recipe` for dashboards / agent context windows. | Trivial; aligns with `codemap context --compact`. | ### Tier B — Ship next (1–3 weeks; meaningful design work) From 9aeea3597cd5a14608c6228cedbfd93b3aa2cca6 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Sat, 2 May 2026 18:18:51 +0300 Subject: [PATCH 7/7] chore(mcp-server): drop outdated // Layer note (PR #41 obsoleted it) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The note described the old state — engine helpers used to live under src/cli/ and we imported them here as 'pure data'. Tracers 1-4 of this PR lifted every one of them into src/application/, so the note's prediction ("future refactor may lift them...") is now history. With every import below from ./*-engine, the comment carried no info a teammate couldn't re-derive in <30s. User-confirmed delete per preserve-comments Rule 4. --- src/application/mcp-server.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/application/mcp-server.ts b/src/application/mcp-server.ts index c508be12..6b86143c 100644 --- a/src/application/mcp-server.ts +++ b/src/application/mcp-server.ts @@ -23,12 +23,6 @@ import type { GroupByMode } from "../group-by"; import { configureResolver } from "../resolver"; import { getProjectRoot, getTsconfigPath, initCodemap } from "../runtime"; import { resolveAuditBaselines, runAudit } from "./audit-engine"; -// Layer note: several modules below live under `src/cli/` because their CLI -// verb owns them today (`query-recipes`, `cmd-audit`'s baseline resolver, -// `cmd-context`'s envelope builder, `cmd-validate`'s row computer). We import -// them here as pure data / pure functions (no execution flow crosses -// cli → application). A future refactor may lift them to `src/application/` -// once a second consumer (HTTP API) needs them. import { buildContextEnvelope } from "./context-engine"; import { getCurrentCommit } from "./index-engine"; import { executeQuery } from "./query-engine";