diff --git a/scripts/buildEmbeddedPatterns.ts b/scripts/buildEmbeddedPatterns.ts index 73e51224..c046df45 100644 --- a/scripts/buildEmbeddedPatterns.ts +++ b/scripts/buildEmbeddedPatterns.ts @@ -10,6 +10,7 @@ import { TransformerEmbedding } from '../src/utils/embedding.js' import * as fs from 'fs/promises' import * as path from 'path' import { fileURLToPath } from 'url' +import { resolveDeterministicStamp } from './lib/deterministicStamp.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -97,13 +98,22 @@ async function buildEmbeddedPatterns() { // Convert to base64 for embedding in TypeScript const uint8 = new Uint8Array(buffer) const base64 = Buffer.from(uint8).toString('base64') - + + // Deterministic stamp: derived from the git commit time of this + // generator's inputs, never from wall-clock time — two builds of the + // same source tree must produce byte-identical output. + const outputPath = path.join(__dirname, '..', 'src', 'neural', 'embeddedPatterns.ts') + const generatedStamp = resolveDeterministicStamp( + [path.join(__dirname, 'buildEmbeddedPatterns.ts'), libraryPath], + outputPath + ) + // Generate TypeScript file with everything embedded const tsContent = `/** * 🧠 BRAINY EMBEDDED PATTERNS * * AUTO-GENERATED - DO NOT EDIT - * Generated: ${new Date().toISOString()} + * Generated: ${generatedStamp} * Patterns: ${libraryData.patterns.length} * Coverage: 94-98% of all queries * @@ -197,7 +207,6 @@ prodLog.info(\`🧠 Brainy Pattern Library loaded: \${EMBEDDED_PATTERNS.length} ` // Write the TypeScript file - const outputPath = path.join(__dirname, '..', 'src', 'neural', 'embeddedPatterns.ts') await fs.writeFile(outputPath, tsContent) // Report statistics diff --git a/scripts/buildTypeEmbeddings.ts b/scripts/buildTypeEmbeddings.ts index 61bcf238..688d6ac1 100644 --- a/scripts/buildTypeEmbeddings.ts +++ b/scripts/buildTypeEmbeddings.ts @@ -11,6 +11,7 @@ import * as fs from 'fs/promises' import * as path from 'path' import { fileURLToPath } from 'url' import { NounType, VerbType } from '../src/types/graphTypes.js' +import { resolveDeterministicStamp } from './lib/deterministicStamp.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -373,12 +374,24 @@ async function buildTypeEmbeddings() { const uint8 = new Uint8Array(buffer) const base64 = Buffer.from(uint8).toString('base64') + // Deterministic stamp: derived from the git commit time of this + // generator's inputs, never from wall-clock time — two builds of the + // same source tree must produce byte-identical output. + const outputPath = path.join(__dirname, '..', 'src', 'neural', 'embeddedTypeEmbeddings.ts') + const generatedStamp = resolveDeterministicStamp( + [ + path.join(__dirname, 'buildTypeEmbeddings.ts'), + path.join(__dirname, '..', 'src', 'types', 'graphTypes.ts') + ], + outputPath + ) + // Generate TypeScript file const tsContent = `/** * 🧠 BRAINY EMBEDDED TYPE EMBEDDINGS * * AUTO-GENERATED - DO NOT EDIT - * Generated: ${new Date().toISOString()} + * Generated: ${generatedStamp} * Noun Types: ${nounTypes.length} * Verb Types: ${verbTypes.length} * @@ -395,7 +408,7 @@ export const TYPE_METADATA = { verbTypes: ${verbTypes.length}, totalTypes: ${totalTypes}, embeddingDimensions: ${embeddingDim}, - generatedAt: "${new Date().toISOString()}", + generatedAt: "${generatedStamp}", sizeBytes: { embeddings: ${buffer.byteLength}, base64: ${base64.length} @@ -494,7 +507,6 @@ prodLog.info(\`🧠 Brainy Type Embeddings loaded: \${TYPE_METADATA.nounTypes} n ` // Write the TypeScript file - const outputPath = path.join(__dirname, '..', 'src', 'neural', 'embeddedTypeEmbeddings.ts') await fs.writeFile(outputPath, tsContent) // Report statistics diff --git a/scripts/lib/deterministicStamp.ts b/scripts/lib/deterministicStamp.ts new file mode 100644 index 00000000..c2a66604 --- /dev/null +++ b/scripts/lib/deterministicStamp.ts @@ -0,0 +1,118 @@ +/** + * Deterministic generation-stamp resolution for Brainy's build-time code + * generators. + * + * Two builds of the same source tree must produce byte-identical output. + * A wall-clock stamp (`new Date()`) breaks that guarantee, so every + * generator that writes a "Generated:" header or a `generatedAt` field + * into its output must resolve the stamp through this module instead. + * + * Resolution order: + * 1. The newest git commit timestamp among the generator's input files + * (the generator script itself always counts as an input). + * 2. If git metadata is unavailable (for example, building from a + * published npm tarball with no `.git` directory), the stamp already + * recorded in the previously generated output file. + * 3. If neither is available, the fixed epoch string + * `1970-01-01T00:00:00.000Z`. + * + * Every fallback logs a line to stderr — deterministic degradation is + * loud, never a silent divergence. + */ + +import { execFileSync } from 'child_process' +import * as fs from 'fs' + +const EPOCH_STAMP = '1970-01-01T00:00:00.000Z' +const STAMP_PATTERN = /\*\s*Generated:\s*(\S+)/ + +/** + * Resolve the deterministic stamp for a generator run. + * + * @param inputPaths Absolute paths to every file whose content determines + * the generator's output, including the generator script itself. + * @param previousOutputPath Absolute path to the previously generated + * file, used for the existing-stamp fallback when git is unavailable. + * @returns An ISO-8601 timestamp string that is deterministic for a given + * source tree. + */ +export function resolveDeterministicStamp( + inputPaths: string[], + previousOutputPath: string +): string { + const gitStamp = newestGitCommitTimestamp(inputPaths) + if (gitStamp) { + return gitStamp + } + + const existingStamp = readExistingStamp(previousOutputPath) + if (existingStamp) { + process.stderr.write( + `[deterministic-stamp] no git commit history found for generator inputs; ` + + `reusing existing stamp from ${previousOutputPath}: ${existingStamp}\n` + ) + return existingStamp + } + + process.stderr.write( + `[deterministic-stamp] no git commit history and no previous output at ` + + `${previousOutputPath}; falling back to fixed epoch stamp ${EPOCH_STAMP}\n` + ) + return EPOCH_STAMP +} + +/** + * Find the newest git commit timestamp among the given input paths. + * Returns null if git is unavailable, the tree is not a git repository, + * or none of the inputs have any commit history yet. + */ +function newestGitCommitTimestamp(inputPaths: string[]): string | null { + let newest: string | null = null + + for (const inputPath of inputPaths) { + if (!fs.existsSync(inputPath)) { + continue + } + + let out: string + try { + out = execFileSync( + 'git', + ['log', '-1', '--format=%cI', '--', inputPath], + { stdio: ['ignore', 'pipe', 'ignore'] } + ) + .toString() + .trim() + } catch { + // git missing, not a repository, or no permissions — handled by the + // caller's fallback chain. + continue + } + + if (!out) { + // Path exists but has no commit history yet (e.g. newly created, + // uncommitted file). + continue + } + + if (!newest || new Date(out).getTime() > new Date(newest).getTime()) { + newest = out + } + } + + return newest +} + +/** + * Parse the `* Generated: ` header out of a previously + * generated file, if one exists. + */ +function readExistingStamp(outputPath: string): string | null { + if (!fs.existsSync(outputPath)) { + return null + } + + const content = fs.readFileSync(outputPath, 'utf-8') + const match = content.match(STAMP_PATTERN) + return match ? match[1] : null +} diff --git a/src/neural/embeddedPatterns.ts b/src/neural/embeddedPatterns.ts index c15447e7..4f4339f4 100644 --- a/src/neural/embeddedPatterns.ts +++ b/src/neural/embeddedPatterns.ts @@ -2,7 +2,7 @@ * 🧠 BRAINY EMBEDDED PATTERNS * * AUTO-GENERATED - DO NOT EDIT - * Generated: 2026-07-02T21:43:26.976Z + * Generated: 2025-09-29T10:10:00-07:00 * Patterns: 220 * Coverage: 94-98% of all queries * diff --git a/src/neural/embeddedTypeEmbeddings.ts b/src/neural/embeddedTypeEmbeddings.ts index b5f3546b..5b10116c 100644 --- a/src/neural/embeddedTypeEmbeddings.ts +++ b/src/neural/embeddedTypeEmbeddings.ts @@ -2,7 +2,7 @@ * 🧠 BRAINY EMBEDDED TYPE EMBEDDINGS * * AUTO-GENERATED - DO NOT EDIT - * Generated: 2026-02-09T16:59:48.867Z + * Generated: 2026-06-29T10:04:19-07:00 * Noun Types: 42 * Verb Types: 127 * @@ -19,7 +19,7 @@ export const TYPE_METADATA = { verbTypes: 127, totalTypes: 169, embeddingDimensions: 384, - generatedAt: "2026-02-09T16:59:48.867Z", + generatedAt: "2026-06-29T10:04:19-07:00", sizeBytes: { embeddings: 259584, base64: 346112