semantic-js-mcp 0.8.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.
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {deepStrictEqual, strictEqual} from "node:assert";
4
+ import {spawn} from "node:child_process";
5
+ import {mkdtemp, rm, writeFile} from "node:fs/promises";
6
+ import {tmpdir} from "node:os";
7
+ import path from "node:path";
8
+ import {fileURLToPath} from "node:url";
9
+ import {
10
+ CI_EXIT_CODE,
11
+ CI_STATUS,
12
+ COLLECTION_STATUS,
13
+ DIAGNOSTIC_SEVERITY,
14
+ EVIDENCE_STATUS,
15
+ PRESENTATION_MODE,
16
+ PRODUCT,
17
+ RESULT_SCHEMA,
18
+ SERVER_VERSION,
19
+ TOOL,
20
+ } from "../protocol.mjs";
21
+
22
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
23
+ const evaluator = path.join(root, "scripts", "semantic-js-mcp-ci.mjs");
24
+ const workspace = await mkdtemp(path.join(tmpdir(), "semantic-js-mcp-ci-smoke-"));
25
+
26
+ function runEvaluator(inputFile) {
27
+ return new Promise((resolve, reject) => {
28
+ const child = spawn(process.execPath, [evaluator, inputFile], {stdio: ["ignore", "pipe", "pipe"]});
29
+ let stdout = "";
30
+ let stderr = "";
31
+ child.stdout.on("data", (chunk) => {
32
+ stdout += chunk;
33
+ });
34
+ child.stderr.on("data", (chunk) => {
35
+ stderr += chunk;
36
+ });
37
+ child.on("error", reject);
38
+ child.on("exit", (code) => resolve({code, stdout, stderr}));
39
+ });
40
+ }
41
+
42
+ async function evaluate(name, input, expectedStatus, expectedExitCode) {
43
+ const file = path.join(workspace, `${name}.json`);
44
+ await writeFile(file, JSON.stringify(input));
45
+ const execution = await runEvaluator(file);
46
+ strictEqual(execution.stderr, "", `${name} wrote stderr`);
47
+ strictEqual(execution.code, expectedExitCode, `${name} returned the wrong process exit code`);
48
+ const result = JSON.parse(execution.stdout);
49
+ deepStrictEqual({status: result.status, exitCode: result.exitCode}, {status: expectedStatus, exitCode: expectedExitCode});
50
+ }
51
+
52
+ function canonicalResult(tool, collectionStatus, result, extra = {}) {
53
+ return {
54
+ server: {name: PRODUCT.NAME, version: SERVER_VERSION},
55
+ resultSchema: {name: RESULT_SCHEMA.NAME, version: RESULT_SCHEMA.VERSION},
56
+ tool,
57
+ request: {},
58
+ result,
59
+ collection: {status: collectionStatus},
60
+ presentation: {mode: PRESENTATION_MODE.ALL_ITEMS},
61
+ continueWith: [],
62
+ ...extra,
63
+ };
64
+ }
65
+
66
+ try {
67
+ await evaluate("pass", canonicalResult(TOOL.COUNT_REFERENCES, COLLECTION_STATUS.COMPLETE, {}), CI_STATUS.PASS, CI_EXIT_CODE.PASS);
68
+ await evaluate(
69
+ "fail",
70
+ canonicalResult(TOOL.DIAGNOSTICS, COLLECTION_STATUS.COMPLETE, {
71
+ evidence: {status: EVIDENCE_STATUS.VERIFIED},
72
+ diagnosticsForCurrentDocument: {items: [{severity: DIAGNOSTIC_SEVERITY.ERROR}]},
73
+ }),
74
+ CI_STATUS.FAIL,
75
+ CI_EXIT_CODE.FAIL,
76
+ );
77
+ await evaluate(
78
+ "untrusted",
79
+ canonicalResult(TOOL.DIAGNOSTICS, COLLECTION_STATUS.PARTIAL, {evidence: {status: EVIDENCE_STATUS.UNTRUSTED}}),
80
+ CI_STATUS.UNTRUSTED,
81
+ CI_EXIT_CODE.UNTRUSTED,
82
+ );
83
+ await evaluate(
84
+ "untrusted-diagnostics-cannot-pass-with-complete-collection",
85
+ canonicalResult(TOOL.DIAGNOSTICS, COLLECTION_STATUS.COMPLETE, {
86
+ evidence: {status: EVIDENCE_STATUS.UNTRUSTED},
87
+ diagnosticsForCurrentDocument: null,
88
+ }),
89
+ CI_STATUS.UNTRUSTED,
90
+ CI_EXIT_CODE.UNTRUSTED,
91
+ );
92
+ await evaluate(
93
+ "blocked",
94
+ canonicalResult(TOOL.REFERENCES, COLLECTION_STATUS.FAILED, {}, {error: {code: "fixture"}}),
95
+ CI_STATUS.BLOCKED,
96
+ CI_EXIT_CODE.BLOCKED,
97
+ );
98
+ await evaluate(
99
+ "missing-canonical-identity",
100
+ {
101
+ tool: TOOL.COUNT_REFERENCES,
102
+ collection: {status: COLLECTION_STATUS.COMPLETE},
103
+ result: {},
104
+ },
105
+ CI_STATUS.BLOCKED,
106
+ CI_EXIT_CODE.BLOCKED,
107
+ );
108
+ await evaluate(
109
+ "wrong-result-schema-version",
110
+ {
111
+ ...canonicalResult(TOOL.COUNT_REFERENCES, COLLECTION_STATUS.COMPLETE, {}),
112
+ resultSchema: {name: RESULT_SCHEMA.NAME, version: RESULT_SCHEMA.VERSION + 1},
113
+ },
114
+ CI_STATUS.BLOCKED,
115
+ CI_EXIT_CODE.BLOCKED,
116
+ );
117
+ await evaluate(
118
+ "unknown-tool",
119
+ {
120
+ ...canonicalResult(TOOL.COUNT_REFERENCES, COLLECTION_STATUS.COMPLETE, {}),
121
+ tool: "unknown_tool",
122
+ },
123
+ CI_STATUS.BLOCKED,
124
+ CI_EXIT_CODE.BLOCKED,
125
+ );
126
+ await evaluate(
127
+ "unknown-presentation-mode",
128
+ {
129
+ ...canonicalResult(TOOL.COUNT_REFERENCES, COLLECTION_STATUS.COMPLETE, {}),
130
+ presentation: {mode: "unknown-mode"},
131
+ },
132
+ CI_STATUS.BLOCKED,
133
+ CI_EXIT_CODE.BLOCKED,
134
+ );
135
+ await evaluate(
136
+ "unknown-continuation-tool",
137
+ {
138
+ ...canonicalResult(TOOL.COUNT_REFERENCES, COLLECTION_STATUS.COMPLETE, {}),
139
+ continueWith: [{tool: "unknown_tool"}],
140
+ },
141
+ CI_STATUS.BLOCKED,
142
+ CI_EXIT_CODE.BLOCKED,
143
+ );
144
+ process.stdout.write(`${JSON.stringify({ciStatuses: Object.values(CI_STATUS), exitCodes: CI_EXIT_CODE}, null, 2)}\n`);
145
+ } finally {
146
+ await rm(workspace, {recursive: true, force: true});
147
+ }
@@ -0,0 +1,33 @@
1
+ export const REQUIRED_PACKAGE_FILE = Object.freeze({
2
+ PACKAGE_MANIFEST: "package.json",
3
+ PRETTIER_CONFIGURATION: ".prettierrc.json",
4
+ README: "README.md",
5
+ LICENSE: "LICENSE",
6
+ CLI: "cli.mjs",
7
+ SERVER: "server.mjs",
8
+ PROTOCOL: "protocol.mjs",
9
+ RUNTIME: "lib/runtime.mjs",
10
+ DOCTOR: "lib/doctor.mjs",
11
+ MCP_CONFIGURATION: ".mcp.json",
12
+ CODEX_PLUGIN_MANIFEST: ".codex-plugin/plugin.json",
13
+ SEMANTIC_NAVIGATION_SKILL: "skills/semantic-navigation/SKILL.md",
14
+ });
15
+
16
+ export const NPM_AUTOMATIC_PACKAGE_FILE = Object.freeze([
17
+ REQUIRED_PACKAGE_FILE.PACKAGE_MANIFEST,
18
+ REQUIRED_PACKAGE_FILE.README,
19
+ REQUIRED_PACKAGE_FILE.LICENSE,
20
+ ]);
21
+
22
+ const WINDOWS_PLATFORM = "win32";
23
+ const WINDOWS_COMMAND_SUFFIX = ".cmd";
24
+
25
+ export function npmExecutableName(name, platform = process.platform) {
26
+ if (platform === WINDOWS_PLATFORM) return `${name}${WINDOWS_COMMAND_SUFFIX}`;
27
+ return name;
28
+ }
29
+
30
+ export function packagePathIsAllowed(file, declaredFiles) {
31
+ if (NPM_AUTOMATIC_PACKAGE_FILE.includes(file)) return true;
32
+ return declaredFiles.some((entry) => (entry.endsWith("/") ? file.startsWith(entry) : file === entry));
33
+ }
@@ -0,0 +1,128 @@
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 {fileURLToPath} from "node:url";
8
+ import {
9
+ CLI_ARGUMENT,
10
+ CLI_MESSAGE,
11
+ PROCESS_EXIT_CODE,
12
+ DOCTOR_DISTRIBUTION_ACCEPTED_STATUS,
13
+ PRODUCT,
14
+ SERVER_VERSION,
15
+ NODE_EVENT,
16
+ } from "../protocol.mjs";
17
+ import {REQUIRED_PACKAGE_FILE, npmExecutableName, packagePathIsAllowed} from "./distribution-policy.mjs";
18
+
19
+ const sourceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
20
+ const workspace = await mkdtemp(path.join(tmpdir(), `${PRODUCT.NAME}-distribution-smoke-`));
21
+ const packageOutput = path.join(workspace, "package");
22
+ const consumer = path.join(workspace, "consumer");
23
+ const npmCache = path.join(workspace, "npm-cache");
24
+ const npmCli = process.env.npm_execpath;
25
+ const npmEnvironment = {...process.env, npm_config_cache: npmCache};
26
+
27
+ function run(command, args, options = {}) {
28
+ return new Promise((resolve, reject) => {
29
+ const child = spawn(command, args, {stdio: ["ignore", "pipe", "pipe"], ...options});
30
+ let stdout = "";
31
+ let stderr = "";
32
+ child.stdout.on("data", (chunk) => {
33
+ stdout += chunk;
34
+ });
35
+ child.stderr.on("data", (chunk) => {
36
+ stderr += chunk;
37
+ });
38
+ child.on("error", reject);
39
+ child.on(NODE_EVENT.CLOSE, (exitCode) => resolve({exitCode, stdout, stderr}));
40
+ });
41
+ }
42
+
43
+ function runNpm(args, cwd) {
44
+ if (npmCli) return run(process.execPath, [npmCli, ...args], {cwd, env: npmEnvironment});
45
+ return run("npm", args, {cwd, env: npmEnvironment});
46
+ }
47
+
48
+ function assert(condition, message) {
49
+ if (!condition) throw new Error(message);
50
+ }
51
+
52
+ try {
53
+ await mkdir(packageOutput, {recursive: true});
54
+ await mkdir(consumer, {recursive: true});
55
+ await mkdir(npmCache, {recursive: true});
56
+ const packed = await runNpm(["pack", "--json", "--pack-destination", packageOutput], sourceRoot);
57
+ assert(packed.exitCode === 0, `npm pack failed: ${packed.stderr}`);
58
+ const packResult = JSON.parse(packed.stdout)[0];
59
+ const packedPaths = packResult.files.map((item) => item.path);
60
+ const sourceManifest = JSON.parse(await readFile(path.join(sourceRoot, "package.json"), "utf8"));
61
+ for (const requiredFile of Object.values(REQUIRED_PACKAGE_FILE)) {
62
+ assert(packedPaths.includes(requiredFile), `Tarball omitted required file: ${requiredFile}`);
63
+ }
64
+ for (const packedPath of packedPaths) {
65
+ assert(packagePathIsAllowed(packedPath, sourceManifest.files), `Tarball included a file outside the public allowlist: ${packedPath}`);
66
+ }
67
+
68
+ const tarball = path.join(packageOutput, packResult.filename);
69
+ await writeFile(path.join(consumer, "package.json"), JSON.stringify({private: true}));
70
+ const installed = await runNpm(["install", "--ignore-scripts", "--no-audit", "--no-fund", "--prefer-offline", tarball], consumer);
71
+ assert(installed.exitCode === 0, `Tarball installation failed: ${installed.stderr}`);
72
+
73
+ const installedRoot = await realpath(path.join(consumer, "node_modules", PRODUCT.NAME));
74
+ const binaryName = npmExecutableName(PRODUCT.NAME);
75
+ const binary = path.join(consumer, "node_modules", ".bin", binaryName);
76
+ const isolatedPath = (process.env.PATH || "")
77
+ .split(path.delimiter)
78
+ .filter((entry) => entry && !path.resolve(entry).startsWith(sourceRoot))
79
+ .join(path.delimiter);
80
+ const doctor = await run(binary, ["doctor"], {cwd: consumer, env: {...process.env, PATH: isolatedPath}});
81
+ const version = await run(binary, [CLI_ARGUMENT.VERSION], {cwd: consumer, env: {...process.env, PATH: isolatedPath}});
82
+ assert(
83
+ version.exitCode === PROCESS_EXIT_CODE.SUCCESS && version.stdout.trim() === SERVER_VERSION,
84
+ "Installed executable returned the wrong version",
85
+ );
86
+ const help = await run(binary, [CLI_ARGUMENT.HELP], {cwd: consumer, env: {...process.env, PATH: isolatedPath}});
87
+ assert(help.exitCode === PROCESS_EXIT_CODE.SUCCESS && help.stdout.includes(PRODUCT.NAME), "Installed executable did not return help");
88
+ const invalid = await run(binary, ["unknown-command"], {cwd: consumer, env: {...process.env, PATH: isolatedPath}});
89
+ assert(
90
+ invalid.exitCode === PROCESS_EXIT_CODE.FAILURE && invalid.stderr.includes(CLI_MESSAGE.UNKNOWN_COMMAND),
91
+ "Installed executable accepted an unknown command",
92
+ );
93
+ const doctorResult = JSON.parse(doctor.stdout);
94
+ assert(
95
+ DOCTOR_DISTRIBUTION_ACCEPTED_STATUS.includes(doctorResult.status),
96
+ `Installed doctor returned ${doctorResult.status}: ${JSON.stringify(doctorResult, null, 2)}\n${doctor.stderr}`,
97
+ );
98
+ assert((await realpath(doctorResult.installationRoot)) === installedRoot, "Doctor resolved runtime files outside the installed package");
99
+ const installedDependencyRoot = await realpath(path.join(consumer, "node_modules"));
100
+ for (const component of doctorResult.runtime.components) {
101
+ const componentFile = await realpath(component.file);
102
+ assert(
103
+ componentFile.startsWith(`${installedDependencyRoot}${path.sep}`),
104
+ `Runtime component resolved outside the installed dependency tree: ${component.file}`,
105
+ );
106
+ assert(!componentFile.startsWith(`${sourceRoot}${path.sep}`), `Runtime component reused the source checkout: ${component.file}`);
107
+ }
108
+ assert(!doctorResult.installationRoot.startsWith(sourceRoot), "Installed doctor reused the source checkout");
109
+
110
+ const installedManifest = JSON.parse(await readFile(path.join(installedRoot, "package.json"), "utf8"));
111
+ process.stdout.write(
112
+ `${JSON.stringify(
113
+ {
114
+ package: {name: installedManifest.name, version: installedManifest.version, filename: packResult.filename},
115
+ packedFiles: packedPaths.length,
116
+ unpackedBytes: packResult.unpackedSize,
117
+ installedRoot,
118
+ executable: {version: version.stdout.trim(), help: "ok", invalidCommand: "rejected"},
119
+ doctor: {status: doctorResult.status, exitCode: doctorResult.exitCode, checks: doctorResult.checks.length},
120
+ sourceCheckoutReused: false,
121
+ },
122
+ null,
123
+ 2,
124
+ )}\n`,
125
+ );
126
+ } finally {
127
+ await rm(workspace, {recursive: true, force: true});
128
+ }
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {readFile, writeFile} from "node:fs/promises";
4
+ import path from "node:path";
5
+ import {fileURLToPath} from "node:url";
6
+ import {
7
+ ACCOUNTING_STATUS,
8
+ COLLECTION_STATUS,
9
+ CI_EXIT_CODE,
10
+ CI_REASON,
11
+ CI_STATUS,
12
+ CLI_ARGUMENT,
13
+ CLI_COMMAND,
14
+ CLI_MESSAGE,
15
+ CONFIGURATION_FILE,
16
+ CONTENT_FRESHNESS,
17
+ DEFAULT,
18
+ DEFINITION_MATCH,
19
+ DEFINITION_RESOLUTION_METHOD,
20
+ DIAGNOSTIC_EVIDENCE_REASON,
21
+ DIAGNOSTIC_FRESHNESS,
22
+ DIAGNOSTIC_SEVERITY,
23
+ DOCTOR_CHECK,
24
+ DOCTOR_DISTRIBUTION_ACCEPTED_STATUS,
25
+ DOCTOR_REASON,
26
+ DOCTOR_STATUS_PRIORITY,
27
+ EVIDENCE_STATUS,
28
+ ENVIRONMENT_VARIABLE,
29
+ ERROR_CODE,
30
+ EVIDENCE_TYPE,
31
+ FINGERPRINT_ALGORITHM,
32
+ FORBIDDEN_PUBLIC_FIELD,
33
+ INTERNAL_RESOLUTION_SOURCE,
34
+ LANGUAGE_ID,
35
+ LIMIT_MODE,
36
+ NODE_EVENT,
37
+ PRESENTATION_MODE,
38
+ PACKAGE_PATH,
39
+ PRODUCT,
40
+ REFERENCE_DISCOVERY_METHOD,
41
+ REFERENCE_SET_CHANGE_TYPE,
42
+ REQUIRED_RUNTIME_COMPONENT,
43
+ RUNTIME_COMMAND,
44
+ RUNTIME_REQUIREMENT,
45
+ RUNTIME_REQUIREMENT_KIND,
46
+ RESULT_SCHEMA,
47
+ SERVER_VERSION,
48
+ SIGNATURE_SOURCE,
49
+ PROCESS_EXIT_CODE,
50
+ RUNTIME_STATUS,
51
+ TOOL_ORDER,
52
+ TYPESCRIPT_PROJECT_KIND,
53
+ SOURCE_EXCLUDED_GLOBS,
54
+ SOURCE_EXTENSION,
55
+ SOURCE_FILE_GLOBS,
56
+ WORKSPACE_CONFIGURATION_FILE_NAMES,
57
+ WORKSPACE_ROOT_MARKER_FILE_NAMES,
58
+ UNRESOLVED_REFERENCE_REASON,
59
+ VUE_SCRIPT_LANGUAGE,
60
+ } from "../protocol.mjs";
61
+
62
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
63
+ const outputFile = path.join(root, "skills", "semantic-navigation", "references", "protocol-literals.md");
64
+
65
+ function section(title, values) {
66
+ return [`## ${title}`, "", ...Object.values(values).map((value) => `- \`${value}\``), ""].join("\n");
67
+ }
68
+
69
+ function namedSection(title, values) {
70
+ return [
71
+ `## ${title}`,
72
+ "",
73
+ ...Object.entries(values).map(([name, value]) => `- \`${name}\`: \`${Array.isArray(value) ? value.join(",") : value}\``),
74
+ "",
75
+ ].join("\n");
76
+ }
77
+
78
+ const content =
79
+ [
80
+ "# Protocol Literals",
81
+ "",
82
+ "Generated from `protocol.mjs`. Do not edit this file directly.",
83
+ "",
84
+ `Server version: \`${SERVER_VERSION}\``,
85
+ `Server name: \`${PRODUCT.NAME}\``,
86
+ `Display name: \`${PRODUCT.DISPLAY_NAME}\``,
87
+ `Result schema: \`${RESULT_SCHEMA.NAME}\` version \`${RESULT_SCHEMA.VERSION}\``,
88
+ "",
89
+ section("Tool Order", Object.fromEntries(TOOL_ORDER.map((tool) => [tool, tool]))),
90
+ section("Workspace Configuration File Names", Object.fromEntries(WORKSPACE_CONFIGURATION_FILE_NAMES.map((name) => [name, name]))),
91
+ section("Configuration Files", CONFIGURATION_FILE),
92
+ section("Workspace Root Marker File Names", Object.fromEntries(WORKSPACE_ROOT_MARKER_FILE_NAMES.map((name) => [name, name]))),
93
+ section("Source File Globs", Object.fromEntries(SOURCE_FILE_GLOBS.map((glob) => [glob, glob]))),
94
+ section("Excluded Source Globs", Object.fromEntries(SOURCE_EXCLUDED_GLOBS.map((glob) => [glob, glob]))),
95
+ section("Source Extensions", SOURCE_EXTENSION),
96
+ section("Language IDs", LANGUAGE_ID),
97
+ section("Vue Script Languages", VUE_SCRIPT_LANGUAGE),
98
+ section("Collection Status", COLLECTION_STATUS),
99
+ section("Presentation Mode", PRESENTATION_MODE),
100
+ section("Limit Mode", LIMIT_MODE),
101
+ section("Accounting Status", ACCOUNTING_STATUS),
102
+ section("Definition Match", DEFINITION_MATCH),
103
+ section("Signature Source", SIGNATURE_SOURCE),
104
+ section("Diagnostic Freshness", DIAGNOSTIC_FRESHNESS),
105
+ section("Diagnostic Severity", DIAGNOSTIC_SEVERITY),
106
+ section("Diagnostic Evidence Reason", DIAGNOSTIC_EVIDENCE_REASON),
107
+ section("Evidence Status", EVIDENCE_STATUS),
108
+ section("Reference Content Freshness", CONTENT_FRESHNESS),
109
+ section("Error Code", ERROR_CODE),
110
+ section("Evidence Type", EVIDENCE_TYPE),
111
+ section("Fingerprint Algorithm", FINGERPRINT_ALGORITHM),
112
+ section("Reference Discovery Method", REFERENCE_DISCOVERY_METHOD),
113
+ section("Reference Set Change Type", REFERENCE_SET_CHANGE_TYPE),
114
+ section("Definition Resolution Method", DEFINITION_RESOLUTION_METHOD),
115
+ section("Unresolved Reference Reason", UNRESOLVED_REFERENCE_REASON),
116
+ section("TypeScript Project Kind", TYPESCRIPT_PROJECT_KIND),
117
+ section("CI Status", CI_STATUS),
118
+ namedSection("CI Exit Codes", CI_EXIT_CODE),
119
+ section("CI Reason", CI_REASON),
120
+ section("CLI Commands", CLI_COMMAND),
121
+ section("CLI Arguments", CLI_ARGUMENT),
122
+ section("CLI Messages", CLI_MESSAGE),
123
+ section("Doctor Checks", DOCTOR_CHECK),
124
+ section("Doctor Reasons", DOCTOR_REASON),
125
+ section(
126
+ "Doctor Distribution Accepted Status",
127
+ Object.fromEntries(DOCTOR_DISTRIBUTION_ACCEPTED_STATUS.map((status) => [status, status])),
128
+ ),
129
+ namedSection("Doctor Status Priority", DOCTOR_STATUS_PRIORITY),
130
+ namedSection("Required Runtime Components", REQUIRED_RUNTIME_COMPONENT),
131
+ section("Runtime Commands", RUNTIME_COMMAND),
132
+ namedSection("Runtime Requirements", RUNTIME_REQUIREMENT),
133
+ section("Runtime Requirement Kinds", RUNTIME_REQUIREMENT_KIND),
134
+ section("Package Paths", PACKAGE_PATH),
135
+ section("Runtime Status", RUNTIME_STATUS),
136
+ section("Node Event", NODE_EVENT),
137
+ namedSection("Process Exit Codes", PROCESS_EXIT_CODE),
138
+ section("Internal Resolution Source", INTERNAL_RESOLUTION_SOURCE),
139
+ section("Forbidden Ambiguous Public Fields", Object.fromEntries(FORBIDDEN_PUBLIC_FIELD.map((field) => [field, field]))),
140
+ namedSection("Default Configuration Values", DEFAULT),
141
+ namedSection("Environment Variables", ENVIRONMENT_VARIABLE),
142
+ ]
143
+ .join("\n")
144
+ .trimEnd() + "\n";
145
+
146
+ if (process.argv.includes("--check")) {
147
+ const current = await readFile(outputFile, "utf8").catch(() => "");
148
+ if (current !== content) {
149
+ process.stderr.write("Generated protocol reference is out of date. Run: node scripts/generate-protocol-reference.mjs\n");
150
+ process.exitCode = 1;
151
+ }
152
+ } else {
153
+ await writeFile(outputFile, content);
154
+ }
@@ -0,0 +1,122 @@
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 {
6
+ CI_EXIT_CODE,
7
+ CI_REASON,
8
+ CI_STATUS,
9
+ COLLECTION_STATUS,
10
+ DIAGNOSTIC_SEVERITY,
11
+ EVIDENCE_STATUS,
12
+ PRESENTATION_MODE,
13
+ PRODUCT,
14
+ RESULT_SCHEMA,
15
+ TOOL,
16
+ TOOL_ORDER,
17
+ } from "../protocol.mjs";
18
+
19
+ const STANDARD_INPUT_PATH = "-";
20
+ const YAML_OUTPUT_ARGUMENT = "--yaml";
21
+ const COLLECTION_STATUSES = new Set(Object.values(COLLECTION_STATUS));
22
+ const PRESENTATION_MODES = new Set(Object.values(PRESENTATION_MODE));
23
+ const PUBLIC_TOOL_NAMES = new Set(TOOL_ORDER);
24
+
25
+ function isObject(value) {
26
+ return value !== null && typeof value === "object" && !Array.isArray(value);
27
+ }
28
+
29
+ function isCanonicalResult(result) {
30
+ const continuationsAreCanonical =
31
+ Array.isArray(result?.continueWith) &&
32
+ result.continueWith.every((continuation) => isObject(continuation) && PUBLIC_TOOL_NAMES.has(continuation.tool));
33
+ return (
34
+ result?.server?.name === PRODUCT.NAME &&
35
+ typeof result.server.version === "string" &&
36
+ result.server.version.length > 0 &&
37
+ result?.resultSchema?.name === RESULT_SCHEMA.NAME &&
38
+ result.resultSchema.version === RESULT_SCHEMA.VERSION &&
39
+ PUBLIC_TOOL_NAMES.has(result.tool) &&
40
+ isObject(result.request) &&
41
+ isObject(result.result) &&
42
+ isObject(result.collection) &&
43
+ isObject(result.presentation) &&
44
+ PRESENTATION_MODES.has(result.presentation.mode) &&
45
+ continuationsAreCanonical &&
46
+ COLLECTION_STATUSES.has(result.collection.status)
47
+ );
48
+ }
49
+
50
+ async function readInput(inputPath) {
51
+ return inputPath && inputPath !== STANDARD_INPUT_PATH
52
+ ? readFile(inputPath, "utf8")
53
+ : new Promise((resolve, reject) => {
54
+ const chunks = [];
55
+ process.stdin.setEncoding("utf8");
56
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
57
+ process.stdin.on("end", () => resolve(chunks.join("")));
58
+ process.stdin.on("error", reject);
59
+ });
60
+ }
61
+
62
+ function parseInput(text) {
63
+ try {
64
+ return JSON.parse(text);
65
+ } catch {
66
+ return parseYaml(text);
67
+ }
68
+ }
69
+
70
+ function decision(status, reason) {
71
+ return {status, exitCode: CI_EXIT_CODE[status.toUpperCase()], reason};
72
+ }
73
+
74
+ function evaluate(data) {
75
+ const result = data?.structuredContent || data;
76
+ if (!isCanonicalResult(result)) {
77
+ return {...decision(CI_STATUS.BLOCKED, CI_REASON.INVALID_INPUT), source: {}};
78
+ }
79
+ const source = {tool: result.tool, collectionStatus: result.collection.status};
80
+ if (result.collection.status === COLLECTION_STATUS.FAILED || result.error) {
81
+ return {...decision(CI_STATUS.BLOCKED, CI_REASON.TOOL_EXECUTION_FAILED), source};
82
+ }
83
+ if (
84
+ result.tool === TOOL.DIAGNOSTICS &&
85
+ (result.result?.evidence?.status !== EVIDENCE_STATUS.VERIFIED || !result.result?.diagnosticsForCurrentDocument)
86
+ ) {
87
+ return {...decision(CI_STATUS.UNTRUSTED, CI_REASON.UNTRUSTED_DIAGNOSTICS), source};
88
+ }
89
+ if (result.collection.status === COLLECTION_STATUS.LIMITED || result.collection.status === COLLECTION_STATUS.PARTIAL) {
90
+ return {
91
+ ...decision(CI_STATUS.UNTRUSTED, CI_REASON.INCOMPLETE_EVIDENCE),
92
+ source,
93
+ };
94
+ }
95
+ if (result.tool === TOOL.DIAGNOSTICS) {
96
+ const diagnostics = result.result?.diagnosticsForCurrentDocument?.items || [];
97
+ if (diagnostics.some((item) => item.severity === DIAGNOSTIC_SEVERITY.ERROR)) {
98
+ return {...decision(CI_STATUS.FAIL, CI_REASON.VERIFIED_DIAGNOSTIC_ERRORS), source};
99
+ }
100
+ }
101
+ return {...decision(CI_STATUS.PASS, CI_REASON.COMPLETE_EVIDENCE), source};
102
+ }
103
+
104
+ let output;
105
+ try {
106
+ const input = parseInput(await readInput(process.argv[2]));
107
+ output = {
108
+ protocol: {name: RESULT_SCHEMA.NAME, resultSchemaVersion: RESULT_SCHEMA.VERSION},
109
+ ...evaluate(input),
110
+ };
111
+ } catch (error) {
112
+ output = {
113
+ protocol: {name: RESULT_SCHEMA.NAME, resultSchemaVersion: RESULT_SCHEMA.VERSION},
114
+ ...decision(CI_STATUS.BLOCKED, CI_REASON.INVALID_INPUT),
115
+ source: {},
116
+ message: error instanceof Error ? error.message : String(error),
117
+ };
118
+ }
119
+
120
+ const useYaml = process.argv.includes(YAML_OUTPUT_ARGUMENT);
121
+ process.stdout.write(useYaml ? stringifyYaml(output, {lineWidth: 0}) : `${JSON.stringify(output, null, 2)}\n`);
122
+ process.exitCode = output.exitCode;