@coreyuan/vector-mind 1.0.53 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +99 -72
  2. package/admin-panel/README.md +77 -0
  3. package/admin-panel/dist/client/assets/index-Bzyjdhh2.css +1 -0
  4. package/admin-panel/dist/client/assets/index-Ph73ewzt.js +9 -0
  5. package/admin-panel/dist/client/index.html +14 -0
  6. package/admin-panel/package.json +25 -0
  7. package/admin-panel/server/index.mjs +1424 -0
  8. package/admin-panel/server/security.mjs +117 -0
  9. package/admin-panel/server/security.test.mjs +113 -0
  10. package/admin-panel/server/storage.mjs +49 -0
  11. package/admin-panel/server/storage.test.mjs +36 -0
  12. package/dist/builtin-conventions.js +1 -1
  13. package/dist/builtin-conventions.js.map +1 -1
  14. package/dist/builtin-instructions.d.ts +1 -1
  15. package/dist/builtin-instructions.js +1 -1
  16. package/dist/builtin-instructions.js.map +1 -1
  17. package/dist/checkpoint-snapshot.d.ts +56 -0
  18. package/dist/checkpoint-snapshot.js +411 -0
  19. package/dist/checkpoint-snapshot.js.map +1 -0
  20. package/dist/config.d.ts +4 -1
  21. package/dist/config.js +25 -1
  22. package/dist/config.js.map +1 -1
  23. package/dist/context-governance.d.ts +56 -0
  24. package/dist/context-governance.js +176 -0
  25. package/dist/context-governance.js.map +1 -0
  26. package/dist/database-runtime.d.ts +6 -1
  27. package/dist/database-runtime.js +555 -127
  28. package/dist/database-runtime.js.map +1 -1
  29. package/dist/development-warnings.d.ts +2 -1
  30. package/dist/development-warnings.js +92 -22
  31. package/dist/development-warnings.js.map +1 -1
  32. package/dist/file-indexing.d.ts +2 -1
  33. package/dist/file-indexing.js +127 -47
  34. package/dist/file-indexing.js.map +1 -1
  35. package/dist/grep.js +4 -0
  36. package/dist/grep.js.map +1 -1
  37. package/dist/index.js +8 -2
  38. package/dist/index.js.map +1 -1
  39. package/dist/large-file-split.d.ts +53 -8
  40. package/dist/large-file-split.js +294 -75
  41. package/dist/large-file-split.js.map +1 -1
  42. package/dist/memory-maintenance/compaction.d.ts +7 -0
  43. package/dist/memory-maintenance/compaction.js +133 -0
  44. package/dist/memory-maintenance/compaction.js.map +1 -0
  45. package/dist/memory-maintenance/context.d.ts +37 -0
  46. package/dist/memory-maintenance/context.js +52 -0
  47. package/dist/memory-maintenance/context.js.map +1 -0
  48. package/dist/memory-maintenance/index-prune.d.ts +22 -0
  49. package/dist/memory-maintenance/index-prune.js +243 -0
  50. package/dist/memory-maintenance/index-prune.js.map +1 -0
  51. package/dist/memory-maintenance/metrics.d.ts +5 -0
  52. package/dist/memory-maintenance/metrics.js +30 -0
  53. package/dist/memory-maintenance/metrics.js.map +1 -0
  54. package/dist/memory-maintenance/pending.d.ts +4 -0
  55. package/dist/memory-maintenance/pending.js +74 -0
  56. package/dist/memory-maintenance/pending.js.map +1 -0
  57. package/dist/memory-maintenance/purge.d.ts +7 -0
  58. package/dist/memory-maintenance/purge.js +115 -0
  59. package/dist/memory-maintenance/purge.js.map +1 -0
  60. package/dist/memory-maintenance/runner.d.ts +5 -0
  61. package/dist/memory-maintenance/runner.js +182 -0
  62. package/dist/memory-maintenance/runner.js.map +1 -0
  63. package/dist/memory-maintenance/sqlite.d.ts +10 -0
  64. package/dist/memory-maintenance/sqlite.js +67 -0
  65. package/dist/memory-maintenance/sqlite.js.map +1 -0
  66. package/dist/memory-maintenance/types.d.ts +87 -0
  67. package/dist/memory-maintenance/types.js +2 -0
  68. package/dist/memory-maintenance/types.js.map +1 -0
  69. package/dist/memory-maintenance.d.ts +5 -80
  70. package/dist/memory-maintenance.js +3 -536
  71. package/dist/memory-maintenance.js.map +1 -1
  72. package/dist/memory-mutations.d.ts +3 -3
  73. package/dist/memory-mutations.js +109 -43
  74. package/dist/memory-mutations.js.map +1 -1
  75. package/dist/memory-recall.d.ts +3 -1
  76. package/dist/memory-recall.js +33 -1
  77. package/dist/memory-recall.js.map +1 -1
  78. package/dist/operation-scope.js +42 -12
  79. package/dist/operation-scope.js.map +1 -1
  80. package/dist/path-containment.d.ts +5 -0
  81. package/dist/path-containment.js +64 -0
  82. package/dist/path-containment.js.map +1 -0
  83. package/dist/path-filters.d.ts +1 -1
  84. package/dist/path-filters.js +7 -6
  85. package/dist/path-filters.js.map +1 -1
  86. package/dist/path-rules.d.ts +1 -0
  87. package/dist/path-rules.js +23 -1
  88. package/dist/path-rules.js.map +1 -1
  89. package/dist/pending-changes.d.ts +5 -0
  90. package/dist/pending-changes.js +30 -16
  91. package/dist/pending-changes.js.map +1 -1
  92. package/dist/project-files.d.ts +1 -0
  93. package/dist/project-files.js +19 -10
  94. package/dist/project-files.js.map +1 -1
  95. package/dist/root.d.ts +4 -1
  96. package/dist/root.js +34 -4
  97. package/dist/root.js.map +1 -1
  98. package/dist/rtk-integrity.d.ts +5 -0
  99. package/dist/rtk-integrity.js +35 -0
  100. package/dist/rtk-integrity.js.map +1 -0
  101. package/dist/rtk-shim.js +22 -9
  102. package/dist/rtk-shim.js.map +1 -1
  103. package/dist/rtk-tools.d.ts +8 -0
  104. package/dist/rtk-tools.js +59 -42
  105. package/dist/rtk-tools.js.map +1 -1
  106. package/dist/server-instructions.js +8 -88
  107. package/dist/server-instructions.js.map +1 -1
  108. package/dist/tool-catalog.d.ts +2 -0
  109. package/dist/tool-catalog.js +77 -24
  110. package/dist/tool-catalog.js.map +1 -1
  111. package/dist/tool-handlers/context-recovery.js +79 -54
  112. package/dist/tool-handlers/context-recovery.js.map +1 -1
  113. package/dist/tool-handlers/large-files.js +615 -41
  114. package/dist/tool-handlers/large-files.js.map +1 -1
  115. package/dist/tool-handlers/memory-diagnostics.js +240 -45
  116. package/dist/tool-handlers/memory-diagnostics.js.map +1 -1
  117. package/dist/tool-handlers/memory.js +167 -22
  118. package/dist/tool-handlers/memory.js.map +1 -1
  119. package/dist/tool-handlers/operations.js +28 -8
  120. package/dist/tool-handlers/operations.js.map +1 -1
  121. package/dist/tool-handlers/requirement-status.d.ts +5 -0
  122. package/dist/tool-handlers/requirement-status.js +267 -0
  123. package/dist/tool-handlers/requirement-status.js.map +1 -0
  124. package/dist/tool-handlers/requirements.js +851 -115
  125. package/dist/tool-handlers/requirements.js.map +1 -1
  126. package/dist/tool-handlers.d.ts +4 -0
  127. package/dist/tool-handlers.js +439 -6
  128. package/dist/tool-handlers.js.map +1 -1
  129. package/dist/tool-output.d.ts +71 -2
  130. package/dist/tool-output.js +56 -13
  131. package/dist/tool-output.js.map +1 -1
  132. package/dist/tool-schemas.d.ts +243 -0
  133. package/dist/tool-schemas.js +112 -26
  134. package/dist/tool-schemas.js.map +1 -1
  135. package/dist/types.d.ts +10 -1
  136. package/docs/capability-matrix.md +23 -17
  137. package/package.json +21 -5
  138. package/skills/vector-mind-autopilot/SKILL.md +82 -23
  139. package/skills/vector-mind-autopilot/references/claude-project-instructions.md +14 -11
  140. package/skills/vector-mind-autopilot/references/universal-system-prompt.md +24 -16
