Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { resolveProposalReviewDisposition, resolveReviewDisposition } from '../c
import { livingExecutionLifecycleOperations } from './execution/living-execution-lifecycle.ts';
import type { ProviderAssignmentExplanationWrite } from '../observability/assignment-explanation-service.ts';
import { projectCompletedPlanningOutputs,type AssignmentPlanningOutputStore } from '../planning/assignment-planning-output-service.ts';
import { integrateAssignmentEstimate } from '../planning/estimates/integration.ts';
import { normalizeProviderAssignmentLeaseSeconds } from './assignment-lease-service.ts';
import { terminalAssignmentAuthority } from './assignment-terminal-authority.ts';
import { composeAssignmentLifecycleOutput } from './assignment-lifecycle-output.ts';
Expand Down Expand Up @@ -290,6 +291,7 @@ export class ProviderAssignmentLifecycleService {
const assignmentResult = assignment.assignmentAttempt
? validateAssignmentResultCompletion(assignment, terminalInput as JsonRecord)
: null;
if (assignmentResult) await integrateAssignmentEstimate(this.store, assignment, assignmentResult);
const reviewDisposition = assignmentResult
? await resolveReviewDisposition(this.store, assignment, assignmentResult)
: null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { validatePortableContentData } from '@treeseed/sdk/content-validation';
import type { AssignmentResult } from '@treeseed/sdk/agent-capacity';
import { CapacityGovernanceError } from '../../../../../database.ts';
import type { DurableProviderAssignment } from '../../../../../repositories/capacity/assignments/assignment.ts';
import { readExactProposal } from '../../../../../../governance/executable-proposal.ts';
import { commitProposalVersionContent } from '../../../../../../control-plane/governance/proposal-version-content.ts';
import { resolveKnowledgeGatewayConnection } from '../../../../../../knowledge/gateway-treedx-connection.ts';
import type { AssignmentPlanningOutputStore } from '../assignment-planning-output-service.ts';

type Row = Record<string, unknown>;
const record = (value: unknown): Row => value && typeof value === 'object' && !Array.isArray(value) ? value as Row : {};
const text = (value: unknown): string => typeof value === 'string' ? value.trim() : '';
const stable = (value: unknown): string => Array.isArray(value) ? `[${value.map(stable).join(',')}]`
: value && typeof value === 'object' ? `{${Object.entries(value as Row).sort(([a], [b]) => a.localeCompare(b))
.map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`).join(',')}}` : JSON.stringify(value);

function workItems(value: Row): Row[] {
return Array.isArray(record(value.executionPlan).workItems) ? record(value.executionPlan).workItems as Row[] : [];
}

/** One estimate belongs to the exact work item selected by the graph, not to a new plan authority. */
export function mergeAssignmentEstimate(input: {
frozen: Row; candidate: Row; current: Row; workItemId: string | null;
}): Row {
const frozenItems = workItems(input.frozen);
const candidateItems = workItems(input.candidate);
const currentItems = workItems(input.current);
const reviewer = input.workItemId === null;
const index = reviewer ? -1 : frozenItems.findIndex((item) => item.id === input.workItemId);
if ((!reviewer && index < 0) || !frozenItems.length || frozenItems.length !== candidateItems.length || frozenItems.length !== currentItems.length
|| new Set(frozenItems.map((item) => item.id)).size !== frozenItems.length) {
throw new CapacityGovernanceError('assignment_estimate_work_item_invalid', 'Estimator result does not target one frozen proposal work item.', 409);
}
const withoutEstimate = (item: Row): Row => { const { estimate: _estimate, reviewEstimate: _reviewEstimate, ...rest } = item; return rest; };
if (stable({ ...input.candidate, executionPlan: { ...record(input.candidate.executionPlan), workItems: candidateItems.map(withoutEstimate) } })
!== stable({ ...input.frozen, executionPlan: { ...record(input.frozen.executionPlan), workItems: frozenItems.map(withoutEstimate) } })) {
throw new CapacityGovernanceError('assignment_estimate_unrelated_change', 'Estimator result changed proposal fields outside its assigned estimate.', 409);
}
if (candidateItems.some((item, itemIndex) =>
stable(reviewer ? item.estimate : item.reviewEstimate) !== stable(reviewer ? frozenItems[itemIndex].estimate : frozenItems[itemIndex].reviewEstimate)
|| (!reviewer && itemIndex !== index && stable(item.estimate) !== stable(frozenItems[itemIndex].estimate)))) {
throw new CapacityGovernanceError('assignment_estimate_unrelated_change', 'Estimator result changed another work item estimate.', 409);
}
if (currentItems.some((item, itemIndex) => item.id !== frozenItems[itemIndex].id
|| stable(withoutEstimate(item)) !== stable(withoutEstimate(frozenItems[itemIndex])))) {
throw new CapacityGovernanceError('assignment_estimate_source_changed', 'Current proposal work items no longer match the frozen assignment source.', 409);
}
const field = reviewer ? 'reviewEstimate' : 'estimate';
const targets = reviewer ? frozenItems.map((_, itemIndex) => itemIndex) : [index];
for (const target of targets) {
const estimate = record(candidateItems[target][field]);
if (!Object.keys(estimate).length || !text(estimate.rationale)) throw new CapacityGovernanceError(
'assignment_estimate_missing', 'Estimator result must include a structured estimate and rationale for each assigned work item.', 409);
const existing = currentItems[target][field];
if (existing !== undefined && stable(existing) !== stable(estimate)) throw new CapacityGovernanceError(
'assignment_estimate_conflict', 'The assigned work item already has a different estimate.', 409);
}
return { ...input.current, executionPlan: { ...record(input.current.executionPlan),
workItems: currentItems.map((item, itemIndex) => targets.includes(itemIndex)
? { ...item, [field]: candidateItems[itemIndex][field] } : item) } };
}

export async function integrateAssignmentEstimate(
store: AssignmentPlanningOutputStore, assignment: DurableProviderAssignment, result: AssignmentResult,
): Promise<void> {
if (assignment.assignmentAttempt?.effectiveProfile.activity !== 'estimating') return;
const attempt = assignment.assignmentAttempt;
if (attempt.sourceRef.model !== 'proposal' || !assignment.agentId) throw new CapacityGovernanceError(
'assignment_estimate_source_missing', 'Estimating assignment lacks a frozen proposal, work item, or agent.', 409);
const proposal = await store.getGovernanceProposal(attempt.sourceRef.id);
if (!proposal || proposal.projectId !== assignment.projectId || proposal.teamId !== assignment.teamId) throw new CapacityGovernanceError(
'assignment_estimate_proposal_mismatch', 'Estimating assignment proposal is outside its project and team.', 409);
const frozen = await readExactProposal(store, proposal, attempt.sourceRef);
const current = await readExactProposal(store, proposal);
const matches = result.references.filter((reference): reference is Extract<AssignmentResult['references'][number], { kind: 'treedx' }> => reference.kind === 'treedx'
&& reference.projectId === assignment.projectId && reference.repository === attempt.sourceRef.repository
&& reference.path === attempt.sourceRef.path);
if (matches.length !== 1) throw new CapacityGovernanceError('assignment_estimate_reference_invalid',
'Estimator result must cite exactly one proposal commit in its authorized project library.', 409);
const reference = matches[0]!;
if (attempt.workspace.mode !== 'treedx' || reference.workspaceId !== attempt.workspace.workspaceId) throw new CapacityGovernanceError(
'assignment_estimate_workspace_mismatch', 'Estimator result did not originate from its authorized workspace.', 409);
const connection = await resolveKnowledgeGatewayConnection(store, { projectId: assignment.projectId,
write: false, relationPaths: true, readRefs: [reference.commit] });
if (!connection || connection.repositoryId !== reference.repository) throw new CapacityGovernanceError(
'assignment_estimate_repository_changed', 'Estimator proposal repository binding changed.', 409);
const response = record(await connection.client.readRepositoryFile({ repoId: reference.repository,
ref: reference.commit, path: reference.path, encoding: 'utf8', parseFrontmatter: true, allowProtected: true }));
if (text(response.resolvedRef) !== reference.commit) throw new CapacityGovernanceError(
'assignment_estimate_ref_moved', 'Estimator proposal result did not resolve to its exact commit.', 409);
const file = record(response.file ?? (Array.isArray(response.files) ? response.files[0] : null));
const source = text(file.content);
if (!source) throw new CapacityGovernanceError(
'assignment_estimate_content_missing', 'Estimator proposal result has no content.', 409);
const parsed = validatePortableContentData('proposal', record(file.frontmatter));
if (!parsed.ok || !parsed.data) throw new CapacityGovernanceError('assignment_estimate_content_invalid',
'Estimator proposal content is invalid.', 409, { diagnostics: parsed.diagnostics });
const candidate = parsed.data as Row;
const merged = mergeAssignmentEstimate({ frozen: frozen.definition, candidate, current: current.definition,
workItemId: attempt.workItemId ?? null });
if (stable(merged) === stable(current.definition)) return;
const allEstimated = workItems(merged).every((item) => Object.keys(record(item.estimate)).length > 0
&& (item.review !== 'required' || Object.keys(record(item.reviewEstimate)).length > 0));
const authored = await commitProposalVersionContent({ store, proposal,
principal: { id: assignment.agentId, name: assignment.agentId },
update: { title: proposal.title, summary: proposal.summary, body: proposal.body,
proposalTypes: proposal.proposalTypes, status: allEstimated ? 'ready' : merged.status,
objectiveRefs: merged.objectiveRefs, evidenceRefs: merged.evidenceRefs,
discussionRef: merged.discussionRef, executionPlan: merged.executionPlan,
workdayId: assignment.workDayId, expectedProposalVersion: proposal.activeVersion,
changeReason: `Integrate ${attempt.workItemId ?? 'review'} estimate from assignment ${assignment.id}.` } });
await store.updateGovernanceProposalDraft({ id: assignment.agentId, type: 'agent' }, text(proposal.id),
{ ...authored.update, createdByType: 'agent', createdById: assignment.agentId });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, expect, it } from 'vitest';
import { mergeAssignmentEstimate } from '../../../../../src/api/capacity/services/capacity/assignments/planning/estimates/integration.ts';

const estimate = { minimumSeconds: 100, expectedSeconds: 200, maximumSeconds: 300, rationale: 'Exact source inspection.' };
const frozen = { id: 'proposal-1', projectId: 'project-1', status: 'draft', executionPlan: { workItems: [
{ id: 'research', objective: 'Inspect the source.' },
{ id: 'implementation', objective: 'Implement the contract.' },
] } };
const candidate = { ...frozen, executionPlan: { workItems: [
frozen.executionPlan.workItems[0], { ...frozen.executionPlan.workItems[1], estimate },
] } };

describe('exact estimator result integration', () => {
it('merges only the assigned estimate while preserving another completed estimate', () => {
const prior = { minimumSeconds: 60, expectedSeconds: 120, maximumSeconds: 180, rationale: 'Research evidence.' };
const current = { ...frozen, executionPlan: { workItems: [
{ ...frozen.executionPlan.workItems[0], estimate: prior }, frozen.executionPlan.workItems[1],
] } };
const merged = mergeAssignmentEstimate({ frozen, candidate, current, workItemId: 'implementation' });
expect(merged.executionPlan).toEqual({ workItems: [
{ ...frozen.executionPlan.workItems[0], estimate: prior },
{ ...frozen.executionPlan.workItems[1], estimate },
] });
expect(mergeAssignmentEstimate({ frozen, candidate, current: merged, workItemId: 'implementation' })).toEqual(merged);
});
it('rejects unrelated content and a competing estimate', () => {
expect(() => mergeAssignmentEstimate({ frozen, candidate: { ...candidate, title: 'New authority' }, current: frozen,
workItemId: 'implementation' })).toThrow('outside its assigned estimate');
const current = { ...frozen, executionPlan: { workItems: [frozen.executionPlan.workItems[0],
{ ...frozen.executionPlan.workItems[1], estimate: { ...estimate, expectedSeconds: 250 } }] } };
expect(() => mergeAssignmentEstimate({ frozen, candidate, current, workItemId: 'implementation' })).toThrow('different estimate');
});
it('rejects missing, mistargeted, or changed work items', () => {
expect(() => mergeAssignmentEstimate({ frozen, candidate: frozen, current: frozen,
workItemId: 'implementation' })).toThrow('structured estimate');
expect(() => mergeAssignmentEstimate({ frozen, candidate, current: frozen,
workItemId: 'unknown' })).toThrow('frozen proposal work item');
const changed = { ...frozen, executionPlan: { workItems: [frozen.executionPlan.workItems[0],
{ ...frozen.executionPlan.workItems[1], objective: 'Changed objective.' }] } };
expect(() => mergeAssignmentEstimate({ frozen, candidate, current: changed,
workItemId: 'implementation' })).toThrow('no longer match');
});
it('merges Reviewer estimates for every work item without replacing actor estimates', () => {
const current = { ...frozen, executionPlan: { workItems: [
{ ...frozen.executionPlan.workItems[0], estimate }, frozen.executionPlan.workItems[1],
] } };
const reviewEstimate = { ...estimate, rationale: 'Independent review of the work.' };
const reviewCandidate = { ...frozen, executionPlan: { workItems: frozen.executionPlan.workItems.map((item) =>
({ ...item, reviewEstimate })) } };
const merged = mergeAssignmentEstimate({ frozen, candidate: reviewCandidate, current, workItemId: null });
expect((merged.executionPlan as { workItems: unknown[] }).workItems).toEqual([
{ ...current.executionPlan.workItems[0], reviewEstimate },
{ ...current.executionPlan.workItems[1], reviewEstimate },
]);
expect(() => mergeAssignmentEstimate({ frozen, candidate: { ...reviewCandidate,
executionPlan: { workItems: [reviewCandidate.executionPlan.workItems[0], frozen.executionPlan.workItems[1]] } },
current, workItemId: null })).toThrow('each assigned work item');
});
});
Loading