@polymorphism-tech/morph-spec 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +75 -371
- package/LICENSE +72 -72
- package/bin/detect-agents.js +225 -225
- package/bin/render-template.js +302 -302
- package/bin/semantic-detect-agents.js +246 -246
- package/bin/validate-agents-skills.js +251 -251
- package/bin/validate-agents.js +69 -69
- package/bin/validate-phase.js +263 -263
- package/content/.azure/README.md +293 -293
- package/content/.azure/docs/azure-devops-setup.md +454 -454
- package/content/.azure/docs/branch-strategy.md +398 -398
- package/content/.azure/docs/local-development.md +515 -515
- package/content/.azure/pipelines/pipeline-variables.yml +34 -34
- package/content/.azure/pipelines/prod-pipeline.yml +319 -319
- package/content/.azure/pipelines/staging-pipeline.yml +234 -234
- package/content/.azure/pipelines/templates/build-dotnet.yml +75 -75
- package/content/.azure/pipelines/templates/deploy-app-service.yml +94 -94
- package/content/.azure/pipelines/templates/deploy-container-app.yml +120 -120
- package/content/.azure/pipelines/templates/infra-deploy.yml +90 -90
- package/content/.claude/commands/morph-archive.md +79 -79
- package/content/.claude/commands/morph-deploy.md +529 -529
- package/content/.claude/commands/morph-infra.md +209 -209
- package/content/.claude/commands/morph-preflight.md +227 -227
- package/content/.claude/commands/morph-troubleshoot.md +122 -122
- package/content/.claude/settings.local.json +15 -15
- package/content/.claude/skills/{specialists → level-2-domains/architecture}/prompt-engineer.md +189 -189
- package/content/.claude/skills/{specialists → level-2-domains/architecture}/seo-growth-hacker.md +320 -320
- package/content/.claude/skills/{infra → level-2-domains/infrastructure}/azure-deploy-specialist.md +699 -699
- package/content/.morph/.morphversion +5 -5
- package/content/.morph/archive/.gitkeep +25 -25
- package/content/.morph/config/agents.json +7 -5
- package/content/.morph/docs/STORY-DRIVEN-DEVELOPMENT.md +392 -392
- package/content/.morph/examples/api-nextjs/README.md +241 -241
- package/content/.morph/examples/api-nextjs/contracts.ts +307 -307
- package/content/.morph/examples/api-nextjs/spec.md +399 -399
- package/content/.morph/examples/api-nextjs/tasks.md +168 -168
- package/content/.morph/examples/micro-saas/README.md +125 -125
- package/content/.morph/examples/micro-saas/contracts.cs +358 -358
- package/content/.morph/examples/micro-saas/decisions.md +246 -246
- package/content/.morph/examples/micro-saas/spec.md +236 -236
- package/content/.morph/examples/micro-saas/tasks.md +150 -150
- package/content/.morph/examples/multi-agent/README.md +309 -309
- package/content/.morph/examples/multi-agent/contracts.cs +433 -433
- package/content/.morph/examples/multi-agent/spec.md +479 -479
- package/content/.morph/examples/multi-agent/tasks.md +185 -185
- package/content/.morph/examples/state-v3.json +188 -188
- package/content/.morph/features/.gitkeep +25 -25
- package/content/.morph/hooks/pre-commit-all.sh +48 -48
- package/content/.morph/hooks/pre-commit-specs.sh +49 -49
- package/content/.morph/hooks/pre-commit-tests.sh +60 -60
- package/content/.morph/project.md +160 -160
- package/content/.morph/schemas/agent.schema.json +296 -296
- package/content/.morph/specs/.gitkeep +20 -20
- package/content/.morph/standards/coding.md +377 -377
- package/content/.morph/standards/fluent-ui-setup.md +590 -590
- package/content/.morph/standards/migration-guide.md +514 -514
- package/content/.morph/standards/passkeys-auth.md +423 -423
- package/content/.morph/standards/vector-search-rag.md +536 -536
- package/content/.morph/state.json +17 -17
- package/content/.morph/templates/FluentDesignTheme.cs +149 -149
- package/content/.morph/templates/MudTheme.cs +281 -281
- package/content/.morph/templates/component.razor +239 -239
- package/content/.morph/templates/contracts.cs +217 -217
- package/content/.morph/templates/design-system.css +226 -226
- package/content/.morph/templates/infra/.dockerignore.example +89 -89
- package/content/.morph/templates/infra/Dockerfile.example +82 -82
- package/content/.morph/templates/infra/README.md +286 -286
- package/content/.morph/templates/infra/app-insights.bicep +63 -63
- package/content/.morph/templates/infra/app-service.bicep +164 -164
- package/content/.morph/templates/infra/azure-pipelines-deploy.yml +480 -480
- package/content/.morph/templates/infra/container-app-env.bicep +49 -49
- package/content/.morph/templates/infra/container-app.bicep +156 -156
- package/content/.morph/templates/infra/deploy-checklist.md +426 -426
- package/content/.morph/templates/infra/deploy.ps1 +229 -229
- package/content/.morph/templates/infra/deploy.sh +208 -208
- package/content/.morph/templates/infra/key-vault.bicep +91 -91
- package/content/.morph/templates/infra/main.bicep +189 -189
- package/content/.morph/templates/infra/parameters.dev.json +29 -29
- package/content/.morph/templates/infra/parameters.prod.json +29 -29
- package/content/.morph/templates/infra/parameters.staging.json +29 -29
- package/content/.morph/templates/infra/sql-database.bicep +103 -103
- package/content/.morph/templates/infra/storage.bicep +106 -106
- package/content/.morph/templates/integrations/asaas-client.cs +387 -387
- package/content/.morph/templates/integrations/asaas-webhook.cs +351 -351
- package/content/.morph/templates/integrations/azure-identity-config.cs +288 -288
- package/content/.morph/templates/integrations/clerk-config.cs +258 -258
- package/content/.morph/templates/job.cs +171 -171
- package/content/.morph/templates/migration.cs +83 -83
- package/content/.morph/templates/repository.cs +141 -141
- package/content/.morph/templates/saas/subscription.cs +347 -347
- package/content/.morph/templates/saas/tenant.cs +338 -338
- package/content/.morph/templates/service.cs +139 -139
- package/content/.morph/templates/sprint-status.yaml +68 -68
- package/content/.morph/templates/story.md +143 -143
- package/content/.morph/templates/test.cs +239 -239
- package/content/.morph/templates/ui-design-system.md +286 -286
- package/content/.morph/templates/ui-flows.md +336 -336
- package/content/.morph/templates/ui-mockups.md +133 -133
- package/content/.morph/test-infra/example.bicep +59 -59
- package/content/README.md +79 -79
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +977 -977
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1048 -1048
- package/docs/api/scripts/collapse.js +38 -38
- package/docs/api/scripts/commonNav.js +28 -28
- package/docs/api/scripts/linenumber.js +25 -25
- package/docs/api/scripts/nav.js +12 -12
- package/docs/api/scripts/polyfill.js +3 -3
- package/docs/api/scripts/prettify/Apache-License-2.0.txt +202 -202
- package/docs/api/scripts/prettify/lang-css.js +2 -2
- package/docs/api/scripts/prettify/prettify.js +28 -28
- package/docs/api/scripts/search.js +98 -98
- package/docs/api/styles/jsdoc.css +776 -776
- package/docs/api/styles/prettify.css +80 -80
- package/docs/examples.md +328 -328
- package/docs/templates.md +418 -418
- package/package.json +1 -2
- package/scripts/postinstall.js +132 -132
- package/scripts/reorganize-skills.cjs +175 -0
- package/scripts/validate-agents-structure.cjs +52 -0
- package/scripts/validate-skills.cjs +180 -0
- package/src/commands/analyze-blazor-concurrency.js +193 -193
- package/src/commands/create-story.js +351 -351
- package/src/commands/deploy.js +780 -780
- package/src/commands/detect-agents.js +9 -0
- package/src/commands/detect.js +104 -104
- package/src/commands/generate.js +149 -149
- package/src/commands/lint-fluent.js +352 -352
- package/src/commands/rollback-phase.js +185 -185
- package/src/commands/session-summary.js +291 -291
- package/src/commands/shard-spec.js +224 -224
- package/src/commands/sprint-status.js +250 -250
- package/src/commands/state.js +334 -333
- package/src/commands/sync.js +167 -167
- package/src/commands/troubleshoot.js +222 -222
- package/src/commands/update.js +13 -1
- package/src/commands/validate-blazor-state.js +210 -210
- package/src/commands/validate-blazor.js +156 -156
- package/src/commands/validate-css.js +84 -84
- package/src/commands/validate-phase.js +221 -221
- package/src/lib/blazor-concurrency-analyzer.js +288 -288
- package/src/lib/blazor-state-validator.js +291 -291
- package/src/lib/blazor-validator.js +374 -374
- package/src/lib/css-validator.js +352 -352
- package/src/lib/design-system-generator.js +298 -298
- package/{detectors → src/lib/detectors}/config-detector.js +223 -223
- package/{detectors → src/lib/detectors}/conversation-analyzer.js +163 -163
- package/{detectors → src/lib/detectors}/index.js +84 -84
- package/{detectors → src/lib/detectors}/standards-generator.js +275 -275
- package/src/lib/learning-system.js +520 -520
- package/src/lib/mockup-generator.js +366 -366
- package/src/lib/state-manager.js +21 -4
- package/src/lib/troubleshoot-grep.js +194 -194
- package/src/lib/troubleshoot-index.js +144 -144
- package/src/lib/ui-detector.js +350 -350
- package/src/lib/validators/architecture-validator.js +387 -387
- package/src/lib/validators/package-validator.js +360 -360
- package/src/lib/validators/ui-contrast-validator.js +422 -422
- package/src/utils/logger.js +32 -32
- package/src/utils/version-checker.js +175 -175
- /package/{detectors → src/lib/detectors}/structure-detector.js +0 -0
package/src/lib/state-manager.js
CHANGED
|
@@ -291,6 +291,21 @@ export function removeAgent(featureName, agentId) {
|
|
|
291
291
|
return false;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Normalize output type from kebab-case to camelCase for UI types (BUG #12 fix)
|
|
296
|
+
* @param {string} type - Output type (e.g., 'ui-design-system' or 'uiDesignSystem')
|
|
297
|
+
* @returns {string} Normalized type in camelCase
|
|
298
|
+
*/
|
|
299
|
+
function normalizeOutputType(type) {
|
|
300
|
+
const kebabMap = {
|
|
301
|
+
'ui-design-system': 'uiDesignSystem',
|
|
302
|
+
'ui-mockups': 'uiMockups',
|
|
303
|
+
'ui-components': 'uiComponents',
|
|
304
|
+
'ui-flows': 'uiFlows'
|
|
305
|
+
};
|
|
306
|
+
return kebabMap[type] || type;
|
|
307
|
+
}
|
|
308
|
+
|
|
294
309
|
/**
|
|
295
310
|
* Mark output as created
|
|
296
311
|
* @param {string} featureName - Feature name
|
|
@@ -300,15 +315,17 @@ export function markOutput(featureName, outputType) {
|
|
|
300
315
|
ensureFeature(featureName);
|
|
301
316
|
const state = loadState();
|
|
302
317
|
|
|
303
|
-
|
|
304
|
-
|
|
318
|
+
const normalized = normalizeOutputType(outputType);
|
|
319
|
+
|
|
320
|
+
if (!state.features[featureName].outputs[normalized]) {
|
|
321
|
+
throw new Error(`Output type '${outputType}' not valid. Valid types: proposal, spec, contracts, tasks, uiDesignSystem (or ui-design-system), uiMockups (or ui-mockups), uiComponents (or ui-components), uiFlows (or ui-flows), decisions, recap`);
|
|
305
322
|
}
|
|
306
323
|
|
|
307
|
-
state.features[featureName].outputs[
|
|
324
|
+
state.features[featureName].outputs[normalized].created = true;
|
|
308
325
|
state.features[featureName].updatedAt = new Date().toISOString();
|
|
309
326
|
|
|
310
327
|
// If marking tasks output, try to sync task count from state tasks array
|
|
311
|
-
if (
|
|
328
|
+
if (normalized === 'tasks') {
|
|
312
329
|
syncTasksCount(state.features[featureName]);
|
|
313
330
|
}
|
|
314
331
|
|
|
@@ -1,194 +1,194 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Troubleshoot Grep Fallback
|
|
3
|
-
* Searches markdown files for matching content when index doesn't find results
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
7
|
-
import { join, dirname, basename } from 'path';
|
|
8
|
-
import { fileURLToPath } from 'url';
|
|
9
|
-
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
-
const __dirname = dirname(__filename);
|
|
12
|
-
|
|
13
|
-
// Directories to search
|
|
14
|
-
const SEARCH_PATHS = [
|
|
15
|
-
'framework/standards',
|
|
16
|
-
'content/.morph/standards',
|
|
17
|
-
'.wiki'
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Get all markdown files from search paths
|
|
22
|
-
* @param {string} basePath - Base path of the project
|
|
23
|
-
* @returns {string[]} Array of file paths
|
|
24
|
-
*/
|
|
25
|
-
function getMarkdownFiles(basePath) {
|
|
26
|
-
const files = [];
|
|
27
|
-
|
|
28
|
-
for (const searchPath of SEARCH_PATHS) {
|
|
29
|
-
const fullPath = join(basePath, searchPath);
|
|
30
|
-
|
|
31
|
-
if (!existsSync(fullPath)) {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const entries = readdirSync(fullPath, { withFileTypes: true });
|
|
37
|
-
|
|
38
|
-
for (const entry of entries) {
|
|
39
|
-
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
40
|
-
files.push(join(fullPath, entry.name));
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
} catch (error) {
|
|
44
|
-
// Skip directories we can't read
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return files;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Extract section containing the match
|
|
53
|
-
* @param {string} content - File content
|
|
54
|
-
* @param {number} matchIndex - Index of the match
|
|
55
|
-
* @returns {Object} Section object with title and content
|
|
56
|
-
*/
|
|
57
|
-
function extractSection(content, matchIndex) {
|
|
58
|
-
const lines = content.split('\n');
|
|
59
|
-
let charCount = 0;
|
|
60
|
-
let matchLineIndex = 0;
|
|
61
|
-
|
|
62
|
-
// Find the line containing the match
|
|
63
|
-
for (let i = 0; i < lines.length; i++) {
|
|
64
|
-
charCount += lines[i].length + 1; // +1 for newline
|
|
65
|
-
if (charCount > matchIndex) {
|
|
66
|
-
matchLineIndex = i;
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Find the section header (## heading) above the match
|
|
72
|
-
let sectionTitle = 'Unknown Section';
|
|
73
|
-
let sectionStart = 0;
|
|
74
|
-
|
|
75
|
-
for (let i = matchLineIndex; i >= 0; i--) {
|
|
76
|
-
if (lines[i].startsWith('## ')) {
|
|
77
|
-
sectionTitle = lines[i].replace('## ', '').trim();
|
|
78
|
-
sectionStart = i;
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Find the end of the section (next ## or end of file)
|
|
84
|
-
let sectionEnd = lines.length;
|
|
85
|
-
for (let i = sectionStart + 1; i < lines.length; i++) {
|
|
86
|
-
if (lines[i].startsWith('## ')) {
|
|
87
|
-
sectionEnd = i;
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Extract section content (limit to ~20 lines around match)
|
|
93
|
-
const contextStart = Math.max(sectionStart, matchLineIndex - 10);
|
|
94
|
-
const contextEnd = Math.min(sectionEnd, matchLineIndex + 10);
|
|
95
|
-
const sectionContent = lines.slice(contextStart, contextEnd).join('\n');
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
title: sectionTitle,
|
|
99
|
-
content: sectionContent,
|
|
100
|
-
lineNumber: matchLineIndex + 1
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Search a file for keywords
|
|
106
|
-
* @param {string} filePath - Path to the file
|
|
107
|
-
* @param {string[]} keywords - Keywords to search for
|
|
108
|
-
* @returns {Object[]} Array of matches
|
|
109
|
-
*/
|
|
110
|
-
function searchFile(filePath, keywords) {
|
|
111
|
-
const matches = [];
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
115
|
-
const contentLower = content.toLowerCase();
|
|
116
|
-
|
|
117
|
-
for (const keyword of keywords) {
|
|
118
|
-
const keywordLower = keyword.toLowerCase();
|
|
119
|
-
let index = contentLower.indexOf(keywordLower);
|
|
120
|
-
|
|
121
|
-
while (index !== -1) {
|
|
122
|
-
const section = extractSection(content, index);
|
|
123
|
-
|
|
124
|
-
// Check if we already have a match for this section
|
|
125
|
-
const existingMatch = matches.find(m => m.section === section.title);
|
|
126
|
-
|
|
127
|
-
if (existingMatch) {
|
|
128
|
-
existingMatch.matchCount++;
|
|
129
|
-
existingMatch.keywords.add(keyword);
|
|
130
|
-
} else {
|
|
131
|
-
matches.push({
|
|
132
|
-
file: filePath,
|
|
133
|
-
fileName: basename(filePath),
|
|
134
|
-
section: section.title,
|
|
135
|
-
content: section.content,
|
|
136
|
-
lineNumber: section.lineNumber,
|
|
137
|
-
matchCount: 1,
|
|
138
|
-
keywords: new Set([keyword])
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
index = contentLower.indexOf(keywordLower, index + 1);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
} catch (error) {
|
|
146
|
-
// Skip files we can't read
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return matches;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Search all markdown files for keywords
|
|
154
|
-
* @param {string[]} keywords - Keywords to search for
|
|
155
|
-
* @param {Object} options - Search options
|
|
156
|
-
* @returns {Object[]} Array of matches sorted by relevance
|
|
157
|
-
*/
|
|
158
|
-
export function searchGrep(keywords, options = {}) {
|
|
159
|
-
// Determine base path
|
|
160
|
-
const basePath = options.basePath || join(__dirname, '../..');
|
|
161
|
-
|
|
162
|
-
const files = getMarkdownFiles(basePath);
|
|
163
|
-
let allMatches = [];
|
|
164
|
-
|
|
165
|
-
for (const file of files) {
|
|
166
|
-
const matches = searchFile(file, keywords);
|
|
167
|
-
allMatches = allMatches.concat(matches);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Convert keywords Set to Array and calculate score
|
|
171
|
-
const results = allMatches.map(match => ({
|
|
172
|
-
...match,
|
|
173
|
-
keywords: Array.from(match.keywords),
|
|
174
|
-
score: match.matchCount * 5 + match.keywords.size * 3,
|
|
175
|
-
source: 'grep'
|
|
176
|
-
}));
|
|
177
|
-
|
|
178
|
-
// Sort by score (higher is better) and deduplicate
|
|
179
|
-
const seen = new Set();
|
|
180
|
-
const uniqueResults = results
|
|
181
|
-
.sort((a, b) => b.score - a.score)
|
|
182
|
-
.filter(r => {
|
|
183
|
-
const key = `${r.file}:${r.section}`;
|
|
184
|
-
if (seen.has(key)) return false;
|
|
185
|
-
seen.add(key);
|
|
186
|
-
return true;
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
return uniqueResults.slice(0, 10); // Limit to top 10
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export default {
|
|
193
|
-
searchGrep
|
|
194
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Troubleshoot Grep Fallback
|
|
3
|
+
* Searches markdown files for matching content when index doesn't find results
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
7
|
+
import { join, dirname, basename } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
// Directories to search
|
|
14
|
+
const SEARCH_PATHS = [
|
|
15
|
+
'framework/standards',
|
|
16
|
+
'content/.morph/standards',
|
|
17
|
+
'.wiki'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get all markdown files from search paths
|
|
22
|
+
* @param {string} basePath - Base path of the project
|
|
23
|
+
* @returns {string[]} Array of file paths
|
|
24
|
+
*/
|
|
25
|
+
function getMarkdownFiles(basePath) {
|
|
26
|
+
const files = [];
|
|
27
|
+
|
|
28
|
+
for (const searchPath of SEARCH_PATHS) {
|
|
29
|
+
const fullPath = join(basePath, searchPath);
|
|
30
|
+
|
|
31
|
+
if (!existsSync(fullPath)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const entries = readdirSync(fullPath, { withFileTypes: true });
|
|
37
|
+
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
40
|
+
files.push(join(fullPath, entry.name));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Skip directories we can't read
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract section containing the match
|
|
53
|
+
* @param {string} content - File content
|
|
54
|
+
* @param {number} matchIndex - Index of the match
|
|
55
|
+
* @returns {Object} Section object with title and content
|
|
56
|
+
*/
|
|
57
|
+
function extractSection(content, matchIndex) {
|
|
58
|
+
const lines = content.split('\n');
|
|
59
|
+
let charCount = 0;
|
|
60
|
+
let matchLineIndex = 0;
|
|
61
|
+
|
|
62
|
+
// Find the line containing the match
|
|
63
|
+
for (let i = 0; i < lines.length; i++) {
|
|
64
|
+
charCount += lines[i].length + 1; // +1 for newline
|
|
65
|
+
if (charCount > matchIndex) {
|
|
66
|
+
matchLineIndex = i;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Find the section header (## heading) above the match
|
|
72
|
+
let sectionTitle = 'Unknown Section';
|
|
73
|
+
let sectionStart = 0;
|
|
74
|
+
|
|
75
|
+
for (let i = matchLineIndex; i >= 0; i--) {
|
|
76
|
+
if (lines[i].startsWith('## ')) {
|
|
77
|
+
sectionTitle = lines[i].replace('## ', '').trim();
|
|
78
|
+
sectionStart = i;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Find the end of the section (next ## or end of file)
|
|
84
|
+
let sectionEnd = lines.length;
|
|
85
|
+
for (let i = sectionStart + 1; i < lines.length; i++) {
|
|
86
|
+
if (lines[i].startsWith('## ')) {
|
|
87
|
+
sectionEnd = i;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Extract section content (limit to ~20 lines around match)
|
|
93
|
+
const contextStart = Math.max(sectionStart, matchLineIndex - 10);
|
|
94
|
+
const contextEnd = Math.min(sectionEnd, matchLineIndex + 10);
|
|
95
|
+
const sectionContent = lines.slice(contextStart, contextEnd).join('\n');
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
title: sectionTitle,
|
|
99
|
+
content: sectionContent,
|
|
100
|
+
lineNumber: matchLineIndex + 1
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Search a file for keywords
|
|
106
|
+
* @param {string} filePath - Path to the file
|
|
107
|
+
* @param {string[]} keywords - Keywords to search for
|
|
108
|
+
* @returns {Object[]} Array of matches
|
|
109
|
+
*/
|
|
110
|
+
function searchFile(filePath, keywords) {
|
|
111
|
+
const matches = [];
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
115
|
+
const contentLower = content.toLowerCase();
|
|
116
|
+
|
|
117
|
+
for (const keyword of keywords) {
|
|
118
|
+
const keywordLower = keyword.toLowerCase();
|
|
119
|
+
let index = contentLower.indexOf(keywordLower);
|
|
120
|
+
|
|
121
|
+
while (index !== -1) {
|
|
122
|
+
const section = extractSection(content, index);
|
|
123
|
+
|
|
124
|
+
// Check if we already have a match for this section
|
|
125
|
+
const existingMatch = matches.find(m => m.section === section.title);
|
|
126
|
+
|
|
127
|
+
if (existingMatch) {
|
|
128
|
+
existingMatch.matchCount++;
|
|
129
|
+
existingMatch.keywords.add(keyword);
|
|
130
|
+
} else {
|
|
131
|
+
matches.push({
|
|
132
|
+
file: filePath,
|
|
133
|
+
fileName: basename(filePath),
|
|
134
|
+
section: section.title,
|
|
135
|
+
content: section.content,
|
|
136
|
+
lineNumber: section.lineNumber,
|
|
137
|
+
matchCount: 1,
|
|
138
|
+
keywords: new Set([keyword])
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
index = contentLower.indexOf(keywordLower, index + 1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// Skip files we can't read
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return matches;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Search all markdown files for keywords
|
|
154
|
+
* @param {string[]} keywords - Keywords to search for
|
|
155
|
+
* @param {Object} options - Search options
|
|
156
|
+
* @returns {Object[]} Array of matches sorted by relevance
|
|
157
|
+
*/
|
|
158
|
+
export function searchGrep(keywords, options = {}) {
|
|
159
|
+
// Determine base path
|
|
160
|
+
const basePath = options.basePath || join(__dirname, '../..');
|
|
161
|
+
|
|
162
|
+
const files = getMarkdownFiles(basePath);
|
|
163
|
+
let allMatches = [];
|
|
164
|
+
|
|
165
|
+
for (const file of files) {
|
|
166
|
+
const matches = searchFile(file, keywords);
|
|
167
|
+
allMatches = allMatches.concat(matches);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Convert keywords Set to Array and calculate score
|
|
171
|
+
const results = allMatches.map(match => ({
|
|
172
|
+
...match,
|
|
173
|
+
keywords: Array.from(match.keywords),
|
|
174
|
+
score: match.matchCount * 5 + match.keywords.size * 3,
|
|
175
|
+
source: 'grep'
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
// Sort by score (higher is better) and deduplicate
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
const uniqueResults = results
|
|
181
|
+
.sort((a, b) => b.score - a.score)
|
|
182
|
+
.filter(r => {
|
|
183
|
+
const key = `${r.file}:${r.section}`;
|
|
184
|
+
if (seen.has(key)) return false;
|
|
185
|
+
seen.add(key);
|
|
186
|
+
return true;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return uniqueResults.slice(0, 10); // Limit to top 10
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export default {
|
|
193
|
+
searchGrep
|
|
194
|
+
};
|