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/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +25 -0
- package/CONTRIBUTING.md +17 -1
- package/README.md +33 -68
- package/ROADMAP.md +1 -1
- package/docs/architecture-decisions.md +35 -22
- package/docs/distribution.md +11 -0
- package/docs/result-schema-migration.md +56 -0
- package/package.json +21 -2
- package/protocol.mjs +84 -3
- package/scripts/agent-evaluation-contract.mjs +142 -0
- package/scripts/agent-evaluation-smoke.mjs +44 -0
- package/scripts/agent-evaluation.mjs +37 -0
- package/scripts/check-protocol-literals.mjs +20 -0
- package/scripts/ci-smoke.mjs +19 -4
- package/scripts/codex-plugin-verification.mjs +129 -0
- package/scripts/distribution-policy.mjs +28 -0
- package/scripts/distribution-smoke.mjs +2 -1
- package/scripts/generate-protocol-reference.mjs +22 -3
- package/scripts/postpublication-smoke.mjs +246 -0
- package/scripts/release-contract.mjs +70 -0
- package/scripts/release-smoke.mjs +123 -0
- package/scripts/release-verify.mjs +64 -0
- package/scripts/repository-matrix-contract.mjs +80 -0
- package/scripts/repository-matrix-smoke.mjs +153 -0
- package/scripts/repository-matrix.mjs +276 -0
- package/scripts/semantic-js-mcp-ci.mjs +5 -7
- package/scripts/smoke.mjs +223 -36
- package/scripts/vue-smoke.mjs +320 -5
- package/server.mjs +447 -208
- package/skills/semantic-navigation/SKILL.md +21 -10
- package/skills/semantic-navigation/references/protocol-literals.md +82 -4
|
@@ -12,13 +12,18 @@ import {
|
|
|
12
12
|
CLI_ARGUMENT,
|
|
13
13
|
CLI_COMMAND,
|
|
14
14
|
CLI_MESSAGE,
|
|
15
|
+
COMMON_VALUE,
|
|
15
16
|
CONFIGURATION_FILE,
|
|
16
17
|
CONTENT_FRESHNESS,
|
|
17
18
|
DEFAULT,
|
|
18
19
|
DEFINITION_MATCH,
|
|
19
20
|
DEFINITION_RESOLUTION_METHOD,
|
|
21
|
+
DEFINITION_SELECTION_STATUS,
|
|
20
22
|
DIAGNOSTIC_EVIDENCE_REASON,
|
|
21
23
|
DIAGNOSTIC_FRESHNESS,
|
|
24
|
+
DIAGNOSTIC_LANGUAGE,
|
|
25
|
+
DIAGNOSTIC_PROVIDER,
|
|
26
|
+
DIAGNOSTIC_REGION,
|
|
22
27
|
DIAGNOSTIC_SEVERITY,
|
|
23
28
|
DOCTOR_CHECK,
|
|
24
29
|
DOCTOR_DISTRIBUTION_ACCEPTED_STATUS,
|
|
@@ -29,10 +34,12 @@ import {
|
|
|
29
34
|
ERROR_CODE,
|
|
30
35
|
EVIDENCE_TYPE,
|
|
31
36
|
FINGERPRINT_ALGORITHM,
|
|
37
|
+
FINGERPRINT_FORMAT,
|
|
32
38
|
FORBIDDEN_PUBLIC_FIELD,
|
|
33
39
|
INTERNAL_RESOLUTION_SOURCE,
|
|
34
40
|
LANGUAGE_ID,
|
|
35
41
|
LIMIT_MODE,
|
|
42
|
+
LSP_METHOD,
|
|
36
43
|
NODE_EVENT,
|
|
37
44
|
PRESENTATION_MODE,
|
|
38
45
|
PACKAGE_PATH,
|
|
@@ -41,14 +48,17 @@ import {
|
|
|
41
48
|
REFERENCE_SET_CHANGE_TYPE,
|
|
42
49
|
REQUIRED_RUNTIME_COMPONENT,
|
|
43
50
|
RUNTIME_COMMAND,
|
|
51
|
+
RUNTIME_PACKAGE,
|
|
44
52
|
RUNTIME_REQUIREMENT,
|
|
45
53
|
RUNTIME_REQUIREMENT_KIND,
|
|
46
54
|
RESULT_SCHEMA,
|
|
47
55
|
SERVER_VERSION,
|
|
56
|
+
SEARCH_SCOPE,
|
|
48
57
|
SIGNATURE_SOURCE,
|
|
49
58
|
PROCESS_EXIT_CODE,
|
|
50
59
|
RUNTIME_STATUS,
|
|
51
60
|
TOOL_ORDER,
|
|
61
|
+
TOOL_DESCRIPTION,
|
|
52
62
|
TYPESCRIPT_PROJECT_KIND,
|
|
53
63
|
SOURCE_EXCLUDED_GLOBS,
|
|
54
64
|
SOURCE_EXTENSION,
|
|
@@ -81,12 +91,15 @@ const content =
|
|
|
81
91
|
"",
|
|
82
92
|
"Generated from `protocol.mjs`. Do not edit this file directly.",
|
|
83
93
|
"",
|
|
84
|
-
`
|
|
85
|
-
`Server name: \`${PRODUCT.NAME}\``,
|
|
94
|
+
`Producer: \`${PRODUCT.NAME}\` version \`${SERVER_VERSION}\``,
|
|
86
95
|
`Display name: \`${PRODUCT.DISPLAY_NAME}\``,
|
|
87
|
-
`Result schema
|
|
96
|
+
`Result schema version: \`${RESULT_SCHEMA.VERSION}\``,
|
|
88
97
|
"",
|
|
89
98
|
section("Tool Order", Object.fromEntries(TOOL_ORDER.map((tool) => [tool, tool]))),
|
|
99
|
+
section("Common Values", COMMON_VALUE),
|
|
100
|
+
section("Search Scope", SEARCH_SCOPE),
|
|
101
|
+
section("Runtime Packages", RUNTIME_PACKAGE),
|
|
102
|
+
namedSection("Tool Descriptions", TOOL_DESCRIPTION),
|
|
90
103
|
section("Workspace Configuration File Names", Object.fromEntries(WORKSPACE_CONFIGURATION_FILE_NAMES.map((name) => [name, name]))),
|
|
91
104
|
section("Configuration Files", CONFIGURATION_FILE),
|
|
92
105
|
section("Workspace Root Marker File Names", Object.fromEntries(WORKSPACE_ROOT_MARKER_FILE_NAMES.map((name) => [name, name]))),
|
|
@@ -98,10 +111,15 @@ const content =
|
|
|
98
111
|
section("Collection Status", COLLECTION_STATUS),
|
|
99
112
|
section("Presentation Mode", PRESENTATION_MODE),
|
|
100
113
|
section("Limit Mode", LIMIT_MODE),
|
|
114
|
+
section("LSP Methods", LSP_METHOD),
|
|
101
115
|
section("Accounting Status", ACCOUNTING_STATUS),
|
|
102
116
|
section("Definition Match", DEFINITION_MATCH),
|
|
117
|
+
section("Definition Selection Status", DEFINITION_SELECTION_STATUS),
|
|
103
118
|
section("Signature Source", SIGNATURE_SOURCE),
|
|
104
119
|
section("Diagnostic Freshness", DIAGNOSTIC_FRESHNESS),
|
|
120
|
+
section("Diagnostic Provider", DIAGNOSTIC_PROVIDER),
|
|
121
|
+
section("Diagnostic Region", DIAGNOSTIC_REGION),
|
|
122
|
+
section("Diagnostic Language", DIAGNOSTIC_LANGUAGE),
|
|
105
123
|
section("Diagnostic Severity", DIAGNOSTIC_SEVERITY),
|
|
106
124
|
section("Diagnostic Evidence Reason", DIAGNOSTIC_EVIDENCE_REASON),
|
|
107
125
|
section("Evidence Status", EVIDENCE_STATUS),
|
|
@@ -109,6 +127,7 @@ const content =
|
|
|
109
127
|
section("Error Code", ERROR_CODE),
|
|
110
128
|
section("Evidence Type", EVIDENCE_TYPE),
|
|
111
129
|
section("Fingerprint Algorithm", FINGERPRINT_ALGORITHM),
|
|
130
|
+
section("Fingerprint Format", FINGERPRINT_FORMAT),
|
|
112
131
|
section("Reference Discovery Method", REFERENCE_DISCOVERY_METHOD),
|
|
113
132
|
section("Reference Set Change Type", REFERENCE_SET_CHANGE_TYPE),
|
|
114
133
|
section("Definition Resolution Method", DEFINITION_RESOLUTION_METHOD),
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {spawn} from "node:child_process";
|
|
4
|
+
import {mkdtemp, mkdir, readFile, realpath, rm, writeFile} from "node:fs/promises";
|
|
5
|
+
import {tmpdir} from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
CI_EXIT_CODE,
|
|
9
|
+
CI_STATUS,
|
|
10
|
+
CLI_ARGUMENT,
|
|
11
|
+
CLI_COMMAND,
|
|
12
|
+
CONFIGURATION_FILE,
|
|
13
|
+
DOCTOR_CHECK,
|
|
14
|
+
DOCTOR_DISTRIBUTION_ACCEPTED_STATUS,
|
|
15
|
+
NODE_EVENT,
|
|
16
|
+
PRODUCT,
|
|
17
|
+
} from "../protocol.mjs";
|
|
18
|
+
import {verifyCodexPlugin} from "./codex-plugin-verification.mjs";
|
|
19
|
+
import {NPM_DISTRIBUTION, npmExecutableName} from "./distribution-policy.mjs";
|
|
20
|
+
import {RELEASE_ARGUMENT, RELEASE_CHECK, RELEASE_MODE, RELEASE_REASON, releaseStatus} from "./release-contract.mjs";
|
|
21
|
+
|
|
22
|
+
const POSTPUBLICATION_PATH = Object.freeze({
|
|
23
|
+
NPM_CACHE: "npm-cache",
|
|
24
|
+
CONSUMER: "consumer",
|
|
25
|
+
DEPENDENCY_DIRECTORY: "node_modules",
|
|
26
|
+
BINARY_DIRECTORY: ".bin",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const NPM_COMMAND = Object.freeze({
|
|
30
|
+
VIEW: "view",
|
|
31
|
+
INSTALL: "install",
|
|
32
|
+
VERSION: "version",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const NPM_ARGUMENT = Object.freeze({
|
|
36
|
+
JSON: "--json",
|
|
37
|
+
IGNORE_SCRIPTS: "--ignore-scripts",
|
|
38
|
+
NO_AUDIT: "--no-audit",
|
|
39
|
+
NO_FUND: "--no-fund",
|
|
40
|
+
SAVE_EXACT: "--save-exact",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const requestedVersion = process.argv[RELEASE_ARGUMENT.PACKAGE_VERSION_OFFSET];
|
|
44
|
+
const npmCli = process.env.npm_execpath;
|
|
45
|
+
const checks = [];
|
|
46
|
+
let workspace;
|
|
47
|
+
|
|
48
|
+
function run(command, args, options = {}) {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
const child = spawn(command, args, {stdio: ["ignore", "pipe", "pipe"], ...options});
|
|
51
|
+
const stdout = [];
|
|
52
|
+
const stderr = [];
|
|
53
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
54
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
55
|
+
child.on(NODE_EVENT.ERROR, (error) => resolve({exitCode: undefined, stdout: "", stderr: error.message}));
|
|
56
|
+
child.on(NODE_EVENT.CLOSE, (exitCode) =>
|
|
57
|
+
resolve({exitCode, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8")}),
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function runNpm(args, cwd, environment) {
|
|
63
|
+
if (npmCli) return run(process.execPath, [npmCli, ...args], {cwd, env: environment});
|
|
64
|
+
return run("npm", args, {cwd, env: environment});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function check(name, status, reason, details) {
|
|
68
|
+
return {name, status, reason, ...(details ? {details} : {})};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resultMessage(result) {
|
|
72
|
+
return result.stderr.trim() || result.stdout.trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function registryFailure(result) {
|
|
76
|
+
const message = `${result.stdout}\n${result.stderr}`;
|
|
77
|
+
if (message.includes("E404")) {
|
|
78
|
+
return check(RELEASE_CHECK.REGISTRY, CI_STATUS.FAIL, RELEASE_REASON.VERSION_NOT_PUBLISHED, {
|
|
79
|
+
packageVersion: requestedVersion,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return check(RELEASE_CHECK.REGISTRY, CI_STATUS.BLOCKED, RELEASE_REASON.REGISTRY_UNAVAILABLE, {
|
|
83
|
+
packageVersion: requestedVersion,
|
|
84
|
+
message: resultMessage(result),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function verifyRegistry(context) {
|
|
89
|
+
const registry = await runNpm(
|
|
90
|
+
[NPM_COMMAND.VIEW, context.packageVersion, NPM_COMMAND.VERSION, NPM_ARGUMENT.JSON],
|
|
91
|
+
context.consumer,
|
|
92
|
+
context.environment,
|
|
93
|
+
);
|
|
94
|
+
if (registry.exitCode !== CI_EXIT_CODE.PASS) return registryFailure(registry);
|
|
95
|
+
let publishedVersion;
|
|
96
|
+
try {
|
|
97
|
+
publishedVersion = JSON.parse(registry.stdout);
|
|
98
|
+
} catch {
|
|
99
|
+
return check(RELEASE_CHECK.REGISTRY, CI_STATUS.FAIL, RELEASE_REASON.REGISTRY_RESPONSE_INVALID, {
|
|
100
|
+
packageVersion: requestedVersion,
|
|
101
|
+
message: resultMessage(registry),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const matches = publishedVersion === requestedVersion;
|
|
105
|
+
return check(
|
|
106
|
+
RELEASE_CHECK.REGISTRY,
|
|
107
|
+
matches ? CI_STATUS.PASS : CI_STATUS.FAIL,
|
|
108
|
+
matches ? RELEASE_REASON.CHECK_COMPLETED : RELEASE_REASON.VERSION_NOT_PUBLISHED,
|
|
109
|
+
{packageVersion: requestedVersion},
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function verifyInstallation(context) {
|
|
114
|
+
const installed = await runNpm(
|
|
115
|
+
[
|
|
116
|
+
NPM_COMMAND.INSTALL,
|
|
117
|
+
NPM_ARGUMENT.IGNORE_SCRIPTS,
|
|
118
|
+
NPM_ARGUMENT.NO_AUDIT,
|
|
119
|
+
NPM_ARGUMENT.NO_FUND,
|
|
120
|
+
NPM_ARGUMENT.SAVE_EXACT,
|
|
121
|
+
context.packageVersion,
|
|
122
|
+
],
|
|
123
|
+
context.consumer,
|
|
124
|
+
context.environment,
|
|
125
|
+
);
|
|
126
|
+
const passed = installed.exitCode === CI_EXIT_CODE.PASS;
|
|
127
|
+
return check(
|
|
128
|
+
RELEASE_CHECK.INSTALLATION,
|
|
129
|
+
passed ? CI_STATUS.PASS : CI_STATUS.FAIL,
|
|
130
|
+
passed ? RELEASE_REASON.CHECK_COMPLETED : RELEASE_REASON.INSTALLATION_FAILED,
|
|
131
|
+
passed ? undefined : {message: resultMessage(installed)},
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function verifyInstalledVersion(context) {
|
|
136
|
+
const installedRoot = await realpath(path.join(context.consumer, POSTPUBLICATION_PATH.DEPENDENCY_DIRECTORY, PRODUCT.NAME));
|
|
137
|
+
const installedManifest = JSON.parse(await readFile(path.join(installedRoot, CONFIGURATION_FILE.PACKAGE), "utf8"));
|
|
138
|
+
const binary = path.join(
|
|
139
|
+
context.consumer,
|
|
140
|
+
POSTPUBLICATION_PATH.DEPENDENCY_DIRECTORY,
|
|
141
|
+
POSTPUBLICATION_PATH.BINARY_DIRECTORY,
|
|
142
|
+
npmExecutableName(PRODUCT.NAME),
|
|
143
|
+
);
|
|
144
|
+
const version = await run(binary, [CLI_ARGUMENT.VERSION], {cwd: context.consumer, env: context.environment});
|
|
145
|
+
const passed =
|
|
146
|
+
version.exitCode === CI_EXIT_CODE.PASS && version.stdout.trim() === requestedVersion && installedManifest.version === requestedVersion;
|
|
147
|
+
return {
|
|
148
|
+
binary,
|
|
149
|
+
result: check(
|
|
150
|
+
RELEASE_CHECK.INSTALLED_VERSION,
|
|
151
|
+
passed ? CI_STATUS.PASS : CI_STATUS.FAIL,
|
|
152
|
+
passed ? RELEASE_REASON.CHECK_COMPLETED : RELEASE_REASON.INSTALLED_VERSION_DIFFERENT,
|
|
153
|
+
{packageVersion: requestedVersion},
|
|
154
|
+
),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function verifyInstalledDoctor(context, binary) {
|
|
159
|
+
const doctor = await run(binary, [CLI_COMMAND.DOCTOR], {cwd: context.consumer, env: context.environment});
|
|
160
|
+
let doctorResult;
|
|
161
|
+
try {
|
|
162
|
+
doctorResult = JSON.parse(doctor.stdout);
|
|
163
|
+
} catch {
|
|
164
|
+
doctorResult = undefined;
|
|
165
|
+
}
|
|
166
|
+
const requiredDoctorChecks = new Set([
|
|
167
|
+
DOCTOR_CHECK.MCP_STARTUP,
|
|
168
|
+
DOCTOR_CHECK.TOOL_DISCOVERY,
|
|
169
|
+
DOCTOR_CHECK.TYPESCRIPT_SYMBOLS,
|
|
170
|
+
DOCTOR_CHECK.TYPESCRIPT_REFERENCES,
|
|
171
|
+
DOCTOR_CHECK.VUE_SYMBOLS,
|
|
172
|
+
DOCTOR_CHECK.VUE_TEMPLATE_DEFINITION,
|
|
173
|
+
]);
|
|
174
|
+
const completedDoctorChecks = new Set(
|
|
175
|
+
(doctorResult?.checks || []).filter((item) => item.status === CI_STATUS.PASS).map((item) => item.name),
|
|
176
|
+
);
|
|
177
|
+
const passed =
|
|
178
|
+
DOCTOR_DISTRIBUTION_ACCEPTED_STATUS.includes(doctorResult?.status) &&
|
|
179
|
+
[...requiredDoctorChecks].every((name) => completedDoctorChecks.has(name));
|
|
180
|
+
return check(
|
|
181
|
+
RELEASE_CHECK.INSTALLED_DOCTOR,
|
|
182
|
+
passed ? CI_STATUS.PASS : CI_STATUS.FAIL,
|
|
183
|
+
passed ? RELEASE_REASON.CHECK_COMPLETED : RELEASE_REASON.DOCTOR_REJECTED_INSTALLATION,
|
|
184
|
+
passed
|
|
185
|
+
? {doctorStatus: doctorResult.status, semanticChecks: requiredDoctorChecks.size}
|
|
186
|
+
: {doctorStatus: doctorResult?.status, message: resultMessage(doctor)},
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function runVerification() {
|
|
191
|
+
if (!requestedVersion) {
|
|
192
|
+
checks.push(check(RELEASE_CHECK.REGISTRY, CI_STATUS.BLOCKED, RELEASE_REASON.VERSION_REQUIRED));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
workspace = await mkdtemp(path.join(tmpdir(), `${PRODUCT.NAME}-postpublication-`));
|
|
197
|
+
const npmCache = path.join(workspace, POSTPUBLICATION_PATH.NPM_CACHE);
|
|
198
|
+
const consumer = path.join(workspace, POSTPUBLICATION_PATH.CONSUMER);
|
|
199
|
+
await mkdir(npmCache, {recursive: true});
|
|
200
|
+
await mkdir(consumer, {recursive: true});
|
|
201
|
+
await writeFile(path.join(consumer, CONFIGURATION_FILE.PACKAGE), JSON.stringify({private: true}));
|
|
202
|
+
const context = {
|
|
203
|
+
consumer,
|
|
204
|
+
environment: {...process.env, [NPM_DISTRIBUTION.CACHE_ENVIRONMENT_VARIABLE]: npmCache},
|
|
205
|
+
packageVersion: `${PRODUCT.NAME}@${requestedVersion}`,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const registry = await verifyRegistry(context);
|
|
209
|
+
checks.push(registry);
|
|
210
|
+
if (registry.status !== CI_STATUS.PASS) return;
|
|
211
|
+
|
|
212
|
+
const installation = await verifyInstallation(context);
|
|
213
|
+
checks.push(installation);
|
|
214
|
+
if (installation.status !== CI_STATUS.PASS) return;
|
|
215
|
+
|
|
216
|
+
const installedVersion = await verifyInstalledVersion(context);
|
|
217
|
+
checks.push(installedVersion.result);
|
|
218
|
+
if (installedVersion.result.status !== CI_STATUS.PASS) return;
|
|
219
|
+
|
|
220
|
+
checks.push(await verifyInstalledDoctor(context, installedVersion.binary));
|
|
221
|
+
checks.push(...(await verifyCodexPlugin({version: requestedVersion, workspace, spawn})));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
await runVerification();
|
|
226
|
+
} catch (error) {
|
|
227
|
+
checks.push(
|
|
228
|
+
check(RELEASE_CHECK.POSTPUBLICATION_ENVIRONMENT, CI_STATUS.BLOCKED, RELEASE_REASON.VERIFICATION_ENVIRONMENT_UNAVAILABLE, {
|
|
229
|
+
message: error instanceof Error ? error.message : String(error),
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
} finally {
|
|
233
|
+
if (workspace) await rm(workspace, {recursive: true, force: true});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const status = releaseStatus(checks);
|
|
237
|
+
const output = {
|
|
238
|
+
product: {name: PRODUCT.NAME, version: requestedVersion},
|
|
239
|
+
mode: RELEASE_MODE.PUBLISHED,
|
|
240
|
+
status,
|
|
241
|
+
exitCode: CI_EXIT_CODE[status.toUpperCase()],
|
|
242
|
+
checks,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
|
|
246
|
+
process.exitCode = output.exitCode;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {CI_STATUS} from "../protocol.mjs";
|
|
2
|
+
|
|
3
|
+
export const RELEASE_MODE = Object.freeze({
|
|
4
|
+
LOCAL: "local",
|
|
5
|
+
PUBLISHED: "published",
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const RELEASE_CHECK = Object.freeze({
|
|
9
|
+
STATIC: "static",
|
|
10
|
+
RUNTIME: "runtime",
|
|
11
|
+
CI_POLICY: "ci-policy",
|
|
12
|
+
TYPESCRIPT_SMOKE: "typescript-smoke",
|
|
13
|
+
VUE_SMOKE: "vue-smoke",
|
|
14
|
+
RELEASE_CONTRACT_SMOKE: "release-contract-smoke",
|
|
15
|
+
AGENT_EVALUATION_SMOKE: "agent-evaluation-smoke",
|
|
16
|
+
REPOSITORY_MATRIX_SMOKE: "repository-matrix-smoke",
|
|
17
|
+
DISTRIBUTION_SMOKE: "distribution-smoke",
|
|
18
|
+
BENCHMARK: "benchmark",
|
|
19
|
+
REGISTRY: "registry",
|
|
20
|
+
INSTALLATION: "installation",
|
|
21
|
+
INSTALLED_VERSION: "installed-version",
|
|
22
|
+
INSTALLED_DOCTOR: "installed-doctor",
|
|
23
|
+
CODEX_MARKETPLACE: "codex-marketplace",
|
|
24
|
+
CODEX_PLUGIN_INSTALLATION: "codex-plugin-installation",
|
|
25
|
+
POSTPUBLICATION_ENVIRONMENT: "postpublication-environment",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const RELEASE_REASON = Object.freeze({
|
|
29
|
+
CHECK_COMPLETED: "check-completed",
|
|
30
|
+
CHECK_FAILED: "check-failed",
|
|
31
|
+
VERSION_REQUIRED: "version-required",
|
|
32
|
+
REGISTRY_UNAVAILABLE: "registry-unavailable",
|
|
33
|
+
VERSION_NOT_PUBLISHED: "version-not-published",
|
|
34
|
+
INSTALLATION_FAILED: "installation-failed",
|
|
35
|
+
INSTALLED_VERSION_DIFFERENT: "installed-version-different",
|
|
36
|
+
DOCTOR_REJECTED_INSTALLATION: "doctor-rejected-installation",
|
|
37
|
+
CODEX_CLI_UNAVAILABLE: "codex-cli-unavailable",
|
|
38
|
+
CODEX_MARKETPLACE_UNAVAILABLE: "codex-marketplace-unavailable",
|
|
39
|
+
CODEX_MARKETPLACE_FAILED: "codex-marketplace-installation-failed",
|
|
40
|
+
CODEX_PLUGIN_SOURCE_UNAVAILABLE: "codex-plugin-source-unavailable",
|
|
41
|
+
CODEX_PLUGIN_INSTALLATION_FAILED: "codex-plugin-installation-failed",
|
|
42
|
+
CODEX_PLUGIN_VERSION_DIFFERENT: "codex-plugin-version-different",
|
|
43
|
+
REGISTRY_RESPONSE_INVALID: "registry-response-invalid",
|
|
44
|
+
VERIFICATION_ENVIRONMENT_UNAVAILABLE: "verification-environment-unavailable",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const RELEASE_ARGUMENT = Object.freeze({
|
|
48
|
+
PACKAGE_VERSION_OFFSET: 2,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const RELEASE_LOCAL_CHECKS = Object.freeze([
|
|
52
|
+
Object.freeze({name: RELEASE_CHECK.STATIC, npmScript: "check"}),
|
|
53
|
+
Object.freeze({name: RELEASE_CHECK.RUNTIME, npmScript: "check:runtime"}),
|
|
54
|
+
Object.freeze({name: RELEASE_CHECK.CI_POLICY, npmScript: "smoke:ci"}),
|
|
55
|
+
Object.freeze({name: RELEASE_CHECK.TYPESCRIPT_SMOKE, npmScript: "smoke"}),
|
|
56
|
+
Object.freeze({name: RELEASE_CHECK.VUE_SMOKE, npmScript: "smoke:vue"}),
|
|
57
|
+
Object.freeze({name: RELEASE_CHECK.RELEASE_CONTRACT_SMOKE, npmScript: "smoke:release"}),
|
|
58
|
+
Object.freeze({name: RELEASE_CHECK.AGENT_EVALUATION_SMOKE, npmScript: "smoke:evaluation"}),
|
|
59
|
+
Object.freeze({name: RELEASE_CHECK.REPOSITORY_MATRIX_SMOKE, npmScript: "smoke:matrix"}),
|
|
60
|
+
Object.freeze({name: RELEASE_CHECK.DISTRIBUTION_SMOKE, npmScript: "smoke:distribution"}),
|
|
61
|
+
Object.freeze({name: RELEASE_CHECK.BENCHMARK, npmScript: "benchmark"}),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
export function releaseStatus(checks) {
|
|
65
|
+
if (checks.length === 0) return CI_STATUS.BLOCKED;
|
|
66
|
+
if (checks.some((check) => check.status === CI_STATUS.BLOCKED)) return CI_STATUS.BLOCKED;
|
|
67
|
+
if (checks.some((check) => check.status === CI_STATUS.FAIL)) return CI_STATUS.FAIL;
|
|
68
|
+
if (checks.some((check) => check.status === CI_STATUS.UNTRUSTED)) return CI_STATUS.UNTRUSTED;
|
|
69
|
+
return CI_STATUS.PASS;
|
|
70
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {spawn} from "node:child_process";
|
|
4
|
+
import {mkdtemp, rm} from "node:fs/promises";
|
|
5
|
+
import {tmpdir} from "node:os";
|
|
6
|
+
import {fileURLToPath} from "node:url";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import {CI_EXIT_CODE, CI_STATUS, NODE_EVENT, PRODUCT} from "../protocol.mjs";
|
|
9
|
+
import {verifyCodexPlugin} from "./codex-plugin-verification.mjs";
|
|
10
|
+
import {CODEX_DISTRIBUTION} from "./distribution-policy.mjs";
|
|
11
|
+
import {RELEASE_MODE, RELEASE_REASON, releaseStatus} from "./release-contract.mjs";
|
|
12
|
+
|
|
13
|
+
function assert(condition, message) {
|
|
14
|
+
if (!condition) throw new Error(message);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function run(command, args, options = {}) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const child = spawn(command, args, {stdio: ["ignore", "pipe", "pipe"], ...options});
|
|
20
|
+
const stdout = [];
|
|
21
|
+
const stderr = [];
|
|
22
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
23
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
24
|
+
child.on(NODE_EVENT.ERROR, (error) => resolve({exitCode: undefined, stdout: "", stderr: error.message}));
|
|
25
|
+
child.on(NODE_EVENT.CLOSE, (exitCode) =>
|
|
26
|
+
resolve({exitCode, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8")}),
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
assert(releaseStatus([]) === CI_STATUS.BLOCKED, "Empty release check set was not blocked");
|
|
32
|
+
assert(releaseStatus([{status: CI_STATUS.UNTRUSTED}]) === CI_STATUS.UNTRUSTED, "Untrusted release state was not preserved");
|
|
33
|
+
assert(
|
|
34
|
+
releaseStatus([{status: CI_STATUS.UNTRUSTED}, {status: CI_STATUS.FAIL}]) === CI_STATUS.FAIL,
|
|
35
|
+
"Release failure did not outrank untrusted evidence",
|
|
36
|
+
);
|
|
37
|
+
assert(
|
|
38
|
+
releaseStatus([{status: CI_STATUS.FAIL}, {status: CI_STATUS.BLOCKED}]) === CI_STATUS.BLOCKED,
|
|
39
|
+
"Release blocker did not outrank a failed check",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const scriptRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
const postpublication = await run(process.execPath, [path.join(scriptRoot, "postpublication-smoke.mjs")]);
|
|
44
|
+
assert(postpublication.exitCode === CI_EXIT_CODE.BLOCKED, "Postpublication verification without a version used the wrong exit code");
|
|
45
|
+
const postpublicationResult = JSON.parse(postpublication.stdout);
|
|
46
|
+
assert(postpublicationResult.mode === RELEASE_MODE.PUBLISHED, "Postpublication result omitted its mode");
|
|
47
|
+
assert(postpublicationResult.status === CI_STATUS.BLOCKED, "Missing version was not blocked");
|
|
48
|
+
assert(postpublicationResult.checks[0]?.reason === RELEASE_REASON.VERSION_REQUIRED, "Missing version omitted its literal reason");
|
|
49
|
+
|
|
50
|
+
const codexWorkspace = await mkdtemp(path.join(tmpdir(), "semantic-js-mcp-codex-release-smoke-"));
|
|
51
|
+
try {
|
|
52
|
+
const version = "1.2.3";
|
|
53
|
+
const calls = [];
|
|
54
|
+
const runCommand = async (command, args, options) => {
|
|
55
|
+
calls.push({command, args, options});
|
|
56
|
+
if (args.includes(CODEX_DISTRIBUTION.LIST_COMMAND)) {
|
|
57
|
+
return {
|
|
58
|
+
exitCode: CI_EXIT_CODE.PASS,
|
|
59
|
+
stdout: JSON.stringify({
|
|
60
|
+
installed: [
|
|
61
|
+
{
|
|
62
|
+
name: PRODUCT.NAME,
|
|
63
|
+
marketplaceName: CODEX_DISTRIBUTION.MARKETPLACE_NAME,
|
|
64
|
+
version,
|
|
65
|
+
enabled: true,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
}),
|
|
69
|
+
stderr: "",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {exitCode: CI_EXIT_CODE.PASS, stdout: "{}", stderr: ""};
|
|
73
|
+
};
|
|
74
|
+
const codexChecks = await verifyCodexPlugin({version, workspace: codexWorkspace, runCommand});
|
|
75
|
+
assert(codexChecks.length === 2, "Codex verification omitted a dependent check");
|
|
76
|
+
assert(
|
|
77
|
+
codexChecks.every((item) => item.status === CI_STATUS.PASS),
|
|
78
|
+
"Codex verification rejected valid installation evidence",
|
|
79
|
+
);
|
|
80
|
+
assert(
|
|
81
|
+
calls[0].args.includes(`${CODEX_DISTRIBUTION.VERSION_REF_PREFIX}${version}`),
|
|
82
|
+
"Codex marketplace verification did not use an immutable version ref",
|
|
83
|
+
);
|
|
84
|
+
assert(calls[1].args.includes(CODEX_DISTRIBUTION.PLUGIN_SELECTOR), "Codex verification used the wrong plugin selector");
|
|
85
|
+
assert(
|
|
86
|
+
calls.every((call) => call.options.env[CODEX_DISTRIBUTION.HOME_ENVIRONMENT_VARIABLE].startsWith(codexWorkspace)),
|
|
87
|
+
"Codex verification did not isolate its state under the temporary workspace",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const unavailableChecks = await verifyCodexPlugin({
|
|
91
|
+
version,
|
|
92
|
+
workspace: codexWorkspace,
|
|
93
|
+
runCommand: async () => ({exitCode: undefined, stdout: "", stderr: "missing codex"}),
|
|
94
|
+
});
|
|
95
|
+
assert(unavailableChecks[0].status === CI_STATUS.BLOCKED, "Missing Codex CLI was not blocked");
|
|
96
|
+
assert(unavailableChecks[0].reason === RELEASE_REASON.CODEX_CLI_UNAVAILABLE, "Missing Codex CLI used the wrong reason");
|
|
97
|
+
|
|
98
|
+
const networkChecks = await verifyCodexPlugin({
|
|
99
|
+
version,
|
|
100
|
+
workspace: codexWorkspace,
|
|
101
|
+
runCommand: async () => ({exitCode: CI_EXIT_CODE.FAIL, stdout: "", stderr: "Could not resolve host"}),
|
|
102
|
+
});
|
|
103
|
+
assert(networkChecks[0].status === CI_STATUS.BLOCKED, "Unavailable Codex marketplace network was not blocked");
|
|
104
|
+
assert(
|
|
105
|
+
networkChecks[0].reason === RELEASE_REASON.CODEX_MARKETPLACE_UNAVAILABLE,
|
|
106
|
+
"Unavailable Codex marketplace network used the wrong reason",
|
|
107
|
+
);
|
|
108
|
+
} finally {
|
|
109
|
+
await rm(codexWorkspace, {recursive: true, force: true});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
process.stdout.write(
|
|
113
|
+
`${JSON.stringify(
|
|
114
|
+
{
|
|
115
|
+
releaseStatusPriority: "ok",
|
|
116
|
+
missingPublishedVersion: "blocked",
|
|
117
|
+
isolatedCodexPluginInstallation: "ok",
|
|
118
|
+
unavailableCodexMarketplace: "blocked",
|
|
119
|
+
},
|
|
120
|
+
null,
|
|
121
|
+
2,
|
|
122
|
+
)}\n`,
|
|
123
|
+
);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {spawn} from "node:child_process";
|
|
4
|
+
import {performance} from "node:perf_hooks";
|
|
5
|
+
import {CI_EXIT_CODE, CI_STATUS, NODE_EVENT, PRODUCT, SERVER_VERSION} from "../protocol.mjs";
|
|
6
|
+
import {RELEASE_LOCAL_CHECKS, RELEASE_MODE, RELEASE_REASON, releaseStatus} from "./release-contract.mjs";
|
|
7
|
+
|
|
8
|
+
const npmCli = process.env.npm_execpath;
|
|
9
|
+
|
|
10
|
+
function runNpmScript(npmScript) {
|
|
11
|
+
const command = npmCli ? process.execPath : "npm";
|
|
12
|
+
const args = npmCli ? [npmCli, "run", npmScript] : ["run", npmScript];
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const startedAt = performance.now();
|
|
15
|
+
const child = spawn(command, args, {cwd: process.cwd(), env: process.env, stdio: ["ignore", "pipe", "pipe"]});
|
|
16
|
+
const stdout = [];
|
|
17
|
+
const stderr = [];
|
|
18
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
19
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
20
|
+
child.on(NODE_EVENT.ERROR, (error) =>
|
|
21
|
+
resolve({
|
|
22
|
+
exitCode: undefined,
|
|
23
|
+
message: error.message,
|
|
24
|
+
durationMilliseconds: Math.round(performance.now() - startedAt),
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
child.on(NODE_EVENT.CLOSE, (exitCode) =>
|
|
28
|
+
resolve({
|
|
29
|
+
exitCode,
|
|
30
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
31
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
32
|
+
durationMilliseconds: Math.round(performance.now() - startedAt),
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const checks = [];
|
|
39
|
+
for (const configuredCheck of RELEASE_LOCAL_CHECKS) {
|
|
40
|
+
process.stderr.write(`[${PRODUCT.NAME}:release] ${configuredCheck.name}\n`);
|
|
41
|
+
const result = await runNpmScript(configuredCheck.npmScript);
|
|
42
|
+
const passed = result.exitCode === CI_EXIT_CODE.PASS;
|
|
43
|
+
checks.push({
|
|
44
|
+
name: configuredCheck.name,
|
|
45
|
+
npmScript: configuredCheck.npmScript,
|
|
46
|
+
status: passed ? CI_STATUS.PASS : CI_STATUS.FAIL,
|
|
47
|
+
reason: passed ? RELEASE_REASON.CHECK_COMPLETED : RELEASE_REASON.CHECK_FAILED,
|
|
48
|
+
exitCode: result.exitCode,
|
|
49
|
+
durationMilliseconds: result.durationMilliseconds,
|
|
50
|
+
...(passed ? {} : {message: result.message || result.stderr.trim() || result.stdout.trim()}),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const status = releaseStatus(checks);
|
|
55
|
+
const output = {
|
|
56
|
+
product: {name: PRODUCT.NAME, version: SERVER_VERSION},
|
|
57
|
+
mode: RELEASE_MODE.LOCAL,
|
|
58
|
+
status,
|
|
59
|
+
exitCode: CI_EXIT_CODE[status.toUpperCase()],
|
|
60
|
+
checks,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
|
|
64
|
+
process.exitCode = output.exitCode;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {CI_EXIT_CODE, CI_STATUS, COLLECTION_STATUS, DEFINITION_SELECTION_STATUS, EVIDENCE_STATUS} from "../protocol.mjs";
|
|
2
|
+
|
|
3
|
+
export const REPOSITORY_MATRIX_ARGUMENT = Object.freeze({
|
|
4
|
+
CONFIGURATION_OFFSET: 2,
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export const REPOSITORY_MATRIX_MODE = "repository-matrix";
|
|
8
|
+
|
|
9
|
+
export const REPOSITORY_MATRIX_PROBE_KIND = Object.freeze({
|
|
10
|
+
NAMED_SYMBOL: "named-symbol",
|
|
11
|
+
DIAGNOSTICS: "diagnostics",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const REPOSITORY_MATRIX_REASON = Object.freeze({
|
|
15
|
+
CHECK_COMPLETED: "check-completed",
|
|
16
|
+
CONFIGURATION_REQUIRED: "configuration-required",
|
|
17
|
+
CONFIGURATION_INVALID: "configuration-invalid",
|
|
18
|
+
REPOSITORY_UNAVAILABLE: "repository-unavailable",
|
|
19
|
+
MCP_STARTUP_FAILED: "mcp-startup-failed",
|
|
20
|
+
TOOL_EXECUTION_FAILED: "tool-execution-failed",
|
|
21
|
+
COLLECTION_INCOMPLETE: "collection-is-limited-or-partial",
|
|
22
|
+
DEFINITION_SELECTION_AMBIGUOUS: "definition-selection-is-not-exactly-one",
|
|
23
|
+
DIAGNOSTICS_UNTRUSTED: "diagnostics-for-current-document-are-untrusted",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const STATUS_PRIORITY = Object.freeze({
|
|
27
|
+
[CI_STATUS.PASS]: 0,
|
|
28
|
+
[CI_STATUS.UNTRUSTED]: 1,
|
|
29
|
+
[CI_STATUS.FAIL]: 2,
|
|
30
|
+
[CI_STATUS.BLOCKED]: 3,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export function repositoryMatrixStatus(items) {
|
|
34
|
+
if (items.length === 0) return CI_STATUS.BLOCKED;
|
|
35
|
+
return items.reduce((current, item) => (STATUS_PRIORITY[item.status] > STATUS_PRIORITY[current] ? item.status : current), CI_STATUS.PASS);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function repositoryMatrixExitCode(status) {
|
|
39
|
+
return CI_EXIT_CODE[status.toUpperCase()];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function collectionEvaluation(collectionStatus) {
|
|
43
|
+
if (collectionStatus === COLLECTION_STATUS.COMPLETE) {
|
|
44
|
+
return {status: CI_STATUS.PASS, reason: REPOSITORY_MATRIX_REASON.CHECK_COMPLETED};
|
|
45
|
+
}
|
|
46
|
+
if (collectionStatus === COLLECTION_STATUS.LIMITED || collectionStatus === COLLECTION_STATUS.PARTIAL) {
|
|
47
|
+
return {status: CI_STATUS.UNTRUSTED, reason: REPOSITORY_MATRIX_REASON.COLLECTION_INCOMPLETE};
|
|
48
|
+
}
|
|
49
|
+
return {status: CI_STATUS.FAIL, reason: REPOSITORY_MATRIX_REASON.TOOL_EXECUTION_FAILED};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function namedSymbolEvaluation(countResult, auditResult) {
|
|
53
|
+
const collection = repositoryMatrixStatus([
|
|
54
|
+
collectionEvaluation(countResult.collection.status),
|
|
55
|
+
collectionEvaluation(auditResult.collection.status),
|
|
56
|
+
]);
|
|
57
|
+
if (collection !== CI_STATUS.PASS) {
|
|
58
|
+
return {
|
|
59
|
+
status: collection,
|
|
60
|
+
reason:
|
|
61
|
+
collection === CI_STATUS.UNTRUSTED
|
|
62
|
+
? REPOSITORY_MATRIX_REASON.COLLECTION_INCOMPLETE
|
|
63
|
+
: REPOSITORY_MATRIX_REASON.TOOL_EXECUTION_FAILED,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const exactSelection =
|
|
67
|
+
countResult.result.definitionSelectionStatus === DEFINITION_SELECTION_STATUS.ONE &&
|
|
68
|
+
auditResult.result.definitionSelectionStatus === DEFINITION_SELECTION_STATUS.ONE;
|
|
69
|
+
return exactSelection
|
|
70
|
+
? {status: CI_STATUS.PASS, reason: REPOSITORY_MATRIX_REASON.CHECK_COMPLETED}
|
|
71
|
+
: {status: CI_STATUS.UNTRUSTED, reason: REPOSITORY_MATRIX_REASON.DEFINITION_SELECTION_AMBIGUOUS};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function diagnosticEvaluation(result) {
|
|
75
|
+
const collection = collectionEvaluation(result.collection.status);
|
|
76
|
+
if (result.result.evidence?.status !== EVIDENCE_STATUS.VERIFIED) {
|
|
77
|
+
return {status: CI_STATUS.UNTRUSTED, reason: REPOSITORY_MATRIX_REASON.DIAGNOSTICS_UNTRUSTED};
|
|
78
|
+
}
|
|
79
|
+
return collection;
|
|
80
|
+
}
|