@ktpartners/dgs-platform 2.6.2

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 (256) hide show
  1. package/LICENSE +38 -0
  2. package/README.md +851 -0
  3. package/agents/dgs-codebase-cross-analyzer.md +183 -0
  4. package/agents/dgs-codebase-mapper.md +782 -0
  5. package/agents/dgs-codebase-synthesizer.md +156 -0
  6. package/agents/dgs-debugger.md +1256 -0
  7. package/agents/dgs-executor.md +550 -0
  8. package/agents/dgs-integration-checker.md +481 -0
  9. package/agents/dgs-nyquist-auditor.md +178 -0
  10. package/agents/dgs-phase-researcher.md +563 -0
  11. package/agents/dgs-phase-verifier.md +450 -0
  12. package/agents/dgs-plan-checker.md +708 -0
  13. package/agents/dgs-planner.md +1324 -0
  14. package/agents/dgs-project-researcher.md +631 -0
  15. package/agents/dgs-research-synthesizer.md +249 -0
  16. package/agents/dgs-roadmapper.md +652 -0
  17. package/agents/dgs-verifier.md +607 -0
  18. package/bin/install.js +2073 -0
  19. package/commands/dgs/add-doc.md +45 -0
  20. package/commands/dgs/add-idea.md +38 -0
  21. package/commands/dgs/add-phase.md +43 -0
  22. package/commands/dgs/add-repo.md +54 -0
  23. package/commands/dgs/add-tests.md +41 -0
  24. package/commands/dgs/add-todo.md +47 -0
  25. package/commands/dgs/approve-spec.md +38 -0
  26. package/commands/dgs/audit-milestone.md +36 -0
  27. package/commands/dgs/audit-phase.md +37 -0
  28. package/commands/dgs/cancel-job.md +23 -0
  29. package/commands/dgs/capture-principle.md +143 -0
  30. package/commands/dgs/check-todos.md +45 -0
  31. package/commands/dgs/cleanup.md +18 -0
  32. package/commands/dgs/complete-milestone.md +136 -0
  33. package/commands/dgs/complete-project.md +70 -0
  34. package/commands/dgs/consolidate-ideas.md +50 -0
  35. package/commands/dgs/create-milestone-job.md +37 -0
  36. package/commands/dgs/debug.md +164 -0
  37. package/commands/dgs/develop-idea.md +53 -0
  38. package/commands/dgs/discuss-idea.md +41 -0
  39. package/commands/dgs/discuss-phase.md +83 -0
  40. package/commands/dgs/execute-phase.md +41 -0
  41. package/commands/dgs/fast.md +38 -0
  42. package/commands/dgs/find-related-ideas.md +43 -0
  43. package/commands/dgs/health.md +28 -0
  44. package/commands/dgs/help.md +22 -0
  45. package/commands/dgs/import-spec.md +36 -0
  46. package/commands/dgs/init-product.md +28 -0
  47. package/commands/dgs/insert-phase.md +32 -0
  48. package/commands/dgs/join-discord.md +18 -0
  49. package/commands/dgs/list-docs.md +40 -0
  50. package/commands/dgs/list-ideas.md +42 -0
  51. package/commands/dgs/list-jobs.md +22 -0
  52. package/commands/dgs/list-phase-assumptions.md +46 -0
  53. package/commands/dgs/list-projects.md +57 -0
  54. package/commands/dgs/list-specs.md +40 -0
  55. package/commands/dgs/map-codebase.md +92 -0
  56. package/commands/dgs/new-milestone.md +44 -0
  57. package/commands/dgs/new-project.md +42 -0
  58. package/commands/dgs/node-repair.md +26 -0
  59. package/commands/dgs/overlap-check.md +20 -0
  60. package/commands/dgs/pause-work.md +38 -0
  61. package/commands/dgs/plan-milestone-gaps.md +34 -0
  62. package/commands/dgs/plan-phase.md +44 -0
  63. package/commands/dgs/progress.md +24 -0
  64. package/commands/dgs/quick.md +41 -0
  65. package/commands/dgs/reactivate-project.md +70 -0
  66. package/commands/dgs/reapply-patches.md +110 -0
  67. package/commands/dgs/refine-spec.md +38 -0
  68. package/commands/dgs/reject-idea.md +43 -0
  69. package/commands/dgs/remove-doc.md +44 -0
  70. package/commands/dgs/remove-phase.md +31 -0
  71. package/commands/dgs/remove-repo.md +69 -0
  72. package/commands/dgs/research-idea.md +43 -0
  73. package/commands/dgs/research-phase.md +189 -0
  74. package/commands/dgs/restore-idea.md +45 -0
  75. package/commands/dgs/resume-work.md +40 -0
  76. package/commands/dgs/rollback-job.md +24 -0
  77. package/commands/dgs/run-job.md +35 -0
  78. package/commands/dgs/search.md +40 -0
  79. package/commands/dgs/set-profile.md +34 -0
  80. package/commands/dgs/settings.md +38 -0
  81. package/commands/dgs/switch-project.md +58 -0
  82. package/commands/dgs/undo-consolidation.md +42 -0
  83. package/commands/dgs/update-idea.md +44 -0
  84. package/commands/dgs/update.md +37 -0
  85. package/commands/dgs/validate-phase.md +35 -0
  86. package/commands/dgs/verify-work.md +39 -0
  87. package/commands/dgs/write-spec.md +49 -0
  88. package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-01-SUMMARY.md +84 -0
  89. package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-02-SUMMARY.md +86 -0
  90. package/deliver-great-systems/.planning/phases/10-v1-to-v2-migration-flow/10-01-SUMMARY.md +85 -0
  91. package/deliver-great-systems/bin/dgs-tools.cjs +1444 -0
  92. package/deliver-great-systems/bin/lib/auto-test.cjs +1365 -0
  93. package/deliver-great-systems/bin/lib/commands.cjs +570 -0
  94. package/deliver-great-systems/bin/lib/config.cjs +417 -0
  95. package/deliver-great-systems/bin/lib/conflict-agent.cjs +1063 -0
  96. package/deliver-great-systems/bin/lib/conflict-agent.test.cjs +554 -0
  97. package/deliver-great-systems/bin/lib/context.cjs +929 -0
  98. package/deliver-great-systems/bin/lib/context.test.cjs +693 -0
  99. package/deliver-great-systems/bin/lib/core.cjs +744 -0
  100. package/deliver-great-systems/bin/lib/core.test.cjs +822 -0
  101. package/deliver-great-systems/bin/lib/docs.cjs +919 -0
  102. package/deliver-great-systems/bin/lib/docs.test.cjs +211 -0
  103. package/deliver-great-systems/bin/lib/execution.cjs +705 -0
  104. package/deliver-great-systems/bin/lib/execution.test.cjs +1472 -0
  105. package/deliver-great-systems/bin/lib/frontmatter.cjs +324 -0
  106. package/deliver-great-systems/bin/lib/ideas.cjs +1406 -0
  107. package/deliver-great-systems/bin/lib/ideas.test.cjs +1417 -0
  108. package/deliver-great-systems/bin/lib/identity.cjs +125 -0
  109. package/deliver-great-systems/bin/lib/init.cjs +1114 -0
  110. package/deliver-great-systems/bin/lib/init.test.cjs +1271 -0
  111. package/deliver-great-systems/bin/lib/jobs.cjs +2015 -0
  112. package/deliver-great-systems/bin/lib/jobs.test.cjs +2619 -0
  113. package/deliver-great-systems/bin/lib/merge-conflicts.cjs +654 -0
  114. package/deliver-great-systems/bin/lib/merge-conflicts.test.cjs +370 -0
  115. package/deliver-great-systems/bin/lib/migration.cjs +352 -0
  116. package/deliver-great-systems/bin/lib/migration.test.cjs +582 -0
  117. package/deliver-great-systems/bin/lib/milestone.cjs +243 -0
  118. package/deliver-great-systems/bin/lib/overlap.cjs +437 -0
  119. package/deliver-great-systems/bin/lib/overlap.test.cjs +747 -0
  120. package/deliver-great-systems/bin/lib/path-audit.test.cjs +384 -0
  121. package/deliver-great-systems/bin/lib/paths.cjs +144 -0
  122. package/deliver-great-systems/bin/lib/paths.test.cjs +486 -0
  123. package/deliver-great-systems/bin/lib/phase.cjs +910 -0
  124. package/deliver-great-systems/bin/lib/projects.cjs +691 -0
  125. package/deliver-great-systems/bin/lib/projects.test.cjs +871 -0
  126. package/deliver-great-systems/bin/lib/repos.cjs +1432 -0
  127. package/deliver-great-systems/bin/lib/repos.test.cjs +1882 -0
  128. package/deliver-great-systems/bin/lib/roadmap.cjs +305 -0
  129. package/deliver-great-systems/bin/lib/search.cjs +570 -0
  130. package/deliver-great-systems/bin/lib/specs.cjs +1303 -0
  131. package/deliver-great-systems/bin/lib/state.cjs +893 -0
  132. package/deliver-great-systems/bin/lib/template.cjs +228 -0
  133. package/deliver-great-systems/bin/lib/test-helpers.cjs +291 -0
  134. package/deliver-great-systems/bin/lib/verify.cjs +796 -0
  135. package/deliver-great-systems/references/checkpoints.md +776 -0
  136. package/deliver-great-systems/references/conflict-resolution.md +66 -0
  137. package/deliver-great-systems/references/context-tiers.md +166 -0
  138. package/deliver-great-systems/references/continuation-format.md +249 -0
  139. package/deliver-great-systems/references/decimal-phase-calculation.md +67 -0
  140. package/deliver-great-systems/references/git-integration.md +250 -0
  141. package/deliver-great-systems/references/git-planning-commit.md +40 -0
  142. package/deliver-great-systems/references/model-profile-resolution.md +36 -0
  143. package/deliver-great-systems/references/model-profiles.md +95 -0
  144. package/deliver-great-systems/references/phase-argument-parsing.md +61 -0
  145. package/deliver-great-systems/references/planning-config.md +224 -0
  146. package/deliver-great-systems/references/questioning.md +162 -0
  147. package/deliver-great-systems/references/spec-review-loop.md +177 -0
  148. package/deliver-great-systems/references/tdd.md +265 -0
  149. package/deliver-great-systems/references/ui-brand.md +160 -0
  150. package/deliver-great-systems/references/verification-patterns.md +612 -0
  151. package/deliver-great-systems/templates/DEBUG.md +166 -0
  152. package/deliver-great-systems/templates/UAT.md +251 -0
  153. package/deliver-great-systems/templates/VALIDATION.md +95 -0
  154. package/deliver-great-systems/templates/claude-md.md +74 -0
  155. package/deliver-great-systems/templates/codebase/architecture.md +257 -0
  156. package/deliver-great-systems/templates/codebase/concerns.md +312 -0
  157. package/deliver-great-systems/templates/codebase/conventions.md +309 -0
  158. package/deliver-great-systems/templates/codebase/integrations.md +282 -0
  159. package/deliver-great-systems/templates/codebase/stack.md +188 -0
  160. package/deliver-great-systems/templates/codebase/structure.md +287 -0
  161. package/deliver-great-systems/templates/codebase/testing.md +482 -0
  162. package/deliver-great-systems/templates/config.json +38 -0
  163. package/deliver-great-systems/templates/context.md +354 -0
  164. package/deliver-great-systems/templates/continue-here.md +80 -0
  165. package/deliver-great-systems/templates/debug-subagent-prompt.md +93 -0
  166. package/deliver-great-systems/templates/discovery.md +148 -0
  167. package/deliver-great-systems/templates/milestone-archive.md +125 -0
  168. package/deliver-great-systems/templates/milestone.md +117 -0
  169. package/deliver-great-systems/templates/phase-prompt.md +615 -0
  170. package/deliver-great-systems/templates/planner-subagent-prompt.md +119 -0
  171. package/deliver-great-systems/templates/project.md +186 -0
  172. package/deliver-great-systems/templates/requirements.md +233 -0
  173. package/deliver-great-systems/templates/research-project/ARCHITECTURE.md +206 -0
  174. package/deliver-great-systems/templates/research-project/FEATURES.md +149 -0
  175. package/deliver-great-systems/templates/research-project/PITFALLS.md +202 -0
  176. package/deliver-great-systems/templates/research-project/STACK.md +122 -0
  177. package/deliver-great-systems/templates/research-project/SUMMARY.md +172 -0
  178. package/deliver-great-systems/templates/research.md +554 -0
  179. package/deliver-great-systems/templates/retrospective.md +54 -0
  180. package/deliver-great-systems/templates/roadmap.md +204 -0
  181. package/deliver-great-systems/templates/state.md +178 -0
  182. package/deliver-great-systems/templates/summary-complex.md +59 -0
  183. package/deliver-great-systems/templates/summary-minimal.md +41 -0
  184. package/deliver-great-systems/templates/summary-standard.md +48 -0
  185. package/deliver-great-systems/templates/summary.md +253 -0
  186. package/deliver-great-systems/templates/user-setup.md +313 -0
  187. package/deliver-great-systems/templates/verification-report.md +324 -0
  188. package/deliver-great-systems/workflows/add-doc.md +151 -0
  189. package/deliver-great-systems/workflows/add-idea.md +96 -0
  190. package/deliver-great-systems/workflows/add-phase.md +120 -0
  191. package/deliver-great-systems/workflows/add-tests.md +359 -0
  192. package/deliver-great-systems/workflows/add-todo.md +162 -0
  193. package/deliver-great-systems/workflows/approve-spec.md +194 -0
  194. package/deliver-great-systems/workflows/audit-milestone.md +364 -0
  195. package/deliver-great-systems/workflows/audit-phase.md +462 -0
  196. package/deliver-great-systems/workflows/cancel-job.md +108 -0
  197. package/deliver-great-systems/workflows/check-todos.md +181 -0
  198. package/deliver-great-systems/workflows/cleanup.md +247 -0
  199. package/deliver-great-systems/workflows/codereview.md +526 -0
  200. package/deliver-great-systems/workflows/complete-milestone.md +1298 -0
  201. package/deliver-great-systems/workflows/consolidate-ideas.md +365 -0
  202. package/deliver-great-systems/workflows/create-milestone-job.md +177 -0
  203. package/deliver-great-systems/workflows/develop-idea.md +544 -0
  204. package/deliver-great-systems/workflows/diagnose-issues.md +231 -0
  205. package/deliver-great-systems/workflows/discovery-phase.md +301 -0
  206. package/deliver-great-systems/workflows/discuss-idea.md +263 -0
  207. package/deliver-great-systems/workflows/discuss-phase.md +733 -0
  208. package/deliver-great-systems/workflows/execute-phase.md +571 -0
  209. package/deliver-great-systems/workflows/execute-plan.md +592 -0
  210. package/deliver-great-systems/workflows/find-related-ideas.md +271 -0
  211. package/deliver-great-systems/workflows/health.md +173 -0
  212. package/deliver-great-systems/workflows/help.md +997 -0
  213. package/deliver-great-systems/workflows/import-spec.md +381 -0
  214. package/deliver-great-systems/workflows/init-product.md +767 -0
  215. package/deliver-great-systems/workflows/insert-phase.md +138 -0
  216. package/deliver-great-systems/workflows/list-docs.md +119 -0
  217. package/deliver-great-systems/workflows/list-ideas.md +154 -0
  218. package/deliver-great-systems/workflows/list-jobs.md +89 -0
  219. package/deliver-great-systems/workflows/list-phase-assumptions.md +192 -0
  220. package/deliver-great-systems/workflows/list-specs.md +101 -0
  221. package/deliver-great-systems/workflows/map-codebase.md +621 -0
  222. package/deliver-great-systems/workflows/new-milestone.md +591 -0
  223. package/deliver-great-systems/workflows/new-project.md +1113 -0
  224. package/deliver-great-systems/workflows/node-repair.md +94 -0
  225. package/deliver-great-systems/workflows/overlap-check.md +86 -0
  226. package/deliver-great-systems/workflows/pause-work.md +134 -0
  227. package/deliver-great-systems/workflows/plan-milestone-gaps.md +306 -0
  228. package/deliver-great-systems/workflows/plan-phase.md +698 -0
  229. package/deliver-great-systems/workflows/progress.md +386 -0
  230. package/deliver-great-systems/workflows/quick.md +845 -0
  231. package/deliver-great-systems/workflows/refine-spec.md +275 -0
  232. package/deliver-great-systems/workflows/reject-idea.md +109 -0
  233. package/deliver-great-systems/workflows/remove-doc.md +117 -0
  234. package/deliver-great-systems/workflows/remove-phase.md +163 -0
  235. package/deliver-great-systems/workflows/research-idea.md +325 -0
  236. package/deliver-great-systems/workflows/research-phase.md +81 -0
  237. package/deliver-great-systems/workflows/restore-idea.md +101 -0
  238. package/deliver-great-systems/workflows/resume-project.md +311 -0
  239. package/deliver-great-systems/workflows/rollback-job.md +130 -0
  240. package/deliver-great-systems/workflows/run-job.md +498 -0
  241. package/deliver-great-systems/workflows/search.md +130 -0
  242. package/deliver-great-systems/workflows/set-profile.md +83 -0
  243. package/deliver-great-systems/workflows/settings.md +470 -0
  244. package/deliver-great-systems/workflows/transition.md +563 -0
  245. package/deliver-great-systems/workflows/undo-consolidation.md +155 -0
  246. package/deliver-great-systems/workflows/update-idea.md +157 -0
  247. package/deliver-great-systems/workflows/update.md +242 -0
  248. package/deliver-great-systems/workflows/validate-phase.md +177 -0
  249. package/deliver-great-systems/workflows/verify-phase.md +253 -0
  250. package/deliver-great-systems/workflows/verify-work.md +671 -0
  251. package/deliver-great-systems/workflows/write-spec.md +450 -0
  252. package/hooks/dist/dgs-check-update.js +62 -0
  253. package/hooks/dist/dgs-context-monitor.js +141 -0
  254. package/hooks/dist/dgs-statusline.js +115 -0
  255. package/package.json +60 -0
  256. package/scripts/build-hooks.js +43 -0
