feat(8.0): brain.fillSubtypes migration helper + pre-RC1 gap closure

- brain.fillSubtypes(rules): idempotent subtype back-fill for pre-8.0 data.
  One rule per NounType/VerbType (literal default or per-entry function);
  fills only entries still missing a subtype through the public update()/
  updateRelation() paths; returns { scanned, filled, skipped, errors, byType }.
  Full unit suite in tests/unit/brainy/fill-subtypes.test.ts.
- Fix getNouns/getVerbs pagination hasMore (peek one past the window) —
  was permanently false, silently truncating every multi-page walk.
- find({ near }) without near.id now throws a teaching error instead of an
  opaque storage sharding failure; CLI --threshold without --near applies a
  plain score floor.
- CLI init/close audit: every one-shot command init()s, close()s, and exits
  explicitly; delete the unmaintained interactive REPL; replace the cloud-era
  storage subcommands with status/batch-delete; new types/validate commands.
- requireSubtype JSDoc now documents the 8.0 default-on contract; audit()
  recommendation points at fillSubtypes.
- Docs: data-storage-architecture rewritten to the real 8.0 on-disk layout;
  README storage section reflects filesystem+memory and snapshots; eli5 and
  SEMANTIC_VFS /as-of/ semantics corrected; internal tracker IDs and
  .strategy references scrubbed from published files.
This commit is contained in:
David Snelling 2026-06-11 10:42:34 -07:00
parent 9b0f4acd5b
commit c44678390e
30 changed files with 1517 additions and 3226 deletions

View file

@ -22,8 +22,7 @@ interface StatsOptions extends UtilityOptions {
}
interface CleanOptions extends UtilityOptions {
removeOrphans?: boolean
rebuildIndex?: boolean
force?: boolean
}
interface BenchmarkOptions extends UtilityOptions {
@ -63,6 +62,7 @@ export const utilityCommands = {
try {
const brain = getBrainy()
await brain.init()
const nounCount = await brain.getNounCount()
const verbCount = await brain.getVerbCount()
const memUsage = process.memoryUsage()
@ -77,7 +77,11 @@ export const utilityCommands = {
if (options.json) {
formatOutput(stats, options)
return
// close() releases the writer lock and indexes, but global timers
// (UnifiedCache bookkeeping, PathResolver stats) keep the event loop
// alive. CLI commands are one-shot — exit explicitly.
await brain.close()
process.exit(0)
}
console.log(chalk.cyan('\n📊 Database Statistics\n'))
@ -112,7 +116,10 @@ export const utilityCommands = {
)
console.log(memTable.toString())
// One-shot command — see the --json branch for why the explicit exit.
await brain.close()
process.exit(0)
} catch (error: any) {
spinner.fail('Failed to gather statistics')
console.error(chalk.red(error.message))
@ -121,30 +128,33 @@ export const utilityCommands = {
},
/**
* Clean and optimize database
* Clear the database (all entities, relationships, and indexes).
* Destructive asks for confirmation unless --force is passed.
*/
async clean(options: CleanOptions) {
const spinner = ora('Cleaning database...').start()
let spinner: ReturnType<typeof ora> | null = null
try {
const brain = getBrainy()
// Destructive operation — confirm first (skipped with --force).
if (!options.force) {
const inquirer = (await import('inquirer')).default
const { confirm } = await inquirer.prompt([{
type: 'confirm',
name: 'confirm',
message: chalk.yellow('⚠️ Permanently delete ALL data (entities, relationships, indexes)?'),
default: false
}])
// For now, only support full clear
// removeOrphans and rebuildIndex would require new Brainy APIs
if (options.removeOrphans || options.rebuildIndex) {
spinner.warn('Advanced cleanup options not yet implemented')
console.log(chalk.yellow('\n⚠ Advanced cleanup features coming soon:'))
console.log(chalk.dim(' • --remove-orphans: Remove disconnected items'))
console.log(chalk.dim(' • --rebuild-index: Rebuild vector index'))
console.log(chalk.dim('\nUse "brainy clean" without options to clear the database'))
return
if (!confirm) {
console.log(chalk.yellow('Clean cancelled'))
process.exit(0)
}
}
// Show warning before clearing
console.log(chalk.yellow('\n⚠ WARNING: This will permanently delete ALL data!'))
const brain = getBrainy()
// Clear all data (entities, relationships, and every index)
spinner.text = 'Clearing all data...'
spinner = ora('Clearing all data...').start()
await brain.init()
await brain.clear()
@ -163,7 +173,7 @@ export const utilityCommands = {
await brain.close()
process.exit(0)
} catch (error: any) {
spinner.fail('Cleanup failed')
if (spinner) spinner.fail('Cleanup failed')
console.error(chalk.red(error.message))
process.exit(1)
}
@ -185,7 +195,8 @@ export const utilityCommands = {
try {
const brain = getBrainy()
await brain.init()
// Benchmark different operations
const benchmarks = [
{ name: 'add', enabled: operations === 'all' || operations.includes('add') },
@ -205,7 +216,8 @@ export const utilityCommands = {
switch (bench.name) {
case 'add':
await brain.add({ data: `Test item ${i}`, type: NounType.Thing, metadata: { benchmark: true } })
// 8.0 requires a subtype on every write by default.
await brain.add({ data: `Test item ${i}`, type: NounType.Thing, subtype: 'benchmark', metadata: { benchmark: true } })
break
case 'search':
await brain.find({ query: 'test', limit: 10 })
@ -284,7 +296,10 @@ export const utilityCommands = {
} else {
formatOutput(results, options)
}
// One-shot command — see stats() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
console.error(chalk.red('Benchmark failed:'), error.message)
process.exit(1)