From 51a5530995803c36308801219a95345bb2c85c3c Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Mon, 18 May 2026 00:37:09 +0200 Subject: [PATCH] feat(ingest): repair isolated diff semantic gate failures --- .../isolated-diff/patch-integrator.test.ts | 103 ++++++++++++++++++ .../ingest/isolated-diff/patch-integrator.ts | 103 +++++++++++++++++- 2 files changed, 200 insertions(+), 6 deletions(-) diff --git a/packages/context/src/ingest/isolated-diff/patch-integrator.test.ts b/packages/context/src/ingest/isolated-diff/patch-integrator.test.ts index 1af5b6fa..d55cfc5b 100644 --- a/packages/context/src/ingest/isolated-diff/patch-integrator.test.ts +++ b/packages/context/src/ingest/isolated-diff/patch-integrator.test.ts @@ -298,4 +298,107 @@ describe('integrateWorkUnitPatch', () => { expect(await git.revParseHead()).toBe(acceptedHead); await expect(readFile(join(configDir, 'wiki/global/a.md'), 'utf-8')).resolves.toBe('accepted\n'); }); + + it('repairs semantic gate failures after a patch applies cleanly', async () => { + const { homeDir, configDir, git, baseSha } = await makeRepo(); + const childDir = join(homeDir, 'child-semantic-repair'); + await git.addWorktree(childDir, 'child-semantic-repair', baseSha); + const childGit = git.forWorktree(childDir); + await writeFile(join(childDir, 'wiki/global/a.md'), 'bad semantic ref\n'); + await childGit.commitFiles(['wiki/global/a.md'], 'bad semantic edit', 'System User', 'system@example.com'); + const patchPath = join(homeDir, 'patches/semantic-repair.patch'); + await childGit.writeBinaryNoRenamePatch(baseSha, 'HEAD', patchPath); + const trace = new FileIngestTraceWriter({ + tracePath: join(homeDir, '.ktx/ingest-traces/job-semantic-repair/trace.jsonl'), + jobId: 'job-semantic-repair', + connectionId: 'c1', + sourceKey: 'fake', + level: 'trace', + }); + const validateAppliedTree = vi + .fn() + .mockRejectedValueOnce(new Error('final artifact gates failed:\na: unknown semantic-layer entity')) + .mockResolvedValueOnce(undefined); + + const result = await integrateWorkUnitPatch({ + unitKey: 'wu-repairable', + patchPath, + integrationGit: git, + trace, + author: { name: 'KTX Test', email: 'system@ktx.local' }, + validateAppliedTree, + slDisallowed: false, + allowedTargetConnectionIds: new Set(['c1']), + repairGateFailure: vi.fn(async (context) => { + expect(context).toMatchObject({ + unitKey: 'wu-repairable', + patchPath, + touchedPaths: ['wiki/global/a.md'], + }); + await writeFile(join(configDir, 'wiki/global/a.md'), 'repaired semantic ref\n', 'utf-8'); + return { + status: 'repaired' as const, + attempts: 1, + changedPaths: ['wiki/global/a.md'], + }; + }), + }); + + expect(result).toMatchObject({ + status: 'accepted', + touchedPaths: ['wiki/global/a.md'], + gateRepair: { + status: 'repaired', + attempts: 1, + changedPaths: ['wiki/global/a.md'], + }, + }); + expect(validateAppliedTree).toHaveBeenCalledTimes(2); + await expect(readFile(join(configDir, 'wiki/global/a.md'), 'utf-8')).resolves.toBe('repaired semantic ref\n'); + await expect(readFile(trace.tracePath, 'utf-8')).resolves.toContain('patch_accepted_after_gate_repair'); + }); + + it('keeps the pre-apply tree when semantic gate repair fails', async () => { + const { homeDir, configDir, git, baseSha } = await makeRepo(); + const childDir = join(homeDir, 'child-semantic-repair-fails'); + await git.addWorktree(childDir, 'child-semantic-repair-fails', baseSha); + const childGit = git.forWorktree(childDir); + await writeFile(join(childDir, 'wiki/global/a.md'), 'bad semantic ref\n'); + await childGit.commitFiles(['wiki/global/a.md'], 'bad semantic edit', 'System User', 'system@example.com'); + const patchPath = join(homeDir, 'patches/semantic-repair-fails.patch'); + await childGit.writeBinaryNoRenamePatch(baseSha, 'HEAD', patchPath); + const trace = new FileIngestTraceWriter({ + tracePath: join(homeDir, '.ktx/ingest-traces/job-semantic-repair-fails/trace.jsonl'), + jobId: 'job-semantic-repair-fails', + connectionId: 'c1', + sourceKey: 'fake', + level: 'trace', + }); + + const result = await integrateWorkUnitPatch({ + unitKey: 'wu-not-repaired', + patchPath, + integrationGit: git, + trace, + author: { name: 'KTX Test', email: 'system@ktx.local' }, + validateAppliedTree: vi.fn().mockRejectedValue(new Error('final artifact gates failed')), + slDisallowed: false, + allowedTargetConnectionIds: new Set(['c1']), + repairGateFailure: vi.fn(async () => ({ + status: 'failed' as const, + attempts: 1, + reason: 'gate repair completed without editing an allowed path', + })), + }); + + expect(result).toMatchObject({ + status: 'semantic_conflict', + gateRepair: { + status: 'failed', + attempts: 1, + reason: 'gate repair completed without editing an allowed path', + }, + }); + 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 index ad9d25cd..a4542576 100644 --- a/packages/context/src/ingest/isolated-diff/patch-integrator.ts +++ b/packages/context/src/ingest/isolated-diff/patch-integrator.ts @@ -1,5 +1,6 @@ import { readFile } from 'node:fs/promises'; import type { GitService } from '../../core/index.js'; +import type { FinalGateRepairResult } from '../final-gate-repair.js'; import type { IngestTraceWriter } from '../ingest-trace.js'; import { traceTimed } from '../ingest-trace.js'; import { assertPatchAllowedForWorkUnit, parsePatchTouchedPaths } from './git-patch.js'; @@ -10,18 +11,26 @@ export type PatchIntegrationTextualResolution = | { status: 'failed'; attempts: number; reason: string }; export type PatchIntegrationResult = - | { status: 'accepted'; commitSha: string; touchedPaths: string[]; textualResolution?: PatchIntegrationTextualResolution } + | { + status: 'accepted'; + commitSha: string; + touchedPaths: string[]; + textualResolution?: PatchIntegrationTextualResolution; + gateRepair?: FinalGateRepairResult; + } | { status: 'textual_conflict'; reason: string; touchedPaths: string[]; textualResolution?: PatchIntegrationTextualResolution; + gateRepair?: FinalGateRepairResult; } | { status: 'semantic_conflict'; reason: string; touchedPaths: string[]; textualResolution?: PatchIntegrationTextualResolution; + gateRepair?: FinalGateRepairResult; }; export interface IntegrateWorkUnitPatchInput { @@ -39,6 +48,12 @@ export interface IntegrateWorkUnitPatchInput { touchedPaths: string[]; reason: string; }): Promise; + repairGateFailure?(input: { + unitKey: string; + patchPath: string; + touchedPaths: string[]; + reason: string; + }): Promise; } function errorMessage(error: unknown): string { @@ -200,18 +215,94 @@ export async function integrateWorkUnitPatch(input: IntegrateWorkUnitPatchInput) await input.validateAppliedTree(touchedPaths); }); } catch (error) { - if (preApplyHead) { - await input.integrationGit.resetHardTo(preApplyHead); - } + const reason = errorMessage(error); await input.trace.event('error', 'integration', 'patch_semantic_conflict', { unitKey: input.unitKey, patchPath: input.patchPath, touchedPaths, - reason: errorMessage(error), + reason, }); + + if (input.repairGateFailure) { + const gateRepair = await input.repairGateFailure({ + unitKey: input.unitKey, + patchPath: input.patchPath, + touchedPaths, + reason, + }); + + if (gateRepair.status === 'failed') { + if (preApplyHead) { + await input.integrationGit.resetHardTo(preApplyHead); + } + return { + status: 'semantic_conflict', + reason: gateRepair.reason, + touchedPaths, + gateRepair, + }; + } + + try { + await traceTimed( + input.trace, + 'integration', + 'semantic_gate_after_gate_repair', + { unitKey: input.unitKey, touchedPaths: gateRepair.changedPaths }, + async () => { + await input.validateAppliedTree(gateRepair.changedPaths); + }, + ); + } catch (repairValidationError) { + if (preApplyHead) { + await input.integrationGit.resetHardTo(preApplyHead); + } + return { + status: 'semantic_conflict', + reason: errorMessage(repairValidationError), + touchedPaths: gateRepair.changedPaths, + gateRepair, + }; + } + + const commit = await input.integrationGit.commitFiles( + gateRepair.changedPaths, + `ingest: repair WorkUnit ${input.unitKey} gates`, + input.author.name, + input.author.email, + ); + if (!commit.created) { + if (preApplyHead) { + await input.integrationGit.resetHardTo(preApplyHead); + } + return { + status: 'semantic_conflict', + reason: 'gate repair produced no committable changes', + touchedPaths: gateRepair.changedPaths, + gateRepair, + }; + } + + await input.trace.event('debug', 'integration', 'patch_accepted_after_gate_repair', { + unitKey: input.unitKey, + commitSha: commit.commitHash, + touchedPaths: gateRepair.changedPaths, + attempts: gateRepair.attempts, + }); + return { + status: 'accepted', + commitSha: commit.commitHash, + touchedPaths: gateRepair.changedPaths, + gateRepair, + }; + } + + if (preApplyHead) { + await input.integrationGit.resetHardTo(preApplyHead); + } return { status: 'semantic_conflict', - reason: errorMessage(error), + reason, touchedPaths, }; }