feat(research): expose truth runtime status #224
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000246-work-graph-runtime-status.json",
|
||||
"task_id": "DEV-000246",
|
||||
"task_file": "dev/tasks/DEV-000247-research-runtime-status.json",
|
||||
"task_id": "DEV-000247",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -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