Compare commits

...
Author SHA1 Message Date
m 18d0911810 fix: serialize graph synthesis request fields
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 1m3s
2026-08-23 02:08:24 +03:00
m dcf8cbc855 Merge pull request 'feat: persist verified graph synthesis runtime' (#162) from codex/wp-403-graph-persistence into main
CI / test (push) Successful in 1m16s
2026-08-22 22:56:25 +00:00
m 3d30681279 feat: persist verified graph synthesis runtime
CI / test (push) Successful in 3m1s
CI / test (pull_request) Successful in 2m1s
2026-08-23 01:52:39 +03:00
m 41bc4b567b Merge pull request 'feat: gate graph synthesis on independent verification' (#161) from codex/wp-403-conflict-synthesis into main
CI / test (push) Successful in 1m3s
2026-08-22 22:45:18 +00:00
m 38f8f816d5 feat: gate graph synthesis on independent verification
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m33s
2026-08-23 01:43:35 +03:00
m 4d9d1b8023 Merge pull request 'feat: add isolated governed scaffold candidate' (#160) from codex/wp-602-scaffold-factory into main
CI / test (push) Successful in 1m28s
2026-08-22 22:37:46 +00:00
m c60a575fda feat: add isolated governed scaffold candidate
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 57s
2026-08-23 01:36:03 +03:00
m a09a7e811f Merge pull request 'feat: generate governed extension proposals from repeated evidence' (#159) from codex/wp-500-reflection-worker into main
CI / test (push) Successful in 1m10s
2026-08-22 22:29:49 +00:00
m ecbb34627a feat: generate governed extension proposals from repeated evidence
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m46s
2026-08-23 01:26:41 +03:00
m cdbccb6238 Merge pull request 'fix: align agent worker degraded status log' (#158) from codex/agent-bridge-protected-config into main
CI / test (push) Successful in 1m6s
2026-08-22 21:57:11 +00:00
19 changed files with 680 additions and 18 deletions
+20
View File
@@ -199,6 +199,26 @@ func (r *WorkRepository) LoadWorkGraph(ctx context.Context, graphID string, revi
return graph, nil
}
func (r *WorkRepository) SaveGraphSynthesis(ctx context.Context, conflict work.ConflictSet, candidates []work.ResultCandidate, synthesis work.GraphSynthesisCandidate) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
for _, candidate := range candidates {
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_result_candidates(result_candidate_id,graph_id,graph_revision,node_id,author_run_id,artifact_refs,evidence_summary,summary) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(result_candidate_id) DO NOTHING`, candidate.ID, candidate.GraphID, candidate.GraphRevision, candidate.NodeID, candidate.AuthorRunID, candidate.ArtifactRefs, candidate.EvidenceSummary, candidate.Summary); err != nil {
return err
}
}
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_conflicts(conflict_id,graph_id,graph_revision,node_id,candidate_ids,resolved) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(conflict_id) DO NOTHING`, conflict.ID, conflict.GraphID, conflict.GraphRevision, conflict.NodeID, conflict.CandidateIDs, conflict.Resolved); err != nil {
return err
}
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_synthesis_candidates(synthesis_id,graph_id,graph_revision,node_id,candidate_ids,selected_candidate_id,dissent_candidate_ids,verifier_run_id,verifier_identity,evidence_refs,summary,state) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT(synthesis_id) DO NOTHING`, synthesis.ID, synthesis.GraphID, synthesis.GraphRevision, synthesis.NodeID, synthesis.CandidateIDs, synthesis.SelectedCandidateID, synthesis.DissentCandidateIDs, synthesis.VerifierRunID, synthesis.VerifierIdentity, synthesis.EvidenceRefs, synthesis.Summary, synthesis.State); err != nil {
return err
}
return tx.Commit(ctx)
}
func (r *WorkRepository) RecoverStaleClaims(ctx context.Context, nowUnixMillis, staleAfterMillis int64) (int64, error) {
command, err := r.pool.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state='QUEUED',claimed_by='',updated_at_unix_millis=$1 WHERE state='CLAIMED' AND updated_at_unix_millis <= $2`, nowUnixMillis, nowUnixMillis-staleAfterMillis)
if err != nil {
+13
View File
@@ -55,6 +55,19 @@ func main() {
}
writeJSON(w, graph)
})
mux.HandleFunc("POST /v1/work-graphs/synthesis", func(w http.ResponseWriter, r *http.Request) {
var request application.GraphSynthesisRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&request); err != nil {
http.Error(w, "invalid graph synthesis request", http.StatusBadRequest)
return
}
conflict, synthesis, err := application.SynthesizeGraph(r.Context(), repository, request)
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
writeJSON(w, map[string]any{"conflict": conflict, "synthesis": synthesis})
})
addr := os.Getenv("LISTEN_ADDR")
if addr == "" {
addr = ":8088"
+36 -14
View File
@@ -53,21 +53,43 @@ type WorkGraph struct {
}
type ResultCandidate struct {
ID string
GraphID string
GraphRevision int64
NodeID GraphNodeID
AuthorRunID AgentRunID
ArtifactRefs []EvidenceRef
EvidenceSummary string
Summary string
ID string `json:"id"`
GraphID string `json:"graph_id"`
GraphRevision int64 `json:"graph_revision"`
NodeID GraphNodeID `json:"node_id"`
AuthorRunID AgentRunID `json:"author_run_id"`
ArtifactRefs []EvidenceRef `json:"artifact_refs"`
EvidenceSummary string `json:"evidence_summary"`
Summary string `json:"summary"`
}
type ConflictSet struct {
ID string
GraphID string
GraphRevision int64
NodeID GraphNodeID
CandidateIDs []string
Resolved bool
ID string `json:"id"`
GraphID string `json:"graph_id"`
GraphRevision int64 `json:"graph_revision"`
NodeID GraphNodeID `json:"node_id"`
CandidateIDs []string `json:"candidate_ids"`
Resolved bool `json:"resolved"`
}
type GraphSynthesisState string
const GraphSynthesisVerified GraphSynthesisState = "VERIFIED"
// GraphSynthesisCandidate retains every competing result while recording the
// independently verified selection. It is a candidate, not an external action
// or authority grant.
type GraphSynthesisCandidate struct {
ID string `json:"id"`
GraphID string `json:"graph_id"`
GraphRevision int64 `json:"graph_revision"`
NodeID GraphNodeID `json:"node_id"`
CandidateIDs []string `json:"candidate_ids"`
SelectedCandidateID string `json:"selected_candidate_id"`
DissentCandidateIDs []string `json:"dissent_candidate_ids"`
VerifierRunID AgentRunID `json:"verifier_run_id"`
VerifierIdentity string `json:"verifier_identity"`
EvidenceRefs []EvidenceRef `json:"evidence_refs"`
Summary string `json:"summary"`
State GraphSynthesisState `json:"state"`
}
+100
View File
@@ -1,5 +1,7 @@
package work
import "time"
type DevelopmentStage string
const (
@@ -27,3 +29,101 @@ type DevelopmentGate struct {
PromotionRef string
RollbackRef string
}
// ExtensionProposalStage is deliberately proposal-only in this contract. A
// proposal does not authorize source changes, provider calls, deployment or
// promotion; those stages require the existing governed development gate.
type ExtensionProposalStage string
const ExtensionProposalProposed ExtensionProposalStage = "PROPOSED"
// ExtensionEvidence is an immutable reference to an observation that points
// at a repeated or corroborated improvement signal. The payload is owned by
// its source module; Work stores only the typed reference and stable signal
// key.
type ExtensionEvidence struct {
Ref string
SignalKey string
}
// ExtensionProposalRequest contains the operator/governance information
// needed to decide whether an evidence pattern is worth reviewing. Empty
// fields are rejected by the proposal generator rather than inferred.
type ExtensionProposalRequest struct {
ID string
DesiredOutcome string
SmallestChangeLayer string
AffectedOwners []string
AffectedContracts []string
RiskTier string
EvaluationPlan []string
Alternatives []string
RollbackPlan string
Assumptions []string
CreatedAt time.Time
}
// ExtensionProposal is a bounded, evidence-bearing candidate for governed
// development. It intentionally contains no executable command, credential,
// authority grant, implementation body or promotion decision.
type ExtensionProposal struct {
ID string
Stage ExtensionProposalStage
EvidenceRefs []string
DeduplicatedSignalKeys []string
DesiredOutcome string
SmallestChangeLayer string
AffectedOwners []string
AffectedContracts []string
RiskTier string
EvaluationPlan []string
Alternatives []string
RollbackPlan string
Assumptions []string
CreatedAtUnixMillis int64
}
type DevelopmentScaffoldStage string
const DevelopmentScaffoldSandboxed DevelopmentScaffoldStage = "SANDBOXED"
// DevelopmentScaffoldRequest describes the boundary a later control plane
// would use to create an isolated worktree. This value object itself performs
// no checkout or filesystem operation.
type DevelopmentScaffoldRequest struct {
ID string
ProposalRef string
BaseRepoRevision string
WorktreeRef string
TaskIntent string
Modules []string
AllowedPaths []string
ForbiddenPaths []string
ProtectedPaths []string
RequiredTests []string
ArtifactLineageRefs []string
ProtectedMainAccess string
ProductionAccess string
SecretAccess string
CreatedAt time.Time
}
// DevelopmentScaffold is a provenance-linked task-packet candidate. It has
// no executable instruction, credential, checkout handle or authority grant.
type DevelopmentScaffold struct {
ID string
Stage DevelopmentScaffoldStage
ProposalRef string
BaseRepoRevision string
WorktreeRef string
TaskIntent string
Modules []string
AllowedPaths []string
ForbiddenPaths []string
RequiredTests []string
ArtifactLineageRefs []string
ProtectedMainAccess string
ProductionAccess string
SecretAccess string
CreatedAtUnixMillis int64
}
@@ -0,0 +1,58 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.graph_result_candidates (
result_candidate_id TEXT PRIMARY KEY,
graph_id TEXT NOT NULL,
graph_revision BIGINT NOT NULL,
node_id TEXT NOT NULL,
author_run_id TEXT NOT NULL,
artifact_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
evidence_summary TEXT NOT NULL,
summary TEXT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (result_candidate_id <> ''), CHECK (graph_id <> ''), CHECK (graph_revision > 0),
CHECK (node_id <> ''), CHECK (author_run_id <> ''), CHECK (evidence_summary <> ''), CHECK (summary <> ''),
FOREIGN KEY (graph_id, graph_revision) REFERENCES work.graphs(graph_id, revision)
);
CREATE TABLE IF NOT EXISTS work.graph_conflicts (
conflict_id TEXT PRIMARY KEY,
graph_id TEXT NOT NULL,
graph_revision BIGINT NOT NULL,
node_id TEXT NOT NULL,
candidate_ids TEXT[] NOT NULL,
resolved BOOLEAN NOT NULL DEFAULT FALSE,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (conflict_id <> ''), CHECK (graph_id <> ''), CHECK (graph_revision > 0), CHECK (node_id <> ''),
CHECK (cardinality(candidate_ids) >= 2),
FOREIGN KEY (graph_id, graph_revision) REFERENCES work.graphs(graph_id, revision)
);
CREATE TABLE IF NOT EXISTS work.graph_synthesis_candidates (
synthesis_id TEXT PRIMARY KEY,
graph_id TEXT NOT NULL,
graph_revision BIGINT NOT NULL,
node_id TEXT NOT NULL,
candidate_ids TEXT[] NOT NULL,
selected_candidate_id TEXT NOT NULL,
dissent_candidate_ids TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
verifier_run_id TEXT NOT NULL,
verifier_identity TEXT NOT NULL,
evidence_refs TEXT[] NOT NULL,
summary TEXT NOT NULL,
state TEXT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (synthesis_id <> ''), CHECK (graph_id <> ''), CHECK (graph_revision > 0), CHECK (node_id <> ''),
CHECK (cardinality(candidate_ids) >= 2), CHECK (selected_candidate_id <> ''), CHECK (verifier_run_id <> ''),
CHECK (verifier_identity <> ''), CHECK (cardinality(evidence_refs) >= 1), CHECK (summary <> ''), CHECK (state = 'VERIFIED'),
FOREIGN KEY (graph_id, graph_revision) REFERENCES work.graphs(graph_id, revision)
);
CREATE INDEX IF NOT EXISTS graph_result_candidates_node_idx ON work.graph_result_candidates(graph_id, graph_revision, node_id);
CREATE INDEX IF NOT EXISTS graph_conflicts_node_idx ON work.graph_conflicts(graph_id, graph_revision, node_id);
CREATE INDEX IF NOT EXISTS graph_synthesis_node_idx ON work.graph_synthesis_candidates(graph_id, graph_revision, node_id);
RESET ROLE;
COMMIT;
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000185-agent-bridge-protected-config.json",
"task_id": "DEV-000185",
"task_file": "dev/tasks/DEV-000189-durable-graph-synthesis-runtime.json",
"task_id": "DEV-000189",
"task_revision": 1
}
@@ -0,0 +1,37 @@
{
"task_id": "DEV-000186",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "cdbccb6",
"risk_tier": "E",
"intent": "Add a typed, proposal-only ExtensionProposal generator from repeated or corroborated evidence without granting mutation, authority, deployment, or promotion capability.",
"requirement_ids": ["WP-601", "Extension proposal generator", "Evidence deduplication", "Governed rollback plan"],
"allowed_change_scope": {
"modules": ["work-agent-execution"],
"paths": ["contracts/work/self_development.go", "modules/work/domain/self_development.go", "modules/work/domain/self_development_test.go", "tests/semantic_evolution/self_development_contract_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000186-extension-proposal-generator.json"]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "database/**", "cmd/**", "adapters/**", "deploy/**", "contracts/authority/**", ".gitea/**"]
},
"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": ["ADR-0022 governed self-development boundary", "existing Work DevelopmentGate"],
"may_change": ["typed Work ExtensionProposal value objects"]
},
"semantic_owner": "work-agent-execution",
"canonical_state": ["No durable canonical state; proposal value object only"],
"state_transitions": ["repeated evidence -> PROPOSED ExtensionProposal", "insufficient/conflicting/incomplete evidence -> rejected result"],
"side_effects_and_privileges": ["No source mutation, provider invocation, secret access, deployment, promotion, or authority expansion"],
"persistence_and_migrations": ["No schema or migration changes"],
"failure_retry_restart": ["Deterministic generation is rerunnable; unique evidence refs and signal keys preserve lineage; one failure cannot generate a proposal"],
"assumptions": ["Human governance remains the promotion authority", "A later task may add durable proposal persistence only after owner and migration review"],
"acceptance_scenarios": ["one failure is rejected", "repeated same-signal evidence produces one proposal with deduplicated signal keys and retained refs", "missing assumptions/evaluation/rollback fails closed", "proposal contains no executable authority or mutation field"],
"required_tests": ["go test ./modules/work/domain ./tests/semantic_evolution", "go vet ./...", "go run ./cmd/devctl preflight -task dev/tasks/DEV-000186-extension-proposal-generator.json", "go run ./cmd/devctl check-diff -task dev/tasks/DEV-000186-extension-proposal-generator.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,26 @@
{
"task_id": "DEV-000187",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "a09a7e8",
"risk_tier": "E",
"intent": "Add a fail-closed typed scaffold candidate factory that produces an isolated task-packet descriptor without creating files, reading secrets, or accessing protected main/production.",
"requirement_ids": ["WP-602", "Scoped task packet candidate", "Isolated worktree provenance", "Protected path denial"],
"allowed_change_scope": {
"modules": ["work-agent-execution"],
"paths": ["contracts/work/self_development.go", "modules/work/domain/self_development.go", "modules/work/domain/self_development_test.go", "tests/semantic_evolution/self_development_contract_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000187-scaffold-task-packet-candidate.json"]
},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "cmd/**", "adapters/**", "deploy/**", "contracts/authority/**", ".gitea/**"]},
"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": ["Existing ExtensionProposal", "ADR-0022 governed development boundary"], "may_change": ["typed Work scaffold candidate value objects"]},
"semantic_owner": "work-agent-execution",
"canonical_state": ["No durable state; isolated scaffold candidate value object only"],
"state_transitions": ["proposal -> SANDBOXED task-packet candidate", "invalid scope/policy -> rejected result"],
"side_effects_and_privileges": ["No filesystem mutation, source checkout, secret access, provider call, deployment, promotion, or authority expansion"],
"persistence_and_migrations": ["No schema or migration changes"],
"failure_retry_restart": ["Deterministic validation is rerunnable; invalid or protected scope fails closed"],
"assumptions": ["A later external development control plane performs actual worktree creation", "Human governance remains promotion authority"],
"acceptance_scenarios": ["valid proposal produces scoped SANDBOXED descriptor", "absolute/traversal/protected path is rejected", "production and secret access policies must be DENIED", "missing provenance or test plan is rejected", "descriptor contains no command or credential material"],
"required_tests": ["go test ./modules/work/domain ./tests/semantic_evolution", "go vet ./...", "go run ./cmd/devctl preflight -task dev/tasks/DEV-000187-scaffold-task-packet-candidate.json", "go run ./cmd/devctl check-diff -task dev/tasks/DEV-000187-scaffold-task-packet-candidate.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000188",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "4d9d1b8",
"risk_tier": "E",
"intent": "Add typed Work Graph synthesis and independent-verifier gating so conflicting node results remain attributable and cannot be resolved by the producing run.",
"requirement_ids": ["WP-403", "ResultCandidate conflict retention", "Synthesis candidate", "Independent graph verifier"],
"allowed_change_scope": {"modules": ["work-agent-execution"], "paths": ["contracts/work/graph.go", "modules/work/domain/graph_scheduler.go", "tests/agent_execution/graph_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000188-graph-synthesis-independent-verifier.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "cmd/**", "adapters/**", "deploy/**", "contracts/authority/**", ".gitea/**"]},
"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": ["Existing WorkGraph, ResultCandidate and ConflictSet contracts", "IndependentVerification semantics"], "may_change": ["typed GraphSynthesisCandidate contract"]},
"semantic_owner": "work-agent-execution",
"canonical_state": ["No new persistence; typed graph synthesis candidate only"],
"state_transitions": ["conflicting candidates -> attributable synthesis candidate", "verified independent synthesis -> resolved conflict", "failed/forged/self-verification -> rejected"],
"side_effects_and_privileges": ["No external mutation, provider call, deployment, or authority expansion"],
"persistence_and_migrations": ["No schema or migration changes"],
"failure_retry_restart": ["Dissent and candidate lineage are retained; verifier must differ from every producer; rejected synthesis does not resolve conflict"],
"assumptions": ["Durable persistence and runtime orchestration already exist and are unchanged", "A later runtime task may persist this typed result"],
"acceptance_scenarios": ["conflicting candidates remain attributable", "same producer cannot verify synthesis", "unknown candidate or missing evidence fails closed", "successful independent verification retains dissent and resolves conflict"],
"required_tests": ["go test ./tests/agent_execution", "go vet ./...", "go run ./cmd/devctl preflight -task dev/tasks/DEV-000188-graph-synthesis-independent-verifier.json", "go run ./cmd/devctl check-diff -task dev/tasks/DEV-000188-graph-synthesis-independent-verifier.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000189",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "41bc4b5",
"risk_tier": "E",
"intent": "Persist Work Graph result candidates, conflicts and independently verified synthesis through the work owner and expose the governed synthesis runtime path.",
"requirement_ids": ["WP-403", "Durable graph result evidence", "Durable conflict resolution", "Synthesis runtime endpoint"],
"allowed_change_scope": {"modules": ["work-agent-execution", "work-storage", "work-graph-runtime"], "paths": ["contracts/work/graph.go", "database/migrations/036_work_graph_result_synthesis.sql", "adapters/postgres/work_repository.go", "modules/work/ports/graph.go", "modules/work/application/graph_runtime.go", "cmd/work-graph-runtime/main.go", "tests/agent_execution/graph_persistence_sql_test.go", "tests/agent_execution/graph_runtime_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000189-durable-graph-synthesis-runtime.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "contracts/authority/**", "modules/epistemics/**", "deploy/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {"consume": ["Existing Work Graph ResultCandidate/ConflictSet/GraphSynthesisCandidate contracts", "WP-403 independent verifier gate"], "may_change": ["GraphResult JSON serialization tags", "GraphStore persistence port"]},
"semantic_owner": "work-agent-execution",
"canonical_state": ["work.graph_result_candidates", "work.graph_conflicts", "work.graph_synthesis_candidates"],
"state_transitions": ["result candidate append -> conflict append -> independently verified synthesis append", "failed verification does not persist a resolved synthesis"],
"side_effects_and_privileges": ["Work schema writes only; no external mutation, provider call, secret expansion, or authority change"],
"persistence_and_migrations": ["Append-only migration 036; one owner-local transaction persists candidates, conflict and synthesis"],
"failure_retry_restart": ["Idempotent exact-record inserts; transaction rollback preserves no partial resolution; restart reloads durable evidence"],
"assumptions": ["Production migration runner applies migration before runtime endpoint use", "Authenticated upstream boundary already protects runtime access"],
"acceptance_scenarios": ["SQL is revision scoped and work-owned", "runtime rejects invalid/self-verifying synthesis", "successful synthesis persists all candidates, dissent and verifier identity atomically", "production canary returns durable success without external side effect"],
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -task dev/tasks/DEV-000189-durable-graph-synthesis-runtime.json", "go run ./cmd/devctl check-diff -task dev/tasks/DEV-000189-durable-graph-synthesis-runtime.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "git diff --check", "CI", "production migration/readiness and authenticated synthesis canary"],
"terminal_gap_states": ["BLOCKED_BY_MIGRATION_DECISION", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
+22
View File
@@ -25,6 +25,28 @@ type GraphCompleteRequest struct {
Succeeded bool `json:"succeeded"`
}
type GraphSynthesisRequest struct {
Conflict work.ConflictSet `json:"conflict"`
Candidates []work.ResultCandidate `json:"candidates"`
SelectedCandidate string `json:"selected_candidate_id"`
VerifierRunID work.AgentRunID `json:"verifier_run_id"`
VerifierIdentity string `json:"verifier_identity"`
EvidenceRefs []work.EvidenceRef `json:"evidence_refs"`
Summary string `json:"summary"`
Verified bool `json:"verified"`
}
func SynthesizeGraph(ctx context.Context, store ports.GraphResultStore, request GraphSynthesisRequest) (work.ConflictSet, work.GraphSynthesisCandidate, error) {
conflict, synthesis, err := domain.BuildVerifiedGraphSynthesis(request.Conflict, request.Candidates, request.SelectedCandidate, request.VerifierRunID, request.VerifierIdentity, request.EvidenceRefs, request.Summary, request.Verified)
if err != nil {
return work.ConflictSet{}, work.GraphSynthesisCandidate{}, err
}
if err := store.SaveGraphSynthesis(ctx, conflict, request.Candidates, synthesis); err != nil {
return work.ConflictSet{}, work.GraphSynthesisCandidate{}, err
}
return conflict, synthesis, nil
}
func AdmitAndSchedule(ctx context.Context, store ports.GraphStore, request GraphScheduleRequest) (work.WorkGraph, []work.GraphNodeID, error) {
if request.Now < 1 {
request.Now = time.Now().UnixMilli()
+49 -2
View File
@@ -8,10 +8,57 @@ import (
)
var (
ErrGraphBudgetExceeded = errors.New("work graph budget exceeded")
ErrConflictUnresolved = errors.New("conflict resolution is not explicit")
ErrGraphBudgetExceeded = errors.New("work graph budget exceeded")
ErrConflictUnresolved = errors.New("conflict resolution is not explicit")
ErrGraphSynthesisRejected = errors.New("graph synthesis rejected")
)
// BuildVerifiedGraphSynthesis resolves a conflict only after an independent
// verifier has evaluated all candidates. The verifier may not be any result
// producer, and every candidate remains in the returned lineage.
func BuildVerifiedGraphSynthesis(conflict work.ConflictSet, candidates []work.ResultCandidate, selectedCandidate string, verifierRunID work.AgentRunID, verifierIdentity string, evidence []work.EvidenceRef, summary string, verified bool) (work.ConflictSet, work.GraphSynthesisCandidate, error) {
if conflict.ID == "" || conflict.GraphID == "" || conflict.GraphRevision < 1 || conflict.NodeID == "" || conflict.Resolved || len(conflict.CandidateIDs) < 2 || selectedCandidate == "" || verifierRunID == "" || verifierIdentity == "" || len(evidence) == 0 || summary == "" || !verified {
return work.ConflictSet{}, work.GraphSynthesisCandidate{}, ErrGraphSynthesisRejected
}
candidateByID := make(map[string]work.ResultCandidate, len(candidates))
for _, candidate := range candidates {
if candidate.ID == "" || candidate.GraphID != conflict.GraphID || candidate.GraphRevision != conflict.GraphRevision || candidate.NodeID != conflict.NodeID || candidate.AuthorRunID == "" {
return work.ConflictSet{}, work.GraphSynthesisCandidate{}, ErrGraphSynthesisRejected
}
candidateByID[candidate.ID] = candidate
if candidate.AuthorRunID == verifierRunID {
return work.ConflictSet{}, work.GraphSynthesisCandidate{}, ErrGraphSynthesisRejected
}
}
ordered := append([]string(nil), conflict.CandidateIDs...)
for _, id := range ordered {
if _, ok := candidateByID[id]; !ok {
return work.ConflictSet{}, work.GraphSynthesisCandidate{}, ErrGraphSynthesisRejected
}
}
if !containsString(ordered, selectedCandidate) {
return work.ConflictSet{}, work.GraphSynthesisCandidate{}, ErrGraphSynthesisRejected
}
dissent := make([]string, 0, len(ordered)-1)
for _, id := range ordered {
if id != selectedCandidate {
dissent = append(dissent, id)
}
}
conflict.Resolved = true
synthesis := work.GraphSynthesisCandidate{ID: conflict.ID + ":synthesis", GraphID: conflict.GraphID, GraphRevision: conflict.GraphRevision, NodeID: conflict.NodeID, CandidateIDs: ordered, SelectedCandidateID: selectedCandidate, DissentCandidateIDs: dissent, VerifierRunID: verifierRunID, VerifierIdentity: verifierIdentity, EvidenceRefs: append([]work.EvidenceRef(nil), evidence...), Summary: summary, State: work.GraphSynthesisVerified}
return conflict, synthesis, nil
}
func containsString(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
func ScheduleReadyNodes(graph work.WorkGraph, active int) ([]work.GraphNodeID, error) {
if err := ValidateWorkGraph(graph); err != nil {
return nil, err
+136
View File
@@ -3,11 +3,147 @@ package domain
import (
"errors"
"slices"
"strings"
"git.cin.su/m/Assistant/contracts/work"
)
var ErrInvalidDevelopmentGate = errors.New("invalid governed self-development gate")
var ErrInsufficientExtensionEvidence = errors.New("insufficient repeated extension evidence")
var ErrInvalidExtensionProposal = errors.New("invalid extension proposal")
var ErrInvalidDevelopmentScaffold = errors.New("invalid development scaffold candidate")
// BuildExtensionProposal deterministically turns a repeated evidence signal
// into a proposal. It retains every distinct evidence reference for lineage,
// while deduplicating signal keys so repeated reports do not create duplicate
// proposals. It never creates a task, mutates an implementation or grants
// authority.
func BuildExtensionProposal(evidence []work.ExtensionEvidence, request work.ExtensionProposalRequest) (work.ExtensionProposal, error) {
owners := cleanStrings(request.AffectedOwners)
contracts := cleanStrings(request.AffectedContracts)
evaluation := cleanStrings(request.EvaluationPlan)
alternatives := cleanStrings(request.Alternatives)
assumptions := cleanStrings(request.Assumptions)
if request.ID == "" || request.CreatedAt.IsZero() || strings.TrimSpace(request.DesiredOutcome) == "" || strings.TrimSpace(request.SmallestChangeLayer) == "" || strings.TrimSpace(request.RiskTier) == "" || strings.TrimSpace(request.RollbackPlan) == "" || len(owners) == 0 || len(contracts) == 0 || len(evaluation) == 0 || len(alternatives) == 0 || len(assumptions) == 0 {
return work.ExtensionProposal{}, ErrInvalidExtensionProposal
}
refs := make([]string, 0, len(evidence))
refSeen := make(map[string]struct{}, len(evidence))
counts := make(map[string]int, len(evidence))
keys := make([]string, 0, len(evidence))
keySeen := make(map[string]struct{}, len(evidence))
for _, item := range evidence {
ref := strings.TrimSpace(item.Ref)
key := strings.TrimSpace(item.SignalKey)
if ref == "" || key == "" {
return work.ExtensionProposal{}, ErrInvalidExtensionProposal
}
if _, ok := refSeen[ref]; !ok {
refs = append(refs, ref)
refSeen[ref] = struct{}{}
}
counts[key]++
if _, ok := keySeen[key]; !ok {
keys = append(keys, key)
keySeen[key] = struct{}{}
}
}
if len(refs) < 2 {
return work.ExtensionProposal{}, ErrInsufficientExtensionEvidence
}
repeated := false
for _, count := range counts {
if count >= 2 {
repeated = true
break
}
}
if !repeated {
return work.ExtensionProposal{}, ErrInsufficientExtensionEvidence
}
return work.ExtensionProposal{
ID: request.ID, Stage: work.ExtensionProposalProposed, EvidenceRefs: refs, DeduplicatedSignalKeys: keys,
DesiredOutcome: strings.TrimSpace(request.DesiredOutcome), SmallestChangeLayer: strings.TrimSpace(request.SmallestChangeLayer),
AffectedOwners: owners, AffectedContracts: contracts,
RiskTier: strings.TrimSpace(request.RiskTier), EvaluationPlan: evaluation, Alternatives: alternatives,
RollbackPlan: strings.TrimSpace(request.RollbackPlan), Assumptions: assumptions, CreatedAtUnixMillis: request.CreatedAt.UnixMilli(),
}, nil
}
func cleanStrings(values []string) []string {
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result
}
// BuildDevelopmentScaffold creates only a typed, isolated candidate. The
// caller supplies the protected path set so this domain function cannot infer
// or widen repository authority from ambient process state.
func BuildDevelopmentScaffold(request work.DevelopmentScaffoldRequest) (work.DevelopmentScaffold, error) {
modules := cleanStrings(request.Modules)
allowed := cleanPaths(request.AllowedPaths)
forbidden := cleanPaths(request.ForbiddenPaths)
protected := cleanPaths(request.ProtectedPaths)
tests := cleanStrings(request.RequiredTests)
lineage := cleanStrings(request.ArtifactLineageRefs)
if request.ID == "" || request.ProposalRef == "" || strings.TrimSpace(request.BaseRepoRevision) == "" || strings.TrimSpace(request.WorktreeRef) == "" || strings.TrimSpace(request.TaskIntent) == "" || request.CreatedAt.IsZero() || len(modules) == 0 || len(allowed) == 0 || len(tests) == 0 || len(lineage) == 0 || request.ProtectedMainAccess != "DENIED" || request.ProductionAccess != "DENIED" || request.SecretAccess != "DENIED" {
return work.DevelopmentScaffold{}, ErrInvalidDevelopmentScaffold
}
for _, path := range allowed {
if unsafePath(path) || pathMatchesAny(path, forbidden) || pathMatchesAny(path, protected) {
return work.DevelopmentScaffold{}, ErrInvalidDevelopmentScaffold
}
}
worktree := strings.ToLower(strings.TrimSpace(request.WorktreeRef))
if worktree == "main" || worktree == "protected-main" || worktree == "production" {
return work.DevelopmentScaffold{}, ErrInvalidDevelopmentScaffold
}
return work.DevelopmentScaffold{ID: request.ID, Stage: work.DevelopmentScaffoldSandboxed, ProposalRef: request.ProposalRef, BaseRepoRevision: strings.TrimSpace(request.BaseRepoRevision), WorktreeRef: strings.TrimSpace(request.WorktreeRef), TaskIntent: strings.TrimSpace(request.TaskIntent), Modules: modules, AllowedPaths: allowed, ForbiddenPaths: forbidden, RequiredTests: tests, ArtifactLineageRefs: lineage, ProtectedMainAccess: "DENIED", ProductionAccess: "DENIED", SecretAccess: "DENIED", CreatedAtUnixMillis: request.CreatedAt.UnixMilli()}, nil
}
func cleanPaths(values []string) []string {
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
value = strings.TrimSpace(strings.ReplaceAll(value, "\\", "/"))
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result
}
func unsafePath(path string) bool {
return strings.HasPrefix(path, "/") || strings.HasPrefix(path, "~") || (len(path) > 1 && path[1] == ':') || slices.Contains(strings.Split(path, "/"), "..")
}
func pathMatchesAny(path string, scopes []string) bool {
for _, scope := range scopes {
scope = strings.TrimSuffix(scope, "/")
scope = strings.TrimSuffix(scope, "**")
scope = strings.TrimSuffix(scope, "/")
if path == scope || strings.HasPrefix(path, scope+"/") {
return true
}
}
return false
}
func AdvanceDevelopmentGate(current work.DevelopmentGate, next work.DevelopmentStage) (work.DevelopmentGate, error) {
if err := ValidateDevelopmentGate(current); err != nil {
@@ -2,6 +2,7 @@ package domain
import (
"testing"
"time"
"git.cin.su/m/Assistant/contracts/work"
)
@@ -20,6 +21,52 @@ func TestGovernedSelfDevelopmentRequiresIndependentVerifierBeforeCanary(t *testi
}
}
func TestBuildExtensionProposalRequiresRepeatedEvidenceAndRetainsLineage(t *testing.T) {
proposal, err := BuildExtensionProposal([]work.ExtensionEvidence{
{Ref: "reflection-1", SignalKey: "send-email-gap"},
{Ref: "reflection-2", SignalKey: "send-email-gap"},
{Ref: "reflection-2", SignalKey: "send-email-gap"},
}, work.ExtensionProposalRequest{
ID: "proposal-1", DesiredOutcome: "connect a reviewed email capability", SmallestChangeLayer: "capability-adapter",
AffectedOwners: []string{"capability", "commit"}, AffectedContracts: []string{"CapabilityImplementation@v1"}, RiskTier: "D",
EvaluationPlan: []string{"fake transport", "adversarial corpus"}, Alternatives: []string{"keep capability gap"},
RollbackPlan: "restore previous implementation reference", Assumptions: []string{"human promotion required"}, CreatedAt: time.UnixMilli(42),
})
if err != nil {
t.Fatal(err)
}
if proposal.Stage != work.ExtensionProposalProposed || len(proposal.EvidenceRefs) != 2 || len(proposal.DeduplicatedSignalKeys) != 1 || proposal.CreatedAtUnixMillis != 42 {
t.Fatalf("unexpected proposal: %+v", proposal)
}
}
func TestBuildExtensionProposalRejectsSingleFailureAndIncompleteGovernance(t *testing.T) {
request := work.ExtensionProposalRequest{ID: "proposal-1", DesiredOutcome: "x", SmallestChangeLayer: "skill", AffectedOwners: []string{"capability"}, AffectedContracts: []string{"Skill@v1"}, RiskTier: "B", EvaluationPlan: []string{"test"}, Alternatives: []string{"keep current behavior"}, RollbackPlan: "disable", Assumptions: []string{"human review"}, CreatedAt: time.UnixMilli(42)}
if _, err := BuildExtensionProposal([]work.ExtensionEvidence{{Ref: "failure-1", SignalKey: "same"}}, request); err != ErrInsufficientExtensionEvidence {
t.Fatalf("single failure must be rejected, got %v", err)
}
request.Assumptions = nil
if _, err := BuildExtensionProposal([]work.ExtensionEvidence{{Ref: "failure-1", SignalKey: "same"}, {Ref: "failure-2", SignalKey: "same"}}, request); err != ErrInvalidExtensionProposal {
t.Fatalf("missing assumptions must be rejected, got %v", err)
}
}
func TestBuildDevelopmentScaffoldFailsClosedForProtectedScope(t *testing.T) {
base := work.DevelopmentScaffoldRequest{ID: "scaffold-1", ProposalRef: "proposal-1", BaseRepoRevision: "abc123", WorktreeRef: "worktree-1", TaskIntent: "add reviewed adapter", Modules: []string{"capability"}, AllowedPaths: []string{"modules/capability/**"}, ForbiddenPaths: []string{"security/**"}, ProtectedPaths: []string{"architecture/**", "deploy/**"}, RequiredTests: []string{"go test ./..."}, ArtifactLineageRefs: []string{"proposal-1"}, ProtectedMainAccess: "DENIED", ProductionAccess: "DENIED", SecretAccess: "DENIED", CreatedAt: time.UnixMilli(1)}
scaffold, err := BuildDevelopmentScaffold(base)
if err != nil || scaffold.Stage != work.DevelopmentScaffoldSandboxed {
t.Fatalf("valid scaffold rejected: %+v %v", scaffold, err)
}
base.AllowedPaths = []string{"deploy/docker-compose.prod.yml"}
if _, err := BuildDevelopmentScaffold(base); err != ErrInvalidDevelopmentScaffold {
t.Fatalf("protected path must fail closed, got %v", err)
}
base.AllowedPaths = []string{"modules/capability/../security"}
if _, err := BuildDevelopmentScaffold(base); err != ErrInvalidDevelopmentScaffold {
t.Fatalf("traversal path must fail closed, got %v", err)
}
}
func TestGovernedSelfDevelopmentRejectsSameProducerAndVerifier(t *testing.T) {
gate := work.DevelopmentGate{ID: "dev-1", Stage: work.DevelopmentIsolated, EvidenceRefs: []string{"obs-1"}, ProducerRunRefs: []work.AgentRunID{"same"}, VerifierRunRefs: []work.AgentRunID{"same"}, VerificationEvidence: "verify-1"}
if _, err := AdvanceDevelopmentGate(gate, work.DevelopmentVerified); err == nil {
+4
View File
@@ -12,3 +12,7 @@ type GraphStore interface {
SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error
EnqueueDispatch(context.Context, work.AgentDispatchQueueItem) error
}
type GraphResultStore interface {
SaveGraphSynthesis(context.Context, work.ConflictSet, []work.ResultCandidate, work.GraphSynthesisCandidate) error
}
@@ -18,3 +18,16 @@ func TestGraphPersistenceMigrationIsRevisionScoped(t *testing.T) {
}
}
}
func TestGraphSynthesisMigrationIsWorkOwnedAndAtomicInputsExist(t *testing.T) {
data, err := os.ReadFile("../../database/migrations/036_work_graph_result_synthesis.sql")
if err != nil {
t.Fatal(err)
}
sql := string(data)
for _, marker := range []string{"work.graph_result_candidates", "work.graph_conflicts", "work.graph_synthesis_candidates", "verifier_run_id", "dissent_candidate_ids", "FOREIGN KEY (graph_id, graph_revision)", "SET ROLE work_role"} {
if !strings.Contains(sql, marker) {
t.Fatalf("graph synthesis persistence marker missing: %s", marker)
}
}
}
@@ -2,7 +2,9 @@ package agent_execution_test
import (
"context"
"encoding/json"
"strconv"
"strings"
"testing"
"git.cin.su/m/Assistant/contracts/work"
@@ -58,3 +60,37 @@ func TestGraphRuntimeSchedulesFanOutAndCompletesFanIn(t *testing.T) {
t.Fatalf("completed=%+v err=%v", completed, err)
}
}
type graphResultStoreStub struct {
conflict work.ConflictSet
count int
}
func (s *graphResultStoreStub) SaveGraphSynthesis(_ context.Context, conflict work.ConflictSet, candidates []work.ResultCandidate, _ work.GraphSynthesisCandidate) error {
s.conflict = conflict
s.count = len(candidates)
return nil
}
func TestSynthesizeGraphPersistsVerifiedDissent(t *testing.T) {
store := &graphResultStoreStub{}
conflict, synthesis, err := application.SynthesizeGraph(context.Background(), store, application.GraphSynthesisRequest{
Conflict: work.ConflictSet{ID: "conflict-1", GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", CandidateIDs: []string{"candidate-a", "candidate-b"}},
Candidates: []work.ResultCandidate{{ID: "candidate-a", GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", AuthorRunID: "producer-a", EvidenceSummary: "a", Summary: "a"}, {ID: "candidate-b", GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", AuthorRunID: "producer-b", EvidenceSummary: "b", Summary: "b"}},
SelectedCandidate: "candidate-a", VerifierRunID: "verifier-1", VerifierIdentity: "independent", EvidenceRefs: []work.EvidenceRef{"tests:1"}, Summary: "verified", Verified: true,
})
if err != nil || !conflict.Resolved || synthesis.State != work.GraphSynthesisVerified || store.count != 2 || !store.conflict.Resolved {
t.Fatalf("unexpected runtime synthesis: conflict=%+v synthesis=%+v stored=%+v err=%v", conflict, synthesis, store.conflict, err)
}
}
func TestGraphSynthesisJSONUsesStableSnakeCaseContract(t *testing.T) {
payload, err := json.Marshal(application.GraphSynthesisRequest{Conflict: work.ConflictSet{ID: "c", GraphID: "g", GraphRevision: 1, NodeID: "n", CandidateIDs: []string{"a", "b"}}, SelectedCandidate: "a", VerifierRunID: "v", VerifierIdentity: "independent", EvidenceRefs: []work.EvidenceRef{"e"}, Summary: "s", Verified: true})
if err != nil || !strings.Contains(string(payload), `"graph_id":"g"`) || !strings.Contains(string(payload), `"selected_candidate_id":"a"`) {
t.Fatalf("unexpected synthesis JSON: %s err=%v", payload, err)
}
var decoded application.GraphSynthesisRequest
if err := json.Unmarshal(payload, &decoded); err != nil || decoded.Conflict.GraphID != "g" || decoded.SelectedCandidate != "a" || !decoded.Verified {
t.Fatalf("snake_case request did not round-trip: %+v err=%v", decoded, err)
}
}
+15
View File
@@ -56,3 +56,18 @@ func TestGraphSchedulerEnforcesParallelBudgetAndExplicitConflict(t *testing.T) {
t.Fatalf("unknown candidate was accepted: %v", err)
}
}
func TestGraphSynthesisRequiresIndependentVerifierAndRetainsDissent(t *testing.T) {
conflict := work.ConflictSet{ID: "conflict-1", GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", CandidateIDs: []string{"candidate-a", "candidate-b"}}
candidates := []work.ResultCandidate{{ID: "candidate-a", GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", AuthorRunID: "producer-a", EvidenceSummary: "a"}, {ID: "candidate-b", GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", AuthorRunID: "producer-b", EvidenceSummary: "b"}}
resolved, synthesis, err := workdomain.BuildVerifiedGraphSynthesis(conflict, candidates, "candidate-a", "verifier-1", "independent-verifier", []work.EvidenceRef{"tests:1"}, "candidate a is supported", true)
if err != nil || !resolved.Resolved || synthesis.SelectedCandidateID != "candidate-a" || len(synthesis.DissentCandidateIDs) != 1 || synthesis.VerifierRunID == candidates[0].AuthorRunID {
t.Fatalf("unexpected synthesis: conflict=%+v synthesis=%+v err=%v", resolved, synthesis, err)
}
if _, _, err := workdomain.BuildVerifiedGraphSynthesis(conflict, candidates, "candidate-a", "producer-a", "producer", []work.EvidenceRef{"tests:1"}, "forged", true); err != workdomain.ErrGraphSynthesisRejected {
t.Fatalf("producer must not verify its own result, got %v", err)
}
if _, _, err := workdomain.BuildVerifiedGraphSynthesis(conflict, candidates, "candidate-a", "verifier-1", "independent-verifier", []work.EvidenceRef{"tests:1"}, "failed", false); err != workdomain.ErrGraphSynthesisRejected {
t.Fatalf("failed verification must not resolve conflict, got %v", err)
}
}
@@ -2,6 +2,7 @@ package semantic_evolution_test
import (
"testing"
"time"
"git.cin.su/m/Assistant/contracts/work"
workdomain "git.cin.su/m/Assistant/modules/work/domain"
@@ -13,3 +14,22 @@ func TestSelfDevelopmentRollbackIsAvailableOnlyFromCanary(t *testing.T) {
t.Fatal(err)
}
}
func TestExtensionProposalIsProposalOnlyAndDeduplicatesSignals(t *testing.T) {
proposal, err := workdomain.BuildExtensionProposal([]work.ExtensionEvidence{{Ref: "gap-1", SignalKey: "printer-driver"}, {Ref: "gap-2", SignalKey: "printer-driver"}}, work.ExtensionProposalRequest{
ID: "proposal-printer", DesiredOutcome: "review printer adapter", SmallestChangeLayer: "capability-adapter", AffectedOwners: []string{"capability"}, AffectedContracts: []string{"CapabilityImplementation@v1"}, RiskTier: "C", EvaluationPlan: []string{"contract tests"}, Alternatives: []string{"retain capability gap"}, RollbackPlan: "disable candidate", Assumptions: []string{"human promotion"}, CreatedAt: time.UnixMilli(7),
})
if err != nil {
t.Fatal(err)
}
if proposal.Stage != work.ExtensionProposalProposed || len(proposal.DeduplicatedSignalKeys) != 1 {
t.Fatalf("unexpected proposal stage/keys: %+v", proposal)
}
}
func TestDevelopmentScaffoldRequiresExplicitDeniedAccessPolicies(t *testing.T) {
_, err := workdomain.BuildDevelopmentScaffold(work.DevelopmentScaffoldRequest{ID: "s", ProposalRef: "p", BaseRepoRevision: "r", WorktreeRef: "isolated", TaskIntent: "test", Modules: []string{"work"}, AllowedPaths: []string{"modules/work/**"}, RequiredTests: []string{"go test ./..."}, ArtifactLineageRefs: []string{"p"}, ProtectedMainAccess: "DENIED", ProductionAccess: "DENIED", SecretAccess: "ALLOW", CreatedAt: time.UnixMilli(1)})
if err == nil {
t.Fatal("secret access must be denied explicitly")
}
}