@@ -1,18 +1,139 @@
1
1
  import fs from "node:fs";
2
- import { DEVELOPMENT_HUGE_FILE_LINES } from "../config.js";
2
+ import path from "node:path";
3
+ import { DEVELOPMENT_BLOCK_FILE_LINES, DEVELOPMENT_HUGE_FILE_LINES } from "../config.js";
3
4
  import { PlanLargeFileSplitArgsSchema, RecordLargeFileSplitArgsSchema } from "../tool-schemas.js";
4
- import { countFileLinesBounded, isLikelySourceImplementationFile } from "../development-warnings.js";
5
- import { buildLargeFileSplitPlan } from "../large-file-split.js";
5
+ import { isLikelySourceImplementationFile } from "../development-warnings.js";
6
+ import { buildLargeFileSplitPlan, countFileLinesStreaming, hashFileContentStreaming, hasOrdinalModuleName, LARGE_FILE_SPLIT_PLANNER_VERSION, } from "../large-file-split.js";
6
7
  import { toolCompactOrJson } from "../token-savings.js";
7
8
  import { flushPendingChangeBuffer } from "../file-indexing.js";
8
9
  import { logActivity } from "../activity-log.js";
9
10
  import { resolveProjectPathUnderRoot, resolveReadPathUnderProjectRoot } from "../project-files.js";
10
11
  import { compactLargeFileSplitPlanText, safeJson, toolJson } from "../tool-output.js";
