semantic-js-mcp 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/.prettierrc.json +1 -0
  3. package/CHANGELOG.md +43 -0
  4. package/CONTRIBUTING.md +23 -3
  5. package/README.md +44 -69
  6. package/ROADMAP.md +2 -2
  7. package/docs/architecture-decisions.md +35 -22
  8. package/docs/distribution.md +19 -0
  9. package/docs/getting-started.md +72 -0
  10. package/docs/result-schema-migration.md +67 -0
  11. package/lib/doctor.mjs +3 -2
  12. package/lib/file-identity.mjs +18 -0
  13. package/lib/pending-requests.mjs +38 -0
  14. package/lib/runtime.mjs +4 -2
  15. package/lib/semantic-evidence.mjs +44 -0
  16. package/lib/stable-collection.mjs +10 -0
  17. package/lib/temporary-directory.mjs +15 -0
  18. package/package.json +28 -4
  19. package/protocol.mjs +112 -3
  20. package/scripts/agent-evaluation-contract.mjs +165 -0
  21. package/scripts/agent-evaluation-smoke.mjs +44 -0
  22. package/scripts/agent-evaluation.mjs +37 -0
  23. package/scripts/check-protocol-literals.mjs +30 -0
  24. package/scripts/ci-smoke.mjs +77 -5
  25. package/scripts/codex-plugin-verification.mjs +129 -0
  26. package/scripts/distribution-policy.mjs +28 -7
  27. package/scripts/distribution-smoke.mjs +19 -14
  28. package/scripts/doctor-smoke.mjs +40 -0
  29. package/scripts/documentation-contract.mjs +39 -0
  30. package/scripts/documentation-gate-smoke.mjs +55 -0
  31. package/scripts/documentation-gate.mjs +110 -0
  32. package/scripts/generate-protocol-reference.mjs +32 -3
  33. package/scripts/lifecycle-memory-benchmark.mjs +300 -0
  34. package/scripts/lifecycle-memory-contract.mjs +53 -0
  35. package/scripts/lifecycle-memory-observer.mjs +29 -0
  36. package/scripts/lifecycle-smoke.mjs +309 -0
  37. package/scripts/negative-verification-smoke.mjs +243 -0
  38. package/scripts/postpublication-smoke.mjs +245 -0
  39. package/scripts/release-contract.mjs +78 -0
  40. package/scripts/release-smoke.mjs +123 -0
  41. package/scripts/release-verify.mjs +64 -0
  42. package/scripts/repository-matrix-contract.mjs +80 -0
  43. package/scripts/repository-matrix-smoke.mjs +153 -0
  44. package/scripts/repository-matrix.mjs +276 -0
  45. package/scripts/semantic-js-mcp-ci.mjs +40 -26
  46. package/scripts/smoke.mjs +307 -47
  47. package/scripts/vue-smoke.mjs +352 -7
  48. package/server.mjs +736 -315
  49. package/skills/semantic-navigation/SKILL.md +23 -10
  50. package/skills/semantic-navigation/references/protocol-literals.md +110 -4
package/lib/doctor.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import {mkdtemp, mkdir, realpath, rm, writeFile} from "node:fs/promises";
2
+ import {mkdtemp, mkdir, realpath, writeFile} from "node:fs/promises";
3
3
  import {tmpdir} from "node:os";
4
4
  import {Client} from "@modelcontextprotocol/sdk/client/index.js";
5
5
  import {StdioClientTransport} from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -24,6 +24,7 @@ import {
24
24
  TOOL_ORDER,
25
25
  } from "../protocol.mjs";
26
26
  import {PACKAGE_ROOT, inspectExternalCommand, inspectNodeRuntime, inspectRuntimeComponents} from "./runtime.mjs";
27
+ import {removeTemporaryDirectory} from "./temporary-directory.mjs";
27
28
 
