@straiffi/archon 1.5.0 → 1.6.1

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 (112) hide show
  1. package/dist/client/assets/GitHubActionsDialog-BLJ3yCeK.js +1 -0
  2. package/dist/client/assets/TestsDialog-ar_RUzA3.js +4 -0
  3. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BG4rc0pD.js → architectureDiagram-3BPJPVTR-cWqD9YRv.js} +1 -1
  4. package/dist/client/assets/{blockDiagram-GPEHLZMM-BifkewDx.js → blockDiagram-GPEHLZMM-B8yyMr1c.js} +1 -1
  5. package/dist/client/assets/{c4Diagram-AAUBKEIU-DEYVUDCs.js → c4Diagram-AAUBKEIU-B_yL8Tbe.js} +1 -1
  6. package/dist/client/assets/channel-1Ezn2R-x.js +1 -0
  7. package/dist/client/assets/{chunk-2J33WTMH-DD50YIcA.js → chunk-2J33WTMH-BF8hfYEU.js} +1 -1
  8. package/dist/client/assets/{chunk-3OPIFGDE-DLbwwbdv.js → chunk-3OPIFGDE-BPDdyRDK.js} +1 -1
  9. package/dist/client/assets/{chunk-5ZQYHXKU-BW-r3gXJ.js → chunk-5ZQYHXKU-DEi4bnIa.js} +1 -1
  10. package/dist/client/assets/{chunk-727SXJPM-B5C_ENX7.js → chunk-727SXJPM-BTXHOTJi.js} +1 -1
  11. package/dist/client/assets/{chunk-AQP2D5EJ-BUT3wmDg.js → chunk-AQP2D5EJ-D2TAJwxM.js} +1 -1
  12. package/dist/client/assets/{chunk-CSCIHK7Q-BO7wWVh_.js → chunk-CSCIHK7Q-Bdgg7p9T.js} +1 -1
  13. package/dist/client/assets/{chunk-KSCS5N6A-DUNFfKB2.js → chunk-KSCS5N6A-Cey3fOI1.js} +1 -1
  14. package/dist/client/assets/{chunk-L5ZTLDWV-SYJQkU8_.js → chunk-L5ZTLDWV-BSNGBgBH.js} +1 -1
  15. package/dist/client/assets/{chunk-LZXEDZCA-DSVJIylK.js → chunk-LZXEDZCA-B8qP34jM.js} +2 -2
  16. package/dist/client/assets/{chunk-ND2GUHAM-CxL5ue9A.js → chunk-ND2GUHAM-COde7fTi.js} +1 -1
  17. package/dist/client/assets/{chunk-NZK2D7GU-BwxeYeqR.js → chunk-NZK2D7GU-C62VVWrt.js} +1 -1
  18. package/dist/client/assets/{chunk-O5CBEL6O-Covi8YcB.js → chunk-O5CBEL6O-_20MzuKm.js} +1 -1
  19. package/dist/client/assets/{chunk-WU5MYG2G-DlQoHSB3.js → chunk-WU5MYG2G-C4G-J1jx.js} +1 -1
  20. package/dist/client/assets/classDiagram-4FO5ZUOK-DrjiwEWN.js +1 -0
  21. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DWHyuysv.js +1 -0
  22. package/dist/client/assets/{dagre-BM42HDAG-CuNExSrh.js → dagre-BM42HDAG-jLUWHvr3.js} +1 -1
  23. package/dist/client/assets/{diagram-2AECGRRQ-Bk7EYe6Z.js → diagram-2AECGRRQ-B6D2TKLb.js} +1 -1
  24. package/dist/client/assets/{diagram-5GNKFQAL-koHJCfAk.js → diagram-5GNKFQAL-bFJLLDWx.js} +1 -1
  25. package/dist/client/assets/{diagram-KO2AKTUF-DI1Ecx54.js → diagram-KO2AKTUF-Bp01SL5f.js} +1 -1
  26. package/dist/client/assets/{diagram-LMA3HP47-CmFk1MNf.js → diagram-LMA3HP47-DwoeZFg9.js} +1 -1
  27. package/dist/client/assets/{diagram-OG6HWLK6-YTiDn39n.js → diagram-OG6HWLK6-CjRd_tge.js} +1 -1
  28. package/dist/client/assets/{erDiagram-TEJ5UH35-DyWU43DX.js → erDiagram-TEJ5UH35-DzfIVd9j.js} +1 -1
  29. package/dist/client/assets/{flowDiagram-I6XJVG4X-DgbU15ZJ.js → flowDiagram-I6XJVG4X-Dq04zf9A.js} +1 -1
  30. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CT39xvdk.js → ganttDiagram-6RSMTGT7-D5f16YYP.js} +1 -1
  31. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DfK_1xRj.js → gitGraphDiagram-PVQCEYII-CCD1CXBG.js} +1 -1
  32. package/dist/client/assets/index-DI9Vi2-3.js +265 -0
  33. package/dist/client/assets/index-DSap5tSX.css +2 -0
  34. package/dist/client/assets/{infoDiagram-5YYISTIA-C7z_7194.js → infoDiagram-5YYISTIA-Bde4Z0o8.js} +1 -1
  35. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DhJ7h17n.js → ishikawaDiagram-YF4QCWOH-CJ89ymGL.js} +1 -1
  36. package/dist/client/assets/{journeyDiagram-JHISSGLW-CtkhoMAA.js → journeyDiagram-JHISSGLW-Dw_sScmo.js} +1 -1
  37. package/dist/client/assets/{kanban-definition-UN3LZRKU-Bcas7cg9.js → kanban-definition-UN3LZRKU-DfWOcnxV.js} +1 -1
  38. package/dist/client/assets/{line-WO_ReaCl.js → line-CUQIbS7r.js} +1 -1
  39. package/dist/client/assets/{mermaid-parser.core-WmRuf19A.js → mermaid-parser.core-d287AjoM.js} +1 -1
  40. package/dist/client/assets/{mermaid.core-BMdju4xN.js → mermaid.core-QqY2ZNMl.js} +3 -3
  41. package/dist/client/assets/{mindmap-definition-RKZ34NQL-DLxsUQmn.js → mindmap-definition-RKZ34NQL-Bz3Yc70v.js} +1 -1
  42. package/dist/client/assets/{pieDiagram-4H26LBE5-D-oaWpEg.js → pieDiagram-4H26LBE5-BjMDHR6y.js} +1 -1
  43. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-B9krq8I-.js → quadrantDiagram-W4KKPZXB-BWDqf0X1.js} +1 -1
  44. package/dist/client/assets/{requirementDiagram-4Y6WPE33-m2VnH3vv.js → requirementDiagram-4Y6WPE33-kspE4S60.js} +1 -1
  45. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-bCF9Ot9-.js → sankeyDiagram-5OEKKPKP-EuLhHbDR.js} +1 -1
  46. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-C-4zDJcn.js → sequenceDiagram-3UESZ5HK-BvXG3DJh.js} +1 -1
  47. package/dist/client/assets/{stateDiagram-AJRCARHV-CksbJK59.js → stateDiagram-AJRCARHV-B-D0PYkH.js} +1 -1
  48. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-f36jgr8A.js +1 -0
  49. package/dist/client/assets/{timeline-definition-PNZ67QCA-DDxpdMfo.js → timeline-definition-PNZ67QCA-CJnod6Jh.js} +1 -1
  50. package/dist/client/assets/{vennDiagram-CIIHVFJN-BfQM7_zD.js → vennDiagram-CIIHVFJN-CBh3tqZu.js} +1 -1
  51. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-CuixXlAX.js → wardleyDiagram-YWT4CUSO-BBQOjYBd.js} +1 -1
  52. package/dist/client/assets/{xychartDiagram-2RQKCTM6-53YB-tf6.js → xychartDiagram-2RQKCTM6-C8gU1LNj.js} +1 -1
  53. package/dist/client/index.html +2 -2
  54. package/dist/server/db.js +288 -3
  55. package/dist/server/db.js.map +1 -1
  56. package/dist/server/index.js +323 -97
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/server/lib/git.js +18 -0
  59. package/dist/server/lib/git.js.map +1 -1
  60. package/dist/server/lib/models.js +2 -38
  61. package/dist/server/lib/models.js.map +1 -1
  62. package/dist/server/lib/projectMemory.js +1073 -276
  63. package/dist/server/lib/projectMemory.js.map +1 -1
  64. package/dist/server/lib/projectMemoryCursor.js +27 -0
  65. package/dist/server/lib/projectMemoryCursor.js.map +1 -0
  66. package/dist/server/lib/projectMemoryEvents.js +24 -0
  67. package/dist/server/lib/projectMemoryEvents.js.map +1 -0
  68. package/dist/server/lib/projectMemoryLimits.js +61 -0
  69. package/dist/server/lib/projectMemoryLimits.js.map +1 -0
  70. package/dist/server/lib/projectMemoryPrompt.js +299 -110
  71. package/dist/server/lib/projectMemoryPrompt.js.map +1 -1
  72. package/dist/server/lib/projectMemoryScan.js +461 -493
  73. package/dist/server/lib/projectMemoryScan.js.map +1 -1
  74. package/dist/server/lib/projectMemorySelections.js +97 -0
  75. package/dist/server/lib/projectMemorySelections.js.map +1 -0
  76. package/dist/server/lib/projectMemorySuggestions.js +19 -4
  77. package/dist/server/lib/projectMemorySuggestions.js.map +1 -1
  78. package/dist/server/lib/projectMemoryTransfer.js +635 -83
  79. package/dist/server/lib/projectMemoryTransfer.js.map +1 -1
  80. package/dist/server/lib/projectPathGlob.js +40 -0
  81. package/dist/server/lib/projectPathGlob.js.map +1 -0
  82. package/dist/server/lib/projects.js +50 -0
  83. package/dist/server/lib/projects.js.map +1 -1
  84. package/dist/server/lib/specReviewContext.js +2 -1
  85. package/dist/server/lib/specReviewContext.js.map +1 -1
  86. package/dist/server/lib/tickets.js +6 -0
  87. package/dist/server/lib/tickets.js.map +1 -1
  88. package/dist/server/workers/build.js +6 -2
  89. package/dist/server/workers/build.js.map +1 -1
  90. package/dist/server/workers/chat.js +9 -6
  91. package/dist/server/workers/chat.js.map +1 -1
  92. package/dist/server/workers/followUp.js +15 -8
  93. package/dist/server/workers/followUp.js.map +1 -1
  94. package/dist/server/workers/plan.js +61 -11
  95. package/dist/server/workers/plan.js.map +1 -1
  96. package/dist/server/workers/planFollowUp.js +3 -0
  97. package/dist/server/workers/planFollowUp.js.map +1 -1
  98. package/dist/server/workers/review.js +26 -11
  99. package/dist/server/workers/review.js.map +1 -1
  100. package/dist/server/workers/specReview.js +10 -0
  101. package/dist/server/workers/specReview.js.map +1 -1
  102. package/dist/server/workers/specSession.js +5 -2
  103. package/dist/server/workers/specSession.js.map +1 -1
  104. package/package.json +1 -1
  105. package/dist/client/assets/GitHubActionsDialog-CQMqcAiI.js +0 -1
  106. package/dist/client/assets/TestsDialog-iGukzvw2.js +0 -5
  107. package/dist/client/assets/channel-gy-JdvF3.js +0 -1
  108. package/dist/client/assets/classDiagram-4FO5ZUOK-B1Xs78Ge.js +0 -1
  109. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-BU_Soq1k.js +0 -1
  110. package/dist/client/assets/index-BCqJfUGA.css +0 -2
  111. package/dist/client/assets/index-C_XaTJyb.js +0 -261
  112. 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
