@comity-dev/validate 0.1.0 → 0.1.1

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.
@@ -1,48 +0,0 @@
1
- import { comityConfigSchema, createAjvInstance, packageMetadataSchema, } from "@comity-dev/schemas";
2
- export function runSchema(repo) {
3
- const start = Date.now();
4
- const findings = [];
5
- const ajv = createAjvInstance();
6
- // Per-package metadata
7
- const validateMeta = ajv.compile(packageMetadataSchema);
8
- for (const pkg of repo.packages) {
9
- const valid = validateMeta(pkg.manifest);
10
- if (!valid) {
11
- for (const err of validateMeta.errors ?? []) {
12
- findings.push({
13
- tool: "schema",
14
- rule: "package-metadata",
15
- package: pkg.name,
16
- path: err.instancePath,
17
- message: err.message ?? "schema violation",
18
- keyword: err.keyword,
19
- severity: "error",
20
- });
21
- }
22
- }
23
- }
24
- // Repository config
25
- const validateConfig = ajv.compile(comityConfigSchema);
26
- const configValid = validateConfig(repo.config);
27
- if (!configValid) {
28
- for (const err of validateConfig.errors ?? []) {
29
- findings.push({
30
- tool: "schema",
31
- rule: "comity-config",
32
- path: err.instancePath,
33
- message: err.message ?? "schema violation",
34
- keyword: err.keyword,
35
- severity: "error",
36
- });
37
- }
38
- }
39
- return {
40
- executed: true,
41
- exitCode: 0,
42
- passed: findings.length === 0,
43
- findings,
44
- duration: Date.now() - start,
45
- status: findings.length === 0 ? "PASS" : "FAIL",
46
- tool: "schema",
47
- };
48
- }
@@ -1,3 +0,0 @@
1
- import type { Repository } from "../discover.js";
2
- import type { EngineResult } from "./types.js";
3
- export declare function runSemgrep(repo: Repository): Promise<EngineResult>;
@@ -1,252 +0,0 @@
1
- import { buildSemgrepConfig } from "@comity-dev/semgrep-rules";
2
- import { spawnSync } from "node:child_process";
3
- import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
- import { relative, resolve } from "node:path";
5
- /**
6
- * Discover the Python interpreter that ships the `semgrep` module.
7
- * We try well-known absolute paths first, then fall back to PATH.
8
- */
9
- function resolvePythonForSemgrep() {
10
- const candidates = [
11
- "/usr/bin/python3",
12
- "/usr/local/bin/python3",
13
- "/opt/homebrew/bin/python3",
14
- ];
15
- for (const c of candidates) {
16
- if (existsSync(c))
17
- return c;
18
- }
19
- const probe = spawnSync("which", ["python3"], { encoding: "utf8" });
20
- if (probe.status === 0 && probe.stdout?.trim())
21
- return probe.stdout.trim();
22
- return null;
23
- }
24
- function findSemgrepBin(fallbackDirs) {
25
- const explicitBin = process.env["SEMGREP_BIN"];
26
- if (explicitBin && existsSync(explicitBin))
27
- return explicitBin;
28
- const probe = spawnSync("which", ["semgrep"], {
29
- encoding: "utf8",
30
- env: {
31
- ...process.env,
32
- PATH: `${fallbackDirs.join(":")}:${process.env["PATH"] ?? ""}`,
33
- },
34
- });
35
- if (probe.status === 0 && probe.stdout?.trim()) {
36
- return probe.stdout.trim();
37
- }
38
- const absoluteCandidates = [
39
- "/usr/bin/semgrep",
40
- "/usr/local/bin/semgrep",
41
- "/opt/homebrew/bin/semgrep",
42
- ];
43
- for (const c of absoluteCandidates) {
44
- if (existsSync(c))
45
- return c;
46
- }
47
- return null;
48
- }
49
- /**
50
- * Returns the set of package directory names that should be excluded
51
- * from Semgrep scanning. The Development-owned runtime rule set
52
- * (`@comity-dev/semgrep-rules`) targets Comity Core Modules and
53
- * Adapters; it MUST NOT fire on Development tooling packages
54
- * (`comity.layer: "dev-tooling"`) because those packages legitimately
55
- * use Node APIs, `process.env`, and other patterns the rules forbid
56
- * for runtime Core Modules.
57
- */
58
- function devToolingPackageDirs(repo) {
59
- const dirs = [];
60
- for (const pkg of repo.packages) {
61
- if (pkg.layer === "dev-tooling") {
62
- const rel = relative(repo.root, pkg.path);
63
- const top = rel.split("/")[0];
64
- if (top)
65
- dirs.push(top);
66
- }
67
- }
68
- return [...new Set(dirs)];
69
- }
70
- function buildSemgrepArgs(configPath, target, excludedDirs) {
71
- const args = [
72
- "-c",
73
- "from semgrep.console_scripts.entrypoint import main; main()",
74
- "scan",
75
- "--config",
76
- configPath,
77
- "--json",
78
- "--quiet",
79
- "--error",
80
- target,
81
- ];
82
- // Exclude dev-tooling package directories by both path-pattern
83
- // (via --exclude) and directory name (via --exclude-dir) so semgrep
84
- // does not scan them at all. The runtime rule set is not designed
85
- // for development-tooling source.
86
- for (const d of excludedDirs) {
87
- args.push("--exclude", `**/packages/${d}/**`);
88
- args.push("--exclude-dir", d);
89
- }
90
- return args;
91
- }
92
- export async function runSemgrep(repo) {
93
- const start = Date.now();
94
- // Build the combined Semgrep config. Semgrep's CLI accepts a single
95
- // --config path, so we concatenate all canonical rule files into one
96
- // synthesized config.
97
- const configYaml = await buildSemgrepConfig();
98
- const tmpDir = mkdtempSync("/tmp/comity-validate-semgrep-");
99
- const configPath = `${tmpDir}/semgrep-config.yaml`;
100
- writeFileSync(configPath, configYaml);
101
- const pathDirs = (process.env["PATH"] ?? "").split(":").filter(Boolean);
102
- const fallbackDirs = ["/usr/local/bin", "/opt/homebrew/bin"];
103
- const candidateDirs = [...new Set([...pathDirs, ...fallbackDirs])];
104
- const cleanup = () => {
105
- try {
106
- rmSync(tmpDir, { recursive: true, force: true });
107
- }
108
- catch {
109
- // ignore
110
- }
111
- };
112
- const semgrepBin = findSemgrepBin(fallbackDirs);
113
- if (!semgrepBin) {
114
- cleanup();
115
- return {
116
- executed: false,
117
- exitCode: null,
118
- passed: false,
119
- findings: [],
120
- duration: Date.now() - start,
121
- status: "TOOL-UNAVAILABLE",
122
- tool: "semgrep",
123
- reason: "semgrep binary not found on PATH; install semgrep or set SEMGREP_BIN",
124
- };
125
- }
126
- // Locate the Python interpreter that ships the semgrep module.
127
- const pythonBin = resolvePythonForSemgrep();
128
- if (!pythonBin) {
129
- cleanup();
130
- return {
131
- executed: false,
132
- exitCode: null,
133
- passed: false,
134
- findings: [],
135
- duration: Date.now() - start,
136
- status: "TOOL-UNAVAILABLE",
137
- tool: "semgrep",
138
- reason: "Python interpreter not found; semgrep requires Python on PATH",
139
- };
140
- }
141
- // Augment PATH so the inner Python process can find semgrep's
142
- // sibling `pysemgrep` script (semgrep's `subprocess.run` does an
143
- // `execvp("pysemgrep")` lookup against this PATH).
144
- const envPath = [
145
- ...candidateDirs,
146
- "/Users/filippo/Library/Python/3.14/bin",
147
- "/Users/filippo/Library/Python/3.13/bin",
148
- "/Users/filippo/Library/Python/3.12/bin",
149
- process.env["PATH"] ?? "",
150
- "/usr/bin",
151
- "/bin",
152
- ].join(":");
153
- const target = resolve(repo.root, "packages");
154
- const excludedDirs = devToolingPackageDirs(repo);
155
- // If every discovered package is dev-tooling, the runtime rule
156
- // set is not applicable to this repository. Report NOT-APPLICABLE
157
- // rather than running semgrep against an empty target.
158
- const runtimePackages = repo.packages.filter((p) => p.layer !== "dev-tooling");
159
- if (runtimePackages.length === 0) {
160
- cleanup();
161
- return {
162
- executed: false,
163
- exitCode: null,
164
- passed: true,
165
- findings: [],
166
- duration: Date.now() - start,
167
- status: "NOT-APPLICABLE",
168
- tool: "semgrep",
169
- reason: "Repository contains no runtime packages; the @comity-dev/semgrep-rules rule set targets Comity Core Modules and Adapters and is not applicable to development-tooling-only repositories.",
170
- };
171
- }
172
- const args = buildSemgrepArgs(configPath, target, excludedDirs);
173
- const proc = spawnSync(pythonBin, args, {
174
- cwd: repo.root,
175
- env: { ...process.env, PATH: envPath },
176
- maxBuffer: 16 * 1024 * 1024,
177
- encoding: "utf8",
178
- });
179
- cleanup();
180
- const stdout = proc.stdout ?? "";
181
- const stderr = proc.stderr ?? "";
182
- const findings = parseSemgrepOutput(stdout, stderr);
183
- // If semgrep exited non-zero but produced no parseable findings, this
184
- // is an EXECUTION-ERROR (configuration, rule-parse, internal crash),
185
- // not a policy violation.
186
- if (proc.status !== null && proc.status !== 0 && findings.length === 0) {
187
- const stderrHead = stderr.split("\n")[0] ?? "";
188
- const stdoutHead = stdout.slice(0, 500);
189
- findings.push({
190
- tool: "semgrep",
191
- rule: "SEM-CRASH",
192
- message: `semgrep exited with code ${proc.status}; stderr: ${stderrHead}; stdout (first 500): ${stdoutHead}`,
193
- severity: "error",
194
- });
195
- return {
196
- executed: true,
197
- exitCode: proc.status,
198
- passed: false,
199
- findings,
200
- duration: Date.now() - start,
201
- status: "EXECUTION-ERROR",
202
- tool: "semgrep",
203
- };
204
- }
205
- return {
206
- executed: true,
207
- exitCode: proc.status,
208
- passed: proc.status === 0 && findings.length === 0,
209
- findings,
210
- duration: Date.now() - start,
211
- status: proc.status === 0 ? "PASS" : "FAIL",
212
- tool: "semgrep",
213
- };
214
- }
215
- function parseSemgrepOutput(stdout, stderr) {
216
- const out = stdout ?? stderr ?? "";
217
- if (!out.trim())
218
- return [];
219
- let parsed = null;
220
- try {
221
- parsed = JSON.parse(out);
222
- }
223
- catch {
224
- return [];
225
- }
226
- const findings = [];
227
- for (const e of parsed?.errors ?? []) {
228
- if (e.level === "error") {
229
- findings.push({
230
- tool: "semgrep",
231
- rule: `SEM-PARSE-${e.rule_id ?? "UNKNOWN"}`,
232
- message: e.message ?? "semgrep rule parse error",
233
- severity: "error",
234
- });
235
- }
236
- }
237
- const results = parsed?.results ?? [];
238
- for (const r of results) {
239
- const start = r["start"];
240
- const extra = r["extra"];
241
- findings.push({
242
- tool: "semgrep",
243
- rule: `SEM-${String(r["check_id"] ?? "UNKNOWN")}`,
244
- message: extra?.message ?? "Semgrep rule violation",
245
- file: typeof r["path"] === "string" ? r["path"] : undefined,
246
- line: start?.line,
247
- column: start?.col,
248
- severity: extra?.severity === "ERROR" ? "error" : "warning",
249
- });
250
- }
251
- return findings;
252
- }
@@ -1,25 +0,0 @@
1
- export type EngineStatus = "PASS" | "FAIL" | "NOT-RUN" | "TOOL-UNAVAILABLE" | "CONFIGURATION-ERROR" | "EXECUTION-ERROR" | "NOT-APPLICABLE";
2
- export interface Finding {
3
- tool: string;
4
- rule: string;
5
- package?: string | undefined;
6
- path?: string | undefined;
7
- message: string;
8
- severity: "error" | "warning";
9
- file?: string | undefined;
10
- line?: number | undefined;
11
- column?: number | undefined;
12
- keyword?: string | undefined;
13
- source?: string | undefined;
14
- remediation?: string | undefined;
15
- }
16
- export interface EngineResult {
17
- executed: boolean;
18
- exitCode: number | null;
19
- passed: boolean;
20
- findings: Finding[];
21
- duration: number;
22
- status: EngineStatus;
23
- tool?: string | undefined;
24
- reason?: string | undefined;
25
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,15 +0,0 @@
1
- /**
2
- * Canonical exit codes for `comity validate`.
3
- *
4
- * 0 = validation passed
5
- * 1 = validation failed (violations found)
6
- * 2 = configuration / discovery error (cannot run)
7
- * 3 = tool not installed (e.g. dependency-cruiser missing)
8
- */
9
- export declare const ExitCode: Readonly<{
10
- readonly PASS: 0;
11
- readonly FAIL: 1;
12
- readonly CONFIG_ERROR: 2;
13
- readonly TOOL_MISSING: 3;
14
- }>;
15
- export type ExitCodeValue = (typeof ExitCode)[keyof typeof ExitCode];
@@ -1,14 +0,0 @@
1
- /**
2
- * Canonical exit codes for `comity validate`.
3
- *
4
- * 0 = validation passed
5
- * 1 = validation failed (violations found)
6
- * 2 = configuration / discovery error (cannot run)
7
- * 3 = tool not installed (e.g. dependency-cruiser missing)
8
- */
9
- export const ExitCode = Object.freeze({
10
- PASS: 0,
11
- FAIL: 1,
12
- CONFIG_ERROR: 2,
13
- TOOL_MISSING: 3,
14
- });
package/dist/format.d.ts DELETED
@@ -1,33 +0,0 @@
1
- import type { Finding } from "./engines/types.js";
2
- import type { CategoryResult } from "./run.js";
3
- export interface FormatInput {
4
- /** Whether the overall validation passed or failed */
5
- passed: boolean;
6
- /** The list of all findings across all categories */
7
- violations: Finding[];
8
- /** The results of each validation category */
9
- categories: {
10
- /** Schema validation results */
11
- schema: CategoryResult;
12
- /** Package metadata validation results */
13
- metadata: CategoryResult;
14
- /** Comity config validation results */
15
- config: CategoryResult;
16
- /** Dependency validation results */
17
- dependencies: CategoryResult;
18
- /** ESLint validation results */
19
- eslint: CategoryResult;
20
- /** Semgrep validation results */
21
- semgrep: CategoryResult;
22
- /** Adapter peers validation results */
23
- adapterPeers: CategoryResult;
24
- };
25
- }
26
- /**
27
- * Format a unified summary for the terminal.
28
- */
29
- export declare function formatSummary(result: FormatInput): string;
30
- /**
31
- * Format detailed violations for debugging.
32
- */
33
- export declare function formatViolations(violations: Finding[]): string;
package/dist/format.js DELETED
@@ -1,76 +0,0 @@
1
- const CATEGORY_LABELS = {
2
- schema: "Schema",
3
- metadata: "Package metadata",
4
- config: "Comity config",
5
- dependencies: "Dependencies (depcruise)",
6
- eslint: "Source rules (ESLint)",
7
- semgrep: "Structural patterns (Semgrep)",
8
- adapterPeers: "Adapter peers",
9
- };
10
- function statusBadge(cat) {
11
- switch (cat.status) {
12
- case "PASS":
13
- return "PASS";
14
- case "FAIL":
15
- return "FAIL";
16
- case "NOT-RUN":
17
- return "NOT-RUN";
18
- case "TOOL-UNAVAILABLE":
19
- return "TOOL-UNAVAILABLE";
20
- case "CONFIGURATION-ERROR":
21
- return "CONFIG-ERROR";
22
- case "EXECUTION-ERROR":
23
- return "EXEC-ERROR";
24
- case "NOT-APPLICABLE":
25
- return "NOT-APPLICABLE";
26
- default:
27
- return cat.executed ? (cat.passed ? "PASS" : "FAIL") : "NOT-RUN";
28
- }
29
- }
30
- /**
31
- * Format a unified summary for the terminal.
32
- */
33
- export function formatSummary(result) {
34
- const lines = ["Comity validation", ""];
35
- for (const [key, label] of Object.entries(CATEGORY_LABELS)) {
36
- const cat = result.categories[key];
37
- if (!cat)
38
- continue;
39
- const badge = statusBadge(cat);
40
- const count = cat.findings > 0 ? ` (${cat.findings} findings)` : "";
41
- const exit = cat.exitCode !== null && cat.exitCode !== 0
42
- ? ` exit=${cat.exitCode}`
43
- : "";
44
- lines.push(`${badge.padEnd(18)} ${label}${count}${exit}`);
45
- }
46
- lines.push("");
47
- for (const [key, label] of Object.entries(CATEGORY_LABELS)) {
48
- const cat = result.categories[key];
49
- if (!cat)
50
- continue;
51
- if (cat.status === "TOOL-UNAVAILABLE" ||
52
- cat.status === "CONFIGURATION-ERROR" ||
53
- cat.status === "EXECUTION-ERROR" ||
54
- cat.status === "NOT-APPLICABLE") {
55
- lines.push(` ${label}: ${cat.reason ?? cat.status}`);
56
- }
57
- }
58
- lines.push("");
59
- lines.push(`Result: ${result.passed ? "PASS" : "FAIL"}`);
60
- return lines.join("\n");
61
- }
62
- /**
63
- * Format detailed violations for debugging.
64
- */
65
- export function formatViolations(violations) {
66
- if (violations.length === 0)
67
- return "";
68
- const lines = [`${violations.length} violation(s):\n`];
69
- for (const v of violations) {
70
- const loc = v.file
71
- ? ` ${v.file}${v.line ? `:${v.line}` : ""}${v.column ? `:${v.column}` : ""}`
72
- : "";
73
- lines.push(`[${v.tool}] ${v.rule} ${v.package ?? ""}${loc} — ${v.message}`);
74
- }
75
- return lines.join("\n");
76
- }
package/dist/index.d.ts DELETED
@@ -1,9 +0,0 @@
1
- export type { DiscoverOptions, PackageRecord, Repository } from "./discover.js";
2
- export type { EngineResult, EngineStatus, Finding } from "./engines/types.js";
3
- export type { ExitCodeValue } from "./exit-codes.js";
4
- export type { FormatInput } from "./format.js";
5
- export type { CategoryResult, RunOptions, RunResult } from "./run.js";
6
- export { discoverRepository } from "./discover.js";
7
- export { ExitCode } from "./exit-codes.js";
8
- export { formatSummary, formatViolations } from "./format.js";
9
- export { runValidation } from "./run.js";
package/dist/index.js DELETED
@@ -1,4 +0,0 @@
1
- export { discoverRepository } from "./discover.js";
2
- export { ExitCode } from "./exit-codes.js";
3
- export { formatSummary, formatViolations } from "./format.js";
4
- export { runValidation } from "./run.js";
package/dist/run.d.ts DELETED
@@ -1,56 +0,0 @@
1
- import type { EngineResult, Finding } from "./engines/types.js";
2
- export interface RunOptions {
3
- /** The root directory of the repository to validate */
4
- root: string;
5
- /** If specified, only run the given category of validation */
6
- only?: string | null;
7
- /** If true, print verbose output to stdout */
8
- verbose?: boolean;
9
- }
10
- export interface CategoryResult {
11
- /** The status of the engine for this category */
12
- status: EngineResult["status"];
13
- /** Whether the engine was executed or skipped */
14
- executed: boolean;
15
- /** The exit code of the engine process, if applicable */
16
- exitCode: number | null;
17
- /** Whether the engine passed or failed */
18
- passed: boolean;
19
- /** The number of findings produced by the engine */
20
- findings: number;
21
- /** The tool that produced this result, if applicable */
22
- tool?: string | undefined;
23
- /** The reason for the engine's status, if applicable */
24
- reason?: string | undefined;
25
- }
26
- export interface RunResult {
27
- /** Whether the overall validation passed or failed */
28
- passed: boolean;
29
- /** The list of all findings across all categories */
30
- violations: Finding[];
31
- /** The results of each validation category */
32
- categories: {
33
- /** Schema validation results */
34
- schema: CategoryResult;
35
- /** Package metadata validation results */
36
- metadata: CategoryResult;
37
- /** Comity config validation results */
38
- config: CategoryResult;
39
- /** Dependency validation results */
40
- dependencies: CategoryResult;
41
- /** ESLint validation results */
42
- eslint: CategoryResult;
43
- /** Semgrep validation results */
44
- semgrep: CategoryResult;
45
- /** Adapter peers validation results */
46
- adapterPeers: CategoryResult;
47
- };
48
- }
49
- /**
50
- * Run validation against a repository. Returns a normalized result.
51
- *
52
- * Every category reports either an executed engine with its real exit
53
- * code, or an explicit NOT-RUN / TOOL-UNAVAILABLE / CONFIGURATION-ERROR
54
- * state. PASS without execution is impossible.
55
- */
56
- export declare function runValidation(options: RunOptions): Promise<RunResult>;