From 0be264dde025a23e2858df5be8b94e9c50aec150 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Sun, 17 May 2026 21:24:21 +0200 Subject: [PATCH] feat: integrate isolated work unit patches --- .../isolated-diff/patch-integrator.test.ts | 92 ++++++++++++++++++ .../ingest/isolated-diff/patch-integrator.ts | 96 +++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 packages/context/src/ingest/isolated-diff/patch-integrator.test.ts create mode 100644 packages/context/src/ingest/isolated-diff/patch-integrator.ts diff --git a/packages/context/src/ingest/isolated-diff/patch-integrator.test.ts b/packages/context/src/ingest/isolated-diff/patch-integrator.test.ts new file mode 100644 index 00000000..ea78eea6 --- /dev/null +++ b/packages/context/src/ingest/isolated-diff/patch-integrator.test.ts @@ -0,0 +1,92 @@ +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, expect, it, vi } from 'vitest'; +import { GitService } from '../../core/index.js'; +import { FileIngestTraceWriter } from '../ingest-trace.js'; +import { integrateWorkUnitPatch } from './patch-integrator.js'; + +async function makeRepo() { + const homeDir = await mkdtemp(join(tmpdir(), 'ktx-integrate-')); + const configDir = join(homeDir, 'config'); + const git = new GitService({ + storage: { configDir, homeDir }, + git: { + userName: 'System User', + userEmail: 'system@example.com', + bootstrapMessage: 'init', + bootstrapAuthor: 'system', + bootstrapAuthorEmail: 'system@example.com', + }, + }); + await git.onModuleInit(); + await mkdir(join(configDir, 'wiki/global'), { recursive: true }); + await writeFile(join(configDir, 'wiki/global/a.md'), 'old\n'); + await git.commitFiles(['wiki/global/a.md'], 'base', 'System User', 'system@example.com'); + return { homeDir, configDir, git, baseSha: await git.revParseHead() }; +} + +describe('integrateWorkUnitPatch', () => { + it('applies a clean patch, runs semantic gates, and commits accepted changes', async () => { + const { homeDir, configDir, git, baseSha } = await makeRepo(); + const childDir = join(homeDir, 'child'); + await git.addWorktree(childDir, 'child', baseSha); + const childGit = git.forWorktree(childDir); + await writeFile(join(childDir, 'wiki/global/a.md'), 'new\n'); + await childGit.commitFiles(['wiki/global/a.md'], 'edit', 'System User', 'system@example.com'); + const patchPath = join(homeDir, 'patches/wu.patch'); + await childGit.writeBinaryNoRenamePatch(baseSha, 'HEAD', patchPath); + const trace = new FileIngestTraceWriter({ + tracePath: join(homeDir, '.ktx/ingest-traces/job-1/trace.jsonl'), + jobId: 'job-1', + connectionId: 'c1', + sourceKey: 'fake', + level: 'trace', + }); + + const result = await integrateWorkUnitPatch({ + unitKey: 'wu-1', + patchPath, + integrationGit: git, + trace, + author: { name: 'KTX Test', email: 'system@ktx.local' }, + validateAppliedTree: vi.fn().mockResolvedValue(undefined), + slDisallowed: false, + }); + + expect(result.status).toBe('accepted'); + await expect(readFile(join(configDir, 'wiki/global/a.md'), 'utf-8')).resolves.toBe('new\n'); + await expect(readFile(trace.tracePath, 'utf-8')).resolves.toContain('patch_apply_finished'); + }); + + it('rolls back and classifies semantic conflicts', async () => { + const { homeDir, configDir, git, baseSha } = await makeRepo(); + const childDir = join(homeDir, 'child-semantic'); + await git.addWorktree(childDir, 'child-semantic', baseSha); + const childGit = git.forWorktree(childDir); + await writeFile(join(childDir, 'wiki/global/a.md'), 'bad\n'); + await childGit.commitFiles(['wiki/global/a.md'], 'bad edit', 'System User', 'system@example.com'); + const patchPath = join(homeDir, 'patches/bad.patch'); + await childGit.writeBinaryNoRenamePatch(baseSha, 'HEAD', patchPath); + const trace = new FileIngestTraceWriter({ + tracePath: join(homeDir, '.ktx/ingest-traces/job-2/trace.jsonl'), + jobId: 'job-2', + connectionId: 'c1', + sourceKey: 'fake', + level: 'trace', + }); + + const result = await integrateWorkUnitPatch({ + unitKey: 'wu-bad', + patchPath, + integrationGit: git, + trace, + author: { name: 'KTX Test', email: 'system@ktx.local' }, + validateAppliedTree: vi.fn().mockRejectedValue(new Error('final artifact gates failed')), + slDisallowed: false, + }); + + expect(result.status).toBe('semantic_conflict'); + await expect(readFile(join(configDir, 'wiki/global/a.md'), 'utf-8')).resolves.toBe('old\n'); + }); +}); diff --git a/packages/context/src/ingest/isolated-diff/patch-integrator.ts b/packages/context/src/ingest/isolated-diff/patch-integrator.ts new file mode 100644 index 00000000..4cabd0cf --- /dev/null +++ b/packages/context/src/ingest/isolated-diff/patch-integrator.ts @@ -0,0 +1,96 @@ +import { readFile } from 'node:fs/promises'; +import type { GitService } from '../../core/index.js'; +import type { IngestTraceWriter } from '../ingest-trace.js'; +import { traceTimed } from '../ingest-trace.js'; +import { assertPatchAllowedForWorkUnit } from './git-patch.js'; + +export type PatchIntegrationResult = + | { status: 'accepted'; commitSha: string; touchedPaths: string[] } + | { status: 'textual_conflict'; reason: string; touchedPaths: string[] } + | { status: 'semantic_conflict'; reason: string; touchedPaths: string[] }; + +export interface IntegrateWorkUnitPatchInput { + unitKey: string; + patchPath: string; + integrationGit: GitService; + trace: IngestTraceWriter; + author: { name: string; email: string }; + slDisallowed: boolean; + validateAppliedTree(touchedPaths: string[]): Promise; +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +export async function integrateWorkUnitPatch(input: IntegrateWorkUnitPatchInput): Promise { + const preApplyHead = await input.integrationGit.revParseHead(); + const patch = await readFile(input.patchPath, 'utf-8'); + const touched = assertPatchAllowedForWorkUnit({ + unitKey: input.unitKey, + patch, + slDisallowed: input.slDisallowed, + }); + const touchedPaths = touched.map((entry) => entry.path); + + try { + await traceTimed( + input.trace, + 'integration', + 'patch_apply', + { unitKey: input.unitKey, patchPath: input.patchPath, touchedPaths }, + async () => { + await input.integrationGit.applyPatchFile3WayIndex(input.patchPath); + await input.integrationGit.assertWorktreeClean(); + }, + ); + } catch (error) { + if (preApplyHead) { + await input.integrationGit.resetHardTo(preApplyHead); + } + await input.trace.event('error', 'integration', 'patch_textual_conflict', { + unitKey: input.unitKey, + patchPath: input.patchPath, + touchedPaths, + reason: errorMessage(error), + }); + return { + status: 'textual_conflict', + reason: errorMessage(error), + touchedPaths, + }; + } + + try { + await traceTimed(input.trace, 'integration', 'semantic_gate', { unitKey: input.unitKey, touchedPaths }, async () => { + await input.validateAppliedTree(touchedPaths); + }); + } catch (error) { + if (preApplyHead) { + await input.integrationGit.resetHardTo(preApplyHead); + } + await input.trace.event('error', 'integration', 'patch_semantic_conflict', { + unitKey: input.unitKey, + patchPath: input.patchPath, + touchedPaths, + reason: errorMessage(error), + }); + return { + status: 'semantic_conflict', + reason: errorMessage(error), + touchedPaths, + }; + } + + const commit = await input.integrationGit.commitStaged( + `ingest: accept WorkUnit ${input.unitKey}`, + input.author.name, + input.author.email, + ); + await input.trace.event('debug', 'integration', 'patch_accepted', { + unitKey: input.unitKey, + commitSha: commit.commitHash, + touchedPaths, + }); + return { status: 'accepted', commitSha: commit.commitHash, touchedPaths }; +}