musubi-sdd 5.0.0 → 5.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +106 -48
- package/README.md +110 -32
- package/bin/musubi-analyze.js +74 -67
- package/bin/musubi-browser.js +27 -26
- package/bin/musubi-change.js +48 -47
- package/bin/musubi-checkpoint.js +10 -7
- package/bin/musubi-convert.js +25 -25
- package/bin/musubi-costs.js +27 -10
- package/bin/musubi-gui.js +52 -46
- package/bin/musubi-init.js +1952 -10
- package/bin/musubi-orchestrate.js +327 -239
- package/bin/musubi-remember.js +69 -56
- package/bin/musubi-resolve.js +53 -45
- package/bin/musubi-trace.js +51 -22
- package/bin/musubi-validate.js +39 -30
- package/bin/musubi-workflow.js +33 -34
- package/bin/musubi.js +39 -2
- package/package.json +1 -1
- package/src/agents/agent-loop.js +94 -95
- package/src/agents/agentic/code-generator.js +119 -109
- package/src/agents/agentic/code-reviewer.js +105 -108
- package/src/agents/agentic/index.js +4 -4
- package/src/agents/browser/action-executor.js +13 -13
- package/src/agents/browser/ai-comparator.js +11 -10
- package/src/agents/browser/context-manager.js +6 -6
- package/src/agents/browser/index.js +5 -5
- package/src/agents/browser/nl-parser.js +31 -46
- package/src/agents/browser/screenshot.js +2 -2
- package/src/agents/browser/test-generator.js +6 -4
- package/src/agents/function-tool.js +71 -65
- package/src/agents/index.js +7 -7
- package/src/agents/schema-generator.js +98 -94
- package/src/analyzers/ast-extractor.js +164 -145
- package/src/analyzers/codegraph-auto-update.js +858 -0
- package/src/analyzers/complexity-analyzer.js +536 -0
- package/src/analyzers/context-optimizer.js +247 -125
- package/src/analyzers/impact-analyzer.js +1 -1
- package/src/analyzers/large-project-analyzer.js +766 -0
- package/src/analyzers/repository-map.js +83 -80
- package/src/analyzers/security-analyzer.js +19 -11
- package/src/analyzers/stuck-detector.js +19 -17
- package/src/converters/index.js +78 -57
- package/src/converters/ir/types.js +12 -12
- package/src/converters/parsers/musubi-parser.js +134 -126
- package/src/converters/parsers/openapi-parser.js +70 -53
- package/src/converters/parsers/speckit-parser.js +239 -175
- package/src/converters/writers/musubi-writer.js +123 -118
- package/src/converters/writers/speckit-writer.js +124 -113
- package/src/generators/rust-migration-generator.js +512 -0
- package/src/gui/public/index.html +1365 -1211
- package/src/gui/server.js +41 -40
- package/src/gui/services/file-watcher.js +23 -8
- package/src/gui/services/project-scanner.js +26 -20
- package/src/gui/services/replanning-service.js +27 -23
- package/src/gui/services/traceability-service.js +8 -8
- package/src/gui/services/workflow-service.js +14 -7
- package/src/index.js +151 -0
- package/src/integrations/cicd.js +90 -104
- package/src/integrations/codegraph-mcp.js +643 -0
- package/src/integrations/documentation.js +142 -103
- package/src/integrations/examples.js +95 -80
- package/src/integrations/github-client.js +17 -17
- package/src/integrations/index.js +5 -5
- package/src/integrations/mcp/index.js +21 -21
- package/src/integrations/mcp/mcp-context-provider.js +76 -78
- package/src/integrations/mcp/mcp-discovery.js +74 -72
- package/src/integrations/mcp/mcp-tool-registry.js +99 -94
- package/src/integrations/mcp-connector.js +70 -66
- package/src/integrations/platforms.js +50 -49
- package/src/integrations/tool-discovery.js +37 -31
- package/src/llm-providers/anthropic-provider.js +11 -11
- package/src/llm-providers/base-provider.js +16 -18
- package/src/llm-providers/copilot-provider.js +22 -19
- package/src/llm-providers/index.js +26 -25
- package/src/llm-providers/ollama-provider.js +11 -11
- package/src/llm-providers/openai-provider.js +12 -12
- package/src/managers/agent-memory.js +36 -24
- package/src/managers/checkpoint-manager.js +4 -8
- package/src/managers/delta-spec.js +19 -19
- package/src/managers/index.js +13 -4
- package/src/managers/memory-condenser.js +35 -45
- package/src/managers/repo-skill-manager.js +57 -31
- package/src/managers/skill-loader.js +25 -22
- package/src/managers/skill-tools.js +36 -72
- package/src/managers/workflow.js +30 -22
- package/src/monitoring/cost-tracker.js +53 -44
- package/src/monitoring/incident-manager.js +123 -103
- package/src/monitoring/index.js +144 -134
- package/src/monitoring/observability.js +82 -59
- package/src/monitoring/quality-dashboard.js +51 -39
- package/src/monitoring/release-manager.js +70 -50
- package/src/orchestration/agent-skill-binding.js +39 -47
- package/src/orchestration/error-handler.js +65 -107
- package/src/orchestration/guardrails/base-guardrail.js +26 -24
- package/src/orchestration/guardrails/guardrail-rules.js +50 -64
- package/src/orchestration/guardrails/index.js +5 -5
- package/src/orchestration/guardrails/input-guardrail.js +58 -45
- package/src/orchestration/guardrails/output-guardrail.js +104 -81
- package/src/orchestration/guardrails/safety-check.js +79 -79
- package/src/orchestration/index.js +38 -55
- package/src/orchestration/mcp-tool-adapters.js +96 -99
- package/src/orchestration/orchestration-engine.js +21 -21
- package/src/orchestration/pattern-registry.js +60 -45
- package/src/orchestration/patterns/auto.js +34 -47
- package/src/orchestration/patterns/group-chat.js +59 -65
- package/src/orchestration/patterns/handoff.js +67 -65
- package/src/orchestration/patterns/human-in-loop.js +51 -72
- package/src/orchestration/patterns/nested.js +25 -40
- package/src/orchestration/patterns/sequential.js +35 -34
- package/src/orchestration/patterns/swarm.js +63 -56
- package/src/orchestration/patterns/triage.js +150 -109
- package/src/orchestration/reasoning/index.js +9 -9
- package/src/orchestration/reasoning/planning-engine.js +143 -140
- package/src/orchestration/reasoning/reasoning-engine.js +206 -144
- package/src/orchestration/reasoning/self-correction.js +121 -128
- package/src/orchestration/replanning/adaptive-goal-modifier.js +107 -112
- package/src/orchestration/replanning/alternative-generator.js +37 -42
- package/src/orchestration/replanning/config.js +63 -59
- package/src/orchestration/replanning/goal-progress-tracker.js +98 -100
- package/src/orchestration/replanning/index.js +24 -20
- package/src/orchestration/replanning/plan-evaluator.js +49 -50
- package/src/orchestration/replanning/plan-monitor.js +32 -28
- package/src/orchestration/replanning/proactive-path-optimizer.js +175 -178
- package/src/orchestration/replanning/replan-history.js +33 -26
- package/src/orchestration/replanning/replanning-engine.js +106 -108
- package/src/orchestration/skill-executor.js +107 -109
- package/src/orchestration/skill-registry.js +85 -89
- package/src/orchestration/workflow-examples.js +228 -231
- package/src/orchestration/workflow-executor.js +65 -68
- package/src/orchestration/workflow-orchestrator.js +72 -73
- package/src/phase4-integration.js +47 -40
- package/src/phase5-integration.js +89 -30
- package/src/reporters/coverage-report.js +82 -30
- package/src/reporters/hierarchical-reporter.js +498 -0
- package/src/reporters/traceability-matrix-report.js +29 -20
- package/src/resolvers/issue-resolver.js +43 -31
- package/src/steering/advanced-validation.js +133 -124
- package/src/steering/auto-updater.js +60 -73
- package/src/steering/index.js +6 -6
- package/src/steering/quality-metrics.js +41 -35
- package/src/steering/steering-auto-update.js +83 -86
- package/src/steering/steering-validator.js +98 -106
- package/src/steering/template-constraints.js +53 -54
- package/src/templates/agents/claude-code/CLAUDE.md +32 -32
- package/src/templates/agents/claude-code/skills/agent-assistant/SKILL.md +13 -5
- package/src/templates/agents/claude-code/skills/ai-ml-engineer/mlops-guide.md +23 -23
- package/src/templates/agents/claude-code/skills/ai-ml-engineer/model-card-template.md +60 -41
- package/src/templates/agents/claude-code/skills/api-designer/api-patterns.md +27 -19
- package/src/templates/agents/claude-code/skills/api-designer/openapi-template.md +11 -7
- package/src/templates/agents/claude-code/skills/bug-hunter/SKILL.md +4 -3
- package/src/templates/agents/claude-code/skills/bug-hunter/root-cause-analysis.md +37 -15
- package/src/templates/agents/claude-code/skills/change-impact-analyzer/dependency-graph-patterns.md +36 -42
- package/src/templates/agents/claude-code/skills/change-impact-analyzer/impact-analysis-template.md +69 -60
- package/src/templates/agents/claude-code/skills/cloud-architect/aws-patterns.md +31 -38
- package/src/templates/agents/claude-code/skills/cloud-architect/azure-patterns.md +28 -23
- package/src/templates/agents/claude-code/skills/code-reviewer/SKILL.md +61 -0
- package/src/templates/agents/claude-code/skills/code-reviewer/best-practices.md +27 -0
- package/src/templates/agents/claude-code/skills/code-reviewer/review-checklist.md +29 -10
- package/src/templates/agents/claude-code/skills/code-reviewer/review-standards.md +29 -24
- package/src/templates/agents/claude-code/skills/constitution-enforcer/SKILL.md +8 -6
- package/src/templates/agents/claude-code/skills/constitution-enforcer/constitutional-articles.md +62 -26
- package/src/templates/agents/claude-code/skills/constitution-enforcer/phase-minus-one-gates.md +35 -16
- package/src/templates/agents/claude-code/skills/database-administrator/backup-recovery.md +27 -17
- package/src/templates/agents/claude-code/skills/database-administrator/tuning-guide.md +25 -20
- package/src/templates/agents/claude-code/skills/database-schema-designer/schema-patterns.md +39 -22
- package/src/templates/agents/claude-code/skills/devops-engineer/ci-cd-templates.md +25 -22
- package/src/templates/agents/claude-code/skills/issue-resolver/SKILL.md +24 -21
- package/src/templates/agents/claude-code/skills/orchestrator/SKILL.md +148 -63
- package/src/templates/agents/claude-code/skills/orchestrator/patterns.md +35 -16
- package/src/templates/agents/claude-code/skills/orchestrator/selection-matrix.md +69 -64
- package/src/templates/agents/claude-code/skills/performance-engineer/optimization-playbook.md +47 -47
- package/src/templates/agents/claude-code/skills/performance-optimizer/SKILL.md +69 -0
- package/src/templates/agents/claude-code/skills/performance-optimizer/benchmark-template.md +63 -45
- package/src/templates/agents/claude-code/skills/performance-optimizer/optimization-patterns.md +33 -35
- package/src/templates/agents/claude-code/skills/project-manager/SKILL.md +7 -6
- package/src/templates/agents/claude-code/skills/project-manager/agile-ceremonies.md +47 -28
- package/src/templates/agents/claude-code/skills/project-manager/project-templates.md +94 -78
- package/src/templates/agents/claude-code/skills/quality-assurance/SKILL.md +20 -17
- package/src/templates/agents/claude-code/skills/quality-assurance/qa-plan-template.md +63 -49
- package/src/templates/agents/claude-code/skills/release-coordinator/SKILL.md +5 -5
- package/src/templates/agents/claude-code/skills/release-coordinator/feature-flag-guide.md +30 -26
- package/src/templates/agents/claude-code/skills/release-coordinator/release-plan-template.md +67 -35
- package/src/templates/agents/claude-code/skills/requirements-analyst/ears-format.md +54 -42
- package/src/templates/agents/claude-code/skills/requirements-analyst/validation-rules.md +36 -33
- package/src/templates/agents/claude-code/skills/security-auditor/SKILL.md +77 -19
- package/src/templates/agents/claude-code/skills/security-auditor/audit-checklists.md +24 -24
- package/src/templates/agents/claude-code/skills/security-auditor/owasp-top-10.md +61 -20
- package/src/templates/agents/claude-code/skills/security-auditor/vulnerability-patterns.md +43 -11
- package/src/templates/agents/claude-code/skills/site-reliability-engineer/SKILL.md +1 -0
- package/src/templates/agents/claude-code/skills/site-reliability-engineer/incident-response-template.md +55 -25
- package/src/templates/agents/claude-code/skills/site-reliability-engineer/observability-patterns.md +78 -68
- package/src/templates/agents/claude-code/skills/site-reliability-engineer/slo-sli-guide.md +73 -53
- package/src/templates/agents/claude-code/skills/software-developer/solid-principles.md +83 -37
- package/src/templates/agents/claude-code/skills/software-developer/test-first-workflow.md +38 -31
- package/src/templates/agents/claude-code/skills/steering/SKILL.md +1 -0
- package/src/templates/agents/claude-code/skills/steering/auto-update-rules.md +31 -0
- package/src/templates/agents/claude-code/skills/system-architect/adr-template.md +25 -7
- package/src/templates/agents/claude-code/skills/system-architect/c4-model-guide.md +74 -61
- package/src/templates/agents/claude-code/skills/technical-writer/doc-templates/documentation-templates.md +70 -52
- package/src/templates/agents/claude-code/skills/test-engineer/SKILL.md +2 -0
- package/src/templates/agents/claude-code/skills/test-engineer/ears-test-mapping.md +75 -71
- package/src/templates/agents/claude-code/skills/test-engineer/test-types.md +85 -63
- package/src/templates/agents/claude-code/skills/traceability-auditor/coverage-matrix-template.md +39 -36
- package/src/templates/agents/claude-code/skills/traceability-auditor/gap-detection-rules.md +22 -17
- package/src/templates/agents/claude-code/skills/ui-ux-designer/SKILL.md +1 -0
- package/src/templates/agents/claude-code/skills/ui-ux-designer/accessibility-guidelines.md +49 -75
- package/src/templates/agents/claude-code/skills/ui-ux-designer/design-system-components.md +71 -59
- package/src/templates/agents/codex/AGENTS.md +74 -42
- package/src/templates/agents/cursor/AGENTS.md +74 -42
- package/src/templates/agents/gemini-cli/GEMINI.md +74 -42
- package/src/templates/agents/github-copilot/AGENTS.md +83 -51
- package/src/templates/agents/qwen-code/QWEN.md +74 -42
- package/src/templates/agents/windsurf/AGENTS.md +74 -42
- package/src/templates/architectures/README.md +41 -0
- package/src/templates/architectures/clean-architecture/README.md +113 -0
- package/src/templates/architectures/event-driven/README.md +162 -0
- package/src/templates/architectures/hexagonal/README.md +130 -0
- package/src/templates/index.js +6 -1
- package/src/templates/locale-manager.js +16 -16
- package/src/templates/shared/delta-spec-template.md +20 -13
- package/src/templates/shared/github-actions/musubi-issue-resolver.yml +5 -5
- package/src/templates/shared/github-actions/musubi-security-check.yml +3 -3
- package/src/templates/shared/github-actions/musubi-validate.yml +4 -4
- package/src/templates/shared/steering/structure.md +95 -0
- package/src/templates/skills/browser-agent.md +21 -16
- package/src/templates/skills/web-gui.md +8 -0
- package/src/templates/template-constraints.js +50 -53
- package/src/validators/advanced-validation.js +30 -36
- package/src/validators/constitutional-validator.js +77 -73
- package/src/validators/critic-system.js +49 -59
- package/src/validators/delta-format.js +59 -55
- package/src/validators/traceability-validator.js +7 -11
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Spec Kit Parser
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Parses Spec Kit project structure into Intermediate Representation (IR)
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* Spec Kit structure:
|
|
7
7
|
* .specify/
|
|
8
8
|
* ├── memory/
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
|
|
24
24
|
const fs = require('fs-extra');
|
|
25
25
|
const path = require('path');
|
|
26
|
-
const {
|
|
27
|
-
createEmptyProjectIR,
|
|
26
|
+
const {
|
|
27
|
+
createEmptyProjectIR,
|
|
28
28
|
createEmptyFeatureIR,
|
|
29
|
-
userScenarioToRequirement
|
|
29
|
+
userScenarioToRequirement,
|
|
30
30
|
} = require('../ir/types');
|
|
31
31
|
|
|
32
32
|
/**
|
|
@@ -36,37 +36,37 @@ const {
|
|
|
36
36
|
*/
|
|
37
37
|
async function parseSpeckitProject(projectPath) {
|
|
38
38
|
const specifyPath = path.join(projectPath, '.specify');
|
|
39
|
-
|
|
40
|
-
if (!await fs.pathExists(specifyPath)) {
|
|
39
|
+
|
|
40
|
+
if (!(await fs.pathExists(specifyPath))) {
|
|
41
41
|
throw new Error(`Not a Spec Kit project: .specify directory not found at ${projectPath}`);
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
const ir = createEmptyProjectIR();
|
|
45
45
|
ir.metadata.sourceFormat = 'speckit';
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
// Parse project name from directory
|
|
48
48
|
ir.metadata.name = path.basename(projectPath);
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
// Parse constitution
|
|
51
51
|
ir.constitution = await parseConstitution(specifyPath);
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
// Parse features
|
|
54
54
|
ir.features = await parseFeatures(specifyPath);
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
// Parse templates
|
|
57
57
|
ir.templates = await parseTemplates(specifyPath);
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
return ir;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Parse constitution from .specify/memory/constitution.md
|
|
64
|
-
* @param {string} specifyPath
|
|
64
|
+
* @param {string} specifyPath
|
|
65
65
|
* @returns {Promise<import('../ir/types').ConstitutionIR>}
|
|
66
66
|
*/
|
|
67
67
|
async function parseConstitution(specifyPath) {
|
|
68
68
|
const constitutionPath = path.join(specifyPath, 'memory', 'constitution.md');
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
const constitution = {
|
|
71
71
|
articles: [],
|
|
72
72
|
corePrinciples: [],
|
|
@@ -75,26 +75,26 @@ async function parseConstitution(specifyPath) {
|
|
|
75
75
|
rules: [],
|
|
76
76
|
},
|
|
77
77
|
};
|
|
78
|
-
|
|
79
|
-
if (!await fs.pathExists(constitutionPath)) {
|
|
78
|
+
|
|
79
|
+
if (!(await fs.pathExists(constitutionPath))) {
|
|
80
80
|
return constitution;
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
try {
|
|
84
84
|
const content = await fs.readFile(constitutionPath, 'utf-8');
|
|
85
85
|
constitution.rawContent = content;
|
|
86
|
-
|
|
86
|
+
|
|
87
87
|
// Parse Core Principles section (Spec Kit format)
|
|
88
88
|
const principlesSection = content.match(/##\s+Core\s+Principles?\s*\n([\s\S]+?)(?=\n##|$)/i);
|
|
89
89
|
if (principlesSection) {
|
|
90
90
|
const principlesContent = principlesSection[1];
|
|
91
91
|
const principleRegex = /###\s+(.+?)\n([\s\S]+?)(?=\n###|$)/g;
|
|
92
92
|
let match;
|
|
93
|
-
|
|
93
|
+
|
|
94
94
|
while ((match = principleRegex.exec(principlesContent)) !== null) {
|
|
95
95
|
const name = match[1].trim();
|
|
96
96
|
const description = match[2].trim();
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
constitution.corePrinciples.push({
|
|
99
99
|
name,
|
|
100
100
|
description,
|
|
@@ -102,7 +102,7 @@ async function parseConstitution(specifyPath) {
|
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
-
|
|
105
|
+
|
|
106
106
|
// Parse simple bullet list principles
|
|
107
107
|
if (constitution.corePrinciples.length === 0) {
|
|
108
108
|
const bulletPrinciples = content.match(/^[-*]\s+\*\*(.+?)\*\*[:\s]+(.+)$/gm);
|
|
@@ -119,30 +119,29 @@ async function parseConstitution(specifyPath) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
// Parse Governance section
|
|
124
124
|
const governanceSection = content.match(/##\s+Governance\s*\n([\s\S]+?)(?=\n##|$)/i);
|
|
125
125
|
if (governanceSection) {
|
|
126
126
|
const govContent = governanceSection[1];
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
const versionMatch = govContent.match(/version[:\s]+(\d+\.\d+)/i);
|
|
129
129
|
if (versionMatch) {
|
|
130
130
|
constitution.governance.version = versionMatch[1];
|
|
131
131
|
}
|
|
132
|
-
|
|
132
|
+
|
|
133
133
|
const ruleLines = govContent.match(/^[-*]\s+(.+)/gm);
|
|
134
134
|
if (ruleLines) {
|
|
135
135
|
constitution.governance.rules = ruleLines.map(l => l.replace(/^[-*]\s+/, ''));
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
|
|
138
|
+
|
|
139
139
|
// Convert principles to articles
|
|
140
140
|
constitution.articles = mapPrinciplesToArticles(constitution.corePrinciples);
|
|
141
|
-
|
|
142
141
|
} catch (error) {
|
|
143
142
|
console.warn(`Warning: Failed to parse constitution: ${error.message}`);
|
|
144
143
|
}
|
|
145
|
-
|
|
144
|
+
|
|
146
145
|
return constitution;
|
|
147
146
|
}
|
|
148
147
|
|
|
@@ -150,29 +149,65 @@ async function parseConstitution(specifyPath) {
|
|
|
150
149
|
* MUSUBI 9 Articles mapping keywords
|
|
151
150
|
*/
|
|
152
151
|
const MUSUBI_ARTICLES = [
|
|
153
|
-
{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
152
|
+
{
|
|
153
|
+
number: 1,
|
|
154
|
+
name: 'Specification Primacy',
|
|
155
|
+
keywords: ['spec', 'requirement', 'documentation', 'define', 'specify'],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
number: 2,
|
|
159
|
+
name: 'Test-First Development',
|
|
160
|
+
keywords: ['test', 'quality', 'validation', 'verify', 'tdd'],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
number: 3,
|
|
164
|
+
name: 'Architectural Compliance',
|
|
165
|
+
keywords: ['architecture', 'structure', 'design', 'pattern', 'component'],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
number: 4,
|
|
169
|
+
name: 'Traceability Requirements',
|
|
170
|
+
keywords: ['trace', 'track', 'link', 'reference', 'map'],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
number: 5,
|
|
174
|
+
name: 'Change Control Protocol',
|
|
175
|
+
keywords: ['change', 'version', 'control', 'update', 'modify'],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
number: 6,
|
|
179
|
+
name: 'Separation of Concerns',
|
|
180
|
+
keywords: ['separation', 'modular', 'concern', 'decouple', 'isolate'],
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
number: 7,
|
|
184
|
+
name: 'Documentation Standards',
|
|
185
|
+
keywords: ['document', 'standard', 'format', 'readme', 'comment'],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
number: 8,
|
|
189
|
+
name: 'Continuous Validation',
|
|
190
|
+
keywords: ['continuous', 'validate', 'check', 'ci', 'automate'],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
number: 9,
|
|
194
|
+
name: 'Graceful Degradation',
|
|
195
|
+
keywords: ['graceful', 'fallback', 'degrade', 'error', 'recover'],
|
|
196
|
+
},
|
|
162
197
|
];
|
|
163
198
|
|
|
164
199
|
/**
|
|
165
200
|
* Map a principle name/description to a MUSUBI article number
|
|
166
|
-
* @param {string} name
|
|
167
|
-
* @param {string} description
|
|
201
|
+
* @param {string} name
|
|
202
|
+
* @param {string} description
|
|
168
203
|
* @returns {number|undefined}
|
|
169
204
|
*/
|
|
170
205
|
function mapPrincipleToArticle(name, description) {
|
|
171
206
|
const combined = `${name} ${description}`.toLowerCase();
|
|
172
|
-
|
|
207
|
+
|
|
173
208
|
let bestMatch = null;
|
|
174
209
|
let bestScore = 0;
|
|
175
|
-
|
|
210
|
+
|
|
176
211
|
for (const article of MUSUBI_ARTICLES) {
|
|
177
212
|
let score = 0;
|
|
178
213
|
for (const keyword of article.keywords) {
|
|
@@ -180,25 +215,25 @@ function mapPrincipleToArticle(name, description) {
|
|
|
180
215
|
score++;
|
|
181
216
|
}
|
|
182
217
|
}
|
|
183
|
-
|
|
218
|
+
|
|
184
219
|
if (score > bestScore) {
|
|
185
220
|
bestScore = score;
|
|
186
221
|
bestMatch = article.number;
|
|
187
222
|
}
|
|
188
223
|
}
|
|
189
|
-
|
|
224
|
+
|
|
190
225
|
return bestScore > 0 ? bestMatch : undefined;
|
|
191
226
|
}
|
|
192
227
|
|
|
193
228
|
/**
|
|
194
229
|
* Map principles to MUSUBI articles
|
|
195
|
-
* @param {import('../ir/types').PrincipleIR[]} principles
|
|
230
|
+
* @param {import('../ir/types').PrincipleIR[]} principles
|
|
196
231
|
* @returns {import('../ir/types').ArticleIR[]}
|
|
197
232
|
*/
|
|
198
233
|
function mapPrinciplesToArticles(principles) {
|
|
199
234
|
const articles = [];
|
|
200
235
|
const usedArticles = new Set();
|
|
201
|
-
|
|
236
|
+
|
|
202
237
|
// First pass: map principles to articles
|
|
203
238
|
for (const principle of principles) {
|
|
204
239
|
if (principle.mappedToArticle && !usedArticles.has(principle.mappedToArticle)) {
|
|
@@ -215,7 +250,7 @@ function mapPrinciplesToArticles(principles) {
|
|
|
215
250
|
}
|
|
216
251
|
}
|
|
217
252
|
}
|
|
218
|
-
|
|
253
|
+
|
|
219
254
|
// Fill missing articles with defaults
|
|
220
255
|
for (const article of MUSUBI_ARTICLES) {
|
|
221
256
|
if (!usedArticles.has(article.number)) {
|
|
@@ -227,31 +262,31 @@ function mapPrinciplesToArticles(principles) {
|
|
|
227
262
|
});
|
|
228
263
|
}
|
|
229
264
|
}
|
|
230
|
-
|
|
265
|
+
|
|
231
266
|
return articles.sort((a, b) => a.number - b.number);
|
|
232
267
|
}
|
|
233
268
|
|
|
234
269
|
/**
|
|
235
270
|
* Extract rules from description
|
|
236
|
-
* @param {string} description
|
|
271
|
+
* @param {string} description
|
|
237
272
|
* @returns {string[]}
|
|
238
273
|
*/
|
|
239
274
|
function extractRulesFromDescription(description) {
|
|
240
275
|
const rules = [];
|
|
241
276
|
const sentences = description.split(/[.!?]+/).filter(s => s.trim().length > 0);
|
|
242
|
-
|
|
277
|
+
|
|
243
278
|
for (const sentence of sentences) {
|
|
244
279
|
if (sentence.includes('must') || sentence.includes('shall') || sentence.includes('should')) {
|
|
245
280
|
rules.push(sentence.trim());
|
|
246
281
|
}
|
|
247
282
|
}
|
|
248
|
-
|
|
283
|
+
|
|
249
284
|
return rules.length > 0 ? rules : [description];
|
|
250
285
|
}
|
|
251
286
|
|
|
252
287
|
/**
|
|
253
288
|
* Get default description for article
|
|
254
|
-
* @param {number} articleNumber
|
|
289
|
+
* @param {number} articleNumber
|
|
255
290
|
* @returns {string}
|
|
256
291
|
*/
|
|
257
292
|
function getDefaultDescription(articleNumber) {
|
|
@@ -271,12 +306,15 @@ function getDefaultDescription(articleNumber) {
|
|
|
271
306
|
|
|
272
307
|
/**
|
|
273
308
|
* Get default rules for article
|
|
274
|
-
* @param {number} articleNumber
|
|
309
|
+
* @param {number} articleNumber
|
|
275
310
|
* @returns {string[]}
|
|
276
311
|
*/
|
|
277
312
|
function getDefaultRules(articleNumber) {
|
|
278
313
|
const defaults = {
|
|
279
|
-
1: [
|
|
314
|
+
1: [
|
|
315
|
+
'All features must be specified before implementation',
|
|
316
|
+
'Specifications are the single source of truth',
|
|
317
|
+
],
|
|
280
318
|
2: ['Tests must be written before production code', 'All code must have corresponding tests'],
|
|
281
319
|
3: ['Follow established architectural patterns', 'Document architectural decisions'],
|
|
282
320
|
4: ['Requirements trace to tests', 'Tests trace to implementation'],
|
|
@@ -291,20 +329,20 @@ function getDefaultRules(articleNumber) {
|
|
|
291
329
|
|
|
292
330
|
/**
|
|
293
331
|
* Parse features from .specify/specs/
|
|
294
|
-
* @param {string} specifyPath
|
|
332
|
+
* @param {string} specifyPath
|
|
295
333
|
* @returns {Promise<import('../ir/types').FeatureIR[]>}
|
|
296
334
|
*/
|
|
297
335
|
async function parseFeatures(specifyPath) {
|
|
298
336
|
const specsPath = path.join(specifyPath, 'specs');
|
|
299
337
|
const features = [];
|
|
300
|
-
|
|
301
|
-
if (!await fs.pathExists(specsPath)) {
|
|
338
|
+
|
|
339
|
+
if (!(await fs.pathExists(specsPath))) {
|
|
302
340
|
return features;
|
|
303
341
|
}
|
|
304
|
-
|
|
342
|
+
|
|
305
343
|
try {
|
|
306
344
|
const entries = await fs.readdir(specsPath, { withFileTypes: true });
|
|
307
|
-
|
|
345
|
+
|
|
308
346
|
for (const entry of entries) {
|
|
309
347
|
if (entry.isDirectory()) {
|
|
310
348
|
const featurePath = path.join(specsPath, entry.name);
|
|
@@ -317,67 +355,67 @@ async function parseFeatures(specifyPath) {
|
|
|
317
355
|
} catch (error) {
|
|
318
356
|
console.warn(`Warning: Failed to parse features: ${error.message}`);
|
|
319
357
|
}
|
|
320
|
-
|
|
358
|
+
|
|
321
359
|
return features;
|
|
322
360
|
}
|
|
323
361
|
|
|
324
362
|
/**
|
|
325
363
|
* Parse a single feature
|
|
326
|
-
* @param {string} featurePath
|
|
327
|
-
* @param {string} featureId
|
|
364
|
+
* @param {string} featurePath
|
|
365
|
+
* @param {string} featureId
|
|
328
366
|
* @returns {Promise<import('../ir/types').FeatureIR|null>}
|
|
329
367
|
*/
|
|
330
368
|
async function parseFeature(featurePath, featureId) {
|
|
331
369
|
const feature = createEmptyFeatureIR(featureId, extractFeatureName(featureId));
|
|
332
|
-
|
|
370
|
+
|
|
333
371
|
// Parse spec.md
|
|
334
372
|
const specPath = path.join(featurePath, 'spec.md');
|
|
335
373
|
if (await fs.pathExists(specPath)) {
|
|
336
374
|
feature.specification = await parseSpecification(specPath);
|
|
337
375
|
}
|
|
338
|
-
|
|
376
|
+
|
|
339
377
|
// Parse plan.md
|
|
340
378
|
const planPath = path.join(featurePath, 'plan.md');
|
|
341
379
|
if (await fs.pathExists(planPath)) {
|
|
342
380
|
feature.plan = await parsePlan(planPath);
|
|
343
381
|
}
|
|
344
|
-
|
|
382
|
+
|
|
345
383
|
// Parse tasks.md
|
|
346
384
|
const tasksPath = path.join(featurePath, 'tasks.md');
|
|
347
385
|
if (await fs.pathExists(tasksPath)) {
|
|
348
386
|
feature.tasks = await parseTasks(tasksPath);
|
|
349
387
|
}
|
|
350
|
-
|
|
388
|
+
|
|
351
389
|
// Parse research.md
|
|
352
390
|
const researchPath = path.join(featurePath, 'research.md');
|
|
353
391
|
if (await fs.pathExists(researchPath)) {
|
|
354
392
|
feature.research = await parseResearch(researchPath);
|
|
355
393
|
}
|
|
356
|
-
|
|
394
|
+
|
|
357
395
|
// Parse data-model.md
|
|
358
396
|
const dataModelPath = path.join(featurePath, 'data-model.md');
|
|
359
397
|
if (await fs.pathExists(dataModelPath)) {
|
|
360
398
|
feature.dataModel = await parseDataModel(dataModelPath);
|
|
361
399
|
}
|
|
362
|
-
|
|
400
|
+
|
|
363
401
|
// Parse contracts directory
|
|
364
402
|
const contractsPath = path.join(featurePath, 'contracts');
|
|
365
403
|
if (await fs.pathExists(contractsPath)) {
|
|
366
404
|
feature.contracts = await parseContracts(contractsPath);
|
|
367
405
|
}
|
|
368
|
-
|
|
406
|
+
|
|
369
407
|
// Parse quickstart.md
|
|
370
408
|
const quickstartPath = path.join(featurePath, 'quickstart.md');
|
|
371
409
|
if (await fs.pathExists(quickstartPath)) {
|
|
372
410
|
feature.quickstart = await parseQuickstart(quickstartPath);
|
|
373
411
|
}
|
|
374
|
-
|
|
412
|
+
|
|
375
413
|
return feature;
|
|
376
414
|
}
|
|
377
415
|
|
|
378
416
|
/**
|
|
379
417
|
* Extract feature name from ID (e.g., "001-photo-albums" -> "Photo Albums")
|
|
380
|
-
* @param {string} featureId
|
|
418
|
+
* @param {string} featureId
|
|
381
419
|
* @returns {string}
|
|
382
420
|
*/
|
|
383
421
|
function extractFeatureName(featureId) {
|
|
@@ -392,12 +430,12 @@ function extractFeatureName(featureId) {
|
|
|
392
430
|
|
|
393
431
|
/**
|
|
394
432
|
* Parse specification file (Spec Kit format with User Scenarios)
|
|
395
|
-
* @param {string} specPath
|
|
433
|
+
* @param {string} specPath
|
|
396
434
|
* @returns {Promise<import('../ir/types').SpecificationIR>}
|
|
397
435
|
*/
|
|
398
436
|
async function parseSpecification(specPath) {
|
|
399
437
|
const content = await fs.readFile(specPath, 'utf-8');
|
|
400
|
-
|
|
438
|
+
|
|
401
439
|
const specification = {
|
|
402
440
|
title: '',
|
|
403
441
|
description: '',
|
|
@@ -406,24 +444,24 @@ async function parseSpecification(specPath) {
|
|
|
406
444
|
successCriteria: [],
|
|
407
445
|
rawContent: content,
|
|
408
446
|
};
|
|
409
|
-
|
|
447
|
+
|
|
410
448
|
// Extract title from first heading
|
|
411
449
|
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
412
450
|
if (titleMatch) {
|
|
413
451
|
specification.title = titleMatch[1].trim();
|
|
414
452
|
}
|
|
415
|
-
|
|
453
|
+
|
|
416
454
|
// Extract description (content before first ## heading)
|
|
417
455
|
const descMatch = content.match(/^#\s+.+\n([\s\S]+?)(?=\n##|$)/);
|
|
418
456
|
if (descMatch) {
|
|
419
457
|
specification.description = descMatch[1].trim();
|
|
420
458
|
}
|
|
421
|
-
|
|
459
|
+
|
|
422
460
|
// Parse User Scenarios (Spec Kit format)
|
|
423
461
|
const scenariosSection = content.match(/##\s+User\s+Scenarios?\s*\n([\s\S]+?)(?=\n##|$)/i);
|
|
424
462
|
if (scenariosSection) {
|
|
425
463
|
specification.userScenarios = parseUserScenarios(scenariosSection[1]);
|
|
426
|
-
|
|
464
|
+
|
|
427
465
|
// Convert user scenarios to EARS requirements
|
|
428
466
|
let reqIndex = 1;
|
|
429
467
|
for (const scenario of specification.userScenarios) {
|
|
@@ -431,14 +469,14 @@ async function parseSpecification(specPath) {
|
|
|
431
469
|
specification.requirements.push(userScenarioToRequirement(scenario, reqId));
|
|
432
470
|
}
|
|
433
471
|
}
|
|
434
|
-
|
|
472
|
+
|
|
435
473
|
// Parse Requirements section if present (direct requirements)
|
|
436
474
|
const requirementsSection = content.match(/##\s+Requirements?\s*\n([\s\S]+?)(?=\n##|$)/i);
|
|
437
475
|
if (requirementsSection && !scenariosSection) {
|
|
438
476
|
// Parse requirements directly
|
|
439
477
|
const reqContent = requirementsSection[1];
|
|
440
478
|
const reqLines = reqContent.match(/^[-*]\s+(.+)$/gm);
|
|
441
|
-
|
|
479
|
+
|
|
442
480
|
if (reqLines) {
|
|
443
481
|
let reqIndex = specification.requirements.length + 1;
|
|
444
482
|
for (const line of reqLines) {
|
|
@@ -455,7 +493,7 @@ async function parseSpecification(specPath) {
|
|
|
455
493
|
}
|
|
456
494
|
}
|
|
457
495
|
}
|
|
458
|
-
|
|
496
|
+
|
|
459
497
|
// Parse Success Criteria
|
|
460
498
|
const successSection = content.match(/##\s+Success\s+Criteria\s*\n([\s\S]+?)(?=\n##|$)/i);
|
|
461
499
|
if (successSection) {
|
|
@@ -465,30 +503,32 @@ async function parseSpecification(specPath) {
|
|
|
465
503
|
specification.successCriteria = criteria.map(c => c.replace(/^[-*]\s+/, ''));
|
|
466
504
|
}
|
|
467
505
|
}
|
|
468
|
-
|
|
506
|
+
|
|
469
507
|
return specification;
|
|
470
508
|
}
|
|
471
509
|
|
|
472
510
|
/**
|
|
473
511
|
* Parse user scenarios from content
|
|
474
|
-
* @param {string} content
|
|
512
|
+
* @param {string} content
|
|
475
513
|
* @returns {import('../ir/types').UserScenarioIR[]}
|
|
476
514
|
*/
|
|
477
515
|
function parseUserScenarios(content) {
|
|
478
516
|
const scenarios = [];
|
|
479
|
-
|
|
517
|
+
|
|
480
518
|
// Match user story format: "As a [actor], I want to [action] so that [benefit]"
|
|
481
519
|
const storyRegex = /###\s+(.+?)\n([\s\S]+?)(?=\n###|$)/g;
|
|
482
520
|
let match;
|
|
483
521
|
let storyIndex = 1;
|
|
484
|
-
|
|
522
|
+
|
|
485
523
|
while ((match = storyRegex.exec(content)) !== null) {
|
|
486
524
|
const title = match[1].trim();
|
|
487
525
|
const body = match[2].trim();
|
|
488
|
-
|
|
526
|
+
|
|
489
527
|
// Parse "As a X, I want Y so that Z" pattern
|
|
490
|
-
const asMatch = body.match(
|
|
491
|
-
|
|
528
|
+
const asMatch = body.match(
|
|
529
|
+
/As\s+(?:a|an)\s+(.+?),\s+I\s+want\s+(?:to\s+)?(.+?)\s+so\s+that\s+(.+?)(?:\.|$)/i
|
|
530
|
+
);
|
|
531
|
+
|
|
492
532
|
if (asMatch) {
|
|
493
533
|
const scenario = {
|
|
494
534
|
id: `US${storyIndex++}`,
|
|
@@ -513,13 +553,15 @@ function parseUserScenarios(content) {
|
|
|
513
553
|
});
|
|
514
554
|
}
|
|
515
555
|
}
|
|
516
|
-
|
|
556
|
+
|
|
517
557
|
// Also check for simple bullet list format
|
|
518
558
|
if (scenarios.length === 0) {
|
|
519
559
|
const bulletStories = content.match(/^[-*]\s+As\s+(?:a|an)\s+.+$/gm);
|
|
520
560
|
if (bulletStories) {
|
|
521
561
|
for (const line of bulletStories) {
|
|
522
|
-
const asMatch = line.match(
|
|
562
|
+
const asMatch = line.match(
|
|
563
|
+
/As\s+(?:a|an)\s+(.+?),\s+I\s+want\s+(?:to\s+)?(.+?)\s+so\s+that\s+(.+?)(?:\.|$)/i
|
|
564
|
+
);
|
|
523
565
|
if (asMatch) {
|
|
524
566
|
scenarios.push({
|
|
525
567
|
id: `US${storyIndex++}`,
|
|
@@ -534,24 +576,24 @@ function parseUserScenarios(content) {
|
|
|
534
576
|
}
|
|
535
577
|
}
|
|
536
578
|
}
|
|
537
|
-
|
|
579
|
+
|
|
538
580
|
return scenarios;
|
|
539
581
|
}
|
|
540
582
|
|
|
541
583
|
/**
|
|
542
584
|
* Parse acceptance criteria from content
|
|
543
|
-
* @param {string} content
|
|
585
|
+
* @param {string} content
|
|
544
586
|
* @returns {import('../ir/types').AcceptanceCriterionIR[]}
|
|
545
587
|
*/
|
|
546
588
|
function parseAcceptanceCriteria(content) {
|
|
547
589
|
const criteria = [];
|
|
548
|
-
|
|
590
|
+
|
|
549
591
|
// Look for Acceptance Criteria section
|
|
550
592
|
const acSection = content.match(/(?:Acceptance\s+Criteria|AC)[:\s]*([\s\S]+?)(?=\n\n|$)/i);
|
|
551
593
|
if (acSection) {
|
|
552
594
|
const acContent = acSection[1];
|
|
553
595
|
const acLines = acContent.match(/^[-*]\s+(.+)$/gm);
|
|
554
|
-
|
|
596
|
+
|
|
555
597
|
if (acLines) {
|
|
556
598
|
let acIndex = 1;
|
|
557
599
|
for (const line of acLines) {
|
|
@@ -563,13 +605,13 @@ function parseAcceptanceCriteria(content) {
|
|
|
563
605
|
}
|
|
564
606
|
}
|
|
565
607
|
}
|
|
566
|
-
|
|
608
|
+
|
|
567
609
|
return criteria;
|
|
568
610
|
}
|
|
569
611
|
|
|
570
612
|
/**
|
|
571
613
|
* Extract priority from content
|
|
572
|
-
* @param {string} content
|
|
614
|
+
* @param {string} content
|
|
573
615
|
* @returns {import('../ir/types').Priority}
|
|
574
616
|
*/
|
|
575
617
|
function extractPriorityFromContent(content) {
|
|
@@ -577,7 +619,7 @@ function extractPriorityFromContent(content) {
|
|
|
577
619
|
if (priorityMatch) {
|
|
578
620
|
return priorityMatch[1];
|
|
579
621
|
}
|
|
580
|
-
|
|
622
|
+
|
|
581
623
|
if (content.toLowerCase().includes('critical') || content.toLowerCase().includes('must have')) {
|
|
582
624
|
return 'P0';
|
|
583
625
|
}
|
|
@@ -590,18 +632,18 @@ function extractPriorityFromContent(content) {
|
|
|
590
632
|
if (content.toLowerCase().includes('low') || content.toLowerCase().includes('nice to have')) {
|
|
591
633
|
return 'P3';
|
|
592
634
|
}
|
|
593
|
-
|
|
635
|
+
|
|
594
636
|
return 'P1'; // Default
|
|
595
637
|
}
|
|
596
638
|
|
|
597
639
|
/**
|
|
598
640
|
* Parse plan file
|
|
599
|
-
* @param {string} planPath
|
|
641
|
+
* @param {string} planPath
|
|
600
642
|
* @returns {Promise<import('../ir/types').PlanIR>}
|
|
601
643
|
*/
|
|
602
644
|
async function parsePlan(planPath) {
|
|
603
645
|
const content = await fs.readFile(planPath, 'utf-8');
|
|
604
|
-
|
|
646
|
+
|
|
605
647
|
const plan = {
|
|
606
648
|
summary: '',
|
|
607
649
|
technicalContext: {
|
|
@@ -620,46 +662,48 @@ async function parsePlan(planPath) {
|
|
|
620
662
|
phases: [],
|
|
621
663
|
rawContent: content,
|
|
622
664
|
};
|
|
623
|
-
|
|
665
|
+
|
|
624
666
|
// Extract summary
|
|
625
667
|
const summaryMatch = content.match(/^#\s+.+\n([\s\S]+?)(?=\n##|$)/);
|
|
626
668
|
if (summaryMatch) {
|
|
627
669
|
plan.summary = summaryMatch[1].trim();
|
|
628
670
|
}
|
|
629
|
-
|
|
671
|
+
|
|
630
672
|
// Parse Technical Context
|
|
631
673
|
const techSection = content.match(/##\s+Technical\s+(?:Context|Stack)\s*\n([\s\S]+?)(?=\n##|$)/i);
|
|
632
674
|
if (techSection) {
|
|
633
675
|
const techContent = techSection[1];
|
|
634
|
-
|
|
676
|
+
|
|
635
677
|
const langMatch = techContent.match(/(?:language|lang)[:\s]+(.+)/i);
|
|
636
678
|
if (langMatch) plan.technicalContext.language = langMatch[1].trim();
|
|
637
|
-
|
|
679
|
+
|
|
638
680
|
const versionMatch = techContent.match(/version[:\s]+(.+)/i);
|
|
639
681
|
if (versionMatch) plan.technicalContext.version = versionMatch[1].trim();
|
|
640
|
-
|
|
682
|
+
|
|
641
683
|
const frameworkMatch = techContent.match(/framework[:\s]+(.+)/i);
|
|
642
684
|
if (frameworkMatch) plan.technicalContext.framework = frameworkMatch[1].trim();
|
|
643
|
-
|
|
685
|
+
|
|
644
686
|
const testingMatch = techContent.match(/testing[:\s]+(.+)/i);
|
|
645
687
|
if (testingMatch) plan.technicalContext.testing = testingMatch[1].trim();
|
|
646
|
-
|
|
688
|
+
|
|
647
689
|
const platformMatch = techContent.match(/platform[:\s]+(.+)/i);
|
|
648
690
|
if (platformMatch) plan.technicalContext.targetPlatform = platformMatch[1].trim();
|
|
649
691
|
}
|
|
650
|
-
|
|
692
|
+
|
|
651
693
|
// Parse Phases
|
|
652
|
-
const phasesSection = content.match(
|
|
694
|
+
const phasesSection = content.match(
|
|
695
|
+
/##\s+(?:Implementation\s+)?Phases?\s*\n([\s\S]+?)(?=\n##|$)/i
|
|
696
|
+
);
|
|
653
697
|
if (phasesSection) {
|
|
654
698
|
const phasesContent = phasesSection[1];
|
|
655
699
|
const phaseRegex = /###\s+Phase\s+(\d+)[:\s]*(.+?)(?=\n###|\n##|$)/gs;
|
|
656
700
|
let match;
|
|
657
|
-
|
|
701
|
+
|
|
658
702
|
while ((match = phaseRegex.exec(phasesContent)) !== null) {
|
|
659
703
|
const phaseNumber = parseInt(match[1], 10);
|
|
660
704
|
const phaseContent = match[2].trim();
|
|
661
705
|
const lines = phaseContent.split('\n');
|
|
662
|
-
|
|
706
|
+
|
|
663
707
|
plan.phases.push({
|
|
664
708
|
number: phaseNumber,
|
|
665
709
|
name: lines[0].trim(),
|
|
@@ -669,28 +713,28 @@ async function parsePlan(planPath) {
|
|
|
669
713
|
});
|
|
670
714
|
}
|
|
671
715
|
}
|
|
672
|
-
|
|
716
|
+
|
|
673
717
|
return plan;
|
|
674
718
|
}
|
|
675
719
|
|
|
676
720
|
/**
|
|
677
721
|
* Parse tasks file (Spec Kit format)
|
|
678
|
-
* @param {string} tasksPath
|
|
722
|
+
* @param {string} tasksPath
|
|
679
723
|
* @returns {Promise<import('../ir/types').TaskIR[]>}
|
|
680
724
|
*/
|
|
681
725
|
async function parseTasks(tasksPath) {
|
|
682
726
|
const content = await fs.readFile(tasksPath, 'utf-8');
|
|
683
727
|
const tasks = [];
|
|
684
|
-
|
|
728
|
+
|
|
685
729
|
// Spec Kit task format: - [ ] T001 [P] [US1] Description at path/
|
|
686
730
|
const taskRegex = /^[-*]\s+\[([xX ])\]\s+(T\d+)\s*(\[P\])?\s*(\[US\d+\])?\s*(.+)$/gm;
|
|
687
731
|
let match;
|
|
688
732
|
let currentPhase = 1;
|
|
689
|
-
|
|
733
|
+
|
|
690
734
|
// Track current phase from headings
|
|
691
|
-
const
|
|
692
|
-
let
|
|
693
|
-
|
|
735
|
+
const _lines = content.split('\n');
|
|
736
|
+
let _lineIndex = 0;
|
|
737
|
+
|
|
694
738
|
while ((match = taskRegex.exec(content)) !== null) {
|
|
695
739
|
// Find current phase by looking at preceding headings
|
|
696
740
|
const textBefore = content.slice(0, match.index);
|
|
@@ -702,20 +746,20 @@ async function parseTasks(tasksPath) {
|
|
|
702
746
|
currentPhase = parseInt(phaseNum[1], 10);
|
|
703
747
|
}
|
|
704
748
|
}
|
|
705
|
-
|
|
749
|
+
|
|
706
750
|
const completed = match[1].toLowerCase() === 'x';
|
|
707
751
|
const taskId = match[2];
|
|
708
752
|
const isParallel = !!match[3];
|
|
709
|
-
const userStory = match[4] ? match[4].replace(/[
|
|
753
|
+
const userStory = match[4] ? match[4].replace(/[[\]]/g, '') : undefined;
|
|
710
754
|
const description = match[5].trim();
|
|
711
|
-
|
|
755
|
+
|
|
712
756
|
// Extract file path if present
|
|
713
757
|
const filePathMatch = description.match(/(?:at|in)\s+([^\s]+\/?)$/i);
|
|
714
758
|
const filePath = filePathMatch ? filePathMatch[1] : undefined;
|
|
715
|
-
const cleanDescription = filePathMatch
|
|
716
|
-
? description.replace(filePathMatch[0], '').trim()
|
|
759
|
+
const cleanDescription = filePathMatch
|
|
760
|
+
? description.replace(filePathMatch[0], '').trim()
|
|
717
761
|
: description;
|
|
718
|
-
|
|
762
|
+
|
|
719
763
|
tasks.push({
|
|
720
764
|
id: taskId,
|
|
721
765
|
description: cleanDescription,
|
|
@@ -726,38 +770,38 @@ async function parseTasks(tasksPath) {
|
|
|
726
770
|
completed,
|
|
727
771
|
});
|
|
728
772
|
}
|
|
729
|
-
|
|
773
|
+
|
|
730
774
|
return tasks;
|
|
731
775
|
}
|
|
732
776
|
|
|
733
777
|
/**
|
|
734
778
|
* Parse research file
|
|
735
|
-
* @param {string} researchPath
|
|
779
|
+
* @param {string} researchPath
|
|
736
780
|
* @returns {Promise<import('../ir/types').ResearchIR>}
|
|
737
781
|
*/
|
|
738
782
|
async function parseResearch(researchPath) {
|
|
739
783
|
const content = await fs.readFile(researchPath, 'utf-8');
|
|
740
|
-
|
|
784
|
+
|
|
741
785
|
const research = {
|
|
742
786
|
decisions: [],
|
|
743
787
|
alternatives: [],
|
|
744
788
|
rawContent: content,
|
|
745
789
|
};
|
|
746
|
-
|
|
790
|
+
|
|
747
791
|
// Parse Decisions section
|
|
748
792
|
const decisionsSection = content.match(/##\s+Decisions?\s*\n([\s\S]+?)(?=\n##|$)/i);
|
|
749
793
|
if (decisionsSection) {
|
|
750
794
|
const decisionContent = decisionsSection[1];
|
|
751
795
|
const decisionRegex = /###\s+(.+?)\n([\s\S]+?)(?=\n###|$)/g;
|
|
752
796
|
let match;
|
|
753
|
-
|
|
797
|
+
|
|
754
798
|
while ((match = decisionRegex.exec(decisionContent)) !== null) {
|
|
755
799
|
const topic = match[1].trim();
|
|
756
800
|
const body = match[2].trim();
|
|
757
|
-
|
|
801
|
+
|
|
758
802
|
const decisionMatch = body.match(/(?:decision|chose|selected)[:\s]+(.+)/i);
|
|
759
803
|
const rationaleMatch = body.match(/(?:rationale|because|reason)[:\s]+(.+)/i);
|
|
760
|
-
|
|
804
|
+
|
|
761
805
|
research.decisions.push({
|
|
762
806
|
topic,
|
|
763
807
|
decision: decisionMatch ? decisionMatch[1].trim() : body.split('\n')[0],
|
|
@@ -765,96 +809,101 @@ async function parseResearch(researchPath) {
|
|
|
765
809
|
});
|
|
766
810
|
}
|
|
767
811
|
}
|
|
768
|
-
|
|
812
|
+
|
|
769
813
|
// Parse Alternatives section
|
|
770
|
-
const alternativesSection = content.match(
|
|
814
|
+
const alternativesSection = content.match(
|
|
815
|
+
/##\s+Alternatives?\s+Considered\s*\n([\s\S]+?)(?=\n##|$)/i
|
|
816
|
+
);
|
|
771
817
|
if (alternativesSection) {
|
|
772
818
|
const altContent = alternativesSection[1];
|
|
773
819
|
const altRegex = /###\s+(.+?)\n([\s\S]+?)(?=\n###|$)/g;
|
|
774
820
|
let match;
|
|
775
|
-
|
|
821
|
+
|
|
776
822
|
while ((match = altRegex.exec(altContent)) !== null) {
|
|
777
823
|
const name = match[1].trim();
|
|
778
824
|
const body = match[2].trim();
|
|
779
|
-
|
|
825
|
+
|
|
780
826
|
const prosMatch = body.match(/pros?[:\s]*([\s\S]+?)(?=cons?|rejected|$)/i);
|
|
781
827
|
const consMatch = body.match(/cons?[:\s]*([\s\S]+?)(?=pros?|rejected|$)/i);
|
|
782
828
|
const rejectedMatch = body.match(/(?:rejected|status)[:\s]*(yes|no|true|false|rejected)/i);
|
|
783
829
|
const reasonMatch = body.match(/reason[:\s]+(.+)/i);
|
|
784
|
-
|
|
785
|
-
const pros = prosMatch
|
|
830
|
+
|
|
831
|
+
const pros = prosMatch
|
|
786
832
|
? (prosMatch[1].match(/^[-*]\s+(.+)/gm) || []).map(p => p.replace(/^[-*]\s+/, ''))
|
|
787
833
|
: [];
|
|
788
|
-
const cons = consMatch
|
|
834
|
+
const cons = consMatch
|
|
789
835
|
? (consMatch[1].match(/^[-*]\s+(.+)/gm) || []).map(c => c.replace(/^[-*]\s+/, ''))
|
|
790
836
|
: [];
|
|
791
|
-
|
|
837
|
+
|
|
792
838
|
research.alternatives.push({
|
|
793
839
|
name,
|
|
794
840
|
pros,
|
|
795
841
|
cons,
|
|
796
|
-
rejected: rejectedMatch
|
|
842
|
+
rejected: rejectedMatch
|
|
843
|
+
? ['yes', 'true', 'rejected'].includes(rejectedMatch[1].toLowerCase())
|
|
844
|
+
: false,
|
|
797
845
|
reason: reasonMatch ? reasonMatch[1].trim() : undefined,
|
|
798
846
|
});
|
|
799
847
|
}
|
|
800
848
|
}
|
|
801
|
-
|
|
849
|
+
|
|
802
850
|
return research;
|
|
803
851
|
}
|
|
804
852
|
|
|
805
853
|
/**
|
|
806
854
|
* Parse data model file
|
|
807
|
-
* @param {string} dataModelPath
|
|
855
|
+
* @param {string} dataModelPath
|
|
808
856
|
* @returns {Promise<import('../ir/types').DataModelIR>}
|
|
809
857
|
*/
|
|
810
858
|
async function parseDataModel(dataModelPath) {
|
|
811
859
|
const content = await fs.readFile(dataModelPath, 'utf-8');
|
|
812
|
-
|
|
860
|
+
|
|
813
861
|
const dataModel = {
|
|
814
862
|
entities: [],
|
|
815
863
|
relationships: [],
|
|
816
864
|
rawContent: content,
|
|
817
865
|
};
|
|
818
|
-
|
|
866
|
+
|
|
819
867
|
// Parse entities
|
|
820
868
|
const entityRegex = /###\s+(?:Entity:?\s+)?(\w+)\s*\n([\s\S]+?)(?=\n###|$)/gi;
|
|
821
869
|
let match;
|
|
822
|
-
|
|
870
|
+
|
|
823
871
|
while ((match = entityRegex.exec(content)) !== null) {
|
|
824
872
|
const name = match[1];
|
|
825
873
|
const body = match[2].trim();
|
|
826
|
-
|
|
874
|
+
|
|
827
875
|
const fields = [];
|
|
828
876
|
const fieldRegex = /[-*]\s+(\w+)(?:\s*:\s*|\s+\()(.+?)(?:\)|$)/g;
|
|
829
877
|
let fieldMatch;
|
|
830
|
-
|
|
878
|
+
|
|
831
879
|
while ((fieldMatch = fieldRegex.exec(body)) !== null) {
|
|
832
880
|
const fieldName = fieldMatch[1];
|
|
833
881
|
const fieldType = fieldMatch[2].trim();
|
|
834
|
-
|
|
882
|
+
|
|
835
883
|
fields.push({
|
|
836
884
|
name: fieldName,
|
|
837
885
|
type: fieldType,
|
|
838
|
-
required:
|
|
839
|
-
|
|
886
|
+
required:
|
|
887
|
+
body.toLowerCase().includes(`${fieldName}`.toLowerCase() + ' required') ||
|
|
888
|
+
body.includes(`${fieldName}*`),
|
|
840
889
|
unique: body.toLowerCase().includes(`${fieldName}`.toLowerCase() + ' unique'),
|
|
841
890
|
});
|
|
842
891
|
}
|
|
843
|
-
|
|
892
|
+
|
|
844
893
|
dataModel.entities.push({
|
|
845
894
|
name,
|
|
846
895
|
description: '',
|
|
847
896
|
fields,
|
|
848
897
|
});
|
|
849
898
|
}
|
|
850
|
-
|
|
899
|
+
|
|
851
900
|
// Parse relationships
|
|
852
901
|
const relationshipSection = content.match(/##\s+Relationships?\s*\n([\s\S]+?)(?=\n##|$)/i);
|
|
853
902
|
if (relationshipSection) {
|
|
854
903
|
const relContent = relationshipSection[1];
|
|
855
904
|
const relRegex = /(\w+)\s*(?:→|->|has many|has one|belongs to|references)\s*(\w+)/gi;
|
|
856
905
|
let relMatch;
|
|
857
|
-
|
|
906
|
+
|
|
858
907
|
while ((relMatch = relRegex.exec(relContent)) !== null) {
|
|
859
908
|
dataModel.relationships.push({
|
|
860
909
|
from: relMatch[1],
|
|
@@ -863,40 +912,55 @@ async function parseDataModel(dataModelPath) {
|
|
|
863
912
|
});
|
|
864
913
|
}
|
|
865
914
|
}
|
|
866
|
-
|
|
915
|
+
|
|
867
916
|
return dataModel;
|
|
868
917
|
}
|
|
869
918
|
|
|
870
919
|
/**
|
|
871
920
|
* Parse contracts directory
|
|
872
|
-
* @param {string} contractsPath
|
|
921
|
+
* @param {string} contractsPath
|
|
873
922
|
* @returns {Promise<import('../ir/types').ContractIR[]>}
|
|
874
923
|
*/
|
|
875
924
|
async function parseContracts(contractsPath) {
|
|
876
925
|
const contracts = [];
|
|
877
|
-
|
|
926
|
+
|
|
878
927
|
try {
|
|
879
928
|
const entries = await fs.readdir(contractsPath, { withFileTypes: true });
|
|
880
|
-
|
|
929
|
+
|
|
881
930
|
for (const entry of entries) {
|
|
882
|
-
if (
|
|
931
|
+
if (
|
|
932
|
+
entry.isFile() &&
|
|
933
|
+
(entry.name.endsWith('.md') || entry.name.endsWith('.yaml') || entry.name.endsWith('.json'))
|
|
934
|
+
) {
|
|
883
935
|
const contractFile = path.join(contractsPath, entry.name);
|
|
884
936
|
const content = await fs.readFile(contractFile, 'utf-8');
|
|
885
|
-
|
|
937
|
+
|
|
886
938
|
// Determine contract type
|
|
887
939
|
let type = 'other';
|
|
888
|
-
if (
|
|
940
|
+
if (
|
|
941
|
+
content.includes('openapi') ||
|
|
942
|
+
content.includes('swagger') ||
|
|
943
|
+
entry.name.includes('openapi')
|
|
944
|
+
) {
|
|
889
945
|
type = 'rest';
|
|
890
|
-
} else if (
|
|
946
|
+
} else if (
|
|
947
|
+
content.includes('REST') ||
|
|
948
|
+
content.includes('GET') ||
|
|
949
|
+
content.includes('POST')
|
|
950
|
+
) {
|
|
891
951
|
type = 'rest';
|
|
892
|
-
} else if (
|
|
952
|
+
} else if (
|
|
953
|
+
content.includes('GraphQL') ||
|
|
954
|
+
content.includes('query') ||
|
|
955
|
+
content.includes('mutation')
|
|
956
|
+
) {
|
|
893
957
|
type = 'graphql';
|
|
894
958
|
} else if (content.includes('gRPC') || content.includes('protobuf')) {
|
|
895
959
|
type = 'grpc';
|
|
896
960
|
} else if (content.includes('WebSocket') || content.includes('ws://')) {
|
|
897
961
|
type = 'websocket';
|
|
898
962
|
}
|
|
899
|
-
|
|
963
|
+
|
|
900
964
|
contracts.push({
|
|
901
965
|
type,
|
|
902
966
|
name: entry.name.replace(/\.(md|yaml|json)$/, ''),
|
|
@@ -908,57 +972,57 @@ async function parseContracts(contractsPath) {
|
|
|
908
972
|
} catch (error) {
|
|
909
973
|
console.warn(`Warning: Failed to parse contracts: ${error.message}`);
|
|
910
974
|
}
|
|
911
|
-
|
|
975
|
+
|
|
912
976
|
return contracts;
|
|
913
977
|
}
|
|
914
978
|
|
|
915
979
|
/**
|
|
916
980
|
* Parse quickstart file
|
|
917
|
-
* @param {string} quickstartPath
|
|
981
|
+
* @param {string} quickstartPath
|
|
918
982
|
* @returns {Promise<import('../ir/types').QuickstartIR>}
|
|
919
983
|
*/
|
|
920
984
|
async function parseQuickstart(quickstartPath) {
|
|
921
985
|
const content = await fs.readFile(quickstartPath, 'utf-8');
|
|
922
|
-
|
|
986
|
+
|
|
923
987
|
const quickstart = {
|
|
924
988
|
steps: [],
|
|
925
989
|
rawContent: content,
|
|
926
990
|
};
|
|
927
|
-
|
|
991
|
+
|
|
928
992
|
// Parse numbered steps
|
|
929
993
|
const stepRegex = /^\d+\.\s+(.+)$/gm;
|
|
930
994
|
let match;
|
|
931
|
-
|
|
995
|
+
|
|
932
996
|
while ((match = stepRegex.exec(content)) !== null) {
|
|
933
997
|
quickstart.steps.push({
|
|
934
998
|
step: match[1].trim(),
|
|
935
999
|
});
|
|
936
1000
|
}
|
|
937
|
-
|
|
1001
|
+
|
|
938
1002
|
return quickstart;
|
|
939
1003
|
}
|
|
940
1004
|
|
|
941
1005
|
/**
|
|
942
1006
|
* Parse templates from .specify/templates/
|
|
943
|
-
* @param {string} specifyPath
|
|
1007
|
+
* @param {string} specifyPath
|
|
944
1008
|
* @returns {Promise<import('../ir/types').TemplateIR[]>}
|
|
945
1009
|
*/
|
|
946
1010
|
async function parseTemplates(specifyPath) {
|
|
947
1011
|
const templatesPath = path.join(specifyPath, 'templates');
|
|
948
1012
|
const templates = [];
|
|
949
|
-
|
|
950
|
-
if (!await fs.pathExists(templatesPath)) {
|
|
1013
|
+
|
|
1014
|
+
if (!(await fs.pathExists(templatesPath))) {
|
|
951
1015
|
return templates;
|
|
952
1016
|
}
|
|
953
|
-
|
|
1017
|
+
|
|
954
1018
|
try {
|
|
955
1019
|
const entries = await fs.readdir(templatesPath, { withFileTypes: true });
|
|
956
|
-
|
|
1020
|
+
|
|
957
1021
|
for (const entry of entries) {
|
|
958
1022
|
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
959
1023
|
const templateFile = path.join(templatesPath, entry.name);
|
|
960
1024
|
const content = await fs.readFile(templateFile, 'utf-8');
|
|
961
|
-
|
|
1025
|
+
|
|
962
1026
|
// Determine template type
|
|
963
1027
|
let type = 'other';
|
|
964
1028
|
if (entry.name.includes('spec')) {
|
|
@@ -968,7 +1032,7 @@ async function parseTemplates(specifyPath) {
|
|
|
968
1032
|
} else if (entry.name.includes('task')) {
|
|
969
1033
|
type = 'tasks';
|
|
970
1034
|
}
|
|
971
|
-
|
|
1035
|
+
|
|
972
1036
|
templates.push({
|
|
973
1037
|
name: entry.name.replace('.md', ''),
|
|
974
1038
|
type,
|
|
@@ -979,7 +1043,7 @@ async function parseTemplates(specifyPath) {
|
|
|
979
1043
|
} catch (error) {
|
|
980
1044
|
console.warn(`Warning: Failed to parse templates: ${error.message}`);
|
|
981
1045
|
}
|
|
982
|
-
|
|
1046
|
+
|
|
983
1047
|
return templates;
|
|
984
1048
|
}
|
|
985
1049
|
|