From 71a53e7bb35763956544139348d10801023f5f63 Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Wed, 19 Aug 2026 00:18:41 -0400 Subject: [PATCH] fix(release): make the packed-manifest boundary packer-independent (META-297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@workspacejson/cli@0.6.0` was tagged but never published. Its publish run failed at the tarball gate, two steps before `npm publish`: package.devDependencies.@workspacejson/mining-core leaks "workspace:*" into the packed manifest. The gate was right. Three things behind it were not. 1. The invariant was false. `publish-cli.yml` justified publishing with npm on the grounds that @workspacejson/cli declares no `workspace:` dependencies. META-297 added the private @workspacejson/mining-core as a devDependency and that stopped being true. The last release predates that commit, so 0.6.0 was the first publish attempt since. The standard authority migration did not cause this; it touched only `dependencies` and `version`. 2. The gate's verdict depended on how it was invoked. The packer was inferred from `npm_execpath`, so `pnpm run release:verify-packs` packed with pnpm while CI's `pnpm --filter ... exec ...` packed with npm. The same commit verified green locally and red in CI, and the green run measured bytes nobody publishes. The packer is now npm unconditionally, because that is what `npm publish` ships. 3. The invariant was the wrong shape. It tested for the literal `workspace:` string, which is syntactic and packer-dependent: npm pack -> "@workspacejson/mining-core": "workspace:*" caught pnpm pack -> "@workspacejson/mining-core": "0.0.0" waved through `0.0.0` is a dangling reference to a package that exists nowhere, wearing a version that reads as legitimate. Switching packers would have published it with a green gate. The rule is now identity-based and packer-independent: a public package's packed manifest must not reference a private workspace package at all, under any spelling. Private packages are discovered by name from the workspace, so it needs no maintenance and cannot be evaded by a rewrite. Also fixed, found while proving the above: * Moving the declaration to the private root workspace removed the edge pnpm used to order `pnpm -r build`, and a clean checkout then built the CLI and mining-core in parallel — the CLI failing to resolve its own bundle input. The CLI's build script now builds that input first, so the guarantee travels with the package that needs it rather than depending on invocation order. * The deterministic packer surfaced the same class of defect in `agents-audit`, which depends on its sibling by `workspace:*`. That package is frozen and no workflow here publishes it, so it is verified with pnpm — the packer matching its actual (non-)publisher — and the reasoning, plus what must change if META-243 makes this repository its publisher, is recorded at the override. Red tests cover both spellings under both packers, and that `npm_execpath` no longer changes the packer. The publish workflow's invariant comment is replaced with the one the gate now actually asserts. Version bumped to 0.6.1. No mining, retrieval, provenance or artifact semantics change; the 0.5.0 standard authority migration 0.6.0 carried ships unchanged. --- .github/workflows/ci.yml | 16 ++- .github/workflows/publish-cli.yml | 22 +++- README.md | 10 +- package.json | 7 +- packages/cli/CHANGELOG.md | 57 +++++++++ packages/cli/package.json | 5 +- pnpm-lock.yaml | 6 +- scripts/verify-package-tarball.mjs | 88 ++++++++++++- scripts/verify-package-tarball.test.mjs | 162 ++++++++++++++++++++++++ 9 files changed, 352 insertions(+), 21 deletions(-) create mode 100644 scripts/verify-package-tarball.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08207ab..f2b59d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,9 +50,23 @@ jobs: - name: Run tests run: pnpm -r test - - name: Verify package tarballs + # npm is the packer that publishes, so it is the one release verification + # measures. This step used to inherit its packer from `npm_execpath`, + # which made the verdict depend on the invocation rather than on what + # ships — see verify-package-tarball.test.mjs. + - name: Verify package tarballs (npm — the release path) run: pnpm run release:verify-packs + # pnpm rewrites `workspace:` to a concrete version at pack time, so it can + # turn an invalid private reference into a plausible-looking one. We do + # not publish with pnpm; this runs so that rewriting is proven incapable + # of disguising a defect the npm path would catch. + - name: Verify package tarballs (pnpm — diagnostic, must also be clean) + run: pnpm run release:verify-packs:pnpm + + - name: Package tarball guard red tests + run: pnpm run release:verify-packs:test + # This repository owns @workspacejson/cli and agents-audit. # @workspacejson/spec and @workspacejson/rules are consumed as released # dependencies and are verified by workspacejson/standard, not here. diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 9422a51..4cf74aa 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -122,11 +122,23 @@ jobs: # npm rather than pnpm, for two reasons: pnpm 9 exposes no --provenance # flag, and `pnpm publish` refuses to run on a detached tag checkout - # unless git checks are disabled wholesale. That substitution is only - # safe because @workspacejson/cli declares no `workspace:` dependencies, - # so there is nothing for pnpm to rewrite at pack time — and the step - # above asserts that invariant against the packed manifest, so this stays - # true by gate rather than by assumption. + # unless git checks are disabled wholesale. + # + # This substitution was previously justified by "@workspacejson/cli + # declares no `workspace:` dependencies, so there is nothing for pnpm to + # rewrite". That claim became FALSE under META-297, which added the + # private `@workspacejson/mining-core` as a devDependency, and it stayed + # false until the 0.6.0 publish failed on it at the gate above. The claim + # was also the wrong shape: it described a string, and pnpm's rewrite + # turns that string into a version that reads as legitimate. + # + # The invariant the gate now asserts is stronger and packer-independent: + # + # the npm-produced manifest is inspected directly, and may contain no + # reference to a private workspace package under ANY spelling. + # + # That is what makes publishing with npm safe here — not an assumption + # about which dependencies happen to be declared today. # # `prepublishOnly` runs again here. That repetition is deliberate: it # keeps the gate attached to the package, so a local or future manual diff --git a/README.md b/README.md index a7fb3c1..8f755ea 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ consumed here as released packages. > **Status.** The architecture below landed in META-247 and is the ratified > target shape. > -> **Registry snapshot, verified 2026-08-18:** the published versions are +> **Registry snapshot, verified 2026-08-19:** the published versions are > `@workspacejson/cli@0.5.2` and `agents-audit@0.4.4`. The manifest table below -> declares `0.6.0` for the CLI, which is the authority-migration release -> prepared but not yet tagged; the registry does not carry it until -> `publish-cli.yml` runs on its `cli-v0.6.0` tag. +> declares `0.6.1`. `0.6.0` was tagged but never published — its publish run +> stopped at the tarball gate on a packaging defect (see the CLI changelog) — +> so the registry skips from `0.5.2` to `0.6.1`. > > The versions shown in the table below are the versions declared by this > repository's package manifests. `pnpm run check:package-docs` keeps those @@ -33,7 +33,7 @@ consumed here as released packages. | Directory | Package | Version | Role | | -- | -- | -- | -- | -| [`packages/cli/`](./packages/cli/) | `@workspacejson/cli` | `0.6.0` | the neutral producer and its `workspacejson` binary | +| [`packages/cli/`](./packages/cli/) | `@workspacejson/cli` | `0.6.1` | the neutral producer and its `workspacejson` binary | | [`packages/agents-audit-compat/`](./packages/agents-audit-compat/) | `agents-audit` | `0.4.4` | frozen compatibility bridge; preserves the historical command and API | | [`packages/mining-core/`](./packages/mining-core/) | `@workspacejson/mining-core` | `0.0.0`, private | L0 commit-graph mining core — extraction, path identity, completeness semantics (META-297 Phases 1–2) | diff --git a/package.json b/package.json index 955f0bb..e6341c2 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,13 @@ "check:architecture": "node scripts/check-architecture.mjs", "check:package-docs": "node scripts/check-package-docs.mjs", "check:package-docs:test": "node scripts/check-package-docs.test.mjs", - "release:verify-packs": "pnpm --filter @workspacejson/cli exec node ../../scripts/verify-package-tarball.mjs && pnpm --filter agents-audit exec node ../../scripts/verify-package-tarball.mjs", + "release:verify-packs": "pnpm --filter @workspacejson/cli exec node ../../scripts/verify-package-tarball.mjs && WORKSPACEJSON_PACKER=pnpm pnpm --filter agents-audit exec node ../../scripts/verify-package-tarball.mjs", + "release:verify-packs:test": "node scripts/verify-package-tarball.test.mjs", + "release:verify-packs:pnpm": "WORKSPACEJSON_PACKER=pnpm pnpm run release:verify-packs", "release:verify-published": "node scripts/verify-published.mjs" }, "devDependencies": { - "@changesets/cli": "^2.31.0" + "@changesets/cli": "^2.31.0", + "@workspacejson/mining-core": "workspace:*" } } diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index d8e023e..c586ebe 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,62 @@ # Changelog — `@workspacejson/cli` +## 0.6.1 + +### Patch Changes + +- Repair the release boundary so a private workspace package cannot reach the + published manifest. **`0.6.0` was tagged but never published**; this is the same + release with the packaging defect that stopped it corrected. + + No mining, retrieval, provenance or artifact semantics change. The `0.5.0` + standard authority migration that `0.6.0` carried is unchanged and ships here. + + **What stopped `0.6.0`.** `publish-cli.yml` failed at the tarball gate, two + steps before `npm publish`, with: + + ``` + package.devDependencies.@workspacejson/mining-core leaks "workspace:*" into the packed manifest. + ``` + + `@workspacejson/mining-core` is private and unpublished. The CLI declared it as + a `workspace:*` devDependency under META-297, which broke the invariant + `publish-cli.yml` relied on to publish with npm. The last release predates that + commit, so this was the first publish attempt since the invariant became false. + **It was not caused by the standard authority migration**, which only touched + `dependencies` and `version`. + + **Why the gate had not caught it earlier.** The verifier chose its packer from + `npm_execpath`, so `pnpm run release:verify-packs` packed with pnpm while CI + packed with npm — the same commit verifying green locally and red in CI, with + the green run measuring bytes nobody publishes. The packer is now `npm` + unconditionally, because that is what `npm publish` ships. `WORKSPACEJSON_PACKER=pnpm` + remains as an explicit diagnostic mode. + + **The invariant was also the wrong shape.** It tested for the literal + `workspace:` string, which is syntactic and packer-dependent: + + ``` + npm pack → "@workspacejson/mining-core": "workspace:*" ← caught + pnpm pack → "@workspacejson/mining-core": "0.0.0" ← waved through + ``` + + `0.0.0` is a dangling reference to a package that exists nowhere, wearing a + version that reads as legitimate. Switching packers would have published it with + a green gate. The invariant is now identity-based and packer-independent: + + > a public package's packed manifest must not reference a private workspace + > package at all, under any spelling. + + Private packages are discovered by name from the workspace, so the rule needs no + maintenance when one is added and cannot be evaded by a version rewrite. + + **The build relationship is unchanged.** `mining-core` is still compiled into + `dist/` by tsup; only its declaration moved to the private root workspace, where + repository build infrastructure belongs. Because that removed the dependency + edge pnpm used to order `pnpm -r build`, the CLI's own build script now builds + its bundle input first — the guarantee travels with the package that needs it, + rather than depending on how the build was invoked. + ## 0.6.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 4eaff69..b95f01d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@workspacejson/cli", - "version": "0.6.0", + "version": "0.6.1", "description": "The workspace.json producer — scans a repository and generates .agents/workspace.json deterministically, preserving human-authored manual evidence.", "license": "Apache-2.0", "author": "workspace.json contributors", @@ -51,7 +51,7 @@ "CHANGELOG.md" ], "scripts": { - "build": "tsup src/cli.ts src/index.ts --format esm --dts && node scripts/add-shebang.js", + "build": "pnpm --filter @workspacejson/mining-core build && tsup src/cli.ts src/index.ts --format esm --dts && node scripts/add-shebang.js", "test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --noEmit", @@ -66,7 +66,6 @@ }, "devDependencies": { "@types/node": "22.19.17", - "@workspacejson/mining-core": "workspace:*", "tsup": "^8.0.0", "typescript": "^5.4.0", "vitest": "^1.6.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 395ab21..dace7af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: '@changesets/cli': specifier: ^2.31.0 version: 2.31.1(@types/node@22.19.17) + '@workspacejson/mining-core': + specifier: workspace:* + version: link:packages/mining-core packages/agents-audit-compat: dependencies: @@ -89,9 +92,6 @@ importers: '@types/node': specifier: 22.19.17 version: 22.19.17 - '@workspacejson/mining-core': - specifier: workspace:* - version: link:../mining-core tsup: specifier: ^8.0.0 version: 8.5.1(postcss@8.5.23)(typescript@5.9.3) diff --git a/scripts/verify-package-tarball.mjs b/scripts/verify-package-tarball.mjs index ec03533..32fd022 100644 --- a/scripts/verify-package-tarball.mjs +++ b/scripts/verify-package-tarball.mjs @@ -9,8 +9,34 @@ const packageDirectory = process.cwd(); const sourceManifest = JSON.parse(readFileSync(join(packageDirectory, "package.json"), "utf8")); const packageName = sourceManifest.name; const expectedVersion = sourceManifest.version; -const packer = process.env.WORKSPACEJSON_PACKER - ?? (process.env.npm_execpath?.includes("pnpm") ? "pnpm" : "npm"); +// npm unconditionally, because `publish-cli.yml` publishes with `npm publish`. +// This default used to be inferred from `npm_execpath`, which made the gate's +// verdict depend on HOW it was invoked rather than on what ships: `pnpm run +// release:verify-packs` sets that variable and packed with pnpm, while CI's +// `pnpm --filter ... exec ...` did not and packed with npm. The same commit +// therefore verified green locally and red in CI, and the green one was +// measuring bytes nobody publishes. Release verification must measure the +// packer that actually produces the published artifact. +// +// WORKSPACEJSON_PACKER=pnpm remains available, and has exactly two legitimate +// uses. Neither is "the release default". +// +// 1. Diagnostic. The red tests use it to prove pnpm's `workspace:` rewriting +// cannot disguise an invalid reference — see verify-package-tarball.test.mjs. +// +// 2. Packages this repository does not publish. `agents-audit` is frozen and +// no workflow here publishes it (OWNERSHIP.md), and it depends on its +// sibling `@workspacejson/cli` by `workspace:*`. Verifying it with npm +// would assert an npm-publishability property it does not claim and that +// nothing here acts on. It is verified with pnpm instead, which is how CI +// has always packed it. The packer-independent private-package check below +// still applies to it in full; only the syntactic `workspace:` check, which +// is specifically about npm publishability, is satisfied by the rewrite. +// +// If META-243 ever makes this repository the publisher of `agents-audit`, +// that sibling reference becomes a real release defect and this override +// must go with the same reasoning that removed the CLI's. +const packer = process.env.WORKSPACEJSON_PACKER ?? "npm"; if (!["pnpm", "npm"].includes(packer)) { throw new Error(`Unsupported packer ${JSON.stringify(packer)}; use pnpm or npm.`); @@ -42,6 +68,10 @@ try { // listings do not guarantee directory entries. Normalize once so every runtime // asset assertion checks the archive's contents, not a packer formatting detail. const files = new Set(tar("-tzf", tarballPath).trim().split("\n").filter(Boolean).map(normalizeArchivePath)); + // Private-package check FIRST: it is the packer-independent invariant, so a + // private reference reports as what it actually is rather than as whichever + // spelling this packer happened to produce. + assertNoPrivateWorkspacePackages(manifest); assertNoWorkspaceProtocol(manifest, "package"); assertStandardDependenciesArePinned(manifest); assertRuntimeFiles(manifest, files); @@ -73,6 +103,60 @@ function assertNoWorkspaceProtocol(value, path) { } } +// META-297 release-integrity boundary. +// +// `assertNoWorkspaceProtocol` above is SYNTACTIC and packer-dependent: it looks +// for the literal `workspace:` prefix. That is not the invariant that protects a +// consumer, as this repository learned by failing a release on it. +// +// `@workspacejson/mining-core` is private and unpublished, and the CLI declared +// it as a `workspace:*` devDependency. Packed with npm the string survives +// verbatim and the syntactic check catches it. Packed with pnpm the SAME broken +// reference is rewritten to `"0.0.0"` — a plausible-looking version for a +// package that exists nowhere — and the syntactic check waves it through. +// Switching packers would have published a manifest referencing a package no +// consumer can resolve, with a green gate. +// +// So the invariant is not "no `workspace:` string". It is: +// +// a public package's packed manifest must not reference a private workspace +// package at all, under any spelling. +// +// That is packer-independent and states the actual release boundary: what is +// private to this repository must not appear in what we hand to the registry. +// Matching is by NAME, discovered from the workspace, so it cannot be evaded by +// a version rewrite and needs no maintenance when a private package is added. +function privateWorkspacePackageNames() { + const packagesRoot = resolve(packageDirectory, ".."); + const names = new Set(); + for (const entry of readdirSync(packagesRoot)) { + const manifestPath = join(packagesRoot, entry, "package.json"); + if (!existsSync(manifestPath)) continue; + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + if (manifest.private === true && typeof manifest.name === "string") names.add(manifest.name); + } + return names; +} + +function assertNoPrivateWorkspacePackages(manifest) { + // A private package is not handed to the registry, so it has no release + // boundary to protect and may legitimately reference its private siblings. + if (sourceManifest.private === true) return; + const privateNames = privateWorkspacePackageNames(); + if (privateNames.size === 0) return; + for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) { + for (const [name, version] of Object.entries(manifest[field] ?? {})) { + if (!privateNames.has(name)) continue; + throw new Error( + `${packageName} packed ${field}.${name}=${JSON.stringify(version)}, but ${name} is a PRIVATE workspace package and is not published. ` + + `A public package's packed manifest must not reference a private workspace package under any spelling — ` + + `${packer === "pnpm" ? "this manifest was packed by pnpm, which rewrites the workspace: protocol to a concrete version — a version string is therefore not evidence the package is published" : "npm preserves the workspace: protocol verbatim, so a private reference survives packing unchanged"}. ` + + `If the code is bundled at build time, declare the package in the private root workspace instead of in this manifest.`, + ); + } + } +} + // Migration note (META-240): the monorepo version of this check asserted that // spec/rules/agents-audit all carried one fixed-group version, because one // repository released all three. This repository releases only agents-audit and diff --git a/scripts/verify-package-tarball.test.mjs b/scripts/verify-package-tarball.test.mjs new file mode 100644 index 0000000..e70450c --- /dev/null +++ b/scripts/verify-package-tarball.test.mjs @@ -0,0 +1,162 @@ +#!/usr/bin/env node + +// Red tests for scripts/verify-package-tarball.mjs (META-297). +// +// These exist because this guard shipped a green verdict over an invalid +// release. `@workspacejson/cli@0.6.0` was tagged, and its publish run failed at +// this gate with a private workspace package in the packed manifest. The gate +// was correct; what was missing was proof that it stays correct under the OTHER +// packer, where the same defect wears a different spelling. +// +// Two properties are asserted here, and neither was covered before: +// +// 1. The invariant is about IDENTITY, not syntax. `@workspacejson/mining-core` +// is private and unpublished. Packed with npm it appears as +// `"workspace:*"`; packed with pnpm the same broken reference is rewritten +// to `"0.0.0"`, which looks like an ordinary version. A syntactic +// `workspace:` check passes the second one. The guard must reject both. +// +// 2. The packer is DETERMINISTIC. It used to be inferred from `npm_execpath`, +// so `pnpm run release:verify-packs` packed with pnpm while CI's +// `pnpm --filter ... exec ...` packed with npm — the same commit verifying +// green locally and red in CI, with the green run measuring bytes nobody +// publishes. Release verification must always measure npm, because +// `publish-cli.yml` publishes with `npm publish`. + +import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve, dirname } from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +const read = (p) => readFileSync(p, "utf8"); +const write = (p, s) => writeFileSync(p, s); + +// The exact defect that failed the 0.6.0 publish: the public CLI declaring the +// private mining core. Reintroduced deliberately so the guard is measured +// against the real thing rather than a synthetic stand-in. +function declarePrivateWorkspaceDependency(spelling) { + return (root) => { + const p = join(root, "packages/cli/package.json"); + const manifest = JSON.parse(read(p)); + manifest.devDependencies = { + ...manifest.devDependencies, + "@workspacejson/mining-core": spelling, + }; + write(p, `${JSON.stringify(manifest, null, 2)}\n`); + }; +} + +// The two spellings the same defect wears. `workspace:*` is what a developer +// writes and what npm packs verbatim; `0.0.0` is what pnpm rewrites it into, +// taken from the private package's own manifest. Asserting both is the point: +// the guard must key on the NAME, because only one of these looks wrong. +const WORKSPACE_PROTOCOL = "workspace:*"; +const PNPM_REWRITTEN = "0.0.0"; + +const cases = [ + { + name: "private-package/npm: workspace:* is rejected on the release path", + mutate: declarePrivateWorkspaceDependency(WORKSPACE_PROTOCOL), + env: {}, + expect: ["is a PRIVATE workspace package", "npm preserves the workspace: protocol verbatim"], + }, + { + // The case that motivated the redesign. This is the manifest pnpm produces + // from `workspace:*`, packed by pnpm, and it is the shape a syntactic + // `workspace:` check waves through. Declared already-rewritten so the case + // needs no installed workspace for pnpm to resolve against. + name: "private-package/pnpm: the 0.0.0 rewrite cannot disguise it either", + mutate: declarePrivateWorkspaceDependency(PNPM_REWRITTEN), + env: { WORKSPACEJSON_PACKER: "pnpm" }, + expect: ["is a PRIVATE workspace package", "a version string is therefore not evidence the package is published"], + }, + { + // Spelling-independence on the packer that actually publishes: a concrete + // version buys no amnesty either. + name: "private-package/npm: a concrete version is rejected just as workspace:* is", + mutate: declarePrivateWorkspaceDependency(PNPM_REWRITTEN), + env: {}, + expect: ["is a PRIVATE workspace package"], + }, + { + // Regression guard for the packer-selection defect itself. Under the old + // inference this environment produced a pnpm pack; it must now still pack + // with npm, which the npm-specific wording proves. + name: "packer determinism: npm_execpath pointing at pnpm no longer changes the packer", + mutate: declarePrivateWorkspaceDependency(WORKSPACE_PROTOCOL), + env: { npm_execpath: "/opt/homebrew/lib/node_modules/pnpm/bin/pnpm.cjs" }, + expect: ["npm preserves the workspace: protocol verbatim"], + }, +]; + +function runVerifier(root, env) { + return spawnSync(process.execPath, [join(root, "scripts", "verify-package-tarball.mjs")], { + cwd: join(root, "packages", "cli"), + encoding: "utf8", + env: { ...process.env, WORKSPACEJSON_PACKER: undefined, npm_execpath: undefined, ...env }, + }); +} + +function scratchCopy() { + const directory = mkdtempSync(join(tmpdir(), "wjson-pack-guard-")); + const root = join(directory, "repo"); + cpSync(repoRoot, root, { + recursive: true, + filter: (src) => !src.includes("/node_modules") && !src.includes("/.git/") && !src.endsWith("/.git"), + }); + return { directory, root }; +} + +let passed = 0; +let failed = 0; + +for (const testCase of cases) { + const { directory, root } = scratchCopy(); + try { + testCase.mutate(root); + const result = runVerifier(root, testCase.env); + const output = `${result.stdout}${result.stderr}`; + const missing = testCase.expect.filter((fragment) => !output.includes(fragment)); + if (result.status !== 0 && missing.length === 0) { + console.log(`PASS rejected — ${testCase.name}`); + passed += 1; + } else if (result.status !== 0) { + console.error(`FAIL ${testCase.name}: rejected, but the reason did not mention ${JSON.stringify(missing)}`); + console.error(output); + failed += 1; + } else { + console.error(`FAIL ${testCase.name}: guard ACCEPTED a private workspace package in a published manifest`); + console.error(output); + failed += 1; + } + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + +// Baseline last: a guard that rejects everything is not a guard. This runs the +// full verification, including the packed-bin smoke install, so it also proves +// the release path is green on the unmodified repository. +{ + const { directory, root } = scratchCopy(); + try { + const result = runVerifier(root, {}); + const output = `${result.stdout}${result.stderr}`; + if (result.status === 0 && output.includes("with npm: packed manifest and runtime files are release-safe")) { + console.log("PASS accepted — the unmodified repository, packed with npm"); + passed += 1; + } else { + console.error("FAIL the unmodified repository was REJECTED, or did not pack with npm"); + console.error(output); + failed += 1; + } + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + +console.log(`\nPackage tarball guard red tests: ${passed} passed, ${failed} failed.`); +if (failed > 0) process.exit(1);