Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
931a0b0f0c | ||
|
|
e70be0103a | ||
|
|
2353f1f3bb | ||
|
|
355ba7db56 |
@@ -11,6 +11,14 @@ import (
|
||||
|
||||
type EpistemicsRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
type TruthRuntimeStatus struct {
|
||||
BundleCount int64 `json:"bundle_count"`
|
||||
ClaimsByVerification map[string]int64 `json:"claims_by_verification"`
|
||||
BeliefsByState map[string]int64 `json:"beliefs_by_state"`
|
||||
SourcesByFreshness map[string]int64 `json:"sources_by_freshness"`
|
||||
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
|
||||
}
|
||||
|
||||
func OpenEpistemics(ctx context.Context, config ContinuityConfig) (*EpistemicsRepository, error) {
|
||||
pool, err := openPool(ctx, config)
|
||||
if err != nil {
|
||||
@@ -25,6 +33,49 @@ func (r *EpistemicsRepository) Ping(ctx context.Context) error {
|
||||
return r.pool.Ping(ctx)
|
||||
}
|
||||
|
||||
// TruthRuntimeStatus is a read-only projection of durable truth bundles.
|
||||
// It does not promote claims, revise beliefs, or mutate epistemic state.
|
||||
func (r *EpistemicsRepository) TruthRuntimeStatus(ctx context.Context) (TruthRuntimeStatus, error) {
|
||||
status := TruthRuntimeStatus{
|
||||
ClaimsByVerification: map[string]int64{},
|
||||
BeliefsByState: map[string]int64{},
|
||||
SourcesByFreshness: map[string]int64{},
|
||||
ObservedAtUnixMillis: time.Now().UnixMilli(),
|
||||
}
|
||||
if err := r.pool.QueryRow(ctx, `SELECT count(*) FROM epistemics.truth_bundles`).Scan(&status.BundleCount); err != nil {
|
||||
return TruthRuntimeStatus{}, err
|
||||
}
|
||||
queries := []struct {
|
||||
query string
|
||||
destination map[string]int64
|
||||
}{
|
||||
{`SELECT claim_verification, count(*) FROM epistemics.truth_bundles GROUP BY claim_verification`, status.ClaimsByVerification},
|
||||
{`SELECT belief_state, count(*) FROM epistemics.truth_bundles GROUP BY belief_state`, status.BeliefsByState},
|
||||
{`SELECT snapshot_freshness, count(*) FROM epistemics.truth_bundles GROUP BY snapshot_freshness`, status.SourcesByFreshness},
|
||||
}
|
||||
for _, item := range queries {
|
||||
rows, err := r.pool.Query(ctx, item.query)
|
||||
if err != nil {
|
||||
return TruthRuntimeStatus{}, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var state string
|
||||
var count int64
|
||||
if err := rows.Scan(&state, &count); err != nil {
|
||||
rows.Close()
|
||||
return TruthRuntimeStatus{}, err
|
||||
}
|
||||
item.destination[state] = count
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return TruthRuntimeStatus{}, err
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (r *EpistemicsRepository) SaveSystemExpectation(ctx context.Context, value epistemics.SystemExpectation) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.system_expectations (
|
||||
expectation_id, original_intent_id, source_utterance_id, expected_outcome, expected_effect,
|
||||
|
||||
@@ -32,6 +32,54 @@ func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, er
|
||||
}
|
||||
return &WorkRepository{pool}, nil
|
||||
}
|
||||
|
||||
// WorkGraphRuntimeStatus is a read-only operational projection owned by Work.
|
||||
// It is not canonical graph state and cannot advance any lifecycle.
|
||||
type WorkGraphRuntimeStatus struct {
|
||||
GraphsByState map[string]int64 `json:"graphs_by_state"`
|
||||
NodesByState map[string]int64 `json:"nodes_by_state"`
|
||||
QueueByState map[string]int64 `json:"queue_by_state"`
|
||||
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
|
||||
}
|
||||
|
||||
func (r *WorkRepository) RuntimeStatus(ctx context.Context) (WorkGraphRuntimeStatus, error) {
|
||||
status := WorkGraphRuntimeStatus{
|
||||
GraphsByState: map[string]int64{},
|
||||
NodesByState: map[string]int64{},
|
||||
QueueByState: map[string]int64{},
|
||||
ObservedAtUnixMillis: time.Now().UnixMilli(),
|
||||
}
|
||||
queries := []struct {
|
||||
query string
|
||||
destination map[string]int64
|
||||
}{
|
||||
{`SELECT state, count(*) FROM work.graphs GROUP BY state`, status.GraphsByState},
|
||||
{`SELECT state, count(*) FROM work.graph_nodes GROUP BY state`, status.NodesByState},
|
||||
{`SELECT state, count(*) FROM work.agent_dispatch_queue GROUP BY state`, status.QueueByState},
|
||||
}
|
||||
for _, item := range queries {
|
||||
rows, err := r.pool.Query(ctx, item.query)
|
||||
if err != nil {
|
||||
return WorkGraphRuntimeStatus{}, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var state string
|
||||
var count int64
|
||||
if err := rows.Scan(&state, &count); err != nil {
|
||||
rows.Close()
|
||||
return WorkGraphRuntimeStatus{}, err
|
||||
}
|
||||
item.destination[state] = count
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return WorkGraphRuntimeStatus{}, err
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) Close() { r.pool.Close() }
|
||||
func (r *WorkRepository) Ping(ctx context.Context) error { return r.pool.Ping(ctx) }
|
||||
func (r *WorkRepository) Create(ctx context.Context, target work.TaskTarget) error {
|
||||
|
||||
@@ -34,6 +34,7 @@ func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/healthz", health.Handler{Runtime: "research-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
|
||||
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "research-runtime", Probes: []health.Probe{{Name: "epistemics-postgres", Check: func(ctx context.Context) error { return repository.Ping(ctx) }}}})
|
||||
mux.Handle("/v1/research/status", truthRuntimeStatusHandler(repository))
|
||||
mux.HandleFunc("POST /v1/research/evaluate", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request epistemics.ResearchEvaluationRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 2<<20)).Decode(&request); err != nil {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
)
|
||||
|
||||
type truthRuntimeStatusReader interface {
|
||||
TruthRuntimeStatus(context.Context) (postgres.TruthRuntimeStatus, error)
|
||||
}
|
||||
|
||||
type truthRuntimeStatusResponse struct {
|
||||
Runtime string `json:"runtime"`
|
||||
Status string `json:"status"`
|
||||
BundleCount int64 `json:"bundle_count"`
|
||||
ClaimsByVerification map[string]int64 `json:"claims_by_verification"`
|
||||
BeliefsByState map[string]int64 `json:"beliefs_by_state"`
|
||||
SourcesByFreshness map[string]int64 `json:"sources_by_freshness"`
|
||||
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
|
||||
}
|
||||
|
||||
func truthRuntimeStatusHandler(reader truthRuntimeStatusReader) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || r.URL.Path != "/v1/research/status" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
status, err := reader.TruthRuntimeStatus(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "research status unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(truthRuntimeStatusResponse{
|
||||
Runtime: "research-runtime",
|
||||
Status: "ready",
|
||||
BundleCount: status.BundleCount,
|
||||
ClaimsByVerification: status.ClaimsByVerification,
|
||||
BeliefsByState: status.BeliefsByState,
|
||||
SourcesByFreshness: status.SourcesByFreshness,
|
||||
ObservedAtUnixMillis: status.ObservedAtUnixMillis,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
)
|
||||
|
||||
type fakeTruthRuntimeStatusReader struct {
|
||||
status postgres.TruthRuntimeStatus
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeTruthRuntimeStatusReader) TruthRuntimeStatus(context.Context) (postgres.TruthRuntimeStatus, error) {
|
||||
return f.status, f.err
|
||||
}
|
||||
|
||||
func TestTruthRuntimeStatusReportsEvidenceCounts(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
truthRuntimeStatusHandler(fakeTruthRuntimeStatusReader{status: postgres.TruthRuntimeStatus{
|
||||
BundleCount: 4,
|
||||
ClaimsByVerification: map[string]int64{"UNVERIFIED": 3, "VERIFIED": 1},
|
||||
BeliefsByState: map[string]int64{"PROVISIONAL": 4},
|
||||
SourcesByFreshness: map[string]int64{"FRESH": 4},
|
||||
ObservedAtUnixMillis: 1700000000000,
|
||||
}}).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/v1/research/status", nil))
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("status code = %d", response.Code)
|
||||
}
|
||||
for _, expected := range []string{"\"status\":\"ready\"", "\"bundle_count\":4", "\"VERIFIED\":1", "\"PROVISIONAL\":4", "1700000000000"} {
|
||||
if !strings.Contains(response.Body.String(), expected) {
|
||||
t.Fatalf("response missing %q: %s", expected, response.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruthRuntimeStatusFailsClosedOnDatabaseError(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
truthRuntimeStatusHandler(fakeTruthRuntimeStatusReader{err: errors.New("database unavailable")}).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/v1/research/status", nil))
|
||||
if response.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status code = %d", response.Code)
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/healthz", health.Handler{Runtime: "work-graph-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
|
||||
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "work-graph-runtime", Probes: []health.Probe{{Name: "work-postgres", Check: func(ctx context.Context) error { return repository.Ping(ctx) }}}})
|
||||
mux.Handle("/v1/work-graphs/status", workGraphStatusHandler(repository))
|
||||
mux.HandleFunc("POST /v1/work-graphs/schedule", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request application.GraphScheduleRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&request); err != nil {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
)
|
||||
|
||||
type workGraphStatusReader interface {
|
||||
RuntimeStatus(context.Context) (postgres.WorkGraphRuntimeStatus, error)
|
||||
}
|
||||
|
||||
type workGraphStatusResponse struct {
|
||||
Runtime string `json:"runtime"`
|
||||
Status string `json:"status"`
|
||||
GraphsByState map[string]int64 `json:"graphs_by_state"`
|
||||
NodesByState map[string]int64 `json:"nodes_by_state"`
|
||||
QueueByState map[string]int64 `json:"queue_by_state"`
|
||||
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
|
||||
}
|
||||
|
||||
func workGraphStatusHandler(reader workGraphStatusReader) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || r.URL.Path != "/v1/work-graphs/status" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
status, err := reader.RuntimeStatus(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "work graph status unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(workGraphStatusResponse{
|
||||
Runtime: "work-graph-runtime",
|
||||
Status: "ready",
|
||||
GraphsByState: status.GraphsByState,
|
||||
NodesByState: status.NodesByState,
|
||||
QueueByState: status.QueueByState,
|
||||
ObservedAtUnixMillis: status.ObservedAtUnixMillis,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
)
|
||||
|
||||
type fakeWorkGraphStatusReader struct {
|
||||
status postgres.WorkGraphRuntimeStatus
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeWorkGraphStatusReader) RuntimeStatus(context.Context) (postgres.WorkGraphRuntimeStatus, error) {
|
||||
return f.status, f.err
|
||||
}
|
||||
|
||||
func TestWorkGraphStatusReportsDurableCounts(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
workGraphStatusHandler(fakeWorkGraphStatusReader{status: postgres.WorkGraphRuntimeStatus{
|
||||
GraphsByState: map[string]int64{"RUNNING": 2},
|
||||
NodesByState: map[string]int64{"LEASED": 3, "SUCCEEDED": 1},
|
||||
QueueByState: map[string]int64{"QUEUED": 3},
|
||||
ObservedAtUnixMillis: 1700000000000,
|
||||
}}).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/v1/work-graphs/status", nil))
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("status code = %d", response.Code)
|
||||
}
|
||||
body := response.Body.String()
|
||||
for _, expected := range []string{"\"status\":\"ready\"", "\"RUNNING\":2", "\"LEASED\":3", "\"QUEUED\":3", "1700000000000"} {
|
||||
if !contains(body, expected) {
|
||||
t.Fatalf("response missing %q: %s", expected, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkGraphStatusFailsClosedOnDatabaseError(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
workGraphStatusHandler(fakeWorkGraphStatusReader{err: errors.New("database unavailable")}).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/v1/work-graphs/status", nil))
|
||||
if response.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status code = %d", response.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(value, expected string) bool {
|
||||
for i := 0; i+len(expected) <= len(value); i++ {
|
||||
if value[i:i+len(expected)] == expected {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000245-agent-worker-recovery-status.json",
|
||||
"task_id": "DEV-000245",
|
||||
"task_file": "dev/tasks/DEV-000247-research-runtime-status.json",
|
||||
"task_id": "DEV-000247",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000246",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "355ba7db5621a11b2c4e0827d9301ab7fdb2d419",
|
||||
"risk_tier": "E",
|
||||
"intent": "Expose read-only durable Work Graph runtime counts for operational verification without changing graph scheduling, fan-out/fan-in, verifier, or replan semantics.",
|
||||
"requirement_ids": ["Multi-Agent Work Graph", "fan-out/fan-in observability", "fast operational verification"],
|
||||
"allowed_change_scope": {"modules": ["work"], "paths": ["adapters/postgres/work_repository.go", "cmd/work-graph-runtime/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000246-work-graph-runtime-status.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "LLM-local/**", "modules/work/secrets/**"]},
|
||||
"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": ["work-owned graph and dispatch queue tables", "existing runtime health conventions"], "may_change": ["internal read-only status response"]},
|
||||
"semantic_owner": "Work Graph runtime observability",
|
||||
"canonical_state": ["none; counts are derived projections"],
|
||||
"state_transitions": ["none"],
|
||||
"side_effects_and_privileges": ["read-only aggregate queries only"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["status fails closed with 503 when work database is unavailable"],
|
||||
"assumptions": ["status is consumed by internal diagnostics and can be queried from the production network"],
|
||||
"acceptance_scenarios": ["readyz remains database-backed", "status returns graph state and queue counts", "status does not mutate canonical state"],
|
||||
"required_tests": ["go test ./cmd/work-graph-runtime ./adapters/postgres", "go vet ./cmd/work-graph-runtime ./adapters/postgres", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "git diff --check", "independent", "CI", "production health smoke"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000247",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "e70be0103a03c31f7ecea851fd1aeabcb7abcc55",
|
||||
"risk_tier": "E",
|
||||
"intent": "Expose read-only Truth/Research evidence counts from durable truth bundles without changing epistemic evaluation or belief semantics.",
|
||||
"requirement_ids": ["Truth/Research runtime", "Observation-Claim-Belief evidence visibility", "fast operational verification"],
|
||||
"allowed_change_scope": {"modules": ["epistemics"], "paths": ["adapters/postgres/epistemics_repository.go", "cmd/research-runtime/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000247-research-runtime-status.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "LLM-local/**", "modules/work/secrets/**"]},
|
||||
"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": ["epistemics.truth_bundles", "existing research health conventions"], "may_change": ["internal read-only status response"]},
|
||||
"semantic_owner": "Epistemics Truth/Research runtime observability",
|
||||
"canonical_state": ["none; counts are derived projections"],
|
||||
"state_transitions": ["none"],
|
||||
"side_effects_and_privileges": ["read-only aggregate queries only"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["status fails closed with 503 when epistemics database is unavailable"],
|
||||
"assumptions": ["truth bundle migration is present in production"],
|
||||
"acceptance_scenarios": ["readyz remains database-backed", "status reports bundle, claim verification, belief and freshness counts", "status does not mutate epistemic state"],
|
||||
"required_tests": ["go test ./cmd/research-runtime ./adapters/postgres", "go vet ./cmd/research-runtime ./adapters/postgres", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "git diff --check", "independent", "CI", "production health smoke"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
Reference in New Issue
Block a user