diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index fab6881..576218b 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000260-deep-authenticated-acceptance.json", - "task_id": "DEV-000260", + "task_file": "dev/tasks/DEV-000261-durable-additional-task-evaluation-scheduling.json", + "task_id": "DEV-000261", "task_revision": 1 } diff --git a/dev/tasks/DEV-000261-durable-additional-task-evaluation-scheduling.json b/dev/tasks/DEV-000261-durable-additional-task-evaluation-scheduling.json new file mode 100644 index 0000000..87dd181 --- /dev/null +++ b/dev/tasks/DEV-000261-durable-additional-task-evaluation-scheduling.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000261", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "d2dd38b", + "risk_tier": "E", + "intent": "Connect unresolved model-evaluation evidence to a bounded additional Work Graph and durable dispatch schedule.", + "requirement_ids": ["model fleet/failover", "general-first qualification", "adaptive additional tests", "durable parallel dispatch"], + "allowed_change_scope": {"modules": ["work", "capability-evaluation"], "paths": ["modules/work/application/graph_runtime.go", "modules/work/application/task_evaluation_dispatch_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000261-durable-additional-task-evaluation-scheduling.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "LLM-local/**", "cmd/**", "adapters/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["TaskEvaluationExpansionRequest", "UnresolvedTaskEvidence", "Work Graph admission and dispatch ports"], "may_change": ["application orchestration function only"]}, + "semantic_owner": "Work application schedules; Capability domain decides qualification expansion", + "canonical_state": ["work graph and dispatch queue remain durable owners"], + "state_transitions": ["completed unresolved general evaluation -> bounded additional graph -> leased/queued specialist dispatch"], + "side_effects_and_privileges": ["durable graph/dispatch persistence through existing ports; no provider or external mutation"], + "persistence_and_migrations": ["none; existing graph and dispatch persistence is reused"], + "failure_retry_restart": ["existing graph admission and worker lease/recovery semantics remain authoritative; qualified candidate suppresses expansion"], + "assumptions": ["caller supplies fresh dispatch identities and bounded additional benchmark version", "independent verifier remains required before qualification promotion"], + "acceptance_scenarios": ["unresolved general result creates exactly two bounded additional cases", "additional graph preserves parent evidence lineage", "fresh dispatch intent is queued through existing scheduler", "qualified candidate creates no additional graph"], + "required_tests": ["go test ./modules/work/application ./modules/work/domain ./modules/capability/domain", "go vet ./modules/work/application ./modules/work/domain ./modules/capability/domain", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "independent evidence review", "git diff --check", "CI", "production health smoke"], + "terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"] +} diff --git a/modules/work/application/graph_runtime.go b/modules/work/application/graph_runtime.go index 0ef6598..427ffcb 100644 --- a/modules/work/application/graph_runtime.go +++ b/modules/work/application/graph_runtime.go @@ -29,6 +29,19 @@ type TaskEvaluationScheduleRequest struct { Now int64 `json:"now_unix_millis"` } +// TaskEvaluationExpansionScheduleRequest carries a typed expansion decision +// from Capability to Work. Capability owns the decision and test-case +// construction; Work owns durable graph lineage, persistence and dispatch. +type TaskEvaluationExpansionScheduleRequest struct { + Profile capability.TaskEvaluationProfile + Cases []capability.TaskEvaluationCase + ParentGraphState work.GraphState + TriggerEvidenceRef string + Plan work.TaskEvaluationGraphRequest + Intents []work.AgentDispatchIntent + Now int64 +} + type GraphCompleteRequest struct { GraphID string `json:"graph_id"` Revision int64 `json:"revision"` @@ -229,6 +242,26 @@ func AdmitAndScheduleTaskEvaluation(ctx context.Context, store ports.GraphStore, return AdmitAndSchedule(ctx, store, GraphScheduleRequest{Graph: graph, Intents: intents, Now: request.Now}) } +// ScheduleAdditionalTaskEvaluation is the runtime hand-off for the +// "general-first, additional-when-unresolved" policy. Capability decides +// whether to expand and supplies the bounded cases; this function never +// invents cases or chooses a model, and only persists/enqueues the new graph. +func ScheduleAdditionalTaskEvaluation(ctx context.Context, store ports.GraphStore, request TaskEvaluationExpansionScheduleRequest) (work.WorkGraph, []work.GraphNodeID, error) { + if request.Profile.Stage != capability.TaskEvaluationAdditional || request.Profile.ParentProfileID == "" || request.TriggerEvidenceRef == "" || len(request.Cases) == 0 { + return work.WorkGraph{}, nil, errors.New("additional task evaluation lineage is incomplete") + } + plan := request.Plan + plan.Profile = request.Profile + plan.Cases = append([]capability.TaskEvaluationCase(nil), request.Cases...) + plan.ParentGraphState = request.ParentGraphState + plan.TriggerEvidenceRef = request.TriggerEvidenceRef + graph, ready, err := AdmitAndScheduleTaskEvaluation(ctx, store, TaskEvaluationScheduleRequest{Plan: plan, Intents: request.Intents, Now: request.Now}) + if err != nil { + return work.WorkGraph{}, nil, err + } + return graph, ready, nil +} + func CompleteGraphNode(ctx context.Context, store ports.GraphStore, graphID string, revision int64, nodeID work.GraphNodeID, success bool) (work.WorkGraph, error) { graph, err := store.LoadWorkGraph(ctx, graphID, revision) if err != nil { diff --git a/modules/work/application/task_evaluation_dispatch_test.go b/modules/work/application/task_evaluation_dispatch_test.go index ab6d4b8..e86db36 100644 --- a/modules/work/application/task_evaluation_dispatch_test.go +++ b/modules/work/application/task_evaluation_dispatch_test.go @@ -45,3 +45,35 @@ func TestAdmitAndScheduleTaskEvaluationCarriesSpecialistContext(t *testing.T) { t.Fatalf("specialist context was not carried into dispatch: %+v", store.intents) } } + +func TestExpandAndScheduleTaskEvaluationCreatesAdditionalDurableGraph(t *testing.T) { + store := &taskGraphStore{} + additional := capability.TaskEvaluationProfile{ID: "profile-additional", Revision: 1, CapabilityID: "capability-chat", Stage: capability.TaskEvaluationAdditional, ParentProfileID: "profile-general", TriggerEvidenceRef: "artifact-unresolved", BenchmarkVersion: "additional-v1", RequiredCaseIDs: []string{"case-a:reproduce", "case-a:address-failure"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100} + request := TaskEvaluationExpansionScheduleRequest{ + Profile: additional, Cases: []capability.TaskEvaluationCase{{CaseID: "case-a:reproduce", Prompt: "проверь доступность", EvidenceArtifactRef: "artifact-unresolved"}, {CaseID: "case-a:address-failure", Prompt: "проверь доступность; обработай неизвестный результат", EvidenceArtifactRef: "artifact-unresolved"}}, + ParentGraphState: work.GraphCompleted, TriggerEvidenceRef: "artifact-unresolved", + Plan: work.TaskEvaluationGraphRequest{ + GraphID: "graph-additional", BindingIDs: []capability.CapabilityBindingID{"binding-a"}, DispatchIDs: []string{"dispatch-additional"}, IdempotencyPrefix: "additional", Budget: work.GraphBudget{MaxParallel: 1, MaxAttempts: 2}, ParentGraphID: "graph-general", ParentGraphRevision: 2, + }, + Intents: []work.AgentDispatchIntent{{DispatchID: "dispatch-additional", IdempotencyKey: "idem-additional", DelegationID: "delegation-a", RunID: "run-a", ImplementationID: "impl", ImplementationRevision: 1, SourceRevision: "source", WorktreeID: "tree", VerificationPlanRef: "verify", CreatedAtUnixMillis: 1}}, + Now: 20, + } + graph, ready, err := ScheduleAdditionalTaskEvaluation(context.Background(), store, request) + if err != nil || len(ready) != 1 || graph.State != work.GraphRunning { + t.Fatalf("graph=%+v ready=%v err=%v", graph, ready, err) + } + if len(graph.Nodes) != 1 || graph.Nodes[0].TaskProfileID != "profile-additional" || len(graph.Nodes[0].CaseIDs) != 2 || graph.TriggerEvidenceRef != "artifact-unresolved" { + t.Fatalf("additional graph did not preserve bounded expansion lineage: %+v", graph) + } + if len(store.intents) != 1 || store.intents[0].TaskEvaluationProfileID != "profile-additional" { + t.Fatalf("additional dispatch context missing: %+v", store.intents) + } +} + +func TestScheduleAdditionalTaskEvaluationRejectsIncompleteLineage(t *testing.T) { + store := &taskGraphStore{} + _, _, err := ScheduleAdditionalTaskEvaluation(context.Background(), store, TaskEvaluationExpansionScheduleRequest{Profile: capability.TaskEvaluationProfile{Stage: capability.TaskEvaluationGeneral}}) + if err == nil || len(store.intents) != 0 { + t.Fatalf("incomplete expansion lineage was accepted: intents=%v err=%v", store.intents, err) + } +}