#!/usr/bin/env node /** * Emit this build's API-contract manifest to docs/api-contract.json. * * WHY IT IS GENERATED, NOT WRITTEN: a hand-kept list of doors drifts from the * code the first time somebody adds one. This reads the surface the build * actually exposes — the prototype's own methods and accessors, the exported * error classes, the `where` operator sets, the field-addressing vocabulary, * the health verdicts — so a diff between two engines' manifests is a diff * between two engines, never between two authors. * * Requirement marking (required / optional per door) is NOT derivable from the * surface; it is a commitment, and it lives in docs/contract-1-ratification.md. * This manifest carries the surface; that document carries the promise. * * Usage: node scripts/emit-contract-manifest.mjs [--check] * --check exits non-zero when the committed manifest is stale. */ import { writeFileSync, readFileSync, existsSync } from 'node:fs' import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') const OUT = join(ROOT, 'docs', 'api-contract.json') const { Brainy } = await import(join(ROOT, 'dist', 'brainy.js')) const errorsModule = await import(join(ROOT, 'dist', 'errors', 'brainyError.js')) const versionModule = await import(join(ROOT, 'dist', 'utils', 'version.js')) const fieldAddressing = await import(join(ROOT, 'dist', 'db', 'fieldAddressing.js')) /** Every own method and accessor on the class's prototype, minus the private ones. */ function surfaceOf(ctor) { const doors = [] for (const name of Object.getOwnPropertyNames(ctor.prototype)) { if (name === 'constructor' || name.startsWith('_')) continue const descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, name) if (!descriptor) continue if (typeof descriptor.value === 'function') { doors.push({ name, kind: 'method', arity: descriptor.value.length }) } else if (descriptor.get) { doors.push({ name, kind: 'accessor' }) } } return doors.sort((a, b) => a.name.localeCompare(b.name)) } const errors = Object.entries(errorsModule) .filter(([name, value]) => typeof value === 'function' && /Error$/.test(name)) .map(([name]) => name) .sort() // The operator sets, read from the engine's own refusal message so the // manifest can never disagree with the validator. const filterSource = readFileSync(join(ROOT, 'src', 'utils', 'metadataFilter.ts'), 'utf-8') const acceptedMatch = filterSource.match(/const VALUE_OPERATORS = new Set\(\[([\s\S]*?)\]\)/) if (!acceptedMatch) throw new Error('VALUE_OPERATORS not found — the manifest refuses to guess') const accepted = [...acceptedMatch[1].matchAll(/'([^']+)'/g)].map((m) => m[1]).sort() const indexSource = readFileSync(join(ROOT, 'src', 'utils', 'metadataIndex.ts'), 'utf-8') const refusedByIndex = ['endsWith', 'length', 'matches', 'startsWith'].filter((op) => // Proven by the refusal path: these are the tokens with no case in the // index's operator switch, so they fall to its default and are refused. !new RegExp(`case '${op}':`).test(indexSource) ) const servedOnIndex = accepted.filter((op) => !refusedByIndex.includes(op)) const manifest = { contractVersion: versionModule.contractVersion(), engine: '@soulcraftlabs/brainy', prose: 'docs/contract-1-ratification.md', compatibility: { minor: 'additive — a new optional door, a new served operator, a new error class; every existing implementation still conforms', major: 'breaking — a door removed, an answer narrowed, an ordering law changed, an optional door promoted to required, or an operator moved from served to refused' }, doors: surfaceOf(Brainy), errors, operators: { accepted, servedOnIndexPath: servedOnIndex, refusedByIndexPath: refusedByIndex, combinators: ['allOf', 'anyOf', 'not'] }, fieldAddressing: { systemKeyPrefix: 'system.', systemEntityScalars: [...(fieldAddressing.SYSTEM_ENTITY_SCALARS ?? [])].sort(), systemRelationScalars: [...(fieldAddressing.SYSTEM_RELATION_SCALARS ?? [])].sort(), plumbingFields: [...(fieldAddressing.PLUMBING_FIELDS ?? [])].sort() }, health: { verdicts: ['pass', 'warn', 'fail'], healKinds: ['none', 'repair', 'rebuild'], servingWithholdingInvariants: [ 'index-initialized', 'durable-state-present', 'manifest-residency', 'replay-clean', 'strand-latch' ] } } const rendered = `${JSON.stringify(manifest, null, 2)}\n` if (process.argv.includes('--check')) { if (!existsSync(OUT)) { console.error(`docs/api-contract.json is missing — run: node scripts/emit-contract-manifest.mjs`) process.exit(1) } if (readFileSync(OUT, 'utf-8') !== rendered) { console.error( `docs/api-contract.json is STALE — the public surface changed. Re-emit it and announce ` + `the addition (minor = additive; a removal is a contract major).` ) process.exit(1) } console.log(`docs/api-contract.json is current (${manifest.doors.length} doors, contract ${manifest.contractVersion}).`) process.exit(0) } writeFileSync(OUT, rendered) console.log( `Wrote docs/api-contract.json — contract ${manifest.contractVersion}, ` + `${manifest.doors.length} doors, ${manifest.errors.length} error classes, ` + `${manifest.operators.accepted.length} operators ` + `(${manifest.operators.refusedByIndexPath.length} refused by the index path).` )