Skip to content

Commit 1dd6ab0

Browse files
committed
code hardening and changelog corrections
1 parent d63b87b commit 1dd6ab0

File tree

5 files changed

+386
-226
lines changed

5 files changed

+386
-226
lines changed

CHANGELOG.md

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [0.9.2] - 2026-03-21
8+
## [0.9.1] - 2026-03-20
99

1010
### Added
1111

@@ -16,37 +16,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- Sends concise text summaries to the user (e.g., `symbol.search "foo" → 5 results`)
1717
- JSON responses remain unchanged for the LLM
1818
- `logging` capability declared in MCP Server capabilities
19+
- Unified runtime diagnostics via CLI `sdl-mcp info` and MCP `sdl.info`
20+
- `prepare-release` and `inspector` npm workflows for release preflight and MCP inspection
21+
- Performance cache for repo overview with singleflight deduplication (5s TTL)
1922

2023
### Changed
2124

2225
- `renderSessionSummary` now skips lifetime section when no lifetime data exists (avoids misleading zeros when DB is unavailable)
2326
- Session-scope usage stats now fetch lifetime data from LadybugDB for combined summary display
2427
- Usage stats `formattedSummary` is sent as MCP notification to user and stripped from tool response (reduces unnecessary LLM context)
25-
26-
### Removed
27-
28-
- `renderTaskSummary` function (redundant with `renderSessionSummary`)
29-
30-
## [0.9.1] - 2026-03-20
31-
32-
### Added
33-
34-
- Unified runtime diagnostics via CLI `sdl-mcp info` and MCP `sdl.info`
35-
- `prepare-release` and `inspector` npm workflows for release preflight and MCP inspection
36-
37-
### Changed
38-
28+
- Refactored pass2 edge builder into focused sub-modules (`enclosing-symbol`, `symbol-mapping`, `target-selection`, `unresolved-imports`)
3929
- MCP tool registration now publishes human-friendly titles, version-stamped descriptions, and shared request normalization across flat, gateway, and CLI tool-dispatch surfaces
4030
- Gateway `tools/list` schemas now preserve action-specific fields, descriptions, and defaults instead of collapsing to a bare envelope
4131
- Logging is now file-first with `SDL_LOG_FILE`, `SDL_CONSOLE_LOGGING`, temp-path fallback, and explicit shutdown flushing
4232
- Documentation was refreshed across CLI, architecture, troubleshooting, gateway, code-mode, and release workflow references
4333

4434
### Fixed
4535

36+
- Cypher query bug: pre-compute word-boundary search strings on JS side to avoid LadybugDB expression evaluation issues in CASE expressions
37+
- Cypher query bug: ORDER BY in memory queries now references projected aliases instead of qualified node properties
4638
- Native platform package versions are synchronized with the root `sdl-mcp` release version
4739
- `prepare-release` now launches npm subcommands correctly on Windows
4840
- Release preflight packaging and stdio smoke coverage now validate the new diagnostics and tool registration surfaces
4941

42+
### Removed
43+
44+
- `renderTaskSummary` function (redundant with `renderSessionSummary`)
45+
5046
## [0.9.0] - 2026-03-17
5147

5248
### Added

src/mcp/tool-call-formatter.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ function tok(n: number): string {
2525
return (n / 1_000_000).toFixed(2) + "M";
2626
}
2727

