Skip to content

Commit 7c30ccb

Browse files
committed
harden: parity and watch-ordering for codebase map bootstrap
Align skill shard, MCP tool description, and HTTP tests with map_id fields; prime watch before baking initialize instructions appendix.
1 parent c98b2cb commit 7c30ccb

6 files changed

Lines changed: 61 additions & 13 deletions

File tree

src/application/context-engine.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,11 @@ describe("formatCodebaseMapMcpAppendix", () => {
512512
expect(text).not.toContain("`src/extra.ts`");
513513
expect(text).toContain("call MCP tool `context`");
514514
});
515+
516+
it("handles empty hub paths", () => {
517+
const text = formatCodebaseMapMcpAppendix("abc", []);
518+
expect(text).toContain("top hubs: (none indexed)");
519+
});
515520
});
516521

517522
describe("buildContextEnvelope", () => {
@@ -550,6 +555,29 @@ describe("buildContextEnvelope", () => {
550555
}
551556
});
552557

558+
it("changes map_id when hub rankings change", () => {
559+
const revParse = spyOn(indexEngine, "getCurrentCommit").mockReturnValue("");
560+
try {
561+
withSeededDb((db) => {
562+
const before = buildContextEnvelope(db, benchDir, {
563+
compact: false,
564+
intent: null,
565+
});
566+
insertDependencies(db, [
567+
{ from_path: "src/hub.ts", to_path: "src/leaf.ts" },
568+
{ from_path: "src/hub.ts", to_path: "src/other.ts" },
569+
]);
570+
const after = buildContextEnvelope(db, benchDir, {
571+
compact: false,
572+
intent: null,
573+
});
574+
expect(before.map_id).not.toBe(after.map_id);
575+
});
576+
} finally {
577+
revParse.mockRestore();
578+
}
579+
});
580+
553581
it("omits map fields when include_codebase_map is false", () => {
554582
const revParse = spyOn(indexEngine, "getCurrentCommit").mockReturnValue("");
555583
try {

src/application/http-server.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,25 @@ describe("http-server — POST /tool/{other tools}", () => {
427427
expect(leader?.signatures?.[0]?.snippet).toContain("export const SNIP");
428428
});
429429

430+
it("context includes map_id and codebase_map by default", async () => {
431+
serverHandle = await startServer();
432+
const r = await postTool(serverHandle.port, "context", {});
433+
expect(r.status).toBe(200);
434+
expect(r.json.map_id).toMatch(/^[0-9a-f]{16}$/);
435+
expect(r.json.codebase_map?.cli_entry_hints?.length).toBe(12);
436+
});
437+
438+
it("context omits map fields when include_codebase_map is false", async () => {
439+
serverHandle = await startServer();
440+
const r = await postTool(serverHandle.port, "context", {
441+
include_codebase_map: false,
442+
});
443+
expect(r.status).toBe(200);
444+
expect(r.json.start_here).toBeDefined();
445+
expect(r.json.map_id).toBeUndefined();
446+
expect(r.json.codebase_map).toBeUndefined();
447+
});
448+
430449
it("validate returns staleness rows", async () => {
431450
serverHandle = await startServer();
432451
const r = await postTool(serverHandle.port, "validate", {});

src/application/mcp-server.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ function registerContextTool(server: McpServer): void {
258258
"context",
259259
withToolAnnotations("context", {
260260
description:
261-
"Project bootstrap snapshot — returns the same envelope `codemap context` prints (project root, schema version, file count, start_here shortcuts, recipe catalog, index_freshness). Pass include_snippets for one-line export previews on hub leaders (ignored when compact: true).",
261+
"Project bootstrap snapshot — returns the same envelope `codemap context` prints (project root, schema version, file count, start_here shortcuts, map_id + codebase_map routing card, recipe catalog, index_freshness). Pass include_snippets for one-line export previews on hub leaders (ignored when compact: true). Omit map fields with compact: true or include_codebase_map: false.",
262262
inputSchema: contextArgsSchema,
263263
}),
264264
(args) => wrapToolResult(handleContext(args)),
@@ -677,6 +677,10 @@ export async function runMcpServer(opts: ServerOpts): Promise<void> {
677677
warnIndexFreshnessToStderr("codemap mcp");
678678
}
679679

680+
if (watchSession !== undefined) {
681+
await watchSession.acquireClient();
682+
}
683+
680684
let instructions: string;
681685
try {
682686
const { openDb, closeDb } = await import("../db");
@@ -695,10 +699,6 @@ export async function runMcpServer(opts: ServerOpts): Promise<void> {
695699
const server = createMcpServer({ ...opts, instructions });
696700
const transport = new StdioServerTransport();
697701

698-
if (watchSession !== undefined) {
699-
await watchSession.acquireClient();
700-
}
701-
702702
await server.connect(transport);
703703

704704
let shuttingDown = false;

src/cli/cmd-context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Flags:
3737
Examples:
3838
codemap context
3939
codemap context --compact
40+
codemap context --no-codebase-map
4041
codemap context --for "refactor the auth module"
4142
codemap context --include-snippets
4243
`);

templates/agent-content/mcp-instructions.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ Operational playbook injected into the MCP initialize handshake. Full schema, re
1212

1313
Every successful JSON tool response carries index-level freshness metadata (not a pass/fail verdict):
1414

15-
| Surface | Where to read it |
16-
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
17-
| **`context`** | `index_freshness`; **`start_here`** when not `compact` (optional `include_snippets`) |
18-
| **Object payloads** (`show`, `query` summary, …) | `index_freshness` merged inline |
19-
| **Array payloads** (`query` rows) | second `content` block prefixed `@codemap/index_freshness` |
20-
| **HTTP** | `X-Codemap-Pending-Sync`, `X-Codemap-Commit-Drift`, `X-Codemap-Warning` headers (JSON body unchanged) |
15+
| Surface | Where to read it |
16+
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17+
| **`context`** | `index_freshness`; **`start_here`** when not `compact` (optional `include_snippets`); **`map_id`** + **`codebase_map`** when not `compact` (optional `include_codebase_map: false`) |
18+
| **Object payloads** (`show`, `query` summary, …) | `index_freshness` merged inline |
19+
| **Array payloads** (`query` rows) | second `content` block prefixed `@codemap/index_freshness` |
20+
| **HTTP** | `X-Codemap-Pending-Sync`, `X-Codemap-Commit-Drift`, `X-Codemap-Warning` headers (JSON body unchanged) |
2121

2222
Key fields: `pending_sync` (watcher debounce queue or in-flight reindex), `commit_drift` (`HEAD``last_indexed_commit`), `warning` (single agent-readable line when anything is off).
2323

templates/agent-content/skill/10-recipes-context.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Replace placeholders (`'...'`) with your module path, file glob, or symbol name.
3131

3232
Each emitted delta carries its own `base` metadata so mixed-baseline audits are first-class. **`--base <ref>`** materialises any git committish via `git archive | tar -x` + reindex (mutually exclusive with `--baseline`). Each `added` row on ref-sourced audits carries **`attribution: introduced | inherited`** — `introduced` = branch-new drift; `inherited` = finding already at merge base (e.g. multiset surplus). Filter branch-only debt with `jq '.deltas.deprecated.added[] | select(.attribution == "introduced")'`. **`--format sarif`** emits SARIF 2.1.0 for Code Scanning; **`--ci`** aliases `--format sarif` + non-zero exit on additions (mutually exclusive with `--json`). `--summary` collapses each delta to `{added: N, removed: N}` plus `added_introduced` / `added_inherited` when `--base` is set. `--no-index` skips the auto-incremental-index prelude (default is to re-index first so `head` reflects current source). v1 ships no `verdict` / threshold config — `codemap audit --json | jq -e '.deltas.dependencies.added | length <= 50'` is the CI exit-code idiom until v1.x ships native thresholds. Each delta pins a canonical SQL projection and validates baseline column-set membership before diffing — schema-bump-resilient (extras dropped, missing columns surface a clean re-save command).
3333

34-
**MCP server (`codemap mcp [--no-watch] [--debounce <ms>]`)** — separate top-level command exposing the structural-query surface (21 JSON-RPC tools — list below) to agent hosts (Claude Code, Cursor, Codex, generic MCP clients) over stdio. Eliminates the bash round-trip on every agent call. Bootstrap once at server boot; each tool returns the same JSON payload its CLI `--json` would print (including `query batch`, `trace`, `explore`, `node`, `file`, `schema`, `symbols`, `context --include-snippets`, `ingest-coverage`, and `ingest-churn`). MCP wraps payloads in `{content: [{type: "text", text: …}]}`. **`tools/list` ToolAnnotations** — advisory `readOnlyHint` / `destructiveHint` / `idempotentHint` per tool: read paths (`query`, `show`, `audit`, …) → `readOnlyHint: true`; apply tools (`apply`, `apply_rows`, `apply_diff_input`) → `destructiveHint: true` (writes still require `yes: true`); index mutators (`save_baseline`, `drop_baseline`, `ingest_coverage`, `ingest_churn`) → `readOnlyHint: false` without `destructiveHint`. HTTP `GET /tools` exposes the same hints. **`initialize` instructions** + resource `codemap://mcp-instructions` carry the tool-selection playbook. **Watcher default-ON since 2026-05** — every tool reads a live index, `audit`'s incremental-index prelude becomes a no-op. Pass `--no-watch` (or `CODEMAP_WATCH=0`) for one-shot fire-and-forget calls without the in-process chokidar loop.
34+
**MCP server (`codemap mcp [--no-watch] [--debounce <ms>]`)** — separate top-level command exposing the structural-query surface (21 JSON-RPC tools — list below) to agent hosts (Claude Code, Cursor, Codex, generic MCP clients) over stdio. Eliminates the bash round-trip on every agent call. Bootstrap once at server boot; each tool returns the same JSON payload its CLI `--json` would print (including `query batch`, `trace`, `explore`, `node`, `file`, `schema`, `symbols`, `context --include-snippets`, `ingest-coverage`, and `ingest-churn`). MCP wraps payloads in `{content: [{type: "text", text: …}]}`. **`tools/list` ToolAnnotations** — advisory `readOnlyHint` / `destructiveHint` / `idempotentHint` per tool: read paths (`query`, `show`, `audit`, …) → `readOnlyHint: true`; apply tools (`apply`, `apply_rows`, `apply_diff_input`) → `destructiveHint: true` (writes still require `yes: true`); index mutators (`save_baseline`, `drop_baseline`, `ingest_coverage`, `ingest_churn`) → `readOnlyHint: false` without `destructiveHint`. HTTP `GET /tools` exposes the same hints. **`initialize` instructions** + resource `codemap://mcp-instructions` carry the tool-selection playbook; after bootstrap they also append **`map_id`** and top hub paths (full routing card via **`context`**). **Watcher default-ON since 2026-05** — every tool reads a live index, `audit`'s incremental-index prelude becomes a no-op. Pass `--no-watch` (or `CODEMAP_WATCH=0`) for one-shot fire-and-forget calls without the in-process chokidar loop.
3535

3636
**HTTP server (`codemap serve [--host 127.0.0.1] [--port 7878] [--token <secret>] [--no-watch] [--debounce <ms>]`)** — same tool taxonomy as MCP, exposed over `POST /tool/{name}` for non-MCP consumers (CI scripts, simple `curl`, IDE plugins that don't speak MCP). Loopback-default; any `127.0.0.0/8` bind counts as loopback for the token rule. Bearer-token auth optional on loopback binds and **required** on non-loopback binds (`--host 0.0.0.0`, etc.). HTTP returns each tool's native JSON payload directly (NOT MCP's `{content: [...]}` wrapper); SARIF / annotations / mermaid / diff payloads ship with `application/sarif+json` or `text/plain` Content-Type; `format: "diff-json"` / `"codeclimate"` / `"badge"` + `badge_style: "json"` use `application/json`; badge markdown uses `text/plain`. Resources mirrored at `GET /resources/{encoded-uri}`. `GET /health` is auth-exempt; `GET /tools` / `GET /resources` are catalogs. **Watcher default-ON since 2026-05** — same `--no-watch` / `CODEMAP_WATCH=0` opt-out as `mcp`.
3737

@@ -46,7 +46,7 @@ Each emitted delta carries its own `base` metadata so mixed-baseline audits are
4646
- **`save_baseline`** — polymorphic `{name, sql? | recipe?}` (exactly one of `sql` / `recipe`).
4747
- **`list_baselines`** — no args; returns the array `codemap query --baselines --json` would print.
4848
- **`drop_baseline`**`{name}``{dropped}` on success; structured `{error}` on unknown name (MCP sets `isError: true`).
49-
- **`context`**`{compact?, intent?, include_snippets?}`. CLI: `codemap context [--include-snippets]`. Session-start project envelope with `start_here` shortcuts (one call replaces 4-5 `query`s). `index_summary.file_churn` row count; **`churn_hint`** when empty (steers to index, **`ingest-churn`**, or **`churn.file`**). `include_snippets` adds one-line export previews on hub leaders (capped to adaptive `signature_max_chars`; may set `stale`/`missing`); no-op when `compact: true`. Whitespace-only `intent` is treated as no intent. Prefer `start_here.hub_leaders` over legacy `hubs` for signatures — `hubs` keeps the full bundled `fan-in` recipe limit for backward compatibility. `sample_markers` count scales down on repos >500 / >5000 files.
49+
- **`context`**`{compact?, intent?, include_snippets?, include_codebase_map?}`. CLI: `codemap context [--include-snippets] [--no-codebase-map]`. Session-start project envelope with `start_here` shortcuts (one call replaces 4-5 `query`s). Non-compact responses also ship **`map_id`** (hash-stable fingerprint) and **`codebase_map`** (hub paths + codemap CLI/MCP routing hints); omit with `compact: true`, `include_codebase_map: false`, or CLI `--no-codebase-map`. `index_summary.file_churn` row count; **`churn_hint`** when empty (steers to index, **`ingest-churn`**, or **`churn.file`**). `include_snippets` adds one-line export previews on hub leaders (capped to adaptive `signature_max_chars`; may set `stale`/`missing`); no-op when `compact: true`. Whitespace-only `intent` is treated as no intent. Prefer `start_here.hub_leaders` over legacy `hubs` for signatures — `hubs` keeps the full bundled `fan-in` recipe limit for backward compatibility. `sample_markers` count scales down on repos >500 / >5000 files.
5050
- **`validate`**`{paths?: string[]}`. SHA-256 vs `files.content_hash`; returns only out-of-sync rows (`stale` / `missing` / `unindexed` / `rejected` — fresh paths are omitted; `rejected` includes optional `reason`: `path escapes project root` | `path escapes via symlink` | `path resolves outside project root`). Output `path` keys are project-relative POSIX paths.
5151
- **`show`**`{name, kind?, in?}` or `{query, with_fts?}`. Exact symbol lookup or field-qualified search (`kind:`, `name:`, `path:`, `in:` + free text) → `{matches, disambiguation?, warning?}`. CLI: `codemap show --query '…' [--print-sql]`.
5252
- **`snippet`** — same as `show` (`{name, kind?, in?}` or `{query, with_fts?}`) but each match also carries `source` (file text) + `stale` / `missing` flags → `{matches, disambiguation?, warning?}`. No reindex side-effects.

0 commit comments

Comments
 (0)