musubi-sdd 5.1.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.
Files changed (232) hide show
  1. package/README.ja.md +106 -48
  2. package/README.md +110 -32
  3. package/bin/musubi-analyze.js +74 -67
  4. package/bin/musubi-browser.js +27 -26
  5. package/bin/musubi-change.js +48 -47
  6. package/bin/musubi-checkpoint.js +10 -7
  7. package/bin/musubi-convert.js +25 -25
  8. package/bin/musubi-costs.js +27 -10
  9. package/bin/musubi-gui.js +52 -46
  10. package/bin/musubi-init.js +1952 -10
  11. package/bin/musubi-orchestrate.js +327 -239
  12. package/bin/musubi-remember.js +69 -56
  13. package/bin/musubi-resolve.js +53 -45
  14. package/bin/musubi-trace.js +51 -22
  15. package/bin/musubi-validate.js +39 -30
  16. package/bin/musubi-workflow.js +33 -34
  17. package/bin/musubi.js +39 -2
  18. package/package.json +1 -1
  19. package/src/agents/agent-loop.js +94 -95
  20. package/src/agents/agentic/code-generator.js +119 -109
  21. package/src/agents/agentic/code-reviewer.js +105 -108
  22. package/src/agents/agentic/index.js +4 -4
  23. package/src/agents/browser/action-executor.js +13 -13
  24. package/src/agents/browser/ai-comparator.js +11 -10
  25. package/src/agents/browser/context-manager.js +6 -6
  26. package/src/agents/browser/index.js +5 -5
  27. package/src/agents/browser/nl-parser.js +31 -46
  28. package/src/agents/browser/screenshot.js +2 -2
  29. package/src/agents/browser/test-generator.js +6 -4
  30. package/src/agents/function-tool.js +71 -65
  31. package/src/agents/index.js +7 -7
  32. package/src/agents/schema-generator.js +98 -94
  33. package/src/analyzers/ast-extractor.js +158 -146
  34. package/src/analyzers/codegraph-auto-update.js +858 -0
  35. package/src/analyzers/complexity-analyzer.js +536 -0
  36. package/src/analyzers/context-optimizer.js +241 -126
  37. package/src/analyzers/impact-analyzer.js +1 -1
  38. package/src/analyzers/large-project-analyzer.js +766 -0
  39. package/src/analyzers/repository-map.js +77 -81
  40. package/src/analyzers/security-analyzer.js +19 -11
  41. package/src/analyzers/stuck-detector.js +19 -17
  42. package/src/converters/index.js +78 -57
  43. package/src/converters/ir/types.js +12 -12
  44. package/src/converters/parsers/musubi-parser.js +134 -126
  45. package/src/converters/parsers/openapi-parser.js +70 -53
  46. package/src/converters/parsers/speckit-parser.js +239 -175
  47. package/src/converters/writers/musubi-writer.js +123 -118
  48. package/src/converters/writers/speckit-writer.js +124 -113
  49. package/src/generators/rust-migration-generator.js +512 -0
  50. package/src/gui/public/index.html +1365 -1211
  51. package/src/gui/server.js +41 -40
  52. package/src/gui/services/file-watcher.js +23 -8
  53. package/src/gui/services/project-scanner.js +26 -20
  54. package/src/gui/services/replanning-service.js +27 -23
  55. package/src/gui/services/traceability-service.js +8 -8
  56. package/src/gui/services/workflow-service.js +14 -7
  57. package/src/index.js +151 -0
  58. package/src/integrations/cicd.js +90 -104
  59. package/src/integrations/codegraph-mcp.js +643 -0
  60. package/src/integrations/documentation.js +142 -103
  61. package/src/integrations/examples.js +95 -80
  62. package/src/integrations/github-client.js +17 -17
  63. package/src/integrations/index.js +5 -5
  64. package/src/integrations/mcp/index.js +21 -21
  65. package/src/integrations/mcp/mcp-context-provider.js +76 -78
  66. package/src/integrations/mcp/mcp-discovery.js +74 -72
  67. package/src/integrations/mcp/mcp-tool-registry.js +99 -94
  68. package/src/integrations/mcp-connector.js +70 -66
  69. package/src/integrations/platforms.js +50 -49
  70. package/src/integrations/tool-discovery.js +37 -31
  71. package/src/llm-providers/anthropic-provider.js +11 -11
  72. package/src/llm-providers/base-provider.js +16 -18
  73. package/src/llm-providers/copilot-provider.js +22 -19
  74. package/src/llm-providers/index.js +26 -25
  75. package/src/llm-providers/ollama-provider.js +11 -11
  76. package/src/llm-providers/openai-provider.js +12 -12
  77. package/src/managers/agent-memory.js +36 -24
  78. package/src/managers/checkpoint-manager.js +4 -8
  79. package/src/managers/delta-spec.js +19 -19
  80. package/src/managers/index.js +13 -4
  81. package/src/managers/memory-condenser.js +35 -45
  82. package/src/managers/repo-skill-manager.js +57 -31
  83. package/src/managers/skill-loader.js +25 -22
  84. package/src/managers/skill-tools.js +36 -72
  85. package/src/managers/workflow.js +30 -22
  86. package/src/monitoring/cost-tracker.js +48 -46
  87. package/src/monitoring/incident-manager.js +116 -106
  88. package/src/monitoring/index.js +144 -134
  89. package/src/monitoring/observability.js +75 -62
  90. package/src/monitoring/quality-dashboard.js +45 -41
  91. package/src/monitoring/release-manager.js +63 -53
  92. package/src/orchestration/agent-skill-binding.js +39 -47
  93. package/src/orchestration/error-handler.js +65 -107
  94. package/src/orchestration/guardrails/base-guardrail.js +26 -24
  95. package/src/orchestration/guardrails/guardrail-rules.js +50 -64
  96. package/src/orchestration/guardrails/index.js +5 -5
  97. package/src/orchestration/guardrails/input-guardrail.js +58 -45
  98. package/src/orchestration/guardrails/output-guardrail.js +104 -81
  99. package/src/orchestration/guardrails/safety-check.js +79 -79
  100. package/src/orchestration/index.js +38 -55
  101. package/src/orchestration/mcp-tool-adapters.js +96 -99
  102. package/src/orchestration/orchestration-engine.js +21 -21
  103. package/src/orchestration/pattern-registry.js +60 -45
  104. package/src/orchestration/patterns/auto.js +34 -47
  105. package/src/orchestration/patterns/group-chat.js +59 -65
  106. package/src/orchestration/patterns/handoff.js +67 -65
  107. package/src/orchestration/patterns/human-in-loop.js +51 -72
  108. package/src/orchestration/patterns/nested.js +25 -40
  109. package/src/orchestration/patterns/sequential.js +35 -34
  110. package/src/orchestration/patterns/swarm.js +63 -56
  111. package/src/orchestration/patterns/triage.js +150 -109
  112. package/src/orchestration/reasoning/index.js +9 -9
  113. package/src/orchestration/reasoning/planning-engine.js +143 -140
  114. package/src/orchestration/reasoning/reasoning-engine.js +206 -144
  115. package/src/orchestration/reasoning/self-correction.js +121 -128
  116. package/src/orchestration/replanning/adaptive-goal-modifier.js +107 -112
  117. package/src/orchestration/replanning/alternative-generator.js +37 -42
  118. package/src/orchestration/replanning/config.js +63 -59
  119. package/src/orchestration/replanning/goal-progress-tracker.js +98 -100
  120. package/src/orchestration/replanning/index.js +24 -20
  121. package/src/orchestration/replanning/plan-evaluator.js +49 -50
  122. package/src/orchestration/replanning/plan-monitor.js +32 -28
  123. package/src/orchestration/replanning/proactive-path-optimizer.js +175 -178
  124. package/src/orchestration/replanning/replan-history.js +33 -26
  125. package/src/orchestration/replanning/replanning-engine.js +106 -108
  126. package/src/orchestration/skill-executor.js +107 -109
  127. package/src/orchestration/skill-registry.js +85 -89
  128. package/src/orchestration/workflow-examples.js +228 -231
  129. package/src/orchestration/workflow-executor.js +65 -68
  130. package/src/orchestration/workflow-orchestrator.js +72 -73
  131. package/src/phase4-integration.js +47 -40
  132. package/src/phase5-integration.js +89 -30
  133. package/src/reporters/coverage-report.js +82 -30
  134. package/src/reporters/hierarchical-reporter.js +498 -0
  135. package/src/reporters/traceability-matrix-report.js +29 -20
  136. package/src/resolvers/issue-resolver.js +43 -31
  137. package/src/steering/advanced-validation.js +133 -124
  138. package/src/steering/auto-updater.js +60 -73
  139. package/src/steering/index.js +6 -6
  140. package/src/steering/quality-metrics.js +41 -35
  141. package/src/steering/steering-auto-update.js +83 -86
  142. package/src/steering/steering-validator.js +98 -106
  143. package/src/steering/template-constraints.js +53 -54
  144. package/src/templates/agents/claude-code/CLAUDE.md +32 -32
  145. package/src/templates/agents/claude-code/skills/agent-assistant/SKILL.md +13 -5
  146. package/src/templates/agents/claude-code/skills/ai-ml-engineer/mlops-guide.md +23 -23
  147. package/src/templates/agents/claude-code/skills/ai-ml-engineer/model-card-template.md +60 -41
  148. package/src/templates/agents/claude-code/skills/api-designer/api-patterns.md +27 -19
  149. package/src/templates/agents/claude-code/skills/api-designer/openapi-template.md +11 -7
  150. package/src/templates/agents/claude-code/skills/bug-hunter/SKILL.md +4 -3
  151. package/src/templates/agents/claude-code/skills/bug-hunter/root-cause-analysis.md +37 -15
  152. package/src/templates/agents/claude-code/skills/change-impact-analyzer/dependency-graph-patterns.md +36 -42
  153. package/src/templates/agents/claude-code/skills/change-impact-analyzer/impact-analysis-template.md +69 -60
  154. package/src/templates/agents/claude-code/skills/cloud-architect/aws-patterns.md +31 -38
  155. package/src/templates/agents/claude-code/skills/cloud-architect/azure-patterns.md +28 -23
  156. package/src/templates/agents/claude-code/skills/code-reviewer/SKILL.md +61 -0
  157. package/src/templates/agents/claude-code/skills/code-reviewer/best-practices.md +27 -0
  158. package/src/templates/agents/claude-code/skills/code-reviewer/review-checklist.md +29 -10
  159. package/src/templates/agents/claude-code/skills/code-reviewer/review-standards.md +29 -24
  160. package/src/templates/agents/claude-code/skills/constitution-enforcer/SKILL.md +8 -6
  161. package/src/templates/agents/claude-code/skills/constitution-enforcer/constitutional-articles.md +62 -26
  162. package/src/templates/agents/claude-code/skills/constitution-enforcer/phase-minus-one-gates.md +35 -16
  163. package/src/templates/agents/claude-code/skills/database-administrator/backup-recovery.md +27 -17
  164. package/src/templates/agents/claude-code/skills/database-administrator/tuning-guide.md +25 -20
  165. package/src/templates/agents/claude-code/skills/database-schema-designer/schema-patterns.md +39 -22
  166. package/src/templates/agents/claude-code/skills/devops-engineer/ci-cd-templates.md +25 -22
  167. package/src/templates/agents/claude-code/skills/issue-resolver/SKILL.md +24 -21
  168. package/src/templates/agents/claude-code/skills/orchestrator/SKILL.md +148 -63
  169. package/src/templates/agents/claude-code/skills/orchestrator/patterns.md +35 -16
  170. package/src/templates/agents/claude-code/skills/orchestrator/selection-matrix.md +69 -64
  171. package/src/templates/agents/claude-code/skills/performance-engineer/optimization-playbook.md +47 -47
  172. package/src/templates/agents/claude-code/skills/performance-optimizer/SKILL.md +69 -0
  173. package/src/templates/agents/claude-code/skills/performance-optimizer/benchmark-template.md +63 -45
  174. package/src/templates/agents/claude-code/skills/performance-optimizer/optimization-patterns.md +33 -35
  175. package/src/templates/agents/claude-code/skills/project-manager/SKILL.md +7 -6
  176. package/src/templates/agents/claude-code/skills/project-manager/agile-ceremonies.md +47 -28
  177. package/src/templates/agents/claude-code/skills/project-manager/project-templates.md +94 -78
  178. package/src/templates/agents/claude-code/skills/quality-assurance/SKILL.md +20 -17
  179. package/src/templates/agents/claude-code/skills/quality-assurance/qa-plan-template.md +63 -49
  180. package/src/templates/agents/claude-code/skills/release-coordinator/SKILL.md +5 -5
  181. package/src/templates/agents/claude-code/skills/release-coordinator/feature-flag-guide.md +30 -26
  182. package/src/templates/agents/claude-code/skills/release-coordinator/release-plan-template.md +67 -35
  183. package/src/templates/agents/claude-code/skills/requirements-analyst/ears-format.md +54 -42
  184. package/src/templates/agents/claude-code/skills/requirements-analyst/validation-rules.md +36 -33
  185. package/src/templates/agents/claude-code/skills/security-auditor/SKILL.md +77 -19
  186. package/src/templates/agents/claude-code/skills/security-auditor/audit-checklists.md +24 -24
  187. package/src/templates/agents/claude-code/skills/security-auditor/owasp-top-10.md +61 -20
  188. package/src/templates/agents/claude-code/skills/security-auditor/vulnerability-patterns.md +43 -11
  189. package/src/templates/agents/claude-code/skills/site-reliability-engineer/SKILL.md +1 -0
  190. package/src/templates/agents/claude-code/skills/site-reliability-engineer/incident-response-template.md +55 -25
  191. package/src/templates/agents/claude-code/skills/site-reliability-engineer/observability-patterns.md +78 -68
  192. package/src/templates/agents/claude-code/skills/site-reliability-engineer/slo-sli-guide.md +73 -53
  193. package/src/templates/agents/claude-code/skills/software-developer/solid-principles.md +83 -37
  194. package/src/templates/agents/claude-code/skills/software-developer/test-first-workflow.md +38 -31
  195. package/src/templates/agents/claude-code/skills/steering/SKILL.md +1 -0
  196. package/src/templates/agents/claude-code/skills/steering/auto-update-rules.md +31 -0
  197. package/src/templates/agents/claude-code/skills/system-architect/adr-template.md +25 -7
  198. package/src/templates/agents/claude-code/skills/system-architect/c4-model-guide.md +74 -61
  199. package/src/templates/agents/claude-code/skills/technical-writer/doc-templates/documentation-templates.md +70 -52
  200. package/src/templates/agents/claude-code/skills/test-engineer/SKILL.md +2 -0
  201. package/src/templates/agents/claude-code/skills/test-engineer/ears-test-mapping.md +75 -71
  202. package/src/templates/agents/claude-code/skills/test-engineer/test-types.md +85 -63
  203. package/src/templates/agents/claude-code/skills/traceability-auditor/coverage-matrix-template.md +39 -36
  204. package/src/templates/agents/claude-code/skills/traceability-auditor/gap-detection-rules.md +22 -17
  205. package/src/templates/agents/claude-code/skills/ui-ux-designer/SKILL.md +1 -0
  206. package/src/templates/agents/claude-code/skills/ui-ux-designer/accessibility-guidelines.md +49 -75
  207. package/src/templates/agents/claude-code/skills/ui-ux-designer/design-system-components.md +71 -59
  208. package/src/templates/agents/codex/AGENTS.md +74 -42
  209. package/src/templates/agents/cursor/AGENTS.md +74 -42
  210. package/src/templates/agents/gemini-cli/GEMINI.md +74 -42
  211. package/src/templates/agents/github-copilot/AGENTS.md +83 -51
  212. package/src/templates/agents/qwen-code/QWEN.md +74 -42
  213. package/src/templates/agents/windsurf/AGENTS.md +74 -42
  214. package/src/templates/architectures/README.md +41 -0
  215. package/src/templates/architectures/clean-architecture/README.md +113 -0
  216. package/src/templates/architectures/event-driven/README.md +162 -0
  217. package/src/templates/architectures/hexagonal/README.md +130 -0
  218. package/src/templates/index.js +6 -1
  219. package/src/templates/locale-manager.js +16 -16
  220. package/src/templates/shared/delta-spec-template.md +20 -13
  221. package/src/templates/shared/github-actions/musubi-issue-resolver.yml +5 -5
  222. package/src/templates/shared/github-actions/musubi-security-check.yml +3 -3
  223. package/src/templates/shared/github-actions/musubi-validate.yml +4 -4
  224. package/src/templates/shared/steering/structure.md +95 -0
  225. package/src/templates/skills/browser-agent.md +21 -16
  226. package/src/templates/skills/web-gui.md +8 -0
  227. package/src/templates/template-constraints.js +50 -53
  228. package/src/validators/advanced-validation.js +30 -36
  229. package/src/validators/constitutional-validator.js +77 -73
  230. package/src/validators/critic-system.js +49 -59
  231. package/src/validators/delta-format.js +59 -55
  232. 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
