@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.
Files changed (124) hide show
  1. package/README.md +224 -0
  2. package/dist/cli.js +216 -0
  3. package/dist/client/assets/index-8_-boBBA.css +2 -0
  4. package/dist/client/assets/index-s_jjeqha.js +176 -0
  5. package/dist/client/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  6. package/dist/client/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  7. package/dist/client/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  8. package/dist/client/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  9. package/dist/client/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  10. package/dist/client/favicon.svg +62 -0
  11. package/dist/client/icons.svg +24 -0
  12. package/dist/client/index.html +14 -0
  13. package/dist/server/db.js +764 -0
  14. package/dist/server/db.js.map +1 -0
  15. package/dist/server/index.js +5134 -0
  16. package/dist/server/index.js.map +1 -0
  17. package/dist/server/lib/agent.js +1302 -0
  18. package/dist/server/lib/agent.js.map +1 -0
  19. package/dist/server/lib/buildChains.js +2 -0
  20. package/dist/server/lib/buildChains.js.map +1 -0
  21. package/dist/server/lib/buildFlow.js +59 -0
  22. package/dist/server/lib/buildFlow.js.map +1 -0
  23. package/dist/server/lib/buildSequences.js +599 -0
  24. package/dist/server/lib/buildSequences.js.map +1 -0
  25. package/dist/server/lib/bundleActivity.js +95 -0
  26. package/dist/server/lib/bundleActivity.js.map +1 -0
  27. package/dist/server/lib/bundlePullRequests.js +126 -0
  28. package/dist/server/lib/bundlePullRequests.js.map +1 -0
  29. package/dist/server/lib/chatMessages.js +60 -0
  30. package/dist/server/lib/chatMessages.js.map +1 -0
  31. package/dist/server/lib/chatTargets.js +123 -0
  32. package/dist/server/lib/chatTargets.js.map +1 -0
  33. package/dist/server/lib/chatTicketProposals.js +180 -0
  34. package/dist/server/lib/chatTicketProposals.js.map +1 -0
  35. package/dist/server/lib/chats.js +279 -0
  36. package/dist/server/lib/chats.js.map +1 -0
  37. package/dist/server/lib/config.js +3 -0
  38. package/dist/server/lib/config.js.map +1 -0
  39. package/dist/server/lib/cors.js +30 -0
  40. package/dist/server/lib/cors.js.map +1 -0
  41. package/dist/server/lib/directoryPicker.js +174 -0
  42. package/dist/server/lib/directoryPicker.js.map +1 -0
  43. package/dist/server/lib/git.js +1284 -0
  44. package/dist/server/lib/git.js.map +1 -0
  45. package/dist/server/lib/integrations/github.js +511 -0
  46. package/dist/server/lib/integrations/github.js.map +1 -0
  47. package/dist/server/lib/integrations/index.js +162 -0
  48. package/dist/server/lib/integrations/index.js.map +1 -0
  49. package/dist/server/lib/integrations/jira.js +283 -0
  50. package/dist/server/lib/integrations/jira.js.map +1 -0
  51. package/dist/server/lib/integrations/planning.js +27 -0
  52. package/dist/server/lib/integrations/planning.js.map +1 -0
  53. package/dist/server/lib/integrations/types.js +2 -0
  54. package/dist/server/lib/integrations/types.js.map +1 -0
  55. package/dist/server/lib/lightweightPrompt.js +88 -0
  56. package/dist/server/lib/lightweightPrompt.js.map +1 -0
  57. package/dist/server/lib/models.js +219 -0
  58. package/dist/server/lib/models.js.map +1 -0
  59. package/dist/server/lib/preview.js +377 -0
  60. package/dist/server/lib/preview.js.map +1 -0
  61. package/dist/server/lib/previewProxy.js +659 -0
  62. package/dist/server/lib/previewProxy.js.map +1 -0
  63. package/dist/server/lib/projectAutoConfig.js +682 -0
  64. package/dist/server/lib/projectAutoConfig.js.map +1 -0
  65. package/dist/server/lib/projectFileSuggestions.js +133 -0
  66. package/dist/server/lib/projectFileSuggestions.js.map +1 -0
  67. package/dist/server/lib/projectMemory.js +1519 -0
  68. package/dist/server/lib/projectMemory.js.map +1 -0
  69. package/dist/server/lib/projectMemoryPrompt.js +390 -0
  70. package/dist/server/lib/projectMemoryPrompt.js.map +1 -0
  71. package/dist/server/lib/projectMemoryScan.js +681 -0
  72. package/dist/server/lib/projectMemoryScan.js.map +1 -0
  73. package/dist/server/lib/projectMemorySuggestions.js +166 -0
  74. package/dist/server/lib/projectMemorySuggestions.js.map +1 -0
  75. package/dist/server/lib/projectMemoryTransfer.js +958 -0
  76. package/dist/server/lib/projectMemoryTransfer.js.map +1 -0
  77. package/dist/server/lib/projects.js +569 -0
  78. package/dist/server/lib/projects.js.map +1 -0
  79. package/dist/server/lib/promptSkills.js +28 -0
  80. package/dist/server/lib/promptSkills.js.map +1 -0
  81. package/dist/server/lib/queue.js +15 -0
  82. package/dist/server/lib/queue.js.map +1 -0
  83. package/dist/server/lib/reviewFindings.js +390 -0
  84. package/dist/server/lib/reviewFindings.js.map +1 -0
  85. package/dist/server/lib/run.js +416 -0
  86. package/dist/server/lib/run.js.map +1 -0
  87. package/dist/server/lib/runtimePaths.js +93 -0
  88. package/dist/server/lib/runtimePaths.js.map +1 -0
  89. package/dist/server/lib/shell.js +27 -0
  90. package/dist/server/lib/shell.js.map +1 -0
  91. package/dist/server/lib/skills.js +124 -0
  92. package/dist/server/lib/skills.js.map +1 -0
  93. package/dist/server/lib/startDev.js +18 -0
  94. package/dist/server/lib/startDev.js.map +1 -0
  95. package/dist/server/lib/staticClient.js +80 -0
  96. package/dist/server/lib/staticClient.js.map +1 -0
  97. package/dist/server/lib/terminal.js +366 -0
  98. package/dist/server/lib/terminal.js.map +1 -0
  99. package/dist/server/lib/ticketDependencies.js +174 -0
  100. package/dist/server/lib/ticketDependencies.js.map +1 -0
  101. package/dist/server/lib/ticketMessages.js +65 -0
  102. package/dist/server/lib/ticketMessages.js.map +1 -0
  103. package/dist/server/lib/ticketOpenQuestions.js +128 -0
  104. package/dist/server/lib/ticketOpenQuestions.js.map +1 -0
  105. package/dist/server/lib/ticketUndo.js +549 -0
  106. package/dist/server/lib/ticketUndo.js.map +1 -0
  107. package/dist/server/lib/tickets.js +981 -0
  108. package/dist/server/lib/tickets.js.map +1 -0
  109. package/dist/server/lib/types.js +2 -0
  110. package/dist/server/lib/types.js.map +1 -0
  111. package/dist/server/package.json +3 -0
  112. package/dist/server/workers/build.js +229 -0
  113. package/dist/server/workers/build.js.map +1 -0
  114. package/dist/server/workers/chat.js +190 -0
  115. package/dist/server/workers/chat.js.map +1 -0
  116. package/dist/server/workers/followUp.js +204 -0
  117. package/dist/server/workers/followUp.js.map +1 -0
  118. package/dist/server/workers/plan.js +1130 -0
  119. package/dist/server/workers/plan.js.map +1 -0
  120. package/dist/server/workers/planFollowUp.js +360 -0
  121. package/dist/server/workers/planFollowUp.js.map +1 -0
  122. package/dist/server/workers/review.js +167 -0
  123. package/dist/server/workers/review.js.map +1 -0
  124. 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