@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.
- package/LICENSE +38 -0
- package/README.md +851 -0
- package/agents/dgs-codebase-cross-analyzer.md +183 -0
- package/agents/dgs-codebase-mapper.md +782 -0
- package/agents/dgs-codebase-synthesizer.md +156 -0
- package/agents/dgs-debugger.md +1256 -0
- package/agents/dgs-executor.md +550 -0
- package/agents/dgs-integration-checker.md +481 -0
- package/agents/dgs-nyquist-auditor.md +178 -0
- package/agents/dgs-phase-researcher.md +563 -0
- package/agents/dgs-phase-verifier.md +450 -0
- package/agents/dgs-plan-checker.md +708 -0
- package/agents/dgs-planner.md +1324 -0
- package/agents/dgs-project-researcher.md +631 -0
- package/agents/dgs-research-synthesizer.md +249 -0
- package/agents/dgs-roadmapper.md +652 -0
- package/agents/dgs-verifier.md +607 -0
- package/bin/install.js +2073 -0
- package/commands/dgs/add-doc.md +45 -0
- package/commands/dgs/add-idea.md +38 -0
- package/commands/dgs/add-phase.md +43 -0
- package/commands/dgs/add-repo.md +54 -0
- package/commands/dgs/add-tests.md +41 -0
- package/commands/dgs/add-todo.md +47 -0
- package/commands/dgs/approve-spec.md +38 -0
- package/commands/dgs/audit-milestone.md +36 -0
- package/commands/dgs/audit-phase.md +37 -0
- package/commands/dgs/cancel-job.md +23 -0
- package/commands/dgs/capture-principle.md +143 -0
- package/commands/dgs/check-todos.md +45 -0
- package/commands/dgs/cleanup.md +18 -0
- package/commands/dgs/complete-milestone.md +136 -0
- package/commands/dgs/complete-project.md +70 -0
- package/commands/dgs/consolidate-ideas.md +50 -0
- package/commands/dgs/create-milestone-job.md +37 -0
- package/commands/dgs/debug.md +164 -0
- package/commands/dgs/develop-idea.md +53 -0
- package/commands/dgs/discuss-idea.md +41 -0
- package/commands/dgs/discuss-phase.md +83 -0
- package/commands/dgs/execute-phase.md +41 -0
- package/commands/dgs/fast.md +38 -0
- package/commands/dgs/find-related-ideas.md +43 -0
- package/commands/dgs/health.md +28 -0
- package/commands/dgs/help.md +22 -0
- package/commands/dgs/import-spec.md +36 -0
- package/commands/dgs/init-product.md +28 -0
- package/commands/dgs/insert-phase.md +32 -0
- package/commands/dgs/join-discord.md +18 -0
- package/commands/dgs/list-docs.md +40 -0
- package/commands/dgs/list-ideas.md +42 -0
- package/commands/dgs/list-jobs.md +22 -0
- package/commands/dgs/list-phase-assumptions.md +46 -0
- package/commands/dgs/list-projects.md +57 -0
- package/commands/dgs/list-specs.md +40 -0
- package/commands/dgs/map-codebase.md +92 -0
- package/commands/dgs/new-milestone.md +44 -0
- package/commands/dgs/new-project.md +42 -0
- package/commands/dgs/node-repair.md +26 -0
- package/commands/dgs/overlap-check.md +20 -0
- package/commands/dgs/pause-work.md +38 -0
- package/commands/dgs/plan-milestone-gaps.md +34 -0
- package/commands/dgs/plan-phase.md +44 -0
- package/commands/dgs/progress.md +24 -0
- package/commands/dgs/quick.md +41 -0
- package/commands/dgs/reactivate-project.md +70 -0
- package/commands/dgs/reapply-patches.md +110 -0
- package/commands/dgs/refine-spec.md +38 -0
- package/commands/dgs/reject-idea.md +43 -0
- package/commands/dgs/remove-doc.md +44 -0
- package/commands/dgs/remove-phase.md +31 -0
- package/commands/dgs/remove-repo.md +69 -0
- package/commands/dgs/research-idea.md +43 -0
- package/commands/dgs/research-phase.md +189 -0
- package/commands/dgs/restore-idea.md +45 -0
- package/commands/dgs/resume-work.md +40 -0
- package/commands/dgs/rollback-job.md +24 -0
- package/commands/dgs/run-job.md +35 -0
- package/commands/dgs/search.md +40 -0
- package/commands/dgs/set-profile.md +34 -0
- package/commands/dgs/settings.md +38 -0
- package/commands/dgs/switch-project.md +58 -0
- package/commands/dgs/undo-consolidation.md +42 -0
- package/commands/dgs/update-idea.md +44 -0
- package/commands/dgs/update.md +37 -0
- package/commands/dgs/validate-phase.md +35 -0
- package/commands/dgs/verify-work.md +39 -0
- package/commands/dgs/write-spec.md +49 -0
- package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-01-SUMMARY.md +84 -0
- package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-02-SUMMARY.md +86 -0
- package/deliver-great-systems/.planning/phases/10-v1-to-v2-migration-flow/10-01-SUMMARY.md +85 -0
- package/deliver-great-systems/bin/dgs-tools.cjs +1444 -0
- package/deliver-great-systems/bin/lib/auto-test.cjs +1365 -0
- package/deliver-great-systems/bin/lib/commands.cjs +570 -0
- package/deliver-great-systems/bin/lib/config.cjs +417 -0
- package/deliver-great-systems/bin/lib/conflict-agent.cjs +1063 -0
- package/deliver-great-systems/bin/lib/conflict-agent.test.cjs +554 -0
- package/deliver-great-systems/bin/lib/context.cjs +929 -0
- package/deliver-great-systems/bin/lib/context.test.cjs +693 -0
- package/deliver-great-systems/bin/lib/core.cjs +744 -0
- package/deliver-great-systems/bin/lib/core.test.cjs +822 -0
- package/deliver-great-systems/bin/lib/docs.cjs +919 -0
- package/deliver-great-systems/bin/lib/docs.test.cjs +211 -0
- package/deliver-great-systems/bin/lib/execution.cjs +705 -0
- package/deliver-great-systems/bin/lib/execution.test.cjs +1472 -0
- package/deliver-great-systems/bin/lib/frontmatter.cjs +324 -0
- package/deliver-great-systems/bin/lib/ideas.cjs +1406 -0
- package/deliver-great-systems/bin/lib/ideas.test.cjs +1417 -0
- package/deliver-great-systems/bin/lib/identity.cjs +125 -0
- package/deliver-great-systems/bin/lib/init.cjs +1114 -0
- package/deliver-great-systems/bin/lib/init.test.cjs +1271 -0
- package/deliver-great-systems/bin/lib/jobs.cjs +2015 -0
- package/deliver-great-systems/bin/lib/jobs.test.cjs +2619 -0
- package/deliver-great-systems/bin/lib/merge-conflicts.cjs +654 -0
- package/deliver-great-systems/bin/lib/merge-conflicts.test.cjs +370 -0
- package/deliver-great-systems/bin/lib/migration.cjs +352 -0
- package/deliver-great-systems/bin/lib/migration.test.cjs +582 -0
- package/deliver-great-systems/bin/lib/milestone.cjs +243 -0
- package/deliver-great-systems/bin/lib/overlap.cjs +437 -0
- package/deliver-great-systems/bin/lib/overlap.test.cjs +747 -0
- package/deliver-great-systems/bin/lib/path-audit.test.cjs +384 -0
- package/deliver-great-systems/bin/lib/paths.cjs +144 -0
- package/deliver-great-systems/bin/lib/paths.test.cjs +486 -0
- package/deliver-great-systems/bin/lib/phase.cjs +910 -0
- package/deliver-great-systems/bin/lib/projects.cjs +691 -0
- package/deliver-great-systems/bin/lib/projects.test.cjs +871 -0
- package/deliver-great-systems/bin/lib/repos.cjs +1432 -0
- package/deliver-great-systems/bin/lib/repos.test.cjs +1882 -0
- package/deliver-great-systems/bin/lib/roadmap.cjs +305 -0
- package/deliver-great-systems/bin/lib/search.cjs +570 -0
- package/deliver-great-systems/bin/lib/specs.cjs +1303 -0
- package/deliver-great-systems/bin/lib/state.cjs +893 -0
- package/deliver-great-systems/bin/lib/template.cjs +228 -0
- package/deliver-great-systems/bin/lib/test-helpers.cjs +291 -0
- package/deliver-great-systems/bin/lib/verify.cjs +796 -0
- package/deliver-great-systems/references/checkpoints.md +776 -0
- package/deliver-great-systems/references/conflict-resolution.md +66 -0
- package/deliver-great-systems/references/context-tiers.md +166 -0
- package/deliver-great-systems/references/continuation-format.md +249 -0
- package/deliver-great-systems/references/decimal-phase-calculation.md +67 -0
- package/deliver-great-systems/references/git-integration.md +250 -0
- package/deliver-great-systems/references/git-planning-commit.md +40 -0
- package/deliver-great-systems/references/model-profile-resolution.md +36 -0
- package/deliver-great-systems/references/model-profiles.md +95 -0
- package/deliver-great-systems/references/phase-argument-parsing.md +61 -0
- package/deliver-great-systems/references/planning-config.md +224 -0
- package/deliver-great-systems/references/questioning.md +162 -0
- package/deliver-great-systems/references/spec-review-loop.md +177 -0
- package/deliver-great-systems/references/tdd.md +265 -0
- package/deliver-great-systems/references/ui-brand.md +160 -0
- package/deliver-great-systems/references/verification-patterns.md +612 -0
- package/deliver-great-systems/templates/DEBUG.md +166 -0
- package/deliver-great-systems/templates/UAT.md +251 -0
- package/deliver-great-systems/templates/VALIDATION.md +95 -0
- package/deliver-great-systems/templates/claude-md.md +74 -0
- package/deliver-great-systems/templates/codebase/architecture.md +257 -0
- package/deliver-great-systems/templates/codebase/concerns.md +312 -0
- package/deliver-great-systems/templates/codebase/conventions.md +309 -0
- package/deliver-great-systems/templates/codebase/integrations.md +282 -0
- package/deliver-great-systems/templates/codebase/stack.md +188 -0
- package/deliver-great-systems/templates/codebase/structure.md +287 -0
- package/deliver-great-systems/templates/codebase/testing.md +482 -0
- package/deliver-great-systems/templates/config.json +38 -0
- package/deliver-great-systems/templates/context.md +354 -0
- package/deliver-great-systems/templates/continue-here.md +80 -0
- package/deliver-great-systems/templates/debug-subagent-prompt.md +93 -0
- package/deliver-great-systems/templates/discovery.md +148 -0
- package/deliver-great-systems/templates/milestone-archive.md +125 -0
- package/deliver-great-systems/templates/milestone.md +117 -0
- package/deliver-great-systems/templates/phase-prompt.md +615 -0
- package/deliver-great-systems/templates/planner-subagent-prompt.md +119 -0
- package/deliver-great-systems/templates/project.md +186 -0
- package/deliver-great-systems/templates/requirements.md +233 -0
- package/deliver-great-systems/templates/research-project/ARCHITECTURE.md +206 -0
- package/deliver-great-systems/templates/research-project/FEATURES.md +149 -0
- package/deliver-great-systems/templates/research-project/PITFALLS.md +202 -0
- package/deliver-great-systems/templates/research-project/STACK.md +122 -0
- package/deliver-great-systems/templates/research-project/SUMMARY.md +172 -0
- package/deliver-great-systems/templates/research.md +554 -0
- package/deliver-great-systems/templates/retrospective.md +54 -0
- package/deliver-great-systems/templates/roadmap.md +204 -0
- package/deliver-great-systems/templates/state.md +178 -0
- package/deliver-great-systems/templates/summary-complex.md +59 -0
- package/deliver-great-systems/templates/summary-minimal.md +41 -0
- package/deliver-great-systems/templates/summary-standard.md +48 -0
- package/deliver-great-systems/templates/summary.md +253 -0
- package/deliver-great-systems/templates/user-setup.md +313 -0
- package/deliver-great-systems/templates/verification-report.md +324 -0
- package/deliver-great-systems/workflows/add-doc.md +151 -0
- package/deliver-great-systems/workflows/add-idea.md +96 -0
- package/deliver-great-systems/workflows/add-phase.md +120 -0
- package/deliver-great-systems/workflows/add-tests.md +359 -0
- package/deliver-great-systems/workflows/add-todo.md +162 -0
- package/deliver-great-systems/workflows/approve-spec.md +194 -0
- package/deliver-great-systems/workflows/audit-milestone.md +364 -0
- package/deliver-great-systems/workflows/audit-phase.md +462 -0
- package/deliver-great-systems/workflows/cancel-job.md +108 -0
- package/deliver-great-systems/workflows/check-todos.md +181 -0
- package/deliver-great-systems/workflows/cleanup.md +247 -0
- package/deliver-great-systems/workflows/codereview.md +526 -0
- package/deliver-great-systems/workflows/complete-milestone.md +1298 -0
- package/deliver-great-systems/workflows/consolidate-ideas.md +365 -0
- package/deliver-great-systems/workflows/create-milestone-job.md +177 -0
- package/deliver-great-systems/workflows/develop-idea.md +544 -0
- package/deliver-great-systems/workflows/diagnose-issues.md +231 -0
- package/deliver-great-systems/workflows/discovery-phase.md +301 -0
- package/deliver-great-systems/workflows/discuss-idea.md +263 -0
- package/deliver-great-systems/workflows/discuss-phase.md +733 -0
- package/deliver-great-systems/workflows/execute-phase.md +571 -0
- package/deliver-great-systems/workflows/execute-plan.md +592 -0
- package/deliver-great-systems/workflows/find-related-ideas.md +271 -0
- package/deliver-great-systems/workflows/health.md +173 -0
- package/deliver-great-systems/workflows/help.md +997 -0
- package/deliver-great-systems/workflows/import-spec.md +381 -0
- package/deliver-great-systems/workflows/init-product.md +767 -0
- package/deliver-great-systems/workflows/insert-phase.md +138 -0
- package/deliver-great-systems/workflows/list-docs.md +119 -0
- package/deliver-great-systems/workflows/list-ideas.md +154 -0
- package/deliver-great-systems/workflows/list-jobs.md +89 -0
- package/deliver-great-systems/workflows/list-phase-assumptions.md +192 -0
- package/deliver-great-systems/workflows/list-specs.md +101 -0
- package/deliver-great-systems/workflows/map-codebase.md +621 -0
- package/deliver-great-systems/workflows/new-milestone.md +591 -0
- package/deliver-great-systems/workflows/new-project.md +1113 -0
- package/deliver-great-systems/workflows/node-repair.md +94 -0
- package/deliver-great-systems/workflows/overlap-check.md +86 -0
- package/deliver-great-systems/workflows/pause-work.md +134 -0
- package/deliver-great-systems/workflows/plan-milestone-gaps.md +306 -0
- package/deliver-great-systems/workflows/plan-phase.md +698 -0
- package/deliver-great-systems/workflows/progress.md +386 -0
- package/deliver-great-systems/workflows/quick.md +845 -0
- package/deliver-great-systems/workflows/refine-spec.md +275 -0
- package/deliver-great-systems/workflows/reject-idea.md +109 -0
- package/deliver-great-systems/workflows/remove-doc.md +117 -0
- package/deliver-great-systems/workflows/remove-phase.md +163 -0
- package/deliver-great-systems/workflows/research-idea.md +325 -0
- package/deliver-great-systems/workflows/research-phase.md +81 -0
- package/deliver-great-systems/workflows/restore-idea.md +101 -0
- package/deliver-great-systems/workflows/resume-project.md +311 -0
- package/deliver-great-systems/workflows/rollback-job.md +130 -0
- package/deliver-great-systems/workflows/run-job.md +498 -0
- package/deliver-great-systems/workflows/search.md +130 -0
- package/deliver-great-systems/workflows/set-profile.md +83 -0
- package/deliver-great-systems/workflows/settings.md +470 -0
- package/deliver-great-systems/workflows/transition.md +563 -0
- package/deliver-great-systems/workflows/undo-consolidation.md +155 -0
- package/deliver-great-systems/workflows/update-idea.md +157 -0
- package/deliver-great-systems/workflows/update.md +242 -0
- package/deliver-great-systems/workflows/validate-phase.md +177 -0
- package/deliver-great-systems/workflows/verify-phase.md +253 -0
- package/deliver-great-systems/workflows/verify-work.md +671 -0
- package/deliver-great-systems/workflows/write-spec.md +450 -0
- package/hooks/dist/dgs-check-update.js +62 -0
- package/hooks/dist/dgs-context-monitor.js +141 -0
- package/hooks/dist/dgs-statusline.js +115 -0
- package/package.json +60 -0
- 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
|
+
};
|