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.
- package/.codex-plugin/plugin.json +1 -1
- package/.prettierrc.json +1 -0
- package/CHANGELOG.md +18 -0
- package/CONTRIBUTING.md +6 -2
- package/README.md +13 -3
- package/ROADMAP.md +1 -1
- package/docs/distribution.md +8 -0
- package/docs/getting-started.md +72 -0
- package/docs/result-schema-migration.md +11 -0
- package/lib/doctor.mjs +3 -2
- package/lib/file-identity.mjs +18 -0
- package/lib/pending-requests.mjs +38 -0
- package/lib/runtime.mjs +4 -2
- package/lib/semantic-evidence.mjs +44 -0
- package/lib/stable-collection.mjs +10 -0
- package/lib/temporary-directory.mjs +15 -0
- package/package.json +9 -4
- package/protocol.mjs +32 -4
- package/scripts/agent-evaluation-contract.mjs +24 -1
- package/scripts/check-protocol-literals.mjs +10 -0
- package/scripts/ci-smoke.mjs +58 -1
- package/scripts/distribution-policy.mjs +0 -7
- package/scripts/distribution-smoke.mjs +17 -13
- package/scripts/doctor-smoke.mjs +40 -0
- package/scripts/documentation-contract.mjs +39 -0
- package/scripts/documentation-gate-smoke.mjs +55 -0
- package/scripts/documentation-gate.mjs +110 -0
- package/scripts/generate-protocol-reference.mjs +10 -0
- package/scripts/lifecycle-memory-benchmark.mjs +300 -0
- package/scripts/lifecycle-memory-contract.mjs +53 -0
- package/scripts/lifecycle-memory-observer.mjs +29 -0
- package/scripts/lifecycle-smoke.mjs +309 -0
- package/scripts/negative-verification-smoke.mjs +243 -0
- package/scripts/postpublication-smoke.mjs +14 -15
- package/scripts/release-contract.mjs +8 -0
- package/scripts/semantic-js-mcp-ci.mjs +35 -19
- package/scripts/smoke.mjs +87 -14
- package/scripts/vue-smoke.mjs +32 -2
- package/server.mjs +306 -124
- package/skills/semantic-navigation/SKILL.md +2 -0
- 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,
|
package/scripts/ci-smoke.mjs
CHANGED
|
@@ -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,
|
|
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:
|
|
51
|
-
return run("npm", args, {cwd, env:
|
|
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
|
|
85
|
-
const
|
|
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
|
|
91
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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]))),
|