28+
function safeRange(obj: unknown): { startLine?: number; endLine?: number } | undefined {
29+
if (!obj || typeof obj !== "object") return undefined;
30+
const r = obj as Record<string, unknown>;
31+
return {
32+
startLine: typeof r.startLine === "number" ? r.startLine : undefined,
33+
endLine: typeof r.endLine === "number" ? r.endLine : undefined,
34+
};
35+
}
36+
2837
function rng(r: { startLine?: number; endLine?: number } | undefined): string {
2938
if (!r) return "";
3039
return `L${r.startLine ?? "?"}${r.endLine ?? "?"}`;
@@ -68,7 +77,7 @@ function fmtSymbolGetCard(_a: Record<string, unknown>, r: Record<string, unknown
6877
const deps = card.deps as Record<string, unknown[]> | undefined;
6978
const imports = deps?.imports?.length ?? 0;
7079
const calls = deps?.calls?.length ?? 0;
71-
return `symbol.getCard → ${str(card.name)} (${str(card.kind)})\n File: ${shortPath(str(card.file))} ${rng(card.range as any)}\n Deps: ${imports} imports, ${calls} calls`;
80+
return `symbol.getCard → ${str(card.name)} (${str(card.kind)})\n File: ${shortPath(str(card.file))} ${rng(safeRange(card.range))}\n Deps: ${imports} imports, ${calls} calls`;
7281
}
7382

7483
function fmtSymbolGetCards(_a: Record<string, unknown>, r: Record<string, unknown>): string | null {
@@ -82,15 +91,15 @@ function fmtCodeSkeleton(_a: Record<string, unknown>, r: Record<string, unknown>
8291
const orig = num(r.originalLines);
8392
const est = num(r.estimatedTokens);
8493
const trunc = r.truncated ? " (truncated)" : "";
85-
return `code.getSkeleton → ${shortPath(file)}\n ${orig} → skeleton ${rng(r.range as any)}${trunc} (~${tok(est)} tokens)`;
94+
return `code.getSkeleton → ${shortPath(file)}\n ${orig} → skeleton ${rng(safeRange(r.range))}${trunc} (~${tok(est)} tokens)`;
8695
}
8796

8897
function fmtCodeHotPath(_a: Record<string, unknown>, r: Record<string, unknown>): string | null {
8998
const matched = r.matchedIdentifiers as string[] | undefined;
9099
const requested = (_a.identifiersToFind as string[])?.length ?? 0;
91100
const found = matched?.length ?? 0;
92101
const trunc = r.truncated ? " (truncated)" : "";
93-
return `code.getHotPath → matched ${found}/${requested} identifiers ${rng(r.range as any)}${trunc}\n (~${tok(num(r.estimatedTokens))} tokens)`;
102+
return `code.getHotPath → matched ${found}/${requested} identifiers ${rng(safeRange(r.range))}${trunc}\n (~${tok(num(r.estimatedTokens))} tokens)`;
94103
}
95104

96105
function fmtCodeNeedWindow(_a: Record<string, unknown>, r: Record<string, unknown>): string | null {
@@ -99,10 +108,10 @@ function fmtCodeNeedWindow(_a: Record<string, unknown>, r: Record<string, unknow
99108
const downgraded = r.downgradedFrom ? ` (downgraded from ${str(r.downgradedFrom)})` : "";
100109
const est = num(r.estimatedTokens);
101110
if (!approved) {
102-
const next = str((r as any).nextBestAction);
111+
const next = str(r.nextBestAction);
103112
return `code.needWindow → [${status}]${next ? "\n Suggestion: " + next : ""}`;
104113
}
105-
return `code.needWindow → [${status}]${downgraded} ${rng(r.range as any)}\n (~${tok(est)} tokens)`;
114+
return `code.needWindow → [${status}]${downgraded} ${rng(safeRange(r.range))}\n (~${tok(est)} tokens)`;
106115
}
107116

108117
function fmtSliceBuild(_a: Record<string, unknown>, r: Record<string, unknown>): string | null {

src/mcp/tools/usage.ts

Lines changed: 47 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,46 @@ import {
2525
import { ValidationError, DatabaseError } from "../errors.js";
2626
import { ZodError } from "zod";
2727

28+
29+
const EMPTY_AGGREGATE: AggregateUsage = {
30+
totalSdlTokens: 0,
31+
totalRawEquivalent: 0,
32+
totalSavedTokens: 0,
33+
overallSavingsPercent: 0,
34+
totalCalls: 0,
35+
sessionCount: 0,
36+
};
37+
38+
function toAggregateUsage(aggregate: Record<string, number>): AggregateUsage {
39+
return {
40+
totalSdlTokens: aggregate.totalSdlTokens,
41+
totalRawEquivalent: aggregate.totalRawEquivalent,
42+
totalSavedTokens: aggregate.totalSavedTokens,
43+
overallSavingsPercent: aggregate.overallSavingsPercent,
44+
totalCalls: aggregate.totalCalls,
45+
sessionCount: aggregate.sessionCount,
46+
};
47+
}
48+
49+
async function fetchLifetimeAggregate(
50+
repoId: string | undefined,
51+
): Promise<{ ltAggregate: AggregateUsage; allToolEntries: import("../token-accumulator.js").ToolUsageEntry[] }> {
52+
try {
53+
const conn = await getLadybugConn();
54+
const aggregate = await getAggregateUsage(conn, { repoId });
55+
const snapshots = await getUsageSnapshots(conn, { repoId, limit: 100 });
56+
const allToolEntries = aggregateToolBreakdowns(
57+
snapshots.map((s) => s.toolBreakdownJson),
58+
);
59+
return { ltAggregate: toAggregateUsage(aggregate), allToolEntries };
60+
} catch {
61+
process.stderr.write(
62+
"[sdl-mcp] Usage stats: could not fetch lifetime data from LadybugDB\n",
63+
);
64+
return { ltAggregate: EMPTY_AGGREGATE, allToolEntries: [] };
65+
}
66+
}
67+
2868
export async function handleUsageStats(
2969
args: unknown,
3070
): Promise<UsageStatsResponse> {
@@ -107,14 +147,7 @@ export async function handleUsageStats(
107147
};
108148

109149
// Build formatted summary for history-aware scopes
110-
const ltAggregate: AggregateUsage = {
111-
totalSdlTokens: aggregate.totalSdlTokens,
112-
totalRawEquivalent: aggregate.totalRawEquivalent,
113-
totalSavedTokens: aggregate.totalSavedTokens,
114-
overallSavingsPercent: aggregate.overallSavingsPercent,
115-
totalCalls: aggregate.totalCalls,
116-
sessionCount: aggregate.sessionCount,
117-
};
150+
const ltAggregate: AggregateUsage = toAggregateUsage(aggregate);
118151

119152
if (request.scope === "both" && response.session) {
120153
response.formattedSummary = renderSessionSummary(
@@ -132,36 +165,12 @@ export async function handleUsageStats(
132165

133166
// Session-only: still fetch lifetime for the combined summary
134167
if (request.scope === "session" && response.session) {
135-
try {
136-
const conn = await getLadybugConn();
137-
const aggregate = await getAggregateUsage(conn, { repoId: request.repoId });
138-
const snapshots = await getUsageSnapshots(conn, { repoId: request.repoId, limit: 100 });
139-
const allToolEntries = aggregateToolBreakdowns(snapshots.map((s) => s.toolBreakdownJson));
140-
const ltAggregate: AggregateUsage = {
141-
totalSdlTokens: aggregate.totalSdlTokens,
142-
totalRawEquivalent: aggregate.totalRawEquivalent,
143-
totalSavedTokens: aggregate.totalSavedTokens,
144-
overallSavingsPercent: aggregate.overallSavingsPercent,
145-
totalCalls: aggregate.totalCalls,
146-
sessionCount: aggregate.sessionCount,
147-
};
148-
response.formattedSummary = renderSessionSummary(
149-
response.session,
150-
ltAggregate,
151-
allToolEntries,
152-
);
153-
} catch {
154-
// Fallback: session-only without lifetime (DB unavailable)
155-
process.stderr.write(
156-
"[sdl-mcp] Usage stats: could not fetch lifetime data from LadybugDB\n",
157-
);
158-
// Render session section only — omit lifetime to avoid showing misleading zeros
159-
response.formattedSummary = renderSessionSummary(
160-
response.session,
161-
{ totalSdlTokens: 0, totalRawEquivalent: 0, totalSavedTokens: 0, overallSavingsPercent: 0, totalCalls: 0, sessionCount: 0 },
162-
[],
163-
);
164-
}
168+
const { ltAggregate, allToolEntries } = await fetchLifetimeAggregate(request.repoId);
169+
response.formattedSummary = renderSessionSummary(
170+
response.session,
171+
ltAggregate,
172+
allToolEntries,
173+
);
165174
}
166175

167176
return response;

0 commit comments

Comments
 (0)