From 8a5c1245a7e9a8c4c4a7307ef3f0b60355d76551 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Thu, 27 Aug 2026 09:18:45 -0700 Subject: [PATCH] build: derive generated-file stamps from git commit time, not wall clock Two builds of the same source tree could publish different artifacts because buildEmbeddedPatterns.ts and buildTypeEmbeddings.ts stamped their generated output with new Date().toISOString(). Route both generators' "Generated:" header and the generatedAt runtime field through a shared resolver: newest git commit time among the generator's inputs (script + source data), falling back to the stamp already present in the previous output when git is unavailable (e.g. a published tarball build), and finally to a fixed epoch value. Every fallback logs to stderr so degradation is never silent. Regenerated both committed output files once so the tree carries deterministic stamps; no other content changed. --- scripts/buildEmbeddedPatterns.ts | 15 +++- scripts/buildTypeEmbeddings.ts | 18 +++- scripts/lib/deterministicStamp.ts | 118 +++++++++++++++++++++++++++ src/neural/embeddedPatterns.ts | 2 +- src/neural/embeddedTypeEmbeddings.ts | 4 +- 5 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 scripts/lib/deterministicStamp.ts 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