fix(integration): recover private ref adoption

TASK-f40126ee-2485-45c8-a67b-4279e4e8704a
This commit is contained in:
thePR0M3TH3AN committed 2026-10-03 17:18:20 -04:00
1 parent 8f894e9863
commit 97973f0566
2 files changed
+194 -82

No files matched your search

+138 -80
View File
@@ -1,14 +1,17 @@
import { createHash, randomUUID } from 'node:crypto';
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { TorchError } from '../kernel/errors.mjs';
import { observeRuntimeTurn } from '../runtime/turn-guard.mjs';
const ADOPTION_SCHEMA = 'torch.dev/integration-adoption/v1alpha1';
const AUTHORITY_SCHEMA = 'torch.dev/integration-adoption-authority/v1alpha1';
const ADAPTER_NAME = 'git-private-ref-cas-v1';
const ACTIONS = new Set(['plan', 'prepare', 'finalize', 'reconcile', 'rollback', 'request']);
const MUTATING_ACTIONS = new Set(['prepare', 'finalize', 'rollback', 'request']);
const SHA = /^[0-9a-f]{40,64}$/;
const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}$/;
const ZERO_SHA = '0'.repeat(40);
function git(root, args, { allowFailure = false } = {}) {
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
@@ -39,6 +42,10 @@ function sha(value, name) {
return normalized;
}
function digest(value) {
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
}
function durableRecord(event) {
if (!event?.details) return null;
try {
@@ -68,38 +75,86 @@ function exactAuthority(authority, binding) {
const fields = ['adoptionId', 'candidateSha', 'baseTargetSha', 'provenanceManifestDigest',
'contributorArea', 'destinationArea', 'targetBranch', 'candidateRef', 'expectedOldRef'];
return fields.every((field) => authority[field] === binding[field])
&& authority.fixture?.id === binding.fixture?.id
&& authority.fixture?.adapter === binding.fixture?.adapter;
&& authority.adapter?.name === binding.adapter?.name
&& authority.adapter?.id === binding.adapter?.id;
}
function adapterReceipt(value) {
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
function exactInput(record, input) {
const fields = ['adoptionId', 'candidateSha', 'baseTargetSha', 'provenanceManifestDigest',
'contributorArea', 'destinationArea', 'targetBranch', 'candidateRef', 'expectedOldRef'];
const mismatches = fields.filter((field) => input[field] !== undefined && input[field] !== record[field]);
if (input.provenance !== undefined && JSON.stringify(input.provenance) !== JSON.stringify(record.provenance)) mismatches.push('provenance');
if (mismatches.length) throw new TorchError('Lifecycle input cannot override immutable prepared adoption fields', {
code: 'ADOPTION_IMMUTABLE_BINDING_STALE', details: { adoptionId: record.id, fields: mismatches },
});
}
/**
* Adoption is source-only. Ref-changing methods can only call an injected
* hermetic fixture adapter; the normal service has none. Approval is an exact,
* revision-bound JSON decision recorded by the existing approval service.
* Real Git private-ref transaction adapter. It is source code only: callers
* decide when it is invoked, while the adapter itself gives temp-repository
* scenarios the same atomic update-ref semantics as a real repository.
*/
export class GitPrivateRefAdapter {
constructor({ repositoryRoot } = {}) {
this.repositoryRoot = resolve(text(repositoryRoot, 'repositoryRoot'));
this.name = ADAPTER_NAME;
this.id = digest({ adapter: this.name, repositoryRoot: this.repositoryRoot });
}
readRefs(refs) {
const values = {};
for (const ref of refs) {
const result = git(this.repositoryRoot, ['rev-parse', '--verify', '--quiet', ref], { allowFailure: true });
values[ref] = result.status === 0 ? result.stdout : null;
}
return values;
}
compareAndSwapRefs({ expected = [], writes = [] } = {}) {
const commands = ['start'];
for (const { ref, sha: expectedSha } of expected) {
commands.push(`verify ${text(ref, 'ref')} ${expectedSha ?? ZERO_SHA}`);
}
for (const { ref, sha: nextSha } of writes) {
const current = expected.find((entry) => entry.ref === ref)?.sha ?? null;
if (nextSha === null) commands.push(`delete ${text(ref, 'ref')} ${current ?? ZERO_SHA}`);
else commands.push(`update ${text(ref, 'ref')} ${sha(nextSha, 'next ref sha')} ${current ?? ZERO_SHA}`);
}
commands.push('prepare', 'commit', '');
const result = spawnSync('git', ['-C', this.repositoryRoot, 'update-ref', '--stdin'], {
input: commands.join('\n'), encoding: 'utf8',
});
if (result.status !== 0) throw new TorchError('Private ref compare-and-swap refused', {
code: 'ADOPTION_REF_CAS_FAILED', details: result.stderr?.trim() || null,
});
return { adapter: { name: this.name, id: this.id }, expected, writes };
}
}
export class AdoptionService {
constructor({ repositoryRoot, controlPlane, clock = () => new Date(), idFactory = randomUUID, policy, fixtureAdapter = null } = {}) {
constructor({ repositoryRoot, controlPlane, clock = () => new Date(), idFactory = randomUUID, policy, refAdapter } = {}) {
this.repositoryRoot = repositoryRoot;
this.controlPlane = controlPlane;
this.clock = clock;
this.idFactory = idFactory;
this.policy = policy ?? {};
this.fixtureAdapter = fixtureAdapter;
this.refAdapter = refAdapter ?? new GitPrivateRefAdapter({ repositoryRoot });
}
plan({ action = 'plan', actorId, approvalId, approvalRevision, adoptionId,
candidateSha, baseTargetSha, provenance, provenanceManifestDigest, contributorArea,
destinationArea, targetBranch, candidateRef, expectedOldRef, fixture } = {}) {
destinationArea, targetBranch, candidateRef, expectedOldRef, adapter } = {}) {
const blockers = [];
const id = adoptionId ? text(adoptionId, 'adoptionId') : null;
if (id && !SAFE_ID.test(id)) blockers.push({ code: 'ADOPTION_ID_INVALID' });
if (!ACTIONS.has(action)) blockers.push({ code: 'ADOPTION_ACTION_INVALID' });
if (actorId !== 'owner') blockers.push({ code: 'OWNER_ACTOR_REQUIRED' });
if (!Number.isInteger(approvalRevision) || approvalRevision < 1) blockers.push({ code: 'ADOPTION_APPROVAL_REVISION_REQUIRED' });
const adapterBinding = adapter?.name && adapter?.id ? { name: adapter.name, id: adapter.id } : null;
if (MUTATING_ACTIONS.has(action) && !adapterBinding) blockers.push({ code: 'ADOPTION_ADAPTER_BINDING_REQUIRED' });
if (adapterBinding && (adapterBinding.name !== this.refAdapter.name || adapterBinding.id !== this.refAdapter.id)) {
blockers.push({ code: 'ADOPTION_ADAPTER_PROVENANCE_INVALID' });
}
let candidate; let base; let provenanceDigest;
try {
@@ -108,10 +163,8 @@ export class AdoptionService {
if (!Array.isArray(provenance) || !provenance.length || provenance.some((entry) => typeof entry !== 'string' || !entry)) {
throw new TorchError('provenance must be a non-empty ordered string list', { code: 'ADOPTION_INPUT_INVALID' });
}
provenanceDigest = createHash('sha256').update(JSON.stringify(provenance)).digest('hex');
if (sha(provenanceManifestDigest, 'provenanceManifestDigest') !== provenanceDigest) {
blockers.push({ code: 'ADOPTION_PROVENANCE_DIGEST_MISMATCH', expected: provenanceDigest });
}
provenanceDigest = digest(provenance);
if (sha(provenanceManifestDigest, 'provenanceManifestDigest') !== provenanceDigest) blockers.push({ code: 'ADOPTION_PROVENANCE_DIGEST_MISMATCH', expected: provenanceDigest });
} catch (error) {
blockers.push({ code: error.code ?? 'ADOPTION_INPUT_INVALID', details: error.details ?? null });
}
@@ -120,14 +173,8 @@ export class AdoptionService {
if (typeof target !== 'string' || !target.trim()) blockers.push({ code: 'ADOPTION_TARGET_REQUIRED' });
const candidateReference = candidateRef ?? (id ? `refs/torch/integration-candidates/${id}` : null);
const expectedReference = expectedOldRef ?? (target ? `refs/heads/${target}` : null);
if (!candidateReference || !/^refs\/torch\/integration-candidates\/[A-Za-z0-9._-]+$/.test(candidateReference)) {
blockers.push({ code: 'ADOPTION_CANDIDATE_REF_INVALID' });
}
if (!expectedReference || !/^refs\/heads\/[A-Za-z0-9._/-]+$/.test(expectedReference)) {
blockers.push({ code: 'ADOPTION_EXPECTED_REF_INVALID' });
}
const normalizedFixture = fixture?.id && fixture?.adapter ? { id: fixture.id, adapter: fixture.adapter } : null;
if (MUTATING_ACTIONS.has(action) && !normalizedFixture) blockers.push({ code: 'ADOPTION_HERMETIC_FIXTURE_REQUIRED' });
if (!candidateReference || !/^refs\/torch\/integration-candidates\/[A-Za-z0-9._-]+$/.test(candidateReference)) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_INVALID' });
if (!expectedReference || !/^refs\/heads\/[A-Za-z0-9._/-]+$/.test(expectedReference)) blockers.push({ code: 'ADOPTION_EXPECTED_REF_INVALID' });
let contributor; let destination;
try { contributor = this.controlPlane.assertIdentity(text(contributorArea, 'contributorArea')); }
@@ -136,11 +183,11 @@ export class AdoptionService {
catch (error) { blockers.push({ code: 'ADOPTION_DESTINATION_INVALID', details: error.code ?? null }); }
if (contributor && destination && contributor === destination) blockers.push({ code: 'ADOPTION_IDENTITIES_MUST_DIFFER' });
const approval = approvalId ? this.controlPlane.database.prepare(`SELECT * FROM approval_requests
WHERE id = ? AND project_id = ?`).get(approvalId, this.controlPlane.projectId) : null;
const binding = { action, adoptionId: id, candidateSha: candidate, baseTargetSha: base,
provenanceManifestDigest: provenanceDigest, contributorArea: contributor, destinationArea: destination,
targetBranch: target, candidateRef: candidateReference, expectedOldRef: expectedReference, fixture: normalizedFixture };
const binding = { action, adoptionId: id, candidateSha: candidate, baseTargetSha: base, provenanceManifestDigest: provenanceDigest,
contributorArea: contributor, destinationArea: destination, targetBranch: target, candidateRef: candidateReference,
expectedOldRef: expectedReference, adapter: adapterBinding };
const approval = approvalId ? this.controlPlane.database.prepare(`SELECT * FROM approval_requests WHERE id = ? AND project_id = ?`)
.get(approvalId, this.controlPlane.projectId) : null;
if (!approval) blockers.push({ code: 'ADOPTION_APPROVAL_MISSING' });
else if (approval.revision !== approvalRevision) blockers.push({ code: 'ADOPTION_APPROVAL_REVISION_STALE', actual: approval.revision });
else if (!exactAuthority(authorityFrom(approval), binding)) blockers.push({ code: 'ADOPTION_APPROVAL_BINDING_INVALID' });
@@ -148,35 +195,27 @@ export class AdoptionService {
if (candidate && git(this.repositoryRoot, ['cat-file', '-e', `${candidate}^{commit}`], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_CANDIDATE_MISSING' });
if (base && git(this.repositoryRoot, ['cat-file', '-e', `${base}^{commit}`], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_BASE_MISSING' });
if (candidate && base && git(this.repositoryRoot, ['merge-base', '--is-ancestor', base, candidate], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_CANDIDATE_NOT_DESCENDED_FROM_BASE' });
const fixtureRefs = normalizedFixture && this.fixtureAdapter
&& this.fixtureAdapter.id === normalizedFixture.id && this.fixtureAdapter.name === normalizedFixture.adapter;
if (expectedReference && base) {
const current = fixtureRefs
? { status: 0, stdout: this.fixtureAdapter.readRefs([expectedReference])[expectedReference] ?? '' }
: git(this.repositoryRoot, ['rev-parse', expectedReference], { allowFailure: true });
if (current.status !== 0) blockers.push({ code: 'ADOPTION_EXPECTED_REF_MISSING' });
else if (current.stdout !== base) blockers.push({ code: 'ADOPTION_EXPECTED_REF_STALE', expected: base, actual: current.stdout });
const current = this.refAdapter.readRefs([expectedReference])[expectedReference];
if (!current) blockers.push({ code: 'ADOPTION_EXPECTED_REF_MISSING' });
else if (current !== base) blockers.push({ code: 'ADOPTION_EXPECTED_REF_STALE', expected: base, actual: current });
}
const candidateExists = candidateReference && (fixtureRefs
? this.fixtureAdapter.readRefs([candidateReference])[candidateReference] != null
: git(this.repositoryRoot, ['show-ref', '--verify', '--quiet', candidateReference], { allowFailure: true }).status === 0);
const candidateExists = candidateReference && this.refAdapter.readRefs([candidateReference])[candidateReference] != null;
if (['plan', 'prepare', 'finalize'].includes(action) && candidateExists) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_ALREADY_EXISTS' });
if (['reconcile', 'rollback', 'request'].includes(action) && !candidateExists) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_MISSING' });
if (action === 'request' && !candidateExists) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_MISSING' });
for (const worktree of listWorktreePaths(this.repositoryRoot)) {
const entries = git(worktree, ['status', '--porcelain=v1', '--untracked-files=all']).stdout.split('\n').filter(Boolean);
if (entries.length) blockers.push({ code: 'ADOPTION_WORKTREE_DIRTY', worktree, entries });
}
if (tableExists(this.controlPlane.database, 'worktree_guards')) {
for (const guard of this.controlPlane.database.prepare(`SELECT area_id, guard_type, reason FROM worktree_guards
WHERE project_id = ? AND released_at IS NULL`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_WORKTREE_GUARD_ACTIVE', guard });
for (const guard of this.controlPlane.database.prepare(`SELECT area_id, guard_type, reason FROM worktree_guards WHERE project_id = ? AND released_at IS NULL`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_WORKTREE_GUARD_ACTIVE', guard });
}
if (tableExists(this.controlPlane.database, 'resource_leases')) {
for (const lease of this.controlPlane.database.prepare(`SELECT id, resource_id, area_id FROM resource_leases WHERE released_at IS NULL`).all()) blockers.push({ code: 'ADOPTION_RESOURCE_LEASE_ACTIVE', lease });
}
if (tableExists(this.controlPlane.database, 'prepared_checks')) {
for (const check of this.controlPlane.database.prepare(`SELECT id, area_id, state FROM prepared_checks
WHERE project_id = ? AND state IN ('prepared', 'running')`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_PREPARED_CHECK_ACTIVE', check });
for (const check of this.controlPlane.database.prepare(`SELECT id, area_id, state FROM prepared_checks WHERE project_id = ? AND state IN ('prepared', 'running')`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_PREPARED_CHECK_ACTIVE', check });
}
for (const area of [contributor, destination].filter(Boolean)) {
const executor = observeRuntimeTurn({ stateRoot: this.controlPlane.stateRoot, repositoryRoot: this.repositoryRoot,
@@ -185,14 +224,20 @@ export class AdoptionService {
}
return { action: `plan-adoption-${action}`, adoptionId: id, approval: approval ? { id: approval.id, revision: approval.revision, status: approval.status, approver: approval.approver_id } : null,
authority: binding, candidateSha: candidate ?? null, baseTargetSha: base ?? null, provenanceManifestDigest: provenanceDigest ?? null,
contributorArea: contributor ?? null, destinationArea: destination ?? null, targetBranch: target ?? null,
candidateRef: candidateReference, expectedOldRef: expectedReference, blockers, canProceed: blockers.length === 0, mutationPerformed: false };
contributorArea: contributor ?? null, destinationArea: destination ?? null, targetBranch: target ?? null, candidateRef: candidateReference,
expectedOldRef: expectedReference, blockers, canProceed: blockers.length === 0, mutationPerformed: false };
}
#record(adoptionId, record, operation) {
#append(adoptionId, expectedRevision, record, operation) {
const now = this.clock().toISOString();
this.controlPlane.database.exec('BEGIN IMMEDIATE');
try {
const current = this.#find(adoptionId);
if (expectedRevision === null ? current : !current || current.revision !== expectedRevision) {
throw new TorchError('Adoption journal changed before this transition could be recorded', {
code: 'ADOPTION_RECORD_REVISION_STALE', details: { adoptionId, expectedRevision, actual: current?.revision ?? null },
});
}
this.controlPlane.database.prepare(`INSERT INTO audit_events
(id, project_id, actor_id, operation, entity_type, entity_id, details, created_at)
VALUES (?, ?, 'owner', ?, 'integration-adoption', ?, ?, ?)`)
@@ -201,78 +246,91 @@ export class AdoptionService {
} catch (error) { this.controlPlane.database.exec('ROLLBACK'); throw error; }
}
#fixture(input) {
if (!this.fixtureAdapter || input.fixture?.adapter !== this.fixtureAdapter.name || input.fixture?.id !== this.fixtureAdapter.id) {
throw new TorchError('Adoption mutation is available only through its named hermetic fixture adapter', {
code: 'ADOPTION_HERMETIC_ADAPTER_REQUIRED', details: { fixture: input.fixture ?? null },
});
}
return this.fixtureAdapter;
#fromRecord(record, input, action) {
exactInput(record, input);
return this.plan({ action, actorId: input.actorId, approvalId: input.approvalId, approvalRevision: input.approvalRevision,
adoptionId: record.id, candidateSha: record.candidateSha, baseTargetSha: record.baseTargetSha, provenance: record.provenance,
provenanceManifestDigest: record.provenanceManifestDigest, contributorArea: record.contributorArea, destinationArea: record.destinationArea,
targetBranch: record.targetBranch, candidateRef: record.candidateRef, expectedOldRef: record.expectedOldRef, adapter: input.adapter });
}
prepare(input = {}) {
const adoptionId = input.adoptionId ?? this.idFactory();
const plan = this.plan({ ...input, action: 'prepare', adoptionId });
if (!plan.canProceed) throw new TorchError('Adoption inputs are not eligible for preparation', { code: 'ADOPTION_PREPARE_BLOCKED', details: plan.blockers });
this.#fixture(input);
if (this.#find(adoptionId)) throw new TorchError('Adoption id is already journaled; reconcile its immutable record instead.', { code: 'ADOPTION_ALREADY_EXISTS', details: { adoptionId } });
const now = this.clock().toISOString();
const record = { schema: ADOPTION_SCHEMA, id: adoptionId, revision: 1, state: 'prepared', approvalId: text(input.approvalId, 'approvalId'),
approvalRevision: input.approvalRevision, authority: plan.authority, provenance: [...input.provenance], candidateSha: plan.candidateSha, baseTargetSha: plan.baseTargetSha,
provenanceManifestDigest: plan.provenanceManifestDigest, candidateRef: plan.candidateRef, expectedOldRef: plan.expectedOldRef,
contributorArea: plan.contributorArea, destinationArea: plan.destinationArea, targetBranch: plan.targetBranch,
approvalRevision: input.approvalRevision, authority: plan.authority, provenance: [...input.provenance], candidateSha: plan.candidateSha,
baseTargetSha: plan.baseTargetSha, provenanceManifestDigest: plan.provenanceManifestDigest, candidateRef: plan.candidateRef,
expectedOldRef: plan.expectedOldRef, contributorArea: plan.contributorArea, destinationArea: plan.destinationArea, targetBranch: plan.targetBranch,
archiveRef: `refs/torch/integration-archives/${adoptionId}`, preparedAt: now, finalizedAt: null, rolledBackAt: null };
this.#record(adoptionId, record, 'integration.adoption.prepare');
this.#append(adoptionId, null, record, 'integration.adoption.prepare');
return { ...record, mutationPerformed: true };
}
finalize(input = {}) {
const record = this.getPrepared(text(input.adoptionId, 'adoptionId'));
if (record.state !== 'prepared') throw new TorchError('Only a prepared adoption can be finalized', { code: 'ADOPTION_FINALIZE_STATE_INVALID', details: { state: record.state } });
const plan = this.plan({ ...record, ...input, action: 'finalize' });
const plan = this.#fromRecord(record, input, 'finalize');
if (!plan.canProceed) throw new TorchError('Adoption finalize is blocked', { code: 'ADOPTION_FINALIZE_BLOCKED', details: plan.blockers });
const adapter = this.#fixture(input);
const receipt = adapter.compareAndSwapRefs({ expected: [{ ref: record.expectedOldRef, sha: record.baseTargetSha }, { ref: record.candidateRef, sha: null }, { ref: record.archiveRef, sha: null }],
const receipt = this.refAdapter.compareAndSwapRefs({ expected: [{ ref: record.expectedOldRef, sha: record.baseTargetSha }, { ref: record.candidateRef, sha: null }, { ref: record.archiveRef, sha: null }],
writes: [{ ref: record.archiveRef, sha: record.baseTargetSha }, { ref: record.candidateRef, sha: record.candidateSha }] });
const finalized = { ...record, revision: record.revision + 1, state: 'finalized', finalizeApprovalId: text(input.approvalId, 'approvalId'),
finalizeApprovalRevision: input.approvalRevision, finalizedAt: this.clock().toISOString(), fixtureReceiptDigest: adapterReceipt(receipt) };
this.#record(record.id, finalized, 'integration.adoption.finalize');
finalizeApprovalRevision: input.approvalRevision, finalizedAt: this.clock().toISOString(), refReceiptDigest: digest(receipt) };
this.#append(record.id, record.revision, finalized, 'integration.adoption.finalize');
return { ...finalized, mutationPerformed: true };
}
reconcile(input = {}) {
const record = this.getPrepared(text(input.adoptionId, 'adoptionId'));
const plan = this.plan({ ...record, ...input, action: 'reconcile' });
const plan = this.#fromRecord(record, input, 'reconcile');
if (!plan.canProceed) return { record, plan, state: 'blocked', mutationPerformed: false };
const adapter = this.#fixture(input);
const refs = adapter.readRefs([record.expectedOldRef, record.candidateRef, record.archiveRef]);
const consistent = refs[record.expectedOldRef] === record.baseTargetSha && refs[record.candidateRef] === record.candidateSha && refs[record.archiveRef] === record.baseTargetSha;
return { record, refs, state: consistent ? 'consistent' : 'diverged', mutationPerformed: false };
const refs = this.refAdapter.readRefs([record.expectedOldRef, record.candidateRef, record.archiveRef]);
const finalizedRefs = refs[record.expectedOldRef] === record.baseTargetSha && refs[record.candidateRef] === record.candidateSha && refs[record.archiveRef] === record.baseTargetSha;
if (record.state === 'prepared' && finalizedRefs) {
const recovered = { ...record, revision: record.revision + 1, state: 'finalized', finalizedAt: this.clock().toISOString(),
recoveredAt: this.clock().toISOString(), recovery: 'private-ref-cas-completed-before-journal' };
this.#append(record.id, record.revision, recovered, 'integration.adoption.reconcile-finalized');
return { record: recovered, refs, state: 'recovered-finalized', mutationPerformed: true };
}
if (record.state === 'finalized' && finalizedRefs) return { record, refs, state: 'consistent', mutationPerformed: false };
if (record.state === 'prepared' && refs[record.candidateRef] === null && refs[record.archiveRef] === null) return { record, refs, state: 'prepared', mutationPerformed: false };
return { record, refs, state: 'diverged', mutationPerformed: false };
}
rollback(input = {}) {
const record = this.getPrepared(text(input.adoptionId, 'adoptionId'));
if (record.state !== 'finalized') throw new TorchError('Only a finalized adoption can be rolled back', { code: 'ADOPTION_ROLLBACK_STATE_INVALID', details: { state: record.state } });
const plan = this.plan({ ...record, ...input, action: 'rollback' });
const plan = this.#fromRecord(record, input, 'rollback');
if (!plan.canProceed) throw new TorchError('Adoption rollback is blocked', { code: 'ADOPTION_ROLLBACK_BLOCKED', details: plan.blockers });
const receipt = this.#fixture(input).compareAndSwapRefs({ expected: [{ ref: record.candidateRef, sha: record.candidateSha }], writes: [{ ref: record.candidateRef, sha: null }] });
const refs = this.refAdapter.readRefs([record.expectedOldRef, record.candidateRef, record.archiveRef]);
const recovered = refs[record.expectedOldRef] === record.baseTargetSha
&& refs[record.candidateRef] === null && refs[record.archiveRef] === record.baseTargetSha;
if (!recovered && refs[record.candidateRef] !== record.candidateSha) {
throw new TorchError('Candidate ref changed before rollback', { code: 'ADOPTION_ROLLBACK_REF_DIVERGED', details: refs });
}
const receipt = recovered ? { adapter: { name: this.refAdapter.name, id: this.refAdapter.id }, recovered: true, refs }
: this.refAdapter.compareAndSwapRefs({ expected: [{ ref: record.candidateRef, sha: record.candidateSha }], writes: [{ ref: record.candidateRef, sha: null }] });
const rolledBack = { ...record, revision: record.revision + 1, state: 'rolled-back', rollbackApprovalId: text(input.approvalId, 'approvalId'),
rollbackApprovalRevision: input.approvalRevision, rolledBackAt: this.clock().toISOString(), rollbackReceiptDigest: adapterReceipt(receipt) };
this.#record(record.id, rolledBack, 'integration.adoption.rollback');
rollbackApprovalRevision: input.approvalRevision, rolledBackAt: this.clock().toISOString(), rollbackReceiptDigest: digest(receipt),
...(recovered ? { recoveredAt: this.clock().toISOString(), recovery: 'private-ref-rollback-completed-before-journal' } : {}) };
this.#append(record.id, record.revision, rolledBack, 'integration.adoption.rollback');
return { ...rolledBack, mutationPerformed: true };
}
requestAdoptedCandidate(input = {}, { requestIntegration } = {}) {
requestAdoptedCandidate(input = {}, { requestIntegration, findIntegrationRequest } = {}) {
const record = this.getPrepared(text(input.adoptionId, 'adoptionId'));
if (record.state !== 'finalized') throw new TorchError('Only a finalized adoption can request native qualification', { code: 'ADOPTION_REQUEST_STATE_INVALID', details: { state: record.state } });
const plan = this.plan({ ...record, ...input, action: 'request' });
if (!['finalized', 'requesting'].includes(record.state)) throw new TorchError('Only a finalized adoption can request native qualification', { code: 'ADOPTION_REQUEST_STATE_INVALID', details: { state: record.state } });
const plan = this.#fromRecord(record, input, 'request');
if (!plan.canProceed) throw new TorchError('Adopted candidate request is blocked', { code: 'ADOPTION_REQUEST_BLOCKED', details: plan.blockers });
this.#fixture(input);
if (typeof requestIntegration !== 'function') throw new TorchError('Native integration request adapter is unavailable', { code: 'ADOPTION_NATIVE_REQUEST_UNAVAILABLE' });
const request = requestIntegration({ areaId: record.contributorArea, commit: record.candidateSha });
const requested = { ...record, revision: record.revision + 1, state: 'requested', requestApprovalId: text(input.approvalId, 'approvalId'),
requestApprovalRevision: input.approvalRevision, integrationRequestId: request.id, requestedAt: this.clock().toISOString() };
this.#record(record.id, requested, 'integration.adoption.request');
const requesting = record.state === 'requesting' ? record : { ...record, revision: record.revision + 1, state: 'requesting',
requestApprovalId: text(input.approvalId, 'approvalId'), requestApprovalRevision: input.approvalRevision, requestStartedAt: this.clock().toISOString() };
if (record.state !== 'requesting') this.#append(record.id, record.revision, requesting, 'integration.adoption.request-started');
const request = findIntegrationRequest?.({ candidateRef: requesting.candidateRef, commit: requesting.candidateSha })
?? requestIntegration({ areaId: requesting.contributorArea, commit: requesting.candidateSha, candidateRef: requesting.candidateRef });
const requested = { ...requesting, revision: requesting.revision + 1, state: 'requested', integrationRequestId: request.id, requestedAt: this.clock().toISOString() };
this.#append(requesting.id, requesting.revision, requested, 'integration.adoption.request');
return { ...requested, integrationRequest: request, mutationPerformed: true };
}
+56 -2
View File
@@ -16,6 +16,21 @@ function git(root, args, { allowFailure = false } = {}) {
return { status: result.status, stdout: result.stdout?.trim() ?? '', stderr: result.stderr?.trim() ?? '' };
}
function text(value, name) {
if (typeof value !== 'string' || !value.trim()) {
throw new TorchError(`${name} is required`, { code: 'INTEGRATION_INPUT_INVALID', details: { name } });
}
return value.trim();
}
function sha(value, name) {
const normalized = text(value, name);
if (!/^[0-9a-f]{40,64}$/.test(normalized)) {
throw new TorchError(`${name} must be a full Git object id`, { code: 'INTEGRATION_INPUT_INVALID', details: { name } });
}
return normalized;
}
function integrationRow(row) {
if (!row) return null;
return {
@@ -87,12 +102,43 @@ export class IntegrationService {
code: 'INTEGRATION_SOURCE_MOVED', details: { branchTip, sourceCommit },
});
}
return this.#registerRequest({ area, sourceBranch: worktree.branch, sourceCommit });
}
requestCandidate({ areaId, commit, candidateRef } = {}) {
const area = this.controlPlane.assertIdentity(areaId);
if (area === 'session-manager') {
throw new TorchError('Session Manager does not own a feature worktree by default', {
code: 'INTEGRATION_SOURCE_INVALID',
});
}
const ref = text(candidateRef, 'candidateRef');
if (!/^refs\/torch\/integration-candidates\/[A-Za-z0-9._-]+$/.test(ref)) {
throw new TorchError('Adopted candidates must use a private integration candidate ref', {
code: 'INTEGRATION_ADOPTED_REF_INVALID', details: { candidateRef: ref },
});
}
const sourceCommit = sha(commit, 'commit');
const refTip = git(this.repositoryRoot, ['rev-parse', '--verify', ref], { allowFailure: true });
if (refTip.status !== 0 || refTip.stdout !== sourceCommit) {
throw new TorchError('Adopted candidate ref no longer names the exact requested commit', {
code: 'INTEGRATION_ADOPTED_REF_STALE', details: { candidateRef: ref, sourceCommit, actual: refTip.stdout || null },
});
}
const existing = this.controlPlane.database.prepare(`SELECT id FROM integration_requests
WHERE project_id = ? AND source_branch = ? AND source_commit = ? ORDER BY rowid LIMIT 1`)
.get(this.controlPlane.projectId, ref, sourceCommit);
if (existing) return this.get(existing.id);
return this.#registerRequest({ area, sourceBranch: ref, sourceCommit });
}
#registerRequest({ area, sourceBranch, sourceCommit }) {
const targetBranch = this.policy.target;
const baseTargetCommit = git(this.repositoryRoot, ['rev-parse', targetBranch]).stdout;
const now = this.clock().toISOString();
const record = {
id: this.idFactory(), projectId: this.controlPlane.projectId, sourceArea: area,
sourceBranch: worktree.branch, sourceCommit, targetBranch, baseTargetCommit,
sourceBranch, sourceCommit, targetBranch, baseTargetCommit,
state: 'testing', reason: null, requiredChecks: this.policy.required_checks ?? [],
authorizedBy: null, authorizedAt: null, createdAt: now, updatedAt: now, landedAt: null,
};
@@ -139,10 +185,18 @@ export class IntegrationService {
requestAdoptedCandidate(input = {}) {
return this.adoption.requestAdoptedCandidate(input, {
requestIntegration: ({ areaId, commit }) => this.request({ areaId, commit }),
requestIntegration: ({ areaId, commit, candidateRef }) => this.requestCandidate({ areaId, commit, candidateRef }),
findIntegrationRequest: ({ candidateRef, commit }) => this.findCandidateRequest({ candidateRef, commit }),
});
}
findCandidateRequest({ candidateRef, commit } = {}) {
const row = this.controlPlane.database.prepare(`SELECT id FROM integration_requests
WHERE project_id = ? AND source_branch = ? AND source_commit = ? ORDER BY rowid LIMIT 1`)
.get(this.controlPlane.projectId, candidateRef, commit);
return row ? this.get(row.id) : null;
}
get(requestId) {
const row = this.controlPlane.database.prepare('SELECT * FROM integration_requests WHERE id = ?').get(requestId);
if (!row) throw new TorchError(`Unknown integration request: ${requestId}`, { code: 'INTEGRATION_NOT_FOUND' });