agentsys 5.0.3 → 5.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 (264) hide show
  1. package/.claude-plugin/marketplace.json +21 -14
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +2 -1
  4. package/CHANGELOG.md +18 -0
  5. package/README.md +7 -6
  6. package/adapters/codex/skills/agnix/SKILL.md +0 -1
  7. package/adapters/codex/skills/audit-project/SKILL.md +0 -1
  8. package/adapters/codex/skills/audit-project-agents/SKILL.md +0 -1
  9. package/adapters/codex/skills/audit-project-github/SKILL.md +0 -1
  10. package/adapters/codex/skills/consult/SKILL.md +132 -57
  11. package/adapters/codex/skills/debate/SKILL.md +214 -0
  12. package/adapters/codex/skills/delivery-approval/SKILL.md +0 -1
  13. package/adapters/codex/skills/deslop/SKILL.md +0 -1
  14. package/adapters/codex/skills/drift-detect/SKILL.md +0 -1
  15. package/adapters/codex/skills/enhance/SKILL.md +0 -1
  16. package/adapters/codex/skills/learn/SKILL.md +0 -1
  17. package/adapters/codex/skills/next-task/SKILL.md +0 -1
  18. package/adapters/codex/skills/perf/SKILL.md +0 -1
  19. package/adapters/codex/skills/repo-map/SKILL.md +0 -1
  20. package/adapters/codex/skills/ship/SKILL.md +0 -1
  21. package/adapters/codex/skills/ship-ci-review-loop/SKILL.md +0 -1
  22. package/adapters/codex/skills/ship-deployment/SKILL.md +0 -1
  23. package/adapters/codex/skills/ship-error-handling/SKILL.md +0 -1
  24. package/adapters/codex/skills/sync-docs/SKILL.md +0 -1
  25. package/adapters/opencode/agents/agent-enhancer.md +0 -1
  26. package/adapters/opencode/agents/agnix-agent.md +0 -1
  27. package/adapters/opencode/agents/ci-fixer.md +0 -1
  28. package/adapters/opencode/agents/ci-monitor.md +0 -1
  29. package/adapters/opencode/agents/claudemd-enhancer.md +0 -1
  30. package/adapters/opencode/agents/consult-agent.md +122 -30
  31. package/adapters/opencode/agents/cross-file-enhancer.md +0 -1
  32. package/adapters/opencode/agents/debate-orchestrator.md +169 -0
  33. package/adapters/opencode/agents/delivery-validator.md +0 -1
  34. package/adapters/opencode/agents/deslop-agent.md +0 -1
  35. package/adapters/opencode/agents/docs-enhancer.md +0 -1
  36. package/adapters/opencode/agents/exploration-agent.md +0 -1
  37. package/adapters/opencode/agents/hooks-enhancer.md +0 -1
  38. package/adapters/opencode/agents/implementation-agent.md +0 -1
  39. package/adapters/opencode/agents/learn-agent.md +0 -1
  40. package/adapters/opencode/agents/map-validator.md +0 -1
  41. package/adapters/opencode/agents/perf-analyzer.md +0 -1
  42. package/adapters/opencode/agents/perf-code-paths.md +0 -1
  43. package/adapters/opencode/agents/perf-investigation-logger.md +0 -1
  44. package/adapters/opencode/agents/perf-orchestrator.md +0 -1
  45. package/adapters/opencode/agents/perf-theory-gatherer.md +0 -1
  46. package/adapters/opencode/agents/perf-theory-tester.md +0 -1
  47. package/adapters/opencode/agents/plan-synthesizer.md +0 -1
  48. package/adapters/opencode/agents/planning-agent.md +0 -1
  49. package/adapters/opencode/agents/plugin-enhancer.md +0 -1
  50. package/adapters/opencode/agents/prompt-enhancer.md +0 -1
  51. package/adapters/opencode/agents/simple-fixer.md +0 -1
  52. package/adapters/opencode/agents/skills-enhancer.md +0 -1
  53. package/adapters/opencode/agents/sync-docs-agent.md +0 -1
  54. package/adapters/opencode/agents/task-discoverer.md +0 -1
  55. package/adapters/opencode/agents/test-coverage-checker.md +0 -1
  56. package/adapters/opencode/agents/worktree-manager.md +0 -1
  57. package/adapters/opencode/commands/agnix.md +0 -1
  58. package/adapters/opencode/commands/audit-project-agents.md +0 -1
  59. package/adapters/opencode/commands/audit-project-github.md +0 -1
  60. package/adapters/opencode/commands/audit-project.md +0 -1
  61. package/adapters/opencode/commands/consult.md +133 -57
  62. package/adapters/opencode/commands/debate.md +224 -0
  63. package/adapters/opencode/commands/delivery-approval.md +0 -1
  64. package/adapters/opencode/commands/deslop.md +0 -1
  65. package/adapters/opencode/commands/drift-detect.md +0 -1
  66. package/adapters/opencode/commands/enhance.md +0 -1
  67. package/adapters/opencode/commands/learn.md +0 -1
  68. package/adapters/opencode/commands/next-task.md +0 -1
  69. package/adapters/opencode/commands/perf.md +0 -1
  70. package/adapters/opencode/commands/repo-map.md +0 -1
  71. package/adapters/opencode/commands/ship-ci-review-loop.md +0 -1
  72. package/adapters/opencode/commands/ship-deployment.md +0 -1
  73. package/adapters/opencode/commands/ship-error-handling.md +0 -1
  74. package/adapters/opencode/commands/ship.md +0 -1
  75. package/adapters/opencode/commands/sync-docs.md +0 -1
  76. package/adapters/opencode/skills/agnix/SKILL.md +1 -2
  77. package/adapters/opencode/skills/consult/SKILL.md +33 -23
  78. package/adapters/opencode/skills/debate/SKILL.md +245 -0
  79. package/adapters/opencode/skills/deslop/SKILL.md +1 -2
  80. package/adapters/opencode/skills/discover-tasks/SKILL.md +1 -2
  81. package/adapters/opencode/skills/drift-analysis/SKILL.md +1 -2
  82. package/adapters/opencode/skills/enhance-agent-prompts/SKILL.md +1 -2
  83. package/adapters/opencode/skills/enhance-claude-memory/SKILL.md +1 -2
  84. package/adapters/opencode/skills/enhance-cross-file/SKILL.md +1 -2
  85. package/adapters/opencode/skills/enhance-docs/SKILL.md +1 -2
  86. package/adapters/opencode/skills/enhance-hooks/SKILL.md +1 -2
  87. package/adapters/opencode/skills/enhance-orchestrator/SKILL.md +1 -2
  88. package/adapters/opencode/skills/enhance-plugins/SKILL.md +1 -2
  89. package/adapters/opencode/skills/enhance-prompts/SKILL.md +1 -2
  90. package/adapters/opencode/skills/enhance-skills/SKILL.md +1 -2
  91. package/adapters/opencode/skills/learn/SKILL.md +1 -2
  92. package/adapters/opencode/skills/orchestrate-review/SKILL.md +0 -1
  93. package/adapters/opencode/skills/perf-analyzer/SKILL.md +1 -2
  94. package/adapters/opencode/skills/perf-baseline-manager/SKILL.md +1 -2
  95. package/adapters/opencode/skills/perf-benchmarker/SKILL.md +1 -2
  96. package/adapters/opencode/skills/perf-code-paths/SKILL.md +1 -2
  97. package/adapters/opencode/skills/perf-investigation-logger/SKILL.md +1 -2
  98. package/adapters/opencode/skills/perf-profiler/SKILL.md +1 -2
  99. package/adapters/opencode/skills/perf-theory-gatherer/SKILL.md +1 -2
  100. package/adapters/opencode/skills/perf-theory-tester/SKILL.md +1 -2
  101. package/adapters/opencode/skills/repo-mapping/SKILL.md +1 -2
  102. package/adapters/opencode/skills/sync-docs/SKILL.md +1 -2
  103. package/adapters/opencode/skills/validate-delivery/SKILL.md +1 -2
  104. package/lib/adapter-transforms.js +24 -4
  105. package/package.json +1 -1
  106. package/plugins/agnix/.claude-plugin/plugin.json +1 -1
  107. package/plugins/agnix/skills/agnix/SKILL.md +1 -1
  108. package/plugins/audit-project/.claude-plugin/plugin.json +1 -1
  109. package/plugins/audit-project/lib/adapter-transforms.js +24 -4
  110. package/plugins/consult/.claude-plugin/plugin.json +1 -1
  111. package/plugins/consult/agents/consult-agent.md +122 -29
  112. package/plugins/consult/commands/consult.md +135 -58
  113. package/plugins/consult/skills/consult/SKILL.md +31 -20
  114. package/plugins/debate/.claude-plugin/plugin.json +21 -0
  115. package/plugins/debate/agents/debate-orchestrator.md +175 -0
  116. package/plugins/debate/commands/debate.md +221 -0
  117. package/plugins/debate/lib/adapter-transforms.js +298 -0
  118. package/plugins/debate/lib/collectors/codebase.js +392 -0
  119. package/plugins/debate/lib/collectors/docs-patterns.js +713 -0
  120. package/plugins/debate/lib/collectors/documentation.js +219 -0
  121. package/plugins/debate/lib/collectors/github.js +330 -0
  122. package/plugins/debate/lib/collectors/index.js +126 -0
  123. package/plugins/debate/lib/config/index.js +14 -0
  124. package/plugins/debate/lib/cross-platform/index.js +539 -0
  125. package/plugins/debate/lib/discovery/index.js +352 -0
  126. package/plugins/debate/lib/drift-detect/collectors.js +37 -0
  127. package/plugins/debate/lib/enhance/agent-analyzer.js +421 -0
  128. package/plugins/debate/lib/enhance/agent-patterns.js +571 -0
  129. package/plugins/debate/lib/enhance/auto-suppression.js +622 -0
  130. package/plugins/debate/lib/enhance/benchmark.js +417 -0
  131. package/plugins/debate/lib/enhance/cross-file-analyzer.js +930 -0
  132. package/plugins/debate/lib/enhance/cross-file-patterns.js +370 -0
  133. package/plugins/debate/lib/enhance/docs-analyzer.js +325 -0
  134. package/plugins/debate/lib/enhance/docs-patterns.js +671 -0
  135. package/plugins/debate/lib/enhance/fixer.js +721 -0
  136. package/plugins/debate/lib/enhance/hook-analyzer.js +135 -0
  137. package/plugins/debate/lib/enhance/hook-patterns.js +40 -0
  138. package/plugins/debate/lib/enhance/index.js +127 -0
  139. package/plugins/debate/lib/enhance/plugin-analyzer.js +402 -0
  140. package/plugins/debate/lib/enhance/plugin-patterns.js +326 -0
  141. package/plugins/debate/lib/enhance/projectmemory-analyzer.js +551 -0
  142. package/plugins/debate/lib/enhance/projectmemory-patterns.js +617 -0
  143. package/plugins/debate/lib/enhance/prompt-analyzer.js +457 -0
  144. package/plugins/debate/lib/enhance/prompt-patterns.js +1484 -0
  145. package/plugins/debate/lib/enhance/reporter.js +1348 -0
  146. package/plugins/debate/lib/enhance/security-patterns.js +284 -0
  147. package/plugins/debate/lib/enhance/skill-analyzer.js +182 -0
  148. package/plugins/debate/lib/enhance/skill-patterns.js +147 -0
  149. package/plugins/debate/lib/enhance/suppression.js +352 -0
  150. package/plugins/debate/lib/enhance/tool-patterns.js +373 -0
  151. package/plugins/debate/lib/index.js +270 -0
  152. package/plugins/debate/lib/patterns/cli-enhancers.js +611 -0
  153. package/plugins/debate/lib/patterns/pipeline.js +948 -0
  154. package/plugins/debate/lib/patterns/review-patterns.js +558 -0
  155. package/plugins/debate/lib/patterns/slop-analyzers.js +2305 -0
  156. package/plugins/debate/lib/patterns/slop-patterns.js +1187 -0
  157. package/plugins/debate/lib/perf/analyzer/index.js +22 -0
  158. package/plugins/debate/lib/perf/argument-parser.js +105 -0
  159. package/plugins/debate/lib/perf/baseline-comparator.js +50 -0
  160. package/plugins/debate/lib/perf/baseline-store.js +127 -0
  161. package/plugins/debate/lib/perf/benchmark-runner.js +404 -0
  162. package/plugins/debate/lib/perf/breaking-point-finder.js +52 -0
  163. package/plugins/debate/lib/perf/breaking-point-runner.js +60 -0
  164. package/plugins/debate/lib/perf/checkpoint.js +123 -0
  165. package/plugins/debate/lib/perf/code-paths.js +86 -0
  166. package/plugins/debate/lib/perf/consolidation.js +37 -0
  167. package/plugins/debate/lib/perf/constraint-runner.js +71 -0
  168. package/plugins/debate/lib/perf/experiment-runner.js +32 -0
  169. package/plugins/debate/lib/perf/index.js +41 -0
  170. package/plugins/debate/lib/perf/investigation-state.js +874 -0
  171. package/plugins/debate/lib/perf/optimization-runner.js +79 -0
  172. package/plugins/debate/lib/perf/profilers/go.js +22 -0
  173. package/plugins/debate/lib/perf/profilers/index.js +46 -0
  174. package/plugins/debate/lib/perf/profilers/java.js +23 -0
  175. package/plugins/debate/lib/perf/profilers/node.js +27 -0
  176. package/plugins/debate/lib/perf/profilers/python.js +23 -0
  177. package/plugins/debate/lib/perf/profilers/rust.js +23 -0
  178. package/plugins/debate/lib/perf/profiling-runner.js +75 -0
  179. package/plugins/debate/lib/perf/schemas.js +140 -0
  180. package/plugins/debate/lib/platform/detect-platform.js +413 -0
  181. package/plugins/debate/lib/platform/detection-configs.js +93 -0
  182. package/plugins/debate/lib/platform/state-dir.js +132 -0
  183. package/plugins/debate/lib/platform/verify-tools.js +182 -0
  184. package/plugins/debate/lib/repo-map/cache.js +152 -0
  185. package/plugins/debate/lib/repo-map/concurrency.js +29 -0
  186. package/plugins/debate/lib/repo-map/index.js +222 -0
  187. package/plugins/debate/lib/repo-map/installer.js +212 -0
  188. package/plugins/debate/lib/repo-map/queries/go.js +27 -0
  189. package/plugins/debate/lib/repo-map/queries/index.js +100 -0
  190. package/plugins/debate/lib/repo-map/queries/java.js +38 -0
  191. package/plugins/debate/lib/repo-map/queries/javascript.js +55 -0
  192. package/plugins/debate/lib/repo-map/queries/python.js +24 -0
  193. package/plugins/debate/lib/repo-map/queries/rust.js +73 -0
  194. package/plugins/debate/lib/repo-map/queries/typescript.js +38 -0
  195. package/plugins/debate/lib/repo-map/runner.js +1364 -0
  196. package/plugins/debate/lib/repo-map/updater.js +562 -0
  197. package/plugins/debate/lib/repo-map/usage-analyzer.js +407 -0
  198. package/plugins/debate/lib/schemas/plugin-manifest.schema.json +57 -0
  199. package/plugins/debate/lib/schemas/validator.js +247 -0
  200. package/plugins/debate/lib/sources/custom-handler.js +199 -0
  201. package/plugins/debate/lib/sources/policy-questions.js +246 -0
  202. package/plugins/debate/lib/sources/source-cache.js +165 -0
  203. package/plugins/debate/lib/state/workflow-state.js +576 -0
  204. package/plugins/debate/lib/types/agent-frontmatter.d.ts +134 -0
  205. package/plugins/debate/lib/types/command-frontmatter.d.ts +107 -0
  206. package/plugins/debate/lib/types/hook-frontmatter.d.ts +115 -0
  207. package/plugins/debate/lib/types/index.d.ts +84 -0
  208. package/plugins/debate/lib/types/plugin-manifest.d.ts +102 -0
  209. package/plugins/debate/lib/types/skill-frontmatter.d.ts +89 -0
  210. package/plugins/debate/lib/utils/atomic-write.js +94 -0
  211. package/plugins/debate/lib/utils/cache-manager.js +159 -0
  212. package/plugins/debate/lib/utils/command-parser.js +0 -0
  213. package/plugins/debate/lib/utils/context-optimizer.js +300 -0
  214. package/plugins/debate/lib/utils/deprecation.js +37 -0
  215. package/plugins/debate/lib/utils/shell-escape.js +88 -0
  216. package/plugins/debate/lib/utils/state-helpers.js +61 -0
  217. package/plugins/debate/skills/debate/SKILL.md +264 -0
  218. package/plugins/deslop/.claude-plugin/plugin.json +1 -1
  219. package/plugins/deslop/lib/adapter-transforms.js +24 -4
  220. package/plugins/deslop/skills/deslop/SKILL.md +1 -1
  221. package/plugins/drift-detect/.claude-plugin/plugin.json +1 -1
  222. package/plugins/drift-detect/lib/adapter-transforms.js +24 -4
  223. package/plugins/drift-detect/skills/drift-analysis/SKILL.md +1 -1
  224. package/plugins/enhance/.claude-plugin/plugin.json +1 -1
  225. package/plugins/enhance/lib/adapter-transforms.js +24 -4
  226. package/plugins/enhance/skills/enhance-agent-prompts/SKILL.md +1 -1
  227. package/plugins/enhance/skills/enhance-claude-memory/SKILL.md +1 -1
  228. package/plugins/enhance/skills/enhance-cross-file/SKILL.md +1 -1
  229. package/plugins/enhance/skills/enhance-docs/SKILL.md +1 -1
  230. package/plugins/enhance/skills/enhance-hooks/SKILL.md +1 -1
  231. package/plugins/enhance/skills/enhance-orchestrator/SKILL.md +1 -1
  232. package/plugins/enhance/skills/enhance-plugins/SKILL.md +1 -1
  233. package/plugins/enhance/skills/enhance-prompts/SKILL.md +1 -1
  234. package/plugins/enhance/skills/enhance-skills/SKILL.md +1 -1
  235. package/plugins/learn/.claude-plugin/plugin.json +1 -1
  236. package/plugins/learn/agents/learn-agent.md +1 -1
  237. package/plugins/learn/lib/adapter-transforms.js +24 -4
  238. package/plugins/learn/skills/learn/SKILL.md +1 -1
  239. package/plugins/next-task/.claude-plugin/plugin.json +1 -1
  240. package/plugins/next-task/agents/exploration-agent.md +1 -1
  241. package/plugins/next-task/lib/adapter-transforms.js +24 -4
  242. package/plugins/next-task/skills/discover-tasks/SKILL.md +1 -1
  243. package/plugins/next-task/skills/validate-delivery/SKILL.md +1 -1
  244. package/plugins/perf/.claude-plugin/plugin.json +1 -1
  245. package/plugins/perf/lib/adapter-transforms.js +24 -4
  246. package/plugins/perf/skills/perf-analyzer/SKILL.md +1 -1
  247. package/plugins/perf/skills/perf-baseline-manager/SKILL.md +1 -1
  248. package/plugins/perf/skills/perf-benchmarker/SKILL.md +1 -1
  249. package/plugins/perf/skills/perf-code-paths/SKILL.md +1 -1
  250. package/plugins/perf/skills/perf-investigation-logger/SKILL.md +1 -1
  251. package/plugins/perf/skills/perf-profiler/SKILL.md +1 -1
  252. package/plugins/perf/skills/perf-theory-gatherer/SKILL.md +1 -1
  253. package/plugins/perf/skills/perf-theory-tester/SKILL.md +1 -1
  254. package/plugins/repo-map/.claude-plugin/plugin.json +1 -1
  255. package/plugins/repo-map/lib/adapter-transforms.js +24 -4
  256. package/plugins/ship/.claude-plugin/plugin.json +1 -1
  257. package/plugins/ship/lib/adapter-transforms.js +24 -4
  258. package/plugins/sync-docs/.claude-plugin/plugin.json +1 -1
  259. package/plugins/sync-docs/lib/adapter-transforms.js +24 -4
  260. package/plugins/sync-docs/skills/sync-docs/SKILL.md +1 -1
  261. package/scripts/gen-adapters.js +6 -7
  262. package/scripts/generate-docs.js +4 -2
  263. package/scripts/plugins.txt +1 -0
  264. package/site/content.json +6 -6