- { number: 1, name: 'Specification Primacy', keywords: ['spec', 'requirement', 'documentation', 'define', 'specify'] },
154
- { number: 2, name: 'Test-First Development', keywords: ['test', 'quality', 'validation', 'verify', 'tdd'] },
155
- { number: 3, name: 'Architectural Compliance', keywords: ['architecture', 'structure', 'design', 'pattern', 'component'] },
156
- { number: 4, name: 'Traceability Requirements', keywords: ['trace', 'track', 'link', 'reference', 'map'] },
157
- { number: 5, name: 'Change Control Protocol', keywords: ['change', 'version', 'control', 'update', 'modify'] },
158
- { number: 6, name: 'Separation of Concerns', keywords: ['separation', 'modular', 'concern', 'decouple', 'isolate'] },
159
- { number: 7, name: 'Documentation Standards', keywords: ['document', 'standard', 'format', 'readme', 'comment'] },
160
- { number: 8, name: 'Continuous Validation', keywords: ['continuous', 'validate', 'check', 'ci', 'automate'] },
161
- { number: 9, name: 'Graceful Degradation', keywords: ['graceful', 'fallback', 'degrade', 'error', 'recover'] }
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: ['All features must be specified before implementation', 'Specifications are the single source of truth'],
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(/As\s+(?:a|an)\s+(.+?),\s+I\s+want\s+(?:to\s+)?(.+?)\s+so\s+that\s+(.+?)(?:\.|$)/i);
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(/As\s+(?:a|an)\s+(.+?),\s+I\s+want\s+(?:to\s+)?(.+?)\s+so\s+that\s+(.+?)(?:\.|$)/i);
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(/##\s+(?:Implementation\s+)?Phases?\s*\n([\s\S]+?)(?=\n##|$)/i);
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 lines = content.split('\n');
692
- let lineIndex = 0;
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(/[\[\]]/g, '') : undefined;
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(/##\s+Alternatives?\s+Considered\s*\n([\s\S]+?)(?=\n##|$)/i);
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 ? ['yes', 'true', 'rejected'].includes(rejectedMatch[1].toLowerCase()) : false,
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: body.toLowerCase().includes(`${fieldName}`.toLowerCase() + ' required')
839
- || body.includes(`${fieldName}*`),
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 (entry.isFile() && (entry.name.endsWith('.md') || entry.name.endsWith('.yaml') || entry.name.endsWith('.json'))) {
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 (content.includes('openapi') || content.includes('swagger') || entry.name.includes('openapi')) {
940
+ if (
941
+ content.includes('openapi') ||
942
+ content.includes('swagger') ||
943
+ entry.name.includes('openapi')
944
+ ) {
889
945
  type = 'rest';
890
- } else if (content.includes('REST') || content.includes('GET') || content.includes('POST')) {
946
+ } else if (
947
+ content.includes('REST') ||
948
+ content.includes('GET') ||
949
+ content.includes('POST')
950
+ ) {
891
951
  type = 'rest';
892
- } else if (content.includes('GraphQL') || content.includes('query') || content.includes('mutation')) {
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