mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 08:53:22 +00:00
* feat(exports): expose runThink synthesis via gbrain/think subpath The think synthesis pipeline (runThink, stripGapsSection, persistSynthesis, maxOutputTokensFor + the ThinkResult/ParsedCitation types) lives in src/core/think/index.ts but is not reachable through the public exports map. Downstream consumers importing `gbrain/think` fail to resolve it, and no other exported entrypoint re-exports runThink. Add `./think` to package.json exports and extend the public-exports contract test (count 20 -> 21; new EXPECTED_EXPORTS row with runtime canaries runThink + stripGapsSection). Test passes 38/38. Left the VERSION / package.json version / CHANGELOG / llms bumps to the maintainer /ship flow to avoid colliding with the version-queue allocator. * fix(ci): bump public-exports guard baseline to 21 for gbrain/think The new ./think subpath grows the exports map to 21 entries; scripts/check-exports-count.sh still pinned EXPECTED_COUNT=20 and exits 1 on growth, failing CI. Co-authored-by: mnemonik-dev <dev@mnemonik.xyz> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Garry Tan <garrytan@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
107 lines
5.0 KiB
TypeScript
107 lines
5.0 KiB
TypeScript
/**
|
|
* Public exports contract test (v0.21.0 — Lane 2 / R2).
|
|
*
|
|
* Reads package.json "exports" at runtime, imports each subpath via the
|
|
* package name ("gbrain/<subpath>") so it actually exercises the
|
|
* resolver — then asserts each resolves AND has at least one canary
|
|
* symbol. Importing from relative paths (e.g. "../src/core/engine.ts")
|
|
* would bypass the exports map and miss resolver/wiring breakage.
|
|
*
|
|
* The canary symbols are concrete values each module re-exports today.
|
|
* If a refactor renames or removes one, this test fails in CI so the
|
|
* downstream consumer (gbrain-evals) doesn't silently break first.
|
|
*
|
|
* To add a new public export: extend `EXPECTED_EXPORTS` below. To
|
|
* REMOVE one: bump gbrain's minor (breaking-interface per CLAUDE.md
|
|
* "Removing any of these is a breaking change going forward").
|
|
*/
|
|
|
|
import { readFileSync } from 'fs';
|
|
import { resolve } from 'path';
|
|
import { describe, expect, test } from 'bun:test';
|
|
|
|
interface ExpectedExport {
|
|
/** Subpath key as it appears in package.json exports. */
|
|
subpath: string;
|
|
/** At least one named export that MUST exist at runtime. Chosen from the
|
|
* module's current surface; if it goes away, that's a breaking change. */
|
|
canary: string[];
|
|
}
|
|
|
|
/**
|
|
* Canary symbols pinned to the v0.21.0 contract. Changes to this list
|
|
* are intentional breaking changes to the public exports surface.
|
|
*/
|
|
const EXPECTED_EXPORTS: ExpectedExport[] = [
|
|
{ subpath: 'gbrain', canary: [] }, // root "." export; no single canary — just require import success
|
|
{ subpath: 'gbrain/engine', canary: ['clampSearchLimit', 'MAX_SEARCH_LIMIT'] },
|
|
{ subpath: 'gbrain/types', canary: ['GBrainError'] },
|
|
{ subpath: 'gbrain/operations', canary: ['operations', 'OperationError'] },
|
|
{ subpath: 'gbrain/minions', canary: [] }, // barrel module; re-exports many names
|
|
{ subpath: 'gbrain/engine-factory', canary: [] }, // factory exports a default creator
|
|
{ subpath: 'gbrain/pglite-engine', canary: ['PGLiteEngine'] },
|
|
{ subpath: 'gbrain/link-extraction', canary: ['extractEntityRefs', 'extractPageLinks'] },
|
|
{ subpath: 'gbrain/import-file', canary: ['importFromContent'] },
|
|
{ subpath: 'gbrain/transcription', canary: [] },
|
|
{ subpath: 'gbrain/embedding', canary: ['embed'] },
|
|
{ subpath: 'gbrain/config', canary: ['loadConfig'] },
|
|
{ subpath: 'gbrain/markdown', canary: ['splitBody', 'parseMarkdown', 'serializeMarkdown'] },
|
|
{ subpath: 'gbrain/backoff', canary: [] },
|
|
{ subpath: 'gbrain/search/hybrid', canary: ['hybridSearch', 'rrfFusion'] },
|
|
{ subpath: 'gbrain/search/expansion', canary: ['expandQuery'] },
|
|
{ subpath: 'gbrain/think', canary: ['runThink', 'stripGapsSection'] },
|
|
{ subpath: 'gbrain/ai/gateway', canary: ['configureGateway', 'embed'] },
|
|
{ subpath: 'gbrain/extract', canary: [] },
|
|
{ subpath: 'gbrain/ingestion', canary: ['INGESTION_SOURCE_API_VERSION', 'validateIngestionEvent', 'computeContentHash'] },
|
|
{ subpath: 'gbrain/ingestion/test-harness', canary: ['IngestionTestHarness', 'expectEvent'] },
|
|
];
|
|
|
|
function readPackageExports(): Record<string, string> {
|
|
const pkgPath = resolve(import.meta.dir, '..', 'package.json');
|
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
return pkg.exports as Record<string, string>;
|
|
}
|
|
|
|
describe('public exports — package.json exports map', () => {
|
|
test('has the expected number of subpaths (v0.21.0 locks the surface)', () => {
|
|
const exports = readPackageExports();
|
|
const count = Object.keys(exports).length;
|
|
// Adding new exports: increment this + add to EXPECTED_EXPORTS below.
|
|
// Removing exports: see CLAUDE.md "Removing any of these is a
|
|
// breaking change going forward" — bump minor and update this count.
|
|
expect(count).toBe(21);
|
|
});
|
|
|
|
test('EXPECTED_EXPORTS list matches the exports map exactly (no drift)', () => {
|
|
const exports = readPackageExports();
|
|
const exportedSubpaths = Object.keys(exports).map(k => (k === '.' ? 'gbrain' : `gbrain${k.slice(1)}`)).sort();
|
|
const expectedSubpaths = EXPECTED_EXPORTS.map(e => e.subpath).sort();
|
|
expect(expectedSubpaths).toEqual(exportedSubpaths);
|
|
});
|
|
});
|
|
|
|
describe('public exports — every subpath resolves via package name', () => {
|
|
for (const entry of EXPECTED_EXPORTS) {
|
|
test(`${entry.subpath} imports without throwing`, async () => {
|
|
// Package-path import goes through the exports map — bypassing a
|
|
// broken/removed subpath surfaces here. Importing "../src/..."
|
|
// would resolve via filesystem and miss the contract.
|
|
const mod = await import(entry.subpath);
|
|
expect(mod).toBeDefined();
|
|
expect(typeof mod).toBe('object');
|
|
});
|
|
|
|
if (entry.canary.length > 0) {
|
|
test(`${entry.subpath} exports canary symbols: ${entry.canary.join(', ')}`, async () => {
|
|
const mod = await import(entry.subpath);
|
|
for (const name of entry.canary) {
|
|
expect(mod).toHaveProperty(name);
|
|
// Must be something truthy — value, class, or function. Not
|
|
// just a TypeScript type (those don't exist at runtime).
|
|
expect(mod[name]).toBeDefined();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|