v0.31.4.1 chore: align VERSION + package.json with #795 + mandate 4-segment versions (#815)

* v0.31.4.1 chore: align VERSION/package.json with #795 + mandate MAJOR.MINOR.PATCH.MICRO

PR #795 (takes v2) landed on master with `v0.31.4` in its commit subject but
never bumped VERSION, package.json, or CHANGELOG.md. Master shipped at 0.31.3.

This corrective release:
- Bumps VERSION + package.json to 0.31.4.1 (the dot-suffix follow-up channel
  documented in CLAUDE.md, so the patch number doesn't churn to 0.31.5)
- Adds the v0.31.4.1 CHANGELOG entry covering takes v2 (lessons from a 100K-take
  production extraction), the auth-on-Postgres regression fix, and the new
  `gbrain eval takes-quality` CLI surface
- Updates CLAUDE.md to mandate `MAJOR.MINOR.PATCH.MICRO` for every new release.
  Historical 3-segment versions in git log + migration filenames stay valid;
  do not rewrite. Going forward only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: regenerate llms-full.txt for v0.31.4.1 doc edits

The build-llms regen-drift guard caught that llms-full.txt was stale relative
to the CHANGELOG + CLAUDE.md edits in the prior commit. Per CLAUDE.md the
bundle is auto-derived: bump VERSION/CHANGELOG/CLAUDE.md, then run
`bun run build:llms`. Did the second part now.

llms.txt unchanged (it's just the curated index). Only llms-full.txt picks
up the v0.31.4.1 CHANGELOG entry and the new "Version format is mandatory"
section in CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): exclude *.serial.test.ts from test-shard.sh hash buckets

Root cause of test (2) failing on the v0.31.4.1 PR (and on master since
#795 landed): CI's scripts/test-shard.sh hashed every test file into 4
shards via FNV-1a, INCLUDING *.serial.test.ts files. Serial files share
file-wide state (top-level mock.module, module singletons) that's
supposed to be quarantined by the .serial.test.ts naming + local
run-serial-tests.sh running them at --max-concurrency=1.

In CI the quarantine didn't apply. eval-takes-quality-runner.serial.test.ts
(new in #795) hashes into shard 2, where it calls:

  mock.module('../src/core/ai/gateway.ts', () => ({
    chat: async (opts) => { ... },
    configureGateway: () => undefined,
  }));

That replaces every export of gateway.ts at module-load time for the
WHOLE shard process. voyage-multimodal.test.ts also lives in shard 2
(both files happen to hash there), and it imports `embedMultimodal` from
gateway.ts. After the serial file loads, `embedMultimodal` is undefined
inside the shard process, and all 18 of voyage-multimodal's
embedMultimodal tests fail. Tests still passed locally because
run-unit-shard.sh excludes .serial files from its parallel pass.

Fix:
  - scripts/test-shard.sh: add `-not -name '*.serial.test.ts'` to the
    find expression so serial files no longer compete for shard buckets.
    Add --dry-run-list flag to mirror run-unit-shard.sh's interface so
    the regression test can introspect without spawning bun test.
  - .github/workflows/test.yml: add a `bun run test:serial` step that
    runs on shard 1 (which already runs `bun run verify`). Uses the
    existing scripts/run-serial-tests.sh which invokes bun test at
    --max-concurrency=1, matching local behavior.
  - test/scripts/test-shard.slow.test.ts: 4 regression cases that pin
    the contract (no serial files in any shard, no e2e files in any
    shard, plain files partitioned without overlap). .slow.test.ts
    because it shells out 4× with pure-bash FNV-1a hashing (~14s
    wallclock); excluded from the local fast loop, runs in CI via the
    same hash bucketing as other slow tests.
  - CLAUDE.md: update the CI vs local divergence section so this
    intentional asymmetry is documented going forward.

Build-llms drift in test (1) was fixed in the prior commit (c99a4af1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: regenerate llms-full.txt for the CI-fix CLAUDE.md edits

The prior commit updated the "CI vs local: intentionally divergent file sets"
section in CLAUDE.md, which drifted llms-full.txt. Per CLAUDE.md the bundle
is auto-derived: edit CLAUDE.md, then run `bun run build:llms`. Did the
second part now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-10 11:28:01 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 7267462311
commit 943e7b9dec
8 changed files with 242 additions and 18 deletions
+78
View File
@@ -0,0 +1,78 @@
/**
* Regression test: scripts/test-shard.sh exclusion symmetry.
*
* Pins the contract that CI's hash-bucketed shard script EXCLUDES
* *.serial.test.ts and test/e2e/ from every shard. Serial files share
* file-wide state (top-level mock.module, module singletons) that leaks
* across files in the same `bun test` shard process. Before v0.31.4.1
* they were hashed into the same buckets as parallel files, which broke
* the quarantine — `eval-takes-quality-runner.serial.test.ts` stubbed
* `gateway.ts` and broke every `gateway.embedMultimodal` test in
* `voyage-multimodal.test.ts` on shard 2.
*
* Without this guard, a future refactor that drops the `-not -name
* '*.serial.test.ts'` clause from test-shard.sh would silently undo the
* fix and re-introduce the contention flake.
*/
import { describe, it, expect, beforeAll } from 'bun:test';
import { execFileSync } from 'child_process';
import { resolve } from 'path';
const REPO_ROOT = resolve(import.meta.dir, '..', '..');
const SHARD_SH = resolve(REPO_ROOT, 'scripts/test-shard.sh');
// Pure-bash FNV-1a per shard takes ~4s on a M-series Mac because the script
// computes a hash for every test file. Compute all 4 shards once in beforeAll
// and reuse across cases so the suite finishes in one shell-out per shard.
const shardCache: Record<number, string[]> = {};
function dryRunList(shard: number, total: number): string[] {
if (shardCache[shard]) return shardCache[shard];
const out = execFileSync('bash', [SHARD_SH, '--dry-run-list', String(shard), String(total)], {
cwd: REPO_ROOT,
encoding: 'utf-8',
});
shardCache[shard] = out.split('\n').map(s => s.trim()).filter(Boolean);
return shardCache[shard];
}
describe('test-shard.sh exclusion symmetry', () => {
beforeAll(() => {
for (const shard of [1, 2, 3, 4]) dryRunList(shard, 4);
}, 60_000);
it('includes plain *.test.ts files in at least one shard', () => {
const allFiles = [1, 2, 3, 4].flatMap(s => dryRunList(s, 4));
expect(allFiles.length).toBeGreaterThan(0);
expect(allFiles.some(f => /\.test\.ts$/.test(f) && !/\.serial\.test\.ts$/.test(f))).toBe(true);
});
it('excludes every *.serial.test.ts file from every shard', () => {
for (const shard of [1, 2, 3, 4]) {
const files = dryRunList(shard, 4);
const leaks = files.filter(f => /\.serial\.test\.ts$/.test(f));
expect(leaks, `shard ${shard} contains serial files`).toEqual([]);
}
});
it('excludes the test/e2e/ subtree from every shard', () => {
for (const shard of [1, 2, 3, 4]) {
const files = dryRunList(shard, 4);
const leaks = files.filter(f => f.startsWith('test/e2e/'));
expect(leaks, `shard ${shard} contains e2e files`).toEqual([]);
}
});
it('partitions plain files across shards without overlap', () => {
const seen = new Map<string, number>();
for (const shard of [1, 2, 3, 4]) {
for (const f of dryRunList(shard, 4)) {
if (seen.has(f)) {
throw new Error(`file ${f} appears in shard ${seen.get(f)} AND shard ${shard}`);
}
seen.set(f, shard);
}
}
expect(seen.size).toBeGreaterThan(0);
});
});