@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.
@@ -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 };
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 0 = validation passed
5
5
  * 1 = validation failed (violations found)
6
- * 2 = configuration / discovery error (cannot run)
6
+ * 2 = configuration / discovery error (cannot run; may co-occur with violations)
7
7
  * 3 = tool not installed (e.g. dependency-cruiser missing)
8
8
  */
9
9
  export declare const ExitCode: Readonly<{
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 0 = validation passed
5
5
  * 1 = validation failed (violations found)
6
- * 2 = configuration / discovery error (cannot run)
6
+ * 2 = configuration / discovery error (cannot run; may co-occur with violations)
7
7
  * 3 = tool not installed (e.g. dependency-cruiser missing)
8
8
  */
9
9
  export const ExitCode = Object.freeze({
package/dist/format.d.ts CHANGED
@@ -21,6 +21,10 @@ export interface FormatInput {
21
21
  semgrep: CategoryResult;
22
22
  /** Adapter peers validation results */
23
23
  adapterPeers: CategoryResult;
24
+ /** Published declaration dependency validation results */
25
+ declarations: CategoryResult;
26
+ /** Packed-surface tarball validation results */
27
+ packedSurface: CategoryResult;
24
28
  };
25
29
  }
26
30
  /**