semantic-js-mcp 0.9.0 → 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 (41) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/.prettierrc.json +1 -0
  3. package/CHANGELOG.md +18 -0
  4. package/CONTRIBUTING.md +6 -2
  5. package/README.md +13 -3
  6. package/ROADMAP.md +1 -1
  7. package/docs/distribution.md +8 -0
  8. package/docs/getting-started.md +72 -0
  9. package/docs/result-schema-migration.md +11 -0
  10. package/lib/doctor.mjs +3 -2
  11. package/lib/file-identity.mjs +18 -0
  12. package/lib/pending-requests.mjs +38 -0
  13. package/lib/runtime.mjs +4 -2
  14. package/lib/semantic-evidence.mjs +44 -0
  15. package/lib/stable-collection.mjs +10 -0
  16. package/lib/temporary-directory.mjs +15 -0
  17. package/package.json +9 -4
  18. package/protocol.mjs +32 -4
  19. package/scripts/agent-evaluation-contract.mjs +24 -1
  20. package/scripts/check-protocol-literals.mjs +10 -0
  21. package/scripts/ci-smoke.mjs +58 -1
  22. package/scripts/distribution-policy.mjs +0 -7
  23. package/scripts/distribution-smoke.mjs +17 -13
  24. package/scripts/doctor-smoke.mjs +40 -0
  25. package/scripts/documentation-contract.mjs +39 -0
  26. package/scripts/documentation-gate-smoke.mjs +55 -0
  27. package/scripts/documentation-gate.mjs +110 -0
  28. package/scripts/generate-protocol-reference.mjs +10 -0
  29. package/scripts/lifecycle-memory-benchmark.mjs +300 -0
  30. package/scripts/lifecycle-memory-contract.mjs +53 -0
  31. package/scripts/lifecycle-memory-observer.mjs +29 -0
  32. package/scripts/lifecycle-smoke.mjs +309 -0
  33. package/scripts/negative-verification-smoke.mjs +243 -0
  34. package/scripts/postpublication-smoke.mjs +14 -15
  35. package/scripts/release-contract.mjs +8 -0
  36. package/scripts/semantic-js-mcp-ci.mjs +35 -19
  37. package/scripts/smoke.mjs +87 -14
  38. package/scripts/vue-smoke.mjs +32 -2
  39. package/server.mjs +306 -124
  40. package/skills/semantic-navigation/SKILL.md +2 -0
  41. package/skills/semantic-navigation/references/protocol-literals.md +32 -4
