@straiffi/archon 1.0.0
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 +224 -0
- package/dist/cli.js +216 -0
- package/dist/client/assets/index-8_-boBBA.css +2 -0
- package/dist/client/assets/index-s_jjeqha.js +176 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/client/favicon.svg +62 -0
- package/dist/client/icons.svg +24 -0
- package/dist/client/index.html +14 -0
- package/dist/server/db.js +764 -0
- package/dist/server/db.js.map +1 -0
- package/dist/server/index.js +5134 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/lib/agent.js +1302 -0
- package/dist/server/lib/agent.js.map +1 -0
- package/dist/server/lib/buildChains.js +2 -0
- package/dist/server/lib/buildChains.js.map +1 -0
- package/dist/server/lib/buildFlow.js +59 -0
- package/dist/server/lib/buildFlow.js.map +1 -0
- package/dist/server/lib/buildSequences.js +599 -0
- package/dist/server/lib/buildSequences.js.map +1 -0
- package/dist/server/lib/bundleActivity.js +95 -0
- package/dist/server/lib/bundleActivity.js.map +1 -0
- package/dist/server/lib/bundlePullRequests.js +126 -0
- package/dist/server/lib/bundlePullRequests.js.map +1 -0
- package/dist/server/lib/chatMessages.js +60 -0
- package/dist/server/lib/chatMessages.js.map +1 -0
- package/dist/server/lib/chatTargets.js +123 -0
- package/dist/server/lib/chatTargets.js.map +1 -0
- package/dist/server/lib/chatTicketProposals.js +180 -0
- package/dist/server/lib/chatTicketProposals.js.map +1 -0
- package/dist/server/lib/chats.js +279 -0
- package/dist/server/lib/chats.js.map +1 -0
- package/dist/server/lib/config.js +3 -0
- package/dist/server/lib/config.js.map +1 -0
- package/dist/server/lib/cors.js +30 -0
- package/dist/server/lib/cors.js.map +1 -0
- package/dist/server/lib/directoryPicker.js +174 -0
- package/dist/server/lib/directoryPicker.js.map +1 -0
- package/dist/server/lib/git.js +1284 -0
- package/dist/server/lib/git.js.map +1 -0
- package/dist/server/lib/integrations/github.js +511 -0
- package/dist/server/lib/integrations/github.js.map +1 -0
- package/dist/server/lib/integrations/index.js +162 -0
- package/dist/server/lib/integrations/index.js.map +1 -0
- package/dist/server/lib/integrations/jira.js +283 -0
- package/dist/server/lib/integrations/jira.js.map +1 -0
- package/dist/server/lib/integrations/planning.js +27 -0
- package/dist/server/lib/integrations/planning.js.map +1 -0
- package/dist/server/lib/integrations/types.js +2 -0
- package/dist/server/lib/integrations/types.js.map +1 -0
- package/dist/server/lib/lightweightPrompt.js +88 -0
- package/dist/server/lib/lightweightPrompt.js.map +1 -0
- package/dist/server/lib/models.js +219 -0
- package/dist/server/lib/models.js.map +1 -0
- package/dist/server/lib/preview.js +377 -0
- package/dist/server/lib/preview.js.map +1 -0
- package/dist/server/lib/previewProxy.js +659 -0
- package/dist/server/lib/previewProxy.js.map +1 -0
- package/dist/server/lib/projectAutoConfig.js +682 -0
- package/dist/server/lib/projectAutoConfig.js.map +1 -0
- package/dist/server/lib/projectFileSuggestions.js +133 -0
- package/dist/server/lib/projectFileSuggestions.js.map +1 -0
- package/dist/server/lib/projectMemory.js +1519 -0
- package/dist/server/lib/projectMemory.js.map +1 -0
- package/dist/server/lib/projectMemoryPrompt.js +390 -0
- package/dist/server/lib/projectMemoryPrompt.js.map +1 -0
- package/dist/server/lib/projectMemoryScan.js +681 -0
- package/dist/server/lib/projectMemoryScan.js.map +1 -0
- package/dist/server/lib/projectMemorySuggestions.js +166 -0
- package/dist/server/lib/projectMemorySuggestions.js.map +1 -0
- package/dist/server/lib/projectMemoryTransfer.js +958 -0
- package/dist/server/lib/projectMemoryTransfer.js.map +1 -0
- package/dist/server/lib/projects.js +569 -0
- package/dist/server/lib/projects.js.map +1 -0
- package/dist/server/lib/promptSkills.js +28 -0
- package/dist/server/lib/promptSkills.js.map +1 -0
- package/dist/server/lib/queue.js +15 -0
- package/dist/server/lib/queue.js.map +1 -0
- package/dist/server/lib/reviewFindings.js +390 -0
- package/dist/server/lib/reviewFindings.js.map +1 -0
- package/dist/server/lib/run.js +416 -0
- package/dist/server/lib/run.js.map +1 -0
- package/dist/server/lib/runtimePaths.js +93 -0
- package/dist/server/lib/runtimePaths.js.map +1 -0
- package/dist/server/lib/shell.js +27 -0
- package/dist/server/lib/shell.js.map +1 -0
- package/dist/server/lib/skills.js +124 -0
- package/dist/server/lib/skills.js.map +1 -0
- package/dist/server/lib/startDev.js +18 -0
- package/dist/server/lib/startDev.js.map +1 -0
- package/dist/server/lib/staticClient.js +80 -0
- package/dist/server/lib/staticClient.js.map +1 -0
- package/dist/server/lib/terminal.js +366 -0
- package/dist/server/lib/terminal.js.map +1 -0
- package/dist/server/lib/ticketDependencies.js +174 -0
- package/dist/server/lib/ticketDependencies.js.map +1 -0
- package/dist/server/lib/ticketMessages.js +65 -0
- package/dist/server/lib/ticketMessages.js.map +1 -0
- package/dist/server/lib/ticketOpenQuestions.js +128 -0
- package/dist/server/lib/ticketOpenQuestions.js.map +1 -0
- package/dist/server/lib/ticketUndo.js +549 -0
- package/dist/server/lib/ticketUndo.js.map +1 -0
- package/dist/server/lib/tickets.js +981 -0
- package/dist/server/lib/tickets.js.map +1 -0
- package/dist/server/lib/types.js +2 -0
- package/dist/server/lib/types.js.map +1 -0
- package/dist/server/package.json +3 -0
- package/dist/server/workers/build.js +229 -0
- package/dist/server/workers/build.js.map +1 -0
- package/dist/server/workers/chat.js +190 -0
- package/dist/server/workers/chat.js.map +1 -0
- package/dist/server/workers/followUp.js +204 -0
- package/dist/server/workers/followUp.js.map +1 -0
- package/dist/server/workers/plan.js +1130 -0
- package/dist/server/workers/plan.js.map +1 -0
- package/dist/server/workers/planFollowUp.js +360 -0
- package/dist/server/workers/planFollowUp.js.map +1 -0
- package/dist/server/workers/review.js +167 -0
- package/dist/server/workers/review.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,1519 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import db from '../db.js';
|
|
3
|
+
const PROJECT_MEMORY_STAGES = ['plan', 'build', 'review', 'follow_up', 'plan_follow_up', 'chat'];
|
|
4
|
+
const PROJECT_CONVENTION_PRIORITIES = ['critical', 'normal', 'low'];
|
|
5
|
+
const PROJECT_CONVENTION_INJECTION_MODES = ['always', 'relevant'];
|
|
6
|
+
const PROJECT_MEMORY_KINDS = ['decision', 'convention'];
|
|
7
|
+
const PROJECT_MEMORY_STATES = ['suggested', 'active', 'archived', 'superseded', 'dismissed', 'applied_to_existing'];
|
|
8
|
+
const PROJECT_MEMORY_SOURCES = ['manual', 'agent'];
|
|
9
|
+
const PROJECT_MEMORY_SUGGESTION_KINDS = ['decision', 'convention'];
|
|
10
|
+
const PROJECT_MEMORY_SUGGESTION_STATUSES = ['pending', 'accepted', 'dismissed', 'applied_to_existing'];
|
|
11
|
+
const PROJECT_MEMORY_SUGGESTION_DUPLICATE_KINDS = ['none', 'near_duplicate'];
|
|
12
|
+
const PROJECT_CONTEXT_SCAN_STATUSES = ['pending', 'running', 'done', 'error'];
|
|
13
|
+
const PROJECT_CONTEXT_ARTIFACT_KINDS = ['architecture', 'key_areas', 'conventions', 'risks'];
|
|
14
|
+
const ACCEPTED_PROJECT_DECISION_STATUS = 'accepted';
|
|
15
|
+
const ACCEPTED_MEMORY_COMPARISON_STOPWORDS = new Set([
|
|
16
|
+
'about',
|
|
17
|
+
'again',
|
|
18
|
+
'all',
|
|
19
|
+
'also',
|
|
20
|
+
'and',
|
|
21
|
+
'any',
|
|
22
|
+
'are',
|
|
23
|
+
'around',
|
|
24
|
+
'because',
|
|
25
|
+
'been',
|
|
26
|
+
'before',
|
|
27
|
+
'being',
|
|
28
|
+
'between',
|
|
29
|
+
'both',
|
|
30
|
+
'but',
|
|
31
|
+
'can',
|
|
32
|
+
'continue',
|
|
33
|
+
'default',
|
|
34
|
+
'does',
|
|
35
|
+
'during',
|
|
36
|
+
'each',
|
|
37
|
+
'for',
|
|
38
|
+
'from',
|
|
39
|
+
'have',
|
|
40
|
+
'into',
|
|
41
|
+
'its',
|
|
42
|
+
'just',
|
|
43
|
+
'keep',
|
|
44
|
+
'last',
|
|
45
|
+
'more',
|
|
46
|
+
'most',
|
|
47
|
+
'must',
|
|
48
|
+
'new',
|
|
49
|
+
'none',
|
|
50
|
+
'not',
|
|
51
|
+
'only',
|
|
52
|
+
'other',
|
|
53
|
+
'our',
|
|
54
|
+
'out',
|
|
55
|
+
'over',
|
|
56
|
+
'rather',
|
|
57
|
+
'same',
|
|
58
|
+
'should',
|
|
59
|
+
'still',
|
|
60
|
+
'such',
|
|
61
|
+
'than',
|
|
62
|
+
'that',
|
|
63
|
+
'the',
|
|
64
|
+
'their',
|
|
65
|
+
'them',
|
|
66
|
+
'then',
|
|
67
|
+
'there',
|
|
68
|
+
'these',
|
|
69
|
+
'this',
|
|
70
|
+
'those',
|
|
71
|
+
'through',
|
|
72
|
+
'use',
|
|
73
|
+
'used',
|
|
74
|
+
'using',
|
|
75
|
+
'when',
|
|
76
|
+
'while',
|
|
77
|
+
'with',
|
|
78
|
+
'without',
|
|
79
|
+
]);
|
|
80
|
+
const parseStringArray = (value) => {
|
|
81
|
+
if (!value) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(value);
|
|
86
|
+
if (!Array.isArray(parsed)) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
return parsed
|
|
90
|
+
.filter((entry) => typeof entry === 'string')
|
|
91
|
+
.map(entry => entry.trim())
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.filter((entry, index, values) => values.indexOf(entry) === index);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const normalizeComparisonText = (value) => {
|
|
100
|
+
return String(value ?? '')
|
|
101
|
+
.toLowerCase()
|
|
102
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
103
|
+
.trim()
|
|
104
|
+
.replace(/\s+/g, ' ');
|
|
105
|
+
};
|
|
106
|
+
const tokenizeForComparison = (...parts) => {
|
|
107
|
+
return parts
|
|
108
|
+
.flatMap(part => normalizeComparisonText(part).split(' '))
|
|
109
|
+
.map(token => token.trim())
|
|
110
|
+
.filter(token => token.length >= 3);
|
|
111
|
+
};
|
|
112
|
+
const getComparisonTokenSet = (parts, { excludeStopwords = false } = {}) => {
|
|
113
|
+
const tokens = tokenizeForComparison(...parts)
|
|
114
|
+
.filter(token => !excludeStopwords || !ACCEPTED_MEMORY_COMPARISON_STOPWORDS.has(token));
|
|
115
|
+
return new Set(tokens);
|
|
116
|
+
};
|
|
117
|
+
const countTokenSetOverlap = (leftTokens, rightTokens) => {
|
|
118
|
+
let overlap = 0;
|
|
119
|
+
for (const token of leftTokens) {
|
|
120
|
+
if (rightTokens.has(token)) {
|
|
121
|
+
overlap += 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return overlap;
|
|
125
|
+
};
|
|
126
|
+
const getTokenSetCoverage = (leftTokens, rightTokens) => {
|
|
127
|
+
const smallerTokenCount = Math.min(leftTokens.size, rightTokens.size);
|
|
128
|
+
if (smallerTokenCount === 0) {
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
return countTokenSetOverlap(leftTokens, rightTokens) / smallerTokenCount;
|
|
132
|
+
};
|
|
133
|
+
const countTokenOverlap = (leftParts, rightParts) => {
|
|
134
|
+
const leftTokens = getComparisonTokenSet(leftParts);
|
|
135
|
+
const rightTokens = getComparisonTokenSet(rightParts);
|
|
136
|
+
return countTokenSetOverlap(leftTokens, rightTokens);
|
|
137
|
+
};
|
|
138
|
+
const hasSubstantialContainment = (left, right) => {
|
|
139
|
+
const shorter = left.length <= right.length ? left : right;
|
|
140
|
+
const longer = left.length <= right.length ? right : left;
|
|
141
|
+
if (shorter.length < 24) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return longer.includes(shorter);
|
|
145
|
+
};
|
|
146
|
+
const isAcceptedMemoryNearDuplicate = ({ suggestionTitle, suggestionScope, suggestionContent, targetTitle, targetScope, targetContent, }) => {
|
|
147
|
+
const sameTitle = suggestionTitle !== '' && suggestionTitle === targetTitle;
|
|
148
|
+
const sameScope = suggestionScope !== '' && suggestionScope === targetScope;
|
|
149
|
+
const contentOverlap = countTokenOverlap([suggestionContent], [targetContent]);
|
|
150
|
+
const fullOverlap = countTokenOverlap([suggestionTitle, suggestionScope, suggestionContent], [targetTitle, targetScope, targetContent]);
|
|
151
|
+
const distinctiveContentTokens = getComparisonTokenSet([suggestionContent], { excludeStopwords: true });
|
|
152
|
+
const distinctiveTargetContentTokens = getComparisonTokenSet([targetContent], { excludeStopwords: true });
|
|
153
|
+
const distinctiveFullSuggestionTokens = getComparisonTokenSet([suggestionTitle, suggestionScope, suggestionContent], { excludeStopwords: true });
|
|
154
|
+
const distinctiveFullTargetTokens = getComparisonTokenSet([targetTitle, targetScope, targetContent], { excludeStopwords: true });
|
|
155
|
+
const distinctiveContentOverlap = countTokenSetOverlap(distinctiveContentTokens, distinctiveTargetContentTokens);
|
|
156
|
+
const distinctiveFullOverlap = countTokenSetOverlap(distinctiveFullSuggestionTokens, distinctiveFullTargetTokens);
|
|
157
|
+
const distinctiveContentCoverage = getTokenSetCoverage(distinctiveContentTokens, distinctiveTargetContentTokens);
|
|
158
|
+
const distinctiveFullCoverage = getTokenSetCoverage(distinctiveFullSuggestionTokens, distinctiveFullTargetTokens);
|
|
159
|
+
const contentContainment = hasSubstantialContainment(suggestionContent, targetContent);
|
|
160
|
+
if (contentContainment && distinctiveContentOverlap >= 2 && (sameTitle || sameScope || distinctiveFullCoverage >= 0.45)) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
if (sameTitle && distinctiveContentOverlap >= 2 && distinctiveContentCoverage >= 0.35) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
if (sameScope && distinctiveContentOverlap >= 2 && distinctiveContentCoverage >= 0.5) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
return contentOverlap >= 5
|
|
170
|
+
&& fullOverlap >= 7
|
|
171
|
+
&& distinctiveContentOverlap >= 3
|
|
172
|
+
&& distinctiveFullOverlap >= 3
|
|
173
|
+
&& distinctiveContentCoverage >= 0.5
|
|
174
|
+
&& distinctiveFullCoverage >= 0.35;
|
|
175
|
+
};
|
|
176
|
+
const normalizeOptionalString = (value, fieldName) => {
|
|
177
|
+
if (value == null) {
|
|
178
|
+
return { values: null };
|
|
179
|
+
}
|
|
180
|
+
if (typeof value !== 'string') {
|
|
181
|
+
return { error: `${fieldName} must be a string` };
|
|
182
|
+
}
|
|
183
|
+
const trimmed = value.trim();
|
|
184
|
+
return { values: trimmed === '' ? null : trimmed };
|
|
185
|
+
};
|
|
186
|
+
const validateRequiredString = (value, fieldName) => {
|
|
187
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
188
|
+
return { error: `${fieldName} must be a non-empty string` };
|
|
189
|
+
}
|
|
190
|
+
return { values: value.trim() };
|
|
191
|
+
};
|
|
192
|
+
const validateStages = (value) => {
|
|
193
|
+
if (value == null) {
|
|
194
|
+
return { values: null };
|
|
195
|
+
}
|
|
196
|
+
if (!Array.isArray(value)) {
|
|
197
|
+
return { error: 'stages must be an array of valid project memory stages' };
|
|
198
|
+
}
|
|
199
|
+
const normalized = [];
|
|
200
|
+
for (const stage of value) {
|
|
201
|
+
if (!PROJECT_MEMORY_STAGES.includes(stage)) {
|
|
202
|
+
return { error: 'stages must be an array of valid project memory stages' };
|
|
203
|
+
}
|
|
204
|
+
if (!normalized.includes(stage)) {
|
|
205
|
+
normalized.push(stage);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { values: normalized.length > 0 ? JSON.stringify(normalized) : null };
|
|
209
|
+
};
|
|
210
|
+
const validatePriority = (value) => {
|
|
211
|
+
if (!PROJECT_CONVENTION_PRIORITIES.includes(value)) {
|
|
212
|
+
return { error: 'priority must be one of critical, normal, or low' };
|
|
213
|
+
}
|
|
214
|
+
return { values: value };
|
|
215
|
+
};
|
|
216
|
+
const validateConventionInjectionMode = (value) => {
|
|
217
|
+
if (!PROJECT_CONVENTION_INJECTION_MODES.includes(value)) {
|
|
218
|
+
return { error: 'injection_mode must be one of always or relevant' };
|
|
219
|
+
}
|
|
220
|
+
return { values: value };
|
|
221
|
+
};
|
|
222
|
+
const validateImplications = (value) => {
|
|
223
|
+
if (value == null) {
|
|
224
|
+
return { values: null };
|
|
225
|
+
}
|
|
226
|
+
if (!Array.isArray(value)) {
|
|
227
|
+
return { error: 'implications must be an array of non-empty strings' };
|
|
228
|
+
}
|
|
229
|
+
const normalized = [];
|
|
230
|
+
for (const implication of value) {
|
|
231
|
+
if (typeof implication !== 'string' || implication.trim() === '') {
|
|
232
|
+
return { error: 'implications must be an array of non-empty strings' };
|
|
233
|
+
}
|
|
234
|
+
const trimmed = implication.trim();
|
|
235
|
+
if (!normalized.includes(trimmed)) {
|
|
236
|
+
normalized.push(trimmed);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return { values: normalized.length > 0 ? JSON.stringify(normalized) : null };
|
|
240
|
+
};
|
|
241
|
+
const serializeConvention = (row) => {
|
|
242
|
+
return {
|
|
243
|
+
id: row.id,
|
|
244
|
+
project_id: row.project_id,
|
|
245
|
+
title: row.title,
|
|
246
|
+
scope: row.scope,
|
|
247
|
+
instruction: row.instruction,
|
|
248
|
+
rationale: row.rationale,
|
|
249
|
+
stages: parseStringArray(row.stages_json).filter((stage) => PROJECT_MEMORY_STAGES.includes(stage)),
|
|
250
|
+
priority: row.priority,
|
|
251
|
+
injection_mode: PROJECT_CONVENTION_INJECTION_MODES.includes(row.injection_mode)
|
|
252
|
+
? row.injection_mode
|
|
253
|
+
: 'relevant',
|
|
254
|
+
status: row.status,
|
|
255
|
+
created_at: row.created_at,
|
|
256
|
+
updated_at: row.updated_at,
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
const serializeDecision = (row) => {
|
|
260
|
+
return {
|
|
261
|
+
id: row.id,
|
|
262
|
+
project_id: row.project_id,
|
|
263
|
+
title: row.title,
|
|
264
|
+
scope: row.scope,
|
|
265
|
+
decision: row.decision,
|
|
266
|
+
rationale: row.rationale,
|
|
267
|
+
implications: parseStringArray(row.implications_json),
|
|
268
|
+
status: row.status,
|
|
269
|
+
created_at: row.created_at,
|
|
270
|
+
updated_at: row.updated_at,
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
export const serializeProjectMemoryFromConventionValue = (convention) => {
|
|
274
|
+
return {
|
|
275
|
+
id: convention.id,
|
|
276
|
+
project_id: convention.project_id,
|
|
277
|
+
kind: 'convention',
|
|
278
|
+
state: convention.status === 'active' ? 'active' : 'archived',
|
|
279
|
+
source: 'manual',
|
|
280
|
+
title: convention.title,
|
|
281
|
+
scope: convention.scope,
|
|
282
|
+
content: convention.instruction,
|
|
283
|
+
rationale: convention.rationale,
|
|
284
|
+
implications: [],
|
|
285
|
+
stages: convention.stages,
|
|
286
|
+
priority: convention.priority,
|
|
287
|
+
injection_mode: convention.injection_mode,
|
|
288
|
+
source_stage: null,
|
|
289
|
+
ticket_id: null,
|
|
290
|
+
chat_session_id: null,
|
|
291
|
+
duplicate_match: null,
|
|
292
|
+
created_at: convention.created_at,
|
|
293
|
+
updated_at: convention.updated_at,
|
|
294
|
+
};
|
|
295
|
+
};
|
|
296
|
+
const serializeProjectMemoryFromConvention = (row) => {
|
|
297
|
+
return serializeProjectMemoryFromConventionValue(serializeConvention(row));
|
|
298
|
+
};
|
|
299
|
+
export const serializeProjectMemoryFromDecisionValue = (decision) => {
|
|
300
|
+
return {
|
|
301
|
+
id: decision.id,
|
|
302
|
+
project_id: decision.project_id,
|
|
303
|
+
kind: 'decision',
|
|
304
|
+
state: decision.status === 'accepted' ? 'active' : decision.status,
|
|
305
|
+
source: 'manual',
|
|
306
|
+
title: decision.title,
|
|
307
|
+
scope: decision.scope,
|
|
308
|
+
content: decision.decision,
|
|
309
|
+
rationale: decision.rationale,
|
|
310
|
+
implications: decision.implications,
|
|
311
|
+
stages: [],
|
|
312
|
+
priority: null,
|
|
313
|
+
injection_mode: null,
|
|
314
|
+
source_stage: null,
|
|
315
|
+
ticket_id: null,
|
|
316
|
+
chat_session_id: null,
|
|
317
|
+
duplicate_match: null,
|
|
318
|
+
created_at: decision.created_at,
|
|
319
|
+
updated_at: decision.updated_at,
|
|
320
|
+
};
|
|
321
|
+
};
|
|
322
|
+
const serializeProjectMemoryFromDecision = (row) => {
|
|
323
|
+
return serializeProjectMemoryFromDecisionValue(serializeDecision(row));
|
|
324
|
+
};
|
|
325
|
+
const parseJsonValue = (value) => {
|
|
326
|
+
if (!value) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
return JSON.parse(value);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const serializeProjectContextScan = (row) => {
|
|
337
|
+
return {
|
|
338
|
+
id: row.id,
|
|
339
|
+
project_id: row.project_id,
|
|
340
|
+
status: PROJECT_CONTEXT_SCAN_STATUSES.includes(row.status)
|
|
341
|
+
? row.status
|
|
342
|
+
: 'error',
|
|
343
|
+
repo_head: row.repo_head,
|
|
344
|
+
repo_branch: row.repo_branch,
|
|
345
|
+
scanner_tool: row.scanner_tool === 'claude' || row.scanner_tool === 'opencode' ? row.scanner_tool : null,
|
|
346
|
+
scanner_model: row.scanner_model,
|
|
347
|
+
scanner_variant: row.scanner_variant,
|
|
348
|
+
summary_markdown: row.summary_markdown,
|
|
349
|
+
error: row.error,
|
|
350
|
+
created_at: row.created_at,
|
|
351
|
+
updated_at: row.updated_at,
|
|
352
|
+
};
|
|
353
|
+
};
|
|
354
|
+
const serializeProjectContextArtifact = (row) => {
|
|
355
|
+
return {
|
|
356
|
+
id: row.id,
|
|
357
|
+
scan_id: row.scan_id,
|
|
358
|
+
project_id: row.project_id,
|
|
359
|
+
kind: PROJECT_CONTEXT_ARTIFACT_KINDS.includes(row.kind)
|
|
360
|
+
? row.kind
|
|
361
|
+
: 'risks',
|
|
362
|
+
title: row.title,
|
|
363
|
+
content_json: parseJsonValue(row.content_json),
|
|
364
|
+
content_markdown: row.content_markdown,
|
|
365
|
+
created_at: row.created_at,
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
const parseProjectMemorySuggestionDetails = (value) => {
|
|
369
|
+
if (!value) {
|
|
370
|
+
return {};
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const parsed = JSON.parse(value);
|
|
374
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
375
|
+
? parsed
|
|
376
|
+
: {};
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return {};
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
const parseProjectMemorySuggestionValues = (row) => {
|
|
383
|
+
const details = parseProjectMemorySuggestionDetails(row.details_json);
|
|
384
|
+
if (row.kind === 'decision') {
|
|
385
|
+
const implications = Array.isArray(details.implications)
|
|
386
|
+
? details.implications
|
|
387
|
+
.filter((entry) => typeof entry === 'string')
|
|
388
|
+
.map(entry => entry.trim())
|
|
389
|
+
.filter(Boolean)
|
|
390
|
+
.filter((entry, index, values) => values.indexOf(entry) === index)
|
|
391
|
+
: [];
|
|
392
|
+
return {
|
|
393
|
+
kind: 'decision',
|
|
394
|
+
title: row.title,
|
|
395
|
+
scope: row.scope,
|
|
396
|
+
decision: row.content_text,
|
|
397
|
+
rationale: row.rationale,
|
|
398
|
+
implications,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
if (row.kind === 'convention') {
|
|
402
|
+
const stages = Array.isArray(details.stages)
|
|
403
|
+
? details.stages
|
|
404
|
+
.filter((entry) => typeof entry === 'string' && PROJECT_MEMORY_STAGES.includes(entry))
|
|
405
|
+
.filter((entry, index, values) => values.indexOf(entry) === index)
|
|
406
|
+
: [];
|
|
407
|
+
const priority = PROJECT_CONVENTION_PRIORITIES.includes(details.priority)
|
|
408
|
+
? details.priority
|
|
409
|
+
: 'normal';
|
|
410
|
+
const injectionMode = PROJECT_CONVENTION_INJECTION_MODES.includes(details.injection_mode)
|
|
411
|
+
? details.injection_mode
|
|
412
|
+
: 'relevant';
|
|
413
|
+
return {
|
|
414
|
+
kind: 'convention',
|
|
415
|
+
title: row.title,
|
|
416
|
+
scope: row.scope,
|
|
417
|
+
instruction: row.content_text,
|
|
418
|
+
rationale: row.rationale,
|
|
419
|
+
stages,
|
|
420
|
+
priority,
|
|
421
|
+
injection_mode: injectionMode,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
};
|
|
426
|
+
const getProjectMemorySuggestionContent = (suggestion) => {
|
|
427
|
+
return suggestion.kind === 'decision' ? suggestion.decision : suggestion.instruction;
|
|
428
|
+
};
|
|
429
|
+
const getProjectMemorySuggestionTextParts = (suggestion) => {
|
|
430
|
+
return [suggestion.title, suggestion.scope, getProjectMemorySuggestionContent(suggestion)];
|
|
431
|
+
};
|
|
432
|
+
export const getProjectMemorySuggestionDedupeKey = (suggestion) => {
|
|
433
|
+
const baseParts = [
|
|
434
|
+
suggestion.kind,
|
|
435
|
+
suggestion.title.toLowerCase(),
|
|
436
|
+
suggestion.scope?.toLowerCase() ?? '',
|
|
437
|
+
getProjectMemorySuggestionContent(suggestion).toLowerCase(),
|
|
438
|
+
];
|
|
439
|
+
if (suggestion.kind === 'convention') {
|
|
440
|
+
baseParts.push(suggestion.stages.join(','), suggestion.priority);
|
|
441
|
+
}
|
|
442
|
+
return baseParts.join('::');
|
|
443
|
+
};
|
|
444
|
+
const serializeProjectMemorySuggestion = (row) => {
|
|
445
|
+
const values = parseProjectMemorySuggestionValues(row);
|
|
446
|
+
if (!values) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
const duplicateKind = PROJECT_MEMORY_SUGGESTION_DUPLICATE_KINDS.includes(row.duplicate_kind)
|
|
450
|
+
? row.duplicate_kind
|
|
451
|
+
: 'none';
|
|
452
|
+
if (values.kind === 'decision') {
|
|
453
|
+
const duplicateDecision = duplicateKind === 'near_duplicate' && row.duplicate_target_id
|
|
454
|
+
? getProjectDecisionById(row.project_id, row.duplicate_target_id)
|
|
455
|
+
: null;
|
|
456
|
+
return {
|
|
457
|
+
id: row.id,
|
|
458
|
+
project_id: row.project_id,
|
|
459
|
+
ticket_id: row.ticket_id,
|
|
460
|
+
chat_session_id: row.chat_session_id,
|
|
461
|
+
source_stage: row.source_stage,
|
|
462
|
+
kind: 'decision',
|
|
463
|
+
title: values.title,
|
|
464
|
+
scope: values.scope,
|
|
465
|
+
decision: values.decision,
|
|
466
|
+
rationale: values.rationale,
|
|
467
|
+
implications: values.implications,
|
|
468
|
+
status: row.status,
|
|
469
|
+
duplicate_match: duplicateDecision
|
|
470
|
+
? {
|
|
471
|
+
type: 'near_duplicate',
|
|
472
|
+
decision: duplicateDecision,
|
|
473
|
+
}
|
|
474
|
+
: null,
|
|
475
|
+
created_at: row.created_at,
|
|
476
|
+
updated_at: row.updated_at,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const duplicateConvention = duplicateKind === 'near_duplicate' && row.duplicate_target_id
|
|
480
|
+
? getProjectConventionById(row.project_id, row.duplicate_target_id)
|
|
481
|
+
: null;
|
|
482
|
+
return {
|
|
483
|
+
id: row.id,
|
|
484
|
+
project_id: row.project_id,
|
|
485
|
+
ticket_id: row.ticket_id,
|
|
486
|
+
chat_session_id: row.chat_session_id,
|
|
487
|
+
source_stage: row.source_stage,
|
|
488
|
+
kind: 'convention',
|
|
489
|
+
title: values.title,
|
|
490
|
+
scope: values.scope,
|
|
491
|
+
instruction: values.instruction,
|
|
492
|
+
rationale: values.rationale,
|
|
493
|
+
stages: values.stages,
|
|
494
|
+
priority: values.priority,
|
|
495
|
+
injection_mode: values.injection_mode,
|
|
496
|
+
status: row.status,
|
|
497
|
+
duplicate_match: duplicateConvention
|
|
498
|
+
? {
|
|
499
|
+
type: 'near_duplicate',
|
|
500
|
+
convention: duplicateConvention,
|
|
501
|
+
}
|
|
502
|
+
: null,
|
|
503
|
+
created_at: row.created_at,
|
|
504
|
+
updated_at: row.updated_at,
|
|
505
|
+
};
|
|
506
|
+
};
|
|
507
|
+
export const serializeProjectMemoryFromSuggestionValue = (suggestion) => {
|
|
508
|
+
if (suggestion.status === 'accepted') {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
const state = suggestion.status === 'pending'
|
|
512
|
+
? 'suggested'
|
|
513
|
+
: suggestion.status === 'dismissed'
|
|
514
|
+
? 'dismissed'
|
|
515
|
+
: 'applied_to_existing';
|
|
516
|
+
if (suggestion.kind === 'decision') {
|
|
517
|
+
return {
|
|
518
|
+
id: suggestion.id,
|
|
519
|
+
project_id: suggestion.project_id,
|
|
520
|
+
kind: 'decision',
|
|
521
|
+
state,
|
|
522
|
+
source: 'agent',
|
|
523
|
+
title: suggestion.title,
|
|
524
|
+
scope: suggestion.scope,
|
|
525
|
+
content: suggestion.decision,
|
|
526
|
+
rationale: suggestion.rationale,
|
|
527
|
+
implications: suggestion.implications,
|
|
528
|
+
stages: [],
|
|
529
|
+
priority: null,
|
|
530
|
+
injection_mode: null,
|
|
531
|
+
source_stage: suggestion.source_stage,
|
|
532
|
+
ticket_id: suggestion.ticket_id,
|
|
533
|
+
chat_session_id: suggestion.chat_session_id,
|
|
534
|
+
duplicate_match: suggestion.duplicate_match?.decision
|
|
535
|
+
? {
|
|
536
|
+
type: 'near_duplicate',
|
|
537
|
+
memory_id: suggestion.duplicate_match.decision.id,
|
|
538
|
+
memory_kind: 'decision',
|
|
539
|
+
title: suggestion.duplicate_match.decision.title,
|
|
540
|
+
content: suggestion.duplicate_match.decision.decision,
|
|
541
|
+
}
|
|
542
|
+
: null,
|
|
543
|
+
created_at: suggestion.created_at,
|
|
544
|
+
updated_at: suggestion.updated_at,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
id: suggestion.id,
|
|
549
|
+
project_id: suggestion.project_id,
|
|
550
|
+
kind: 'convention',
|
|
551
|
+
state,
|
|
552
|
+
source: 'agent',
|
|
553
|
+
title: suggestion.title,
|
|
554
|
+
scope: suggestion.scope,
|
|
555
|
+
content: suggestion.instruction,
|
|
556
|
+
rationale: suggestion.rationale,
|
|
557
|
+
implications: [],
|
|
558
|
+
stages: suggestion.stages,
|
|
559
|
+
priority: suggestion.priority,
|
|
560
|
+
injection_mode: suggestion.injection_mode,
|
|
561
|
+
source_stage: suggestion.source_stage,
|
|
562
|
+
ticket_id: suggestion.ticket_id,
|
|
563
|
+
chat_session_id: suggestion.chat_session_id,
|
|
564
|
+
duplicate_match: suggestion.duplicate_match?.convention
|
|
565
|
+
? {
|
|
566
|
+
type: 'near_duplicate',
|
|
567
|
+
memory_id: suggestion.duplicate_match.convention.id,
|
|
568
|
+
memory_kind: 'convention',
|
|
569
|
+
title: suggestion.duplicate_match.convention.title,
|
|
570
|
+
content: suggestion.duplicate_match.convention.instruction,
|
|
571
|
+
}
|
|
572
|
+
: null,
|
|
573
|
+
created_at: suggestion.created_at,
|
|
574
|
+
updated_at: suggestion.updated_at,
|
|
575
|
+
};
|
|
576
|
+
};
|
|
577
|
+
const serializeProjectMemoryFromSuggestion = (row) => {
|
|
578
|
+
const suggestion = serializeProjectMemorySuggestion(row);
|
|
579
|
+
return suggestion ? serializeProjectMemoryFromSuggestionValue(suggestion) : null;
|
|
580
|
+
};
|
|
581
|
+
export const isProjectMemoryEnabled = (project) => {
|
|
582
|
+
return Boolean(project?.memory_enabled);
|
|
583
|
+
};
|
|
584
|
+
export const getLatestProjectContextScan = (projectId) => {
|
|
585
|
+
const row = db.prepare(`
|
|
586
|
+
SELECT *
|
|
587
|
+
FROM project_context_scans
|
|
588
|
+
WHERE project_id = ?
|
|
589
|
+
ORDER BY created_at DESC, id DESC
|
|
590
|
+
LIMIT 1
|
|
591
|
+
`).get(projectId);
|
|
592
|
+
return row ? serializeProjectContextScan(row) : null;
|
|
593
|
+
};
|
|
594
|
+
export const getLatestSuccessfulProjectContextScan = (projectId) => {
|
|
595
|
+
const row = db.prepare(`
|
|
596
|
+
SELECT *
|
|
597
|
+
FROM project_context_scans
|
|
598
|
+
WHERE project_id = ?
|
|
599
|
+
AND status = 'done'
|
|
600
|
+
ORDER BY created_at DESC, id DESC
|
|
601
|
+
LIMIT 1
|
|
602
|
+
`).get(projectId);
|
|
603
|
+
return row ? serializeProjectContextScan(row) : null;
|
|
604
|
+
};
|
|
605
|
+
export const getActiveProjectContextScan = (projectId) => {
|
|
606
|
+
const row = db.prepare(`
|
|
607
|
+
SELECT *
|
|
608
|
+
FROM project_context_scans
|
|
609
|
+
WHERE project_id = ?
|
|
610
|
+
AND status IN ('pending', 'running')
|
|
611
|
+
ORDER BY created_at DESC, id DESC
|
|
612
|
+
LIMIT 1
|
|
613
|
+
`).get(projectId);
|
|
614
|
+
return row ? serializeProjectContextScan(row) : null;
|
|
615
|
+
};
|
|
616
|
+
export const getProjectContextScanById = (projectId, scanId) => {
|
|
617
|
+
const row = db.prepare(`
|
|
618
|
+
SELECT *
|
|
619
|
+
FROM project_context_scans
|
|
620
|
+
WHERE id = ?
|
|
621
|
+
AND project_id = ?
|
|
622
|
+
`).get(scanId, projectId);
|
|
623
|
+
return row ? serializeProjectContextScan(row) : null;
|
|
624
|
+
};
|
|
625
|
+
export const createProjectContextScan = (projectId, values) => {
|
|
626
|
+
const id = randomUUID();
|
|
627
|
+
db.prepare(`
|
|
628
|
+
INSERT INTO project_context_scans (
|
|
629
|
+
id,
|
|
630
|
+
project_id,
|
|
631
|
+
status,
|
|
632
|
+
repo_head,
|
|
633
|
+
repo_branch,
|
|
634
|
+
scanner_tool,
|
|
635
|
+
scanner_model,
|
|
636
|
+
scanner_variant,
|
|
637
|
+
summary_markdown,
|
|
638
|
+
error
|
|
639
|
+
)
|
|
640
|
+
VALUES (?, ?, 'pending', ?, ?, ?, ?, ?, NULL, NULL)
|
|
641
|
+
`).run(id, projectId, values.repo_head, values.repo_branch, values.scanner_tool, values.scanner_model, values.scanner_variant);
|
|
642
|
+
return getProjectContextScanById(projectId, id);
|
|
643
|
+
};
|
|
644
|
+
export const updateProjectContextScan = (projectId, scanId, values) => {
|
|
645
|
+
const current = getProjectContextScanById(projectId, scanId);
|
|
646
|
+
if (!current) {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
db.prepare(`
|
|
650
|
+
UPDATE project_context_scans
|
|
651
|
+
SET status = ?,
|
|
652
|
+
repo_head = ?,
|
|
653
|
+
repo_branch = ?,
|
|
654
|
+
scanner_tool = ?,
|
|
655
|
+
scanner_model = ?,
|
|
656
|
+
scanner_variant = ?,
|
|
657
|
+
summary_markdown = ?,
|
|
658
|
+
error = ?,
|
|
659
|
+
updated_at = CURRENT_TIMESTAMP
|
|
660
|
+
WHERE id = ?
|
|
661
|
+
AND project_id = ?
|
|
662
|
+
`).run(values.status ?? current.status, Object.hasOwn(values, 'repo_head') ? values.repo_head : current.repo_head, Object.hasOwn(values, 'repo_branch') ? values.repo_branch : current.repo_branch, Object.hasOwn(values, 'scanner_tool') ? values.scanner_tool : current.scanner_tool, Object.hasOwn(values, 'scanner_model') ? values.scanner_model : current.scanner_model, Object.hasOwn(values, 'scanner_variant') ? values.scanner_variant : current.scanner_variant, Object.hasOwn(values, 'summary_markdown') ? values.summary_markdown : current.summary_markdown, Object.hasOwn(values, 'error') ? values.error : current.error, scanId, projectId);
|
|
663
|
+
return getProjectContextScanById(projectId, scanId);
|
|
664
|
+
};
|
|
665
|
+
export const replaceProjectContextArtifacts = (projectId, scanId, artifacts) => {
|
|
666
|
+
const removeArtifacts = db.prepare('DELETE FROM project_context_artifacts WHERE scan_id = ? AND project_id = ?');
|
|
667
|
+
const insertArtifact = db.prepare(`
|
|
668
|
+
INSERT INTO project_context_artifacts (
|
|
669
|
+
id,
|
|
670
|
+
scan_id,
|
|
671
|
+
project_id,
|
|
672
|
+
kind,
|
|
673
|
+
title,
|
|
674
|
+
content_json,
|
|
675
|
+
content_markdown
|
|
676
|
+
)
|
|
677
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
678
|
+
`);
|
|
679
|
+
const transaction = db.transaction((nextArtifacts) => {
|
|
680
|
+
removeArtifacts.run(scanId, projectId);
|
|
681
|
+
for (const artifact of nextArtifacts) {
|
|
682
|
+
insertArtifact.run(randomUUID(), scanId, projectId, artifact.kind, artifact.title, artifact.content_json, artifact.content_markdown);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
transaction(artifacts);
|
|
686
|
+
return listProjectContextArtifactsForScan(projectId, scanId);
|
|
687
|
+
};
|
|
688
|
+
export const listProjectContextArtifactsForScan = (projectId, scanId) => {
|
|
689
|
+
const rows = db.prepare(`
|
|
690
|
+
SELECT *
|
|
691
|
+
FROM project_context_artifacts
|
|
692
|
+
WHERE project_id = ?
|
|
693
|
+
AND scan_id = ?
|
|
694
|
+
ORDER BY created_at ASC, id ASC
|
|
695
|
+
`).all(projectId, scanId);
|
|
696
|
+
return rows.map(serializeProjectContextArtifact);
|
|
697
|
+
};
|
|
698
|
+
export const listLatestSuccessfulProjectContextArtifacts = (projectId) => {
|
|
699
|
+
const scan = getLatestSuccessfulProjectContextScan(projectId);
|
|
700
|
+
if (!scan) {
|
|
701
|
+
return [];
|
|
702
|
+
}
|
|
703
|
+
return listProjectContextArtifactsForScan(projectId, scan.id);
|
|
704
|
+
};
|
|
705
|
+
export const validateProjectConventionPayload = (body, { partial = false } = {}) => {
|
|
706
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
707
|
+
return { error: 'Invalid convention payload' };
|
|
708
|
+
}
|
|
709
|
+
const input = body;
|
|
710
|
+
const values = {};
|
|
711
|
+
if (!partial || Object.hasOwn(input, 'title')) {
|
|
712
|
+
const result = validateRequiredString(input.title, 'title');
|
|
713
|
+
if ('error' in result) {
|
|
714
|
+
return result;
|
|
715
|
+
}
|
|
716
|
+
values.title = result.values;
|
|
717
|
+
}
|
|
718
|
+
if (!partial || Object.hasOwn(input, 'instruction')) {
|
|
719
|
+
const result = validateRequiredString(input.instruction, 'instruction');
|
|
720
|
+
if ('error' in result) {
|
|
721
|
+
return result;
|
|
722
|
+
}
|
|
723
|
+
values.instruction = result.values;
|
|
724
|
+
}
|
|
725
|
+
if (Object.hasOwn(input, 'scope')) {
|
|
726
|
+
const result = normalizeOptionalString(input.scope, 'scope');
|
|
727
|
+
if ('error' in result) {
|
|
728
|
+
return result;
|
|
729
|
+
}
|
|
730
|
+
values.scope = result.values;
|
|
731
|
+
}
|
|
732
|
+
else if (!partial) {
|
|
733
|
+
values.scope = null;
|
|
734
|
+
}
|
|
735
|
+
if (Object.hasOwn(input, 'rationale')) {
|
|
736
|
+
const result = normalizeOptionalString(input.rationale, 'rationale');
|
|
737
|
+
if ('error' in result) {
|
|
738
|
+
return result;
|
|
739
|
+
}
|
|
740
|
+
values.rationale = result.values;
|
|
741
|
+
}
|
|
742
|
+
else if (!partial) {
|
|
743
|
+
values.rationale = null;
|
|
744
|
+
}
|
|
745
|
+
if (Object.hasOwn(input, 'stages')) {
|
|
746
|
+
const result = validateStages(input.stages);
|
|
747
|
+
if ('error' in result) {
|
|
748
|
+
return result;
|
|
749
|
+
}
|
|
750
|
+
values.stages_json = result.values;
|
|
751
|
+
}
|
|
752
|
+
else if (!partial) {
|
|
753
|
+
values.stages_json = null;
|
|
754
|
+
}
|
|
755
|
+
if (Object.hasOwn(input, 'priority')) {
|
|
756
|
+
const result = validatePriority(input.priority);
|
|
757
|
+
if ('error' in result) {
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
760
|
+
values.priority = result.values;
|
|
761
|
+
}
|
|
762
|
+
else if (!partial) {
|
|
763
|
+
values.priority = 'normal';
|
|
764
|
+
}
|
|
765
|
+
if (Object.hasOwn(input, 'injection_mode')) {
|
|
766
|
+
const result = validateConventionInjectionMode(input.injection_mode);
|
|
767
|
+
if ('error' in result) {
|
|
768
|
+
return result;
|
|
769
|
+
}
|
|
770
|
+
values.injection_mode = result.values;
|
|
771
|
+
}
|
|
772
|
+
else if (!partial) {
|
|
773
|
+
values.injection_mode = 'relevant';
|
|
774
|
+
}
|
|
775
|
+
return { values };
|
|
776
|
+
};
|
|
777
|
+
export const validateProjectDecisionPayload = (body, { partial = false } = {}) => {
|
|
778
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
779
|
+
return { error: 'Invalid decision payload' };
|
|
780
|
+
}
|
|
781
|
+
const input = body;
|
|
782
|
+
const values = {};
|
|
783
|
+
if (!partial || Object.hasOwn(input, 'title')) {
|
|
784
|
+
const result = validateRequiredString(input.title, 'title');
|
|
785
|
+
if ('error' in result) {
|
|
786
|
+
return result;
|
|
787
|
+
}
|
|
788
|
+
values.title = result.values;
|
|
789
|
+
}
|
|
790
|
+
if (!partial || Object.hasOwn(input, 'decision')) {
|
|
791
|
+
const result = validateRequiredString(input.decision, 'decision');
|
|
792
|
+
if ('error' in result) {
|
|
793
|
+
return result;
|
|
794
|
+
}
|
|
795
|
+
values.decision = result.values;
|
|
796
|
+
}
|
|
797
|
+
if (Object.hasOwn(input, 'scope')) {
|
|
798
|
+
const result = normalizeOptionalString(input.scope, 'scope');
|
|
799
|
+
if ('error' in result) {
|
|
800
|
+
return result;
|
|
801
|
+
}
|
|
802
|
+
values.scope = result.values;
|
|
803
|
+
}
|
|
804
|
+
else if (!partial) {
|
|
805
|
+
values.scope = null;
|
|
806
|
+
}
|
|
807
|
+
if (Object.hasOwn(input, 'rationale')) {
|
|
808
|
+
const result = normalizeOptionalString(input.rationale, 'rationale');
|
|
809
|
+
if ('error' in result) {
|
|
810
|
+
return result;
|
|
811
|
+
}
|
|
812
|
+
values.rationale = result.values;
|
|
813
|
+
}
|
|
814
|
+
else if (!partial) {
|
|
815
|
+
values.rationale = null;
|
|
816
|
+
}
|
|
817
|
+
if (Object.hasOwn(input, 'implications')) {
|
|
818
|
+
const result = validateImplications(input.implications);
|
|
819
|
+
if ('error' in result) {
|
|
820
|
+
return result;
|
|
821
|
+
}
|
|
822
|
+
values.implications_json = result.values;
|
|
823
|
+
}
|
|
824
|
+
else if (!partial) {
|
|
825
|
+
values.implications_json = null;
|
|
826
|
+
}
|
|
827
|
+
return { values };
|
|
828
|
+
};
|
|
829
|
+
export const validateProjectMemorySuggestionPayload = (body) => {
|
|
830
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
831
|
+
return { error: 'Invalid project memory suggestion payload' };
|
|
832
|
+
}
|
|
833
|
+
const input = body;
|
|
834
|
+
if (!PROJECT_MEMORY_SUGGESTION_KINDS.includes(input.kind)) {
|
|
835
|
+
return { error: 'kind must be one of decision or convention' };
|
|
836
|
+
}
|
|
837
|
+
if (input.kind === 'decision') {
|
|
838
|
+
const decisionValidation = validateProjectDecisionPayload({
|
|
839
|
+
title: input.title,
|
|
840
|
+
scope: input.scope,
|
|
841
|
+
decision: input.decision ?? input.content,
|
|
842
|
+
rationale: input.rationale,
|
|
843
|
+
implications: input.implications,
|
|
844
|
+
});
|
|
845
|
+
if ('error' in decisionValidation) {
|
|
846
|
+
return { error: decisionValidation.error };
|
|
847
|
+
}
|
|
848
|
+
return {
|
|
849
|
+
values: {
|
|
850
|
+
kind: 'decision',
|
|
851
|
+
title: decisionValidation.values.title ?? '',
|
|
852
|
+
scope: decisionValidation.values.scope ?? null,
|
|
853
|
+
decision: decisionValidation.values.decision ?? '',
|
|
854
|
+
rationale: decisionValidation.values.rationale ?? null,
|
|
855
|
+
implications: parseStringArray(decisionValidation.values.implications_json ?? null),
|
|
856
|
+
},
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
const conventionValidation = validateProjectConventionPayload({
|
|
860
|
+
title: input.title,
|
|
861
|
+
scope: input.scope,
|
|
862
|
+
instruction: input.instruction ?? input.content,
|
|
863
|
+
rationale: input.rationale,
|
|
864
|
+
stages: input.stages,
|
|
865
|
+
priority: input.priority,
|
|
866
|
+
injection_mode: input.injection_mode,
|
|
867
|
+
});
|
|
868
|
+
if ('error' in conventionValidation) {
|
|
869
|
+
return { error: conventionValidation.error };
|
|
870
|
+
}
|
|
871
|
+
const scope = conventionValidation.values.scope ?? null;
|
|
872
|
+
const stages = parseStringArray(conventionValidation.values.stages_json ?? null)
|
|
873
|
+
.filter((entry) => PROJECT_MEMORY_STAGES.includes(entry));
|
|
874
|
+
if (!scope && stages.length === 0) {
|
|
875
|
+
return { error: 'Convention suggestions require a scope or at least one stage' };
|
|
876
|
+
}
|
|
877
|
+
return {
|
|
878
|
+
values: {
|
|
879
|
+
kind: 'convention',
|
|
880
|
+
title: conventionValidation.values.title ?? '',
|
|
881
|
+
scope,
|
|
882
|
+
instruction: conventionValidation.values.instruction ?? '',
|
|
883
|
+
rationale: conventionValidation.values.rationale ?? null,
|
|
884
|
+
stages,
|
|
885
|
+
priority: conventionValidation.values.priority ?? 'normal',
|
|
886
|
+
injection_mode: conventionValidation.values.injection_mode ?? 'relevant',
|
|
887
|
+
},
|
|
888
|
+
};
|
|
889
|
+
};
|
|
890
|
+
export const classifyProjectMemorySuggestionDuplicate = ({ suggestion, activeConventions, activeDecisions, pendingSuggestions, }) => {
|
|
891
|
+
const normalizedSuggestionContent = normalizeComparisonText(getProjectMemorySuggestionContent(suggestion));
|
|
892
|
+
const normalizedSuggestionTitle = normalizeComparisonText(suggestion.title);
|
|
893
|
+
const normalizedSuggestionScope = normalizeComparisonText(suggestion.scope);
|
|
894
|
+
if (suggestion.kind === 'decision') {
|
|
895
|
+
for (const decision of activeDecisions) {
|
|
896
|
+
const normalizedDecisionText = normalizeComparisonText(decision.decision);
|
|
897
|
+
const normalizedDecisionTitle = normalizeComparisonText(decision.title);
|
|
898
|
+
const normalizedDecisionScope = normalizeComparisonText(decision.scope);
|
|
899
|
+
if (normalizedDecisionText !== ''
|
|
900
|
+
&& normalizedDecisionText === normalizedSuggestionContent) {
|
|
901
|
+
return {
|
|
902
|
+
action: 'suppress',
|
|
903
|
+
reason: 'exact_duplicate',
|
|
904
|
+
target: 'decision',
|
|
905
|
+
targetId: decision.id,
|
|
906
|
+
targetTitle: decision.title,
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
if (isAcceptedMemoryNearDuplicate({
|
|
910
|
+
suggestionTitle: normalizedSuggestionTitle,
|
|
911
|
+
suggestionScope: normalizedSuggestionScope,
|
|
912
|
+
suggestionContent: normalizedSuggestionContent,
|
|
913
|
+
targetTitle: normalizedDecisionTitle,
|
|
914
|
+
targetScope: normalizedDecisionScope,
|
|
915
|
+
targetContent: normalizedDecisionText,
|
|
916
|
+
})) {
|
|
917
|
+
return {
|
|
918
|
+
action: 'mark_near_duplicate',
|
|
919
|
+
targetMemoryKind: 'decision',
|
|
920
|
+
targetMemoryId: decision.id,
|
|
921
|
+
targetMemoryTitle: decision.title,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
for (const convention of activeConventions) {
|
|
928
|
+
const normalizedConventionInstruction = normalizeComparisonText(convention.instruction);
|
|
929
|
+
const normalizedConventionTitle = normalizeComparisonText(convention.title);
|
|
930
|
+
const normalizedConventionScope = normalizeComparisonText(convention.scope);
|
|
931
|
+
if (normalizedConventionInstruction !== ''
|
|
932
|
+
&& normalizedConventionInstruction === normalizedSuggestionContent) {
|
|
933
|
+
return {
|
|
934
|
+
action: 'suppress',
|
|
935
|
+
reason: 'exact_duplicate',
|
|
936
|
+
target: 'convention',
|
|
937
|
+
targetId: convention.id,
|
|
938
|
+
targetTitle: convention.title,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
if (isAcceptedMemoryNearDuplicate({
|
|
942
|
+
suggestionTitle: normalizedSuggestionTitle,
|
|
943
|
+
suggestionScope: normalizedSuggestionScope,
|
|
944
|
+
suggestionContent: normalizedSuggestionContent,
|
|
945
|
+
targetTitle: normalizedConventionTitle,
|
|
946
|
+
targetScope: normalizedConventionScope,
|
|
947
|
+
targetContent: normalizedConventionInstruction,
|
|
948
|
+
})) {
|
|
949
|
+
return {
|
|
950
|
+
action: 'mark_near_duplicate',
|
|
951
|
+
targetMemoryKind: 'convention',
|
|
952
|
+
targetMemoryId: convention.id,
|
|
953
|
+
targetMemoryTitle: convention.title,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
for (const pendingSuggestion of pendingSuggestions) {
|
|
959
|
+
if (pendingSuggestion.kind !== suggestion.kind) {
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
const samePendingContent = normalizeComparisonText(getProjectMemorySuggestionContent(pendingSuggestion)) === normalizedSuggestionContent;
|
|
963
|
+
const samePendingTitle = normalizeComparisonText(pendingSuggestion.title) === normalizedSuggestionTitle;
|
|
964
|
+
const overlap = countTokenOverlap(getProjectMemorySuggestionTextParts(suggestion), getProjectMemorySuggestionTextParts(pendingSuggestion));
|
|
965
|
+
if (samePendingContent || samePendingTitle || overlap >= 4) {
|
|
966
|
+
return {
|
|
967
|
+
action: 'suppress',
|
|
968
|
+
reason: 'pending_duplicate',
|
|
969
|
+
target: 'pending_suggestion',
|
|
970
|
+
targetId: pendingSuggestion.id,
|
|
971
|
+
targetTitle: pendingSuggestion.title,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return null;
|
|
976
|
+
};
|
|
977
|
+
export const listProjectConventions = (projectId, { includeArchived = false } = {}) => {
|
|
978
|
+
const rows = db.prepare(`
|
|
979
|
+
SELECT *
|
|
980
|
+
FROM project_conventions
|
|
981
|
+
WHERE project_id = ?
|
|
982
|
+
${includeArchived ? '' : "AND status = 'active'"}
|
|
983
|
+
ORDER BY
|
|
984
|
+
CASE status WHEN 'active' THEN 0 ELSE 1 END,
|
|
985
|
+
updated_at DESC,
|
|
986
|
+
created_at DESC,
|
|
987
|
+
id DESC
|
|
988
|
+
`).all(projectId);
|
|
989
|
+
return rows.map(serializeConvention);
|
|
990
|
+
};
|
|
991
|
+
export const listProjectDecisions = (projectId, { includeArchived = false } = {}) => {
|
|
992
|
+
const rows = db.prepare(`
|
|
993
|
+
SELECT *
|
|
994
|
+
FROM project_decisions
|
|
995
|
+
WHERE project_id = ?
|
|
996
|
+
${includeArchived ? '' : `AND status = '${ACCEPTED_PROJECT_DECISION_STATUS}'`}
|
|
997
|
+
ORDER BY
|
|
998
|
+
CASE status WHEN '${ACCEPTED_PROJECT_DECISION_STATUS}' THEN 0 ELSE 1 END,
|
|
999
|
+
updated_at DESC,
|
|
1000
|
+
created_at DESC,
|
|
1001
|
+
id DESC
|
|
1002
|
+
`).all(projectId);
|
|
1003
|
+
return rows.map(serializeDecision);
|
|
1004
|
+
};
|
|
1005
|
+
export const getProjectConventionById = (projectId, conventionId) => {
|
|
1006
|
+
const row = db.prepare('SELECT * FROM project_conventions WHERE id = ? AND project_id = ?').get(conventionId, projectId);
|
|
1007
|
+
return row ? serializeConvention(row) : null;
|
|
1008
|
+
};
|
|
1009
|
+
export const getProjectDecisionById = (projectId, decisionId) => {
|
|
1010
|
+
const row = db.prepare('SELECT * FROM project_decisions WHERE id = ? AND project_id = ?').get(decisionId, projectId);
|
|
1011
|
+
return row ? serializeDecision(row) : null;
|
|
1012
|
+
};
|
|
1013
|
+
export const createProjectConvention = (projectId, values) => {
|
|
1014
|
+
const id = randomUUID();
|
|
1015
|
+
db.prepare(`
|
|
1016
|
+
INSERT INTO project_conventions (id, project_id, title, scope, instruction, rationale, stages_json, priority, injection_mode, status)
|
|
1017
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')
|
|
1018
|
+
`).run(id, projectId, values.title, values.scope, values.instruction, values.rationale, values.stages_json, values.priority, values.injection_mode);
|
|
1019
|
+
return getProjectConventionById(projectId, id);
|
|
1020
|
+
};
|
|
1021
|
+
export const updateProjectConvention = (projectId, conventionId, values) => {
|
|
1022
|
+
const current = getProjectConventionById(projectId, conventionId);
|
|
1023
|
+
if (!current) {
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
db.prepare(`
|
|
1027
|
+
UPDATE project_conventions
|
|
1028
|
+
SET title = ?,
|
|
1029
|
+
scope = ?,
|
|
1030
|
+
instruction = ?,
|
|
1031
|
+
rationale = ?,
|
|
1032
|
+
stages_json = ?,
|
|
1033
|
+
priority = ?,
|
|
1034
|
+
injection_mode = ?,
|
|
1035
|
+
updated_at = CURRENT_TIMESTAMP
|
|
1036
|
+
WHERE id = ?
|
|
1037
|
+
AND project_id = ?
|
|
1038
|
+
`).run(values.title ?? current.title, Object.hasOwn(values, 'scope') ? values.scope : current.scope, values.instruction ?? current.instruction, Object.hasOwn(values, 'rationale') ? values.rationale : current.rationale, Object.hasOwn(values, 'stages_json') ? values.stages_json : (current.stages.length > 0 ? JSON.stringify(current.stages) : null), values.priority ?? current.priority, values.injection_mode ?? current.injection_mode, conventionId, projectId);
|
|
1039
|
+
return getProjectConventionById(projectId, conventionId);
|
|
1040
|
+
};
|
|
1041
|
+
export const archiveProjectConvention = (projectId, conventionId) => {
|
|
1042
|
+
db.prepare(`
|
|
1043
|
+
UPDATE project_conventions
|
|
1044
|
+
SET status = 'archived', updated_at = CURRENT_TIMESTAMP
|
|
1045
|
+
WHERE id = ?
|
|
1046
|
+
AND project_id = ?
|
|
1047
|
+
`).run(conventionId, projectId);
|
|
1048
|
+
return getProjectConventionById(projectId, conventionId);
|
|
1049
|
+
};
|
|
1050
|
+
export const restoreProjectConvention = (projectId, conventionId) => {
|
|
1051
|
+
db.prepare(`
|
|
1052
|
+
UPDATE project_conventions
|
|
1053
|
+
SET status = 'active', updated_at = CURRENT_TIMESTAMP
|
|
1054
|
+
WHERE id = ?
|
|
1055
|
+
AND project_id = ?
|
|
1056
|
+
`).run(conventionId, projectId);
|
|
1057
|
+
return getProjectConventionById(projectId, conventionId);
|
|
1058
|
+
};
|
|
1059
|
+
export const createProjectDecision = (projectId, values) => {
|
|
1060
|
+
const id = randomUUID();
|
|
1061
|
+
db.prepare(`
|
|
1062
|
+
INSERT INTO project_decisions (id, project_id, title, scope, decision, rationale, implications_json, status)
|
|
1063
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1064
|
+
`).run(id, projectId, values.title, values.scope, values.decision, values.rationale, values.implications_json, ACCEPTED_PROJECT_DECISION_STATUS);
|
|
1065
|
+
return getProjectDecisionById(projectId, id);
|
|
1066
|
+
};
|
|
1067
|
+
export const updateProjectDecision = (projectId, decisionId, values) => {
|
|
1068
|
+
const current = getProjectDecisionById(projectId, decisionId);
|
|
1069
|
+
if (!current) {
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
db.prepare(`
|
|
1073
|
+
UPDATE project_decisions
|
|
1074
|
+
SET title = ?,
|
|
1075
|
+
scope = ?,
|
|
1076
|
+
decision = ?,
|
|
1077
|
+
rationale = ?,
|
|
1078
|
+
implications_json = ?,
|
|
1079
|
+
updated_at = CURRENT_TIMESTAMP
|
|
1080
|
+
WHERE id = ?
|
|
1081
|
+
AND project_id = ?
|
|
1082
|
+
`).run(values.title ?? current.title, Object.hasOwn(values, 'scope') ? values.scope : current.scope, values.decision ?? current.decision, Object.hasOwn(values, 'rationale') ? values.rationale : current.rationale, Object.hasOwn(values, 'implications_json') ? values.implications_json : (current.implications.length > 0 ? JSON.stringify(current.implications) : null), decisionId, projectId);
|
|
1083
|
+
return getProjectDecisionById(projectId, decisionId);
|
|
1084
|
+
};
|
|
1085
|
+
export const archiveProjectDecision = (projectId, decisionId) => {
|
|
1086
|
+
db.prepare(`
|
|
1087
|
+
UPDATE project_decisions
|
|
1088
|
+
SET status = 'archived', updated_at = CURRENT_TIMESTAMP
|
|
1089
|
+
WHERE id = ?
|
|
1090
|
+
AND project_id = ?
|
|
1091
|
+
`).run(decisionId, projectId);
|
|
1092
|
+
return getProjectDecisionById(projectId, decisionId);
|
|
1093
|
+
};
|
|
1094
|
+
export const supersedeProjectDecision = (projectId, decisionId) => {
|
|
1095
|
+
db.prepare(`
|
|
1096
|
+
UPDATE project_decisions
|
|
1097
|
+
SET status = 'superseded', updated_at = CURRENT_TIMESTAMP
|
|
1098
|
+
WHERE id = ?
|
|
1099
|
+
AND project_id = ?
|
|
1100
|
+
`).run(decisionId, projectId);
|
|
1101
|
+
return getProjectDecisionById(projectId, decisionId);
|
|
1102
|
+
};
|
|
1103
|
+
export const restoreProjectDecision = (projectId, decisionId) => {
|
|
1104
|
+
db.prepare(`
|
|
1105
|
+
UPDATE project_decisions
|
|
1106
|
+
SET status = ?, updated_at = CURRENT_TIMESTAMP
|
|
1107
|
+
WHERE id = ?
|
|
1108
|
+
AND project_id = ?
|
|
1109
|
+
`).run(ACCEPTED_PROJECT_DECISION_STATUS, decisionId, projectId);
|
|
1110
|
+
return getProjectDecisionById(projectId, decisionId);
|
|
1111
|
+
};
|
|
1112
|
+
const getProjectMemorySuggestionRowById = (projectId, suggestionId) => {
|
|
1113
|
+
return db.prepare(`
|
|
1114
|
+
SELECT *
|
|
1115
|
+
FROM project_memory_suggestions
|
|
1116
|
+
WHERE id = ?
|
|
1117
|
+
AND project_id = ?
|
|
1118
|
+
`).get(suggestionId, projectId);
|
|
1119
|
+
};
|
|
1120
|
+
const listProjectMemorySuggestionRows = (projectId, { ticketId, chatSessionId, status = 'pending', } = {}) => {
|
|
1121
|
+
const rows = db.prepare(`
|
|
1122
|
+
SELECT *
|
|
1123
|
+
FROM project_memory_suggestions
|
|
1124
|
+
WHERE project_id = ?
|
|
1125
|
+
${ticketId ? 'AND ticket_id = ?' : ''}
|
|
1126
|
+
${chatSessionId ? 'AND chat_session_id = ?' : ''}
|
|
1127
|
+
${status ? 'AND status = ?' : ''}
|
|
1128
|
+
ORDER BY
|
|
1129
|
+
updated_at DESC,
|
|
1130
|
+
created_at DESC,
|
|
1131
|
+
id DESC
|
|
1132
|
+
`).all(...([
|
|
1133
|
+
projectId,
|
|
1134
|
+
...(ticketId ? [ticketId] : []),
|
|
1135
|
+
...(chatSessionId ? [chatSessionId] : []),
|
|
1136
|
+
...(status ? [status] : []),
|
|
1137
|
+
]));
|
|
1138
|
+
return rows.filter(row => PROJECT_MEMORY_SUGGESTION_STATUSES.includes(row.status));
|
|
1139
|
+
};
|
|
1140
|
+
const listResolvedProjectMemoryIdsForChatSession = (projectId, chatSessionId) => {
|
|
1141
|
+
return new Set(listProjectMemorySuggestionRows(projectId, { chatSessionId, status: null })
|
|
1142
|
+
.filter(row => {
|
|
1143
|
+
return (row.status === 'accepted' || row.status === 'applied_to_existing')
|
|
1144
|
+
&& typeof row.resolved_memory_id === 'string'
|
|
1145
|
+
&& row.resolved_memory_id.trim().length > 0;
|
|
1146
|
+
})
|
|
1147
|
+
.map(row => row.resolved_memory_id));
|
|
1148
|
+
};
|
|
1149
|
+
const toProjectMemorySuggestionCandidate = (row) => {
|
|
1150
|
+
const values = parseProjectMemorySuggestionValues(row);
|
|
1151
|
+
if (!values) {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
return {
|
|
1155
|
+
id: row.id,
|
|
1156
|
+
...values,
|
|
1157
|
+
};
|
|
1158
|
+
};
|
|
1159
|
+
export const listProjectMemorySuggestions = (projectId, { ticketId, chatSessionId, status = 'pending', } = {}) => {
|
|
1160
|
+
return listProjectMemorySuggestionRows(projectId, { ticketId, chatSessionId, status })
|
|
1161
|
+
.map(serializeProjectMemorySuggestion)
|
|
1162
|
+
.filter((suggestion) => suggestion !== null);
|
|
1163
|
+
};
|
|
1164
|
+
export const listProjectMemories = (projectId, { states, kinds, sources, ticketId, chatSessionId, } = {}) => {
|
|
1165
|
+
const allowedStates = (states ?? []).filter(state => PROJECT_MEMORY_STATES.includes(state));
|
|
1166
|
+
const allowedKinds = (kinds ?? []).filter(kind => PROJECT_MEMORY_KINDS.includes(kind));
|
|
1167
|
+
const allowedSources = (sources ?? []).filter(source => PROJECT_MEMORY_SOURCES.includes(source));
|
|
1168
|
+
const memories = [
|
|
1169
|
+
...listProjectConventions(projectId, { includeArchived: true }).map(convention => serializeProjectMemoryFromConvention({
|
|
1170
|
+
id: convention.id,
|
|
1171
|
+
project_id: convention.project_id,
|
|
1172
|
+
title: convention.title,
|
|
1173
|
+
scope: convention.scope,
|
|
1174
|
+
instruction: convention.instruction,
|
|
1175
|
+
rationale: convention.rationale,
|
|
1176
|
+
stages_json: convention.stages.length > 0 ? JSON.stringify(convention.stages) : null,
|
|
1177
|
+
priority: convention.priority,
|
|
1178
|
+
injection_mode: convention.injection_mode,
|
|
1179
|
+
status: convention.status,
|
|
1180
|
+
created_at: convention.created_at,
|
|
1181
|
+
updated_at: convention.updated_at,
|
|
1182
|
+
})),
|
|
1183
|
+
...listProjectDecisions(projectId, { includeArchived: true }).map(decision => serializeProjectMemoryFromDecision({
|
|
1184
|
+
id: decision.id,
|
|
1185
|
+
project_id: decision.project_id,
|
|
1186
|
+
title: decision.title,
|
|
1187
|
+
scope: decision.scope,
|
|
1188
|
+
decision: decision.decision,
|
|
1189
|
+
rationale: decision.rationale,
|
|
1190
|
+
implications_json: decision.implications.length > 0 ? JSON.stringify(decision.implications) : null,
|
|
1191
|
+
status: decision.status,
|
|
1192
|
+
created_at: decision.created_at,
|
|
1193
|
+
updated_at: decision.updated_at,
|
|
1194
|
+
})),
|
|
1195
|
+
...listProjectMemorySuggestionRows(projectId, {
|
|
1196
|
+
ticketId,
|
|
1197
|
+
chatSessionId,
|
|
1198
|
+
status: null,
|
|
1199
|
+
})
|
|
1200
|
+
.map(serializeProjectMemoryFromSuggestion)
|
|
1201
|
+
.filter((memory) => memory !== null),
|
|
1202
|
+
];
|
|
1203
|
+
return memories
|
|
1204
|
+
.filter(memory => allowedStates.length === 0 || allowedStates.includes(memory.state))
|
|
1205
|
+
.filter(memory => allowedKinds.length === 0 || allowedKinds.includes(memory.kind))
|
|
1206
|
+
.filter(memory => allowedSources.length === 0 || allowedSources.includes(memory.source))
|
|
1207
|
+
.sort((left, right) => {
|
|
1208
|
+
const stateOrder = {
|
|
1209
|
+
suggested: 0,
|
|
1210
|
+
active: 1,
|
|
1211
|
+
archived: 2,
|
|
1212
|
+
superseded: 3,
|
|
1213
|
+
dismissed: 4,
|
|
1214
|
+
applied_to_existing: 5,
|
|
1215
|
+
};
|
|
1216
|
+
if (stateOrder[left.state] !== stateOrder[right.state]) {
|
|
1217
|
+
return stateOrder[left.state] - stateOrder[right.state];
|
|
1218
|
+
}
|
|
1219
|
+
return right.updated_at.localeCompare(left.updated_at) || right.created_at.localeCompare(left.created_at) || right.id.localeCompare(left.id);
|
|
1220
|
+
});
|
|
1221
|
+
};
|
|
1222
|
+
export const getProjectMemorySuggestionById = (projectId, suggestionId) => {
|
|
1223
|
+
const row = getProjectMemorySuggestionRowById(projectId, suggestionId);
|
|
1224
|
+
return row ? serializeProjectMemorySuggestion(row) : null;
|
|
1225
|
+
};
|
|
1226
|
+
export const getProjectMemoryById = (projectId, memoryId) => {
|
|
1227
|
+
const convention = getProjectConventionById(projectId, memoryId);
|
|
1228
|
+
if (convention) {
|
|
1229
|
+
return serializeProjectMemoryFromConventionValue(convention);
|
|
1230
|
+
}
|
|
1231
|
+
const decision = getProjectDecisionById(projectId, memoryId);
|
|
1232
|
+
if (decision) {
|
|
1233
|
+
return serializeProjectMemoryFromDecisionValue(decision);
|
|
1234
|
+
}
|
|
1235
|
+
const suggestion = getProjectMemorySuggestionById(projectId, memoryId);
|
|
1236
|
+
return suggestion ? serializeProjectMemoryFromSuggestionValue(suggestion) : null;
|
|
1237
|
+
};
|
|
1238
|
+
const persistProjectMemorySuggestionsForSource = (projectId, source, sourceStage, suggestions) => {
|
|
1239
|
+
if (!source.ticketId && !source.chatSessionId) {
|
|
1240
|
+
return [];
|
|
1241
|
+
}
|
|
1242
|
+
if (suggestions.length === 0) {
|
|
1243
|
+
return [];
|
|
1244
|
+
}
|
|
1245
|
+
const activeDecisions = listProjectDecisions(projectId);
|
|
1246
|
+
const activeConventions = listProjectConventions(projectId);
|
|
1247
|
+
const pendingSuggestionCandidates = listProjectMemorySuggestionRows(projectId, { status: 'pending' })
|
|
1248
|
+
.map(toProjectMemorySuggestionCandidate)
|
|
1249
|
+
.filter((candidate) => candidate !== null);
|
|
1250
|
+
const resolvedChatSessionMemoryIds = source.chatSessionId
|
|
1251
|
+
? listResolvedProjectMemoryIdsForChatSession(projectId, source.chatSessionId)
|
|
1252
|
+
: new Set();
|
|
1253
|
+
const createdIds = [];
|
|
1254
|
+
for (const suggestion of suggestions) {
|
|
1255
|
+
const duplicate = classifyProjectMemorySuggestionDuplicate({
|
|
1256
|
+
suggestion,
|
|
1257
|
+
activeConventions,
|
|
1258
|
+
activeDecisions,
|
|
1259
|
+
pendingSuggestions: pendingSuggestionCandidates,
|
|
1260
|
+
});
|
|
1261
|
+
if (duplicate?.action === 'suppress') {
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
if (duplicate?.action === 'mark_near_duplicate'
|
|
1265
|
+
&& source.chatSessionId
|
|
1266
|
+
&& resolvedChatSessionMemoryIds.has(duplicate.targetMemoryId)) {
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
const id = randomUUID();
|
|
1270
|
+
db.prepare(`
|
|
1271
|
+
INSERT INTO project_memory_suggestions (
|
|
1272
|
+
id,
|
|
1273
|
+
project_id,
|
|
1274
|
+
ticket_id,
|
|
1275
|
+
chat_session_id,
|
|
1276
|
+
source_stage,
|
|
1277
|
+
kind,
|
|
1278
|
+
title,
|
|
1279
|
+
scope,
|
|
1280
|
+
content_text,
|
|
1281
|
+
rationale,
|
|
1282
|
+
details_json,
|
|
1283
|
+
duplicate_kind,
|
|
1284
|
+
duplicate_target_id,
|
|
1285
|
+
status,
|
|
1286
|
+
resolved_memory_id
|
|
1287
|
+
)
|
|
1288
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NULL)
|
|
1289
|
+
`).run(id, projectId, source.ticketId ?? null, source.chatSessionId ?? null, sourceStage, suggestion.kind, suggestion.title, suggestion.scope, getProjectMemorySuggestionContent(suggestion), suggestion.rationale, JSON.stringify(suggestion.kind === 'decision'
|
|
1290
|
+
? { implications: suggestion.implications }
|
|
1291
|
+
: { stages: suggestion.stages, priority: suggestion.priority, injection_mode: suggestion.injection_mode }), duplicate?.action === 'mark_near_duplicate' ? 'near_duplicate' : 'none', duplicate?.action === 'mark_near_duplicate' ? duplicate.targetMemoryId : null);
|
|
1292
|
+
createdIds.push(id);
|
|
1293
|
+
pendingSuggestionCandidates.push({ id, ...suggestion });
|
|
1294
|
+
}
|
|
1295
|
+
return createdIds
|
|
1296
|
+
.map(id => getProjectMemorySuggestionById(projectId, id))
|
|
1297
|
+
.filter((suggestion) => suggestion !== null);
|
|
1298
|
+
};
|
|
1299
|
+
export const persistProjectMemorySuggestions = (projectId, ticketId, sourceStage, suggestions) => {
|
|
1300
|
+
return persistProjectMemorySuggestionsForSource(projectId, { ticketId }, sourceStage, suggestions);
|
|
1301
|
+
};
|
|
1302
|
+
export const persistChatSessionProjectMemorySuggestions = (projectId, chatSessionId, suggestions) => {
|
|
1303
|
+
return persistProjectMemorySuggestionsForSource(projectId, { chatSessionId }, 'chat', suggestions);
|
|
1304
|
+
};
|
|
1305
|
+
const updateProjectMemorySuggestionStatus = (projectId, suggestionId, status, resolvedMemoryId) => {
|
|
1306
|
+
db.prepare(`
|
|
1307
|
+
UPDATE project_memory_suggestions
|
|
1308
|
+
SET status = ?,
|
|
1309
|
+
resolved_memory_id = ?,
|
|
1310
|
+
updated_at = CURRENT_TIMESTAMP
|
|
1311
|
+
WHERE id = ?
|
|
1312
|
+
AND project_id = ?
|
|
1313
|
+
`).run(status, resolvedMemoryId, suggestionId, projectId);
|
|
1314
|
+
return getProjectMemorySuggestionById(projectId, suggestionId);
|
|
1315
|
+
};
|
|
1316
|
+
const mergeProjectMemorySuggestionDecisionValues = (suggestion, overrides) => {
|
|
1317
|
+
if (suggestion.kind !== 'decision') {
|
|
1318
|
+
return { error: 'Suggestion is not a decision' };
|
|
1319
|
+
}
|
|
1320
|
+
const merged = {
|
|
1321
|
+
title: overrides.title ?? suggestion.title,
|
|
1322
|
+
scope: Object.hasOwn(overrides, 'scope') ? overrides.scope : suggestion.scope,
|
|
1323
|
+
decision: overrides.decision ?? suggestion.decision,
|
|
1324
|
+
rationale: Object.hasOwn(overrides, 'rationale') ? overrides.rationale : suggestion.rationale,
|
|
1325
|
+
implications: Object.hasOwn(overrides, 'implications_json')
|
|
1326
|
+
? parseStringArray(overrides.implications_json)
|
|
1327
|
+
: suggestion.implications,
|
|
1328
|
+
};
|
|
1329
|
+
return validateProjectDecisionPayload(merged);
|
|
1330
|
+
};
|
|
1331
|
+
const mergeProjectMemorySuggestionConventionValues = (suggestion, overrides) => {
|
|
1332
|
+
if (suggestion.kind !== 'convention') {
|
|
1333
|
+
return { error: 'Suggestion is not a convention' };
|
|
1334
|
+
}
|
|
1335
|
+
const merged = {
|
|
1336
|
+
title: overrides.title ?? suggestion.title,
|
|
1337
|
+
scope: Object.hasOwn(overrides, 'scope') ? overrides.scope : suggestion.scope,
|
|
1338
|
+
instruction: overrides.instruction ?? suggestion.instruction,
|
|
1339
|
+
rationale: Object.hasOwn(overrides, 'rationale') ? overrides.rationale : suggestion.rationale,
|
|
1340
|
+
stages: Object.hasOwn(overrides, 'stages_json')
|
|
1341
|
+
? parseStringArray(overrides.stages_json)
|
|
1342
|
+
: suggestion.stages,
|
|
1343
|
+
priority: overrides.priority ?? suggestion.priority,
|
|
1344
|
+
injection_mode: overrides.injection_mode ?? suggestion.injection_mode,
|
|
1345
|
+
};
|
|
1346
|
+
return validateProjectConventionPayload(merged);
|
|
1347
|
+
};
|
|
1348
|
+
export const dismissProjectMemorySuggestion = (projectId, suggestionId) => {
|
|
1349
|
+
return updateProjectMemorySuggestionStatus(projectId, suggestionId, 'dismissed', null);
|
|
1350
|
+
};
|
|
1351
|
+
export const acceptProjectMemorySuggestion = (projectId, suggestionId, overrides = {}) => {
|
|
1352
|
+
const suggestion = getProjectMemorySuggestionById(projectId, suggestionId);
|
|
1353
|
+
if (!suggestion) {
|
|
1354
|
+
return { status: 404, error: 'Suggestion not found' };
|
|
1355
|
+
}
|
|
1356
|
+
return suggestion.kind === 'decision'
|
|
1357
|
+
? acceptProjectMemorySuggestionAsDecision(projectId, suggestionId, overrides)
|
|
1358
|
+
: acceptProjectMemorySuggestionAsConvention(projectId, suggestionId, overrides);
|
|
1359
|
+
};
|
|
1360
|
+
export const acceptProjectMemorySuggestionAsDecision = (projectId, suggestionId, overrides = {}) => {
|
|
1361
|
+
const suggestion = getProjectMemorySuggestionById(projectId, suggestionId);
|
|
1362
|
+
if (!suggestion) {
|
|
1363
|
+
return { status: 404, error: 'Suggestion not found' };
|
|
1364
|
+
}
|
|
1365
|
+
if (suggestion.status !== 'pending') {
|
|
1366
|
+
return { status: 409, error: 'Suggestion is no longer pending', suggestion };
|
|
1367
|
+
}
|
|
1368
|
+
const merged = mergeProjectMemorySuggestionDecisionValues(suggestion, overrides);
|
|
1369
|
+
if ('error' in merged) {
|
|
1370
|
+
return { status: 400, error: merged.error, suggestion };
|
|
1371
|
+
}
|
|
1372
|
+
const duplicate = classifyProjectMemorySuggestionDuplicate({
|
|
1373
|
+
suggestion: {
|
|
1374
|
+
kind: 'decision',
|
|
1375
|
+
title: merged.values.title ?? '',
|
|
1376
|
+
scope: merged.values.scope ?? null,
|
|
1377
|
+
decision: merged.values.decision ?? '',
|
|
1378
|
+
rationale: merged.values.rationale ?? null,
|
|
1379
|
+
implications: parseStringArray(merged.values.implications_json ?? null),
|
|
1380
|
+
},
|
|
1381
|
+
activeConventions: [],
|
|
1382
|
+
activeDecisions: listProjectDecisions(projectId),
|
|
1383
|
+
pendingSuggestions: listProjectMemorySuggestionRows(projectId, { status: 'pending' })
|
|
1384
|
+
.filter(row => row.id !== suggestionId)
|
|
1385
|
+
.map(toProjectMemorySuggestionCandidate)
|
|
1386
|
+
.filter((candidate) => candidate !== null),
|
|
1387
|
+
});
|
|
1388
|
+
if (duplicate?.action === 'suppress' && duplicate.target === 'decision') {
|
|
1389
|
+
return { status: 409, error: 'A matching decision already exists for this project', suggestion };
|
|
1390
|
+
}
|
|
1391
|
+
const createdDecision = createProjectDecision(projectId, {
|
|
1392
|
+
title: merged.values.title ?? '',
|
|
1393
|
+
scope: merged.values.scope ?? null,
|
|
1394
|
+
decision: merged.values.decision ?? '',
|
|
1395
|
+
rationale: merged.values.rationale ?? null,
|
|
1396
|
+
implications_json: merged.values.implications_json ?? null,
|
|
1397
|
+
});
|
|
1398
|
+
if (!createdDecision) {
|
|
1399
|
+
return { status: 500, error: 'Unable to create the suggested decision', suggestion };
|
|
1400
|
+
}
|
|
1401
|
+
const updatedSuggestion = updateProjectMemorySuggestionStatus(projectId, suggestionId, 'accepted', createdDecision.id);
|
|
1402
|
+
if (!updatedSuggestion) {
|
|
1403
|
+
return { status: 500, error: 'Unable to update the suggestion state', suggestion };
|
|
1404
|
+
}
|
|
1405
|
+
return {
|
|
1406
|
+
suggestion: updatedSuggestion,
|
|
1407
|
+
decision: createdDecision,
|
|
1408
|
+
};
|
|
1409
|
+
};
|
|
1410
|
+
export const acceptProjectMemorySuggestionAsConvention = (projectId, suggestionId, overrides = {}) => {
|
|
1411
|
+
const suggestion = getProjectMemorySuggestionById(projectId, suggestionId);
|
|
1412
|
+
if (!suggestion) {
|
|
1413
|
+
return { status: 404, error: 'Suggestion not found' };
|
|
1414
|
+
}
|
|
1415
|
+
if (suggestion.status !== 'pending') {
|
|
1416
|
+
return { status: 409, error: 'Suggestion is no longer pending', suggestion };
|
|
1417
|
+
}
|
|
1418
|
+
const merged = mergeProjectMemorySuggestionConventionValues(suggestion, overrides);
|
|
1419
|
+
if ('error' in merged) {
|
|
1420
|
+
return { status: 400, error: merged.error, suggestion };
|
|
1421
|
+
}
|
|
1422
|
+
const duplicate = classifyProjectMemorySuggestionDuplicate({
|
|
1423
|
+
suggestion: {
|
|
1424
|
+
kind: 'convention',
|
|
1425
|
+
title: merged.values.title ?? '',
|
|
1426
|
+
scope: merged.values.scope ?? null,
|
|
1427
|
+
instruction: merged.values.instruction ?? '',
|
|
1428
|
+
rationale: merged.values.rationale ?? null,
|
|
1429
|
+
stages: parseStringArray(merged.values.stages_json ?? null)
|
|
1430
|
+
.filter((entry) => PROJECT_MEMORY_STAGES.includes(entry)),
|
|
1431
|
+
priority: merged.values.priority ?? 'normal',
|
|
1432
|
+
injection_mode: merged.values.injection_mode ?? 'relevant',
|
|
1433
|
+
},
|
|
1434
|
+
activeConventions: listProjectConventions(projectId),
|
|
1435
|
+
activeDecisions: [],
|
|
1436
|
+
pendingSuggestions: listProjectMemorySuggestionRows(projectId, { status: 'pending' })
|
|
1437
|
+
.filter(row => row.id !== suggestionId)
|
|
1438
|
+
.map(toProjectMemorySuggestionCandidate)
|
|
1439
|
+
.filter((candidate) => candidate !== null),
|
|
1440
|
+
});
|
|
1441
|
+
if (duplicate?.action === 'suppress' && duplicate.target === 'convention') {
|
|
1442
|
+
return { status: 409, error: 'A matching convention already exists for this project', suggestion };
|
|
1443
|
+
}
|
|
1444
|
+
const createdConvention = createProjectConvention(projectId, {
|
|
1445
|
+
title: merged.values.title ?? '',
|
|
1446
|
+
scope: merged.values.scope ?? null,
|
|
1447
|
+
instruction: merged.values.instruction ?? '',
|
|
1448
|
+
rationale: merged.values.rationale ?? null,
|
|
1449
|
+
stages_json: merged.values.stages_json ?? null,
|
|
1450
|
+
priority: merged.values.priority ?? 'normal',
|
|
1451
|
+
injection_mode: merged.values.injection_mode ?? 'relevant',
|
|
1452
|
+
});
|
|
1453
|
+
if (!createdConvention) {
|
|
1454
|
+
return { status: 500, error: 'Unable to create the suggested convention', suggestion };
|
|
1455
|
+
}
|
|
1456
|
+
const updatedSuggestion = updateProjectMemorySuggestionStatus(projectId, suggestionId, 'accepted', createdConvention.id);
|
|
1457
|
+
if (!updatedSuggestion) {
|
|
1458
|
+
return { status: 500, error: 'Unable to update the suggestion state', suggestion };
|
|
1459
|
+
}
|
|
1460
|
+
return {
|
|
1461
|
+
suggestion: updatedSuggestion,
|
|
1462
|
+
convention: createdConvention,
|
|
1463
|
+
};
|
|
1464
|
+
};
|
|
1465
|
+
export const applyProjectMemorySuggestionToDecision = (projectId, suggestionId, decisionId, overrides = {}) => {
|
|
1466
|
+
const suggestion = getProjectMemorySuggestionById(projectId, suggestionId);
|
|
1467
|
+
if (!suggestion) {
|
|
1468
|
+
return { status: 404, error: 'Suggestion not found' };
|
|
1469
|
+
}
|
|
1470
|
+
if (suggestion.status !== 'pending') {
|
|
1471
|
+
return { status: 409, error: 'Suggestion is no longer pending', suggestion };
|
|
1472
|
+
}
|
|
1473
|
+
const existingDecision = getProjectDecisionById(projectId, decisionId);
|
|
1474
|
+
if (!existingDecision || existingDecision.status !== ACCEPTED_PROJECT_DECISION_STATUS) {
|
|
1475
|
+
return { status: 404, error: 'Decision not found', suggestion };
|
|
1476
|
+
}
|
|
1477
|
+
const merged = mergeProjectMemorySuggestionDecisionValues(suggestion, overrides);
|
|
1478
|
+
if ('error' in merged) {
|
|
1479
|
+
return { status: 400, error: merged.error, suggestion };
|
|
1480
|
+
}
|
|
1481
|
+
const duplicate = classifyProjectMemorySuggestionDuplicate({
|
|
1482
|
+
suggestion: {
|
|
1483
|
+
kind: 'decision',
|
|
1484
|
+
title: merged.values.title ?? '',
|
|
1485
|
+
scope: merged.values.scope ?? null,
|
|
1486
|
+
decision: merged.values.decision ?? '',
|
|
1487
|
+
rationale: merged.values.rationale ?? null,
|
|
1488
|
+
implications: parseStringArray(merged.values.implications_json ?? null),
|
|
1489
|
+
},
|
|
1490
|
+
activeConventions: [],
|
|
1491
|
+
activeDecisions: listProjectDecisions(projectId).filter(decision => decision.id !== decisionId),
|
|
1492
|
+
pendingSuggestions: listProjectMemorySuggestionRows(projectId, { status: 'pending' })
|
|
1493
|
+
.filter(row => row.id !== suggestionId)
|
|
1494
|
+
.map(toProjectMemorySuggestionCandidate)
|
|
1495
|
+
.filter((candidate) => candidate !== null),
|
|
1496
|
+
});
|
|
1497
|
+
if (duplicate?.action === 'suppress' && duplicate.target === 'decision') {
|
|
1498
|
+
return { status: 409, error: 'Updating this decision would duplicate another accepted decision', suggestion };
|
|
1499
|
+
}
|
|
1500
|
+
const updatedDecision = updateProjectDecision(projectId, decisionId, {
|
|
1501
|
+
title: merged.values.title ?? existingDecision.title,
|
|
1502
|
+
scope: merged.values.scope ?? null,
|
|
1503
|
+
decision: merged.values.decision ?? existingDecision.decision,
|
|
1504
|
+
rationale: merged.values.rationale ?? null,
|
|
1505
|
+
implications_json: merged.values.implications_json ?? (existingDecision.implications.length > 0 ? JSON.stringify(existingDecision.implications) : null),
|
|
1506
|
+
});
|
|
1507
|
+
if (!updatedDecision) {
|
|
1508
|
+
return { status: 500, error: 'Unable to update the existing decision', suggestion };
|
|
1509
|
+
}
|
|
1510
|
+
const updatedSuggestion = updateProjectMemorySuggestionStatus(projectId, suggestionId, 'applied_to_existing', updatedDecision.id);
|
|
1511
|
+
if (!updatedSuggestion) {
|
|
1512
|
+
return { status: 500, error: 'Unable to update the suggestion state', suggestion };
|
|
1513
|
+
}
|
|
1514
|
+
return {
|
|
1515
|
+
suggestion: updatedSuggestion,
|
|
1516
|
+
decision: updatedDecision,
|
|
1517
|
+
};
|
|
1518
|
+
};
|
|
1519
|
+
//# sourceMappingURL=projectMemory.js.map
|