@@ -0,0 +1,570 @@
1
+ /**
2
+ * Search — Fuzzy keyword search across ideas, specs, docs, and projects
3
+ *
4
+ * Provides the core search engine for cross-content discovery:
5
+ * fuzzy matching, content scanning, scope/tag filtering, result grouping.
6
+ * Used by the /dgs:search command and write-spec auto-search.
7
+ * Search is read-only — no files are created or modified.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { safeReadFile, output, error } = require('./core.cjs');
13
+ const { getPlanningRoot } = require('./paths.cjs');
14
+
15
+ // ─── Fuzzy Matching Engine ──────────────────────────────────────────────────
16
+
17
+ /**
18
+ * Compute Levenshtein distance between two strings.
19
+ * Standard dynamic programming implementation.
20
+ *
21
+ * @param {string} a - First string
22
+ * @param {string} b - Second string
23
+ * @returns {number} Edit distance
24
+ */
25
+ function levenshteinDistance(a, b) {
26
+ const m = a.length;
27
+ const n = b.length;
28
+
29
+ // Quick exits
30
+ if (m === 0) return n;
31
+ if (n === 0) return m;
32
+
33
+ // Use single-row optimization
34
+ let prev = new Array(n + 1);
35
+ let curr = new Array(n + 1);
36
+
37
+ for (let j = 0; j <= n; j++) {
38
+ prev[j] = j;
39
+ }
40
+
41
+ for (let i = 1; i <= m; i++) {
42
+ curr[0] = i;
43
+ for (let j = 1; j <= n; j++) {
44
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
45
+ curr[j] = Math.min(
46
+ prev[j] + 1, // deletion
47
+ curr[j - 1] + 1, // insertion
48
+ prev[j - 1] + cost // substitution
49
+ );
50
+ }
51
+ [prev, curr] = [curr, prev];
52
+ }
53
+
54
+ return prev[n];
55
+ }
56
+
57
+ /**
58
+ * Check if a query word fuzzy-matches a content word.
59
+ * Uses Levenshtein distance with adaptive threshold:
60
+ * - Words <= 4 chars: distance <= 1
61
+ * - Longer words: distance <= 2
62
+ * Also checks substring containment for partial matches.
63
+ *
64
+ * @param {string} text - Content word (lowercase)
65
+ * @param {string} word - Query word (lowercase)
66
+ * @returns {boolean}
67
+ */
68
+ function fuzzyMatch(text, word) {
69
+ // Exact match
70
+ if (text === word) return true;
71
+
72
+ // Substring containment (text contains the query word)
73
+ if (text.includes(word)) return true;
74
+
75
+ // Levenshtein distance with adaptive threshold
76
+ const threshold = word.length <= 4 ? 1 : 2;
77
+ const dist = levenshteinDistance(text, word);
78
+ return dist <= threshold;
79
+ }
80
+
81
+ /**
82
+ * Check if ALL query words appear in the content (AND logic).
83
+ * Each query word is checked via fuzzyMatch against every word in the content.
84
+ * Case-insensitive.
85
+ *
86
+ * @param {string} content - Full text content to search
87
+ * @param {string[]} queryWords - Array of lowercase query words
88
+ * @returns {boolean}
89
+ */
90
+ function matchesQuery(content, queryWords) {
91
+ if (!content || queryWords.length === 0) return false;
92
+
93
+ const contentLower = content.toLowerCase();
94
+ const contentWords = contentLower.split(/\s+/).filter(w => w.length > 0);
95
+
96
+ for (const qWord of queryWords) {
97
+ let found = false;
98
+ for (const cWord of contentWords) {
99
+ if (fuzzyMatch(cWord, qWord)) {
100
+ found = true;
101
+ break;
102
+ }
103
+ }
104
+ if (!found) return false;
105
+ }
106
+
107
+ return true;
108
+ }
109
+
110
+ // ─── Frontmatter Parsing ────────────────────────────────────────────────────
111
+
112
+ /**
113
+ * Parse simple YAML frontmatter from file content.
114
+ * Reusable across content types.
115
+ *
116
+ * @param {string} content - File content
117
+ * @returns {{ frontmatter: object, body: string }}
118
+ */
119
+ function parseFrontmatter(content) {
120
+ const match = content.match(/^---\n([\s\S]+?)\n---\n?([\s\S]*)/);
121
+ if (!match) return { frontmatter: {}, body: content };
122
+
123
+ const yaml = match[1];
124
+ const body = match[2] || '';
125
+ const frontmatter = {};
126
+
127
+ const lines = yaml.split('\n');
128
+ for (const line of lines) {
129
+ if (line.trim() === '') continue;
130
+
131
+ // Handle inline array values
132
+ const arrayMatch = line.match(/^([\w_]+):\s*\[([^\]]*)\]/);
133
+ if (arrayMatch) {
134
+ const key = arrayMatch[1];
135
+ const values = arrayMatch[2]
136
+ .split(',')
137
+ .map(v => v.trim().replace(/^["']|["']$/g, ''))
138
+ .filter(v => v !== '');
139
+ frontmatter[key] = values;
140
+ continue;
141
+ }
142
+
143
+ // Handle simple key: value
144
+ const kvMatch = line.match(/^([\w_]+):\s*(.*)/);
145
+ if (kvMatch) {
146
+ const key = kvMatch[1];
147
+ let value = kvMatch[2].trim();
148
+ value = value.replace(/^["']|["']$/g, '');
149
+ if (key === 'id' && /^\d+$/.test(value)) {
150
+ frontmatter[key] = parseInt(value, 10);
151
+ } else {
152
+ frontmatter[key] = value;
153
+ }
154
+ }
155
+ }
156
+
157
+ return { frontmatter, body };
158
+ }
159
+
160
+ // ─── Content Scanners ───────────────────────────────────────────────────────
161
+
162
+ /**
163
+ * Scan ideas directories for searchable idea files.
164
+ * Scans .planning/ideas/{pending,done,rejected}/ for .md files.
165
+ *
166
+ * @param {string} cwd - Working directory
167
+ * @param {object} options - { include_rejected, tags }
168
+ * @returns {Array<{ type: string, id: number, title: string, filePath: string, state: string, tags: string[], author: string, content: string }>}
169
+ */
170
+ function scanIdeas(cwd, options) {
171
+ const results = [];
172
+ const planRoot = getPlanningRoot(cwd);
173
+ const planRootRel = path.relative(cwd, planRoot) || '.';
174
+ const states = options.include_rejected
175
+ ? ['pending', 'done', 'rejected']
176
+ : ['pending', 'done'];
177
+
178
+ for (const state of states) {
179
+ const dir = path.join(planRoot, 'ideas', state);
180
+ let files;
181
+ try {
182
+ files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
183
+ } catch {
184
+ continue;
185
+ }
186
+
187
+ for (const file of files) {
188
+ const filePath = path.join(dir, file);
189
+ const content = safeReadFile(filePath);
190
+ if (!content) continue;
191
+
192
+ const { frontmatter, body } = parseFrontmatter(content);
193
+ const tags = frontmatter.tags || [];
194
+
195
+ // Tag filter: if tags option set, idea must have at least one matching tag (OR logic)
196
+ if (options.tags) {
197
+ const filterTags = options.tags.split(',').map(t => t.trim().toLowerCase());
198
+ const ideaTags = tags.map(t => t.toLowerCase());
199
+ const hasMatch = filterTags.some(ft => ideaTags.includes(ft));
200
+ if (!hasMatch) continue;
201
+ }
202
+
203
+ results.push({
204
+ type: 'idea',
205
+ id: frontmatter.id,
206
+ title: frontmatter.title || file,
207
+ filePath: path.join(planRootRel, 'ideas', state, file),
208
+ state,
209
+ tags,
210
+ author: frontmatter.author || '',
211
+ content: (frontmatter.title || '') + ' ' + body,
212
+ });
213
+ }
214
+ }
215
+
216
+ return results;
217
+ }
218
+
219
+ /**
220
+ * Scan specs directory for searchable spec files.
221
+ * Scans .planning/specs/ for .md files.
222
+ *
223
+ * @param {string} cwd - Working directory
224
+ * @returns {Array<{ type: string, id: string, title: string, filePath: string, status: string, author: string, content: string }>}
225
+ */
226
+ function scanSpecs(cwd) {
227
+ const results = [];
228
+ const planRoot = getPlanningRoot(cwd);
229
+ const planRootRel = path.relative(cwd, planRoot) || '.';
230
+ const dir = path.join(planRoot, 'specs');
231
+ let files;
232
+ try {
233
+ files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
234
+ } catch {
235
+ return results;
236
+ }
237
+
238
+ for (const file of files) {
239
+ const filePath = path.join(dir, file);
240
+ const content = safeReadFile(filePath);
241
+ if (!content) continue;
242
+
243
+ const { frontmatter, body } = parseFrontmatter(content);
244
+
245
+ results.push({
246
+ type: 'spec',
247
+ id: frontmatter.id || file,
248
+ title: frontmatter.title || file,
249
+ filePath: path.join(planRootRel, 'specs', file),
250
+ status: frontmatter.status || 'draft',
251
+ author: frontmatter.author || '',
252
+ content: (frontmatter.title || '') + ' ' + body,
253
+ });
254
+ }
255
+
256
+ return results;
257
+ }
258
+
259
+ /**
260
+ * Scan docs directories for searchable documents.
261
+ * Scans .planning/docs/ (product), .planning/ideas/star/docs/ (idea-scoped),
262
+ * .planning/specs/star/docs/ (spec-scoped).
263
+ * Uses .extracted.txt sidecars when available.
264
+ *
265
+ * @param {string} cwd - Working directory
266
+ * @returns {Array<{ type: string, title: string, filePath: string, scope: string, content: string }>}
267
+ */
268
+ function scanDocs(cwd) {
269
+ const results = [];
270
+
271
+ /**
272
+ * Scan a single docs directory and add results.
273
+ */
274
+ function scanDocsDir(docsDir, scope, relBase) {
275
+ let files;
276
+ try {
277
+ files = fs.readdirSync(docsDir).filter(f =>
278
+ f !== 'INDEX.md' &&
279
+ !f.endsWith('.extracted.txt') &&
280
+ !f.endsWith('.summary.txt') &&
281
+ !f.startsWith('.')
282
+ );
283
+ } catch {
284
+ return;
285
+ }
286
+
287
+ // Load .names.json for original names
288
+ let nameMap = {};
289
+ const namesPath = path.join(docsDir, '.names.json');
290
+ const namesContent = safeReadFile(namesPath);
291
+ if (namesContent) {
292
+ try { nameMap = JSON.parse(namesContent); } catch { /* ignore */ }
293
+ }
294
+
295
+ for (const file of files) {
296
+ const filePath = path.join(docsDir, file);
297
+ const relPath = path.join(relBase, file);
298
+ const title = nameMap[file] || file;
299
+
300
+ // Check for extracted text sidecar
301
+ const extractedPath = filePath + '.extracted.txt';
302
+ let searchContent;
303
+
304
+ if (fs.existsSync(extractedPath)) {
305
+ searchContent = safeReadFile(extractedPath) || '';
306
+ } else {
307
+ // Only use file content for text-like files
308
+ const ext = path.extname(file).toLowerCase();
309
+ if (ext === '.md' || ext === '.txt') {
310
+ searchContent = safeReadFile(filePath) || '';
311
+ } else {
312
+ searchContent = title; // Fall back to filename/title only
313
+ }
314
+ }
315
+
316
+ results.push({
317
+ type: 'doc',
318
+ title,
319
+ filePath: relPath,
320
+ scope,
321
+ content: title + ' ' + searchContent,
322
+ });
323
+ }
324
+ }
325
+
326
+ const planRoot = getPlanningRoot(cwd);
327
+ const planRootRel = path.relative(cwd, planRoot) || '.';
328
+
329
+ // Product docs
330
+ const productDocsDir = path.join(planRoot, 'docs');
331
+ scanDocsDir(productDocsDir, 'product', path.join(planRootRel, 'docs'));
332
+
333
+ // Idea-scoped docs
334
+ const ideaStates = ['pending', 'done', 'rejected'];
335
+ for (const state of ideaStates) {
336
+ const stateDir = path.join(planRoot, 'ideas', state);
337
+ let ideaDirs;
338
+ try {
339
+ ideaDirs = fs.readdirSync(stateDir, { withFileTypes: true })
340
+ .filter(e => e.isDirectory())
341
+ .map(e => e.name);
342
+ } catch {
343
+ continue;
344
+ }
345
+ for (const ideaSlug of ideaDirs) {
346
+ const docsDir = path.join(stateDir, ideaSlug, 'docs');
347
+ scanDocsDir(docsDir, `idea:${ideaSlug}`, path.join(planRootRel, 'ideas', state, ideaSlug, 'docs'));
348
+ }
349
+ }
350
+
351
+ // Spec-scoped docs
352
+ const specsDir = path.join(planRoot, 'specs');
353
+ let specDirs;
354
+ try {
355
+ specDirs = fs.readdirSync(specsDir, { withFileTypes: true })
356
+ .filter(e => e.isDirectory())
357
+ .map(e => e.name);
358
+ } catch {
359
+ specDirs = [];
360
+ }
361
+ for (const specSlug of specDirs) {
362
+ const docsDir = path.join(specsDir, specSlug, 'docs');
363
+ scanDocsDir(docsDir, `spec:${specSlug}`, path.join(planRootRel, 'specs', specSlug, 'docs'));
364
+ }
365
+
366
+ return results;
367
+ }
368
+
369
+ /**
370
+ * Scan for project directories (those containing STATE.md).
371
+ * Reads STATE.md, ROADMAP.md, and REQUIREMENTS.md as searchable content.
372
+ *
373
+ * @param {string} cwd - Working directory
374
+ * @returns {Array<{ type: string, title: string, filePath: string, content: string }>}
375
+ */
376
+ function scanProjects(cwd) {
377
+ const results = [];
378
+ const planningDir = getPlanningRoot(cwd);
379
+ const planRootRel = path.relative(cwd, planningDir) || '.';
380
+ let entries;
381
+ try {
382
+ entries = fs.readdirSync(planningDir, { withFileTypes: true })
383
+ .filter(e => e.isDirectory());
384
+ } catch {
385
+ return results;
386
+ }
387
+
388
+ for (const entry of entries) {
389
+ const dirPath = path.join(planningDir, entry.name);
390
+ const statePath = path.join(dirPath, 'STATE.md');
391
+
392
+ if (!fs.existsSync(statePath)) continue;
393
+
394
+ let content = '';
395
+
396
+ // Read STATE.md
397
+ const stateContent = safeReadFile(statePath);
398
+ if (stateContent) content += stateContent + '\n';
399
+
400
+ // Read ROADMAP.md if exists
401
+ const roadmapPath = path.join(dirPath, 'ROADMAP.md');
402
+ const roadmapContent = safeReadFile(roadmapPath);
403
+ if (roadmapContent) content += roadmapContent + '\n';
404
+
405
+ // Read REQUIREMENTS.md if exists
406
+ const reqsPath = path.join(dirPath, 'REQUIREMENTS.md');
407
+ const reqsContent = safeReadFile(reqsPath);
408
+ if (reqsContent) content += reqsContent + '\n';
409
+
410
+ results.push({
411
+ type: 'project',
412
+ title: entry.name,
413
+ filePath: path.join(planRootRel, entry.name),
414
+ content: entry.name + ' ' + content,
415
+ });
416
+ }
417
+
418
+ return results;
419
+ }
420
+
421
+ // ─── Context Snippet Extraction ─────────────────────────────────────────────
422
+
423
+ /**
424
+ * Extract a context snippet from content around the first matching query word.
425
+ * Returns the match line plus 1 line above and 1 line below (3-line snippet).
426
+ *
427
+ * @param {string} content - Full text content
428
+ * @param {string[]} queryWords - Lowercase query words
429
+ * @returns {string} Context snippet (trimmed)
430
+ */
431
+ function extractSnippet(content, queryWords) {
432
+ if (!content) return '';
433
+
434
+ const lines = content.split('\n');
435
+
436
+ for (let i = 0; i < lines.length; i++) {
437
+ const lineLower = lines[i].toLowerCase();
438
+ const lineWords = lineLower.split(/\s+/);
439
+
440
+ for (const qWord of queryWords) {
441
+ for (const lWord of lineWords) {
442
+ if (fuzzyMatch(lWord, qWord)) {
443
+ const start = Math.max(0, i - 1);
444
+ const end = Math.min(lines.length - 1, i + 1);
445
+ const snippet = lines.slice(start, end + 1).join('\n').trim();
446
+ // Trim to reasonable length
447
+ return snippet.length > 300 ? snippet.substring(0, 300) + '...' : snippet;
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ // Fallback: first 3 lines
454
+ return lines.slice(0, 3).join('\n').trim().substring(0, 300);
455
+ }
456
+
457
+ // ─── Main Search Function ───────────────────────────────────────────────────
458
+
459
+ /**
460
+ * Search across ideas, specs, docs, and projects.
461
+ *
462
+ * @param {string} cwd - Working directory
463
+ * @param {string} query - Search query string
464
+ * @param {object} options - { ideas_only, specs_only, docs_only, include_rejected, tags, max_per_type }
465
+ * @param {boolean} raw - Raw output mode
466
+ */
467
+ function cmdSearch(cwd, query, options, raw) {
468
+ if (!query || query.trim() === '') {
469
+ error('Search query required');
470
+ }
471
+
472
+ const queryWords = query.toLowerCase().split(/\s+/).filter(w => w.length > 0);
473
+ if (queryWords.length === 0) {
474
+ error('Search query required');
475
+ }
476
+
477
+ const opts = options || {};
478
+ const maxPerType = parseInt(opts.max_per_type, 10) || 5;
479
+
480
+ // Determine which types to scan based on scope filters
481
+ const hasTypeFilter = opts.ideas_only || opts.specs_only || opts.docs_only;
482
+ const scanTypes = {
483
+ ideas: hasTypeFilter ? !!opts.ideas_only : true,
484
+ specs: hasTypeFilter ? !!opts.specs_only : true,
485
+ docs: hasTypeFilter ? !!opts.docs_only : true,
486
+ projects: hasTypeFilter ? false : true, // projects only when no type filter
487
+ };
488
+
489
+ const groups = [];
490
+ let totalMatches = 0;
491
+
492
+ // Fixed order: ideas, specs, docs, projects
493
+ const typeOrder = ['ideas', 'specs', 'docs', 'projects'];
494
+
495
+ for (const typeName of typeOrder) {
496
+ if (!scanTypes[typeName]) continue;
497
+
498
+ let items;
499
+ if (typeName === 'ideas') {
500
+ items = scanIdeas(cwd, {
501
+ include_rejected: opts.include_rejected,
502
+ tags: opts.tags,
503
+ });
504
+ } else if (typeName === 'specs') {
505
+ items = scanSpecs(cwd);
506
+ } else if (typeName === 'docs') {
507
+ items = scanDocs(cwd);
508
+ } else if (typeName === 'projects') {
509
+ items = scanProjects(cwd);
510
+ }
511
+
512
+ // Filter to matching items
513
+ const matches = items.filter(item => matchesQuery(item.content, queryWords));
514
+ const total = matches.length;
515
+
516
+ if (total === 0) continue;
517
+
518
+ const shown = Math.min(total, maxPerType);
519
+ const overflow = total - shown;
520
+
521
+ const results = matches.slice(0, maxPerType).map(item => {
522
+ const result = {
523
+ title: item.title,
524
+ path: item.filePath,
525
+ snippet: extractSnippet(item.content, queryWords),
526
+ };
527
+
528
+ // Add type-specific fields
529
+ if (typeName === 'ideas') {
530
+ result.id = item.id;
531
+ result.state = item.state;
532
+ result.tags = item.tags;
533
+ result.author = item.author;
534
+ } else if (typeName === 'specs') {
535
+ result.id = item.id;
536
+ result.status = item.status;
537
+ result.author = item.author;
538
+ } else if (typeName === 'docs') {
539
+ result.scope = item.scope;
540
+ }
541
+
542
+ return result;
543
+ });
544
+
545
+ groups.push({
546
+ type: typeName,
547
+ results,
548
+ total,
549
+ shown,
550
+ overflow,
551
+ });
552
+
553
+ totalMatches += total;
554
+ }
555
+
556
+ output({
557
+ query,
558
+ groups,
559
+ total_matches: totalMatches,
560
+ }, raw);
561
+ }
562
+
563
+ // ─── Exports ──────────────────────────────────────────────────────────────────
564
+
565
+ module.exports = {
566
+ cmdSearch,
567
+ fuzzyMatch,
568
+ levenshteinDistance,
569
+ matchesQuery,
570
+ };