@@ -36,8 +36,10 @@ import {
36
36
  FINGERPRINT_FORMAT,
37
37
  INTERNAL_RESOLUTION_SOURCE,
38
38
  LIMIT_MODE,
39
+ LSP_COMMAND,
39
40
  LSP_METHOD,
40
41
  NODE_EVENT,
42
+ OPERATING_SYSTEM,
41
43
  PRESENTATION_MODE,
42
44
  PACKAGE_PATH,
43
45
  PRODUCT,
@@ -50,8 +52,11 @@ import {
50
52
  RUNTIME_REQUIREMENT_KIND,
51
53
  RESULT_SCHEMA,
52
54
  SEARCH_SCOPE,
55
+ SEMANTIC_EVIDENCE_FOLLOW_UP_REASON,
56
+ SEMANTIC_EVIDENCE_STATUS,
53
57
  TOOL,
54
58
  TOOL_DESCRIPTION,
59
+ TYPESCRIPT_SERVER_COMMAND,
55
60
  TYPESCRIPT_PROJECT_KIND,
56
61
  SOURCE_EXCLUDED_GLOBS,
57
62
  SOURCE_FILE_GLOBS,
@@ -89,6 +94,8 @@ const groups = [
89
94
  ACCOUNTING_STATUS,
90
95
  DEFINITION_MATCH,
91
96
  DEFINITION_SELECTION_STATUS,
97
+ SEMANTIC_EVIDENCE_STATUS,
98
+ SEMANTIC_EVIDENCE_FOLLOW_UP_REASON,
92
99
  DIAGNOSTIC_FRESHNESS,
93
100
  DIAGNOSTIC_PROVIDER,
94
101
  DIAGNOSTIC_REGION,
@@ -106,6 +113,8 @@ const groups = [
106
113
  DEFINITION_RESOLUTION_METHOD,
107
114
  INTERNAL_RESOLUTION_SOURCE,
108
115
  LSP_METHOD,
116
+ LSP_COMMAND,
117
+ TYPESCRIPT_SERVER_COMMAND,
109
118
  UNRESOLVED_REFERENCE_REASON,
110
119
  TYPESCRIPT_PROJECT_KIND,
111
120
  CI_STATUS,
@@ -124,6 +133,7 @@ const groups = [
124
133
  PACKAGE_PATH,
125
134
  RUNTIME_STATUS,
126
135
  NODE_EVENT,
136
+ OPERATING_SYSTEM,
127
137
  PRODUCT,
128
138
  ENVIRONMENT_VARIABLE,
129
139
  RESULT_SCHEMA,
@@ -8,13 +8,17 @@ import path from "node:path";
8
8
  import {fileURLToPath} from "node:url";
9
9
  import {
10
10
  CI_EXIT_CODE,
11
+ CI_REASON,
11
12
  CI_STATUS,
12
13
  COLLECTION_STATUS,
14
+ DEFINITION_SELECTION_STATUS,
13
15
  DIAGNOSTIC_SEVERITY,
14
16
  EVIDENCE_STATUS,
15
17
  PRESENTATION_MODE,
16
18
  PRODUCT,
17
19
  RESULT_SCHEMA,
20
+ SEMANTIC_EVIDENCE_FOLLOW_UP_REASON,
21
+ SEMANTIC_EVIDENCE_STATUS,
18
22
  SERVER_VERSION,
19
23
  TOOL,
20
24
  } from "../protocol.mjs";
@@ -39,7 +43,7 @@ function runEvaluator(inputFile) {
39
43
  });
40
44
  }
41
45
 
42
- async function evaluate(name, input, expectedStatus, expectedExitCode) {
46
+ async function evaluate(name, input, expectedStatus, expectedExitCode, expectedReason) {
43
47
  const file = path.join(workspace, `${name}.json`);
44
48
  await writeFile(file, JSON.stringify(input));
45
49
  const execution = await runEvaluator(file);
@@ -47,6 +51,7 @@ async function evaluate(name, input, expectedStatus, expectedExitCode) {
47
51
  strictEqual(execution.code, expectedExitCode, `${name} returned the wrong process exit code`);
48
52
  const result = JSON.parse(execution.stdout);
49
53
  deepStrictEqual({status: result.status, exitCode: result.exitCode}, {status: expectedStatus, exitCode: expectedExitCode});
54
+ if (expectedReason) strictEqual(result.reason, expectedReason, `${name} returned the wrong reason`);
50
55
  }
51
56
 
52
57
  function canonicalResult(tool, collectionStatus, result, extra = {}) {
@@ -64,6 +69,44 @@ function canonicalResult(tool, collectionStatus, result, extra = {}) {
64
69
 
65
70
  try {
66
71
  await evaluate("pass", canonicalResult(TOOL.COUNT_REFERENCES, COLLECTION_STATUS.COMPLETE, {}), CI_STATUS.PASS, CI_EXIT_CODE.PASS);
72
+ await evaluate(
73
+ "named-semantic-evidence-usable",
74
+ canonicalResult(TOOL.COUNT_NAMED_SYMBOL, COLLECTION_STATUS.COMPLETE, {
75
+ definitionSelectionStatus: DEFINITION_SELECTION_STATUS.ONE,
76
+ semanticEvidence: {status: SEMANTIC_EVIDENCE_STATUS.USABLE, followUpReasons: []},
77
+ }),
78
+ CI_STATUS.PASS,
79
+ CI_EXIT_CODE.PASS,
80
+ );
81
+ await evaluate(
82
+ "named-semantic-follow-up-required",
83
+ canonicalResult(TOOL.AUDIT_NAMED_SYMBOL, COLLECTION_STATUS.COMPLETE, {
84
+ definitionSelectionStatus: DEFINITION_SELECTION_STATUS.MULTIPLE,
85
+ semanticEvidence: {
86
+ status: SEMANTIC_EVIDENCE_STATUS.FOLLOW_UP_REQUIRED,
87
+ followUpReasons: [SEMANTIC_EVIDENCE_FOLLOW_UP_REASON.MULTIPLE_DEFINITIONS_SELECTED],
88
+ },
89
+ }),
90
+ CI_STATUS.UNTRUSTED,
91
+ CI_EXIT_CODE.UNTRUSTED,
92
+ );
93
+ await evaluate(
94
+ "named-semantic-evidence-conflicts-with-source-fields",
95
+ canonicalResult(TOOL.COUNT_NAMED_SYMBOL, COLLECTION_STATUS.PARTIAL, {
96
+ definitionSelectionStatus: DEFINITION_SELECTION_STATUS.ONE,
97
+ semanticEvidence: {status: SEMANTIC_EVIDENCE_STATUS.USABLE, followUpReasons: []},
98
+ }),
99
+ CI_STATUS.BLOCKED,
100
+ CI_EXIT_CODE.BLOCKED,
101
+ );
102
+ await evaluate(
103
+ "named-semantic-evidence-requires-definition-selection",
104
+ canonicalResult(TOOL.COUNT_NAMED_SYMBOL, COLLECTION_STATUS.COMPLETE, {
105
+ semanticEvidence: {status: SEMANTIC_EVIDENCE_STATUS.USABLE, followUpReasons: []},
106
+ }),
107
+ CI_STATUS.BLOCKED,
108
+ CI_EXIT_CODE.BLOCKED,
109
+ );
67
110
  await evaluate(
68
111
  "fail",
69
112
  canonicalResult(TOOL.DIAGNOSTICS, COLLECTION_STATUS.COMPLETE, {
@@ -94,6 +137,20 @@ try {
94
137
  CI_STATUS.BLOCKED,
95
138
  CI_EXIT_CODE.BLOCKED,
96
139
  );
140
+ await evaluate(
141
+ "named-tool-execution-failed",
142
+ canonicalResult(TOOL.COUNT_NAMED_SYMBOL, COLLECTION_STATUS.FAILED, {}, {error: {code: "fixture"}}),
143
+ CI_STATUS.BLOCKED,
144
+ CI_EXIT_CODE.BLOCKED,
145
+ CI_REASON.TOOL_EXECUTION_FAILED,
146
+ );
147
+ await evaluate(
148
+ "named-tool-invalid-error-shape",
149
+ canonicalResult(TOOL.COUNT_NAMED_SYMBOL, COLLECTION_STATUS.FAILED, {}, {error: "fixture"}),
150
+ CI_STATUS.BLOCKED,
151
+ CI_EXIT_CODE.BLOCKED,
152
+ CI_REASON.INVALID_INPUT,
153
+ );
97
154
  await evaluate(
98
155
  "missing-canonical-identity",
99
156
  {
@@ -47,15 +47,8 @@ export const NPM_DISTRIBUTION = Object.freeze({
47
47
  CACHE_ENVIRONMENT_VARIABLE: "npm_config_cache",
48
48
  });
49
49
 
50
- const WINDOWS_PLATFORM = "win32";
51
- const WINDOWS_COMMAND_SUFFIX = ".cmd";
52
50
  const DEPENDENCY_DIRECTORY_PREFIX = "node_modules/";
53
51
 
54
- export function npmExecutableName(name, platform = process.platform) {
55
- if (platform === WINDOWS_PLATFORM) return `${name}${WINDOWS_COMMAND_SUFFIX}`;
56
- return name;
57
- }
58
-
59
52
  export function allProductionDependenciesAreBundled(manifest) {
60
53
  const dependencies = Object.keys(manifest.dependencies || {}).sort();
61
54
  const bundledDependencies = [...(manifest.bundleDependencies || [])].sort();
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import {spawn} from "node:child_process";
4
- import {mkdtemp, mkdir, readFile, realpath, rm, writeFile} from "node:fs/promises";
4
+ import {mkdtemp, mkdir, readFile, realpath, writeFile} from "node:fs/promises";
5
5
  import {tmpdir} from "node:os";
6
6
  import path from "node:path";
7
7
  import {fileURLToPath} from "node:url";
8
+ import {removeTemporaryDirectory} from "../lib/temporary-directory.mjs";
8
9
  import {
9
10
  CLI_ARGUMENT,
10
11
  CLI_MESSAGE,
@@ -18,7 +19,6 @@ import {
18
19
  REQUIRED_PACKAGE_FILE,
19
20
  NPM_DISTRIBUTION,
20
21
  allProductionDependenciesAreBundled,
21
- npmExecutableName,
22
22
  packagePathIsAllowed,
23
23
  } from "./distribution-policy.mjs";
24
24
 
@@ -46,9 +46,13 @@ function run(command, args, options = {}) {
46
46
  });
47
47
  }
48
48
 
49
- function runNpm(args, cwd) {
50
- if (npmCli) return run(process.execPath, [npmCli, ...args], {cwd, env: npmEnvironment});
51
- return run("npm", args, {cwd, env: npmEnvironment});
49
+ function runNpm(args, cwd, environment = npmEnvironment) {
50
+ if (npmCli) return run(process.execPath, [npmCli, ...args], {cwd, env: environment});
51
+ return run("npm", args, {cwd, env: environment});
52
+ }
53
+
54
+ function runInstalledExecutable(executable, args, cwd, environment) {
55
+ return run(process.execPath, [executable, ...args], {cwd, env: environment});
52
56
  }
53
57
 
54
58
  function assert(condition, message) {
@@ -81,21 +85,22 @@ try {
81
85
  assert(installed.exitCode === 0, `Tarball installation failed: ${installed.stderr}`);
82
86
 
83
87
  const installedRoot = await realpath(path.join(consumer, "node_modules", PRODUCT.NAME));
84
- const binaryName = npmExecutableName(PRODUCT.NAME);
85
- const binary = path.join(consumer, "node_modules", ".bin", binaryName);
88
+ const installedManifest = JSON.parse(await readFile(path.join(installedRoot, "package.json"), "utf8"));
89
+ const installedExecutable = path.join(installedRoot, installedManifest.bin[PRODUCT.NAME]);
86
90
  const isolatedPath = (process.env.PATH || "")
87
91
  .split(path.delimiter)
88
92
  .filter((entry) => entry && !path.resolve(entry).startsWith(sourceRoot))
89
93
  .join(path.delimiter);
90
- const doctor = await run(binary, ["doctor"], {cwd: consumer, env: {...process.env, PATH: isolatedPath}});
91
- const version = await run(binary, [CLI_ARGUMENT.VERSION], {cwd: consumer, env: {...process.env, PATH: isolatedPath}});
94
+ const installedEnvironment = {...npmEnvironment, PATH: isolatedPath};
95
+ const doctor = await runInstalledExecutable(installedExecutable, ["doctor"], consumer, installedEnvironment);
96
+ const version = await runInstalledExecutable(installedExecutable, [CLI_ARGUMENT.VERSION], consumer, installedEnvironment);
92
97
  assert(
93
98
  version.exitCode === PROCESS_EXIT_CODE.SUCCESS && version.stdout.trim() === SERVER_VERSION,
94
99
  "Installed executable returned the wrong version",
95
100
  );
96
- const help = await run(binary, [CLI_ARGUMENT.HELP], {cwd: consumer, env: {...process.env, PATH: isolatedPath}});
101
+ const help = await runInstalledExecutable(installedExecutable, [CLI_ARGUMENT.HELP], consumer, installedEnvironment);
97
102
  assert(help.exitCode === PROCESS_EXIT_CODE.SUCCESS && help.stdout.includes(PRODUCT.NAME), "Installed executable did not return help");
98
- const invalid = await run(binary, ["unknown-command"], {cwd: consumer, env: {...process.env, PATH: isolatedPath}});
103
+ const invalid = await runInstalledExecutable(installedExecutable, ["unknown-command"], consumer, installedEnvironment);
99
104
  assert(
100
105
  invalid.exitCode === PROCESS_EXIT_CODE.FAILURE && invalid.stderr.includes(CLI_MESSAGE.UNKNOWN_COMMAND),
101
106
  "Installed executable accepted an unknown command",
@@ -117,7 +122,6 @@ try {
117
122
  }
118
123
  assert(!doctorResult.installationRoot.startsWith(sourceRoot), "Installed doctor reused the source checkout");
119
124
 
120
- const installedManifest = JSON.parse(await readFile(path.join(installedRoot, "package.json"), "utf8"));
121
125
  process.stdout.write(
122
126
  `${JSON.stringify(
123
127
  {
@@ -136,5 +140,5 @@ try {
136
140
  )}\n`,
137
141
  );
138
142
  } finally {
139
- await rm(workspace, {recursive: true, force: true});
143
+ await removeTemporaryDirectory(workspace);
140
144
  }
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {spawn} from "node:child_process";
4
+ import path from "node:path";
5
+ import {fileURLToPath} from "node:url";
6
+ import {DOCTOR_DISTRIBUTION_ACCEPTED_STATUS, NODE_EVENT, PRODUCT} from "../protocol.mjs";
7
+
8
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+
10
+ const execution = await new Promise((resolve, reject) => {
11
+ const child = spawn(process.execPath, [path.join(root, "cli.mjs"), "doctor"], {
12
+ cwd: root,
13
+ stdio: ["ignore", "pipe", "pipe"],
14
+ });
15
+ const stdout = [];
16
+ const stderr = [];
17
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
18
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
19
+ child.on(NODE_EVENT.ERROR, reject);
20
+ child.on(NODE_EVENT.CLOSE, (exitCode) =>
21
+ resolve({exitCode, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8")}),
22
+ );
23
+ });
24
+
25
+ let report;
26
+ try {
27
+ report = JSON.parse(execution.stdout);
28
+ } catch {
29
+ throw new Error(`Doctor did not return JSON: ${execution.stderr || execution.stdout}`);
30
+ }
31
+ if (!DOCTOR_DISTRIBUTION_ACCEPTED_STATUS.includes(report.status)) {
32
+ throw new Error(`Doctor returned ${report.status}: ${JSON.stringify(report, null, 2)}\n${execution.stderr}`);
33
+ }
34
+ if (report.exitCode !== execution.exitCode) {
35
+ throw new Error(`Doctor process exit ${execution.exitCode} differs from reported exit ${report.exitCode}`);
36
+ }
37
+
38
+ process.stdout.write(
39
+ `${JSON.stringify({product: PRODUCT.NAME, status: report.status, exitCode: execution.exitCode, checks: report.checks.length}, null, 2)}\n`,
40
+ );
@@ -0,0 +1,39 @@
1
+ import {DOCTOR_REASON, SEMANTIC_EVIDENCE_FOLLOW_UP_REASON} from "../protocol.mjs";
2
+
3
+ export const DOCUMENTATION_FILE = Object.freeze({
4
+ README: "README.md",
5
+ GETTING_STARTED: "docs/getting-started.md",
6
+ CONTRIBUTING: "CONTRIBUTING.md",
7
+ SECURITY: "SECURITY.md",
8
+ });
9
+
10
+ export const PUBLIC_ROOT_DOCUMENT = Object.freeze(["CHANGELOG.md", "CONTRIBUTING.md", "README.md", "ROADMAP.md", "SECURITY.md"]);
11
+
12
+ export const PUBLIC_DOCUMENT_DIRECTORY = Object.freeze(["docs", "skills"]);
13
+
14
+ export const DOCUMENTATION_REQUIREMENT = Object.freeze({
15
+ README_HEADINGS: Object.freeze(["Installation", "Runtime", "Verification", "Current Limitations", "Reporting Problems"]),
16
+ GETTING_STARTED_HEADINGS: Object.freeze([
17
+ "Trace A Symbol",
18
+ "Review A Security-Sensitive Change",
19
+ "Check Current Diagnostics",
20
+ "Complete Evidence",
21
+ "Partial Evidence",
22
+ "Untrusted Diagnostics",
23
+ "Startup Failure",
24
+ ]),
25
+ README_LINKS: Object.freeze(["docs/getting-started.md", "CONTRIBUTING.md", "SECURITY.md"]),
26
+ GETTING_STARTED_LITERALS: Object.freeze([SEMANTIC_EVIDENCE_FOLLOW_UP_REASON.COLLECTION_PARTIAL, DOCTOR_REASON.RUNTIME_COMPONENT_MISSING]),
27
+ });
28
+
29
+ export const DOCUMENTATION_REASON = Object.freeze({
30
+ FILE_MISSING: "required-document-missing",
31
+ HEADING_MISSING: "required-heading-missing",
32
+ LINK_MISSING: "required-link-missing",
33
+ LITERAL_MISSING: "required-literal-missing",
34
+ PRIVATE_COORDINATION: "private-coordination-language-found",
35
+ LOCAL_PATH: "local-absolute-path-found",
36
+ });
37
+
38
+ export const PRIVATE_COORDINATION_PATTERN = /\b(?:private (?:ticket|handoff)|internal (?:id|priority|status)|workstream|depends on)\b/i;
39
+ export const LOCAL_ABSOLUTE_PATH_PATTERN = /\/(?:Users|home)\/[^/\s]+\//;
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {strictEqual} from "node:assert";
4
+ import {DOCUMENTATION_FILE, DOCUMENTATION_REASON, DOCUMENTATION_REQUIREMENT} from "./documentation-contract.mjs";
5
+ import {evaluateDocumentation} from "./documentation-gate.mjs";
6
+
7
+ const headingText = (headings) => headings.map((heading) => `## ${heading}`).join("\n");
8
+ const valid = {
9
+ [DOCUMENTATION_FILE.README]: `${headingText(DOCUMENTATION_REQUIREMENT.README_HEADINGS)}\n${DOCUMENTATION_REQUIREMENT.README_LINKS.map((link) => `[doc](${link})`).join("\n")}`,
10
+ [DOCUMENTATION_FILE.GETTING_STARTED]: `${headingText(DOCUMENTATION_REQUIREMENT.GETTING_STARTED_HEADINGS)}\n${DOCUMENTATION_REQUIREMENT.GETTING_STARTED_LITERALS.join("\n")}`,
11
+ [DOCUMENTATION_FILE.CONTRIBUTING]: "# Contributing",
12
+ [DOCUMENTATION_FILE.SECURITY]: "# Security",
13
+ };
14
+
15
+ strictEqual(evaluateDocumentation(valid).length, 0, "Valid public documentation failed the gate");
16
+
17
+ const missingHeading = {...valid, [DOCUMENTATION_FILE.README]: valid[DOCUMENTATION_FILE.README].replace("## Verification", "")};
18
+ strictEqual(
19
+ evaluateDocumentation(missingHeading).some((finding) => finding.reason === DOCUMENTATION_REASON.HEADING_MISSING),
20
+ true,
21
+ "Missing README heading was accepted",
22
+ );
23
+
24
+ const coordinationCode = ["WORK", "123"].join("-");
25
+ const coordinationPhrase = ["private", "handoff"].join(" ");
26
+ const internalLanguage = {
27
+ ...valid,
28
+ [DOCUMENTATION_FILE.CONTRIBUTING]: `Complete ${coordinationCode} from the ${coordinationPhrase}`,
29
+ };
30
+ strictEqual(
31
+ evaluateDocumentation(internalLanguage).some((finding) => finding.reason === DOCUMENTATION_REASON.PRIVATE_COORDINATION),
32
+ true,
33
+ "Private coordination language was accepted",
34
+ );
35
+
36
+ const missingLiteral = {
37
+ ...valid,
38
+ [DOCUMENTATION_FILE.GETTING_STARTED]: valid[DOCUMENTATION_FILE.GETTING_STARTED].replace(
39
+ DOCUMENTATION_REQUIREMENT.GETTING_STARTED_LITERALS[0],
40
+ "",
41
+ ),
42
+ };
43
+ strictEqual(
44
+ evaluateDocumentation(missingLiteral).some((finding) => finding.reason === DOCUMENTATION_REASON.LITERAL_MISSING),
45
+ true,
46
+ "Missing canonical example literal was accepted",
47
+ );
48
+
49
+ process.stdout.write(
50
+ `${JSON.stringify(
51
+ {validDocumentation: "pass", missingHeading: "rejected", missingLiteral: "rejected", privateCoordination: "rejected"},
52
+ null,
53
+ 2,
54
+ )}\n`,
55
+ );
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {readdir, readFile} from "node:fs/promises";
4
+ import path from "node:path";
5
+ import {fileURLToPath} from "node:url";
6
+ import {
7
+ DOCUMENTATION_FILE,
8
+ DOCUMENTATION_REASON,
9
+ DOCUMENTATION_REQUIREMENT,
10
+ LOCAL_ABSOLUTE_PATH_PATTERN,
11
+ PRIVATE_COORDINATION_PATTERN,
12
+ PUBLIC_DOCUMENT_DIRECTORY,
13
+ PUBLIC_ROOT_DOCUMENT,
14
+ } from "./documentation-contract.mjs";
15
+
16
+ function markdownHeadings(source) {
17
+ return new Set(
18
+ source
19
+ .split("\n")
20
+ .map((line) => /^#{1,6}\s+(.+?)\s*$/.exec(line)?.[1])
21
+ .filter(Boolean),
22
+ );
23
+ }
24
+
25
+ function repositoryPath(root, absolute) {
26
+ return path.relative(root, absolute).split(path.sep).join("/");
27
+ }
28
+
29
+ export function evaluateDocumentation(documents) {
30
+ const findings = [];
31
+ for (const file of Object.values(DOCUMENTATION_FILE)) {
32
+ if (documents[file] !== undefined) continue;
33
+ findings.push({file, reason: DOCUMENTATION_REASON.FILE_MISSING});
34
+ }
35
+
36
+ const headingRequirements = [
37
+ [DOCUMENTATION_FILE.README, DOCUMENTATION_REQUIREMENT.README_HEADINGS],
38
+ [DOCUMENTATION_FILE.GETTING_STARTED, DOCUMENTATION_REQUIREMENT.GETTING_STARTED_HEADINGS],
39
+ ];
40
+ for (const [file, requiredHeadings] of headingRequirements) {
41
+ if (documents[file] === undefined) continue;
42
+ const headings = markdownHeadings(documents[file]);
43
+ for (const heading of requiredHeadings) {
44
+ if (headings.has(heading)) continue;
45
+ findings.push({file, reason: DOCUMENTATION_REASON.HEADING_MISSING, heading});
46
+ }
47
+ }
48
+
49
+ const readme = documents[DOCUMENTATION_FILE.README];
50
+ if (readme !== undefined) {
51
+ for (const link of DOCUMENTATION_REQUIREMENT.README_LINKS) {
52
+ if (readme.includes(`](${link})`)) continue;
53
+ findings.push({file: DOCUMENTATION_FILE.README, reason: DOCUMENTATION_REASON.LINK_MISSING, link});
54
+ }
55
+ }
56
+
57
+ const gettingStarted = documents[DOCUMENTATION_FILE.GETTING_STARTED];
58
+ if (gettingStarted !== undefined) {
59
+ for (const literal of DOCUMENTATION_REQUIREMENT.GETTING_STARTED_LITERALS) {
60
+ if (gettingStarted.includes(literal)) continue;
61
+ findings.push({file: DOCUMENTATION_FILE.GETTING_STARTED, reason: DOCUMENTATION_REASON.LITERAL_MISSING, literal});
62
+ }
63
+ }
64
+
65
+ for (const [file, source] of Object.entries(documents)) {
66
+ if (PRIVATE_COORDINATION_PATTERN.test(source)) {
67
+ findings.push({file, reason: DOCUMENTATION_REASON.PRIVATE_COORDINATION});
68
+ }
69
+ if (LOCAL_ABSOLUTE_PATH_PATTERN.test(source)) {
70
+ findings.push({file, reason: DOCUMENTATION_REASON.LOCAL_PATH});
71
+ }
72
+ }
73
+ return findings;
74
+ }
75
+
76
+ async function markdownFiles(directory, root) {
77
+ const entries = await readdir(directory, {withFileTypes: true});
78
+ const files = [];
79
+ for (const entry of entries) {
80
+ const absolute = path.join(directory, entry.name);
81
+ if (entry.isDirectory()) {
82
+ files.push(...(await markdownFiles(absolute, root)));
83
+ continue;
84
+ }
85
+ if (entry.isFile() && entry.name.endsWith(".md")) files.push(repositoryPath(root, absolute));
86
+ }
87
+ return files;
88
+ }
89
+
90
+ async function publicDocumentationFiles(root) {
91
+ const nested = await Promise.all(PUBLIC_DOCUMENT_DIRECTORY.map((directory) => markdownFiles(path.join(root, directory), root)));
92
+ return [...new Set([...PUBLIC_ROOT_DOCUMENT, ...nested.flat()])].sort();
93
+ }
94
+
95
+ async function main() {
96
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
97
+ const documents = {};
98
+ for (const file of await publicDocumentationFiles(root)) {
99
+ try {
100
+ documents[file] = await readFile(path.join(root, file), "utf8");
101
+ } catch (error) {
102
+ if (error?.code !== "ENOENT") throw error;
103
+ }
104
+ }
105
+ const findings = evaluateDocumentation(documents);
106
+ process.stdout.write(`${JSON.stringify({status: findings.length === 0 ? "pass" : "fail", findings}, null, 2)}\n`);
107
+ if (findings.length > 0) process.exitCode = 1;
108
+ }
109
+
110
+ if (path.resolve(process.argv[1] || "") === fileURLToPath(import.meta.url)) await main();
@@ -39,8 +39,10 @@ import {
39
39
  INTERNAL_RESOLUTION_SOURCE,
40
40
  LANGUAGE_ID,
41
41
  LIMIT_MODE,
42
+ LSP_COMMAND,
42
43
  LSP_METHOD,
43
44
  NODE_EVENT,
45
+ OPERATING_SYSTEM,
44
46
  PRESENTATION_MODE,
45
47
  PACKAGE_PATH,
46
48
  PRODUCT,
@@ -54,11 +56,14 @@ import {
54
56
  RESULT_SCHEMA,
55
57
  SERVER_VERSION,
56
58
  SEARCH_SCOPE,
59
+ SEMANTIC_EVIDENCE_FOLLOW_UP_REASON,
60
+ SEMANTIC_EVIDENCE_STATUS,
57
61
  SIGNATURE_SOURCE,
58
62
  PROCESS_EXIT_CODE,
59
63
  RUNTIME_STATUS,
60
64
  TOOL_ORDER,
61
65
  TOOL_DESCRIPTION,
66
+ TYPESCRIPT_SERVER_COMMAND,
62
67
  TYPESCRIPT_PROJECT_KIND,
63
68
  SOURCE_EXCLUDED_GLOBS,
64
69
  SOURCE_EXTENSION,
@@ -112,9 +117,13 @@ const content =
112
117
  section("Presentation Mode", PRESENTATION_MODE),
113
118
  section("Limit Mode", LIMIT_MODE),
114
119
  section("LSP Methods", LSP_METHOD),
120
+ section("LSP Commands", LSP_COMMAND),
121
+ section("TypeScript Server Commands", TYPESCRIPT_SERVER_COMMAND),
115
122
  section("Accounting Status", ACCOUNTING_STATUS),
116
123
  section("Definition Match", DEFINITION_MATCH),
117
124
  section("Definition Selection Status", DEFINITION_SELECTION_STATUS),
125
+ section("Semantic Evidence Status", SEMANTIC_EVIDENCE_STATUS),
126
+ section("Semantic Evidence Follow-up Reason", SEMANTIC_EVIDENCE_FOLLOW_UP_REASON),
118
127
  section("Signature Source", SIGNATURE_SOURCE),
119
128
  section("Diagnostic Freshness", DIAGNOSTIC_FRESHNESS),
120
129
  section("Diagnostic Provider", DIAGNOSTIC_PROVIDER),
@@ -153,6 +162,7 @@ const content =
153
162
  section("Package Paths", PACKAGE_PATH),
154
163
  section("Runtime Status", RUNTIME_STATUS),
155
164
  section("Node Event", NODE_EVENT),
165
+ section("Operating System", OPERATING_SYSTEM),
156
166
  namedSection("Process Exit Codes", PROCESS_EXIT_CODE),
157
167
  section("Internal Resolution Source", INTERNAL_RESOLUTION_SOURCE),
158
168
  section("Forbidden Ambiguous Public Fields", Object.fromEntries(FORBIDDEN_PUBLIC_FIELD.map((field) => [field, field]))),