28
29
  const STATUS_EXIT_CODE = Object.freeze({
29
30
  [CI_STATUS.PASS]: CI_EXIT_CODE.PASS,
@@ -312,7 +313,7 @@ export async function runDoctor({packageRoot = PACKAGE_ROOT} = {}) {
312
313
  }
313
314
  } finally {
314
315
  await client.close().catch(() => undefined);
315
- await rm(fixture.workspace, {recursive: true, force: true});
316
+ await removeTemporaryDirectory(fixture.workspace);
316
317
  }
317
318
 
318
319
  return doctorResult(packageRoot, checks, runtime);
@@ -0,0 +1,18 @@
1
+ import path from "node:path";
2
+ import {OPERATING_SYSTEM} from "../protocol.mjs";
3
+
4
+ export function fileIdentity(file, operatingSystem = process.platform) {
5
+ const pathImplementation = operatingSystem === OPERATING_SYSTEM.WINDOWS ? path.win32 : path;
6
+ const resolved = pathImplementation.resolve(file);
7
+ return operatingSystem === OPERATING_SYSTEM.WINDOWS ? resolved.toLowerCase() : resolved;
8
+ }
9
+
10
+ export function locationKeyAt(file, line, column, operatingSystem = process.platform) {
11
+ return `${fileIdentity(file, operatingSystem)}:${line}:${column}`;
12
+ }
13
+
14
+ export function locationKeyForOperatingSystem(operatingSystem) {
15
+ return (location) => locationKeyAt(location.file, location.range.start.line, location.range.start.column, operatingSystem);
16
+ }
17
+
18
+ export const locationKey = locationKeyForOperatingSystem(process.platform);
@@ -0,0 +1,38 @@
1
+ export class PendingRequestRegistry {
2
+ constructor({timeoutMilliseconds, timeoutMessage}) {
3
+ this.timeoutMilliseconds = timeoutMilliseconds;
4
+ this.timeoutMessage = timeoutMessage;
5
+ this.entries = new Map();
6
+ }
7
+
8
+ get size() {
9
+ return this.entries.size;
10
+ }
11
+
12
+ create(key, operation) {
13
+ if (this.entries.has(key)) throw new Error(`Pending request already exists: ${key}`);
14
+ return new Promise((resolve, reject) => {
15
+ const timer = setTimeout(() => {
16
+ this.entries.delete(key);
17
+ reject(new Error(this.timeoutMessage(operation)));
18
+ }, this.timeoutMilliseconds);
19
+ this.entries.set(key, {resolve, reject, timer});
20
+ });
21
+ }
22
+
23
+ take(key) {
24
+ const entry = this.entries.get(key);
25
+ if (!entry) return undefined;
26
+ clearTimeout(entry.timer);
27
+ this.entries.delete(key);
28
+ return entry;
29
+ }
30
+
31
+ rejectAll(error) {
32
+ for (const entry of this.entries.values()) {
33
+ clearTimeout(entry.timer);
34
+ entry.reject(error);
35
+ }
36
+ this.entries.clear();
37
+ }
38
+ }
package/lib/runtime.mjs CHANGED
@@ -5,6 +5,8 @@ import path from "node:path";
5
5
  import {fileURLToPath} from "node:url";
6
6
  import {CONFIGURATION_FILE, NODE_EVENT, PROCESS_EXIT_CODE, REQUIRED_RUNTIME_COMPONENT, RUNTIME_REQUIREMENT} from "../protocol.mjs";
7
7
 
8
+ const STREAM_EVENT = Object.freeze({DATA: "data"});
9
+
8
10
  export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
11
 
10
12
  export function inspectNodeRuntime() {
@@ -50,10 +52,10 @@ export function inspectExternalCommand(command, args) {
50
52
  const child = spawn(command, args, {stdio: ["ignore", "pipe", "pipe"]});
51
53
  let stdout = "";
52
54
  let stderr = "";
53
- child.stdout.on("data", (chunk) => {
55
+ child.stdout.on(STREAM_EVENT.DATA, (chunk) => {
54
56
  stdout += chunk;
55
57
  });
56
- child.stderr.on("data", (chunk) => {
58
+ child.stderr.on(STREAM_EVENT.DATA, (chunk) => {
57
59
  stderr += chunk;
58
60
  });
59
61
  child.on(NODE_EVENT.ERROR, (error) => resolve({command, available: false, error: error.message}));
@@ -0,0 +1,44 @@
1
+ import {
2
+ COLLECTION_STATUS,
3
+ DEFINITION_SELECTION_STATUS,
4
+ SEMANTIC_EVIDENCE_FOLLOW_UP_REASON,
5
+ SEMANTIC_EVIDENCE_STATUS,
6
+ TOOL,
7
+ } from "../protocol.mjs";
8
+
9
+ const NAMED_SYMBOL_TOOLS = new Set([TOOL.COUNT_NAMED_SYMBOL, TOOL.AUDIT_NAMED_SYMBOL]);
10
+ const DEFINITION_SELECTION_STATUSES = new Set(Object.values(DEFINITION_SELECTION_STATUS));
11
+
12
+ export function isNamedSymbolTool(tool) {
13
+ return NAMED_SYMBOL_TOOLS.has(tool);
14
+ }
15
+
16
+ export function namedSemanticEvidence(selectionStatus, collectionStatus) {
17
+ const followUpReasons = [];
18
+ if (collectionStatus === COLLECTION_STATUS.LIMITED) {
19
+ followUpReasons.push(SEMANTIC_EVIDENCE_FOLLOW_UP_REASON.COLLECTION_LIMITED);
20
+ }
21
+ if (collectionStatus === COLLECTION_STATUS.PARTIAL) {
22
+ followUpReasons.push(SEMANTIC_EVIDENCE_FOLLOW_UP_REASON.COLLECTION_PARTIAL);
23
+ }
24
+ if (selectionStatus === DEFINITION_SELECTION_STATUS.NONE) {
25
+ followUpReasons.push(SEMANTIC_EVIDENCE_FOLLOW_UP_REASON.NO_DEFINITION_SELECTED);
26
+ }
27
+ if (selectionStatus === DEFINITION_SELECTION_STATUS.MULTIPLE) {
28
+ followUpReasons.push(SEMANTIC_EVIDENCE_FOLLOW_UP_REASON.MULTIPLE_DEFINITIONS_SELECTED);
29
+ }
30
+ return {
31
+ status: followUpReasons.length === 0 ? SEMANTIC_EVIDENCE_STATUS.USABLE : SEMANTIC_EVIDENCE_STATUS.FOLLOW_UP_REQUIRED,
32
+ followUpReasons,
33
+ };
34
+ }
35
+
36
+ export function namedSemanticEvidenceMatches(result, collectionStatus) {
37
+ const actual = result?.semanticEvidence;
38
+ if (!actual || !Array.isArray(actual.followUpReasons)) return false;
39
+ if (!DEFINITION_SELECTION_STATUSES.has(result.definitionSelectionStatus)) return false;
40
+ const expected = namedSemanticEvidence(result.definitionSelectionStatus, collectionStatus);
41
+ if (actual.status !== expected.status) return false;
42
+ if (actual.followUpReasons.length !== expected.followUpReasons.length) return false;
43
+ return actual.followUpReasons.every((reason, index) => reason === expected.followUpReasons[index]);
44
+ }
@@ -0,0 +1,10 @@
1
+ export async function collectStableSnapshot({attempts, collect, inventory, sameInventory, fingerprint}) {
2
+ for (let attempt = 1; attempt <= attempts; attempt++) {
3
+ const inventoryBefore = await inventory();
4
+ const value = await collect();
5
+ const [fingerprints, inventoryAfter] = await Promise.all([fingerprint(value), inventory()]);
6
+ if (!sameInventory(inventoryBefore, inventoryAfter)) continue;
7
+ return {value, fingerprints, inventory: inventoryAfter, attempt};
8
+ }
9
+ return undefined;
10
+ }
@@ -0,0 +1,15 @@
1
+ import {rm} from "node:fs/promises";
2
+
3
+ const CLEANUP = Object.freeze({
4
+ MAXIMUM_RETRIES: 10,
5
+ RETRY_DELAY_MILLISECONDS: 100,
6
+ });
7
+
8
+ export async function removeTemporaryDirectory(directory) {
9
+ await rm(directory, {
10
+ recursive: true,
11
+ force: true,
12
+ maxRetries: CLEANUP.MAXIMUM_RETRIES,
13
+ retryDelay: CLEANUP.RETRY_DELAY_MILLISECONDS,
14
+ });
15
+ }
package/package.json CHANGED
@@ -1,7 +1,19 @@
1
1
  {
2
2
  "name": "semantic-js-mcp",
3
- "version": "0.8.1",
3
+ "version": "0.10.0",
4
4
  "description": "Semantic understanding of JavaScript codebases for AI agents",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "lsp",
9
+ "language-server-protocol",
10
+ "javascript",
11
+ "typescript",
12
+ "vue",
13
+ "semantic-analysis",
14
+ "code-navigation",
15
+ "ai-agents"
16
+ ],
5
17
  "author": {
6
18
  "name": "Jonathan Muñoz Lucas",
7
19
  "email": "git@jonathanmunoz.dev",
@@ -32,17 +44,29 @@
32
44
  "node": ">=22"
33
45
  },
34
46
  "scripts": {
35
- "check": "npm run format:check && node --check protocol.mjs && node --check server.mjs && node --check cli.mjs && node --check lib/runtime.mjs && node --check lib/doctor.mjs && node --check scripts/generate-protocol-reference.mjs && node --check scripts/check-protocol-literals.mjs && node --check scripts/check-runtime.mjs && node --check scripts/semantic-js-mcp-ci.mjs && node --check scripts/ci-smoke.mjs && node --check scripts/smoke.mjs && node --check scripts/vue-smoke.mjs && node --check scripts/benchmark.mjs && node --check scripts/distribution-policy.mjs && node --check scripts/distribution-smoke.mjs && node scripts/generate-protocol-reference.mjs --check && node scripts/check-protocol-literals.mjs",
47
+ "check": "npm run format:check && node --check protocol.mjs && node --check server.mjs && node --check cli.mjs && node --check lib/runtime.mjs && node --check lib/doctor.mjs && node --check lib/semantic-evidence.mjs && node --check lib/pending-requests.mjs && node --check lib/stable-collection.mjs && node --check scripts/generate-protocol-reference.mjs && node --check scripts/check-protocol-literals.mjs && node --check scripts/check-runtime.mjs && node --check scripts/documentation-contract.mjs && node --check scripts/documentation-gate.mjs && node --check scripts/documentation-gate-smoke.mjs && node --check scripts/semantic-js-mcp-ci.mjs && node --check scripts/ci-smoke.mjs && node --check scripts/negative-verification-smoke.mjs && node --check scripts/doctor-smoke.mjs && node --check scripts/smoke.mjs && node --check scripts/vue-smoke.mjs && node --check scripts/lifecycle-smoke.mjs && node --check scripts/lifecycle-memory-contract.mjs && node --check scripts/lifecycle-memory-observer.mjs && node --check scripts/lifecycle-memory-benchmark.mjs && node --check scripts/benchmark.mjs && node --check scripts/distribution-policy.mjs && node --check scripts/distribution-smoke.mjs && node --check scripts/release-contract.mjs && node --check scripts/release-verify.mjs && node --check scripts/release-smoke.mjs && node --check scripts/postpublication-smoke.mjs && node --check scripts/codex-plugin-verification.mjs && node --check scripts/agent-evaluation-contract.mjs && node --check scripts/agent-evaluation.mjs && node --check scripts/agent-evaluation-smoke.mjs && node --check scripts/repository-matrix-contract.mjs && node --check scripts/repository-matrix.mjs && node --check scripts/repository-matrix-smoke.mjs && node scripts/generate-protocol-reference.mjs --check && node scripts/check-protocol-literals.mjs && node scripts/documentation-gate-smoke.mjs",
36
48
  "check:runtime": "node scripts/check-runtime.mjs",
49
+ "check:documentation": "node scripts/documentation-gate.mjs",
37
50
  "doctor": "node cli.mjs doctor",
38
51
  "ci:evaluate": "node scripts/semantic-js-mcp-ci.mjs",
39
52
  "smoke": "node scripts/smoke.mjs",
40
53
  "smoke:ci": "node scripts/ci-smoke.mjs",
54
+ "smoke:negative": "node scripts/negative-verification-smoke.mjs",
55
+ "smoke:doctor": "node scripts/doctor-smoke.mjs",
41
56
  "smoke:vue": "node scripts/vue-smoke.mjs",
57
+ "smoke:lifecycle": "node scripts/lifecycle-smoke.mjs",
42
58
  "smoke:distribution": "node scripts/distribution-smoke.mjs",
59
+ "smoke:release": "node scripts/release-smoke.mjs",
60
+ "smoke:evaluation": "node scripts/agent-evaluation-smoke.mjs",
61
+ "smoke:matrix": "node scripts/repository-matrix-smoke.mjs",
62
+ "evaluate:agent": "node scripts/agent-evaluation.mjs",
63
+ "validate:repositories": "node scripts/repository-matrix.mjs",
64
+ "release:verify": "node scripts/release-verify.mjs",
65
+ "verify:published": "node scripts/postpublication-smoke.mjs",
43
66
  "benchmark": "node scripts/benchmark.mjs",
44
- "format": "prettier --write \".codex-plugin/**/*.json\" \".agents/plugins/marketplace.json\" \".mcp.json\" \".prettierrc.json\" \"package.json\" \"package-lock.json\" \"CHANGELOG.md\" \"CONTRIBUTING.md\" \"README.md\" \"ROADMAP.md\" \"SECURITY.md\" \"*.mjs\" \"docs/**/*.md\" \"lib/**/*.mjs\" \"scripts/**/*.mjs\" \"skills/**/SKILL.md\" \"skills/**/agents/*.yaml\"",
45
- "format:check": "prettier --check \".codex-plugin/**/*.json\" \".agents/plugins/marketplace.json\" \".mcp.json\" \".prettierrc.json\" \"package.json\" \"package-lock.json\" \"CHANGELOG.md\" \"CONTRIBUTING.md\" \"README.md\" \"ROADMAP.md\" \"SECURITY.md\" \"*.mjs\" \"docs/**/*.md\" \"lib/**/*.mjs\" \"scripts/**/*.mjs\" \"skills/**/SKILL.md\" \"skills/**/agents/*.yaml\""
67
+ "benchmark:lifecycle-memory": "node scripts/lifecycle-memory-benchmark.mjs",
68
+ "format": "prettier --write \".codex-plugin/**/*.json\" \".agents/plugins/marketplace.json\" \".github/**/*.yml\" \".mcp.json\" \".prettierrc.json\" \"package.json\" \"package-lock.json\" \"CHANGELOG.md\" \"CONTRIBUTING.md\" \"README.md\" \"ROADMAP.md\" \"SECURITY.md\" \"*.mjs\" \"docs/**/*.md\" \"lib/**/*.mjs\" \"scripts/**/*.mjs\" \"skills/**/SKILL.md\" \"skills/**/agents/*.yaml\"",
69
+ "format:check": "prettier --check \".codex-plugin/**/*.json\" \".agents/plugins/marketplace.json\" \".github/**/*.yml\" \".mcp.json\" \".prettierrc.json\" \"package.json\" \"package-lock.json\" \"CHANGELOG.md\" \"CONTRIBUTING.md\" \"README.md\" \"ROADMAP.md\" \"SECURITY.md\" \"*.mjs\" \"docs/**/*.md\" \"lib/**/*.mjs\" \"scripts/**/*.mjs\" \"skills/**/SKILL.md\" \"skills/**/agents/*.yaml\""
46
70
  },
47
71
  "dependencies": {
48
72
  "@modelcontextprotocol/sdk": "1.27.1",
package/protocol.mjs CHANGED
@@ -125,9 +125,53 @@ export const LANGUAGE_ID = Object.freeze({
125
125
  VUE: "vue",
126
126
  });
127
127
 
128
+ export const COMMON_VALUE = Object.freeze({
129
+ UNKNOWN: "unknown",
130
+ });
131
+
132
+ export const SEARCH_SCOPE = Object.freeze({
133
+ DOCUMENT: "document",
134
+ });
135
+
136
+ export const RUNTIME_PACKAGE = Object.freeze({
137
+ TYPESCRIPT: "typescript",
138
+ });
139
+
128
140
  export const VUE_SCRIPT_LANGUAGE = Object.freeze({
129
141
  JAVASCRIPT: "js",
130
142
  JAVASCRIPT_REACT: "jsx",
143
+ TYPESCRIPT: "ts",
144
+ TYPESCRIPT_REACT: "tsx",
145
+ });
146
+
147
+ export const DIAGNOSTIC_PROVIDER = Object.freeze({
148
+ TYPESCRIPT_LANGUAGE_SERVER: "typescript-language-server",
149
+ VUE_LANGUAGE_SERVER: "vue-language-server",
150
+ UNKNOWN: COMMON_VALUE.UNKNOWN,
151
+ });
152
+
153
+ export const DIAGNOSTIC_REGION = Object.freeze({
154
+ DOCUMENT: SEARCH_SCOPE.DOCUMENT,
155
+ SCRIPT: "script",
156
+ SCRIPT_SETUP: "script-setup",
157
+ TEMPLATE: "template",
158
+ STYLE: "style",
159
+ CUSTOM_BLOCK: "custom-block",
160
+ UNKNOWN: COMMON_VALUE.UNKNOWN,
161
+ });
162
+
163
+ export const DIAGNOSTIC_LANGUAGE = Object.freeze({
164
+ TYPESCRIPT: LANGUAGE_ID.TYPESCRIPT,
165
+ TYPESCRIPT_REACT: LANGUAGE_ID.TYPESCRIPT_REACT,
166
+ JAVASCRIPT: LANGUAGE_ID.JAVASCRIPT,
167
+ JAVASCRIPT_REACT: LANGUAGE_ID.JAVASCRIPT_REACT,
168
+ VUE: LANGUAGE_ID.VUE,
169
+ HTML: "html",
170
+ PUG: "pug",
171
+ CSS: "css",
172
+ SCSS: "scss",
173
+ LESS: "less",
174
+ UNKNOWN: COMMON_VALUE.UNKNOWN,
131
175
  });
132
176
 
133
177
  export const FORBIDDEN_PUBLIC_FIELD = Object.freeze([
@@ -151,10 +195,43 @@ export const PRESENTATION_MODE = Object.freeze({
151
195
  ALL_ITEMS: "all-items",
152
196
  SUBSET: "subset",
153
197
  COUNT_ONLY: "count-only",
154
- SUMMARY_BY_FILE: "summary-by-file",
198
+ COMPACT_SUMMARY: "compact-summary",
155
199
  PAGE: "page",
156
200
  });
157
201
 
202
+ export const TOOL_DESCRIPTION = Object.freeze({
203
+ [TOOL.DOCUMENT_SYMBOLS]: `Returns declarations and nested members for one file. Use ${TOOL.DEFINITION} or ${TOOL.AUDIT_SYMBOL} with an exact position for semantic identity.`,
204
+ [TOOL.WORKSPACE_SYMBOLS]: `Returns declaration-shaped symbols whose names contain a query. Use ${TOOL.COUNT_NAMED_SYMBOL} or ${TOOL.AUDIT_NAMED_SYMBOL} with an exact name.`,
205
+ [TOOL.DEFINITION]: `Resolves definitions for one source position. Use ${TOOL.HOVER} for type information or ${TOOL.COUNT_REFERENCES} for impact scope.`,
206
+ [TOOL.HOVER]: `Returns inferred type information and documentation for one source position. Use ${TOOL.DEFINITION} for declaration identity.`,
207
+ [TOOL.DIAGNOSTICS]:
208
+ "Returns diagnostics for one file and marks evidence untrusted when the current document snapshot cannot be confirmed.",
209
+ [TOOL.COUNT_TEXT_MATCHES]: `Counts exact identifier text matches without semantic verification. Use ${TOOL.COUNT_NAMED_SYMBOL} or ${TOOL.AUDIT_NAMED_SYMBOL} to verify identity.`,
210
+ [TOOL.COUNT_NAMED_SYMBOL]: `Returns exact-definition and verified-reference counts plus explicit semantic follow-up status for a symbol name. Use ${TOOL.AUDIT_NAMED_SYMBOL} for identity, signature, or file-hint binding verification and ${TOOL.REFERENCE_PAGE} with a returned referenceSetId for locations.`,
211
+ [TOOL.COUNT_REFERENCES]: `Returns verified-reference counts for the symbol at one source position. Use ${TOOL.AUDIT_SYMBOL} for identity and signature or ${TOOL.REFERENCE_PAGE} with the returned referenceSetId for locations.`,
212
+ [TOOL.AUDIT_NAMED_SYMBOL]: `Returns a compact exact-name audit with explicit semantic follow-up status and verifies source bindings to fileHint when no declaration is selected. Use ${TOOL.REFERENCE_PAGE} with a returned referenceSetId for locations.`,
213
+ [TOOL.AUDIT_SYMBOL]: `Returns a compact definition, signature, coverage, and freshness summary for one source position. Use ${TOOL.REFERENCE_PAGE} with the returned referenceSetId for locations.`,
214
+ [TOOL.REFERENCES]: `Returns the first page of verified source locations and detailed collection evidence. Use ${TOOL.REFERENCE_PAGE} for later pages.`,
215
+ [TOOL.REFERENCE_PAGE]: `Returns a page from a freshness-checked reference set created by a count, audit, or ${TOOL.REFERENCES} call.`,
216
+ [TOOL.UNRESOLVED_REFERENCE_PAGE]:
217
+ "Returns freshness-checked text-match candidates whose definitions could not be resolved, including literal failure reasons.",
218
+ });
219
+
220
+ export const LSP_METHOD = Object.freeze({
221
+ DOCUMENT_DIAGNOSTIC: "textDocument/diagnostic",
222
+ EXECUTE_COMMAND: "workspace/executeCommand",
223
+ PUBLISH_DIAGNOSTICS: "textDocument/publishDiagnostics",
224
+ WORKSPACE_DIAGNOSTIC_REFRESH: "workspace/diagnostic/refresh",
225
+ });
226
+
227
+ export const LSP_COMMAND = Object.freeze({
228
+ GO_TO_SOURCE_DEFINITION: "_typescript.goToSourceDefinition",
229
+ });
230
+
231
+ export const TYPESCRIPT_SERVER_COMMAND = Object.freeze({
232
+ DEFINITION_AND_BOUND_SPAN: "definitionAndBoundSpan",
233
+ });
234
+
158
235
  export const LIMIT_MODE = Object.freeze({
159
236
  UNLIMITED: "unlimited",
160
237
  MAXIMUM: "maximum",
@@ -170,6 +247,24 @@ export const DEFINITION_MATCH = Object.freeze({
170
247
  UNRESOLVED: "unresolved",
171
248
  });
172
249
 
250
+ export const DEFINITION_SELECTION_STATUS = Object.freeze({
251
+ NONE: "no-definition-selected",
252
+ ONE: "one-definition-selected",
253
+ MULTIPLE: "multiple-definitions-selected",
254
+ });
255
+
256
+ export const SEMANTIC_EVIDENCE_STATUS = Object.freeze({
257
+ USABLE: "usable-as-requested",
258
+ FOLLOW_UP_REQUIRED: "follow-up-required",
259
+ });
260
+
261
+ export const SEMANTIC_EVIDENCE_FOLLOW_UP_REASON = Object.freeze({
262
+ COLLECTION_LIMITED: "collection-is-limited",
263
+ COLLECTION_PARTIAL: "collection-is-partial",
264
+ NO_DEFINITION_SELECTED: DEFINITION_SELECTION_STATUS.NONE,
265
+ MULTIPLE_DEFINITIONS_SELECTED: DEFINITION_SELECTION_STATUS.MULTIPLE,
266
+ });
267
+
173
268
  export const SIGNATURE_SOURCE = Object.freeze({
174
269
  QUERY_POSITION_HOVER: "query-position-hover",
175
270
  RESOLVED_DEFINITION_HOVER: "resolved-definition-hover",
@@ -178,6 +273,7 @@ export const SIGNATURE_SOURCE = Object.freeze({
178
273
 
179
274
  export const DIAGNOSTIC_FRESHNESS = Object.freeze({
180
275
  CURRENT: "current-document-version",
276
+ DIFFERENT_CONTENT: "different-document-content",
181
277
  VERSION_NOT_REPORTED: "version-not-reported-by-language-server",
182
278
  DIFFERENT_VERSION: "different-document-version",
183
279
  NOT_REPORTED_FOR_CURRENT_DOCUMENT: "not-reported-for-current-document",
@@ -198,6 +294,8 @@ export const EVIDENCE_STATUS = Object.freeze({
198
294
 
199
295
  export const DIAGNOSTIC_EVIDENCE_REASON = Object.freeze({
200
296
  CURRENT_DOCUMENT_VERSION_CONFIRMED: "current-document-version-confirmed",
297
+ CURRENT_DOCUMENT_SNAPSHOT_CONFIRMED: "current-document-snapshot-confirmed",
298
+ DOCUMENT_CONTENT_CHANGED_DURING_ACQUISITION: "document-content-changed-during-diagnostic-acquisition",
201
299
  LANGUAGE_SERVER_VERSION_NOT_REPORTED: "language-server-version-not-reported",
202
300
  LANGUAGE_SERVER_REPORTED_DIFFERENT_VERSION: "language-server-reported-different-version",
203
301
  LANGUAGE_SERVER_DID_NOT_REPORT_CURRENT_DOCUMENT: "language-server-did-not-report-current-document",
@@ -230,6 +328,10 @@ export const FINGERPRINT_ALGORITHM = Object.freeze({
230
328
  SHA_256: "sha256",
231
329
  });
232
330
 
331
+ export const FINGERPRINT_FORMAT = Object.freeze({
332
+ SHA_256_PREFIX: `${FINGERPRINT_ALGORITHM.SHA_256}:`,
333
+ });
334
+
233
335
  export const REFERENCE_DISCOVERY_METHOD = Object.freeze({
234
336
  OWNING_WORKSPACE_LANGUAGE_SERVER: "owning-workspace-language-server",
235
337
  DEFINITION_MATCH_FROM_ANOTHER_WORKSPACE: "definition-match-from-another-workspace",
@@ -288,6 +390,7 @@ export const CI_REASON = Object.freeze({
288
390
  VERIFIED_DIAGNOSTIC_ERRORS: "verified-current-document-has-error-diagnostics",
289
391
  INCOMPLETE_EVIDENCE: "collection-is-limited-or-partial",
290
392
  UNTRUSTED_DIAGNOSTICS: "diagnostics-for-current-document-are-untrusted",
393
+ SEMANTIC_FOLLOW_UP_REQUIRED: "semantic-evidence-requires-follow-up",
291
394
  TOOL_EXECUTION_FAILED: "tool-execution-failed",
292
395
  INVALID_INPUT: "input-is-not-a-semantic-js-mcp-result",
293
396
  });
@@ -302,10 +405,10 @@ export const INTERNAL_RESOLUTION_SOURCE = Object.freeze({
302
405
 
303
406
  export const RESULT_SCHEMA = Object.freeze({
304
407
  NAME: PRODUCT.NAME,
305
- VERSION: 5,
408
+ VERSION: 7,
306
409
  });
307
410
 
308
- export const SERVER_VERSION = "0.8.1";
411
+ export const SERVER_VERSION = "0.10.0";
309
412
 
310
413
  export const REQUIRED_RUNTIME_COMPONENT = Object.freeze({
311
414
  TYPESCRIPT_LANGUAGE_SERVER: "typescript-language-server/lib/cli.mjs",
@@ -330,6 +433,12 @@ export const NODE_EVENT = Object.freeze({
330
433
  CLOSE: "close",
331
434
  });
332
435
 
436
+ export const OPERATING_SYSTEM = Object.freeze({
437
+ LINUX: "linux",
438
+ MACOS: "darwin",
439
+ WINDOWS: "win32",
440
+ });
441
+
333
442
  export const DEFAULT = Object.freeze({
334
443
  REQUEST_TIMEOUT_MS: 30_000,
335
444
  DIAGNOSTIC_WAIT_MS: 2_000,
@@ -0,0 +1,165 @@
1
+ import {
2
+ ACCOUNTING_STATUS,
3
+ CI_EXIT_CODE,
4
+ CI_STATUS,
5
+ COLLECTION_STATUS,
6
+ DEFINITION_SELECTION_STATUS,
7
+ SEMANTIC_EVIDENCE_FOLLOW_UP_REASON,
8
+ SEMANTIC_EVIDENCE_STATUS,
9
+ TOOL,
10
+ } from "../protocol.mjs";
11
+
12
+ export const AGENT_EVALUATION_ARGUMENT = Object.freeze({
13
+ ANSWERS: "--answers",
14
+ });
15
+
16
+ export const AGENT_EVALUATION_COVERAGE = Object.freeze({
17
+ COMPLETE: "complete",
18
+ INCOMPLETE: "incomplete",
19
+ });
20
+
21
+ export const AGENT_EVALUATION_REASON = Object.freeze({
22
+ ALL_DECISIONS_CORRECT: "all-decisions-correct",
23
+ DECISIONS_INCORRECT: "decisions-incorrect",
24
+ ANSWERS_INVALID: "answers-invalid",
25
+ });
26
+
27
+ export const AGENT_EVALUATION_CASES = Object.freeze([
28
+ Object.freeze({
29
+ id: "inspect-named-symbol-identity",
30
+ question: "Which tool should inspect identity and signature before changing this named symbol?",
31
+ compactResult: Object.freeze({
32
+ tool: TOOL.COUNT_NAMED_SYMBOL,
33
+ result: Object.freeze({
34
+ requestedSymbol: "calculateTotal",
35
+ exactDefinitionsFound: 1,
36
+ definitionSelectionStatus: DEFINITION_SELECTION_STATUS.ONE,
37
+ semanticEvidence: Object.freeze({status: SEMANTIC_EVIDENCE_STATUS.USABLE, followUpReasons: Object.freeze([])}),
38
+ references: Object.freeze({verifiedTotal: 18}),
39
+ unresolvedReferences: Object.freeze({count: 0}),
40
+ }),
41
+ collection: Object.freeze({status: COLLECTION_STATUS.COMPLETE, stoppedByLimit: false}),
42
+ continueWith: Object.freeze([TOOL.AUDIT_NAMED_SYMBOL, TOOL.REFERENCE_PAGE]),
43
+ }),
44
+ expected: Object.freeze({nextTool: TOOL.AUDIT_NAMED_SYMBOL, coverage: AGENT_EVALUATION_COVERAGE.COMPLETE}),
45
+ }),
46
+ Object.freeze({
47
+ id: "inspect-unresolved-references",
48
+ question: "Which tool should inspect the unresolved candidates before claiming full impact coverage?",
49
+ compactResult: Object.freeze({
50
+ tool: TOOL.AUDIT_NAMED_SYMBOL,
51
+ result: Object.freeze({
52
+ requestedSymbol: "RequestHandler",
53
+ exactDefinitionsFound: 1,
54
+ definitionSelectionStatus: DEFINITION_SELECTION_STATUS.ONE,
55
+ semanticEvidence: Object.freeze({
56
+ status: SEMANTIC_EVIDENCE_STATUS.FOLLOW_UP_REQUIRED,
57
+ followUpReasons: Object.freeze([SEMANTIC_EVIDENCE_FOLLOW_UP_REASON.COLLECTION_PARTIAL]),
58
+ }),
59
+ references: Object.freeze({verifiedTotal: 52}),
60
+ unresolvedReferences: Object.freeze({count: 1}),
61
+ }),
62
+ collection: Object.freeze({status: COLLECTION_STATUS.PARTIAL, stoppedByLimit: false}),
63
+ continueWith: Object.freeze([TOOL.UNRESOLVED_REFERENCE_PAGE, TOOL.REFERENCE_PAGE]),
64
+ }),
65
+ expected: Object.freeze({nextTool: TOOL.UNRESOLVED_REFERENCE_PAGE, coverage: AGENT_EVALUATION_COVERAGE.INCOMPLETE}),
66
+ }),
67
+ Object.freeze({
68
+ id: "disambiguate-homonymous-definitions",
69
+ question: "Which tool should identify the intended declaration from an exact source position?",
70
+ compactResult: Object.freeze({
71
+ tool: TOOL.COUNT_NAMED_SYMBOL,
72
+ result: Object.freeze({
73
+ requestedSymbol: "createClient",
74
+ exactDefinitionsFound: 3,
75
+ definitionSelectionStatus: DEFINITION_SELECTION_STATUS.MULTIPLE,
76
+ semanticEvidence: Object.freeze({
77
+ status: SEMANTIC_EVIDENCE_STATUS.FOLLOW_UP_REQUIRED,
78
+ followUpReasons: Object.freeze([SEMANTIC_EVIDENCE_FOLLOW_UP_REASON.MULTIPLE_DEFINITIONS_SELECTED]),
79
+ }),
80
+ unresolvedReferences: Object.freeze({count: 0}),
81
+ }),
82
+ collection: Object.freeze({status: COLLECTION_STATUS.COMPLETE, stoppedByLimit: false}),
83
+ continueWith: Object.freeze([TOOL.AUDIT_SYMBOL, TOOL.AUDIT_NAMED_SYMBOL]),
84
+ }),
85
+ expected: Object.freeze({nextTool: TOOL.AUDIT_SYMBOL, coverage: AGENT_EVALUATION_COVERAGE.COMPLETE}),
86
+ }),
87
+ Object.freeze({
88
+ id: "reuse-file-hint-binding-position",
89
+ question: "Which tool should reuse the verified source position without inventing a filename declaration?",
90
+ compactResult: Object.freeze({
91
+ tool: TOOL.AUDIT_NAMED_SYMBOL,
92
+ result: Object.freeze({
93
+ requestedSymbol: "RenamedPanel",
94
+ exactDefinitionsFound: 0,
95
+ definitionSelectionStatus: DEFINITION_SELECTION_STATUS.NONE,
96
+ semanticEvidence: Object.freeze({
97
+ status: SEMANTIC_EVIDENCE_STATUS.FOLLOW_UP_REQUIRED,
98
+ followUpReasons: Object.freeze([SEMANTIC_EVIDENCE_FOLLOW_UP_REASON.NO_DEFINITION_SELECTED]),
99
+ }),
100
+ fileHintResolution: Object.freeze({
101
+ textMatchesFound: 2,
102
+ textMatchesChecked: 2,
103
+ textMatchesResolvingToFileFilter: 2,
104
+ textMatchesWhoseDefinitionCouldNotBeResolved: 0,
105
+ accountingStatus: ACCOUNTING_STATUS.COMPLETE,
106
+ sourcePositionForAudit: Object.freeze({file: "src/Parent.vue", line: 2, column: 8}),
107
+ }),
108
+ }),
109
+ collection: Object.freeze({status: COLLECTION_STATUS.COMPLETE, stoppedByLimit: false}),
110
+ continueWith: Object.freeze([TOOL.AUDIT_SYMBOL]),
111
+ }),
112
+ expected: Object.freeze({nextTool: TOOL.AUDIT_SYMBOL, coverage: AGENT_EVALUATION_COVERAGE.COMPLETE}),
113
+ }),
114
+ ]);
115
+
116
+ function publicCase(evaluationCase) {
117
+ return {
118
+ id: evaluationCase.id,
119
+ question: evaluationCase.question,
120
+ compactResult: evaluationCase.compactResult,
121
+ };
122
+ }
123
+
124
+ export function evaluationPrompt() {
125
+ return {
126
+ instructions: "For each case, choose one exact tool from continueWith and classify collection coverage as complete or incomplete.",
127
+ answerFormat: {
128
+ answers: "array with one object per case",
129
+ answer: {
130
+ id: "exact case id",
131
+ nextTool: "one exact tool from that case's continueWith list",
132
+ coverage: {enum: Object.values(AGENT_EVALUATION_COVERAGE)},
133
+ },
134
+ },
135
+ cases: AGENT_EVALUATION_CASES.map(publicCase),
136
+ };
137
+ }
138
+
139
+ export function gradeAgentAnswers(input) {
140
+ const suppliedAnswers = Array.isArray(input?.answers) ? input.answers : [];
141
+ const answerById = new Map(suppliedAnswers.map((answer) => [answer.id, answer]));
142
+ const cases = AGENT_EVALUATION_CASES.map((evaluationCase) => {
143
+ const answer = answerById.get(evaluationCase.id);
144
+ const nextToolCorrect = answer?.nextTool === evaluationCase.expected.nextTool;
145
+ const coverageCorrect = answer?.coverage === evaluationCase.expected.coverage;
146
+ return {
147
+ id: evaluationCase.id,
148
+ correct: nextToolCorrect && coverageCorrect,
149
+ nextToolCorrect,
150
+ coverageCorrect,
151
+ };
152
+ });
153
+ const correctCases = cases.filter((item) => item.correct).length;
154
+ const complete = suppliedAnswers.length === AGENT_EVALUATION_CASES.length && correctCases === AGENT_EVALUATION_CASES.length;
155
+ const status = complete ? CI_STATUS.PASS : CI_STATUS.FAIL;
156
+ return {
157
+ status,
158
+ exitCode: CI_EXIT_CODE[status.toUpperCase()],
159
+ reason: complete ? AGENT_EVALUATION_REASON.ALL_DECISIONS_CORRECT : AGENT_EVALUATION_REASON.DECISIONS_INCORRECT,
160
+ casesAvailable: AGENT_EVALUATION_CASES.length,
161
+ answersSupplied: suppliedAnswers.length,
162
+ correctCases,
163
+ cases,
164
+ };
165
+ }
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {CI_STATUS} from "../protocol.mjs";
4
+ import {AGENT_EVALUATION_CASES, evaluationPrompt, gradeAgentAnswers} from "./agent-evaluation-contract.mjs";
5
+
6
+ function assert(condition, message) {
7
+ if (!condition) throw new Error(message);
8
+ }
9
+
10
+ const prompt = evaluationPrompt();
11
+ assert(prompt.cases.length === AGENT_EVALUATION_CASES.length, "Evaluation prompt omitted cases");
12
+ assert(
13
+ prompt.cases.every((item) => !("expected" in item)),
14
+ "Evaluation prompt exposed expected answers",
15
+ );
16
+
17
+ const correctAnswers = {
18
+ answers: AGENT_EVALUATION_CASES.map((evaluationCase) => ({id: evaluationCase.id, ...evaluationCase.expected})),
19
+ };
20
+ const passing = gradeAgentAnswers(correctAnswers);
21
+ assert(passing.status === CI_STATUS.PASS, "Correct agent answers did not pass");
22
+ assert(passing.correctCases === AGENT_EVALUATION_CASES.length, "Correct answer count was wrong");
23
+
24
+ const incorrectAnswers = {
25
+ answers: correctAnswers.answers.map((answer, index) =>
26
+ index === 0 ? {...answer, nextTool: AGENT_EVALUATION_CASES[1].expected.nextTool} : answer,
27
+ ),
28
+ };
29
+ const failing = gradeAgentAnswers(incorrectAnswers);
30
+ assert(failing.status === CI_STATUS.FAIL, "Incorrect agent answer did not fail");
31
+ assert(failing.correctCases === AGENT_EVALUATION_CASES.length - 1, "Incorrect answer count was wrong");
32
+
33
+ process.stdout.write(
34
+ `${JSON.stringify(
35
+ {
36
+ evaluationCases: AGENT_EVALUATION_CASES.length,
37
+ hiddenExpectedAnswers: "ok",
38
+ deterministicGrading: "ok",
39
+ incorrectDecisionRejected: "ok",
40
+ },
41
+ null,
42
+ 2,
43
+ )}\n`,
44
+ );
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {readFile} from "node:fs/promises";
4
+ import {parse as parseYaml, stringify as stringifyYaml} from "yaml";
5
+ import {CI_EXIT_CODE, CI_STATUS, CLI_ARGUMENT} from "../protocol.mjs";
6
+ import {AGENT_EVALUATION_ARGUMENT, AGENT_EVALUATION_REASON, evaluationPrompt, gradeAgentAnswers} from "./agent-evaluation-contract.mjs";
7
+
8
+ const answersArgumentIndex = process.argv.indexOf(AGENT_EVALUATION_ARGUMENT.ANSWERS);
9
+ const answersFile = answersArgumentIndex >= 0 ? process.argv[answersArgumentIndex + 1] : undefined;
10
+ const useYaml = process.argv.includes(CLI_ARGUMENT.YAML);
11
+
12
+ let output;
13
+ if (!answersFile) {
14
+ output = evaluationPrompt();
15
+ } else {
16
+ try {
17
+ const text = await readFile(answersFile, "utf8");
18
+ let answers;
19
+ try {
20
+ answers = JSON.parse(text);
21
+ } catch {
22
+ answers = parseYaml(text);
23
+ }
24
+ output = gradeAgentAnswers(answers);
25
+ process.exitCode = output.exitCode;
26
+ } catch (error) {
27
+ output = {
28
+ status: CI_STATUS.BLOCKED,
29
+ exitCode: CI_EXIT_CODE.BLOCKED,
30
+ reason: AGENT_EVALUATION_REASON.ANSWERS_INVALID,
31
+ message: error instanceof Error ? error.message : String(error),
32
+ };
33
+ process.exitCode = output.exitCode;
34
+ }
35
+ }
36
+
37
+ process.stdout.write(useYaml ? stringifyYaml(output, {lineWidth: 0}) : `${JSON.stringify(output, null, 2)}\n`);