@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,823 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase Workflow State Adapter
|
|
3
|
+
* Version: 0.1.0
|
|
4
|
+
*/
|
|
5
|
+
import { createClient } from '@supabase/supabase-js';
|
|
6
|
+
import { formatOpenGateSummary } from '../../domain/quality-gates.js';
|
|
7
|
+
function getSingleRpcRow(data, operation) {
|
|
8
|
+
if (Array.isArray(data)) {
|
|
9
|
+
const row = data[0];
|
|
10
|
+
if (row != null && typeof row === 'object' && !Array.isArray(row)) {
|
|
11
|
+
return row;
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`Failed to ${operation}: No row returned.`);
|
|
14
|
+
}
|
|
15
|
+
if (data != null && typeof data === 'object') {
|
|
16
|
+
return data;
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`Failed to ${operation}: RPC returned an unexpected shape.`);
|
|
19
|
+
}
|
|
20
|
+
function toFeatureRecord(row) {
|
|
21
|
+
return {
|
|
22
|
+
id: String(row.feature_id ?? row.id),
|
|
23
|
+
name: String(row.feature_name ?? row.name),
|
|
24
|
+
status: String(row.status),
|
|
25
|
+
current_phase: String(row.current_phase),
|
|
26
|
+
complexity_level: Number(row.complexity_level),
|
|
27
|
+
severity: String(row.severity),
|
|
28
|
+
requirements_path: row.requirements_path == null ? undefined : String(row.requirements_path),
|
|
29
|
+
dev_initials: row.dev_initials == null ? undefined : String(row.dev_initials),
|
|
30
|
+
branch_name: row.branch_name == null ? undefined : String(row.branch_name),
|
|
31
|
+
base_branch: row.base_branch == null ? undefined : String(row.base_branch),
|
|
32
|
+
author: row.author == null ? undefined : String(row.author),
|
|
33
|
+
created_at: String(row.created_at ?? new Date().toISOString()),
|
|
34
|
+
updated_at: String(row.updated_at ?? new Date().toISOString()),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function requireSupabaseConfig(config) {
|
|
38
|
+
const url = config.supabase?.url;
|
|
39
|
+
const secret_key = config.supabase?.secret_key;
|
|
40
|
+
if (!url || !secret_key) {
|
|
41
|
+
throw new Error('Supabase runtime mode requires SUPABASE_URL and SUPABASE_SECRET_KEY via .odin/config.yaml interpolation or environment variables.');
|
|
42
|
+
}
|
|
43
|
+
return { url, secret_key };
|
|
44
|
+
}
|
|
45
|
+
function toQualityGateRecord(row) {
|
|
46
|
+
return {
|
|
47
|
+
id: Number(row.id),
|
|
48
|
+
feature_id: String(row.feature_id),
|
|
49
|
+
gate_name: String(row.gate_name),
|
|
50
|
+
phase: String(row.phase),
|
|
51
|
+
status: String(row.status),
|
|
52
|
+
approver: String(row.approver),
|
|
53
|
+
approved_at: String(row.approved_at),
|
|
54
|
+
approval_notes: typeof row.approval_notes === 'string' ? row.approval_notes : null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function shouldTransitionPhaseResult(result) {
|
|
58
|
+
return result.outcome !== 'blocked' && result.next_phase != null && result.next_phase !== result.phase;
|
|
59
|
+
}
|
|
60
|
+
export class SupabaseWorkflowStateAdapter {
|
|
61
|
+
client;
|
|
62
|
+
constructor(config) {
|
|
63
|
+
const { url, secret_key } = requireSupabaseConfig(config);
|
|
64
|
+
this.client = createClient(url, secret_key, {
|
|
65
|
+
auth: {
|
|
66
|
+
autoRefreshToken: false,
|
|
67
|
+
persistSession: false,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async startFeature(feature) {
|
|
72
|
+
const { data, error } = await this.client.rpc('create_feature', {
|
|
73
|
+
p_id: feature.id,
|
|
74
|
+
p_name: feature.name,
|
|
75
|
+
p_complexity_level: feature.complexity_level,
|
|
76
|
+
p_severity: feature.severity,
|
|
77
|
+
p_epic_id: null,
|
|
78
|
+
p_requirements_path: feature.requirements_path ?? null,
|
|
79
|
+
p_created_by: 'odin-runtime',
|
|
80
|
+
p_dev_initials: feature.dev_initials ?? null,
|
|
81
|
+
p_base_branch: feature.base_branch ?? 'main',
|
|
82
|
+
p_author: feature.author ?? null,
|
|
83
|
+
});
|
|
84
|
+
if (error != null || data == null) {
|
|
85
|
+
throw new Error(`Failed to create feature in Supabase: ${error?.message ?? 'No result returned.'}`);
|
|
86
|
+
}
|
|
87
|
+
const created = getSingleRpcRow(data, 'create feature in Supabase');
|
|
88
|
+
return {
|
|
89
|
+
id: String(created.feature_id),
|
|
90
|
+
name: String(created.feature_name),
|
|
91
|
+
status: String(created.status),
|
|
92
|
+
current_phase: '0',
|
|
93
|
+
complexity_level: Number(created.complexity),
|
|
94
|
+
severity: String(created.severity_level),
|
|
95
|
+
requirements_path: feature.requirements_path,
|
|
96
|
+
dev_initials: feature.dev_initials,
|
|
97
|
+
branch_name: created.branch_name == null ? undefined : String(created.branch_name),
|
|
98
|
+
base_branch: created.base_branch == null ? undefined : String(created.base_branch),
|
|
99
|
+
author: created.author == null ? undefined : String(created.author),
|
|
100
|
+
created_at: new Date().toISOString(),
|
|
101
|
+
updated_at: new Date().toISOString(),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async getFeature(feature_id) {
|
|
105
|
+
const { data, error } = await this.client.rpc('get_feature_status', {
|
|
106
|
+
p_feature_id: feature_id,
|
|
107
|
+
});
|
|
108
|
+
if (error != null) {
|
|
109
|
+
throw new Error(`Failed to fetch feature status from Supabase: ${error.message}`);
|
|
110
|
+
}
|
|
111
|
+
if (data == null) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return toFeatureRecord(getSingleRpcRow(data, 'fetch feature status from Supabase'));
|
|
115
|
+
}
|
|
116
|
+
async recordPhaseArtifact(artifact) {
|
|
117
|
+
const { data, error } = await this.client.rpc('record_phase_output', {
|
|
118
|
+
p_feature_id: artifact.feature_id,
|
|
119
|
+
p_phase: artifact.phase,
|
|
120
|
+
p_output_type: artifact.output_type,
|
|
121
|
+
p_content: artifact.content,
|
|
122
|
+
p_created_by: artifact.created_by,
|
|
123
|
+
});
|
|
124
|
+
if (error != null || data == null) {
|
|
125
|
+
throw new Error(`Failed to record phase artifact: ${error?.message ?? 'No result returned.'}`);
|
|
126
|
+
}
|
|
127
|
+
const row = data;
|
|
128
|
+
return {
|
|
129
|
+
id: String(row.id),
|
|
130
|
+
feature_id: String(row.feature_id),
|
|
131
|
+
phase: String(row.phase),
|
|
132
|
+
output_type: String(row.output_type),
|
|
133
|
+
content: row.content,
|
|
134
|
+
created_by: String(row.created_by),
|
|
135
|
+
created_at: String(row.created_at),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async listPhaseArtifacts(feature_id) {
|
|
139
|
+
const { data, error } = await this.client
|
|
140
|
+
.from('phase_outputs')
|
|
141
|
+
.select('*')
|
|
142
|
+
.eq('feature_id', feature_id)
|
|
143
|
+
.order('phase', { ascending: true })
|
|
144
|
+
.order('created_at', { ascending: true });
|
|
145
|
+
if (error != null) {
|
|
146
|
+
throw new Error(`Failed to list phase artifacts from Supabase: ${error.message}`);
|
|
147
|
+
}
|
|
148
|
+
if (data == null) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
return data.map((row) => ({
|
|
152
|
+
id: String(row.id),
|
|
153
|
+
feature_id: String(row.feature_id),
|
|
154
|
+
phase: String(row.phase),
|
|
155
|
+
output_type: String(row.output_type),
|
|
156
|
+
content: row.content,
|
|
157
|
+
created_by: String(row.created_by),
|
|
158
|
+
created_at: String(row.created_at),
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
async recordPhaseResult(result) {
|
|
162
|
+
if (shouldTransitionPhaseResult(result)) {
|
|
163
|
+
const { error } = await this.client.rpc('transition_phase', {
|
|
164
|
+
p_feature_id: result.feature_id,
|
|
165
|
+
p_to_phase: result.next_phase,
|
|
166
|
+
p_transitioned_by: result.created_by,
|
|
167
|
+
p_notes: result.summary,
|
|
168
|
+
});
|
|
169
|
+
if (error != null) {
|
|
170
|
+
throw new Error(`Failed to transition phase in Supabase: ${error.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const status = result.outcome === 'blocked'
|
|
174
|
+
? 'BLOCKED'
|
|
175
|
+
: result.next_phase === '10' || result.phase === '10'
|
|
176
|
+
? 'COMPLETED'
|
|
177
|
+
: 'IN_PROGRESS';
|
|
178
|
+
const { error } = await this.client
|
|
179
|
+
.from('features')
|
|
180
|
+
.update({
|
|
181
|
+
status,
|
|
182
|
+
updated_at: new Date().toISOString(),
|
|
183
|
+
})
|
|
184
|
+
.eq('id', result.feature_id);
|
|
185
|
+
if (error != null) {
|
|
186
|
+
throw new Error(`Failed to update feature result state in Supabase: ${error.message}`);
|
|
187
|
+
}
|
|
188
|
+
return this.getFeature(result.feature_id);
|
|
189
|
+
}
|
|
190
|
+
async listOpenBlockers(feature_id) {
|
|
191
|
+
const { data, error } = await this.client
|
|
192
|
+
.from('blockers')
|
|
193
|
+
.select('title, severity, status')
|
|
194
|
+
.eq('feature_id', feature_id)
|
|
195
|
+
.in('status', ['OPEN', 'IN_PROGRESS'])
|
|
196
|
+
.order('created_at', { ascending: false });
|
|
197
|
+
if (error != null) {
|
|
198
|
+
throw new Error(`Failed to list blockers from Supabase: ${error.message}`);
|
|
199
|
+
}
|
|
200
|
+
if (data == null) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
return data.map((row) => `${row.title} (${row.severity}/${row.status})`);
|
|
204
|
+
}
|
|
205
|
+
async listOpenGates(feature_id) {
|
|
206
|
+
const gates = await this.listOpenGateRecords(feature_id);
|
|
207
|
+
return gates.map(formatOpenGateSummary);
|
|
208
|
+
}
|
|
209
|
+
async listOpenGateRecords(feature_id) {
|
|
210
|
+
const { data, error } = await this.client
|
|
211
|
+
.from('quality_gates')
|
|
212
|
+
.select('id, feature_id, gate_name, status, phase, approver, approved_at, approval_notes')
|
|
213
|
+
.eq('feature_id', feature_id)
|
|
214
|
+
.in('status', ['PENDING', 'REJECTED'])
|
|
215
|
+
.order('phase', { ascending: true })
|
|
216
|
+
.order('approved_at', { ascending: true });
|
|
217
|
+
if (error != null) {
|
|
218
|
+
throw new Error(`Failed to list quality gate records from Supabase: ${error.message}`);
|
|
219
|
+
}
|
|
220
|
+
if (data == null) {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
return data.map(toQualityGateRecord);
|
|
224
|
+
}
|
|
225
|
+
async listOpenFindings(feature_id) {
|
|
226
|
+
const { data, error } = await this.client
|
|
227
|
+
.from('security_findings')
|
|
228
|
+
.select('severity, message, file_path, resolved')
|
|
229
|
+
.eq('feature_id', feature_id)
|
|
230
|
+
.eq('resolved', false)
|
|
231
|
+
.order('created_at', { ascending: false });
|
|
232
|
+
if (error != null) {
|
|
233
|
+
throw new Error(`Failed to list security findings from Supabase: ${error.message}`);
|
|
234
|
+
}
|
|
235
|
+
if (data == null) {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
return data.map((row) => {
|
|
239
|
+
const file_path = row.file_path == null ? 'unknown-file' : String(row.file_path);
|
|
240
|
+
return `${row.severity}: ${row.message} (${file_path})`;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
async listPendingClaims(feature_id) {
|
|
244
|
+
const { data: claims, error: claims_error } = await this.client
|
|
245
|
+
.from('agent_claims')
|
|
246
|
+
.select('id, claim_type, agent_name, created_at')
|
|
247
|
+
.eq('feature_id', feature_id)
|
|
248
|
+
.order('created_at', { ascending: true });
|
|
249
|
+
if (claims_error != null) {
|
|
250
|
+
throw new Error(`Failed to list agent claims from Supabase: ${claims_error.message}`);
|
|
251
|
+
}
|
|
252
|
+
if (claims == null || claims.length === 0) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
const claim_ids = claims.map((claim) => String(claim.id));
|
|
256
|
+
const { data: reviews, error: reviews_error } = await this.client
|
|
257
|
+
.from('watcher_reviews')
|
|
258
|
+
.select('claim_id, verdict, reviewed_at')
|
|
259
|
+
.in('claim_id', claim_ids);
|
|
260
|
+
const { data: verdict_rows, error: verdicts_error } = await this.client
|
|
261
|
+
.from('policy_verdicts')
|
|
262
|
+
.select('claim_id, verdict, created_at')
|
|
263
|
+
.in('claim_id', claim_ids);
|
|
264
|
+
if (verdicts_error != null) {
|
|
265
|
+
throw new Error(`Failed to list policy verdicts from Supabase: ${verdicts_error.message}`);
|
|
266
|
+
}
|
|
267
|
+
if (reviews_error != null) {
|
|
268
|
+
throw new Error(`Failed to list watcher reviews from Supabase: ${reviews_error.message}`);
|
|
269
|
+
}
|
|
270
|
+
const latest_verdict = new Map();
|
|
271
|
+
for (const verdict of (verdict_rows ?? [])) {
|
|
272
|
+
const claim_id = String(verdict.claim_id);
|
|
273
|
+
const existing = latest_verdict.get(claim_id);
|
|
274
|
+
if (existing == null ||
|
|
275
|
+
new Date(String(verdict.created_at)).getTime() > new Date(String(existing.created_at)).getTime()) {
|
|
276
|
+
latest_verdict.set(claim_id, verdict);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const latest_review = new Map();
|
|
280
|
+
for (const review of (reviews ?? [])) {
|
|
281
|
+
const claim_id = String(review.claim_id);
|
|
282
|
+
const existing = latest_review.get(claim_id);
|
|
283
|
+
if (existing == null ||
|
|
284
|
+
new Date(String(review.reviewed_at)).getTime() > new Date(String(existing.reviewed_at)).getTime()) {
|
|
285
|
+
latest_review.set(claim_id, review);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return claims
|
|
289
|
+
.map((claim) => {
|
|
290
|
+
const claim_id = String(claim.id);
|
|
291
|
+
const review = latest_review.get(claim_id);
|
|
292
|
+
const verdict = latest_verdict.get(claim_id);
|
|
293
|
+
if (review != null && String(review.verdict) === 'PASS') {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
if (review != null && String(review.verdict) === 'FAIL') {
|
|
297
|
+
return `${claim.claim_type} by ${claim.agent_name} (watcher FAIL)`;
|
|
298
|
+
}
|
|
299
|
+
if (verdict == null) {
|
|
300
|
+
return `${claim.claim_type} by ${claim.agent_name} (pending policy)`;
|
|
301
|
+
}
|
|
302
|
+
if (String(verdict.verdict) === 'PASS') {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
return `${claim.claim_type} by ${claim.agent_name} (${verdict.verdict})`;
|
|
306
|
+
})
|
|
307
|
+
.filter((value) => value != null);
|
|
308
|
+
}
|
|
309
|
+
async listClaimVerificationStatus(feature_id) {
|
|
310
|
+
const { data, error } = await this.client.rpc('get_feature_claims', {
|
|
311
|
+
p_feature_id: feature_id,
|
|
312
|
+
});
|
|
313
|
+
if (error != null) {
|
|
314
|
+
throw new Error(`Failed to list claim verification state from Supabase: ${error.message}`);
|
|
315
|
+
}
|
|
316
|
+
if (data == null) {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
return data.map((row) => ({
|
|
320
|
+
claim_id: String(row.claim_id),
|
|
321
|
+
claim_type: String(row.claim_type),
|
|
322
|
+
agent_name: String(row.agent_name),
|
|
323
|
+
risk_level: String(row.risk_level),
|
|
324
|
+
policy_verdict: row.policy_verdict == null ? null : String(row.policy_verdict),
|
|
325
|
+
watcher_verdict: row.watcher_verdict == null ? null : String(row.watcher_verdict),
|
|
326
|
+
final_status: String(row.final_status),
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
async submitClaim(claim) {
|
|
330
|
+
const { data, error } = await this.client.rpc('submit_claim', {
|
|
331
|
+
p_feature_id: claim.feature_id,
|
|
332
|
+
p_phase: claim.phase,
|
|
333
|
+
p_agent_name: claim.agent_name,
|
|
334
|
+
p_claim_type: claim.claim_type,
|
|
335
|
+
p_description: claim.claim_description,
|
|
336
|
+
p_evidence_refs: claim.evidence_refs,
|
|
337
|
+
p_risk_level: claim.risk_level,
|
|
338
|
+
p_invocation_id: claim.invocation_id,
|
|
339
|
+
});
|
|
340
|
+
if (error != null || data == null) {
|
|
341
|
+
throw new Error(`Failed to submit claim: ${error?.message ?? 'No result returned.'}`);
|
|
342
|
+
}
|
|
343
|
+
const row = getSingleRpcRow(data, 'submit claim');
|
|
344
|
+
return {
|
|
345
|
+
id: String(row.claim_id),
|
|
346
|
+
feature_id: String(row.feature_id),
|
|
347
|
+
phase: String(row.phase),
|
|
348
|
+
agent_name: claim.agent_name,
|
|
349
|
+
invocation_id: claim.invocation_id,
|
|
350
|
+
claim_type: String(row.claim_type),
|
|
351
|
+
claim_description: claim.claim_description,
|
|
352
|
+
evidence_refs: claim.evidence_refs,
|
|
353
|
+
risk_level: String(row.risk_level),
|
|
354
|
+
created_at: String(row.created_at),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
async runPolicyChecks(feature_id) {
|
|
358
|
+
const { data, error } = await this.client.rpc('run_policy_checks', {
|
|
359
|
+
p_feature_id: feature_id,
|
|
360
|
+
});
|
|
361
|
+
if (error != null) {
|
|
362
|
+
throw new Error(`Failed to run policy checks: ${error.message}`);
|
|
363
|
+
}
|
|
364
|
+
if (data == null) {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
return data.map((row) => ({
|
|
368
|
+
claim_id: String(row.claim_id),
|
|
369
|
+
claim_type: String(row.claim_type),
|
|
370
|
+
verdict: String(row.verdict),
|
|
371
|
+
needs_watcher: Boolean(row.needs_watcher),
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
async listClaimsNeedingReview(feature_id) {
|
|
375
|
+
const { data, error } = await this.client.rpc('get_claims_needing_review', {
|
|
376
|
+
p_feature_id: feature_id ?? null,
|
|
377
|
+
});
|
|
378
|
+
if (error != null) {
|
|
379
|
+
throw new Error(`Failed to list claims needing watcher review: ${error.message}`);
|
|
380
|
+
}
|
|
381
|
+
if (data == null) {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
return data.map((row) => ({
|
|
385
|
+
claim_id: String(row.claim_id),
|
|
386
|
+
feature_id: String(row.feature_id),
|
|
387
|
+
phase: String(row.phase),
|
|
388
|
+
agent_name: String(row.agent_name),
|
|
389
|
+
claim_type: String(row.claim_type),
|
|
390
|
+
claim_description: String(row.claim_description),
|
|
391
|
+
evidence_refs: row.evidence_refs != null && typeof row.evidence_refs === 'object' && !Array.isArray(row.evidence_refs)
|
|
392
|
+
? row.evidence_refs
|
|
393
|
+
: {},
|
|
394
|
+
risk_level: String(row.risk_level),
|
|
395
|
+
policy_verdict: row.policy_verdict == null ? null : String(row.policy_verdict),
|
|
396
|
+
policy_reason: row.policy_reason == null ? null : String(row.policy_reason),
|
|
397
|
+
created_at: String(row.created_at),
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
async recordWatcherReview(review) {
|
|
401
|
+
const { data, error } = await this.client.rpc('record_watcher_review', {
|
|
402
|
+
p_claim_id: review.claim_id,
|
|
403
|
+
p_verdict: review.verdict,
|
|
404
|
+
p_reasoning: review.reasoning,
|
|
405
|
+
p_watcher_agent: review.watcher_agent,
|
|
406
|
+
p_confidence: review.confidence,
|
|
407
|
+
});
|
|
408
|
+
if (error != null || data == null) {
|
|
409
|
+
throw new Error(`Failed to record watcher review: ${error?.message ?? 'No result returned.'}`);
|
|
410
|
+
}
|
|
411
|
+
const row = getSingleRpcRow(data, 'record watcher review');
|
|
412
|
+
return {
|
|
413
|
+
id: String(row.review_id),
|
|
414
|
+
claim_id: String(row.claim_id),
|
|
415
|
+
verdict: String(row.verdict),
|
|
416
|
+
confidence: Number(row.confidence),
|
|
417
|
+
reasoning: review.reasoning,
|
|
418
|
+
watcher_agent: review.watcher_agent,
|
|
419
|
+
reviewed_at: new Date().toISOString(),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
async getLatestFeatureEval(feature_id) {
|
|
423
|
+
const { data, error } = await this.client
|
|
424
|
+
.from('feature_evals')
|
|
425
|
+
.select('id, feature_id, computed_at, efficiency_score, quality_score, overall_score, health_status')
|
|
426
|
+
.eq('feature_id', feature_id)
|
|
427
|
+
.order('computed_at', { ascending: false })
|
|
428
|
+
.limit(1)
|
|
429
|
+
.maybeSingle();
|
|
430
|
+
if (error != null) {
|
|
431
|
+
throw new Error(`Failed to fetch feature eval from Supabase: ${error.message}`);
|
|
432
|
+
}
|
|
433
|
+
if (data == null) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
const row = data;
|
|
437
|
+
return {
|
|
438
|
+
id: String(row.id),
|
|
439
|
+
feature_id: String(row.feature_id),
|
|
440
|
+
computed_at: String(row.computed_at),
|
|
441
|
+
efficiency_score: row.efficiency_score == null ? null : Number(row.efficiency_score),
|
|
442
|
+
quality_score: row.quality_score == null ? null : Number(row.quality_score),
|
|
443
|
+
overall_score: row.overall_score == null ? null : Number(row.overall_score),
|
|
444
|
+
health_status: String(row.health_status),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
async recordReviewCheck(check) {
|
|
448
|
+
const artifact = await this.recordPhaseArtifact({
|
|
449
|
+
id: check.id,
|
|
450
|
+
feature_id: check.feature_id,
|
|
451
|
+
phase: check.phase,
|
|
452
|
+
output_type: 'security_review_runtime',
|
|
453
|
+
content: {
|
|
454
|
+
tool: check.tool,
|
|
455
|
+
status: check.status,
|
|
456
|
+
summary: check.summary,
|
|
457
|
+
changed_files: check.changed_files,
|
|
458
|
+
initiated_by: check.initiated_by,
|
|
459
|
+
},
|
|
460
|
+
created_by: check.initiated_by,
|
|
461
|
+
created_at: check.created_at,
|
|
462
|
+
});
|
|
463
|
+
return {
|
|
464
|
+
...check,
|
|
465
|
+
id: artifact.id,
|
|
466
|
+
created_at: artifact.created_at,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
async listReviewChecks(feature_id) {
|
|
470
|
+
const artifacts = await this.listPhaseArtifacts(feature_id);
|
|
471
|
+
return artifacts
|
|
472
|
+
.filter((artifact) => artifact.output_type === 'security_review_runtime')
|
|
473
|
+
.map((artifact) => {
|
|
474
|
+
const content = artifact.content;
|
|
475
|
+
return {
|
|
476
|
+
id: artifact.id,
|
|
477
|
+
feature_id: artifact.feature_id,
|
|
478
|
+
phase: artifact.phase,
|
|
479
|
+
tool: String(content.tool ?? 'semgrep'),
|
|
480
|
+
status: String(content.status ?? 'queued'),
|
|
481
|
+
summary: String(content.summary ?? ''),
|
|
482
|
+
changed_files: Array.isArray(content.changed_files)
|
|
483
|
+
? content.changed_files.map((value) => String(value))
|
|
484
|
+
: [],
|
|
485
|
+
initiated_by: String(content.initiated_by ?? artifact.created_by),
|
|
486
|
+
created_at: artifact.created_at,
|
|
487
|
+
};
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
async captureLearning(learning) {
|
|
491
|
+
const { data, error } = await this.client
|
|
492
|
+
.from('learnings')
|
|
493
|
+
.insert({
|
|
494
|
+
feature_id: learning.feature_id,
|
|
495
|
+
category: learning.category,
|
|
496
|
+
title: learning.title,
|
|
497
|
+
content: learning.content,
|
|
498
|
+
confidence_score: 0.5,
|
|
499
|
+
validation_count: 0,
|
|
500
|
+
validated_by: [],
|
|
501
|
+
importance: 'MEDIUM',
|
|
502
|
+
tags: learning.tags.length > 0 ? learning.tags : ['odin-runtime'],
|
|
503
|
+
phase: learning.phase,
|
|
504
|
+
agent: learning.created_by,
|
|
505
|
+
created_by: learning.created_by,
|
|
506
|
+
})
|
|
507
|
+
.select('*')
|
|
508
|
+
.single();
|
|
509
|
+
if (error != null || data == null) {
|
|
510
|
+
throw new Error(`Failed to capture learning in Supabase: ${error?.message ?? 'No result returned.'}`);
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
id: String(data.id),
|
|
514
|
+
feature_id: String(data.feature_id),
|
|
515
|
+
phase: String(data.phase),
|
|
516
|
+
title: String(data.title),
|
|
517
|
+
content: String(data.content),
|
|
518
|
+
category: String(data.category),
|
|
519
|
+
tags: Array.isArray(data.tags) ? data.tags : [],
|
|
520
|
+
created_by: String(data.created_by),
|
|
521
|
+
created_at: String(data.created_at),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
async listLearnings(feature_id) {
|
|
525
|
+
const { data, error } = await this.client
|
|
526
|
+
.from('learnings')
|
|
527
|
+
.select('*')
|
|
528
|
+
.eq('feature_id', feature_id)
|
|
529
|
+
.order('created_at', { ascending: false });
|
|
530
|
+
if (error != null) {
|
|
531
|
+
throw new Error(`Failed to list learnings from Supabase: ${error.message}`);
|
|
532
|
+
}
|
|
533
|
+
if (data == null) {
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
return data.map((row) => ({
|
|
537
|
+
id: String(row.id),
|
|
538
|
+
feature_id: String(row.feature_id),
|
|
539
|
+
phase: String(row.phase),
|
|
540
|
+
title: String(row.title),
|
|
541
|
+
content: String(row.content),
|
|
542
|
+
category: String(row.category),
|
|
543
|
+
tags: Array.isArray(row.tags) ? row.tags : [],
|
|
544
|
+
created_by: String(row.created_by),
|
|
545
|
+
created_at: String(row.created_at),
|
|
546
|
+
}));
|
|
547
|
+
}
|
|
548
|
+
async findOpenAgentInvocation(feature_id, phase, agent_name) {
|
|
549
|
+
const { data, error } = await this.client
|
|
550
|
+
.from('agent_invocations')
|
|
551
|
+
.select('*')
|
|
552
|
+
.eq('feature_id', feature_id)
|
|
553
|
+
.eq('phase', phase)
|
|
554
|
+
.eq('agent_name', agent_name)
|
|
555
|
+
.is('ended_at', null)
|
|
556
|
+
.order('started_at', { ascending: false })
|
|
557
|
+
.limit(1)
|
|
558
|
+
.maybeSingle();
|
|
559
|
+
if (error != null) {
|
|
560
|
+
throw new Error(`Failed to find open agent invocation: ${error.message}`);
|
|
561
|
+
}
|
|
562
|
+
if (data == null) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
const row = data;
|
|
566
|
+
return {
|
|
567
|
+
id: String(row.id),
|
|
568
|
+
feature_id: String(row.feature_id),
|
|
569
|
+
phase: String(row.phase),
|
|
570
|
+
agent_name: String(row.agent_name),
|
|
571
|
+
operation: row.operation == null ? null : String(row.operation),
|
|
572
|
+
skills_used: Array.isArray(row.skills_used) ? row.skills_used.map((value) => String(value)) : [],
|
|
573
|
+
started_at: String(row.started_at),
|
|
574
|
+
ended_at: row.ended_at == null ? null : String(row.ended_at),
|
|
575
|
+
duration_ms: row.duration_ms == null ? null : Number(row.duration_ms),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
async startAgentInvocation(feature_id, phase, agent_name, operation, skills) {
|
|
579
|
+
const { data, error } = await this.client.rpc('start_agent_invocation', {
|
|
580
|
+
p_feature_id: feature_id,
|
|
581
|
+
p_phase: phase,
|
|
582
|
+
p_agent_name: agent_name,
|
|
583
|
+
p_operation: operation ?? null,
|
|
584
|
+
p_skills: skills ?? null,
|
|
585
|
+
});
|
|
586
|
+
if (error != null || data == null) {
|
|
587
|
+
throw new Error(`Failed to start agent invocation: ${error?.message ?? 'No result returned.'}`);
|
|
588
|
+
}
|
|
589
|
+
const row = getSingleRpcRow(data, 'start agent invocation');
|
|
590
|
+
return {
|
|
591
|
+
id: String(row.id),
|
|
592
|
+
feature_id: String(row.feature_id),
|
|
593
|
+
phase: String(row.phase),
|
|
594
|
+
agent_name: String(row.agent_name),
|
|
595
|
+
operation: row.operation == null ? null : String(row.operation),
|
|
596
|
+
skills_used: Array.isArray(row.skills_used) ? row.skills_used.map((v) => String(v)) : [],
|
|
597
|
+
started_at: String(row.started_at),
|
|
598
|
+
ended_at: row.ended_at == null ? null : String(row.ended_at),
|
|
599
|
+
duration_ms: row.duration_ms == null ? null : Number(row.duration_ms),
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
async completeAgentInvocation(invocation_id) {
|
|
603
|
+
const { data, error } = await this.client.rpc('end_agent_invocation', {
|
|
604
|
+
p_invocation_id: invocation_id,
|
|
605
|
+
});
|
|
606
|
+
if (error != null || data == null) {
|
|
607
|
+
throw new Error(`Failed to complete agent invocation: ${error?.message ?? 'No result returned.'}`);
|
|
608
|
+
}
|
|
609
|
+
const row = getSingleRpcRow(data, 'complete agent invocation');
|
|
610
|
+
return {
|
|
611
|
+
id: String(row.id),
|
|
612
|
+
feature_id: String(row.feature_id),
|
|
613
|
+
phase: String(row.phase),
|
|
614
|
+
agent_name: String(row.agent_name),
|
|
615
|
+
operation: row.operation == null ? null : String(row.operation),
|
|
616
|
+
skills_used: Array.isArray(row.skills_used) ? row.skills_used.map((v) => String(v)) : [],
|
|
617
|
+
started_at: String(row.started_at),
|
|
618
|
+
ended_at: row.ended_at == null ? null : String(row.ended_at),
|
|
619
|
+
duration_ms: row.duration_ms == null ? null : Number(row.duration_ms),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
async recordCommit(commit) {
|
|
623
|
+
const { data, error } = await this.client.rpc('record_commit', {
|
|
624
|
+
p_feature_id: commit.feature_id,
|
|
625
|
+
p_commit_hash: commit.commit_hash,
|
|
626
|
+
p_phase: commit.phase,
|
|
627
|
+
p_message: commit.message ?? null,
|
|
628
|
+
p_files_changed: commit.files_changed ?? null,
|
|
629
|
+
p_insertions: commit.insertions ?? null,
|
|
630
|
+
p_deletions: commit.deletions ?? null,
|
|
631
|
+
p_committed_by: commit.committed_by,
|
|
632
|
+
});
|
|
633
|
+
if (error != null || data == null) {
|
|
634
|
+
throw new Error(`Failed to record commit: ${error?.message ?? 'No result returned.'}`);
|
|
635
|
+
}
|
|
636
|
+
const row = getSingleRpcRow(data, 'record commit');
|
|
637
|
+
return {
|
|
638
|
+
feature_id: String(row.feature_id),
|
|
639
|
+
commit_hash: String(row.commit_hash),
|
|
640
|
+
phase: String(row.phase),
|
|
641
|
+
message: row.message == null ? undefined : String(row.message),
|
|
642
|
+
files_changed: row.files_changed == null ? undefined : Number(row.files_changed),
|
|
643
|
+
insertions: row.insertions == null ? undefined : Number(row.insertions),
|
|
644
|
+
deletions: row.deletions == null ? undefined : Number(row.deletions),
|
|
645
|
+
committed_at: String(row.committed_at ?? row.created_at ?? new Date().toISOString()),
|
|
646
|
+
committed_by: row.committed_by == null ? commit.committed_by : String(row.committed_by),
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
async recordPullRequest(feature_id, pr_url, pr_number) {
|
|
650
|
+
const { error } = await this.client.rpc('record_pr', {
|
|
651
|
+
p_feature_id: feature_id,
|
|
652
|
+
p_pr_url: pr_url,
|
|
653
|
+
p_pr_number: pr_number,
|
|
654
|
+
});
|
|
655
|
+
if (error != null) {
|
|
656
|
+
throw new Error(`Failed to record pull request: ${error.message}`);
|
|
657
|
+
}
|
|
658
|
+
return { feature_id, pr_url, pr_number };
|
|
659
|
+
}
|
|
660
|
+
async recordMerge(feature_id, merged_by) {
|
|
661
|
+
const { data, error } = await this.client.rpc('record_merge', {
|
|
662
|
+
p_feature_id: feature_id,
|
|
663
|
+
p_merged_by: merged_by,
|
|
664
|
+
});
|
|
665
|
+
if (error != null || data == null) {
|
|
666
|
+
throw new Error(`Failed to record merge: ${error?.message ?? 'No result returned.'}`);
|
|
667
|
+
}
|
|
668
|
+
const row = getSingleRpcRow(data, 'record merge');
|
|
669
|
+
return {
|
|
670
|
+
feature_id,
|
|
671
|
+
merged_at: String(row.merged_at ?? new Date().toISOString()),
|
|
672
|
+
merged_by,
|
|
673
|
+
pr_url: row.pr_url == null ? undefined : String(row.pr_url),
|
|
674
|
+
pr_number: row.pr_number == null ? undefined : Number(row.pr_number),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
async recordQualityGate(feature_id, gate_name, status, approver, notes, phase) {
|
|
678
|
+
const effective_phase = phase ??
|
|
679
|
+
(await (async () => {
|
|
680
|
+
const { data, error } = await this.client
|
|
681
|
+
.from('features')
|
|
682
|
+
.select('current_phase')
|
|
683
|
+
.eq('id', feature_id)
|
|
684
|
+
.single();
|
|
685
|
+
if (error != null) {
|
|
686
|
+
throw new Error(`Failed to resolve gate phase from feature: ${error.message}`);
|
|
687
|
+
}
|
|
688
|
+
return String(data.current_phase);
|
|
689
|
+
})());
|
|
690
|
+
const { data, error } = await this.client
|
|
691
|
+
.from('quality_gates')
|
|
692
|
+
.upsert({
|
|
693
|
+
feature_id,
|
|
694
|
+
gate_name,
|
|
695
|
+
phase: effective_phase,
|
|
696
|
+
status,
|
|
697
|
+
approver,
|
|
698
|
+
approved_at: new Date().toISOString(),
|
|
699
|
+
approval_notes: notes ?? null,
|
|
700
|
+
}, {
|
|
701
|
+
onConflict: 'feature_id,gate_name,phase',
|
|
702
|
+
})
|
|
703
|
+
.select('id')
|
|
704
|
+
.single();
|
|
705
|
+
if (error != null) {
|
|
706
|
+
throw new Error(`Failed to record quality gate: ${error.message}`);
|
|
707
|
+
}
|
|
708
|
+
const gate_id = Number(data.id);
|
|
709
|
+
const { error: audit_error } = await this.client.from('audit_log').insert({
|
|
710
|
+
feature_id,
|
|
711
|
+
operation: `GATE_${status}`,
|
|
712
|
+
agent_name: approver,
|
|
713
|
+
details: {
|
|
714
|
+
gate_id,
|
|
715
|
+
gate_name,
|
|
716
|
+
status,
|
|
717
|
+
phase: effective_phase,
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
if (audit_error != null) {
|
|
721
|
+
console.error(`[Odin Runtime] Failed to record audit log for quality gate ${gate_name}: ${audit_error.message}`);
|
|
722
|
+
}
|
|
723
|
+
return gate_id;
|
|
724
|
+
}
|
|
725
|
+
async computeFeatureEval(feature_id) {
|
|
726
|
+
const { error } = await this.client.rpc('compute_feature_eval', {
|
|
727
|
+
p_feature_id: feature_id,
|
|
728
|
+
});
|
|
729
|
+
if (error != null) {
|
|
730
|
+
throw new Error(`Failed to compute feature eval: ${error.message}`);
|
|
731
|
+
}
|
|
732
|
+
return this.getLatestFeatureEval(feature_id);
|
|
733
|
+
}
|
|
734
|
+
async recordSecurityFindings(feature_id, phase, findings, tool) {
|
|
735
|
+
if (findings.length === 0) {
|
|
736
|
+
return 0;
|
|
737
|
+
}
|
|
738
|
+
const rows = findings.map((finding) => ({
|
|
739
|
+
feature_id,
|
|
740
|
+
phase,
|
|
741
|
+
tool,
|
|
742
|
+
rule_id: finding.rule_id,
|
|
743
|
+
severity: finding.severity,
|
|
744
|
+
file_path: finding.file_path,
|
|
745
|
+
line_number: finding.line_number,
|
|
746
|
+
message: finding.message,
|
|
747
|
+
resolved: false,
|
|
748
|
+
}));
|
|
749
|
+
const { error } = await this.client.from('security_findings').insert(rows);
|
|
750
|
+
if (error != null) {
|
|
751
|
+
throw new Error(`Failed to record security findings: ${error.message}`);
|
|
752
|
+
}
|
|
753
|
+
return findings.length;
|
|
754
|
+
}
|
|
755
|
+
async declarePropagationTarget(learning_id, target_type, target_path, relevance) {
|
|
756
|
+
const { error } = await this.client.rpc('declare_propagation_target', {
|
|
757
|
+
p_learning_id: learning_id,
|
|
758
|
+
p_target_type: target_type,
|
|
759
|
+
p_target_path: target_path,
|
|
760
|
+
p_relevance_score: relevance,
|
|
761
|
+
});
|
|
762
|
+
if (error != null) {
|
|
763
|
+
throw new Error(`Failed to declare propagation target: ${error.message}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
async listAllLearnings(filter) {
|
|
767
|
+
let query = this.client
|
|
768
|
+
.from('learnings')
|
|
769
|
+
.select('*')
|
|
770
|
+
.eq('is_superseded', false)
|
|
771
|
+
.order('created_at', { ascending: false });
|
|
772
|
+
if (filter?.feature_id != null) {
|
|
773
|
+
query = query.eq('feature_id', filter.feature_id);
|
|
774
|
+
}
|
|
775
|
+
if (filter?.category != null) {
|
|
776
|
+
query = query.eq('category', filter.category);
|
|
777
|
+
}
|
|
778
|
+
if (filter?.min_confidence != null) {
|
|
779
|
+
query = query.gte('confidence_score', filter.min_confidence);
|
|
780
|
+
}
|
|
781
|
+
const { data, error } = await query;
|
|
782
|
+
if (error != null) {
|
|
783
|
+
throw new Error(`Failed to list all learnings from Supabase: ${error.message}`);
|
|
784
|
+
}
|
|
785
|
+
if (data == null) {
|
|
786
|
+
return [];
|
|
787
|
+
}
|
|
788
|
+
return data.map((row) => ({
|
|
789
|
+
id: String(row.id),
|
|
790
|
+
feature_id: String(row.feature_id),
|
|
791
|
+
phase: String(row.phase),
|
|
792
|
+
title: String(row.title),
|
|
793
|
+
content: String(row.content),
|
|
794
|
+
category: String(row.category),
|
|
795
|
+
tags: Array.isArray(row.tags) ? row.tags : [],
|
|
796
|
+
created_by: String(row.created_by),
|
|
797
|
+
created_at: String(row.created_at),
|
|
798
|
+
}));
|
|
799
|
+
}
|
|
800
|
+
async listRelatedLearnings(feature_id, limit = 5) {
|
|
801
|
+
const { data, error } = await this.client.rpc('get_related_learnings', {
|
|
802
|
+
p_feature_id: feature_id,
|
|
803
|
+
p_limit: limit,
|
|
804
|
+
});
|
|
805
|
+
if (error != null) {
|
|
806
|
+
throw new Error(`Failed to list related learnings from Supabase: ${error.message}`);
|
|
807
|
+
}
|
|
808
|
+
if (data == null || !Array.isArray(data)) {
|
|
809
|
+
return [];
|
|
810
|
+
}
|
|
811
|
+
return data.map((row) => ({
|
|
812
|
+
id: String(row.id),
|
|
813
|
+
title: String(row.title),
|
|
814
|
+
category: String(row.category),
|
|
815
|
+
content: String(row.content),
|
|
816
|
+
confidence_score: Number(row.confidence_score),
|
|
817
|
+
source_feature_id: String(row.source_feature_id),
|
|
818
|
+
shared_domains: Array.isArray(row.shared_domains) ? row.shared_domains : [],
|
|
819
|
+
shared_domain_count: Number(row.shared_domain_count),
|
|
820
|
+
}));
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
//# sourceMappingURL=supabase.js.map
|