@straiffi/archon 1.5.0 → 1.6.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/dist/client/assets/{GitHubActionsDialog-CQMqcAiI.js → GitHubActionsDialog-oMY61JXF.js} +1 -1
- package/dist/client/assets/{TestsDialog-iGukzvw2.js → TestsDialog-elElGi9L.js} +3 -4
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BG4rc0pD.js → architectureDiagram-3BPJPVTR-BNcdEMha.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-BifkewDx.js → blockDiagram-GPEHLZMM-xHTKb0-z.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-DEYVUDCs.js → c4Diagram-AAUBKEIU-kMngbYRg.js} +1 -1
- package/dist/client/assets/channel-DAW1OrrC.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-DD50YIcA.js → chunk-2J33WTMH-BVZnJcYa.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-DLbwwbdv.js → chunk-3OPIFGDE-Dq_H_MkG.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-BW-r3gXJ.js → chunk-5ZQYHXKU-_TwHLQwE.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-B5C_ENX7.js → chunk-727SXJPM-CKkS_93w.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-BUT3wmDg.js → chunk-AQP2D5EJ-BHSSO0SM.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-BO7wWVh_.js → chunk-CSCIHK7Q-CgzsiMAL.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-DUNFfKB2.js → chunk-KSCS5N6A-DM-rnicK.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-SYJQkU8_.js → chunk-L5ZTLDWV-CnMgoXZs.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-DSVJIylK.js → chunk-LZXEDZCA-CCr2z1iU.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-CxL5ue9A.js → chunk-ND2GUHAM-DtA6gjMn.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-BwxeYeqR.js → chunk-NZK2D7GU-_8fmSSaf.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-Covi8YcB.js → chunk-O5CBEL6O-B84elqwU.js} +1 -1
- package/dist/client/assets/{chunk-WU5MYG2G-DlQoHSB3.js → chunk-WU5MYG2G-DBvmvPPt.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BmFh-n6n.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-tA1aAbh4.js +1 -0
- package/dist/client/assets/{dagre-BM42HDAG-CuNExSrh.js → dagre-BM42HDAG-C-QssoNU.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-Bk7EYe6Z.js → diagram-2AECGRRQ-8RpG5DGb.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-koHJCfAk.js → diagram-5GNKFQAL-Dk2raJWq.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-DI1Ecx54.js → diagram-KO2AKTUF-7iZuIIef.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-CmFk1MNf.js → diagram-LMA3HP47-JLaaaEE6.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-YTiDn39n.js → diagram-OG6HWLK6-jeL3-Rd3.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-DyWU43DX.js → erDiagram-TEJ5UH35-B6uFrMrQ.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-DgbU15ZJ.js → flowDiagram-I6XJVG4X-BFWnhmx2.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CT39xvdk.js → ganttDiagram-6RSMTGT7-nxON-689.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DfK_1xRj.js → gitGraphDiagram-PVQCEYII-DEAsC0GZ.js} +1 -1
- package/dist/client/assets/index-BzDXl4AF.js +265 -0
- package/dist/client/assets/index-DSap5tSX.css +2 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-C7z_7194.js → infoDiagram-5YYISTIA-CAZHR2I8.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DhJ7h17n.js → ishikawaDiagram-YF4QCWOH-DEe4SNzl.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-CtkhoMAA.js → journeyDiagram-JHISSGLW-CG2kTAj9.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-Bcas7cg9.js → kanban-definition-UN3LZRKU-DUWutAjA.js} +1 -1
- package/dist/client/assets/{line-WO_ReaCl.js → line-B1SFN8mz.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-WmRuf19A.js → mermaid-parser.core-C-dyz_nb.js} +1 -1
- package/dist/client/assets/{mermaid.core-BMdju4xN.js → mermaid.core-nm4BMZuF.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-DLxsUQmn.js → mindmap-definition-RKZ34NQL-DZEd-RCz.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-D-oaWpEg.js → pieDiagram-4H26LBE5-WXxrzUwX.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-B9krq8I-.js → quadrantDiagram-W4KKPZXB-BClH2kQx.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-m2VnH3vv.js → requirementDiagram-4Y6WPE33-ByolUxfP.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-bCF9Ot9-.js → sankeyDiagram-5OEKKPKP-B2vcb3U1.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-C-4zDJcn.js → sequenceDiagram-3UESZ5HK-J8Nt0GXG.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-CksbJK59.js → stateDiagram-AJRCARHV-CrgZ4Uwa.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CUEBQOsD.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-DDxpdMfo.js → timeline-definition-PNZ67QCA-CXebl6LI.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-BfQM7_zD.js → vennDiagram-CIIHVFJN-DLBqI39h.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-CuixXlAX.js → wardleyDiagram-YWT4CUSO-ku4fViV9.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-53YB-tf6.js → xychartDiagram-2RQKCTM6-5Jh-bBeb.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/db.js +288 -3
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.js +323 -97
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/git.js +18 -0
- package/dist/server/lib/git.js.map +1 -1
- package/dist/server/lib/models.js +2 -38
- package/dist/server/lib/models.js.map +1 -1
- package/dist/server/lib/projectMemory.js +1073 -276
- package/dist/server/lib/projectMemory.js.map +1 -1
- package/dist/server/lib/projectMemoryCursor.js +27 -0
- package/dist/server/lib/projectMemoryCursor.js.map +1 -0
- package/dist/server/lib/projectMemoryEvents.js +24 -0
- package/dist/server/lib/projectMemoryEvents.js.map +1 -0
- package/dist/server/lib/projectMemoryLimits.js +61 -0
- package/dist/server/lib/projectMemoryLimits.js.map +1 -0
- package/dist/server/lib/projectMemoryPrompt.js +299 -110
- package/dist/server/lib/projectMemoryPrompt.js.map +1 -1
- package/dist/server/lib/projectMemoryScan.js +461 -493
- package/dist/server/lib/projectMemoryScan.js.map +1 -1
- package/dist/server/lib/projectMemorySelections.js +97 -0
- package/dist/server/lib/projectMemorySelections.js.map +1 -0
- package/dist/server/lib/projectMemorySuggestions.js +19 -4
- package/dist/server/lib/projectMemorySuggestions.js.map +1 -1
- package/dist/server/lib/projectMemoryTransfer.js +635 -83
- package/dist/server/lib/projectMemoryTransfer.js.map +1 -1
- package/dist/server/lib/projectPathGlob.js +40 -0
- package/dist/server/lib/projectPathGlob.js.map +1 -0
- package/dist/server/lib/projects.js +50 -0
- package/dist/server/lib/projects.js.map +1 -1
- package/dist/server/lib/specReviewContext.js +2 -1
- package/dist/server/lib/specReviewContext.js.map +1 -1
- package/dist/server/lib/tickets.js +6 -0
- package/dist/server/lib/tickets.js.map +1 -1
- package/dist/server/workers/build.js +6 -2
- package/dist/server/workers/build.js.map +1 -1
- package/dist/server/workers/chat.js +9 -6
- package/dist/server/workers/chat.js.map +1 -1
- package/dist/server/workers/followUp.js +15 -8
- package/dist/server/workers/followUp.js.map +1 -1
- package/dist/server/workers/plan.js +61 -11
- package/dist/server/workers/plan.js.map +1 -1
- package/dist/server/workers/planFollowUp.js +3 -0
- package/dist/server/workers/planFollowUp.js.map +1 -1
- package/dist/server/workers/review.js +26 -11
- package/dist/server/workers/review.js.map +1 -1
- package/dist/server/workers/specReview.js +10 -0
- package/dist/server/workers/specReview.js.map +1 -1
- package/dist/server/workers/specSession.js +5 -2
- package/dist/server/workers/specSession.js.map +1 -1
- package/package.json +1 -1
- package/dist/client/assets/channel-gy-JdvF3.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-B1Xs78Ge.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-BU_Soq1k.js +0 -1
- package/dist/client/assets/index-BCqJfUGA.css +0 -2
- package/dist/client/assets/index-C_XaTJyb.js +0 -261
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BvbN4CmJ.js +0 -1
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
2
|
import db from '../db.js';
|
|
3
|
-
|
|
4
|
-
const
|
|
3
|
+
import { containsReservedProjectMemoryControlMarker, PROJECT_MEMORY_LIMITS } from './projectMemoryLimits.js';
|
|
4
|
+
const PROJECT_MEMORY_ARCHIVE_EXPORT_VERSION = 7;
|
|
5
|
+
const PROJECT_MEMORY_ARCHIVE_SUPPORTED_VERSIONS = [1, 2, 3, 4, 5, 6, 7];
|
|
5
6
|
const PROJECT_MEMORY_STAGES = ['plan', 'build', 'review', 'follow_up', 'plan_follow_up', 'chat'];
|
|
6
7
|
const PROJECT_CONVENTION_PRIORITIES = ['critical', 'normal', 'low'];
|
|
7
8
|
const PROJECT_CONVENTION_INJECTION_MODES = ['always', 'relevant'];
|
|
8
9
|
const PROJECT_CONVENTION_STATUSES = ['active', 'archived'];
|
|
9
10
|
const PROJECT_DECISION_STATUSES = ['accepted', 'archived', 'superseded'];
|
|
10
11
|
const PROJECT_CONTEXT_SCAN_STATUSES = ['pending', 'running', 'done', 'error'];
|
|
12
|
+
const IMPORTED_ACTIVE_SCAN_ERROR = 'Imported scan was active when archived and cannot be resumed.';
|
|
11
13
|
const PROJECT_CONTEXT_ARTIFACT_KINDS = ['architecture', 'key_areas', 'conventions', 'risks'];
|
|
12
14
|
const TOOL_NAMES = ['claude', 'opencode'];
|
|
15
|
+
const PROJECT_CONTEXT_SCAN_ORIGINS = ['local_scan', 'archive_import'];
|
|
16
|
+
const PROJECT_CONTEXT_SCAN_SKIP_REASONS = [
|
|
17
|
+
'hard_excluded',
|
|
18
|
+
'user_excluded',
|
|
19
|
+
'not_included',
|
|
20
|
+
'symlink',
|
|
21
|
+
'binary',
|
|
22
|
+
'unreadable',
|
|
23
|
+
'file_size_limit',
|
|
24
|
+
'file_count_limit',
|
|
25
|
+
'directory_limit',
|
|
26
|
+
];
|
|
13
27
|
const isRecord = (value) => {
|
|
14
28
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15
29
|
};
|
|
@@ -26,25 +40,25 @@ const normalizeOptionalString = (value, fieldName) => {
|
|
|
26
40
|
return { value: null };
|
|
27
41
|
}
|
|
28
42
|
if (typeof value !== 'string') {
|
|
29
|
-
return { error: `${fieldName} must be a string or null
|
|
43
|
+
return { error: `${fieldName} must be a string or null`, kind: 'malformed' };
|
|
30
44
|
}
|
|
31
45
|
const trimmed = value.trim();
|
|
32
46
|
return { value: trimmed === '' ? null : trimmed };
|
|
33
47
|
};
|
|
34
48
|
const validateRequiredString = (value, fieldName) => {
|
|
35
49
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
36
|
-
return { error: `${fieldName} must be a non-empty string
|
|
50
|
+
return { error: `${fieldName} must be a non-empty string`, kind: 'malformed' };
|
|
37
51
|
}
|
|
38
52
|
return { value: value.trim() };
|
|
39
53
|
};
|
|
40
54
|
const normalizeStringArray = (value, fieldName) => {
|
|
41
55
|
if (!Array.isArray(value)) {
|
|
42
|
-
return { error: `${fieldName} must be an array of strings
|
|
56
|
+
return { error: `${fieldName} must be an array of strings`, kind: 'malformed' };
|
|
43
57
|
}
|
|
44
58
|
const normalized = [];
|
|
45
59
|
for (const entry of value) {
|
|
46
60
|
if (typeof entry !== 'string' || entry.trim() === '') {
|
|
47
|
-
return { error: `${fieldName} must be an array of strings
|
|
61
|
+
return { error: `${fieldName} must be an array of strings`, kind: 'malformed' };
|
|
48
62
|
}
|
|
49
63
|
const trimmed = entry.trim();
|
|
50
64
|
if (!normalized.includes(trimmed)) {
|
|
@@ -55,12 +69,12 @@ const normalizeStringArray = (value, fieldName) => {
|
|
|
55
69
|
};
|
|
56
70
|
const validateStages = (value) => {
|
|
57
71
|
if (!Array.isArray(value)) {
|
|
58
|
-
return { error: 'memory.conventions[].stages must be an array of valid project memory stages' };
|
|
72
|
+
return { error: 'memory.conventions[].stages must be an array of valid project memory stages', kind: 'malformed' };
|
|
59
73
|
}
|
|
60
74
|
const normalized = [];
|
|
61
75
|
for (const stage of value) {
|
|
62
76
|
if (!PROJECT_MEMORY_STAGES.includes(stage)) {
|
|
63
|
-
return { error: 'memory.conventions[].stages must be an array of valid project memory stages' };
|
|
77
|
+
return { error: 'memory.conventions[].stages must be an array of valid project memory stages', kind: 'malformed' };
|
|
64
78
|
}
|
|
65
79
|
if (!normalized.includes(stage)) {
|
|
66
80
|
normalized.push(stage);
|
|
@@ -70,10 +84,64 @@ const validateStages = (value) => {
|
|
|
70
84
|
};
|
|
71
85
|
const validateEnum = (value, values, fieldName) => {
|
|
72
86
|
if (!values.includes(value)) {
|
|
73
|
-
return { error: `${fieldName} must be one of ${values.join(', ')}
|
|
87
|
+
return { error: `${fieldName} must be one of ${values.join(', ')}`, kind: 'malformed' };
|
|
74
88
|
}
|
|
75
89
|
return { value: value };
|
|
76
90
|
};
|
|
91
|
+
const validateStringLimit = (result, fieldName, limit) => {
|
|
92
|
+
if ('error' in result) {
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
if (result.value !== null && result.value.length > limit) {
|
|
96
|
+
return { error: `${fieldName} must not exceed ${limit} characters`, kind: 'limit', code: 'field_too_large', field: fieldName, limit };
|
|
97
|
+
}
|
|
98
|
+
if (result.value !== null && containsReservedProjectMemoryControlMarker(result.value)) {
|
|
99
|
+
return { error: `${fieldName} must not contain a reserved control marker`, kind: 'malformed', code: 'reserved_control_marker', field: fieldName };
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
};
|
|
103
|
+
const validateArrayLimit = (value, fieldName, limit) => {
|
|
104
|
+
if (value.length > limit) {
|
|
105
|
+
return { error: `${fieldName} must not contain more than ${limit} items`, kind: 'limit', code: 'field_too_large', field: fieldName, limit };
|
|
106
|
+
}
|
|
107
|
+
return { value };
|
|
108
|
+
};
|
|
109
|
+
const validateImplications = (value, fieldName) => {
|
|
110
|
+
const implications = normalizeStringArray(value, fieldName);
|
|
111
|
+
if ('error' in implications) {
|
|
112
|
+
return implications;
|
|
113
|
+
}
|
|
114
|
+
const count = validateArrayLimit(implications.value, fieldName, PROJECT_MEMORY_LIMITS.implications);
|
|
115
|
+
if ('error' in count) {
|
|
116
|
+
return count;
|
|
117
|
+
}
|
|
118
|
+
for (const implication of count.value) {
|
|
119
|
+
if (implication.length > PROJECT_MEMORY_LIMITS.implication) {
|
|
120
|
+
return { error: `${fieldName}[] must not exceed ${PROJECT_MEMORY_LIMITS.implication} characters`, kind: 'limit', code: 'field_too_large', field: fieldName, limit: PROJECT_MEMORY_LIMITS.implication };
|
|
121
|
+
}
|
|
122
|
+
if (containsReservedProjectMemoryControlMarker(implication)) {
|
|
123
|
+
return { error: `${fieldName}[] must not contain a reserved control marker`, kind: 'malformed', code: 'reserved_control_marker', field: fieldName };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return count;
|
|
127
|
+
};
|
|
128
|
+
const validateApplicabilityStrings = (value, fieldName) => {
|
|
129
|
+
const strings = normalizeStringArray(value ?? [], fieldName);
|
|
130
|
+
if ('error' in strings) {
|
|
131
|
+
return strings;
|
|
132
|
+
}
|
|
133
|
+
const count = validateArrayLimit(strings.value, fieldName, PROJECT_MEMORY_LIMITS.applicabilityItems);
|
|
134
|
+
if ('error' in count) {
|
|
135
|
+
return count;
|
|
136
|
+
}
|
|
137
|
+
for (const entry of count.value) {
|
|
138
|
+
const validated = validateStringLimit({ value: entry }, fieldName, PROJECT_MEMORY_LIMITS.applicabilityItem);
|
|
139
|
+
if ('error' in validated) {
|
|
140
|
+
return validated;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return count;
|
|
144
|
+
};
|
|
77
145
|
const validateNullableEnum = (value, values, fieldName) => {
|
|
78
146
|
if (value == null) {
|
|
79
147
|
return { value: null };
|
|
@@ -101,6 +169,41 @@ const parseStringArrayColumn = (value) => {
|
|
|
101
169
|
.map(entry => entry.trim())
|
|
102
170
|
.filter(Boolean);
|
|
103
171
|
};
|
|
172
|
+
const createManualArchiveOrigin = (createdAt) => ({
|
|
173
|
+
type: 'manual',
|
|
174
|
+
suggestion_id: null,
|
|
175
|
+
source_stage: null,
|
|
176
|
+
source_type: null,
|
|
177
|
+
source_label: null,
|
|
178
|
+
ticket_id: null,
|
|
179
|
+
chat_session_id: null,
|
|
180
|
+
spec_session_id: null,
|
|
181
|
+
suggested_at: null,
|
|
182
|
+
accepted_at: createdAt,
|
|
183
|
+
});
|
|
184
|
+
const parseArchiveOrigin = (value, createdAt) => {
|
|
185
|
+
const parsed = parseJsonColumn(value);
|
|
186
|
+
if (!isRecord(parsed) || parsed.type !== 'agent_suggestion') {
|
|
187
|
+
return createManualArchiveOrigin(createdAt);
|
|
188
|
+
}
|
|
189
|
+
const sourceType = parsed.source_type === 'ticket' || parsed.source_type === 'chat_session' || parsed.source_type === 'spec_session'
|
|
190
|
+
? parsed.source_type
|
|
191
|
+
: null;
|
|
192
|
+
return {
|
|
193
|
+
type: 'agent_suggestion',
|
|
194
|
+
suggestion_id: typeof parsed.suggestion_id === 'string' ? parsed.suggestion_id : null,
|
|
195
|
+
source_stage: PROJECT_MEMORY_STAGES.includes(parsed.source_stage)
|
|
196
|
+
? parsed.source_stage
|
|
197
|
+
: null,
|
|
198
|
+
source_type: sourceType,
|
|
199
|
+
source_label: typeof parsed.source_label === 'string' ? parsed.source_label : null,
|
|
200
|
+
ticket_id: typeof parsed.ticket_id === 'string' ? parsed.ticket_id : null,
|
|
201
|
+
chat_session_id: typeof parsed.chat_session_id === 'string' ? parsed.chat_session_id : null,
|
|
202
|
+
spec_session_id: typeof parsed.spec_session_id === 'string' ? parsed.spec_session_id : null,
|
|
203
|
+
suggested_at: typeof parsed.suggested_at === 'string' ? parsed.suggested_at : null,
|
|
204
|
+
accepted_at: typeof parsed.accepted_at === 'string' ? parsed.accepted_at : createdAt,
|
|
205
|
+
};
|
|
206
|
+
};
|
|
104
207
|
const serializeConvention = (row) => {
|
|
105
208
|
return {
|
|
106
209
|
title: row.title,
|
|
@@ -108,6 +211,8 @@ const serializeConvention = (row) => {
|
|
|
108
211
|
instruction: row.instruction,
|
|
109
212
|
rationale: row.rationale,
|
|
110
213
|
stages: parseStringArrayColumn(row.stages_json),
|
|
214
|
+
path_globs: parseStringArrayColumn(row.path_globs_json),
|
|
215
|
+
component_tags: parseStringArrayColumn(row.component_tags_json),
|
|
111
216
|
priority: row.priority,
|
|
112
217
|
injection_mode: PROJECT_CONVENTION_INJECTION_MODES.includes(row.injection_mode)
|
|
113
218
|
? row.injection_mode
|
|
@@ -124,22 +229,31 @@ const serializeDecision = (row) => {
|
|
|
124
229
|
decision: row.decision,
|
|
125
230
|
rationale: row.rationale,
|
|
126
231
|
implications: parseStringArrayColumn(row.implications_json),
|
|
232
|
+
stages: parseStringArrayColumn(row.stages_json),
|
|
233
|
+
path_globs: parseStringArrayColumn(row.path_globs_json),
|
|
234
|
+
component_tags: parseStringArrayColumn(row.component_tags_json),
|
|
127
235
|
status: row.status,
|
|
128
236
|
created_at: row.created_at,
|
|
129
237
|
updated_at: row.updated_at,
|
|
130
238
|
};
|
|
131
239
|
};
|
|
132
240
|
const serializeMemoryItemFromConvention = (row) => {
|
|
241
|
+
const origin = parseArchiveOrigin(row.origin_json, row.created_at);
|
|
133
242
|
return {
|
|
243
|
+
archive_id: row.id,
|
|
244
|
+
superseded_by_archive_id: null,
|
|
134
245
|
kind: 'convention',
|
|
135
246
|
state: row.status,
|
|
136
|
-
source: 'manual',
|
|
247
|
+
source: origin.type === 'agent_suggestion' ? 'agent' : 'manual',
|
|
248
|
+
origin,
|
|
137
249
|
title: row.title,
|
|
138
250
|
scope: row.scope,
|
|
139
251
|
content: row.instruction,
|
|
140
252
|
rationale: row.rationale,
|
|
141
253
|
implications: [],
|
|
142
254
|
stages: parseStringArrayColumn(row.stages_json),
|
|
255
|
+
path_globs: parseStringArrayColumn(row.path_globs_json),
|
|
256
|
+
component_tags: parseStringArrayColumn(row.component_tags_json),
|
|
143
257
|
priority: row.priority,
|
|
144
258
|
injection_mode: PROJECT_CONVENTION_INJECTION_MODES.includes(row.injection_mode)
|
|
145
259
|
? row.injection_mode
|
|
@@ -149,16 +263,22 @@ const serializeMemoryItemFromConvention = (row) => {
|
|
|
149
263
|
};
|
|
150
264
|
};
|
|
151
265
|
const serializeMemoryItemFromDecision = (row) => {
|
|
266
|
+
const origin = parseArchiveOrigin(row.origin_json, row.created_at);
|
|
152
267
|
return {
|
|
268
|
+
archive_id: row.id,
|
|
269
|
+
superseded_by_archive_id: row.superseded_by_decision_id,
|
|
153
270
|
kind: 'decision',
|
|
154
271
|
state: row.status === 'accepted' ? 'active' : row.status,
|
|
155
|
-
source: 'manual',
|
|
272
|
+
source: origin.type === 'agent_suggestion' ? 'agent' : 'manual',
|
|
273
|
+
origin,
|
|
156
274
|
title: row.title,
|
|
157
275
|
scope: row.scope,
|
|
158
276
|
content: row.decision,
|
|
159
277
|
rationale: row.rationale,
|
|
160
278
|
implications: parseStringArrayColumn(row.implications_json),
|
|
161
|
-
stages:
|
|
279
|
+
stages: parseStringArrayColumn(row.stages_json),
|
|
280
|
+
path_globs: parseStringArrayColumn(row.path_globs_json),
|
|
281
|
+
component_tags: parseStringArrayColumn(row.component_tags_json),
|
|
162
282
|
priority: null,
|
|
163
283
|
injection_mode: null,
|
|
164
284
|
created_at: row.created_at,
|
|
@@ -181,6 +301,8 @@ const fingerprintConvention = (item) => {
|
|
|
181
301
|
normalizeString(item.instruction),
|
|
182
302
|
normalizeString(item.rationale),
|
|
183
303
|
[...item.stages].sort(),
|
|
304
|
+
[...(item.path_globs ?? [])].sort(),
|
|
305
|
+
[...(item.component_tags ?? [])].sort(),
|
|
184
306
|
item.priority,
|
|
185
307
|
item.injection_mode,
|
|
186
308
|
item.status,
|
|
@@ -193,10 +315,26 @@ const fingerprintDecision = (item) => {
|
|
|
193
315
|
normalizeString(item.decision),
|
|
194
316
|
normalizeString(item.rationale),
|
|
195
317
|
item.implications.map(entry => normalizeString(entry)),
|
|
318
|
+
[...(item.stages ?? [])].sort(),
|
|
319
|
+
[...(item.path_globs ?? [])].sort(),
|
|
320
|
+
[...(item.component_tags ?? [])].sort(),
|
|
196
321
|
item.status,
|
|
197
322
|
]);
|
|
198
323
|
};
|
|
199
324
|
const fingerprintScan = (item) => {
|
|
325
|
+
const manifest = item.manifest
|
|
326
|
+
? {
|
|
327
|
+
sampled_paths: [...item.manifest.sampled_paths].sort(),
|
|
328
|
+
skipped_counts: Object.fromEntries(Object.entries(item.manifest.skipped_counts).sort(([left], [right]) => left.localeCompare(right))),
|
|
329
|
+
include_globs: [...item.manifest.include_globs].sort(),
|
|
330
|
+
exclude_globs: [...item.manifest.exclude_globs].sort(),
|
|
331
|
+
hard_exclusions: [...item.manifest.hard_exclusions].sort(),
|
|
332
|
+
repository_characters_sent: item.manifest.repository_characters_sent,
|
|
333
|
+
redaction_count: item.manifest.redaction_count,
|
|
334
|
+
truncated_output_fields: [...item.manifest.truncated_output_fields].sort(),
|
|
335
|
+
repository_changed_during_scan: item.manifest.repository_changed_during_scan,
|
|
336
|
+
}
|
|
337
|
+
: null;
|
|
200
338
|
return JSON.stringify([
|
|
201
339
|
item.status,
|
|
202
340
|
normalizeString(item.repo_head),
|
|
@@ -206,6 +344,12 @@ const fingerprintScan = (item) => {
|
|
|
206
344
|
normalizeString(item.scanner_variant),
|
|
207
345
|
normalizeString(item.summary_markdown),
|
|
208
346
|
normalizeString(item.error),
|
|
347
|
+
item.origin ?? 'local_scan',
|
|
348
|
+
normalizeString(item.scanner_provider),
|
|
349
|
+
item.scanned_at ?? null,
|
|
350
|
+
item.completed_at ?? null,
|
|
351
|
+
item.working_tree_dirty ?? null,
|
|
352
|
+
manifest,
|
|
209
353
|
]);
|
|
210
354
|
};
|
|
211
355
|
const fingerprintArtifact = (item) => {
|
|
@@ -224,21 +368,21 @@ const sanitizeJsonValue = (value) => {
|
|
|
224
368
|
};
|
|
225
369
|
const validateArchiveConvention = (value) => {
|
|
226
370
|
if (!isRecord(value)) {
|
|
227
|
-
return { error: 'memory.conventions[] must be an object' };
|
|
371
|
+
return { error: 'memory.conventions[] must be an object', kind: 'malformed' };
|
|
228
372
|
}
|
|
229
|
-
const title = validateRequiredString(value.title, 'memory.conventions[].title');
|
|
373
|
+
const title = validateStringLimit(validateRequiredString(value.title, 'memory.conventions[].title'), 'memory.conventions[].title', PROJECT_MEMORY_LIMITS.title);
|
|
230
374
|
if ('error' in title) {
|
|
231
375
|
return title;
|
|
232
376
|
}
|
|
233
|
-
const scope = normalizeOptionalString(value.scope, 'memory.conventions[].scope');
|
|
377
|
+
const scope = validateStringLimit(normalizeOptionalString(value.scope, 'memory.conventions[].scope'), 'memory.conventions[].scope', PROJECT_MEMORY_LIMITS.scope);
|
|
234
378
|
if ('error' in scope) {
|
|
235
379
|
return scope;
|
|
236
380
|
}
|
|
237
|
-
const instruction = validateRequiredString(value.instruction, 'memory.conventions[].instruction');
|
|
381
|
+
const instruction = validateStringLimit(validateRequiredString(value.instruction, 'memory.conventions[].instruction'), 'memory.conventions[].instruction', PROJECT_MEMORY_LIMITS.content);
|
|
238
382
|
if ('error' in instruction) {
|
|
239
383
|
return instruction;
|
|
240
384
|
}
|
|
241
|
-
const rationale = normalizeOptionalString(value.rationale, 'memory.conventions[].rationale');
|
|
385
|
+
const rationale = validateStringLimit(normalizeOptionalString(value.rationale, 'memory.conventions[].rationale'), 'memory.conventions[].rationale', PROJECT_MEMORY_LIMITS.rationale);
|
|
242
386
|
if ('error' in rationale) {
|
|
243
387
|
return rationale;
|
|
244
388
|
}
|
|
@@ -246,6 +390,14 @@ const validateArchiveConvention = (value) => {
|
|
|
246
390
|
if ('error' in stages) {
|
|
247
391
|
return stages;
|
|
248
392
|
}
|
|
393
|
+
const pathGlobs = validateApplicabilityStrings(value.path_globs, 'memory.conventions[].path_globs');
|
|
394
|
+
if ('error' in pathGlobs) {
|
|
395
|
+
return pathGlobs;
|
|
396
|
+
}
|
|
397
|
+
const componentTags = validateApplicabilityStrings(value.component_tags, 'memory.conventions[].component_tags');
|
|
398
|
+
if ('error' in componentTags) {
|
|
399
|
+
return componentTags;
|
|
400
|
+
}
|
|
249
401
|
const priority = validateEnum(value.priority, PROJECT_CONVENTION_PRIORITIES, 'memory.conventions[].priority');
|
|
250
402
|
if ('error' in priority) {
|
|
251
403
|
return priority;
|
|
@@ -275,6 +427,8 @@ const validateArchiveConvention = (value) => {
|
|
|
275
427
|
instruction: instruction.value,
|
|
276
428
|
rationale: rationale.value,
|
|
277
429
|
stages: stages.value,
|
|
430
|
+
path_globs: pathGlobs.value,
|
|
431
|
+
component_tags: componentTags.value,
|
|
278
432
|
priority: priority.value,
|
|
279
433
|
injection_mode: injectionMode.value,
|
|
280
434
|
status: status.value,
|
|
@@ -285,28 +439,40 @@ const validateArchiveConvention = (value) => {
|
|
|
285
439
|
};
|
|
286
440
|
const validateArchiveDecision = (value) => {
|
|
287
441
|
if (!isRecord(value)) {
|
|
288
|
-
return { error: 'memory.decisions[] must be an object' };
|
|
442
|
+
return { error: 'memory.decisions[] must be an object', kind: 'malformed' };
|
|
289
443
|
}
|
|
290
|
-
const title = validateRequiredString(value.title, 'memory.decisions[].title');
|
|
444
|
+
const title = validateStringLimit(validateRequiredString(value.title, 'memory.decisions[].title'), 'memory.decisions[].title', PROJECT_MEMORY_LIMITS.title);
|
|
291
445
|
if ('error' in title) {
|
|
292
446
|
return title;
|
|
293
447
|
}
|
|
294
|
-
const scope = normalizeOptionalString(value.scope, 'memory.decisions[].scope');
|
|
448
|
+
const scope = validateStringLimit(normalizeOptionalString(value.scope, 'memory.decisions[].scope'), 'memory.decisions[].scope', PROJECT_MEMORY_LIMITS.scope);
|
|
295
449
|
if ('error' in scope) {
|
|
296
450
|
return scope;
|
|
297
451
|
}
|
|
298
|
-
const decision = validateRequiredString(value.decision, 'memory.decisions[].decision');
|
|
452
|
+
const decision = validateStringLimit(validateRequiredString(value.decision, 'memory.decisions[].decision'), 'memory.decisions[].decision', PROJECT_MEMORY_LIMITS.content);
|
|
299
453
|
if ('error' in decision) {
|
|
300
454
|
return decision;
|
|
301
455
|
}
|
|
302
|
-
const rationale = normalizeOptionalString(value.rationale, 'memory.decisions[].rationale');
|
|
456
|
+
const rationale = validateStringLimit(normalizeOptionalString(value.rationale, 'memory.decisions[].rationale'), 'memory.decisions[].rationale', PROJECT_MEMORY_LIMITS.rationale);
|
|
303
457
|
if ('error' in rationale) {
|
|
304
458
|
return rationale;
|
|
305
459
|
}
|
|
306
|
-
const implications =
|
|
460
|
+
const implications = validateImplications(value.implications, 'memory.decisions[].implications');
|
|
307
461
|
if ('error' in implications) {
|
|
308
462
|
return implications;
|
|
309
463
|
}
|
|
464
|
+
const stages = validateStages(value.stages ?? []);
|
|
465
|
+
if ('error' in stages) {
|
|
466
|
+
return stages;
|
|
467
|
+
}
|
|
468
|
+
const pathGlobs = validateApplicabilityStrings(value.path_globs, 'memory.decisions[].path_globs');
|
|
469
|
+
if ('error' in pathGlobs) {
|
|
470
|
+
return pathGlobs;
|
|
471
|
+
}
|
|
472
|
+
const componentTags = validateApplicabilityStrings(value.component_tags, 'memory.decisions[].component_tags');
|
|
473
|
+
if ('error' in componentTags) {
|
|
474
|
+
return componentTags;
|
|
475
|
+
}
|
|
310
476
|
const status = validateEnum(value.status, PROJECT_DECISION_STATUSES, 'memory.decisions[].status');
|
|
311
477
|
if ('error' in status) {
|
|
312
478
|
return status;
|
|
@@ -326,15 +492,74 @@ const validateArchiveDecision = (value) => {
|
|
|
326
492
|
decision: decision.value,
|
|
327
493
|
rationale: rationale.value,
|
|
328
494
|
implications: implications.value,
|
|
495
|
+
stages: stages.value,
|
|
496
|
+
path_globs: pathGlobs.value,
|
|
497
|
+
component_tags: componentTags.value,
|
|
329
498
|
status: status.value,
|
|
330
499
|
created_at: createdAt.value,
|
|
331
500
|
updated_at: updatedAt.value,
|
|
332
501
|
},
|
|
333
502
|
};
|
|
334
503
|
};
|
|
335
|
-
const
|
|
504
|
+
const validateArchiveOrigin = (value) => {
|
|
505
|
+
if (!isRecord(value)) {
|
|
506
|
+
return { error: 'memory.items[].origin must be an object', kind: 'malformed' };
|
|
507
|
+
}
|
|
508
|
+
const type = validateEnum(value.type, ['manual', 'agent_suggestion'], 'memory.items[].origin.type');
|
|
509
|
+
if ('error' in type) {
|
|
510
|
+
return type;
|
|
511
|
+
}
|
|
512
|
+
if (type.value === 'manual') {
|
|
513
|
+
return { value: createManualArchiveOrigin(typeof value.accepted_at === 'string' ? value.accepted_at : new Date().toISOString()) };
|
|
514
|
+
}
|
|
515
|
+
const sourceType = validateEnum(value.source_type, ['ticket', 'chat_session', 'spec_session'], 'memory.items[].origin.source_type');
|
|
516
|
+
if ('error' in sourceType) {
|
|
517
|
+
return sourceType;
|
|
518
|
+
}
|
|
519
|
+
const sourceLabel = validateRequiredString(value.source_label, 'memory.items[].origin.source_label');
|
|
520
|
+
if ('error' in sourceLabel) {
|
|
521
|
+
return sourceLabel;
|
|
522
|
+
}
|
|
523
|
+
const sourceStage = validateEnum(value.source_stage, PROJECT_MEMORY_STAGES, 'memory.items[].origin.source_stage');
|
|
524
|
+
if ('error' in sourceStage) {
|
|
525
|
+
return sourceStage;
|
|
526
|
+
}
|
|
527
|
+
const suggestedAt = validateRequiredString(value.suggested_at, 'memory.items[].origin.suggested_at');
|
|
528
|
+
if ('error' in suggestedAt) {
|
|
529
|
+
return suggestedAt;
|
|
530
|
+
}
|
|
531
|
+
const acceptedAt = validateRequiredString(value.accepted_at, 'memory.items[].origin.accepted_at');
|
|
532
|
+
if ('error' in acceptedAt) {
|
|
533
|
+
return acceptedAt;
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
value: {
|
|
537
|
+
type: 'agent_suggestion',
|
|
538
|
+
suggestion_id: typeof value.suggestion_id === 'string' ? value.suggestion_id : null,
|
|
539
|
+
source_stage: sourceStage.value,
|
|
540
|
+
source_type: sourceType.value,
|
|
541
|
+
source_label: sourceLabel.value,
|
|
542
|
+
ticket_id: typeof value.ticket_id === 'string' ? value.ticket_id : null,
|
|
543
|
+
chat_session_id: typeof value.chat_session_id === 'string' ? value.chat_session_id : null,
|
|
544
|
+
spec_session_id: typeof value.spec_session_id === 'string' ? value.spec_session_id : null,
|
|
545
|
+
suggested_at: suggestedAt.value,
|
|
546
|
+
accepted_at: acceptedAt.value,
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
};
|
|
550
|
+
const validateArchiveItem = (value, { requiresOrigin = false, requiresRelationships = false } = {}) => {
|
|
336
551
|
if (!isRecord(value)) {
|
|
337
|
-
return { error: 'memory.items[] must be an object' };
|
|
552
|
+
return { error: 'memory.items[] must be an object', kind: 'malformed' };
|
|
553
|
+
}
|
|
554
|
+
const archiveId = requiresRelationships ? validateRequiredString(value.archive_id, 'memory.items[].archive_id') : { value: undefined };
|
|
555
|
+
if ('error' in archiveId) {
|
|
556
|
+
return archiveId;
|
|
557
|
+
}
|
|
558
|
+
const supersededByArchiveId = requiresRelationships
|
|
559
|
+
? normalizeOptionalString(value.superseded_by_archive_id, 'memory.items[].superseded_by_archive_id')
|
|
560
|
+
: { value: undefined };
|
|
561
|
+
if ('error' in supersededByArchiveId) {
|
|
562
|
+
return supersededByArchiveId;
|
|
338
563
|
}
|
|
339
564
|
const kind = validateEnum(value.kind, ['decision', 'convention'], 'memory.items[].kind');
|
|
340
565
|
if ('error' in kind) {
|
|
@@ -344,36 +569,56 @@ const validateArchiveItem = (value) => {
|
|
|
344
569
|
if ('error' in state) {
|
|
345
570
|
return state;
|
|
346
571
|
}
|
|
347
|
-
const source = validateEnum(value.source, ['manual'], 'memory.items[].source');
|
|
572
|
+
const source = validateEnum(value.source, requiresOrigin ? ['manual', 'agent'] : ['manual'], 'memory.items[].source');
|
|
348
573
|
if ('error' in source) {
|
|
349
574
|
return source;
|
|
350
575
|
}
|
|
576
|
+
const origin = requiresOrigin
|
|
577
|
+
? validateArchiveOrigin(value.origin)
|
|
578
|
+
: { value: undefined };
|
|
579
|
+
if ('error' in origin) {
|
|
580
|
+
return origin;
|
|
581
|
+
}
|
|
582
|
+
if (source.value === 'agent' && origin.value?.type !== 'agent_suggestion') {
|
|
583
|
+
return { error: 'memory.items[].agent source requires an agent suggestion origin', kind: 'malformed' };
|
|
584
|
+
}
|
|
585
|
+
if (source.value === 'manual' && origin.value?.type === 'agent_suggestion') {
|
|
586
|
+
return { error: 'memory.items[].manual source cannot include an agent suggestion origin', kind: 'malformed' };
|
|
587
|
+
}
|
|
351
588
|
if (kind.value === 'convention' && state.value === 'superseded') {
|
|
352
|
-
return { error: 'memory.items[] conventions cannot use the superseded state' };
|
|
589
|
+
return { error: 'memory.items[] conventions cannot use the superseded state', kind: 'malformed' };
|
|
353
590
|
}
|
|
354
|
-
const title = validateRequiredString(value.title, 'memory.items[].title');
|
|
591
|
+
const title = validateStringLimit(validateRequiredString(value.title, 'memory.items[].title'), 'memory.items[].title', PROJECT_MEMORY_LIMITS.title);
|
|
355
592
|
if ('error' in title) {
|
|
356
593
|
return title;
|
|
357
594
|
}
|
|
358
|
-
const scope = normalizeOptionalString(value.scope, 'memory.items[].scope');
|
|
595
|
+
const scope = validateStringLimit(normalizeOptionalString(value.scope, 'memory.items[].scope'), 'memory.items[].scope', PROJECT_MEMORY_LIMITS.scope);
|
|
359
596
|
if ('error' in scope) {
|
|
360
597
|
return scope;
|
|
361
598
|
}
|
|
362
|
-
const content = validateRequiredString(value.content, 'memory.items[].content');
|
|
599
|
+
const content = validateStringLimit(validateRequiredString(value.content, 'memory.items[].content'), 'memory.items[].content', PROJECT_MEMORY_LIMITS.content);
|
|
363
600
|
if ('error' in content) {
|
|
364
601
|
return content;
|
|
365
602
|
}
|
|
366
|
-
const rationale = normalizeOptionalString(value.rationale, 'memory.items[].rationale');
|
|
603
|
+
const rationale = validateStringLimit(normalizeOptionalString(value.rationale, 'memory.items[].rationale'), 'memory.items[].rationale', PROJECT_MEMORY_LIMITS.rationale);
|
|
367
604
|
if ('error' in rationale) {
|
|
368
605
|
return rationale;
|
|
369
606
|
}
|
|
370
|
-
const implications =
|
|
607
|
+
const implications = validateImplications(value.implications ?? [], 'memory.items[].implications');
|
|
371
608
|
if ('error' in implications) {
|
|
372
609
|
return implications;
|
|
373
610
|
}
|
|
374
611
|
const stages = validateStages(value.stages ?? []);
|
|
375
612
|
if ('error' in stages) {
|
|
376
|
-
return { error: 'memory.items[].stages must be an array of valid project memory stages' };
|
|
613
|
+
return { error: 'memory.items[].stages must be an array of valid project memory stages', kind: 'malformed' };
|
|
614
|
+
}
|
|
615
|
+
const pathGlobs = validateApplicabilityStrings(value.path_globs, 'memory.items[].path_globs');
|
|
616
|
+
if ('error' in pathGlobs) {
|
|
617
|
+
return pathGlobs;
|
|
618
|
+
}
|
|
619
|
+
const componentTags = validateApplicabilityStrings(value.component_tags, 'memory.items[].component_tags');
|
|
620
|
+
if ('error' in componentTags) {
|
|
621
|
+
return componentTags;
|
|
377
622
|
}
|
|
378
623
|
const priority = value.priority == null
|
|
379
624
|
? { value: null }
|
|
@@ -387,20 +632,17 @@ const validateArchiveItem = (value) => {
|
|
|
387
632
|
if ('error' in injectionMode) {
|
|
388
633
|
return injectionMode;
|
|
389
634
|
}
|
|
390
|
-
if (kind.value === 'decision' && stages.value.length > 0) {
|
|
391
|
-
return { error: 'memory.items[] decisions cannot include stages' };
|
|
392
|
-
}
|
|
393
635
|
if (kind.value === 'decision' && priority.value !== null) {
|
|
394
|
-
return { error: 'memory.items[] decisions cannot include priority' };
|
|
636
|
+
return { error: 'memory.items[] decisions cannot include priority', kind: 'malformed' };
|
|
395
637
|
}
|
|
396
638
|
if (kind.value === 'decision' && injectionMode.value !== null) {
|
|
397
|
-
return { error: 'memory.items[] decisions cannot include injection_mode' };
|
|
639
|
+
return { error: 'memory.items[] decisions cannot include injection_mode', kind: 'malformed' };
|
|
398
640
|
}
|
|
399
641
|
if (kind.value === 'convention' && implications.value.length > 0) {
|
|
400
|
-
return { error: 'memory.items[] conventions cannot include implications' };
|
|
642
|
+
return { error: 'memory.items[] conventions cannot include implications', kind: 'malformed' };
|
|
401
643
|
}
|
|
402
644
|
if (kind.value === 'convention' && injectionMode.value === null) {
|
|
403
|
-
return { error: 'memory.items[] conventions must include injection_mode' };
|
|
645
|
+
return { error: 'memory.items[] conventions must include injection_mode', kind: 'malformed' };
|
|
404
646
|
}
|
|
405
647
|
const createdAt = validateRequiredString(value.created_at, 'memory.items[].created_at');
|
|
406
648
|
if ('error' in createdAt) {
|
|
@@ -412,15 +654,20 @@ const validateArchiveItem = (value) => {
|
|
|
412
654
|
}
|
|
413
655
|
return {
|
|
414
656
|
value: {
|
|
657
|
+
archive_id: archiveId.value,
|
|
658
|
+
superseded_by_archive_id: supersededByArchiveId.value,
|
|
415
659
|
kind: kind.value,
|
|
416
660
|
state: state.value,
|
|
417
661
|
source: source.value,
|
|
662
|
+
origin: origin.value,
|
|
418
663
|
title: title.value,
|
|
419
664
|
scope: scope.value,
|
|
420
665
|
content: content.value,
|
|
421
666
|
rationale: rationale.value,
|
|
422
667
|
implications: implications.value,
|
|
423
668
|
stages: stages.value,
|
|
669
|
+
path_globs: pathGlobs.value,
|
|
670
|
+
component_tags: componentTags.value,
|
|
424
671
|
priority: priority.value,
|
|
425
672
|
injection_mode: injectionMode.value,
|
|
426
673
|
created_at: createdAt.value,
|
|
@@ -428,19 +675,109 @@ const validateArchiveItem = (value) => {
|
|
|
428
675
|
},
|
|
429
676
|
};
|
|
430
677
|
};
|
|
678
|
+
const validateBoundedManifestStrings = (value, fieldName, countLimit, itemLimit = PROJECT_MEMORY_LIMITS.scanManifestPath) => {
|
|
679
|
+
const strings = normalizeStringArray(value, fieldName);
|
|
680
|
+
if ('error' in strings) {
|
|
681
|
+
return strings;
|
|
682
|
+
}
|
|
683
|
+
const count = validateArrayLimit(strings.value, fieldName, countLimit);
|
|
684
|
+
if ('error' in count) {
|
|
685
|
+
return count;
|
|
686
|
+
}
|
|
687
|
+
for (const entry of count.value) {
|
|
688
|
+
if (entry.length > itemLimit) {
|
|
689
|
+
return { error: `${fieldName}[] must not exceed ${itemLimit} characters`, kind: 'limit', code: 'field_too_large', field: fieldName, limit: itemLimit };
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return count;
|
|
693
|
+
};
|
|
694
|
+
const validateNonNegativeInteger = (value, fieldName) => {
|
|
695
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
696
|
+
return { error: `${fieldName} must be a non-negative integer`, kind: 'malformed' };
|
|
697
|
+
}
|
|
698
|
+
return { value };
|
|
699
|
+
};
|
|
700
|
+
const validateArchiveManifest = (value) => {
|
|
701
|
+
if (value == null) {
|
|
702
|
+
return { value: null };
|
|
703
|
+
}
|
|
704
|
+
if (!isRecord(value)) {
|
|
705
|
+
return { error: 'memory.scans[].manifest must be an object or null', kind: 'malformed' };
|
|
706
|
+
}
|
|
707
|
+
const sampledPaths = validateBoundedManifestStrings(value.sampled_paths, 'memory.scans[].manifest.sampled_paths', PROJECT_MEMORY_LIMITS.scanManifestPaths);
|
|
708
|
+
if ('error' in sampledPaths) {
|
|
709
|
+
return sampledPaths;
|
|
710
|
+
}
|
|
711
|
+
const includeGlobs = validateBoundedManifestStrings(value.include_globs, 'memory.scans[].manifest.include_globs', PROJECT_MEMORY_LIMITS.scanGlobs, PROJECT_MEMORY_LIMITS.scanGlob);
|
|
712
|
+
if ('error' in includeGlobs) {
|
|
713
|
+
return includeGlobs;
|
|
714
|
+
}
|
|
715
|
+
const excludeGlobs = validateBoundedManifestStrings(value.exclude_globs, 'memory.scans[].manifest.exclude_globs', PROJECT_MEMORY_LIMITS.scanGlobs, PROJECT_MEMORY_LIMITS.scanGlob);
|
|
716
|
+
if ('error' in excludeGlobs) {
|
|
717
|
+
return excludeGlobs;
|
|
718
|
+
}
|
|
719
|
+
const hardExclusions = validateBoundedManifestStrings(value.hard_exclusions, 'memory.scans[].manifest.hard_exclusions', PROJECT_MEMORY_LIMITS.scanManifestHardExclusions, PROJECT_MEMORY_LIMITS.scanGlob);
|
|
720
|
+
if ('error' in hardExclusions) {
|
|
721
|
+
return hardExclusions;
|
|
722
|
+
}
|
|
723
|
+
const truncatedOutputFields = validateBoundedManifestStrings(value.truncated_output_fields, 'memory.scans[].manifest.truncated_output_fields', PROJECT_MEMORY_LIMITS.scanManifestTruncatedFields);
|
|
724
|
+
if ('error' in truncatedOutputFields) {
|
|
725
|
+
return truncatedOutputFields;
|
|
726
|
+
}
|
|
727
|
+
if (!isRecord(value.skipped_counts)) {
|
|
728
|
+
return { error: 'memory.scans[].manifest.skipped_counts must be an object', kind: 'malformed' };
|
|
729
|
+
}
|
|
730
|
+
const skippedCounts = {};
|
|
731
|
+
for (const [reason, rawCount] of Object.entries(value.skipped_counts)) {
|
|
732
|
+
if (!PROJECT_CONTEXT_SCAN_SKIP_REASONS.includes(reason)) {
|
|
733
|
+
return { error: `memory.scans[].manifest.skipped_counts contains unsupported reason ${reason}`, kind: 'malformed' };
|
|
734
|
+
}
|
|
735
|
+
const count = validateNonNegativeInteger(rawCount, `memory.scans[].manifest.skipped_counts.${reason}`);
|
|
736
|
+
if ('error' in count) {
|
|
737
|
+
return count;
|
|
738
|
+
}
|
|
739
|
+
skippedCounts[reason] = count.value;
|
|
740
|
+
}
|
|
741
|
+
const repositoryCharactersSent = validateNonNegativeInteger(value.repository_characters_sent, 'memory.scans[].manifest.repository_characters_sent');
|
|
742
|
+
if ('error' in repositoryCharactersSent) {
|
|
743
|
+
return repositoryCharactersSent;
|
|
744
|
+
}
|
|
745
|
+
const redactionCount = validateNonNegativeInteger(value.redaction_count, 'memory.scans[].manifest.redaction_count');
|
|
746
|
+
if ('error' in redactionCount) {
|
|
747
|
+
return redactionCount;
|
|
748
|
+
}
|
|
749
|
+
if (typeof value.repository_changed_during_scan !== 'boolean') {
|
|
750
|
+
return { error: 'memory.scans[].manifest.repository_changed_during_scan must be a boolean', kind: 'malformed' };
|
|
751
|
+
}
|
|
752
|
+
const manifest = {
|
|
753
|
+
sampled_paths: sampledPaths.value,
|
|
754
|
+
skipped_counts: skippedCounts,
|
|
755
|
+
include_globs: includeGlobs.value,
|
|
756
|
+
exclude_globs: excludeGlobs.value,
|
|
757
|
+
hard_exclusions: hardExclusions.value,
|
|
758
|
+
repository_characters_sent: repositoryCharactersSent.value,
|
|
759
|
+
redaction_count: redactionCount.value,
|
|
760
|
+
truncated_output_fields: truncatedOutputFields.value,
|
|
761
|
+
repository_changed_during_scan: value.repository_changed_during_scan,
|
|
762
|
+
};
|
|
763
|
+
if (Buffer.byteLength(JSON.stringify(manifest), 'utf8') > PROJECT_MEMORY_LIMITS.scanManifestBytes) {
|
|
764
|
+
return { error: `memory.scans[].manifest must not exceed ${PROJECT_MEMORY_LIMITS.scanManifestBytes} bytes`, kind: 'limit', code: 'field_too_large', field: 'memory.scans[].manifest', limit: PROJECT_MEMORY_LIMITS.scanManifestBytes };
|
|
765
|
+
}
|
|
766
|
+
return { value: manifest };
|
|
767
|
+
};
|
|
431
768
|
const validateArchiveArtifact = (value) => {
|
|
432
769
|
if (!isRecord(value)) {
|
|
433
|
-
return { error: 'memory.scans[].artifacts[] must be an object' };
|
|
770
|
+
return { error: 'memory.scans[].artifacts[] must be an object', kind: 'malformed' };
|
|
434
771
|
}
|
|
435
772
|
const kind = validateEnum(value.kind, PROJECT_CONTEXT_ARTIFACT_KINDS, 'memory.scans[].artifacts[].kind');
|
|
436
773
|
if ('error' in kind) {
|
|
437
774
|
return kind;
|
|
438
775
|
}
|
|
439
|
-
const title = validateRequiredString(value.title, 'memory.scans[].artifacts[].title');
|
|
776
|
+
const title = validateStringLimit(validateRequiredString(value.title, 'memory.scans[].artifacts[].title'), 'memory.scans[].artifacts[].title', PROJECT_MEMORY_LIMITS.title);
|
|
440
777
|
if ('error' in title) {
|
|
441
778
|
return title;
|
|
442
779
|
}
|
|
443
|
-
const markdown = normalizeOptionalString(value.content_markdown, 'memory.scans[].artifacts[].content_markdown');
|
|
780
|
+
const markdown = validateStringLimit(normalizeOptionalString(value.content_markdown, 'memory.scans[].artifacts[].content_markdown'), 'memory.scans[].artifacts[].content_markdown', PROJECT_MEMORY_LIMITS.scanSummary);
|
|
444
781
|
if ('error' in markdown) {
|
|
445
782
|
return markdown;
|
|
446
783
|
}
|
|
@@ -448,19 +785,23 @@ const validateArchiveArtifact = (value) => {
|
|
|
448
785
|
if ('error' in createdAt) {
|
|
449
786
|
return createdAt;
|
|
450
787
|
}
|
|
788
|
+
const contentJson = sanitizeJsonValue(value.content_json);
|
|
789
|
+
if (Buffer.byteLength(JSON.stringify(contentJson), 'utf8') > PROJECT_MEMORY_LIMITS.scanArtifactJsonBytes) {
|
|
790
|
+
return { error: `memory.scans[].artifacts[].content_json must not exceed ${PROJECT_MEMORY_LIMITS.scanArtifactJsonBytes} bytes`, kind: 'limit', code: 'field_too_large', field: 'memory.scans[].artifacts[].content_json', limit: PROJECT_MEMORY_LIMITS.scanArtifactJsonBytes };
|
|
791
|
+
}
|
|
451
792
|
return {
|
|
452
793
|
value: {
|
|
453
794
|
kind: kind.value,
|
|
454
795
|
title: title.value,
|
|
455
|
-
content_json:
|
|
796
|
+
content_json: contentJson,
|
|
456
797
|
content_markdown: markdown.value,
|
|
457
798
|
created_at: createdAt.value,
|
|
458
799
|
},
|
|
459
800
|
};
|
|
460
801
|
};
|
|
461
|
-
const validateArchiveScan = (value) => {
|
|
802
|
+
const validateArchiveScan = (value, requiresMetadata = false) => {
|
|
462
803
|
if (!isRecord(value)) {
|
|
463
|
-
return { error: 'memory.scans[] must be an object' };
|
|
804
|
+
return { error: 'memory.scans[] must be an object', kind: 'malformed' };
|
|
464
805
|
}
|
|
465
806
|
const status = validateEnum(value.status, PROJECT_CONTEXT_SCAN_STATUSES, 'memory.scans[].status');
|
|
466
807
|
if ('error' in status) {
|
|
@@ -486,11 +827,43 @@ const validateArchiveScan = (value) => {
|
|
|
486
827
|
if ('error' in scannerVariant) {
|
|
487
828
|
return scannerVariant;
|
|
488
829
|
}
|
|
489
|
-
const
|
|
830
|
+
const metadataFields = ['scanner_provider', 'origin', 'scanned_at', 'completed_at', 'working_tree_dirty', 'manifest'];
|
|
831
|
+
if (requiresMetadata) {
|
|
832
|
+
const missingField = metadataFields.find(field => !Object.hasOwn(value, field));
|
|
833
|
+
if (missingField) {
|
|
834
|
+
return { error: `memory.scans[].${missingField} must be present`, kind: 'malformed' };
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const scannerProvider = validateStringLimit(normalizeOptionalString(value.scanner_provider, 'memory.scans[].scanner_provider'), 'memory.scans[].scanner_provider', PROJECT_MEMORY_LIMITS.scanMetadata);
|
|
838
|
+
if ('error' in scannerProvider) {
|
|
839
|
+
return scannerProvider;
|
|
840
|
+
}
|
|
841
|
+
const origin = value.origin == null && !requiresMetadata
|
|
842
|
+
? { value: undefined }
|
|
843
|
+
: validateEnum(value.origin, PROJECT_CONTEXT_SCAN_ORIGINS, 'memory.scans[].origin');
|
|
844
|
+
if ('error' in origin) {
|
|
845
|
+
return origin;
|
|
846
|
+
}
|
|
847
|
+
const scannedAt = normalizeOptionalString(value.scanned_at, 'memory.scans[].scanned_at');
|
|
848
|
+
if ('error' in scannedAt) {
|
|
849
|
+
return scannedAt;
|
|
850
|
+
}
|
|
851
|
+
const completedAt = normalizeOptionalString(value.completed_at, 'memory.scans[].completed_at');
|
|
852
|
+
if ('error' in completedAt) {
|
|
853
|
+
return completedAt;
|
|
854
|
+
}
|
|
855
|
+
if (value.working_tree_dirty != null && typeof value.working_tree_dirty !== 'boolean') {
|
|
856
|
+
return { error: 'memory.scans[].working_tree_dirty must be a boolean or null', kind: 'malformed' };
|
|
857
|
+
}
|
|
858
|
+
const manifest = validateArchiveManifest(value.manifest);
|
|
859
|
+
if ('error' in manifest) {
|
|
860
|
+
return manifest;
|
|
861
|
+
}
|
|
862
|
+
const summaryMarkdown = validateStringLimit(normalizeOptionalString(value.summary_markdown, 'memory.scans[].summary_markdown'), 'memory.scans[].summary_markdown', PROJECT_MEMORY_LIMITS.scanSummary);
|
|
490
863
|
if ('error' in summaryMarkdown) {
|
|
491
864
|
return summaryMarkdown;
|
|
492
865
|
}
|
|
493
|
-
const errorValue = normalizeOptionalString(value.error, 'memory.scans[].error');
|
|
866
|
+
const errorValue = validateStringLimit(normalizeOptionalString(value.error, 'memory.scans[].error'), 'memory.scans[].error', PROJECT_MEMORY_LIMITS.scanError);
|
|
494
867
|
if ('error' in errorValue) {
|
|
495
868
|
return errorValue;
|
|
496
869
|
}
|
|
@@ -503,7 +876,10 @@ const validateArchiveScan = (value) => {
|
|
|
503
876
|
return updatedAt;
|
|
504
877
|
}
|
|
505
878
|
if (!Array.isArray(value.artifacts)) {
|
|
506
|
-
return { error: 'memory.scans[].artifacts must be an array' };
|
|
879
|
+
return { error: 'memory.scans[].artifacts must be an array', kind: 'malformed' };
|
|
880
|
+
}
|
|
881
|
+
if (value.artifacts.length > PROJECT_MEMORY_LIMITS.archiveArtifactsPerScan) {
|
|
882
|
+
return { error: `memory.scans[].artifacts must not contain more than ${PROJECT_MEMORY_LIMITS.archiveArtifactsPerScan} items`, kind: 'limit', code: 'field_too_large', field: 'memory.scans[].artifacts', limit: PROJECT_MEMORY_LIMITS.archiveArtifactsPerScan };
|
|
507
883
|
}
|
|
508
884
|
const artifacts = [];
|
|
509
885
|
for (const artifactValue of value.artifacts) {
|
|
@@ -521,6 +897,12 @@ const validateArchiveScan = (value) => {
|
|
|
521
897
|
scanner_tool: scannerTool.value,
|
|
522
898
|
scanner_model: scannerModel.value,
|
|
523
899
|
scanner_variant: scannerVariant.value,
|
|
900
|
+
scanner_provider: scannerProvider.value,
|
|
901
|
+
origin: origin.value,
|
|
902
|
+
scanned_at: scannedAt.value,
|
|
903
|
+
completed_at: completedAt.value,
|
|
904
|
+
working_tree_dirty: value.working_tree_dirty == null ? null : value.working_tree_dirty,
|
|
905
|
+
manifest: manifest.value,
|
|
524
906
|
summary_markdown: summaryMarkdown.value,
|
|
525
907
|
error: errorValue.value,
|
|
526
908
|
created_at: createdAt.value,
|
|
@@ -529,19 +911,23 @@ const validateArchiveScan = (value) => {
|
|
|
529
911
|
},
|
|
530
912
|
};
|
|
531
913
|
};
|
|
914
|
+
const parseStoredScanManifest = (value) => {
|
|
915
|
+
const validation = validateArchiveManifest(parseJsonColumn(value));
|
|
916
|
+
return 'error' in validation ? null : validation.value;
|
|
917
|
+
};
|
|
532
918
|
export const validateProjectMemoryArchive = (value) => {
|
|
533
919
|
if (!isRecord(value)) {
|
|
534
|
-
return { error: 'Project memory archive must be an object' };
|
|
920
|
+
return { error: 'Project memory archive must be an object', kind: 'malformed' };
|
|
535
921
|
}
|
|
536
922
|
if (!PROJECT_MEMORY_ARCHIVE_SUPPORTED_VERSIONS.includes(value.schema_version)) {
|
|
537
|
-
return { error: `Unsupported project memory archive schema version: ${String(value.schema_version ?? 'missing')}
|
|
923
|
+
return { error: `Unsupported project memory archive schema version: ${String(value.schema_version ?? 'missing')}`, kind: 'malformed' };
|
|
538
924
|
}
|
|
539
925
|
const exportedAt = validateRequiredString(value.exported_at, 'exported_at');
|
|
540
926
|
if ('error' in exportedAt) {
|
|
541
927
|
return exportedAt;
|
|
542
928
|
}
|
|
543
929
|
if (!isRecord(value.project)) {
|
|
544
|
-
return { error: 'project must be an object' };
|
|
930
|
+
return { error: 'project must be an object', kind: 'malformed' };
|
|
545
931
|
}
|
|
546
932
|
const sourceProjectId = validateRequiredString(value.project.source_project_id, 'project.source_project_id');
|
|
547
933
|
if ('error' in sourceProjectId) {
|
|
@@ -552,55 +938,133 @@ export const validateProjectMemoryArchive = (value) => {
|
|
|
552
938
|
return projectName;
|
|
553
939
|
}
|
|
554
940
|
if (typeof value.project.memory_enabled !== 'boolean') {
|
|
555
|
-
return { error: 'project.memory_enabled must be a boolean' };
|
|
941
|
+
return { error: 'project.memory_enabled must be a boolean', kind: 'malformed' };
|
|
556
942
|
}
|
|
557
943
|
if (!isRecord(value.memory)) {
|
|
558
|
-
return { error: 'memory must be an object' };
|
|
944
|
+
return { error: 'memory must be an object', kind: 'malformed' };
|
|
559
945
|
}
|
|
560
946
|
if (!Array.isArray(value.memory.scans)) {
|
|
561
|
-
return { error: 'memory.scans must be an array' };
|
|
947
|
+
return { error: 'memory.scans must be an array', kind: 'malformed' };
|
|
948
|
+
}
|
|
949
|
+
if (value.memory.scans.length > PROJECT_MEMORY_LIMITS.archiveScans) {
|
|
950
|
+
return { error: `memory.scans must not contain more than ${PROJECT_MEMORY_LIMITS.archiveScans} items`, kind: 'limit', code: 'field_too_large', field: 'memory.scans', limit: PROJECT_MEMORY_LIMITS.archiveScans };
|
|
951
|
+
}
|
|
952
|
+
const artifactCount = value.memory.scans.reduce((total, scan) => {
|
|
953
|
+
if (!isRecord(scan) || !Array.isArray(scan.artifacts)) {
|
|
954
|
+
return total;
|
|
955
|
+
}
|
|
956
|
+
return total + scan.artifacts.length;
|
|
957
|
+
}, 0);
|
|
958
|
+
if (artifactCount > PROJECT_MEMORY_LIMITS.archiveArtifactsTotal) {
|
|
959
|
+
return { error: `memory.scans[].artifacts must not contain more than ${PROJECT_MEMORY_LIMITS.archiveArtifactsTotal} aggregate items`, kind: 'limit', code: 'field_too_large', field: 'memory.scans[].artifacts', limit: PROJECT_MEMORY_LIMITS.archiveArtifactsTotal };
|
|
562
960
|
}
|
|
563
961
|
const scans = [];
|
|
564
962
|
for (const scanValue of value.memory.scans) {
|
|
565
|
-
const scan = validateArchiveScan(scanValue);
|
|
963
|
+
const scan = validateArchiveScan(scanValue, value.schema_version === 7);
|
|
566
964
|
if ('error' in scan) {
|
|
567
965
|
return scan;
|
|
568
966
|
}
|
|
569
967
|
scans.push(scan.value);
|
|
570
968
|
}
|
|
571
|
-
if (value.schema_version === 3) {
|
|
969
|
+
if (value.schema_version === 3 || value.schema_version === 4 || value.schema_version === 5 || value.schema_version === 6 || value.schema_version === 7) {
|
|
572
970
|
if (!Array.isArray(value.memory.items)) {
|
|
573
|
-
return { error: 'memory.items must be an array' };
|
|
971
|
+
return { error: 'memory.items must be an array', kind: 'malformed' };
|
|
972
|
+
}
|
|
973
|
+
if (value.memory.items.length > PROJECT_MEMORY_LIMITS.archiveMemories) {
|
|
974
|
+
return { error: `memory.items must not contain more than ${PROJECT_MEMORY_LIMITS.archiveMemories} items`, kind: 'limit', code: 'field_too_large', field: 'memory.items', limit: PROJECT_MEMORY_LIMITS.archiveMemories };
|
|
574
975
|
}
|
|
575
976
|
const items = [];
|
|
576
977
|
for (const itemValue of value.memory.items) {
|
|
577
|
-
const item = validateArchiveItem(itemValue);
|
|
978
|
+
const item = validateArchiveItem(itemValue, { requiresOrigin: value.schema_version >= 4, requiresRelationships: value.schema_version >= 6 });
|
|
578
979
|
if ('error' in item) {
|
|
579
980
|
return item;
|
|
580
981
|
}
|
|
581
982
|
items.push(item.value);
|
|
582
983
|
}
|
|
984
|
+
if (value.schema_version === 3) {
|
|
985
|
+
return {
|
|
986
|
+
value: {
|
|
987
|
+
schema_version: 3,
|
|
988
|
+
exported_at: exportedAt.value,
|
|
989
|
+
project: {
|
|
990
|
+
source_project_id: sourceProjectId.value,
|
|
991
|
+
name: projectName.value,
|
|
992
|
+
memory_enabled: value.project.memory_enabled,
|
|
993
|
+
},
|
|
994
|
+
memory: {
|
|
995
|
+
items,
|
|
996
|
+
scans,
|
|
997
|
+
},
|
|
998
|
+
},
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
if (value.schema_version === 4) {
|
|
1002
|
+
return {
|
|
1003
|
+
value: {
|
|
1004
|
+
schema_version: 4,
|
|
1005
|
+
exported_at: exportedAt.value,
|
|
1006
|
+
project: {
|
|
1007
|
+
source_project_id: sourceProjectId.value,
|
|
1008
|
+
name: projectName.value,
|
|
1009
|
+
memory_enabled: value.project.memory_enabled,
|
|
1010
|
+
},
|
|
1011
|
+
memory: {
|
|
1012
|
+
items: items,
|
|
1013
|
+
scans,
|
|
1014
|
+
},
|
|
1015
|
+
},
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
if (value.schema_version === 5) {
|
|
1019
|
+
return {
|
|
1020
|
+
value: {
|
|
1021
|
+
schema_version: 5,
|
|
1022
|
+
exported_at: exportedAt.value,
|
|
1023
|
+
project: {
|
|
1024
|
+
source_project_id: sourceProjectId.value,
|
|
1025
|
+
name: projectName.value,
|
|
1026
|
+
memory_enabled: value.project.memory_enabled,
|
|
1027
|
+
},
|
|
1028
|
+
memory: {
|
|
1029
|
+
items: items,
|
|
1030
|
+
scans,
|
|
1031
|
+
},
|
|
1032
|
+
},
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
if (value.schema_version === 6) {
|
|
1036
|
+
return {
|
|
1037
|
+
value: {
|
|
1038
|
+
schema_version: 6,
|
|
1039
|
+
exported_at: exportedAt.value,
|
|
1040
|
+
project: { source_project_id: sourceProjectId.value, name: projectName.value, memory_enabled: value.project.memory_enabled },
|
|
1041
|
+
memory: {
|
|
1042
|
+
items: items,
|
|
1043
|
+
scans,
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
583
1048
|
return {
|
|
584
1049
|
value: {
|
|
585
|
-
schema_version:
|
|
1050
|
+
schema_version: 7,
|
|
586
1051
|
exported_at: exportedAt.value,
|
|
587
|
-
project: {
|
|
588
|
-
source_project_id: sourceProjectId.value,
|
|
589
|
-
name: projectName.value,
|
|
590
|
-
memory_enabled: value.project.memory_enabled,
|
|
591
|
-
},
|
|
1052
|
+
project: { source_project_id: sourceProjectId.value, name: projectName.value, memory_enabled: value.project.memory_enabled },
|
|
592
1053
|
memory: {
|
|
593
|
-
items,
|
|
594
|
-
scans,
|
|
1054
|
+
items: items,
|
|
1055
|
+
scans: scans,
|
|
595
1056
|
},
|
|
596
1057
|
},
|
|
597
1058
|
};
|
|
598
1059
|
}
|
|
599
1060
|
if (!Array.isArray(value.memory.conventions)) {
|
|
600
|
-
return { error: 'memory.conventions must be an array' };
|
|
1061
|
+
return { error: 'memory.conventions must be an array', kind: 'malformed' };
|
|
601
1062
|
}
|
|
602
1063
|
if (!Array.isArray(value.memory.decisions)) {
|
|
603
|
-
return { error: 'memory.decisions must be an array' };
|
|
1064
|
+
return { error: 'memory.decisions must be an array', kind: 'malformed' };
|
|
1065
|
+
}
|
|
1066
|
+
if (value.memory.conventions.length + value.memory.decisions.length > PROJECT_MEMORY_LIMITS.archiveMemories) {
|
|
1067
|
+
return { error: `memory.conventions and memory.decisions must not contain more than ${PROJECT_MEMORY_LIMITS.archiveMemories} total items`, kind: 'limit', code: 'field_too_large', field: 'memory', limit: PROJECT_MEMORY_LIMITS.archiveMemories };
|
|
604
1068
|
}
|
|
605
1069
|
const conventions = [];
|
|
606
1070
|
for (const conventionValue of value.memory.conventions) {
|
|
@@ -684,6 +1148,12 @@ export const exportProjectMemoryArchive = (project) => {
|
|
|
684
1148
|
scanner_tool: scanRow.scanner_tool,
|
|
685
1149
|
scanner_model: scanRow.scanner_model,
|
|
686
1150
|
scanner_variant: scanRow.scanner_variant,
|
|
1151
|
+
scanner_provider: scanRow.scanner_provider,
|
|
1152
|
+
origin: PROJECT_CONTEXT_SCAN_ORIGINS.includes(scanRow.origin) ? scanRow.origin : 'local_scan',
|
|
1153
|
+
scanned_at: scanRow.scanned_at ?? scanRow.created_at,
|
|
1154
|
+
completed_at: scanRow.completed_at,
|
|
1155
|
+
working_tree_dirty: scanRow.working_tree_dirty == null ? null : Boolean(scanRow.working_tree_dirty),
|
|
1156
|
+
manifest: parseStoredScanManifest(scanRow.manifest_json),
|
|
687
1157
|
summary_markdown: scanRow.summary_markdown,
|
|
688
1158
|
error: scanRow.error,
|
|
689
1159
|
created_at: scanRow.created_at,
|
|
@@ -726,6 +1196,18 @@ const createConflict = (kind, title) => {
|
|
|
726
1196
|
reason: 'title_conflict',
|
|
727
1197
|
};
|
|
728
1198
|
};
|
|
1199
|
+
const getImportedArchiveOrigin = (origin, createdAt) => {
|
|
1200
|
+
if (origin?.type !== 'agent_suggestion') {
|
|
1201
|
+
return createManualArchiveOrigin(createdAt);
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
...origin,
|
|
1205
|
+
suggestion_id: null,
|
|
1206
|
+
ticket_id: null,
|
|
1207
|
+
chat_session_id: null,
|
|
1208
|
+
spec_session_id: null,
|
|
1209
|
+
};
|
|
1210
|
+
};
|
|
729
1211
|
export const importProjectMemoryArchive = (projectId, archiveValue) => {
|
|
730
1212
|
const archive = validateProjectMemoryArchive(archiveValue);
|
|
731
1213
|
if ('error' in archive) {
|
|
@@ -745,8 +1227,17 @@ export const importProjectMemoryArchive = (projectId, archiveValue) => {
|
|
|
745
1227
|
conflicts: [],
|
|
746
1228
|
};
|
|
747
1229
|
const transaction = db.transaction((validatedArchive) => {
|
|
748
|
-
const
|
|
1230
|
+
const archiveItems = validatedArchive.schema_version === 3 || validatedArchive.schema_version === 4 || validatedArchive.schema_version === 5 || validatedArchive.schema_version === 6 || validatedArchive.schema_version === 7
|
|
749
1231
|
? validatedArchive.memory.items
|
|
1232
|
+
: [];
|
|
1233
|
+
const legacyConventions = validatedArchive.schema_version === 1 || validatedArchive.schema_version === 2
|
|
1234
|
+
? validatedArchive.memory.conventions
|
|
1235
|
+
: [];
|
|
1236
|
+
const legacyDecisions = validatedArchive.schema_version === 1 || validatedArchive.schema_version === 2
|
|
1237
|
+
? validatedArchive.memory.decisions
|
|
1238
|
+
: [];
|
|
1239
|
+
const archiveConventions = archiveItems.length > 0 || validatedArchive.schema_version >= 3
|
|
1240
|
+
? archiveItems
|
|
750
1241
|
.filter((item) => item.kind === 'convention')
|
|
751
1242
|
.map(item => ({
|
|
752
1243
|
title: item.title,
|
|
@@ -754,15 +1245,18 @@ export const importProjectMemoryArchive = (projectId, archiveValue) => {
|
|
|
754
1245
|
instruction: item.content,
|
|
755
1246
|
rationale: item.rationale,
|
|
756
1247
|
stages: item.stages,
|
|
1248
|
+
path_globs: item.path_globs ?? [],
|
|
1249
|
+
component_tags: item.component_tags ?? [],
|
|
757
1250
|
priority: item.priority ?? 'normal',
|
|
758
1251
|
injection_mode: item.injection_mode ?? 'relevant',
|
|
759
1252
|
status: item.state === 'active' ? 'active' : 'archived',
|
|
1253
|
+
origin: item.origin,
|
|
760
1254
|
created_at: item.created_at,
|
|
761
1255
|
updated_at: item.updated_at,
|
|
762
1256
|
}))
|
|
763
|
-
:
|
|
764
|
-
const archiveDecisions = validatedArchive.schema_version
|
|
765
|
-
?
|
|
1257
|
+
: legacyConventions;
|
|
1258
|
+
const archiveDecisions = archiveItems.length > 0 || validatedArchive.schema_version >= 3
|
|
1259
|
+
? archiveItems
|
|
766
1260
|
.filter((item) => item.kind === 'decision')
|
|
767
1261
|
.map(item => ({
|
|
768
1262
|
title: item.title,
|
|
@@ -770,11 +1264,17 @@ export const importProjectMemoryArchive = (projectId, archiveValue) => {
|
|
|
770
1264
|
decision: item.content,
|
|
771
1265
|
rationale: item.rationale,
|
|
772
1266
|
implications: item.implications,
|
|
1267
|
+
stages: item.stages,
|
|
1268
|
+
path_globs: item.path_globs ?? [],
|
|
1269
|
+
component_tags: item.component_tags ?? [],
|
|
773
1270
|
status: item.state === 'active' ? 'accepted' : item.state === 'archived' ? 'archived' : 'superseded',
|
|
1271
|
+
archive_id: item.archive_id,
|
|
1272
|
+
superseded_by_archive_id: item.superseded_by_archive_id,
|
|
1273
|
+
origin: item.origin,
|
|
774
1274
|
created_at: item.created_at,
|
|
775
1275
|
updated_at: item.updated_at,
|
|
776
1276
|
}))
|
|
777
|
-
:
|
|
1277
|
+
: legacyDecisions;
|
|
778
1278
|
const existingConventionRows = db.prepare(`
|
|
779
1279
|
SELECT *
|
|
780
1280
|
FROM project_conventions
|
|
@@ -814,12 +1314,16 @@ export const importProjectMemoryArchive = (projectId, archiveValue) => {
|
|
|
814
1314
|
instruction,
|
|
815
1315
|
rationale,
|
|
816
1316
|
stages_json,
|
|
1317
|
+
path_globs_json,
|
|
1318
|
+
component_tags_json,
|
|
817
1319
|
priority,
|
|
818
1320
|
injection_mode,
|
|
819
1321
|
status,
|
|
1322
|
+
source_suggestion_id,
|
|
1323
|
+
origin_json,
|
|
820
1324
|
created_at,
|
|
821
1325
|
updated_at
|
|
822
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1326
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
823
1327
|
`);
|
|
824
1328
|
const insertDecision = db.prepare(`
|
|
825
1329
|
INSERT INTO project_decisions (
|
|
@@ -830,10 +1334,15 @@ export const importProjectMemoryArchive = (projectId, archiveValue) => {
|
|
|
830
1334
|
decision,
|
|
831
1335
|
rationale,
|
|
832
1336
|
implications_json,
|
|
1337
|
+
stages_json,
|
|
1338
|
+
path_globs_json,
|
|
1339
|
+
component_tags_json,
|
|
833
1340
|
status,
|
|
1341
|
+
source_suggestion_id,
|
|
1342
|
+
origin_json,
|
|
834
1343
|
created_at,
|
|
835
1344
|
updated_at
|
|
836
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1345
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
837
1346
|
`);
|
|
838
1347
|
const insertScan = db.prepare(`
|
|
839
1348
|
INSERT INTO project_context_scans (
|
|
@@ -845,11 +1354,17 @@ export const importProjectMemoryArchive = (projectId, archiveValue) => {
|
|
|
845
1354
|
scanner_tool,
|
|
846
1355
|
scanner_model,
|
|
847
1356
|
scanner_variant,
|
|
1357
|
+
scanner_provider,
|
|
1358
|
+
origin,
|
|
1359
|
+
scanned_at,
|
|
1360
|
+
completed_at,
|
|
1361
|
+
working_tree_dirty,
|
|
1362
|
+
manifest_json,
|
|
848
1363
|
summary_markdown,
|
|
849
1364
|
error,
|
|
850
1365
|
created_at,
|
|
851
1366
|
updated_at
|
|
852
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1367
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
853
1368
|
`);
|
|
854
1369
|
const insertArtifact = db.prepare(`
|
|
855
1370
|
INSERT INTO project_context_artifacts (
|
|
@@ -876,13 +1391,15 @@ export const importProjectMemoryArchive = (projectId, archiveValue) => {
|
|
|
876
1391
|
result.conflicts.push(createConflict('convention', convention.title));
|
|
877
1392
|
continue;
|
|
878
1393
|
}
|
|
879
|
-
insertConvention.run(randomUUID(), projectId, convention.title, convention.scope, convention.instruction, convention.rationale, convention.stages.length > 0 ? JSON.stringify(convention.stages) : null, convention.priority, convention.injection_mode, convention.status, convention.created_at, convention.updated_at);
|
|
1394
|
+
insertConvention.run(randomUUID(), projectId, convention.title, convention.scope, convention.instruction, convention.rationale, convention.stages.length > 0 ? JSON.stringify(convention.stages) : null, convention.path_globs && convention.path_globs.length > 0 ? JSON.stringify(convention.path_globs) : null, convention.component_tags && convention.component_tags.length > 0 ? JSON.stringify(convention.component_tags) : null, convention.priority, convention.injection_mode, convention.status, null, JSON.stringify(getImportedArchiveOrigin(convention.origin, convention.created_at)), convention.created_at, convention.updated_at);
|
|
880
1395
|
conventionFingerprints.add(fingerprint);
|
|
881
1396
|
const nextTitleFingerprints = titleFingerprints ?? new Set();
|
|
882
1397
|
nextTitleFingerprints.add(fingerprint);
|
|
883
1398
|
conventionTitles.set(normalizedTitle, nextTitleFingerprints);
|
|
884
1399
|
result.summary.conventions_imported += 1;
|
|
885
1400
|
}
|
|
1401
|
+
const importedDecisionIds = new Map();
|
|
1402
|
+
const importedDecisionRelationships = [];
|
|
886
1403
|
for (const decision of archiveDecisions) {
|
|
887
1404
|
const fingerprint = fingerprintDecision(decision);
|
|
888
1405
|
const normalizedTitle = normalizeString(decision.title);
|
|
@@ -896,13 +1413,26 @@ export const importProjectMemoryArchive = (projectId, archiveValue) => {
|
|
|
896
1413
|
result.conflicts.push(createConflict('decision', decision.title));
|
|
897
1414
|
continue;
|
|
898
1415
|
}
|
|
899
|
-
|
|
1416
|
+
const importedDecisionId = randomUUID();
|
|
1417
|
+
insertDecision.run(importedDecisionId, projectId, decision.title, decision.scope, decision.decision, decision.rationale, decision.implications.length > 0 ? JSON.stringify(decision.implications) : null, decision.stages && decision.stages.length > 0 ? JSON.stringify(decision.stages) : null, decision.path_globs && decision.path_globs.length > 0 ? JSON.stringify(decision.path_globs) : null, decision.component_tags && decision.component_tags.length > 0 ? JSON.stringify(decision.component_tags) : null, decision.status, null, JSON.stringify(getImportedArchiveOrigin(decision.origin, decision.created_at)), decision.created_at, decision.updated_at);
|
|
1418
|
+
if (decision.archive_id) {
|
|
1419
|
+
importedDecisionIds.set(decision.archive_id, importedDecisionId);
|
|
1420
|
+
}
|
|
1421
|
+
if (decision.superseded_by_archive_id) {
|
|
1422
|
+
importedDecisionRelationships.push({ decisionId: importedDecisionId, replacementArchiveId: decision.superseded_by_archive_id });
|
|
1423
|
+
}
|
|
900
1424
|
decisionFingerprints.add(fingerprint);
|
|
901
1425
|
const nextTitleFingerprints = titleFingerprints ?? new Set();
|
|
902
1426
|
nextTitleFingerprints.add(fingerprint);
|
|
903
1427
|
decisionTitles.set(normalizedTitle, nextTitleFingerprints);
|
|
904
1428
|
result.summary.decisions_imported += 1;
|
|
905
1429
|
}
|
|
1430
|
+
for (const relationship of importedDecisionRelationships) {
|
|
1431
|
+
const replacementDecisionId = importedDecisionIds.get(relationship.replacementArchiveId);
|
|
1432
|
+
if (replacementDecisionId) {
|
|
1433
|
+
db.prepare('UPDATE project_decisions SET superseded_by_decision_id = ? WHERE id = ? AND project_id = ?').run(replacementDecisionId, relationship.decisionId, projectId);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
906
1436
|
const scanFingerprintToId = new Map();
|
|
907
1437
|
for (const scanRow of existingScanRows) {
|
|
908
1438
|
scanFingerprintToId.set(fingerprintScan({
|
|
@@ -912,6 +1442,12 @@ export const importProjectMemoryArchive = (projectId, archiveValue) => {
|
|
|
912
1442
|
scanner_tool: scanRow.scanner_tool,
|
|
913
1443
|
scanner_model: scanRow.scanner_model,
|
|
914
1444
|
scanner_variant: scanRow.scanner_variant,
|
|
1445
|
+
scanner_provider: scanRow.scanner_provider,
|
|
1446
|
+
origin: scanRow.origin,
|
|
1447
|
+
scanned_at: scanRow.scanned_at,
|
|
1448
|
+
completed_at: scanRow.completed_at,
|
|
1449
|
+
working_tree_dirty: scanRow.working_tree_dirty == null ? null : Boolean(scanRow.working_tree_dirty),
|
|
1450
|
+
manifest: parseStoredScanManifest(scanRow.manifest_json),
|
|
915
1451
|
summary_markdown: scanRow.summary_markdown,
|
|
916
1452
|
error: scanRow.error,
|
|
917
1453
|
created_at: scanRow.created_at,
|
|
@@ -925,22 +1461,38 @@ export const importProjectMemoryArchive = (projectId, archiveValue) => {
|
|
|
925
1461
|
current.add(fingerprintArtifact(serializeArtifact(artifactRow)));
|
|
926
1462
|
artifactFingerprintsByScanId.set(artifactRow.scan_id, current);
|
|
927
1463
|
}
|
|
928
|
-
for (const
|
|
929
|
-
const
|
|
1464
|
+
for (const archiveScan of validatedArchive.memory.scans) {
|
|
1465
|
+
const scan = archiveScan.status === 'pending' || archiveScan.status === 'running'
|
|
1466
|
+
? {
|
|
1467
|
+
...archiveScan,
|
|
1468
|
+
status: 'error',
|
|
1469
|
+
error: IMPORTED_ACTIVE_SCAN_ERROR,
|
|
1470
|
+
}
|
|
1471
|
+
: archiveScan;
|
|
1472
|
+
const importedScan = {
|
|
1473
|
+
...scan,
|
|
1474
|
+
origin: 'archive_import',
|
|
1475
|
+
scanner_provider: scan.scanner_provider ?? null,
|
|
1476
|
+
scanned_at: scan.scanned_at ?? scan.created_at,
|
|
1477
|
+
completed_at: scan.completed_at ?? (scan.status === 'done' ? scan.updated_at : null),
|
|
1478
|
+
working_tree_dirty: scan.working_tree_dirty ?? null,
|
|
1479
|
+
manifest: scan.manifest ?? null,
|
|
1480
|
+
};
|
|
1481
|
+
const scanFingerprint = fingerprintScan(importedScan);
|
|
930
1482
|
let targetScanId = scanFingerprintToId.get(scanFingerprint);
|
|
931
1483
|
if (targetScanId) {
|
|
932
1484
|
result.summary.scans_skipped += 1;
|
|
933
1485
|
}
|
|
934
1486
|
else {
|
|
935
1487
|
targetScanId = randomUUID();
|
|
936
|
-
insertScan.run(targetScanId, projectId,
|
|
1488
|
+
insertScan.run(targetScanId, projectId, importedScan.status, importedScan.repo_head, importedScan.repo_branch, importedScan.scanner_tool, importedScan.scanner_model, importedScan.scanner_variant, importedScan.scanner_provider, importedScan.origin, importedScan.scanned_at, importedScan.completed_at, importedScan.working_tree_dirty == null ? null : Number(importedScan.working_tree_dirty), importedScan.manifest == null ? null : JSON.stringify(importedScan.manifest), importedScan.summary_markdown, importedScan.error, importedScan.created_at, importedScan.updated_at);
|
|
937
1489
|
scanFingerprintToId.set(scanFingerprint, targetScanId);
|
|
938
1490
|
artifactFingerprintsByScanId.set(targetScanId, new Set());
|
|
939
1491
|
result.summary.scans_imported += 1;
|
|
940
1492
|
}
|
|
941
1493
|
const artifactFingerprints = artifactFingerprintsByScanId.get(targetScanId) ?? new Set();
|
|
942
1494
|
artifactFingerprintsByScanId.set(targetScanId, artifactFingerprints);
|
|
943
|
-
for (const artifact of
|
|
1495
|
+
for (const artifact of importedScan.artifacts) {
|
|
944
1496
|
const artifactFingerprint = fingerprintArtifact(artifact);
|
|
945
1497
|
if (artifactFingerprints.has(artifactFingerprint)) {
|
|
946
1498
|
result.summary.artifacts_skipped += 1;
|