@@ -0,0 +1,874 @@
1
+ /**
2
+ * Performance investigation state management
3
+ *
4
+ * Stores investigation state and logs under the platform-aware state directory:
5
+ * - {state-dir}/perf/investigation.json
6
+ * - {state-dir}/perf/investigations/{id}.md
7
+ *
8
+ * @module lib/perf/investigation-state
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const crypto = require('crypto');
14
+ const { getStateDir } = require('../platform/state-dir');
15
+ const { validateInvestigationState, assertValid } = require('./schemas');
16
+ const { writeJsonAtomic, writeFileAtomic } = require('../utils/atomic-write');
17
+ const { isPlainObject, updatesApplied, sleepForRetry } = require('../utils/state-helpers');
18
+
19
+ const SCHEMA_VERSION = 1;
20
+ const INVESTIGATION_FILE = 'investigation.json';
21
+ const LOG_DIR = 'investigations';
22
+ const BASELINE_DIR = 'baselines';
23
+ const PHASES = [
24
+ 'setup',
25
+ 'baseline',
26
+ 'breaking-point',
27
+ 'constraints',
28
+ 'hypotheses',
29
+ 'code-paths',
30
+ 'profiling',
31
+ 'optimization',
32
+ 'decision',
33
+ 'consolidation'
34
+ ];
35
+
36
+ /**
37
+ * Validate and resolve path to prevent path traversal attacks
38
+ * @param {string} basePath - Base directory path
39
+ * @returns {string} Validated absolute path
40
+ */
41
+ function validatePath(basePath) {
42
+ if (typeof basePath !== 'string' || basePath.length === 0) {
43
+ throw new Error('Path must be a non-empty string');
44
+ }
45
+ const resolved = path.resolve(basePath);
46
+ if (resolved.includes('\0')) {
47
+ throw new Error('Path contains invalid null byte');
48
+ }
49
+ return resolved;
50
+ }
51
+
52
+ /**
53
+ * Validate that target path is within base directory
54
+ * @param {string} targetPath - Target file path
55
+ * @param {string} basePath - Base directory
56
+ */
57
+ function validatePathWithinBase(targetPath, basePath) {
58
+ const resolvedTarget = path.resolve(targetPath);
59
+ const resolvedBase = path.resolve(basePath);
60
+ if (!resolvedTarget.startsWith(resolvedBase + path.sep) && resolvedTarget !== resolvedBase) {
61
+ throw new Error('Path traversal detected');
62
+ }
63
+ }
64
+
65
+ function assertSafeInvestigationId(id) {
66
+ if (!id || typeof id !== 'string') {
67
+ throw new Error('Investigation id is required');
68
+ }
69
+ if (id.includes('..') || id.includes('/') || id.includes('\\') || id.includes('\0')) {
70
+ throw new Error('Investigation id contains invalid characters');
71
+ }
72
+ if (!/^[a-zA-Z0-9._-]+$/.test(id)) {
73
+ throw new Error('Investigation id contains invalid characters');
74
+ }
75
+ return id;
76
+ }
77
+
78
+ /**
79
+ * Generate a unique investigation ID
80
+ * @returns {string}
81
+ */
82
+ function generateInvestigationId() {
83
+ const now = new Date();
84
+ const date = now.toISOString().slice(0, 10).replace(/-/g, '');
85
+ const time = now.toISOString().slice(11, 19).replace(/:/g, '');
86
+ const random = crypto.randomBytes(4).toString('hex');
87
+ return `perf-${date}-${time}-${random}`;
88
+ }
89
+
90
+ /**
91
+ * Get perf state directory path
92
+ * @param {string} basePath
93
+ * @returns {string}
94
+ */
95
+ function getPerfDir(basePath = process.cwd()) {
96
+ const validatedBase = validatePath(basePath);
97
+ const perfDir = path.join(validatedBase, getStateDir(basePath), 'perf');
98
+ validatePathWithinBase(perfDir, validatedBase);
99
+ return perfDir;
100
+ }
101
+
102
+ /**
103
+ * Ensure perf directories exist
104
+ * @param {string} basePath
105
+ * @returns {{ perfDir: string, logDir: string, baselineDir: string }}
106
+ */
107
+ function ensurePerfDirs(basePath = process.cwd()) {
108
+ const perfDir = getPerfDir(basePath);
109
+ const logDir = path.join(perfDir, LOG_DIR);
110
+ const baselineDir = path.join(perfDir, BASELINE_DIR);
111
+
112
+ if (!fs.existsSync(perfDir)) {
113
+ fs.mkdirSync(perfDir, { recursive: true });
114
+ }
115
+ if (!fs.existsSync(logDir)) {
116
+ fs.mkdirSync(logDir, { recursive: true });
117
+ }
118
+ if (!fs.existsSync(baselineDir)) {
119
+ fs.mkdirSync(baselineDir, { recursive: true });
120
+ }
121
+
122
+ return { perfDir, logDir, baselineDir };
123
+ }
124
+
125
+ /**
126
+ * Get path to investigation.json
127
+ * @param {string} basePath
128
+ * @returns {string}
129
+ */
130
+ function getInvestigationPath(basePath = process.cwd()) {
131
+ const perfDir = getPerfDir(basePath);
132
+ return path.join(perfDir, INVESTIGATION_FILE);
133
+ }
134
+
135
+ /**
136
+ * Get path to investigation log
137
+ * @param {string} id
138
+ * @param {string} basePath
139
+ * @returns {string}
140
+ */
141
+ function getInvestigationLogPath(id, basePath = process.cwd()) {
142
+ const safeId = assertSafeInvestigationId(id);
143
+ const { logDir } = ensurePerfDirs(basePath);
144
+ return path.join(logDir, `${safeId}.md`);
145
+ }
146
+
147
+ /**
148
+ * Read investigation.json
149
+ * @param {string} basePath
150
+ * @returns {object|null}
151
+ */
152
+ function readInvestigation(basePath = process.cwd()) {
153
+ const investigationPath = getInvestigationPath(basePath);
154
+ if (!fs.existsSync(investigationPath)) {
155
+ return null;
156
+ }
157
+ try {
158
+ const parsed = JSON.parse(fs.readFileSync(investigationPath, 'utf8'));
159
+ const validation = validateInvestigationState(parsed);
160
+ if (!validation.ok) {
161
+ console.error(`[CRITICAL] Invalid investigation state at ${investigationPath}: ${validation.errors.join(', ')}`);
162
+ return null;
163
+ }
164
+ return parsed;
165
+ } catch (error) {
166
+ console.error(`[CRITICAL] Corrupted investigation.json at ${investigationPath}: ${error.message}`);
167
+ return null;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Write investigation.json
173
+ * Increments version for optimistic locking
174
+ * @param {object} state
175
+ * @param {string} basePath
176
+ * @returns {boolean}
177
+ */
178
+ function writeInvestigation(state, basePath = process.cwd()) {
179
+ ensurePerfDirs(basePath);
180
+ const investigationPath = getInvestigationPath(basePath);
181
+ const nextState = {
182
+ ...state,
183
+ updatedAt: new Date().toISOString(),
184
+ _version: (state._version || 0) + 1
185
+ };
186
+ assertValid(validateInvestigationState(nextState), 'Invalid investigation state');
187
+ writeJsonAtomic(investigationPath, nextState);
188
+ return true;
189
+ }
190
+
191
+ /**
192
+ * Update investigation.json with partial updates
193
+ * Uses optimistic locking with version check and retry
194
+ * @param {object} updates
195
+ * @param {string} basePath
196
+ * @returns {object|null}
197
+ */
198
+ function updateInvestigation(updates, basePath = process.cwd()) {
199
+ const MAX_RETRIES = 5;
200
+ let fallbackState = null;
201
+
202
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
203
+ const current = readInvestigation(basePath) || {};
204
+ fallbackState = current;
205
+ const initialVersion = current._version || 0;
206
+ const nextState = { ...current };
207
+
208
+ for (const [key, value] of Object.entries(updates)) {
209
+ // Skip internal version field from updates
210
+ if (key === '_version') continue;
211
+
212
+ if (value === null) {
213
+ nextState[key] = null;
214
+ } else if (isPlainObject(value) && isPlainObject(nextState[key])) {
215
+ nextState[key] = { ...nextState[key], ...value };
216
+ } else {
217
+ nextState[key] = value;
218
+ }
219
+ }
220
+
221
+ // Preserve version for write (writeInvestigation will increment it)
222
+ nextState._version = initialVersion;
223
+
224
+ writeInvestigation(nextState, basePath);
225
+
226
+ // Re-read to verify our write succeeded
227
+ const afterWrite = readInvestigation(basePath);
228
+ if (afterWrite) {
229
+ fallbackState = afterWrite;
230
+ }
231
+ if (afterWrite && afterWrite._version >= initialVersion + 1 && updatesApplied(afterWrite, updates)) {
232
+ return afterWrite; // Success
233
+ }
234
+
235
+ // Version conflict - retry after brief delay
236
+ if (attempt < MAX_RETRIES - 1) {
237
+ const delay = Math.floor(Math.random() * 50) + 10;
238
+ sleepForRetry(delay);
239
+ }
240
+ }
241
+
242
+ // All retries exhausted
243
+ console.error('[ERROR] updateInvestigation: failed to apply updates after max retries');
244
+ return readInvestigation(basePath) || fallbackState || { ...updates };
245
+ }
246
+
247
+ /**
248
+ * Initialize a new investigation
249
+ * @param {object} options
250
+ * @param {string} basePath
251
+ * @returns {object}
252
+ */
253
+ function initializeInvestigation(options = {}, basePath = process.cwd()) {
254
+ const id = options.id || generateInvestigationId();
255
+ const phase = options.phase || PHASES[0];
256
+
257
+ if (!PHASES.includes(phase)) {
258
+ throw new Error(`Invalid perf phase: ${phase}`);
259
+ }
260
+
261
+ const state = {
262
+ schemaVersion: SCHEMA_VERSION,
263
+ id,
264
+ status: 'in_progress',
265
+ phase,
266
+ createdAt: new Date().toISOString(),
267
+ updatedAt: new Date().toISOString(),
268
+ scenario: {
269
+ description: options.scenario || '',
270
+ metrics: options.metrics || [],
271
+ successCriteria: options.successCriteria || '',
272
+ scenarios: Array.isArray(options.scenarios) ? options.scenarios : []
273
+ },
274
+ baselines: [],
275
+ hypotheses: [],
276
+ codePaths: [],
277
+ experiments: [],
278
+ results: [],
279
+ breakingPoint: null,
280
+ breakingPointHistory: [],
281
+ constraintResults: [],
282
+ profilingResults: [],
283
+ decision: null
284
+ };
285
+
286
+ assertValid(validateInvestigationState(state), 'Invalid initial investigation state');
287
+ writeInvestigation(state, basePath);
288
+ return state;
289
+ }
290
+
291
+ /**
292
+ * Append a line to the investigation log
293
+ * @param {string} id
294
+ * @param {string} content
295
+ * @param {string} basePath
296
+ */
297
+ function appendInvestigationLog(id, content, basePath = process.cwd()) {
298
+ if (!content) return;
299
+ const logPath = getInvestigationLogPath(id, basePath);
300
+ const entry = content.endsWith('\n') ? content : `${content}\n`;
301
+ fs.appendFileSync(logPath, entry, 'utf8');
302
+ }
303
+
304
+ /**
305
+ * Append a baseline section to the investigation log
306
+ * @param {object} input
307
+ * @param {string} input.id
308
+ * @param {string} input.userQuote
309
+ * @param {string} input.command
310
+ * @param {object} input.metrics
311
+ * @param {string} input.baselinePath
312
+ * @param {number} [input.duration]
313
+ * @param {number} [input.runs]
314
+ * @param {string} [input.aggregate]
315
+ * @param {string} [input.date]
316
+ * @param {string} basePath
317
+ */
318
+ function appendBaselineLog(input, basePath = process.cwd()) {
319
+ if (!input || typeof input !== 'object') {
320
+ throw new Error('appendBaselineLog requires an input object');
321
+ }
322
+
323
+ const { id, userQuote, command, metrics, baselinePath, date, scenarios, duration, runs, aggregate } = input;
324
+
325
+ if (!id || typeof id !== 'string') {
326
+ throw new Error('appendBaselineLog requires a valid investigation id');
327
+ }
328
+ if (!userQuote || typeof userQuote !== 'string') {
329
+ throw new Error('appendBaselineLog requires a non-empty userQuote');
330
+ }
331
+ if (!command || typeof command !== 'string') {
332
+ throw new Error('appendBaselineLog requires a non-empty command');
333
+ }
334
+ if (!metrics || typeof metrics !== 'object' || Array.isArray(metrics)) {
335
+ throw new Error('appendBaselineLog requires a metrics object');
336
+ }
337
+ if (!baselinePath || typeof baselinePath !== 'string') {
338
+ throw new Error('appendBaselineLog requires a baselinePath');
339
+ }
340
+
341
+ const logDate = date || new Date().toISOString().slice(0, 10);
342
+ const metricsText = JSON.stringify(metrics);
343
+ const scenarioText = Array.isArray(scenarios) && scenarios.length > 0
344
+ ? scenarios.map((scenario) => scenario.name).filter(Boolean).join(', ')
345
+ : '';
346
+ const durationLine = Number.isFinite(duration) ? `- Duration: ${duration}s` : null;
347
+ const runsLine = Number.isFinite(runs) ? `- Runs: ${runs}` : null;
348
+ const aggregateLine = aggregate ? `- Aggregate: ${aggregate}` : null;
349
+
350
+ const entry = [
351
+ `## Baseline - ${logDate}`,
352
+ '',
353
+ `**User Quote:** "${userQuote}"`,
354
+ '',
355
+ '**Summary**',
356
+ scenarioText ? `- Scenarios: ${scenarioText}` : null,
357
+ `- Baseline command: \`${command}\``,
358
+ durationLine,
359
+ runsLine,
360
+ aggregateLine,
361
+ `- Metrics: ${metricsText}`,
362
+ '',
363
+ '**Evidence**',
364
+ `- Baseline file: ${baselinePath}`,
365
+ ''
366
+ ].filter(Boolean).join('\n');
367
+
368
+ appendInvestigationLog(id, entry, basePath);
369
+ }
370
+
371
+ /**
372
+ * Append a profiling section to the investigation log
373
+ * @param {object} input
374
+ * @param {string} input.id
375
+ * @param {string} input.userQuote
376
+ * @param {string} input.tool
377
+ * @param {string} input.command
378
+ * @param {string[]} input.artifacts
379
+ * @param {string[]} input.hotspots
380
+ * @param {string} [input.date]
381
+ * @param {string} basePath
382
+ */
383
+ function appendProfilingLog(input, basePath = process.cwd()) {
384
+ if (!input || typeof input !== 'object') {
385
+ throw new Error('appendProfilingLog requires an input object');
386
+ }
387
+
388
+ const { id, userQuote, tool, command, artifacts, hotspots, date } = input;
389
+
390
+ if (!id || typeof id !== 'string') {
391
+ throw new Error('appendProfilingLog requires a valid investigation id');
392
+ }
393
+ if (!userQuote || typeof userQuote !== 'string') {
394
+ throw new Error('appendProfilingLog requires a non-empty userQuote');
395
+ }
396
+ if (!tool || typeof tool !== 'string') {
397
+ throw new Error('appendProfilingLog requires a tool');
398
+ }
399
+ if (!command || typeof command !== 'string') {
400
+ throw new Error('appendProfilingLog requires a command');
401
+ }
402
+
403
+ const logDate = date || new Date().toISOString().slice(0, 10);
404
+ const artifactList = Array.isArray(artifacts) ? artifacts : [];
405
+ const hotspotList = Array.isArray(hotspots) ? hotspots : [];
406
+
407
+ const entry = [
408
+ `## Profiling - ${logDate}`,
409
+ '',
410
+ `**User Quote:** "${userQuote}"`,
411
+ '',
412
+ '**Summary**',
413
+ `- Tool: ${tool}`,
414
+ `- Command: \`${command}\``,
415
+ '',
416
+ '**Evidence**',
417
+ artifactList.length ? `- Artifacts: ${artifactList.join(', ')}` : '- Artifacts: n/a',
418
+ hotspotList.length ? `- Hotspots: ${hotspotList.join(', ')}` : '- Hotspots: n/a',
419
+ ''
420
+ ].join('\n');
421
+
422
+ appendInvestigationLog(id, entry, basePath);
423
+ }
424
+
425
+ /**
426
+ * Append a decision section to the investigation log
427
+ * @param {object} input
428
+ * @param {string} input.id
429
+ * @param {string} input.userQuote
430
+ * @param {string} input.verdict
431
+ * @param {string} input.rationale
432
+ * @param {string} [input.date]
433
+ * @param {string} basePath
434
+ */
435
+ function appendDecisionLog(input, basePath = process.cwd()) {
436
+ if (!input || typeof input !== 'object') {
437
+ throw new Error('appendDecisionLog requires an input object');
438
+ }
439
+
440
+ const { id, userQuote, verdict, rationale, date, resultsCount } = input;
441
+
442
+ if (!id || typeof id !== 'string') {
443
+ throw new Error('appendDecisionLog requires a valid investigation id');
444
+ }
445
+ if (!userQuote || typeof userQuote !== 'string') {
446
+ throw new Error('appendDecisionLog requires a non-empty userQuote');
447
+ }
448
+ if (!verdict || typeof verdict !== 'string') {
449
+ throw new Error('appendDecisionLog requires a verdict');
450
+ }
451
+ if (!rationale || typeof rationale !== 'string') {
452
+ throw new Error('appendDecisionLog requires a rationale');
453
+ }
454
+
455
+ const logDate = date || new Date().toISOString().slice(0, 10);
456
+ const resultCountText = typeof resultsCount === 'number' ? String(resultsCount) : 'n/a';
457
+
458
+ const entry = [
459
+ `## Decision - ${logDate}`,
460
+ '',
461
+ `**User Quote:** "${userQuote}"`,
462
+ '',
463
+ '**Summary**',
464
+ `- Verdict: ${verdict}`,
465
+ `- Rationale: ${rationale}`,
466
+ '',
467
+ '**Evidence**',
468
+ `- Results count: ${resultCountText}`,
469
+ ''
470
+ ].join('\n');
471
+
472
+ appendInvestigationLog(id, entry, basePath);
473
+ }
474
+
475
+ /**
476
+ * Append a setup section to the investigation log
477
+ * @param {object} input
478
+ * @param {string} input.id
479
+ * @param {string} input.userQuote
480
+ * @param {string} input.scenario
481
+ * @param {string} input.command
482
+ * @param {string} input.version
483
+ * @param {number} [input.duration]
484
+ * @param {number} [input.runs]
485
+ * @param {string} [input.aggregate]
486
+ * @param {string} [input.date]
487
+ * @param {string} basePath
488
+ */
489
+ function appendSetupLog(input, basePath = process.cwd()) {
490
+ if (!input || typeof input !== 'object') {
491
+ throw new Error('appendSetupLog requires an input object');
492
+ }
493
+
494
+ const { id, userQuote, scenario, command, version, duration, runs, aggregate, date } = input;
495
+
496
+ if (!id || typeof id !== 'string') {
497
+ throw new Error('appendSetupLog requires a valid investigation id');
498
+ }
499
+ if (!userQuote || typeof userQuote !== 'string') {
500
+ throw new Error('appendSetupLog requires a non-empty userQuote');
501
+ }
502
+ if (!scenario || typeof scenario !== 'string') {
503
+ throw new Error('appendSetupLog requires a scenario');
504
+ }
505
+ if (!command || typeof command !== 'string') {
506
+ throw new Error('appendSetupLog requires a command');
507
+ }
508
+ if (!version || typeof version !== 'string') {
509
+ throw new Error('appendSetupLog requires a version');
510
+ }
511
+
512
+ const logDate = date || new Date().toISOString().slice(0, 10);
513
+ const durationLine = Number.isFinite(duration) ? `- Duration: ${duration}s` : null;
514
+ const runsLine = Number.isFinite(runs) ? `- Runs: ${runs}` : null;
515
+ const aggregateLine = aggregate ? `- Aggregate: ${aggregate}` : null;
516
+ const entry = [
517
+ `## Setup - ${logDate}`,
518
+ '',
519
+ `**User Quote:** "${userQuote}"`,
520
+ '',
521
+ '**Summary**',
522
+ `- Scenario: ${scenario}`,
523
+ `- Command: \`${command}\``,
524
+ `- Version: ${version}`,
525
+ durationLine,
526
+ runsLine,
527
+ aggregateLine,
528
+ '',
529
+ '**Evidence**',
530
+ `- Command: \`${command}\``,
531
+ `- Version: ${version}`,
532
+ ''
533
+ ].filter(Boolean).join('\n');
534
+
535
+ appendInvestigationLog(id, entry, basePath);
536
+ }
537
+
538
+ /**
539
+ * Append a breaking point section to the investigation log
540
+ * @param {object} input
541
+ * @param {string} input.id
542
+ * @param {string} input.userQuote
543
+ * @param {string} input.paramEnv
544
+ * @param {number} input.min
545
+ * @param {number} input.max
546
+ * @param {number|null} input.breakingPoint
547
+ * @param {string} [input.date]
548
+ * @param {string} basePath
549
+ */
550
+ function appendBreakingPointLog(input, basePath = process.cwd()) {
551
+ if (!input || typeof input !== 'object') {
552
+ throw new Error('appendBreakingPointLog requires an input object');
553
+ }
554
+ const { id, userQuote, paramEnv, min, max, breakingPoint, history, date } = input;
555
+
556
+ if (!id || typeof id !== 'string') {
557
+ throw new Error('appendBreakingPointLog requires a valid investigation id');
558
+ }
559
+ if (!userQuote || typeof userQuote !== 'string') {
560
+ throw new Error('appendBreakingPointLog requires a non-empty userQuote');
561
+ }
562
+ if (!paramEnv || typeof paramEnv !== 'string') {
563
+ throw new Error('appendBreakingPointLog requires a paramEnv');
564
+ }
565
+ if (typeof min !== 'number' || typeof max !== 'number') {
566
+ throw new Error('appendBreakingPointLog requires numeric min/max');
567
+ }
568
+
569
+ const logDate = date || new Date().toISOString().slice(0, 10);
570
+ const historyText = Array.isArray(history) ? JSON.stringify(history) : 'n/a';
571
+ const entry = [
572
+ `## Breaking Point - ${logDate}`,
573
+ '',
574
+ `**User Quote:** "${userQuote}"`,
575
+ '',
576
+ '**Summary**',
577
+ `- Param env: ${paramEnv}`,
578
+ `- Range: ${min}..${max}`,
579
+ `- Breaking point: ${breakingPoint ?? 'n/a'}`,
580
+ '',
581
+ '**Evidence**',
582
+ `- History: ${historyText}`,
583
+ ''
584
+ ].join('\n');
585
+
586
+ appendInvestigationLog(id, entry, basePath);
587
+ }
588
+
589
+ /**
590
+ * Append a constraints section to the investigation log
591
+ * @param {object} input
592
+ * @param {string} input.id
593
+ * @param {string} input.userQuote
594
+ * @param {object} input.constraints
595
+ * @param {object} input.delta
596
+ * @param {string} [input.date]
597
+ * @param {string} basePath
598
+ */
599
+ function appendConstraintLog(input, basePath = process.cwd()) {
600
+ if (!input || typeof input !== 'object') {
601
+ throw new Error('appendConstraintLog requires an input object');
602
+ }
603
+ const { id, userQuote, constraints, delta, date } = input;
604
+
605
+ if (!id || typeof id !== 'string') {
606
+ throw new Error('appendConstraintLog requires a valid investigation id');
607
+ }
608
+ if (!userQuote || typeof userQuote !== 'string') {
609
+ throw new Error('appendConstraintLog requires a non-empty userQuote');
610
+ }
611
+ if (!constraints || typeof constraints !== 'object') {
612
+ throw new Error('appendConstraintLog requires constraints');
613
+ }
614
+ if (!delta || typeof delta !== 'object') {
615
+ throw new Error('appendConstraintLog requires delta');
616
+ }
617
+
618
+ const logDate = date || new Date().toISOString().slice(0, 10);
619
+ const entry = [
620
+ `## Constraints - ${logDate}`,
621
+ '',
622
+ `**User Quote:** "${userQuote}"`,
623
+ '',
624
+ '**Summary**',
625
+ `- CPU: ${constraints.cpu || 'n/a'}`,
626
+ `- Memory: ${constraints.memory || 'n/a'}`,
627
+ '',
628
+ '**Evidence**',
629
+ `- Delta: ${JSON.stringify(delta.metrics || {})}`,
630
+ ''
631
+ ].join('\n');
632
+
633
+ appendInvestigationLog(id, entry, basePath);
634
+ }
635
+
636
+ /**
637
+ * Append a hypotheses section to the investigation log
638
+ * @param {object} input
639
+ * @param {string} input.id
640
+ * @param {string} input.userQuote
641
+ * @param {Array} input.hypotheses
642
+ * @param {string} [input.date]
643
+ * @param {string} basePath
644
+ */
645
+ function appendHypothesesLog(input, basePath = process.cwd()) {
646
+ if (!input || typeof input !== 'object') {
647
+ throw new Error('appendHypothesesLog requires an input object');
648
+ }
649
+ const { id, userQuote, hypotheses, date, gitHistory, hypothesesFile } = input;
650
+
651
+ if (!id || typeof id !== 'string') {
652
+ throw new Error('appendHypothesesLog requires a valid investigation id');
653
+ }
654
+ if (!userQuote || typeof userQuote !== 'string') {
655
+ throw new Error('appendHypothesesLog requires a non-empty userQuote');
656
+ }
657
+ if (!Array.isArray(hypotheses)) {
658
+ throw new Error('appendHypothesesLog requires hypotheses array');
659
+ }
660
+
661
+ const logDate = date || new Date().toISOString().slice(0, 10);
662
+ const lines = hypotheses.map((item) => {
663
+ if (!item) return null;
664
+ const label = item.id ? `${item.id}: ` : '';
665
+ const evidence = item.evidence ? ` (evidence: ${item.evidence})` : '';
666
+ const confidence = item.confidence ? ` [${item.confidence}]` : '';
667
+ return `- ${label}${item.hypothesis || 'n/a'}${confidence}${evidence}`;
668
+ }).filter(Boolean);
669
+
670
+ const entry = [
671
+ `## Hypotheses - ${logDate}`,
672
+ '',
673
+ `**User Quote:** "${userQuote}"`,
674
+ '',
675
+ '**Summary**',
676
+ lines.length > 0 ? lines.join('\n') : '- n/a',
677
+ '',
678
+ '**Evidence**',
679
+ `- Git history: ${Array.isArray(gitHistory) && gitHistory.length ? gitHistory.join(' | ') : 'n/a'}`,
680
+ hypothesesFile ? `- Hypotheses file: ${hypothesesFile}` : null,
681
+ ''
682
+ ].join('\n');
683
+
684
+ appendInvestigationLog(id, entry, basePath);
685
+ }
686
+
687
+ /**
688
+ * Append a code-paths section to the investigation log
689
+ * @param {object} input
690
+ * @param {string} input.id
691
+ * @param {string} input.userQuote
692
+ * @param {string[]} input.keywords
693
+ * @param {Array} input.paths
694
+ * @param {string} [input.date]
695
+ * @param {string} basePath
696
+ */
697
+ function appendCodePathsLog(input, basePath = process.cwd()) {
698
+ if (!input || typeof input !== 'object') {
699
+ throw new Error('appendCodePathsLog requires an input object');
700
+ }
701
+ const { id, userQuote, keywords, paths, date, repoMapStatus } = input;
702
+
703
+ if (!id || typeof id !== 'string') {
704
+ throw new Error('appendCodePathsLog requires a valid investigation id');
705
+ }
706
+ if (!userQuote || typeof userQuote !== 'string') {
707
+ throw new Error('appendCodePathsLog requires a non-empty userQuote');
708
+ }
709
+ if (!Array.isArray(paths)) {
710
+ throw new Error('appendCodePathsLog requires paths array');
711
+ }
712
+
713
+ const logDate = date || new Date().toISOString().slice(0, 10);
714
+ const keywordText = Array.isArray(keywords) && keywords.length > 0 ? keywords.join(', ') : 'n/a';
715
+ const pathLines = paths.map((pathEntry) => {
716
+ const file = pathEntry.file || 'n/a';
717
+ const score = typeof pathEntry.score === 'number' ? ` (score: ${pathEntry.score})` : '';
718
+ const symbols = Array.isArray(pathEntry.symbols) && pathEntry.symbols.length > 0
719
+ ? ` [${pathEntry.symbols.join(', ')}]`
720
+ : '';
721
+ return `- ${file}${score}${symbols}`;
722
+ });
723
+
724
+ const entry = [
725
+ `## Code Paths - ${logDate}`,
726
+ '',
727
+ `**User Quote:** "${userQuote}"`,
728
+ '',
729
+ '**Summary**',
730
+ `- Keywords: ${keywordText}`,
731
+ pathLines.length > 0 ? pathLines.join('\n') : '- n/a',
732
+ '',
733
+ '**Evidence**',
734
+ `- Repo map: ${repoMapStatus || 'n/a'}`,
735
+ `- Paths count: ${pathLines.length}`,
736
+ ''
737
+ ].join('\n');
738
+
739
+ appendInvestigationLog(id, entry, basePath);
740
+ }
741
+
742
+ /**
743
+ * Append an optimization section to the investigation log
744
+ * @param {object} input
745
+ * @param {string} input.id
746
+ * @param {string} input.userQuote
747
+ * @param {string} input.change
748
+ * @param {object} input.delta
749
+ * @param {string} input.verdict
750
+ * @param {number} [input.runs]
751
+ * @param {string} [input.aggregate]
752
+ * @param {string} [input.date]
753
+ * @param {string} basePath
754
+ */
755
+ function appendOptimizationLog(input, basePath = process.cwd()) {
756
+ if (!input || typeof input !== 'object') {
757
+ throw new Error('appendOptimizationLog requires an input object');
758
+ }
759
+ const { id, userQuote, change, delta, verdict, date, gitHistory, runs, aggregate } = input;
760
+
761
+ if (!id || typeof id !== 'string') {
762
+ throw new Error('appendOptimizationLog requires a valid investigation id');
763
+ }
764
+ if (!userQuote || typeof userQuote !== 'string') {
765
+ throw new Error('appendOptimizationLog requires a non-empty userQuote');
766
+ }
767
+ if (!change || typeof change !== 'string') {
768
+ throw new Error('appendOptimizationLog requires a change summary');
769
+ }
770
+ if (!delta || typeof delta !== 'object') {
771
+ throw new Error('appendOptimizationLog requires delta');
772
+ }
773
+ if (!verdict || typeof verdict !== 'string') {
774
+ throw new Error('appendOptimizationLog requires a verdict');
775
+ }
776
+
777
+ const logDate = date || new Date().toISOString().slice(0, 10);
778
+ const gitHistoryText = Array.isArray(gitHistory) && gitHistory.length
779
+ ? gitHistory.join(' | ')
780
+ : 'n/a';
781
+ const runsLine = Number.isFinite(runs) ? `- Runs: ${runs}` : null;
782
+ const aggregateLine = aggregate ? `- Aggregate: ${aggregate}` : null;
783
+ const entry = [
784
+ `## Optimization - ${logDate}`,
785
+ '',
786
+ `**User Quote:** "${userQuote}"`,
787
+ '',
788
+ '**Summary**',
789
+ `- Change: ${change}`,
790
+ `- Verdict: ${verdict}`,
791
+ runsLine,
792
+ aggregateLine,
793
+ '',
794
+ '**Evidence**',
795
+ `- Delta: ${JSON.stringify(delta.metrics || {})}`,
796
+ `- Git history: ${gitHistoryText}`,
797
+ ''
798
+ ].join('\n');
799
+
800
+ appendInvestigationLog(id, entry, basePath);
801
+ }
802
+
803
+ /**
804
+ * Append a consolidation section to the investigation log
805
+ * @param {object} input
806
+ * @param {string} input.id
807
+ * @param {string} input.userQuote
808
+ * @param {string} input.version
809
+ * @param {string} input.path
810
+ * @param {string} [input.date]
811
+ * @param {string} basePath
812
+ */
813
+ function appendConsolidationLog(input, basePath = process.cwd()) {
814
+ if (!input || typeof input !== 'object') {
815
+ throw new Error('appendConsolidationLog requires an input object');
816
+ }
817
+
818
+ const { id, userQuote, version, path, date } = input;
819
+
820
+ if (!id || typeof id !== 'string') {
821
+ throw new Error('appendConsolidationLog requires a valid investigation id');
822
+ }
823
+ if (!userQuote || typeof userQuote !== 'string') {
824
+ throw new Error('appendConsolidationLog requires a non-empty userQuote');
825
+ }
826
+ if (!version || typeof version !== 'string') {
827
+ throw new Error('appendConsolidationLog requires a version');
828
+ }
829
+ if (!path || typeof path !== 'string') {
830
+ throw new Error('appendConsolidationLog requires a path');
831
+ }
832
+
833
+ const logDate = date || new Date().toISOString().slice(0, 10);
834
+ const entry = [
835
+ `## Consolidation - ${logDate}`,
836
+ '',
837
+ `**User Quote:** "${userQuote}"`,
838
+ '',
839
+ '**Summary**',
840
+ `- Version: ${version}`,
841
+ `- Baseline file: ${path}`,
842
+ '',
843
+ '**Evidence**',
844
+ `- Baseline file: ${path}`,
845
+ ''
846
+ ].join('\n');
847
+
848
+ appendInvestigationLog(id, entry, basePath);
849
+ }
850
+
851
+ module.exports = {
852
+ SCHEMA_VERSION,
853
+ PHASES,
854
+ generateInvestigationId,
855
+ getPerfDir,
856
+ ensurePerfDirs,
857
+ getInvestigationPath,
858
+ getInvestigationLogPath,
859
+ readInvestigation,
860
+ writeInvestigation,
861
+ updateInvestigation,
862
+ initializeInvestigation,
863
+ appendInvestigationLog,
864
+ appendBaselineLog,
865
+ appendProfilingLog,
866
+ appendDecisionLog,
867
+ appendSetupLog,
868
+ appendBreakingPointLog,
869
+ appendConstraintLog,
870
+ appendHypothesesLog,
871
+ appendCodePathsLog,
872
+ appendOptimizationLog,
873
+ appendConsolidationLog
874
+ };