semantic-js-mcp 0.8.1 → 0.9.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.
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,34 @@ 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 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 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
+ PUBLISH_DIAGNOSTICS: "textDocument/publishDiagnostics",
223
+ WORKSPACE_DIAGNOSTIC_REFRESH: "workspace/diagnostic/refresh",
224
+ });
225
+
158
226
  export const LIMIT_MODE = Object.freeze({
159
227
  UNLIMITED: "unlimited",
160
228
  MAXIMUM: "maximum",
@@ -170,6 +238,12 @@ export const DEFINITION_MATCH = Object.freeze({
170
238
  UNRESOLVED: "unresolved",
171
239
  });
172
240
 
241
+ export const DEFINITION_SELECTION_STATUS = Object.freeze({
242
+ NONE: "no-definition-selected",
243
+ ONE: "one-definition-selected",
244
+ MULTIPLE: "multiple-definitions-selected",
245
+ });
246
+
173
247
  export const SIGNATURE_SOURCE = Object.freeze({
174
248
  QUERY_POSITION_HOVER: "query-position-hover",
175
249
  RESOLVED_DEFINITION_HOVER: "resolved-definition-hover",
@@ -178,6 +252,7 @@ export const SIGNATURE_SOURCE = Object.freeze({
178
252
 
179
253
  export const DIAGNOSTIC_FRESHNESS = Object.freeze({
180
254
  CURRENT: "current-document-version",
255
+ DIFFERENT_CONTENT: "different-document-content",
181
256
  VERSION_NOT_REPORTED: "version-not-reported-by-language-server",
182
257
  DIFFERENT_VERSION: "different-document-version",
183
258
  NOT_REPORTED_FOR_CURRENT_DOCUMENT: "not-reported-for-current-document",
@@ -198,6 +273,8 @@ export const EVIDENCE_STATUS = Object.freeze({
198
273
 
199
274
  export const DIAGNOSTIC_EVIDENCE_REASON = Object.freeze({
200
275
  CURRENT_DOCUMENT_VERSION_CONFIRMED: "current-document-version-confirmed",
276
+ CURRENT_DOCUMENT_SNAPSHOT_CONFIRMED: "current-document-snapshot-confirmed",
277
+ DOCUMENT_CONTENT_CHANGED_DURING_ACQUISITION: "document-content-changed-during-diagnostic-acquisition",
201
278
  LANGUAGE_SERVER_VERSION_NOT_REPORTED: "language-server-version-not-reported",
202
279
  LANGUAGE_SERVER_REPORTED_DIFFERENT_VERSION: "language-server-reported-different-version",
203
280
  LANGUAGE_SERVER_DID_NOT_REPORT_CURRENT_DOCUMENT: "language-server-did-not-report-current-document",
@@ -230,6 +307,10 @@ export const FINGERPRINT_ALGORITHM = Object.freeze({
230
307
  SHA_256: "sha256",
231
308
  });
232
309
 
310
+ export const FINGERPRINT_FORMAT = Object.freeze({
311
+ SHA_256_PREFIX: `${FINGERPRINT_ALGORITHM.SHA_256}:`,
312
+ });
313
+
233
314
  export const REFERENCE_DISCOVERY_METHOD = Object.freeze({
234
315
  OWNING_WORKSPACE_LANGUAGE_SERVER: "owning-workspace-language-server",
235
316
  DEFINITION_MATCH_FROM_ANOTHER_WORKSPACE: "definition-match-from-another-workspace",
@@ -302,10 +383,10 @@ export const INTERNAL_RESOLUTION_SOURCE = Object.freeze({
302
383
 
303
384
  export const RESULT_SCHEMA = Object.freeze({
304
385
  NAME: PRODUCT.NAME,
305
- VERSION: 5,
386
+ VERSION: 6,
306
387
  });
307
388
 
308
- export const SERVER_VERSION = "0.8.1";
389
+ export const SERVER_VERSION = "0.9.0";
309
390
 
310
391
  export const REQUIRED_RUNTIME_COMPONENT = Object.freeze({
311
392
  TYPESCRIPT_LANGUAGE_SERVER: "typescript-language-server/lib/cli.mjs",
@@ -0,0 +1,142 @@
1
+ import {ACCOUNTING_STATUS, CI_EXIT_CODE, CI_STATUS, COLLECTION_STATUS, DEFINITION_SELECTION_STATUS, TOOL} from "../protocol.mjs";
2
+
3
+ export const AGENT_EVALUATION_ARGUMENT = Object.freeze({
4
+ ANSWERS: "--answers",
5
+ });
6
+
7
+ export const AGENT_EVALUATION_COVERAGE = Object.freeze({
8
+ COMPLETE: "complete",
9
+ INCOMPLETE: "incomplete",
10
+ });
11
+
12
+ export const AGENT_EVALUATION_REASON = Object.freeze({
13
+ ALL_DECISIONS_CORRECT: "all-decisions-correct",
14
+ DECISIONS_INCORRECT: "decisions-incorrect",
15
+ ANSWERS_INVALID: "answers-invalid",
16
+ });
17
+
18
+ export const AGENT_EVALUATION_CASES = Object.freeze([
19
+ Object.freeze({
20
+ id: "inspect-named-symbol-identity",
21
+ question: "Which tool should inspect identity and signature before changing this named symbol?",
22
+ compactResult: Object.freeze({
23
+ tool: TOOL.COUNT_NAMED_SYMBOL,
24
+ result: Object.freeze({
25
+ requestedSymbol: "calculateTotal",
26
+ exactDefinitionsFound: 1,
27
+ definitionSelectionStatus: DEFINITION_SELECTION_STATUS.ONE,
28
+ references: Object.freeze({verifiedTotal: 18}),
29
+ unresolvedReferences: Object.freeze({count: 0}),
30
+ }),
31
+ collection: Object.freeze({status: COLLECTION_STATUS.COMPLETE, stoppedByLimit: false}),
32
+ continueWith: Object.freeze([TOOL.AUDIT_NAMED_SYMBOL, TOOL.REFERENCE_PAGE]),
33
+ }),
34
+ expected: Object.freeze({nextTool: TOOL.AUDIT_NAMED_SYMBOL, coverage: AGENT_EVALUATION_COVERAGE.COMPLETE}),
35
+ }),
36
+ Object.freeze({
37
+ id: "inspect-unresolved-references",
38
+ question: "Which tool should inspect the unresolved candidates before claiming full impact coverage?",
39
+ compactResult: Object.freeze({
40
+ tool: TOOL.AUDIT_NAMED_SYMBOL,
41
+ result: Object.freeze({
42
+ requestedSymbol: "RequestHandler",
43
+ exactDefinitionsFound: 1,
44
+ references: Object.freeze({verifiedTotal: 52}),
45
+ unresolvedReferences: Object.freeze({count: 1}),
46
+ }),
47
+ collection: Object.freeze({status: COLLECTION_STATUS.PARTIAL, stoppedByLimit: false}),
48
+ continueWith: Object.freeze([TOOL.UNRESOLVED_REFERENCE_PAGE, TOOL.REFERENCE_PAGE]),
49
+ }),
50
+ expected: Object.freeze({nextTool: TOOL.UNRESOLVED_REFERENCE_PAGE, coverage: AGENT_EVALUATION_COVERAGE.INCOMPLETE}),
51
+ }),
52
+ Object.freeze({
53
+ id: "disambiguate-homonymous-definitions",
54
+ question: "Which tool should identify the intended declaration from an exact source position?",
55
+ compactResult: Object.freeze({
56
+ tool: TOOL.COUNT_NAMED_SYMBOL,
57
+ result: Object.freeze({
58
+ requestedSymbol: "createClient",
59
+ exactDefinitionsFound: 3,
60
+ definitionSelectionStatus: DEFINITION_SELECTION_STATUS.MULTIPLE,
61
+ unresolvedReferences: Object.freeze({count: 0}),
62
+ }),
63
+ collection: Object.freeze({status: COLLECTION_STATUS.COMPLETE, stoppedByLimit: false}),
64
+ continueWith: Object.freeze([TOOL.AUDIT_SYMBOL, TOOL.AUDIT_NAMED_SYMBOL]),
65
+ }),
66
+ expected: Object.freeze({nextTool: TOOL.AUDIT_SYMBOL, coverage: AGENT_EVALUATION_COVERAGE.COMPLETE}),
67
+ }),
68
+ Object.freeze({
69
+ id: "reuse-file-hint-binding-position",
70
+ question: "Which tool should reuse the verified source position without inventing a filename declaration?",
71
+ compactResult: Object.freeze({
72
+ tool: TOOL.AUDIT_NAMED_SYMBOL,
73
+ result: Object.freeze({
74
+ requestedSymbol: "RenamedPanel",
75
+ exactDefinitionsFound: 0,
76
+ definitionSelectionStatus: DEFINITION_SELECTION_STATUS.NONE,
77
+ fileHintResolution: Object.freeze({
78
+ textMatchesFound: 2,
79
+ textMatchesChecked: 2,
80
+ textMatchesResolvingToFileFilter: 2,
81
+ textMatchesWhoseDefinitionCouldNotBeResolved: 0,
82
+ accountingStatus: ACCOUNTING_STATUS.COMPLETE,
83
+ sourcePositionForAudit: Object.freeze({file: "src/Parent.vue", line: 2, column: 8}),
84
+ }),
85
+ }),
86
+ collection: Object.freeze({status: COLLECTION_STATUS.COMPLETE, stoppedByLimit: false}),
87
+ continueWith: Object.freeze([TOOL.AUDIT_SYMBOL]),
88
+ }),
89
+ expected: Object.freeze({nextTool: TOOL.AUDIT_SYMBOL, coverage: AGENT_EVALUATION_COVERAGE.COMPLETE}),
90
+ }),
91
+ ]);
92
+
93
+ function publicCase(evaluationCase) {
94
+ return {
95
+ id: evaluationCase.id,
96
+ question: evaluationCase.question,
97
+ compactResult: evaluationCase.compactResult,
98
+ };
99
+ }
100
+
101
+ export function evaluationPrompt() {
102
+ return {
103
+ instructions: "For each case, choose one exact tool from continueWith and classify collection coverage as complete or incomplete.",
104
+ answerFormat: {
105
+ answers: "array with one object per case",
106
+ answer: {
107
+ id: "exact case id",
108
+ nextTool: "one exact tool from that case's continueWith list",
109
+ coverage: {enum: Object.values(AGENT_EVALUATION_COVERAGE)},
110
+ },
111
+ },
112
+ cases: AGENT_EVALUATION_CASES.map(publicCase),
113
+ };
114
+ }
115
+
116
+ export function gradeAgentAnswers(input) {
117
+ const suppliedAnswers = Array.isArray(input?.answers) ? input.answers : [];
118
+ const answerById = new Map(suppliedAnswers.map((answer) => [answer.id, answer]));
119
+ const cases = AGENT_EVALUATION_CASES.map((evaluationCase) => {
120
+ const answer = answerById.get(evaluationCase.id);
121
+ const nextToolCorrect = answer?.nextTool === evaluationCase.expected.nextTool;
122
+ const coverageCorrect = answer?.coverage === evaluationCase.expected.coverage;
123
+ return {
124
+ id: evaluationCase.id,
125
+ correct: nextToolCorrect && coverageCorrect,
126
+ nextToolCorrect,
127
+ coverageCorrect,
128
+ };
129
+ });
130
+ const correctCases = cases.filter((item) => item.correct).length;
131
+ const complete = suppliedAnswers.length === AGENT_EVALUATION_CASES.length && correctCases === AGENT_EVALUATION_CASES.length;
132
+ const status = complete ? CI_STATUS.PASS : CI_STATUS.FAIL;
133
+ return {
134
+ status,
135
+ exitCode: CI_EXIT_CODE[status.toUpperCase()],
136
+ reason: complete ? AGENT_EVALUATION_REASON.ALL_DECISIONS_CORRECT : AGENT_EVALUATION_REASON.DECISIONS_INCORRECT,
137
+ casesAvailable: AGENT_EVALUATION_CASES.length,
138
+ answersSupplied: suppliedAnswers.length,
139
+ correctCases,
140
+ cases,
141
+ };
142
+ }
@@ -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`);
@@ -11,13 +11,18 @@ import {
11
11
  CLI_ARGUMENT,
12
12
  CLI_COMMAND,
13
13
  CLI_MESSAGE,
14
+ COMMON_VALUE,
14
15
  CONFIGURATION_FILE,
15
16
  CONTENT_FRESHNESS,
16
17
  DEFAULT,
17
18
  DEFINITION_MATCH,
18
19
  DEFINITION_RESOLUTION_METHOD,
20
+ DEFINITION_SELECTION_STATUS,
19
21
  DIAGNOSTIC_EVIDENCE_REASON,
20
22
  DIAGNOSTIC_FRESHNESS,
23
+ DIAGNOSTIC_LANGUAGE,
24
+ DIAGNOSTIC_PROVIDER,
25
+ DIAGNOSTIC_REGION,
21
26
  DIAGNOSTIC_SEVERITY,
22
27
  DOCTOR_CHECK,
23
28
  DOCTOR_DISTRIBUTION_ACCEPTED_STATUS,
@@ -28,8 +33,10 @@ import {
28
33
  ERROR_CODE,
29
34
  EVIDENCE_TYPE,
30
35
  FINGERPRINT_ALGORITHM,
36
+ FINGERPRINT_FORMAT,
31
37
  INTERNAL_RESOLUTION_SOURCE,
32
38
  LIMIT_MODE,
39
+ LSP_METHOD,
33
40
  NODE_EVENT,
34
41
  PRESENTATION_MODE,
35
42
  PACKAGE_PATH,
@@ -38,10 +45,13 @@ import {
38
45
  REFERENCE_SET_CHANGE_TYPE,
39
46
  REQUIRED_RUNTIME_COMPONENT,
40
47
  RUNTIME_COMMAND,
48
+ RUNTIME_PACKAGE,
41
49
  RUNTIME_REQUIREMENT,
42
50
  RUNTIME_REQUIREMENT_KIND,
43
51
  RESULT_SCHEMA,
52
+ SEARCH_SCOPE,
44
53
  TOOL,
54
+ TOOL_DESCRIPTION,
45
55
  TYPESCRIPT_PROJECT_KIND,
46
56
  SOURCE_EXCLUDED_GLOBS,
47
57
  SOURCE_FILE_GLOBS,
@@ -68,13 +78,21 @@ if (semanticServer?.tool_timeout_sec !== DEFAULT.MCP_TOOL_TIMEOUT_SECONDS) {
68
78
  throw new Error(".mcp.json tool_timeout_sec differs from protocol.mjs");
69
79
  }
70
80
  const groups = [
81
+ COMMON_VALUE,
82
+ SEARCH_SCOPE,
83
+ RUNTIME_PACKAGE,
71
84
  TOOL,
85
+ TOOL_DESCRIPTION,
72
86
  COLLECTION_STATUS,
73
87
  PRESENTATION_MODE,
74
88
  LIMIT_MODE,
75
89
  ACCOUNTING_STATUS,
76
90
  DEFINITION_MATCH,
91
+ DEFINITION_SELECTION_STATUS,
77
92
  DIAGNOSTIC_FRESHNESS,
93
+ DIAGNOSTIC_PROVIDER,
94
+ DIAGNOSTIC_REGION,
95
+ DIAGNOSTIC_LANGUAGE,
78
96
  DIAGNOSTIC_SEVERITY,
79
97
  DIAGNOSTIC_EVIDENCE_REASON,
80
98
  EVIDENCE_STATUS,
@@ -82,10 +100,12 @@ const groups = [
82
100
  ERROR_CODE,
83
101
  EVIDENCE_TYPE,
84
102
  FINGERPRINT_ALGORITHM,
103
+ FINGERPRINT_FORMAT,
85
104
  REFERENCE_DISCOVERY_METHOD,
86
105
  REFERENCE_SET_CHANGE_TYPE,
87
106
  DEFINITION_RESOLUTION_METHOD,
88
107
  INTERNAL_RESOLUTION_SOURCE,
108
+ LSP_METHOD,
89
109
  UNRESOLVED_REFERENCE_REASON,
90
110
  TYPESCRIPT_PROJECT_KIND,
91
111
  CI_STATUS,
@@ -51,8 +51,7 @@ async function evaluate(name, input, expectedStatus, expectedExitCode) {
51
51
 
52
52
  function canonicalResult(tool, collectionStatus, result, extra = {}) {
53
53
  return {
54
- server: {name: PRODUCT.NAME, version: SERVER_VERSION},
55
- resultSchema: {name: RESULT_SCHEMA.NAME, version: RESULT_SCHEMA.VERSION},
54
+ producer: {name: PRODUCT.NAME, version: SERVER_VERSION, resultSchemaVersion: RESULT_SCHEMA.VERSION},
56
55
  tool,
57
56
  request: {},
58
57
  result,
@@ -109,7 +108,23 @@ try {
109
108
  "wrong-result-schema-version",
110
109
  {
111
110
  ...canonicalResult(TOOL.COUNT_REFERENCES, COLLECTION_STATUS.COMPLETE, {}),
112
- resultSchema: {name: RESULT_SCHEMA.NAME, version: RESULT_SCHEMA.VERSION + 1},
111
+ producer: {name: PRODUCT.NAME, version: SERVER_VERSION, resultSchemaVersion: RESULT_SCHEMA.VERSION + 1},
112
+ },
113
+ CI_STATUS.BLOCKED,
114
+ CI_EXIT_CODE.BLOCKED,
115
+ );
116
+
117
+ await evaluate(
118
+ "schema-5-envelope",
119
+ {
120
+ server: {name: PRODUCT.NAME, version: SERVER_VERSION},
121
+ resultSchema: {name: RESULT_SCHEMA.NAME, version: RESULT_SCHEMA.VERSION - 1},
122
+ tool: TOOL.COUNT_REFERENCES,
123
+ request: {},
124
+ result: {},
125
+ collection: {status: COLLECTION_STATUS.COMPLETE},
126
+ presentation: {mode: PRESENTATION_MODE.COUNT_ONLY},
127
+ continueWith: [{tool: TOOL.AUDIT_SYMBOL}],
113
128
  },
114
129
  CI_STATUS.BLOCKED,
115
130
  CI_EXIT_CODE.BLOCKED,
@@ -136,7 +151,7 @@ try {
136
151
  "unknown-continuation-tool",
137
152
  {
138
153
  ...canonicalResult(TOOL.COUNT_REFERENCES, COLLECTION_STATUS.COMPLETE, {}),
139
- continueWith: [{tool: "unknown_tool"}],
154
+ continueWith: ["unknown_tool"],
140
155
  },
141
156
  CI_STATUS.BLOCKED,
142
157
  CI_EXIT_CODE.BLOCKED,
@@ -0,0 +1,129 @@
1
+ import {mkdir} from "node:fs/promises";
2
+ import path from "node:path";
3
+ import {CI_EXIT_CODE, CI_STATUS, NODE_EVENT, PRODUCT} from "../protocol.mjs";
4
+ import {CODEX_DISTRIBUTION} from "./distribution-policy.mjs";
5
+ import {RELEASE_CHECK, RELEASE_REASON} from "./release-contract.mjs";
6
+
7
+ function check(name, status, reason, details) {
8
+ return {name, status, reason, ...(details ? {details} : {})};
9
+ }
10
+
11
+ function run(command, args, options) {
12
+ return new Promise((resolve) => {
13
+ const child = options.spawn(command, args, {cwd: options.cwd, env: options.env, stdio: ["ignore", "pipe", "pipe"]});
14
+ const stdout = [];
15
+ const stderr = [];
16
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
17
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
18
+ child.on(NODE_EVENT.ERROR, (error) => resolve({exitCode: undefined, stdout: "", stderr: error.message}));
19
+ child.on(NODE_EVENT.CLOSE, (exitCode) =>
20
+ resolve({exitCode, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8")}),
21
+ );
22
+ });
23
+ }
24
+
25
+ function failureMessage(result) {
26
+ return result.stderr.trim() || result.stdout.trim();
27
+ }
28
+
29
+ function networkUnavailable(result) {
30
+ const message = `${result.stdout}\n${result.stderr}`.toLowerCase();
31
+ return CODEX_DISTRIBUTION.NETWORK_UNAVAILABLE_TEXT.some((text) => message.includes(text.toLowerCase()));
32
+ }
33
+
34
+ export async function verifyCodexPlugin({version, workspace, spawn, runCommand}) {
35
+ const codexHome = path.join(workspace, CODEX_DISTRIBUTION.HOME_DIRECTORY);
36
+ await mkdir(codexHome, {recursive: true});
37
+ const environment = {...process.env, [CODEX_DISTRIBUTION.HOME_ENVIRONMENT_VARIABLE]: codexHome};
38
+ const execute = runCommand
39
+ ? (command, args) => runCommand(command, args, {cwd: workspace, env: environment})
40
+ : (command, args) => run(command, args, {cwd: workspace, env: environment, spawn});
41
+ const marketplaceRef = `${CODEX_DISTRIBUTION.VERSION_REF_PREFIX}${version}`;
42
+
43
+ const marketplace = await execute(CODEX_DISTRIBUTION.EXECUTABLE, [
44
+ CODEX_DISTRIBUTION.PLUGIN_COMMAND,
45
+ CODEX_DISTRIBUTION.MARKETPLACE_COMMAND,
46
+ CODEX_DISTRIBUTION.ADD_COMMAND,
47
+ CODEX_DISTRIBUTION.MARKETPLACE_SOURCE,
48
+ CODEX_DISTRIBUTION.REF_ARGUMENT,
49
+ marketplaceRef,
50
+ CODEX_DISTRIBUTION.JSON_ARGUMENT,
51
+ ]);
52
+ if (marketplace.exitCode === undefined) {
53
+ return [
54
+ check(RELEASE_CHECK.CODEX_MARKETPLACE, CI_STATUS.BLOCKED, RELEASE_REASON.CODEX_CLI_UNAVAILABLE, {
55
+ message: failureMessage(marketplace),
56
+ }),
57
+ ];
58
+ }
59
+ if (marketplace.exitCode !== CI_EXIT_CODE.PASS) {
60
+ const unavailable = networkUnavailable(marketplace);
61
+ return [
62
+ check(
63
+ RELEASE_CHECK.CODEX_MARKETPLACE,
64
+ unavailable ? CI_STATUS.BLOCKED : CI_STATUS.FAIL,
65
+ unavailable ? RELEASE_REASON.CODEX_MARKETPLACE_UNAVAILABLE : RELEASE_REASON.CODEX_MARKETPLACE_FAILED,
66
+ {
67
+ marketplaceRef,
68
+ message: failureMessage(marketplace),
69
+ },
70
+ ),
71
+ ];
72
+ }
73
+
74
+ const checks = [check(RELEASE_CHECK.CODEX_MARKETPLACE, CI_STATUS.PASS, RELEASE_REASON.CHECK_COMPLETED, {marketplaceRef})];
75
+ const installation = await execute(CODEX_DISTRIBUTION.EXECUTABLE, [
76
+ CODEX_DISTRIBUTION.PLUGIN_COMMAND,
77
+ CODEX_DISTRIBUTION.ADD_COMMAND,
78
+ CODEX_DISTRIBUTION.PLUGIN_SELECTOR,
79
+ CODEX_DISTRIBUTION.JSON_ARGUMENT,
80
+ ]);
81
+ if (installation.exitCode !== CI_EXIT_CODE.PASS) {
82
+ const unavailable = installation.exitCode === undefined || networkUnavailable(installation);
83
+ checks.push(
84
+ check(
85
+ RELEASE_CHECK.CODEX_PLUGIN_INSTALLATION,
86
+ unavailable ? CI_STATUS.BLOCKED : CI_STATUS.FAIL,
87
+ unavailable ? RELEASE_REASON.CODEX_PLUGIN_SOURCE_UNAVAILABLE : RELEASE_REASON.CODEX_PLUGIN_INSTALLATION_FAILED,
88
+ {message: failureMessage(installation)},
89
+ ),
90
+ );
91
+ return checks;
92
+ }
93
+
94
+ const listed = await execute(CODEX_DISTRIBUTION.EXECUTABLE, [
95
+ CODEX_DISTRIBUTION.PLUGIN_COMMAND,
96
+ CODEX_DISTRIBUTION.LIST_COMMAND,
97
+ CODEX_DISTRIBUTION.JSON_ARGUMENT,
98
+ ]);
99
+ let listResult;
100
+ try {
101
+ listResult = JSON.parse(listed.stdout);
102
+ } catch {
103
+ listResult = undefined;
104
+ }
105
+ const installedPlugin = listResult?.installed?.find(
106
+ (plugin) => plugin.name === PRODUCT.NAME && plugin.marketplaceName === CODEX_DISTRIBUTION.MARKETPLACE_NAME,
107
+ );
108
+ if (listed.exitCode === undefined || networkUnavailable(listed)) {
109
+ checks.push(
110
+ check(RELEASE_CHECK.CODEX_PLUGIN_INSTALLATION, CI_STATUS.BLOCKED, RELEASE_REASON.CODEX_PLUGIN_SOURCE_UNAVAILABLE, {
111
+ message: failureMessage(listed),
112
+ }),
113
+ );
114
+ return checks;
115
+ }
116
+ const installedVersionMatches =
117
+ listed.exitCode === CI_EXIT_CODE.PASS && installedPlugin?.version === version && installedPlugin?.enabled === true;
118
+ checks.push(
119
+ check(
120
+ RELEASE_CHECK.CODEX_PLUGIN_INSTALLATION,
121
+ installedVersionMatches ? CI_STATUS.PASS : CI_STATUS.FAIL,
122
+ installedVersionMatches ? RELEASE_REASON.CHECK_COMPLETED : RELEASE_REASON.CODEX_PLUGIN_VERSION_DIFFERENT,
123
+ installedVersionMatches
124
+ ? {version}
125
+ : {expectedVersion: version, installedVersion: installedPlugin?.version, message: failureMessage(listed)},
126
+ ),
127
+ );
128
+ return checks;
129
+ }
@@ -19,6 +19,34 @@ export const NPM_AUTOMATIC_PACKAGE_FILE = Object.freeze([
19
19
  REQUIRED_PACKAGE_FILE.LICENSE,
20
20
  ]);
21
21
 
22
+ export const CODEX_DISTRIBUTION = Object.freeze({
23
+ EXECUTABLE: "codex",
24
+ HOME_ENVIRONMENT_VARIABLE: "CODEX_HOME",
25
+ MARKETPLACE_NAME: "elnonathan",
26
+ MARKETPLACE_SOURCE: "elnonathan/semantic-js-mcp",
27
+ PLUGIN_SELECTOR: "semantic-js-mcp@elnonathan",
28
+ VERSION_REF_PREFIX: "v",
29
+ HOME_DIRECTORY: "codex-home",
30
+ PLUGIN_COMMAND: "plugin",
31
+ MARKETPLACE_COMMAND: "marketplace",
32
+ ADD_COMMAND: "add",
33
+ LIST_COMMAND: "list",
34
+ REF_ARGUMENT: "--ref",
35
+ JSON_ARGUMENT: "--json",
36
+ NETWORK_UNAVAILABLE_TEXT: Object.freeze([
37
+ "Could not resolve host",
38
+ "failed to lookup address information",
39
+ "network is unreachable",
40
+ "network timeout",
41
+ "timed out",
42
+ "unable to access",
43
+ ]),
44
+ });
45
+
46
+ export const NPM_DISTRIBUTION = Object.freeze({
47
+ CACHE_ENVIRONMENT_VARIABLE: "npm_config_cache",
48
+ });
49
+
22
50
  const WINDOWS_PLATFORM = "win32";
23
51
  const WINDOWS_COMMAND_SUFFIX = ".cmd";
24
52
  const DEPENDENCY_DIRECTORY_PREFIX = "node_modules/";
@@ -16,6 +16,7 @@ import {
16
16
  } from "../protocol.mjs";
17
17
  import {
18
18
  REQUIRED_PACKAGE_FILE,
19
+ NPM_DISTRIBUTION,
19
20
  allProductionDependenciesAreBundled,
20
21
  npmExecutableName,
21
22
  packagePathIsAllowed,
@@ -27,7 +28,7 @@ const packageOutput = path.join(workspace, "package");
27
28
  const consumer = path.join(workspace, "consumer");
28
29
  const npmCache = path.join(workspace, "npm-cache");
29
30
  const npmCli = process.env.npm_execpath;
30
- const npmEnvironment = {...process.env, npm_config_cache: npmCache};
31
+ const npmEnvironment = {...process.env, [NPM_DISTRIBUTION.CACHE_ENVIRONMENT_VARIABLE]: npmCache};
31
32
 
32
33
  function run(command, args, options = {}) {
33
34
  return new Promise((resolve, reject) => {