@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.
Files changed (219) hide show
  1. package/README.md +306 -0
  2. package/dist/adapters/archive/supabase.d.ts +19 -0
  3. package/dist/adapters/archive/supabase.d.ts.map +1 -0
  4. package/dist/adapters/archive/supabase.js +121 -0
  5. package/dist/adapters/archive/supabase.js.map +1 -0
  6. package/dist/adapters/archive/types.d.ts +26 -0
  7. package/dist/adapters/archive/types.d.ts.map +1 -0
  8. package/dist/adapters/archive/types.js +6 -0
  9. package/dist/adapters/archive/types.js.map +1 -0
  10. package/dist/adapters/formal-verification/tla-precheck.d.ts +22 -0
  11. package/dist/adapters/formal-verification/tla-precheck.d.ts.map +1 -0
  12. package/dist/adapters/formal-verification/tla-precheck.js +270 -0
  13. package/dist/adapters/formal-verification/tla-precheck.js.map +1 -0
  14. package/dist/adapters/formal-verification/types.d.ts +37 -0
  15. package/dist/adapters/formal-verification/types.d.ts.map +1 -0
  16. package/dist/adapters/formal-verification/types.js +6 -0
  17. package/dist/adapters/formal-verification/types.js.map +1 -0
  18. package/dist/adapters/review/semgrep.d.ts +12 -0
  19. package/dist/adapters/review/semgrep.d.ts.map +1 -0
  20. package/dist/adapters/review/semgrep.js +175 -0
  21. package/dist/adapters/review/semgrep.js.map +1 -0
  22. package/dist/adapters/review/types.d.ts +14 -0
  23. package/dist/adapters/review/types.d.ts.map +1 -0
  24. package/dist/adapters/review/types.js +6 -0
  25. package/dist/adapters/review/types.js.map +1 -0
  26. package/dist/adapters/skills/filesystem.d.ts +18 -0
  27. package/dist/adapters/skills/filesystem.d.ts.map +1 -0
  28. package/dist/adapters/skills/filesystem.js +398 -0
  29. package/dist/adapters/skills/filesystem.js.map +1 -0
  30. package/dist/adapters/skills/types.d.ts +19 -0
  31. package/dist/adapters/skills/types.d.ts.map +1 -0
  32. package/dist/adapters/skills/types.js +6 -0
  33. package/dist/adapters/skills/types.js.map +1 -0
  34. package/dist/adapters/sql-executor/direct-postgres.d.ts +15 -0
  35. package/dist/adapters/sql-executor/direct-postgres.d.ts.map +1 -0
  36. package/dist/adapters/sql-executor/direct-postgres.js +33 -0
  37. package/dist/adapters/sql-executor/direct-postgres.js.map +1 -0
  38. package/dist/adapters/sql-executor/supabase-management-api.d.ts +17 -0
  39. package/dist/adapters/sql-executor/supabase-management-api.d.ts.map +1 -0
  40. package/dist/adapters/sql-executor/supabase-management-api.js +40 -0
  41. package/dist/adapters/sql-executor/supabase-management-api.js.map +1 -0
  42. package/dist/adapters/sql-executor/types.d.ts +15 -0
  43. package/dist/adapters/sql-executor/types.d.ts.map +1 -0
  44. package/dist/adapters/sql-executor/types.js +6 -0
  45. package/dist/adapters/sql-executor/types.js.map +1 -0
  46. package/dist/adapters/workflow-state/in-memory.d.ts +69 -0
  47. package/dist/adapters/workflow-state/in-memory.d.ts.map +1 -0
  48. package/dist/adapters/workflow-state/in-memory.js +444 -0
  49. package/dist/adapters/workflow-state/in-memory.js.map +1 -0
  50. package/dist/adapters/workflow-state/supabase.d.ts +55 -0
  51. package/dist/adapters/workflow-state/supabase.d.ts.map +1 -0
  52. package/dist/adapters/workflow-state/supabase.js +823 -0
  53. package/dist/adapters/workflow-state/supabase.js.map +1 -0
  54. package/dist/adapters/workflow-state/types.d.ts +55 -0
  55. package/dist/adapters/workflow-state/types.d.ts.map +1 -0
  56. package/dist/adapters/workflow-state/types.js +6 -0
  57. package/dist/adapters/workflow-state/types.js.map +1 -0
  58. package/dist/cli.d.ts +3 -0
  59. package/dist/cli.d.ts.map +1 -0
  60. package/dist/cli.js +52 -0
  61. package/dist/cli.js.map +1 -0
  62. package/dist/config.d.ts +44 -0
  63. package/dist/config.d.ts.map +1 -0
  64. package/dist/config.js +115 -0
  65. package/dist/config.js.map +1 -0
  66. package/dist/domain/actors.d.ts +10 -0
  67. package/dist/domain/actors.d.ts.map +1 -0
  68. package/dist/domain/actors.js +60 -0
  69. package/dist/domain/actors.js.map +1 -0
  70. package/dist/domain/development-evals.d.ts +9 -0
  71. package/dist/domain/development-evals.d.ts.map +1 -0
  72. package/dist/domain/development-evals.js +164 -0
  73. package/dist/domain/development-evals.js.map +1 -0
  74. package/dist/domain/matching.d.ts +8 -0
  75. package/dist/domain/matching.d.ts.map +1 -0
  76. package/dist/domain/matching.js +24 -0
  77. package/dist/domain/matching.js.map +1 -0
  78. package/dist/domain/phases.d.ts +10 -0
  79. package/dist/domain/phases.d.ts.map +1 -0
  80. package/dist/domain/phases.js +165 -0
  81. package/dist/domain/phases.js.map +1 -0
  82. package/dist/domain/quality-gates.d.ts +7 -0
  83. package/dist/domain/quality-gates.d.ts.map +1 -0
  84. package/dist/domain/quality-gates.js +8 -0
  85. package/dist/domain/quality-gates.js.map +1 -0
  86. package/dist/domain/resonance.d.ts +33 -0
  87. package/dist/domain/resonance.d.ts.map +1 -0
  88. package/dist/domain/resonance.js +100 -0
  89. package/dist/domain/resonance.js.map +1 -0
  90. package/dist/domain/tasks.d.ts +9 -0
  91. package/dist/domain/tasks.d.ts.map +1 -0
  92. package/dist/domain/tasks.js +57 -0
  93. package/dist/domain/tasks.js.map +1 -0
  94. package/dist/init.d.ts +7 -0
  95. package/dist/init.d.ts.map +1 -0
  96. package/dist/init.js +387 -0
  97. package/dist/init.js.map +1 -0
  98. package/dist/schemas.d.ts +366 -0
  99. package/dist/schemas.d.ts.map +1 -0
  100. package/dist/schemas.js +184 -0
  101. package/dist/schemas.js.map +1 -0
  102. package/dist/server.d.ts +7 -0
  103. package/dist/server.d.ts.map +1 -0
  104. package/dist/server.js +243 -0
  105. package/dist/server.js.map +1 -0
  106. package/dist/tools/apply-migrations.d.ts +21 -0
  107. package/dist/tools/apply-migrations.d.ts.map +1 -0
  108. package/dist/tools/apply-migrations.js +286 -0
  109. package/dist/tools/apply-migrations.js.map +1 -0
  110. package/dist/tools/archive-feature-release.d.ts +13 -0
  111. package/dist/tools/archive-feature-release.d.ts.map +1 -0
  112. package/dist/tools/archive-feature-release.js +182 -0
  113. package/dist/tools/archive-feature-release.js.map +1 -0
  114. package/dist/tools/capture-learning.d.ts +9 -0
  115. package/dist/tools/capture-learning.d.ts.map +1 -0
  116. package/dist/tools/capture-learning.js +53 -0
  117. package/dist/tools/capture-learning.js.map +1 -0
  118. package/dist/tools/explore-knowledge.d.ts +9 -0
  119. package/dist/tools/explore-knowledge.d.ts.map +1 -0
  120. package/dist/tools/explore-knowledge.js +142 -0
  121. package/dist/tools/explore-knowledge.js.map +1 -0
  122. package/dist/tools/get-claims-needing-review.d.ts +8 -0
  123. package/dist/tools/get-claims-needing-review.d.ts.map +1 -0
  124. package/dist/tools/get-claims-needing-review.js +21 -0
  125. package/dist/tools/get-claims-needing-review.js.map +1 -0
  126. package/dist/tools/get-development-eval-status.d.ts +8 -0
  127. package/dist/tools/get-development-eval-status.d.ts.map +1 -0
  128. package/dist/tools/get-development-eval-status.js +49 -0
  129. package/dist/tools/get-development-eval-status.js.map +1 -0
  130. package/dist/tools/get-feature-status.d.ts +8 -0
  131. package/dist/tools/get-feature-status.d.ts.map +1 -0
  132. package/dist/tools/get-feature-status.js +68 -0
  133. package/dist/tools/get-feature-status.js.map +1 -0
  134. package/dist/tools/get-next-phase.d.ts +8 -0
  135. package/dist/tools/get-next-phase.d.ts.map +1 -0
  136. package/dist/tools/get-next-phase.js +26 -0
  137. package/dist/tools/get-next-phase.js.map +1 -0
  138. package/dist/tools/prepare-phase-context.d.ts +9 -0
  139. package/dist/tools/prepare-phase-context.d.ts.map +1 -0
  140. package/dist/tools/prepare-phase-context.js +151 -0
  141. package/dist/tools/prepare-phase-context.js.map +1 -0
  142. package/dist/tools/record-commit.d.ts +8 -0
  143. package/dist/tools/record-commit.d.ts.map +1 -0
  144. package/dist/tools/record-commit.js +28 -0
  145. package/dist/tools/record-commit.js.map +1 -0
  146. package/dist/tools/record-eval-plan.d.ts +8 -0
  147. package/dist/tools/record-eval-plan.d.ts.map +1 -0
  148. package/dist/tools/record-eval-plan.js +40 -0
  149. package/dist/tools/record-eval-plan.js.map +1 -0
  150. package/dist/tools/record-eval-run.d.ts +8 -0
  151. package/dist/tools/record-eval-run.d.ts.map +1 -0
  152. package/dist/tools/record-eval-run.js +42 -0
  153. package/dist/tools/record-eval-run.js.map +1 -0
  154. package/dist/tools/record-merge.d.ts +8 -0
  155. package/dist/tools/record-merge.d.ts.map +1 -0
  156. package/dist/tools/record-merge.js +16 -0
  157. package/dist/tools/record-merge.js.map +1 -0
  158. package/dist/tools/record-phase-artifact.d.ts +8 -0
  159. package/dist/tools/record-phase-artifact.d.ts.map +1 -0
  160. package/dist/tools/record-phase-artifact.js +26 -0
  161. package/dist/tools/record-phase-artifact.js.map +1 -0
  162. package/dist/tools/record-phase-result.d.ts +9 -0
  163. package/dist/tools/record-phase-result.d.ts.map +1 -0
  164. package/dist/tools/record-phase-result.js +122 -0
  165. package/dist/tools/record-phase-result.js.map +1 -0
  166. package/dist/tools/record-pull-request.d.ts +8 -0
  167. package/dist/tools/record-pull-request.d.ts.map +1 -0
  168. package/dist/tools/record-pull-request.js +16 -0
  169. package/dist/tools/record-pull-request.js.map +1 -0
  170. package/dist/tools/record-quality-gate.d.ts +8 -0
  171. package/dist/tools/record-quality-gate.d.ts.map +1 -0
  172. package/dist/tools/record-quality-gate.js +26 -0
  173. package/dist/tools/record-quality-gate.js.map +1 -0
  174. package/dist/tools/record-watcher-review.d.ts +8 -0
  175. package/dist/tools/record-watcher-review.d.ts.map +1 -0
  176. package/dist/tools/record-watcher-review.js +18 -0
  177. package/dist/tools/record-watcher-review.js.map +1 -0
  178. package/dist/tools/run-policy-checks.d.ts +8 -0
  179. package/dist/tools/run-policy-checks.d.ts.map +1 -0
  180. package/dist/tools/run-policy-checks.js +38 -0
  181. package/dist/tools/run-policy-checks.js.map +1 -0
  182. package/dist/tools/run-review-checks.d.ts +9 -0
  183. package/dist/tools/run-review-checks.d.ts.map +1 -0
  184. package/dist/tools/run-review-checks.js +45 -0
  185. package/dist/tools/run-review-checks.js.map +1 -0
  186. package/dist/tools/start-feature.d.ts +8 -0
  187. package/dist/tools/start-feature.d.ts.map +1 -0
  188. package/dist/tools/start-feature.js +33 -0
  189. package/dist/tools/start-feature.js.map +1 -0
  190. package/dist/tools/submit-claim.d.ts +8 -0
  191. package/dist/tools/submit-claim.d.ts.map +1 -0
  192. package/dist/tools/submit-claim.js +45 -0
  193. package/dist/tools/submit-claim.js.map +1 -0
  194. package/dist/tools/verify-claims.d.ts +8 -0
  195. package/dist/tools/verify-claims.d.ts.map +1 -0
  196. package/dist/tools/verify-claims.js +39 -0
  197. package/dist/tools/verify-claims.js.map +1 -0
  198. package/dist/tools/verify-design.d.ts +8 -0
  199. package/dist/tools/verify-design.d.ts.map +1 -0
  200. package/dist/tools/verify-design.js +31 -0
  201. package/dist/tools/verify-design.js.map +1 -0
  202. package/dist/types.d.ts +333 -0
  203. package/dist/types.d.ts.map +1 -0
  204. package/dist/types.js +52 -0
  205. package/dist/types.js.map +1 -0
  206. package/dist/utils.d.ts +24 -0
  207. package/dist/utils.d.ts.map +1 -0
  208. package/dist/utils.js +50 -0
  209. package/dist/utils.js.map +1 -0
  210. package/migrations/001_schema.sql +795 -0
  211. package/migrations/002_functions.sql +2126 -0
  212. package/migrations/003_views.sql +599 -0
  213. package/migrations/004_seed.sql +106 -0
  214. package/migrations/005_odin_v2_schema.sql +217 -0
  215. package/migrations/006_odin_v2_functions.sql +671 -0
  216. package/migrations/007_odin_v2_phase_alignment.sql +554 -0
  217. package/migrations/008_related_learnings.sql +80 -0
  218. package/migrations/README.md +23 -0
  219. 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';