- const PROJECT_MEMORY_ARCHIVE_EXPORT_VERSION = 3;
4
- const PROJECT_MEMORY_ARCHIVE_SUPPORTED_VERSIONS = [1, 2, 3];
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 = normalizeStringArray(value.implications, 'memory.decisions[].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 validateArchiveItem = (value) => {
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 = normalizeStringArray(value.implications ?? [], 'memory.items[].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: sanitizeJsonValue(value.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 summaryMarkdown = normalizeOptionalString(value.summary_markdown, 'memory.scans[].summary_markdown');
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: 3,
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 archiveConventions = validatedArchive.schema_version === 3
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
- : validatedArchive.memory.conventions;
764
- const archiveDecisions = validatedArchive.schema_version === 3
765
- ? validatedArchive.memory.items
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
- : validatedArchive.memory.decisions;
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
- insertDecision.run(randomUUID(), projectId, decision.title, decision.scope, decision.decision, decision.rationale, decision.implications.length > 0 ? JSON.stringify(decision.implications) : null, decision.status, decision.created_at, decision.updated_at);
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 scan of validatedArchive.memory.scans) {
929
- const scanFingerprint = fingerprintScan(scan);
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, scan.status, scan.repo_head, scan.repo_branch, scan.scanner_tool, scan.scanner_model, scan.scanner_variant, scan.summary_markdown, scan.error, scan.created_at, scan.updated_at);
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 scan.artifacts) {
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;