feat: add GSD Memory cross-project knowledge system
Implements MCP server for semantic search across GSD projects: MCP Tools: - gsd_memory_search: Search across all registered projects - gsd_memory_decisions: Find decisions from SUMMARY/PROJECT.md - gsd_memory_patterns: Find patterns from SUMMARY.md - gsd_memory_pitfalls: Find pitfalls from RESEARCH.md - gsd_memory_stack: Find tech stack entries - gsd_memory_register: Register project with memory - gsd_memory_index: Trigger indexing/update - gsd_memory_status: Show system status Architecture: - QMD wrapper with grep fallback when QMD unavailable - YAML frontmatter extractors for GSD document types - Project registry at ~/.gsd/projects.json - Auto-install via bin/install.js for Claude Code Workflow integrations: - new-project: Registers project with memory - plan-phase: Indexes after planning - execute-phase: Indexes after execution - complete-milestone: Indexes milestone completion - Researcher agents query memory before Context7 Tests: 117 passing (66 unit + 51 integration) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-phase-researcher
|
||||
description: Researches how to implement a phase before planning. Produces RESEARCH.md consumed by gsd-planner. Spawned by /gsd:plan-phase orchestrator.
|
||||
tools: Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*
|
||||
tools: Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__gsd-memory__*
|
||||
color: cyan
|
||||
---
|
||||
|
||||
@@ -100,6 +100,42 @@ When researching "best library for X":
|
||||
|
||||
<tool_strategy>
|
||||
|
||||
## GSD Memory: First for Past Work
|
||||
|
||||
**If `gsd_memory_*` MCP tools are available,** query past project knowledge before external research.
|
||||
|
||||
**When to use:**
|
||||
- Check what decisions were made in similar phases
|
||||
- Find patterns established in past projects
|
||||
- Learn from documented pitfalls
|
||||
- See what tech stack was used for similar domains
|
||||
|
||||
**How to use:**
|
||||
```
|
||||
1. Search for relevant context:
|
||||
gsd_memory_search({ query: "[phase topic]" })
|
||||
|
||||
2. Find relevant decisions:
|
||||
gsd_memory_decisions({ query: "[topic]" })
|
||||
|
||||
3. Find relevant patterns:
|
||||
gsd_memory_patterns({ query: "[topic]" })
|
||||
|
||||
4. Check for pitfalls:
|
||||
gsd_memory_pitfalls({ domain: "[domain]" })
|
||||
|
||||
5. See tech stack choices:
|
||||
gsd_memory_stack({ query: "[library or purpose]" })
|
||||
```
|
||||
|
||||
**Best practices:**
|
||||
- Query memory BEFORE web research — past decisions may answer questions
|
||||
- Note which project decisions came from for context
|
||||
- Past decisions are HIGH confidence for this user's preferences
|
||||
- Memory augments but doesn't replace current documentation
|
||||
|
||||
**If tools not available:** Skip and proceed to Context7.
|
||||
|
||||
## Context7: First for Libraries
|
||||
|
||||
Context7 provides authoritative, current documentation for libraries and frameworks.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-project-researcher
|
||||
description: Researches domain ecosystem before roadmap creation. Produces files in .planning/research/ consumed during roadmap creation. Spawned by /gsd:new-project or /gsd:new-milestone orchestrators.
|
||||
tools: Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*
|
||||
tools: Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__gsd-memory__*
|
||||
color: cyan
|
||||
---
|
||||
|
||||
@@ -139,6 +139,42 @@ When researching "best library for X":
|
||||
|
||||
<tool_strategy>
|
||||
|
||||
## GSD Memory: First for Past Work
|
||||
|
||||
**If `gsd_memory_*` MCP tools are available,** query past project knowledge before external research.
|
||||
|
||||
**When to use:**
|
||||
- Check what tech stack was used in similar projects
|
||||
- Find patterns established in past work
|
||||
- Learn from documented pitfalls
|
||||
- See what decisions were made for similar domains
|
||||
|
||||
**How to use:**
|
||||
```
|
||||
1. Search for relevant context:
|
||||
gsd_memory_search({ query: "[domain or topic]" })
|
||||
|
||||
2. Find relevant decisions:
|
||||
gsd_memory_decisions({ query: "[topic]" })
|
||||
|
||||
3. Find relevant patterns:
|
||||
gsd_memory_patterns({ query: "[topic]" })
|
||||
|
||||
4. Check for pitfalls:
|
||||
gsd_memory_pitfalls({ domain: "[domain]" })
|
||||
|
||||
5. See tech stack choices:
|
||||
gsd_memory_stack({ query: "[library or purpose]" })
|
||||
```
|
||||
|
||||
**Best practices:**
|
||||
- Query memory BEFORE web research — past decisions inform recommendations
|
||||
- Past stack choices are HIGH confidence for this user's preferences
|
||||
- Past pitfalls are HIGH confidence — actually experienced
|
||||
- Memory augments but doesn't replace current documentation
|
||||
|
||||
**If tools not available:** Skip and proceed to Context7.
|
||||
|
||||
## Context7: First for Libraries
|
||||
|
||||
Context7 provides authoritative, current documentation for libraries and frameworks.
|
||||
|
||||
137
bin/install.js
137
bin/install.js
@@ -907,13 +907,34 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove GSD Memory MCP server config
|
||||
if (settings.mcpServers && settings.mcpServers['gsd-memory']) {
|
||||
delete settings.mcpServers['gsd-memory'];
|
||||
settingsModified = true;
|
||||
console.log(` ${green}✓${reset} Removed GSD Memory MCP config from settings`);
|
||||
// Clean up empty mcpServers object
|
||||
if (Object.keys(settings.mcpServers).length === 0) {
|
||||
delete settings.mcpServers;
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsModified) {
|
||||
writeSettings(settingsPath, settings);
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. For OpenCode, clean up permissions from opencode.json
|
||||
// 6. Remove MCP server from ~/.gsd/ (Claude Code only)
|
||||
if (runtime === 'claude') {
|
||||
const mcpServerDir = path.join(os.homedir(), '.gsd', 'mcp-server');
|
||||
if (fs.existsSync(mcpServerDir)) {
|
||||
fs.rmSync(mcpServerDir, { recursive: true });
|
||||
removedCount++;
|
||||
console.log(` ${green}✓${reset} Removed GSD Memory MCP server`);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. For OpenCode, clean up permissions from opencode.json
|
||||
if (isOpencode) {
|
||||
const opencodeConfigDir = getOpencodeGlobalDir();
|
||||
const configPath = path.join(opencodeConfigDir, 'opencode.json');
|
||||
@@ -1029,6 +1050,102 @@ function configureOpencodePermissions() {
|
||||
console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the GSD Memory MCP server
|
||||
* - Copies gsd-memory/ to ~/.gsd/mcp-server/
|
||||
* - Runs npm install in that directory
|
||||
* - Returns the path for MCP config
|
||||
* @returns {{ success: boolean, serverPath?: string, error?: string }}
|
||||
*/
|
||||
function installMcpServer() {
|
||||
const src = path.join(__dirname, '..', 'gsd-memory');
|
||||
|
||||
// Check if gsd-memory exists in the package
|
||||
if (!fs.existsSync(src)) {
|
||||
return { success: false, error: 'gsd-memory not found in package' };
|
||||
}
|
||||
|
||||
const gsdDir = path.join(os.homedir(), '.gsd');
|
||||
const destDir = path.join(gsdDir, 'mcp-server');
|
||||
|
||||
// Ensure ~/.gsd exists
|
||||
fs.mkdirSync(gsdDir, { recursive: true });
|
||||
|
||||
// Clean install: remove existing mcp-server
|
||||
if (fs.existsSync(destDir)) {
|
||||
fs.rmSync(destDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Copy gsd-memory to ~/.gsd/mcp-server
|
||||
copyDirectory(src, destDir);
|
||||
|
||||
// Run npm install in the mcp-server directory
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
execSync('npm install --production', {
|
||||
cwd: destDir,
|
||||
stdio: 'pipe',
|
||||
timeout: 60000
|
||||
});
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
error: `npm install failed: ${err.message}`,
|
||||
serverPath: destDir
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
serverPath: destDir
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the GSD Memory MCP server in Claude Code settings
|
||||
* @param {string} settingsPath - Path to settings.json
|
||||
* @param {object} settings - Settings object
|
||||
* @param {string} serverPath - Path to the MCP server
|
||||
*/
|
||||
function configureMcpServer(settingsPath, settings, serverPath) {
|
||||
if (!settings.mcpServers) {
|
||||
settings.mcpServers = {};
|
||||
}
|
||||
|
||||
// Add gsd-memory MCP server config
|
||||
settings.mcpServers['gsd-memory'] = {
|
||||
command: 'node',
|
||||
args: [path.join(serverPath, 'dist', 'index.js')]
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a directory recursively
|
||||
*/
|
||||
function copyDirectory(src, dest) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
|
||||
const entries = fs.readdirSync(src, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
|
||||
// Skip node_modules, .git, and test directories
|
||||
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'tests') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
copyDirectory(srcPath, destPath);
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a directory exists and contains files
|
||||
*/
|
||||
@@ -1227,6 +1344,18 @@ function install(isGlobal, runtime = 'claude') {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Install GSD Memory MCP server (Claude Code only, global installs only)
|
||||
let mcpServerPath = null;
|
||||
if (runtime === 'claude' && isGlobal) {
|
||||
const mcpResult = installMcpServer();
|
||||
if (mcpResult.success) {
|
||||
mcpServerPath = mcpResult.serverPath;
|
||||
console.log(` ${green}✓${reset} Installed GSD Memory MCP server`);
|
||||
} else if (mcpResult.error) {
|
||||
console.log(` ${yellow}⚠${reset} MCP server install skipped: ${mcpResult.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Configure statusline and hooks in settings.json
|
||||
// Gemini shares same hook system as Claude Code for now
|
||||
const settingsPath = path.join(targetDir, 'settings.json');
|
||||
@@ -1275,6 +1404,12 @@ function install(isGlobal, runtime = 'claude') {
|
||||
}
|
||||
}
|
||||
|
||||
// Configure MCP server if installed (Claude Code only)
|
||||
if (mcpServerPath && runtime === 'claude') {
|
||||
configureMcpServer(settingsPath, settings, mcpServerPath);
|
||||
console.log(` ${green}✓${reset} Configured GSD Memory MCP server`);
|
||||
}
|
||||
|
||||
return { settingsPath, settings, statuslineCommand, runtime };
|
||||
}
|
||||
|
||||
|
||||
@@ -926,6 +926,26 @@ EOF
|
||||
)"
|
||||
```
|
||||
|
||||
## Phase 9.5: Register with Memory (Optional)
|
||||
|
||||
**If gsd_memory MCP tools are available:**
|
||||
|
||||
Check if `gsd_memory_register` tool exists. If available, register this project with GSD memory for cross-project knowledge:
|
||||
|
||||
```
|
||||
gsd_memory_register({
|
||||
path: process.cwd(),
|
||||
name: "[project name from PROJECT.md]"
|
||||
})
|
||||
```
|
||||
|
||||
Display confirmation:
|
||||
```
|
||||
Memory: Project registered ✓
|
||||
```
|
||||
|
||||
**If tool not available:** Skip silently. GSD memory is optional.
|
||||
|
||||
## Phase 10: Done
|
||||
|
||||
Present completion with next steps:
|
||||
|
||||
@@ -814,6 +814,32 @@ Confirm: "Committed: chore: complete v[X.Y] milestone"
|
||||
|
||||
</step>
|
||||
|
||||
<step name="index_memory">
|
||||
|
||||
Index the completed milestone with GSD Memory (if available).
|
||||
|
||||
**Check if gsd_memory MCP tools are available:**
|
||||
|
||||
If `gsd_memory_index` tool exists, trigger indexing:
|
||||
|
||||
```
|
||||
gsd_memory_index({
|
||||
path: process.cwd()
|
||||
})
|
||||
```
|
||||
|
||||
This updates the project's searchable knowledge base with:
|
||||
- Milestone archive documents
|
||||
- Final phase summaries
|
||||
- Key decisions from PROJECT.md
|
||||
- Requirements validation outcomes
|
||||
|
||||
**If tool not available:** Skip silently.
|
||||
|
||||
Report (if indexed): "Indexed milestone in GSD Memory"
|
||||
|
||||
</step>
|
||||
|
||||
<step name="offer_next">
|
||||
|
||||
```
|
||||
|
||||
@@ -627,6 +627,22 @@ git commit -m "docs(phase-{X}): complete phase execution"
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="index_memory">
|
||||
**If gsd_memory MCP tools are available:**
|
||||
|
||||
Check if `gsd_memory_index` tool exists. If available, trigger indexing:
|
||||
|
||||
```
|
||||
gsd_memory_index({
|
||||
path: process.cwd()
|
||||
})
|
||||
```
|
||||
|
||||
This updates the project's searchable knowledge base with new summaries and verification results.
|
||||
|
||||
**If tool not available:** Skip silently.
|
||||
</step>
|
||||
|
||||
<step name="offer_next">
|
||||
Present next steps based on milestone status:
|
||||
|
||||
|
||||
3
gsd-memory/.gitignore
vendored
Normal file
3
gsd-memory/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
*.log
|
||||
.DS_Store
|
||||
194
gsd-memory/README.md
Normal file
194
gsd-memory/README.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# GSD Memory
|
||||
|
||||
Cross-project knowledge system for GSD (Get Shit Done). An MCP server that provides semantic search across all your GSD projects, extracting decisions, patterns, pitfalls, and tech stack from YAML frontmatter.
|
||||
|
||||
## Features
|
||||
|
||||
- **Cross-project search** - Find content across all registered GSD projects
|
||||
- **Decision extraction** - Query decisions from SUMMARY.md and PROJECT.md
|
||||
- **Pattern discovery** - Find established patterns from completed phases
|
||||
- **Pitfall awareness** - Learn from documented pitfalls in RESEARCH.md
|
||||
- **Tech stack tracking** - Track libraries and tools used across projects
|
||||
- **QMD integration** - Uses QMD for semantic search when available, falls back to grep
|
||||
|
||||
## Installation
|
||||
|
||||
### Option 1: Add to Claude Code MCP config (recommended)
|
||||
|
||||
Add to `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gsd-memory": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "gsd-memory"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: Install globally
|
||||
|
||||
```bash
|
||||
npm install -g gsd-memory
|
||||
```
|
||||
|
||||
Then add to `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gsd-memory": {
|
||||
"command": "gsd-memory"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option 3: With QMD (optional, for semantic search)
|
||||
|
||||
Install QMD for better search results:
|
||||
|
||||
```bash
|
||||
bun install -g github:tobi/qmd
|
||||
```
|
||||
|
||||
GSD Memory automatically detects and uses QMD when available.
|
||||
|
||||
## MCP Tools
|
||||
|
||||
### gsd_memory_register
|
||||
|
||||
Register a project with GSD memory.
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "/path/to/project",
|
||||
"name": "my-project"
|
||||
}
|
||||
```
|
||||
|
||||
### gsd_memory_search
|
||||
|
||||
Search across all registered projects.
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "authentication JWT",
|
||||
"project": "optional-filter",
|
||||
"limit": 20
|
||||
}
|
||||
```
|
||||
|
||||
### gsd_memory_decisions
|
||||
|
||||
Find decisions made across projects.
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "database choice",
|
||||
"tags": ["postgres", "prisma"]
|
||||
}
|
||||
```
|
||||
|
||||
### gsd_memory_patterns
|
||||
|
||||
Find established patterns.
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "error handling"
|
||||
}
|
||||
```
|
||||
|
||||
### gsd_memory_pitfalls
|
||||
|
||||
Find documented pitfalls.
|
||||
|
||||
```json
|
||||
{
|
||||
"domain": "authentication"
|
||||
}
|
||||
```
|
||||
|
||||
### gsd_memory_stack
|
||||
|
||||
Find tech stack entries.
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "jose"
|
||||
}
|
||||
```
|
||||
|
||||
### gsd_memory_index
|
||||
|
||||
Trigger re-indexing for a project.
|
||||
|
||||
```json
|
||||
{
|
||||
"project": "my-project"
|
||||
}
|
||||
```
|
||||
|
||||
### gsd_memory_status
|
||||
|
||||
Get status of the memory system.
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
GSD Memory extracts structured data from GSD's YAML frontmatter:
|
||||
|
||||
| Document | Extracted Fields |
|
||||
|----------|------------------|
|
||||
| SUMMARY.md | `subsystem`, `tags`, `requires`, `provides`, `tech-stack`, `key-decisions`, `patterns-established` |
|
||||
| RESEARCH.md | `domain`, `confidence`, standard stack tables, pitfalls sections |
|
||||
| PROJECT.md | Requirements, constraints, key decisions table |
|
||||
|
||||
Data is stored in `~/.gsd/projects.json` (registry) and optionally in QMD collections for semantic search.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cd gsd-memory
|
||||
npm install
|
||||
npm test # Run unit tests
|
||||
npm run build # Build TypeScript
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
gsd-memory/
|
||||
├── src/
|
||||
│ ├── index.ts # MCP server entry point
|
||||
│ ├── qmd.ts # QMD wrapper with grep fallback
|
||||
│ ├── registry.ts # Project registry (~/.gsd/projects.json)
|
||||
│ ├── extractors/ # Frontmatter and content extractors
|
||||
│ │ ├── frontmatter.ts
|
||||
│ │ ├── summary.ts
|
||||
│ │ ├── research.ts
|
||||
│ │ └── project.ts
|
||||
│ └── tools/ # MCP tool implementations
|
||||
│ ├── search.ts
|
||||
│ ├── decisions.ts
|
||||
│ ├── patterns.ts
|
||||
│ ├── pitfalls.ts
|
||||
│ ├── stack.ts
|
||||
│ ├── register.ts
|
||||
│ ├── index-tool.ts
|
||||
│ └── status.ts
|
||||
└── tests/
|
||||
├── fixtures/ # Test fixtures
|
||||
├── extractors/ # Extractor tests
|
||||
└── tools/ # Tool tests
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
13
gsd-memory/dist/extractors/frontmatter.d.ts
vendored
Normal file
13
gsd-memory/dist/extractors/frontmatter.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Extract YAML frontmatter from markdown content
|
||||
* Returns empty object if no frontmatter or invalid YAML
|
||||
*/
|
||||
export declare function extractFrontmatter(markdown: string): Record<string, unknown>;
|
||||
/**
|
||||
* Extract frontmatter and content separately
|
||||
*/
|
||||
export declare function parseFrontmatter(markdown: string): {
|
||||
frontmatter: Record<string, unknown>;
|
||||
content: string;
|
||||
};
|
||||
//# sourceMappingURL=frontmatter.d.ts.map
|
||||
1
gsd-memory/dist/extractors/frontmatter.d.ts.map
vendored
Normal file
1
gsd-memory/dist/extractors/frontmatter.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../src/extractors/frontmatter.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ5E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG;IAClD,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB,CAaA"}
|
||||
34
gsd-memory/dist/extractors/frontmatter.js
vendored
Normal file
34
gsd-memory/dist/extractors/frontmatter.js
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
import matter from 'gray-matter';
|
||||
/**
|
||||
* Extract YAML frontmatter from markdown content
|
||||
* Returns empty object if no frontmatter or invalid YAML
|
||||
*/
|
||||
export function extractFrontmatter(markdown) {
|
||||
try {
|
||||
const { data } = matter(markdown);
|
||||
return data || {};
|
||||
}
|
||||
catch {
|
||||
// Return empty object on parse errors
|
||||
return {};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Extract frontmatter and content separately
|
||||
*/
|
||||
export function parseFrontmatter(markdown) {
|
||||
try {
|
||||
const { data, content } = matter(markdown);
|
||||
return {
|
||||
frontmatter: data || {},
|
||||
content: content.trim()
|
||||
};
|
||||
}
|
||||
catch {
|
||||
return {
|
||||
frontmatter: {},
|
||||
content: markdown.trim()
|
||||
};
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=frontmatter.js.map
|
||||
1
gsd-memory/dist/extractors/frontmatter.js.map
vendored
Normal file
1
gsd-memory/dist/extractors/frontmatter.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../../src/extractors/frontmatter.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,IAAI,CAAC;QACH,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;QAClC,OAAO,IAAI,IAAI,EAAE,CAAC;IACpB,CAAC;IAAC,MAAM,CAAC;QACP,sCAAsC;QACtC,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAI/C,IAAI,CAAC;QACH,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC3C,OAAO;YACL,WAAW,EAAE,IAAI,IAAI,EAAE;YACvB,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE;SACxB,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,WAAW,EAAE,EAAE;YACf,OAAO,EAAE,QAAQ,CAAC,IAAI,EAAE;SACzB,CAAC;IACJ,CAAC;AACH,CAAC"}
|
||||
5
gsd-memory/dist/extractors/index.d.ts
vendored
Normal file
5
gsd-memory/dist/extractors/index.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export { extractFrontmatter, parseFrontmatter } from './frontmatter.js';
|
||||
export { extractSummary, type SummaryData, type TechStackEntry, type Decision as SummaryDecision } from './summary.js';
|
||||
export { extractResearch, type ResearchData, type Pitfall, type DontHandRoll } from './research.js';
|
||||
export { extractProject, type ProjectData, type Decision as ProjectDecision, type Constraint } from './project.js';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
gsd-memory/dist/extractors/index.d.ts.map
vendored
Normal file
1
gsd-memory/dist/extractors/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extractors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,QAAQ,IAAI,eAAe,EAAE,MAAM,cAAc,CAAC;AACvH,OAAO,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AACpG,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,QAAQ,IAAI,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC"}
|
||||
5
gsd-memory/dist/extractors/index.js
vendored
Normal file
5
gsd-memory/dist/extractors/index.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export { extractFrontmatter, parseFrontmatter } from './frontmatter.js';
|
||||
export { extractSummary } from './summary.js';
|
||||
export { extractResearch } from './research.js';
|
||||
export { extractProject } from './project.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
gsd-memory/dist/extractors/index.js.map
vendored
Normal file
1
gsd-memory/dist/extractors/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/extractors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,cAAc,EAA2E,MAAM,cAAc,CAAC;AACvH,OAAO,EAAE,eAAe,EAAsD,MAAM,eAAe,CAAC;AACpG,OAAO,EAAE,cAAc,EAAuE,MAAM,cAAc,CAAC"}
|
||||
33
gsd-memory/dist/extractors/project.d.ts
vendored
Normal file
33
gsd-memory/dist/extractors/project.d.ts
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
export interface OutOfScopeItem {
|
||||
item: string;
|
||||
reason: string;
|
||||
}
|
||||
export interface Constraint {
|
||||
type: string;
|
||||
value: string;
|
||||
why?: string;
|
||||
}
|
||||
export interface Decision {
|
||||
decision: string;
|
||||
rationale: string;
|
||||
outcome: string;
|
||||
}
|
||||
export interface Requirements {
|
||||
validated: string[];
|
||||
active: string[];
|
||||
outOfScope: OutOfScopeItem[];
|
||||
}
|
||||
export interface ProjectData {
|
||||
name: string;
|
||||
description: string;
|
||||
coreValue: string;
|
||||
requirements: Requirements;
|
||||
context: string;
|
||||
constraints: Constraint[];
|
||||
decisions: Decision[];
|
||||
}
|
||||
/**
|
||||
* Extract structured data from a PROJECT.md file
|
||||
*/
|
||||
export declare function extractProject(markdown: string): ProjectData;
|
||||
//# sourceMappingURL=project.d.ts.map
|
||||
1
gsd-memory/dist/extractors/project.d.ts.map
vendored
Normal file
1
gsd-memory/dist/extractors/project.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/extractors/project.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,YAAY,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,SAAS,EAAE,QAAQ,EAAE,CAAC;CACvB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAU5D"}
|
||||
139
gsd-memory/dist/extractors/project.js
vendored
Normal file
139
gsd-memory/dist/extractors/project.js
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Extract structured data from a PROJECT.md file
|
||||
*/
|
||||
export function extractProject(markdown) {
|
||||
return {
|
||||
name: extractName(markdown),
|
||||
description: extractSection(markdown, 'What This Is'),
|
||||
coreValue: extractSection(markdown, 'Core Value'),
|
||||
requirements: extractRequirements(markdown),
|
||||
context: extractSection(markdown, 'Context'),
|
||||
constraints: extractConstraints(markdown),
|
||||
decisions: extractDecisions(markdown)
|
||||
};
|
||||
}
|
||||
function extractName(content) {
|
||||
// First line should be # ProjectName
|
||||
const match = content.match(/^#\s+(.+)/m);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
function extractSection(content, sectionName) {
|
||||
// Find the section and extract content until next ## heading
|
||||
const regex = new RegExp(`##\\s+${sectionName}\\n\\n([\\s\\S]*?)(?=\\n##|$)`, 'i');
|
||||
const match = content.match(regex);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
function extractRequirements(content) {
|
||||
const requirementsSection = content.match(/##\s+Requirements\n([\s\S]*?)(?=\n##[^#]|$)/i);
|
||||
if (!requirementsSection) {
|
||||
return { validated: [], active: [], outOfScope: [] };
|
||||
}
|
||||
const section = requirementsSection[1];
|
||||
return {
|
||||
validated: extractValidatedRequirements(section),
|
||||
active: extractActiveRequirements(section),
|
||||
outOfScope: extractOutOfScope(section)
|
||||
};
|
||||
}
|
||||
function extractValidatedRequirements(section) {
|
||||
const requirements = [];
|
||||
// Look for ### Validated subsection
|
||||
const validatedSection = section.match(/###\s+Validated\n([\s\S]*?)(?=###|$)/i);
|
||||
if (!validatedSection)
|
||||
return requirements;
|
||||
// Extract checkmark items: - ✓ requirement — version
|
||||
const matches = validatedSection[1].match(/^-\s+✓\s+(.+)/gm);
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
const content = match.replace(/^-\s+✓\s+/, '').trim();
|
||||
requirements.push(content);
|
||||
}
|
||||
}
|
||||
return requirements;
|
||||
}
|
||||
function extractActiveRequirements(section) {
|
||||
const requirements = [];
|
||||
// Look for ### Active subsection
|
||||
const activeSection = section.match(/###\s+Active\n([\s\S]*?)(?=###|$)/i);
|
||||
if (!activeSection)
|
||||
return requirements;
|
||||
// Extract checkbox items: - [ ] requirement
|
||||
const matches = activeSection[1].match(/^-\s+\[\s*\]\s+(.+)/gm);
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
const content = match.replace(/^-\s+\[\s*\]\s+/, '').trim();
|
||||
requirements.push(content);
|
||||
}
|
||||
}
|
||||
return requirements;
|
||||
}
|
||||
function extractOutOfScope(section) {
|
||||
const items = [];
|
||||
// Look for ### Out of Scope subsection
|
||||
const outOfScopeSection = section.match(/###\s+Out of Scope\n([\s\S]*?)(?=###|$)/i);
|
||||
if (!outOfScopeSection)
|
||||
return items;
|
||||
// Extract items: - Item — reason
|
||||
const matches = outOfScopeSection[1].match(/^-\s+(.+)/gm);
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
const content = match.replace(/^-\s+/, '').trim();
|
||||
// Split by em-dash or double hyphen
|
||||
const parts = content.split(/\s+—\s+|\s+--\s+/);
|
||||
items.push({
|
||||
item: parts[0].trim(),
|
||||
reason: parts[1]?.trim() || ''
|
||||
});
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
function extractConstraints(content) {
|
||||
const constraints = [];
|
||||
const constraintsSection = content.match(/##\s+Constraints\n([\s\S]*?)(?=\n##|$)/i);
|
||||
if (!constraintsSection)
|
||||
return constraints;
|
||||
// Extract items: - **Type**: Value — Why
|
||||
const matches = constraintsSection[1].match(/^-\s+\*\*([^*]+)\*\*:\s*(.+)/gm);
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
const typeMatch = match.match(/\*\*([^*]+)\*\*/);
|
||||
const valueMatch = match.match(/\*\*[^*]+\*\*:\s*(.+)/);
|
||||
if (typeMatch && valueMatch) {
|
||||
const valueParts = valueMatch[1].split(/\s+—\s+|\s+--\s+/);
|
||||
constraints.push({
|
||||
type: typeMatch[1].trim(),
|
||||
value: valueParts[0].trim(),
|
||||
why: valueParts[1]?.trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return constraints;
|
||||
}
|
||||
function extractDecisions(content) {
|
||||
const decisions = [];
|
||||
const decisionsSection = content.match(/##\s+Key Decisions\n([\s\S]*?)(?=\n##[^#]|\n---|$)/i);
|
||||
if (!decisionsSection)
|
||||
return decisions;
|
||||
// Parse markdown table rows - match rows with 3+ cells
|
||||
const lines = decisionsSection[1].split('\n');
|
||||
for (const line of lines) {
|
||||
// Must be a table row (starts and ends with |)
|
||||
if (!line.trim().startsWith('|'))
|
||||
continue;
|
||||
// Skip header row (contains 'Decision') and separator row (contains ---)
|
||||
if (line.includes('Decision') || line.includes('---'))
|
||||
continue;
|
||||
const cells = line.split('|').filter(c => c.trim());
|
||||
if (cells.length >= 3) {
|
||||
decisions.push({
|
||||
decision: cells[0].trim(),
|
||||
rationale: cells[1].trim(),
|
||||
outcome: cells[2].trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
return decisions;
|
||||
}
|
||||
//# sourceMappingURL=project.js.map
|
||||
1
gsd-memory/dist/extractors/project.js.map
vendored
Normal file
1
gsd-memory/dist/extractors/project.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/extractors/project.ts"],"names":[],"mappings":"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"}
|
||||
40
gsd-memory/dist/extractors/research.d.ts
vendored
Normal file
40
gsd-memory/dist/extractors/research.d.ts
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
export interface StackEntry {
|
||||
library: string;
|
||||
version?: string;
|
||||
purpose: string;
|
||||
whyStandard?: string;
|
||||
}
|
||||
export interface Pitfall {
|
||||
name: string;
|
||||
description: string;
|
||||
cause?: string;
|
||||
prevention: string;
|
||||
warningSigns?: string;
|
||||
}
|
||||
export interface DontHandRoll {
|
||||
problem: string;
|
||||
dontBuild: string;
|
||||
useInstead: string;
|
||||
why: string;
|
||||
}
|
||||
export interface UserConstraints {
|
||||
locked: string[];
|
||||
discretion: string[];
|
||||
deferred: string[];
|
||||
}
|
||||
export interface ResearchData {
|
||||
domain: string;
|
||||
confidence: string;
|
||||
researched: string;
|
||||
standardStack: StackEntry[];
|
||||
pitfalls: Pitfall[];
|
||||
dontHandRoll: DontHandRoll[];
|
||||
antiPatterns: string[];
|
||||
userConstraints: UserConstraints;
|
||||
primaryRecommendation: string;
|
||||
}
|
||||
/**
|
||||
* Extract structured data from a RESEARCH.md file
|
||||
*/
|
||||
export declare function extractResearch(markdown: string): ResearchData;
|
||||
//# sourceMappingURL=research.d.ts.map
|
||||
1
gsd-memory/dist/extractors/research.d.ts.map
vendored
Normal file
1
gsd-memory/dist/extractors/research.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"research.d.ts","sourceRoot":"","sources":["../../src/extractors/research.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAE3B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IAGnB,aAAa,EAAE,UAAU,EAAE,CAAC;IAC5B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,YAAY,EAAE,YAAY,EAAE,CAAC;IAC7B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,eAAe,CAAC;IACjC,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,CAiB9D"}
|
||||
183
gsd-memory/dist/extractors/research.js
vendored
Normal file
183
gsd-memory/dist/extractors/research.js
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
import { parseFrontmatter } from './frontmatter.js';
|
||||
/**
|
||||
* Extract structured data from a RESEARCH.md file
|
||||
*/
|
||||
export function extractResearch(markdown) {
|
||||
const { frontmatter, content } = parseFrontmatter(markdown);
|
||||
return {
|
||||
// Metadata from frontmatter
|
||||
domain: frontmatter.domain || '',
|
||||
confidence: frontmatter.confidence || '',
|
||||
researched: formatDate(frontmatter.researched),
|
||||
// Extracted from content
|
||||
standardStack: extractStandardStack(content),
|
||||
pitfalls: extractPitfalls(content),
|
||||
dontHandRoll: extractDontHandRoll(content),
|
||||
antiPatterns: extractAntiPatterns(content),
|
||||
userConstraints: extractUserConstraints(content),
|
||||
primaryRecommendation: extractPrimaryRecommendation(content)
|
||||
};
|
||||
}
|
||||
function formatDate(date) {
|
||||
if (!date)
|
||||
return '';
|
||||
if (date instanceof Date) {
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
return String(date);
|
||||
}
|
||||
function extractStandardStack(content) {
|
||||
const entries = [];
|
||||
// Look for standard_stack section and parse markdown tables
|
||||
const stackSection = extractSection(content, 'standard_stack');
|
||||
if (!stackSection)
|
||||
return entries;
|
||||
// Parse Core and Supporting tables
|
||||
const tableRows = stackSection.match(/\|[^|]+\|[^|]+\|[^|]+\|[^|]*\|/g);
|
||||
if (!tableRows)
|
||||
return entries;
|
||||
for (const row of tableRows) {
|
||||
// Skip header and separator rows
|
||||
if (row.includes('Library') || row.includes('---'))
|
||||
continue;
|
||||
const cells = row.split('|').filter(c => c.trim());
|
||||
if (cells.length >= 3) {
|
||||
entries.push({
|
||||
library: cells[0].trim(),
|
||||
version: cells[1]?.trim(),
|
||||
purpose: cells[2]?.trim(),
|
||||
whyStandard: cells[3]?.trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
function extractPitfalls(content) {
|
||||
const pitfalls = [];
|
||||
const pitfallSection = extractSection(content, 'common_pitfalls');
|
||||
if (!pitfallSection)
|
||||
return pitfalls;
|
||||
// Parse individual pitfall blocks (### Pitfall N: Name)
|
||||
const pitfallBlocks = pitfallSection.split(/###\s+Pitfall\s+\d+:\s+/);
|
||||
for (const block of pitfallBlocks) {
|
||||
if (!block.trim())
|
||||
continue;
|
||||
const lines = block.split('\n');
|
||||
const name = lines[0]?.trim();
|
||||
if (!name)
|
||||
continue;
|
||||
const description = extractField(block, 'What goes wrong');
|
||||
const cause = extractField(block, 'Why it happens');
|
||||
const prevention = extractField(block, 'How to avoid');
|
||||
const warningSigns = extractField(block, 'Warning signs');
|
||||
if (description || prevention) {
|
||||
pitfalls.push({
|
||||
name,
|
||||
description: description || '',
|
||||
cause,
|
||||
prevention: prevention || '',
|
||||
warningSigns
|
||||
});
|
||||
}
|
||||
}
|
||||
return pitfalls;
|
||||
}
|
||||
function extractDontHandRoll(content) {
|
||||
const items = [];
|
||||
const section = extractSection(content, 'dont_hand_roll');
|
||||
if (!section)
|
||||
return items;
|
||||
// Parse table rows
|
||||
const tableRows = section.match(/\|[^|]+\|[^|]+\|[^|]+\|[^|]+\|/g);
|
||||
if (!tableRows)
|
||||
return items;
|
||||
for (const row of tableRows) {
|
||||
if (row.includes('Problem') || row.includes('---'))
|
||||
continue;
|
||||
const cells = row.split('|').filter(c => c.trim());
|
||||
if (cells.length >= 4) {
|
||||
items.push({
|
||||
problem: cells[0].trim(),
|
||||
dontBuild: cells[1].trim(),
|
||||
useInstead: cells[2].trim(),
|
||||
why: cells[3].trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
function extractAntiPatterns(content) {
|
||||
const patterns = [];
|
||||
const archSection = extractSection(content, 'architecture_patterns');
|
||||
if (!archSection)
|
||||
return patterns;
|
||||
// Look for "Anti-Patterns to Avoid" subsection
|
||||
const antiMatch = archSection.match(/###\s+Anti-Patterns to Avoid\n([\s\S]*?)(?=###|$)/);
|
||||
if (!antiMatch)
|
||||
return patterns;
|
||||
// Extract bullet points
|
||||
const bullets = antiMatch[1].match(/^-\s+\*\*([^*]+)\*\*/gm);
|
||||
if (bullets) {
|
||||
for (const bullet of bullets) {
|
||||
const match = bullet.match(/\*\*([^*]+)\*\*/);
|
||||
if (match)
|
||||
patterns.push(match[1].trim());
|
||||
}
|
||||
}
|
||||
return patterns;
|
||||
}
|
||||
function extractUserConstraints(content) {
|
||||
const constraints = {
|
||||
locked: [],
|
||||
discretion: [],
|
||||
deferred: []
|
||||
};
|
||||
const section = extractSection(content, 'user_constraints');
|
||||
if (!section)
|
||||
return constraints;
|
||||
// Extract locked decisions
|
||||
const lockedMatch = section.match(/###\s+Locked Decisions\n([\s\S]*?)(?=###|$)/);
|
||||
if (lockedMatch) {
|
||||
constraints.locked = extractBulletPoints(lockedMatch[1]);
|
||||
}
|
||||
// Extract Claude's discretion
|
||||
const discretionMatch = section.match(/###\s+Claude's Discretion\n([\s\S]*?)(?=###|$)/);
|
||||
if (discretionMatch) {
|
||||
constraints.discretion = extractBulletPoints(discretionMatch[1]);
|
||||
}
|
||||
// Extract deferred items
|
||||
const deferredMatch = section.match(/###\s+Deferred Ideas[^#]*\n([\s\S]*?)(?=<\/|$)/);
|
||||
if (deferredMatch) {
|
||||
constraints.deferred = extractBulletPoints(deferredMatch[1]);
|
||||
}
|
||||
return constraints;
|
||||
}
|
||||
function extractPrimaryRecommendation(content) {
|
||||
// Look for "Primary recommendation:" in summary section
|
||||
const match = content.match(/\*\*Primary recommendation:\*\*\s*([^\n]+)/);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
// Helper functions
|
||||
function extractSection(content, sectionTag) {
|
||||
const regex = new RegExp(`<${sectionTag}>([\\s\\S]*?)<\\/${sectionTag}>`, 'i');
|
||||
const match = content.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
function extractField(block, fieldName) {
|
||||
const regex = new RegExp(`\\*\\*${fieldName}:\\*\\*\\s*([^\\n]+)`, 'i');
|
||||
const match = block.match(regex);
|
||||
return match ? match[1].trim() : undefined;
|
||||
}
|
||||
function extractBulletPoints(text) {
|
||||
const points = [];
|
||||
const matches = text.match(/^-\s+(.+)/gm);
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
const content = match.replace(/^-\s+/, '').trim();
|
||||
if (content)
|
||||
points.push(content);
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
//# sourceMappingURL=research.js.map
|
||||
1
gsd-memory/dist/extractors/research.js.map
vendored
Normal file
1
gsd-memory/dist/extractors/research.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
38
gsd-memory/dist/extractors/summary.d.ts
vendored
Normal file
38
gsd-memory/dist/extractors/summary.d.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
export interface TechStackEntry {
|
||||
library: string;
|
||||
version: string;
|
||||
purpose?: string;
|
||||
}
|
||||
export interface Dependency {
|
||||
phase: string;
|
||||
provides: string;
|
||||
}
|
||||
export interface KeyFiles {
|
||||
created: string[];
|
||||
modified: string[];
|
||||
}
|
||||
export interface Decision {
|
||||
decision: string;
|
||||
source?: string;
|
||||
}
|
||||
export interface SummaryData {
|
||||
phase: string;
|
||||
plan: number | string;
|
||||
subsystem: string;
|
||||
tags: string[];
|
||||
requires: Dependency[];
|
||||
provides: string[];
|
||||
affects: string[];
|
||||
techStack: TechStackEntry[];
|
||||
patterns: string[];
|
||||
keyFiles: KeyFiles;
|
||||
decisions: Decision[];
|
||||
duration: string;
|
||||
completed: string;
|
||||
oneLiner: string;
|
||||
}
|
||||
/**
|
||||
* Extract structured data from a SUMMARY.md file
|
||||
*/
|
||||
export declare function extractSummary(markdown: string): SummaryData;
|
||||
//# sourceMappingURL=summary.d.ts.map
|
||||
1
gsd-memory/dist/extractors/summary.d.ts.map
vendored
Normal file
1
gsd-memory/dist/extractors/summary.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../../src/extractors/summary.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAE1B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,EAAE,CAAC;IAGf,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAGlB,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,QAAQ,EAAE,MAAM,EAAE,CAAC;IAGnB,QAAQ,EAAE,QAAQ,CAAC;IAGnB,SAAS,EAAE,QAAQ,EAAE,CAAC;IAGtB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAGlB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAgC5D"}
|
||||
88
gsd-memory/dist/extractors/summary.js
vendored
Normal file
88
gsd-memory/dist/extractors/summary.js
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
import { parseFrontmatter } from './frontmatter.js';
|
||||
/**
|
||||
* Extract structured data from a SUMMARY.md file
|
||||
*/
|
||||
export function extractSummary(markdown) {
|
||||
const { frontmatter, content } = parseFrontmatter(markdown);
|
||||
return {
|
||||
// Identifiers
|
||||
phase: frontmatter.phase || '',
|
||||
plan: frontmatter.plan || '',
|
||||
subsystem: frontmatter.subsystem || '',
|
||||
tags: frontmatter.tags || [],
|
||||
// Dependency graph
|
||||
requires: extractRequires(frontmatter.requires),
|
||||
provides: frontmatter.provides || [],
|
||||
affects: frontmatter.affects || [],
|
||||
// Tech tracking
|
||||
techStack: extractTechStack(frontmatter['tech-stack']),
|
||||
patterns: frontmatter['patterns-established'] || [],
|
||||
// Files
|
||||
keyFiles: extractKeyFiles(frontmatter['key-files']),
|
||||
// Decisions
|
||||
decisions: extractDecisions(frontmatter['key-decisions']),
|
||||
// Metadata
|
||||
duration: frontmatter.duration || '',
|
||||
completed: formatCompleted(frontmatter.completed),
|
||||
// Content
|
||||
oneLiner: extractOneLiner(content)
|
||||
};
|
||||
}
|
||||
function extractRequires(requires) {
|
||||
if (!Array.isArray(requires))
|
||||
return [];
|
||||
return requires.map((req) => ({
|
||||
phase: req.phase || '',
|
||||
provides: req.provides || ''
|
||||
}));
|
||||
}
|
||||
function extractTechStack(techStack) {
|
||||
if (!techStack || typeof techStack !== 'object')
|
||||
return [];
|
||||
const ts = techStack;
|
||||
const added = ts.added;
|
||||
if (!Array.isArray(added))
|
||||
return [];
|
||||
return added.map((entry) => {
|
||||
// Handle both object format { library: version } and string format
|
||||
if (typeof entry === 'object' && entry !== null) {
|
||||
const [library, version] = Object.entries(entry)[0] || ['', ''];
|
||||
return { library, version: String(version) };
|
||||
}
|
||||
return { library: String(entry), version: '' };
|
||||
});
|
||||
}
|
||||
function extractKeyFiles(keyFiles) {
|
||||
if (!keyFiles || typeof keyFiles !== 'object') {
|
||||
return { created: [], modified: [] };
|
||||
}
|
||||
const kf = keyFiles;
|
||||
return {
|
||||
created: kf.created || [],
|
||||
modified: kf.modified || []
|
||||
};
|
||||
}
|
||||
function extractDecisions(decisions) {
|
||||
if (!Array.isArray(decisions))
|
||||
return [];
|
||||
return decisions.map((d) => ({
|
||||
decision: String(d),
|
||||
source: 'frontmatter'
|
||||
}));
|
||||
}
|
||||
function formatCompleted(completed) {
|
||||
if (!completed)
|
||||
return '';
|
||||
// If it's a Date object (YAML parsed it), format as ISO date
|
||||
if (completed instanceof Date) {
|
||||
return completed.toISOString().split('T')[0];
|
||||
}
|
||||
return String(completed);
|
||||
}
|
||||
function extractOneLiner(content) {
|
||||
// The one-liner is the bold text after the title
|
||||
// Pattern: # Title\n\n**one-liner**
|
||||
const match = content.match(/^#[^\n]+\n\n\*\*([^*]+)\*\*/m);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
//# sourceMappingURL=summary.js.map
|
||||
1
gsd-memory/dist/extractors/summary.js.map
vendored
Normal file
1
gsd-memory/dist/extractors/summary.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"summary.js","sourceRoot":"","sources":["../../src/extractors/summary.ts"],"names":[],"mappings":"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"}
|
||||
3
gsd-memory/dist/index.d.ts
vendored
Normal file
3
gsd-memory/dist/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
export {};
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
gsd-memory/dist/index.d.ts.map
vendored
Normal file
1
gsd-memory/dist/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
||||
298
gsd-memory/dist/index.js
vendored
Normal file
298
gsd-memory/dist/index.js
vendored
Normal file
@@ -0,0 +1,298 @@
|
||||
#!/usr/bin/env node
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { search } from './tools/search.js';
|
||||
import { findDecisions } from './tools/decisions.js';
|
||||
import { findPatterns } from './tools/patterns.js';
|
||||
import { findPitfalls } from './tools/pitfalls.js';
|
||||
import { findStack } from './tools/stack.js';
|
||||
import { register } from './tools/register.js';
|
||||
import { index, indexAll } from './tools/index-tool.js';
|
||||
import { getStatus } from './tools/status.js';
|
||||
// Define available tools
|
||||
const tools = [
|
||||
{
|
||||
name: 'gsd_memory_search',
|
||||
description: 'Search across all registered GSD projects for content matching a query. Returns enriched results with project, phase, and document type context.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'The search query'
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
description: 'Optional: limit search to a specific project'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results (default: 20)'
|
||||
}
|
||||
},
|
||||
required: ['query']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'gsd_memory_decisions',
|
||||
description: 'Find decisions made across GSD projects. Searches KEY-DECISIONS from SUMMARY.md frontmatter and Key Decisions tables from PROJECT.md.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Optional: filter decisions by keyword'
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
description: 'Optional: limit to a specific project'
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Optional: filter by tags'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results (default: 50)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'gsd_memory_patterns',
|
||||
description: 'Find patterns established across GSD projects. Extracts PATTERNS-ESTABLISHED from SUMMARY.md frontmatter.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Optional: filter patterns by keyword'
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
description: 'Optional: limit to a specific project'
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Optional: filter by tags'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results (default: 50)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'gsd_memory_pitfalls',
|
||||
description: 'Find documented pitfalls from RESEARCH.md files. Includes what went wrong, causes, and prevention strategies.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Optional: filter pitfalls by keyword'
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
description: 'Optional: limit to a specific project'
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'Optional: filter by technology domain'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results (default: 50)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'gsd_memory_stack',
|
||||
description: 'Find tech stack entries across GSD projects. Extracts from SUMMARY.md (tech-stack.added) and RESEARCH.md (standard stack tables).',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Optional: filter by library name or purpose'
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
description: 'Optional: limit to a specific project'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results (default: 100)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'gsd_memory_register',
|
||||
description: 'Register a project with GSD memory. Creates a QMD collection if QMD is available.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Path to the project root (must contain .planning directory)'
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Optional: project name (defaults to directory name)'
|
||||
}
|
||||
},
|
||||
required: ['path']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'gsd_memory_index',
|
||||
description: 'Trigger indexing/update for a project. Updates QMD collection if available, otherwise marks project as indexed for grep fallback.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project: {
|
||||
type: 'string',
|
||||
description: 'Project name to index'
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Alternative: project path to index'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'gsd_memory_status',
|
||||
description: 'Show status of GSD memory system: QMD availability, registered projects, document counts.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
}
|
||||
];
|
||||
// Create server instance
|
||||
const server = new Server({
|
||||
name: 'gsd-memory',
|
||||
version: '1.0.0'
|
||||
}, {
|
||||
capabilities: {
|
||||
tools: {}
|
||||
}
|
||||
});
|
||||
// Handle list tools request
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools
|
||||
}));
|
||||
// Handle tool calls
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
try {
|
||||
let result;
|
||||
switch (name) {
|
||||
case 'gsd_memory_search':
|
||||
result = await search({
|
||||
query: args?.query,
|
||||
project: args?.project,
|
||||
limit: args?.limit
|
||||
});
|
||||
break;
|
||||
case 'gsd_memory_decisions':
|
||||
result = await findDecisions({
|
||||
query: args?.query,
|
||||
project: args?.project,
|
||||
tags: args?.tags,
|
||||
limit: args?.limit
|
||||
});
|
||||
break;
|
||||
case 'gsd_memory_patterns':
|
||||
result = await findPatterns({
|
||||
query: args?.query,
|
||||
project: args?.project,
|
||||
tags: args?.tags,
|
||||
limit: args?.limit
|
||||
});
|
||||
break;
|
||||
case 'gsd_memory_pitfalls':
|
||||
result = await findPitfalls({
|
||||
query: args?.query,
|
||||
project: args?.project,
|
||||
domain: args?.domain,
|
||||
limit: args?.limit
|
||||
});
|
||||
break;
|
||||
case 'gsd_memory_stack':
|
||||
result = await findStack({
|
||||
query: args?.query,
|
||||
project: args?.project,
|
||||
limit: args?.limit
|
||||
});
|
||||
break;
|
||||
case 'gsd_memory_register':
|
||||
result = await register({
|
||||
path: args?.path,
|
||||
name: args?.name
|
||||
});
|
||||
break;
|
||||
case 'gsd_memory_index':
|
||||
if (args?.project || args?.path) {
|
||||
result = await index({
|
||||
project: args?.project,
|
||||
path: args?.path
|
||||
});
|
||||
}
|
||||
else {
|
||||
result = await indexAll();
|
||||
}
|
||||
break;
|
||||
case 'gsd_memory_status':
|
||||
result = await getStatus();
|
||||
break;
|
||||
default:
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Unknown tool: ${name}`
|
||||
}
|
||||
],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
});
|
||||
// Start the server
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error('GSD Memory MCP server started');
|
||||
}
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
gsd-memory/dist/index.js.map
vendored
Normal file
1
gsd-memory/dist/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
67
gsd-memory/dist/qmd.d.ts
vendored
Normal file
67
gsd-memory/dist/qmd.d.ts
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
export interface QmdSearchResult {
|
||||
content: string;
|
||||
source: string;
|
||||
relevance: number;
|
||||
context?: string;
|
||||
}
|
||||
export interface QmdSearchOptions {
|
||||
query: string;
|
||||
paths: string[];
|
||||
limit?: number;
|
||||
collection?: string;
|
||||
}
|
||||
export interface QmdCollectionOptions {
|
||||
name: string;
|
||||
paths: string[];
|
||||
context?: string;
|
||||
}
|
||||
export interface QmdResult {
|
||||
success: boolean;
|
||||
reason?: string;
|
||||
data?: unknown;
|
||||
}
|
||||
export interface QmdStatus {
|
||||
available: boolean;
|
||||
collection?: string;
|
||||
documentCount?: number;
|
||||
lastIndexed?: string;
|
||||
}
|
||||
/**
|
||||
* QMD wrapper - provides semantic search when QMD is available,
|
||||
* falls back to grep-based search otherwise
|
||||
*/
|
||||
export declare const qmd: {
|
||||
/**
|
||||
* Check if QMD is installed and available
|
||||
*/
|
||||
isAvailable(): Promise<boolean>;
|
||||
/**
|
||||
* Search for content using QMD or grep fallback
|
||||
*/
|
||||
search(options: QmdSearchOptions): Promise<QmdSearchResult[]>;
|
||||
/**
|
||||
* Search using QMD (when available)
|
||||
*/
|
||||
qmdSearch(options: QmdSearchOptions): Promise<QmdSearchResult[]>;
|
||||
/**
|
||||
* Grep-based fallback search
|
||||
*/
|
||||
grepFallback(options: QmdSearchOptions): Promise<QmdSearchResult[]>;
|
||||
/**
|
||||
* Find all markdown files in a directory recursively
|
||||
*/
|
||||
findMarkdownFiles(dir: string, files?: string[]): string[];
|
||||
/**
|
||||
* Create a QMD collection for a project
|
||||
*/
|
||||
createCollection(options: QmdCollectionOptions): Promise<QmdResult>;
|
||||
/**
|
||||
* Trigger reindexing of a collection
|
||||
*/
|
||||
index(collectionName: string): Promise<QmdResult>;
|
||||
/**
|
||||
* Get status of a collection
|
||||
*/
|
||||
status(collectionName: string): Promise<QmdStatus>;
|
||||
};
|
||||
//# sourceMappingURL=qmd.d.ts.map
|
||||
1
gsd-memory/dist/qmd.d.ts.map
vendored
Normal file
1
gsd-memory/dist/qmd.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"qmd.d.ts","sourceRoot":"","sources":["../src/qmd.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,eAAO,MAAM,GAAG;IACd;;OAEG;mBACkB,OAAO,CAAC,OAAO,CAAC;IASrC;;OAEG;oBACmB,gBAAgB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAUnE;;OAEG;uBACsB,gBAAgB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAoBtE;;OAEG;0BACyB,gBAAgB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IA8CzE;;OAEG;2BACoB,MAAM,UAAS,MAAM,EAAE,GAAQ,MAAM,EAAE;IAyB9D;;OAEG;8BAC6B,oBAAoB,GAAG,OAAO,CAAC,SAAS,CAAC;IA4BzE;;OAEG;0BACyB,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAsBvD;;OAEG;2BAC0B,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;CAyBzD,CAAC"}
|
||||
199
gsd-memory/dist/qmd.js
vendored
Normal file
199
gsd-memory/dist/qmd.js
vendored
Normal file
@@ -0,0 +1,199 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { readFileSync, readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
const execAsync = promisify(exec);
|
||||
/**
|
||||
* QMD wrapper - provides semantic search when QMD is available,
|
||||
* falls back to grep-based search otherwise
|
||||
*/
|
||||
export const qmd = {
|
||||
/**
|
||||
* Check if QMD is installed and available
|
||||
*/
|
||||
async isAvailable() {
|
||||
try {
|
||||
await execAsync('which qmd');
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Search for content using QMD or grep fallback
|
||||
*/
|
||||
async search(options) {
|
||||
const available = await this.isAvailable();
|
||||
if (available && options.collection) {
|
||||
return this.qmdSearch(options);
|
||||
}
|
||||
return this.grepFallback(options);
|
||||
},
|
||||
/**
|
||||
* Search using QMD (when available)
|
||||
*/
|
||||
async qmdSearch(options) {
|
||||
try {
|
||||
const limit = options.limit || 10;
|
||||
const { stdout } = await execAsync(`qmd query "${options.query}" --collection "${options.collection}" --limit ${limit} --json`);
|
||||
const parsed = JSON.parse(stdout);
|
||||
return (parsed.results || []).map((r) => ({
|
||||
content: r.content,
|
||||
source: r.source,
|
||||
relevance: r.score || 0.5,
|
||||
context: 'qmd'
|
||||
}));
|
||||
}
|
||||
catch {
|
||||
// Fall back to grep if QMD search fails
|
||||
return this.grepFallback(options);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Grep-based fallback search
|
||||
*/
|
||||
async grepFallback(options) {
|
||||
const results = [];
|
||||
const limit = options.limit || 20;
|
||||
for (const searchPath of options.paths) {
|
||||
try {
|
||||
const files = this.findMarkdownFiles(searchPath);
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
const query = options.query.toLowerCase();
|
||||
// Simple relevance: count matches
|
||||
const matches = (content.toLowerCase().match(new RegExp(query, 'g')) || []).length;
|
||||
if (matches > 0) {
|
||||
// Extract context around first match
|
||||
const lowerContent = content.toLowerCase();
|
||||
const matchIndex = lowerContent.indexOf(query);
|
||||
const start = Math.max(0, matchIndex - 100);
|
||||
const end = Math.min(content.length, matchIndex + query.length + 100);
|
||||
const snippet = content.slice(start, end);
|
||||
results.push({
|
||||
content: snippet,
|
||||
source: file,
|
||||
relevance: Math.min(1, matches * 0.1),
|
||||
context: 'grep'
|
||||
});
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Skip files that can't be read
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Skip paths that can't be accessed
|
||||
}
|
||||
}
|
||||
// Sort by relevance and limit
|
||||
return results
|
||||
.sort((a, b) => b.relevance - a.relevance)
|
||||
.slice(0, limit);
|
||||
},
|
||||
/**
|
||||
* Find all markdown files in a directory recursively
|
||||
*/
|
||||
findMarkdownFiles(dir, files = []) {
|
||||
try {
|
||||
const entries = readdirSync(dir);
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
|
||||
this.findMarkdownFiles(fullPath, files);
|
||||
}
|
||||
else if (stat.isFile() && entry.endsWith('.md')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Skip entries that can't be accessed
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Return empty if directory can't be read
|
||||
}
|
||||
return files;
|
||||
},
|
||||
/**
|
||||
* Create a QMD collection for a project
|
||||
*/
|
||||
async createCollection(options) {
|
||||
const available = await this.isAvailable();
|
||||
if (!available) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'qmd_not_available'
|
||||
};
|
||||
}
|
||||
try {
|
||||
const paths = options.paths.join(' ');
|
||||
const contextArg = options.context ? `--context "${options.context}"` : '';
|
||||
await execAsync(`qmd add "${options.name}" ${paths} ${contextArg}`);
|
||||
return { success: true };
|
||||
}
|
||||
catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'create_failed',
|
||||
data: error instanceof Error ? error.message : String(error)
|
||||
};
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Trigger reindexing of a collection
|
||||
*/
|
||||
async index(collectionName) {
|
||||
const available = await this.isAvailable();
|
||||
if (!available) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'qmd_not_available'
|
||||
};
|
||||
}
|
||||
try {
|
||||
await execAsync(`qmd update "${collectionName}"`);
|
||||
return { success: true };
|
||||
}
|
||||
catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'index_failed',
|
||||
data: error instanceof Error ? error.message : String(error)
|
||||
};
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Get status of a collection
|
||||
*/
|
||||
async status(collectionName) {
|
||||
const available = await this.isAvailable();
|
||||
if (!available) {
|
||||
return { available: false };
|
||||
}
|
||||
try {
|
||||
const { stdout } = await execAsync(`qmd status "${collectionName}" --json`);
|
||||
const parsed = JSON.parse(stdout);
|
||||
return {
|
||||
available: true,
|
||||
collection: collectionName,
|
||||
documentCount: parsed.documentCount || 0,
|
||||
lastIndexed: parsed.lastIndexed
|
||||
};
|
||||
}
|
||||
catch {
|
||||
return {
|
||||
available: true,
|
||||
collection: collectionName,
|
||||
documentCount: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
//# sourceMappingURL=qmd.js.map
|
||||
1
gsd-memory/dist/qmd.js.map
vendored
Normal file
1
gsd-memory/dist/qmd.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
44
gsd-memory/dist/registry.d.ts
vendored
Normal file
44
gsd-memory/dist/registry.d.ts
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
export interface RegisteredProject {
|
||||
name: string;
|
||||
path: string;
|
||||
qmdCollection?: string;
|
||||
registeredAt: string;
|
||||
lastIndexed?: string;
|
||||
}
|
||||
export interface Registry {
|
||||
version: string;
|
||||
projects: RegisteredProject[];
|
||||
}
|
||||
/**
|
||||
* Read the project registry
|
||||
*/
|
||||
export declare function readRegistry(): Registry;
|
||||
/**
|
||||
* Write the project registry
|
||||
*/
|
||||
export declare function writeRegistry(registry: Registry): void;
|
||||
/**
|
||||
* Register a new project
|
||||
*/
|
||||
export declare function registerProject(project: Omit<RegisteredProject, 'registeredAt'>): RegisteredProject;
|
||||
/**
|
||||
* Update a project's last indexed time
|
||||
*/
|
||||
export declare function updateProjectIndexTime(name: string): void;
|
||||
/**
|
||||
* Get a project by name
|
||||
*/
|
||||
export declare function getProject(name: string): RegisteredProject | undefined;
|
||||
/**
|
||||
* Get all registered projects
|
||||
*/
|
||||
export declare function getAllProjects(): RegisteredProject[];
|
||||
/**
|
||||
* Remove a project from the registry
|
||||
*/
|
||||
export declare function unregisterProject(name: string): boolean;
|
||||
/**
|
||||
* Get the registry path (for testing)
|
||||
*/
|
||||
export declare function getRegistryPath(): string;
|
||||
//# sourceMappingURL=registry.d.ts.map
|
||||
1
gsd-memory/dist/registry.d.ts.map
vendored
Normal file
1
gsd-memory/dist/registry.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B;AAcD;;GAEG;AACH,wBAAgB,YAAY,IAAI,QAAQ,CAavC;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAGtD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,cAAc,CAAC,GAAG,iBAAiB,CA2BnG;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAQzD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAGtE;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,iBAAiB,EAAE,CAEpD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAWvD;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC"}
|
||||
106
gsd-memory/dist/registry.js
vendored
Normal file
106
gsd-memory/dist/registry.js
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
const REGISTRY_DIR = join(homedir(), '.gsd');
|
||||
const REGISTRY_PATH = join(REGISTRY_DIR, 'projects.json');
|
||||
/**
|
||||
* Ensure the registry directory exists
|
||||
*/
|
||||
function ensureRegistryDir() {
|
||||
if (!existsSync(REGISTRY_DIR)) {
|
||||
mkdirSync(REGISTRY_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Read the project registry
|
||||
*/
|
||||
export function readRegistry() {
|
||||
ensureRegistryDir();
|
||||
if (!existsSync(REGISTRY_PATH)) {
|
||||
return { version: '1.0.0', projects: [] };
|
||||
}
|
||||
try {
|
||||
const content = readFileSync(REGISTRY_PATH, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
catch {
|
||||
return { version: '1.0.0', projects: [] };
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Write the project registry
|
||||
*/
|
||||
export function writeRegistry(registry) {
|
||||
ensureRegistryDir();
|
||||
writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2));
|
||||
}
|
||||
/**
|
||||
* Register a new project
|
||||
*/
|
||||
export function registerProject(project) {
|
||||
const registry = readRegistry();
|
||||
// Check if project already exists
|
||||
const existingIndex = registry.projects.findIndex(p => p.path === project.path || p.name === project.name);
|
||||
const newProject = {
|
||||
...project,
|
||||
registeredAt: new Date().toISOString()
|
||||
};
|
||||
if (existingIndex >= 0) {
|
||||
// Update existing project
|
||||
registry.projects[existingIndex] = {
|
||||
...registry.projects[existingIndex],
|
||||
...newProject,
|
||||
registeredAt: registry.projects[existingIndex].registeredAt
|
||||
};
|
||||
}
|
||||
else {
|
||||
// Add new project
|
||||
registry.projects.push(newProject);
|
||||
}
|
||||
writeRegistry(registry);
|
||||
return newProject;
|
||||
}
|
||||
/**
|
||||
* Update a project's last indexed time
|
||||
*/
|
||||
export function updateProjectIndexTime(name) {
|
||||
const registry = readRegistry();
|
||||
const project = registry.projects.find(p => p.name === name);
|
||||
if (project) {
|
||||
project.lastIndexed = new Date().toISOString();
|
||||
writeRegistry(registry);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get a project by name
|
||||
*/
|
||||
export function getProject(name) {
|
||||
const registry = readRegistry();
|
||||
return registry.projects.find(p => p.name === name);
|
||||
}
|
||||
/**
|
||||
* Get all registered projects
|
||||
*/
|
||||
export function getAllProjects() {
|
||||
return readRegistry().projects;
|
||||
}
|
||||
/**
|
||||
* Remove a project from the registry
|
||||
*/
|
||||
export function unregisterProject(name) {
|
||||
const registry = readRegistry();
|
||||
const index = registry.projects.findIndex(p => p.name === name);
|
||||
if (index >= 0) {
|
||||
registry.projects.splice(index, 1);
|
||||
writeRegistry(registry);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Get the registry path (for testing)
|
||||
*/
|
||||
export function getRegistryPath() {
|
||||
return REGISTRY_PATH;
|
||||
}
|
||||
//# sourceMappingURL=registry.js.map
|
||||
1
gsd-memory/dist/registry.js.map
vendored
Normal file
1
gsd-memory/dist/registry.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
|
||||
20
gsd-memory/dist/tools/decisions.d.ts
vendored
Normal file
20
gsd-memory/dist/tools/decisions.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface DecisionResult {
|
||||
decision: string;
|
||||
rationale?: string;
|
||||
project: string;
|
||||
phase?: string;
|
||||
source: string;
|
||||
date?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
export interface DecisionOptions {
|
||||
query?: string;
|
||||
project?: string;
|
||||
tags?: string[];
|
||||
limit?: number;
|
||||
}
|
||||
/**
|
||||
* Search for decisions across all registered GSD projects
|
||||
*/
|
||||
export declare function findDecisions(options: DecisionOptions): Promise<DecisionResult[]>;
|
||||
//# sourceMappingURL=decisions.d.ts.map
|
||||
1
gsd-memory/dist/tools/decisions.d.ts.map
vendored
Normal file
1
gsd-memory/dist/tools/decisions.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"decisions.d.ts","sourceRoot":"","sources":["../../src/tools/decisions.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAyCvF"}
|
||||
120
gsd-memory/dist/tools/decisions.js
vendored
Normal file
120
gsd-memory/dist/tools/decisions.js
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
import { getAllProjects } from '../registry.js';
|
||||
import { extractSummary } from '../extractors/summary.js';
|
||||
import { extractProject } from '../extractors/project.js';
|
||||
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
/**
|
||||
* Search for decisions across all registered GSD projects
|
||||
*/
|
||||
export async function findDecisions(options) {
|
||||
const projects = options.project
|
||||
? getAllProjects().filter(p => p.name === options.project)
|
||||
: getAllProjects();
|
||||
const allDecisions = [];
|
||||
for (const project of projects) {
|
||||
const planningPath = join(project.path, '.planning');
|
||||
if (!existsSync(planningPath))
|
||||
continue;
|
||||
// Extract decisions from PROJECT.md
|
||||
const projectDecisions = extractProjectDecisions(planningPath, project.name);
|
||||
allDecisions.push(...projectDecisions);
|
||||
// Extract decisions from all SUMMARY.md files
|
||||
const summaryDecisions = extractSummaryDecisions(planningPath, project.name);
|
||||
allDecisions.push(...summaryDecisions);
|
||||
}
|
||||
// Filter by query if provided
|
||||
let filtered = allDecisions;
|
||||
if (options.query) {
|
||||
const query = options.query.toLowerCase();
|
||||
filtered = allDecisions.filter(d => d.decision.toLowerCase().includes(query) ||
|
||||
d.rationale?.toLowerCase().includes(query));
|
||||
}
|
||||
// Filter by tags if provided
|
||||
if (options.tags && options.tags.length > 0) {
|
||||
const tags = options.tags.map(t => t.toLowerCase());
|
||||
filtered = filtered.filter(d => d.tags?.some(t => tags.includes(t.toLowerCase())));
|
||||
}
|
||||
// Apply limit
|
||||
const limit = options.limit || 50;
|
||||
return filtered.slice(0, limit);
|
||||
}
|
||||
/**
|
||||
* Extract decisions from PROJECT.md
|
||||
*/
|
||||
function extractProjectDecisions(planningPath, projectName) {
|
||||
const projectPath = join(planningPath, 'PROJECT.md');
|
||||
if (!existsSync(projectPath))
|
||||
return [];
|
||||
try {
|
||||
const content = readFileSync(projectPath, 'utf-8');
|
||||
const projectData = extractProject(content);
|
||||
return projectData.decisions.map(d => ({
|
||||
decision: d.decision,
|
||||
rationale: d.rationale,
|
||||
project: projectName,
|
||||
source: projectPath
|
||||
}));
|
||||
}
|
||||
catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Extract decisions from all SUMMARY.md files in a project
|
||||
*/
|
||||
function extractSummaryDecisions(planningPath, projectName) {
|
||||
const decisions = [];
|
||||
const phasesPath = join(planningPath, 'phases');
|
||||
if (!existsSync(phasesPath))
|
||||
return decisions;
|
||||
// Find all SUMMARY.md files
|
||||
const summaryFiles = findSummaryFiles(phasesPath);
|
||||
for (const file of summaryFiles) {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
const summaryData = extractSummary(content);
|
||||
for (const decision of summaryData.decisions) {
|
||||
decisions.push({
|
||||
decision: decision.decision,
|
||||
project: projectName,
|
||||
phase: summaryData.phase,
|
||||
source: file,
|
||||
date: summaryData.completed,
|
||||
tags: summaryData.tags
|
||||
});
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Skip files that can't be parsed
|
||||
}
|
||||
}
|
||||
return decisions;
|
||||
}
|
||||
/**
|
||||
* Recursively find all SUMMARY.md files
|
||||
*/
|
||||
function findSummaryFiles(dir, files = []) {
|
||||
try {
|
||||
const entries = readdirSync(dir);
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
findSummaryFiles(fullPath, files);
|
||||
}
|
||||
else if (entry.toUpperCase().includes('SUMMARY') && entry.endsWith('.md')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Skip entries that can't be accessed
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Return empty if directory can't be read
|
||||
}
|
||||
return files;
|
||||
}
|
||||
//# sourceMappingURL=decisions.js.map
|
||||
1
gsd-memory/dist/tools/decisions.js.map
vendored
Normal file
1
gsd-memory/dist/tools/decisions.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"decisions.js","sourceRoot":"","sources":["../../src/tools/decisions.ts"],"names":[],"mappings":"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"}
|
||||
23
gsd-memory/dist/tools/index-tool.d.ts
vendored
Normal file
23
gsd-memory/dist/tools/index-tool.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface IndexOptions {
|
||||
path?: string;
|
||||
project?: string;
|
||||
}
|
||||
export interface IndexResult {
|
||||
success: boolean;
|
||||
project?: string;
|
||||
documentsIndexed?: number;
|
||||
error?: string;
|
||||
qmdUsed: boolean;
|
||||
}
|
||||
/**
|
||||
* Trigger indexing/update for a project
|
||||
*/
|
||||
export declare function index(options: IndexOptions): Promise<IndexResult>;
|
||||
/**
|
||||
* Index all registered projects
|
||||
*/
|
||||
export declare function indexAll(): Promise<{
|
||||
success: boolean;
|
||||
results: IndexResult[];
|
||||
}>;
|
||||
//# sourceMappingURL=index-tool.d.ts.map
|
||||
1
gsd-memory/dist/tools/index-tool.d.ts.map
vendored
Normal file
1
gsd-memory/dist/tools/index-tool.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index-tool.d.ts","sourceRoot":"","sources":["../../src/tools/index-tool.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA2FvE;AAED;;GAEG;AACH,wBAAsB,QAAQ,IAAI,OAAO,CAAC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB,CAAC,CAYD"}
|
||||
107
gsd-memory/dist/tools/index-tool.js
vendored
Normal file
107
gsd-memory/dist/tools/index-tool.js
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
import { getProject, updateProjectIndexTime, getAllProjects } from '../registry.js';
|
||||
import { qmd } from '../qmd.js';
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
/**
|
||||
* Trigger indexing/update for a project
|
||||
*/
|
||||
export async function index(options) {
|
||||
// Find the project
|
||||
let projectName;
|
||||
let projectPath;
|
||||
let qmdCollection;
|
||||
if (options.project) {
|
||||
const project = getProject(options.project);
|
||||
if (!project) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Project not found: ${options.project}`,
|
||||
qmdUsed: false
|
||||
};
|
||||
}
|
||||
projectName = project.name;
|
||||
projectPath = project.path;
|
||||
qmdCollection = project.qmdCollection;
|
||||
}
|
||||
else if (options.path) {
|
||||
// Find project by path
|
||||
const projects = getAllProjects();
|
||||
const project = projects.find(p => p.path === options.path);
|
||||
if (!project) {
|
||||
return {
|
||||
success: false,
|
||||
error: `No project registered for path: ${options.path}`,
|
||||
qmdUsed: false
|
||||
};
|
||||
}
|
||||
projectName = project.name;
|
||||
projectPath = project.path;
|
||||
qmdCollection = project.qmdCollection;
|
||||
}
|
||||
else {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Either project or path must be specified',
|
||||
qmdUsed: false
|
||||
};
|
||||
}
|
||||
// Verify planning directory exists
|
||||
const planningPath = join(projectPath, '.planning');
|
||||
if (!existsSync(planningPath)) {
|
||||
return {
|
||||
success: false,
|
||||
project: projectName,
|
||||
error: `No .planning directory found at: ${planningPath}`,
|
||||
qmdUsed: false
|
||||
};
|
||||
}
|
||||
// Check if QMD is available
|
||||
const qmdAvailable = await qmd.isAvailable();
|
||||
if (qmdAvailable && qmdCollection) {
|
||||
// Use QMD to update the collection
|
||||
const result = await qmd.index(qmdCollection);
|
||||
if (result.success) {
|
||||
updateProjectIndexTime(projectName);
|
||||
// Get document count
|
||||
const status = await qmd.status(qmdCollection);
|
||||
return {
|
||||
success: true,
|
||||
project: projectName,
|
||||
documentsIndexed: status.documentCount,
|
||||
qmdUsed: true
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
success: false,
|
||||
project: projectName,
|
||||
error: `QMD indexing failed: ${result.reason}`,
|
||||
qmdUsed: true
|
||||
};
|
||||
}
|
||||
}
|
||||
else {
|
||||
// No QMD - just update the timestamp
|
||||
// Grep fallback will search files directly
|
||||
updateProjectIndexTime(projectName);
|
||||
return {
|
||||
success: true,
|
||||
project: projectName,
|
||||
qmdUsed: false
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Index all registered projects
|
||||
*/
|
||||
export async function indexAll() {
|
||||
const projects = getAllProjects();
|
||||
const results = [];
|
||||
for (const project of projects) {
|
||||
const result = await index({ project: project.name });
|
||||
results.push(result);
|
||||
}
|
||||
const success = results.every(r => r.success);
|
||||
return { success, results };
|
||||
}
|
||||
//# sourceMappingURL=index-tool.js.map
|
||||
1
gsd-memory/dist/tools/index-tool.js.map
vendored
Normal file
1
gsd-memory/dist/tools/index-tool.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index-tool.js","sourceRoot":"","sources":["../../src/tools/index-tool.ts"],"names":[],"mappings":"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"}
|
||||
19
gsd-memory/dist/tools/patterns.d.ts
vendored
Normal file
19
gsd-memory/dist/tools/patterns.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface PatternResult {
|
||||
pattern: string;
|
||||
project: string;
|
||||
phase: string;
|
||||
source: string;
|
||||
date?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
export interface PatternOptions {
|
||||
query?: string;
|
||||
project?: string;
|
||||
tags?: string[];
|
||||
limit?: number;
|
||||
}
|
||||
/**
|
||||
* Search for patterns established across all registered GSD projects
|
||||
*/
|
||||
export declare function findPatterns(options: PatternOptions): Promise<PatternResult[]>;
|
||||
//# sourceMappingURL=patterns.d.ts.map
|
||||
1
gsd-memory/dist/tools/patterns.d.ts.map
vendored
Normal file
1
gsd-memory/dist/tools/patterns.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"patterns.d.ts","sourceRoot":"","sources":["../../src/tools/patterns.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAuDpF"}
|
||||
81
gsd-memory/dist/tools/patterns.js
vendored
Normal file
81
gsd-memory/dist/tools/patterns.js
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
import { getAllProjects } from '../registry.js';
|
||||
import { extractSummary } from '../extractors/summary.js';
|
||||
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
/**
|
||||
* Search for patterns established across all registered GSD projects
|
||||
*/
|
||||
export async function findPatterns(options) {
|
||||
const projects = options.project
|
||||
? getAllProjects().filter(p => p.name === options.project)
|
||||
: getAllProjects();
|
||||
const allPatterns = [];
|
||||
for (const project of projects) {
|
||||
const phasesPath = join(project.path, '.planning', 'phases');
|
||||
if (!existsSync(phasesPath))
|
||||
continue;
|
||||
// Find all SUMMARY.md files and extract patterns
|
||||
const summaryFiles = findSummaryFiles(phasesPath);
|
||||
for (const file of summaryFiles) {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
const summaryData = extractSummary(content);
|
||||
for (const pattern of summaryData.patterns) {
|
||||
allPatterns.push({
|
||||
pattern,
|
||||
project: project.name,
|
||||
phase: summaryData.phase,
|
||||
source: file,
|
||||
date: summaryData.completed,
|
||||
tags: summaryData.tags
|
||||
});
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Skip files that can't be parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
// Filter by query if provided
|
||||
let filtered = allPatterns;
|
||||
if (options.query) {
|
||||
const query = options.query.toLowerCase();
|
||||
filtered = allPatterns.filter(p => p.pattern.toLowerCase().includes(query));
|
||||
}
|
||||
// Filter by tags if provided
|
||||
if (options.tags && options.tags.length > 0) {
|
||||
const tags = options.tags.map(t => t.toLowerCase());
|
||||
filtered = filtered.filter(p => p.tags?.some(t => tags.includes(t.toLowerCase())));
|
||||
}
|
||||
// Apply limit
|
||||
const limit = options.limit || 50;
|
||||
return filtered.slice(0, limit);
|
||||
}
|
||||
/**
|
||||
* Recursively find all SUMMARY.md files
|
||||
*/
|
||||
function findSummaryFiles(dir, files = []) {
|
||||
try {
|
||||
const entries = readdirSync(dir);
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
findSummaryFiles(fullPath, files);
|
||||
}
|
||||
else if (entry.toUpperCase().includes('SUMMARY') && entry.endsWith('.md')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Skip entries that can't be accessed
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Return empty if directory can't be read
|
||||
}
|
||||
return files;
|
||||
}
|
||||
//# sourceMappingURL=patterns.js.map
|
||||
1
gsd-memory/dist/tools/patterns.js.map
vendored
Normal file
1
gsd-memory/dist/tools/patterns.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"patterns.js","sourceRoot":"","sources":["../../src/tools/patterns.ts"],"names":[],"mappings":"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"}
|
||||
18
gsd-memory/dist/tools/pitfalls.d.ts
vendored
Normal file
18
gsd-memory/dist/tools/pitfalls.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import { type Pitfall } from '../extractors/research.js';
|
||||
export interface PitfallResult extends Pitfall {
|
||||
project: string;
|
||||
phase?: string;
|
||||
domain: string;
|
||||
source: string;
|
||||
}
|
||||
export interface PitfallOptions {
|
||||
query?: string;
|
||||
project?: string;
|
||||
domain?: string;
|
||||
limit?: number;
|
||||
}
|
||||
/**
|
||||
* Search for pitfalls documented across all registered GSD projects
|
||||
*/
|
||||
export declare function findPitfalls(options: PitfallOptions): Promise<PitfallResult[]>;
|
||||
//# sourceMappingURL=pitfalls.d.ts.map
|
||||
1
gsd-memory/dist/tools/pitfalls.d.ts.map
vendored
Normal file
1
gsd-memory/dist/tools/pitfalls.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"pitfalls.d.ts","sourceRoot":"","sources":["../../src/tools/pitfalls.ts"],"names":[],"mappings":"AACA,OAAO,EAAmB,KAAK,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAI1E,MAAM,WAAW,aAAc,SAAQ,OAAO;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA4DpF"}
|
||||
85
gsd-memory/dist/tools/pitfalls.js
vendored
Normal file
85
gsd-memory/dist/tools/pitfalls.js
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
import { getAllProjects } from '../registry.js';
|
||||
import { extractResearch } from '../extractors/research.js';
|
||||
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
/**
|
||||
* Search for pitfalls documented across all registered GSD projects
|
||||
*/
|
||||
export async function findPitfalls(options) {
|
||||
const projects = options.project
|
||||
? getAllProjects().filter(p => p.name === options.project)
|
||||
: getAllProjects();
|
||||
const allPitfalls = [];
|
||||
for (const project of projects) {
|
||||
const planningPath = join(project.path, '.planning');
|
||||
if (!existsSync(planningPath))
|
||||
continue;
|
||||
// Find all RESEARCH.md files
|
||||
const researchFiles = findResearchFiles(planningPath);
|
||||
for (const file of researchFiles) {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
const researchData = extractResearch(content);
|
||||
// Extract phase from path if present
|
||||
const phaseMatch = file.match(/phases\/(\d+-[^/]+)/);
|
||||
const phase = phaseMatch ? phaseMatch[1] : undefined;
|
||||
for (const pitfall of researchData.pitfalls) {
|
||||
allPitfalls.push({
|
||||
...pitfall,
|
||||
project: project.name,
|
||||
phase,
|
||||
domain: researchData.domain,
|
||||
source: file
|
||||
});
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Skip files that can't be parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
// Filter by query if provided
|
||||
let filtered = allPitfalls;
|
||||
if (options.query) {
|
||||
const query = options.query.toLowerCase();
|
||||
filtered = allPitfalls.filter(p => p.name.toLowerCase().includes(query) ||
|
||||
p.description.toLowerCase().includes(query) ||
|
||||
p.prevention.toLowerCase().includes(query));
|
||||
}
|
||||
// Filter by domain if provided
|
||||
if (options.domain) {
|
||||
const domain = options.domain.toLowerCase();
|
||||
filtered = filtered.filter(p => p.domain.toLowerCase().includes(domain));
|
||||
}
|
||||
// Apply limit
|
||||
const limit = options.limit || 50;
|
||||
return filtered.slice(0, limit);
|
||||
}
|
||||
/**
|
||||
* Recursively find all RESEARCH.md files
|
||||
*/
|
||||
function findResearchFiles(dir, files = []) {
|
||||
try {
|
||||
const entries = readdirSync(dir);
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isDirectory() && !entry.startsWith('.')) {
|
||||
findResearchFiles(fullPath, files);
|
||||
}
|
||||
else if (entry.toUpperCase().includes('RESEARCH') && entry.endsWith('.md')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Skip entries that can't be accessed
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Return empty if directory can't be read
|
||||
}
|
||||
return files;
|
||||
}
|
||||
//# sourceMappingURL=pitfalls.js.map
|
||||
1
gsd-memory/dist/tools/pitfalls.js.map
vendored
Normal file
1
gsd-memory/dist/tools/pitfalls.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"pitfalls.js","sourceRoot":"","sources":["../../src/tools/pitfalls.ts"],"names":[],"mappings":"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"}
|
||||
37
gsd-memory/dist/tools/register.d.ts
vendored
Normal file
37
gsd-memory/dist/tools/register.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
import { type RegisteredProject } from '../registry.js';
|
||||
export interface RegisterOptions {
|
||||
path: string;
|
||||
name?: string;
|
||||
}
|
||||
export interface RegisterResult {
|
||||
success: boolean;
|
||||
project?: RegisteredProject;
|
||||
qmdCollection?: string;
|
||||
error?: string;
|
||||
}
|
||||
/**
|
||||
* Register a project with GSD memory
|
||||
*/
|
||||
export declare function register(options: RegisterOptions): Promise<RegisterResult>;
|
||||
/**
|
||||
* Get status of all registered projects
|
||||
*/
|
||||
export declare function status(): Promise<{
|
||||
qmdAvailable: boolean;
|
||||
projects: Array<{
|
||||
name: string;
|
||||
path: string;
|
||||
qmdCollection?: string;
|
||||
registeredAt: string;
|
||||
lastIndexed?: string;
|
||||
documentCount?: number;
|
||||
}>;
|
||||
}>;
|
||||
/**
|
||||
* Unregister a project
|
||||
*/
|
||||
export declare function unregister(name: string): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
//# sourceMappingURL=register.d.ts.map
|
||||
1
gsd-memory/dist/tools/register.d.ts.map
vendored
Normal file
1
gsd-memory/dist/tools/register.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../src/tools/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkE,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAKxH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAqDhF;AAED;;GAEG;AACH,wBAAsB,MAAM,IAAI,OAAO,CAAC;IACtC,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,KAAK,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC,CAAC;CACJ,CAAC,CA4BD;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAwB5F"}
|
||||
105
gsd-memory/dist/tools/register.js
vendored
Normal file
105
gsd-memory/dist/tools/register.js
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
import { registerProject, getProject, getAllProjects, unregisterProject } from '../registry.js';
|
||||
import { qmd } from '../qmd.js';
|
||||
import { existsSync } from 'fs';
|
||||
import { join, basename } from 'path';
|
||||
/**
|
||||
* Register a project with GSD memory
|
||||
*/
|
||||
export async function register(options) {
|
||||
// Validate path exists
|
||||
if (!existsSync(options.path)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Path does not exist: ${options.path}`
|
||||
};
|
||||
}
|
||||
// Validate .planning directory exists
|
||||
const planningPath = join(options.path, '.planning');
|
||||
if (!existsSync(planningPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `No .planning directory found at: ${planningPath}`
|
||||
};
|
||||
}
|
||||
// Determine project name
|
||||
const name = options.name || basename(options.path);
|
||||
// Check if QMD is available for semantic search
|
||||
const qmdAvailable = await qmd.isAvailable();
|
||||
let qmdCollection;
|
||||
if (qmdAvailable) {
|
||||
// Create QMD collection
|
||||
qmdCollection = `gsd-${name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
|
||||
const qmdResult = await qmd.createCollection({
|
||||
name: qmdCollection,
|
||||
paths: [planningPath],
|
||||
context: `GSD project: ${name}`
|
||||
});
|
||||
if (!qmdResult.success) {
|
||||
// QMD collection creation failed, but we can still register
|
||||
qmdCollection = undefined;
|
||||
}
|
||||
}
|
||||
// Register in local registry
|
||||
const project = registerProject({
|
||||
name,
|
||||
path: options.path,
|
||||
qmdCollection
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
project,
|
||||
qmdCollection
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get status of all registered projects
|
||||
*/
|
||||
export async function status() {
|
||||
const qmdAvailable = await qmd.isAvailable();
|
||||
const projects = getAllProjects();
|
||||
const enrichedProjects = await Promise.all(projects.map(async (project) => {
|
||||
let documentCount;
|
||||
if (qmdAvailable && project.qmdCollection) {
|
||||
const qmdStatus = await qmd.status(project.qmdCollection);
|
||||
documentCount = qmdStatus.documentCount;
|
||||
}
|
||||
return {
|
||||
name: project.name,
|
||||
path: project.path,
|
||||
qmdCollection: project.qmdCollection,
|
||||
registeredAt: project.registeredAt,
|
||||
lastIndexed: project.lastIndexed,
|
||||
documentCount
|
||||
};
|
||||
}));
|
||||
return {
|
||||
qmdAvailable,
|
||||
projects: enrichedProjects
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Unregister a project
|
||||
*/
|
||||
export async function unregister(name) {
|
||||
const project = getProject(name);
|
||||
if (!project) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Project not found: ${name}`
|
||||
};
|
||||
}
|
||||
// Remove from QMD if it has a collection
|
||||
if (project.qmdCollection && await qmd.isAvailable()) {
|
||||
try {
|
||||
// QMD doesn't have a direct remove command, so we just skip
|
||||
// The collection will be orphaned but harmless
|
||||
}
|
||||
catch {
|
||||
// Ignore QMD errors
|
||||
}
|
||||
}
|
||||
// Remove from registry
|
||||
unregisterProject(name);
|
||||
return { success: true };
|
||||
}
|
||||
//# sourceMappingURL=register.js.map
|
||||
1
gsd-memory/dist/tools/register.js.map
vendored
Normal file
1
gsd-memory/dist/tools/register.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/tools/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,iBAAiB,EAA0B,MAAM,gBAAgB,CAAC;AACxH,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AActC;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,OAAwB;IACrD,uBAAuB;IACvB,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,wBAAwB,OAAO,CAAC,IAAI,EAAE;SAC9C,CAAC;IACJ,CAAC;IAED,sCAAsC;IACtC,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACrD,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAC9B,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,oCAAoC,YAAY,EAAE;SAC1D,CAAC;IACJ,CAAC;IAED,yBAAyB;IACzB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpD,gDAAgD;IAChD,MAAM,YAAY,GAAG,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC;IAC7C,IAAI,aAAiC,CAAC;IAEtC,IAAI,YAAY,EAAE,CAAC;QACjB,wBAAwB;QACxB,aAAa,GAAG,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC,EAAE,CAAC;QAEvE,MAAM,SAAS,GAAG,MAAM,GAAG,CAAC,gBAAgB,CAAC;YAC3C,IAAI,EAAE,aAAa;YACnB,KAAK,EAAE,CAAC,YAAY,CAAC;YACrB,OAAO,EAAE,gBAAgB,IAAI,EAAE;SAChC,CAAC,CAAC;QAEH,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;YACvB,4DAA4D;YAC5D,aAAa,GAAG,SAAS,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,6BAA6B;IAC7B,MAAM,OAAO,GAAG,eAAe,CAAC;QAC9B,IAAI;QACJ,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,aAAa;KACd,CAAC,CAAC;IAEH,OAAO;QACL,OAAO,EAAE,IAAI;QACb,OAAO;QACP,aAAa;KACd,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM;IAW1B,MAAM,YAAY,GAAG,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC;IAC7C,MAAM,QAAQ,GAAG,cAAc,EAAE,CAAC;IAElC,MAAM,gBAAgB,GAAG,MAAM,OAAO,CAAC,GAAG,CACxC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;QAC7B,IAAI,aAAiC,CAAC;QAEtC,IAAI,YAAY,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;YAC1C,MAAM,SAAS,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;YAC1D,aAAa,GAAG,SAAS,CAAC,aAAa,CAAC;QAC1C,CAAC;QAED,OAAO;YACL,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,aAAa,EAAE,OAAO,CAAC,aAAa;YACpC,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,aAAa;SACd,CAAC;IACJ,CAAC,CAAC,CACH,CAAC;IAEF,OAAO;QACL,YAAY;QACZ,QAAQ,EAAE,gBAAgB;KAC3B,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY;IAC3C,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAEjC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,sBAAsB,IAAI,EAAE;SACpC,CAAC;IACJ,CAAC;IAED,yCAAyC;IACzC,IAAI,OAAO,CAAC,aAAa,IAAI,MAAM,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC;QACrD,IAAI,CAAC;YACH,4DAA4D;YAC5D,+CAA+C;QACjD,CAAC;QAAC,MAAM,CAAC;YACP,oBAAoB;QACtB,CAAC;IACH,CAAC;IAED,uBAAuB;IACvB,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAExB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC"}
|
||||
19
gsd-memory/dist/tools/search.d.ts
vendored
Normal file
19
gsd-memory/dist/tools/search.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface SearchResult {
|
||||
content: string;
|
||||
source: string;
|
||||
project: string;
|
||||
phase?: string;
|
||||
relevance: number;
|
||||
documentType?: 'summary' | 'research' | 'project' | 'plan' | 'other';
|
||||
}
|
||||
export interface SearchOptions {
|
||||
query: string;
|
||||
project?: string;
|
||||
tags?: string[];
|
||||
limit?: number;
|
||||
}
|
||||
/**
|
||||
* Search across all registered GSD projects
|
||||
*/
|
||||
export declare function search(options: SearchOptions): Promise<SearchResult[]>;
|
||||
//# sourceMappingURL=search.d.ts.map
|
||||
1
gsd-memory/dist/tools/search.d.ts.map
vendored
Normal file
1
gsd-memory/dist/tools/search.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/tools/search.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;CACtE;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAqC5E"}
|
||||
92
gsd-memory/dist/tools/search.js
vendored
Normal file
92
gsd-memory/dist/tools/search.js
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
import { qmd } from '../qmd.js';
|
||||
import { getAllProjects } from '../registry.js';
|
||||
import { extractFrontmatter } from '../extractors/frontmatter.js';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join, basename } from 'path';
|
||||
/**
|
||||
* Search across all registered GSD projects
|
||||
*/
|
||||
export async function search(options) {
|
||||
const projects = options.project
|
||||
? getAllProjects().filter(p => p.name === options.project)
|
||||
: getAllProjects();
|
||||
if (projects.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const allResults = [];
|
||||
for (const project of projects) {
|
||||
const planningPath = join(project.path, '.planning');
|
||||
if (!existsSync(planningPath))
|
||||
continue;
|
||||
// Search using QMD or grep fallback
|
||||
const searchResults = await qmd.search({
|
||||
query: options.query,
|
||||
paths: [planningPath],
|
||||
collection: project.qmdCollection,
|
||||
limit: options.limit || 20
|
||||
});
|
||||
// Enrich results with GSD context
|
||||
for (const result of searchResults) {
|
||||
const enriched = enrichResult(result, project.name);
|
||||
allResults.push(enriched);
|
||||
}
|
||||
}
|
||||
// Sort by relevance
|
||||
allResults.sort((a, b) => b.relevance - a.relevance);
|
||||
// Apply limit
|
||||
const limit = options.limit || 20;
|
||||
return allResults.slice(0, limit);
|
||||
}
|
||||
/**
|
||||
* Enrich a search result with GSD-specific context
|
||||
*/
|
||||
function enrichResult(result, projectName) {
|
||||
const enriched = {
|
||||
content: result.content,
|
||||
source: result.source,
|
||||
project: projectName,
|
||||
relevance: result.relevance,
|
||||
documentType: getDocumentType(result.source)
|
||||
};
|
||||
// Try to extract phase from frontmatter or path
|
||||
enriched.phase = extractPhase(result.source);
|
||||
return enriched;
|
||||
}
|
||||
/**
|
||||
* Determine document type from file path/name
|
||||
*/
|
||||
function getDocumentType(source) {
|
||||
const filename = basename(source).toUpperCase();
|
||||
if (filename.includes('SUMMARY'))
|
||||
return 'summary';
|
||||
if (filename.includes('RESEARCH'))
|
||||
return 'research';
|
||||
if (filename.includes('PROJECT'))
|
||||
return 'project';
|
||||
if (filename.includes('PLAN'))
|
||||
return 'plan';
|
||||
return 'other';
|
||||
}
|
||||
/**
|
||||
* Extract phase identifier from file path or content
|
||||
*/
|
||||
function extractPhase(source) {
|
||||
// Try to get from path: .planning/phases/01-foundation/...
|
||||
const pathMatch = source.match(/phases\/(\d+-[^/]+)/);
|
||||
if (pathMatch)
|
||||
return pathMatch[1];
|
||||
// Try to get from frontmatter
|
||||
try {
|
||||
if (existsSync(source)) {
|
||||
const content = readFileSync(source, 'utf-8');
|
||||
const frontmatter = extractFrontmatter(content);
|
||||
if (frontmatter.phase)
|
||||
return String(frontmatter.phase);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Ignore errors reading file
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
//# sourceMappingURL=search.js.map
|
||||
1
gsd-memory/dist/tools/search.js.map
vendored
Normal file
1
gsd-memory/dist/tools/search.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/tools/search.ts"],"names":[],"mappings":"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"}
|
||||
19
gsd-memory/dist/tools/stack.d.ts
vendored
Normal file
19
gsd-memory/dist/tools/stack.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface StackResult {
|
||||
library: string;
|
||||
version?: string;
|
||||
purpose?: string;
|
||||
project: string;
|
||||
phase?: string;
|
||||
source: string;
|
||||
sourceType: 'summary' | 'research';
|
||||
}
|
||||
export interface StackOptions {
|
||||
query?: string;
|
||||
project?: string;
|
||||
limit?: number;
|
||||
}
|
||||
/**
|
||||
* Search for tech stack entries across all registered GSD projects
|
||||
*/
|
||||
export declare function findStack(options: StackOptions): Promise<StackResult[]>;
|
||||
//# sourceMappingURL=stack.d.ts.map
|
||||
1
gsd-memory/dist/tools/stack.d.ts.map
vendored
Normal file
1
gsd-memory/dist/tools/stack.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"stack.d.ts","sourceRoot":"","sources":["../../src/tools/stack.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,SAAS,GAAG,UAAU,CAAC;CACpC;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CA0C7E"}
|
||||
137
gsd-memory/dist/tools/stack.js
vendored
Normal file
137
gsd-memory/dist/tools/stack.js
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
import { getAllProjects } from '../registry.js';
|
||||
import { extractSummary } from '../extractors/summary.js';
|
||||
import { extractResearch } from '../extractors/research.js';
|
||||
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
/**
|
||||
* Search for tech stack entries across all registered GSD projects
|
||||
*/
|
||||
export async function findStack(options) {
|
||||
const projects = options.project
|
||||
? getAllProjects().filter(p => p.name === options.project)
|
||||
: getAllProjects();
|
||||
const allStack = [];
|
||||
for (const project of projects) {
|
||||
const planningPath = join(project.path, '.planning');
|
||||
if (!existsSync(planningPath))
|
||||
continue;
|
||||
// Extract from SUMMARY.md files
|
||||
const summaryStack = extractFromSummaries(planningPath, project.name);
|
||||
allStack.push(...summaryStack);
|
||||
// Extract from RESEARCH.md files
|
||||
const researchStack = extractFromResearch(planningPath, project.name);
|
||||
allStack.push(...researchStack);
|
||||
}
|
||||
// Filter by query if provided
|
||||
let filtered = allStack;
|
||||
if (options.query) {
|
||||
const query = options.query.toLowerCase();
|
||||
filtered = allStack.filter(s => s.library.toLowerCase().includes(query) ||
|
||||
s.purpose?.toLowerCase().includes(query));
|
||||
}
|
||||
// Deduplicate by library name within each project
|
||||
const seen = new Set();
|
||||
const deduplicated = filtered.filter(s => {
|
||||
const key = `${s.project}:${s.library}`;
|
||||
if (seen.has(key))
|
||||
return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
// Apply limit
|
||||
const limit = options.limit || 100;
|
||||
return deduplicated.slice(0, limit);
|
||||
}
|
||||
/**
|
||||
* Extract tech stack from all SUMMARY.md files
|
||||
*/
|
||||
function extractFromSummaries(planningPath, projectName) {
|
||||
const stack = [];
|
||||
const phasesPath = join(planningPath, 'phases');
|
||||
if (!existsSync(phasesPath))
|
||||
return stack;
|
||||
const summaryFiles = findFiles(phasesPath, 'SUMMARY');
|
||||
for (const file of summaryFiles) {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
const summaryData = extractSummary(content);
|
||||
// Extract phase from path
|
||||
const phaseMatch = file.match(/phases\/(\d+-[^/]+)/);
|
||||
const phase = phaseMatch ? phaseMatch[1] : undefined;
|
||||
for (const entry of summaryData.techStack) {
|
||||
stack.push({
|
||||
library: entry.library,
|
||||
version: entry.version,
|
||||
purpose: entry.purpose,
|
||||
project: projectName,
|
||||
phase,
|
||||
source: file,
|
||||
sourceType: 'summary'
|
||||
});
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Skip files that can't be parsed
|
||||
}
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
/**
|
||||
* Extract tech stack from all RESEARCH.md files
|
||||
*/
|
||||
function extractFromResearch(planningPath, projectName) {
|
||||
const stack = [];
|
||||
const researchFiles = findFiles(planningPath, 'RESEARCH');
|
||||
for (const file of researchFiles) {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
const researchData = extractResearch(content);
|
||||
// Extract phase from path
|
||||
const phaseMatch = file.match(/phases\/(\d+-[^/]+)/);
|
||||
const phase = phaseMatch ? phaseMatch[1] : undefined;
|
||||
for (const entry of researchData.standardStack) {
|
||||
stack.push({
|
||||
library: entry.library,
|
||||
version: entry.version,
|
||||
purpose: entry.purpose,
|
||||
project: projectName,
|
||||
phase,
|
||||
source: file,
|
||||
sourceType: 'research'
|
||||
});
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Skip files that can't be parsed
|
||||
}
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
/**
|
||||
* Recursively find files matching a pattern
|
||||
*/
|
||||
function findFiles(dir, pattern, files = []) {
|
||||
try {
|
||||
const entries = readdirSync(dir);
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isDirectory() && !entry.startsWith('.')) {
|
||||
findFiles(fullPath, pattern, files);
|
||||
}
|
||||
else if (entry.toUpperCase().includes(pattern) && entry.endsWith('.md')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Skip entries that can't be accessed
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Return empty if directory can't be read
|
||||
}
|
||||
return files;
|
||||
}
|
||||
//# sourceMappingURL=stack.js.map
|
||||
1
gsd-memory/dist/tools/stack.js.map
vendored
Normal file
1
gsd-memory/dist/tools/stack.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"stack.js","sourceRoot":"","sources":["../../src/tools/stack.ts"],"names":[],"mappings":"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"}
|
||||
23
gsd-memory/dist/tools/status.d.ts
vendored
Normal file
23
gsd-memory/dist/tools/status.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface ProjectStatus {
|
||||
name: string;
|
||||
path: string;
|
||||
exists: boolean;
|
||||
hasPlanningDir: boolean;
|
||||
qmdCollection?: string;
|
||||
qmdAvailable: boolean;
|
||||
documentCount: number;
|
||||
registeredAt: string;
|
||||
lastIndexed?: string;
|
||||
}
|
||||
export interface MemoryStatus {
|
||||
qmdAvailable: boolean;
|
||||
qmdVersion?: string;
|
||||
totalProjects: number;
|
||||
totalDocuments: number;
|
||||
projects: ProjectStatus[];
|
||||
}
|
||||
/**
|
||||
* Get comprehensive status of GSD memory system
|
||||
*/
|
||||
export declare function getStatus(): Promise<MemoryStatus>;
|
||||
//# sourceMappingURL=status.d.ts.map
|
||||
1
gsd-memory/dist/tools/status.d.ts.map
vendored
Normal file
1
gsd-memory/dist/tools/status.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/tools/status.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,cAAc,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC,CAmCvD"}
|
||||
67
gsd-memory/dist/tools/status.js
vendored
Normal file
67
gsd-memory/dist/tools/status.js
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
import { getAllProjects } from '../registry.js';
|
||||
import { qmd } from '../qmd.js';
|
||||
import { existsSync, readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
/**
|
||||
* Get comprehensive status of GSD memory system
|
||||
*/
|
||||
export async function getStatus() {
|
||||
const qmdAvailable = await qmd.isAvailable();
|
||||
const projects = getAllProjects();
|
||||
let totalDocuments = 0;
|
||||
const projectStatuses = [];
|
||||
for (const project of projects) {
|
||||
const planningPath = join(project.path, '.planning');
|
||||
const exists = existsSync(project.path);
|
||||
const hasPlanningDir = existsSync(planningPath);
|
||||
// Count documents in .planning directory
|
||||
const documentCount = hasPlanningDir ? countMarkdownFiles(planningPath) : 0;
|
||||
totalDocuments += documentCount;
|
||||
projectStatuses.push({
|
||||
name: project.name,
|
||||
path: project.path,
|
||||
exists,
|
||||
hasPlanningDir,
|
||||
qmdCollection: project.qmdCollection,
|
||||
qmdAvailable: qmdAvailable && !!project.qmdCollection,
|
||||
documentCount,
|
||||
registeredAt: project.registeredAt,
|
||||
lastIndexed: project.lastIndexed
|
||||
});
|
||||
}
|
||||
return {
|
||||
qmdAvailable,
|
||||
totalProjects: projects.length,
|
||||
totalDocuments,
|
||||
projects: projectStatuses
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Count markdown files in a directory recursively
|
||||
*/
|
||||
function countMarkdownFiles(dir) {
|
||||
let count = 0;
|
||||
try {
|
||||
const entries = readdirSync(dir);
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isDirectory() && !entry.startsWith('.')) {
|
||||
count += countMarkdownFiles(fullPath);
|
||||
}
|
||||
else if (stat.isFile() && entry.endsWith('.md')) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Skip entries that can't be accessed
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Return 0 if directory can't be read
|
||||
}
|
||||
return count;
|
||||
}
|
||||
//# sourceMappingURL=status.js.map
|
||||
1
gsd-memory/dist/tools/status.js.map
vendored
Normal file
1
gsd-memory/dist/tools/status.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/tools/status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAsB5B;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,MAAM,YAAY,GAAG,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC;IAC7C,MAAM,QAAQ,GAAG,cAAc,EAAE,CAAC;IAElC,IAAI,cAAc,GAAG,CAAC,CAAC;IACvB,MAAM,eAAe,GAAoB,EAAE,CAAC;IAE5C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QACrD,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACxC,MAAM,cAAc,GAAG,UAAU,CAAC,YAAY,CAAC,CAAC;QAEhD,yCAAyC;QACzC,MAAM,aAAa,GAAG,cAAc,CAAC,CAAC,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5E,cAAc,IAAI,aAAa,CAAC;QAEhC,eAAe,CAAC,IAAI,CAAC;YACnB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,MAAM;YACN,cAAc;YACd,aAAa,EAAE,OAAO,CAAC,aAAa;YACpC,YAAY,EAAE,YAAY,IAAI,CAAC,CAAC,OAAO,CAAC,aAAa;YACrD,aAAa;YACb,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,WAAW,EAAE,OAAO,CAAC,WAAW;SACjC,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,YAAY;QACZ,aAAa,EAAE,QAAQ,CAAC,MAAM;QAC9B,cAAc;QACd,QAAQ,EAAE,eAAe;KAC1B,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAS,kBAAkB,CAAC,GAAW;IACrC,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;QAEjC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAClC,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;gBAEhC,IAAI,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;oBACjD,KAAK,IAAI,kBAAkB,CAAC,QAAQ,CAAC,CAAC;gBACxC,CAAC;qBAAM,IAAI,IAAI,CAAC,MAAM,EAAE,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;oBAClD,KAAK,EAAE,CAAC;gBACV,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,sCAAsC;YACxC,CAAC;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,sCAAsC;IACxC,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
|
||||
3005
gsd-memory/package-lock.json
generated
Normal file
3005
gsd-memory/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
gsd-memory/package.json
Normal file
31
gsd-memory/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "gsd-memory",
|
||||
"version": "1.0.0",
|
||||
"description": "GSD Memory - Cross-project knowledge system MCP server",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"gsd-memory": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:integration": "vitest run --config vitest.integration.config.ts",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"yaml": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vitest": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
36
gsd-memory/src/extractors/frontmatter.ts
Normal file
36
gsd-memory/src/extractors/frontmatter.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import matter from 'gray-matter';
|
||||
|
||||
/**
|
||||
* Extract YAML frontmatter from markdown content
|
||||
* Returns empty object if no frontmatter or invalid YAML
|
||||
*/
|
||||
export function extractFrontmatter(markdown: string): Record<string, unknown> {
|
||||
try {
|
||||
const { data } = matter(markdown);
|
||||
return data || {};
|
||||
} catch {
|
||||
// Return empty object on parse errors
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract frontmatter and content separately
|
||||
*/
|
||||
export function parseFrontmatter(markdown: string): {
|
||||
frontmatter: Record<string, unknown>;
|
||||
content: string;
|
||||
} {
|
||||
try {
|
||||
const { data, content } = matter(markdown);
|
||||
return {
|
||||
frontmatter: data || {},
|
||||
content: content.trim()
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
frontmatter: {},
|
||||
content: markdown.trim()
|
||||
};
|
||||
}
|
||||
}
|
||||
4
gsd-memory/src/extractors/index.ts
Normal file
4
gsd-memory/src/extractors/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { extractFrontmatter, parseFrontmatter } from './frontmatter.js';
|
||||
export { extractSummary, type SummaryData, type TechStackEntry, type Decision as SummaryDecision } from './summary.js';
|
||||
export { extractResearch, type ResearchData, type Pitfall, type DontHandRoll } from './research.js';
|
||||
export { extractProject, type ProjectData, type Decision as ProjectDecision, type Constraint } from './project.js';
|
||||
193
gsd-memory/src/extractors/project.ts
Normal file
193
gsd-memory/src/extractors/project.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
export interface OutOfScopeItem {
|
||||
item: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface Constraint {
|
||||
type: string;
|
||||
value: string;
|
||||
why?: string;
|
||||
}
|
||||
|
||||
export interface Decision {
|
||||
decision: string;
|
||||
rationale: string;
|
||||
outcome: string;
|
||||
}
|
||||
|
||||
export interface Requirements {
|
||||
validated: string[];
|
||||
active: string[];
|
||||
outOfScope: OutOfScopeItem[];
|
||||
}
|
||||
|
||||
export interface ProjectData {
|
||||
name: string;
|
||||
description: string;
|
||||
coreValue: string;
|
||||
requirements: Requirements;
|
||||
context: string;
|
||||
constraints: Constraint[];
|
||||
decisions: Decision[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract structured data from a PROJECT.md file
|
||||
*/
|
||||
export function extractProject(markdown: string): ProjectData {
|
||||
return {
|
||||
name: extractName(markdown),
|
||||
description: extractSection(markdown, 'What This Is'),
|
||||
coreValue: extractSection(markdown, 'Core Value'),
|
||||
requirements: extractRequirements(markdown),
|
||||
context: extractSection(markdown, 'Context'),
|
||||
constraints: extractConstraints(markdown),
|
||||
decisions: extractDecisions(markdown)
|
||||
};
|
||||
}
|
||||
|
||||
function extractName(content: string): string {
|
||||
// First line should be # ProjectName
|
||||
const match = content.match(/^#\s+(.+)/m);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
function extractSection(content: string, sectionName: string): string {
|
||||
// Find the section and extract content until next ## heading
|
||||
const regex = new RegExp(`##\\s+${sectionName}\\n\\n([\\s\\S]*?)(?=\\n##|$)`, 'i');
|
||||
const match = content.match(regex);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
function extractRequirements(content: string): Requirements {
|
||||
const requirementsSection = content.match(/##\s+Requirements\n([\s\S]*?)(?=\n##[^#]|$)/i);
|
||||
if (!requirementsSection) {
|
||||
return { validated: [], active: [], outOfScope: [] };
|
||||
}
|
||||
|
||||
const section = requirementsSection[1];
|
||||
|
||||
return {
|
||||
validated: extractValidatedRequirements(section),
|
||||
active: extractActiveRequirements(section),
|
||||
outOfScope: extractOutOfScope(section)
|
||||
};
|
||||
}
|
||||
|
||||
function extractValidatedRequirements(section: string): string[] {
|
||||
const requirements: string[] = [];
|
||||
|
||||
// Look for ### Validated subsection
|
||||
const validatedSection = section.match(/###\s+Validated\n([\s\S]*?)(?=###|$)/i);
|
||||
if (!validatedSection) return requirements;
|
||||
|
||||
// Extract checkmark items: - ✓ requirement — version
|
||||
const matches = validatedSection[1].match(/^-\s+✓\s+(.+)/gm);
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
const content = match.replace(/^-\s+✓\s+/, '').trim();
|
||||
requirements.push(content);
|
||||
}
|
||||
}
|
||||
|
||||
return requirements;
|
||||
}
|
||||
|
||||
function extractActiveRequirements(section: string): string[] {
|
||||
const requirements: string[] = [];
|
||||
|
||||
// Look for ### Active subsection
|
||||
const activeSection = section.match(/###\s+Active\n([\s\S]*?)(?=###|$)/i);
|
||||
if (!activeSection) return requirements;
|
||||
|
||||
// Extract checkbox items: - [ ] requirement
|
||||
const matches = activeSection[1].match(/^-\s+\[\s*\]\s+(.+)/gm);
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
const content = match.replace(/^-\s+\[\s*\]\s+/, '').trim();
|
||||
requirements.push(content);
|
||||
}
|
||||
}
|
||||
|
||||
return requirements;
|
||||
}
|
||||
|
||||
function extractOutOfScope(section: string): OutOfScopeItem[] {
|
||||
const items: OutOfScopeItem[] = [];
|
||||
|
||||
// Look for ### Out of Scope subsection
|
||||
const outOfScopeSection = section.match(/###\s+Out of Scope\n([\s\S]*?)(?=###|$)/i);
|
||||
if (!outOfScopeSection) return items;
|
||||
|
||||
// Extract items: - Item — reason
|
||||
const matches = outOfScopeSection[1].match(/^-\s+(.+)/gm);
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
const content = match.replace(/^-\s+/, '').trim();
|
||||
// Split by em-dash or double hyphen
|
||||
const parts = content.split(/\s+—\s+|\s+--\s+/);
|
||||
items.push({
|
||||
item: parts[0].trim(),
|
||||
reason: parts[1]?.trim() || ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function extractConstraints(content: string): Constraint[] {
|
||||
const constraints: Constraint[] = [];
|
||||
|
||||
const constraintsSection = content.match(/##\s+Constraints\n([\s\S]*?)(?=\n##|$)/i);
|
||||
if (!constraintsSection) return constraints;
|
||||
|
||||
// Extract items: - **Type**: Value — Why
|
||||
const matches = constraintsSection[1].match(/^-\s+\*\*([^*]+)\*\*:\s*(.+)/gm);
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
const typeMatch = match.match(/\*\*([^*]+)\*\*/);
|
||||
const valueMatch = match.match(/\*\*[^*]+\*\*:\s*(.+)/);
|
||||
|
||||
if (typeMatch && valueMatch) {
|
||||
const valueParts = valueMatch[1].split(/\s+—\s+|\s+--\s+/);
|
||||
constraints.push({
|
||||
type: typeMatch[1].trim(),
|
||||
value: valueParts[0].trim(),
|
||||
why: valueParts[1]?.trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return constraints;
|
||||
}
|
||||
|
||||
function extractDecisions(content: string): Decision[] {
|
||||
const decisions: Decision[] = [];
|
||||
|
||||
const decisionsSection = content.match(/##\s+Key Decisions\n([\s\S]*?)(?=\n##[^#]|\n---|$)/i);
|
||||
if (!decisionsSection) return decisions;
|
||||
|
||||
// Parse markdown table rows - match rows with 3+ cells
|
||||
const lines = decisionsSection[1].split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
// Must be a table row (starts and ends with |)
|
||||
if (!line.trim().startsWith('|')) continue;
|
||||
|
||||
// Skip header row (contains 'Decision') and separator row (contains ---)
|
||||
if (line.includes('Decision') || line.includes('---')) continue;
|
||||
|
||||
const cells = line.split('|').filter(c => c.trim());
|
||||
if (cells.length >= 3) {
|
||||
decisions.push({
|
||||
decision: cells[0].trim(),
|
||||
rationale: cells[1].trim(),
|
||||
outcome: cells[2].trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return decisions;
|
||||
}
|
||||
250
gsd-memory/src/extractors/research.ts
Normal file
250
gsd-memory/src/extractors/research.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { parseFrontmatter } from './frontmatter.js';
|
||||
|
||||
export interface StackEntry {
|
||||
library: string;
|
||||
version?: string;
|
||||
purpose: string;
|
||||
whyStandard?: string;
|
||||
}
|
||||
|
||||
export interface Pitfall {
|
||||
name: string;
|
||||
description: string;
|
||||
cause?: string;
|
||||
prevention: string;
|
||||
warningSigns?: string;
|
||||
}
|
||||
|
||||
export interface DontHandRoll {
|
||||
problem: string;
|
||||
dontBuild: string;
|
||||
useInstead: string;
|
||||
why: string;
|
||||
}
|
||||
|
||||
export interface UserConstraints {
|
||||
locked: string[];
|
||||
discretion: string[];
|
||||
deferred: string[];
|
||||
}
|
||||
|
||||
export interface ResearchData {
|
||||
// Metadata
|
||||
domain: string;
|
||||
confidence: string;
|
||||
researched: string;
|
||||
|
||||
// Extracted sections
|
||||
standardStack: StackEntry[];
|
||||
pitfalls: Pitfall[];
|
||||
dontHandRoll: DontHandRoll[];
|
||||
antiPatterns: string[];
|
||||
userConstraints: UserConstraints;
|
||||
primaryRecommendation: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract structured data from a RESEARCH.md file
|
||||
*/
|
||||
export function extractResearch(markdown: string): ResearchData {
|
||||
const { frontmatter, content } = parseFrontmatter(markdown);
|
||||
|
||||
return {
|
||||
// Metadata from frontmatter
|
||||
domain: (frontmatter.domain as string) || '',
|
||||
confidence: (frontmatter.confidence as string) || '',
|
||||
researched: formatDate(frontmatter.researched),
|
||||
|
||||
// Extracted from content
|
||||
standardStack: extractStandardStack(content),
|
||||
pitfalls: extractPitfalls(content),
|
||||
dontHandRoll: extractDontHandRoll(content),
|
||||
antiPatterns: extractAntiPatterns(content),
|
||||
userConstraints: extractUserConstraints(content),
|
||||
primaryRecommendation: extractPrimaryRecommendation(content)
|
||||
};
|
||||
}
|
||||
|
||||
function formatDate(date: unknown): string {
|
||||
if (!date) return '';
|
||||
if (date instanceof Date) {
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
return String(date);
|
||||
}
|
||||
|
||||
function extractStandardStack(content: string): StackEntry[] {
|
||||
const entries: StackEntry[] = [];
|
||||
|
||||
// Look for standard_stack section and parse markdown tables
|
||||
const stackSection = extractSection(content, 'standard_stack');
|
||||
if (!stackSection) return entries;
|
||||
|
||||
// Parse Core and Supporting tables
|
||||
const tableRows = stackSection.match(/\|[^|]+\|[^|]+\|[^|]+\|[^|]*\|/g);
|
||||
if (!tableRows) return entries;
|
||||
|
||||
for (const row of tableRows) {
|
||||
// Skip header and separator rows
|
||||
if (row.includes('Library') || row.includes('---')) continue;
|
||||
|
||||
const cells = row.split('|').filter(c => c.trim());
|
||||
if (cells.length >= 3) {
|
||||
entries.push({
|
||||
library: cells[0].trim(),
|
||||
version: cells[1]?.trim(),
|
||||
purpose: cells[2]?.trim(),
|
||||
whyStandard: cells[3]?.trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function extractPitfalls(content: string): Pitfall[] {
|
||||
const pitfalls: Pitfall[] = [];
|
||||
|
||||
const pitfallSection = extractSection(content, 'common_pitfalls');
|
||||
if (!pitfallSection) return pitfalls;
|
||||
|
||||
// Parse individual pitfall blocks (### Pitfall N: Name)
|
||||
const pitfallBlocks = pitfallSection.split(/###\s+Pitfall\s+\d+:\s+/);
|
||||
|
||||
for (const block of pitfallBlocks) {
|
||||
if (!block.trim()) continue;
|
||||
|
||||
const lines = block.split('\n');
|
||||
const name = lines[0]?.trim();
|
||||
if (!name) continue;
|
||||
|
||||
const description = extractField(block, 'What goes wrong');
|
||||
const cause = extractField(block, 'Why it happens');
|
||||
const prevention = extractField(block, 'How to avoid');
|
||||
const warningSigns = extractField(block, 'Warning signs');
|
||||
|
||||
if (description || prevention) {
|
||||
pitfalls.push({
|
||||
name,
|
||||
description: description || '',
|
||||
cause,
|
||||
prevention: prevention || '',
|
||||
warningSigns
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return pitfalls;
|
||||
}
|
||||
|
||||
function extractDontHandRoll(content: string): DontHandRoll[] {
|
||||
const items: DontHandRoll[] = [];
|
||||
|
||||
const section = extractSection(content, 'dont_hand_roll');
|
||||
if (!section) return items;
|
||||
|
||||
// Parse table rows
|
||||
const tableRows = section.match(/\|[^|]+\|[^|]+\|[^|]+\|[^|]+\|/g);
|
||||
if (!tableRows) return items;
|
||||
|
||||
for (const row of tableRows) {
|
||||
if (row.includes('Problem') || row.includes('---')) continue;
|
||||
|
||||
const cells = row.split('|').filter(c => c.trim());
|
||||
if (cells.length >= 4) {
|
||||
items.push({
|
||||
problem: cells[0].trim(),
|
||||
dontBuild: cells[1].trim(),
|
||||
useInstead: cells[2].trim(),
|
||||
why: cells[3].trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function extractAntiPatterns(content: string): string[] {
|
||||
const patterns: string[] = [];
|
||||
|
||||
const archSection = extractSection(content, 'architecture_patterns');
|
||||
if (!archSection) return patterns;
|
||||
|
||||
// Look for "Anti-Patterns to Avoid" subsection
|
||||
const antiMatch = archSection.match(/###\s+Anti-Patterns to Avoid\n([\s\S]*?)(?=###|$)/);
|
||||
if (!antiMatch) return patterns;
|
||||
|
||||
// Extract bullet points
|
||||
const bullets = antiMatch[1].match(/^-\s+\*\*([^*]+)\*\*/gm);
|
||||
if (bullets) {
|
||||
for (const bullet of bullets) {
|
||||
const match = bullet.match(/\*\*([^*]+)\*\*/);
|
||||
if (match) patterns.push(match[1].trim());
|
||||
}
|
||||
}
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
function extractUserConstraints(content: string): UserConstraints {
|
||||
const constraints: UserConstraints = {
|
||||
locked: [],
|
||||
discretion: [],
|
||||
deferred: []
|
||||
};
|
||||
|
||||
const section = extractSection(content, 'user_constraints');
|
||||
if (!section) return constraints;
|
||||
|
||||
// Extract locked decisions
|
||||
const lockedMatch = section.match(/###\s+Locked Decisions\n([\s\S]*?)(?=###|$)/);
|
||||
if (lockedMatch) {
|
||||
constraints.locked = extractBulletPoints(lockedMatch[1]);
|
||||
}
|
||||
|
||||
// Extract Claude's discretion
|
||||
const discretionMatch = section.match(/###\s+Claude's Discretion\n([\s\S]*?)(?=###|$)/);
|
||||
if (discretionMatch) {
|
||||
constraints.discretion = extractBulletPoints(discretionMatch[1]);
|
||||
}
|
||||
|
||||
// Extract deferred items
|
||||
const deferredMatch = section.match(/###\s+Deferred Ideas[^#]*\n([\s\S]*?)(?=<\/|$)/);
|
||||
if (deferredMatch) {
|
||||
constraints.deferred = extractBulletPoints(deferredMatch[1]);
|
||||
}
|
||||
|
||||
return constraints;
|
||||
}
|
||||
|
||||
function extractPrimaryRecommendation(content: string): string {
|
||||
// Look for "Primary recommendation:" in summary section
|
||||
const match = content.match(/\*\*Primary recommendation:\*\*\s*([^\n]+)/);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
function extractSection(content: string, sectionTag: string): string | null {
|
||||
const regex = new RegExp(`<${sectionTag}>([\\s\\S]*?)<\\/${sectionTag}>`, 'i');
|
||||
const match = content.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function extractField(block: string, fieldName: string): string | undefined {
|
||||
const regex = new RegExp(`\\*\\*${fieldName}:\\*\\*\\s*([^\\n]+)`, 'i');
|
||||
const match = block.match(regex);
|
||||
return match ? match[1].trim() : undefined;
|
||||
}
|
||||
|
||||
function extractBulletPoints(text: string): string[] {
|
||||
const points: string[] = [];
|
||||
const matches = text.match(/^-\s+(.+)/gm);
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
const content = match.replace(/^-\s+/, '').trim();
|
||||
if (content) points.push(content);
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
155
gsd-memory/src/extractors/summary.ts
Normal file
155
gsd-memory/src/extractors/summary.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { parseFrontmatter } from './frontmatter.js';
|
||||
|
||||
export interface TechStackEntry {
|
||||
library: string;
|
||||
version: string;
|
||||
purpose?: string;
|
||||
}
|
||||
|
||||
export interface Dependency {
|
||||
phase: string;
|
||||
provides: string;
|
||||
}
|
||||
|
||||
export interface KeyFiles {
|
||||
created: string[];
|
||||
modified: string[];
|
||||
}
|
||||
|
||||
export interface Decision {
|
||||
decision: string;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export interface SummaryData {
|
||||
// Identifiers
|
||||
phase: string;
|
||||
plan: number | string;
|
||||
subsystem: string;
|
||||
tags: string[];
|
||||
|
||||
// Dependency graph
|
||||
requires: Dependency[];
|
||||
provides: string[];
|
||||
affects: string[];
|
||||
|
||||
// Tech tracking
|
||||
techStack: TechStackEntry[];
|
||||
patterns: string[];
|
||||
|
||||
// Files
|
||||
keyFiles: KeyFiles;
|
||||
|
||||
// Decisions
|
||||
decisions: Decision[];
|
||||
|
||||
// Metadata
|
||||
duration: string;
|
||||
completed: string;
|
||||
|
||||
// Content
|
||||
oneLiner: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract structured data from a SUMMARY.md file
|
||||
*/
|
||||
export function extractSummary(markdown: string): SummaryData {
|
||||
const { frontmatter, content } = parseFrontmatter(markdown);
|
||||
|
||||
return {
|
||||
// Identifiers
|
||||
phase: (frontmatter.phase as string) || '',
|
||||
plan: (frontmatter.plan as number | string) || '',
|
||||
subsystem: (frontmatter.subsystem as string) || '',
|
||||
tags: (frontmatter.tags as string[]) || [],
|
||||
|
||||
// Dependency graph
|
||||
requires: extractRequires(frontmatter.requires),
|
||||
provides: (frontmatter.provides as string[]) || [],
|
||||
affects: (frontmatter.affects as string[]) || [],
|
||||
|
||||
// Tech tracking
|
||||
techStack: extractTechStack(frontmatter['tech-stack']),
|
||||
patterns: (frontmatter['patterns-established'] as string[]) || [],
|
||||
|
||||
// Files
|
||||
keyFiles: extractKeyFiles(frontmatter['key-files']),
|
||||
|
||||
// Decisions
|
||||
decisions: extractDecisions(frontmatter['key-decisions']),
|
||||
|
||||
// Metadata
|
||||
duration: (frontmatter.duration as string) || '',
|
||||
completed: formatCompleted(frontmatter.completed),
|
||||
|
||||
// Content
|
||||
oneLiner: extractOneLiner(content)
|
||||
};
|
||||
}
|
||||
|
||||
function extractRequires(requires: unknown): Dependency[] {
|
||||
if (!Array.isArray(requires)) return [];
|
||||
|
||||
return requires.map((req) => ({
|
||||
phase: (req as Record<string, unknown>).phase as string || '',
|
||||
provides: (req as Record<string, unknown>).provides as string || ''
|
||||
}));
|
||||
}
|
||||
|
||||
function extractTechStack(techStack: unknown): TechStackEntry[] {
|
||||
if (!techStack || typeof techStack !== 'object') return [];
|
||||
|
||||
const ts = techStack as Record<string, unknown>;
|
||||
const added = ts.added;
|
||||
|
||||
if (!Array.isArray(added)) return [];
|
||||
|
||||
return added.map((entry) => {
|
||||
// Handle both object format { library: version } and string format
|
||||
if (typeof entry === 'object' && entry !== null) {
|
||||
const [library, version] = Object.entries(entry)[0] || ['', ''];
|
||||
return { library, version: String(version) };
|
||||
}
|
||||
return { library: String(entry), version: '' };
|
||||
});
|
||||
}
|
||||
|
||||
function extractKeyFiles(keyFiles: unknown): KeyFiles {
|
||||
if (!keyFiles || typeof keyFiles !== 'object') {
|
||||
return { created: [], modified: [] };
|
||||
}
|
||||
|
||||
const kf = keyFiles as Record<string, unknown>;
|
||||
return {
|
||||
created: (kf.created as string[]) || [],
|
||||
modified: (kf.modified as string[]) || []
|
||||
};
|
||||
}
|
||||
|
||||
function extractDecisions(decisions: unknown): Decision[] {
|
||||
if (!Array.isArray(decisions)) return [];
|
||||
|
||||
return decisions.map((d) => ({
|
||||
decision: String(d),
|
||||
source: 'frontmatter'
|
||||
}));
|
||||
}
|
||||
|
||||
function formatCompleted(completed: unknown): string {
|
||||
if (!completed) return '';
|
||||
|
||||
// If it's a Date object (YAML parsed it), format as ISO date
|
||||
if (completed instanceof Date) {
|
||||
return completed.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
return String(completed);
|
||||
}
|
||||
|
||||
function extractOneLiner(content: string): string {
|
||||
// The one-liner is the bold text after the title
|
||||
// Pattern: # Title\n\n**one-liner**
|
||||
const match = content.match(/^#[^\n]+\n\n\*\*([^*]+)\*\*/m);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
321
gsd-memory/src/index.ts
Normal file
321
gsd-memory/src/index.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
type Tool
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
import { search } from './tools/search.js';
|
||||
import { findDecisions } from './tools/decisions.js';
|
||||
import { findPatterns } from './tools/patterns.js';
|
||||
import { findPitfalls } from './tools/pitfalls.js';
|
||||
import { findStack } from './tools/stack.js';
|
||||
import { register, status as registerStatus, unregister } from './tools/register.js';
|
||||
import { index, indexAll } from './tools/index-tool.js';
|
||||
import { getStatus } from './tools/status.js';
|
||||
|
||||
// Define available tools
|
||||
const tools: Tool[] = [
|
||||
{
|
||||
name: 'gsd_memory_search',
|
||||
description: 'Search across all registered GSD projects for content matching a query. Returns enriched results with project, phase, and document type context.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'The search query'
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
description: 'Optional: limit search to a specific project'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results (default: 20)'
|
||||
}
|
||||
},
|
||||
required: ['query']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'gsd_memory_decisions',
|
||||
description: 'Find decisions made across GSD projects. Searches KEY-DECISIONS from SUMMARY.md frontmatter and Key Decisions tables from PROJECT.md.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Optional: filter decisions by keyword'
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
description: 'Optional: limit to a specific project'
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Optional: filter by tags'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results (default: 50)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'gsd_memory_patterns',
|
||||
description: 'Find patterns established across GSD projects. Extracts PATTERNS-ESTABLISHED from SUMMARY.md frontmatter.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Optional: filter patterns by keyword'
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
description: 'Optional: limit to a specific project'
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Optional: filter by tags'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results (default: 50)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'gsd_memory_pitfalls',
|
||||
description: 'Find documented pitfalls from RESEARCH.md files. Includes what went wrong, causes, and prevention strategies.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Optional: filter pitfalls by keyword'
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
description: 'Optional: limit to a specific project'
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'Optional: filter by technology domain'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results (default: 50)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'gsd_memory_stack',
|
||||
description: 'Find tech stack entries across GSD projects. Extracts from SUMMARY.md (tech-stack.added) and RESEARCH.md (standard stack tables).',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Optional: filter by library name or purpose'
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
description: 'Optional: limit to a specific project'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results (default: 100)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'gsd_memory_register',
|
||||
description: 'Register a project with GSD memory. Creates a QMD collection if QMD is available.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Path to the project root (must contain .planning directory)'
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Optional: project name (defaults to directory name)'
|
||||
}
|
||||
},
|
||||
required: ['path']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'gsd_memory_index',
|
||||
description: 'Trigger indexing/update for a project. Updates QMD collection if available, otherwise marks project as indexed for grep fallback.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project: {
|
||||
type: 'string',
|
||||
description: 'Project name to index'
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Alternative: project path to index'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'gsd_memory_status',
|
||||
description: 'Show status of GSD memory system: QMD availability, registered projects, document counts.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Create server instance
|
||||
const server = new Server(
|
||||
{
|
||||
name: 'gsd-memory',
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Handle list tools request
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools
|
||||
}));
|
||||
|
||||
// Handle tool calls
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
let result: unknown;
|
||||
|
||||
switch (name) {
|
||||
case 'gsd_memory_search':
|
||||
result = await search({
|
||||
query: args?.query as string,
|
||||
project: args?.project as string | undefined,
|
||||
limit: args?.limit as number | undefined
|
||||
});
|
||||
break;
|
||||
|
||||
case 'gsd_memory_decisions':
|
||||
result = await findDecisions({
|
||||
query: args?.query as string | undefined,
|
||||
project: args?.project as string | undefined,
|
||||
tags: args?.tags as string[] | undefined,
|
||||
limit: args?.limit as number | undefined
|
||||
});
|
||||
break;
|
||||
|
||||
case 'gsd_memory_patterns':
|
||||
result = await findPatterns({
|
||||
query: args?.query as string | undefined,
|
||||
project: args?.project as string | undefined,
|
||||
tags: args?.tags as string[] | undefined,
|
||||
limit: args?.limit as number | undefined
|
||||
});
|
||||
break;
|
||||
|
||||
case 'gsd_memory_pitfalls':
|
||||
result = await findPitfalls({
|
||||
query: args?.query as string | undefined,
|
||||
project: args?.project as string | undefined,
|
||||
domain: args?.domain as string | undefined,
|
||||
limit: args?.limit as number | undefined
|
||||
});
|
||||
break;
|
||||
|
||||
case 'gsd_memory_stack':
|
||||
result = await findStack({
|
||||
query: args?.query as string | undefined,
|
||||
project: args?.project as string | undefined,
|
||||
limit: args?.limit as number | undefined
|
||||
});
|
||||
break;
|
||||
|
||||
case 'gsd_memory_register':
|
||||
result = await register({
|
||||
path: args?.path as string,
|
||||
name: args?.name as string | undefined
|
||||
});
|
||||
break;
|
||||
|
||||
case 'gsd_memory_index':
|
||||
if (args?.project || args?.path) {
|
||||
result = await index({
|
||||
project: args?.project as string | undefined,
|
||||
path: args?.path as string | undefined
|
||||
});
|
||||
} else {
|
||||
result = await indexAll();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'gsd_memory_status':
|
||||
result = await getStatus();
|
||||
break;
|
||||
|
||||
default:
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Unknown tool: ${name}`
|
||||
}
|
||||
],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}
|
||||
]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Start the server
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error('GSD Memory MCP server started');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
255
gsd-memory/src/qmd.ts
Normal file
255
gsd-memory/src/qmd.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { readFileSync, readdirSync, statSync } from 'fs';
|
||||
import { join, relative } from 'path';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export interface QmdSearchResult {
|
||||
content: string;
|
||||
source: string;
|
||||
relevance: number;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
export interface QmdSearchOptions {
|
||||
query: string;
|
||||
paths: string[];
|
||||
limit?: number;
|
||||
collection?: string;
|
||||
}
|
||||
|
||||
export interface QmdCollectionOptions {
|
||||
name: string;
|
||||
paths: string[];
|
||||
context?: string;
|
||||
}
|
||||
|
||||
export interface QmdResult {
|
||||
success: boolean;
|
||||
reason?: string;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
export interface QmdStatus {
|
||||
available: boolean;
|
||||
collection?: string;
|
||||
documentCount?: number;
|
||||
lastIndexed?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* QMD wrapper - provides semantic search when QMD is available,
|
||||
* falls back to grep-based search otherwise
|
||||
*/
|
||||
export const qmd = {
|
||||
/**
|
||||
* Check if QMD is installed and available
|
||||
*/
|
||||
async isAvailable(): Promise<boolean> {
|
||||
try {
|
||||
await execAsync('which qmd');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Search for content using QMD or grep fallback
|
||||
*/
|
||||
async search(options: QmdSearchOptions): Promise<QmdSearchResult[]> {
|
||||
const available = await this.isAvailable();
|
||||
|
||||
if (available && options.collection) {
|
||||
return this.qmdSearch(options);
|
||||
}
|
||||
|
||||
return this.grepFallback(options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Search using QMD (when available)
|
||||
*/
|
||||
async qmdSearch(options: QmdSearchOptions): Promise<QmdSearchResult[]> {
|
||||
try {
|
||||
const limit = options.limit || 10;
|
||||
const { stdout } = await execAsync(
|
||||
`qmd query "${options.query}" --collection "${options.collection}" --limit ${limit} --json`
|
||||
);
|
||||
|
||||
const parsed = JSON.parse(stdout);
|
||||
return (parsed.results || []).map((r: { content: string; source: string; score: number }) => ({
|
||||
content: r.content,
|
||||
source: r.source,
|
||||
relevance: r.score || 0.5,
|
||||
context: 'qmd'
|
||||
}));
|
||||
} catch {
|
||||
// Fall back to grep if QMD search fails
|
||||
return this.grepFallback(options);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Grep-based fallback search
|
||||
*/
|
||||
async grepFallback(options: QmdSearchOptions): Promise<QmdSearchResult[]> {
|
||||
const results: QmdSearchResult[] = [];
|
||||
const limit = options.limit || 20;
|
||||
|
||||
for (const searchPath of options.paths) {
|
||||
try {
|
||||
const files = this.findMarkdownFiles(searchPath);
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
const query = options.query.toLowerCase();
|
||||
|
||||
// Simple relevance: count matches
|
||||
const matches = (content.toLowerCase().match(new RegExp(query, 'g')) || []).length;
|
||||
|
||||
if (matches > 0) {
|
||||
// Extract context around first match
|
||||
const lowerContent = content.toLowerCase();
|
||||
const matchIndex = lowerContent.indexOf(query);
|
||||
const start = Math.max(0, matchIndex - 100);
|
||||
const end = Math.min(content.length, matchIndex + query.length + 100);
|
||||
const snippet = content.slice(start, end);
|
||||
|
||||
results.push({
|
||||
content: snippet,
|
||||
source: file,
|
||||
relevance: Math.min(1, matches * 0.1),
|
||||
context: 'grep'
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Skip files that can't be read
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip paths that can't be accessed
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by relevance and limit
|
||||
return results
|
||||
.sort((a, b) => b.relevance - a.relevance)
|
||||
.slice(0, limit);
|
||||
},
|
||||
|
||||
/**
|
||||
* Find all markdown files in a directory recursively
|
||||
*/
|
||||
findMarkdownFiles(dir: string, files: string[] = []): string[] {
|
||||
try {
|
||||
const entries = readdirSync(dir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
|
||||
this.findMarkdownFiles(fullPath, files);
|
||||
} else if (stat.isFile() && entry.endsWith('.md')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
} catch {
|
||||
// Skip entries that can't be accessed
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Return empty if directory can't be read
|
||||
}
|
||||
|
||||
return files;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a QMD collection for a project
|
||||
*/
|
||||
async createCollection(options: QmdCollectionOptions): Promise<QmdResult> {
|
||||
const available = await this.isAvailable();
|
||||
|
||||
if (!available) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'qmd_not_available'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const paths = options.paths.join(' ');
|
||||
const contextArg = options.context ? `--context "${options.context}"` : '';
|
||||
|
||||
await execAsync(
|
||||
`qmd add "${options.name}" ${paths} ${contextArg}`
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'create_failed',
|
||||
data: error instanceof Error ? error.message : String(error)
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Trigger reindexing of a collection
|
||||
*/
|
||||
async index(collectionName: string): Promise<QmdResult> {
|
||||
const available = await this.isAvailable();
|
||||
|
||||
if (!available) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'qmd_not_available'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await execAsync(`qmd update "${collectionName}"`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'index_failed',
|
||||
data: error instanceof Error ? error.message : String(error)
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get status of a collection
|
||||
*/
|
||||
async status(collectionName: string): Promise<QmdStatus> {
|
||||
const available = await this.isAvailable();
|
||||
|
||||
if (!available) {
|
||||
return { available: false };
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout } = await execAsync(`qmd status "${collectionName}" --json`);
|
||||
const parsed = JSON.parse(stdout);
|
||||
|
||||
return {
|
||||
available: true,
|
||||
collection: collectionName,
|
||||
documentCount: parsed.documentCount || 0,
|
||||
lastIndexed: parsed.lastIndexed
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
available: true,
|
||||
collection: collectionName,
|
||||
documentCount: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
137
gsd-memory/src/registry.ts
Normal file
137
gsd-memory/src/registry.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
export interface RegisteredProject {
|
||||
name: string;
|
||||
path: string;
|
||||
qmdCollection?: string;
|
||||
registeredAt: string;
|
||||
lastIndexed?: string;
|
||||
}
|
||||
|
||||
export interface Registry {
|
||||
version: string;
|
||||
projects: RegisteredProject[];
|
||||
}
|
||||
|
||||
const REGISTRY_DIR = join(homedir(), '.gsd');
|
||||
const REGISTRY_PATH = join(REGISTRY_DIR, 'projects.json');
|
||||
|
||||
/**
|
||||
* Ensure the registry directory exists
|
||||
*/
|
||||
function ensureRegistryDir(): void {
|
||||
if (!existsSync(REGISTRY_DIR)) {
|
||||
mkdirSync(REGISTRY_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the project registry
|
||||
*/
|
||||
export function readRegistry(): Registry {
|
||||
ensureRegistryDir();
|
||||
|
||||
if (!existsSync(REGISTRY_PATH)) {
|
||||
return { version: '1.0.0', projects: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(REGISTRY_PATH, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
return { version: '1.0.0', projects: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the project registry
|
||||
*/
|
||||
export function writeRegistry(registry: Registry): void {
|
||||
ensureRegistryDir();
|
||||
writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new project
|
||||
*/
|
||||
export function registerProject(project: Omit<RegisteredProject, 'registeredAt'>): RegisteredProject {
|
||||
const registry = readRegistry();
|
||||
|
||||
// Check if project already exists
|
||||
const existingIndex = registry.projects.findIndex(
|
||||
p => p.path === project.path || p.name === project.name
|
||||
);
|
||||
|
||||
const newProject: RegisteredProject = {
|
||||
...project,
|
||||
registeredAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
// Update existing project
|
||||
registry.projects[existingIndex] = {
|
||||
...registry.projects[existingIndex],
|
||||
...newProject,
|
||||
registeredAt: registry.projects[existingIndex].registeredAt
|
||||
};
|
||||
} else {
|
||||
// Add new project
|
||||
registry.projects.push(newProject);
|
||||
}
|
||||
|
||||
writeRegistry(registry);
|
||||
return newProject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a project's last indexed time
|
||||
*/
|
||||
export function updateProjectIndexTime(name: string): void {
|
||||
const registry = readRegistry();
|
||||
const project = registry.projects.find(p => p.name === name);
|
||||
|
||||
if (project) {
|
||||
project.lastIndexed = new Date().toISOString();
|
||||
writeRegistry(registry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a project by name
|
||||
*/
|
||||
export function getProject(name: string): RegisteredProject | undefined {
|
||||
const registry = readRegistry();
|
||||
return registry.projects.find(p => p.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered projects
|
||||
*/
|
||||
export function getAllProjects(): RegisteredProject[] {
|
||||
return readRegistry().projects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a project from the registry
|
||||
*/
|
||||
export function unregisterProject(name: string): boolean {
|
||||
const registry = readRegistry();
|
||||
const index = registry.projects.findIndex(p => p.name === name);
|
||||
|
||||
if (index >= 0) {
|
||||
registry.projects.splice(index, 1);
|
||||
writeRegistry(registry);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the registry path (for testing)
|
||||
*/
|
||||
export function getRegistryPath(): string {
|
||||
return REGISTRY_PATH;
|
||||
}
|
||||
153
gsd-memory/src/tools/decisions.ts
Normal file
153
gsd-memory/src/tools/decisions.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { getAllProjects } from '../registry.js';
|
||||
import { extractSummary, type SummaryData } from '../extractors/summary.js';
|
||||
import { extractProject, type ProjectData } from '../extractors/project.js';
|
||||
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
||||
import { join, basename } from 'path';
|
||||
|
||||
export interface DecisionResult {
|
||||
decision: string;
|
||||
rationale?: string;
|
||||
project: string;
|
||||
phase?: string;
|
||||
source: string;
|
||||
date?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface DecisionOptions {
|
||||
query?: string;
|
||||
project?: string;
|
||||
tags?: string[];
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for decisions across all registered GSD projects
|
||||
*/
|
||||
export async function findDecisions(options: DecisionOptions): Promise<DecisionResult[]> {
|
||||
const projects = options.project
|
||||
? getAllProjects().filter(p => p.name === options.project)
|
||||
: getAllProjects();
|
||||
|
||||
const allDecisions: DecisionResult[] = [];
|
||||
|
||||
for (const project of projects) {
|
||||
const planningPath = join(project.path, '.planning');
|
||||
if (!existsSync(planningPath)) continue;
|
||||
|
||||
// Extract decisions from PROJECT.md
|
||||
const projectDecisions = extractProjectDecisions(planningPath, project.name);
|
||||
allDecisions.push(...projectDecisions);
|
||||
|
||||
// Extract decisions from all SUMMARY.md files
|
||||
const summaryDecisions = extractSummaryDecisions(planningPath, project.name);
|
||||
allDecisions.push(...summaryDecisions);
|
||||
}
|
||||
|
||||
// Filter by query if provided
|
||||
let filtered = allDecisions;
|
||||
if (options.query) {
|
||||
const query = options.query.toLowerCase();
|
||||
filtered = allDecisions.filter(d =>
|
||||
d.decision.toLowerCase().includes(query) ||
|
||||
d.rationale?.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by tags if provided
|
||||
if (options.tags && options.tags.length > 0) {
|
||||
const tags = options.tags.map(t => t.toLowerCase());
|
||||
filtered = filtered.filter(d =>
|
||||
d.tags?.some(t => tags.includes(t.toLowerCase()))
|
||||
);
|
||||
}
|
||||
|
||||
// Apply limit
|
||||
const limit = options.limit || 50;
|
||||
return filtered.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract decisions from PROJECT.md
|
||||
*/
|
||||
function extractProjectDecisions(planningPath: string, projectName: string): DecisionResult[] {
|
||||
const projectPath = join(planningPath, 'PROJECT.md');
|
||||
if (!existsSync(projectPath)) return [];
|
||||
|
||||
try {
|
||||
const content = readFileSync(projectPath, 'utf-8');
|
||||
const projectData = extractProject(content);
|
||||
|
||||
return projectData.decisions.map(d => ({
|
||||
decision: d.decision,
|
||||
rationale: d.rationale,
|
||||
project: projectName,
|
||||
source: projectPath
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract decisions from all SUMMARY.md files in a project
|
||||
*/
|
||||
function extractSummaryDecisions(planningPath: string, projectName: string): DecisionResult[] {
|
||||
const decisions: DecisionResult[] = [];
|
||||
const phasesPath = join(planningPath, 'phases');
|
||||
|
||||
if (!existsSync(phasesPath)) return decisions;
|
||||
|
||||
// Find all SUMMARY.md files
|
||||
const summaryFiles = findSummaryFiles(phasesPath);
|
||||
|
||||
for (const file of summaryFiles) {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
const summaryData = extractSummary(content);
|
||||
|
||||
for (const decision of summaryData.decisions) {
|
||||
decisions.push({
|
||||
decision: decision.decision,
|
||||
project: projectName,
|
||||
phase: summaryData.phase,
|
||||
source: file,
|
||||
date: summaryData.completed,
|
||||
tags: summaryData.tags
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Skip files that can't be parsed
|
||||
}
|
||||
}
|
||||
|
||||
return decisions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all SUMMARY.md files
|
||||
*/
|
||||
function findSummaryFiles(dir: string, files: string[] = []): string[] {
|
||||
try {
|
||||
const entries = readdirSync(dir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
findSummaryFiles(fullPath, files);
|
||||
} else if (entry.toUpperCase().includes('SUMMARY') && entry.endsWith('.md')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
} catch {
|
||||
// Skip entries that can't be accessed
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Return empty if directory can't be read
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
133
gsd-memory/src/tools/index-tool.ts
Normal file
133
gsd-memory/src/tools/index-tool.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { getProject, updateProjectIndexTime, getAllProjects } from '../registry.js';
|
||||
import { qmd } from '../qmd.js';
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface IndexOptions {
|
||||
path?: string;
|
||||
project?: string;
|
||||
}
|
||||
|
||||
export interface IndexResult {
|
||||
success: boolean;
|
||||
project?: string;
|
||||
documentsIndexed?: number;
|
||||
error?: string;
|
||||
qmdUsed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger indexing/update for a project
|
||||
*/
|
||||
export async function index(options: IndexOptions): Promise<IndexResult> {
|
||||
// Find the project
|
||||
let projectName: string | undefined;
|
||||
let projectPath: string | undefined;
|
||||
let qmdCollection: string | undefined;
|
||||
|
||||
if (options.project) {
|
||||
const project = getProject(options.project);
|
||||
if (!project) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Project not found: ${options.project}`,
|
||||
qmdUsed: false
|
||||
};
|
||||
}
|
||||
projectName = project.name;
|
||||
projectPath = project.path;
|
||||
qmdCollection = project.qmdCollection;
|
||||
} else if (options.path) {
|
||||
// Find project by path
|
||||
const projects = getAllProjects();
|
||||
const project = projects.find(p => p.path === options.path);
|
||||
|
||||
if (!project) {
|
||||
return {
|
||||
success: false,
|
||||
error: `No project registered for path: ${options.path}`,
|
||||
qmdUsed: false
|
||||
};
|
||||
}
|
||||
|
||||
projectName = project.name;
|
||||
projectPath = project.path;
|
||||
qmdCollection = project.qmdCollection;
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Either project or path must be specified',
|
||||
qmdUsed: false
|
||||
};
|
||||
}
|
||||
|
||||
// Verify planning directory exists
|
||||
const planningPath = join(projectPath, '.planning');
|
||||
if (!existsSync(planningPath)) {
|
||||
return {
|
||||
success: false,
|
||||
project: projectName,
|
||||
error: `No .planning directory found at: ${planningPath}`,
|
||||
qmdUsed: false
|
||||
};
|
||||
}
|
||||
|
||||
// Check if QMD is available
|
||||
const qmdAvailable = await qmd.isAvailable();
|
||||
|
||||
if (qmdAvailable && qmdCollection) {
|
||||
// Use QMD to update the collection
|
||||
const result = await qmd.index(qmdCollection);
|
||||
|
||||
if (result.success) {
|
||||
updateProjectIndexTime(projectName);
|
||||
|
||||
// Get document count
|
||||
const status = await qmd.status(qmdCollection);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
project: projectName,
|
||||
documentsIndexed: status.documentCount,
|
||||
qmdUsed: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
project: projectName,
|
||||
error: `QMD indexing failed: ${result.reason}`,
|
||||
qmdUsed: true
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// No QMD - just update the timestamp
|
||||
// Grep fallback will search files directly
|
||||
updateProjectIndexTime(projectName);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
project: projectName,
|
||||
qmdUsed: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Index all registered projects
|
||||
*/
|
||||
export async function indexAll(): Promise<{
|
||||
success: boolean;
|
||||
results: IndexResult[];
|
||||
}> {
|
||||
const projects = getAllProjects();
|
||||
const results: IndexResult[] = [];
|
||||
|
||||
for (const project of projects) {
|
||||
const result = await index({ project: project.name });
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
const success = results.every(r => r.success);
|
||||
|
||||
return { success, results };
|
||||
}
|
||||
108
gsd-memory/src/tools/patterns.ts
Normal file
108
gsd-memory/src/tools/patterns.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { getAllProjects } from '../registry.js';
|
||||
import { extractSummary } from '../extractors/summary.js';
|
||||
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface PatternResult {
|
||||
pattern: string;
|
||||
project: string;
|
||||
phase: string;
|
||||
source: string;
|
||||
date?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface PatternOptions {
|
||||
query?: string;
|
||||
project?: string;
|
||||
tags?: string[];
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for patterns established across all registered GSD projects
|
||||
*/
|
||||
export async function findPatterns(options: PatternOptions): Promise<PatternResult[]> {
|
||||
const projects = options.project
|
||||
? getAllProjects().filter(p => p.name === options.project)
|
||||
: getAllProjects();
|
||||
|
||||
const allPatterns: PatternResult[] = [];
|
||||
|
||||
for (const project of projects) {
|
||||
const phasesPath = join(project.path, '.planning', 'phases');
|
||||
if (!existsSync(phasesPath)) continue;
|
||||
|
||||
// Find all SUMMARY.md files and extract patterns
|
||||
const summaryFiles = findSummaryFiles(phasesPath);
|
||||
|
||||
for (const file of summaryFiles) {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
const summaryData = extractSummary(content);
|
||||
|
||||
for (const pattern of summaryData.patterns) {
|
||||
allPatterns.push({
|
||||
pattern,
|
||||
project: project.name,
|
||||
phase: summaryData.phase,
|
||||
source: file,
|
||||
date: summaryData.completed,
|
||||
tags: summaryData.tags
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Skip files that can't be parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by query if provided
|
||||
let filtered = allPatterns;
|
||||
if (options.query) {
|
||||
const query = options.query.toLowerCase();
|
||||
filtered = allPatterns.filter(p =>
|
||||
p.pattern.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by tags if provided
|
||||
if (options.tags && options.tags.length > 0) {
|
||||
const tags = options.tags.map(t => t.toLowerCase());
|
||||
filtered = filtered.filter(p =>
|
||||
p.tags?.some(t => tags.includes(t.toLowerCase()))
|
||||
);
|
||||
}
|
||||
|
||||
// Apply limit
|
||||
const limit = options.limit || 50;
|
||||
return filtered.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all SUMMARY.md files
|
||||
*/
|
||||
function findSummaryFiles(dir: string, files: string[] = []): string[] {
|
||||
try {
|
||||
const entries = readdirSync(dir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
findSummaryFiles(fullPath, files);
|
||||
} else if (entry.toUpperCase().includes('SUMMARY') && entry.endsWith('.md')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
} catch {
|
||||
// Skip entries that can't be accessed
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Return empty if directory can't be read
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
111
gsd-memory/src/tools/pitfalls.ts
Normal file
111
gsd-memory/src/tools/pitfalls.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { getAllProjects } from '../registry.js';
|
||||
import { extractResearch, type Pitfall } from '../extractors/research.js';
|
||||
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface PitfallResult extends Pitfall {
|
||||
project: string;
|
||||
phase?: string;
|
||||
domain: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface PitfallOptions {
|
||||
query?: string;
|
||||
project?: string;
|
||||
domain?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for pitfalls documented across all registered GSD projects
|
||||
*/
|
||||
export async function findPitfalls(options: PitfallOptions): Promise<PitfallResult[]> {
|
||||
const projects = options.project
|
||||
? getAllProjects().filter(p => p.name === options.project)
|
||||
: getAllProjects();
|
||||
|
||||
const allPitfalls: PitfallResult[] = [];
|
||||
|
||||
for (const project of projects) {
|
||||
const planningPath = join(project.path, '.planning');
|
||||
if (!existsSync(planningPath)) continue;
|
||||
|
||||
// Find all RESEARCH.md files
|
||||
const researchFiles = findResearchFiles(planningPath);
|
||||
|
||||
for (const file of researchFiles) {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
const researchData = extractResearch(content);
|
||||
|
||||
// Extract phase from path if present
|
||||
const phaseMatch = file.match(/phases\/(\d+-[^/]+)/);
|
||||
const phase = phaseMatch ? phaseMatch[1] : undefined;
|
||||
|
||||
for (const pitfall of researchData.pitfalls) {
|
||||
allPitfalls.push({
|
||||
...pitfall,
|
||||
project: project.name,
|
||||
phase,
|
||||
domain: researchData.domain,
|
||||
source: file
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Skip files that can't be parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by query if provided
|
||||
let filtered = allPitfalls;
|
||||
if (options.query) {
|
||||
const query = options.query.toLowerCase();
|
||||
filtered = allPitfalls.filter(p =>
|
||||
p.name.toLowerCase().includes(query) ||
|
||||
p.description.toLowerCase().includes(query) ||
|
||||
p.prevention.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by domain if provided
|
||||
if (options.domain) {
|
||||
const domain = options.domain.toLowerCase();
|
||||
filtered = filtered.filter(p =>
|
||||
p.domain.toLowerCase().includes(domain)
|
||||
);
|
||||
}
|
||||
|
||||
// Apply limit
|
||||
const limit = options.limit || 50;
|
||||
return filtered.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all RESEARCH.md files
|
||||
*/
|
||||
function findResearchFiles(dir: string, files: string[] = []): string[] {
|
||||
try {
|
||||
const entries = readdirSync(dir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory() && !entry.startsWith('.')) {
|
||||
findResearchFiles(fullPath, files);
|
||||
} else if (entry.toUpperCase().includes('RESEARCH') && entry.endsWith('.md')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
} catch {
|
||||
// Skip entries that can't be accessed
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Return empty if directory can't be read
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
146
gsd-memory/src/tools/register.ts
Normal file
146
gsd-memory/src/tools/register.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { registerProject, getProject, getAllProjects, unregisterProject, type RegisteredProject } from '../registry.js';
|
||||
import { qmd } from '../qmd.js';
|
||||
import { existsSync } from 'fs';
|
||||
import { join, basename } from 'path';
|
||||
|
||||
export interface RegisterOptions {
|
||||
path: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface RegisterResult {
|
||||
success: boolean;
|
||||
project?: RegisteredProject;
|
||||
qmdCollection?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a project with GSD memory
|
||||
*/
|
||||
export async function register(options: RegisterOptions): Promise<RegisterResult> {
|
||||
// Validate path exists
|
||||
if (!existsSync(options.path)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Path does not exist: ${options.path}`
|
||||
};
|
||||
}
|
||||
|
||||
// Validate .planning directory exists
|
||||
const planningPath = join(options.path, '.planning');
|
||||
if (!existsSync(planningPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `No .planning directory found at: ${planningPath}`
|
||||
};
|
||||
}
|
||||
|
||||
// Determine project name
|
||||
const name = options.name || basename(options.path);
|
||||
|
||||
// Check if QMD is available for semantic search
|
||||
const qmdAvailable = await qmd.isAvailable();
|
||||
let qmdCollection: string | undefined;
|
||||
|
||||
if (qmdAvailable) {
|
||||
// Create QMD collection
|
||||
qmdCollection = `gsd-${name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
|
||||
|
||||
const qmdResult = await qmd.createCollection({
|
||||
name: qmdCollection,
|
||||
paths: [planningPath],
|
||||
context: `GSD project: ${name}`
|
||||
});
|
||||
|
||||
if (!qmdResult.success) {
|
||||
// QMD collection creation failed, but we can still register
|
||||
qmdCollection = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Register in local registry
|
||||
const project = registerProject({
|
||||
name,
|
||||
path: options.path,
|
||||
qmdCollection
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
project,
|
||||
qmdCollection
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status of all registered projects
|
||||
*/
|
||||
export async function status(): Promise<{
|
||||
qmdAvailable: boolean;
|
||||
projects: Array<{
|
||||
name: string;
|
||||
path: string;
|
||||
qmdCollection?: string;
|
||||
registeredAt: string;
|
||||
lastIndexed?: string;
|
||||
documentCount?: number;
|
||||
}>;
|
||||
}> {
|
||||
const qmdAvailable = await qmd.isAvailable();
|
||||
const projects = getAllProjects();
|
||||
|
||||
const enrichedProjects = await Promise.all(
|
||||
projects.map(async (project) => {
|
||||
let documentCount: number | undefined;
|
||||
|
||||
if (qmdAvailable && project.qmdCollection) {
|
||||
const qmdStatus = await qmd.status(project.qmdCollection);
|
||||
documentCount = qmdStatus.documentCount;
|
||||
}
|
||||
|
||||
return {
|
||||
name: project.name,
|
||||
path: project.path,
|
||||
qmdCollection: project.qmdCollection,
|
||||
registeredAt: project.registeredAt,
|
||||
lastIndexed: project.lastIndexed,
|
||||
documentCount
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
qmdAvailable,
|
||||
projects: enrichedProjects
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a project
|
||||
*/
|
||||
export async function unregister(name: string): Promise<{ success: boolean; error?: string }> {
|
||||
const project = getProject(name);
|
||||
|
||||
if (!project) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Project not found: ${name}`
|
||||
};
|
||||
}
|
||||
|
||||
// Remove from QMD if it has a collection
|
||||
if (project.qmdCollection && await qmd.isAvailable()) {
|
||||
try {
|
||||
// QMD doesn't have a direct remove command, so we just skip
|
||||
// The collection will be orphaned but harmless
|
||||
} catch {
|
||||
// Ignore QMD errors
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from registry
|
||||
unregisterProject(name);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
117
gsd-memory/src/tools/search.ts
Normal file
117
gsd-memory/src/tools/search.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { qmd, QmdSearchResult } from '../qmd.js';
|
||||
import { getAllProjects } from '../registry.js';
|
||||
import { extractFrontmatter } from '../extractors/frontmatter.js';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join, basename, dirname } from 'path';
|
||||
|
||||
export interface SearchResult {
|
||||
content: string;
|
||||
source: string;
|
||||
project: string;
|
||||
phase?: string;
|
||||
relevance: number;
|
||||
documentType?: 'summary' | 'research' | 'project' | 'plan' | 'other';
|
||||
}
|
||||
|
||||
export interface SearchOptions {
|
||||
query: string;
|
||||
project?: string;
|
||||
tags?: string[];
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search across all registered GSD projects
|
||||
*/
|
||||
export async function search(options: SearchOptions): Promise<SearchResult[]> {
|
||||
const projects = options.project
|
||||
? getAllProjects().filter(p => p.name === options.project)
|
||||
: getAllProjects();
|
||||
|
||||
if (projects.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allResults: SearchResult[] = [];
|
||||
|
||||
for (const project of projects) {
|
||||
const planningPath = join(project.path, '.planning');
|
||||
|
||||
if (!existsSync(planningPath)) continue;
|
||||
|
||||
// Search using QMD or grep fallback
|
||||
const searchResults = await qmd.search({
|
||||
query: options.query,
|
||||
paths: [planningPath],
|
||||
collection: project.qmdCollection,
|
||||
limit: options.limit || 20
|
||||
});
|
||||
|
||||
// Enrich results with GSD context
|
||||
for (const result of searchResults) {
|
||||
const enriched = enrichResult(result, project.name);
|
||||
allResults.push(enriched);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by relevance
|
||||
allResults.sort((a, b) => b.relevance - a.relevance);
|
||||
|
||||
// Apply limit
|
||||
const limit = options.limit || 20;
|
||||
return allResults.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich a search result with GSD-specific context
|
||||
*/
|
||||
function enrichResult(result: QmdSearchResult, projectName: string): SearchResult {
|
||||
const enriched: SearchResult = {
|
||||
content: result.content,
|
||||
source: result.source,
|
||||
project: projectName,
|
||||
relevance: result.relevance,
|
||||
documentType: getDocumentType(result.source)
|
||||
};
|
||||
|
||||
// Try to extract phase from frontmatter or path
|
||||
enriched.phase = extractPhase(result.source);
|
||||
|
||||
return enriched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine document type from file path/name
|
||||
*/
|
||||
function getDocumentType(source: string): SearchResult['documentType'] {
|
||||
const filename = basename(source).toUpperCase();
|
||||
|
||||
if (filename.includes('SUMMARY')) return 'summary';
|
||||
if (filename.includes('RESEARCH')) return 'research';
|
||||
if (filename.includes('PROJECT')) return 'project';
|
||||
if (filename.includes('PLAN')) return 'plan';
|
||||
|
||||
return 'other';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract phase identifier from file path or content
|
||||
*/
|
||||
function extractPhase(source: string): string | undefined {
|
||||
// Try to get from path: .planning/phases/01-foundation/...
|
||||
const pathMatch = source.match(/phases\/(\d+-[^/]+)/);
|
||||
if (pathMatch) return pathMatch[1];
|
||||
|
||||
// Try to get from frontmatter
|
||||
try {
|
||||
if (existsSync(source)) {
|
||||
const content = readFileSync(source, 'utf-8');
|
||||
const frontmatter = extractFrontmatter(content);
|
||||
if (frontmatter.phase) return String(frontmatter.phase);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors reading file
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
171
gsd-memory/src/tools/stack.ts
Normal file
171
gsd-memory/src/tools/stack.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { getAllProjects } from '../registry.js';
|
||||
import { extractSummary, type TechStackEntry } from '../extractors/summary.js';
|
||||
import { extractResearch } from '../extractors/research.js';
|
||||
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface StackResult {
|
||||
library: string;
|
||||
version?: string;
|
||||
purpose?: string;
|
||||
project: string;
|
||||
phase?: string;
|
||||
source: string;
|
||||
sourceType: 'summary' | 'research';
|
||||
}
|
||||
|
||||
export interface StackOptions {
|
||||
query?: string;
|
||||
project?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for tech stack entries across all registered GSD projects
|
||||
*/
|
||||
export async function findStack(options: StackOptions): Promise<StackResult[]> {
|
||||
const projects = options.project
|
||||
? getAllProjects().filter(p => p.name === options.project)
|
||||
: getAllProjects();
|
||||
|
||||
const allStack: StackResult[] = [];
|
||||
|
||||
for (const project of projects) {
|
||||
const planningPath = join(project.path, '.planning');
|
||||
if (!existsSync(planningPath)) continue;
|
||||
|
||||
// Extract from SUMMARY.md files
|
||||
const summaryStack = extractFromSummaries(planningPath, project.name);
|
||||
allStack.push(...summaryStack);
|
||||
|
||||
// Extract from RESEARCH.md files
|
||||
const researchStack = extractFromResearch(planningPath, project.name);
|
||||
allStack.push(...researchStack);
|
||||
}
|
||||
|
||||
// Filter by query if provided
|
||||
let filtered = allStack;
|
||||
if (options.query) {
|
||||
const query = options.query.toLowerCase();
|
||||
filtered = allStack.filter(s =>
|
||||
s.library.toLowerCase().includes(query) ||
|
||||
s.purpose?.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
// Deduplicate by library name within each project
|
||||
const seen = new Set<string>();
|
||||
const deduplicated = filtered.filter(s => {
|
||||
const key = `${s.project}:${s.library}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Apply limit
|
||||
const limit = options.limit || 100;
|
||||
return deduplicated.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tech stack from all SUMMARY.md files
|
||||
*/
|
||||
function extractFromSummaries(planningPath: string, projectName: string): StackResult[] {
|
||||
const stack: StackResult[] = [];
|
||||
const phasesPath = join(planningPath, 'phases');
|
||||
|
||||
if (!existsSync(phasesPath)) return stack;
|
||||
|
||||
const summaryFiles = findFiles(phasesPath, 'SUMMARY');
|
||||
|
||||
for (const file of summaryFiles) {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
const summaryData = extractSummary(content);
|
||||
|
||||
// Extract phase from path
|
||||
const phaseMatch = file.match(/phases\/(\d+-[^/]+)/);
|
||||
const phase = phaseMatch ? phaseMatch[1] : undefined;
|
||||
|
||||
for (const entry of summaryData.techStack) {
|
||||
stack.push({
|
||||
library: entry.library,
|
||||
version: entry.version,
|
||||
purpose: entry.purpose,
|
||||
project: projectName,
|
||||
phase,
|
||||
source: file,
|
||||
sourceType: 'summary'
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Skip files that can't be parsed
|
||||
}
|
||||
}
|
||||
|
||||
return stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tech stack from all RESEARCH.md files
|
||||
*/
|
||||
function extractFromResearch(planningPath: string, projectName: string): StackResult[] {
|
||||
const stack: StackResult[] = [];
|
||||
|
||||
const researchFiles = findFiles(planningPath, 'RESEARCH');
|
||||
|
||||
for (const file of researchFiles) {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
const researchData = extractResearch(content);
|
||||
|
||||
// Extract phase from path
|
||||
const phaseMatch = file.match(/phases\/(\d+-[^/]+)/);
|
||||
const phase = phaseMatch ? phaseMatch[1] : undefined;
|
||||
|
||||
for (const entry of researchData.standardStack) {
|
||||
stack.push({
|
||||
library: entry.library,
|
||||
version: entry.version,
|
||||
purpose: entry.purpose,
|
||||
project: projectName,
|
||||
phase,
|
||||
source: file,
|
||||
sourceType: 'research'
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Skip files that can't be parsed
|
||||
}
|
||||
}
|
||||
|
||||
return stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find files matching a pattern
|
||||
*/
|
||||
function findFiles(dir: string, pattern: string, files: string[] = []): string[] {
|
||||
try {
|
||||
const entries = readdirSync(dir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory() && !entry.startsWith('.')) {
|
||||
findFiles(fullPath, pattern, files);
|
||||
} else if (entry.toUpperCase().includes(pattern) && entry.endsWith('.md')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
} catch {
|
||||
// Skip entries that can't be accessed
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Return empty if directory can't be read
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
94
gsd-memory/src/tools/status.ts
Normal file
94
gsd-memory/src/tools/status.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { getAllProjects } from '../registry.js';
|
||||
import { qmd } from '../qmd.js';
|
||||
import { existsSync, readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface ProjectStatus {
|
||||
name: string;
|
||||
path: string;
|
||||
exists: boolean;
|
||||
hasPlanningDir: boolean;
|
||||
qmdCollection?: string;
|
||||
qmdAvailable: boolean;
|
||||
documentCount: number;
|
||||
registeredAt: string;
|
||||
lastIndexed?: string;
|
||||
}
|
||||
|
||||
export interface MemoryStatus {
|
||||
qmdAvailable: boolean;
|
||||
qmdVersion?: string;
|
||||
totalProjects: number;
|
||||
totalDocuments: number;
|
||||
projects: ProjectStatus[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive status of GSD memory system
|
||||
*/
|
||||
export async function getStatus(): Promise<MemoryStatus> {
|
||||
const qmdAvailable = await qmd.isAvailable();
|
||||
const projects = getAllProjects();
|
||||
|
||||
let totalDocuments = 0;
|
||||
const projectStatuses: ProjectStatus[] = [];
|
||||
|
||||
for (const project of projects) {
|
||||
const planningPath = join(project.path, '.planning');
|
||||
const exists = existsSync(project.path);
|
||||
const hasPlanningDir = existsSync(planningPath);
|
||||
|
||||
// Count documents in .planning directory
|
||||
const documentCount = hasPlanningDir ? countMarkdownFiles(planningPath) : 0;
|
||||
totalDocuments += documentCount;
|
||||
|
||||
projectStatuses.push({
|
||||
name: project.name,
|
||||
path: project.path,
|
||||
exists,
|
||||
hasPlanningDir,
|
||||
qmdCollection: project.qmdCollection,
|
||||
qmdAvailable: qmdAvailable && !!project.qmdCollection,
|
||||
documentCount,
|
||||
registeredAt: project.registeredAt,
|
||||
lastIndexed: project.lastIndexed
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
qmdAvailable,
|
||||
totalProjects: projects.length,
|
||||
totalDocuments,
|
||||
projects: projectStatuses
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Count markdown files in a directory recursively
|
||||
*/
|
||||
function countMarkdownFiles(dir: string): number {
|
||||
let count = 0;
|
||||
|
||||
try {
|
||||
const entries = readdirSync(dir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory() && !entry.startsWith('.')) {
|
||||
count += countMarkdownFiles(fullPath);
|
||||
} else if (stat.isFile() && entry.endsWith('.md')) {
|
||||
count++;
|
||||
}
|
||||
} catch {
|
||||
// Skip entries that can't be accessed
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Return 0 if directory can't be read
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
141
gsd-memory/tests/extractors/frontmatter.test.ts
Normal file
141
gsd-memory/tests/extractors/frontmatter.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { extractFrontmatter } from '../../src/extractors/frontmatter.js';
|
||||
import { readFixture } from '../setup.js';
|
||||
|
||||
describe('frontmatter extractor', () => {
|
||||
it('extracts YAML frontmatter from markdown', () => {
|
||||
const md = `---
|
||||
phase: 01-foundation
|
||||
plan: "01"
|
||||
tags: [auth, jwt]
|
||||
---
|
||||
# Content`;
|
||||
const result = extractFrontmatter(md);
|
||||
expect(result.phase).toBe('01-foundation');
|
||||
expect(result.plan).toBe('01');
|
||||
expect(result.tags).toEqual(['auth', 'jwt']);
|
||||
});
|
||||
|
||||
it('parses numeric plan values as numbers', () => {
|
||||
const md = `---
|
||||
phase: 01-foundation
|
||||
plan: 01
|
||||
---
|
||||
# Content`;
|
||||
const result = extractFrontmatter(md);
|
||||
expect(result.plan).toBe(1); // YAML parses 01 as number
|
||||
});
|
||||
|
||||
it('returns empty object for no frontmatter', () => {
|
||||
const result = extractFrontmatter('# Just content');
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('returns empty object for markdown starting with heading', () => {
|
||||
const md = `# Title
|
||||
|
||||
Some content here.`;
|
||||
const result = extractFrontmatter(md);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('handles malformed YAML gracefully', () => {
|
||||
const md = `---
|
||||
invalid: [unclosed
|
||||
broken: yaml: here
|
||||
---
|
||||
# Content`;
|
||||
expect(() => extractFrontmatter(md)).not.toThrow();
|
||||
const result = extractFrontmatter(md);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('handles empty frontmatter', () => {
|
||||
const md = `---
|
||||
---
|
||||
# Content`;
|
||||
const result = extractFrontmatter(md);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('extracts nested objects', () => {
|
||||
const md = `---
|
||||
tech-stack:
|
||||
added:
|
||||
- jose: 5.2.0
|
||||
- bcrypt: 5.1.1
|
||||
patterns:
|
||||
- httpOnly cookies
|
||||
---
|
||||
# Content`;
|
||||
const result = extractFrontmatter(md);
|
||||
expect(result['tech-stack']).toBeDefined();
|
||||
// YAML parses these as objects with key-value pairs
|
||||
expect(result['tech-stack'].added).toHaveLength(2);
|
||||
expect(result['tech-stack'].added[0]).toHaveProperty('jose', '5.2.0');
|
||||
expect(result['tech-stack'].patterns).toContain('httpOnly cookies');
|
||||
});
|
||||
|
||||
it('extracts requires/provides structure', () => {
|
||||
const md = `---
|
||||
requires:
|
||||
- phase: 01-auth
|
||||
provides: User model
|
||||
provides:
|
||||
- Dashboard component
|
||||
- Real-time updates
|
||||
---
|
||||
# Content`;
|
||||
const result = extractFrontmatter(md);
|
||||
expect(result.requires).toHaveLength(1);
|
||||
expect(result.requires[0].phase).toBe('01-auth');
|
||||
expect(result.provides).toContain('Dashboard component');
|
||||
});
|
||||
|
||||
it('extracts from real SUMMARY.md fixture', () => {
|
||||
const summary = readFixture('sample-summary.md');
|
||||
const result = extractFrontmatter(summary);
|
||||
|
||||
expect(result.phase).toBe('01-foundation');
|
||||
expect(result.plan).toBe(1); // YAML parses 01 as number
|
||||
expect(result.subsystem).toBe('auth');
|
||||
expect(result.tags).toContain('jwt');
|
||||
expect(result['key-decisions']).toBeDefined();
|
||||
expect(result['key-decisions'].length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('extracts from real RESEARCH.md fixture', () => {
|
||||
const research = readFixture('sample-research.md');
|
||||
const result = extractFrontmatter(research);
|
||||
|
||||
// Research frontmatter structure (lowercase keys)
|
||||
expect(result).toHaveProperty('researched');
|
||||
expect(result).toHaveProperty('domain');
|
||||
expect(result).toHaveProperty('confidence');
|
||||
expect(result.confidence).toBe('HIGH');
|
||||
});
|
||||
|
||||
it('handles multiline strings in frontmatter', () => {
|
||||
const md = `---
|
||||
description: |
|
||||
This is a multiline
|
||||
description that spans
|
||||
multiple lines
|
||||
---
|
||||
# Content`;
|
||||
const result = extractFrontmatter(md);
|
||||
expect(result.description).toContain('multiline');
|
||||
expect(result.description).toContain('multiple lines');
|
||||
});
|
||||
|
||||
it('handles special characters in values', () => {
|
||||
const md = `---
|
||||
decision: "Used jose instead of jsonwebtoken - ESM native"
|
||||
path: src/lib/auth.ts
|
||||
---
|
||||
# Content`;
|
||||
const result = extractFrontmatter(md);
|
||||
expect(result.decision).toBe('Used jose instead of jsonwebtoken - ESM native');
|
||||
expect(result.path).toBe('src/lib/auth.ts');
|
||||
});
|
||||
});
|
||||
109
gsd-memory/tests/extractors/project.test.ts
Normal file
109
gsd-memory/tests/extractors/project.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { extractProject, type ProjectData } from '../../src/extractors/project.js';
|
||||
import { readFixture } from '../setup.js';
|
||||
|
||||
describe('project extractor', () => {
|
||||
it('extracts project name from title', () => {
|
||||
const project = readFixture('sample-project.md');
|
||||
const result = extractProject(project);
|
||||
|
||||
expect(result.name).toBe('TaskFlow');
|
||||
});
|
||||
|
||||
it('extracts what this is description', () => {
|
||||
const project = readFixture('sample-project.md');
|
||||
const result = extractProject(project);
|
||||
|
||||
expect(result.description).toContain('task management');
|
||||
expect(result.description).toContain('terminal');
|
||||
});
|
||||
|
||||
it('extracts core value', () => {
|
||||
const project = readFixture('sample-project.md');
|
||||
const result = extractProject(project);
|
||||
|
||||
expect(result.coreValue).toContain('Fast task capture');
|
||||
});
|
||||
|
||||
it('extracts validated requirements', () => {
|
||||
const project = readFixture('sample-project.md');
|
||||
const result = extractProject(project);
|
||||
|
||||
expect(result.requirements.validated.length).toBeGreaterThan(0);
|
||||
expect(result.requirements.validated[0]).toContain('CLI task creation');
|
||||
});
|
||||
|
||||
it('extracts active requirements', () => {
|
||||
const project = readFixture('sample-project.md');
|
||||
const result = extractProject(project);
|
||||
|
||||
expect(result.requirements.active.length).toBeGreaterThan(0);
|
||||
expect(result.requirements.active).toContainEqual(
|
||||
expect.stringContaining('dashboard')
|
||||
);
|
||||
});
|
||||
|
||||
it('extracts out of scope items', () => {
|
||||
const project = readFixture('sample-project.md');
|
||||
const result = extractProject(project);
|
||||
|
||||
expect(result.requirements.outOfScope.length).toBeGreaterThan(0);
|
||||
expect(result.requirements.outOfScope[0]).toHaveProperty('item');
|
||||
expect(result.requirements.outOfScope[0]).toHaveProperty('reason');
|
||||
});
|
||||
|
||||
it('extracts constraints', () => {
|
||||
const project = readFixture('sample-project.md');
|
||||
const result = extractProject(project);
|
||||
|
||||
expect(result.constraints.length).toBeGreaterThan(0);
|
||||
expect(result.constraints[0]).toHaveProperty('type');
|
||||
expect(result.constraints[0]).toHaveProperty('value');
|
||||
});
|
||||
|
||||
it('extracts key decisions table', () => {
|
||||
const project = readFixture('sample-project.md');
|
||||
const result = extractProject(project);
|
||||
|
||||
expect(result.decisions.length).toBeGreaterThan(0);
|
||||
expect(result.decisions[0]).toHaveProperty('decision');
|
||||
expect(result.decisions[0]).toHaveProperty('rationale');
|
||||
expect(result.decisions[0]).toHaveProperty('outcome');
|
||||
});
|
||||
|
||||
it('extracts context section', () => {
|
||||
const project = readFixture('sample-project.md');
|
||||
const result = extractProject(project);
|
||||
|
||||
expect(result.context).toBeDefined();
|
||||
expect(result.context).toContain('developers');
|
||||
});
|
||||
|
||||
it('handles project without optional sections', () => {
|
||||
const minimalProject = `# MinimalProject
|
||||
|
||||
## What This Is
|
||||
|
||||
A minimal test project.
|
||||
|
||||
## Core Value
|
||||
|
||||
Test core value.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Active
|
||||
|
||||
- [ ] One feature
|
||||
`;
|
||||
|
||||
const result = extractProject(minimalProject);
|
||||
|
||||
expect(result.name).toBe('MinimalProject');
|
||||
expect(result.description).toContain('minimal');
|
||||
expect(result.coreValue).toContain('Test core value');
|
||||
expect(result.requirements.active).toHaveLength(1);
|
||||
expect(result.requirements.validated).toEqual([]);
|
||||
expect(result.decisions).toEqual([]);
|
||||
});
|
||||
});
|
||||
89
gsd-memory/tests/extractors/research.test.ts
Normal file
89
gsd-memory/tests/extractors/research.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { extractResearch, type ResearchData } from '../../src/extractors/research.js';
|
||||
import { readFixture } from '../setup.js';
|
||||
|
||||
describe('research extractor', () => {
|
||||
it('extracts metadata from frontmatter', () => {
|
||||
const research = readFixture('sample-research.md');
|
||||
const result = extractResearch(research);
|
||||
|
||||
expect(result.domain).toBe('WebSocket and Server-Sent Events for real-time updates');
|
||||
expect(result.confidence).toBe('HIGH');
|
||||
expect(result.researched).toBe('2025-01-20');
|
||||
});
|
||||
|
||||
it('extracts pitfalls section', () => {
|
||||
const research = readFixture('sample-research.md');
|
||||
const result = extractResearch(research);
|
||||
|
||||
expect(result.pitfalls.length).toBeGreaterThan(0);
|
||||
expect(result.pitfalls[0]).toHaveProperty('name');
|
||||
expect(result.pitfalls[0]).toHaveProperty('description');
|
||||
expect(result.pitfalls[0]).toHaveProperty('prevention');
|
||||
});
|
||||
|
||||
it('extracts standard stack table', () => {
|
||||
const research = readFixture('sample-research.md');
|
||||
const result = extractResearch(research);
|
||||
|
||||
expect(result.standardStack.length).toBeGreaterThan(0);
|
||||
expect(result.standardStack[0]).toHaveProperty('library');
|
||||
expect(result.standardStack[0]).toHaveProperty('purpose');
|
||||
});
|
||||
|
||||
it('extracts dont-hand-roll items', () => {
|
||||
const research = readFixture('sample-research.md');
|
||||
const result = extractResearch(research);
|
||||
|
||||
expect(result.dontHandRoll.length).toBeGreaterThan(0);
|
||||
expect(result.dontHandRoll[0]).toHaveProperty('problem');
|
||||
expect(result.dontHandRoll[0]).toHaveProperty('useInstead');
|
||||
});
|
||||
|
||||
it('extracts anti-patterns', () => {
|
||||
const research = readFixture('sample-research.md');
|
||||
const result = extractResearch(research);
|
||||
|
||||
expect(result.antiPatterns.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('extracts user constraints if present', () => {
|
||||
const research = readFixture('sample-research.md');
|
||||
const result = extractResearch(research);
|
||||
|
||||
expect(result.userConstraints).toBeDefined();
|
||||
expect(result.userConstraints.locked.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('extracts primary recommendation', () => {
|
||||
const research = readFixture('sample-research.md');
|
||||
const result = extractResearch(research);
|
||||
|
||||
expect(result.primaryRecommendation).toBeDefined();
|
||||
expect(result.primaryRecommendation).toContain('SSE');
|
||||
});
|
||||
|
||||
it('handles research without optional sections', () => {
|
||||
const minimalResearch = `---
|
||||
domain: testing
|
||||
confidence: LOW
|
||||
researched: 2025-01-20
|
||||
---
|
||||
|
||||
# Research
|
||||
|
||||
## Summary
|
||||
|
||||
Basic research document.
|
||||
|
||||
**Primary recommendation:** Use standard tools.
|
||||
`;
|
||||
|
||||
const result = extractResearch(minimalResearch);
|
||||
|
||||
expect(result.domain).toBe('testing');
|
||||
expect(result.pitfalls).toEqual([]);
|
||||
expect(result.standardStack).toEqual([]);
|
||||
expect(result.dontHandRoll).toEqual([]);
|
||||
});
|
||||
});
|
||||
112
gsd-memory/tests/extractors/summary.test.ts
Normal file
112
gsd-memory/tests/extractors/summary.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { extractSummary, type SummaryData } from '../../src/extractors/summary.js';
|
||||
import { readFixture } from '../setup.js';
|
||||
|
||||
describe('summary extractor', () => {
|
||||
it('extracts decisions from frontmatter', () => {
|
||||
const summary = readFixture('sample-summary.md');
|
||||
const result = extractSummary(summary);
|
||||
|
||||
expect(result.decisions.length).toBeGreaterThan(0);
|
||||
expect(result.decisions[0]).toHaveProperty('decision');
|
||||
expect(result.decisions[0].decision).toContain('jose');
|
||||
});
|
||||
|
||||
it('extracts tech stack additions', () => {
|
||||
const summary = readFixture('sample-summary.md');
|
||||
const result = extractSummary(summary);
|
||||
|
||||
expect(result.techStack.length).toBeGreaterThan(0);
|
||||
expect(result.techStack).toContainEqual(
|
||||
expect.objectContaining({
|
||||
library: 'jose',
|
||||
version: '5.2.0'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('extracts patterns established', () => {
|
||||
const summary = readFixture('sample-summary.md');
|
||||
const result = extractSummary(summary);
|
||||
|
||||
expect(result.patterns.length).toBeGreaterThan(0);
|
||||
expect(result.patterns[0]).toContain('Token rotation');
|
||||
});
|
||||
|
||||
it('extracts requires/provides for dependency graph', () => {
|
||||
const summary = readFixture('sample-summary.md');
|
||||
const result = extractSummary(summary);
|
||||
|
||||
expect(result.requires).toBeDefined();
|
||||
expect(result.provides).toBeDefined();
|
||||
expect(result.provides.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('extracts phase and plan identifiers', () => {
|
||||
const summary = readFixture('sample-summary.md');
|
||||
const result = extractSummary(summary);
|
||||
|
||||
expect(result.phase).toBe('01-foundation');
|
||||
expect(result.plan).toBe(1);
|
||||
});
|
||||
|
||||
it('extracts subsystem and tags', () => {
|
||||
const summary = readFixture('sample-summary.md');
|
||||
const result = extractSummary(summary);
|
||||
|
||||
expect(result.subsystem).toBe('auth');
|
||||
expect(result.tags).toContain('jwt');
|
||||
expect(result.tags).toContain('prisma');
|
||||
});
|
||||
|
||||
it('extracts affects list', () => {
|
||||
const summary = readFixture('sample-summary.md');
|
||||
const result = extractSummary(summary);
|
||||
|
||||
expect(result.affects).toBeDefined();
|
||||
expect(result.affects).toContain('02-features');
|
||||
});
|
||||
|
||||
it('extracts key files', () => {
|
||||
const summary = readFixture('sample-summary.md');
|
||||
const result = extractSummary(summary);
|
||||
|
||||
expect(result.keyFiles).toBeDefined();
|
||||
expect(result.keyFiles.created).toContain('src/lib/auth.ts');
|
||||
});
|
||||
|
||||
it('extracts duration and completion date', () => {
|
||||
const summary = readFixture('sample-summary.md');
|
||||
const result = extractSummary(summary);
|
||||
|
||||
expect(result.duration).toBe('28min');
|
||||
expect(result.completed).toBe('2025-01-15');
|
||||
});
|
||||
|
||||
it('handles summary without optional fields', () => {
|
||||
const minimalSummary = `---
|
||||
phase: 01-test
|
||||
plan: 01
|
||||
subsystem: testing
|
||||
tags: [test]
|
||||
---
|
||||
# Phase 1 Summary
|
||||
|
||||
**Test phase**`;
|
||||
|
||||
const result = extractSummary(minimalSummary);
|
||||
|
||||
expect(result.phase).toBe('01-test');
|
||||
expect(result.decisions).toEqual([]);
|
||||
expect(result.techStack).toEqual([]);
|
||||
expect(result.patterns).toEqual([]);
|
||||
});
|
||||
|
||||
it('extracts one-liner from content', () => {
|
||||
const summary = readFixture('sample-summary.md');
|
||||
const result = extractSummary(summary);
|
||||
|
||||
expect(result.oneLiner).toBeDefined();
|
||||
expect(result.oneLiner).toContain('JWT auth');
|
||||
});
|
||||
});
|
||||
43
gsd-memory/tests/fixtures/mock-planning/PROJECT.md
vendored
Normal file
43
gsd-memory/tests/fixtures/mock-planning/PROJECT.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# MockProject
|
||||
|
||||
## What This Is
|
||||
|
||||
A test project for validating GSD memory extraction. Contains all expected document types and frontmatter structures.
|
||||
|
||||
## Core Value
|
||||
|
||||
Test coverage for GSD memory extractors.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Validated
|
||||
|
||||
- ✓ Extractor parses SUMMARY.md frontmatter — Phase 1
|
||||
|
||||
### Active
|
||||
|
||||
- [ ] Cross-project search works
|
||||
- [ ] Decision extraction includes rationale
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Production deployment — test fixture only
|
||||
|
||||
## Context
|
||||
|
||||
Mock project used in GSD memory test suite.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Tech stack**: TypeScript, Vitest
|
||||
- **Scope**: Test fixtures only
|
||||
|
||||
## Key Decisions
|
||||
|
||||
| Decision | Rationale | Outcome |
|
||||
|----------|-----------|---------|
|
||||
| YAML frontmatter | Machine-readable metadata | ✓ Good |
|
||||
| Gray-matter parser | Battle-tested, handles edge cases | — Pending |
|
||||
|
||||
---
|
||||
*Last updated: 2025-01-20 after creation*
|
||||
60
gsd-memory/tests/fixtures/mock-planning/STATE.md
vendored
Normal file
60
gsd-memory/tests/fixtures/mock-planning/STATE.md
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
# Project State
|
||||
|
||||
## Project Reference
|
||||
|
||||
See: .planning/PROJECT.md (updated 2025-01-20)
|
||||
|
||||
**Core value:** Test coverage for GSD memory extractors
|
||||
**Current focus:** Phase 1 Foundation
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: 1 of 2 (Foundation)
|
||||
Plan: 1 of 1 in current phase
|
||||
Status: Phase complete
|
||||
Last activity: 2025-01-20 — Completed foundation phase
|
||||
|
||||
Progress: [█████░░░░░] 50%
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
**Velocity:**
|
||||
- Total plans completed: 1
|
||||
- Average duration: 15 min
|
||||
- Total execution time: 0.25 hours
|
||||
|
||||
**By Phase:**
|
||||
|
||||
| Phase | Plans | Total | Avg/Plan |
|
||||
|-------|-------|-------|----------|
|
||||
| 01-foundation | 1 | 15min | 15min |
|
||||
|
||||
**Recent Trend:**
|
||||
- Last 5 plans: [15min]
|
||||
- Trend: Stable
|
||||
|
||||
*Updated after each plan completion*
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
### Decisions
|
||||
|
||||
Decisions are logged in PROJECT.md Key Decisions table.
|
||||
Recent decisions affecting current work:
|
||||
|
||||
- [Phase 1]: Used YAML frontmatter for metadata
|
||||
- [Phase 1]: Selected gray-matter parser
|
||||
|
||||
### Pending Todos
|
||||
|
||||
None yet.
|
||||
|
||||
### Blockers/Concerns
|
||||
|
||||
None yet.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2025-01-20 14:30
|
||||
Stopped at: Phase 1 complete, ready for Phase 2
|
||||
Resume file: None
|
||||
9
gsd-memory/tests/fixtures/mock-planning/config.json
vendored
Normal file
9
gsd-memory/tests/fixtures/mock-planning/config.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"project": "MockProject",
|
||||
"version": "1.0.0",
|
||||
"created": "2025-01-20",
|
||||
"settings": {
|
||||
"parallelization": true,
|
||||
"atomicCommits": true
|
||||
}
|
||||
}
|
||||
95
gsd-memory/tests/fixtures/mock-planning/phases/01-foundation/01-01-SUMMARY.md
vendored
Normal file
95
gsd-memory/tests/fixtures/mock-planning/phases/01-foundation/01-01-SUMMARY.md
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
phase: 01-foundation
|
||||
plan: 01
|
||||
subsystem: testing
|
||||
tags: [vitest, typescript, testing]
|
||||
|
||||
requires:
|
||||
- phase: none
|
||||
provides: greenfield project
|
||||
|
||||
provides:
|
||||
- Test fixture structure
|
||||
- Extractor test coverage
|
||||
- Mock planning directory
|
||||
|
||||
affects: [02-integration]
|
||||
|
||||
tech-stack:
|
||||
added:
|
||||
- vitest: 1.0.0
|
||||
- gray-matter: 4.0.3
|
||||
patterns:
|
||||
- TDD for extractors
|
||||
- Fixture-based testing
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- tests/fixtures/sample-summary.md
|
||||
- tests/fixtures/sample-research.md
|
||||
- tests/fixtures/sample-project.md
|
||||
modified: []
|
||||
|
||||
key-decisions:
|
||||
- "Used Vitest for test runner - fast, ESM native"
|
||||
- "Created realistic fixtures from actual GSD templates"
|
||||
|
||||
patterns-established:
|
||||
- "Fixture files mirror real GSD output"
|
||||
- "Tests verify extraction accuracy"
|
||||
|
||||
duration: 15min
|
||||
completed: 2025-01-20
|
||||
---
|
||||
|
||||
# Phase 1: Foundation Summary
|
||||
|
||||
**Test fixtures and extractor test coverage for GSD memory server**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 15 min
|
||||
- **Started:** 2025-01-20T14:00:00Z
|
||||
- **Completed:** 2025-01-20T14:15:00Z
|
||||
- **Tasks:** 3
|
||||
- **Files modified:** 5
|
||||
|
||||
## Accomplishments
|
||||
- Created realistic test fixtures
|
||||
- Set up Vitest configuration
|
||||
- Established TDD pattern for extractors
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1: Create fixtures** - `fix001` (feat)
|
||||
2. **Task 2: Configure Vitest** - `fix002` (chore)
|
||||
3. **Task 3: Write extractor tests** - `fix003` (test)
|
||||
|
||||
## Files Created/Modified
|
||||
- `tests/fixtures/sample-summary.md` - SUMMARY.md fixture
|
||||
- `tests/fixtures/sample-research.md` - RESEARCH.md fixture
|
||||
- `tests/fixtures/sample-project.md` - PROJECT.md fixture
|
||||
- `vitest.config.ts` - Test configuration
|
||||
|
||||
## Decisions Made
|
||||
- Used Vitest over Jest for ESM support
|
||||
- Created comprehensive fixtures from real templates
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written.
|
||||
|
||||
## Issues Encountered
|
||||
None.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
- Fixtures complete, ready for extractor implementation
|
||||
- Test patterns established
|
||||
|
||||
---
|
||||
*Phase: 01-foundation*
|
||||
*Completed: 2025-01-20*
|
||||
100
gsd-memory/tests/fixtures/mock-planning/phases/01-foundation/01-RESEARCH.md
vendored
Normal file
100
gsd-memory/tests/fixtures/mock-planning/phases/01-foundation/01-RESEARCH.md
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
# Phase 1: Foundation - Research
|
||||
|
||||
**Researched:** 2025-01-18
|
||||
**Domain:** TypeScript testing and YAML parsing
|
||||
**Confidence:** HIGH
|
||||
|
||||
<research_summary>
|
||||
## Summary
|
||||
|
||||
Researched testing frameworks and YAML parsing for TypeScript MCP server development. Vitest is the clear standard for ESM TypeScript projects. Gray-matter is battle-tested for markdown frontmatter parsing.
|
||||
|
||||
**Primary recommendation:** Use Vitest + gray-matter. Follow TDD for all extractors.
|
||||
</research_summary>
|
||||
|
||||
<standard_stack>
|
||||
## Standard Stack
|
||||
|
||||
### Core
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| vitest | 1.0.0 | Test runner | Fast, ESM native, TS support |
|
||||
| gray-matter | 4.0.3 | Frontmatter parsing | Handles edge cases, widely used |
|
||||
|
||||
### Supporting
|
||||
| Library | Version | Purpose | When to Use |
|
||||
|---------|---------|---------|-------------|
|
||||
| yaml | 2.3.0 | YAML parsing | Complex YAML beyond frontmatter |
|
||||
|
||||
### Alternatives Considered
|
||||
| Instead of | Could Use | Tradeoff |
|
||||
|------------|-----------|----------|
|
||||
| Vitest | Jest | Jest slower for ESM, more config |
|
||||
| gray-matter | manual regex | Gray-matter handles edge cases |
|
||||
</standard_stack>
|
||||
|
||||
<dont_hand_roll>
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| YAML frontmatter | Regex parsing | gray-matter | Multiline, escaping, edge cases |
|
||||
| Test runner | Custom harness | Vitest | Watch mode, coverage, parallel |
|
||||
|
||||
**Key insight:** YAML parsing has many edge cases. Gray-matter handles them all.
|
||||
</dont_hand_roll>
|
||||
|
||||
<common_pitfalls>
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Malformed YAML
|
||||
**What goes wrong:** Parser throws on invalid YAML
|
||||
**Why it happens:** User-edited files may have syntax errors
|
||||
**How to avoid:** Wrap parsing in try-catch, return empty object on failure
|
||||
**Warning signs:** Tests fail with YAML parse errors
|
||||
|
||||
### Pitfall 2: Missing Frontmatter
|
||||
**What goes wrong:** Code assumes frontmatter exists
|
||||
**Why it happens:** Not all markdown files have frontmatter
|
||||
**How to avoid:** Always check for empty result
|
||||
**Warning signs:** Undefined errors on .data access
|
||||
</common_pitfalls>
|
||||
|
||||
<sources>
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- Vitest documentation - test configuration
|
||||
- gray-matter npm - API reference
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- None
|
||||
|
||||
### Tertiary (LOW confidence - needs validation)
|
||||
- None
|
||||
</sources>
|
||||
|
||||
<metadata>
|
||||
## Metadata
|
||||
|
||||
**Research scope:**
|
||||
- Core technology: TypeScript testing
|
||||
- Ecosystem: Vitest, gray-matter
|
||||
- Patterns: TDD, fixture-based testing
|
||||
- Pitfalls: YAML parsing edge cases
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH - widely used
|
||||
- Architecture: HIGH - standard patterns
|
||||
- Pitfalls: HIGH - documented extensively
|
||||
- Code examples: HIGH - from official docs
|
||||
|
||||
**Research date:** 2025-01-18
|
||||
**Valid until:** 2025-02-18 (30 days - stable tech)
|
||||
</metadata>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 01-foundation*
|
||||
*Research completed: 2025-01-18*
|
||||
*Ready for planning: yes*
|
||||
21
gsd-memory/tests/fixtures/mock-planning/research/SUMMARY.md
vendored
Normal file
21
gsd-memory/tests/fixtures/mock-planning/research/SUMMARY.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
domain: typescript-testing
|
||||
confidence: HIGH
|
||||
researched: 2025-01-18
|
||||
---
|
||||
|
||||
# Research Summary
|
||||
|
||||
Research completed on TypeScript testing ecosystem for GSD memory server.
|
||||
|
||||
## Key Findings
|
||||
|
||||
- Vitest is the standard for TypeScript projects
|
||||
- Gray-matter handles YAML frontmatter parsing
|
||||
- MCP SDK provides standard server implementation
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. Use Vitest for all tests
|
||||
2. Use gray-matter for frontmatter extraction
|
||||
3. Follow MCP SDK patterns for tool implementation
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user