mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge origin/master at ecbf75a5e7 into skill catalog hot refresh
This commit is contained in:
File diff suppressed because one or more lines are too long
50
scripts/translation-pairing-git.ts
Normal file
50
scripts/translation-pairing-git.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/** Git-blob operations owned by the bilingual pairing workflow. */
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { createHash } from 'node:crypto'
|
||||
|
||||
const SNAPSHOT_REF_PREFIX = 'refs/dsh/translation-pairing/snapshots'
|
||||
|
||||
/** Full SHA-1 Git blob hash (the 40-hex format used by pairing records). */
|
||||
export function gitBlobHash(content: Buffer): string {
|
||||
const hash = createHash('sha1')
|
||||
hash.update(`blob ${content.byteLength}\0`)
|
||||
hash.update(content)
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
function runGit(root: string, args: string[], operation: string, input?: Buffer): Buffer {
|
||||
const result = spawnSync('git', ['-C', root, ...args], {
|
||||
input,
|
||||
maxBuffer: 1 << 26,
|
||||
})
|
||||
if (result.error) {
|
||||
throw new Error(`${operation} failed: ${result.error.message}`, { cause: result.error })
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${operation} failed with status ${String(result.status)}: ${result.stderr.toString('utf8').trim()}`)
|
||||
}
|
||||
return result.stdout
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist exact working-tree bytes so a pairing record can later recover them
|
||||
* with `git cat-file`, even when they have never appeared in the index or a
|
||||
* commit. The returned object ID is checked against the pairing format's own
|
||||
* content hash before the caller writes a sidecar.
|
||||
*/
|
||||
export function storeGitBlob(root: string, content: Buffer): string {
|
||||
const expected = gitBlobHash(content)
|
||||
const stored = runGit(root, ['hash-object', '-w', '--stdin'], 'git hash-object -w --stdin', content)
|
||||
.toString('utf8')
|
||||
.trim()
|
||||
if (stored !== expected) {
|
||||
throw new Error(`git hash-object -w --stdin returned unexpected object ID ${JSON.stringify(stored)}; expected ${expected}`)
|
||||
}
|
||||
runGit(
|
||||
root,
|
||||
['update-ref', `${SNAPSHOT_REF_PREFIX}/${stored}`, stored],
|
||||
'git update-ref for translation snapshot',
|
||||
)
|
||||
return stored
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
/** Regression tests for the bilingual corpus scope and structural signature. */
|
||||
/** Regression tests for bilingual snapshots, corpus scope, and structure. */
|
||||
|
||||
import { execFileSync, spawnSync } from 'node:child_process'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { gitBlobHash, storeGitBlob } from './translation-pairing-git.ts'
|
||||
import {
|
||||
isTranslationScopeFile,
|
||||
pairAnchorOfArgument,
|
||||
@@ -15,6 +20,71 @@ function signature(markdown: string) {
|
||||
return translationStructureSignature(parseTranslationMarkdown(markdown), 'counterpart.zh.md')
|
||||
}
|
||||
|
||||
function gitSupportsObjectFormat(format: 'sha256'): boolean {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-git-object-format-'))
|
||||
try {
|
||||
return spawnSync('git', ['init', '--quiet', `--object-format=${format}`, root], {
|
||||
stdio: 'ignore',
|
||||
}).status === 0
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
const supportsSha256ObjectFormat = gitSupportsObjectFormat('sha256')
|
||||
|
||||
describe('translation pairing snapshots', () => {
|
||||
it('stores exact uncommitted bytes for later recovery by object ID', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-translation-pairing-'))
|
||||
try {
|
||||
execFileSync('git', ['init', '--quiet', root], {
|
||||
env: { ...process.env, GIT_DEFAULT_HASH: 'sha1' },
|
||||
})
|
||||
const content = Buffer.from([0x75, 0x6e, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x0a, 0xff])
|
||||
|
||||
const objectId = storeGitBlob(root, content)
|
||||
|
||||
expect(objectId).toBe(gitBlobHash(content))
|
||||
expect(execFileSync('git', [
|
||||
'-C', root, 'rev-parse', `refs/dsh/translation-pairing/snapshots/${objectId}`,
|
||||
], { encoding: 'utf8' }).trim()).toBe(objectId)
|
||||
execFileSync('git', ['-C', root, 'gc', '--prune=now'])
|
||||
expect(execFileSync('git', ['-C', root, 'cat-file', '-p', objectId])).toEqual(content)
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('fails before a sidecar can reference an unavailable object', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-translation-pairing-'))
|
||||
try {
|
||||
expect(() => storeGitBlob(root, Buffer.from('snapshot'))).toThrow('git hash-object -w --stdin failed')
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('fails clearly when Git cannot be started', () => {
|
||||
const previousPath = process.env.PATH
|
||||
try {
|
||||
process.env.PATH = ''
|
||||
expect(() => storeGitBlob('.', Buffer.from('snapshot'))).toThrow('git hash-object -w --stdin failed')
|
||||
} finally {
|
||||
process.env.PATH = previousPath
|
||||
}
|
||||
})
|
||||
|
||||
it.skipIf(!supportsSha256ObjectFormat)('rejects an object format that pairing records cannot represent', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-translation-pairing-'))
|
||||
try {
|
||||
execFileSync('git', ['init', '--quiet', '--object-format=sha256', root])
|
||||
expect(() => storeGitBlob(root, Buffer.from('snapshot'))).toThrow('returned unexpected object ID')
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('translation pairing manifest', () => {
|
||||
it('accepts an exclusions-only manifest', () => {
|
||||
expect(parseTranslationPairingManifest(JSON.stringify({
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
* See `docs/i18n/README.md` for the owning contract.
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import { existsSync, globSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { basename, join, resolve, sep } from 'node:path'
|
||||
import { gitBlobHash, storeGitBlob } from './translation-pairing-git.ts'
|
||||
import {
|
||||
linksTo,
|
||||
parseTranslationMarkdown,
|
||||
@@ -54,14 +54,6 @@ function isExcluded(file: string): boolean {
|
||||
return manifest.excluded.some(entry => (entry.endsWith('/') ? file.startsWith(entry) : file === entry))
|
||||
}
|
||||
|
||||
/** Full git blob hash (what `git hash-object` prints). */
|
||||
function blobHash(content: Buffer): string {
|
||||
const hash = createHash('sha1')
|
||||
hash.update(`blob ${content.byteLength}\0`)
|
||||
hash.update(content)
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
/** The three paths of a pair, derived from the English-file path. */
|
||||
function pairPaths(source: string): { zh: string; meta: string } {
|
||||
return { zh: source.replace(/\.md$/, '.zh.md'), meta: source.replace(/\.md$/, '.i18n.yaml') }
|
||||
@@ -148,7 +140,12 @@ if (writeMode) {
|
||||
}
|
||||
continue
|
||||
}
|
||||
const record = renderMeta(source, blobHash(readFileSync(join(root, source))), zh, blobHash(readFileSync(join(root, zh))))
|
||||
const sourceContent = readFileSync(join(root, source))
|
||||
const zhContent = readFileSync(join(root, zh))
|
||||
// A consistency record is also a recovery pointer for the briefing
|
||||
// generator. Persist both snapshots even when the sidecar text is already
|
||||
// current, because the bytes may exist only in this working tree.
|
||||
const record = renderMeta(source, storeGitBlob(root, sourceContent), zh, storeGitBlob(root, zhContent))
|
||||
if (existsSync(join(root, meta)) && readFileSync(join(root, meta), 'utf8') === record) continue
|
||||
writeFileSync(join(root, meta), record)
|
||||
console.log(`verify-translation-pairing: recorded ${meta}`)
|
||||
@@ -203,7 +200,7 @@ for (const source of [...pairAnchors].sort()) {
|
||||
|
||||
let consistent = true
|
||||
for (const [file, content] of [[source, sourceContent], [zh, zhContent]] as const) {
|
||||
const current = blobHash(content)
|
||||
const current = gitBlobHash(content)
|
||||
if (record.get(basename(file)) !== current) {
|
||||
errors.push(`${file}: out of sync — content no longer matches the pair's last confirmed-consistent state in ${meta} (bring the other side along, then re-record with --write)`)
|
||||
consistent = false
|
||||
|
||||
Reference in New Issue
Block a user