@comity-dev/validate 0.1.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/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/bin/comity-validate.d.ts +26 -0
- package/dist/bin/comity-validate.js +99 -0
- package/dist/discover.d.ts +41 -0
- package/dist/discover.js +74 -0
- package/dist/engines/adapter-peers.d.ts +3 -0
- package/dist/engines/adapter-peers.js +82 -0
- package/dist/engines/depcruise.d.ts +3 -0
- package/dist/engines/depcruise.js +109 -0
- package/dist/engines/eslint.d.ts +3 -0
- package/dist/engines/eslint.js +170 -0
- package/dist/engines/schema.d.ts +3 -0
- package/dist/engines/schema.js +48 -0
- package/dist/engines/semgrep.d.ts +3 -0
- package/dist/engines/semgrep.js +252 -0
- package/dist/engines/types.d.ts +25 -0
- package/dist/engines/types.js +1 -0
- package/dist/exit-codes.d.ts +15 -0
- package/dist/exit-codes.js +14 -0
- package/dist/format.d.ts +33 -0
- package/dist/format.js +76 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +4 -0
- package/dist/run.d.ts +56 -0
- package/dist/run.js +144 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Filippo Bovo and contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @comity-dev/validate
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Thin orchestrator over Comity shared validation tooling. Discovers a repository, invokes the specialized engines (schema, dependency-cruiser, ESLint, Semgrep, adapter-peers), and returns a normalized result.
|
|
6
|
+
|
|
7
|
+
This package is intentionally small. It does **not** implement any rule. Its only responsibilities are:
|
|
8
|
+
|
|
9
|
+
1. Discover repository configuration (`comity.config.json`).
|
|
10
|
+
2. Discover packages.
|
|
11
|
+
3. Invoke specialized engines.
|
|
12
|
+
4. Collect and normalize results.
|
|
13
|
+
5. Present a unified summary.
|
|
14
|
+
|
|
15
|
+
## Scope
|
|
16
|
+
|
|
17
|
+
The engines wrap shared Development-owned tooling:
|
|
18
|
+
|
|
19
|
+
| Engine | Tool | Concern |
|
|
20
|
+
| --------------- | --------------------------------------- | ---------------------------------------------------------- |
|
|
21
|
+
| `schema` | `@comity-dev/schemas` (Ajv) | JSON Schema for package metadata and `comity.config.json`. |
|
|
22
|
+
| `depcruise` | `dependency-cruiser` | Dependency graph layering. |
|
|
23
|
+
| `eslint` | ESLint + `@comity-dev/eslint-plugin` | Source-code lint policy. |
|
|
24
|
+
| `semgrep` | `semgrep` + `@comity-dev/semgrep-rules` | Structural source patterns. |
|
|
25
|
+
| `adapter-peers` | in-process | Adapter peer-dependency contract. |
|
|
26
|
+
|
|
27
|
+
Engines report one of `PASS` / `FAIL` / `NOT-RUN` / `TOOL-UNAVAILABLE` / `CONFIGURATION-ERROR` / `EXECUTION-ERROR` / `NOT-APPLICABLE`. `PASS` is only reported when the engine actually executed.
|
|
28
|
+
|
|
29
|
+
## Ownership
|
|
30
|
+
|
|
31
|
+
Owned by `comity-development`. Consumed by Comity repositories via the `comity-validate` CLI.
|
|
32
|
+
|
|
33
|
+
## Public API
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import {
|
|
37
|
+
runValidation,
|
|
38
|
+
discoverRepository,
|
|
39
|
+
formatSummary,
|
|
40
|
+
formatViolations,
|
|
41
|
+
ExitCode,
|
|
42
|
+
} from "@comity-dev/validate";
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## CLI
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
comity-validate --repo /path/to/repo
|
|
49
|
+
comity-validate --only=schema # one engine only
|
|
50
|
+
comity-validate --only=eslint --verbose
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Exit codes:
|
|
54
|
+
|
|
55
|
+
- `0` — validation passed
|
|
56
|
+
- `1` — validation failed (violations found)
|
|
57
|
+
- `2` — configuration / discovery error
|
|
58
|
+
- `3` — required tool not installed
|
|
59
|
+
|
|
60
|
+
## Negative fixtures
|
|
61
|
+
|
|
62
|
+
The `src/__fixtures__/third-repo/` directory contains a deliberately invalid repository used by the package's own integration tests. It is **not** scanned by the canonical validate run (the engine excludes `__fixtures__/**`); it is asserted against failure by `src/__tests__/integration.test.ts`.
|
|
63
|
+
|
|
64
|
+
## Development
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pnpm build
|
|
68
|
+
pnpm test
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Relationship to Comity Standards
|
|
72
|
+
|
|
73
|
+
- `layering-policy.md`
|
|
74
|
+
- `architecture-validation.md`
|
|
75
|
+
- `ADR-008`, `ADR-026`
|
|
76
|
+
- `adapters.md` §13 (adapter-peer engine)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* comity-validate — thin orchestrator over Comity shared validation tooling.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* comity-validate [options]
|
|
7
|
+
*
|
|
8
|
+
* Options:
|
|
9
|
+
* --help, -h Show this help
|
|
10
|
+
* --verbose, -v Print underlying tool output
|
|
11
|
+
* --repo <path> Repository root (default: cwd)
|
|
12
|
+
* --only <category> Only run a single category
|
|
13
|
+
* (schema | metadata | config | dependencies | eslint | semgrep | adapter-peers)
|
|
14
|
+
* Default: run all categories.
|
|
15
|
+
*
|
|
16
|
+
* Exit codes:
|
|
17
|
+
* 0 validation passed
|
|
18
|
+
* 1 validation failed (violations found)
|
|
19
|
+
* 2 configuration / discovery error
|
|
20
|
+
* 3 required tool not installed
|
|
21
|
+
*
|
|
22
|
+
* The TypeScript source for this binary lives at
|
|
23
|
+
* `src/bin/comity-validate.ts` and is compiled to
|
|
24
|
+
* `dist/bin/comity-validate.js` by `tsc -p tsconfig.json`.
|
|
25
|
+
*/
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* comity-validate — thin orchestrator over Comity shared validation tooling.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* comity-validate [options]
|
|
7
|
+
*
|
|
8
|
+
* Options:
|
|
9
|
+
* --help, -h Show this help
|
|
10
|
+
* --verbose, -v Print underlying tool output
|
|
11
|
+
* --repo <path> Repository root (default: cwd)
|
|
12
|
+
* --only <category> Only run a single category
|
|
13
|
+
* (schema | metadata | config | dependencies | eslint | semgrep | adapter-peers)
|
|
14
|
+
* Default: run all categories.
|
|
15
|
+
*
|
|
16
|
+
* Exit codes:
|
|
17
|
+
* 0 validation passed
|
|
18
|
+
* 1 validation failed (violations found)
|
|
19
|
+
* 2 configuration / discovery error
|
|
20
|
+
* 3 required tool not installed
|
|
21
|
+
*
|
|
22
|
+
* The TypeScript source for this binary lives at
|
|
23
|
+
* `src/bin/comity-validate.ts` and is compiled to
|
|
24
|
+
* `dist/bin/comity-validate.js` by `tsc -p tsconfig.json`.
|
|
25
|
+
*/
|
|
26
|
+
import { resolve } from "node:path";
|
|
27
|
+
import { ExitCode } from "../exit-codes.js";
|
|
28
|
+
import { runValidation } from "../run.js";
|
|
29
|
+
import { formatSummary, formatViolations } from "../format.js";
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const args = {
|
|
32
|
+
root: process.cwd(),
|
|
33
|
+
verbose: false,
|
|
34
|
+
only: null,
|
|
35
|
+
help: false,
|
|
36
|
+
};
|
|
37
|
+
for (let i = 0; i < argv.length; i++) {
|
|
38
|
+
const arg = argv[i];
|
|
39
|
+
if (arg === "--help" || arg === "-h")
|
|
40
|
+
args.help = true;
|
|
41
|
+
else if (arg === "--verbose" || arg === "-v")
|
|
42
|
+
args.verbose = true;
|
|
43
|
+
else if (arg === "--repo")
|
|
44
|
+
args.root = resolve(argv[++i] ?? "");
|
|
45
|
+
else if (arg?.startsWith("--repo="))
|
|
46
|
+
args.root = resolve(arg.slice("--repo=".length));
|
|
47
|
+
else if (arg === "--only")
|
|
48
|
+
args.only = argv[++i] ?? null;
|
|
49
|
+
else if (arg?.startsWith("--only="))
|
|
50
|
+
args.only = arg.slice("--only=".length);
|
|
51
|
+
}
|
|
52
|
+
return args;
|
|
53
|
+
}
|
|
54
|
+
function printHelp() {
|
|
55
|
+
console.log(`comity-validate — thin orchestrator over Comity shared validation tooling
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
comity-validate [options]
|
|
59
|
+
|
|
60
|
+
Options:
|
|
61
|
+
--help, -h Show this help
|
|
62
|
+
--verbose, -v Print underlying tool output
|
|
63
|
+
--repo <path> Repository root (default: cwd)
|
|
64
|
+
--only <category> Only run a single category
|
|
65
|
+
(schema | metadata | config | dependencies | eslint | semgrep | adapter-peers)
|
|
66
|
+
|
|
67
|
+
Exit codes:
|
|
68
|
+
0 validation passed
|
|
69
|
+
1 validation failed
|
|
70
|
+
2 configuration / discovery error
|
|
71
|
+
3 required tool not installed
|
|
72
|
+
`);
|
|
73
|
+
}
|
|
74
|
+
async function main() {
|
|
75
|
+
const args = parseArgs(process.argv.slice(2));
|
|
76
|
+
if (args.help) {
|
|
77
|
+
printHelp();
|
|
78
|
+
process.exit(ExitCode.PASS);
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const result = await runValidation({
|
|
82
|
+
root: args.root,
|
|
83
|
+
verbose: args.verbose,
|
|
84
|
+
only: args.only,
|
|
85
|
+
});
|
|
86
|
+
console.log(formatSummary(result));
|
|
87
|
+
if (!result.passed && args.verbose) {
|
|
88
|
+
console.log("");
|
|
89
|
+
console.log(formatViolations(result.violations));
|
|
90
|
+
}
|
|
91
|
+
process.exit(result.passed ? ExitCode.PASS : ExitCode.FAIL);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
95
|
+
console.error("comity-validate: configuration error:", message);
|
|
96
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
main();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/validate — repository discovery.
|
|
3
|
+
*
|
|
4
|
+
* Walks a repository's `comity.config.json` and the configured package
|
|
5
|
+
* roots to produce a normalized Repository handle that the orchestrator
|
|
6
|
+
* passes to specialized validators.
|
|
7
|
+
*/
|
|
8
|
+
export interface PackageRecord {
|
|
9
|
+
/** The package's `name` field from its `package.json`. */
|
|
10
|
+
name: string;
|
|
11
|
+
/** The absolute path of the package directory. */
|
|
12
|
+
path: string;
|
|
13
|
+
/** The package's `comity.layer` field from its `package.json`, or null if not set. */
|
|
14
|
+
layer: string | null;
|
|
15
|
+
/** The parsed contents of the package's `package.json`. */
|
|
16
|
+
manifest: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
export interface Repository {
|
|
19
|
+
/** The absolute path of the repository root. */
|
|
20
|
+
root: string;
|
|
21
|
+
/** The parsed contents of the repository's `comity.config.json`. */
|
|
22
|
+
repository: {
|
|
23
|
+
name: string;
|
|
24
|
+
type: string;
|
|
25
|
+
};
|
|
26
|
+
/** The configured package roots, relative to the repository root. */
|
|
27
|
+
packageRoots: string[];
|
|
28
|
+
/** The discovered packages in the repository, sorted by name. */
|
|
29
|
+
packages: PackageRecord[];
|
|
30
|
+
/** The parsed contents of the repository's `comity.config.json`. */
|
|
31
|
+
config: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
export interface DiscoverOptions {
|
|
34
|
+
/** The absolute path of the repository root. */
|
|
35
|
+
root: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Discover a repository: load comity.config.json, walk the configured
|
|
39
|
+
* package roots, and produce a normalized Repository handle.
|
|
40
|
+
*/
|
|
41
|
+
export declare function discoverRepository({ root, }: DiscoverOptions): Promise<Repository>;
|
package/dist/discover.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/validate — repository discovery.
|
|
3
|
+
*
|
|
4
|
+
* Walks a repository's `comity.config.json` and the configured package
|
|
5
|
+
* roots to produce a normalized Repository handle that the orchestrator
|
|
6
|
+
* passes to specialized validators.
|
|
7
|
+
*/
|
|
8
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
/**
|
|
11
|
+
* Discover a repository: load comity.config.json, walk the configured
|
|
12
|
+
* package roots, and produce a normalized Repository handle.
|
|
13
|
+
*/
|
|
14
|
+
export async function discoverRepository({ root, }) {
|
|
15
|
+
const absRoot = resolve(root);
|
|
16
|
+
const configPath = join(absRoot, "comity.config.json");
|
|
17
|
+
let config = null;
|
|
18
|
+
try {
|
|
19
|
+
const text = await readFile(configPath, "utf8");
|
|
20
|
+
config = JSON.parse(text);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
config = {
|
|
24
|
+
repository: { name: "unknown", type: "third-party" },
|
|
25
|
+
packages: { roots: ["packages"] },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const cfg = config;
|
|
29
|
+
const roots = cfg.packages?.roots ?? ["packages"];
|
|
30
|
+
const packages = [];
|
|
31
|
+
for (const rel of roots) {
|
|
32
|
+
const dir = join(absRoot, rel);
|
|
33
|
+
try {
|
|
34
|
+
await stat(dir);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (!entry.isDirectory())
|
|
42
|
+
continue;
|
|
43
|
+
const pkgDir = join(dir, entry.name);
|
|
44
|
+
const manifestPath = join(pkgDir, "package.json");
|
|
45
|
+
let manifest = null;
|
|
46
|
+
try {
|
|
47
|
+
const text = await readFile(manifestPath, "utf8");
|
|
48
|
+
manifest = JSON.parse(text);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!manifest?.["name"] ||
|
|
54
|
+
(!String(manifest["name"]).startsWith("@comity/") &&
|
|
55
|
+
!String(manifest["name"]).startsWith("@comity-dev/")))
|
|
56
|
+
continue;
|
|
57
|
+
const m = manifest;
|
|
58
|
+
packages.push({
|
|
59
|
+
name: m.name,
|
|
60
|
+
path: pkgDir,
|
|
61
|
+
layer: m.comity?.layer ?? null,
|
|
62
|
+
manifest,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
67
|
+
return {
|
|
68
|
+
root: absRoot,
|
|
69
|
+
repository: cfg.repository ?? { name: "unknown", type: "third-party" },
|
|
70
|
+
packageRoots: roots,
|
|
71
|
+
packages,
|
|
72
|
+
config: config ?? {},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const KERNEL_PACKAGES = new Set([
|
|
2
|
+
"@comity/primitives",
|
|
3
|
+
"@comity/kernel",
|
|
4
|
+
"@comity/composition",
|
|
5
|
+
]);
|
|
6
|
+
const SOURCE = "adapters.md §13 / architecture-validation.md §7.4 (docs/standards/)";
|
|
7
|
+
export function runAdapterPeers(repo) {
|
|
8
|
+
const start = Date.now();
|
|
9
|
+
const findings = [];
|
|
10
|
+
for (const pkg of repo.packages) {
|
|
11
|
+
if (pkg.layer !== "technology-adapter")
|
|
12
|
+
continue;
|
|
13
|
+
const m = pkg.manifest;
|
|
14
|
+
const implementsMeta = m.comity?.implements;
|
|
15
|
+
// Validate that the technology-adapter declares its implemented Core Module in comity.implements
|
|
16
|
+
if (typeof implementsMeta !== "string" || !implementsMeta.trim()) {
|
|
17
|
+
findings.push({
|
|
18
|
+
tool: "schema",
|
|
19
|
+
rule: "ARCH-ADAPTER-PEER-005",
|
|
20
|
+
message: `Technology Adapter "${pkg.name}" cannot determine implemented Core Module (missing or invalid comity.implements)`,
|
|
21
|
+
package: pkg.name,
|
|
22
|
+
severity: "error",
|
|
23
|
+
source: SOURCE,
|
|
24
|
+
remediation: 'Ensure package.json has "comity.implements" set to a string Core Module package name.',
|
|
25
|
+
});
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const coreModule = implementsMeta.trim();
|
|
29
|
+
const peerDeps = m.peerDependencies ?? {};
|
|
30
|
+
const deps = m.dependencies ?? {};
|
|
31
|
+
// Validate that the technology-adapter declares its implemented Core Module as a peerDependency
|
|
32
|
+
if (!peerDeps[coreModule]) {
|
|
33
|
+
findings.push({
|
|
34
|
+
tool: "schema",
|
|
35
|
+
rule: "ARCH-ADAPTER-PEER-001",
|
|
36
|
+
message: `Technology Adapter "${pkg.name}" implements "${coreModule}" but does not declare it as a peerDependency`,
|
|
37
|
+
package: pkg.name,
|
|
38
|
+
severity: "error",
|
|
39
|
+
source: SOURCE,
|
|
40
|
+
remediation: `Add "${coreModule}" to peerDependencies.`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Validate that the technology-adapter declares its implemented Core Module as a dependency
|
|
44
|
+
if (!deps[coreModule] && !peerDeps[coreModule]) {
|
|
45
|
+
findings.push({
|
|
46
|
+
tool: "schema",
|
|
47
|
+
rule: "ARCH-ADAPTER-PEER-002",
|
|
48
|
+
message: `Technology Adapter "${pkg.name}" implements "${coreModule}" but does not depend on it at all`,
|
|
49
|
+
package: pkg.name,
|
|
50
|
+
severity: "error",
|
|
51
|
+
source: SOURCE,
|
|
52
|
+
remediation: `Add "${coreModule}" to dependencies and peerDependencies.`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Validate that the technology-adapter does not declare peerDependencies on disallowed @comity/* packages
|
|
56
|
+
for (const peerDep of Object.keys(peerDeps)) {
|
|
57
|
+
if (!peerDep.startsWith("@comity/"))
|
|
58
|
+
continue;
|
|
59
|
+
const allowed = [coreModule, ...KERNEL_PACKAGES].includes(peerDep);
|
|
60
|
+
if (!allowed) {
|
|
61
|
+
findings.push({
|
|
62
|
+
tool: "schema",
|
|
63
|
+
rule: "ARCH-ADAPTER-PEER-004",
|
|
64
|
+
message: `Technology Adapter "${pkg.name}" declares peerDependency on "${peerDep}" which is not the implemented Core Module or a Kernel package`,
|
|
65
|
+
package: pkg.name,
|
|
66
|
+
severity: "error",
|
|
67
|
+
source: SOURCE,
|
|
68
|
+
remediation: `Remove "${peerDep}" from peerDependencies unless it is the implemented Core Module, a Kernel package, or a required technology dependency.`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
executed: true,
|
|
75
|
+
exitCode: 0,
|
|
76
|
+
passed: findings.length === 0,
|
|
77
|
+
findings,
|
|
78
|
+
duration: Date.now() - start,
|
|
79
|
+
status: findings.length === 0 ? "PASS" : "FAIL",
|
|
80
|
+
tool: "adapter-peers",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { buildDependencyRulesForRoot, classifyByLayer, } from "@comity-dev/dependency-rules";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
export function runDepcruise(repo, binDir) {
|
|
6
|
+
const start = Date.now();
|
|
7
|
+
const classification = classifyByLayer(repo.packages.map((p) => ({ name: p.name, layer: p.layer })));
|
|
8
|
+
const depConfig = buildDependencyRulesForRoot(classification, repo.root);
|
|
9
|
+
// Write the generated config to an OS temp directory so the consumer
|
|
10
|
+
// repository is never mutated by the validator. The temp directory is
|
|
11
|
+
// unique per run and cleaned up after the engine returns.
|
|
12
|
+
const tmpDir = mkdtempSync("/tmp/comity-validate-depcruise-");
|
|
13
|
+
const configPath = `${tmpDir}/depcruise.json`;
|
|
14
|
+
writeFileSync(configPath, JSON.stringify(depConfig, null, 2));
|
|
15
|
+
const bin = resolve(binDir, "depcruise");
|
|
16
|
+
if (!existsSync(bin)) {
|
|
17
|
+
return {
|
|
18
|
+
executed: false,
|
|
19
|
+
exitCode: null,
|
|
20
|
+
passed: false,
|
|
21
|
+
findings: [],
|
|
22
|
+
duration: Date.now() - start,
|
|
23
|
+
status: "TOOL-UNAVAILABLE",
|
|
24
|
+
tool: "dependency-cruiser",
|
|
25
|
+
reason: `dependency-cruiser binary not found at ${bin}`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// Build target paths from discovered package directories
|
|
29
|
+
// Use the repository's actual package directories, not a hardcoded "packages"
|
|
30
|
+
const packageDirs = repo.packages.map((p) => resolve(repo.root, p.path));
|
|
31
|
+
const targets = packageDirs.length > 0 ? packageDirs : [resolve(repo.root, "packages")];
|
|
32
|
+
// Run depcruise from the validate package's directory so it can resolve
|
|
33
|
+
// the shared @comity-dev/dependency-rules via workspace node_modules.
|
|
34
|
+
// The tsConfig.fileName path is resolved relative to this CWD; the
|
|
35
|
+
// generated config uses an absolute path so this is robust.
|
|
36
|
+
const engineCwd = resolve(binDir, "..", "..");
|
|
37
|
+
const proc = spawnSync(bin, ["--config", configPath, "--output-type", "json", ...targets], {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
40
|
+
cwd: engineCwd,
|
|
41
|
+
env: {
|
|
42
|
+
...process.env,
|
|
43
|
+
TSCONFIG_PATH: resolve(repo.root, "tsconfig.json"),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
const findings = parseDepcruiseOutput(proc.stdout, proc.stderr);
|
|
47
|
+
// Clean up the temp config directory.
|
|
48
|
+
try {
|
|
49
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// ignore
|
|
53
|
+
}
|
|
54
|
+
// If depcruise exited non-zero but produced no parseable JSON output,
|
|
55
|
+
// that is an EXECUTION-ERROR, not a policy violation. Surface stderr
|
|
56
|
+
// so the operator can diagnose, but mark status accordingly.
|
|
57
|
+
if (proc.status !== 0 && findings.length === 0) {
|
|
58
|
+
const stderrHead = (proc.stderr ?? "").split("\n")[0] ?? "";
|
|
59
|
+
findings.push({
|
|
60
|
+
tool: "dependency-cruiser",
|
|
61
|
+
rule: "DEP-CRASH",
|
|
62
|
+
message: `dependency-cruiser exited with code ${proc.status}; stderr: ${stderrHead}`,
|
|
63
|
+
severity: "error",
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
executed: true,
|
|
67
|
+
exitCode: proc.status,
|
|
68
|
+
passed: false,
|
|
69
|
+
findings,
|
|
70
|
+
duration: Date.now() - start,
|
|
71
|
+
status: "EXECUTION-ERROR",
|
|
72
|
+
tool: "dependency-cruiser",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
executed: true,
|
|
77
|
+
exitCode: proc.status,
|
|
78
|
+
passed: proc.status === 0 && findings.length === 0,
|
|
79
|
+
findings,
|
|
80
|
+
duration: Date.now() - start,
|
|
81
|
+
status: proc.status === 0 ? "PASS" : "FAIL",
|
|
82
|
+
tool: "dependency-cruiser",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function parseDepcruiseOutput(stdout, stderr) {
|
|
86
|
+
const out = stdout ?? stderr ?? "";
|
|
87
|
+
if (!out.trim())
|
|
88
|
+
return [];
|
|
89
|
+
let parsed = null;
|
|
90
|
+
try {
|
|
91
|
+
parsed = JSON.parse(out);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const violations = parsed?.violations ?? [];
|
|
97
|
+
return violations.map((v) => {
|
|
98
|
+
const from = v["from"];
|
|
99
|
+
const rule = v["rule"];
|
|
100
|
+
return {
|
|
101
|
+
tool: "dependency-cruiser",
|
|
102
|
+
rule: `DEP-${rule?.name ?? "UNKNOWN"}`,
|
|
103
|
+
message: rule?.comment ?? "Dependency rule violation",
|
|
104
|
+
file: from?.file,
|
|
105
|
+
line: from?.line,
|
|
106
|
+
severity: "error",
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|