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.
This commit is contained in:
parent
aad9e2eeb1
commit
8a5c1245a7
5 changed files with 148 additions and 9 deletions
|
|
@ -10,6 +10,7 @@ import { TransformerEmbedding } from '../src/utils/embedding.js'
|
||||||
import * as fs from 'fs/promises'
|
import * as fs from 'fs/promises'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
import { resolveDeterministicStamp } from './lib/deterministicStamp.js'
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
|
@ -98,12 +99,21 @@ async function buildEmbeddedPatterns() {
|
||||||
const uint8 = new Uint8Array(buffer)
|
const uint8 = new Uint8Array(buffer)
|
||||||
const base64 = Buffer.from(uint8).toString('base64')
|
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
|
// Generate TypeScript file with everything embedded
|
||||||
const tsContent = `/**
|
const tsContent = `/**
|
||||||
* 🧠 BRAINY EMBEDDED PATTERNS
|
* 🧠 BRAINY EMBEDDED PATTERNS
|
||||||
*
|
*
|
||||||
* AUTO-GENERATED - DO NOT EDIT
|
* AUTO-GENERATED - DO NOT EDIT
|
||||||
* Generated: ${new Date().toISOString()}
|
* Generated: ${generatedStamp}
|
||||||
* Patterns: ${libraryData.patterns.length}
|
* Patterns: ${libraryData.patterns.length}
|
||||||
* Coverage: 94-98% of all queries
|
* Coverage: 94-98% of all queries
|
||||||
*
|
*
|
||||||
|
|
@ -197,7 +207,6 @@ prodLog.info(\`🧠 Brainy Pattern Library loaded: \${EMBEDDED_PATTERNS.length}
|
||||||
`
|
`
|
||||||
|
|
||||||
// Write the TypeScript file
|
// Write the TypeScript file
|
||||||
const outputPath = path.join(__dirname, '..', 'src', 'neural', 'embeddedPatterns.ts')
|
|
||||||
await fs.writeFile(outputPath, tsContent)
|
await fs.writeFile(outputPath, tsContent)
|
||||||
|
|
||||||
// Report statistics
|
// Report statistics
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import * as fs from 'fs/promises'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import { NounType, VerbType } from '../src/types/graphTypes.js'
|
import { NounType, VerbType } from '../src/types/graphTypes.js'
|
||||||
|
import { resolveDeterministicStamp } from './lib/deterministicStamp.js'
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
|
@ -373,12 +374,24 @@ async function buildTypeEmbeddings() {
|
||||||
const uint8 = new Uint8Array(buffer)
|
const uint8 = new Uint8Array(buffer)
|
||||||
const base64 = Buffer.from(uint8).toString('base64')
|
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
|
// Generate TypeScript file
|
||||||
const tsContent = `/**
|
const tsContent = `/**
|
||||||
* 🧠 BRAINY EMBEDDED TYPE EMBEDDINGS
|
* 🧠 BRAINY EMBEDDED TYPE EMBEDDINGS
|
||||||
*
|
*
|
||||||
* AUTO-GENERATED - DO NOT EDIT
|
* AUTO-GENERATED - DO NOT EDIT
|
||||||
* Generated: ${new Date().toISOString()}
|
* Generated: ${generatedStamp}
|
||||||
* Noun Types: ${nounTypes.length}
|
* Noun Types: ${nounTypes.length}
|
||||||
* Verb Types: ${verbTypes.length}
|
* Verb Types: ${verbTypes.length}
|
||||||
*
|
*
|
||||||
|
|
@ -395,7 +408,7 @@ export const TYPE_METADATA = {
|
||||||
verbTypes: ${verbTypes.length},
|
verbTypes: ${verbTypes.length},
|
||||||
totalTypes: ${totalTypes},
|
totalTypes: ${totalTypes},
|
||||||
embeddingDimensions: ${embeddingDim},
|
embeddingDimensions: ${embeddingDim},
|
||||||
generatedAt: "${new Date().toISOString()}",
|
generatedAt: "${generatedStamp}",
|
||||||
sizeBytes: {
|
sizeBytes: {
|
||||||
embeddings: ${buffer.byteLength},
|
embeddings: ${buffer.byteLength},
|
||||||
base64: ${base64.length}
|
base64: ${base64.length}
|
||||||
|
|
@ -494,7 +507,6 @@ prodLog.info(\`🧠 Brainy Type Embeddings loaded: \${TYPE_METADATA.nounTypes} n
|
||||||
`
|
`
|
||||||
|
|
||||||
// Write the TypeScript file
|
// Write the TypeScript file
|
||||||
const outputPath = path.join(__dirname, '..', 'src', 'neural', 'embeddedTypeEmbeddings.ts')
|
|
||||||
await fs.writeFile(outputPath, tsContent)
|
await fs.writeFile(outputPath, tsContent)
|
||||||
|
|
||||||
// Report statistics
|
// Report statistics
|
||||||
|
|
|
||||||
118
scripts/lib/deterministicStamp.ts
Normal file
118
scripts/lib/deterministicStamp.ts
Normal file
|
|
@ -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: <ISO timestamp>` 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
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* 🧠 BRAINY EMBEDDED PATTERNS
|
* 🧠 BRAINY EMBEDDED PATTERNS
|
||||||
*
|
*
|
||||||
* AUTO-GENERATED - DO NOT EDIT
|
* AUTO-GENERATED - DO NOT EDIT
|
||||||
* Generated: 2026-07-02T21:43:26.976Z
|
* Generated: 2025-09-29T10:10:00-07:00
|
||||||
* Patterns: 220
|
* Patterns: 220
|
||||||
* Coverage: 94-98% of all queries
|
* Coverage: 94-98% of all queries
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* 🧠 BRAINY EMBEDDED TYPE EMBEDDINGS
|
* 🧠 BRAINY EMBEDDED TYPE EMBEDDINGS
|
||||||
*
|
*
|
||||||
* AUTO-GENERATED - DO NOT EDIT
|
* AUTO-GENERATED - DO NOT EDIT
|
||||||
* Generated: 2026-02-09T16:59:48.867Z
|
* Generated: 2026-06-29T10:04:19-07:00
|
||||||
* Noun Types: 42
|
* Noun Types: 42
|
||||||
* Verb Types: 127
|
* Verb Types: 127
|
||||||
*
|
*
|
||||||
|
|
@ -19,7 +19,7 @@ export const TYPE_METADATA = {
|
||||||
verbTypes: 127,
|
verbTypes: 127,
|
||||||
totalTypes: 169,
|
totalTypes: 169,
|
||||||
embeddingDimensions: 384,
|
embeddingDimensions: 384,
|
||||||
generatedAt: "2026-02-09T16:59:48.867Z",
|
generatedAt: "2026-06-29T10:04:19-07:00",
|
||||||
sizeBytes: {
|
sizeBytes: {
|
||||||
embeddings: 259584,
|
embeddings: 259584,
|
||||||
base64: 346112
|
base64: 346112
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue