Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79c99289e7 | ||
|
|
07a8de9cb5 | ||
|
|
fdf38196f0 | ||
|
|
b753761e1a | ||
|
|
5faa5321dd | ||
|
|
7271d7950d | ||
|
|
533243da25 | ||
|
|
b1fba3ccef | ||
|
|
b0afa2417f | ||
|
|
6f5f7e1ee3 | ||
|
|
94c5bcb017 | ||
|
|
3f7c4705fe | ||
|
|
1ccd43c552 | ||
|
|
d38406fbc5 | ||
|
|
f73c69960c | ||
|
|
32e33e3050 | ||
|
|
e9bb42cd07 | ||
|
|
4a800df1e1 | ||
|
|
b2f922cbbc | ||
|
|
a4450348a1 |
@@ -3,6 +3,7 @@ package postgres
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
@@ -64,7 +65,7 @@ func (r *EpistemicsRepository) RecentReflectionCandidates(ctx context.Context, l
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT
|
||||
reflection_candidate_id,trigger,state,source_experience_id,original_intent_id,source_utterance_id,
|
||||
summary,evidence_refs,requires_cognition,recorded_at_unix_millis,revision
|
||||
summary,evidence_refs,requires_cognition,recorded_at_unix_millis,revision,claimed_by,claim_expires_at_unix_millis
|
||||
FROM epistemics.reflection_candidates ORDER BY recorded_at_unix_millis DESC, reflection_candidate_id DESC LIMIT $1`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -74,7 +75,7 @@ func (r *EpistemicsRepository) RecentReflectionCandidates(ctx context.Context, l
|
||||
for rows.Next() {
|
||||
var value epistemics.ReflectionCandidate
|
||||
if err := rows.Scan(&value.ID, &value.Trigger, &value.State, &value.SourceExperienceID, &value.OriginalIntentID, &value.SourceUtteranceID,
|
||||
&value.Summary, &value.EvidenceRefs, &value.RequiresCognition, &value.RecordedAtUnixMillis, &value.Revision); err != nil {
|
||||
&value.Summary, &value.EvidenceRefs, &value.RequiresCognition, &value.RecordedAtUnixMillis, &value.Revision, &value.ClaimedBy, &value.ClaimExpiresAtUnixMillis); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, value)
|
||||
@@ -82,6 +83,61 @@ func (r *EpistemicsRepository) RecentReflectionCandidates(ctx context.Context, l
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (r *EpistemicsRepository) ClaimReflectionCandidate(ctx context.Context, workerID string, nowUnixMillis, leaseMillis int64) (epistemics.ReflectionCandidate, error) {
|
||||
if workerID == "" || leaseMillis < 1 {
|
||||
return epistemics.ReflectionCandidate{}, fmt.Errorf("invalid reflection claim")
|
||||
}
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return epistemics.ReflectionCandidate{}, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
var value epistemics.ReflectionCandidate
|
||||
err = tx.QueryRow(ctx, `SELECT reflection_candidate_id,trigger,state,source_experience_id,original_intent_id,source_utterance_id,summary,evidence_refs,requires_cognition,recorded_at_unix_millis,revision,claimed_by,claim_expires_at_unix_millis
|
||||
FROM epistemics.reflection_candidates
|
||||
WHERE state='OPEN' AND (claim_expires_at_unix_millis=0 OR claim_expires_at_unix_millis < $1)
|
||||
ORDER BY recorded_at_unix_millis,reflection_candidate_id
|
||||
FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&value.ID, &value.Trigger, &value.State, &value.SourceExperienceID, &value.OriginalIntentID, &value.SourceUtteranceID, &value.Summary, &value.EvidenceRefs, &value.RequiresCognition, &value.RecordedAtUnixMillis, &value.Revision, &value.ClaimedBy, &value.ClaimExpiresAtUnixMillis)
|
||||
if err != nil {
|
||||
return epistemics.ReflectionCandidate{}, err
|
||||
}
|
||||
value.State = epistemics.ReflectionCandidateEvaluating
|
||||
value.ClaimedBy = workerID
|
||||
value.ClaimExpiresAtUnixMillis = nowUnixMillis + leaseMillis
|
||||
_, err = tx.Exec(ctx, `UPDATE epistemics.reflection_candidates SET state=$1,claimed_by=$2,claim_expires_at_unix_millis=$3 WHERE reflection_candidate_id=$4 AND state='OPEN'`, value.State, value.ClaimedBy, value.ClaimExpiresAtUnixMillis, value.ID)
|
||||
if err != nil {
|
||||
return epistemics.ReflectionCandidate{}, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return epistemics.ReflectionCandidate{}, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (r *EpistemicsRepository) RecoverReflectionClaims(ctx context.Context, nowUnixMillis int64) (int64, error) {
|
||||
result, err := r.pool.Exec(ctx, `UPDATE epistemics.reflection_candidates SET state='OPEN',claimed_by='',claim_expires_at_unix_millis=0 WHERE state='EVALUATING' AND claim_expires_at_unix_millis > 0 AND claim_expires_at_unix_millis < $1`, nowUnixMillis)
|
||||
return result.RowsAffected(), err
|
||||
}
|
||||
|
||||
func (r *EpistemicsRepository) ResolveReflectionCandidate(ctx context.Context, id epistemics.ReflectionCandidateID, workerID string, nowUnixMillis int64) error {
|
||||
if id == "" || workerID == "" {
|
||||
return fmt.Errorf("invalid reflection resolution")
|
||||
}
|
||||
result, err := r.pool.Exec(ctx, `UPDATE epistemics.reflection_candidates SET state='RESOLVED',claimed_by='',claim_expires_at_unix_millis=0,revision=revision+1 WHERE reflection_candidate_id=$1 AND state='EVALUATING' AND claimed_by=$2 AND claim_expires_at_unix_millis >= $3`, id, workerID, nowUnixMillis)
|
||||
if err == nil && result.RowsAffected() != 1 {
|
||||
return fmt.Errorf("reflection candidate claim lost")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *EpistemicsRepository) WaitForReflectionCandidate(ctx context.Context, interval time.Duration) error {
|
||||
if interval <= 0 {
|
||||
return fmt.Errorf("invalid reflection poll interval")
|
||||
}
|
||||
_, err := r.pool.Exec(ctx, `SELECT pg_sleep($1)`, interval.Seconds())
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *EpistemicsRepository) SaveExperienceRecord(ctx context.Context, value epistemics.ExperienceRecord) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.experience_records (
|
||||
experience_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
@@ -68,6 +69,15 @@ func (r *WorkRepository) SaveDispatchIntent(ctx context.Context, intent work.Age
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *WorkRepository) LoadDispatchIntent(ctx context.Context, dispatchID string) (work.AgentDispatchIntent, error) {
|
||||
var intent work.AgentDispatchIntent
|
||||
err := r.pool.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis FROM work.agent_dispatch_intents WHERE dispatch_id=$1`, dispatchID).Scan(&intent.DispatchID, &intent.IdempotencyKey, &intent.DelegationID, &intent.RunID, &intent.ImplementationID, &intent.ImplementationRevision, &intent.SourceRevision, &intent.WorktreeID, &intent.VerificationPlanRef, &intent.CreatedAtUnixMillis)
|
||||
if err != nil {
|
||||
return work.AgentDispatchIntent{}, fmt.Errorf("load dispatch intent: %w", err)
|
||||
}
|
||||
return intent, nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) EnqueueDispatch(ctx context.Context, item work.AgentDispatchQueueItem) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO work.agent_dispatch_queue(dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(dispatch_id) DO NOTHING`, item.DispatchID, item.State, item.AvailableAtUnixMillis, item.Attempts, item.ClaimedBy, item.UpdatedAtUnixMillis)
|
||||
return err
|
||||
@@ -82,6 +92,9 @@ func (r *WorkRepository) ClaimNextDispatch(ctx context.Context, workerID string,
|
||||
var item work.AgentDispatchQueueItem
|
||||
err = tx.QueryRow(ctx, `SELECT dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis FROM work.agent_dispatch_queue WHERE state='QUEUED' AND available_at_unix_millis <= $1 ORDER BY available_at_unix_millis,dispatch_id FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&item.DispatchID, &item.State, &item.AvailableAtUnixMillis, &item.Attempts, &item.ClaimedBy, &item.UpdatedAtUnixMillis)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return work.AgentDispatchQueueItem{}, nil
|
||||
}
|
||||
return work.AgentDispatchQueueItem{}, err
|
||||
}
|
||||
item.State = work.AgentDispatchClaimed
|
||||
@@ -98,6 +111,14 @@ func (r *WorkRepository) ClaimNextDispatch(ctx context.Context, workerID string,
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) WaitForDispatch(ctx context.Context, interval time.Duration) error {
|
||||
if interval <= 0 {
|
||||
return fmt.Errorf("invalid worker poll interval")
|
||||
}
|
||||
_, err := r.pool.Exec(ctx, `SELECT pg_sleep($1)`, interval.Seconds())
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *WorkRepository) CompleteDispatch(ctx context.Context, dispatchID, workerID string, state work.AgentDispatchQueueState, nowUnixMillis int64) error {
|
||||
if state != work.AgentDispatchCompleted && state != work.AgentDispatchFailed {
|
||||
return fmt.Errorf("invalid terminal dispatch state: %s", state)
|
||||
@@ -112,6 +133,79 @@ func (r *WorkRepository) CompleteDispatch(ctx context.Context, dispatchID, worke
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO work.graphs(graph_id,revision,state,max_parallel,max_attempts) VALUES($1,$2,$3,$4,$5) ON CONFLICT(graph_id,revision) DO NOTHING`, graph.ID, graph.Revision, graph.State, graph.Budget.MaxParallel, graph.Budget.MaxAttempts); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range graph.Nodes {
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_nodes(graph_id,revision,node_id,idempotency_key,state,attempts) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(graph_id,revision,node_id) DO NOTHING`, graph.ID, graph.Revision, node.ID, node.IdempotencyKey, node.State, node.Attempts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, edge := range graph.Edges {
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_edges(graph_id,revision,from_node_id,to_node_id) VALUES($1,$2,$3,$4) ON CONFLICT(graph_id,revision,from_node_id,to_node_id) DO NOTHING`, graph.ID, graph.Revision, edge.From, edge.To); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *WorkRepository) LoadWorkGraph(ctx context.Context, graphID string, revision int64) (work.WorkGraph, error) {
|
||||
var graph work.WorkGraph
|
||||
err := r.pool.QueryRow(ctx, `SELECT graph_id,revision,state,max_parallel,max_attempts FROM work.graphs WHERE graph_id=$1 AND revision=$2`, graphID, revision).Scan(&graph.ID, &graph.Revision, &graph.State, &graph.Budget.MaxParallel, &graph.Budget.MaxAttempts)
|
||||
if err != nil {
|
||||
return work.WorkGraph{}, fmt.Errorf("load graph: %w", err)
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT node_id,idempotency_key,state,attempts FROM work.graph_nodes WHERE graph_id=$1 AND revision=$2 ORDER BY node_id`, graphID, revision)
|
||||
if err != nil {
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var node work.GraphNode
|
||||
if err := rows.Scan(&node.ID, &node.IdempotencyKey, &node.State, &node.Attempts); err != nil {
|
||||
rows.Close()
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
graph.Nodes = append(graph.Nodes, node)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
rows.Close()
|
||||
edges, err := r.pool.Query(ctx, `SELECT from_node_id,to_node_id FROM work.graph_edges WHERE graph_id=$1 AND revision=$2 ORDER BY from_node_id,to_node_id`, graphID, revision)
|
||||
if err != nil {
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
for edges.Next() {
|
||||
var edge work.GraphEdge
|
||||
if err := edges.Scan(&edge.From, &edge.To); err != nil {
|
||||
edges.Close()
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
graph.Edges = append(graph.Edges, edge)
|
||||
}
|
||||
if err := edges.Err(); err != nil {
|
||||
edges.Close()
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
edges.Close()
|
||||
return graph, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
return 0, err
|
||||
}
|
||||
return command.RowsAffected(), nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveAgentLease(ctx context.Context, lease work.AgentLease) error {
|
||||
err := r.pool.QueryRow(ctx, `INSERT INTO work.agent_leases(lease_id,run_id,worker_id,fencing_token,state,acquired_at_unix_millis,expires_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(run_id) DO UPDATE SET lease_id=EXCLUDED.lease_id,worker_id=EXCLUDED.worker_id,fencing_token=EXCLUDED.fencing_token,state=EXCLUDED.state,acquired_at_unix_millis=EXCLUDED.acquired_at_unix_millis,expires_at_unix_millis=EXCLUDED.expires_at_unix_millis WHERE work.agent_leases.state='EXPIRED' OR work.agent_leases.expires_at_unix_millis <= EXCLUDED.acquired_at_unix_millis RETURNING run_id`, lease.ID, lease.RunID, lease.WorkerID, lease.FencingToken, lease.State, lease.AcquiredAt.UnixMilli(), lease.ExpiresAt.UnixMilli()).Scan(new(string))
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"git.cin.su/m/Assistant/modules/work/application"
|
||||
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
|
||||
)
|
||||
|
||||
func main() {
|
||||
workerID := flag.String("worker-id", "agent-worker-1", "durable worker identity")
|
||||
staleAfter := flag.Duration("stale-after", 10*time.Minute, "age after which a claimed item is recovered")
|
||||
leaseTTL := flag.Duration("lease-ttl", 15*time.Minute, "dry-run lease duration")
|
||||
pollInterval := flag.Duration("poll-interval", 5*time.Second, "database-owned idle wake-up interval")
|
||||
once := flag.Bool("once", false, "run one cycle and exit")
|
||||
flag.Parse()
|
||||
|
||||
password, err := resolveDatabasePassword("WORK_POSTGRES_PASSWORD", "WORK_POSTGRES_CREDENTIAL_REF", secretcontracts.RuntimeConsumerWorkWorker)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
repository, err := postgres.OpenWork(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: password})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer repository.Close()
|
||||
ctx := context.Background()
|
||||
for {
|
||||
result, cycleErr := application.ExecuteWorkerCycle(ctx, repository, *workerID, work.AgentFencingToken(1), time.Now().UTC(), *staleAfter, *leaseTTL)
|
||||
if cycleErr != nil {
|
||||
fmt.Fprintln(os.Stderr, cycleErr)
|
||||
if *once {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if cycleErr == nil && (result.Completed || result.RecoveredClaims > 0) {
|
||||
_ = json.NewEncoder(os.Stdout).Encode(result)
|
||||
}
|
||||
if *once {
|
||||
return
|
||||
}
|
||||
if err := repository.WaitForDispatch(ctx, *pollInterval); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveDatabasePassword(name, refName string, consumer secretcontracts.RuntimeConsumer) (string, error) {
|
||||
if _, ok := os.LookupEnv(name); ok {
|
||||
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
|
||||
}
|
||||
path, ref := os.Getenv(name+"_FILE"), os.Getenv(refName)
|
||||
if path == "" || ref == "" {
|
||||
return "", fmt.Errorf("%s_FILE and %s are required", name, refName)
|
||||
}
|
||||
root := os.Getenv("SECRET_BACKEND_ROOT")
|
||||
if root == "" {
|
||||
root = "/run/secrets"
|
||||
}
|
||||
resolver, err := platformsecrets.NewFileResolver(root)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: consumer, Path: path})
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
|
||||
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
|
||||
)
|
||||
|
||||
func main() {
|
||||
workerID := flag.String("worker-id", "reflection-worker-1", "stable worker identity")
|
||||
lease := flag.Duration("lease-ttl", 5*time.Minute, "candidate claim lease")
|
||||
pollInterval := flag.Duration("poll-interval", 15*time.Second, "database-owned idle wake-up interval")
|
||||
once := flag.Bool("once", false, "run one cycle and exit")
|
||||
flag.Parse()
|
||||
ctx := context.Background()
|
||||
password, err := resolveDatabasePassword("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
config := postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("EPISTEMICS_POSTGRES_USER"), Password: password}
|
||||
repository, err := postgres.OpenEpistemics(ctx, config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer repository.Close()
|
||||
for {
|
||||
now := time.Now().UnixMilli()
|
||||
if _, err := repository.RecoverReflectionClaims(ctx, now); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
candidate, claimErr := repository.ClaimReflectionCandidate(ctx, *workerID, now, lease.Milliseconds())
|
||||
if claimErr == nil {
|
||||
// This worker only closes the durable evaluation loop. Any cognitive
|
||||
// proposal remains a candidate and must be admitted by a separate governed flow.
|
||||
if err := repository.ResolveReflectionCandidate(ctx, candidate.ID, *workerID, time.Now().UnixMilli()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("resolved reflection candidate %s\n", candidate.ID)
|
||||
} else if *once {
|
||||
fmt.Println("no reflection candidate")
|
||||
}
|
||||
if *once {
|
||||
return
|
||||
}
|
||||
if err := repository.WaitForReflectionCandidate(ctx, *pollInterval); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveDatabasePassword(name, refName string) (string, error) {
|
||||
if _, ok := os.LookupEnv(name); ok {
|
||||
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
|
||||
}
|
||||
path, ref := os.Getenv(name+"_FILE"), os.Getenv(refName)
|
||||
if path == "" || ref == "" {
|
||||
return "", fmt.Errorf("%s_FILE and %s are required", name, refName)
|
||||
}
|
||||
root := os.Getenv("SECRET_BACKEND_ROOT")
|
||||
if root == "" {
|
||||
root = "/run/secrets"
|
||||
}
|
||||
resolver, err := platformsecrets.NewFileResolver(root)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: path})
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package capability
|
||||
|
||||
type FleetTier string
|
||||
|
||||
const (
|
||||
FleetTierFast FleetTier = "FAST"
|
||||
FleetTierMedium FleetTier = "MEDIUM"
|
||||
FleetTierHeavy FleetTier = "HEAVY"
|
||||
FleetTierLifeboat FleetTier = "LIFEBOAT"
|
||||
)
|
||||
|
||||
type FleetMember struct {
|
||||
ProviderInstanceID ProviderInstanceID
|
||||
ModelDescriptorID ModelDescriptorID
|
||||
Tier FleetTier
|
||||
Ready bool
|
||||
Reason string
|
||||
}
|
||||
|
||||
type FleetReadiness struct {
|
||||
ObservedAtUnixMillis int64
|
||||
Members []FleetMember
|
||||
ReadyTiers []FleetTier
|
||||
FallbackTier FleetTier
|
||||
}
|
||||
@@ -16,19 +16,24 @@ type FreshnessState string
|
||||
type CoverageState string
|
||||
type ConflictState string
|
||||
type EvidenceKind string
|
||||
type ResearchFindingState string
|
||||
|
||||
const (
|
||||
FreshnessCurrent FreshnessState = "CURRENT"
|
||||
FreshnessStale FreshnessState = "STALE"
|
||||
FreshnessUnknown FreshnessState = "UNKNOWN"
|
||||
CoverageComplete CoverageState = "COMPLETE"
|
||||
CoveragePartial CoverageState = "PARTIAL"
|
||||
CoverageUnknown CoverageState = "UNKNOWN"
|
||||
ConflictNone ConflictState = "NONE"
|
||||
ConflictPresent ConflictState = "PRESENT"
|
||||
ConflictUnknown ConflictState = "UNKNOWN"
|
||||
EvidenceObserved EvidenceKind = "OBSERVED"
|
||||
EvidenceAssumed EvidenceKind = "ASSUMED"
|
||||
FreshnessCurrent FreshnessState = "CURRENT"
|
||||
FreshnessStale FreshnessState = "STALE"
|
||||
FreshnessUnknown FreshnessState = "UNKNOWN"
|
||||
CoverageComplete CoverageState = "COMPLETE"
|
||||
CoveragePartial CoverageState = "PARTIAL"
|
||||
CoverageUnknown CoverageState = "UNKNOWN"
|
||||
ConflictNone ConflictState = "NONE"
|
||||
ConflictPresent ConflictState = "PRESENT"
|
||||
ConflictUnknown ConflictState = "UNKNOWN"
|
||||
EvidenceObserved EvidenceKind = "OBSERVED"
|
||||
EvidenceAssumed EvidenceKind = "ASSUMED"
|
||||
FindingSupported ResearchFindingState = "SUPPORTED"
|
||||
FindingStale ResearchFindingState = "STALE"
|
||||
FindingConflicted ResearchFindingState = "CONFLICTED"
|
||||
FindingUnknown ResearchFindingState = "UNKNOWN"
|
||||
)
|
||||
|
||||
type ResearchPlan struct {
|
||||
@@ -72,6 +77,35 @@ type Citation struct {
|
||||
ResolvedAtUnixMillis int64
|
||||
}
|
||||
|
||||
// ResearchCandidate is read-only evidence returned by a connector. The
|
||||
// connector supplies the immutable snapshot and exact excerpt; the evaluator
|
||||
// never fetches, rewrites or invents either one.
|
||||
type ResearchCandidate struct {
|
||||
PropositionKey string
|
||||
Statement string
|
||||
Source Source
|
||||
Snapshot SourceSnapshot
|
||||
Excerpt SourceExcerpt
|
||||
Evidence EvidenceKind
|
||||
Coverage CoverageState
|
||||
Conflict ConflictState
|
||||
}
|
||||
|
||||
type ResearchFinding struct {
|
||||
PropositionKey string
|
||||
Statement string
|
||||
Claim Claim
|
||||
Citations []Citation
|
||||
State ResearchFindingState
|
||||
Reason string
|
||||
}
|
||||
|
||||
type ResearchResult struct {
|
||||
PlanID ResearchPlanID
|
||||
Findings []ResearchFinding
|
||||
EvaluatedAtUnixMillis int64
|
||||
}
|
||||
|
||||
// ResolveCitation creates evidence only when all identifiers form one exact
|
||||
// source chain. It deliberately does not fetch, infer or rewrite source text.
|
||||
func ResolveCitation(source Source, snapshot SourceSnapshot, excerpt SourceExcerpt, evidence EvidenceKind, coverage CoverageState, conflict ConflictState, resolvedAtUnixMillis int64) (Citation, error) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package epistemics
|
||||
|
||||
type BeliefState string
|
||||
|
||||
const (
|
||||
BeliefProvisional BeliefState = "PROVISIONAL"
|
||||
BeliefConflicted BeliefState = "CONFLICTED"
|
||||
BeliefVerified BeliefState = "VERIFIED"
|
||||
)
|
||||
|
||||
type Observation struct {
|
||||
ID ObservationID
|
||||
SourceSnapshot SourceSnapshotID
|
||||
Excerpt SourceExcerptID
|
||||
Text string
|
||||
ObservedAt int64
|
||||
}
|
||||
|
||||
type Claim struct {
|
||||
ID ClaimID
|
||||
Statement string
|
||||
ObservationID ObservationID
|
||||
CitationID CitationID
|
||||
Verification VerificationState
|
||||
}
|
||||
|
||||
type Belief struct {
|
||||
ID BeliefID
|
||||
ClaimID ClaimID
|
||||
State BeliefState
|
||||
}
|
||||
|
||||
type TruthBundle struct {
|
||||
Observation Observation
|
||||
Claim Claim
|
||||
Belief Belief
|
||||
Citation Citation
|
||||
}
|
||||
@@ -120,15 +120,17 @@ type ExperienceRecord struct {
|
||||
// Cognitive reflection, if used, must go through Cognitive Gateway/Router and
|
||||
// produce candidates only; it cannot directly rewrite canonical state.
|
||||
type ReflectionCandidate struct {
|
||||
ID ReflectionCandidateID
|
||||
Trigger ReflectionTriggerKind
|
||||
State ReflectionCandidateState
|
||||
SourceExperienceID ExperienceRecordID
|
||||
OriginalIntentID OriginalIntentRef
|
||||
SourceUtteranceID SourceUtteranceRef
|
||||
Summary string
|
||||
EvidenceRefs []EvidenceRef
|
||||
RequiresCognition bool
|
||||
RecordedAtUnixMillis int64
|
||||
Revision int64
|
||||
ID ReflectionCandidateID
|
||||
Trigger ReflectionTriggerKind
|
||||
State ReflectionCandidateState
|
||||
SourceExperienceID ExperienceRecordID
|
||||
OriginalIntentID OriginalIntentRef
|
||||
SourceUtteranceID SourceUtteranceRef
|
||||
Summary string
|
||||
EvidenceRefs []EvidenceRef
|
||||
RequiresCognition bool
|
||||
RecordedAtUnixMillis int64
|
||||
Revision int64
|
||||
ClaimedBy string
|
||||
ClaimExpiresAtUnixMillis int64
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ const (
|
||||
type RuntimeConsumer string
|
||||
|
||||
const (
|
||||
RuntimeConsumerCore RuntimeConsumer = "core-runtime"
|
||||
RuntimeConsumerCore RuntimeConsumer = "core-runtime"
|
||||
RuntimeConsumerWorkWorker RuntimeConsumer = "agent-worker"
|
||||
RuntimeConsumerEpistemicsWorker RuntimeConsumer = "reflection-worker"
|
||||
)
|
||||
|
||||
// SecretFileRef is bootstrap delivery metadata for the V1 protected local
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package work
|
||||
|
||||
type GraphNodeID string
|
||||
|
||||
type GraphState string
|
||||
type GraphNodeState string
|
||||
|
||||
const (
|
||||
GraphDraft GraphState = "DRAFT"
|
||||
GraphAdmitted GraphState = "ADMITTED"
|
||||
GraphRunning GraphState = "RUNNING"
|
||||
GraphPartial GraphState = "PARTIAL"
|
||||
GraphVerifying GraphState = "VERIFYING"
|
||||
GraphCompleted GraphState = "COMPLETED"
|
||||
GraphFailed GraphState = "FAILED"
|
||||
GraphUnknown GraphState = "UNKNOWN"
|
||||
|
||||
GraphNodeBlocked GraphNodeState = "BLOCKED_PREREQUISITE"
|
||||
GraphNodeReady GraphNodeState = "READY"
|
||||
GraphNodeLeased GraphNodeState = "LEASED"
|
||||
GraphNodeRunning GraphNodeState = "RUNNING"
|
||||
GraphNodeSuccess GraphNodeState = "SUCCEEDED"
|
||||
GraphNodeFailed GraphNodeState = "FAILED"
|
||||
GraphNodeCancel GraphNodeState = "CANCELLED"
|
||||
GraphNodeUnknown GraphNodeState = "UNKNOWN"
|
||||
)
|
||||
|
||||
type GraphBudget struct {
|
||||
MaxParallel int
|
||||
MaxAttempts int
|
||||
}
|
||||
|
||||
type GraphNode struct {
|
||||
ID GraphNodeID
|
||||
IdempotencyKey string
|
||||
State GraphNodeState
|
||||
Attempts int
|
||||
}
|
||||
|
||||
type GraphEdge struct {
|
||||
From GraphNodeID
|
||||
To GraphNodeID
|
||||
}
|
||||
|
||||
type WorkGraph struct {
|
||||
ID string
|
||||
Revision int64
|
||||
State GraphState
|
||||
Nodes []GraphNode
|
||||
Edges []GraphEdge
|
||||
Budget GraphBudget
|
||||
}
|
||||
|
||||
type ResultCandidate struct {
|
||||
ID string
|
||||
GraphID string
|
||||
GraphRevision int64
|
||||
NodeID GraphNodeID
|
||||
AuthorRunID AgentRunID
|
||||
ArtifactRefs []EvidenceRef
|
||||
EvidenceSummary string
|
||||
Summary string
|
||||
}
|
||||
|
||||
type ConflictSet struct {
|
||||
ID string
|
||||
GraphID string
|
||||
GraphRevision int64
|
||||
NodeID GraphNodeID
|
||||
CandidateIDs []string
|
||||
Resolved bool
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package work
|
||||
|
||||
type DevelopmentStage string
|
||||
|
||||
const (
|
||||
DevelopmentEvidenceCollected DevelopmentStage = "EVIDENCE_COLLECTED"
|
||||
DevelopmentProposalCreated DevelopmentStage = "PROPOSAL_CREATED"
|
||||
DevelopmentTaskAdmitted DevelopmentStage = "TASK_ADMITTED"
|
||||
DevelopmentIsolated DevelopmentStage = "ISOLATED"
|
||||
DevelopmentVerified DevelopmentStage = "VERIFIED"
|
||||
DevelopmentCanary DevelopmentStage = "CANARY"
|
||||
DevelopmentPromoted DevelopmentStage = "PROMOTED"
|
||||
DevelopmentRolledBack DevelopmentStage = "ROLLED_BACK"
|
||||
)
|
||||
|
||||
type DevelopmentGate struct {
|
||||
ID string
|
||||
Stage DevelopmentStage
|
||||
EvidenceRefs []string
|
||||
ProposalRef string
|
||||
TaskPacketRef string
|
||||
WorktreeRef string
|
||||
ProducerRunRefs []AgentRunID
|
||||
VerifierRunRefs []AgentRunID
|
||||
VerificationEvidence string
|
||||
CanaryEvidence string
|
||||
PromotionRef string
|
||||
RollbackRef string
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.graphs (
|
||||
graph_id TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
max_parallel INTEGER NOT NULL,
|
||||
max_attempts INTEGER NOT NULL,
|
||||
PRIMARY KEY (graph_id, revision),
|
||||
CHECK (graph_id <> ''), CHECK (revision > 0),
|
||||
CHECK (state IN ('DRAFT','ADMITTED','RUNNING','PAUSED','CANCELLING','PARTIAL','VERIFYING','COMPLETED','FAILED','UNKNOWN','RECOVERING')),
|
||||
CHECK (max_parallel > 0), CHECK (max_attempts > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.graph_nodes (
|
||||
graph_id TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
node_id TEXT NOT NULL,
|
||||
idempotency_key TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
attempts INTEGER NOT NULL,
|
||||
PRIMARY KEY (graph_id, revision, node_id),
|
||||
FOREIGN KEY (graph_id, revision) REFERENCES work.graphs(graph_id, revision),
|
||||
CHECK (node_id <> ''), CHECK (idempotency_key <> ''), CHECK (attempts >= 0),
|
||||
CHECK (state IN ('BLOCKED_PREREQUISITE','READY','LEASED','RUNNING','SUCCEEDED','FAILED','CANCELLED','UNKNOWN','RECOVERING'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.graph_edges (
|
||||
graph_id TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
from_node_id TEXT NOT NULL,
|
||||
to_node_id TEXT NOT NULL,
|
||||
PRIMARY KEY (graph_id, revision, from_node_id, to_node_id),
|
||||
FOREIGN KEY (graph_id, revision) REFERENCES work.graphs(graph_id, revision),
|
||||
CHECK (from_node_id <> ''), CHECK (to_node_id <> ''), CHECK (from_node_id <> to_node_id)
|
||||
);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,19 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE epistemics_role;
|
||||
|
||||
ALTER TABLE epistemics.reflection_candidates
|
||||
ADD COLUMN IF NOT EXISTS claimed_by TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS claim_expires_at_unix_millis BIGINT NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE epistemics.reflection_candidates
|
||||
DROP CONSTRAINT IF EXISTS reflection_candidates_claim_expiry_check;
|
||||
ALTER TABLE epistemics.reflection_candidates
|
||||
ADD CONSTRAINT reflection_candidates_claim_expiry_check
|
||||
CHECK (claim_expires_at_unix_millis >= 0);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS reflection_candidates_claimable_idx
|
||||
ON epistemics.reflection_candidates (state, claim_expires_at_unix_millis, recorded_at_unix_millis, reflection_candidate_id);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -97,6 +97,38 @@ services:
|
||||
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
|
||||
depends_on: { postgres: { condition: service_healthy } }
|
||||
|
||||
agent-worker:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: agent-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
command: ["-worker-id", "agent-worker-production"]
|
||||
environment:
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
|
||||
WORK_POSTGRES_USER: work_role
|
||||
WORK_POSTGRES_CREDENTIAL_REF: credential://domovoy/work/db-password/v1
|
||||
WORK_POSTGRES_PASSWORD_FILE: /run/secrets/work_db_password
|
||||
SECRET_BACKEND_ROOT: /run/secrets
|
||||
depends_on: { postgres: { condition: service_healthy } }
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
|
||||
reflection-worker:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: reflection-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
command: ["-worker-id", "reflection-worker-production"]
|
||||
environment:
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
|
||||
EPISTEMICS_POSTGRES_USER: epistemics_role
|
||||
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
|
||||
EPISTEMICS_POSTGRES_PASSWORD_FILE: /run/secrets/epistemics_db_password
|
||||
SECRET_BACKEND_ROOT: /run/secrets
|
||||
depends_on: { postgres: { condition: service_healthy } }
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
|
||||
continuity-runtime:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: continuity-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000129-agent-dispatch-queue.json",
|
||||
"task_id": "DEV-000129",
|
||||
"task_file": "dev/tasks/DEV-000140-research-evaluation-runtime.json",
|
||||
"task_id": "DEV-000140",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000130",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "323a67a",
|
||||
"risk_tier": "E",
|
||||
"intent": "Implement a one-shot durable agent worker cycle that recovers stale claims, claims one queue item, executes the deterministic worker path and completes it with worker fencing.",
|
||||
"requirement_ids": ["WP-301", "Agent worker", "Stale-run/claim recovery"],
|
||||
"allowed_change_scope": {"modules": ["work-agent-execution", "work-storage"], "paths": ["adapters/postgres/work_repository.go", "modules/work/ports/worker.go", "modules/work/application/worker.go", "cmd/agent-worker/main.go", "tests/agent_execution/worker_sql_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000130-agent-worker-recovery.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "runtime/**", ".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": ["DEV-000129 durable queue", "DEV-000122 deterministic worker", "DEV-000123 orchestration"], "may_change": ["WorkerStore port and one-shot worker application"]},
|
||||
"semantic_owner": "work-agent-execution",
|
||||
"canonical_state": ["Queue state and dispatch intent remain Work-owned"],
|
||||
"state_transitions": ["stale CLAIMED -> QUEUED -> CLAIMED -> COMPLETED/FAILED"],
|
||||
"side_effects_and_privileges": ["Explicit one-shot command; no unmanaged goroutine or in-memory queue"],
|
||||
"persistence_and_migrations": ["Reuses migration 030; production apply follows separate canary task"],
|
||||
"failure_retry_restart": ["Stale recovery is durable; worker completion requires claimed_by match; failures become FAILED"],
|
||||
"assumptions": ["Dispatcher enqueue path is supplied by a later integration task", "Deterministic worker remains the only implementation in this canary stage"],
|
||||
"acceptance_scenarios": ["Worker recovers stale claims", "Worker claim is row-locked", "Wrong worker cannot complete claim", "Worker cycle is one-shot and restartable"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MIGRATION_DECISION", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000131",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "a445034",
|
||||
"risk_tier": "E",
|
||||
"intent": "Implement the first deterministic Multi-Agent Work Graph harness: typed graph revision, DAG validation, budgets and dependency-aware ready-node selection.",
|
||||
"requirement_ids": ["WP-400", "DAG validation", "Fan-out/fan-in foundation"],
|
||||
"allowed_change_scope": {"modules": ["work-agent-execution"], "paths": ["contracts/work/graph.go", "modules/work/domain/graph.go", "tests/agent_execution/graph_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000131-work-graph-harness.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "adapters/**", "cmd/**", "runtime/**", ".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-0021 graph proposal", "DEV-000130 worker/recovery contracts"], "may_change": ["Work graph typed contracts and pure domain harness"]},
|
||||
"semantic_owner": "work-agent-execution",
|
||||
"canonical_state": ["Typed WorkGraph value only; no persistence in this harness"],
|
||||
"state_transitions": ["DRAFT/ADMITTED/RUNNING graph with dependency-aware READY nodes"],
|
||||
"side_effects_and_privileges": ["Pure deterministic validation; no dispatch or external mutation"],
|
||||
"persistence_and_migrations": ["Persistence/scheduler implementation follows after harness"],
|
||||
"failure_retry_restart": ["Cycles, duplicate IDs/keys and invalid budgets reject before scheduling"],
|
||||
"assumptions": ["Graph scheduler must not bypass Agent Execution leases or verifier policy"],
|
||||
"acceptance_scenarios": ["Cycle rejected", "Ready nodes deterministic", "Dependent node remains blocked until prerequisite succeeds", "Budget is validated"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000132",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "b2f922c",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add deterministic Work Graph admission, bounded ready-node scheduling and explicit conflict resolution to the graph harness.",
|
||||
"requirement_ids": ["WP-400", "Graph admission", "Bounded scheduler", "Conflict resolution"],
|
||||
"allowed_change_scope": {"modules": ["work-agent-execution"], "paths": ["modules/work/domain/graph_scheduler.go", "tests/agent_execution/graph_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000132-work-graph-admission-scheduler.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "adapters/**", "cmd/**", "runtime/**", ".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": ["DEV-000131 graph harness", "ADR-0021 graph lifecycle"], "may_change": ["Pure graph admission/scheduling/conflict domain functions"]},
|
||||
"semantic_owner": "work-agent-execution",
|
||||
"canonical_state": ["No persistence; graph values remain deterministic harness state"],
|
||||
"state_transitions": ["DRAFT -> ADMITTED; READY nodes bounded by MaxParallel; conflict unresolved -> explicitly resolved"],
|
||||
"side_effects_and_privileges": ["Pure domain logic only"],
|
||||
"persistence_and_migrations": ["Durable scheduler follows after the harness"],
|
||||
"failure_retry_restart": ["Budget overflow and unknown conflict candidate reject without mutation"],
|
||||
"assumptions": ["Runtime scheduler must use Agent Execution queue/leases in a later task"],
|
||||
"acceptance_scenarios": ["Admission validates attempt budget", "Scheduler respects active/max parallel", "Conflict cannot resolve without explicit known candidate"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000133",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "4a800df",
|
||||
"risk_tier": "E",
|
||||
"intent": "Persist immutable Work Graph revisions with typed nodes and dependency edges for durable scheduler integration.",
|
||||
"requirement_ids": ["WP-401", "Durable graph revision", "Graph node dependencies"],
|
||||
"allowed_change_scope": {"modules": ["work-agent-execution", "work-storage"], "paths": ["database/migrations/031_work_graph_persistence.sql", "adapters/postgres/work_repository.go", "tests/agent_execution/graph_persistence_sql_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000133-work-graph-persistence.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "cmd/**", "runtime/**", ".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": ["DEV-000131/132 graph contracts and scheduler"], "may_change": ["Work graph persistence schema and repository methods"]},
|
||||
"semantic_owner": "work-agent-execution",
|
||||
"canonical_state": ["work.graphs", "work.graph_nodes", "work.graph_edges"],
|
||||
"state_transitions": ["graph revision persisted once; nodes and edges immutable within revision"],
|
||||
"side_effects_and_privileges": ["Work schema only; no cross-owner SQL"],
|
||||
"persistence_and_migrations": ["Append-only migration 031; production apply is later canary"],
|
||||
"failure_retry_restart": ["Idempotent inserts; load reconstructs deterministic node/edge order"],
|
||||
"assumptions": ["Durable scheduler and result/conflict persistence follow"],
|
||||
"acceptance_scenarios": ["Graph revision cannot collide into mutation", "Nodes/edges remain tied to revision", "Load reconstructs graph"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MIGRATION_DECISION", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000134",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "e9bb42c",
|
||||
"risk_tier": "E",
|
||||
"intent": "Implement the first Truth runtime compiler that produces typed Observation, Claim, Belief and Citation values from an exact source chain.",
|
||||
"requirement_ids": ["WP-100", "Observation/Claim/Belief", "Truth citation provenance"],
|
||||
"allowed_change_scope": {"modules": ["epistemics"], "paths": ["contracts/epistemics/truth.go", "modules/epistemics/domain/truth.go", "tests/research_contracts/truth_bundle_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000134-truth-bundle-compiler.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "adapters/**", "cmd/**", "runtime/**", ".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": ["DEV-000109 research contracts", "DEV-000110 citation resolver"], "may_change": ["Typed Truth bundle contracts and pure compiler"]},
|
||||
"semantic_owner": "epistemics",
|
||||
"canonical_state": ["Typed TruthBundle value only; persistence follows"],
|
||||
"state_transitions": ["source chain -> observed observation -> unverified claim -> provisional/conflicted belief"],
|
||||
"side_effects_and_privileges": ["Pure compiler; no fetching, external mutation or authority grant"],
|
||||
"persistence_and_migrations": ["Truth persistence is a later task"],
|
||||
"failure_retry_restart": ["Broken source chain rejects; conflict remains visible"],
|
||||
"assumptions": ["Research executor will supply exact source snapshots/excerpts"],
|
||||
"acceptance_scenarios": ["Observation/claim/citation refs align", "Conflict produces CONFLICTED belief", "Broken source chain is rejected"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000135",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "32e33e3",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add durable lease, stale recovery, and one-shot governed closure for reflection candidates.",
|
||||
"requirement_ids": ["WP-501", "Reflection candidate worker", "Stale claim recovery"],
|
||||
"allowed_change_scope": {"modules": ["epistemics", "epistemics-storage"], "paths": ["contracts/epistemics/types.go", "database/migrations/032_reflection_candidate_leases.sql", "adapters/postgres/epistemics_repository.go", "cmd/reflection-worker/main.go", "tests/research_contracts/reflection_worker_sql_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000135-reflection-worker.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "contracts/authority/**", "modules/cognitive/**", ".gitea/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
|
||||
"contracts": {"consume": ["Existing epistemics ReflectionCandidate contract"], "may_change": ["ReflectionCandidate lease metadata"]},
|
||||
"semantic_owner": "epistemics",
|
||||
"canonical_state": ["epistemics.reflection_candidates"],
|
||||
"state_transitions": ["OPEN to EVALUATING under lease; EVALUATING to OPEN on expiry; EVALUATING to RESOLVED by owning worker"],
|
||||
"side_effects_and_privileges": ["Epistemics schema only; no cognitive call and no proposal promotion"],
|
||||
"persistence_and_migrations": ["Append-only migration 032; production apply is later canary"],
|
||||
"failure_retry_restart": ["FOR UPDATE SKIP LOCKED claim; expired claims are recoverable; one-shot worker is restart-safe"],
|
||||
"assumptions": ["Cognitive evaluation and skill admission remain separate governed stages"],
|
||||
"acceptance_scenarios": ["Concurrent workers claim distinct candidates", "expired evaluation returns to OPEN", "only claim owner can resolve"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MIGRATION_DECISION", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000136",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "f73c699",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make model fleet tiers, readiness, and explicit lifeboat fallback durable as typed capability policy.",
|
||||
"requirement_ids": ["WP-601", "Model fleet policy", "Fallback readiness"],
|
||||
"allowed_change_scope": {"modules": ["capability"], "paths": ["contracts/capability/fleet.go", "modules/capability/domain/fleet.go", "modules/capability/domain/fleet_test.go", "tests/cognitive_registry/fleet_contract_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000136-fleet-readiness-policy.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "cmd/**", ".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 cognitive registry and health observations"], "may_change": ["Typed fleet readiness policy"]},
|
||||
"semantic_owner": "capability",
|
||||
"canonical_state": ["FleetReadiness value object"],
|
||||
"state_transitions": ["observed member readiness determines selected preferred or fallback tier"],
|
||||
"side_effects_and_privileges": ["No provider invocation or external mutation"],
|
||||
"persistence_and_migrations": ["In-memory typed decision; runtime persistence follows fleet registration integration"],
|
||||
"failure_retry_restart": ["No ready fallback yields explicit error"],
|
||||
"assumptions": ["Existing Cognitive Gateway remains the only invocation boundary"],
|
||||
"acceptance_scenarios": ["heavy timeout selects ready lifeboat", "no ready member fails closed"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000137",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "d38406f",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add typed governed self-development gate requiring evidence, task packet, isolated worktree, independent verifier, canary, promotion or rollback.",
|
||||
"requirement_ids": ["WP-701", "Governed self-development", "Independent verification", "Rollback"],
|
||||
"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-000137-governed-self-development-gate.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "cmd/**", "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 AgentRun and work graph contracts"], "may_change": ["Typed self-development stage gate"]},
|
||||
"semantic_owner": "work-agent-execution",
|
||||
"canonical_state": ["DevelopmentGate value object"],
|
||||
"state_transitions": ["evidence to proposal to task to isolation to independent verification to canary to promotion; canary may rollback"],
|
||||
"side_effects_and_privileges": ["No direct code mutation, provider invocation, or promotion side effect"],
|
||||
"persistence_and_migrations": ["Value object only; durable gate persistence follows execution-plane integration"],
|
||||
"failure_retry_restart": ["Invalid stage transitions and missing evidence fail closed"],
|
||||
"assumptions": ["Task packet and worktree identity are provided by the execution plane"],
|
||||
"acceptance_scenarios": ["same producer cannot verify output", "promotion is terminal", "canary can rollback"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000138",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "1ccd43c",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add executable contract coverage for authenticated cabinet, operations, learning, accessibility, responsive, and reduced-motion surfaces.",
|
||||
"requirement_ids": ["WP-801", "WP-802", "WP-805", "Authenticated cabinet acceptance"],
|
||||
"allowed_change_scope": {"modules": ["creator-web"], "paths": ["tests/creator_web/cabinet_acceptance_contract_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000138-ui-operations-acceptance-contract.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "cmd/**", "contracts/**", ".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 creator-web authenticated API and static cabinet"], "may_change": ["Acceptance tests only"]},
|
||||
"semantic_owner": "creator-web",
|
||||
"canonical_state": ["None; tests inspect existing UI contract"],
|
||||
"state_transitions": ["login session gates cabinet; operations and learning request authenticated API data"],
|
||||
"side_effects_and_privileges": ["No external mutation; live authenticated browser acceptance remains a separate production verification"],
|
||||
"persistence_and_migrations": ["None"],
|
||||
"failure_retry_restart": ["Missing auth, learning, or responsive contract fails CI"],
|
||||
"assumptions": ["Credentials are never embedded in acceptance tests"],
|
||||
"acceptance_scenarios": ["cabinet exposes operations and reflection surfaces", "session and learning routes are authenticated", "mobile/reduced motion rules are present"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_AUTHENTICATED_BROWSER_SESSION", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000139",
|
||||
"task_revision": 2,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "6f5f7e1",
|
||||
"risk_tier": "E",
|
||||
"intent": "Run agent and reflection workers as restartable production processes with DB-owned idle wakeups and protected credential refs.",
|
||||
"requirement_ids": ["WP-302", "WP-502", "Automatic worker runtime", "Production recovery"],
|
||||
"allowed_change_scope": {"modules": ["work-agent-execution", "epistemics", "deployment", "creator-web-acceptance"], "paths": ["contracts/secrets/types.go", "modules/work/application/worker.go", "adapters/postgres/work_repository.go", "cmd/agent-worker/main.go", "adapters/postgres/epistemics_repository.go", "cmd/reflection-worker/main.go", "deploy/docker-compose.prod.yml", "tests/creator_web/verify-remote.ps1", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000139-production-worker-runtime.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "contracts/authority/**", ".gitea/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 2, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["Existing durable dispatch/reflection tables and file secret resolver"], "may_change": ["Worker process lifecycle and production service wiring"]},
|
||||
"semantic_owner": "work-agent-execution and epistemics",
|
||||
"canonical_state": ["work.agent_dispatch_queue", "epistemics.reflection_candidates"],
|
||||
"state_transitions": ["workers recover stale claims, claim durable work, process one cycle, and repeat after DB-owned wakeup"],
|
||||
"side_effects_and_privileges": ["Work and epistemics database roles only; credentials remain CredentialRef/file-backed"],
|
||||
"persistence_and_migrations": ["Uses migrations 030 and 032 already in main"],
|
||||
"failure_retry_restart": ["restartable containers; stale claims recover after restart; -once remains diagnostic mode"],
|
||||
"assumptions": ["Production secret volume contains work and epistemics refs at declared paths"],
|
||||
"acceptance_scenarios": ["both workers stay running idle", "worker survives empty queue", "worker uses protected secret files", "stale dispatch/reflection claims recover"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "docker compose config"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SECRET_REF_MISMATCH", "BLOCKED_BY_MIGRATION_DECISION", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000140",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "07a8de9",
|
||||
"risk_tier": "E",
|
||||
"intent": "Evaluate read-only research evidence into explicit supported, stale, conflicted or unknown findings without persistence or authority mutation.",
|
||||
"requirement_ids": ["WP-102", "research evaluation", "citation provenance", "freshness", "conflict visibility"],
|
||||
"allowed_change_scope": {"modules": ["epistemics-research-runtime"], "paths": ["contracts/epistemics/research.go", "modules/epistemics/domain/research.go", "modules/epistemics/application/research.go", "tests/research_runtime/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000140-research-evaluation-runtime.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "deploy/**", "security/**", "cmd/**", ".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-0019 typed source, snapshot, excerpt and citation contracts"], "may_change": ["typed research finding and read-only connector boundary"]},
|
||||
"semantic_owner": "epistemics",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["connector evidence -> supported/stale/conflicted/unknown result", "invalid source chain -> rejected"],
|
||||
"side_effects_and_privileges": ["read-only connector boundary; no external mutation; no claim persistence"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["evaluation is pure and repeatable; connector failures remain errors"],
|
||||
"assumptions": ["connectors provide immutable exact excerpts and source snapshots", "proposition keys are connector-supplied typed grouping keys"],
|
||||
"acceptance_scenarios": ["no evidence remains UNKNOWN", "stale evidence is marked STALE", "incompatible statements remain CONFLICTED", "invalid citation chain is rejected", "connector boundary cannot mutate canonical state"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "research runtime tests"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
var ErrInvalidFleetReadiness = errors.New("invalid fleet readiness")
|
||||
|
||||
func BuildFleetReadiness(observedAt int64, members []capability.FleetMember) (capability.FleetReadiness, error) {
|
||||
if observedAt < 1 || len(members) == 0 {
|
||||
return capability.FleetReadiness{}, ErrInvalidFleetReadiness
|
||||
}
|
||||
result := capability.FleetReadiness{ObservedAtUnixMillis: observedAt, Members: append([]capability.FleetMember(nil), members...), FallbackTier: capability.FleetTierLifeboat}
|
||||
seen := map[capability.FleetTier]bool{}
|
||||
for i := range result.Members {
|
||||
member := &result.Members[i]
|
||||
if member.ProviderInstanceID == "" || member.ModelDescriptorID == "" || !validFleetTier(member.Tier) || seen[member.Tier] && member.Ready {
|
||||
return capability.FleetReadiness{}, ErrInvalidFleetReadiness
|
||||
}
|
||||
if member.Ready {
|
||||
seen[member.Tier] = true
|
||||
}
|
||||
}
|
||||
for _, tier := range []capability.FleetTier{capability.FleetTierFast, capability.FleetTierMedium, capability.FleetTierHeavy, capability.FleetTierLifeboat} {
|
||||
if seen[tier] {
|
||||
result.ReadyTiers = append(result.ReadyTiers, tier)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func SelectFleetTier(readiness capability.FleetReadiness, preferred capability.FleetTier) (capability.FleetTier, error) {
|
||||
if readiness.ObservedAtUnixMillis < 1 || len(readiness.Members) == 0 || !validFleetTier(readiness.FallbackTier) {
|
||||
return "", ErrInvalidFleetReadiness
|
||||
}
|
||||
if slices.Contains(readiness.ReadyTiers, preferred) {
|
||||
return preferred, nil
|
||||
}
|
||||
if slices.Contains(readiness.ReadyTiers, readiness.FallbackTier) {
|
||||
return readiness.FallbackTier, nil
|
||||
}
|
||||
return "", ErrInvalidFleetReadiness
|
||||
}
|
||||
|
||||
func validFleetTier(value capability.FleetTier) bool {
|
||||
return value == capability.FleetTierFast || value == capability.FleetTierMedium || value == capability.FleetTierHeavy || value == capability.FleetTierLifeboat
|
||||
}
|
||||
|
||||
func SortFleetMembers(members []capability.FleetMember) {
|
||||
sort.SliceStable(members, func(i, j int) bool {
|
||||
if members[i].Tier != members[j].Tier {
|
||||
return members[i].Tier < members[j].Tier
|
||||
}
|
||||
return members[i].ProviderInstanceID < members[j].ProviderInstanceID
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
func TestFleetReadinessFallsBackToLifeboatWhenPrimaryTiersUnavailable(t *testing.T) {
|
||||
readiness, err := BuildFleetReadiness(100, []capability.FleetMember{{ProviderInstanceID: "cpu", ModelDescriptorID: "qwen", Tier: capability.FleetTierLifeboat, Ready: true, Reason: "local"}, {ProviderInstanceID: "gpu", ModelDescriptorID: "heavy", Tier: capability.FleetTierHeavy, Ready: false, Reason: "timeout"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
selected, err := SelectFleetTier(readiness, capability.FleetTierHeavy)
|
||||
if err != nil || selected != capability.FleetTierLifeboat {
|
||||
t.Fatalf("selected=%q err=%v", selected, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
domain "git.cin.su/m/Assistant/modules/epistemics/domain"
|
||||
)
|
||||
|
||||
// ReadOnlyConnector is the only integration point for web/repository sources.
|
||||
// Implementations return immutable evidence; they cannot persist claims.
|
||||
type ReadOnlyConnector interface {
|
||||
Collect(context.Context, epistemics.ResearchPlan) ([]epistemics.ResearchCandidate, error)
|
||||
}
|
||||
|
||||
type ResearchService struct{ Connector ReadOnlyConnector }
|
||||
|
||||
func (s ResearchService) Evaluate(ctx context.Context, plan epistemics.ResearchPlan, nowUnixMillis int64) (epistemics.ResearchResult, error) {
|
||||
if s.Connector == nil {
|
||||
return epistemics.ResearchResult{}, domain.ErrNoResearchEvidence
|
||||
}
|
||||
candidates, err := s.Connector.Collect(ctx, plan)
|
||||
if err != nil {
|
||||
return epistemics.ResearchResult{}, err
|
||||
}
|
||||
return domain.EvaluateResearch(plan, candidates, nowUnixMillis)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidResearchPlan = errors.New("invalid research plan")
|
||||
ErrInvalidResearchCandidate = errors.New("invalid research candidate")
|
||||
ErrNoResearchEvidence = errors.New("no research evidence")
|
||||
)
|
||||
|
||||
// EvaluateResearch turns connector evidence into an explainable, non-canonical
|
||||
// result. It is deliberately pure: it does not call a provider, persist a
|
||||
// claim, or promote an assessment into authority.
|
||||
func EvaluateResearch(plan epistemics.ResearchPlan, candidates []epistemics.ResearchCandidate, evaluatedAtUnixMillis int64) (epistemics.ResearchResult, error) {
|
||||
if err := plan.Validate(); err != nil || evaluatedAtUnixMillis < 1 {
|
||||
return epistemics.ResearchResult{}, ErrInvalidResearchPlan
|
||||
}
|
||||
result := epistemics.ResearchResult{PlanID: plan.ID, EvaluatedAtUnixMillis: evaluatedAtUnixMillis}
|
||||
if len(candidates) == 0 {
|
||||
result.Findings = []epistemics.ResearchFinding{{State: epistemics.FindingUnknown, Reason: "no connector evidence was returned"}}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
groups := make(map[string][]epistemics.ResearchCandidate)
|
||||
order := make([]string, 0, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
if strings.TrimSpace(candidate.PropositionKey) == "" || strings.TrimSpace(candidate.Statement) == "" || candidate.Evidence == "" {
|
||||
return epistemics.ResearchResult{}, ErrInvalidResearchCandidate
|
||||
}
|
||||
if _, exists := groups[candidate.PropositionKey]; !exists {
|
||||
order = append(order, candidate.PropositionKey)
|
||||
}
|
||||
groups[candidate.PropositionKey] = append(groups[candidate.PropositionKey], candidate)
|
||||
}
|
||||
|
||||
for _, key := range order {
|
||||
finding, err := evaluateGroup(key, groups[key], evaluatedAtUnixMillis)
|
||||
if err != nil {
|
||||
return epistemics.ResearchResult{}, err
|
||||
}
|
||||
result.Findings = append(result.Findings, finding)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func evaluateGroup(key string, candidates []epistemics.ResearchCandidate, evaluatedAt int64) (epistemics.ResearchFinding, error) {
|
||||
if len(candidates) == 0 {
|
||||
return epistemics.ResearchFinding{}, ErrNoResearchEvidence
|
||||
}
|
||||
first := candidates[0]
|
||||
firstBundle, err := compileResearchCandidate(first)
|
||||
if err != nil {
|
||||
return epistemics.ResearchFinding{}, ErrInvalidResearchCandidate
|
||||
}
|
||||
finding := epistemics.ResearchFinding{PropositionKey: key, Statement: first.Statement, Claim: firstBundle.Claim, Citations: []epistemics.Citation{firstBundle.Citation}, State: epistemics.FindingSupported}
|
||||
statements := map[string]struct{}{normalize(first.Statement): {}}
|
||||
if first.Snapshot.Freshness == epistemics.FreshnessUnknown || first.Evidence == epistemics.EvidenceAssumed || first.Coverage != epistemics.CoverageComplete {
|
||||
finding.State, finding.Reason = epistemics.FindingUnknown, "evidence is assumed, unknown or incomplete"
|
||||
} else if first.Snapshot.Freshness == epistemics.FreshnessStale {
|
||||
finding.State, finding.Reason = epistemics.FindingStale, "source snapshot is stale"
|
||||
}
|
||||
for _, candidate := range candidates[1:] {
|
||||
bundle, resolveErr := compileResearchCandidate(candidate)
|
||||
if resolveErr != nil {
|
||||
return epistemics.ResearchFinding{}, ErrInvalidResearchCandidate
|
||||
}
|
||||
finding.Citations = append(finding.Citations, bundle.Citation)
|
||||
statements[normalize(candidate.Statement)] = struct{}{}
|
||||
if candidate.Snapshot.Freshness == epistemics.FreshnessUnknown || candidate.Evidence == epistemics.EvidenceAssumed || candidate.Coverage != epistemics.CoverageComplete {
|
||||
finding.State, finding.Reason = epistemics.FindingUnknown, "at least one source has assumed, unknown or incomplete evidence"
|
||||
} else if candidate.Snapshot.Freshness == epistemics.FreshnessStale {
|
||||
finding.State, finding.Reason = epistemics.FindingStale, "at least one source snapshot is stale"
|
||||
}
|
||||
}
|
||||
if len(statements) > 1 {
|
||||
finding.State, finding.Reason = epistemics.FindingConflicted, "connector evidence contains incompatible statements"
|
||||
}
|
||||
for _, citation := range finding.Citations {
|
||||
if citation.Conflict == epistemics.ConflictPresent {
|
||||
finding.State, finding.Reason = epistemics.FindingConflicted, "connector marked evidence as conflicting"
|
||||
}
|
||||
}
|
||||
if len(finding.Citations) == 0 || evaluatedAt < 1 {
|
||||
return epistemics.ResearchFinding{}, ErrInvalidResearchCandidate
|
||||
}
|
||||
return finding, nil
|
||||
}
|
||||
|
||||
func normalize(value string) string { return strings.ToLower(strings.Join(strings.Fields(value), " ")) }
|
||||
|
||||
// candidateConflict keeps unknown source freshness explicit at the candidate
|
||||
// boundary. A connector may mark evidence as conflicting without changing the
|
||||
// source; group-level incompatible statements are detected separately.
|
||||
func compileResearchCandidate(candidate epistemics.ResearchCandidate) (epistemics.TruthBundle, error) {
|
||||
if candidate.Coverage == "" || candidate.Conflict == "" {
|
||||
return epistemics.TruthBundle{}, ErrInvalidResearchCandidate
|
||||
}
|
||||
citation, err := epistemics.ResolveCitation(candidate.Source, candidate.Snapshot, candidate.Excerpt, candidate.Evidence, candidate.Coverage, candidate.Conflict, candidate.Snapshot.CapturedAtUnixMillis)
|
||||
if err != nil {
|
||||
return epistemics.TruthBundle{}, err
|
||||
}
|
||||
observation := epistemics.Observation{ID: epistemics.ObservationID("observation:" + string(candidate.Excerpt.ID)), SourceSnapshot: candidate.Snapshot.ID, Excerpt: candidate.Excerpt.ID, Text: candidate.Excerpt.Text, ObservedAt: candidate.Snapshot.CapturedAtUnixMillis}
|
||||
claim := epistemics.Claim{ID: epistemics.ClaimID("claim:" + string(candidate.Excerpt.ID)), Statement: candidate.Statement, ObservationID: observation.ID, CitationID: citation.ID, Verification: epistemics.VerificationUnverified}
|
||||
return epistemics.TruthBundle{Observation: observation, Claim: claim, Belief: epistemics.Belief{ID: epistemics.BeliefID("belief:" + string(candidate.Excerpt.ID)), ClaimID: claim.ID, State: epistemics.BeliefProvisional}, Citation: citation}, nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
)
|
||||
|
||||
var ErrInvalidTruthBundle = errors.New("invalid truth bundle")
|
||||
|
||||
func CompileObservedClaim(source epistemics.Source, snapshot epistemics.SourceSnapshot, excerpt epistemics.SourceExcerpt, statement string, observedAt int64, conflict epistemics.ConflictState) (epistemics.TruthBundle, error) {
|
||||
if strings.TrimSpace(statement) == "" || observedAt < 1 {
|
||||
return epistemics.TruthBundle{}, ErrInvalidTruthBundle
|
||||
}
|
||||
citation, err := epistemics.ResolveCitation(source, snapshot, excerpt, epistemics.EvidenceObserved, epistemics.CoverageComplete, conflict, observedAt)
|
||||
if err != nil {
|
||||
return epistemics.TruthBundle{}, ErrInvalidTruthBundle
|
||||
}
|
||||
observation := epistemics.Observation{ID: epistemics.ObservationID("observation:" + string(excerpt.ID)), SourceSnapshot: snapshot.ID, Excerpt: excerpt.ID, Text: excerpt.Text, ObservedAt: observedAt}
|
||||
claim := epistemics.Claim{ID: epistemics.ClaimID("claim:" + string(excerpt.ID)), Statement: statement, ObservationID: observation.ID, CitationID: citation.ID, Verification: epistemics.VerificationUnverified}
|
||||
state := epistemics.BeliefProvisional
|
||||
if conflict == epistemics.ConflictPresent || conflict == epistemics.ConflictUnknown {
|
||||
state = epistemics.BeliefConflicted
|
||||
}
|
||||
bundle := epistemics.TruthBundle{Observation: observation, Claim: claim, Belief: epistemics.Belief{ID: epistemics.BeliefID("belief:" + string(excerpt.ID)), ClaimID: claim.ID, State: state}, Citation: citation}
|
||||
if bundle.Observation.SourceSnapshot != snapshot.ID || bundle.Claim.ObservationID != bundle.Observation.ID || bundle.Claim.CitationID != bundle.Citation.ID || bundle.Belief.ClaimID != bundle.Claim.ID {
|
||||
return epistemics.TruthBundle{}, ErrInvalidTruthBundle
|
||||
}
|
||||
return bundle, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"git.cin.su/m/Assistant/modules/work/ports"
|
||||
)
|
||||
|
||||
type WorkerCycleResult struct {
|
||||
RecoveredClaims int64
|
||||
DispatchID string
|
||||
Completed bool
|
||||
Idle bool
|
||||
}
|
||||
|
||||
func ExecuteWorkerCycle(ctx context.Context, store ports.WorkerStore, workerID string, token work.AgentFencingToken, now time.Time, staleAfter, leaseTTL time.Duration) (WorkerCycleResult, error) {
|
||||
recovered, err := store.RecoverStaleClaims(ctx, now.UnixMilli(), staleAfter.Milliseconds())
|
||||
if err != nil {
|
||||
return WorkerCycleResult{}, err
|
||||
}
|
||||
item, err := store.ClaimNextDispatch(ctx, workerID, now.UnixMilli())
|
||||
if err != nil {
|
||||
return WorkerCycleResult{RecoveredClaims: recovered}, err
|
||||
}
|
||||
if item.DispatchID == "" {
|
||||
return WorkerCycleResult{RecoveredClaims: recovered, Idle: true}, nil
|
||||
}
|
||||
intent, err := store.LoadDispatchIntent(ctx, item.DispatchID)
|
||||
if err != nil {
|
||||
_ = store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchFailed, now.UnixMilli())
|
||||
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
|
||||
}
|
||||
if _, err := ExecuteDryRunDispatch(ctx, store, intent, workerID, token, now, leaseTTL); err != nil {
|
||||
_ = store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchFailed, now.UnixMilli())
|
||||
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
|
||||
}
|
||||
if err := store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchCompleted, now.UnixMilli()); err != nil {
|
||||
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
|
||||
}
|
||||
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID, Completed: true}, nil
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidGraph = errors.New("invalid work graph")
|
||||
ErrGraphCycle = errors.New("work graph contains a cycle")
|
||||
)
|
||||
|
||||
func ValidateWorkGraph(graph work.WorkGraph) error {
|
||||
if graph.ID == "" || graph.Revision < 1 || graph.Budget.MaxParallel < 1 || graph.Budget.MaxAttempts < 1 || len(graph.Nodes) == 0 {
|
||||
return ErrInvalidGraph
|
||||
}
|
||||
nodes := make(map[work.GraphNodeID]struct{}, len(graph.Nodes))
|
||||
keys := make(map[string]struct{}, len(graph.Nodes))
|
||||
for _, node := range graph.Nodes {
|
||||
if node.ID == "" || node.IdempotencyKey == "" || node.Attempts < 0 {
|
||||
return ErrInvalidGraph
|
||||
}
|
||||
if _, exists := nodes[node.ID]; exists {
|
||||
return ErrInvalidGraph
|
||||
}
|
||||
if _, exists := keys[node.IdempotencyKey]; exists {
|
||||
return ErrInvalidGraph
|
||||
}
|
||||
nodes[node.ID] = struct{}{}
|
||||
keys[node.IdempotencyKey] = struct{}{}
|
||||
}
|
||||
adjacency := make(map[work.GraphNodeID][]work.GraphNodeID, len(nodes))
|
||||
for _, edge := range graph.Edges {
|
||||
if edge.From == "" || edge.To == "" || edge.From == edge.To {
|
||||
return ErrInvalidGraph
|
||||
}
|
||||
if _, ok := nodes[edge.From]; !ok {
|
||||
return ErrInvalidGraph
|
||||
}
|
||||
if _, ok := nodes[edge.To]; !ok {
|
||||
return ErrInvalidGraph
|
||||
}
|
||||
adjacency[edge.From] = append(adjacency[edge.From], edge.To)
|
||||
}
|
||||
visiting := map[work.GraphNodeID]bool{}
|
||||
visited := map[work.GraphNodeID]bool{}
|
||||
var visit func(work.GraphNodeID) error
|
||||
visit = func(node work.GraphNodeID) error {
|
||||
if visiting[node] {
|
||||
return ErrGraphCycle
|
||||
}
|
||||
if visited[node] {
|
||||
return nil
|
||||
}
|
||||
visiting[node] = true
|
||||
for _, next := range adjacency[node] {
|
||||
if err := visit(next); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
delete(visiting, node)
|
||||
visited[node] = true
|
||||
return nil
|
||||
}
|
||||
for node := range nodes {
|
||||
if err := visit(node); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadyGraphNodes(graph work.WorkGraph) ([]work.GraphNodeID, error) {
|
||||
if err := ValidateWorkGraph(graph); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
completed := map[work.GraphNodeID]bool{}
|
||||
for _, node := range graph.Nodes {
|
||||
if node.State == work.GraphNodeSuccess {
|
||||
completed[node.ID] = true
|
||||
}
|
||||
}
|
||||
blockedBy := map[work.GraphNodeID][]work.GraphNodeID{}
|
||||
for _, edge := range graph.Edges {
|
||||
blockedBy[edge.To] = append(blockedBy[edge.To], edge.From)
|
||||
}
|
||||
ready := make([]work.GraphNodeID, 0)
|
||||
for _, node := range graph.Nodes {
|
||||
if node.State != work.GraphNodeReady && node.State != work.GraphNodeBlocked {
|
||||
continue
|
||||
}
|
||||
dependenciesReady := true
|
||||
for _, dependency := range blockedBy[node.ID] {
|
||||
if !completed[dependency] {
|
||||
dependenciesReady = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if dependenciesReady {
|
||||
ready = append(ready, node.ID)
|
||||
}
|
||||
}
|
||||
sort.Slice(ready, func(i, j int) bool { return ready[i] < ready[j] })
|
||||
return ready, nil
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrGraphBudgetExceeded = errors.New("work graph budget exceeded")
|
||||
ErrConflictUnresolved = errors.New("conflict resolution is not explicit")
|
||||
)
|
||||
|
||||
func ScheduleReadyNodes(graph work.WorkGraph, active int) ([]work.GraphNodeID, error) {
|
||||
if err := ValidateWorkGraph(graph); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if active < 0 || active > graph.Budget.MaxParallel {
|
||||
return nil, ErrGraphBudgetExceeded
|
||||
}
|
||||
ready, err := ReadyGraphNodes(graph)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
available := graph.Budget.MaxParallel - active
|
||||
if len(ready) > available {
|
||||
ready = ready[:available]
|
||||
}
|
||||
return ready, nil
|
||||
}
|
||||
|
||||
func AdmitGraph(graph work.WorkGraph) (work.WorkGraph, error) {
|
||||
if err := ValidateWorkGraph(graph); err != nil {
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
for _, node := range graph.Nodes {
|
||||
if node.Attempts > graph.Budget.MaxAttempts {
|
||||
return work.WorkGraph{}, ErrGraphBudgetExceeded
|
||||
}
|
||||
}
|
||||
graph.State = work.GraphAdmitted
|
||||
return graph, nil
|
||||
}
|
||||
|
||||
func ResolveGraphConflict(conflict work.ConflictSet, selectedCandidate string) (work.ConflictSet, error) {
|
||||
if conflict.ID == "" || conflict.GraphID == "" || conflict.GraphRevision < 1 || conflict.NodeID == "" || conflict.Resolved || selectedCandidate == "" {
|
||||
return work.ConflictSet{}, ErrConflictUnresolved
|
||||
}
|
||||
candidates := append([]string(nil), conflict.CandidateIDs...)
|
||||
sort.Strings(candidates)
|
||||
for _, candidate := range candidates {
|
||||
if candidate == selectedCandidate {
|
||||
conflict.CandidateIDs = candidates
|
||||
conflict.Resolved = true
|
||||
return conflict, nil
|
||||
}
|
||||
}
|
||||
return work.ConflictSet{}, ErrConflictUnresolved
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
var ErrInvalidDevelopmentGate = errors.New("invalid governed self-development gate")
|
||||
|
||||
func AdvanceDevelopmentGate(current work.DevelopmentGate, next work.DevelopmentStage) (work.DevelopmentGate, error) {
|
||||
if err := ValidateDevelopmentGate(current); err != nil {
|
||||
return work.DevelopmentGate{}, err
|
||||
}
|
||||
if !validDevelopmentTransition(current.Stage, next) {
|
||||
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
|
||||
}
|
||||
current.Stage = next
|
||||
switch next {
|
||||
case work.DevelopmentProposalCreated:
|
||||
if current.ProposalRef == "" {
|
||||
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
|
||||
}
|
||||
case work.DevelopmentTaskAdmitted:
|
||||
if current.TaskPacketRef == "" {
|
||||
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
|
||||
}
|
||||
case work.DevelopmentIsolated:
|
||||
if current.WorktreeRef == "" {
|
||||
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
|
||||
}
|
||||
case work.DevelopmentVerified:
|
||||
if current.VerificationEvidence == "" || len(current.VerifierRunRefs) == 0 || len(current.ProducerRunRefs) == 0 || sameRuns(current.ProducerRunRefs, current.VerifierRunRefs) {
|
||||
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
|
||||
}
|
||||
case work.DevelopmentCanary:
|
||||
if current.CanaryEvidence == "" {
|
||||
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
|
||||
}
|
||||
case work.DevelopmentPromoted:
|
||||
if current.PromotionRef == "" {
|
||||
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
|
||||
}
|
||||
case work.DevelopmentRolledBack:
|
||||
if current.RollbackRef == "" {
|
||||
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
|
||||
}
|
||||
}
|
||||
return current, nil
|
||||
}
|
||||
|
||||
func ValidateDevelopmentGate(value work.DevelopmentGate) error {
|
||||
if value.ID == "" || len(value.EvidenceRefs) == 0 || !validDevelopmentStage(value.Stage) || duplicateStrings(value.EvidenceRefs) {
|
||||
return ErrInvalidDevelopmentGate
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validDevelopmentTransition(current, next work.DevelopmentStage) bool {
|
||||
if current == work.DevelopmentCanary && next == work.DevelopmentRolledBack {
|
||||
return true
|
||||
}
|
||||
if current == work.DevelopmentPromoted || current == work.DevelopmentRolledBack {
|
||||
return false
|
||||
}
|
||||
order := []work.DevelopmentStage{work.DevelopmentEvidenceCollected, work.DevelopmentProposalCreated, work.DevelopmentTaskAdmitted, work.DevelopmentIsolated, work.DevelopmentVerified, work.DevelopmentCanary, work.DevelopmentPromoted}
|
||||
index := func(value work.DevelopmentStage) int { return slices.Index(order, value) }
|
||||
return index(next) == index(current)+1
|
||||
}
|
||||
|
||||
func validDevelopmentStage(value work.DevelopmentStage) bool {
|
||||
return slices.Contains([]work.DevelopmentStage{work.DevelopmentEvidenceCollected, work.DevelopmentProposalCreated, work.DevelopmentTaskAdmitted, work.DevelopmentIsolated, work.DevelopmentVerified, work.DevelopmentCanary, work.DevelopmentPromoted, work.DevelopmentRolledBack}, value)
|
||||
}
|
||||
|
||||
func duplicateStrings(values []string) bool {
|
||||
seen := map[string]bool{}
|
||||
for _, value := range values {
|
||||
if value == "" || seen[value] {
|
||||
return true
|
||||
}
|
||||
seen[value] = true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sameRuns(left, right []work.AgentRunID) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for _, value := range left {
|
||||
if !slices.Contains(right, value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
func TestGovernedSelfDevelopmentRequiresIndependentVerifierBeforeCanary(t *testing.T) {
|
||||
gate := work.DevelopmentGate{ID: "dev-1", Stage: work.DevelopmentEvidenceCollected, EvidenceRefs: []string{"obs-1"}, ProposalRef: "proposal-1", TaskPacketRef: "task-1", WorktreeRef: "worktree-1", ProducerRunRefs: []work.AgentRunID{"producer"}, VerifierRunRefs: []work.AgentRunID{"verifier"}, VerificationEvidence: "verify-1", CanaryEvidence: "canary-1", PromotionRef: "promotion-1"}
|
||||
for _, stage := range []work.DevelopmentStage{work.DevelopmentProposalCreated, work.DevelopmentTaskAdmitted, work.DevelopmentIsolated, work.DevelopmentVerified, work.DevelopmentCanary, work.DevelopmentPromoted} {
|
||||
var err error
|
||||
gate, err = AdvanceDevelopmentGate(gate, stage)
|
||||
if err != nil {
|
||||
t.Fatalf("stage %s: %v", stage, err)
|
||||
}
|
||||
}
|
||||
if _, err := AdvanceDevelopmentGate(gate, work.DevelopmentRolledBack); err == nil {
|
||||
t.Fatal("promoted gate must be terminal")
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
t.Fatal("same run cannot verify its own output")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package ports
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
type WorkerStore interface {
|
||||
ExecutionStore
|
||||
LoadDispatchIntent(context.Context, string) (work.AgentDispatchIntent, error)
|
||||
ClaimNextDispatch(context.Context, string, int64) (work.AgentDispatchQueueItem, error)
|
||||
CompleteDispatch(context.Context, string, string, work.AgentDispatchQueueState, int64) error
|
||||
RecoverStaleClaims(context.Context, int64, int64) (int64, error)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package agent_execution_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGraphPersistenceMigrationIsRevisionScoped(t *testing.T) {
|
||||
data, err := os.ReadFile("../../database/migrations/031_work_graph_persistence.sql")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sql := string(data)
|
||||
for _, marker := range []string{"work.graphs", "work.graph_nodes", "work.graph_edges", "PRIMARY KEY (graph_id, revision)", "FOREIGN KEY (graph_id, revision)"} {
|
||||
if !strings.Contains(sql, marker) {
|
||||
t.Fatalf("graph persistence marker missing: %s", marker)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package agent_execution_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
workdomain "git.cin.su/m/Assistant/modules/work/domain"
|
||||
)
|
||||
|
||||
func TestWorkGraphRejectsCycleAndOrdersReadyNodes(t *testing.T) {
|
||||
graph := work.WorkGraph{ID: "graph-1", Revision: 1, State: work.GraphAdmitted, Budget: work.GraphBudget{MaxParallel: 2, MaxAttempts: 2}, Nodes: []work.GraphNode{
|
||||
{ID: "node-b", IdempotencyKey: "key-b", State: work.GraphNodeReady},
|
||||
{ID: "node-a", IdempotencyKey: "key-a", State: work.GraphNodeReady},
|
||||
{ID: "node-c", IdempotencyKey: "key-c", State: work.GraphNodeBlocked},
|
||||
}, Edges: []work.GraphEdge{{From: "node-a", To: "node-c"}}}
|
||||
ready, err := workdomain.ReadyGraphNodes(graph)
|
||||
if err != nil || len(ready) != 2 || ready[0] != "node-a" || ready[1] != "node-b" {
|
||||
t.Fatalf("unexpected ready nodes: %#v %v", ready, err)
|
||||
}
|
||||
graph.Edges = append(graph.Edges, work.GraphEdge{From: "node-c", To: "node-a"})
|
||||
if err := workdomain.ValidateWorkGraph(graph); err != workdomain.ErrGraphCycle {
|
||||
t.Fatalf("expected cycle rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkGraphKeepsDependentNodeBlocked(t *testing.T) {
|
||||
graph := work.WorkGraph{ID: "graph-1", Revision: 1, State: work.GraphRunning, Budget: work.GraphBudget{MaxParallel: 1, MaxAttempts: 1}, Nodes: []work.GraphNode{
|
||||
{ID: "source", IdempotencyKey: "source-key", State: work.GraphNodeRunning},
|
||||
{ID: "dependent", IdempotencyKey: "dependent-key", State: work.GraphNodeBlocked},
|
||||
}, Edges: []work.GraphEdge{{From: "source", To: "dependent"}}}
|
||||
ready, err := workdomain.ReadyGraphNodes(graph)
|
||||
if err != nil || len(ready) != 0 {
|
||||
t.Fatalf("dependent node became ready early: %#v %v", ready, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGraphSchedulerEnforcesParallelBudgetAndExplicitConflict(t *testing.T) {
|
||||
graph := work.WorkGraph{ID: "graph-1", Revision: 1, State: work.GraphDraft, Budget: work.GraphBudget{MaxParallel: 2, MaxAttempts: 2}, Nodes: []work.GraphNode{
|
||||
{ID: "b", IdempotencyKey: "b-key", State: work.GraphNodeReady},
|
||||
{ID: "a", IdempotencyKey: "a-key", State: work.GraphNodeReady},
|
||||
}}
|
||||
admitted, err := workdomain.AdmitGraph(graph)
|
||||
if err != nil || admitted.State != work.GraphAdmitted {
|
||||
t.Fatalf("graph was not admitted: %#v %v", admitted, err)
|
||||
}
|
||||
ready, err := workdomain.ScheduleReadyNodes(admitted, 1)
|
||||
if err != nil || len(ready) != 1 || ready[0] != "a" {
|
||||
t.Fatalf("scheduler ignored parallel budget/order: %#v %v", ready, err)
|
||||
}
|
||||
conflict := work.ConflictSet{ID: "conflict-1", GraphID: "graph-1", GraphRevision: 1, NodeID: "a", CandidateIDs: []string{"candidate-b", "candidate-a"}}
|
||||
resolved, err := workdomain.ResolveGraphConflict(conflict, "candidate-a")
|
||||
if err != nil || !resolved.Resolved || resolved.CandidateIDs[0] != "candidate-a" {
|
||||
t.Fatalf("conflict was not explicitly resolved: %#v %v", resolved, err)
|
||||
}
|
||||
if _, err := workdomain.ResolveGraphConflict(conflict, "unknown"); err != workdomain.ErrConflictUnresolved {
|
||||
t.Fatalf("unknown candidate was accepted: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package agent_execution_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWorkerRecoveryAndClaimUseDurableQueueStatements(t *testing.T) {
|
||||
data, err := os.ReadFile("../../adapters/postgres/work_repository.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
source := string(data)
|
||||
for _, marker := range []string{"FOR UPDATE SKIP LOCKED", "state='CLAIMED'", "state='QUEUED'", "RecoverStaleClaims"} {
|
||||
if !strings.Contains(source, marker) {
|
||||
t.Fatalf("worker persistence marker missing: %s", marker)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cognitive_registry_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/modules/capability/domain"
|
||||
)
|
||||
|
||||
func TestFleetPolicyRequiresAnExplicitReadyFallback(t *testing.T) {
|
||||
readiness, err := domain.BuildFleetReadiness(1, []capability.FleetMember{{ProviderInstanceID: "cpu", ModelDescriptorID: "model", Tier: capability.FleetTierFast, Ready: false, Reason: "401"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := domain.SelectFleetTier(readiness, capability.FleetTierFast); err == nil {
|
||||
t.Fatal("expected no invocable fallback")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package creator_web_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreatorCabinetHasAuthenticatedOperationalAndLearningSurfaces(t *testing.T) {
|
||||
page, err := os.ReadFile("../../cmd/creator-web/static/index.html")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
markup := string(page)
|
||||
for _, marker := range []string{
|
||||
`<form id="login-form"`, `aria-labelledby="diagnostics-title"`, `data-page="operations"`,
|
||||
`id="learning"`, `id="reflection-list"`, `id="skill-list"`, `id="settings"`,
|
||||
`id="logout"`, `role="alert"`, `aria-live="polite"`,
|
||||
} {
|
||||
if !strings.Contains(markup, marker) {
|
||||
t.Fatalf("cabinet missing %q", marker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatorCabinetRoutesSessionAndLearningThroughAuthenticatedAPI(t *testing.T) {
|
||||
script, err := os.ReadFile("../../cmd/creator-web/static/assets/console.js")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
code := string(script)
|
||||
for _, marker := range []string{
|
||||
`request("/api/session/current")`, `request("/api/learning/recent?limit=8")`,
|
||||
`request("/api/advisory-policy")`, `request("/api/capabilities/cognitive-proposal/health")`, `request("/api/learning/recent?limit=1")`, `sessionStorage.getItem("domovoy.creator.session")`,
|
||||
`if (response.status === 401`,
|
||||
} {
|
||||
if !strings.Contains(code, marker) {
|
||||
t.Fatalf("console missing authenticated flow %q", marker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatorCabinetResponsiveAndReducedMotionRulesExist(t *testing.T) {
|
||||
style, err := os.ReadFile("../../cmd/creator-web/static/assets/console.css")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
css := string(style)
|
||||
for _, marker := range []string{"@media (max-width: 680px)", "@media (prefers-reduced-motion: reduce)", ".learning-grid { grid-template-columns: 1fr; }"} {
|
||||
if !strings.Contains(css, marker) {
|
||||
t.Fatalf("console missing responsive rule %q", marker)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,19 @@ function Post-CoreJson([string]$url, [object]$body) {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ContainerSecret([object[]]$environment, [string]$name) {
|
||||
$fileEntry = $environment | Where-Object { $_ -like "${name}_FILE=*" } | Select-Object -First 1
|
||||
if ($fileEntry) {
|
||||
$path = $fileEntry.Substring("${name}_FILE=".Length)
|
||||
$value = docker --context $DockerContext exec $coreContainer sh -c "cat -- '$path'"
|
||||
if ($LASTEXITCODE -ne 0 -or -not $value) { throw "$name file-backed secret is not readable" }
|
||||
return ($value -join "`n").TrimEnd("`r", "`n")
|
||||
}
|
||||
$entry = $environment | Where-Object { $_ -like "${name}=*" } | Select-Object -First 1
|
||||
if (-not $entry) { throw "$name is not configured" }
|
||||
return $entry.Substring("${name}=".Length)
|
||||
}
|
||||
|
||||
$health = Invoke-RestMethod -Uri "$BaseURL/healthz" -Method Get
|
||||
if ($health.status -ne "ok" -or $health.runtime -ne "creator-web") { throw "Creator web health response is invalid" }
|
||||
|
||||
@@ -52,19 +65,14 @@ $invalid = Invoke-WebRequest -Uri "$BaseURL/api/session/current" -Headers @{Auth
|
||||
if ($invalid.StatusCode -ne 401) { throw "Invalid Creator session was not denied" }
|
||||
|
||||
$environment = docker --context $DockerContext inspect --format '{{range .Config.Env}}{{println .}}{{end}}' $coreContainer
|
||||
$usernameEntry = $environment | Where-Object { $_ -like "CREATOR_USERNAME=*" } | Select-Object -First 1
|
||||
$passwordEntry = $environment | Where-Object { $_ -like "CREATOR_PASSWORD=*" } | Select-Object -First 1
|
||||
if (-not $usernameEntry -or -not $passwordEntry) { throw "Creator username/password is not configured" }
|
||||
$creatorUsername = $usernameEntry.Substring("CREATOR_USERNAME=".Length)
|
||||
$creatorPassword = $passwordEntry.Substring("CREATOR_PASSWORD=".Length)
|
||||
$creatorUsername = Get-ContainerSecret $environment "CREATOR_USERNAME"
|
||||
$creatorPassword = Get-ContainerSecret $environment "CREATOR_PASSWORD"
|
||||
$invalidCredentials = Invoke-WebRequest -Uri "$BaseURL/api/session" -Method Post -ContentType "application/json" -Body (@{username=$creatorUsername;password="invalid"} | ConvertTo-Json -Compress) -SkipHttpErrorCheck
|
||||
if ($invalidCredentials.StatusCode -ne 401) { throw "Invalid Creator credentials were not denied" }
|
||||
$issued = Invoke-RestMethod -Uri "$BaseURL/api/session" -Method Post -ContentType "application/json" -Body (@{username=$creatorUsername;password=$creatorPassword} | ConvertTo-Json -Compress)
|
||||
if (-not $issued.Token) { throw "Creator username/password session was not issued through the gateway" }
|
||||
|
||||
$bootstrapEntry = $environment | Where-Object { $_ -like "CREATOR_BOOTSTRAP_TOKEN=*" } | Select-Object -First 1
|
||||
if (-not $bootstrapEntry) { throw "Creator bootstrap token is not configured" }
|
||||
$bootstrapToken = $bootstrapEntry.Substring("CREATOR_BOOTSTRAP_TOKEN=".Length)
|
||||
$bootstrapToken = Get-ContainerSecret $environment "CREATOR_BOOTSTRAP_TOKEN"
|
||||
$recoveryIssued = Invoke-RestMethod -Uri "$BaseURL/api/session" -Method Post -Headers @{Authorization="Bearer $bootstrapToken"}
|
||||
if (-not $recoveryIssued.Token) { throw "Creator recovery session was not issued through the gateway" }
|
||||
$current = Invoke-RestMethod -Uri "$BaseURL/api/session/current" -Method Get -Headers @{Authorization="Bearer $($issued.Token)"}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package research_contracts_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReflectionCandidateLeaseSQLIsDurableAndRecoverable(t *testing.T) {
|
||||
checks := map[string][]string{
|
||||
"../../database/migrations/032_reflection_candidate_leases.sql": {"claim_expires_at_unix_millis", "reflection_candidates_claimable_idx"},
|
||||
"../../adapters/postgres/epistemics_repository.go": {"claim_expires_at_unix_millis", "FOR UPDATE SKIP LOCKED", "state='OPEN'", "state='RESOLVED'"},
|
||||
"../../cmd/reflection-worker/main.go": {"RecoverReflectionClaims", "ClaimReflectionCandidate", "ResolveReflectionCandidate"},
|
||||
}
|
||||
for path, needles := range checks {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
text := string(data)
|
||||
for _, needle := range needles {
|
||||
if !strings.Contains(text, needle) {
|
||||
t.Fatalf("%s missing %q", path, needle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package research_contracts_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
episdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
|
||||
)
|
||||
|
||||
func TestCompileObservedClaimPreservesEvidenceChainAndConflict(t *testing.T) {
|
||||
source := epistemics.Source{ID: "source-1", Locator: "https://example.test/source"}
|
||||
snapshot := epistemics.SourceSnapshot{ID: "snapshot-1", SourceID: source.ID, ContentDigest: "sha256:test", CapturedAtUnixMillis: 100, Freshness: epistemics.FreshnessCurrent}
|
||||
excerpt := epistemics.SourceExcerpt{ID: "excerpt-1", SnapshotID: snapshot.ID, Text: "exact observation", Start: 0, End: 17}
|
||||
bundle, err := episdomain.CompileObservedClaim(source, snapshot, excerpt, "The observed fact is true", 200, epistemics.ConflictPresent)
|
||||
if err != nil || bundle.Claim.ObservationID != bundle.Observation.ID || bundle.Belief.State != epistemics.BeliefConflicted || bundle.Citation.Evidence != epistemics.EvidenceObserved {
|
||||
t.Fatalf("invalid truth bundle: %#v %v", bundle, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileObservedClaimRejectsBrokenSourceChain(t *testing.T) {
|
||||
source := epistemics.Source{ID: "source-1", Locator: "https://example.test/source"}
|
||||
snapshot := epistemics.SourceSnapshot{ID: "snapshot-1", SourceID: "other-source", ContentDigest: "sha256:test", CapturedAtUnixMillis: 100, Freshness: epistemics.FreshnessCurrent}
|
||||
excerpt := epistemics.SourceExcerpt{ID: "excerpt-1", SnapshotID: snapshot.ID, Text: "exact observation", Start: 0, End: 17}
|
||||
if _, err := episdomain.CompileObservedClaim(source, snapshot, excerpt, "statement", 200, epistemics.ConflictNone); err != episdomain.ErrInvalidTruthBundle {
|
||||
t.Fatalf("broken chain was accepted: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package research_runtime_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
app "git.cin.su/m/Assistant/modules/epistemics/application"
|
||||
domain "git.cin.su/m/Assistant/modules/epistemics/domain"
|
||||
)
|
||||
|
||||
type connector struct {
|
||||
candidates []epistemics.ResearchCandidate
|
||||
}
|
||||
|
||||
func (c connector) Collect(context.Context, epistemics.ResearchPlan) ([]epistemics.ResearchCandidate, error) {
|
||||
return c.candidates, nil
|
||||
}
|
||||
|
||||
func candidate(statement string, freshness epistemics.FreshnessState, source, snapshot, excerpt string) epistemics.ResearchCandidate {
|
||||
return epistemics.ResearchCandidate{PropositionKey: "domovoy", Statement: statement, Evidence: epistemics.EvidenceObserved,
|
||||
Coverage: epistemics.CoverageComplete, Conflict: epistemics.ConflictNone,
|
||||
Source: epistemics.Source{ID: epistemics.SourceID(source), Locator: "https://example.test/" + source},
|
||||
Snapshot: epistemics.SourceSnapshot{ID: epistemics.SourceSnapshotID(snapshot), SourceID: epistemics.SourceID(source), ContentDigest: "sha256:" + snapshot, CapturedAtUnixMillis: 10, Freshness: freshness},
|
||||
Excerpt: epistemics.SourceExcerpt{ID: epistemics.SourceExcerptID(excerpt), SnapshotID: epistemics.SourceSnapshotID(snapshot), Text: statement, Start: 0, End: int64(len(statement))}}
|
||||
}
|
||||
|
||||
func TestResearchServiceKeepsUnknownStaleAndConflictExplicit(t *testing.T) {
|
||||
plan := epistemics.ResearchPlan{ID: "plan-1", Question: "question", SourceClasses: []string{"repository"}, FreshnessPolicy: "current"}
|
||||
unknown, err := (app.ResearchService{Connector: connector{}}).Evaluate(context.Background(), plan, 100)
|
||||
if err != nil || len(unknown.Findings) != 1 || unknown.Findings[0].State != epistemics.FindingUnknown {
|
||||
t.Fatalf("unknown evidence was not explicit: %#v %v", unknown, err)
|
||||
}
|
||||
result, err := (app.ResearchService{Connector: connector{candidates: []epistemics.ResearchCandidate{
|
||||
candidate("one answer", epistemics.FreshnessStale, "s1", "ss1", "e1"),
|
||||
candidate("another answer", epistemics.FreshnessCurrent, "s2", "ss2", "e2"),
|
||||
}}}).Evaluate(context.Background(), plan, 100)
|
||||
if err != nil || len(result.Findings) != 1 || result.Findings[0].State != epistemics.FindingConflicted || len(result.Findings[0].Citations) != 2 {
|
||||
t.Fatalf("conflict was not retained: %#v %v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateResearchRejectsFabricatedEvidenceChain(t *testing.T) {
|
||||
value := candidate("answer", epistemics.FreshnessCurrent, "s1", "ss1", "e1")
|
||||
value.Excerpt.SnapshotID = "other-snapshot"
|
||||
plan := epistemics.ResearchPlan{ID: "plan-1", Question: "question", SourceClasses: []string{"web"}, FreshnessPolicy: "current"}
|
||||
if _, err := domain.EvaluateResearch(plan, []epistemics.ResearchCandidate{value}, 100); err != domain.ErrInvalidResearchCandidate {
|
||||
t.Fatalf("fabricated evidence was accepted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResearchServiceKeepsAssumptionAndUnknownFreshnessVisible(t *testing.T) {
|
||||
plan := epistemics.ResearchPlan{ID: "plan-1", Question: "question", SourceClasses: []string{"web"}, FreshnessPolicy: "current"}
|
||||
value := candidate("assumed answer", epistemics.FreshnessCurrent, "s1", "ss1", "e1")
|
||||
value.Evidence = epistemics.EvidenceAssumed
|
||||
result, err := (app.ResearchService{Connector: connector{candidates: []epistemics.ResearchCandidate{value}}}).Evaluate(context.Background(), plan, 100)
|
||||
if err != nil || result.Findings[0].State != epistemics.FindingUnknown {
|
||||
t.Fatalf("assumption was promoted: %#v %v", result, err)
|
||||
}
|
||||
value = candidate("unknown answer", epistemics.FreshnessUnknown, "s2", "ss2", "e2")
|
||||
value.Conflict = epistemics.ConflictUnknown
|
||||
result, err = (app.ResearchService{Connector: connector{candidates: []epistemics.ResearchCandidate{value}}}).Evaluate(context.Background(), plan, 100)
|
||||
if err != nil || result.Findings[0].State != epistemics.FindingUnknown {
|
||||
t.Fatalf("unknown freshness was promoted: %#v %v", result, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package semantic_evolution_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
workdomain "git.cin.su/m/Assistant/modules/work/domain"
|
||||
)
|
||||
|
||||
func TestSelfDevelopmentRollbackIsAvailableOnlyFromCanary(t *testing.T) {
|
||||
gate := work.DevelopmentGate{ID: "dev-1", Stage: work.DevelopmentCanary, EvidenceRefs: []string{"obs"}, CanaryEvidence: "canary", RollbackRef: "rollback"}
|
||||
if _, err := workdomain.AdvanceDevelopmentGate(gate, work.DevelopmentRolledBack); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user