From c52c567cdd389c10f7ff0e6a91b7b31ed47aedfe Mon Sep 17 00:00:00 2001 From: Paul Logan Date: Mon, 27 Jul 2026 11:55:29 -0700 Subject: [PATCH 1/6] fix: couple core and CLI releases --- package-lock.json | 10 +++--- package.json | 5 +-- packages/cli/package.json | 4 +-- scripts/check-workspace-versions.mjs | 49 ++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 scripts/check-workspace-versions.mjs diff --git a/package-lock.json b/package-lock.json index c36a79eb..9087e8ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codeburn-monorepo", - "version": "0.9.19", + "version": "0.9.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codeburn-monorepo", - "version": "0.9.19", + "version": "0.9.20", "license": "MIT", "workspaces": [ "packages/*" @@ -4116,10 +4116,10 @@ }, "packages/cli": { "name": "codeburn", - "version": "0.9.19", + "version": "0.9.20", "license": "MIT", "dependencies": { - "@codeburn/core": "*", + "@codeburn/core": "0.9.20", "@modelcontextprotocol/sdk": "^1.29.0", "bonjour-service": "^1.4.1", "chalk": "^5.4.1", @@ -4150,7 +4150,7 @@ }, "packages/core": { "name": "@codeburn/core", - "version": "0.9.19", + "version": "0.9.20", "license": "MIT", "dependencies": { "zod": "^3.25.76" diff --git a/package.json b/package.json index 156eaa75..8a8ebd7e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeburn-monorepo", - "version": "0.9.19", + "version": "0.9.20", "description": "CodeBurn monorepo — CLI (packages/cli) + @codeburn/core (packages/core)", "private": true, "type": "module", @@ -13,7 +13,8 @@ "build:cli": "npm run build:cli -w codeburn", "build:dash": "npm run build:dash -w codeburn", "dev": "npm run dev -w codeburn", - "test": "npm run test -w codeburn" + "test": "npm run test -w codeburn", + "check:workspace-versions": "node scripts/check-workspace-versions.mjs" }, "engines": { "node": ">=22.13.0" diff --git a/packages/cli/package.json b/packages/cli/package.json index ac76c3cc..1f79d864 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "codeburn", - "version": "0.9.19", + "version": "0.9.20", "description": "See where your AI spend goes - by task, tool, model, and project", "type": "module", "main": "./dist/cli.js", @@ -50,7 +50,7 @@ }, "homepage": "https://github.com/getagentseal/codeburn#readme", "dependencies": { - "@codeburn/core": "*", + "@codeburn/core": "0.9.20", "@modelcontextprotocol/sdk": "^1.29.0", "bonjour-service": "^1.4.1", "chalk": "^5.4.1", diff --git a/scripts/check-workspace-versions.mjs b/scripts/check-workspace-versions.mjs new file mode 100644 index 00000000..3e0de75f --- /dev/null +++ b/scripts/check-workspace-versions.mjs @@ -0,0 +1,49 @@ +#!/usr/bin/env node +// Fails when the monorepo root, the CLI, and @codeburn/core drift apart. +// The CLI must depend on the exact core version it ships with, otherwise a +// published CLI can resolve a core it was never tested against. +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { dirname, join } from 'node:path' + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..') + +const read = (relative) => { + const path = join(repoRoot, relative) + try { + return JSON.parse(readFileSync(path, 'utf8')) + } catch (error) { + throw new Error(`cannot read ${relative}: ${error.message}`) + } +} + +const root = read('package.json') +const cli = read('packages/cli/package.json') +const core = read('packages/core/package.json') + +const coreDep = cli.dependencies?.['@codeburn/core'] + +const problems = [] + +if (root.version !== cli.version) { + problems.push(`root version ${root.version} !== cli version ${cli.version}`) +} +if (root.version !== core.version) { + problems.push(`root version ${root.version} !== core version ${core.version}`) +} +if (coreDep !== core.version) { + problems.push( + `packages/cli depends on "@codeburn/core": ${JSON.stringify(coreDep)}, expected the exact core version "${core.version}"`, + ) +} + +if (problems.length > 0) { + console.error('workspace version check failed:') + for (const problem of problems) console.error(` - ${problem}`) + console.error('\nSet root, packages/cli, and packages/core to the same version,') + console.error('pin the CLI dependency to that exact version, then run:') + console.error(' npm install --package-lock-only --ignore-scripts') + process.exit(1) +} + +console.log(`workspace versions agree at ${root.version}`) From 0054c9cecab05815ff452b0fb7a3c332da4074f9 Mon Sep 17 00:00:00 2001 From: Paul Logan Date: Mon, 27 Jul 2026 11:57:05 -0700 Subject: [PATCH 2/6] fix: emit core declarations with TypeScript --- packages/core/package.json | 2 +- packages/core/tsconfig.build.json | 14 ++++++++++++++ packages/core/tsup.config.ts | 5 ++++- 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 packages/core/tsconfig.build.json diff --git a/packages/core/package.json b/packages/core/package.json index 719f5469..d4e8ee0b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -177,7 +177,7 @@ "schemas" ], "scripts": { - "build": "tsup", + "build": "tsup && tsc -p tsconfig.build.json", "typecheck": "tsc --noEmit", "test": "vitest run", "emit-schemas": "tsx scripts/emit-schemas.mts" diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json new file mode 100644 index 00000000..b29acc38 --- /dev/null +++ b/packages/core/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "rootDir": "src", + "outDir": "dist", + "sourceMap": false, + "noEmit": false + }, + "include": ["src/**/*"], + "exclude": ["tests/**/*", "scripts/**/*", "dist", "node_modules"] +} diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index 9846e7e8..396313a9 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -52,5 +52,8 @@ export default defineConfig({ clean: true, splitting: false, sourcemap: true, - dts: true, + // Declarations come from `tsc -p tsconfig.build.json` instead. tsup's dts + // worker bundles types for all 41 entries in one pass and exhausts the heap + // (ERR_WORKER_OUT_OF_MEMORY) on Node 22 through 26. + dts: false, }) From 7e29455d4fac2f460527a477a9ae270839b17c7d Mon Sep 17 00:00:00 2001 From: Paul Logan Date: Mon, 27 Jul 2026 12:01:52 -0700 Subject: [PATCH 3/6] ci: gate core builds and package integrity --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++++++++ packages/core/package.json | 1 + 2 files changed, 33 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d369dba4..0e77873e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,38 @@ on: pull_request: jobs: + core: + name: core (node ${{ matrix.node }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: ['22.13.x', '24.x', '26.x'] + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + - name: Install from lockfile + run: npm ci + + - name: Check workspace versions + run: npm run check:workspace-versions + + - name: Typecheck core + run: npm run typecheck --workspace=@codeburn/core + + - name: Test core + run: npm test --workspace=@codeburn/core + + - name: Build core + run: npm run build --workspace=@codeburn/core + + - name: Verify package contents + run: npm pack --workspace=@codeburn/core --dry-run + semgrep: runs-on: ubuntu-latest steps: diff --git a/packages/core/package.json b/packages/core/package.json index d4e8ee0b..1936151c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -3,6 +3,7 @@ "version": "0.9.20", "description": "Pure decode/detect engine for CodeBurn: provider session-log decoding, content-minimized observations, and detector contracts. No I/O, no pricing \u2014 hosts supply both.", "type": "module", + "sideEffects": false, "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", From 6f1f8a44624eb7f5ffb20e5cf11e459df02864ea Mon Sep 17 00:00:00 2001 From: Paul Logan Date: Mon, 27 Jul 2026 15:12:04 -0700 Subject: [PATCH 4/6] fix: check package-lock.json in the workspace version gate The gate compared only the three package.json manifests, so it passed while package-lock.json still recorded a stale workspace version. npm ci does not reject that either (checked on npm 10.9.2, 11.12.1, 11.16.0, 11.17.0), so nothing caught the exact drift the script exists to catch. Reproduced with a fixture whose manifests all read 0.9.20 while the lockfile recorded packages/core at 0.9.19 and a "*" CLI dependency: the gate exited 0. It now exits 1. --- scripts/check-workspace-versions.mjs | 42 +++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/scripts/check-workspace-versions.mjs b/scripts/check-workspace-versions.mjs index 3e0de75f..9be1cd84 100644 --- a/scripts/check-workspace-versions.mjs +++ b/scripts/check-workspace-versions.mjs @@ -2,6 +2,12 @@ // Fails when the monorepo root, the CLI, and @codeburn/core drift apart. // The CLI must depend on the exact core version it ships with, otherwise a // published CLI can resolve a core it was never tested against. +// +// package-lock.json is checked too. Comparing only the manifests leaves the +// gate blind to the case it exists for: all three manifests can agree while the +// lockfile still records a stale workspace version, and `npm ci` does not +// reject that (verified on npm 10.9.2, 11.12.1, 11.16.0, 11.17.0). A gate that +// cannot catch its own failure mode is decoration. import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { dirname, join } from 'node:path' @@ -20,6 +26,7 @@ const read = (relative) => { const root = read('package.json') const cli = read('packages/cli/package.json') const core = read('packages/core/package.json') +const lock = read('package-lock.json') const coreDep = cli.dependencies?.['@codeburn/core'] @@ -37,6 +44,39 @@ if (coreDep !== core.version) { ) } +// --- lockfile agreement ----------------------------------------------------- + +const lockEntry = (key) => lock.packages?.[key] + +if (!lock.packages) { + problems.push('package-lock.json has no "packages" map; expected lockfileVersion 2 or 3') +} else { + const expected = [ + ['', root.version, 'root'], + ['packages/cli', cli.version, 'packages/cli'], + ['packages/core', core.version, 'packages/core'], + ] + for (const [key, manifestVersion, label] of expected) { + const entry = lockEntry(key) + if (!entry) { + problems.push(`package-lock.json is missing an entry for ${label}`) + continue + } + if (entry.version !== manifestVersion) { + problems.push( + `package-lock.json records ${label} at ${entry.version}, but its manifest says ${manifestVersion}`, + ) + } + } + + const lockedDep = lockEntry('packages/cli')?.dependencies?.['@codeburn/core'] + if (lockedDep !== undefined && lockedDep !== coreDep) { + problems.push( + `package-lock.json records the CLI's core dependency as ${JSON.stringify(lockedDep)}, but packages/cli/package.json says ${JSON.stringify(coreDep)}`, + ) + } +} + if (problems.length > 0) { console.error('workspace version check failed:') for (const problem of problems) console.error(` - ${problem}`) @@ -46,4 +86,4 @@ if (problems.length > 0) { process.exit(1) } -console.log(`workspace versions agree at ${root.version}`) +console.log(`workspace versions and package-lock.json agree at ${root.version}`) From 0ec9ea93cbbb484b4405b4a876bebb7dbaedc8d3 Mon Sep 17 00:00:00 2001 From: Paul Logan Date: Mon, 27 Jul 2026 15:12:13 -0700 Subject: [PATCH 5/6] fix: guard publish against a half-built dist The build is `tsup && tsc`. tsup runs with clean:true, so it wipes dist and writes JavaScript; if tsc then fails, dist holds .js with no declarations. The build exits non-zero, but packages/core declared no prepublishOnly, so nothing rebuilt at publish time and a later npm publish would ship it. Reproduced: remove the declarations from a copy of dist and npm pack --dry-run still succeeds, with all 41 exports subpaths pointing at files absent from the tarball. npm pack was never the guard. Adds prepublishOnly (build then verify) and scripts/verify-dist.mjs, which asserts every exports target exists. CI runs verify-dist as well, so the guard is exercised on every push rather than only on the rare publish. --- .github/workflows/ci.yml | 3 ++ packages/core/package.json | 4 +- packages/core/scripts/verify-dist.mjs | 54 +++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 packages/core/scripts/verify-dist.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e77873e..ab7616c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,9 @@ jobs: - name: Build core run: npm run build --workspace=@codeburn/core + - name: Verify every export target exists + run: npm run verify-dist --workspace=@codeburn/core + - name: Verify package contents run: npm pack --workspace=@codeburn/core --dry-run diff --git a/packages/core/package.json b/packages/core/package.json index 1936151c..16fc4331 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -181,7 +181,9 @@ "build": "tsup && tsc -p tsconfig.build.json", "typecheck": "tsc --noEmit", "test": "vitest run", - "emit-schemas": "tsx scripts/emit-schemas.mts" + "emit-schemas": "tsx scripts/emit-schemas.mts", + "verify-dist": "node scripts/verify-dist.mjs", + "prepublishOnly": "npm run build && npm run verify-dist" }, "engines": { "node": ">=22.13.0" diff --git a/packages/core/scripts/verify-dist.mjs b/packages/core/scripts/verify-dist.mjs new file mode 100644 index 00000000..dd21249d --- /dev/null +++ b/packages/core/scripts/verify-dist.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node +// Refuses to let a half-built dist reach npm. +// +// `npm run build` is `tsup && tsc`. tsup runs with clean:true, so it wipes dist +// and writes JavaScript; if tsc then fails, dist holds .js with no declarations. +// The build exits non-zero, but nothing rebuilds at publish time, so a later +// `npm publish` would ship a package whose every `types` target 404s. Verified: +// removing the declarations and running `npm pack --dry-run` succeeds with +// 41/41 exports subpaths pointing at files that are not in the tarball. +// +// This script is the assertion that closes that gap. It runs from +// `prepublishOnly` (after the build) and in CI, so it is exercised on every +// push rather than only on the rare publish. +import { existsSync, readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { dirname, join } from 'node:path' + +const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..') +const pkg = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')) + +const problems = [] +let checked = 0 + +for (const [subpath, entry] of Object.entries(pkg.exports ?? {})) { + for (const condition of ['import', 'types']) { + const relative = entry?.[condition] + if (!relative) { + problems.push(`exports["${subpath}"] declares no "${condition}" target`) + continue + } + checked++ + if (!existsSync(join(pkgRoot, relative))) { + problems.push(`exports["${subpath}"].${condition} -> ${relative} does not exist`) + } + } +} + +// `files` decides what actually ships; a target outside it is unreachable to a +// consumer even when it exists on disk here. +const shipped = pkg.files ?? [] +if (!shipped.includes('dist')) { + problems.push('package.json#files does not include "dist", so no export target would ship') +} + +if (problems.length > 0) { + console.error(`dist verification failed (${problems.length} problem(s)):`) + for (const problem of problems) console.error(` - ${problem}`) + console.error('\nRun `npm run build --workspace=@codeburn/core` and re-check.') + process.exit(1) +} + +console.log( + `dist verified: ${checked} export targets across ${Object.keys(pkg.exports ?? {}).length} subpaths all present`, +) From ed4f84da45a15f1aced5e51d9f689fa8dc377138 Mon Sep 17 00:00:00 2001 From: Paul Logan Date: Mon, 27 Jul 2026 15:22:15 -0700 Subject: [PATCH 6/6] chore: stop emitting declaration maps package.json#files ships dist and schemas but not src, so every emitted .d.ts.map pointed at a source file the consumer never receives. That was 151 files and roughly 135 kB of maps that resolve to nothing. Dropping declarationMap takes the tarball from 389 files to 238 and 376 kB to 354 kB. The declarations themselves are unchanged: 151 .d.ts, all 41 export targets still verified present. --- packages/core/tsconfig.build.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json index b29acc38..5f24448c 100644 --- a/packages/core/tsconfig.build.json +++ b/packages/core/tsconfig.build.json @@ -2,7 +2,10 @@ "extends": "./tsconfig.json", "compilerOptions": { "declaration": true, - "declarationMap": true, + // No declarationMap: `files` ships dist and schemas but not src, so the + // emitted .d.ts.map would point at sources the consumer never receives. + // That is 151 files and ~135 kB of maps that resolve to nothing. + "declarationMap": false, "emitDeclarationOnly": true, "rootDir": "src", "outDir": "dist",