Files
callstack__agent-device/scripts/layering/generate-contracts-exports-snapshot.ts
Michał Pierzchała a04b587993 chore(layering): derive the contracts export inventory from package.json (#2297)
* chore(gates): derive the contracts export inventory from package.json

R11's package-boundaries test pinned every packages/contracts export
subpath by hand (~115 entries, compared with deepEqual), so every merge
that touched packages/contracts/package.json also had to hand-edit the
pin. Replace it with a structural check derived from the manifest
itself: every exports target must resolve to an existing, tracked
source file, and the number of resolved targets must equal
Object.keys(manifest.exports).length. The must-not-resolve negative
list (Node resolution enforces the exports map at runtime) is
untouched.

* chore(gates): restore exact-membership for the contracts export surface

assertExportTargetsMatchManifest's two checks (targets resolve to
tracked files, count equals manifest.exports keys) both read from the
same manifest.exports object, so a subpath added or removed moves both
sides together and the equality holds regardless -- the widen/shrink
guarantee the deleted ~120-line CONTRACT_EXPORTS pin gave was silently
gone (#2297 review).

Add scripts/layering/contracts-exports.snapshot.json, an independently
committed baseline regenerated by the new
generate-contracts-exports-snapshot.ts, and deepEqual the live export
specifiers against it. A subpath change now fails until the
contributor reruns the generator and reviews the snapshot diff --
"run a script and commit its output" instead of hand-retyping an
alphabetized array, so the original maintenance-burden goal holds too.

Planted red: widened packages/contracts/package.json#exports with a
throwaway "./planted-red-widen" subpath, then separately deleted the
"./wait" subpath; `node --test --experimental-strip-types
scripts/layering/package-boundaries.test.ts` failed both times on the
new deepEqual with the regenerate-and-review message. Reverted before
committing.
2026-09-05 20:47:40 +02:00

34 lines
1.6 KiB
TypeScript

// Regenerates contracts-exports.snapshot.json from packages/contracts/package.json#exports.
//
// package-boundaries.test.ts diffs the live manifest's export specifiers against this committed
// snapshot so widening or shrinking `@agent-device/contracts`'s export surface fails the gate
// (the property the deleted ~120-line inline CONTRACT_EXPORTS pin used to give). The snapshot is a
// separate file rather than an inline list so an editor who adds or removes a contracts subpath
// runs this script and reviews the diff, instead of hand-retyping an alphabetized array.
//
// Run after any packages/contracts/package.json#exports change:
// node --experimental-strip-types scripts/layering/generate-contracts-exports-snapshot.ts
// then commit the resulting snapshot diff alongside the manifest change.
import fs from 'node:fs';
import path from 'node:path';
const repoRoot = path.resolve(import.meta.dirname, '../..');
const manifestFile = 'packages/contracts/package.json';
const snapshotFile = path.join(import.meta.dirname, 'contracts-exports.snapshot.json');
const manifest = JSON.parse(fs.readFileSync(path.join(repoRoot, manifestFile), 'utf8')) as {
name?: string;
exports?: Record<string, unknown>;
};
if (!manifest.name) throw new Error(`${manifestFile} has no "name"`);
const specifiers = Object.keys(manifest.exports ?? {})
.map((subpath) => path.posix.join(manifest.name!, subpath))
.sort();
fs.writeFileSync(snapshotFile, `${JSON.stringify(specifiers, null, 2)}\n`);
console.log(
`Wrote ${specifiers.length} export specifiers to ${path.relative(repoRoot, snapshotFile)}`,
);