@comity-dev/validate 0.3.0 → 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 +17 -0
- package/dist/bin/comity-validate.d.ts +1 -1
- package/dist/bin/comity-validate.js +2 -2
- package/dist/engines/declaration-dependencies.d.ts +28 -2
- package/dist/engines/declaration-dependencies.js +9 -7
- package/dist/engines/packed-surface.d.ts +22 -0
- package/dist/engines/packed-surface.js +654 -0
- package/dist/format.d.ts +2 -0
- package/dist/format.js +1 -0
- package/dist/run.d.ts +2 -0
- package/dist/run.js +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ The engines wrap shared Development-owned tooling:
|
|
|
25
25
|
| `semgrep` | `semgrep` + `@comity-dev/semgrep-rules` | Structural source patterns. |
|
|
26
26
|
| `adapter-peers` | in-process | Adapter peer-dependency contract. |
|
|
27
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). |
|
|
28
29
|
|
|
29
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.
|
|
30
31
|
|
|
@@ -57,6 +58,22 @@ not evaluated: `--only semgrep` does not check ESLint-owned policy, including
|
|
|
57
58
|
the required `@internal` marker rule (`@comity-dev/require-internal-marker`).
|
|
58
59
|
Use the default run (no `--only`) for full policy coverage.
|
|
59
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
|
+
|
|
60
77
|
Exit codes:
|
|
61
78
|
|
|
62
79
|
- `0` — validation passed
|
|
@@ -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 | declarations | declaration-dependencies)
|
|
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 | declarations | declaration-dependencies)
|
|
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 | declarations | declaration-dependencies)
|
|
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
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
-
import type { Repository } from "../discover.js";
|
|
2
|
-
import type { EngineResult } from "./types.js";
|
|
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;
|
|
3
28
|
export declare function runDeclarationDependencies(repo: Repository): EngineResult;
|
|
29
|
+
export {};
|
|
@@ -13,7 +13,7 @@ function stringSet(value) {
|
|
|
13
13
|
return new Set();
|
|
14
14
|
return new Set(Object.keys(value));
|
|
15
15
|
}
|
|
16
|
-
function dependencySets(manifest) {
|
|
16
|
+
export function dependencySets(manifest) {
|
|
17
17
|
const peers = stringSet(manifest["peerDependencies"]);
|
|
18
18
|
const meta = isRecord(manifest["peerDependenciesMeta"])
|
|
19
19
|
? manifest["peerDependenciesMeta"]
|
|
@@ -30,7 +30,7 @@ function dependencySets(manifest) {
|
|
|
30
30
|
exports: isRecord(manifest["exports"]) ? Object.keys(manifest["exports"]) : [],
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
-
function isDeclarationFile(path) {
|
|
33
|
+
export function isDeclarationFile(path) {
|
|
34
34
|
return /\.d\.(?:ts|mts|cts)$/.test(path);
|
|
35
35
|
}
|
|
36
36
|
function collectDeclarationTargets(pkg) {
|
|
@@ -86,8 +86,7 @@ function collectDeclarationTargets(pkg) {
|
|
|
86
86
|
}
|
|
87
87
|
return targets;
|
|
88
88
|
}
|
|
89
|
-
function
|
|
90
|
-
const text = readFileSync(file, "utf8");
|
|
89
|
+
export function parseDeclarationText(file, text) {
|
|
91
90
|
const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
92
91
|
const relativeSpecifiers = new Set();
|
|
93
92
|
const externalSpecifiers = new Set();
|
|
@@ -136,6 +135,9 @@ function parseDeclaration(file) {
|
|
|
136
135
|
external: [...externalSpecifiers],
|
|
137
136
|
};
|
|
138
137
|
}
|
|
138
|
+
function parseDeclaration(file) {
|
|
139
|
+
return parseDeclarationText(file, readFileSync(file, "utf8"));
|
|
140
|
+
}
|
|
139
141
|
function resolveRelative(specifier, fromFile) {
|
|
140
142
|
const base = resolve(dirname(fromFile), specifier);
|
|
141
143
|
const candidates = [];
|
|
@@ -189,7 +191,7 @@ function isBuiltin(specifier) {
|
|
|
189
191
|
}
|
|
190
192
|
function finding(context, rule, specifier, file, message, remediation) {
|
|
191
193
|
return {
|
|
192
|
-
tool: "declarations",
|
|
194
|
+
tool: context.tool ?? "declarations",
|
|
193
195
|
rule,
|
|
194
196
|
package: context.packageRecord.name,
|
|
195
197
|
file,
|
|
@@ -200,7 +202,7 @@ function finding(context, rule, specifier, file, message, remediation) {
|
|
|
200
202
|
remediation,
|
|
201
203
|
};
|
|
202
204
|
}
|
|
203
|
-
function classifyExternal(specifier, file, context) {
|
|
205
|
+
export function classifyExternal(specifier, file, context) {
|
|
204
206
|
const owner = packageNameOf(specifier);
|
|
205
207
|
if (isBuiltin(specifier))
|
|
206
208
|
return null;
|
|
@@ -258,7 +260,7 @@ function countDeclarationFiles(directory) {
|
|
|
258
260
|
}
|
|
259
261
|
return count;
|
|
260
262
|
}
|
|
261
|
-
function sortFindings(findings) {
|
|
263
|
+
export function sortFindings(findings) {
|
|
262
264
|
findings.sort((a, b) => {
|
|
263
265
|
return ((a.package ?? "").localeCompare(b.package ?? "") ||
|
|
264
266
|
(a.file ?? "").localeCompare(b.file ?? "") ||
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PackageRecord, Repository } from "../discover.js";
|
|
2
|
+
import type { EngineResult, Finding } from "./types.js";
|
|
3
|
+
interface TarPackage {
|
|
4
|
+
files: string[];
|
|
5
|
+
fileSet: Set<string>;
|
|
6
|
+
read(file: string): string;
|
|
7
|
+
}
|
|
8
|
+
declare function readTarball(tarballPath: string): TarPackage;
|
|
9
|
+
/**
|
|
10
|
+
* Rules `ARCH-PACK-004` (unrewritten `workspace:`/`file:`/`link:` specifiers)
|
|
11
|
+
* and `ARCH-PACK-005` (exact sibling pins without a recorded ADR-033 exception),
|
|
12
|
+
* evaluated against the **packed** manifest.
|
|
13
|
+
*
|
|
14
|
+
* Exported so the `workspace:` branch of `ARCH-PACK-004` has direct test
|
|
15
|
+
* coverage: a real `pnpm pack` always rewrites `workspace:` protocols (and fails
|
|
16
|
+
* outright when it cannot resolve them), so a published tarball carrying a
|
|
17
|
+
* `workspace:` specifier can only come from a broken publish path, never from
|
|
18
|
+
* the packer this engine invokes.
|
|
19
|
+
*/
|
|
20
|
+
export declare function packedDependencySpecifierFindings(pkg: Pick<PackageRecord, "name" | "path">, manifest: Record<string, unknown>, siblingNames: ReadonlySet<string>): Finding[];
|
|
21
|
+
export declare function runPackedSurface(repo: Repository): EngineResult;
|
|
22
|
+
export { readTarball };
|
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, matchesGlob, posix } from "node:path";
|
|
5
|
+
import { classifyExternal, dependencySets, isDeclarationFile, parseDeclarationText, sortFindings, } from "./declaration-dependencies.js";
|
|
6
|
+
import { discoverPublishablePackages } from "../discover.js";
|
|
7
|
+
const SOURCE = "ADR-035";
|
|
8
|
+
const DEPENDENCY_FIELDS = [
|
|
9
|
+
"dependencies",
|
|
10
|
+
"devDependencies",
|
|
11
|
+
"peerDependencies",
|
|
12
|
+
"optionalDependencies",
|
|
13
|
+
];
|
|
14
|
+
const EXACT_VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
15
|
+
const IMPORT_CONDITIONS = new Set(["node", "import", "default"]);
|
|
16
|
+
const REQUIRE_CONDITIONS = new Set(["node", "require", "default"]);
|
|
17
|
+
class ToolUnavailableError extends Error {
|
|
18
|
+
}
|
|
19
|
+
class PackedConfigurationError extends Error {
|
|
20
|
+
}
|
|
21
|
+
function isRecord(value) {
|
|
22
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
function run(command, args, cwd, timeout) {
|
|
25
|
+
const result = spawnSync(command, args, {
|
|
26
|
+
cwd,
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
env: process.env,
|
|
29
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
30
|
+
timeout,
|
|
31
|
+
});
|
|
32
|
+
if (result.error) {
|
|
33
|
+
const code = result.error.code;
|
|
34
|
+
if (code === "ENOENT") {
|
|
35
|
+
throw new ToolUnavailableError(`Required tool "${command}" was not found on PATH`);
|
|
36
|
+
}
|
|
37
|
+
throw new PackedConfigurationError(`Failed to execute "${command} ${args.join(" ")}": ${result.error.message}`);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
status: result.status,
|
|
41
|
+
stdout: result.stdout ?? "",
|
|
42
|
+
stderr: result.stderr ?? "",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function normalizeTarPath(raw) {
|
|
46
|
+
if (raw.startsWith("/") || raw.includes("\0")) {
|
|
47
|
+
throw new PackedConfigurationError(`Unsafe absolute path in tarball: ${raw}`);
|
|
48
|
+
}
|
|
49
|
+
const normalized = posix.normalize(raw).replace(/^\.\//, "");
|
|
50
|
+
if (normalized === ".." ||
|
|
51
|
+
normalized.startsWith("../") ||
|
|
52
|
+
normalized.includes("/../")) {
|
|
53
|
+
throw new PackedConfigurationError(`Unsafe path traversal entry in tarball: ${raw}`);
|
|
54
|
+
}
|
|
55
|
+
if (!normalized.startsWith("package/")) {
|
|
56
|
+
throw new PackedConfigurationError(`Tarball entry is not under the "package/" prefix: ${raw}`);
|
|
57
|
+
}
|
|
58
|
+
return normalized.slice("package/".length);
|
|
59
|
+
}
|
|
60
|
+
function readTarball(tarballPath) {
|
|
61
|
+
const listing = run("tar", ["-tzf", tarballPath], process.cwd(), 60_000);
|
|
62
|
+
if (listing.status !== 0) {
|
|
63
|
+
throw new PackedConfigurationError(`Cannot read tarball ${tarballPath}: ${listing.stderr.trim() || `tar exited ${listing.status}`}`);
|
|
64
|
+
}
|
|
65
|
+
const files = listing.stdout
|
|
66
|
+
.split(/\r?\n/)
|
|
67
|
+
.map((line) => line.trim())
|
|
68
|
+
.filter((line) => line.length > 0)
|
|
69
|
+
.map(normalizeTarPath)
|
|
70
|
+
.filter((file) => file.length > 0 && !file.endsWith("/"))
|
|
71
|
+
.sort();
|
|
72
|
+
const fileSet = new Set(files);
|
|
73
|
+
const cache = new Map();
|
|
74
|
+
return {
|
|
75
|
+
files,
|
|
76
|
+
fileSet,
|
|
77
|
+
read(file) {
|
|
78
|
+
const cached = cache.get(file);
|
|
79
|
+
if (cached !== undefined)
|
|
80
|
+
return cached;
|
|
81
|
+
if (!fileSet.has(file)) {
|
|
82
|
+
throw new PackedConfigurationError(`Tarball entry is missing: package/${file}`);
|
|
83
|
+
}
|
|
84
|
+
const extracted = run("tar", ["-xOf", tarballPath, `package/${file}`], process.cwd(), 60_000);
|
|
85
|
+
if (extracted.status !== 0) {
|
|
86
|
+
throw new PackedConfigurationError(`Cannot read package/${file} from ${tarballPath}: ${extracted.stderr.trim() || `tar exited ${extracted.status}`}`);
|
|
87
|
+
}
|
|
88
|
+
cache.set(file, extracted.stdout);
|
|
89
|
+
return extracted.stdout;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function readPackedManifest(tar) {
|
|
94
|
+
const parsed = JSON.parse(tar.read("package.json"));
|
|
95
|
+
if (!isRecord(parsed)) {
|
|
96
|
+
throw new PackedConfigurationError("Packed package.json is not a JSON object");
|
|
97
|
+
}
|
|
98
|
+
return parsed;
|
|
99
|
+
}
|
|
100
|
+
function packPackage(pkg, destination) {
|
|
101
|
+
const result = run("pnpm", ["pack", "--pack-destination", destination, "--silent"], pkg.path, 120_000);
|
|
102
|
+
if (result.status !== 0) {
|
|
103
|
+
throw new PackedConfigurationError(`pnpm pack failed for "${pkg.name}": ${result.stderr.trim() || `pnpm exited ${result.status}`}`);
|
|
104
|
+
}
|
|
105
|
+
const tarballs = readdirSync(destination)
|
|
106
|
+
.filter((file) => file.endsWith(".tgz"))
|
|
107
|
+
.sort();
|
|
108
|
+
if (tarballs.length !== 1) {
|
|
109
|
+
throw new PackedConfigurationError(`pnpm pack for "${pkg.name}" produced ${tarballs.length} tarball(s) in ${destination}`);
|
|
110
|
+
}
|
|
111
|
+
const tarball = tarballs[0];
|
|
112
|
+
if (!tarball) {
|
|
113
|
+
throw new PackedConfigurationError(`pnpm pack for "${pkg.name}" produced no tarball`);
|
|
114
|
+
}
|
|
115
|
+
return join(destination, tarball);
|
|
116
|
+
}
|
|
117
|
+
function normalizeTarget(value) {
|
|
118
|
+
const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
119
|
+
return posix.normalize(normalized);
|
|
120
|
+
}
|
|
121
|
+
function isSafeRelativeTarget(target) {
|
|
122
|
+
return (target.length > 0 &&
|
|
123
|
+
!target.startsWith("/") &&
|
|
124
|
+
target !== ".." &&
|
|
125
|
+
!target.startsWith("../"));
|
|
126
|
+
}
|
|
127
|
+
function addTarget(targets, field, value, entry) {
|
|
128
|
+
const target = normalizeTarget(value);
|
|
129
|
+
if (!isSafeRelativeTarget(target))
|
|
130
|
+
return;
|
|
131
|
+
targets.push({ field, target, entry });
|
|
132
|
+
}
|
|
133
|
+
function walkExportValue(targets, field, entry, value) {
|
|
134
|
+
if (typeof value === "string") {
|
|
135
|
+
addTarget(targets, field, value, entry);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(value)) {
|
|
139
|
+
for (const item of value)
|
|
140
|
+
walkExportValue(targets, field, entry, item);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (!isRecord(value))
|
|
144
|
+
return;
|
|
145
|
+
for (const [key, child] of Object.entries(value)) {
|
|
146
|
+
walkExportValue(targets, field, key.startsWith(".") ? key : entry, child);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function collectManifestTargets(manifest) {
|
|
150
|
+
const targets = [];
|
|
151
|
+
const exportsField = manifest["exports"];
|
|
152
|
+
if (typeof exportsField === "string") {
|
|
153
|
+
addTarget(targets, "exports", exportsField, ".");
|
|
154
|
+
}
|
|
155
|
+
else if (isRecord(exportsField)) {
|
|
156
|
+
const hasSubpaths = Object.keys(exportsField).some((key) => key.startsWith("."));
|
|
157
|
+
if (hasSubpaths) {
|
|
158
|
+
for (const [key, value] of Object.entries(exportsField)) {
|
|
159
|
+
if (key.startsWith(".")) {
|
|
160
|
+
walkExportValue(targets, `exports["${key}"]`, key, value);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
walkExportValue(targets, "exports", ".", exportsField);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const field of ["main", "module", "types"]) {
|
|
169
|
+
const value = manifest[field];
|
|
170
|
+
if (typeof value === "string")
|
|
171
|
+
addTarget(targets, field, value, ".");
|
|
172
|
+
}
|
|
173
|
+
const bin = manifest["bin"];
|
|
174
|
+
if (typeof bin === "string") {
|
|
175
|
+
addTarget(targets, "bin", bin, null);
|
|
176
|
+
}
|
|
177
|
+
else if (isRecord(bin)) {
|
|
178
|
+
for (const value of Object.values(bin)) {
|
|
179
|
+
if (typeof value === "string")
|
|
180
|
+
addTarget(targets, "bin", value, null);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const typesVersions = manifest["typesVersions"];
|
|
184
|
+
if (isRecord(typesVersions)) {
|
|
185
|
+
for (const mapping of Object.values(typesVersions)) {
|
|
186
|
+
if (!isRecord(mapping))
|
|
187
|
+
continue;
|
|
188
|
+
for (const [subpath, value] of Object.entries(mapping)) {
|
|
189
|
+
const entry = subpath.startsWith(".")
|
|
190
|
+
? subpath
|
|
191
|
+
: subpath === "*"
|
|
192
|
+
? "*"
|
|
193
|
+
: `./${subpath}`;
|
|
194
|
+
if (typeof value === "string") {
|
|
195
|
+
addTarget(targets, "typesVersions", value, entry);
|
|
196
|
+
}
|
|
197
|
+
else if (Array.isArray(value)) {
|
|
198
|
+
for (const item of value) {
|
|
199
|
+
if (typeof item === "string") {
|
|
200
|
+
addTarget(targets, "typesVersions", item, entry);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return targets;
|
|
208
|
+
}
|
|
209
|
+
function targetFiles(tar, target) {
|
|
210
|
+
if (target.includes("*")) {
|
|
211
|
+
return tar.files.filter((file) => matchesGlob(file, target));
|
|
212
|
+
}
|
|
213
|
+
return tar.fileSet.has(target) ? [target] : [];
|
|
214
|
+
}
|
|
215
|
+
function relativeCandidates(specifier, fromFile) {
|
|
216
|
+
const base = posix.normalize(posix.join(posix.dirname(fromFile), specifier));
|
|
217
|
+
const candidates = [];
|
|
218
|
+
if (base.endsWith(".js"))
|
|
219
|
+
candidates.push(`${base.slice(0, -3)}.d.ts`);
|
|
220
|
+
else if (base.endsWith(".mjs"))
|
|
221
|
+
candidates.push(`${base.slice(0, -4)}.d.mts`);
|
|
222
|
+
else if (base.endsWith(".cjs"))
|
|
223
|
+
candidates.push(`${base.slice(0, -4)}.d.cts`);
|
|
224
|
+
else if (isDeclarationFile(base))
|
|
225
|
+
candidates.push(base);
|
|
226
|
+
candidates.push(`${base}.d.ts`, posix.join(base, "index.d.ts"));
|
|
227
|
+
return candidates;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A recorded ARCH-PACK-005 exception (ADR-033 §4): a line in the package's
|
|
231
|
+
* `docs/conventions.md` that names the sibling and records the exception or
|
|
232
|
+
* its justification. ADR-033 does not prescribe a machine format, so the check
|
|
233
|
+
* stays deliberately permissive: the sibling must be named on the same line as
|
|
234
|
+
* the exception or justification wording.
|
|
235
|
+
*/
|
|
236
|
+
function exactPinException(packagePath, sibling) {
|
|
237
|
+
const convention = join(packagePath, "docs", "conventions.md");
|
|
238
|
+
if (!existsSync(convention))
|
|
239
|
+
return false;
|
|
240
|
+
try {
|
|
241
|
+
return readFileSync(convention, "utf8")
|
|
242
|
+
.split(/\r?\n/)
|
|
243
|
+
.some((line) => line.includes(sibling) &&
|
|
244
|
+
/exception|justification/i.test(line) &&
|
|
245
|
+
!/\bno\s+(?:recorded\s+)?exceptions?\b/i.test(line));
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function finding(pkg, rule, message, remediation, options = {}) {
|
|
252
|
+
return {
|
|
253
|
+
tool: "packed-surface",
|
|
254
|
+
rule,
|
|
255
|
+
package: pkg,
|
|
256
|
+
file: options.file,
|
|
257
|
+
keyword: options.keyword,
|
|
258
|
+
severity: "error",
|
|
259
|
+
source: SOURCE,
|
|
260
|
+
message,
|
|
261
|
+
remediation,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function dedupeFindings(findings) {
|
|
265
|
+
const seen = new Set();
|
|
266
|
+
const unique = [];
|
|
267
|
+
for (const item of findings) {
|
|
268
|
+
const key = [
|
|
269
|
+
item.package ?? "",
|
|
270
|
+
item.rule,
|
|
271
|
+
item.file ?? "",
|
|
272
|
+
item.keyword ?? "",
|
|
273
|
+
item.message,
|
|
274
|
+
].join("\0");
|
|
275
|
+
if (seen.has(key))
|
|
276
|
+
continue;
|
|
277
|
+
seen.add(key);
|
|
278
|
+
unique.push(item);
|
|
279
|
+
}
|
|
280
|
+
return unique;
|
|
281
|
+
}
|
|
282
|
+
function resolveRuntimeTarget(value, conditions) {
|
|
283
|
+
if (typeof value === "string")
|
|
284
|
+
return value;
|
|
285
|
+
if (Array.isArray(value)) {
|
|
286
|
+
for (const item of value) {
|
|
287
|
+
const resolved = resolveRuntimeTarget(item, conditions);
|
|
288
|
+
if (resolved)
|
|
289
|
+
return resolved;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
if (!isRecord(value))
|
|
294
|
+
return null;
|
|
295
|
+
for (const [key, child] of Object.entries(value)) {
|
|
296
|
+
if (key === "types")
|
|
297
|
+
continue;
|
|
298
|
+
if (key !== "default" && !conditions.has(key))
|
|
299
|
+
continue;
|
|
300
|
+
const resolved = resolveRuntimeTarget(child, conditions);
|
|
301
|
+
if (resolved)
|
|
302
|
+
return resolved;
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
function runtimeExports(manifest) {
|
|
307
|
+
const exportsField = manifest["exports"];
|
|
308
|
+
if (typeof exportsField === "string") {
|
|
309
|
+
return [{ key: ".", value: exportsField }];
|
|
310
|
+
}
|
|
311
|
+
if (!isRecord(exportsField))
|
|
312
|
+
return [];
|
|
313
|
+
const hasSubpaths = Object.keys(exportsField).some((key) => key.startsWith("."));
|
|
314
|
+
if (!hasSubpaths)
|
|
315
|
+
return [{ key: ".", value: exportsField }];
|
|
316
|
+
return Object.entries(exportsField)
|
|
317
|
+
.filter(([key]) => key.startsWith("."))
|
|
318
|
+
.map(([key, value]) => ({ key, value }));
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Rules `ARCH-PACK-004` (unrewritten `workspace:`/`file:`/`link:` specifiers)
|
|
322
|
+
* and `ARCH-PACK-005` (exact sibling pins without a recorded ADR-033 exception),
|
|
323
|
+
* evaluated against the **packed** manifest.
|
|
324
|
+
*
|
|
325
|
+
* Exported so the `workspace:` branch of `ARCH-PACK-004` has direct test
|
|
326
|
+
* coverage: a real `pnpm pack` always rewrites `workspace:` protocols (and fails
|
|
327
|
+
* outright when it cannot resolve them), so a published tarball carrying a
|
|
328
|
+
* `workspace:` specifier can only come from a broken publish path, never from
|
|
329
|
+
* the packer this engine invokes.
|
|
330
|
+
*/
|
|
331
|
+
export function packedDependencySpecifierFindings(pkg, manifest, siblingNames) {
|
|
332
|
+
const findings = [];
|
|
333
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
334
|
+
const values = manifest[field];
|
|
335
|
+
if (!isRecord(values))
|
|
336
|
+
continue;
|
|
337
|
+
for (const [name, value] of Object.entries(values)) {
|
|
338
|
+
if (typeof value !== "string")
|
|
339
|
+
continue;
|
|
340
|
+
if (!/^(?:workspace:|file:|link:)/.test(value))
|
|
341
|
+
continue;
|
|
342
|
+
findings.push(finding(pkg.name, "ARCH-PACK-004", `Package "${pkg.name}" ships "${name}": "${value}" in ${field}; published manifests must not contain workspace:, file: or link: specifiers`, "Rewrite the dependency as a published semver range.", { file: "package.json", keyword: name }));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
for (const field of ["dependencies", "peerDependencies"]) {
|
|
346
|
+
const values = manifest[field];
|
|
347
|
+
if (!isRecord(values))
|
|
348
|
+
continue;
|
|
349
|
+
for (const [name, value] of Object.entries(values)) {
|
|
350
|
+
if (typeof value !== "string" ||
|
|
351
|
+
!siblingNames.has(name) ||
|
|
352
|
+
!EXACT_VERSION.test(value) ||
|
|
353
|
+
exactPinException(pkg.path, name)) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
findings.push(finding(pkg.name, "ARCH-PACK-005", `Package "${pkg.name}" pins sibling "${name}" to exact version "${value}" in ${field} without a recorded exception in docs/conventions.md`, `Use a caret range (workspace:^ publishes as ^${value}) or record the exception in docs/conventions.md.`, { file: "package.json", keyword: name }));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return findings;
|
|
360
|
+
}
|
|
361
|
+
function checkPackedPackage(packed, packages, siblingNames) {
|
|
362
|
+
const findings = [];
|
|
363
|
+
const pkg = packed.packageRecord;
|
|
364
|
+
const manifest = packed.manifest;
|
|
365
|
+
const targets = collectManifestTargets(manifest);
|
|
366
|
+
const entries = new Map();
|
|
367
|
+
let externalReferences = 0;
|
|
368
|
+
for (const target of targets) {
|
|
369
|
+
const matches = targetFiles(packed.tar, target.target);
|
|
370
|
+
if (matches.length === 0) {
|
|
371
|
+
findings.push(finding(pkg.name, "ARCH-PACK-001", `Package "${pkg.name}" declares ${target.field} target "${target.target}" but the tarball does not contain it`, "Include the target in the package files field, or correct the manifest target.", { file: target.target, keyword: target.target }));
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (target.entry === null)
|
|
375
|
+
continue;
|
|
376
|
+
for (const file of matches) {
|
|
377
|
+
if (!isDeclarationFile(file))
|
|
378
|
+
continue;
|
|
379
|
+
const keys = entries.get(file) ?? new Set();
|
|
380
|
+
keys.add(target.entry);
|
|
381
|
+
entries.set(file, keys);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const parsed = new Map();
|
|
385
|
+
const entryKeysByFile = new Map();
|
|
386
|
+
const unresolved = new Map();
|
|
387
|
+
const queue = [];
|
|
388
|
+
for (const [file, keys] of [...entries.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
389
|
+
for (const entry of [...keys].sort())
|
|
390
|
+
queue.push({ file, entry });
|
|
391
|
+
}
|
|
392
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
393
|
+
const item = queue[index];
|
|
394
|
+
if (!item)
|
|
395
|
+
continue;
|
|
396
|
+
const keys = entryKeysByFile.get(item.file) ?? new Set();
|
|
397
|
+
if (keys.has(item.entry))
|
|
398
|
+
continue;
|
|
399
|
+
keys.add(item.entry);
|
|
400
|
+
entryKeysByFile.set(item.file, keys);
|
|
401
|
+
let declaration = parsed.get(item.file);
|
|
402
|
+
if (!declaration) {
|
|
403
|
+
declaration = parseDeclarationText(item.file, packed.tar.read(item.file));
|
|
404
|
+
parsed.set(item.file, declaration);
|
|
405
|
+
}
|
|
406
|
+
for (const specifier of declaration.relative) {
|
|
407
|
+
const resolved = relativeCandidates(specifier, item.file).find((file) => packed.tar.fileSet.has(file));
|
|
408
|
+
if (resolved) {
|
|
409
|
+
queue.push({ file: resolved, entry: item.entry });
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
const key = `${item.file}\0${specifier}`;
|
|
413
|
+
const entryKeys = unresolved.get(key) ?? new Set();
|
|
414
|
+
entryKeys.add(item.entry);
|
|
415
|
+
unresolved.set(key, entryKeys);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
for (const [key, entryKeys] of unresolved) {
|
|
420
|
+
const [file, specifier] = key.split("\0");
|
|
421
|
+
if (!file || !specifier)
|
|
422
|
+
continue;
|
|
423
|
+
const entry = [...entryKeys].sort()[0] ?? "?";
|
|
424
|
+
findings.push(finding(pkg.name, "ARCH-PACK-002", `Package "${pkg.name}" publishes entry "${entry}" whose declaration "${file}" references "${specifier}", but the resolved declaration is absent from the tarball`, "Include the emitted declaration in the package files field, or fix the relative reference.", { file, keyword: specifier }));
|
|
425
|
+
}
|
|
426
|
+
if (entryKeysByFile.size === 0 && targets.some((target) => target.entry !== null)) {
|
|
427
|
+
findings.push(finding(pkg.name, "ARCH-PACK-002", `Package "${pkg.name}" declares declaration entry points but the tarball contains no reachable declaration file`, "Include the emitted declarations in the package files field."));
|
|
428
|
+
}
|
|
429
|
+
const sets = dependencySets(manifest);
|
|
430
|
+
const packedPackages = new Map(packages);
|
|
431
|
+
for (const [file, entryKeys] of [...entryKeysByFile.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
432
|
+
const declaration = parsed.get(file);
|
|
433
|
+
if (!declaration)
|
|
434
|
+
continue;
|
|
435
|
+
const entry = [...entryKeys].sort()[0] ?? "?";
|
|
436
|
+
const context = {
|
|
437
|
+
packageRecord: pkg,
|
|
438
|
+
sets,
|
|
439
|
+
packages: packedPackages,
|
|
440
|
+
rootReached: entryKeys.has("."),
|
|
441
|
+
entry,
|
|
442
|
+
tool: "packed-surface",
|
|
443
|
+
};
|
|
444
|
+
for (const specifier of [...declaration.external].sort()) {
|
|
445
|
+
externalReferences += 1;
|
|
446
|
+
const result = classifyExternal(specifier, file, context);
|
|
447
|
+
if (result)
|
|
448
|
+
findings.push(result);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
findings.push(...packedDependencySpecifierFindings({ name: pkg.name, path: packed.workspacePath }, manifest, siblingNames));
|
|
452
|
+
const cjs = packed.tar.files.filter((file) => file === "dist/cjs" || file.startsWith("dist/cjs/"));
|
|
453
|
+
if (cjs.length > 0) {
|
|
454
|
+
findings.push(finding(pkg.name, "ARCH-PACK-006", `Package "${pkg.name}" ships ${cjs.length} file(s) under dist/cjs/; Comity publishes a single ESM distribution`, "Remove the CommonJS build from the package outputs.", { file: "dist/cjs", keyword: "dist/cjs" }));
|
|
455
|
+
}
|
|
456
|
+
for (const entry of runtimeExports(manifest)) {
|
|
457
|
+
const importTarget = resolveRuntimeTarget(entry.value, IMPORT_CONDITIONS);
|
|
458
|
+
const requireTarget = resolveRuntimeTarget(entry.value, REQUIRE_CONDITIONS);
|
|
459
|
+
if (!importTarget || !requireTarget) {
|
|
460
|
+
findings.push(finding(pkg.name, "ARCH-PACK-006", `Package "${pkg.name}" exports "${entry.key}" but does not resolve for both import and require consumers`, "Add a default (or matching import/require) condition that resolves to the ESM artifact.", { file: "package.json", keyword: entry.key }));
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (importTarget !== requireTarget) {
|
|
464
|
+
findings.push(finding(pkg.name, "ARCH-PACK-006", `Package "${pkg.name}" exports "${entry.key}" with import "${importTarget}" and require "${requireTarget}"; both must resolve to the same ESM artifact`, "Point import and require at the same ESM file.", { file: "package.json", keyword: entry.key }));
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (/\.(?:cjs|cts)$/.test(importTarget)) {
|
|
468
|
+
findings.push(finding(pkg.name, "ARCH-PACK-006", `Package "${pkg.name}" exports "${entry.key}" to a CommonJS artifact "${importTarget}"`, "Publish the ESM artifact for this export.", { file: importTarget, keyword: entry.key }));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
findings: dedupeFindings(findings),
|
|
473
|
+
declarationFiles: entryKeysByFile.size,
|
|
474
|
+
externalReferences,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
export function runPackedSurface(repo) {
|
|
478
|
+
const start = Date.now();
|
|
479
|
+
const discovery = discoverPublishablePackages(repo.root);
|
|
480
|
+
if (discovery.publishable.length === 0) {
|
|
481
|
+
if (discovery.allPackages.length === 0) {
|
|
482
|
+
const defaultNote = discovery.usedDefaultRoots
|
|
483
|
+
? " The default package root was used."
|
|
484
|
+
: "";
|
|
485
|
+
const excludedNote = discovery.excluded.length > 0
|
|
486
|
+
? ` Excluded package directories: ${discovery.excluded.map((pkg) => pkg.path).join(", ")}.`
|
|
487
|
+
: "";
|
|
488
|
+
return {
|
|
489
|
+
executed: false,
|
|
490
|
+
exitCode: null,
|
|
491
|
+
passed: false,
|
|
492
|
+
findings: [],
|
|
493
|
+
duration: Date.now() - start,
|
|
494
|
+
status: "CONFIGURATION-ERROR",
|
|
495
|
+
tool: "packed-surface",
|
|
496
|
+
reason: `No package directory with a readable package.json was found under root(s): ${discovery.roots.join(", ")}.${defaultNote}${excludedNote}`,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
executed: false,
|
|
501
|
+
exitCode: null,
|
|
502
|
+
passed: false,
|
|
503
|
+
findings: [],
|
|
504
|
+
duration: Date.now() - start,
|
|
505
|
+
status: "NOT-APPLICABLE",
|
|
506
|
+
tool: "packed-surface",
|
|
507
|
+
reason: `All discovered packages are private: ${discovery.excluded
|
|
508
|
+
.filter((pkg) => pkg.reason === "private package")
|
|
509
|
+
.map((pkg) => pkg.name ?? pkg.path)
|
|
510
|
+
.join(", ")}.`,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
const tempRoot = mkdtempSync(join(tmpdir(), "comity-packed-surface-"));
|
|
514
|
+
const packed = [];
|
|
515
|
+
const configurationErrors = [];
|
|
516
|
+
try {
|
|
517
|
+
for (const [index, pkg] of discovery.publishable.entries()) {
|
|
518
|
+
const destination = join(tempRoot, `pack-${index}`);
|
|
519
|
+
mkdirSync(destination, { recursive: true });
|
|
520
|
+
try {
|
|
521
|
+
const tarballPath = packPackage(pkg, destination);
|
|
522
|
+
const tar = readTarball(tarballPath);
|
|
523
|
+
const manifest = readPackedManifest(tar);
|
|
524
|
+
packed.push({
|
|
525
|
+
packageRecord: { ...pkg, manifest },
|
|
526
|
+
workspacePath: pkg.path,
|
|
527
|
+
tarballPath,
|
|
528
|
+
tar,
|
|
529
|
+
manifest,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
if (error instanceof ToolUnavailableError)
|
|
534
|
+
throw error;
|
|
535
|
+
configurationErrors.push(error instanceof Error ? error.message : String(error));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
541
|
+
if (error instanceof ToolUnavailableError) {
|
|
542
|
+
return {
|
|
543
|
+
executed: false,
|
|
544
|
+
exitCode: null,
|
|
545
|
+
passed: false,
|
|
546
|
+
findings: [],
|
|
547
|
+
duration: Date.now() - start,
|
|
548
|
+
status: "TOOL-UNAVAILABLE",
|
|
549
|
+
tool: "packed-surface",
|
|
550
|
+
reason: error.message,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
executed: false,
|
|
555
|
+
exitCode: null,
|
|
556
|
+
passed: false,
|
|
557
|
+
findings: [],
|
|
558
|
+
duration: Date.now() - start,
|
|
559
|
+
status: "CONFIGURATION-ERROR",
|
|
560
|
+
tool: "packed-surface",
|
|
561
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
const siblingNames = new Set(discovery.allPackages.map((pkg) => pkg.name));
|
|
566
|
+
const packedByName = new Map(packed.map((item) => [item.packageRecord.name, item]));
|
|
567
|
+
const packages = new Map();
|
|
568
|
+
for (const pkg of discovery.allPackages) {
|
|
569
|
+
const item = packedByName.get(pkg.name);
|
|
570
|
+
packages.set(pkg.name, item
|
|
571
|
+
? { ...pkg, manifest: item.manifest }
|
|
572
|
+
: pkg);
|
|
573
|
+
}
|
|
574
|
+
const findings = [];
|
|
575
|
+
let declarationFiles = 0;
|
|
576
|
+
let externalReferences = 0;
|
|
577
|
+
for (const item of packed) {
|
|
578
|
+
try {
|
|
579
|
+
const result = checkPackedPackage(item, packages, siblingNames);
|
|
580
|
+
findings.push(...result.findings);
|
|
581
|
+
declarationFiles += result.declarationFiles;
|
|
582
|
+
externalReferences += result.externalReferences;
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
if (error instanceof ToolUnavailableError)
|
|
586
|
+
throw error;
|
|
587
|
+
configurationErrors.push(`${item.packageRecord.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
sortFindings(findings);
|
|
591
|
+
if (configurationErrors.length > 0) {
|
|
592
|
+
return {
|
|
593
|
+
executed: false,
|
|
594
|
+
exitCode: null,
|
|
595
|
+
passed: false,
|
|
596
|
+
findings,
|
|
597
|
+
duration: Date.now() - start,
|
|
598
|
+
status: "CONFIGURATION-ERROR",
|
|
599
|
+
tool: "packed-surface",
|
|
600
|
+
reason: configurationErrors.join("; "),
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
const excludedNotes = discovery.excluded.map((pkg) => `${pkg.name ?? pkg.path} (${pkg.reason})`);
|
|
604
|
+
const notes = [
|
|
605
|
+
`${packed.length} publishable package(s)`,
|
|
606
|
+
`${declarationFiles} reachable declaration file(s)`,
|
|
607
|
+
`${externalReferences} external reference(s)`,
|
|
608
|
+
"external references evaluated against the packed manifests (ARCH-PACK-003 umbrella; violations are reported as ARCH-DECL-DEP-001…005)",
|
|
609
|
+
];
|
|
610
|
+
if (discovery.usedDefaultRoots)
|
|
611
|
+
notes.push("default package root was used");
|
|
612
|
+
if (excludedNotes.length > 0) {
|
|
613
|
+
notes.push(`excluded: ${excludedNotes.join(", ")}`);
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
executed: true,
|
|
617
|
+
exitCode: 0,
|
|
618
|
+
passed: findings.length === 0,
|
|
619
|
+
findings,
|
|
620
|
+
duration: Date.now() - start,
|
|
621
|
+
status: findings.length === 0 ? "PASS" : "FAIL",
|
|
622
|
+
tool: "packed-surface",
|
|
623
|
+
reason: notes.join("; "),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
if (error instanceof ToolUnavailableError) {
|
|
628
|
+
return {
|
|
629
|
+
executed: false,
|
|
630
|
+
exitCode: null,
|
|
631
|
+
passed: false,
|
|
632
|
+
findings: [],
|
|
633
|
+
duration: Date.now() - start,
|
|
634
|
+
status: "TOOL-UNAVAILABLE",
|
|
635
|
+
tool: "packed-surface",
|
|
636
|
+
reason: error.message,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
executed: false,
|
|
641
|
+
exitCode: null,
|
|
642
|
+
passed: false,
|
|
643
|
+
findings: [],
|
|
644
|
+
duration: Date.now() - start,
|
|
645
|
+
status: "CONFIGURATION-ERROR",
|
|
646
|
+
tool: "packed-surface",
|
|
647
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
finally {
|
|
651
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
export { readTarball };
|
package/dist/format.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface FormatInput {
|
|
|
23
23
|
adapterPeers: CategoryResult;
|
|
24
24
|
/** Published declaration dependency validation results */
|
|
25
25
|
declarations: CategoryResult;
|
|
26
|
+
/** Packed-surface tarball validation results */
|
|
27
|
+
packedSurface: CategoryResult;
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
30
|
/**
|
package/dist/format.js
CHANGED
package/dist/run.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface RunResult {
|
|
|
47
47
|
adapterPeers: CategoryResult;
|
|
48
48
|
/** Published declaration dependency validation results */
|
|
49
49
|
declarations: CategoryResult;
|
|
50
|
+
/** Packed-surface tarball validation results */
|
|
51
|
+
packedSurface: CategoryResult;
|
|
50
52
|
};
|
|
51
53
|
}
|
|
52
54
|
/**
|
package/dist/run.js
CHANGED
|
@@ -5,6 +5,7 @@ import { runAdapterPeers } from "./engines/adapter-peers.js";
|
|
|
5
5
|
import { runDeclarationDependencies } from "./engines/declaration-dependencies.js";
|
|
6
6
|
import { runDepcruise } from "./engines/depcruise.js";
|
|
7
7
|
import { runEslint } from "./engines/eslint.js";
|
|
8
|
+
import { runPackedSurface } from "./engines/packed-surface.js";
|
|
8
9
|
import { runSchema } from "./engines/schema.js";
|
|
9
10
|
import { runSemgrep } from "./engines/semgrep.js";
|
|
10
11
|
import { ExitCode } from "./exit-codes.js";
|
|
@@ -125,6 +126,15 @@ export async function runValidation(options) {
|
|
|
125
126
|
else {
|
|
126
127
|
declarationResult = emptyEngineResult("declarations engine skipped");
|
|
127
128
|
}
|
|
129
|
+
let packedSurfaceResult;
|
|
130
|
+
if (only === "packed-surface") {
|
|
131
|
+
packedSurfaceResult = runPackedSurface(repo);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
packedSurfaceResult = emptyEngineResult(only
|
|
135
|
+
? "packed-surface engine skipped"
|
|
136
|
+
: "packed-surface engine requires explicit --only=packed-surface");
|
|
137
|
+
}
|
|
128
138
|
const violations = [];
|
|
129
139
|
for (const r of [
|
|
130
140
|
schemaResult,
|
|
@@ -133,6 +143,7 @@ export async function runValidation(options) {
|
|
|
133
143
|
semgrepResult,
|
|
134
144
|
adapterResult,
|
|
135
145
|
declarationResult,
|
|
146
|
+
packedSurfaceResult,
|
|
136
147
|
]) {
|
|
137
148
|
if (Array.isArray(r.findings)) {
|
|
138
149
|
violations.push(...r.findings);
|
|
@@ -144,6 +155,7 @@ export async function runValidation(options) {
|
|
|
144
155
|
semgrepResult,
|
|
145
156
|
adapterResult,
|
|
146
157
|
declarationResult,
|
|
158
|
+
packedSurfaceResult,
|
|
147
159
|
];
|
|
148
160
|
const allPassed = allExecuted.every(categoryIsAcceptable) &&
|
|
149
161
|
categoryIsAcceptable(metaResult) &&
|
|
@@ -160,6 +172,7 @@ export async function runValidation(options) {
|
|
|
160
172
|
semgrep: categoryOf(semgrepResult),
|
|
161
173
|
adapterPeers: categoryOf(adapterResult),
|
|
162
174
|
declarations: categoryOf(declarationResult),
|
|
175
|
+
packedSurface: categoryOf(packedSurfaceResult),
|
|
163
176
|
},
|
|
164
177
|
};
|
|
165
178
|
}
|