@comity-dev/validate 0.2.3 → 0.4.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/README.md +23 -4
- package/dist/bin/comity-validate.d.ts +1 -1
- package/dist/bin/comity-validate.js +9 -4
- package/dist/discover.d.ts +28 -0
- package/dist/discover.js +127 -1
- package/dist/engines/declaration-dependencies.d.ts +29 -0
- package/dist/engines/declaration-dependencies.js +439 -0
- package/dist/engines/packed-surface.d.ts +22 -0
- package/dist/engines/packed-surface.js +654 -0
- package/dist/exit-codes.d.ts +1 -1
- package/dist/exit-codes.js +1 -1
- package/dist/format.d.ts +4 -0
- package/dist/format.js +5 -0
- package/dist/run.d.ts +4 -0
- package/dist/run.js +32 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
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.
|
|
5
|
+
Thin orchestrator over Comity shared validation tooling. Discovers a repository, invokes the specialized engines (schema, dependency-cruiser, ESLint, Semgrep, adapter-peers, declarations), and returns a normalized result.
|
|
6
6
|
|
|
7
|
-
This package is intentionally small. It does **not** implement
|
|
7
|
+
This package is intentionally small. It does **not** implement source-code rules; the in-process declaration engine
|
|
8
|
+
implements the rules in ADR-035. Its only responsibilities are:
|
|
8
9
|
|
|
9
10
|
1. Discover repository configuration (`comity.config.json`).
|
|
10
11
|
2. Discover packages.
|
|
@@ -23,6 +24,8 @@ The engines wrap shared Development-owned tooling:
|
|
|
23
24
|
| `eslint` | ESLint + `@comity-dev/eslint-plugin` | Source-code lint policy. |
|
|
24
25
|
| `semgrep` | `semgrep` + `@comity-dev/semgrep-rules` | Structural source patterns. |
|
|
25
26
|
| `adapter-peers` | in-process | Adapter peer-dependency contract. |
|
|
27
|
+
| `declarations` | in-process + TypeScript | Published declaration dependency contract (ADR-035). |
|
|
28
|
+
| `packed-surface` | in-process + `pnpm pack` + system `tar` | Packed tarball surface: rules `ARCH-PACK-001…006` (ADR-035 stage 2). |
|
|
26
29
|
|
|
27
30
|
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
31
|
|
|
@@ -47,7 +50,7 @@ import {
|
|
|
47
50
|
```bash
|
|
48
51
|
comity-validate --repo /path/to/repo
|
|
49
52
|
comity-validate --only=schema # one engine only
|
|
50
|
-
comity-validate --only=
|
|
53
|
+
comity-validate --only=declarations
|
|
51
54
|
```
|
|
52
55
|
|
|
53
56
|
`--only <category>` runs a single engine. Policies owned by another engine are
|
|
@@ -55,11 +58,27 @@ not evaluated: `--only semgrep` does not check ESLint-owned policy, including
|
|
|
55
58
|
the required `@internal` marker rule (`@comity-dev/require-internal-marker`).
|
|
56
59
|
Use the default run (no `--only`) for full policy coverage.
|
|
57
60
|
|
|
61
|
+
### `packed-surface` (ADR-035 stage 2)
|
|
62
|
+
|
|
63
|
+
`--only=packed-surface` packs every publishable package with `pnpm pack`, reads the resulting tarball (file list,
|
|
64
|
+
contents and packed `package.json`) with the system `tar`, and applies `ARCH-PACK-001…006`. It never reads the
|
|
65
|
+
workspace `dist`, so it observes the effect of the `files` field, `exports` and `workspace:` rewriting — which stage 1
|
|
66
|
+
cannot see.
|
|
67
|
+
|
|
68
|
+
It is deliberately **not part of the default run**: it packs every package (slower) and it runs in the producer
|
|
69
|
+
repository after `pnpm build` and before publication. Both release workflows run it after `Build` and before the
|
|
70
|
+
Changesets `Release` step.
|
|
71
|
+
|
|
72
|
+
**Enforcement is gated.** The release-workflow step blocks publication only when the repository variable
|
|
73
|
+
`COMITY_PACKED_SURFACE_ENFORCEMENT` is `true`. It must stay unset until **WP-A1** (Community `workspace:*` migration),
|
|
74
|
+
**WP-A4** (Development `@comity-dev/*` migration) and **WP-B** formal ratification are complete, because `pnpm pack`
|
|
75
|
+
rewrites `workspace:*` to an exact pin and `ARCH-PACK-005` reports those pins.
|
|
76
|
+
|
|
58
77
|
Exit codes:
|
|
59
78
|
|
|
60
79
|
- `0` — validation passed
|
|
61
80
|
- `1` — validation failed (violations found)
|
|
62
|
-
- `2` — configuration / discovery error
|
|
81
|
+
- `2` — configuration / discovery error (may co-occur with findings)
|
|
63
82
|
- `3` — required tool not installed
|
|
64
83
|
|
|
65
84
|
## Environment
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* --verbose, -v Print underlying tool output
|
|
11
11
|
* --repo <path> Repository root (default: cwd)
|
|
12
12
|
* --only <category> Only run a single category
|
|
13
|
-
* (schema | metadata | config | dependencies | eslint | semgrep | adapter-peers)
|
|
13
|
+
* (schema | metadata | config | dependencies | eslint | semgrep | adapter-peers | declarations | declaration-dependencies | packed-surface)
|
|
14
14
|
* Default: run all categories.
|
|
15
15
|
*
|
|
16
16
|
* Exit codes:
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* --verbose, -v Print underlying tool output
|
|
11
11
|
* --repo <path> Repository root (default: cwd)
|
|
12
12
|
* --only <category> Only run a single category
|
|
13
|
-
* (schema | metadata | config | dependencies | eslint | semgrep | adapter-peers)
|
|
13
|
+
* (schema | metadata | config | dependencies | eslint | semgrep | adapter-peers | declarations | declaration-dependencies | packed-surface)
|
|
14
14
|
* Default: run all categories.
|
|
15
15
|
*
|
|
16
16
|
* Exit codes:
|
|
@@ -62,7 +62,7 @@ Options:
|
|
|
62
62
|
--verbose, -v Print underlying tool output
|
|
63
63
|
--repo <path> Repository root (default: cwd)
|
|
64
64
|
--only <category> Only run a single category
|
|
65
|
-
(schema | metadata | config | dependencies | eslint | semgrep | adapter-peers)
|
|
65
|
+
(schema | metadata | config | dependencies | eslint | semgrep | adapter-peers | declarations | declaration-dependencies | packed-surface)
|
|
66
66
|
|
|
67
67
|
Exit codes:
|
|
68
68
|
0 validation passed
|
|
@@ -83,15 +83,20 @@ async function main() {
|
|
|
83
83
|
verbose: args.verbose,
|
|
84
84
|
only: args.only,
|
|
85
85
|
});
|
|
86
|
+
const exitCode = exitCodeForResult(result);
|
|
86
87
|
console.log(formatSummary(result));
|
|
87
|
-
if (
|
|
88
|
+
if (result.violations.length > 0) {
|
|
88
89
|
console.log("");
|
|
89
90
|
console.log(formatViolations(result.violations));
|
|
90
91
|
}
|
|
92
|
+
if (exitCode === ExitCode.CONFIG_ERROR && result.violations.length > 0) {
|
|
93
|
+
console.log("");
|
|
94
|
+
console.log("Configuration error(s) took precedence over validation findings (exit code 2).");
|
|
95
|
+
}
|
|
91
96
|
// A required tool that could not run must never surface as PASS:
|
|
92
97
|
// report it with the documented TOOL_MISSING exit code instead of
|
|
93
98
|
// fabricating a passing result.
|
|
94
|
-
process.exit(
|
|
99
|
+
process.exit(exitCode);
|
|
95
100
|
}
|
|
96
101
|
catch (err) {
|
|
97
102
|
const message = err instanceof Error ? err.message : String(err);
|
package/dist/discover.d.ts
CHANGED
|
@@ -39,3 +39,31 @@ export interface DiscoverOptions {
|
|
|
39
39
|
* package roots, and produce a normalized Repository handle.
|
|
40
40
|
*/
|
|
41
41
|
export declare function discoverRepository({ root, }: DiscoverOptions): Promise<Repository>;
|
|
42
|
+
export interface ExcludedPackage {
|
|
43
|
+
/** The absolute path of the excluded package directory. */
|
|
44
|
+
path: string;
|
|
45
|
+
/** The package's `name` field, when it has a readable string name. */
|
|
46
|
+
name: string | null;
|
|
47
|
+
/** Why this package is not publishable. */
|
|
48
|
+
reason: string;
|
|
49
|
+
}
|
|
50
|
+
export interface PublishableDiscovery {
|
|
51
|
+
/** The configured package roots, relative to the repository root. */
|
|
52
|
+
roots: string[];
|
|
53
|
+
/** Whether the default root was used because no explicit roots were configured. */
|
|
54
|
+
usedDefaultRoots: boolean;
|
|
55
|
+
/** Every discovered package with a readable manifest and a string name, including private packages. */
|
|
56
|
+
allPackages: PackageRecord[];
|
|
57
|
+
/** Packages that participate in stage 1 declaration validation. */
|
|
58
|
+
publishable: PackageRecord[];
|
|
59
|
+
/** Packages excluded from validation, with the reason recorded. */
|
|
60
|
+
excluded: ExcludedPackage[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Discover every package under the configured roots, with no namespace filter.
|
|
64
|
+
*
|
|
65
|
+
* This is the discovery used by the stage 1 and stage 2 engines. It deliberately
|
|
66
|
+
* does not change `discoverRepository`, whose existing engines still use the
|
|
67
|
+
* legacy name-based classification.
|
|
68
|
+
*/
|
|
69
|
+
export declare function discoverPublishablePackages(root: string): PublishableDiscovery;
|
package/dist/discover.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* roots to produce a normalized Repository handle that the orchestrator
|
|
6
6
|
* passes to specialized validators.
|
|
7
7
|
*/
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
8
9
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
9
|
-
import { join, resolve } from "node:path";
|
|
10
|
+
import { join, matchesGlob, relative, resolve } from "node:path";
|
|
10
11
|
/**
|
|
11
12
|
* Discover a repository: load comity.config.json, walk the configured
|
|
12
13
|
* package roots, and produce a normalized Repository handle.
|
|
@@ -72,3 +73,128 @@ export async function discoverRepository({ root, }) {
|
|
|
72
73
|
config: config ?? {},
|
|
73
74
|
};
|
|
74
75
|
}
|
|
76
|
+
function isRecord(value) {
|
|
77
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
78
|
+
}
|
|
79
|
+
function readConfig(root) {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(readFileSync(join(root, "comity.config.json"), "utf8"));
|
|
82
|
+
return isRecord(parsed) ? parsed : null;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function configuredRoots(config) {
|
|
89
|
+
const packages = isRecord(config) && isRecord(config["packages"])
|
|
90
|
+
? config["packages"]
|
|
91
|
+
: null;
|
|
92
|
+
const configured = packages?.["roots"];
|
|
93
|
+
const roots = Array.isArray(configured) &&
|
|
94
|
+
configured.length > 0 &&
|
|
95
|
+
configured.every((root) => typeof root === "string")
|
|
96
|
+
? configured
|
|
97
|
+
: ["packages"];
|
|
98
|
+
const defaulted = !Array.isArray(configured) ||
|
|
99
|
+
configured.length === 0 ||
|
|
100
|
+
!configured.every((root) => typeof root === "string");
|
|
101
|
+
const excludeValue = packages?.["exclude"];
|
|
102
|
+
const exclude = Array.isArray(excludeValue)
|
|
103
|
+
? excludeValue.filter((pattern) => typeof pattern === "string")
|
|
104
|
+
: [];
|
|
105
|
+
return { roots, usedDefaultRoots: defaulted, exclude };
|
|
106
|
+
}
|
|
107
|
+
function isExcluded(relativePath, patterns) {
|
|
108
|
+
return patterns.some((pattern) => matchesGlob(relativePath, pattern));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Discover every package under the configured roots, with no namespace filter.
|
|
112
|
+
*
|
|
113
|
+
* This is the discovery used by the stage 1 and stage 2 engines. It deliberately
|
|
114
|
+
* does not change `discoverRepository`, whose existing engines still use the
|
|
115
|
+
* legacy name-based classification.
|
|
116
|
+
*/
|
|
117
|
+
export function discoverPublishablePackages(root) {
|
|
118
|
+
const absRoot = resolve(root);
|
|
119
|
+
const config = readConfig(absRoot);
|
|
120
|
+
const { roots, usedDefaultRoots, exclude } = configuredRoots(config);
|
|
121
|
+
const allPackages = [];
|
|
122
|
+
const publishable = [];
|
|
123
|
+
const excluded = [];
|
|
124
|
+
for (const relRoot of roots) {
|
|
125
|
+
const dir = join(absRoot, relRoot);
|
|
126
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
127
|
+
continue;
|
|
128
|
+
const entries = readdirSync(dir, { withFileTypes: true })
|
|
129
|
+
.filter((entry) => entry.isDirectory())
|
|
130
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
const pkgDir = join(dir, entry.name);
|
|
133
|
+
const manifestPath = join(pkgDir, "package.json");
|
|
134
|
+
if (!existsSync(manifestPath) || !statSync(manifestPath).isFile())
|
|
135
|
+
continue;
|
|
136
|
+
let manifest;
|
|
137
|
+
try {
|
|
138
|
+
const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
139
|
+
if (!isRecord(parsed)) {
|
|
140
|
+
excluded.push({
|
|
141
|
+
path: pkgDir,
|
|
142
|
+
name: null,
|
|
143
|
+
reason: "package.json is not a JSON object",
|
|
144
|
+
});
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
manifest = parsed;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
excluded.push({
|
|
151
|
+
path: pkgDir,
|
|
152
|
+
name: null,
|
|
153
|
+
reason: "package.json is not readable JSON",
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const name = manifest["name"];
|
|
158
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
159
|
+
excluded.push({
|
|
160
|
+
path: pkgDir,
|
|
161
|
+
name: null,
|
|
162
|
+
reason: "package.json has no string name",
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const relativePath = relative(absRoot, pkgDir).split("\\").join("/");
|
|
167
|
+
if (isExcluded(relativePath, exclude)) {
|
|
168
|
+
excluded.push({
|
|
169
|
+
path: pkgDir,
|
|
170
|
+
name,
|
|
171
|
+
reason: "matched packages.exclude",
|
|
172
|
+
});
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const comity = isRecord(manifest["comity"]) ? manifest["comity"] : null;
|
|
176
|
+
const layer = comity?.["layer"];
|
|
177
|
+
const record = {
|
|
178
|
+
name,
|
|
179
|
+
path: pkgDir,
|
|
180
|
+
layer: typeof layer === "string" ? layer : null,
|
|
181
|
+
manifest,
|
|
182
|
+
};
|
|
183
|
+
allPackages.push(record);
|
|
184
|
+
if (manifest["private"] === true) {
|
|
185
|
+
excluded.push({
|
|
186
|
+
path: pkgDir,
|
|
187
|
+
name,
|
|
188
|
+
reason: "private package",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
publishable.push(record);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
allPackages.sort((a, b) => a.path.localeCompare(b.path));
|
|
197
|
+
publishable.sort((a, b) => a.path.localeCompare(b.path));
|
|
198
|
+
excluded.sort((a, b) => a.path.localeCompare(b.path));
|
|
199
|
+
return { roots, usedDefaultRoots, allPackages, publishable, excluded };
|
|
200
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PackageRecord, Repository } from "../discover.js";
|
|
2
|
+
import type { EngineResult, Finding } from "./types.js";
|
|
3
|
+
interface DependencySets {
|
|
4
|
+
dependencies: Set<string>;
|
|
5
|
+
peers: Set<string>;
|
|
6
|
+
optionalPeers: Set<string>;
|
|
7
|
+
dev: Set<string>;
|
|
8
|
+
optional: Set<string>;
|
|
9
|
+
exports: string[];
|
|
10
|
+
}
|
|
11
|
+
interface ParsedDeclaration {
|
|
12
|
+
relative: string[];
|
|
13
|
+
external: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface QueryContext {
|
|
16
|
+
packageRecord: PackageRecord;
|
|
17
|
+
sets: DependencySets;
|
|
18
|
+
packages: Map<string, PackageRecord>;
|
|
19
|
+
rootReached: boolean;
|
|
20
|
+
entry: string;
|
|
21
|
+
tool?: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
export declare function dependencySets(manifest: Record<string, unknown>): DependencySets;
|
|
24
|
+
export declare function isDeclarationFile(path: string): boolean;
|
|
25
|
+
export declare function parseDeclarationText(file: string, text: string): ParsedDeclaration;
|
|
26
|
+
export declare function classifyExternal(specifier: string, file: string, context: QueryContext): Finding | null;
|
|
27
|
+
export declare function sortFindings(findings: Finding[]): void;
|
|
28
|
+
export declare function runDeclarationDependencies(repo: Repository): EngineResult;
|
|
29
|
+
export {};
|