12
+ import { parseMetadataJson } from "../memory-recall.js";
13
+ import { looksLikeGeneratedFile, shouldIgnoreDbFilePath } from "../path-rules.js";
14
+ function readFileHead(absPath, maxBytes = 16_384) {
15
+ const fd = fs.openSync(absPath, "r");
16
+ try {
17
+ const buffer = Buffer.alloc(maxBytes);
18
+ const bytesRead = fs.readSync(fd, buffer, 0, maxBytes, 0);
19
+ return buffer.subarray(0, bytesRead).toString("utf8");
20
+ }
21
+ finally {
22
+ fs.closeSync(fd);
23
+ }
24
+ }
25
+ function filePathIdentity(input) {
26
+ const normalized = input.replace(/\\/g, "/");
27
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
28
+ }
29
+ function validSplitPlanStatus(status) {
30
+ return ["planned", "in_progress", "partial", "resolved", "needs_refinement"].includes(String(status));
31
+ }
32
+ function parsePersistedPlan(content) {
33
+ try {
34
+ const payload = JSON.parse(content);
35
+ return payload?.plan && typeof payload.plan === "object" && !Array.isArray(payload.plan)
36
+ ? payload.plan
37
+ : null;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ function compactPlanMetadata(args) {
44
+ return {
45
+ type: "large_file_split_plan",
46
+ status: args.status,
47
+ file: args.file,
48
+ source_state_hash: args.sourceStateHash,
49
+ source_content_hash: args.sourceStateHash,
50
+ planner_fingerprint: args.plannerFingerprint,
51
+ planner_version: LARGE_FILE_SPLIT_PLANNER_VERSION,
52
+ current_state_hash: args.currentStateHash ?? args.sourceStateHash,
53
+ requirement_id: args.requirement.id,
54
+ goal_key: args.requirement.goal_key ?? null,
55
+ plan_ok: args.plan.ok,
56
+ coverage_complete: args.plan.coverage.complete,
57
+ module_constraints_satisfied: args.plan.module_constraints?.satisfied === true,
58
+ target_paths: args.plan.modules.map((module) => module.target_path),
59
+ analysis_mode: args.plan.analysis_mode,
60
+ confidence: args.plan.confidence,
61
+ verification: args.verification ?? [],
62
+ verification_gaps: args.verificationGaps ?? args.plan.warnings,
63
+ };
64
+ }
65
+ function supersedePlanRow(context, row, replacementPlanId, reason) {
66
+ const metadata = parseMetadataJson(row.metadata_json);
67
+ let content = row.content;
68
+ try {
69
+ const parsed = JSON.parse(row.content);
70
+ content = safeJson({
71
+ ...parsed,
72
+ status: "superseded",
73
+ superseded: true,
74
+ superseded_reason: reason,
75
+ superseded_by_plan_id: replacementPlanId,
76
+ }) ?? row.content;
77
+ }
78
+ catch {
79
+ // Keep legacy content intact while making metadata authoritative.
80
+ }
81
+ context.getDb().prepare(`UPDATE memory_items
82
+ SET content = ?, metadata_json = ?, content_hash = ?, updated_at = CURRENT_TIMESTAMP
83
+ WHERE id = ? AND kind = 'large_file_split_plan'`).run(content, safeJson({
84
+ ...metadata,
85
+ status: "superseded",
86
+ superseded: true,
87
+ superseded_reason: reason,
88
+ superseded_by_plan_id: replacementPlanId,
89
+ superseded_at: new Date().toISOString(),
90
+ }), context.sha256Hex(content), row.id);
91
+ }
92
+ function resolveSplitRequirement(context, args) {
93
+ const { getActiveRequirementByIdStmt, getActiveRequirementByGoalKeyStmt, listActiveRequirementsStmt, } = context.getStatements();
94
+ if (args.req_id) {
95
+ const requirement = getActiveRequirementByIdStmt.get(args.req_id);
96
+ return { requirement, ambiguous: false, active_count: requirement ? 1 : 0 };
97
+ }
98
+ if (args.goal_key?.trim()) {
99
+ const requirement = getActiveRequirementByGoalKeyStmt.get(args.goal_key.trim());
100
+ return { requirement, ambiguous: false, active_count: requirement ? 1 : 0 };
101
+ }
102
+ const active = listActiveRequirementsStmt.all(2);
103
+ return {
104
+ requirement: active.length === 1 ? active[0] : undefined,
105
+ ambiguous: active.length > 1,
106
+ active_count: active.length,
107
+ };
108
+ }
109
+ function splitRequirementError(resolution) {
110
+ if (resolution.requirement)
111
+ return null;
112
+ return {
113
+ isError: true,
114
+ content: [{
115
+ type: "text",
116
+ text: toolJson({
117
+ ok: false,
118
+ error: resolution.ambiguous
119
+ ? "Multiple active requirements exist. Pass req_id or goal_key for the large-file split workflow."
120
+ : "No matching active requirement. Start or resume the requirement before planning or recording a large-file split.",
121
+ active_count: resolution.active_count,
122
+ }),
123
+ }],
124
+ };
125
+ }
11
126
  export async function handlePlanLargeFileSplit(rawArgs, context) {
12
127
  const projectRoot = context.getProjectRoot();
13
128
  const normalizeToDbPath = context.normalizeToDbPath;
129
+ const { insertMemoryItemStmt } = context.getStatements();
14
130
  const args = PlanLargeFileSplitArgsSchema.parse(rawArgs);
15
131
  flushPendingChangeBuffer();
132
+ const requirementResolution = resolveSplitRequirement(context, args);
133
+ const requirementError = splitRequirementError(requirementResolution);
134
+ if (requirementError)
135
+ return requirementError;
136
+ const active = requirementResolution.requirement;
16
137
  const resolved = resolveReadPathUnderProjectRoot(projectRoot, normalizeToDbPath, args.file);
17
138
  let stat;
18
139
  try {
@@ -42,37 +163,241 @@ export async function handlePlanLargeFileSplit(rawArgs, context) {
42
163
  ],
43
164
  };
44
165
  }
45
- const lineInfo = countFileLinesBounded(resolved.absPath, 8_000_000);
46
- const lineCount = lineInfo?.lines ?? 0;
47
- if (lineCount < DEVELOPMENT_HUGE_FILE_LINES) {
166
+ if (shouldIgnoreDbFilePath(resolved.dbFilePath) || looksLikeGeneratedFile(readFileHead(resolved.absPath))) {
48
167
  return {
49
- content: [
50
- {
168
+ isError: true,
169
+ content: [{
51
170
  type: "text",
52
171
  text: toolJson({
53
172
  ok: false,
173
+ error: "Generated, vendored, dependency, or build-output source files must be regenerated or excluded, not mechanically modularized.",
54
174
  file_path: resolved.dbFilePath,
55
- line_count: lineInfo?.truncated ? `${lineCount}+` : lineCount,
56
- huge_threshold_lines: DEVELOPMENT_HUGE_FILE_LINES,
57
- recommended_action: "This file is not above the huge-file threshold. Use normal focused modularity rules unless the user explicitly asked for refactoring.",
58
175
  }),
59
- },
60
- ],
176
+ }],
61
177
  };
62
178
  }
63
- let targetDir = args.target_dir;
64
- if (targetDir) {
65
- targetDir = resolveProjectPathUnderRoot(projectRoot, normalizeToDbPath, targetDir, { allowRoot: true }).dbFilePath;
179
+ const fileBaseName = path.basename(resolved.dbFilePath, path.extname(resolved.dbFilePath));
180
+ const fileParentDir = path.dirname(resolved.dbFilePath).replace(/\\/g, "/");
181
+ const defaultTargetDir = fileParentDir === "." ? fileBaseName : `${fileParentDir}/${fileBaseName}`;
182
+ let targetDir;
183
+ try {
184
+ const resolvedTargetDir = resolveProjectPathUnderRoot(projectRoot, normalizeToDbPath, args.target_dir ?? defaultTargetDir, { allowRoot: true });
185
+ targetDir = resolvedTargetDir.dbFilePath;
186
+ if (filePathIdentity(targetDir) === filePathIdentity(resolved.dbFilePath)) {
187
+ throw new Error("Large-file split target_dir must not be the source file.");
188
+ }
189
+ if (fs.existsSync(resolvedTargetDir.absPath) && !fs.statSync(resolvedTargetDir.absPath).isDirectory()) {
190
+ throw new Error(`Large-file split target_dir must be a directory boundary: ${targetDir}`);
191
+ }
192
+ }
193
+ catch (err) {
194
+ return {
195
+ isError: true,
196
+ content: [{ type: "text", text: toolJson({ ok: false, error: String(err) }) }],
197
+ };
66
198
  }
67
- const plan = buildLargeFileSplitPlan({
199
+ let moduleOverrides;
200
+ try {
201
+ const seenModules = new Set();
202
+ const seenTargets = new Set();
203
+ moduleOverrides = args.module_overrides?.map((override) => {
204
+ const moduleName = override.module.trim();
205
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(moduleName) || hasOrdinalModuleName(moduleName)) {
206
+ throw new Error(`Invalid semantic module name: ${override.module}`);
207
+ }
208
+ const moduleKey = moduleName.toLowerCase();
209
+ if (seenModules.has(moduleKey))
210
+ throw new Error(`Duplicate module override: ${moduleName}`);
211
+ seenModules.add(moduleKey);
212
+ const targetPath = resolveProjectPathUnderRoot(projectRoot, normalizeToDbPath, override.target_path).dbFilePath;
213
+ const targetKey = filePathIdentity(targetPath);
214
+ if (seenTargets.has(targetKey))
215
+ throw new Error(`Duplicate module target_path: ${targetPath}`);
216
+ seenTargets.add(targetKey);
217
+ const relativeToTargetDir = path.posix.relative(targetDir.replace(/\\/g, "/"), targetPath.replace(/\\/g, "/"));
218
+ if (relativeToTargetDir.startsWith("../") || relativeToTargetDir === ".." || path.posix.isAbsolute(relativeToTargetDir)) {
219
+ throw new Error(`module_overrides target_path must stay under target_dir ${targetDir}: ${targetPath}`);
220
+ }
221
+ const normalizedTarget = targetPath.toLowerCase();
222
+ const targetBase = path.basename(normalizedTarget);
223
+ const sourceExtension = path.extname(resolved.dbFilePath).toLowerCase();
224
+ const targetExtension = path.extname(targetPath).toLowerCase();
225
+ const targetAbsPath = path.join(projectRoot, targetPath);
226
+ if (targetKey === filePathIdentity(resolved.dbFilePath) ||
227
+ !sourceExtension || targetExtension !== sourceExtension ||
228
+ fs.existsSync(targetAbsPath) && fs.statSync(targetAbsPath).isDirectory() ||
229
+ hasOrdinalModuleName(targetPath) ||
230
+ shouldIgnoreDbFilePath(targetPath) ||
231
+ normalizedTarget.includes(".parts/") ||
232
+ targetBase.includes(".generated.") ||
233
+ /(?:^|[_-])part\d*(?:\.|$)/.test(targetBase)) {
234
+ throw new Error(`Invalid module override target_path: ${targetPath}`);
235
+ }
236
+ return {
237
+ module: moduleName,
238
+ target_path: targetPath,
239
+ declaration_names: override.declaration_names
240
+ ? [...new Set(override.declaration_names.map((name) => name.trim()))].sort()
241
+ : undefined,
242
+ line_ranges: override.line_ranges
243
+ ? [...override.line_ranges].sort((a, b) => a.start - b.start || a.end - b.end)
244
+ : undefined,
245
+ };
246
+ });
247
+ }
248
+ catch (err) {
249
+ return {
250
+ isError: true,
251
+ content: [{ type: "text", text: toolJson({ ok: false, error: String(err) }) }],
252
+ };
253
+ }
254
+ const effectiveMaxDeclarations = Math.min(200, args.max_declarations_per_module);
255
+ const effectiveMaxLines = Math.min(DEVELOPMENT_BLOCK_FILE_LINES, args.max_lines_per_module);
256
+ const fingerprintPayload = {
257
+ planner_version: LARGE_FILE_SPLIT_PLANNER_VERSION,
258
+ target_dir: targetDir,
259
+ max_modules: args.max_modules,
260
+ max_declarations_per_module: effectiveMaxDeclarations,
261
+ max_lines_per_module: effectiveMaxLines,
262
+ module_overrides: moduleOverrides ?? [],
263
+ };
264
+ const plannerFingerprint = context.sha256Hex(safeJson(fingerprintPayload) ?? "{}");
265
+ const plan = await buildLargeFileSplitPlan({
68
266
  filePath: resolved.dbFilePath,
69
267
  absPath: resolved.absPath,
70
268
  intent: args.intent,
71
269
  targetDir,
72
270
  maxModules: args.max_modules,
73
271
  hugeThresholdLines: DEVELOPMENT_HUGE_FILE_LINES,
272
+ maxDeclarationsPerModule: effectiveMaxDeclarations,
273
+ maxEstimatedLinesPerModule: effectiveMaxLines,
274
+ moduleOverrides,
275
+ plannerFingerprint,
276
+ });
277
+ if (plan.line_count < DEVELOPMENT_HUGE_FILE_LINES) {
278
+ return {
279
+ content: [{
280
+ type: "text",
281
+ text: toolJson({
282
+ ok: false,
283
+ file_path: resolved.dbFilePath,
284
+ line_count: plan.line_count,
285
+ huge_threshold_lines: DEVELOPMENT_HUGE_FILE_LINES,
286
+ recommended_action: "This file is not above the huge-file threshold. Use normal focused modularity rules unless the user explicitly asked for refactoring.",
287
+ }),
288
+ }],
289
+ };
290
+ }
291
+ const sourceStateHash = plan.source_content_hash;
292
+ const existingRows = context.getDb().prepare(`SELECT id, kind, content, req_id, file_path, metadata_json
293
+ FROM memory_items
294
+ WHERE kind = 'large_file_split_plan' AND req_id = ? AND file_path = ?
295
+ ORDER BY updated_at DESC, id DESC
296
+ LIMIT 20`).all(active.id, resolved.dbFilePath);
297
+ const activeStatuses = new Set(["planned", "in_progress", "partial", "needs_refinement"]);
298
+ const progressingRows = existingRows.filter((row) => {
299
+ const status = String(parseMetadataJson(row.metadata_json).status);
300
+ return status === "in_progress" || status === "partial";
301
+ });
302
+ const reusable = existingRows.find((row) => {
303
+ const metadata = parseMetadataJson(row.metadata_json);
304
+ const status = String(metadata.status);
305
+ const expectedState = (status === "in_progress" || status === "partial") && typeof metadata.current_state_hash === "string"
306
+ ? metadata.current_state_hash
307
+ : typeof metadata.source_content_hash === "string"
308
+ ? metadata.source_content_hash
309
+ : metadata.source_state_hash;
310
+ return activeStatuses.has(String(metadata.status)) &&
311
+ expectedState === sourceStateHash &&
312
+ metadata.planner_fingerprint === plannerFingerprint &&
313
+ !!parsePersistedPlan(row.content);
314
+ });
315
+ if (progressingRows.length && (progressingRows.length !== 1 || reusable?.id !== progressingRows[0].id)) {
316
+ return {
317
+ isError: true,
318
+ content: [{
319
+ type: "text",
320
+ text: toolJson({
321
+ ok: false,
322
+ error: "An in-progress or partial split plan already owns this requirement/file. Resume it with record_large_file_split or complete/defer the requirement before replanning.",
323
+ active_progress_plans: progressingRows.map((row) => ({
324
+ plan_id: row.id,
325
+ status: parseMetadataJson(row.metadata_json).status,
326
+ })),
327
+ }),
328
+ }],
329
+ };
330
+ }
331
+ if (reusable) {
332
+ const reusedPlan = parsePersistedPlan(reusable.content);
333
+ for (const duplicate of existingRows) {
334
+ if (duplicate.id === reusable.id)
335
+ continue;
336
+ const metadata = parseMetadataJson(duplicate.metadata_json);
337
+ if (!activeStatuses.has(String(metadata.status)))
338
+ continue;
339
+ if (metadata.status === "planned" || metadata.status === "needs_refinement") {
340
+ supersedePlanRow(context, duplicate, reusable.id, "duplicate_plan_reused");
341
+ }
342
+ }
343
+ logActivity("plan_large_file_split", {
344
+ plan_id: reusable.id,
345
+ req_id: active.id,
346
+ file_path: reusedPlan.file_path,
347
+ reused: true,
348
+ });
349
+ return {
350
+ content: [{
351
+ type: "text",
352
+ text: toolCompactOrJson("plan_large_file_split", {
353
+ ...reusedPlan,
354
+ plan_id: reusable.id,
355
+ plan_status: parseMetadataJson(reusable.metadata_json).status,
356
+ source_state_hash: sourceStateHash,
357
+ reused: true,
358
+ requirement: { id: active.id, title: active.title, goal_key: active.goal_key ?? null },
359
+ }, compactLargeFileSplitPlanText({
360
+ ...reusedPlan,
361
+ plan_id: reusable.id,
362
+ plan_status: String(parseMetadataJson(reusable.metadata_json).status ?? "planned"),
363
+ requirement: { id: active.id, title: active.title },
364
+ }), args.format),
365
+ }],
366
+ };
367
+ }
368
+ const planStatus = plan.ok ? "planned" : "needs_refinement";
369
+ const planMetadata = compactPlanMetadata({
370
+ status: planStatus,
371
+ file: resolved.dbFilePath,
372
+ sourceStateHash,
373
+ plannerFingerprint,
374
+ requirement: active,
375
+ plan,
74
376
  });
377
+ const planContent = safeJson({
378
+ type: "large_file_split_plan",
379
+ status: planStatus,
380
+ file: resolved.dbFilePath,
381
+ requirement: { id: active.id, title: active.title, goal_key: active.goal_key ?? null },
382
+ source_state_hash: sourceStateHash,
383
+ source_content_hash: sourceStateHash,
384
+ planner_fingerprint: plannerFingerprint,
385
+ plan,
386
+ });
387
+ const planInfo = insertMemoryItemStmt.run("large_file_split_plan", `large-file-split:${resolved.dbFilePath}`, planContent, resolved.dbFilePath, null, null, active.id, safeJson(planMetadata), context.sha256Hex(`${resolved.dbFilePath}\n${sourceStateHash ?? "missing"}\n${safeJson(plan)}`));
388
+ const planId = Number(planInfo.lastInsertRowid);
389
+ for (const stale of existingRows) {
390
+ const metadata = parseMetadataJson(stale.metadata_json);
391
+ if (metadata.status !== "planned" && metadata.status !== "needs_refinement")
392
+ continue;
393
+ const reason = metadata.source_content_hash === sourceStateHash || metadata.source_state_hash === sourceStateHash
394
+ ? "planner_configuration_changed"
395
+ : "source_content_changed";
396
+ supersedePlanRow(context, stale, planId, reason);
397
+ }
75
398
  logActivity("plan_large_file_split", {
399
+ plan_id: planId,
400
+ req_id: active.id,
76
401
  file_path: plan.file_path,
77
402
  line_count: plan.line_count,
78
403
  target_dir: plan.target_dir,
@@ -82,43 +407,288 @@ export async function handlePlanLargeFileSplit(rawArgs, context) {
82
407
  content: [
83
408
  {
84
409
  type: "text",
85
- text: toolCompactOrJson("plan_large_file_split", plan, compactLargeFileSplitPlanText(plan), args.format),
410
+ text: toolCompactOrJson("plan_large_file_split", {
411
+ ...plan,
412
+ plan_id: planId,
413
+ plan_status: planStatus,
414
+ source_state_hash: sourceStateHash,
415
+ requirement: { id: active.id, title: active.title, goal_key: active.goal_key ?? null },
416
+ }, compactLargeFileSplitPlanText({
417
+ ...plan,
418
+ plan_id: planId,
419
+ plan_status: planStatus,
420
+ requirement: { id: active.id, title: active.title },
421
+ }), args.format),
86
422
  },
87
423
  ],
88
424
  };
89
425
  }
90
426
  export async function handleRecordLargeFileSplit(rawArgs, context) {
427
+ const projectRoot = context.getProjectRoot();
91
428
  const normalizeToDbPath = context.normalizeToDbPath;
92
429
  const sha256Hex = context.sha256Hex;
93
- const { insertMemoryItemStmt, getActiveRequirementStmt } = context.getStatements();
430
+ const { getMemoryItemByIdStmt } = context.getStatements();
94
431
  const args = RecordLargeFileSplitArgsSchema.parse(rawArgs);
95
432
  flushPendingChangeBuffer();
96
- const normalizedFile = normalizeToDbPath(args.file);
97
- const active = getActiveRequirementStmt.get();
98
- const modules = (args.modules ?? []).map(normalizeToDbPath);
99
- const content = [
100
- `Huge-file mechanical modularization ${args.status}: ${normalizedFile}`,
101
- "",
102
- args.summary,
103
- modules.length ? `\nModules:\n${modules.map((m) => `- ${m}`).join("\n")}` : "",
104
- args.remaining_lines != null ? `\nRemaining lines: ${args.remaining_lines}` : "",
105
- ].filter(Boolean).join("\n");
433
+ const requirementResolution = resolveSplitRequirement(context, args);
434
+ const requirementError = splitRequirementError(requirementResolution);
435
+ if (requirementError)
436
+ return requirementError;
437
+ const active = requirementResolution.requirement;
438
+ const resolvedFile = resolveProjectPathUnderRoot(projectRoot, normalizeToDbPath, args.file);
439
+ const normalizedFile = resolvedFile.dbFilePath;
440
+ const planRow = getMemoryItemByIdStmt.get(args.plan_id);
441
+ const planMetadata = parseMetadataJson(planRow?.metadata_json);
442
+ const legacyPlan = planMetadata.plan && typeof planMetadata.plan === "object" && !Array.isArray(planMetadata.plan)
443
+ ? planMetadata.plan
444
+ : null;
445
+ const persistedPlan = planRow ? (parsePersistedPlan(planRow.content) ?? legacyPlan) : null;
446
+ const storedStatus = planMetadata.status;
447
+ if (!planRow ||
448
+ planRow.kind !== "large_file_split_plan" ||
449
+ planMetadata.type !== "large_file_split_plan" ||
450
+ planRow.req_id !== active.id ||
451
+ planMetadata.file !== normalizedFile ||
452
+ !persistedPlan ||
453
+ !validSplitPlanStatus(storedStatus)) {
454
+ return {
455
+ isError: true,
456
+ content: [{
457
+ type: "text",
458
+ text: toolJson({
459
+ ok: false,
460
+ error: "plan_id does not identify a compatible large-file split plan for this requirement and file.",
461
+ plan_id: args.plan_id,
462
+ req_id: active.id,
463
+ file_path: normalizedFile,
464
+ }),
465
+ }],
466
+ };
467
+ }
468
+ if (storedStatus === "needs_refinement") {
469
+ return {
470
+ isError: true,
471
+ content: [{
472
+ type: "text",
473
+ text: toolJson({
474
+ ok: false,
475
+ error: "This split plan is incomplete and must be refined before progress can be recorded.",
476
+ plan_id: args.plan_id,
477
+ }),
478
+ }],
479
+ };
480
+ }
481
+ const modules = (args.modules ?? []).map((modulePath) => resolveProjectPathUnderRoot(projectRoot, normalizeToDbPath, modulePath).dbFilePath);
482
+ const ordinalModules = modules.filter(hasOrdinalModuleName);
483
+ const ignoredModules = modules.filter(shouldIgnoreDbFilePath);
484
+ const fakeSplitModules = modules.filter((modulePath) => {
485
+ const normalized = modulePath.replace(/\\/g, "/").toLowerCase();
486
+ const base = path.basename(normalized);
487
+ return normalized.includes(".parts/") || base.includes(".generated.") || /(?:^|[_-])part\d*(?:\.|$)/.test(base);
488
+ });
489
+ if (ordinalModules.length || ignoredModules.length || fakeSplitModules.length) {
490
+ return {
491
+ isError: true,
492
+ content: [
493
+ {
494
+ type: "text",
495
+ text: toolJson({
496
+ ok: false,
497
+ error: "Ordinal-prefixed, generated, parts, or ignored module paths are not allowed for mechanical modularization.",
498
+ file_path: normalizedFile,
499
+ invalid_modules: [...new Set([...ordinalModules, ...ignoredModules, ...fakeSplitModules])],
500
+ required_naming: "Use stable semantic names such as config.ts, api.ts, service.ts, storage.ts, ui.ts, or maintenance.ts; do not use 1_xxx, 2_xxx, 03-xxx, or other ordering prefixes.",
501
+ }),
502
+ },
503
+ ],
504
+ };
505
+ }
506
+ const allowedTransitions = {
507
+ planned: ["in_progress", "partial", "resolved"],
508
+ in_progress: ["partial", "resolved"],
509
+ partial: ["in_progress", "resolved"],
510
+ resolved: ["resolved"],
511
+ };
512
+ if (!allowedTransitions[storedStatus].includes(args.status)) {
513
+ return {
514
+ isError: true,
515
+ content: [{
516
+ type: "text",
517
+ text: toolJson({
518
+ ok: false,
519
+ error: `Invalid split plan status transition: ${storedStatus} -> ${args.status}.`,
520
+ allowed_statuses: allowedTransitions[storedStatus],
521
+ }),
522
+ }],
523
+ };
524
+ }
525
+ const storedPlanModules = persistedPlan.modules.map((module) => module.target_path);
526
+ const unexpectedModules = modules.filter((modulePath) => !storedPlanModules.includes(modulePath));
527
+ if (args.status === "planned" && modules.length) {
528
+ return {
529
+ isError: true,
530
+ content: [{
531
+ type: "text",
532
+ text: toolJson({
533
+ ok: false,
534
+ error: "Planned status uses the persisted plan modules and does not accept caller module overrides.",
535
+ plan_id: args.plan_id,
536
+ }),
537
+ }],
538
+ };
539
+ }
540
+ if (unexpectedModules.length) {
541
+ return {
542
+ isError: true,
543
+ content: [{
544
+ type: "text",
545
+ text: toolJson({
546
+ ok: false,
547
+ error: "Progress modules must be a subset of the persisted split plan.",
548
+ unexpected_modules: unexpectedModules,
549
+ }),
550
+ }],
551
+ };
552
+ }
553
+ const missingModules = modules.filter((modulePath) => {
554
+ try {
555
+ return !fs.statSync(path.join(projectRoot, modulePath)).isFile();
556
+ }
557
+ catch {
558
+ return true;
559
+ }
560
+ });
561
+ const missingPlannedModules = storedPlanModules.filter((modulePath) => !modules.includes(modulePath));
562
+ if ((args.status === "partial" || args.status === "resolved") && (!modules.length || missingModules.length) ||
563
+ args.status === "in_progress" && modules.length > 0 && missingModules.length > 0) {
564
+ return {
565
+ isError: true,
566
+ content: [{
567
+ type: "text",
568
+ text: toolJson({
569
+ ok: false,
570
+ error: "Submitted in-progress, partial, or resolved module paths must exist under project_root; partial/resolved also require at least one module.",
571
+ missing_modules: missingModules,
572
+ }),
573
+ }],
574
+ };
575
+ }
576
+ if (args.status === "resolved" && missingPlannedModules.length) {
577
+ return {
578
+ isError: true,
579
+ content: [{
580
+ type: "text",
581
+ text: toolJson({
582
+ ok: false,
583
+ error: "Resolved status must account for every module in the persisted split plan.",
584
+ missing_planned_modules: missingPlannedModules,
585
+ }),
586
+ }],
587
+ };
588
+ }
589
+ const actualRemainingLines = fs.existsSync(resolvedFile.absPath)
590
+ ? await countFileLinesStreaming(resolvedFile.absPath)
591
+ : 0;
592
+ const verification = args.verification ?? [];
593
+ const verificationGaps = args.verification_gaps ?? [];
594
+ const oversizedResolvedModules = [];
595
+ if (args.status === "resolved") {
596
+ for (const modulePath of modules) {
597
+ const lines = await countFileLinesStreaming(path.join(projectRoot, modulePath));
598
+ if (lines >= DEVELOPMENT_BLOCK_FILE_LINES)
599
+ oversizedResolvedModules.push({ file: modulePath, lines });
600
+ }
601
+ }
602
+ if (args.remaining_lines != null && args.remaining_lines !== actualRemainingLines) {
603
+ return {
604
+ isError: true,
605
+ content: [{
606
+ type: "text",
607
+ text: toolJson({
608
+ ok: false,
609
+ error: "remaining_lines does not match the current source file.",
610
+ provided: args.remaining_lines,
611
+ actual: actualRemainingLines,
612
+ }),
613
+ }],
614
+ };
615
+ }
616
+ if (args.status === "resolved" &&
617
+ (actualRemainingLines >= DEVELOPMENT_BLOCK_FILE_LINES ||
618
+ oversizedResolvedModules.length > 0 ||
619
+ verification.length === 0 ||
620
+ verificationGaps.length > 0)) {
621
+ return {
622
+ isError: true,
623
+ content: [{
624
+ type: "text",
625
+ text: toolJson({
626
+ ok: false,
627
+ error: "Resolved status requires the source and every target module below the very-large threshold, verification evidence, and no verification gaps.",
628
+ remaining_lines: actualRemainingLines,
629
+ resolved_line_threshold: DEVELOPMENT_BLOCK_FILE_LINES,
630
+ oversized_modules: oversizedResolvedModules,
631
+ verification,
632
+ verification_gaps: verificationGaps,
633
+ }),
634
+ }],
635
+ };
636
+ }
637
+ const currentStateHash = fs.existsSync(resolvedFile.absPath)
638
+ ? await hashFileContentStreaming(resolvedFile.absPath)
639
+ : context.sha256Hex("missing");
640
+ const originalSourceStateHash = typeof planMetadata.source_content_hash === "string"
641
+ ? planMetadata.source_content_hash
642
+ : typeof planMetadata.source_state_hash === "string"
643
+ ? planMetadata.source_state_hash
644
+ : null;
106
645
  const meta = {
107
- tags: ["large-file-split", "mechanical-modularization"],
108
- file: normalizedFile,
109
- status: args.status,
110
- modules,
111
- remaining_lines: args.remaining_lines ?? null,
112
- active_requirement_id: active?.id ?? null,
646
+ ...compactPlanMetadata({
647
+ status: args.status,
648
+ file: normalizedFile,
649
+ sourceStateHash: originalSourceStateHash,
650
+ plannerFingerprint: typeof planMetadata.planner_fingerprint === "string"
651
+ ? planMetadata.planner_fingerprint
652
+ : persistedPlan.planner_fingerprint,
653
+ currentStateHash,
654
+ requirement: active,
655
+ plan: persistedPlan,
656
+ verification,
657
+ verificationGaps,
658
+ }),
659
+ progress_modules: modules,
660
+ remaining_lines: actualRemainingLines,
661
+ updated_at: new Date().toISOString(),
113
662
  };
114
- const info = insertMemoryItemStmt.run("note", `large-file-split:${normalizedFile}:${args.status}`, content, normalizedFile, null, null, active?.id ?? null, safeJson(meta), sha256Hex(content));
115
- const id = Number(info.lastInsertRowid);
663
+ const content = safeJson({
664
+ type: "large_file_split_plan",
665
+ summary: args.summary,
666
+ status: args.status,
667
+ file: normalizedFile,
668
+ requirement: { id: active.id, title: active.title, goal_key: active.goal_key ?? null },
669
+ source_state_hash: originalSourceStateHash,
670
+ source_content_hash: originalSourceStateHash,
671
+ planner_fingerprint: persistedPlan.planner_fingerprint,
672
+ current_state_hash: currentStateHash,
673
+ plan: persistedPlan,
674
+ progress: {
675
+ modules,
676
+ remaining_lines: actualRemainingLines,
677
+ verification,
678
+ verification_gaps: verificationGaps,
679
+ updated_at: meta.updated_at,
680
+ },
681
+ }) ?? "{}";
682
+ context.getDb().prepare(`UPDATE memory_items
683
+ SET content = ?, metadata_json = ?, content_hash = ?, updated_at = CURRENT_TIMESTAMP
684
+ WHERE id = ? AND kind = 'large_file_split_plan'`).run(content, safeJson(meta), sha256Hex(content), args.plan_id);
116
685
  logActivity("record_large_file_split", {
117
- memory_item_id: id,
686
+ plan_id: args.plan_id,
687
+ req_id: active.id,
118
688
  file_path: normalizedFile,
119
689
  status: args.status,
120
690
  modules: modules.slice(0, 20),
121
- remaining_lines: args.remaining_lines ?? null,
691
+ remaining_lines: actualRemainingLines,
122
692
  });
123
693
  return {
124
694
  content: [
@@ -126,11 +696,15 @@ export async function handleRecordLargeFileSplit(rawArgs, context) {
126
696
  type: "text",
127
697
  text: toolJson({
128
698
  ok: true,
129
- note: { id },
699
+ plan: { id: args.plan_id },
700
+ note: { id: args.plan_id },
701
+ linked_to_requirement: { id: active.id, title: active.title, goal_key: active.goal_key ?? null },
130
702
  file_path: normalizedFile,
131
703
  status: args.status,
132
704
  modules,
133
- remaining_lines: args.remaining_lines ?? null,
705
+ remaining_lines: actualRemainingLines,
706
+ verification,
707
+ verification_gaps: verificationGaps,
134
708
  }),
135
709
  },
136
710
  ],