@plazmodium/odin 0.3.2-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +306 -0
- package/dist/adapters/archive/supabase.d.ts +19 -0
- package/dist/adapters/archive/supabase.d.ts.map +1 -0
- package/dist/adapters/archive/supabase.js +121 -0
- package/dist/adapters/archive/supabase.js.map +1 -0
- package/dist/adapters/archive/types.d.ts +26 -0
- package/dist/adapters/archive/types.d.ts.map +1 -0
- package/dist/adapters/archive/types.js +6 -0
- package/dist/adapters/archive/types.js.map +1 -0
- package/dist/adapters/formal-verification/tla-precheck.d.ts +22 -0
- package/dist/adapters/formal-verification/tla-precheck.d.ts.map +1 -0
- package/dist/adapters/formal-verification/tla-precheck.js +270 -0
- package/dist/adapters/formal-verification/tla-precheck.js.map +1 -0
- package/dist/adapters/formal-verification/types.d.ts +37 -0
- package/dist/adapters/formal-verification/types.d.ts.map +1 -0
- package/dist/adapters/formal-verification/types.js +6 -0
- package/dist/adapters/formal-verification/types.js.map +1 -0
- package/dist/adapters/review/semgrep.d.ts +12 -0
- package/dist/adapters/review/semgrep.d.ts.map +1 -0
- package/dist/adapters/review/semgrep.js +175 -0
- package/dist/adapters/review/semgrep.js.map +1 -0
- package/dist/adapters/review/types.d.ts +14 -0
- package/dist/adapters/review/types.d.ts.map +1 -0
- package/dist/adapters/review/types.js +6 -0
- package/dist/adapters/review/types.js.map +1 -0
- package/dist/adapters/skills/filesystem.d.ts +18 -0
- package/dist/adapters/skills/filesystem.d.ts.map +1 -0
- package/dist/adapters/skills/filesystem.js +398 -0
- package/dist/adapters/skills/filesystem.js.map +1 -0
- package/dist/adapters/skills/types.d.ts +19 -0
- package/dist/adapters/skills/types.d.ts.map +1 -0
- package/dist/adapters/skills/types.js +6 -0
- package/dist/adapters/skills/types.js.map +1 -0
- package/dist/adapters/sql-executor/direct-postgres.d.ts +15 -0
- package/dist/adapters/sql-executor/direct-postgres.d.ts.map +1 -0
- package/dist/adapters/sql-executor/direct-postgres.js +33 -0
- package/dist/adapters/sql-executor/direct-postgres.js.map +1 -0
- package/dist/adapters/sql-executor/supabase-management-api.d.ts +17 -0
- package/dist/adapters/sql-executor/supabase-management-api.d.ts.map +1 -0
- package/dist/adapters/sql-executor/supabase-management-api.js +40 -0
- package/dist/adapters/sql-executor/supabase-management-api.js.map +1 -0
- package/dist/adapters/sql-executor/types.d.ts +15 -0
- package/dist/adapters/sql-executor/types.d.ts.map +1 -0
- package/dist/adapters/sql-executor/types.js +6 -0
- package/dist/adapters/sql-executor/types.js.map +1 -0
- package/dist/adapters/workflow-state/in-memory.d.ts +69 -0
- package/dist/adapters/workflow-state/in-memory.d.ts.map +1 -0
- package/dist/adapters/workflow-state/in-memory.js +444 -0
- package/dist/adapters/workflow-state/in-memory.js.map +1 -0
- package/dist/adapters/workflow-state/supabase.d.ts +55 -0
- package/dist/adapters/workflow-state/supabase.d.ts.map +1 -0
- package/dist/adapters/workflow-state/supabase.js +823 -0
- package/dist/adapters/workflow-state/supabase.js.map +1 -0
- package/dist/adapters/workflow-state/types.d.ts +55 -0
- package/dist/adapters/workflow-state/types.d.ts.map +1 -0
- package/dist/adapters/workflow-state/types.js +6 -0
- package/dist/adapters/workflow-state/types.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +52 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +115 -0
- package/dist/config.js.map +1 -0
- package/dist/domain/actors.d.ts +10 -0
- package/dist/domain/actors.d.ts.map +1 -0
- package/dist/domain/actors.js +60 -0
- package/dist/domain/actors.js.map +1 -0
- package/dist/domain/development-evals.d.ts +9 -0
- package/dist/domain/development-evals.d.ts.map +1 -0
- package/dist/domain/development-evals.js +164 -0
- package/dist/domain/development-evals.js.map +1 -0
- package/dist/domain/matching.d.ts +8 -0
- package/dist/domain/matching.d.ts.map +1 -0
- package/dist/domain/matching.js +24 -0
- package/dist/domain/matching.js.map +1 -0
- package/dist/domain/phases.d.ts +10 -0
- package/dist/domain/phases.d.ts.map +1 -0
- package/dist/domain/phases.js +165 -0
- package/dist/domain/phases.js.map +1 -0
- package/dist/domain/quality-gates.d.ts +7 -0
- package/dist/domain/quality-gates.d.ts.map +1 -0
- package/dist/domain/quality-gates.js +8 -0
- package/dist/domain/quality-gates.js.map +1 -0
- package/dist/domain/resonance.d.ts +33 -0
- package/dist/domain/resonance.d.ts.map +1 -0
- package/dist/domain/resonance.js +100 -0
- package/dist/domain/resonance.js.map +1 -0
- package/dist/domain/tasks.d.ts +9 -0
- package/dist/domain/tasks.d.ts.map +1 -0
- package/dist/domain/tasks.js +57 -0
- package/dist/domain/tasks.js.map +1 -0
- package/dist/init.d.ts +7 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +387 -0
- package/dist/init.js.map +1 -0
- package/dist/schemas.d.ts +366 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +184 -0
- package/dist/schemas.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +243 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/apply-migrations.d.ts +21 -0
- package/dist/tools/apply-migrations.d.ts.map +1 -0
- package/dist/tools/apply-migrations.js +286 -0
- package/dist/tools/apply-migrations.js.map +1 -0
- package/dist/tools/archive-feature-release.d.ts +13 -0
- package/dist/tools/archive-feature-release.d.ts.map +1 -0
- package/dist/tools/archive-feature-release.js +182 -0
- package/dist/tools/archive-feature-release.js.map +1 -0
- package/dist/tools/capture-learning.d.ts +9 -0
- package/dist/tools/capture-learning.d.ts.map +1 -0
- package/dist/tools/capture-learning.js +53 -0
- package/dist/tools/capture-learning.js.map +1 -0
- package/dist/tools/explore-knowledge.d.ts +9 -0
- package/dist/tools/explore-knowledge.d.ts.map +1 -0
- package/dist/tools/explore-knowledge.js +142 -0
- package/dist/tools/explore-knowledge.js.map +1 -0
- package/dist/tools/get-claims-needing-review.d.ts +8 -0
- package/dist/tools/get-claims-needing-review.d.ts.map +1 -0
- package/dist/tools/get-claims-needing-review.js +21 -0
- package/dist/tools/get-claims-needing-review.js.map +1 -0
- package/dist/tools/get-development-eval-status.d.ts +8 -0
- package/dist/tools/get-development-eval-status.d.ts.map +1 -0
- package/dist/tools/get-development-eval-status.js +49 -0
- package/dist/tools/get-development-eval-status.js.map +1 -0
- package/dist/tools/get-feature-status.d.ts +8 -0
- package/dist/tools/get-feature-status.d.ts.map +1 -0
- package/dist/tools/get-feature-status.js +68 -0
- package/dist/tools/get-feature-status.js.map +1 -0
- package/dist/tools/get-next-phase.d.ts +8 -0
- package/dist/tools/get-next-phase.d.ts.map +1 -0
- package/dist/tools/get-next-phase.js +26 -0
- package/dist/tools/get-next-phase.js.map +1 -0
- package/dist/tools/prepare-phase-context.d.ts +9 -0
- package/dist/tools/prepare-phase-context.d.ts.map +1 -0
- package/dist/tools/prepare-phase-context.js +151 -0
- package/dist/tools/prepare-phase-context.js.map +1 -0
- package/dist/tools/record-commit.d.ts +8 -0
- package/dist/tools/record-commit.d.ts.map +1 -0
- package/dist/tools/record-commit.js +28 -0
- package/dist/tools/record-commit.js.map +1 -0
- package/dist/tools/record-eval-plan.d.ts +8 -0
- package/dist/tools/record-eval-plan.d.ts.map +1 -0
- package/dist/tools/record-eval-plan.js +40 -0
- package/dist/tools/record-eval-plan.js.map +1 -0
- package/dist/tools/record-eval-run.d.ts +8 -0
- package/dist/tools/record-eval-run.d.ts.map +1 -0
- package/dist/tools/record-eval-run.js +42 -0
- package/dist/tools/record-eval-run.js.map +1 -0
- package/dist/tools/record-merge.d.ts +8 -0
- package/dist/tools/record-merge.d.ts.map +1 -0
- package/dist/tools/record-merge.js +16 -0
- package/dist/tools/record-merge.js.map +1 -0
- package/dist/tools/record-phase-artifact.d.ts +8 -0
- package/dist/tools/record-phase-artifact.d.ts.map +1 -0
- package/dist/tools/record-phase-artifact.js +26 -0
- package/dist/tools/record-phase-artifact.js.map +1 -0
- package/dist/tools/record-phase-result.d.ts +9 -0
- package/dist/tools/record-phase-result.d.ts.map +1 -0
- package/dist/tools/record-phase-result.js +122 -0
- package/dist/tools/record-phase-result.js.map +1 -0
- package/dist/tools/record-pull-request.d.ts +8 -0
- package/dist/tools/record-pull-request.d.ts.map +1 -0
- package/dist/tools/record-pull-request.js +16 -0
- package/dist/tools/record-pull-request.js.map +1 -0
- package/dist/tools/record-quality-gate.d.ts +8 -0
- package/dist/tools/record-quality-gate.d.ts.map +1 -0
- package/dist/tools/record-quality-gate.js +26 -0
- package/dist/tools/record-quality-gate.js.map +1 -0
- package/dist/tools/record-watcher-review.d.ts +8 -0
- package/dist/tools/record-watcher-review.d.ts.map +1 -0
- package/dist/tools/record-watcher-review.js +18 -0
- package/dist/tools/record-watcher-review.js.map +1 -0
- package/dist/tools/run-policy-checks.d.ts +8 -0
- package/dist/tools/run-policy-checks.d.ts.map +1 -0
- package/dist/tools/run-policy-checks.js +38 -0
- package/dist/tools/run-policy-checks.js.map +1 -0
- package/dist/tools/run-review-checks.d.ts +9 -0
- package/dist/tools/run-review-checks.d.ts.map +1 -0
- package/dist/tools/run-review-checks.js +45 -0
- package/dist/tools/run-review-checks.js.map +1 -0
- package/dist/tools/start-feature.d.ts +8 -0
- package/dist/tools/start-feature.d.ts.map +1 -0
- package/dist/tools/start-feature.js +33 -0
- package/dist/tools/start-feature.js.map +1 -0
- package/dist/tools/submit-claim.d.ts +8 -0
- package/dist/tools/submit-claim.d.ts.map +1 -0
- package/dist/tools/submit-claim.js +45 -0
- package/dist/tools/submit-claim.js.map +1 -0
- package/dist/tools/verify-claims.d.ts +8 -0
- package/dist/tools/verify-claims.d.ts.map +1 -0
- package/dist/tools/verify-claims.js +39 -0
- package/dist/tools/verify-claims.js.map +1 -0
- package/dist/tools/verify-design.d.ts +8 -0
- package/dist/tools/verify-design.d.ts.map +1 -0
- package/dist/tools/verify-design.js +31 -0
- package/dist/tools/verify-design.js.map +1 -0
- package/dist/types.d.ts +333 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +52 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +24 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +50 -0
- package/dist/utils.js.map +1 -0
- package/migrations/001_schema.sql +795 -0
- package/migrations/002_functions.sql +2126 -0
- package/migrations/003_views.sql +599 -0
- package/migrations/004_seed.sql +106 -0
- package/migrations/005_odin_v2_schema.sql +217 -0
- package/migrations/006_odin_v2_functions.sql +671 -0
- package/migrations/007_odin_v2_phase_alignment.sql +554 -0
- package/migrations/008_related_learnings.sql +80 -0
- package/migrations/README.md +23 -0
- package/package.json +63 -0
|
@@ -0,0 +1,2126 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- Odin SDD Framework - Consolidated Functions
|
|
3
|
+
-- Version: 1.1.0
|
|
4
|
+
-- Created: 2026-02-16
|
|
5
|
+
-- Updated: 2026-02-16 (reconciled with live Supabase DB)
|
|
6
|
+
-- Description: All PostgreSQL functions for Odin. Run after 001_schema.sql.
|
|
7
|
+
-- Functions are organized by domain: Core Workflow, Agent Invocations,
|
|
8
|
+
-- Git Tracking, Learnings, EVALS, Batch Execution, Memories, Archives.
|
|
9
|
+
-- ============================================================================
|
|
10
|
+
|
|
11
|
+
-- ============================================================================
|
|
12
|
+
-- CORE WORKFLOW FUNCTIONS
|
|
13
|
+
-- ============================================================================
|
|
14
|
+
|
|
15
|
+
-- Create a new feature with git branch tracking
|
|
16
|
+
CREATE OR REPLACE FUNCTION create_feature(
|
|
17
|
+
p_id TEXT,
|
|
18
|
+
p_name TEXT,
|
|
19
|
+
p_complexity_level INTEGER,
|
|
20
|
+
p_severity severity DEFAULT 'ROUTINE',
|
|
21
|
+
p_epic_id TEXT DEFAULT NULL,
|
|
22
|
+
p_requirements_path TEXT DEFAULT NULL,
|
|
23
|
+
p_created_by TEXT DEFAULT 'system',
|
|
24
|
+
p_dev_initials TEXT DEFAULT NULL,
|
|
25
|
+
p_base_branch TEXT DEFAULT 'main',
|
|
26
|
+
p_author TEXT DEFAULT NULL
|
|
27
|
+
) RETURNS TABLE(
|
|
28
|
+
feature_id TEXT,
|
|
29
|
+
feature_name TEXT,
|
|
30
|
+
complexity INTEGER,
|
|
31
|
+
severity_level severity,
|
|
32
|
+
status feature_status,
|
|
33
|
+
branch_name TEXT,
|
|
34
|
+
base_branch TEXT,
|
|
35
|
+
author TEXT
|
|
36
|
+
) AS $$
|
|
37
|
+
DECLARE
|
|
38
|
+
v_branch_name TEXT;
|
|
39
|
+
v_feature features;
|
|
40
|
+
BEGIN
|
|
41
|
+
-- Generate branch name from dev_initials
|
|
42
|
+
IF p_dev_initials IS NOT NULL THEN
|
|
43
|
+
v_branch_name := p_dev_initials || '/feature/' || p_id;
|
|
44
|
+
ELSE
|
|
45
|
+
v_branch_name := 'feature/' || p_id;
|
|
46
|
+
END IF;
|
|
47
|
+
|
|
48
|
+
INSERT INTO features (id, name, complexity_level, severity, epic_id, requirements_path, branch_name, base_branch, dev_initials, author)
|
|
49
|
+
VALUES (p_id, p_name, p_complexity_level, p_severity, p_epic_id, p_requirements_path, v_branch_name, p_base_branch, p_dev_initials, p_author)
|
|
50
|
+
RETURNING * INTO v_feature;
|
|
51
|
+
|
|
52
|
+
-- Initial phase transition (0 -> 0 = created)
|
|
53
|
+
INSERT INTO phase_transitions (feature_id, from_phase, to_phase, transitioned_by, transition_type, notes)
|
|
54
|
+
VALUES (p_id, '0'::phase, '0'::phase, p_created_by, 'FORWARD', 'Feature created');
|
|
55
|
+
|
|
56
|
+
-- Audit log
|
|
57
|
+
INSERT INTO audit_log (feature_id, operation, agent_name, details)
|
|
58
|
+
VALUES (p_id, 'FEATURE_CREATED', p_created_by, jsonb_build_object(
|
|
59
|
+
'name', p_name,
|
|
60
|
+
'complexity_level', p_complexity_level,
|
|
61
|
+
'severity', p_severity::text,
|
|
62
|
+
'branch_name', v_branch_name,
|
|
63
|
+
'author', p_author
|
|
64
|
+
));
|
|
65
|
+
|
|
66
|
+
RETURN QUERY SELECT
|
|
67
|
+
v_feature.id,
|
|
68
|
+
v_feature.name,
|
|
69
|
+
v_feature.complexity_level,
|
|
70
|
+
v_feature.severity,
|
|
71
|
+
v_feature.status,
|
|
72
|
+
v_feature.branch_name,
|
|
73
|
+
v_feature.base_branch,
|
|
74
|
+
v_feature.author;
|
|
75
|
+
END;
|
|
76
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
77
|
+
|
|
78
|
+
COMMENT ON FUNCTION create_feature IS 'Create a new feature with git branch tracking';
|
|
79
|
+
|
|
80
|
+
-- Get comprehensive feature status (column names match dashboard TypeScript FeatureStatusResult)
|
|
81
|
+
CREATE OR REPLACE FUNCTION get_feature_status(p_feature_id TEXT)
|
|
82
|
+
RETURNS TABLE(
|
|
83
|
+
feature_id TEXT,
|
|
84
|
+
feature_name TEXT,
|
|
85
|
+
complexity_level INTEGER,
|
|
86
|
+
severity severity,
|
|
87
|
+
current_phase phase,
|
|
88
|
+
status feature_status,
|
|
89
|
+
assigned_agent TEXT,
|
|
90
|
+
total_duration_ms BIGINT,
|
|
91
|
+
phase_count BIGINT,
|
|
92
|
+
open_blockers_count BIGINT,
|
|
93
|
+
pending_gates_count BIGINT,
|
|
94
|
+
total_transitions BIGINT,
|
|
95
|
+
total_learnings BIGINT,
|
|
96
|
+
active_invocations BIGINT,
|
|
97
|
+
created_at TIMESTAMPTZ,
|
|
98
|
+
updated_at TIMESTAMPTZ,
|
|
99
|
+
branch_name TEXT,
|
|
100
|
+
base_branch TEXT,
|
|
101
|
+
dev_initials TEXT,
|
|
102
|
+
pr_url TEXT,
|
|
103
|
+
pr_number INTEGER,
|
|
104
|
+
merged_at TIMESTAMPTZ,
|
|
105
|
+
author TEXT
|
|
106
|
+
) AS $$
|
|
107
|
+
BEGIN
|
|
108
|
+
RETURN QUERY
|
|
109
|
+
SELECT
|
|
110
|
+
f.id,
|
|
111
|
+
f.name,
|
|
112
|
+
f.complexity_level,
|
|
113
|
+
f.severity,
|
|
114
|
+
f.current_phase,
|
|
115
|
+
f.status,
|
|
116
|
+
f.assigned_agent,
|
|
117
|
+
(SELECT coalesce(sum(ai.duration_ms), 0) FROM agent_invocations ai WHERE ai.feature_id = f.id),
|
|
118
|
+
(SELECT count(DISTINCT pt.to_phase) FROM phase_transitions pt WHERE pt.feature_id = f.id),
|
|
119
|
+
(SELECT count(*) FROM blockers b WHERE b.feature_id = f.id AND b.status = 'OPEN'),
|
|
120
|
+
(SELECT count(*) FROM quality_gates qg WHERE qg.feature_id = f.id AND qg.status = 'PENDING'),
|
|
121
|
+
(SELECT count(*) FROM phase_transitions pt WHERE pt.feature_id = f.id),
|
|
122
|
+
(SELECT count(*) FROM learnings l WHERE l.feature_id = f.id),
|
|
123
|
+
(SELECT count(*) FROM agent_invocations ai WHERE ai.feature_id = f.id AND ai.ended_at IS NULL),
|
|
124
|
+
f.created_at,
|
|
125
|
+
f.updated_at,
|
|
126
|
+
f.branch_name,
|
|
127
|
+
f.base_branch,
|
|
128
|
+
f.dev_initials,
|
|
129
|
+
f.pr_url,
|
|
130
|
+
f.pr_number,
|
|
131
|
+
f.merged_at,
|
|
132
|
+
f.author
|
|
133
|
+
FROM features f
|
|
134
|
+
WHERE f.id = p_feature_id;
|
|
135
|
+
END;
|
|
136
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
137
|
+
|
|
138
|
+
COMMENT ON FUNCTION get_feature_status IS 'Get comprehensive feature status including git tracking and metrics';
|
|
139
|
+
|
|
140
|
+
-- Transition to a new phase (with sequential enforcement)
|
|
141
|
+
CREATE OR REPLACE FUNCTION transition_phase(
|
|
142
|
+
p_feature_id TEXT,
|
|
143
|
+
p_to_phase phase,
|
|
144
|
+
p_transitioned_by TEXT,
|
|
145
|
+
p_notes TEXT DEFAULT NULL
|
|
146
|
+
) RETURNS phase_transitions AS $$
|
|
147
|
+
DECLARE
|
|
148
|
+
v_current_phase phase;
|
|
149
|
+
v_current_int INTEGER;
|
|
150
|
+
v_to_int INTEGER;
|
|
151
|
+
v_transition_type transition_type;
|
|
152
|
+
v_transition phase_transitions;
|
|
153
|
+
BEGIN
|
|
154
|
+
-- Get current phase
|
|
155
|
+
SELECT current_phase INTO v_current_phase FROM features WHERE id = p_feature_id;
|
|
156
|
+
IF NOT FOUND THEN
|
|
157
|
+
RAISE EXCEPTION 'Feature % not found', p_feature_id;
|
|
158
|
+
END IF;
|
|
159
|
+
|
|
160
|
+
-- Cast to integers for comparison
|
|
161
|
+
v_current_int := v_current_phase::TEXT::INTEGER;
|
|
162
|
+
v_to_int := p_to_phase::TEXT::INTEGER;
|
|
163
|
+
|
|
164
|
+
-- Determine transition type and enforce rules
|
|
165
|
+
IF v_to_int = v_current_int THEN
|
|
166
|
+
-- RESET: Re-running the same phase (allowed, recorded as FORWARD)
|
|
167
|
+
v_transition_type := 'FORWARD';
|
|
168
|
+
|
|
169
|
+
ELSIF v_to_int = v_current_int + 1 THEN
|
|
170
|
+
-- FORWARD: Sequential advance by exactly 1 phase (allowed)
|
|
171
|
+
v_transition_type := 'FORWARD';
|
|
172
|
+
|
|
173
|
+
ELSIF v_to_int > v_current_int + 1 THEN
|
|
174
|
+
-- SKIP: Attempting to jump more than 1 phase forward (REJECTED)
|
|
175
|
+
RAISE EXCEPTION 'Phase skip not allowed: cannot jump from phase % to phase %. '
|
|
176
|
+
'Phases must advance sequentially (one at a time). '
|
|
177
|
+
'Current: % (%), Target: % (%). '
|
|
178
|
+
'All 8 phases must be executed — complexity level affects depth within each phase, not which phases run.',
|
|
179
|
+
v_current_int, v_to_int,
|
|
180
|
+
v_current_int,
|
|
181
|
+
CASE v_current_int
|
|
182
|
+
WHEN 0 THEN 'Planning'
|
|
183
|
+
WHEN 1 THEN 'Discovery'
|
|
184
|
+
WHEN 2 THEN 'Architect'
|
|
185
|
+
WHEN 3 THEN 'Guardian'
|
|
186
|
+
WHEN 4 THEN 'Builder'
|
|
187
|
+
WHEN 5 THEN 'Integrator'
|
|
188
|
+
WHEN 6 THEN 'Documenter'
|
|
189
|
+
WHEN 7 THEN 'Release'
|
|
190
|
+
ELSE 'Complete'
|
|
191
|
+
END,
|
|
192
|
+
v_to_int,
|
|
193
|
+
CASE v_to_int
|
|
194
|
+
WHEN 0 THEN 'Planning'
|
|
195
|
+
WHEN 1 THEN 'Discovery'
|
|
196
|
+
WHEN 2 THEN 'Architect'
|
|
197
|
+
WHEN 3 THEN 'Guardian'
|
|
198
|
+
WHEN 4 THEN 'Builder'
|
|
199
|
+
WHEN 5 THEN 'Integrator'
|
|
200
|
+
WHEN 6 THEN 'Documenter'
|
|
201
|
+
WHEN 7 THEN 'Release'
|
|
202
|
+
ELSE 'Complete'
|
|
203
|
+
END;
|
|
204
|
+
|
|
205
|
+
ELSIF v_to_int < v_current_int THEN
|
|
206
|
+
-- BACKWARD: Going to an earlier phase for rework (allowed)
|
|
207
|
+
v_transition_type := 'BACKWARD';
|
|
208
|
+
END IF;
|
|
209
|
+
|
|
210
|
+
-- Update feature
|
|
211
|
+
UPDATE features
|
|
212
|
+
SET current_phase = p_to_phase, updated_at = now()
|
|
213
|
+
WHERE id = p_feature_id;
|
|
214
|
+
|
|
215
|
+
-- Record transition
|
|
216
|
+
INSERT INTO phase_transitions (feature_id, from_phase, to_phase, transitioned_by, transition_type, notes)
|
|
217
|
+
VALUES (p_feature_id, v_current_phase, p_to_phase, p_transitioned_by, v_transition_type, p_notes)
|
|
218
|
+
RETURNING * INTO v_transition;
|
|
219
|
+
|
|
220
|
+
-- Audit log
|
|
221
|
+
INSERT INTO audit_log (feature_id, operation, agent_name, details) VALUES (
|
|
222
|
+
p_feature_id, 'PHASE_TRANSITION', p_transitioned_by,
|
|
223
|
+
jsonb_build_object(
|
|
224
|
+
'from_phase', v_current_phase::TEXT,
|
|
225
|
+
'to_phase', p_to_phase::TEXT,
|
|
226
|
+
'transition_type', v_transition_type::TEXT
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
RETURN v_transition;
|
|
231
|
+
END;
|
|
232
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
233
|
+
|
|
234
|
+
COMMENT ON FUNCTION transition_phase IS 'Transition feature to a new phase with sequential enforcement. Forward transitions must advance exactly one phase at a time. Back transitions (rework) can go to any earlier phase.';
|
|
235
|
+
|
|
236
|
+
-- Complete a feature
|
|
237
|
+
CREATE OR REPLACE FUNCTION complete_feature(p_feature_id TEXT, p_completed_by TEXT)
|
|
238
|
+
RETURNS BOOLEAN AS $$
|
|
239
|
+
DECLARE
|
|
240
|
+
v_missing_pairs TEXT;
|
|
241
|
+
BEGIN
|
|
242
|
+
-- Agent invocation telemetry coverage guardrail (phases 1-7)
|
|
243
|
+
WITH expected AS (
|
|
244
|
+
SELECT *
|
|
245
|
+
FROM (VALUES
|
|
246
|
+
('1'::phase, 'discovery-agent'::text),
|
|
247
|
+
('2'::phase, 'architect-agent'::text),
|
|
248
|
+
('3'::phase, 'guardian-agent'::text),
|
|
249
|
+
('4'::phase, 'builder-agent'::text),
|
|
250
|
+
('5'::phase, 'integrator-agent'::text),
|
|
251
|
+
('6'::phase, 'documenter-agent'::text),
|
|
252
|
+
('7'::phase, 'release-agent'::text)
|
|
253
|
+
) AS t(phase, agent_name)
|
|
254
|
+
),
|
|
255
|
+
actual AS (
|
|
256
|
+
SELECT DISTINCT phase, agent_name
|
|
257
|
+
FROM agent_invocations
|
|
258
|
+
WHERE feature_id = p_feature_id
|
|
259
|
+
AND ended_at IS NOT NULL
|
|
260
|
+
AND duration_ms IS NOT NULL
|
|
261
|
+
),
|
|
262
|
+
missing AS (
|
|
263
|
+
SELECT e.phase, e.agent_name
|
|
264
|
+
FROM expected e
|
|
265
|
+
LEFT JOIN actual a
|
|
266
|
+
ON a.phase = e.phase
|
|
267
|
+
AND a.agent_name = e.agent_name
|
|
268
|
+
WHERE a.phase IS NULL
|
|
269
|
+
)
|
|
270
|
+
SELECT string_agg(
|
|
271
|
+
format('phase %s -> %s', m.phase::TEXT, m.agent_name),
|
|
272
|
+
', '
|
|
273
|
+
ORDER BY m.phase::TEXT, m.agent_name
|
|
274
|
+
)
|
|
275
|
+
INTO v_missing_pairs
|
|
276
|
+
FROM missing m;
|
|
277
|
+
|
|
278
|
+
IF v_missing_pairs IS NOT NULL THEN
|
|
279
|
+
-- Record gate failure (status REJECTED conveys FAIL semantics)
|
|
280
|
+
INSERT INTO quality_gates (
|
|
281
|
+
feature_id, gate_name, phase, status, approver, approval_notes, decision_log
|
|
282
|
+
) VALUES (
|
|
283
|
+
p_feature_id,
|
|
284
|
+
'agent_invocation_coverage',
|
|
285
|
+
'7',
|
|
286
|
+
'REJECTED',
|
|
287
|
+
p_completed_by,
|
|
288
|
+
'Missing completed agent invocation telemetry: ' || v_missing_pairs,
|
|
289
|
+
'Completion blocked by telemetry coverage guardrail'
|
|
290
|
+
)
|
|
291
|
+
ON CONFLICT (feature_id, gate_name, phase)
|
|
292
|
+
DO UPDATE SET
|
|
293
|
+
status = EXCLUDED.status,
|
|
294
|
+
approver = EXCLUDED.approver,
|
|
295
|
+
approved_at = now(),
|
|
296
|
+
approval_notes = EXCLUDED.approval_notes,
|
|
297
|
+
decision_log = EXCLUDED.decision_log;
|
|
298
|
+
|
|
299
|
+
-- Create or refresh blocker with missing phase/agent pairs
|
|
300
|
+
IF NOT EXISTS (
|
|
301
|
+
SELECT 1
|
|
302
|
+
FROM blockers
|
|
303
|
+
WHERE feature_id = p_feature_id
|
|
304
|
+
AND phase = '7'
|
|
305
|
+
AND blocker_type = 'VALIDATION_FAILED'
|
|
306
|
+
AND title = 'Agent invocation telemetry coverage failed'
|
|
307
|
+
AND status IN ('OPEN', 'IN_PROGRESS')
|
|
308
|
+
) THEN
|
|
309
|
+
INSERT INTO blockers (
|
|
310
|
+
feature_id, blocker_type, phase, status, severity, title, description, created_by
|
|
311
|
+
) VALUES (
|
|
312
|
+
p_feature_id,
|
|
313
|
+
'VALIDATION_FAILED',
|
|
314
|
+
'7',
|
|
315
|
+
'OPEN',
|
|
316
|
+
'HIGH',
|
|
317
|
+
'Agent invocation telemetry coverage failed',
|
|
318
|
+
'Missing completed agent invocation telemetry: ' || v_missing_pairs,
|
|
319
|
+
p_completed_by
|
|
320
|
+
);
|
|
321
|
+
ELSE
|
|
322
|
+
UPDATE blockers
|
|
323
|
+
SET status = 'OPEN',
|
|
324
|
+
severity = 'HIGH',
|
|
325
|
+
description = 'Missing completed agent invocation telemetry: ' || v_missing_pairs,
|
|
326
|
+
escalation_notes = 'Coverage guardrail re-checked and still failing'
|
|
327
|
+
WHERE feature_id = p_feature_id
|
|
328
|
+
AND phase = '7'
|
|
329
|
+
AND blocker_type = 'VALIDATION_FAILED'
|
|
330
|
+
AND title = 'Agent invocation telemetry coverage failed'
|
|
331
|
+
AND status IN ('OPEN', 'IN_PROGRESS');
|
|
332
|
+
END IF;
|
|
333
|
+
|
|
334
|
+
-- Reflect blocked state in feature status
|
|
335
|
+
UPDATE features
|
|
336
|
+
SET status = 'BLOCKED',
|
|
337
|
+
updated_at = now()
|
|
338
|
+
WHERE id = p_feature_id
|
|
339
|
+
AND status <> 'COMPLETED';
|
|
340
|
+
|
|
341
|
+
-- Audit log
|
|
342
|
+
INSERT INTO audit_log (feature_id, operation, agent_name, details) VALUES (
|
|
343
|
+
p_feature_id,
|
|
344
|
+
'AGENT_INVOCATION_COVERAGE_FAILED',
|
|
345
|
+
p_completed_by,
|
|
346
|
+
jsonb_build_object(
|
|
347
|
+
'missing_pairs', v_missing_pairs,
|
|
348
|
+
'checkpoint', 'complete_feature'
|
|
349
|
+
)
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
RAISE NOTICE 'Cannot complete feature % - missing telemetry coverage: %', p_feature_id, v_missing_pairs;
|
|
353
|
+
RETURN FALSE;
|
|
354
|
+
END IF;
|
|
355
|
+
|
|
356
|
+
-- Check for open blockers
|
|
357
|
+
IF EXISTS (SELECT 1 FROM blockers WHERE feature_id = p_feature_id AND status = 'OPEN') THEN
|
|
358
|
+
RAISE EXCEPTION 'Cannot complete feature % - has open blockers', p_feature_id;
|
|
359
|
+
END IF;
|
|
360
|
+
|
|
361
|
+
-- Update feature status AND current_phase to '8' (Complete)
|
|
362
|
+
UPDATE features
|
|
363
|
+
SET status = 'COMPLETED',
|
|
364
|
+
current_phase = '8',
|
|
365
|
+
completed_at = now(),
|
|
366
|
+
updated_at = now()
|
|
367
|
+
WHERE id = p_feature_id;
|
|
368
|
+
|
|
369
|
+
IF NOT FOUND THEN
|
|
370
|
+
RAISE EXCEPTION 'Feature % not found', p_feature_id;
|
|
371
|
+
END IF;
|
|
372
|
+
|
|
373
|
+
-- Record the phase transition to Complete
|
|
374
|
+
INSERT INTO phase_transitions (feature_id, from_phase, to_phase, transitioned_by, notes, transition_type)
|
|
375
|
+
VALUES (p_feature_id, '7', '8', p_completed_by, 'Feature completed', 'FORWARD');
|
|
376
|
+
|
|
377
|
+
-- Compute feature eval
|
|
378
|
+
PERFORM compute_feature_eval(p_feature_id);
|
|
379
|
+
|
|
380
|
+
-- Audit log
|
|
381
|
+
INSERT INTO audit_log (feature_id, operation, agent_name, details) VALUES (
|
|
382
|
+
p_feature_id, 'FEATURE_COMPLETED', p_completed_by,
|
|
383
|
+
jsonb_build_object('completed_at', now())
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
RETURN TRUE;
|
|
387
|
+
END;
|
|
388
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
389
|
+
|
|
390
|
+
COMMENT ON FUNCTION complete_feature IS 'Mark feature as completed, transition to phase 8, compute eval. Returns FALSE if telemetry coverage guardrail fails.';
|
|
391
|
+
|
|
392
|
+
-- Create a blocker (matches live DB: uses p_severity, returns INTEGER)
|
|
393
|
+
CREATE OR REPLACE FUNCTION create_blocker(
|
|
394
|
+
p_feature_id TEXT,
|
|
395
|
+
p_blocker_type blocker_type,
|
|
396
|
+
p_severity blocker_severity,
|
|
397
|
+
p_title TEXT,
|
|
398
|
+
p_description TEXT,
|
|
399
|
+
p_created_by TEXT
|
|
400
|
+
) RETURNS INTEGER AS $$
|
|
401
|
+
DECLARE
|
|
402
|
+
v_blocker_id INTEGER;
|
|
403
|
+
v_phase phase;
|
|
404
|
+
BEGIN
|
|
405
|
+
-- Get current phase
|
|
406
|
+
SELECT current_phase INTO v_phase FROM features WHERE id = p_feature_id;
|
|
407
|
+
|
|
408
|
+
IF NOT FOUND THEN
|
|
409
|
+
RAISE EXCEPTION 'Feature % not found', p_feature_id;
|
|
410
|
+
END IF;
|
|
411
|
+
|
|
412
|
+
-- Create blocker
|
|
413
|
+
INSERT INTO blockers (
|
|
414
|
+
feature_id, blocker_type, phase, severity, title, description, created_by, status
|
|
415
|
+
) VALUES (
|
|
416
|
+
p_feature_id, p_blocker_type, v_phase, p_severity, p_title, p_description, p_created_by, 'OPEN'
|
|
417
|
+
)
|
|
418
|
+
RETURNING id INTO v_blocker_id;
|
|
419
|
+
|
|
420
|
+
-- Update feature status to BLOCKED
|
|
421
|
+
UPDATE features SET status = 'BLOCKED', updated_at = now() WHERE id = p_feature_id;
|
|
422
|
+
|
|
423
|
+
-- Audit log
|
|
424
|
+
INSERT INTO audit_log (feature_id, operation, agent_name, details) VALUES (
|
|
425
|
+
p_feature_id, 'BLOCKER_CREATED', p_created_by,
|
|
426
|
+
jsonb_build_object(
|
|
427
|
+
'blocker_id', v_blocker_id,
|
|
428
|
+
'blocker_type', p_blocker_type,
|
|
429
|
+
'severity', p_severity,
|
|
430
|
+
'title', p_title
|
|
431
|
+
)
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
RETURN v_blocker_id;
|
|
435
|
+
END;
|
|
436
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
437
|
+
|
|
438
|
+
COMMENT ON FUNCTION create_blocker IS 'Create a blocker and set feature status to BLOCKED';
|
|
439
|
+
|
|
440
|
+
-- Resolve a blocker (matches live DB: takes INTEGER, returns BOOLEAN)
|
|
441
|
+
CREATE OR REPLACE FUNCTION resolve_blocker(
|
|
442
|
+
p_blocker_id INTEGER,
|
|
443
|
+
p_resolved_by TEXT,
|
|
444
|
+
p_resolution_notes TEXT DEFAULT NULL
|
|
445
|
+
) RETURNS BOOLEAN AS $$
|
|
446
|
+
DECLARE
|
|
447
|
+
v_feature_id TEXT;
|
|
448
|
+
v_remaining_blockers INTEGER;
|
|
449
|
+
BEGIN
|
|
450
|
+
-- Get feature_id from blocker
|
|
451
|
+
SELECT feature_id INTO v_feature_id FROM blockers WHERE id = p_blocker_id;
|
|
452
|
+
|
|
453
|
+
IF NOT FOUND THEN
|
|
454
|
+
RAISE EXCEPTION 'Blocker % not found', p_blocker_id;
|
|
455
|
+
END IF;
|
|
456
|
+
|
|
457
|
+
-- Resolve blocker
|
|
458
|
+
UPDATE blockers
|
|
459
|
+
SET status = 'RESOLVED',
|
|
460
|
+
resolved_by = p_resolved_by,
|
|
461
|
+
resolved_at = now(),
|
|
462
|
+
resolution_notes = p_resolution_notes
|
|
463
|
+
WHERE id = p_blocker_id;
|
|
464
|
+
|
|
465
|
+
-- Check if there are remaining open blockers
|
|
466
|
+
SELECT COUNT(*) INTO v_remaining_blockers
|
|
467
|
+
FROM blockers
|
|
468
|
+
WHERE feature_id = v_feature_id AND status = 'OPEN';
|
|
469
|
+
|
|
470
|
+
-- If no more blockers, set feature back to IN_PROGRESS
|
|
471
|
+
IF v_remaining_blockers = 0 THEN
|
|
472
|
+
UPDATE features SET status = 'IN_PROGRESS', updated_at = now() WHERE id = v_feature_id;
|
|
473
|
+
END IF;
|
|
474
|
+
|
|
475
|
+
-- Audit log
|
|
476
|
+
INSERT INTO audit_log (feature_id, operation, agent_name, details) VALUES (
|
|
477
|
+
v_feature_id, 'BLOCKER_RESOLVED', p_resolved_by,
|
|
478
|
+
jsonb_build_object(
|
|
479
|
+
'blocker_id', p_blocker_id,
|
|
480
|
+
'resolution_notes', p_resolution_notes
|
|
481
|
+
)
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
RETURN TRUE;
|
|
485
|
+
END;
|
|
486
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
487
|
+
|
|
488
|
+
COMMENT ON FUNCTION resolve_blocker IS 'Resolve blocker, return feature to IN_PROGRESS if no blockers remain';
|
|
489
|
+
|
|
490
|
+
-- Approve a quality gate (matches live DB: uses p_gate_name TEXT, returns INTEGER)
|
|
491
|
+
CREATE OR REPLACE FUNCTION approve_gate(
|
|
492
|
+
p_feature_id TEXT,
|
|
493
|
+
p_gate_name TEXT,
|
|
494
|
+
p_status gate_status,
|
|
495
|
+
p_approver TEXT,
|
|
496
|
+
p_approval_notes TEXT DEFAULT NULL,
|
|
497
|
+
p_decision_log TEXT DEFAULT NULL
|
|
498
|
+
) RETURNS INTEGER AS $$
|
|
499
|
+
DECLARE
|
|
500
|
+
v_gate_id INTEGER;
|
|
501
|
+
v_phase phase;
|
|
502
|
+
BEGIN
|
|
503
|
+
-- Get current phase
|
|
504
|
+
SELECT current_phase INTO v_phase FROM features WHERE id = p_feature_id;
|
|
505
|
+
|
|
506
|
+
IF NOT FOUND THEN
|
|
507
|
+
RAISE EXCEPTION 'Feature % not found', p_feature_id;
|
|
508
|
+
END IF;
|
|
509
|
+
|
|
510
|
+
-- Create gate record
|
|
511
|
+
INSERT INTO quality_gates (
|
|
512
|
+
feature_id, gate_name, phase, status, approver, approval_notes, decision_log
|
|
513
|
+
) VALUES (
|
|
514
|
+
p_feature_id, p_gate_name, v_phase, p_status, p_approver, p_approval_notes, p_decision_log
|
|
515
|
+
)
|
|
516
|
+
RETURNING id INTO v_gate_id;
|
|
517
|
+
|
|
518
|
+
-- Audit log
|
|
519
|
+
INSERT INTO audit_log (feature_id, operation, agent_name, details) VALUES (
|
|
520
|
+
p_feature_id, 'GATE_' || p_status::TEXT, p_approver,
|
|
521
|
+
jsonb_build_object(
|
|
522
|
+
'gate_id', v_gate_id,
|
|
523
|
+
'gate_name', p_gate_name,
|
|
524
|
+
'status', p_status,
|
|
525
|
+
'phase', v_phase
|
|
526
|
+
)
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
RETURN v_gate_id;
|
|
530
|
+
END;
|
|
531
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
532
|
+
|
|
533
|
+
COMMENT ON FUNCTION approve_gate IS 'Approve or reject a quality gate';
|
|
534
|
+
|
|
535
|
+
-- ============================================================================
|
|
536
|
+
-- AGENT INVOCATION FUNCTIONS (Duration tracking)
|
|
537
|
+
-- ============================================================================
|
|
538
|
+
|
|
539
|
+
-- Start an agent invocation (with optional skills tracking)
|
|
540
|
+
CREATE OR REPLACE FUNCTION start_agent_invocation(
|
|
541
|
+
p_feature_id TEXT,
|
|
542
|
+
p_phase phase,
|
|
543
|
+
p_agent_name TEXT,
|
|
544
|
+
p_operation TEXT DEFAULT NULL,
|
|
545
|
+
p_skills TEXT[] DEFAULT NULL
|
|
546
|
+
) RETURNS agent_invocations
|
|
547
|
+
LANGUAGE plpgsql
|
|
548
|
+
SET search_path = public
|
|
549
|
+
AS $$
|
|
550
|
+
DECLARE
|
|
551
|
+
v_invocation agent_invocations;
|
|
552
|
+
BEGIN
|
|
553
|
+
INSERT INTO agent_invocations (feature_id, phase, agent_name, operation, skills_used)
|
|
554
|
+
VALUES (p_feature_id, p_phase, p_agent_name, p_operation, p_skills)
|
|
555
|
+
RETURNING * INTO v_invocation;
|
|
556
|
+
|
|
557
|
+
-- Write SKILLS_LOADED audit entry when skills are provided
|
|
558
|
+
IF p_skills IS NOT NULL AND array_length(p_skills, 1) > 0 THEN
|
|
559
|
+
INSERT INTO audit_log (feature_id, operation, agent_name, details)
|
|
560
|
+
VALUES (
|
|
561
|
+
p_feature_id,
|
|
562
|
+
'SKILLS_LOADED',
|
|
563
|
+
p_agent_name,
|
|
564
|
+
jsonb_build_object(
|
|
565
|
+
'skills', to_jsonb(p_skills),
|
|
566
|
+
'phase', p_phase::text,
|
|
567
|
+
'operation', p_operation,
|
|
568
|
+
'invocation_id', v_invocation.id::text
|
|
569
|
+
)
|
|
570
|
+
);
|
|
571
|
+
END IF;
|
|
572
|
+
|
|
573
|
+
RETURN v_invocation;
|
|
574
|
+
END;
|
|
575
|
+
$$;
|
|
576
|
+
|
|
577
|
+
COMMENT ON FUNCTION start_agent_invocation IS 'Start tracking an agent invocation with optional skills';
|
|
578
|
+
|
|
579
|
+
-- End an agent invocation (computes duration)
|
|
580
|
+
CREATE OR REPLACE FUNCTION end_agent_invocation(
|
|
581
|
+
p_invocation_id UUID,
|
|
582
|
+
p_notes TEXT DEFAULT NULL
|
|
583
|
+
) RETURNS agent_invocations AS $$
|
|
584
|
+
DECLARE
|
|
585
|
+
v_invocation agent_invocations;
|
|
586
|
+
BEGIN
|
|
587
|
+
UPDATE agent_invocations
|
|
588
|
+
SET ended_at = now(),
|
|
589
|
+
duration_ms = EXTRACT(EPOCH FROM (now() - started_at))::INTEGER * 1000,
|
|
590
|
+
notes = COALESCE(p_notes, notes)
|
|
591
|
+
WHERE id = p_invocation_id AND ended_at IS NULL
|
|
592
|
+
RETURNING * INTO v_invocation;
|
|
593
|
+
|
|
594
|
+
IF NOT FOUND THEN
|
|
595
|
+
RAISE EXCEPTION 'Invocation not found or already ended: %', p_invocation_id;
|
|
596
|
+
END IF;
|
|
597
|
+
|
|
598
|
+
RETURN v_invocation;
|
|
599
|
+
END;
|
|
600
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
601
|
+
|
|
602
|
+
COMMENT ON FUNCTION end_agent_invocation IS 'End an agent invocation and compute duration_ms';
|
|
603
|
+
|
|
604
|
+
-- Get phase durations for a feature
|
|
605
|
+
CREATE OR REPLACE FUNCTION get_phase_durations(p_feature_id TEXT)
|
|
606
|
+
RETURNS TABLE (
|
|
607
|
+
phase phase,
|
|
608
|
+
phase_name TEXT,
|
|
609
|
+
started_at TIMESTAMPTZ,
|
|
610
|
+
ended_at TIMESTAMPTZ,
|
|
611
|
+
duration_minutes NUMERIC,
|
|
612
|
+
agent_invocation_count BIGINT,
|
|
613
|
+
total_agent_duration_ms BIGINT
|
|
614
|
+
) AS $$
|
|
615
|
+
BEGIN
|
|
616
|
+
RETURN QUERY
|
|
617
|
+
WITH phase_windows AS (
|
|
618
|
+
SELECT
|
|
619
|
+
pt.to_phase,
|
|
620
|
+
pt.transitioned_at AS phase_started,
|
|
621
|
+
LEAD(pt.transitioned_at) OVER (ORDER BY pt.transitioned_at) AS phase_ended
|
|
622
|
+
FROM phase_transitions pt
|
|
623
|
+
WHERE pt.feature_id = p_feature_id
|
|
624
|
+
ORDER BY pt.transitioned_at
|
|
625
|
+
)
|
|
626
|
+
SELECT
|
|
627
|
+
pw.to_phase,
|
|
628
|
+
CASE pw.to_phase
|
|
629
|
+
WHEN '0' THEN 'Planning'
|
|
630
|
+
WHEN '1' THEN 'Discovery'
|
|
631
|
+
WHEN '2' THEN 'Architect'
|
|
632
|
+
WHEN '3' THEN 'Guardian'
|
|
633
|
+
WHEN '4' THEN 'Builder'
|
|
634
|
+
WHEN '5' THEN 'Integrator'
|
|
635
|
+
WHEN '6' THEN 'Documenter'
|
|
636
|
+
WHEN '7' THEN 'Release'
|
|
637
|
+
WHEN '8' THEN 'Complete'
|
|
638
|
+
ELSE 'Unknown'
|
|
639
|
+
END,
|
|
640
|
+
pw.phase_started,
|
|
641
|
+
pw.phase_ended,
|
|
642
|
+
ROUND(EXTRACT(EPOCH FROM (COALESCE(pw.phase_ended, now()) - pw.phase_started)) / 60, 1),
|
|
643
|
+
COALESCE((
|
|
644
|
+
SELECT COUNT(*) FROM agent_invocations ai
|
|
645
|
+
WHERE ai.feature_id = p_feature_id AND ai.phase = pw.to_phase
|
|
646
|
+
), 0),
|
|
647
|
+
COALESCE((
|
|
648
|
+
SELECT SUM(ai.duration_ms) FROM agent_invocations ai
|
|
649
|
+
WHERE ai.feature_id = p_feature_id AND ai.phase = pw.to_phase AND ai.ended_at IS NOT NULL
|
|
650
|
+
), 0)
|
|
651
|
+
FROM phase_windows pw
|
|
652
|
+
ORDER BY pw.phase_started;
|
|
653
|
+
END;
|
|
654
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
655
|
+
|
|
656
|
+
COMMENT ON FUNCTION get_phase_durations IS 'Get duration breakdown by phase for a feature';
|
|
657
|
+
|
|
658
|
+
-- Get agent duration breakdown for a feature
|
|
659
|
+
CREATE OR REPLACE FUNCTION get_agent_durations(p_feature_id TEXT)
|
|
660
|
+
RETURNS TABLE (
|
|
661
|
+
agent_name TEXT,
|
|
662
|
+
phase phase,
|
|
663
|
+
invocation_count BIGINT,
|
|
664
|
+
total_duration_ms BIGINT,
|
|
665
|
+
avg_duration_ms BIGINT,
|
|
666
|
+
min_duration_ms INTEGER,
|
|
667
|
+
max_duration_ms INTEGER
|
|
668
|
+
) AS $$
|
|
669
|
+
BEGIN
|
|
670
|
+
RETURN QUERY
|
|
671
|
+
SELECT
|
|
672
|
+
ai.agent_name,
|
|
673
|
+
ai.phase,
|
|
674
|
+
COUNT(*),
|
|
675
|
+
COALESCE(SUM(ai.duration_ms), 0)::BIGINT,
|
|
676
|
+
COALESCE(AVG(ai.duration_ms), 0)::BIGINT,
|
|
677
|
+
MIN(ai.duration_ms),
|
|
678
|
+
MAX(ai.duration_ms)
|
|
679
|
+
FROM agent_invocations ai
|
|
680
|
+
WHERE ai.feature_id = p_feature_id AND ai.ended_at IS NOT NULL
|
|
681
|
+
GROUP BY ai.agent_name, ai.phase
|
|
682
|
+
ORDER BY ai.phase, ai.agent_name;
|
|
683
|
+
END;
|
|
684
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
685
|
+
|
|
686
|
+
COMMENT ON FUNCTION get_agent_durations IS 'Get agent duration statistics for a feature';
|
|
687
|
+
|
|
688
|
+
-- ============================================================================
|
|
689
|
+
-- GIT TRACKING FUNCTIONS
|
|
690
|
+
-- ============================================================================
|
|
691
|
+
|
|
692
|
+
-- Record a commit
|
|
693
|
+
CREATE OR REPLACE FUNCTION record_commit(
|
|
694
|
+
p_feature_id TEXT,
|
|
695
|
+
p_commit_hash TEXT,
|
|
696
|
+
p_phase phase,
|
|
697
|
+
p_message TEXT DEFAULT NULL,
|
|
698
|
+
p_files_changed INTEGER DEFAULT NULL,
|
|
699
|
+
p_insertions INTEGER DEFAULT NULL,
|
|
700
|
+
p_deletions INTEGER DEFAULT NULL,
|
|
701
|
+
p_committed_by TEXT DEFAULT 'agent'
|
|
702
|
+
) RETURNS feature_commits AS $$
|
|
703
|
+
DECLARE
|
|
704
|
+
v_commit feature_commits;
|
|
705
|
+
BEGIN
|
|
706
|
+
IF NOT EXISTS (SELECT 1 FROM features WHERE id = p_feature_id) THEN
|
|
707
|
+
RAISE EXCEPTION 'Feature % not found', p_feature_id;
|
|
708
|
+
END IF;
|
|
709
|
+
|
|
710
|
+
INSERT INTO feature_commits (
|
|
711
|
+
feature_id, commit_hash, phase, message,
|
|
712
|
+
files_changed, insertions, deletions, committed_by
|
|
713
|
+
) VALUES (
|
|
714
|
+
p_feature_id, p_commit_hash, p_phase, p_message,
|
|
715
|
+
p_files_changed, p_insertions, p_deletions, p_committed_by
|
|
716
|
+
)
|
|
717
|
+
RETURNING * INTO v_commit;
|
|
718
|
+
|
|
719
|
+
INSERT INTO audit_log (feature_id, operation, agent_name, details) VALUES (
|
|
720
|
+
p_feature_id, 'COMMIT_RECORDED', p_committed_by,
|
|
721
|
+
jsonb_build_object('commit_hash', p_commit_hash, 'phase', p_phase::TEXT, 'message', p_message)
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
RETURN v_commit;
|
|
725
|
+
END;
|
|
726
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
727
|
+
|
|
728
|
+
COMMENT ON FUNCTION record_commit IS 'Record a git commit for a feature';
|
|
729
|
+
|
|
730
|
+
-- Get commits for a feature
|
|
731
|
+
CREATE OR REPLACE FUNCTION get_feature_commits(p_feature_id TEXT)
|
|
732
|
+
RETURNS SETOF feature_commits AS $$
|
|
733
|
+
SELECT * FROM feature_commits
|
|
734
|
+
WHERE feature_id = p_feature_id
|
|
735
|
+
ORDER BY committed_at;
|
|
736
|
+
$$ LANGUAGE sql SET search_path = public;
|
|
737
|
+
|
|
738
|
+
COMMENT ON FUNCTION get_feature_commits IS 'Get all commits for a feature';
|
|
739
|
+
|
|
740
|
+
-- Record PR creation
|
|
741
|
+
CREATE OR REPLACE FUNCTION record_pr(
|
|
742
|
+
p_feature_id TEXT,
|
|
743
|
+
p_pr_url TEXT,
|
|
744
|
+
p_pr_number INTEGER
|
|
745
|
+
) RETURNS features AS $$
|
|
746
|
+
DECLARE
|
|
747
|
+
v_feature features;
|
|
748
|
+
BEGIN
|
|
749
|
+
UPDATE features
|
|
750
|
+
SET pr_url = p_pr_url, pr_number = p_pr_number, updated_at = now()
|
|
751
|
+
WHERE id = p_feature_id
|
|
752
|
+
RETURNING * INTO v_feature;
|
|
753
|
+
|
|
754
|
+
IF NOT FOUND THEN
|
|
755
|
+
RAISE EXCEPTION 'Feature % not found', p_feature_id;
|
|
756
|
+
END IF;
|
|
757
|
+
|
|
758
|
+
INSERT INTO audit_log (feature_id, operation, agent_name, details) VALUES (
|
|
759
|
+
p_feature_id, 'PR_CREATED', 'release-agent',
|
|
760
|
+
jsonb_build_object('pr_url', p_pr_url, 'pr_number', p_pr_number)
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
RETURN v_feature;
|
|
764
|
+
END;
|
|
765
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
766
|
+
|
|
767
|
+
COMMENT ON FUNCTION record_pr IS 'Record PR creation for a feature';
|
|
768
|
+
|
|
769
|
+
-- Record merge (called by human, NEVER by agent)
|
|
770
|
+
CREATE OR REPLACE FUNCTION record_merge(
|
|
771
|
+
p_feature_id TEXT,
|
|
772
|
+
p_merged_by TEXT DEFAULT 'human'
|
|
773
|
+
) RETURNS features AS $$
|
|
774
|
+
DECLARE
|
|
775
|
+
v_feature features;
|
|
776
|
+
BEGIN
|
|
777
|
+
UPDATE features
|
|
778
|
+
SET merged_at = now(), updated_at = now()
|
|
779
|
+
WHERE id = p_feature_id
|
|
780
|
+
RETURNING * INTO v_feature;
|
|
781
|
+
|
|
782
|
+
IF NOT FOUND THEN
|
|
783
|
+
RAISE EXCEPTION 'Feature % not found', p_feature_id;
|
|
784
|
+
END IF;
|
|
785
|
+
|
|
786
|
+
INSERT INTO audit_log (feature_id, operation, agent_name, details) VALUES (
|
|
787
|
+
p_feature_id, 'PR_MERGED', p_merged_by,
|
|
788
|
+
jsonb_build_object('merged_at', now()::TEXT, 'pr_url', v_feature.pr_url, 'pr_number', v_feature.pr_number)
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
RETURN v_feature;
|
|
792
|
+
END;
|
|
793
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
794
|
+
|
|
795
|
+
COMMENT ON FUNCTION record_merge IS 'Record PR merge (called by human, NEVER by agent)';
|
|
796
|
+
|
|
797
|
+
-- ============================================================================
|
|
798
|
+
-- PHASE OUTPUT FUNCTIONS
|
|
799
|
+
-- ============================================================================
|
|
800
|
+
|
|
801
|
+
-- Record phase output (UPSERT)
|
|
802
|
+
CREATE OR REPLACE FUNCTION record_phase_output(
|
|
803
|
+
p_feature_id TEXT,
|
|
804
|
+
p_phase phase,
|
|
805
|
+
p_output_type TEXT,
|
|
806
|
+
p_content JSONB,
|
|
807
|
+
p_created_by TEXT DEFAULT 'agent'
|
|
808
|
+
) RETURNS phase_outputs
|
|
809
|
+
LANGUAGE plpgsql
|
|
810
|
+
SET search_path = public
|
|
811
|
+
AS $$
|
|
812
|
+
DECLARE
|
|
813
|
+
v_output phase_outputs;
|
|
814
|
+
BEGIN
|
|
815
|
+
INSERT INTO phase_outputs (feature_id, phase, output_type, content, created_by)
|
|
816
|
+
VALUES (p_feature_id, p_phase, p_output_type, p_content, p_created_by)
|
|
817
|
+
ON CONFLICT (feature_id, phase, output_type)
|
|
818
|
+
DO UPDATE SET
|
|
819
|
+
content = EXCLUDED.content,
|
|
820
|
+
created_by = EXCLUDED.created_by,
|
|
821
|
+
created_at = now()
|
|
822
|
+
RETURNING * INTO v_output;
|
|
823
|
+
|
|
824
|
+
RETURN v_output;
|
|
825
|
+
END;
|
|
826
|
+
$$;
|
|
827
|
+
|
|
828
|
+
COMMENT ON FUNCTION record_phase_output IS 'Record or update a phase output (requirements, perspectives, tasks)';
|
|
829
|
+
|
|
830
|
+
-- Get phase outputs for a feature
|
|
831
|
+
CREATE OR REPLACE FUNCTION get_phase_outputs(p_feature_id TEXT)
|
|
832
|
+
RETURNS SETOF phase_outputs
|
|
833
|
+
LANGUAGE sql
|
|
834
|
+
SET search_path = public
|
|
835
|
+
AS $$
|
|
836
|
+
SELECT * FROM phase_outputs
|
|
837
|
+
WHERE feature_id = p_feature_id
|
|
838
|
+
ORDER BY phase, output_type;
|
|
839
|
+
$$;
|
|
840
|
+
|
|
841
|
+
COMMENT ON FUNCTION get_phase_outputs IS 'Get all phase outputs for a feature';
|
|
842
|
+
|
|
843
|
+
-- ============================================================================
|
|
844
|
+
-- LEARNING FUNCTIONS
|
|
845
|
+
-- ============================================================================
|
|
846
|
+
|
|
847
|
+
-- Evolve a learning (create successor)
|
|
848
|
+
CREATE OR REPLACE FUNCTION evolve_learning(
|
|
849
|
+
p_predecessor_id UUID,
|
|
850
|
+
p_title VARCHAR(255),
|
|
851
|
+
p_content TEXT,
|
|
852
|
+
p_delta_summary TEXT,
|
|
853
|
+
p_created_by TEXT
|
|
854
|
+
) RETURNS learnings AS $$
|
|
855
|
+
DECLARE
|
|
856
|
+
v_predecessor learnings;
|
|
857
|
+
v_new_learning learnings;
|
|
858
|
+
v_new_iteration INTEGER;
|
|
859
|
+
BEGIN
|
|
860
|
+
SELECT * INTO v_predecessor FROM learnings WHERE id = p_predecessor_id;
|
|
861
|
+
IF NOT FOUND THEN
|
|
862
|
+
RAISE EXCEPTION 'Predecessor learning % not found', p_predecessor_id;
|
|
863
|
+
END IF;
|
|
864
|
+
|
|
865
|
+
v_new_iteration := v_predecessor.iteration_number + 1;
|
|
866
|
+
|
|
867
|
+
INSERT INTO learnings (
|
|
868
|
+
predecessor_id, iteration_number, feature_id, task_id, category, title, content,
|
|
869
|
+
delta_summary, confidence_score, importance, tags, phase, agent, created_by
|
|
870
|
+
) VALUES (
|
|
871
|
+
p_predecessor_id, v_new_iteration, v_predecessor.feature_id, v_predecessor.task_id,
|
|
872
|
+
v_predecessor.category, p_title, p_content, p_delta_summary,
|
|
873
|
+
v_predecessor.confidence_score, v_predecessor.importance, v_predecessor.tags,
|
|
874
|
+
v_predecessor.phase, v_predecessor.agent, p_created_by
|
|
875
|
+
)
|
|
876
|
+
RETURNING * INTO v_new_learning;
|
|
877
|
+
|
|
878
|
+
-- Mark predecessor as superseded
|
|
879
|
+
UPDATE learnings
|
|
880
|
+
SET is_superseded = true, superseded_by = v_new_learning.id, updated_at = now()
|
|
881
|
+
WHERE id = p_predecessor_id;
|
|
882
|
+
|
|
883
|
+
RETURN v_new_learning;
|
|
884
|
+
END;
|
|
885
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
886
|
+
|
|
887
|
+
COMMENT ON FUNCTION evolve_learning IS 'Create a new version of a learning (L_n -> L_{n+1})';
|
|
888
|
+
|
|
889
|
+
-- Validate a learning (increase confidence)
|
|
890
|
+
CREATE OR REPLACE FUNCTION validate_learning(
|
|
891
|
+
p_learning_id UUID,
|
|
892
|
+
p_validated_by TEXT
|
|
893
|
+
) RETURNS learnings AS $$
|
|
894
|
+
DECLARE
|
|
895
|
+
v_learning learnings;
|
|
896
|
+
BEGIN
|
|
897
|
+
UPDATE learnings
|
|
898
|
+
SET confidence_score = LEAST(1.00, confidence_score + 0.15),
|
|
899
|
+
validation_count = validation_count + 1,
|
|
900
|
+
last_validated_at = now(),
|
|
901
|
+
validated_by = array_append(COALESCE(validated_by, ARRAY[]::TEXT[]), p_validated_by),
|
|
902
|
+
updated_at = now()
|
|
903
|
+
WHERE id = p_learning_id AND NOT is_superseded
|
|
904
|
+
RETURNING * INTO v_learning;
|
|
905
|
+
|
|
906
|
+
IF NOT FOUND THEN
|
|
907
|
+
RAISE EXCEPTION 'Learning % not found or is superseded', p_learning_id;
|
|
908
|
+
END IF;
|
|
909
|
+
|
|
910
|
+
RETURN v_learning;
|
|
911
|
+
END;
|
|
912
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
913
|
+
|
|
914
|
+
COMMENT ON FUNCTION validate_learning IS 'Validate a learning, increasing confidence by 0.15';
|
|
915
|
+
|
|
916
|
+
-- Record a learning reference (increase confidence by 0.10)
|
|
917
|
+
CREATE OR REPLACE FUNCTION record_learning_reference(
|
|
918
|
+
p_learning_id UUID,
|
|
919
|
+
p_referencing_feature_id TEXT
|
|
920
|
+
) RETURNS NUMERIC AS $$
|
|
921
|
+
DECLARE
|
|
922
|
+
v_new_confidence NUMERIC(3,2);
|
|
923
|
+
BEGIN
|
|
924
|
+
UPDATE learnings
|
|
925
|
+
SET confidence_score = LEAST(confidence_score + 0.10, 1.00)
|
|
926
|
+
WHERE id = p_learning_id AND NOT is_superseded
|
|
927
|
+
RETURNING confidence_score INTO v_new_confidence;
|
|
928
|
+
|
|
929
|
+
IF NOT FOUND THEN
|
|
930
|
+
RAISE EXCEPTION 'Learning not found or superseded';
|
|
931
|
+
END IF;
|
|
932
|
+
|
|
933
|
+
RETURN v_new_confidence;
|
|
934
|
+
END;
|
|
935
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
936
|
+
|
|
937
|
+
COMMENT ON FUNCTION record_learning_reference IS 'Record a reference to a learning, increasing confidence by 0.10';
|
|
938
|
+
|
|
939
|
+
-- Get learnings for a feature
|
|
940
|
+
CREATE OR REPLACE FUNCTION get_learnings_for_feature(p_feature_id TEXT)
|
|
941
|
+
RETURNS TABLE(
|
|
942
|
+
id UUID,
|
|
943
|
+
iteration_number INTEGER,
|
|
944
|
+
category learning_category,
|
|
945
|
+
title VARCHAR(255),
|
|
946
|
+
content TEXT,
|
|
947
|
+
confidence_score NUMERIC,
|
|
948
|
+
is_superseded BOOLEAN,
|
|
949
|
+
created_at TIMESTAMPTZ
|
|
950
|
+
) AS $$
|
|
951
|
+
BEGIN
|
|
952
|
+
RETURN QUERY
|
|
953
|
+
SELECT
|
|
954
|
+
l.id, l.iteration_number, l.category, l.title, l.content,
|
|
955
|
+
l.confidence_score, l.is_superseded, l.created_at
|
|
956
|
+
FROM learnings l
|
|
957
|
+
WHERE l.feature_id = p_feature_id
|
|
958
|
+
ORDER BY l.created_at DESC;
|
|
959
|
+
END;
|
|
960
|
+
$$ LANGUAGE plpgsql STABLE SET search_path = public;
|
|
961
|
+
|
|
962
|
+
COMMENT ON FUNCTION get_learnings_for_feature IS 'Get all learnings for a feature';
|
|
963
|
+
|
|
964
|
+
-- Get learning evolution chain
|
|
965
|
+
CREATE OR REPLACE FUNCTION get_learning_chain(p_learning_id UUID)
|
|
966
|
+
RETURNS TABLE(
|
|
967
|
+
id UUID,
|
|
968
|
+
predecessor_id UUID,
|
|
969
|
+
iteration_number INTEGER,
|
|
970
|
+
title VARCHAR(255),
|
|
971
|
+
confidence_score NUMERIC,
|
|
972
|
+
is_superseded BOOLEAN,
|
|
973
|
+
created_at TIMESTAMPTZ
|
|
974
|
+
) AS $$
|
|
975
|
+
BEGIN
|
|
976
|
+
RETURN QUERY
|
|
977
|
+
WITH RECURSIVE chain AS (
|
|
978
|
+
-- Find root
|
|
979
|
+
SELECT l.id, l.predecessor_id, l.iteration_number, l.title, l.confidence_score, l.is_superseded, l.created_at
|
|
980
|
+
FROM learnings l
|
|
981
|
+
WHERE l.id = p_learning_id
|
|
982
|
+
|
|
983
|
+
UNION ALL
|
|
984
|
+
|
|
985
|
+
-- Walk up to predecessors
|
|
986
|
+
SELECT l.id, l.predecessor_id, l.iteration_number, l.title, l.confidence_score, l.is_superseded, l.created_at
|
|
987
|
+
FROM learnings l
|
|
988
|
+
JOIN chain c ON l.id = c.predecessor_id
|
|
989
|
+
|
|
990
|
+
UNION ALL
|
|
991
|
+
|
|
992
|
+
-- Walk down to successors
|
|
993
|
+
SELECT l.id, l.predecessor_id, l.iteration_number, l.title, l.confidence_score, l.is_superseded, l.created_at
|
|
994
|
+
FROM learnings l
|
|
995
|
+
JOIN chain c ON l.predecessor_id = c.id
|
|
996
|
+
)
|
|
997
|
+
SELECT DISTINCT * FROM chain ORDER BY iteration_number;
|
|
998
|
+
END;
|
|
999
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1000
|
+
|
|
1001
|
+
COMMENT ON FUNCTION get_learning_chain IS 'Get the full evolution chain for a learning';
|
|
1002
|
+
|
|
1003
|
+
-- Detect learning conflicts
|
|
1004
|
+
CREATE OR REPLACE FUNCTION detect_learning_conflicts(p_learning_id UUID)
|
|
1005
|
+
RETURNS TABLE(
|
|
1006
|
+
potential_conflict_id UUID,
|
|
1007
|
+
conflict_type learning_conflict_type,
|
|
1008
|
+
similarity_reason TEXT
|
|
1009
|
+
) AS $$
|
|
1010
|
+
DECLARE
|
|
1011
|
+
v_learning RECORD;
|
|
1012
|
+
BEGIN
|
|
1013
|
+
SELECT * INTO v_learning FROM learnings WHERE id = p_learning_id;
|
|
1014
|
+
|
|
1015
|
+
IF NOT FOUND THEN
|
|
1016
|
+
RETURN;
|
|
1017
|
+
END IF;
|
|
1018
|
+
|
|
1019
|
+
RETURN QUERY
|
|
1020
|
+
SELECT
|
|
1021
|
+
l.id,
|
|
1022
|
+
'SCOPE_OVERLAP'::learning_conflict_type,
|
|
1023
|
+
'Same category with overlapping tags or similar title'
|
|
1024
|
+
FROM learnings l
|
|
1025
|
+
WHERE l.id != p_learning_id
|
|
1026
|
+
AND NOT l.is_superseded
|
|
1027
|
+
AND l.category = v_learning.category
|
|
1028
|
+
AND (
|
|
1029
|
+
-- Same tags overlap
|
|
1030
|
+
l.tags && v_learning.tags
|
|
1031
|
+
OR
|
|
1032
|
+
-- Similar title (simple word match)
|
|
1033
|
+
ts_rank(
|
|
1034
|
+
to_tsvector('english', l.title),
|
|
1035
|
+
plainto_tsquery('english', v_learning.title)
|
|
1036
|
+
) > 0.1
|
|
1037
|
+
)
|
|
1038
|
+
-- Not already in conflict
|
|
1039
|
+
AND NOT EXISTS (
|
|
1040
|
+
SELECT 1 FROM learning_conflicts c
|
|
1041
|
+
WHERE (c.learning_a_id = p_learning_id AND c.learning_b_id = l.id)
|
|
1042
|
+
OR (c.learning_a_id = l.id AND c.learning_b_id = p_learning_id)
|
|
1043
|
+
);
|
|
1044
|
+
END;
|
|
1045
|
+
$$ LANGUAGE plpgsql STABLE SET search_path = public;
|
|
1046
|
+
|
|
1047
|
+
COMMENT ON FUNCTION detect_learning_conflicts IS 'Detect potential conflicts with other learnings';
|
|
1048
|
+
|
|
1049
|
+
-- ============================================================================
|
|
1050
|
+
-- LEARNING PROPAGATION FUNCTIONS
|
|
1051
|
+
-- ============================================================================
|
|
1052
|
+
|
|
1053
|
+
-- Check propagation eligibility
|
|
1054
|
+
CREATE OR REPLACE FUNCTION check_propagation_eligibility(p_learning_id UUID)
|
|
1055
|
+
RETURNS TABLE(
|
|
1056
|
+
eligible BOOLEAN,
|
|
1057
|
+
reason TEXT,
|
|
1058
|
+
learning_title TEXT,
|
|
1059
|
+
confidence_score NUMERIC
|
|
1060
|
+
) AS $$
|
|
1061
|
+
DECLARE
|
|
1062
|
+
v_learning learnings%ROWTYPE;
|
|
1063
|
+
BEGIN
|
|
1064
|
+
-- Get the learning
|
|
1065
|
+
SELECT * INTO v_learning FROM learnings WHERE id = p_learning_id;
|
|
1066
|
+
|
|
1067
|
+
IF NOT FOUND THEN
|
|
1068
|
+
RETURN QUERY SELECT false, 'Learning not found', NULL::text, NULL::numeric;
|
|
1069
|
+
RETURN;
|
|
1070
|
+
END IF;
|
|
1071
|
+
|
|
1072
|
+
-- Check if superseded
|
|
1073
|
+
IF v_learning.is_superseded THEN
|
|
1074
|
+
RETURN QUERY SELECT false, 'Learning has been superseded by a newer version',
|
|
1075
|
+
v_learning.title::text, v_learning.confidence_score;
|
|
1076
|
+
RETURN;
|
|
1077
|
+
END IF;
|
|
1078
|
+
|
|
1079
|
+
-- Check confidence threshold
|
|
1080
|
+
IF v_learning.confidence_score < 0.80 THEN
|
|
1081
|
+
RETURN QUERY SELECT false,
|
|
1082
|
+
format('Confidence score (%.2f) below threshold (0.80)', v_learning.confidence_score),
|
|
1083
|
+
v_learning.title::text, v_learning.confidence_score;
|
|
1084
|
+
RETURN;
|
|
1085
|
+
END IF;
|
|
1086
|
+
|
|
1087
|
+
-- Check if already propagated
|
|
1088
|
+
IF array_length(v_learning.propagated_to, 1) IS NOT NULL THEN
|
|
1089
|
+
RETURN QUERY SELECT false,
|
|
1090
|
+
format('Already propagated to: %s', array_to_string(v_learning.propagated_to, ', ')),
|
|
1091
|
+
v_learning.title::text, v_learning.confidence_score;
|
|
1092
|
+
RETURN;
|
|
1093
|
+
END IF;
|
|
1094
|
+
|
|
1095
|
+
-- Check for open conflicts
|
|
1096
|
+
IF EXISTS (
|
|
1097
|
+
SELECT 1 FROM learning_conflicts
|
|
1098
|
+
WHERE (learning_a_id = p_learning_id OR learning_b_id = p_learning_id)
|
|
1099
|
+
AND status IN ('OPEN', 'INVESTIGATING')
|
|
1100
|
+
) THEN
|
|
1101
|
+
RETURN QUERY SELECT false, 'Learning has unresolved conflicts',
|
|
1102
|
+
v_learning.title::text, v_learning.confidence_score;
|
|
1103
|
+
RETURN;
|
|
1104
|
+
END IF;
|
|
1105
|
+
|
|
1106
|
+
-- Eligible!
|
|
1107
|
+
RETURN QUERY SELECT true, 'Eligible for propagation',
|
|
1108
|
+
v_learning.title::text, v_learning.confidence_score;
|
|
1109
|
+
END;
|
|
1110
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1111
|
+
|
|
1112
|
+
COMMENT ON FUNCTION check_propagation_eligibility IS 'Check if a learning is eligible for propagation';
|
|
1113
|
+
|
|
1114
|
+
-- Format learning for propagation
|
|
1115
|
+
CREATE OR REPLACE FUNCTION format_learning_for_propagation(p_learning_id UUID)
|
|
1116
|
+
RETURNS TABLE(
|
|
1117
|
+
formatted_markdown TEXT,
|
|
1118
|
+
title TEXT,
|
|
1119
|
+
category learning_category,
|
|
1120
|
+
confidence NUMERIC,
|
|
1121
|
+
error_message TEXT
|
|
1122
|
+
) AS $$
|
|
1123
|
+
DECLARE
|
|
1124
|
+
v_learning RECORD;
|
|
1125
|
+
v_feature_name text;
|
|
1126
|
+
v_validators text;
|
|
1127
|
+
v_formatted text;
|
|
1128
|
+
v_eligibility RECORD;
|
|
1129
|
+
v_confidence_str text;
|
|
1130
|
+
BEGIN
|
|
1131
|
+
-- Check eligibility first
|
|
1132
|
+
SELECT * INTO v_eligibility FROM check_propagation_eligibility(p_learning_id);
|
|
1133
|
+
|
|
1134
|
+
IF NOT v_eligibility.eligible THEN
|
|
1135
|
+
RETURN QUERY SELECT NULL::text, v_eligibility.learning_title, NULL::learning_category,
|
|
1136
|
+
v_eligibility.confidence_score, v_eligibility.reason;
|
|
1137
|
+
RETURN;
|
|
1138
|
+
END IF;
|
|
1139
|
+
|
|
1140
|
+
-- Get full learning details
|
|
1141
|
+
SELECT l.*, f.name as feature_name
|
|
1142
|
+
INTO v_learning
|
|
1143
|
+
FROM learnings l
|
|
1144
|
+
LEFT JOIN features f ON l.feature_id = f.id
|
|
1145
|
+
WHERE l.id = p_learning_id;
|
|
1146
|
+
|
|
1147
|
+
-- Build validators list
|
|
1148
|
+
IF array_length(v_learning.validated_by, 1) > 0 THEN
|
|
1149
|
+
v_validators := array_to_string(v_learning.validated_by, ', ');
|
|
1150
|
+
ELSE
|
|
1151
|
+
v_validators := 'none';
|
|
1152
|
+
END IF;
|
|
1153
|
+
|
|
1154
|
+
-- Format confidence as string (avoid format() issues)
|
|
1155
|
+
v_confidence_str := to_char(v_learning.confidence_score, '0.00');
|
|
1156
|
+
|
|
1157
|
+
-- Format the markdown using concatenation instead of format() for complex strings
|
|
1158
|
+
v_formatted := '### ' || v_learning.category::text || ': ' || v_learning.title || ' (' ||
|
|
1159
|
+
to_char(v_learning.created_at, 'YYYY-MM-DD') || ')' || E'\\n\\n' ||
|
|
1160
|
+
COALESCE(v_learning.propagation_summary,
|
|
1161
|
+
CASE
|
|
1162
|
+
WHEN length(v_learning.content) > 300
|
|
1163
|
+
THEN left(v_learning.content, 300) || '...'
|
|
1164
|
+
ELSE v_learning.content
|
|
1165
|
+
END) || E'\\n\\n' ||
|
|
1166
|
+
'**Confidence**: ' || v_confidence_str ||
|
|
1167
|
+
' | **Validated by**: ' || v_validators ||
|
|
1168
|
+
' | **Source**: ' || COALESCE(v_learning.feature_id, 'general');
|
|
1169
|
+
|
|
1170
|
+
RETURN QUERY SELECT v_formatted, v_learning.title::text, v_learning.category,
|
|
1171
|
+
v_learning.confidence_score, NULL::text;
|
|
1172
|
+
END;
|
|
1173
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1174
|
+
|
|
1175
|
+
COMMENT ON FUNCTION format_learning_for_propagation IS 'Format a learning as markdown for propagation';
|
|
1176
|
+
|
|
1177
|
+
-- Get propagation candidate (full details + eligibility + formatted markdown)
|
|
1178
|
+
CREATE OR REPLACE FUNCTION get_propagation_candidate(p_learning_id UUID)
|
|
1179
|
+
RETURNS TABLE(
|
|
1180
|
+
id UUID,
|
|
1181
|
+
title TEXT,
|
|
1182
|
+
category learning_category,
|
|
1183
|
+
content TEXT,
|
|
1184
|
+
propagation_summary TEXT,
|
|
1185
|
+
confidence_score NUMERIC,
|
|
1186
|
+
validation_count INTEGER,
|
|
1187
|
+
validated_by TEXT[],
|
|
1188
|
+
importance learning_importance,
|
|
1189
|
+
feature_id TEXT,
|
|
1190
|
+
feature_name TEXT,
|
|
1191
|
+
tags TEXT[],
|
|
1192
|
+
created_at TIMESTAMPTZ,
|
|
1193
|
+
created_by TEXT,
|
|
1194
|
+
formatted_markdown TEXT,
|
|
1195
|
+
eligible BOOLEAN,
|
|
1196
|
+
eligibility_reason TEXT
|
|
1197
|
+
) AS $$
|
|
1198
|
+
DECLARE
|
|
1199
|
+
v_formatted RECORD;
|
|
1200
|
+
v_eligibility RECORD;
|
|
1201
|
+
BEGIN
|
|
1202
|
+
-- Get eligibility
|
|
1203
|
+
SELECT * INTO v_eligibility FROM check_propagation_eligibility(p_learning_id);
|
|
1204
|
+
|
|
1205
|
+
-- Get formatted markdown (will be NULL if not eligible)
|
|
1206
|
+
SELECT * INTO v_formatted FROM format_learning_for_propagation(p_learning_id);
|
|
1207
|
+
|
|
1208
|
+
RETURN QUERY
|
|
1209
|
+
SELECT
|
|
1210
|
+
l.id,
|
|
1211
|
+
l.title::text,
|
|
1212
|
+
l.category,
|
|
1213
|
+
l.content,
|
|
1214
|
+
l.propagation_summary,
|
|
1215
|
+
l.confidence_score,
|
|
1216
|
+
l.validation_count,
|
|
1217
|
+
l.validated_by,
|
|
1218
|
+
l.importance,
|
|
1219
|
+
l.feature_id,
|
|
1220
|
+
f.name::text as feature_name,
|
|
1221
|
+
l.tags,
|
|
1222
|
+
l.created_at,
|
|
1223
|
+
l.created_by::text,
|
|
1224
|
+
v_formatted.formatted_markdown,
|
|
1225
|
+
v_eligibility.eligible,
|
|
1226
|
+
v_eligibility.reason
|
|
1227
|
+
FROM learnings l
|
|
1228
|
+
LEFT JOIN features f ON l.feature_id = f.id
|
|
1229
|
+
WHERE l.id = p_learning_id;
|
|
1230
|
+
END;
|
|
1231
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1232
|
+
|
|
1233
|
+
COMMENT ON FUNCTION get_propagation_candidate IS 'Get full propagation candidate details with eligibility';
|
|
1234
|
+
|
|
1235
|
+
-- Get propagation history (from audit log)
|
|
1236
|
+
CREATE OR REPLACE FUNCTION get_propagation_history(p_limit INTEGER DEFAULT 50)
|
|
1237
|
+
RETURNS TABLE(
|
|
1238
|
+
propagated_at TIMESTAMPTZ,
|
|
1239
|
+
learning_id UUID,
|
|
1240
|
+
learning_title TEXT,
|
|
1241
|
+
destination TEXT,
|
|
1242
|
+
propagated_by TEXT,
|
|
1243
|
+
confidence_score NUMERIC
|
|
1244
|
+
) AS $$
|
|
1245
|
+
BEGIN
|
|
1246
|
+
RETURN QUERY
|
|
1247
|
+
SELECT
|
|
1248
|
+
al.timestamp as propagated_at,
|
|
1249
|
+
(al.details->>'learning_id')::uuid as learning_id,
|
|
1250
|
+
al.details->>'learning_title' as learning_title,
|
|
1251
|
+
al.details->>'destination' as destination,
|
|
1252
|
+
al.agent_name as propagated_by,
|
|
1253
|
+
(al.details->>'confidence_score')::numeric as confidence_score
|
|
1254
|
+
FROM audit_log al
|
|
1255
|
+
WHERE al.operation = 'LEARNING_PROPAGATED'
|
|
1256
|
+
ORDER BY al.timestamp DESC
|
|
1257
|
+
LIMIT p_limit;
|
|
1258
|
+
END;
|
|
1259
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1260
|
+
|
|
1261
|
+
COMMENT ON FUNCTION get_propagation_history IS 'Get propagation history from audit log';
|
|
1262
|
+
|
|
1263
|
+
-- Mark learning as propagated (legacy, updates JSONB arrays)
|
|
1264
|
+
CREATE OR REPLACE FUNCTION mark_learning_propagated(
|
|
1265
|
+
p_learning_id UUID,
|
|
1266
|
+
p_propagated_to TEXT[],
|
|
1267
|
+
p_propagation_summary TEXT
|
|
1268
|
+
) RETURNS BOOLEAN AS $$
|
|
1269
|
+
BEGIN
|
|
1270
|
+
UPDATE learnings
|
|
1271
|
+
SET propagated_to = p_propagated_to,
|
|
1272
|
+
propagated_at = now(),
|
|
1273
|
+
propagation_summary = p_propagation_summary
|
|
1274
|
+
WHERE id = p_learning_id AND NOT is_superseded;
|
|
1275
|
+
|
|
1276
|
+
RETURN FOUND;
|
|
1277
|
+
END;
|
|
1278
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1279
|
+
|
|
1280
|
+
COMMENT ON FUNCTION mark_learning_propagated IS 'Mark learning as propagated (legacy array-based)';
|
|
1281
|
+
|
|
1282
|
+
-- Record propagation (legacy, appends to propagated_to array + audit log)
|
|
1283
|
+
CREATE OR REPLACE FUNCTION record_propagation(
|
|
1284
|
+
p_learning_id UUID,
|
|
1285
|
+
p_destination TEXT,
|
|
1286
|
+
p_propagated_by TEXT,
|
|
1287
|
+
p_propagation_summary TEXT DEFAULT NULL
|
|
1288
|
+
) RETURNS TABLE(
|
|
1289
|
+
success BOOLEAN,
|
|
1290
|
+
message TEXT,
|
|
1291
|
+
learning_title TEXT
|
|
1292
|
+
) AS $$
|
|
1293
|
+
DECLARE
|
|
1294
|
+
v_learning learnings%ROWTYPE;
|
|
1295
|
+
v_current_destinations text[];
|
|
1296
|
+
BEGIN
|
|
1297
|
+
-- Get current learning
|
|
1298
|
+
SELECT * INTO v_learning FROM learnings WHERE id = p_learning_id;
|
|
1299
|
+
|
|
1300
|
+
IF NOT FOUND THEN
|
|
1301
|
+
RETURN QUERY SELECT false, 'Learning not found', NULL::text;
|
|
1302
|
+
RETURN;
|
|
1303
|
+
END IF;
|
|
1304
|
+
|
|
1305
|
+
-- Get current destinations (or empty array)
|
|
1306
|
+
v_current_destinations := COALESCE(v_learning.propagated_to, ARRAY[]::text[]);
|
|
1307
|
+
|
|
1308
|
+
-- Check if already propagated to this destination
|
|
1309
|
+
IF p_destination = ANY(v_current_destinations) THEN
|
|
1310
|
+
RETURN QUERY SELECT false,
|
|
1311
|
+
format('Already propagated to %s', p_destination),
|
|
1312
|
+
v_learning.title::text;
|
|
1313
|
+
RETURN;
|
|
1314
|
+
END IF;
|
|
1315
|
+
|
|
1316
|
+
-- Add destination to array
|
|
1317
|
+
v_current_destinations := array_append(v_current_destinations, p_destination);
|
|
1318
|
+
|
|
1319
|
+
-- Update the learning
|
|
1320
|
+
UPDATE learnings
|
|
1321
|
+
SET propagated_to = v_current_destinations,
|
|
1322
|
+
propagated_at = now(),
|
|
1323
|
+
propagation_summary = COALESCE(p_propagation_summary, learnings.propagation_summary),
|
|
1324
|
+
updated_at = now()
|
|
1325
|
+
WHERE id = p_learning_id;
|
|
1326
|
+
|
|
1327
|
+
-- Create audit log entry
|
|
1328
|
+
INSERT INTO audit_log (operation, agent_name, details)
|
|
1329
|
+
VALUES (
|
|
1330
|
+
'LEARNING_PROPAGATED',
|
|
1331
|
+
p_propagated_by,
|
|
1332
|
+
jsonb_build_object(
|
|
1333
|
+
'learning_id', p_learning_id,
|
|
1334
|
+
'learning_title', v_learning.title,
|
|
1335
|
+
'destination', p_destination,
|
|
1336
|
+
'confidence_score', v_learning.confidence_score,
|
|
1337
|
+
'propagated_at', now()
|
|
1338
|
+
)
|
|
1339
|
+
);
|
|
1340
|
+
|
|
1341
|
+
RETURN QUERY SELECT true,
|
|
1342
|
+
format('Successfully propagated to %s', p_destination),
|
|
1343
|
+
v_learning.title::text;
|
|
1344
|
+
END;
|
|
1345
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1346
|
+
|
|
1347
|
+
COMMENT ON FUNCTION record_propagation IS 'Record propagation to a destination (legacy array-based + audit log)';
|
|
1348
|
+
|
|
1349
|
+
-- Declare a propagation target
|
|
1350
|
+
CREATE OR REPLACE FUNCTION declare_propagation_target(
|
|
1351
|
+
p_learning_id UUID,
|
|
1352
|
+
p_target_type TEXT,
|
|
1353
|
+
p_target_path TEXT DEFAULT NULL,
|
|
1354
|
+
p_relevance_score NUMERIC DEFAULT 0.80
|
|
1355
|
+
) RETURNS learning_propagation_targets AS $$
|
|
1356
|
+
DECLARE
|
|
1357
|
+
v_target learning_propagation_targets;
|
|
1358
|
+
BEGIN
|
|
1359
|
+
IF NOT EXISTS (SELECT 1 FROM learnings WHERE id = p_learning_id) THEN
|
|
1360
|
+
RAISE EXCEPTION 'Learning % not found', p_learning_id;
|
|
1361
|
+
END IF;
|
|
1362
|
+
|
|
1363
|
+
IF p_relevance_score < 0.60 THEN
|
|
1364
|
+
RAISE EXCEPTION 'Relevance score %.2f is below threshold 0.60', p_relevance_score;
|
|
1365
|
+
END IF;
|
|
1366
|
+
|
|
1367
|
+
IF p_target_type NOT IN ('agents_md', 'skill', 'agent_definition') THEN
|
|
1368
|
+
RAISE EXCEPTION 'Invalid target_type: %. Must be agents_md, skill, or agent_definition', p_target_type;
|
|
1369
|
+
END IF;
|
|
1370
|
+
|
|
1371
|
+
INSERT INTO learning_propagation_targets (
|
|
1372
|
+
learning_id, target_type, target_path, relevance_score
|
|
1373
|
+
) VALUES (
|
|
1374
|
+
p_learning_id, p_target_type, p_target_path, p_relevance_score
|
|
1375
|
+
)
|
|
1376
|
+
ON CONFLICT DO NOTHING
|
|
1377
|
+
RETURNING * INTO v_target;
|
|
1378
|
+
|
|
1379
|
+
IF v_target.id IS NULL THEN
|
|
1380
|
+
SELECT * INTO v_target
|
|
1381
|
+
FROM learning_propagation_targets
|
|
1382
|
+
WHERE learning_id = p_learning_id
|
|
1383
|
+
AND target_type = p_target_type
|
|
1384
|
+
AND target_path IS NOT DISTINCT FROM p_target_path;
|
|
1385
|
+
END IF;
|
|
1386
|
+
|
|
1387
|
+
RETURN v_target;
|
|
1388
|
+
END;
|
|
1389
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1390
|
+
|
|
1391
|
+
COMMENT ON FUNCTION declare_propagation_target IS 'Declare a propagation target for a learning (idempotent)';
|
|
1392
|
+
|
|
1393
|
+
-- Get skill propagation queue
|
|
1394
|
+
CREATE OR REPLACE FUNCTION get_skill_propagation_queue()
|
|
1395
|
+
RETURNS TABLE(
|
|
1396
|
+
learning_id UUID,
|
|
1397
|
+
title VARCHAR(255),
|
|
1398
|
+
category learning_category,
|
|
1399
|
+
content TEXT,
|
|
1400
|
+
confidence_score NUMERIC,
|
|
1401
|
+
feature_id TEXT,
|
|
1402
|
+
target_type TEXT,
|
|
1403
|
+
target_path TEXT,
|
|
1404
|
+
relevance_score NUMERIC
|
|
1405
|
+
) AS $$
|
|
1406
|
+
BEGIN
|
|
1407
|
+
RETURN QUERY
|
|
1408
|
+
SELECT
|
|
1409
|
+
l.id,
|
|
1410
|
+
l.title,
|
|
1411
|
+
l.category,
|
|
1412
|
+
l.content,
|
|
1413
|
+
l.confidence_score,
|
|
1414
|
+
l.feature_id,
|
|
1415
|
+
lpt.target_type,
|
|
1416
|
+
lpt.target_path,
|
|
1417
|
+
lpt.relevance_score
|
|
1418
|
+
FROM learnings l
|
|
1419
|
+
JOIN learning_propagation_targets lpt ON l.id = lpt.learning_id
|
|
1420
|
+
LEFT JOIN learning_propagations lp
|
|
1421
|
+
ON lpt.learning_id = lp.learning_id
|
|
1422
|
+
AND lpt.target_type = lp.target_type
|
|
1423
|
+
AND lpt.target_path IS NOT DISTINCT FROM lp.target_path
|
|
1424
|
+
WHERE l.confidence_score >= 0.80
|
|
1425
|
+
AND NOT l.is_superseded
|
|
1426
|
+
AND lpt.relevance_score >= 0.60
|
|
1427
|
+
AND lp.id IS NULL
|
|
1428
|
+
AND NOT EXISTS (
|
|
1429
|
+
SELECT 1 FROM learning_conflicts lc
|
|
1430
|
+
WHERE (lc.learning_a_id = l.id OR lc.learning_b_id = l.id)
|
|
1431
|
+
AND lc.status = 'OPEN'
|
|
1432
|
+
)
|
|
1433
|
+
ORDER BY l.confidence_score DESC, lpt.relevance_score DESC;
|
|
1434
|
+
END;
|
|
1435
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1436
|
+
|
|
1437
|
+
COMMENT ON FUNCTION get_skill_propagation_queue IS 'Get learnings ready for propagation to skills';
|
|
1438
|
+
|
|
1439
|
+
-- Record skill propagation
|
|
1440
|
+
CREATE OR REPLACE FUNCTION record_skill_propagation(
|
|
1441
|
+
p_learning_id UUID,
|
|
1442
|
+
p_target_type TEXT,
|
|
1443
|
+
p_target_path TEXT DEFAULT NULL,
|
|
1444
|
+
p_propagated_by TEXT DEFAULT 'orchestrator',
|
|
1445
|
+
p_section TEXT DEFAULT NULL
|
|
1446
|
+
) RETURNS learning_propagations AS $$
|
|
1447
|
+
DECLARE
|
|
1448
|
+
v_propagation learning_propagations;
|
|
1449
|
+
v_section TEXT;
|
|
1450
|
+
BEGIN
|
|
1451
|
+
IF p_section IS NOT NULL THEN
|
|
1452
|
+
v_section := p_section;
|
|
1453
|
+
ELSIF p_target_type = 'agent_definition' THEN
|
|
1454
|
+
v_section := 'Learnings';
|
|
1455
|
+
ELSE
|
|
1456
|
+
v_section := 'Session Learnings';
|
|
1457
|
+
END IF;
|
|
1458
|
+
|
|
1459
|
+
INSERT INTO learning_propagations (
|
|
1460
|
+
learning_id, target_type, target_path, propagated_by, section
|
|
1461
|
+
) VALUES (
|
|
1462
|
+
p_learning_id, p_target_type, p_target_path, p_propagated_by, v_section
|
|
1463
|
+
)
|
|
1464
|
+
ON CONFLICT DO NOTHING
|
|
1465
|
+
RETURNING * INTO v_propagation;
|
|
1466
|
+
|
|
1467
|
+
IF v_propagation.id IS NULL THEN
|
|
1468
|
+
RETURN NULL;
|
|
1469
|
+
END IF;
|
|
1470
|
+
|
|
1471
|
+
INSERT INTO audit_log (feature_id, operation, agent_name, details) VALUES (
|
|
1472
|
+
(SELECT feature_id FROM learnings WHERE id = p_learning_id),
|
|
1473
|
+
'SKILL_PROPAGATION',
|
|
1474
|
+
p_propagated_by,
|
|
1475
|
+
jsonb_build_object(
|
|
1476
|
+
'learning_id', p_learning_id,
|
|
1477
|
+
'target_type', p_target_type,
|
|
1478
|
+
'target_path', p_target_path,
|
|
1479
|
+
'section', v_section
|
|
1480
|
+
)
|
|
1481
|
+
);
|
|
1482
|
+
|
|
1483
|
+
RETURN v_propagation;
|
|
1484
|
+
END;
|
|
1485
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1486
|
+
|
|
1487
|
+
COMMENT ON FUNCTION record_skill_propagation IS 'Record a completed propagation (idempotent)';
|
|
1488
|
+
|
|
1489
|
+
-- Get learning propagation status
|
|
1490
|
+
CREATE OR REPLACE FUNCTION get_learning_propagation_status(p_learning_id UUID)
|
|
1491
|
+
RETURNS TABLE(
|
|
1492
|
+
target_type TEXT,
|
|
1493
|
+
target_path TEXT,
|
|
1494
|
+
relevance_score NUMERIC,
|
|
1495
|
+
is_propagated BOOLEAN,
|
|
1496
|
+
propagated_at TIMESTAMPTZ,
|
|
1497
|
+
propagated_by TEXT
|
|
1498
|
+
) AS $$
|
|
1499
|
+
BEGIN
|
|
1500
|
+
RETURN QUERY
|
|
1501
|
+
SELECT
|
|
1502
|
+
lpt.target_type,
|
|
1503
|
+
lpt.target_path,
|
|
1504
|
+
lpt.relevance_score,
|
|
1505
|
+
(lp.id IS NOT NULL)::BOOLEAN,
|
|
1506
|
+
lp.propagated_at,
|
|
1507
|
+
lp.propagated_by
|
|
1508
|
+
FROM learning_propagation_targets lpt
|
|
1509
|
+
LEFT JOIN learning_propagations lp
|
|
1510
|
+
ON lpt.learning_id = lp.learning_id
|
|
1511
|
+
AND lpt.target_type = lp.target_type
|
|
1512
|
+
AND lpt.target_path IS NOT DISTINCT FROM lp.target_path
|
|
1513
|
+
WHERE lpt.learning_id = p_learning_id
|
|
1514
|
+
ORDER BY lpt.relevance_score DESC;
|
|
1515
|
+
END;
|
|
1516
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1517
|
+
|
|
1518
|
+
COMMENT ON FUNCTION get_learning_propagation_status IS 'Get propagation status for all targets of a learning';
|
|
1519
|
+
|
|
1520
|
+
-- Get propagations for a skill
|
|
1521
|
+
CREATE OR REPLACE FUNCTION get_propagations_for_skill(p_target_path TEXT)
|
|
1522
|
+
RETURNS TABLE(
|
|
1523
|
+
learning_id UUID,
|
|
1524
|
+
title VARCHAR(255),
|
|
1525
|
+
category learning_category,
|
|
1526
|
+
content TEXT,
|
|
1527
|
+
confidence_score NUMERIC,
|
|
1528
|
+
propagated_at TIMESTAMPTZ,
|
|
1529
|
+
feature_id TEXT
|
|
1530
|
+
) AS $$
|
|
1531
|
+
BEGIN
|
|
1532
|
+
RETURN QUERY
|
|
1533
|
+
SELECT
|
|
1534
|
+
l.id,
|
|
1535
|
+
l.title,
|
|
1536
|
+
l.category,
|
|
1537
|
+
l.content,
|
|
1538
|
+
l.confidence_score,
|
|
1539
|
+
lp.propagated_at,
|
|
1540
|
+
l.feature_id
|
|
1541
|
+
FROM learning_propagations lp
|
|
1542
|
+
JOIN learnings l ON lp.learning_id = l.id
|
|
1543
|
+
WHERE lp.target_path = p_target_path
|
|
1544
|
+
ORDER BY lp.propagated_at DESC;
|
|
1545
|
+
END;
|
|
1546
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1547
|
+
|
|
1548
|
+
COMMENT ON FUNCTION get_propagations_for_skill IS 'Get all learnings propagated to a specific skill';
|
|
1549
|
+
|
|
1550
|
+
-- Get pending evolution syncs
|
|
1551
|
+
CREATE OR REPLACE FUNCTION get_pending_evolution_syncs()
|
|
1552
|
+
RETURNS TABLE(
|
|
1553
|
+
old_learning_id UUID,
|
|
1554
|
+
new_learning_id UUID,
|
|
1555
|
+
old_title VARCHAR(255),
|
|
1556
|
+
new_title VARCHAR(255),
|
|
1557
|
+
new_content TEXT,
|
|
1558
|
+
target_type TEXT,
|
|
1559
|
+
target_path TEXT,
|
|
1560
|
+
propagated_at TIMESTAMPTZ
|
|
1561
|
+
) AS $$
|
|
1562
|
+
BEGIN
|
|
1563
|
+
RETURN QUERY
|
|
1564
|
+
SELECT
|
|
1565
|
+
old_l.id,
|
|
1566
|
+
new_l.id,
|
|
1567
|
+
old_l.title,
|
|
1568
|
+
new_l.title,
|
|
1569
|
+
new_l.content,
|
|
1570
|
+
lp.target_type,
|
|
1571
|
+
lp.target_path,
|
|
1572
|
+
lp.propagated_at
|
|
1573
|
+
FROM learnings old_l
|
|
1574
|
+
JOIN learnings new_l ON old_l.superseded_by = new_l.id
|
|
1575
|
+
JOIN learning_propagations lp ON old_l.id = lp.learning_id
|
|
1576
|
+
LEFT JOIN learning_propagations new_lp
|
|
1577
|
+
ON new_l.id = new_lp.learning_id
|
|
1578
|
+
AND lp.target_type = new_lp.target_type
|
|
1579
|
+
AND lp.target_path IS NOT DISTINCT FROM new_lp.target_path
|
|
1580
|
+
WHERE old_l.is_superseded = true
|
|
1581
|
+
AND old_l.superseded_by IS NOT NULL
|
|
1582
|
+
AND new_lp.id IS NULL
|
|
1583
|
+
ORDER BY lp.propagated_at;
|
|
1584
|
+
END;
|
|
1585
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1586
|
+
|
|
1587
|
+
COMMENT ON FUNCTION get_pending_evolution_syncs IS 'Find propagated learnings that have been superseded';
|
|
1588
|
+
|
|
1589
|
+
-- ============================================================================
|
|
1590
|
+
-- EVALS FUNCTIONS
|
|
1591
|
+
-- ============================================================================
|
|
1592
|
+
|
|
1593
|
+
-- Compute feature evaluation (duration-based)
|
|
1594
|
+
CREATE OR REPLACE FUNCTION compute_feature_eval(p_feature_id TEXT)
|
|
1595
|
+
RETURNS UUID AS $$
|
|
1596
|
+
DECLARE
|
|
1597
|
+
v_feature RECORD;
|
|
1598
|
+
v_duration RECORD;
|
|
1599
|
+
v_quality RECORD;
|
|
1600
|
+
v_iterations RECORD;
|
|
1601
|
+
v_learnings RECORD;
|
|
1602
|
+
v_efficiency_score NUMERIC(5,2);
|
|
1603
|
+
v_quality_score NUMERIC(5,2);
|
|
1604
|
+
v_overall_score NUMERIC(5,2);
|
|
1605
|
+
v_health_status eval_health;
|
|
1606
|
+
v_eval_id UUID;
|
|
1607
|
+
v_expected_duration_minutes INTEGER;
|
|
1608
|
+
v_actual_duration_minutes NUMERIC;
|
|
1609
|
+
v_duration_source TEXT;
|
|
1610
|
+
BEGIN
|
|
1611
|
+
SELECT * INTO v_feature FROM features WHERE id = p_feature_id;
|
|
1612
|
+
IF NOT FOUND THEN
|
|
1613
|
+
RAISE EXCEPTION 'Feature not found: %', p_feature_id;
|
|
1614
|
+
END IF;
|
|
1615
|
+
|
|
1616
|
+
-- Expected duration by complexity (in minutes)
|
|
1617
|
+
v_expected_duration_minutes := CASE v_feature.complexity_level
|
|
1618
|
+
WHEN 1 THEN 60
|
|
1619
|
+
WHEN 2 THEN 180
|
|
1620
|
+
WHEN 3 THEN 480
|
|
1621
|
+
ELSE 180
|
|
1622
|
+
END;
|
|
1623
|
+
|
|
1624
|
+
-- Calculate durations
|
|
1625
|
+
SELECT
|
|
1626
|
+
COALESCE(EXTRACT(EPOCH FROM (
|
|
1627
|
+
COALESCE(v_feature.completed_at, now()) - v_feature.created_at
|
|
1628
|
+
)) / 60, 0)::INTEGER AS wall_clock_minutes,
|
|
1629
|
+
COALESCE(SUM(ai.duration_ms), 0)::BIGINT AS total_agent_ms,
|
|
1630
|
+
COUNT(ai.id) AS invocation_count
|
|
1631
|
+
INTO v_duration
|
|
1632
|
+
FROM agent_invocations ai
|
|
1633
|
+
WHERE ai.feature_id = p_feature_id AND ai.ended_at IS NOT NULL;
|
|
1634
|
+
|
|
1635
|
+
IF v_duration.total_agent_ms > 0 THEN
|
|
1636
|
+
v_actual_duration_minutes := v_duration.total_agent_ms / 60000.0;
|
|
1637
|
+
v_duration_source := 'agent_invocations';
|
|
1638
|
+
ELSE
|
|
1639
|
+
v_actual_duration_minutes := v_duration.wall_clock_minutes;
|
|
1640
|
+
v_duration_source := 'wall_clock';
|
|
1641
|
+
END IF;
|
|
1642
|
+
|
|
1643
|
+
-- Quality metrics
|
|
1644
|
+
SELECT
|
|
1645
|
+
COUNT(*) FILTER (WHERE status = 'APPROVED') AS approvals,
|
|
1646
|
+
COUNT(*) FILTER (WHERE status = 'REJECTED') AS rejections,
|
|
1647
|
+
COUNT(*) AS total_gates
|
|
1648
|
+
INTO v_quality
|
|
1649
|
+
FROM quality_gates WHERE feature_id = p_feature_id;
|
|
1650
|
+
|
|
1651
|
+
-- Iteration metrics
|
|
1652
|
+
SELECT
|
|
1653
|
+
COALESCE(MAX(iteration_number), 0) AS max_iterations,
|
|
1654
|
+
COUNT(*) FILTER (WHERE thrashing_detected) AS thrashing_count
|
|
1655
|
+
INTO v_iterations
|
|
1656
|
+
FROM iteration_tracking WHERE feature_id = p_feature_id;
|
|
1657
|
+
|
|
1658
|
+
-- Learning metrics
|
|
1659
|
+
SELECT
|
|
1660
|
+
COUNT(*) AS total_learnings,
|
|
1661
|
+
COUNT(*) FILTER (WHERE confidence_score >= 0.80) AS high_confidence,
|
|
1662
|
+
COUNT(*) FILTER (WHERE array_length(propagated_to, 1) > 0) AS propagated
|
|
1663
|
+
INTO v_learnings
|
|
1664
|
+
FROM learnings WHERE feature_id = p_feature_id;
|
|
1665
|
+
|
|
1666
|
+
-- Efficiency score (0-100)
|
|
1667
|
+
v_efficiency_score := GREATEST(0, LEAST(100,
|
|
1668
|
+
CASE
|
|
1669
|
+
WHEN v_actual_duration_minutes <= v_expected_duration_minutes THEN 70
|
|
1670
|
+
WHEN v_actual_duration_minutes <= v_expected_duration_minutes * 1.5 THEN 55
|
|
1671
|
+
WHEN v_actual_duration_minutes <= v_expected_duration_minutes * 2 THEN 35
|
|
1672
|
+
ELSE 15
|
|
1673
|
+
END +
|
|
1674
|
+
CASE
|
|
1675
|
+
WHEN v_iterations.max_iterations <= 2 THEN 30
|
|
1676
|
+
WHEN v_iterations.max_iterations <= 3 THEN 25
|
|
1677
|
+
WHEN v_iterations.max_iterations <= 4 THEN 15
|
|
1678
|
+
ELSE 5
|
|
1679
|
+
END
|
|
1680
|
+
));
|
|
1681
|
+
|
|
1682
|
+
-- Quality score (0-100)
|
|
1683
|
+
v_quality_score := GREATEST(0, LEAST(100,
|
|
1684
|
+
CASE
|
|
1685
|
+
WHEN v_quality.total_gates = 0 THEN 50
|
|
1686
|
+
ELSE (v_quality.approvals::NUMERIC / NULLIF(v_quality.total_gates, 0)) * 50
|
|
1687
|
+
END +
|
|
1688
|
+
CASE
|
|
1689
|
+
WHEN (SELECT COUNT(*) FROM blockers WHERE feature_id = p_feature_id) = 0 THEN 30
|
|
1690
|
+
WHEN (SELECT COUNT(*) FROM blockers WHERE feature_id = p_feature_id) = 1 THEN 20
|
|
1691
|
+
WHEN (SELECT COUNT(*) FROM blockers WHERE feature_id = p_feature_id) <= 3 THEN 10
|
|
1692
|
+
ELSE 0
|
|
1693
|
+
END +
|
|
1694
|
+
CASE
|
|
1695
|
+
WHEN v_iterations.thrashing_count = 0 THEN 20
|
|
1696
|
+
ELSE 0
|
|
1697
|
+
END
|
|
1698
|
+
));
|
|
1699
|
+
|
|
1700
|
+
-- Overall score
|
|
1701
|
+
v_overall_score := (v_efficiency_score * 0.4 + v_quality_score * 0.6);
|
|
1702
|
+
|
|
1703
|
+
-- Health status
|
|
1704
|
+
v_health_status := CASE
|
|
1705
|
+
WHEN v_overall_score >= 70 THEN 'HEALTHY'
|
|
1706
|
+
WHEN v_overall_score >= 50 THEN 'CONCERNING'
|
|
1707
|
+
ELSE 'CRITICAL'
|
|
1708
|
+
END;
|
|
1709
|
+
|
|
1710
|
+
-- Insert eval
|
|
1711
|
+
INSERT INTO feature_evals (
|
|
1712
|
+
feature_id, efficiency_score, quality_score, overall_score, health_status,
|
|
1713
|
+
efficiency_breakdown, quality_breakdown, learning_metrics, raw_metrics
|
|
1714
|
+
) VALUES (
|
|
1715
|
+
p_feature_id,
|
|
1716
|
+
v_efficiency_score,
|
|
1717
|
+
v_quality_score,
|
|
1718
|
+
v_overall_score,
|
|
1719
|
+
v_health_status,
|
|
1720
|
+
jsonb_build_object(
|
|
1721
|
+
'actual_duration_minutes', ROUND(v_actual_duration_minutes, 1),
|
|
1722
|
+
'expected_duration_minutes', v_expected_duration_minutes,
|
|
1723
|
+
'duration_ratio', ROUND(v_actual_duration_minutes / NULLIF(v_expected_duration_minutes, 0), 2),
|
|
1724
|
+
'wall_clock_minutes', v_duration.wall_clock_minutes,
|
|
1725
|
+
'total_agent_duration_ms', v_duration.total_agent_ms,
|
|
1726
|
+
'agent_invocations', v_duration.invocation_count,
|
|
1727
|
+
'iterations', v_iterations.max_iterations,
|
|
1728
|
+
'duration_source', v_duration_source
|
|
1729
|
+
),
|
|
1730
|
+
jsonb_build_object(
|
|
1731
|
+
'approvals', v_quality.approvals,
|
|
1732
|
+
'rejections', v_quality.rejections,
|
|
1733
|
+
'total_gates', v_quality.total_gates,
|
|
1734
|
+
'approval_rate', CASE
|
|
1735
|
+
WHEN v_quality.total_gates > 0
|
|
1736
|
+
THEN ROUND((v_quality.approvals::NUMERIC / v_quality.total_gates) * 100, 1)
|
|
1737
|
+
ELSE 100
|
|
1738
|
+
END,
|
|
1739
|
+
'thrashing_incidents', v_iterations.thrashing_count,
|
|
1740
|
+
'blocker_count', (SELECT COUNT(*) FROM blockers WHERE feature_id = p_feature_id)
|
|
1741
|
+
),
|
|
1742
|
+
jsonb_build_object(
|
|
1743
|
+
'total_learnings', v_learnings.total_learnings,
|
|
1744
|
+
'high_confidence', v_learnings.high_confidence,
|
|
1745
|
+
'propagated', v_learnings.propagated
|
|
1746
|
+
),
|
|
1747
|
+
jsonb_build_object(
|
|
1748
|
+
'computed_at', now(),
|
|
1749
|
+
'feature_status', v_feature.status,
|
|
1750
|
+
'feature_phase', v_feature.current_phase
|
|
1751
|
+
)
|
|
1752
|
+
)
|
|
1753
|
+
RETURNING id INTO v_eval_id;
|
|
1754
|
+
|
|
1755
|
+
-- Generate alerts
|
|
1756
|
+
IF v_overall_score < 50 THEN
|
|
1757
|
+
INSERT INTO eval_alerts (severity, dimension, message, current_value, threshold, source_type, source_id, feature_id)
|
|
1758
|
+
VALUES ('CRITICAL', 'overall_score', 'Feature health is critical', v_overall_score, 50, 'feature', v_eval_id, p_feature_id);
|
|
1759
|
+
ELSIF v_overall_score < 70 THEN
|
|
1760
|
+
INSERT INTO eval_alerts (severity, dimension, message, current_value, threshold, source_type, source_id, feature_id)
|
|
1761
|
+
VALUES ('WARNING', 'overall_score', 'Feature health is concerning', v_overall_score, 70, 'feature', v_eval_id, p_feature_id);
|
|
1762
|
+
END IF;
|
|
1763
|
+
|
|
1764
|
+
IF v_actual_duration_minutes > v_expected_duration_minutes * 2 THEN
|
|
1765
|
+
INSERT INTO eval_alerts (severity, dimension, message, current_value, threshold, source_type, source_id, feature_id)
|
|
1766
|
+
VALUES ('WARNING', 'duration', 'Feature duration significantly exceeds expectation', v_actual_duration_minutes, v_expected_duration_minutes * 2, 'feature', v_eval_id, p_feature_id);
|
|
1767
|
+
END IF;
|
|
1768
|
+
|
|
1769
|
+
IF v_iterations.thrashing_count > 0 THEN
|
|
1770
|
+
INSERT INTO eval_alerts (severity, dimension, message, current_value, threshold, source_type, source_id, feature_id)
|
|
1771
|
+
VALUES ('WARNING', 'thrashing', 'Spec thrashing detected', v_iterations.thrashing_count, 0, 'feature', v_eval_id, p_feature_id);
|
|
1772
|
+
END IF;
|
|
1773
|
+
|
|
1774
|
+
RETURN v_eval_id;
|
|
1775
|
+
END;
|
|
1776
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1777
|
+
|
|
1778
|
+
COMMENT ON FUNCTION compute_feature_eval IS 'Compute duration-based feature evaluation and generate alerts';
|
|
1779
|
+
|
|
1780
|
+
-- Compute system health
|
|
1781
|
+
CREATE OR REPLACE FUNCTION compute_system_health(p_period_days INTEGER DEFAULT 7)
|
|
1782
|
+
RETURNS UUID AS $$
|
|
1783
|
+
DECLARE
|
|
1784
|
+
v_period_start TIMESTAMPTZ;
|
|
1785
|
+
v_workflow RECORD;
|
|
1786
|
+
v_duration RECORD;
|
|
1787
|
+
v_quality RECORD;
|
|
1788
|
+
v_learnings RECORD;
|
|
1789
|
+
v_overall_score NUMERIC(5,2);
|
|
1790
|
+
v_health_status eval_health;
|
|
1791
|
+
v_alerts JSONB := '[]'::jsonb;
|
|
1792
|
+
v_eval_id UUID;
|
|
1793
|
+
BEGIN
|
|
1794
|
+
v_period_start := now() - (p_period_days || ' days')::INTERVAL;
|
|
1795
|
+
|
|
1796
|
+
SELECT
|
|
1797
|
+
COUNT(*) FILTER (WHERE status = 'COMPLETED' AND completed_at >= v_period_start) AS completed,
|
|
1798
|
+
COUNT(*) FILTER (WHERE status = 'BLOCKED') AS blocked,
|
|
1799
|
+
COUNT(*) FILTER (WHERE status = 'IN_PROGRESS') AS in_progress,
|
|
1800
|
+
AVG(EXTRACT(EPOCH FROM (completed_at - created_at)) / 3600)
|
|
1801
|
+
FILTER (WHERE status = 'COMPLETED' AND completed_at >= v_period_start) AS avg_cycle_hours
|
|
1802
|
+
INTO v_workflow
|
|
1803
|
+
FROM features;
|
|
1804
|
+
|
|
1805
|
+
SELECT
|
|
1806
|
+
COALESCE(AVG(ai.duration_ms), 0) AS avg_invocation_ms,
|
|
1807
|
+
COALESCE(SUM(ai.duration_ms), 0) AS total_duration_ms,
|
|
1808
|
+
COUNT(DISTINCT ai.feature_id) AS features_with_invocations
|
|
1809
|
+
INTO v_duration
|
|
1810
|
+
FROM agent_invocations ai
|
|
1811
|
+
WHERE ai.created_at >= v_period_start AND ai.ended_at IS NOT NULL;
|
|
1812
|
+
|
|
1813
|
+
SELECT
|
|
1814
|
+
AVG(iteration_number) AS avg_iterations,
|
|
1815
|
+
(COUNT(*) FILTER (WHERE thrashing_detected)::NUMERIC / NULLIF(COUNT(*), 0)) * 100 AS thrashing_rate
|
|
1816
|
+
INTO v_quality
|
|
1817
|
+
FROM iteration_tracking
|
|
1818
|
+
WHERE recorded_at >= v_period_start;
|
|
1819
|
+
|
|
1820
|
+
SELECT
|
|
1821
|
+
COUNT(*) AS total_learnings,
|
|
1822
|
+
COUNT(*) FILTER (WHERE confidence_score >= 0.80) AS high_confidence,
|
|
1823
|
+
(SELECT COUNT(*) FROM learning_conflicts WHERE status = 'OPEN') AS open_conflicts
|
|
1824
|
+
INTO v_learnings
|
|
1825
|
+
FROM learnings
|
|
1826
|
+
WHERE created_at >= v_period_start;
|
|
1827
|
+
|
|
1828
|
+
v_overall_score := GREATEST(0, LEAST(100,
|
|
1829
|
+
CASE
|
|
1830
|
+
WHEN COALESCE(v_workflow.completed, 0) > 0 THEN 30
|
|
1831
|
+
WHEN COALESCE(v_workflow.in_progress, 0) > 0 THEN 20
|
|
1832
|
+
ELSE 15
|
|
1833
|
+
END +
|
|
1834
|
+
CASE
|
|
1835
|
+
WHEN COALESCE(v_quality.avg_iterations, 1) <= 2 THEN 25
|
|
1836
|
+
WHEN COALESCE(v_quality.avg_iterations, 1) <= 3 THEN 20
|
|
1837
|
+
WHEN COALESCE(v_quality.avg_iterations, 1) <= 4 THEN 10
|
|
1838
|
+
ELSE 5
|
|
1839
|
+
END +
|
|
1840
|
+
CASE
|
|
1841
|
+
WHEN COALESCE(v_quality.thrashing_rate, 0) <= 5 THEN 20
|
|
1842
|
+
WHEN COALESCE(v_quality.thrashing_rate, 0) <= 15 THEN 10
|
|
1843
|
+
ELSE 0
|
|
1844
|
+
END +
|
|
1845
|
+
CASE
|
|
1846
|
+
WHEN v_workflow.in_progress = 0 THEN 25
|
|
1847
|
+
WHEN (v_workflow.blocked::NUMERIC / NULLIF(v_workflow.in_progress + v_workflow.blocked, 0)) * 100 <= 10 THEN 25
|
|
1848
|
+
WHEN (v_workflow.blocked::NUMERIC / NULLIF(v_workflow.in_progress + v_workflow.blocked, 0)) * 100 <= 25 THEN 15
|
|
1849
|
+
ELSE 5
|
|
1850
|
+
END
|
|
1851
|
+
));
|
|
1852
|
+
|
|
1853
|
+
v_health_status := CASE
|
|
1854
|
+
WHEN v_overall_score >= 70 THEN 'HEALTHY'
|
|
1855
|
+
WHEN v_overall_score >= 50 THEN 'CONCERNING'
|
|
1856
|
+
ELSE 'CRITICAL'
|
|
1857
|
+
END;
|
|
1858
|
+
|
|
1859
|
+
IF v_overall_score < 50 THEN
|
|
1860
|
+
v_alerts := v_alerts || jsonb_build_object(
|
|
1861
|
+
'severity', 'CRITICAL', 'dimension', 'overall_health',
|
|
1862
|
+
'message', 'System health is critical', 'current_value', v_overall_score, 'threshold', 50
|
|
1863
|
+
);
|
|
1864
|
+
END IF;
|
|
1865
|
+
|
|
1866
|
+
IF COALESCE(v_quality.thrashing_rate, 0) > 15 THEN
|
|
1867
|
+
v_alerts := v_alerts || jsonb_build_object(
|
|
1868
|
+
'severity', 'CRITICAL', 'dimension', 'thrashing_rate',
|
|
1869
|
+
'message', 'High thrashing rate detected', 'current_value', v_quality.thrashing_rate, 'threshold', 15
|
|
1870
|
+
);
|
|
1871
|
+
END IF;
|
|
1872
|
+
|
|
1873
|
+
IF v_learnings.open_conflicts > 0 THEN
|
|
1874
|
+
v_alerts := v_alerts || jsonb_build_object(
|
|
1875
|
+
'severity', 'WARNING', 'dimension', 'learning_conflicts',
|
|
1876
|
+
'message', 'Open learning conflicts require attention', 'current_value', v_learnings.open_conflicts, 'threshold', 0
|
|
1877
|
+
);
|
|
1878
|
+
END IF;
|
|
1879
|
+
|
|
1880
|
+
INSERT INTO system_health_evals (
|
|
1881
|
+
period_days, overall_health_score, health_status,
|
|
1882
|
+
workflow_metrics, quality_metrics, learning_metrics, alerts
|
|
1883
|
+
) VALUES (
|
|
1884
|
+
p_period_days,
|
|
1885
|
+
v_overall_score,
|
|
1886
|
+
v_health_status,
|
|
1887
|
+
jsonb_build_object(
|
|
1888
|
+
'features_completed', COALESCE(v_workflow.completed, 0),
|
|
1889
|
+
'features_blocked', COALESCE(v_workflow.blocked, 0),
|
|
1890
|
+
'features_in_progress', COALESCE(v_workflow.in_progress, 0),
|
|
1891
|
+
'avg_cycle_time_hours', ROUND(COALESCE(v_workflow.avg_cycle_hours, 0)::NUMERIC, 1)
|
|
1892
|
+
),
|
|
1893
|
+
jsonb_build_object(
|
|
1894
|
+
'avg_iterations_to_approval', ROUND(COALESCE(v_quality.avg_iterations, 1)::NUMERIC, 1),
|
|
1895
|
+
'thrashing_rate', ROUND(COALESCE(v_quality.thrashing_rate, 0)::NUMERIC, 1)
|
|
1896
|
+
),
|
|
1897
|
+
jsonb_build_object(
|
|
1898
|
+
'total_learnings', COALESCE(v_learnings.total_learnings, 0),
|
|
1899
|
+
'high_confidence_learnings', COALESCE(v_learnings.high_confidence, 0),
|
|
1900
|
+
'open_conflicts', COALESCE(v_learnings.open_conflicts, 0)
|
|
1901
|
+
),
|
|
1902
|
+
v_alerts
|
|
1903
|
+
)
|
|
1904
|
+
RETURNING id INTO v_eval_id;
|
|
1905
|
+
|
|
1906
|
+
IF v_health_status = 'CRITICAL' THEN
|
|
1907
|
+
INSERT INTO eval_alerts (severity, dimension, message, current_value, threshold, source_type, source_id)
|
|
1908
|
+
VALUES ('CRITICAL', 'system_health', 'System health is critical', v_overall_score, 50, 'system', v_eval_id);
|
|
1909
|
+
END IF;
|
|
1910
|
+
|
|
1911
|
+
RETURN v_eval_id;
|
|
1912
|
+
END;
|
|
1913
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1914
|
+
|
|
1915
|
+
COMMENT ON FUNCTION compute_system_health IS 'Compute system-wide health evaluation';
|
|
1916
|
+
|
|
1917
|
+
-- Acknowledge an eval alert
|
|
1918
|
+
CREATE OR REPLACE FUNCTION acknowledge_alert(
|
|
1919
|
+
p_alert_id UUID,
|
|
1920
|
+
p_acknowledged_by VARCHAR
|
|
1921
|
+
) RETURNS BOOLEAN AS $$
|
|
1922
|
+
BEGIN
|
|
1923
|
+
UPDATE eval_alerts
|
|
1924
|
+
SET acknowledged_at = now(),
|
|
1925
|
+
acknowledged_by = p_acknowledged_by
|
|
1926
|
+
WHERE id = p_alert_id AND acknowledged_at IS NULL;
|
|
1927
|
+
|
|
1928
|
+
RETURN FOUND;
|
|
1929
|
+
END;
|
|
1930
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1931
|
+
|
|
1932
|
+
COMMENT ON FUNCTION acknowledge_alert IS 'Acknowledge an eval alert';
|
|
1933
|
+
|
|
1934
|
+
-- Resolve an eval alert
|
|
1935
|
+
CREATE OR REPLACE FUNCTION resolve_alert(
|
|
1936
|
+
p_alert_id UUID,
|
|
1937
|
+
p_resolved_by VARCHAR,
|
|
1938
|
+
p_resolution_notes TEXT DEFAULT NULL
|
|
1939
|
+
) RETURNS BOOLEAN AS $$
|
|
1940
|
+
BEGIN
|
|
1941
|
+
UPDATE eval_alerts
|
|
1942
|
+
SET resolved_at = now(),
|
|
1943
|
+
resolved_by = p_resolved_by,
|
|
1944
|
+
resolution_notes = p_resolution_notes
|
|
1945
|
+
WHERE id = p_alert_id AND resolved_at IS NULL;
|
|
1946
|
+
|
|
1947
|
+
RETURN FOUND;
|
|
1948
|
+
END;
|
|
1949
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
1950
|
+
|
|
1951
|
+
COMMENT ON FUNCTION resolve_alert IS 'Resolve an eval alert';
|
|
1952
|
+
|
|
1953
|
+
-- ============================================================================
|
|
1954
|
+
-- BATCH EXECUTION FUNCTIONS
|
|
1955
|
+
-- ============================================================================
|
|
1956
|
+
|
|
1957
|
+
-- Record batch execution
|
|
1958
|
+
CREATE OR REPLACE FUNCTION record_batch_execution(
|
|
1959
|
+
p_script_name VARCHAR(100),
|
|
1960
|
+
p_template_used VARCHAR(100),
|
|
1961
|
+
p_total_steps INTEGER,
|
|
1962
|
+
p_completed_steps INTEGER,
|
|
1963
|
+
p_success BOOLEAN,
|
|
1964
|
+
p_failed_step INTEGER DEFAULT NULL,
|
|
1965
|
+
p_error_message TEXT DEFAULT NULL,
|
|
1966
|
+
p_duration_ms INTEGER DEFAULT NULL,
|
|
1967
|
+
p_feature_id TEXT DEFAULT NULL,
|
|
1968
|
+
p_agent_name VARCHAR(100) DEFAULT NULL,
|
|
1969
|
+
p_script_json JSONB DEFAULT NULL,
|
|
1970
|
+
p_results_json JSONB DEFAULT NULL
|
|
1971
|
+
) RETURNS UUID AS $$
|
|
1972
|
+
DECLARE
|
|
1973
|
+
v_exec_id UUID;
|
|
1974
|
+
v_script_hash VARCHAR(64);
|
|
1975
|
+
BEGIN
|
|
1976
|
+
IF p_script_json IS NOT NULL THEN
|
|
1977
|
+
v_script_hash := encode(sha256(p_script_json::TEXT::BYTEA), 'hex');
|
|
1978
|
+
END IF;
|
|
1979
|
+
|
|
1980
|
+
INSERT INTO batch_executions (
|
|
1981
|
+
script_name, script_hash, template_used, total_steps, completed_steps,
|
|
1982
|
+
success, failed_step, error_message, duration_ms,
|
|
1983
|
+
feature_id, agent_name, script_json, results_json
|
|
1984
|
+
) VALUES (
|
|
1985
|
+
p_script_name, v_script_hash, p_template_used, p_total_steps, p_completed_steps,
|
|
1986
|
+
p_success, p_failed_step, p_error_message, p_duration_ms,
|
|
1987
|
+
p_feature_id, p_agent_name, p_script_json, p_results_json
|
|
1988
|
+
)
|
|
1989
|
+
RETURNING id INTO v_exec_id;
|
|
1990
|
+
|
|
1991
|
+
IF p_template_used IS NOT NULL THEN
|
|
1992
|
+
UPDATE batch_templates
|
|
1993
|
+
SET usage_count = usage_count + 1,
|
|
1994
|
+
last_used_at = now(),
|
|
1995
|
+
avg_duration_ms = CASE
|
|
1996
|
+
WHEN avg_duration_ms IS NULL THEN p_duration_ms
|
|
1997
|
+
ELSE (avg_duration_ms * (usage_count - 1) + COALESCE(p_duration_ms, 0)) / usage_count
|
|
1998
|
+
END,
|
|
1999
|
+
success_rate = CASE
|
|
2000
|
+
WHEN success_rate IS NULL THEN CASE WHEN p_success THEN 100 ELSE 0 END
|
|
2001
|
+
ELSE (success_rate * (usage_count - 1) + CASE WHEN p_success THEN 100 ELSE 0 END) / usage_count
|
|
2002
|
+
END
|
|
2003
|
+
WHERE name = p_template_used;
|
|
2004
|
+
END IF;
|
|
2005
|
+
|
|
2006
|
+
RETURN v_exec_id;
|
|
2007
|
+
END;
|
|
2008
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
2009
|
+
|
|
2010
|
+
COMMENT ON FUNCTION record_batch_execution IS 'Record batch execution and update template stats';
|
|
2011
|
+
|
|
2012
|
+
-- ============================================================================
|
|
2013
|
+
-- MEMORY FUNCTIONS
|
|
2014
|
+
-- ============================================================================
|
|
2015
|
+
|
|
2016
|
+
-- Get relevant memories for a feature
|
|
2017
|
+
CREATE OR REPLACE FUNCTION get_relevant_memories(
|
|
2018
|
+
for_feature_id VARCHAR DEFAULT NULL,
|
|
2019
|
+
include_global BOOLEAN DEFAULT true,
|
|
2020
|
+
max_memories INTEGER DEFAULT 10
|
|
2021
|
+
) RETURNS TABLE(
|
|
2022
|
+
id UUID,
|
|
2023
|
+
feature_id VARCHAR(50),
|
|
2024
|
+
category memory_category,
|
|
2025
|
+
importance memory_importance,
|
|
2026
|
+
title VARCHAR(200),
|
|
2027
|
+
content TEXT,
|
|
2028
|
+
rationale TEXT
|
|
2029
|
+
) AS $$
|
|
2030
|
+
BEGIN
|
|
2031
|
+
RETURN QUERY
|
|
2032
|
+
SELECT m.id, m.feature_id, m.category, m.importance, m.title, m.content, m.rationale
|
|
2033
|
+
FROM memories m
|
|
2034
|
+
WHERE m.is_archived = FALSE
|
|
2035
|
+
AND ((for_feature_id IS NOT NULL AND m.feature_id = for_feature_id) OR (include_global AND m.feature_id IS NULL))
|
|
2036
|
+
ORDER BY m.importance DESC, CASE WHEN m.feature_id = for_feature_id THEN 0 ELSE 1 END, m.created_at DESC
|
|
2037
|
+
LIMIT max_memories;
|
|
2038
|
+
END;
|
|
2039
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
2040
|
+
|
|
2041
|
+
COMMENT ON FUNCTION get_relevant_memories IS 'Get relevant memories for a feature (includes global)';
|
|
2042
|
+
|
|
2043
|
+
-- Search memories using full-text search
|
|
2044
|
+
CREATE OR REPLACE FUNCTION search_memories(
|
|
2045
|
+
search_query TEXT,
|
|
2046
|
+
category_filter memory_category DEFAULT NULL,
|
|
2047
|
+
importance_filter memory_importance DEFAULT NULL,
|
|
2048
|
+
feature_filter VARCHAR DEFAULT NULL,
|
|
2049
|
+
result_limit INTEGER DEFAULT 20
|
|
2050
|
+
) RETURNS TABLE(
|
|
2051
|
+
id UUID,
|
|
2052
|
+
feature_id VARCHAR(50),
|
|
2053
|
+
category memory_category,
|
|
2054
|
+
importance memory_importance,
|
|
2055
|
+
title VARCHAR(200),
|
|
2056
|
+
content TEXT,
|
|
2057
|
+
rationale TEXT,
|
|
2058
|
+
tags TEXT[],
|
|
2059
|
+
created_at TIMESTAMPTZ,
|
|
2060
|
+
rank REAL
|
|
2061
|
+
) AS $$
|
|
2062
|
+
BEGIN
|
|
2063
|
+
RETURN QUERY
|
|
2064
|
+
SELECT m.id, m.feature_id, m.category, m.importance, m.title, m.content, m.rationale, m.tags, m.created_at,
|
|
2065
|
+
ts_rank(m.search_vector, websearch_to_tsquery('english', search_query)) as rank
|
|
2066
|
+
FROM memories m
|
|
2067
|
+
WHERE m.is_archived = FALSE
|
|
2068
|
+
AND m.search_vector @@ websearch_to_tsquery('english', search_query)
|
|
2069
|
+
AND (category_filter IS NULL OR m.category = category_filter)
|
|
2070
|
+
AND (importance_filter IS NULL OR m.importance = importance_filter)
|
|
2071
|
+
AND (feature_filter IS NULL OR m.feature_id = feature_filter)
|
|
2072
|
+
ORDER BY rank DESC, m.importance DESC, m.created_at DESC
|
|
2073
|
+
LIMIT result_limit;
|
|
2074
|
+
END;
|
|
2075
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
2076
|
+
|
|
2077
|
+
COMMENT ON FUNCTION search_memories IS 'Search memories using full-text search with filters';
|
|
2078
|
+
|
|
2079
|
+
-- ============================================================================
|
|
2080
|
+
-- ARCHIVE FUNCTIONS
|
|
2081
|
+
-- ============================================================================
|
|
2082
|
+
|
|
2083
|
+
-- Get feature archive
|
|
2084
|
+
CREATE OR REPLACE FUNCTION get_feature_archive(p_feature_id VARCHAR)
|
|
2085
|
+
RETURNS TABLE(
|
|
2086
|
+
id UUID,
|
|
2087
|
+
feature_id VARCHAR(50),
|
|
2088
|
+
storage_path TEXT,
|
|
2089
|
+
summary TEXT,
|
|
2090
|
+
files_archived TEXT[],
|
|
2091
|
+
total_size_bytes INTEGER,
|
|
2092
|
+
spec_snapshot JSONB,
|
|
2093
|
+
release_version VARCHAR(50),
|
|
2094
|
+
release_notes TEXT,
|
|
2095
|
+
archived_at TIMESTAMPTZ
|
|
2096
|
+
) AS $$
|
|
2097
|
+
BEGIN
|
|
2098
|
+
RETURN QUERY SELECT fa.id, fa.feature_id, fa.storage_path, fa.summary, fa.files_archived,
|
|
2099
|
+
fa.total_size_bytes, fa.spec_snapshot, fa.release_version, fa.release_notes, fa.archived_at
|
|
2100
|
+
FROM feature_archives fa WHERE fa.feature_id = p_feature_id;
|
|
2101
|
+
END;
|
|
2102
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
2103
|
+
|
|
2104
|
+
COMMENT ON FUNCTION get_feature_archive IS 'Get archive for a feature';
|
|
2105
|
+
|
|
2106
|
+
-- Search archives using full-text search
|
|
2107
|
+
CREATE OR REPLACE FUNCTION search_archives(
|
|
2108
|
+
search_query TEXT,
|
|
2109
|
+
result_limit INTEGER DEFAULT 20
|
|
2110
|
+
) RETURNS TABLE(
|
|
2111
|
+
id UUID,
|
|
2112
|
+
feature_id VARCHAR(50),
|
|
2113
|
+
summary TEXT,
|
|
2114
|
+
release_version VARCHAR(50),
|
|
2115
|
+
archived_at TIMESTAMPTZ,
|
|
2116
|
+
rank REAL
|
|
2117
|
+
) AS $$
|
|
2118
|
+
BEGIN
|
|
2119
|
+
RETURN QUERY SELECT fa.id, fa.feature_id, fa.summary, fa.release_version, fa.archived_at,
|
|
2120
|
+
ts_rank(fa.search_vector, websearch_to_tsquery('english', search_query)) AS rank
|
|
2121
|
+
FROM feature_archives fa WHERE fa.search_vector @@ websearch_to_tsquery('english', search_query)
|
|
2122
|
+
ORDER BY rank DESC LIMIT result_limit;
|
|
2123
|
+
END;
|
|
2124
|
+
$$ LANGUAGE plpgsql SET search_path = public;
|
|
2125
|
+
|
|
2126
|
+
COMMENT ON FUNCTION search_archives IS 'Search feature archives using full-text search';
|