specweave 0.21.2 → 0.21.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +232 -5
- package/dist/plugins/specweave-github/lib/IssueStateManager.d.ts +98 -0
- package/dist/plugins/specweave-github/lib/IssueStateManager.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/IssueStateManager.js +146 -0
- package/dist/plugins/specweave-github/lib/IssueStateManager.js.map +1 -0
- package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/user-story-issue-builder.js +6 -0
- package/dist/plugins/specweave-github/lib/user-story-issue-builder.js.map +1 -1
- package/dist/src/cli/commands/check-hooks.d.ts +11 -0
- package/dist/src/cli/commands/check-hooks.d.ts.map +1 -0
- package/dist/src/cli/commands/check-hooks.js +144 -0
- package/dist/src/cli/commands/check-hooks.js.map +1 -0
- package/dist/src/cli/commands/cicd-monitor.js +3 -3
- package/dist/src/cli/commands/cicd-monitor.js.map +1 -1
- package/dist/src/cli/commands/import-docs.js +2 -2
- package/dist/src/cli/commands/import-docs.js.map +1 -1
- package/dist/src/cli/commands/init-multiproject.js +4 -4
- package/dist/src/cli/commands/init-multiproject.js.map +1 -1
- package/dist/src/cli/commands/migrate-to-multiproject.js +3 -3
- package/dist/src/cli/commands/migrate-to-multiproject.js.map +1 -1
- package/dist/src/cli/commands/plan/agent-invoker.d.ts +79 -0
- package/dist/src/cli/commands/plan/agent-invoker.d.ts.map +1 -0
- package/dist/src/cli/commands/plan/agent-invoker.js +383 -0
- package/dist/src/cli/commands/plan/agent-invoker.js.map +1 -0
- package/dist/src/cli/commands/plan/increment-detector.d.ts +27 -0
- package/dist/src/cli/commands/plan/increment-detector.d.ts.map +1 -0
- package/dist/src/cli/commands/plan/increment-detector.js +159 -0
- package/dist/src/cli/commands/plan/increment-detector.js.map +1 -0
- package/dist/src/cli/commands/plan/plan-orchestrator.d.ts +45 -0
- package/dist/src/cli/commands/plan/plan-orchestrator.d.ts.map +1 -0
- package/dist/src/cli/commands/plan/plan-orchestrator.js +229 -0
- package/dist/src/cli/commands/plan/plan-orchestrator.js.map +1 -0
- package/dist/src/cli/commands/plan/plan-validator.d.ts +36 -0
- package/dist/src/cli/commands/plan/plan-validator.d.ts.map +1 -0
- package/dist/src/cli/commands/plan/plan-validator.js +174 -0
- package/dist/src/cli/commands/plan/plan-validator.js.map +1 -0
- package/dist/src/cli/commands/plan/types.d.ts +170 -0
- package/dist/src/cli/commands/plan/types.d.ts.map +1 -0
- package/dist/src/cli/commands/plan/types.js +42 -0
- package/dist/src/cli/commands/plan/types.js.map +1 -0
- package/dist/src/cli/commands/plan-command.d.ts +16 -0
- package/dist/src/cli/commands/plan-command.d.ts.map +1 -0
- package/dist/src/cli/commands/plan-command.js +127 -0
- package/dist/src/cli/commands/plan-command.js.map +1 -0
- package/dist/src/cli/commands/switch-project.js +3 -3
- package/dist/src/cli/commands/switch-project.js.map +1 -1
- package/dist/src/cli/commands/validate-parent-repo.js +1 -1
- package/dist/src/cli/commands/validate-parent-repo.js.map +1 -1
- package/dist/src/config/ConfigManager.d.ts +69 -0
- package/dist/src/config/ConfigManager.d.ts.map +1 -0
- package/dist/src/config/ConfigManager.js +130 -0
- package/dist/src/config/ConfigManager.js.map +1 -0
- package/dist/src/config/types.d.ts +1357 -0
- package/dist/src/config/types.d.ts.map +1 -0
- package/dist/src/config/types.js +67 -0
- package/dist/src/config/types.js.map +1 -0
- package/dist/src/core/brownfield/importer.d.ts +1 -1
- package/dist/src/core/brownfield/importer.d.ts.map +1 -1
- package/dist/src/core/brownfield/importer.js +3 -3
- package/dist/src/core/brownfield/importer.js.map +1 -1
- package/dist/src/core/cicd/config-loader.d.ts +1 -1
- package/dist/src/core/cicd/config-loader.d.ts.map +1 -1
- package/dist/src/core/cicd/index.d.ts +6 -6
- package/dist/src/core/cicd/index.d.ts.map +1 -1
- package/dist/src/core/cicd/index.js +6 -6
- package/dist/src/core/cicd/index.js.map +1 -1
- package/dist/src/core/cicd/monitor-service.d.ts +3 -3
- package/dist/src/core/cicd/monitor-service.d.ts.map +1 -1
- package/dist/src/core/cicd/monitor-service.js +3 -3
- package/dist/src/core/cicd/monitor-service.js.map +1 -1
- package/dist/src/core/cicd/notifier.d.ts +1 -1
- package/dist/src/core/cicd/notifier.d.ts.map +1 -1
- package/dist/src/core/cicd/state-manager.d.ts +1 -1
- package/dist/src/core/cicd/state-manager.d.ts.map +1 -1
- package/dist/src/core/cicd/state-manager.js +1 -1
- package/dist/src/core/cicd/state-manager.js.map +1 -1
- package/dist/src/core/cicd/workflow-monitor.d.ts +1 -1
- package/dist/src/core/cicd/workflow-monitor.d.ts.map +1 -1
- package/dist/src/core/cicd/workflow-monitor.js +1 -1
- package/dist/src/core/cicd/workflow-monitor.js.map +1 -1
- package/dist/src/core/cost-tracker.d.ts +2 -2
- package/dist/src/core/cost-tracker.d.ts.map +1 -1
- package/dist/src/core/cost-tracker.js +1 -1
- package/dist/src/core/cost-tracker.js.map +1 -1
- package/dist/src/core/hooks/HealthReporter.d.ts +55 -0
- package/dist/src/core/hooks/HealthReporter.d.ts.map +1 -0
- package/dist/src/core/hooks/HealthReporter.js +268 -0
- package/dist/src/core/hooks/HealthReporter.js.map +1 -0
- package/dist/src/core/hooks/HookAutoFixer.d.ts +41 -0
- package/dist/src/core/hooks/HookAutoFixer.d.ts.map +1 -0
- package/dist/src/core/hooks/HookAutoFixer.js +222 -0
- package/dist/src/core/hooks/HookAutoFixer.js.map +1 -0
- package/dist/src/core/hooks/HookExecutor.d.ts +57 -0
- package/dist/src/core/hooks/HookExecutor.d.ts.map +1 -0
- package/dist/src/core/hooks/HookExecutor.js +287 -0
- package/dist/src/core/hooks/HookExecutor.js.map +1 -0
- package/dist/src/core/hooks/HookHealthChecker.d.ts +51 -0
- package/dist/src/core/hooks/HookHealthChecker.d.ts.map +1 -0
- package/dist/src/core/hooks/HookHealthChecker.js +212 -0
- package/dist/src/core/hooks/HookHealthChecker.js.map +1 -0
- package/dist/src/core/hooks/HookScanner.d.ts +65 -0
- package/dist/src/core/hooks/HookScanner.d.ts.map +1 -0
- package/dist/src/core/hooks/HookScanner.js +214 -0
- package/dist/src/core/hooks/HookScanner.js.map +1 -0
- package/dist/src/core/hooks/types.d.ts +281 -0
- package/dist/src/core/hooks/types.d.ts.map +1 -0
- package/dist/src/core/hooks/types.js +10 -0
- package/dist/src/core/hooks/types.js.map +1 -0
- package/dist/src/core/iac/index.d.ts +10 -0
- package/dist/src/core/iac/index.d.ts.map +1 -0
- package/dist/src/core/iac/index.js +11 -0
- package/dist/src/core/iac/index.js.map +1 -0
- package/dist/src/core/iac/template-engine.d.ts +77 -0
- package/dist/src/core/iac/template-engine.d.ts.map +1 -0
- package/dist/src/core/iac/template-engine.js +182 -0
- package/dist/src/core/iac/template-engine.js.map +1 -0
- package/dist/src/core/increment/ac-status-manager.d.ts +115 -0
- package/dist/src/core/increment/ac-status-manager.d.ts.map +1 -0
- package/dist/src/core/increment/ac-status-manager.js +343 -0
- package/dist/src/core/increment/ac-status-manager.js.map +1 -0
- package/dist/src/core/increment/auto-transition-manager.d.ts +60 -0
- package/dist/src/core/increment/auto-transition-manager.d.ts.map +1 -0
- package/dist/src/core/increment/auto-transition-manager.js +192 -0
- package/dist/src/core/increment/auto-transition-manager.js.map +1 -0
- package/dist/src/core/increment/limits.d.ts +1 -1
- package/dist/src/core/increment/limits.d.ts.map +1 -1
- package/dist/src/core/increment/limits.js +2 -2
- package/dist/src/core/increment/limits.js.map +1 -1
- package/dist/src/core/increment/spec-sync-manager.d.ts +177 -0
- package/dist/src/core/increment/spec-sync-manager.d.ts.map +1 -0
- package/dist/src/core/increment/spec-sync-manager.js +496 -0
- package/dist/src/core/increment/spec-sync-manager.js.map +1 -0
- package/dist/src/core/increment/status-auto-transition.d.ts +46 -0
- package/dist/src/core/increment/status-auto-transition.d.ts.map +1 -0
- package/dist/src/core/increment/status-auto-transition.js +178 -0
- package/dist/src/core/increment/status-auto-transition.js.map +1 -0
- package/dist/src/core/increment/task-state-manager.d.ts +75 -0
- package/dist/src/core/increment/task-state-manager.d.ts.map +1 -0
- package/dist/src/core/increment/task-state-manager.js +117 -0
- package/dist/src/core/increment/task-state-manager.js.map +1 -0
- package/dist/src/core/living-docs/CodeValidator.d.ts +68 -0
- package/dist/src/core/living-docs/CodeValidator.d.ts.map +1 -0
- package/dist/src/core/living-docs/CodeValidator.js +160 -0
- package/dist/src/core/living-docs/CodeValidator.js.map +1 -0
- package/dist/src/core/living-docs/CompletionPropagator.d.ts +84 -0
- package/dist/src/core/living-docs/CompletionPropagator.d.ts.map +1 -0
- package/dist/src/core/living-docs/CompletionPropagator.js +205 -0
- package/dist/src/core/living-docs/CompletionPropagator.js.map +1 -0
- package/dist/src/core/living-docs/ProjectDetector.d.ts +29 -0
- package/dist/src/core/living-docs/ProjectDetector.d.ts.map +1 -0
- package/dist/src/core/living-docs/ProjectDetector.js +94 -0
- package/dist/src/core/living-docs/ProjectDetector.js.map +1 -0
- package/dist/src/core/living-docs/SpecDistributor.d.ts +55 -0
- package/dist/src/core/living-docs/SpecDistributor.d.ts.map +1 -0
- package/dist/src/core/living-docs/SpecDistributor.js +216 -0
- package/dist/src/core/living-docs/SpecDistributor.js.map +1 -0
- package/dist/src/core/living-docs/ThreeLayerSyncManager.d.ts +116 -0
- package/dist/src/core/living-docs/ThreeLayerSyncManager.d.ts.map +1 -0
- package/dist/src/core/living-docs/ThreeLayerSyncManager.js +356 -0
- package/dist/src/core/living-docs/ThreeLayerSyncManager.js.map +1 -0
- package/dist/src/core/living-docs/hierarchy-mapper.d.ts.map +1 -1
- package/dist/src/core/living-docs/hierarchy-mapper.js +2 -1
- package/dist/src/core/living-docs/hierarchy-mapper.js.map +1 -1
- package/dist/src/core/living-docs/index.d.ts +1 -1
- package/dist/src/core/living-docs/index.d.ts.map +1 -1
- package/dist/src/core/living-docs/index.js +5 -1
- package/dist/src/core/living-docs/index.js.map +1 -1
- package/dist/src/core/living-docs/types.d.ts +70 -300
- package/dist/src/core/living-docs/types.d.ts.map +1 -1
- package/dist/src/core/living-docs/types.js +2 -10
- package/dist/src/core/living-docs/types.js.map +1 -1
- package/dist/src/core/project-manager.d.ts +1 -1
- package/dist/src/core/project-manager.d.ts.map +1 -1
- package/dist/src/core/project-manager.js +2 -2
- package/dist/src/core/project-manager.js.map +1 -1
- package/dist/src/core/qa/quality-gate-decider.d.ts +1 -1
- package/dist/src/core/qa/quality-gate-decider.d.ts.map +1 -1
- package/dist/src/core/qa/risk-calculator.d.ts +1 -1
- package/dist/src/core/qa/risk-calculator.d.ts.map +1 -1
- package/dist/src/core/repo-structure/setup-summary.d.ts +1 -1
- package/dist/src/core/repo-structure/setup-summary.d.ts.map +1 -1
- package/dist/src/core/rfc-generator-v2.d.ts +1 -1
- package/dist/src/core/rfc-generator-v2.d.ts.map +1 -1
- package/dist/src/core/rfc-generator-v2.js +1 -1
- package/dist/src/core/rfc-generator-v2.js.map +1 -1
- package/dist/src/core/serverless/context-detector.d.ts +12 -0
- package/dist/src/core/serverless/context-detector.d.ts.map +1 -0
- package/dist/src/core/serverless/context-detector.js +213 -0
- package/dist/src/core/serverless/context-detector.js.map +1 -0
- package/dist/src/core/serverless/cost-comparison.d.ts +73 -0
- package/dist/src/core/serverless/cost-comparison.d.ts.map +1 -0
- package/dist/src/core/serverless/cost-comparison.js +198 -0
- package/dist/src/core/serverless/cost-comparison.js.map +1 -0
- package/dist/src/core/serverless/cost-estimator.d.ts +69 -0
- package/dist/src/core/serverless/cost-estimator.d.ts.map +1 -0
- package/dist/src/core/serverless/cost-estimator.js +127 -0
- package/dist/src/core/serverless/cost-estimator.js.map +1 -0
- package/dist/src/core/serverless/cost-optimizer.d.ts +57 -0
- package/dist/src/core/serverless/cost-optimizer.d.ts.map +1 -0
- package/dist/src/core/serverless/cost-optimizer.js +221 -0
- package/dist/src/core/serverless/cost-optimizer.js.map +1 -0
- package/dist/src/core/serverless/index.d.ts +20 -0
- package/dist/src/core/serverless/index.d.ts.map +1 -0
- package/dist/src/core/serverless/index.js +26 -0
- package/dist/src/core/serverless/index.js.map +1 -0
- package/dist/src/core/serverless/learning-path-recommender.d.ts +152 -0
- package/dist/src/core/serverless/learning-path-recommender.d.ts.map +1 -0
- package/dist/src/core/serverless/learning-path-recommender.js +389 -0
- package/dist/src/core/serverless/learning-path-recommender.js.map +1 -0
- package/dist/src/core/serverless/platform-data-loader.d.ts +22 -0
- package/dist/src/core/serverless/platform-data-loader.d.ts.map +1 -0
- package/dist/src/core/serverless/platform-data-loader.js +49 -0
- package/dist/src/core/serverless/platform-data-loader.js.map +1 -0
- package/dist/src/core/serverless/platform-selector.d.ts +20 -0
- package/dist/src/core/serverless/platform-selector.d.ts.map +1 -0
- package/dist/src/core/serverless/platform-selector.js +279 -0
- package/dist/src/core/serverless/platform-selector.js.map +1 -0
- package/dist/src/core/serverless/recommendation-formatter.d.ts +24 -0
- package/dist/src/core/serverless/recommendation-formatter.d.ts.map +1 -0
- package/dist/src/core/serverless/recommendation-formatter.js +70 -0
- package/dist/src/core/serverless/recommendation-formatter.js.map +1 -0
- package/dist/src/core/serverless/suitability-analyzer.d.ts +22 -0
- package/dist/src/core/serverless/suitability-analyzer.d.ts.map +1 -0
- package/dist/src/core/serverless/suitability-analyzer.js +262 -0
- package/dist/src/core/serverless/suitability-analyzer.js.map +1 -0
- package/dist/src/core/serverless/types.d.ts +96 -0
- package/dist/src/core/serverless/types.d.ts.map +1 -0
- package/dist/src/core/serverless/types.js +5 -0
- package/dist/src/core/serverless/types.js.map +1 -0
- package/dist/src/core/sync/bidirectional-engine.d.ts +1 -1
- package/dist/src/core/sync/bidirectional-engine.d.ts.map +1 -1
- package/dist/src/core/sync/enhanced-content-builder.d.ts +1 -1
- package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -1
- package/dist/src/core/sync/profile-manager.d.ts +1 -1
- package/dist/src/core/sync/profile-manager.d.ts.map +1 -1
- package/dist/src/core/sync/profile-selector.d.ts +1 -1
- package/dist/src/core/sync/profile-selector.d.ts.map +1 -1
- package/dist/src/core/sync/profile-selector.js +1 -1
- package/dist/src/core/sync/profile-selector.js.map +1 -1
- package/dist/src/core/sync/project-context.d.ts +1 -1
- package/dist/src/core/sync/project-context.d.ts.map +1 -1
- package/dist/src/core/sync/rate-limiter.d.ts +1 -1
- package/dist/src/core/sync/rate-limiter.d.ts.map +1 -1
- package/dist/src/core/sync/status-sync-engine.d.ts +2 -2
- package/dist/src/core/sync/status-sync-engine.d.ts.map +1 -1
- package/dist/src/core/sync/status-sync-engine.js +2 -2
- package/dist/src/core/sync/status-sync-engine.js.map +1 -1
- package/dist/src/core/sync/time-range-selector.d.ts +1 -1
- package/dist/src/core/sync/time-range-selector.d.ts.map +1 -1
- package/dist/src/core/sync/time-range-selector.js +1 -1
- package/dist/src/core/sync/time-range-selector.js.map +1 -1
- package/dist/src/core/types/increment-metadata.d.ts +27 -0
- package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
- package/dist/src/core/types/increment-metadata.js +45 -1
- package/dist/src/core/types/increment-metadata.js.map +1 -1
- package/dist/src/core/types/sync-profile.d.ts +2 -0
- package/dist/src/core/types/sync-profile.d.ts.map +1 -1
- package/dist/src/core/types/sync-profile.js.map +1 -1
- package/dist/src/core/validation/increment-structure-validator.d.ts +47 -0
- package/dist/src/core/validation/increment-structure-validator.d.ts.map +1 -0
- package/dist/src/core/validation/increment-structure-validator.js +187 -0
- package/dist/src/core/validation/increment-structure-validator.js.map +1 -0
- package/dist/src/core/validation/three-file-validator.d.ts +82 -0
- package/dist/src/core/validation/three-file-validator.d.ts.map +1 -0
- package/dist/src/core/validation/three-file-validator.js +320 -0
- package/dist/src/core/validation/three-file-validator.js.map +1 -0
- package/dist/src/core/workflow/phase-detector.d.ts +103 -0
- package/dist/src/core/workflow/phase-detector.d.ts.map +1 -0
- package/dist/src/core/workflow/phase-detector.js +704 -0
- package/dist/src/core/workflow/phase-detector.js.map +1 -0
- package/dist/src/core/workflow/types.d.ts +153 -0
- package/dist/src/core/workflow/types.d.ts.map +1 -0
- package/dist/src/core/workflow/types.js +47 -0
- package/dist/src/core/workflow/types.js.map +1 -0
- package/dist/src/init/InitFlow.d.ts +37 -0
- package/dist/src/init/InitFlow.d.ts.map +1 -0
- package/dist/src/init/InitFlow.js +209 -0
- package/dist/src/init/InitFlow.js.map +1 -0
- package/dist/src/init/architecture/ArchitectureDecisionEngine.d.ts +107 -0
- package/dist/src/init/architecture/ArchitectureDecisionEngine.d.ts.map +1 -0
- package/dist/src/init/architecture/ArchitectureDecisionEngine.js +405 -0
- package/dist/src/init/architecture/ArchitectureDecisionEngine.js.map +1 -0
- package/dist/src/init/architecture/CloudCreditsDatabase.d.ts +11 -0
- package/dist/src/init/architecture/CloudCreditsDatabase.d.ts.map +1 -0
- package/dist/src/init/architecture/CloudCreditsDatabase.js +92 -0
- package/dist/src/init/architecture/CloudCreditsDatabase.js.map +1 -0
- package/dist/src/init/architecture/types.d.ts +251 -0
- package/dist/src/init/architecture/types.d.ts.map +1 -0
- package/dist/src/init/architecture/types.js +54 -0
- package/dist/src/init/architecture/types.js.map +1 -0
- package/dist/src/init/compliance/ComplianceDetector.d.ts +17 -0
- package/dist/src/init/compliance/ComplianceDetector.d.ts.map +1 -0
- package/dist/src/init/compliance/ComplianceDetector.js +64 -0
- package/dist/src/init/compliance/ComplianceDetector.js.map +1 -0
- package/dist/src/init/compliance/standards-database.d.ts +48 -0
- package/dist/src/init/compliance/standards-database.d.ts.map +1 -0
- package/dist/src/init/compliance/standards-database.js +506 -0
- package/dist/src/init/compliance/standards-database.js.map +1 -0
- package/dist/src/init/compliance/types.d.ts +91 -0
- package/dist/src/init/compliance/types.d.ts.map +1 -0
- package/dist/src/init/compliance/types.js +43 -0
- package/dist/src/init/compliance/types.js.map +1 -0
- package/dist/src/init/repo/GitHubAPIClient.d.ts +51 -0
- package/dist/src/init/repo/GitHubAPIClient.d.ts.map +1 -0
- package/dist/src/init/repo/GitHubAPIClient.js +144 -0
- package/dist/src/init/repo/GitHubAPIClient.js.map +1 -0
- package/dist/src/init/repo/RepositorySelector.d.ts +45 -0
- package/dist/src/init/repo/RepositorySelector.d.ts.map +1 -0
- package/dist/src/init/repo/RepositorySelector.js +106 -0
- package/dist/src/init/repo/RepositorySelector.js.map +1 -0
- package/dist/src/init/repo/types.d.ts +95 -0
- package/dist/src/init/repo/types.d.ts.map +1 -0
- package/dist/src/init/repo/types.js +25 -0
- package/dist/src/init/repo/types.js.map +1 -0
- package/dist/src/init/research/CompetitorAnalyzer.d.ts +79 -0
- package/dist/src/init/research/CompetitorAnalyzer.d.ts.map +1 -0
- package/dist/src/init/research/CompetitorAnalyzer.js +265 -0
- package/dist/src/init/research/CompetitorAnalyzer.js.map +1 -0
- package/dist/src/init/research/MarketDetector.d.ts +62 -0
- package/dist/src/init/research/MarketDetector.d.ts.map +1 -0
- package/dist/src/init/research/MarketDetector.js +247 -0
- package/dist/src/init/research/MarketDetector.js.map +1 -0
- package/dist/src/init/research/OpportunityScorer.d.ts +58 -0
- package/dist/src/init/research/OpportunityScorer.d.ts.map +1 -0
- package/dist/src/init/research/OpportunityScorer.js +194 -0
- package/dist/src/init/research/OpportunityScorer.js.map +1 -0
- package/dist/src/init/research/QuestionGenerator.d.ts +68 -0
- package/dist/src/init/research/QuestionGenerator.d.ts.map +1 -0
- package/dist/src/init/research/QuestionGenerator.js +244 -0
- package/dist/src/init/research/QuestionGenerator.js.map +1 -0
- package/dist/src/init/research/ReportGenerator.d.ts +36 -0
- package/dist/src/init/research/ReportGenerator.d.ts.map +1 -0
- package/dist/src/init/research/ReportGenerator.js +125 -0
- package/dist/src/init/research/ReportGenerator.js.map +1 -0
- package/dist/src/init/research/VisionAnalyzer.d.ts +129 -0
- package/dist/src/init/research/VisionAnalyzer.d.ts.map +1 -0
- package/dist/src/init/research/VisionAnalyzer.js +212 -0
- package/dist/src/init/research/VisionAnalyzer.js.map +1 -0
- package/dist/src/init/research/keyword-extractor.d.ts +78 -0
- package/dist/src/init/research/keyword-extractor.d.ts.map +1 -0
- package/dist/src/init/research/keyword-extractor.js +230 -0
- package/dist/src/init/research/keyword-extractor.js.map +1 -0
- package/dist/src/init/research/src/config/ConfigManager.d.ts +14 -0
- package/dist/src/init/research/src/config/ConfigManager.d.ts.map +1 -0
- package/dist/src/init/research/src/config/ConfigManager.js +45 -0
- package/dist/src/init/research/src/config/ConfigManager.js.map +1 -0
- package/dist/src/init/research/src/config/types.d.ts +102 -0
- package/dist/src/init/research/src/config/types.d.ts.map +1 -0
- package/dist/src/init/research/src/config/types.js +24 -0
- package/dist/src/init/research/src/config/types.js.map +1 -0
- package/dist/src/init/research/types.d.ts +183 -0
- package/dist/src/init/research/types.d.ts.map +1 -0
- package/dist/src/init/research/types.js +65 -0
- package/dist/src/init/research/types.js.map +1 -0
- package/dist/src/init/team/ServerlessSavingsCalculator.d.ts +136 -0
- package/dist/src/init/team/ServerlessSavingsCalculator.d.ts.map +1 -0
- package/dist/src/init/team/ServerlessSavingsCalculator.js +360 -0
- package/dist/src/init/team/ServerlessSavingsCalculator.js.map +1 -0
- package/dist/src/init/team/TeamRecommender.d.ts +122 -0
- package/dist/src/init/team/TeamRecommender.d.ts.map +1 -0
- package/dist/src/init/team/TeamRecommender.js +405 -0
- package/dist/src/init/team/TeamRecommender.js.map +1 -0
- package/dist/src/init/team/types.d.ts +95 -0
- package/dist/src/init/team/types.d.ts.map +1 -0
- package/dist/src/init/team/types.js +23 -0
- package/dist/src/init/team/types.js.map +1 -0
- package/dist/src/integrations/jira/jira-mapper.d.ts +1 -1
- package/dist/src/integrations/jira/jira-mapper.d.ts.map +1 -1
- package/dist/src/types/cost-tracking.d.ts +1 -1
- package/dist/src/types/cost-tracking.d.ts.map +1 -1
- package/dist/src/utils/cost-reporter.d.ts +2 -2
- package/dist/src/utils/cost-reporter.d.ts.map +1 -1
- package/dist/src/utils/docs-preview/config-generator.d.ts +1 -1
- package/dist/src/utils/docs-preview/config-generator.d.ts.map +1 -1
- package/dist/src/utils/docs-preview/config-generator.js +1 -1
- package/dist/src/utils/docs-preview/docusaurus-setup.d.ts +1 -1
- package/dist/src/utils/docs-preview/docusaurus-setup.d.ts.map +1 -1
- package/dist/src/utils/docs-preview/docusaurus-setup.js +4 -4
- package/dist/src/utils/docs-preview/docusaurus-setup.js.map +1 -1
- package/dist/src/utils/docs-preview/index.d.ts +6 -6
- package/dist/src/utils/docs-preview/index.d.ts.map +1 -1
- package/dist/src/utils/docs-preview/index.js +6 -6
- package/dist/src/utils/docs-preview/index.js.map +1 -1
- package/dist/src/utils/docs-preview/package-installer.d.ts +1 -1
- package/dist/src/utils/docs-preview/package-installer.d.ts.map +1 -1
- package/dist/src/utils/docs-preview/package-installer.js +1 -1
- package/dist/src/utils/docs-preview/package-installer.js.map +1 -1
- package/dist/src/utils/docs-preview/server-manager.d.ts +1 -1
- package/dist/src/utils/docs-preview/server-manager.d.ts.map +1 -1
- package/dist/src/utils/docs-preview/server-manager.js +1 -1
- package/dist/src/utils/docs-preview/server-manager.js.map +1 -1
- package/dist/src/utils/docs-preview/sidebar-builder.d.ts +1 -1
- package/dist/src/utils/docs-preview/sidebar-builder.d.ts.map +1 -1
- package/dist/src/utils/generate-skills-index.d.ts +1 -1
- package/dist/src/utils/generate-skills-index.js +1 -1
- package/dist/src/utils/project-detection.js +1 -1
- package/dist/src/utils/project-detection.js.map +1 -1
- package/package.json +9 -3
- package/plugins/specweave/agents/architect/AGENT.md +605 -0
- package/plugins/specweave/agents/infrastructure/AGENT.md +760 -0
- package/plugins/specweave/agents/pm/AGENT.md +14 -13
- package/plugins/specweave/commands/specweave-check-hooks.md +186 -0
- package/plugins/specweave/commands/specweave-plan.md +151 -0
- package/plugins/specweave/commands/specweave-sync-acs.md +342 -0
- package/plugins/specweave/commands/specweave-validate.md +60 -11
- package/plugins/specweave/hooks/lib/update-status-line.sh +8 -4
- package/plugins/specweave/hooks/post-increment-change.sh +4 -0
- package/plugins/specweave/hooks/post-increment-completion.sh +7 -1
- package/plugins/specweave/hooks/post-increment-planning.sh +4 -0
- package/plugins/specweave/hooks/post-increment-status-change.sh +4 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh +78 -0
- package/plugins/specweave/iac-templates/aws-lambda/README.md.hbs +280 -0
- package/plugins/specweave/iac-templates/aws-lambda/defaults.json +118 -0
- package/plugins/specweave/iac-templates/aws-lambda/environments/dev.defaults.json +46 -0
- package/plugins/specweave/iac-templates/aws-lambda/environments/prod.defaults.json +67 -0
- package/plugins/specweave/iac-templates/aws-lambda/environments/staging.defaults.json +47 -0
- package/plugins/specweave/iac-templates/aws-lambda/main.tf.hbs +241 -0
- package/plugins/specweave/iac-templates/aws-lambda/outputs.tf.hbs +61 -0
- package/plugins/specweave/iac-templates/aws-lambda/provider.tf.hbs +15 -0
- package/plugins/specweave/iac-templates/aws-lambda/variables.tf.hbs +88 -0
- package/plugins/specweave/iac-templates/azure-functions/README.md.hbs +315 -0
- package/plugins/specweave/iac-templates/azure-functions/defaults.json +65 -0
- package/plugins/specweave/iac-templates/azure-functions/environments/dev.defaults.json +30 -0
- package/plugins/specweave/iac-templates/azure-functions/environments/prod.defaults.json +34 -0
- package/plugins/specweave/iac-templates/azure-functions/environments/staging.defaults.json +31 -0
- package/plugins/specweave/iac-templates/azure-functions/iam.tf.hbs +34 -0
- package/plugins/specweave/iac-templates/azure-functions/main.tf.hbs +247 -0
- package/plugins/specweave/iac-templates/azure-functions/outputs.tf.hbs +72 -0
- package/plugins/specweave/iac-templates/azure-functions/provider.tf.hbs +14 -0
- package/plugins/specweave/iac-templates/azure-functions/variables.tf.hbs +64 -0
- package/plugins/specweave/iac-templates/firebase/README.md.hbs +487 -0
- package/plugins/specweave/iac-templates/firebase/defaults.json +55 -0
- package/plugins/specweave/iac-templates/firebase/environments/dev.defaults.json +44 -0
- package/plugins/specweave/iac-templates/firebase/environments/prod.defaults.json +52 -0
- package/plugins/specweave/iac-templates/firebase/environments/staging.defaults.json +43 -0
- package/plugins/specweave/iac-templates/firebase/iam.tf.hbs +75 -0
- package/plugins/specweave/iac-templates/firebase/main.tf.hbs +297 -0
- package/plugins/specweave/iac-templates/firebase/outputs.tf.hbs +67 -0
- package/plugins/specweave/iac-templates/firebase/provider.tf.hbs +26 -0
- package/plugins/specweave/iac-templates/firebase/variables.tf.hbs +68 -0
- package/plugins/specweave/iac-templates/gcp-cloud-functions/README.md.hbs +330 -0
- package/plugins/specweave/iac-templates/gcp-cloud-functions/defaults.json +69 -0
- package/plugins/specweave/iac-templates/gcp-cloud-functions/environments/dev.defaults.json +33 -0
- package/plugins/specweave/iac-templates/gcp-cloud-functions/environments/prod.defaults.json +40 -0
- package/plugins/specweave/iac-templates/gcp-cloud-functions/environments/staging.defaults.json +33 -0
- package/plugins/specweave/iac-templates/gcp-cloud-functions/iam.tf.hbs +54 -0
- package/plugins/specweave/iac-templates/gcp-cloud-functions/main.tf.hbs +211 -0
- package/plugins/specweave/iac-templates/gcp-cloud-functions/outputs.tf.hbs +44 -0
- package/plugins/specweave/iac-templates/gcp-cloud-functions/provider.tf.hbs +14 -0
- package/plugins/specweave/iac-templates/gcp-cloud-functions/variables.tf.hbs +82 -0
- package/plugins/specweave/iac-templates/supabase/README.md.hbs +534 -0
- package/plugins/specweave/iac-templates/supabase/defaults.json +69 -0
- package/plugins/specweave/iac-templates/supabase/environments/dev.defaults.json +55 -0
- package/plugins/specweave/iac-templates/supabase/environments/prod.defaults.json +75 -0
- package/plugins/specweave/iac-templates/supabase/environments/staging.defaults.json +54 -0
- package/plugins/specweave/iac-templates/supabase/iam.tf.hbs +146 -0
- package/plugins/specweave/iac-templates/supabase/main.tf.hbs +310 -0
- package/plugins/specweave/iac-templates/supabase/outputs.tf.hbs +74 -0
- package/plugins/specweave/iac-templates/supabase/provider.tf.hbs +19 -0
- package/plugins/specweave/iac-templates/supabase/variables.tf.hbs +78 -0
- package/plugins/specweave/knowledge-base/serverless/FRESHNESS.md +69 -0
- package/plugins/specweave/knowledge-base/serverless/learning-paths.json +865 -0
- package/plugins/specweave/knowledge-base/serverless/platforms/aws-lambda.json +41 -0
- package/plugins/specweave/knowledge-base/serverless/platforms/azure-functions.json +41 -0
- package/plugins/specweave/knowledge-base/serverless/platforms/firebase.json +46 -0
- package/plugins/specweave/knowledge-base/serverless/platforms/gcp-cloud-functions.json +41 -0
- package/plugins/specweave/knowledge-base/serverless/platforms/supabase.json +41 -0
- package/plugins/specweave/knowledge-base/serverless/schema.json +155 -0
- package/plugins/specweave/lib/hooks/auto-transition.js +50 -0
- package/plugins/specweave/lib/hooks/auto-transition.ts +84 -0
- package/plugins/specweave/lib/hooks/invoke-translator-skill.js +1 -1
- package/plugins/specweave/lib/hooks/invoke-translator-skill.ts +1 -1
- package/plugins/specweave/lib/hooks/sync-living-docs.js +4 -31
- package/plugins/specweave/lib/hooks/{sync-living-docs.ts → sync-living-docs.ts.DISABLED} +9 -48
- package/plugins/specweave/lib/hooks/translate-file.js +1 -1
- package/plugins/specweave/lib/hooks/translate-file.ts +1 -1
- package/plugins/specweave/lib/hooks/update-ac-status.js +24 -75
- package/plugins/specweave/lib/hooks/update-ac-status.ts +46 -135
- package/plugins/specweave/lib/hooks/update-tasks-md.js +115 -3
- package/plugins/specweave/lib/hooks/update-tasks-md.ts +182 -10
- package/plugins/specweave/lib/utils/validate-dev-setup.sh +133 -0
- package/plugins/specweave/skills/increment-planner/SKILL.md +25 -15
- package/plugins/specweave/skills/serverless-recommender/SKILL.md +368 -0
- package/plugins/specweave/templates/iac/aws-lambda/templates/iam.tf.hbs +137 -0
- package/plugins/specweave/templates/iac/aws-lambda/templates/main.tf.hbs +216 -0
- package/plugins/specweave-github/lib/IssueStateManager.js +117 -0
- package/plugins/specweave-github/lib/IssueStateManager.ts +231 -0
- package/plugins/specweave-github/lib/user-story-issue-builder.js +7 -0
- package/plugins/specweave-github/lib/user-story-issue-builder.ts +11 -0
- package/plugins/specweave-ui/.mcp.json +0 -10
- package/plugins/specweave-ui/README.md +26 -26
- package/plugins/specweave-ui/skills/browser-automation/SKILL.md +31 -18
- package/src/templates/tasks.md.template +51 -33
- package/dist/plugins/specweave/lib/hooks/git-diff-analyzer.d.ts +0 -89
- package/dist/plugins/specweave/lib/hooks/git-diff-analyzer.d.ts.map +0 -1
- package/dist/plugins/specweave/lib/hooks/git-diff-analyzer.js +0 -226
- package/dist/plugins/specweave/lib/hooks/git-diff-analyzer.js.map +0 -1
- package/dist/plugins/specweave/lib/hooks/invoke-translator-skill.d.ts +0 -60
- package/dist/plugins/specweave/lib/hooks/invoke-translator-skill.d.ts.map +0 -1
- package/dist/plugins/specweave/lib/hooks/invoke-translator-skill.js +0 -201
- package/dist/plugins/specweave/lib/hooks/invoke-translator-skill.js.map +0 -1
- package/dist/plugins/specweave/lib/hooks/prepare-reflection-context.d.ts +0 -42
- package/dist/plugins/specweave/lib/hooks/prepare-reflection-context.d.ts.map +0 -1
- package/dist/plugins/specweave/lib/hooks/prepare-reflection-context.js +0 -123
- package/dist/plugins/specweave/lib/hooks/prepare-reflection-context.js.map +0 -1
- package/dist/plugins/specweave/lib/hooks/reflection-config-loader.d.ts +0 -45
- package/dist/plugins/specweave/lib/hooks/reflection-config-loader.d.ts.map +0 -1
- package/dist/plugins/specweave/lib/hooks/reflection-config-loader.js +0 -132
- package/dist/plugins/specweave/lib/hooks/reflection-config-loader.js.map +0 -1
- package/dist/plugins/specweave/lib/hooks/reflection-parser.d.ts +0 -33
- package/dist/plugins/specweave/lib/hooks/reflection-parser.d.ts.map +0 -1
- package/dist/plugins/specweave/lib/hooks/reflection-parser.js +0 -419
- package/dist/plugins/specweave/lib/hooks/reflection-parser.js.map +0 -1
- package/dist/plugins/specweave/lib/hooks/reflection-prompt-builder.d.ts +0 -56
- package/dist/plugins/specweave/lib/hooks/reflection-prompt-builder.d.ts.map +0 -1
- package/dist/plugins/specweave/lib/hooks/reflection-prompt-builder.js +0 -239
- package/dist/plugins/specweave/lib/hooks/reflection-prompt-builder.js.map +0 -1
- package/dist/plugins/specweave/lib/hooks/reflection-storage.d.ts +0 -64
- package/dist/plugins/specweave/lib/hooks/reflection-storage.d.ts.map +0 -1
- package/dist/plugins/specweave/lib/hooks/reflection-storage.js +0 -305
- package/dist/plugins/specweave/lib/hooks/reflection-storage.js.map +0 -1
- package/dist/plugins/specweave/lib/hooks/run-self-reflection.d.ts +0 -43
- package/dist/plugins/specweave/lib/hooks/run-self-reflection.d.ts.map +0 -1
- package/dist/plugins/specweave/lib/hooks/run-self-reflection.js +0 -203
- package/dist/plugins/specweave/lib/hooks/run-self-reflection.js.map +0 -1
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.d.ts +0 -32
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.d.ts.map +0 -1
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.js +0 -405
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.js.map +0 -1
- package/dist/plugins/specweave/lib/hooks/translate-file.d.ts +0 -59
- package/dist/plugins/specweave/lib/hooks/translate-file.d.ts.map +0 -1
- package/dist/plugins/specweave/lib/hooks/translate-file.js +0 -350
- package/dist/plugins/specweave/lib/hooks/translate-file.js.map +0 -1
- package/dist/plugins/specweave/lib/hooks/translate-living-docs.d.ts +0 -13
- package/dist/plugins/specweave/lib/hooks/translate-living-docs.d.ts.map +0 -1
- package/dist/plugins/specweave/lib/hooks/translate-living-docs.js +0 -175
- package/dist/plugins/specweave/lib/hooks/translate-living-docs.js.map +0 -1
- package/dist/plugins/specweave/lib/hooks/types/reflection-types.d.ts +0 -164
- package/dist/plugins/specweave/lib/hooks/types/reflection-types.d.ts.map +0 -1
- package/dist/plugins/specweave/lib/hooks/types/reflection-types.js +0 -73
- package/dist/plugins/specweave/lib/hooks/types/reflection-types.js.map +0 -1
- package/dist/plugins/specweave/lib/hooks/update-ac-status.d.ts +0 -21
- package/dist/plugins/specweave/lib/hooks/update-ac-status.d.ts.map +0 -1
- package/dist/plugins/specweave/lib/hooks/update-ac-status.js +0 -162
- package/dist/plugins/specweave/lib/hooks/update-ac-status.js.map +0 -1
- package/dist/plugins/specweave/lib/hooks/update-tasks-md.d.ts +0 -29
- package/dist/plugins/specweave/lib/hooks/update-tasks-md.d.ts.map +0 -1
- package/dist/plugins/specweave/lib/hooks/update-tasks-md.js +0 -203
- package/dist/plugins/specweave/lib/hooks/update-tasks-md.js.map +0 -1
- package/dist/src/core/living-docs/spec-distributor.d.ts +0 -180
- package/dist/src/core/living-docs/spec-distributor.d.ts.map +0 -1
- package/dist/src/core/living-docs/spec-distributor.js +0 -1840
- package/dist/src/core/living-docs/spec-distributor.js.map +0 -1
|
@@ -1,1840 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SpecWeave Spec Distributor
|
|
3
|
-
*
|
|
4
|
-
* Distributes increment specs into hierarchical living docs structure:
|
|
5
|
-
* - Epic (SPEC-###.md) - High-level summary
|
|
6
|
-
* - User Stories (us-###.md) - Detailed requirements
|
|
7
|
-
* - Tasks (tasks.md) - Implementation details (already exists)
|
|
8
|
-
*
|
|
9
|
-
* @author SpecWeave Team
|
|
10
|
-
* @version 2.0.0
|
|
11
|
-
*/
|
|
12
|
-
import fs from 'fs-extra';
|
|
13
|
-
import path from 'path';
|
|
14
|
-
import { HierarchyMapper } from './hierarchy-mapper.js';
|
|
15
|
-
import { detectPrimaryGitHubRemote } from '../../utils/git-detector.js';
|
|
16
|
-
import { ACProjectSpecificGenerator } from './ac-project-specific-generator.js';
|
|
17
|
-
import { TaskProjectSpecificGenerator } from './task-project-specific-generator.js';
|
|
18
|
-
/**
|
|
19
|
-
* SpecDistributor - Distributes increment specs into hierarchical living docs
|
|
20
|
-
*/
|
|
21
|
-
export class SpecDistributor {
|
|
22
|
-
constructor(projectRoot, config) {
|
|
23
|
-
this.githubRemote = null;
|
|
24
|
-
this.projectRoot = projectRoot;
|
|
25
|
-
// Detect project ID from config or use default
|
|
26
|
-
const projectId = config?.specsDir?.includes('/specs/')
|
|
27
|
-
? config.specsDir.split('/specs/')[1]?.split('/')[0] || 'default'
|
|
28
|
-
: 'default';
|
|
29
|
-
this.config = {
|
|
30
|
-
specsDir: path.join(projectRoot, '.specweave', 'docs', 'internal', 'specs', projectId),
|
|
31
|
-
userStoriesSubdir: 'user-stories',
|
|
32
|
-
epicFilePattern: 'SPEC-{id}-{name}.md',
|
|
33
|
-
userStoryFilePattern: 'us-{id}-{name}.md',
|
|
34
|
-
generateFrontmatter: true,
|
|
35
|
-
generateCrossLinks: true,
|
|
36
|
-
preserveOriginal: true,
|
|
37
|
-
overwriteExisting: false,
|
|
38
|
-
createBackups: true,
|
|
39
|
-
...config,
|
|
40
|
-
};
|
|
41
|
-
// Initialize HierarchyMapper
|
|
42
|
-
this.hierarchyMapper = new HierarchyMapper(projectRoot);
|
|
43
|
-
// Initialize AC Generator
|
|
44
|
-
this.acGenerator = new ACProjectSpecificGenerator();
|
|
45
|
-
// ✅ NEW: Initialize Task Generator
|
|
46
|
-
this.taskGenerator = new TaskProjectSpecificGenerator(projectRoot);
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Distribute increment spec into universal hierarchy (epic + feature + user stories)
|
|
50
|
-
*/
|
|
51
|
-
async distribute(incrementId) {
|
|
52
|
-
const errors = [];
|
|
53
|
-
const warnings = [];
|
|
54
|
-
try {
|
|
55
|
-
// Detect GitHub remote for generating GitHub URLs (if not already detected)
|
|
56
|
-
if (!this.githubRemote) {
|
|
57
|
-
this.githubRemote = await detectPrimaryGitHubRemote(this.projectRoot);
|
|
58
|
-
}
|
|
59
|
-
// Step 1: Parse increment spec (with epic and project detection)
|
|
60
|
-
const parsed = await this.parseIncrementSpec(incrementId);
|
|
61
|
-
// Step 1.5: Filter abandoned/archived increments (CRITICAL: prevent pollution)
|
|
62
|
-
if (parsed.status === 'abandoned' || parsed.status === 'archived') {
|
|
63
|
-
console.log(` ⚠️ Skipping distribution for ${parsed.status} increment: ${incrementId}`);
|
|
64
|
-
console.log(` 💡 Living docs are NOT updated for ${parsed.status} increments`);
|
|
65
|
-
return {
|
|
66
|
-
epic: {},
|
|
67
|
-
userStories: [],
|
|
68
|
-
incrementId,
|
|
69
|
-
specId: `${parsed.status.toUpperCase()}-${incrementId}`,
|
|
70
|
-
totalStories: 0,
|
|
71
|
-
totalFiles: 0,
|
|
72
|
-
epicPath: '',
|
|
73
|
-
userStoryPaths: [],
|
|
74
|
-
success: false,
|
|
75
|
-
errors: [`Increment ${incrementId} is ${parsed.status} - skipping living docs distribution`],
|
|
76
|
-
warnings: [`To preserve history, archived content remains in the increment folder`],
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
// Step 2: Check if we should create epics (skip for GitHub)
|
|
80
|
-
const config = await this.hierarchyMapper.getSpecweaveConfig();
|
|
81
|
-
const syncProvider = config.sync?.provider || config.sync?.activeProfile ?
|
|
82
|
-
config.sync.profiles?.[config.sync.activeProfile]?.provider : undefined;
|
|
83
|
-
const shouldCreateEpic = syncProvider !== 'github'; // GitHub doesn't have epics concept
|
|
84
|
-
// Step 3: Detect epic mapping (OPTIONAL - skip for GitHub)
|
|
85
|
-
let epicMapping = null;
|
|
86
|
-
if (shouldCreateEpic) {
|
|
87
|
-
epicMapping = await this.hierarchyMapper.detectEpicMapping(incrementId);
|
|
88
|
-
if (epicMapping) {
|
|
89
|
-
console.log(` 🎯 Mapped to epic ${epicMapping.epicId} (confidence: ${epicMapping.confidence}%)`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
console.log(` ⚡ Skipping epic creation (GitHub integration)`);
|
|
94
|
-
}
|
|
95
|
-
// Step 4: Detect feature mapping (REQUIRED)
|
|
96
|
-
console.log(` 🔍 Detecting feature folder for ${incrementId}...`);
|
|
97
|
-
const featureMapping = await this.hierarchyMapper.detectFeatureMapping(incrementId);
|
|
98
|
-
console.log(` 📁 Mapped to feature ${featureMapping.featureId} (confidence: ${featureMapping.confidence}%, method: ${featureMapping.detectionMethod})`);
|
|
99
|
-
console.log(` 📦 Projects: ${featureMapping.projects.join(', ')}`);
|
|
100
|
-
// Step 5: Classify content by project (NEW)
|
|
101
|
-
const storiesByProject = await this.classifyContentByProject(parsed, featureMapping);
|
|
102
|
-
console.log(` 📊 Classified ${parsed.userStories.length} user stories across ${storiesByProject.size} project(s)`);
|
|
103
|
-
// Step 6: Generate epic file (OPTIONAL - only if not GitHub)
|
|
104
|
-
const epicFile = epicMapping && shouldCreateEpic ? await this.generateEpicFile(parsed, epicMapping, featureMapping) : null;
|
|
105
|
-
// Step 7: Generate feature file (REQUIRED)
|
|
106
|
-
const featureFile = await this.generateFeatureFile(parsed, featureMapping, storiesByProject, incrementId);
|
|
107
|
-
// Step 8: Generate project context files (REQUIRED)
|
|
108
|
-
const projectContextFiles = await this.generateProjectContextFiles(featureMapping, parsed);
|
|
109
|
-
// Step 9: Generate user story files by project (REQUIRED)
|
|
110
|
-
const userStoryFilesByProject = await this.generateUserStoryFilesByProject(storiesByProject, featureMapping, incrementId);
|
|
111
|
-
// Step 10: Write epic file (if exists and not GitHub)
|
|
112
|
-
const epicPath = epicFile && epicMapping && shouldCreateEpic ? await this.writeEpicFile(epicFile, epicMapping) : null;
|
|
113
|
-
// Step 11: Write feature file
|
|
114
|
-
const featurePath = await this.writeFeatureFile(featureFile, featureMapping);
|
|
115
|
-
// Step 12: Write project context files
|
|
116
|
-
const contextPaths = await this.writeProjectContextFiles(projectContextFiles, featureMapping);
|
|
117
|
-
// Step 13: Write user story files by project
|
|
118
|
-
const storyPathsByProject = await this.writeUserStoryFilesByProject(userStoryFilesByProject, featureMapping, incrementId);
|
|
119
|
-
// Step 14: Update tasks.md with bidirectional links (project-aware)
|
|
120
|
-
await this.updateTasksWithUserStoryLinks(incrementId, userStoryFilesByProject, featureMapping);
|
|
121
|
-
// Prepare legacy result (for backward compatibility)
|
|
122
|
-
const allUserStories = Array.from(userStoryFilesByProject.values()).flat();
|
|
123
|
-
const allStoryPaths = Array.from(storyPathsByProject.values()).flat();
|
|
124
|
-
return {
|
|
125
|
-
epic: featureFile, // Type compatibility hack
|
|
126
|
-
userStories: allUserStories,
|
|
127
|
-
incrementId,
|
|
128
|
-
specId: featureFile.id,
|
|
129
|
-
totalStories: allUserStories.length,
|
|
130
|
-
totalFiles: 1 + (epicFile ? 1 : 0) + contextPaths.length + allStoryPaths.length,
|
|
131
|
-
epicPath: featurePath, // Feature is the new "epic"
|
|
132
|
-
userStoryPaths: allStoryPaths,
|
|
133
|
-
success: true,
|
|
134
|
-
errors,
|
|
135
|
-
warnings,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
errors.push(`Distribution failed: ${error}`);
|
|
140
|
-
throw new Error(`Failed to distribute increment ${incrementId}: ${error}`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Parse increment spec into structured data
|
|
145
|
-
*/
|
|
146
|
-
async parseIncrementSpec(incrementId) {
|
|
147
|
-
const specPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId, 'spec.md');
|
|
148
|
-
if (!fs.existsSync(specPath)) {
|
|
149
|
-
throw new Error(`Increment spec not found: ${specPath}`);
|
|
150
|
-
}
|
|
151
|
-
const content = await fs.readFile(specPath, 'utf-8');
|
|
152
|
-
// Load external links from metadata.json (NEW: source of truth for external integrations)
|
|
153
|
-
const externalLinks = await this.loadExternalLinks(incrementId);
|
|
154
|
-
// Extract YAML frontmatter if present
|
|
155
|
-
let frontmatter = {};
|
|
156
|
-
let bodyContent = content;
|
|
157
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
158
|
-
if (frontmatterMatch) {
|
|
159
|
-
try {
|
|
160
|
-
const yaml = await import('yaml');
|
|
161
|
-
frontmatter = yaml.parse(frontmatterMatch[1]);
|
|
162
|
-
bodyContent = content.slice(frontmatterMatch[0].length).trim();
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
console.warn(` ⚠️ Failed to parse frontmatter for ${incrementId}`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
// Extract title (try multiple patterns)
|
|
169
|
-
let title = frontmatter.title || '';
|
|
170
|
-
if (!title) {
|
|
171
|
-
// Pattern 1: # SPEC-####: Title
|
|
172
|
-
const specTitleMatch = bodyContent.match(/^#\s+SPEC-\d+:\s+(.+)$/m);
|
|
173
|
-
if (specTitleMatch)
|
|
174
|
-
title = specTitleMatch[1].trim();
|
|
175
|
-
}
|
|
176
|
-
if (!title) {
|
|
177
|
-
// Pattern 2: # Increment ####: Title
|
|
178
|
-
const incTitleMatch = bodyContent.match(/^#\s+Increment\s+\d+:\s+(.+)$/m);
|
|
179
|
-
if (incTitleMatch)
|
|
180
|
-
title = incTitleMatch[1].trim();
|
|
181
|
-
}
|
|
182
|
-
if (!title) {
|
|
183
|
-
// Pattern 3: First # heading
|
|
184
|
-
const headingMatch = bodyContent.match(/^#\s+(.+)$/m);
|
|
185
|
-
if (headingMatch) {
|
|
186
|
-
title = headingMatch[1]
|
|
187
|
-
.replace(/^SPEC-\d+:\s*/, '')
|
|
188
|
-
.replace(/^Increment\s+\d+:\s*/, '')
|
|
189
|
-
.trim();
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (!title) {
|
|
193
|
-
// Fallback: Use increment ID
|
|
194
|
-
title = incrementId
|
|
195
|
-
.replace(/^\d+-/, '')
|
|
196
|
-
.split('-')
|
|
197
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
198
|
-
.join(' ');
|
|
199
|
-
}
|
|
200
|
-
// Extract overview (try multiple sections)
|
|
201
|
-
let overview = '';
|
|
202
|
-
// Try "Quick Overview" or "Executive Summary"
|
|
203
|
-
let overviewMatch = bodyContent.match(/##\s+(?:Quick\s+)?(?:Overview|Executive\s+Summary)\s*\n+([\s\S]*?)(?=\n\*\*Business Value\*\*:|\n##|\n---|$)/im);
|
|
204
|
-
if (overviewMatch) {
|
|
205
|
-
overview = overviewMatch[1].trim();
|
|
206
|
-
// Remove any trailing fragments or incomplete sentences
|
|
207
|
-
if (overview && !overview.endsWith('.') && !overview.endsWith('!') && !overview.endsWith('?')) {
|
|
208
|
-
// Try to complete the sentence by looking ahead
|
|
209
|
-
const extendedMatch = bodyContent.match(/##\s+(?:Quick\s+)?(?:Overview|Executive\s+Summary)\s*\n+([\s\S]{0,1000}?)(?:\n\n|\n\*\*|\n##|\n---|$)/im);
|
|
210
|
-
if (extendedMatch) {
|
|
211
|
-
const extended = extendedMatch[1].trim();
|
|
212
|
-
// Find the last complete sentence
|
|
213
|
-
const sentences = extended.match(/[^.!?]+[.!?]/g);
|
|
214
|
-
if (sentences) {
|
|
215
|
-
overview = sentences.join('').trim();
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (!overview) {
|
|
221
|
-
// Try "Overview" section
|
|
222
|
-
overviewMatch = bodyContent.match(/##\s+Overview\s*\n+([\s\S]*?)(?=\n##|\n---|$)/im);
|
|
223
|
-
if (overviewMatch) {
|
|
224
|
-
const parts = overviewMatch[1].trim().split(/\n\n|\n---/);
|
|
225
|
-
overview = parts[0].trim();
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (!overview) {
|
|
229
|
-
// Try "Problem Statement" section
|
|
230
|
-
const problemMatch = bodyContent.match(/##\s+Problem\s+Statement\s*\n+([\s\S]*?)(?=\n##|\n---|$)/im);
|
|
231
|
-
if (problemMatch) {
|
|
232
|
-
// Take first paragraph only
|
|
233
|
-
const firstPara = problemMatch[1].trim().split('\n\n')[0];
|
|
234
|
-
overview = firstPara;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (!overview) {
|
|
238
|
-
// Fallback: First paragraph after title
|
|
239
|
-
const firstParaMatch = bodyContent.match(/^#[^\n]+\n+([^\n]+)/);
|
|
240
|
-
if (firstParaMatch)
|
|
241
|
-
overview = firstParaMatch[1].trim();
|
|
242
|
-
}
|
|
243
|
-
// Extract business value
|
|
244
|
-
const businessValue = [];
|
|
245
|
-
const businessValueMatch = content.match(/\*\*Business Value\*\*:\s*\n([\s\S]*?)(?=\n---|\n##|\Z)/i);
|
|
246
|
-
if (businessValueMatch) {
|
|
247
|
-
const lines = businessValueMatch[1].split('\n');
|
|
248
|
-
for (const line of lines) {
|
|
249
|
-
const bulletMatch = line.match(/^[-*]\s+\*\*(.+?)\*\*:\s+(.+)$/);
|
|
250
|
-
if (bulletMatch) {
|
|
251
|
-
businessValue.push(`${bulletMatch[1]}: ${bulletMatch[2]}`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
// Extract user stories
|
|
256
|
-
const userStories = await this.extractUserStories(content, incrementId);
|
|
257
|
-
// Extract priority, status, and created date from frontmatter
|
|
258
|
-
const priority = frontmatter.priority || 'P1';
|
|
259
|
-
const status = frontmatter.status || 'planning';
|
|
260
|
-
const created = frontmatter.created || new Date().toISOString();
|
|
261
|
-
return {
|
|
262
|
-
incrementId,
|
|
263
|
-
title,
|
|
264
|
-
overview,
|
|
265
|
-
businessValue,
|
|
266
|
-
epic: frontmatter.epic, // Epic ID from frontmatter (optional)
|
|
267
|
-
project: frontmatter.project, // Project ID from frontmatter (if present)
|
|
268
|
-
projects: frontmatter.projects || [], // Multiple projects (for cross-project features)
|
|
269
|
-
priority,
|
|
270
|
-
status,
|
|
271
|
-
created,
|
|
272
|
-
userStories,
|
|
273
|
-
externalLinks, // External links from metadata.json
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Load external links from metadata.json (source of truth)
|
|
278
|
-
*/
|
|
279
|
-
async loadExternalLinks(incrementId) {
|
|
280
|
-
const metadataPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId, 'metadata.json');
|
|
281
|
-
const links = {};
|
|
282
|
-
if (!fs.existsSync(metadataPath)) {
|
|
283
|
-
return links;
|
|
284
|
-
}
|
|
285
|
-
try {
|
|
286
|
-
const metadata = JSON.parse(await fs.readFile(metadataPath, 'utf-8'));
|
|
287
|
-
// Extract GitHub link
|
|
288
|
-
if (metadata.github?.url) {
|
|
289
|
-
links.github = metadata.github.url;
|
|
290
|
-
}
|
|
291
|
-
// Extract JIRA link
|
|
292
|
-
if (metadata.jira?.epicKey) {
|
|
293
|
-
links.jira = metadata.jira.epicKey; // Store just the key, can be converted to URL in template
|
|
294
|
-
}
|
|
295
|
-
// Extract Azure DevOps link
|
|
296
|
-
if (metadata.ado?.workItemUrl) {
|
|
297
|
-
links.ado = metadata.ado.workItemUrl;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
catch (error) {
|
|
301
|
-
console.warn(` ⚠️ Failed to parse metadata.json for ${incrementId}: ${error}`);
|
|
302
|
-
}
|
|
303
|
-
return links;
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* Detect external tool mapping from metadata.json
|
|
307
|
-
*
|
|
308
|
-
* Maps SpecWeave hierarchy to external tool hierarchy with clear indicators.
|
|
309
|
-
* Examples:
|
|
310
|
-
* - SpecWeave Feature (FS-031) → JIRA Epic (AUTH-100)
|
|
311
|
-
* - SpecWeave Feature (FS-031) → GitHub Issue (#45)
|
|
312
|
-
* - SpecWeave Feature (FS-031) → ADO Feature (12345)
|
|
313
|
-
*/
|
|
314
|
-
async detectExternalToolMapping(incrementId) {
|
|
315
|
-
const metadataPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId, 'metadata.json');
|
|
316
|
-
if (!fs.existsSync(metadataPath)) {
|
|
317
|
-
return undefined;
|
|
318
|
-
}
|
|
319
|
-
try {
|
|
320
|
-
const metadata = JSON.parse(await fs.readFile(metadataPath, 'utf-8'));
|
|
321
|
-
// Priority order: JIRA > ADO > GitHub
|
|
322
|
-
// (JIRA has most divergence, GitHub has least)
|
|
323
|
-
// JIRA mapping (Epic → Feature divergence)
|
|
324
|
-
if (metadata.jira?.epicKey) {
|
|
325
|
-
return {
|
|
326
|
-
provider: 'jira',
|
|
327
|
-
externalType: 'epic',
|
|
328
|
-
externalId: metadata.jira.epicKey,
|
|
329
|
-
externalUrl: metadata.jira.url || `https://jira.atlassian.com/browse/${metadata.jira.epicKey}`,
|
|
330
|
-
hierarchyLevel: 'feature',
|
|
331
|
-
mappingNote: 'JIRA Epic maps to SpecWeave Feature',
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
// Azure DevOps mapping (Feature → Feature same level)
|
|
335
|
-
if (metadata.ado?.workItemId) {
|
|
336
|
-
return {
|
|
337
|
-
provider: 'ado',
|
|
338
|
-
externalType: 'feature',
|
|
339
|
-
externalId: String(metadata.ado.workItemId),
|
|
340
|
-
externalUrl: metadata.ado.workItemUrl,
|
|
341
|
-
hierarchyLevel: 'feature',
|
|
342
|
-
mappingNote: 'ADO Feature maps to SpecWeave Feature',
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
// GitHub mapping - Features should NOT map to issues
|
|
346
|
-
// According to Universal Hierarchy:
|
|
347
|
-
// - SpecWeave Feature (FS-*) → GitHub Milestone
|
|
348
|
-
// - SpecWeave User Story (US-*) → GitHub Issue
|
|
349
|
-
// We should NOT show GitHub issue mapping at feature level
|
|
350
|
-
// This will be handled at user story level instead
|
|
351
|
-
}
|
|
352
|
-
catch (error) {
|
|
353
|
-
console.warn(` ⚠️ Failed to parse metadata.json for external mapping: ${error}`);
|
|
354
|
-
}
|
|
355
|
-
return undefined;
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Extract user stories from increment spec
|
|
359
|
-
*/
|
|
360
|
-
async extractUserStories(content, incrementId) {
|
|
361
|
-
const userStories = [];
|
|
362
|
-
// Split content into individual user stories
|
|
363
|
-
// Each story runs from "### US-XXX:" or "#### US-XXX:" until the next user story or end of content
|
|
364
|
-
const storyParts = content.split(/(?=^#{3,4}\s+US-\d+:)/m);
|
|
365
|
-
for (const part of storyParts) {
|
|
366
|
-
// Skip empty parts or parts that don't start with US-
|
|
367
|
-
if (!part.trim() || !part.match(/^#{3,4}\s+US-\d+:/))
|
|
368
|
-
continue;
|
|
369
|
-
// Extract the user story ID and title from the first line (supports both ### and ####)
|
|
370
|
-
const headerMatch = part.match(/^#{3,4}\s+(US-\d+):\s+(.+?)$/m);
|
|
371
|
-
if (!headerMatch)
|
|
372
|
-
continue;
|
|
373
|
-
const id = headerMatch[1];
|
|
374
|
-
const title = headerMatch[2];
|
|
375
|
-
// Get everything after the header line, removing trailing ---
|
|
376
|
-
const storyContent = part
|
|
377
|
-
.substring(part.indexOf('\n') + 1)
|
|
378
|
-
.replace(/\n---\s*$/, '')
|
|
379
|
-
.trim();
|
|
380
|
-
// Extract description (As a... I want... So that...) - supports both inline and separate line formats
|
|
381
|
-
const descMatch = storyContent.match(/\*\*As a\*\*\s+(.*?)\n\*\*I want\*\*\s+(.*?)\n\*\*So that\*\*\s+(.*?)(?:\n|$)/is);
|
|
382
|
-
const description = descMatch
|
|
383
|
-
? `**As a** ${descMatch[1].trim()}\n**I want** ${descMatch[2].trim()}\n**So that** ${descMatch[3].trim()}`
|
|
384
|
-
: '';
|
|
385
|
-
// Extract acceptance criteria from the Acceptance Criteria section
|
|
386
|
-
// Try full content first (works for #### headings), then story content (works for ### headings)
|
|
387
|
-
let acceptanceCriteria = this.extractAcceptanceCriteriaFromSection(content, id);
|
|
388
|
-
if (!acceptanceCriteria || acceptanceCriteria.length === 0) {
|
|
389
|
-
acceptanceCriteria = this.extractAcceptanceCriteria(storyContent);
|
|
390
|
-
}
|
|
391
|
-
// Extract business rationale
|
|
392
|
-
const rationaleMatch = storyContent.match(/\*\*Business Rationale\*\*:\s+(.*?)(?=\n\n---|\n\n##|$)/is);
|
|
393
|
-
const businessRationale = rationaleMatch ? rationaleMatch[1].trim() : undefined;
|
|
394
|
-
// Extract phase (look for phase header before this story in the original content)
|
|
395
|
-
const storyIndex = content.indexOf(part);
|
|
396
|
-
const phaseMatch = content.substring(0, storyIndex).match(/###\s+(Phase\s+\d+:.*?)$/im);
|
|
397
|
-
const phase = phaseMatch ? phaseMatch[1] : undefined;
|
|
398
|
-
// Determine status (assume complete if in completed increment)
|
|
399
|
-
const status = 'complete'; // Can be enhanced later
|
|
400
|
-
userStories.push({
|
|
401
|
-
id,
|
|
402
|
-
title,
|
|
403
|
-
description,
|
|
404
|
-
acceptanceCriteria,
|
|
405
|
-
tasks: [], // Will be populated later
|
|
406
|
-
businessRationale,
|
|
407
|
-
status,
|
|
408
|
-
phase,
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
return userStories;
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* Extract acceptance criteria from user story content
|
|
415
|
-
*/
|
|
416
|
-
extractAcceptanceCriteria(content) {
|
|
417
|
-
const criteria = [];
|
|
418
|
-
// Pattern 1: - [x] **AC-US1-01**: Description (P1, testable)
|
|
419
|
-
// Pattern 2: - [ ] **AC-001**: Description (P0, testable)
|
|
420
|
-
// Pattern 3: - AC-001: Description (P0, testable) [without checkbox]
|
|
421
|
-
// Also handles both ** bold ** and plain text formats
|
|
422
|
-
const acPatterns = [
|
|
423
|
-
// With checkbox and bold
|
|
424
|
-
/^[-*]\s+\[([ x])\]\s+\*\*(AC-[^:]+)\*\*:\s+(.+?)(?:\s+\(([^)]+)\))?$/gm,
|
|
425
|
-
// Without checkbox but with bold
|
|
426
|
-
/^[-*]\s+\*\*(AC-[^:]+)\*\*:\s+(.+?)(?:\s+\(([^)]+)\))?$/gm,
|
|
427
|
-
// Without checkbox and without bold (common in specs)
|
|
428
|
-
/^[-*]\s+(AC-\d+):\s+(.+?)(?:\s+\(([^)]+)\))?$/gm,
|
|
429
|
-
];
|
|
430
|
-
// Try each pattern
|
|
431
|
-
for (const pattern of acPatterns) {
|
|
432
|
-
const contentCopy = content; // Work with a copy for each pattern
|
|
433
|
-
pattern.lastIndex = 0; // Reset regex state
|
|
434
|
-
let match;
|
|
435
|
-
while ((match = pattern.exec(contentCopy)) !== null) {
|
|
436
|
-
let completed = false;
|
|
437
|
-
let id = '';
|
|
438
|
-
let description = '';
|
|
439
|
-
let metaString = '';
|
|
440
|
-
// Handle different match groups based on pattern
|
|
441
|
-
if (match.length === 5) {
|
|
442
|
-
// Pattern with checkbox: [1]=checkbox, [2]=id, [3]=desc, [4]=meta
|
|
443
|
-
completed = match[1] === 'x';
|
|
444
|
-
id = match[2];
|
|
445
|
-
description = match[3];
|
|
446
|
-
metaString = match[4] || '';
|
|
447
|
-
}
|
|
448
|
-
else if (match.length === 4) {
|
|
449
|
-
// Pattern without checkbox: [1]=id, [2]=desc, [3]=meta
|
|
450
|
-
completed = false; // Default to not completed
|
|
451
|
-
id = match[1];
|
|
452
|
-
description = match[2];
|
|
453
|
-
metaString = match[3] || '';
|
|
454
|
-
}
|
|
455
|
-
// Skip if already added (avoid duplicates from multiple patterns)
|
|
456
|
-
if (criteria.some(c => c.id === id)) {
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
const priority = metaString.match(/P\d/)?.[0];
|
|
460
|
-
const testable = metaString.includes('testable');
|
|
461
|
-
criteria.push({
|
|
462
|
-
id,
|
|
463
|
-
description,
|
|
464
|
-
priority,
|
|
465
|
-
testable,
|
|
466
|
-
completed,
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
return criteria;
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* Extract acceptance criteria from the full spec content for a specific user story
|
|
474
|
-
* This handles cases where AC is in a separate section after the user story
|
|
475
|
-
*/
|
|
476
|
-
extractAcceptanceCriteriaFromSection(content, userStoryId) {
|
|
477
|
-
const criteria = [];
|
|
478
|
-
// Find the user story section and its acceptance criteria
|
|
479
|
-
const usNumber = userStoryId.replace('US-', '');
|
|
480
|
-
// Try multiple patterns to find AC section (supports both ### and #### headings)
|
|
481
|
-
const patterns = [
|
|
482
|
-
// Pattern 1: AC in "**Acceptance Criteria**:" section (most common - matches spec format)
|
|
483
|
-
// This matches: **Acceptance Criteria**:\n- AC-XXX: description
|
|
484
|
-
new RegExp(`####+?\\s+${userStoryId}:.*?\\n[\\s\\S]*?\\*\\*Acceptance Criteria\\*\\*:\\s*\\n+((?:[-*]\\s+AC-\\d+:[^\\n]+\\n?)+)`, 'im'),
|
|
485
|
-
// Pattern 2: AC with checkboxes format
|
|
486
|
-
new RegExp(`####+?\\s+${userStoryId}:.*?\\n[\\s\\S]*?\\*\\*Acceptance Criteria\\*\\*:\\s*\\n+((?:[-*]\\s+\\[[ x]\\]\\s+\\*\\*AC-[^\\n]+\\n?)+)`, 'im'),
|
|
487
|
-
// Pattern 3: AC directly after user story (no separate section)
|
|
488
|
-
new RegExp(`####+?\\s+${userStoryId}:.*?\\n[\\s\\S]*?(?:\\n\\n|\\n)+([-*]\\s+\\[[ x]\\]\\s+\\*\\*AC-US${usNumber}-\\d+[\\s\\S]*?)(?=\\n\\s*\\*\\*Business|\\n###|\\n---|$)`, 'im'),
|
|
489
|
-
// Pattern 4: AC in a dedicated "### Acceptance Criteria" subsection
|
|
490
|
-
new RegExp(`####+?\\s+${userStoryId}:.*?\\n[\\s\\S]*?####+?\\s+Acceptance Criteria\\s*\\n+([\\s\\S]*?)(?=\\n\\s*\\*\\*Business|\\n###|\\n---|$)`, 'im'),
|
|
491
|
-
];
|
|
492
|
-
let acContent = '';
|
|
493
|
-
for (const pattern of patterns) {
|
|
494
|
-
const match = content.match(pattern);
|
|
495
|
-
if (match && match[1]) {
|
|
496
|
-
acContent = match[1];
|
|
497
|
-
break;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
if (!acContent) {
|
|
501
|
-
return criteria;
|
|
502
|
-
}
|
|
503
|
-
// Multiple patterns to handle different AC formats
|
|
504
|
-
// Pattern 1: - [ ] **AC-USX-XX**: Description (P1, testable)
|
|
505
|
-
// Pattern 2: - [x] **AC-001**: Description (P0, testable)
|
|
506
|
-
// Pattern 3: - AC-001: Description (P0, testable) [without checkbox]
|
|
507
|
-
const acPatterns = [
|
|
508
|
-
// With checkbox and bold (specific user story format)
|
|
509
|
-
/^[-*]\s+\[([ x])\]\s+\*\*(AC-US\d+-\d+)\*\*:\s+(.+?)(?:\s+\(([^)]+)\))?$/gm,
|
|
510
|
-
// With checkbox and bold (general format)
|
|
511
|
-
/^[-*]\s+\[([ x])\]\s+\*\*(AC-\d+)\*\*:\s+(.+?)(?:\s+\(([^)]+)\))?$/gm,
|
|
512
|
-
// Without checkbox but with bold
|
|
513
|
-
/^[-*]\s+\*\*(AC-\d+)\*\*:\s+(.+?)(?:\s+\(([^)]+)\))?$/gm,
|
|
514
|
-
// Without checkbox and without bold (common in specs)
|
|
515
|
-
/^[-*]\s+(AC-\d+):\s+(.+?)(?:\s+\(([^)]+)\))?$/gm,
|
|
516
|
-
];
|
|
517
|
-
// Try each pattern
|
|
518
|
-
for (const pattern of acPatterns) {
|
|
519
|
-
pattern.lastIndex = 0; // Reset regex state
|
|
520
|
-
let match;
|
|
521
|
-
while ((match = pattern.exec(acContent)) !== null) {
|
|
522
|
-
let completed = false;
|
|
523
|
-
let id = '';
|
|
524
|
-
let description = '';
|
|
525
|
-
let metaString = '';
|
|
526
|
-
// Handle different match groups based on pattern
|
|
527
|
-
if (match.length === 5) {
|
|
528
|
-
// Pattern with checkbox: [1]=checkbox, [2]=id, [3]=desc, [4]=meta
|
|
529
|
-
completed = match[1] === 'x';
|
|
530
|
-
id = match[2];
|
|
531
|
-
description = match[3];
|
|
532
|
-
metaString = match[4] || '';
|
|
533
|
-
}
|
|
534
|
-
else if (match.length === 4) {
|
|
535
|
-
// Pattern without checkbox: [1]=id, [2]=desc, [3]=meta
|
|
536
|
-
completed = false; // Default to not completed
|
|
537
|
-
id = match[1];
|
|
538
|
-
description = match[2];
|
|
539
|
-
metaString = match[3] || '';
|
|
540
|
-
}
|
|
541
|
-
// Skip if already added (avoid duplicates from multiple patterns)
|
|
542
|
-
if (criteria.some(c => c.id === id)) {
|
|
543
|
-
continue;
|
|
544
|
-
}
|
|
545
|
-
const priority = metaString.match(/P\d/)?.[0];
|
|
546
|
-
const testable = metaString.includes('testable');
|
|
547
|
-
criteria.push({
|
|
548
|
-
id,
|
|
549
|
-
description,
|
|
550
|
-
priority,
|
|
551
|
-
testable,
|
|
552
|
-
completed,
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
return criteria;
|
|
557
|
-
}
|
|
558
|
-
/**
|
|
559
|
-
* Generate user story files
|
|
560
|
-
*/
|
|
561
|
-
async generateUserStoryFiles(classified, incrementId) {
|
|
562
|
-
const userStoryFiles = [];
|
|
563
|
-
// Load tasks from tasks.md to extract task references
|
|
564
|
-
const taskMap = await this.loadTaskReferences(incrementId);
|
|
565
|
-
for (const userStory of classified.userStories) {
|
|
566
|
-
// Find tasks that implement this user story
|
|
567
|
-
const tasks = this.findTasksForUserStory(userStory.id, taskMap);
|
|
568
|
-
// Find related user stories (same phase)
|
|
569
|
-
const relatedStories = classified.userStories
|
|
570
|
-
.filter((us) => us.id !== userStory.id && us.phase === userStory.phase)
|
|
571
|
-
.map((us) => ({
|
|
572
|
-
id: us.id,
|
|
573
|
-
title: us.title,
|
|
574
|
-
status: us.status,
|
|
575
|
-
phase: us.phase,
|
|
576
|
-
filePath: this.generateUserStoryFilename(us.id, us.title),
|
|
577
|
-
}));
|
|
578
|
-
userStoryFiles.push({
|
|
579
|
-
id: userStory.id,
|
|
580
|
-
epic: classified.epic.id,
|
|
581
|
-
title: userStory.title,
|
|
582
|
-
status: userStory.status,
|
|
583
|
-
priority: userStory.priority,
|
|
584
|
-
created: new Date().toISOString().split('T')[0],
|
|
585
|
-
completed: userStory.status === 'complete' ? new Date().toISOString().split('T')[0] : undefined,
|
|
586
|
-
description: userStory.description,
|
|
587
|
-
acceptanceCriteria: userStory.acceptanceCriteria,
|
|
588
|
-
implementation: {
|
|
589
|
-
increment: incrementId,
|
|
590
|
-
tasks,
|
|
591
|
-
},
|
|
592
|
-
businessRationale: userStory.businessRationale,
|
|
593
|
-
relatedStories,
|
|
594
|
-
phase: userStory.phase,
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
return userStoryFiles;
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* Load task references from tasks.md (with AC-ID extraction)
|
|
601
|
-
*/
|
|
602
|
-
async loadTaskReferences(incrementId) {
|
|
603
|
-
const tasksPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId, 'tasks.md');
|
|
604
|
-
const taskMap = new Map();
|
|
605
|
-
if (!fs.existsSync(tasksPath)) {
|
|
606
|
-
return taskMap;
|
|
607
|
-
}
|
|
608
|
-
const content = await fs.readFile(tasksPath, 'utf-8');
|
|
609
|
-
// Pattern: ### T-001: Task Title followed by **AC**: field
|
|
610
|
-
// Supports both ## and ### headings
|
|
611
|
-
const taskPattern = /^##+ (T-\d+):\s+(.+?)$[\s\S]*?\*\*AC\*\*:\s*([^\n]+)?/gm;
|
|
612
|
-
let match;
|
|
613
|
-
while ((match = taskPattern.exec(content)) !== null) {
|
|
614
|
-
const taskId = match[1]; // T-001
|
|
615
|
-
const taskTitle = match[2];
|
|
616
|
-
const acList = match[3] || ''; // AC-US1-01, AC-US1-02
|
|
617
|
-
const anchor = this.generateTaskAnchor(taskId, taskTitle);
|
|
618
|
-
// Extract AC-IDs from the list
|
|
619
|
-
const acIds = [];
|
|
620
|
-
const acPattern = /AC-US\d+-\d+/g;
|
|
621
|
-
let acMatch;
|
|
622
|
-
while ((acMatch = acPattern.exec(acList)) !== null) {
|
|
623
|
-
acIds.push(acMatch[0]); // AC-US1-01
|
|
624
|
-
}
|
|
625
|
-
taskMap.set(taskId, {
|
|
626
|
-
id: taskId,
|
|
627
|
-
title: taskTitle,
|
|
628
|
-
anchor,
|
|
629
|
-
path: `../../../../../increments/${incrementId}/tasks.md${anchor}`,
|
|
630
|
-
acIds,
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
return taskMap;
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Find tasks that implement a user story (using AC-ID based filtering)
|
|
637
|
-
*/
|
|
638
|
-
findTasksForUserStory(userStoryId, taskMap) {
|
|
639
|
-
const tasks = [];
|
|
640
|
-
// Extract US number from userStoryId (US-001 → "1")
|
|
641
|
-
const usMatch = userStoryId.match(/US-(\d+)/);
|
|
642
|
-
if (!usMatch) {
|
|
643
|
-
return tasks;
|
|
644
|
-
}
|
|
645
|
-
const usNumber = parseInt(usMatch[1], 10); // 1
|
|
646
|
-
// Find tasks that reference this user story's AC-IDs
|
|
647
|
-
for (const task of taskMap.values()) {
|
|
648
|
-
// Check if task has AC-IDs for this user story (AC-US1-01, AC-US1-02, etc.)
|
|
649
|
-
const hasMatchingAC = task.acIds.some((acId) => {
|
|
650
|
-
const acMatch = acId.match(/AC-US(\d+)-\d+/);
|
|
651
|
-
return acMatch && parseInt(acMatch[1], 10) === usNumber;
|
|
652
|
-
});
|
|
653
|
-
if (hasMatchingAC) {
|
|
654
|
-
tasks.push(task);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return tasks;
|
|
658
|
-
}
|
|
659
|
-
/**
|
|
660
|
-
* Generate task anchor
|
|
661
|
-
*/
|
|
662
|
-
generateTaskAnchor(taskId, taskTitle) {
|
|
663
|
-
const slug = taskTitle
|
|
664
|
-
.toLowerCase()
|
|
665
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
666
|
-
.replace(/^-|-$/g, '');
|
|
667
|
-
return `#${taskId.toLowerCase()}-${slug}`;
|
|
668
|
-
}
|
|
669
|
-
/**
|
|
670
|
-
* Generate user story filename
|
|
671
|
-
*/
|
|
672
|
-
generateUserStoryFilename(id, title) {
|
|
673
|
-
const slug = title
|
|
674
|
-
.toLowerCase()
|
|
675
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
676
|
-
.replace(/^-|-$/g, '');
|
|
677
|
-
return `${id.toLowerCase()}-${slug}.md`;
|
|
678
|
-
}
|
|
679
|
-
/**
|
|
680
|
-
* Format epic file as markdown
|
|
681
|
-
*/
|
|
682
|
-
formatEpicFile(epic) {
|
|
683
|
-
const lines = [];
|
|
684
|
-
// Frontmatter
|
|
685
|
-
lines.push('---');
|
|
686
|
-
lines.push(`id: ${epic.id}`);
|
|
687
|
-
lines.push(`title: "${epic.title}"`);
|
|
688
|
-
lines.push(`type: epic`);
|
|
689
|
-
lines.push(`status: ${epic.status}`);
|
|
690
|
-
if (epic.priority)
|
|
691
|
-
lines.push(`priority: ${epic.priority}`);
|
|
692
|
-
lines.push(`created: ${epic.created}`);
|
|
693
|
-
lines.push(`last_updated: ${epic.lastUpdated}`);
|
|
694
|
-
lines.push('---');
|
|
695
|
-
lines.push('');
|
|
696
|
-
// Title
|
|
697
|
-
lines.push(`# ${epic.id}: ${epic.title}`);
|
|
698
|
-
lines.push('');
|
|
699
|
-
lines.push(epic.overview);
|
|
700
|
-
lines.push('');
|
|
701
|
-
// Business Value
|
|
702
|
-
if (epic.businessValue.length > 0) {
|
|
703
|
-
lines.push('**Business Value**:');
|
|
704
|
-
lines.push('');
|
|
705
|
-
for (const value of epic.businessValue) {
|
|
706
|
-
lines.push(`- **${value.split(':')[0]}**: ${value.split(':').slice(1).join(':').trim()}`);
|
|
707
|
-
}
|
|
708
|
-
lines.push('');
|
|
709
|
-
}
|
|
710
|
-
lines.push('---');
|
|
711
|
-
lines.push('');
|
|
712
|
-
// Implementation History
|
|
713
|
-
if (epic.implementationHistory.length > 0) {
|
|
714
|
-
lines.push('## Implementation History');
|
|
715
|
-
lines.push('');
|
|
716
|
-
lines.push('| Increment | User Stories | Status | Completion Date |');
|
|
717
|
-
lines.push('|-----------|--------------|--------|----------------|');
|
|
718
|
-
for (const entry of epic.implementationHistory) {
|
|
719
|
-
const statusEmoji = entry.status === 'complete' ? '✅' : entry.status === 'in-progress' ? '⏳' : '📋';
|
|
720
|
-
// Generate increment link (prefer GitHub URL for deployed version, fallback to relative path)
|
|
721
|
-
let incrementLink;
|
|
722
|
-
if (this.githubRemote && this.githubRemote.owner && this.githubRemote.repo) {
|
|
723
|
-
// GitHub URL (works on deployed version)
|
|
724
|
-
incrementLink = `[${entry.increment}](https://github.com/${this.githubRemote.owner}/${this.githubRemote.repo}/tree/develop/.specweave/increments/${entry.increment})`;
|
|
725
|
-
}
|
|
726
|
-
else {
|
|
727
|
-
// Fallback to relative path (5 levels up, not 4!)
|
|
728
|
-
incrementLink = `[${entry.increment}](../../../../../increments/${entry.increment}/tasks.md)`;
|
|
729
|
-
}
|
|
730
|
-
// Handle empty stories array (no user stories in spec)
|
|
731
|
-
let storiesText;
|
|
732
|
-
if (entry.stories.length === 0) {
|
|
733
|
-
storiesText = 'Implementation only (no user stories)';
|
|
734
|
-
}
|
|
735
|
-
else if (entry.stories.length === 1) {
|
|
736
|
-
storiesText = entry.stories[0];
|
|
737
|
-
}
|
|
738
|
-
else {
|
|
739
|
-
const firstStory = entry.stories[0];
|
|
740
|
-
const lastStory = entry.stories[entry.stories.length - 1];
|
|
741
|
-
storiesText = `${firstStory} through ${lastStory} (all)`;
|
|
742
|
-
}
|
|
743
|
-
lines.push(`| ${incrementLink} | ${storiesText} | ${statusEmoji} ${entry.status.charAt(0).toUpperCase() + entry.status.slice(1)} | ${entry.date || '-'} |`);
|
|
744
|
-
}
|
|
745
|
-
lines.push('');
|
|
746
|
-
// Handle division by zero (no user stories)
|
|
747
|
-
const progressText = epic.totalStories === 0
|
|
748
|
-
? 'No user stories (implementation only)'
|
|
749
|
-
: `${epic.completedStories}/${epic.totalStories} user stories complete (${epic.overallProgress}%)`;
|
|
750
|
-
lines.push(`**Overall Progress**: ${progressText}`);
|
|
751
|
-
lines.push('');
|
|
752
|
-
lines.push('---');
|
|
753
|
-
lines.push('');
|
|
754
|
-
}
|
|
755
|
-
// User Stories
|
|
756
|
-
lines.push('## User Stories');
|
|
757
|
-
lines.push('');
|
|
758
|
-
// Group by phase
|
|
759
|
-
const phases = new Map();
|
|
760
|
-
for (const story of epic.userStories) {
|
|
761
|
-
const phase = story.phase || 'General';
|
|
762
|
-
if (!phases.has(phase)) {
|
|
763
|
-
phases.set(phase, []);
|
|
764
|
-
}
|
|
765
|
-
phases.get(phase).push(story);
|
|
766
|
-
}
|
|
767
|
-
for (const [phase, stories] of phases) {
|
|
768
|
-
if (phase !== 'General') {
|
|
769
|
-
lines.push(`### ${phase}`);
|
|
770
|
-
lines.push('');
|
|
771
|
-
}
|
|
772
|
-
for (const story of stories) {
|
|
773
|
-
const statusEmoji = story.status === 'complete' ? '✅' : story.status === 'in-progress' ? '⏳' : '📋';
|
|
774
|
-
lines.push(`- [${story.id}: ${story.title}](${story.filePath}) - ${statusEmoji} ${story.status.charAt(0).toUpperCase() + story.status.slice(1)}`);
|
|
775
|
-
}
|
|
776
|
-
lines.push('');
|
|
777
|
-
}
|
|
778
|
-
lines.push('---');
|
|
779
|
-
lines.push('');
|
|
780
|
-
// External Tool Integration (only if there are actual links)
|
|
781
|
-
const hasExternalLinks = epic.externalLinks.github || epic.externalLinks.jira || epic.externalLinks.ado;
|
|
782
|
-
if (hasExternalLinks) {
|
|
783
|
-
lines.push('## External Tool Integration');
|
|
784
|
-
lines.push('');
|
|
785
|
-
// Only show tools that have actual links
|
|
786
|
-
if (epic.externalLinks.github) {
|
|
787
|
-
lines.push(`**GitHub Project**: [${epic.externalLinks.github}](${epic.externalLinks.github})`);
|
|
788
|
-
}
|
|
789
|
-
if (epic.externalLinks.jira) {
|
|
790
|
-
// Convert JIRA key to URL (if it's just a key like SCRUM-23)
|
|
791
|
-
const jiraUrl = epic.externalLinks.jira.startsWith('http')
|
|
792
|
-
? epic.externalLinks.jira
|
|
793
|
-
: `https://jira.atlassian.com/browse/${epic.externalLinks.jira}`;
|
|
794
|
-
lines.push(`**JIRA Epic**: [${epic.externalLinks.jira}](${jiraUrl})`);
|
|
795
|
-
}
|
|
796
|
-
if (epic.externalLinks.ado) {
|
|
797
|
-
lines.push(`**Azure DevOps**: [${epic.externalLinks.ado}](${epic.externalLinks.ado})`);
|
|
798
|
-
}
|
|
799
|
-
lines.push('');
|
|
800
|
-
}
|
|
801
|
-
return lines.join('\n');
|
|
802
|
-
}
|
|
803
|
-
/**
|
|
804
|
-
* Format user story file as markdown
|
|
805
|
-
*/
|
|
806
|
-
formatUserStoryFile(userStory) {
|
|
807
|
-
const lines = [];
|
|
808
|
-
// Frontmatter
|
|
809
|
-
lines.push('---');
|
|
810
|
-
lines.push(`id: ${userStory.id}`);
|
|
811
|
-
lines.push(`feature: ${userStory.epic}`); // ✅ FIX: Use 'feature:' not 'epic:' (Universal Hierarchy)
|
|
812
|
-
lines.push(`title: "${userStory.title}"`);
|
|
813
|
-
lines.push(`status: ${userStory.status}`);
|
|
814
|
-
if (userStory.priority)
|
|
815
|
-
lines.push(`priority: ${userStory.priority}`);
|
|
816
|
-
lines.push(`created: ${userStory.created}`);
|
|
817
|
-
if (userStory.completed)
|
|
818
|
-
lines.push(`completed: ${userStory.completed}`);
|
|
819
|
-
// Add external tool mapping to frontmatter
|
|
820
|
-
if (userStory.externalToolMapping) {
|
|
821
|
-
lines.push('externalTool:');
|
|
822
|
-
lines.push(` provider: ${userStory.externalToolMapping.provider}`);
|
|
823
|
-
lines.push(` type: ${userStory.externalToolMapping.externalType}`);
|
|
824
|
-
lines.push(` id: "${userStory.externalToolMapping.externalId}"`);
|
|
825
|
-
lines.push(` url: "${userStory.externalToolMapping.externalUrl}"`);
|
|
826
|
-
}
|
|
827
|
-
lines.push('---');
|
|
828
|
-
lines.push('');
|
|
829
|
-
// Title with external tool indicator
|
|
830
|
-
let titleLine = `# ${userStory.id}: ${userStory.title}`;
|
|
831
|
-
if (userStory.externalToolMapping) {
|
|
832
|
-
const provider = userStory.externalToolMapping.provider.toUpperCase();
|
|
833
|
-
const type = userStory.externalToolMapping.externalType.charAt(0).toUpperCase() +
|
|
834
|
-
userStory.externalToolMapping.externalType.slice(1);
|
|
835
|
-
titleLine += ` (${provider} ${type}: ${userStory.externalToolMapping.externalId})`;
|
|
836
|
-
}
|
|
837
|
-
lines.push(titleLine);
|
|
838
|
-
lines.push('');
|
|
839
|
-
// Feature link (to _features folder)
|
|
840
|
-
// User story is at: .specweave/docs/internal/specs/{project}/{FS-XXX}/us-*.md
|
|
841
|
-
// Feature is at: .specweave/docs/internal/specs/_features/{FS-XXX}/FEATURE.md
|
|
842
|
-
// From {project}/{FS-XXX}/ we need to go up 2 levels to specs/, then into _features/
|
|
843
|
-
// Relative path: ../../_features/{FS-XXX}/FEATURE.md
|
|
844
|
-
const featureRelativePath = `../../_features/${userStory.epic}/FEATURE.md`;
|
|
845
|
-
lines.push(`**Feature**: [${userStory.epic}](${featureRelativePath})`);
|
|
846
|
-
lines.push('');
|
|
847
|
-
// Description
|
|
848
|
-
if (userStory.description) {
|
|
849
|
-
lines.push(userStory.description);
|
|
850
|
-
lines.push('');
|
|
851
|
-
}
|
|
852
|
-
lines.push('---');
|
|
853
|
-
lines.push('');
|
|
854
|
-
// Acceptance Criteria
|
|
855
|
-
lines.push('## Acceptance Criteria');
|
|
856
|
-
lines.push('');
|
|
857
|
-
if (userStory.acceptanceCriteria && userStory.acceptanceCriteria.length > 0) {
|
|
858
|
-
for (const ac of userStory.acceptanceCriteria) {
|
|
859
|
-
const checkbox = ac.completed ? '[x]' : '[ ]';
|
|
860
|
-
const priorityText = ac.priority ? ` (${ac.priority}, testable)` : '';
|
|
861
|
-
lines.push(`- ${checkbox} **${ac.id}**: ${ac.description}${priorityText}`);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
else {
|
|
865
|
-
lines.push('*Acceptance criteria to be extracted from increment specification*');
|
|
866
|
-
}
|
|
867
|
-
lines.push('');
|
|
868
|
-
lines.push('---');
|
|
869
|
-
lines.push('');
|
|
870
|
-
// ✅ NEW: Tasks section with checkboxes (project-specific)
|
|
871
|
-
if (userStory.tasks && userStory.tasks.length > 0) {
|
|
872
|
-
lines.push('## Tasks');
|
|
873
|
-
lines.push('');
|
|
874
|
-
for (const task of userStory.tasks) {
|
|
875
|
-
const checkbox = task.completed ? '[x]' : '[ ]';
|
|
876
|
-
lines.push(`- ${checkbox} **${task.id}**: ${task.title}`);
|
|
877
|
-
}
|
|
878
|
-
lines.push('');
|
|
879
|
-
lines.push('> **Note**: Tasks are project-specific. For the full increment task list, see [increment tasks.md](../../../../../increments/${userStory.implementation.increment}/tasks.md)');
|
|
880
|
-
lines.push('');
|
|
881
|
-
lines.push('---');
|
|
882
|
-
lines.push('');
|
|
883
|
-
}
|
|
884
|
-
// Implementation (source reference)
|
|
885
|
-
lines.push('## Implementation');
|
|
886
|
-
lines.push('');
|
|
887
|
-
const incrementLink = userStory.implementation.tasks[0]?.path.replace(/#.*$/, '') || `../../../../../increments/${userStory.implementation.increment}/tasks.md`;
|
|
888
|
-
lines.push(`**Increment**: [${userStory.implementation.increment}](${incrementLink})`);
|
|
889
|
-
lines.push('');
|
|
890
|
-
if (userStory.implementation.tasks.length > 0) {
|
|
891
|
-
lines.push('**Source Tasks**: See increment tasks.md for complete task breakdown');
|
|
892
|
-
lines.push('');
|
|
893
|
-
}
|
|
894
|
-
// Business Rationale
|
|
895
|
-
if (userStory.businessRationale) {
|
|
896
|
-
lines.push('---');
|
|
897
|
-
lines.push('');
|
|
898
|
-
lines.push('## Business Rationale');
|
|
899
|
-
lines.push('');
|
|
900
|
-
lines.push(userStory.businessRationale);
|
|
901
|
-
lines.push('');
|
|
902
|
-
}
|
|
903
|
-
// Related User Stories
|
|
904
|
-
if (userStory.relatedStories.length > 0) {
|
|
905
|
-
lines.push('---');
|
|
906
|
-
lines.push('');
|
|
907
|
-
lines.push('## Related User Stories');
|
|
908
|
-
lines.push('');
|
|
909
|
-
for (const related of userStory.relatedStories) {
|
|
910
|
-
lines.push(`- [${related.id}: ${related.title}](${related.filePath})`);
|
|
911
|
-
}
|
|
912
|
-
lines.push('');
|
|
913
|
-
}
|
|
914
|
-
lines.push('---');
|
|
915
|
-
lines.push('');
|
|
916
|
-
lines.push(`**Status**: ${userStory.status === 'complete' ? '✅' : '⏳'} ${userStory.status.charAt(0).toUpperCase() + userStory.status.slice(1)}`);
|
|
917
|
-
if (userStory.completed) {
|
|
918
|
-
lines.push(`**Completed**: ${userStory.completed}`);
|
|
919
|
-
}
|
|
920
|
-
lines.push('');
|
|
921
|
-
return lines.join('\n');
|
|
922
|
-
}
|
|
923
|
-
/**
|
|
924
|
-
* Update tasks.md with bidirectional links to user stories (CRITICAL!)
|
|
925
|
-
*
|
|
926
|
-
* This creates bidirectional traceability:
|
|
927
|
-
|
|
928
|
-
/**
|
|
929
|
-
* Classify content by project (NEW for universal hierarchy)
|
|
930
|
-
* Split user stories across projects based on keywords and frontmatter
|
|
931
|
-
*/
|
|
932
|
-
async classifyContentByProject(parsed, featureMapping) {
|
|
933
|
-
const storiesByProject = new Map();
|
|
934
|
-
for (const story of parsed.userStories) {
|
|
935
|
-
// Detect which project(s) this story belongs to
|
|
936
|
-
const storyProjects = await this.detectUserStoryProjects(story, featureMapping.projects);
|
|
937
|
-
for (const project of storyProjects) {
|
|
938
|
-
if (!storiesByProject.has(project)) {
|
|
939
|
-
storiesByProject.set(project, []);
|
|
940
|
-
}
|
|
941
|
-
storiesByProject.get(project).push(story);
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
// If no stories were classified to any project, add all to default/first project
|
|
945
|
-
if (storiesByProject.size === 0 && parsed.userStories.length > 0) {
|
|
946
|
-
const defaultProject = featureMapping.projects[0] || 'default';
|
|
947
|
-
storiesByProject.set(defaultProject, parsed.userStories);
|
|
948
|
-
}
|
|
949
|
-
return storiesByProject;
|
|
950
|
-
}
|
|
951
|
-
/**
|
|
952
|
-
* Detect which projects a user story belongs to
|
|
953
|
-
*/
|
|
954
|
-
async detectUserStoryProjects(story, availableProjects) {
|
|
955
|
-
// If only one project available, return it
|
|
956
|
-
if (availableProjects.length === 1) {
|
|
957
|
-
return availableProjects;
|
|
958
|
-
}
|
|
959
|
-
const projects = [];
|
|
960
|
-
const config = await this.hierarchyMapper.getSpecweaveConfig();
|
|
961
|
-
// Method 1: Check if story has explicit project field
|
|
962
|
-
if (story.project) {
|
|
963
|
-
if (availableProjects.includes(story.project)) {
|
|
964
|
-
return [story.project];
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
// Method 2: Keyword detection in title and description
|
|
968
|
-
const text = `${story.title} ${story.description}`.toLowerCase();
|
|
969
|
-
for (const projectId of availableProjects) {
|
|
970
|
-
if (projectId === 'default') {
|
|
971
|
-
continue; // Skip default for keyword matching
|
|
972
|
-
}
|
|
973
|
-
// Get project config for keywords
|
|
974
|
-
const projectConfig = config.multiProject?.projects?.[projectId];
|
|
975
|
-
if (projectConfig?.keywords) {
|
|
976
|
-
const hasKeyword = projectConfig.keywords.some(keyword => text.includes(keyword.toLowerCase()));
|
|
977
|
-
if (hasKeyword) {
|
|
978
|
-
projects.push(projectId);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
// Also check if project name is mentioned
|
|
982
|
-
if (text.includes(projectId.toLowerCase())) {
|
|
983
|
-
if (!projects.includes(projectId)) {
|
|
984
|
-
projects.push(projectId);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
// Method 3: Fallback - if no projects detected, assign to all
|
|
989
|
-
if (projects.length === 0) {
|
|
990
|
-
return availableProjects;
|
|
991
|
-
}
|
|
992
|
-
return projects;
|
|
993
|
-
}
|
|
994
|
-
/**
|
|
995
|
-
* Generate epic file (OPTIONAL - for strategic themes)
|
|
996
|
-
*/
|
|
997
|
-
async generateEpicFile(parsed, epicMapping, featureMapping) {
|
|
998
|
-
if (!epicMapping)
|
|
999
|
-
return null;
|
|
1000
|
-
return {
|
|
1001
|
-
id: epicMapping.epicId,
|
|
1002
|
-
title: parsed.title,
|
|
1003
|
-
type: 'epic',
|
|
1004
|
-
status: parsed.status || 'in-progress',
|
|
1005
|
-
created: parsed.created || new Date().toISOString(),
|
|
1006
|
-
lastUpdated: new Date().toISOString(),
|
|
1007
|
-
strategicOverview: parsed.overview,
|
|
1008
|
-
features: [featureMapping.featureId],
|
|
1009
|
-
successMetrics: [],
|
|
1010
|
-
timeline: {
|
|
1011
|
-
start: parsed.created || new Date().toISOString(),
|
|
1012
|
-
targetCompletion: 'TBD',
|
|
1013
|
-
currentPhase: 'Phase 1',
|
|
1014
|
-
},
|
|
1015
|
-
stakeholders: {},
|
|
1016
|
-
externalLinks: parsed.externalLinks || {},
|
|
1017
|
-
};
|
|
1018
|
-
}
|
|
1019
|
-
/**
|
|
1020
|
-
* Generate feature file (REQUIRED - cross-project overview)
|
|
1021
|
-
*/
|
|
1022
|
-
async generateFeatureFile(parsed, featureMapping, storiesByProject, incrementId) {
|
|
1023
|
-
// Convert stories to summaries grouped by project
|
|
1024
|
-
const userStoriesByProject = new Map();
|
|
1025
|
-
for (const [project, stories] of storiesByProject.entries()) {
|
|
1026
|
-
const summaries = stories.map(s => ({
|
|
1027
|
-
id: s.id,
|
|
1028
|
-
title: s.title,
|
|
1029
|
-
status: s.status,
|
|
1030
|
-
phase: s.phase,
|
|
1031
|
-
filePath: `../../${project}/${featureMapping.featureFolder}/${this.generateUserStoryFilename(s.id, s.title)}`,
|
|
1032
|
-
}));
|
|
1033
|
-
userStoriesByProject.set(project, summaries);
|
|
1034
|
-
}
|
|
1035
|
-
// Count completed stories
|
|
1036
|
-
let completedStories = 0;
|
|
1037
|
-
for (const stories of storiesByProject.values()) {
|
|
1038
|
-
completedStories += stories.filter(s => s.status === 'complete').length;
|
|
1039
|
-
}
|
|
1040
|
-
// Detect external tool mapping
|
|
1041
|
-
const externalToolMapping = incrementId ? await this.detectExternalToolMapping(incrementId) : undefined;
|
|
1042
|
-
return {
|
|
1043
|
-
id: featureMapping.featureId,
|
|
1044
|
-
title: parsed.title,
|
|
1045
|
-
type: 'feature',
|
|
1046
|
-
status: parsed.status || 'in-progress',
|
|
1047
|
-
priority: parsed.priority || 'P1',
|
|
1048
|
-
created: parsed.created || new Date().toISOString(),
|
|
1049
|
-
lastUpdated: new Date().toISOString(),
|
|
1050
|
-
epic: featureMapping.epic,
|
|
1051
|
-
projects: featureMapping.projects,
|
|
1052
|
-
overview: parsed.overview,
|
|
1053
|
-
businessValue: parsed.businessValue || [],
|
|
1054
|
-
implementationHistory: [],
|
|
1055
|
-
userStoriesByProject,
|
|
1056
|
-
externalLinks: parsed.externalLinks || {},
|
|
1057
|
-
totalStories: parsed.userStories.length,
|
|
1058
|
-
completedStories,
|
|
1059
|
-
overallProgress: parsed.userStories.length > 0
|
|
1060
|
-
? Math.round((completedStories / parsed.userStories.length) * 100)
|
|
1061
|
-
: 0,
|
|
1062
|
-
sourceIncrement: incrementId, // Track the source increment
|
|
1063
|
-
externalToolMapping, // Include external tool mapping
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
|
-
/**
|
|
1067
|
-
* Generate project context files (README.md for each project)
|
|
1068
|
-
*/
|
|
1069
|
-
async generateProjectContextFiles(featureMapping, parsed) {
|
|
1070
|
-
const contextFiles = new Map();
|
|
1071
|
-
for (const project of featureMapping.projects) {
|
|
1072
|
-
const projectContext = await this.hierarchyMapper.getProjectContext(project);
|
|
1073
|
-
if (!projectContext)
|
|
1074
|
-
continue;
|
|
1075
|
-
const content = this.formatProjectContextFile(featureMapping, projectContext, parsed);
|
|
1076
|
-
contextFiles.set(project, content);
|
|
1077
|
-
}
|
|
1078
|
-
return contextFiles;
|
|
1079
|
-
}
|
|
1080
|
-
/**
|
|
1081
|
-
* Format project context file (README.md)
|
|
1082
|
-
* IMPORTANT: This README is created for ALL increments, even without user stories!
|
|
1083
|
-
*/
|
|
1084
|
-
formatProjectContextFile(featureMapping, projectContext, parsed) {
|
|
1085
|
-
const featurePathUp = featureMapping.projects.length > 1 ? '../../../' : '../../';
|
|
1086
|
-
// Determine if this increment has user stories
|
|
1087
|
-
const hasUserStories = parsed.userStories && parsed.userStories.length > 0;
|
|
1088
|
-
const statusNote = parsed.status === 'abandoned' ? 'abandoned' :
|
|
1089
|
-
parsed.status === 'complete' ? 'complete' : 'in-progress';
|
|
1090
|
-
return `---
|
|
1091
|
-
id: ${featureMapping.featureId}-${projectContext.projectId}
|
|
1092
|
-
title: "${parsed.title} - ${projectContext.projectName} Implementation"
|
|
1093
|
-
feature: ${featureMapping.featureId}
|
|
1094
|
-
project: ${projectContext.projectId}
|
|
1095
|
-
type: feature-context
|
|
1096
|
-
status: ${statusNote}
|
|
1097
|
-
sourceIncrement: ${parsed.incrementId}
|
|
1098
|
-
---
|
|
1099
|
-
|
|
1100
|
-
# ${projectContext.projectName} Implementation: ${parsed.title}
|
|
1101
|
-
|
|
1102
|
-
**Feature**: [${featureMapping.featureId}](${featurePathUp}_features/${featureMapping.featureFolder}/FEATURE.md)
|
|
1103
|
-
|
|
1104
|
-
## Overview
|
|
1105
|
-
|
|
1106
|
-
${parsed.overview}
|
|
1107
|
-
|
|
1108
|
-
## ${projectContext.projectName}-Specific Context
|
|
1109
|
-
|
|
1110
|
-
This document contains the ${projectContext.projectName} implementation details for the ${parsed.title} feature.
|
|
1111
|
-
|
|
1112
|
-
## Tech Stack
|
|
1113
|
-
|
|
1114
|
-
${projectContext.techStack.map(t => `- ${t}`).join('\n')}
|
|
1115
|
-
|
|
1116
|
-
## User Stories (${projectContext.projectName})
|
|
1117
|
-
|
|
1118
|
-
${hasUserStories ? 'User stories for this project are listed below.' : `_This increment has no user stories. See [FEATURE.md](${featurePathUp}_features/${featureMapping.featureFolder}/FEATURE.md) for overview and implementation details._`}
|
|
1119
|
-
|
|
1120
|
-
## Dependencies
|
|
1121
|
-
|
|
1122
|
-
[Project-specific dependencies will be documented here]
|
|
1123
|
-
|
|
1124
|
-
## Architecture Considerations
|
|
1125
|
-
|
|
1126
|
-
[${projectContext.projectName}-specific architecture notes]
|
|
1127
|
-
|
|
1128
|
-
---
|
|
1129
|
-
|
|
1130
|
-
**Source**: [Increment ${parsed.incrementId}](../../../../../increments/${parsed.incrementId})
|
|
1131
|
-
`;
|
|
1132
|
-
}
|
|
1133
|
-
/**
|
|
1134
|
-
* Generate user story files by project
|
|
1135
|
-
*/
|
|
1136
|
-
async generateUserStoryFilesByProject(storiesByProject, featureMapping, incrementId) {
|
|
1137
|
-
const filesByProject = new Map();
|
|
1138
|
-
// Load tasks for linking (LEGACY - for backward compatibility)
|
|
1139
|
-
const taskMap = await this.loadTaskReferences(incrementId);
|
|
1140
|
-
for (const [project, stories] of storiesByProject.entries()) {
|
|
1141
|
-
const userStoryFiles = [];
|
|
1142
|
-
// Get project context for AC and task transformation
|
|
1143
|
-
const rawProjectContext = await this.hierarchyMapper.getProjectContext(project);
|
|
1144
|
-
for (const userStory of stories) {
|
|
1145
|
-
// LEGACY: Find tasks that implement this user story (for backward compatibility)
|
|
1146
|
-
const taskReferences = this.findTasksForUserStory(userStory.id, taskMap);
|
|
1147
|
-
// ✅ NEW: Generate project-specific tasks with completion status
|
|
1148
|
-
const projectSpecificTasks = await this.taskGenerator.generateProjectSpecificTasks(incrementId, userStory.id, rawProjectContext ? {
|
|
1149
|
-
id: rawProjectContext.projectId,
|
|
1150
|
-
name: rawProjectContext.projectName,
|
|
1151
|
-
type: this.detectProjectType(rawProjectContext),
|
|
1152
|
-
techStack: rawProjectContext.techStack,
|
|
1153
|
-
keywords: rawProjectContext.keywords,
|
|
1154
|
-
} : undefined);
|
|
1155
|
-
// Find related user stories (same project, same phase)
|
|
1156
|
-
const relatedStories = stories
|
|
1157
|
-
.filter((us) => us.id !== userStory.id && us.phase === userStory.phase)
|
|
1158
|
-
.map((us) => ({
|
|
1159
|
-
id: us.id,
|
|
1160
|
-
title: us.title,
|
|
1161
|
-
status: us.status,
|
|
1162
|
-
phase: us.phase,
|
|
1163
|
-
filePath: this.generateUserStoryFilename(us.id, us.title),
|
|
1164
|
-
}));
|
|
1165
|
-
// ✨ Apply project-specific AC transformation
|
|
1166
|
-
let projectSpecificACs = userStory.acceptanceCriteria;
|
|
1167
|
-
if (rawProjectContext) {
|
|
1168
|
-
// Map ProjectContext to AC generator's expected format
|
|
1169
|
-
const acGeneratorContext = {
|
|
1170
|
-
id: rawProjectContext.projectId,
|
|
1171
|
-
name: rawProjectContext.projectName,
|
|
1172
|
-
type: this.detectProjectType(rawProjectContext),
|
|
1173
|
-
techStack: rawProjectContext.techStack,
|
|
1174
|
-
keywords: rawProjectContext.keywords,
|
|
1175
|
-
};
|
|
1176
|
-
projectSpecificACs = this.acGenerator.makeProjectSpecific(userStory.acceptanceCriteria, userStory.id, acGeneratorContext);
|
|
1177
|
-
}
|
|
1178
|
-
userStoryFiles.push({
|
|
1179
|
-
id: userStory.id,
|
|
1180
|
-
epic: featureMapping.featureId, // Feature is the parent
|
|
1181
|
-
title: userStory.title,
|
|
1182
|
-
status: userStory.status,
|
|
1183
|
-
priority: userStory.priority,
|
|
1184
|
-
created: new Date().toISOString().split('T')[0],
|
|
1185
|
-
completed: userStory.status === 'complete' ? new Date().toISOString().split('T')[0] : undefined,
|
|
1186
|
-
description: userStory.description,
|
|
1187
|
-
acceptanceCriteria: projectSpecificACs, // ✅ Use project-specific AC
|
|
1188
|
-
tasks: projectSpecificTasks, // ✅ NEW: Project-specific tasks with completion status
|
|
1189
|
-
implementation: {
|
|
1190
|
-
increment: incrementId,
|
|
1191
|
-
tasks: taskReferences, // LEGACY: Keep for backward compatibility
|
|
1192
|
-
},
|
|
1193
|
-
businessRationale: userStory.businessRationale,
|
|
1194
|
-
relatedStories,
|
|
1195
|
-
phase: userStory.phase,
|
|
1196
|
-
project, // Add project field
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
filesByProject.set(project, userStoryFiles);
|
|
1200
|
-
}
|
|
1201
|
-
return filesByProject;
|
|
1202
|
-
}
|
|
1203
|
-
/**
|
|
1204
|
-
* Write epic file to _epics/ folder
|
|
1205
|
-
*/
|
|
1206
|
-
async writeEpicFile(epic, epicMapping) {
|
|
1207
|
-
if (!epic || !epicMapping)
|
|
1208
|
-
return null;
|
|
1209
|
-
const epicPath = path.join(epicMapping.epicPath, 'EPIC.md');
|
|
1210
|
-
const content = this.formatEpicThemeFile(epic);
|
|
1211
|
-
await fs.ensureDir(path.dirname(epicPath));
|
|
1212
|
-
await fs.writeFile(epicPath, content, 'utf-8');
|
|
1213
|
-
console.log(` ✅ Written epic overview to _epics/${epicMapping.epicFolder}/EPIC.md`);
|
|
1214
|
-
return epicPath;
|
|
1215
|
-
}
|
|
1216
|
-
/**
|
|
1217
|
-
* Format epic theme file content
|
|
1218
|
-
*/
|
|
1219
|
-
formatEpicThemeFile(epic) {
|
|
1220
|
-
const yaml = `---
|
|
1221
|
-
id: ${epic.id}
|
|
1222
|
-
title: "${epic.title}"
|
|
1223
|
-
type: ${epic.type}
|
|
1224
|
-
status: ${epic.status}
|
|
1225
|
-
---`;
|
|
1226
|
-
return `${yaml}
|
|
1227
|
-
|
|
1228
|
-
# ${epic.title}
|
|
1229
|
-
|
|
1230
|
-
## Strategic Overview
|
|
1231
|
-
|
|
1232
|
-
${epic.strategicOverview}
|
|
1233
|
-
|
|
1234
|
-
## Features
|
|
1235
|
-
|
|
1236
|
-
${epic.features.map(f => `- ${f}`).join('\n')}
|
|
1237
|
-
|
|
1238
|
-
## Timeline
|
|
1239
|
-
|
|
1240
|
-
- **Start**: ${epic.timeline.start}
|
|
1241
|
-
- **Target Completion**: ${epic.timeline.targetCompletion}
|
|
1242
|
-
- **Current Phase**: ${epic.timeline.currentPhase}
|
|
1243
|
-
`;
|
|
1244
|
-
}
|
|
1245
|
-
/**
|
|
1246
|
-
* Write feature file to _features/ folder
|
|
1247
|
-
*/
|
|
1248
|
-
async writeFeatureFile(feature, featureMapping) {
|
|
1249
|
-
const featurePath = path.join(featureMapping.featurePath, 'FEATURE.md');
|
|
1250
|
-
const content = this.formatFeatureFile(feature);
|
|
1251
|
-
await fs.ensureDir(path.dirname(featurePath));
|
|
1252
|
-
await fs.writeFile(featurePath, content, 'utf-8');
|
|
1253
|
-
console.log(` ✅ Written feature overview to _features/${featureMapping.featureFolder}/FEATURE.md`);
|
|
1254
|
-
return featurePath;
|
|
1255
|
-
}
|
|
1256
|
-
/**
|
|
1257
|
-
* Format feature file content
|
|
1258
|
-
*/
|
|
1259
|
-
formatFeatureFile(feature) {
|
|
1260
|
-
// Build YAML frontmatter with external tool mapping
|
|
1261
|
-
let yaml = `---
|
|
1262
|
-
id: ${feature.id}
|
|
1263
|
-
title: "${feature.title}"
|
|
1264
|
-
type: ${feature.type}
|
|
1265
|
-
status: ${feature.status}
|
|
1266
|
-
priority: ${feature.priority}
|
|
1267
|
-
created: ${feature.created}
|
|
1268
|
-
lastUpdated: ${feature.lastUpdated}
|
|
1269
|
-
projects: [${feature.projects.map(p => `"${p}"`).join(', ')}]
|
|
1270
|
-
${feature.epic ? `epic: ${feature.epic}` : ''}
|
|
1271
|
-
${feature.sourceIncrement ? `sourceIncrement: ${feature.sourceIncrement}` : ''}`;
|
|
1272
|
-
// Add external tool mapping to frontmatter
|
|
1273
|
-
if (feature.externalToolMapping) {
|
|
1274
|
-
yaml += `
|
|
1275
|
-
externalTool:
|
|
1276
|
-
provider: ${feature.externalToolMapping.provider}
|
|
1277
|
-
type: ${feature.externalToolMapping.externalType}
|
|
1278
|
-
id: "${feature.externalToolMapping.externalId}"
|
|
1279
|
-
url: "${feature.externalToolMapping.externalUrl}"`;
|
|
1280
|
-
}
|
|
1281
|
-
yaml += '\n---';
|
|
1282
|
-
const storiesByProjectSection = Array.from(feature.userStoriesByProject.entries())
|
|
1283
|
-
.map(([project, stories]) => `
|
|
1284
|
-
### ${project}
|
|
1285
|
-
|
|
1286
|
-
${stories.map(s => `- [${s.id}: ${s.title}](${s.filePath}) - ${s.status}`).join('\n')}`)
|
|
1287
|
-
.join('\n');
|
|
1288
|
-
// Build title with external tool indicator
|
|
1289
|
-
let titleLine = `# ${feature.title}`;
|
|
1290
|
-
if (feature.externalToolMapping) {
|
|
1291
|
-
const provider = feature.externalToolMapping.provider.toUpperCase();
|
|
1292
|
-
const type = feature.externalToolMapping.externalType.charAt(0).toUpperCase() +
|
|
1293
|
-
feature.externalToolMapping.externalType.slice(1);
|
|
1294
|
-
titleLine += ` (${provider} ${type}: ${feature.externalToolMapping.externalId})`;
|
|
1295
|
-
}
|
|
1296
|
-
// Add external tool mapping section
|
|
1297
|
-
let externalMappingSection = '';
|
|
1298
|
-
if (feature.externalToolMapping) {
|
|
1299
|
-
externalMappingSection = `
|
|
1300
|
-
## External Tool Mapping
|
|
1301
|
-
|
|
1302
|
-
**Mapped from**: ${feature.externalToolMapping.provider.toUpperCase()} ${feature.externalToolMapping.externalType.charAt(0).toUpperCase() + feature.externalToolMapping.externalType.slice(1)} [${feature.externalToolMapping.externalId}](${feature.externalToolMapping.externalUrl})
|
|
1303
|
-
|
|
1304
|
-
> **Note**: ${feature.externalToolMapping.mappingNote}
|
|
1305
|
-
|
|
1306
|
-
This SpecWeave Feature corresponds to a ${feature.externalToolMapping.externalType} in ${feature.externalToolMapping.provider.toUpperCase()}. The hierarchy mapping is:
|
|
1307
|
-
- **SpecWeave**: Feature (${feature.id})
|
|
1308
|
-
- **${feature.externalToolMapping.provider.toUpperCase()}**: ${feature.externalToolMapping.externalType.charAt(0).toUpperCase() + feature.externalToolMapping.externalType.slice(1)} (${feature.externalToolMapping.externalId})
|
|
1309
|
-
|
|
1310
|
-
`;
|
|
1311
|
-
}
|
|
1312
|
-
// Add source section based on where the feature came from
|
|
1313
|
-
let sourceSection = '';
|
|
1314
|
-
if (feature.sourceIncrement) {
|
|
1315
|
-
sourceSection = `
|
|
1316
|
-
## Source
|
|
1317
|
-
|
|
1318
|
-
This feature was created from increment: [\`${feature.sourceIncrement}\`](../../../../../increments/${feature.sourceIncrement})
|
|
1319
|
-
`;
|
|
1320
|
-
}
|
|
1321
|
-
else if (feature.externalLinks?.github || feature.externalLinks?.jira || feature.externalLinks?.ado) {
|
|
1322
|
-
sourceSection = `
|
|
1323
|
-
## Source
|
|
1324
|
-
|
|
1325
|
-
This feature was imported from external tool:
|
|
1326
|
-
${feature.externalLinks?.github ? `- GitHub: ${feature.externalLinks.github}` : ''}
|
|
1327
|
-
${feature.externalLinks?.jira ? `- JIRA: ${feature.externalLinks.jira}` : ''}
|
|
1328
|
-
${feature.externalLinks?.ado ? `- Azure DevOps: ${feature.externalLinks.ado}` : ''}
|
|
1329
|
-
|
|
1330
|
-
Note: No explicit feature was specified during import.
|
|
1331
|
-
`;
|
|
1332
|
-
}
|
|
1333
|
-
return `${yaml}
|
|
1334
|
-
|
|
1335
|
-
${titleLine}
|
|
1336
|
-
${externalMappingSection}
|
|
1337
|
-
## Overview
|
|
1338
|
-
|
|
1339
|
-
${feature.overview}
|
|
1340
|
-
${sourceSection}
|
|
1341
|
-
## Business Value
|
|
1342
|
-
|
|
1343
|
-
${feature.businessValue.map(v => `- ${v}`).join('\n') || 'See overview above'}
|
|
1344
|
-
|
|
1345
|
-
## Projects
|
|
1346
|
-
|
|
1347
|
-
This feature spans the following projects:
|
|
1348
|
-
${feature.projects.map(p => `- ${p}`).join('\n')}
|
|
1349
|
-
|
|
1350
|
-
## User Stories by Project
|
|
1351
|
-
${storiesByProjectSection}
|
|
1352
|
-
|
|
1353
|
-
## Progress
|
|
1354
|
-
|
|
1355
|
-
- **Total Stories**: ${feature.totalStories}
|
|
1356
|
-
- **Completed**: ${feature.completedStories}
|
|
1357
|
-
- **Progress**: ${feature.overallProgress}%
|
|
1358
|
-
`;
|
|
1359
|
-
}
|
|
1360
|
-
/**
|
|
1361
|
-
* Write project context files (README.md for each project)
|
|
1362
|
-
* IMPORTANT: Always create README.md even if increment has no user stories!
|
|
1363
|
-
* This ensures every increment is represented in living docs, preventing gaps.
|
|
1364
|
-
*/
|
|
1365
|
-
async writeProjectContextFiles(contextFiles, featureMapping) {
|
|
1366
|
-
const writtenPaths = [];
|
|
1367
|
-
// Write README.md for EVERY project, even if no user stories exist
|
|
1368
|
-
for (const [project, content] of contextFiles.entries()) {
|
|
1369
|
-
const projectPath = featureMapping.projectPaths.get(project);
|
|
1370
|
-
if (!projectPath) {
|
|
1371
|
-
console.warn(` ⚠️ No project path found for ${project}, skipping README`);
|
|
1372
|
-
continue;
|
|
1373
|
-
}
|
|
1374
|
-
// Ensure directory exists
|
|
1375
|
-
await fs.ensureDir(projectPath);
|
|
1376
|
-
// Write README.md
|
|
1377
|
-
const readmePath = path.join(projectPath, 'README.md');
|
|
1378
|
-
await fs.writeFile(readmePath, content, 'utf-8');
|
|
1379
|
-
writtenPaths.push(readmePath);
|
|
1380
|
-
}
|
|
1381
|
-
if (writtenPaths.length > 0) {
|
|
1382
|
-
console.log(` ✅ Written README.md to ${writtenPaths.length} project folder(s)`);
|
|
1383
|
-
}
|
|
1384
|
-
return writtenPaths;
|
|
1385
|
-
}
|
|
1386
|
-
/**
|
|
1387
|
-
* Write user story files by project
|
|
1388
|
-
* IMPORTANT: Ensures project folders exist even if no user stories!
|
|
1389
|
-
*/
|
|
1390
|
-
async writeUserStoryFilesByProject(userStoryFilesByProject, featureMapping, incrementId) {
|
|
1391
|
-
const pathsByProject = new Map();
|
|
1392
|
-
// CRITICAL FIX: Ensure ALL project folders exist, even without user stories
|
|
1393
|
-
for (const project of featureMapping.projects) {
|
|
1394
|
-
const projectPath = featureMapping.projectPaths.get(project);
|
|
1395
|
-
if (!projectPath) {
|
|
1396
|
-
console.warn(` ⚠️ No project path found for ${project}, skipping`);
|
|
1397
|
-
continue;
|
|
1398
|
-
}
|
|
1399
|
-
// Ensure directory exists (even if no stories to write)
|
|
1400
|
-
await fs.ensureDir(projectPath);
|
|
1401
|
-
const stories = userStoryFilesByProject.get(project) || [];
|
|
1402
|
-
const paths = [];
|
|
1403
|
-
// Write user story files (if any exist)
|
|
1404
|
-
for (const story of stories) {
|
|
1405
|
-
const filename = this.generateUserStoryFilename(story.id, story.title);
|
|
1406
|
-
const filePath = path.join(projectPath, filename);
|
|
1407
|
-
const content = this.formatUserStoryFile(story);
|
|
1408
|
-
await fs.writeFile(filePath, content, 'utf-8');
|
|
1409
|
-
paths.push(filePath);
|
|
1410
|
-
}
|
|
1411
|
-
pathsByProject.set(project, paths);
|
|
1412
|
-
}
|
|
1413
|
-
const totalStories = Array.from(userStoryFilesByProject.values())
|
|
1414
|
-
.reduce((sum, stories) => sum + stories.length, 0);
|
|
1415
|
-
if (totalStories > 0) {
|
|
1416
|
-
console.log(` ✅ Written ${totalStories} user stories to ${featureMapping.projects.length} project(s)`);
|
|
1417
|
-
}
|
|
1418
|
-
else {
|
|
1419
|
-
console.log(` ℹ️ No user stories to write, but created ${featureMapping.projects.length} project folder(s)`);
|
|
1420
|
-
}
|
|
1421
|
-
return pathsByProject;
|
|
1422
|
-
}
|
|
1423
|
-
/**
|
|
1424
|
-
* Update tasks.md with bidirectional links (project-aware)
|
|
1425
|
-
*/
|
|
1426
|
-
async updateTasksWithUserStoryLinks(incrementId, userStoryFilesByProject, featureMapping) {
|
|
1427
|
-
const tasksPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId, 'tasks.md');
|
|
1428
|
-
if (!fs.existsSync(tasksPath)) {
|
|
1429
|
-
console.warn(` ⚠️ tasks.md not found for ${incrementId}, skipping bidirectional linking`);
|
|
1430
|
-
return;
|
|
1431
|
-
}
|
|
1432
|
-
let content = await fs.readFile(tasksPath, 'utf-8');
|
|
1433
|
-
// Build a map of all user stories across all projects
|
|
1434
|
-
const allUserStories = new Map();
|
|
1435
|
-
for (const [project, stories] of userStoryFilesByProject.entries()) {
|
|
1436
|
-
for (const story of stories) {
|
|
1437
|
-
allUserStories.set(story.id, { story, project });
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
// Pattern to find task headings and their AC fields
|
|
1441
|
-
const taskSections = content.split(/(?=^##+ T-\d+:)/m);
|
|
1442
|
-
const updatedSections = [];
|
|
1443
|
-
for (const section of taskSections) {
|
|
1444
|
-
if (!section.trim()) {
|
|
1445
|
-
updatedSections.push(section);
|
|
1446
|
-
continue;
|
|
1447
|
-
}
|
|
1448
|
-
// Extract task ID and AC list
|
|
1449
|
-
const taskMatch = section.match(/^(##+ T-\d+:.+?)$/m);
|
|
1450
|
-
const acMatch = section.match(/\*\*AC\*\*:\s*([^\n]+)/);
|
|
1451
|
-
if (!taskMatch || !acMatch) {
|
|
1452
|
-
updatedSections.push(section);
|
|
1453
|
-
continue;
|
|
1454
|
-
}
|
|
1455
|
-
const taskHeading = taskMatch[1];
|
|
1456
|
-
const acList = acMatch[1];
|
|
1457
|
-
// Find which user story this task belongs to based on AC-IDs
|
|
1458
|
-
let linkedStory = null;
|
|
1459
|
-
const acPattern = /AC-US(\d+)-\d+/g;
|
|
1460
|
-
let acIdMatch;
|
|
1461
|
-
while ((acIdMatch = acPattern.exec(acList)) !== null) {
|
|
1462
|
-
const usNumber = acIdMatch[1];
|
|
1463
|
-
const usId = `US-${usNumber.padStart(3, '0')}`;
|
|
1464
|
-
if (allUserStories.has(usId)) {
|
|
1465
|
-
linkedStory = allUserStories.get(usId);
|
|
1466
|
-
break;
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
if (linkedStory) {
|
|
1470
|
-
const { story, project } = linkedStory;
|
|
1471
|
-
// Use the FS-XXX feature ID from featureMapping, not featureFolder
|
|
1472
|
-
const newRelativePath = `../../docs/internal/specs/${project}/${featureMapping.featureId}/${this.generateUserStoryFilename(story.id, story.title)}`;
|
|
1473
|
-
const newLinkLine = `**User Story**: [${story.id}: ${story.title}](${newRelativePath})`;
|
|
1474
|
-
// Check for existing user story links (old or new format)
|
|
1475
|
-
const userStoryLinkPattern = /\*\*User Story\*\*:.*/g;
|
|
1476
|
-
const existingLinks = section.match(userStoryLinkPattern);
|
|
1477
|
-
if (!existingLinks || existingLinks.length === 0) {
|
|
1478
|
-
// No link exists - add new one
|
|
1479
|
-
const lines = section.split('\n');
|
|
1480
|
-
const headingIndex = lines.findIndex(line => line.match(/^##+ T-\d+:/));
|
|
1481
|
-
if (headingIndex >= 0) {
|
|
1482
|
-
lines.splice(headingIndex + 1, 0, '', newLinkLine);
|
|
1483
|
-
updatedSections.push(lines.join('\n'));
|
|
1484
|
-
}
|
|
1485
|
-
else {
|
|
1486
|
-
updatedSections.push(section);
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
else {
|
|
1490
|
-
// Link(s) exist - update them and remove duplicates/blank lines
|
|
1491
|
-
const lines = section.split('\n');
|
|
1492
|
-
let foundFirst = false;
|
|
1493
|
-
const cleanedLines = [];
|
|
1494
|
-
let consecutiveBlankLines = 0;
|
|
1495
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1496
|
-
const line = lines[i];
|
|
1497
|
-
// Handle user story links
|
|
1498
|
-
if (line.includes('**User Story**:')) {
|
|
1499
|
-
if (!foundFirst) {
|
|
1500
|
-
foundFirst = true;
|
|
1501
|
-
cleanedLines.push(newLinkLine); // Replace with updated link
|
|
1502
|
-
consecutiveBlankLines = 0;
|
|
1503
|
-
}
|
|
1504
|
-
// Skip duplicate links
|
|
1505
|
-
continue;
|
|
1506
|
-
}
|
|
1507
|
-
// Handle blank lines (keep max 1 consecutive)
|
|
1508
|
-
if (line.trim() === '') {
|
|
1509
|
-
consecutiveBlankLines++;
|
|
1510
|
-
if (consecutiveBlankLines <= 1) {
|
|
1511
|
-
cleanedLines.push(line);
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
else {
|
|
1515
|
-
consecutiveBlankLines = 0;
|
|
1516
|
-
cleanedLines.push(line);
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
updatedSections.push(cleanedLines.join('\n'));
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
else {
|
|
1523
|
-
updatedSections.push(section);
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
const updatedContent = updatedSections.join('');
|
|
1527
|
-
if (content !== updatedContent) {
|
|
1528
|
-
await fs.writeFile(tasksPath, updatedContent, 'utf-8');
|
|
1529
|
-
console.log(` ✅ Updated tasks.md with bidirectional user story links`);
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
/**
|
|
1533
|
-
* Update acceptance criteria status in user stories based on completed tasks
|
|
1534
|
-
* This method synchronizes AC checkboxes with task completion status
|
|
1535
|
-
*/
|
|
1536
|
-
async updateAcceptanceCriteriaStatus(incrementId) {
|
|
1537
|
-
console.log(`\n📊 Updating acceptance criteria status for increment: ${incrementId}`);
|
|
1538
|
-
const incrementPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId);
|
|
1539
|
-
const tasksPath = path.join(incrementPath, 'tasks.md');
|
|
1540
|
-
const specPath = path.join(incrementPath, 'spec.md');
|
|
1541
|
-
if (!await fs.pathExists(tasksPath)) {
|
|
1542
|
-
console.log(` ⚠️ No tasks.md found, skipping AC status update`);
|
|
1543
|
-
return;
|
|
1544
|
-
}
|
|
1545
|
-
const tasksContent = await fs.readFile(tasksPath, 'utf-8');
|
|
1546
|
-
// Parse completed tasks and their AC references
|
|
1547
|
-
const completedACs = this.extractCompletedAcceptanceCriteria(tasksContent);
|
|
1548
|
-
if (completedACs.size === 0) {
|
|
1549
|
-
console.log(` ℹ️ No completed acceptance criteria found`);
|
|
1550
|
-
return;
|
|
1551
|
-
}
|
|
1552
|
-
console.log(` 📝 Found ${completedACs.size} user stories with completed acceptance criteria`);
|
|
1553
|
-
// Try to detect the feature folder this increment maps to
|
|
1554
|
-
let featureId = null;
|
|
1555
|
-
if (await fs.pathExists(specPath)) {
|
|
1556
|
-
const specContent = await fs.readFile(specPath, 'utf-8');
|
|
1557
|
-
// Look for feature mapping in frontmatter or content
|
|
1558
|
-
const featureMatch = specContent.match(/feature:\s*(FS-[^\s\n]+)/i);
|
|
1559
|
-
if (featureMatch) {
|
|
1560
|
-
const declaredFeature = featureMatch[1];
|
|
1561
|
-
// Try to find the actual feature folder that exists
|
|
1562
|
-
featureId = await this.findActualFeatureFolder(incrementId, declaredFeature);
|
|
1563
|
-
console.log(` 📁 Feature mapping: ${declaredFeature} → ${featureId || 'not found'}`);
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
// Update user story files with completed ACs
|
|
1567
|
-
await this.updateUserStoryACStatus(completedACs, incrementId, featureId);
|
|
1568
|
-
}
|
|
1569
|
-
/**
|
|
1570
|
-
* Extract completed acceptance criteria from tasks
|
|
1571
|
-
*/
|
|
1572
|
-
extractCompletedAcceptanceCriteria(tasksContent) {
|
|
1573
|
-
const completedACs = new Map(); // US-ID -> Set of AC-IDs
|
|
1574
|
-
// Split into task sections
|
|
1575
|
-
const taskSections = tasksContent.split(/^#{2,3}\s+T-\d+:/m);
|
|
1576
|
-
for (const section of taskSections) {
|
|
1577
|
-
if (!section.trim())
|
|
1578
|
-
continue;
|
|
1579
|
-
// Check if task is completed
|
|
1580
|
-
const statusMatch = section.match(/\*\*Status\*\*:\s*\[x\]/i);
|
|
1581
|
-
if (!statusMatch)
|
|
1582
|
-
continue; // Skip incomplete tasks
|
|
1583
|
-
// Extract AC field
|
|
1584
|
-
const acMatch = section.match(/\*\*AC\*\*:\s*([^\n]+)/);
|
|
1585
|
-
if (!acMatch)
|
|
1586
|
-
continue;
|
|
1587
|
-
// Parse AC-IDs
|
|
1588
|
-
const acIds = acMatch[1].split(/[,\s]+/).filter(id => id.match(/^AC-/));
|
|
1589
|
-
for (const acId of acIds) {
|
|
1590
|
-
// Determine user story from AC-ID
|
|
1591
|
-
// Patterns: AC-US1-01 -> US-001, AC-001 -> US-001 (assume single story)
|
|
1592
|
-
let userStoryId = '';
|
|
1593
|
-
const usMatch = acId.match(/AC-US(\d+)-/);
|
|
1594
|
-
if (usMatch) {
|
|
1595
|
-
userStoryId = `US-${usMatch[1].padStart(3, '0')}`;
|
|
1596
|
-
}
|
|
1597
|
-
else {
|
|
1598
|
-
// For generic AC-XXX format, try to infer from task's user story link
|
|
1599
|
-
const usLinkMatch = section.match(/\[US-(\d+):/);
|
|
1600
|
-
if (usLinkMatch) {
|
|
1601
|
-
userStoryId = `US-${usLinkMatch[1].padStart(3, '0')}`;
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
if (userStoryId) {
|
|
1605
|
-
if (!completedACs.has(userStoryId)) {
|
|
1606
|
-
completedACs.set(userStoryId, new Set());
|
|
1607
|
-
}
|
|
1608
|
-
completedACs.get(userStoryId).add(acId);
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
return completedACs;
|
|
1613
|
-
}
|
|
1614
|
-
/**
|
|
1615
|
-
* Update user story files with completed AC status
|
|
1616
|
-
*/
|
|
1617
|
-
async updateUserStoryACStatus(completedACs, incrementId, featureId = null) {
|
|
1618
|
-
const specsPath = path.join(this.projectRoot, '.specweave', 'docs', 'internal', 'specs');
|
|
1619
|
-
// Find all user story files
|
|
1620
|
-
const projects = await this.getProjectFolders();
|
|
1621
|
-
let updatedCount = 0;
|
|
1622
|
-
for (const project of projects) {
|
|
1623
|
-
const projectSpecsPath = path.join(specsPath, project);
|
|
1624
|
-
if (!await fs.pathExists(projectSpecsPath))
|
|
1625
|
-
continue;
|
|
1626
|
-
// Find all feature folders
|
|
1627
|
-
const entries = await fs.readdir(projectSpecsPath, { withFileTypes: true });
|
|
1628
|
-
let featureFolders = entries.filter(e => e.isDirectory() && e.name.startsWith('FS-'));
|
|
1629
|
-
// If we have a specific feature ID, only look in that folder
|
|
1630
|
-
if (featureId) {
|
|
1631
|
-
featureFolders = featureFolders.filter(e => e.name === featureId || e.name.includes(featureId));
|
|
1632
|
-
if (featureFolders.length === 0) {
|
|
1633
|
-
console.log(` ⚠️ Feature folder ${featureId} not found in ${project}`);
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
for (const folder of featureFolders) {
|
|
1637
|
-
const featurePath = path.join(projectSpecsPath, folder.name);
|
|
1638
|
-
// Find all user story files
|
|
1639
|
-
const files = await fs.readdir(featurePath);
|
|
1640
|
-
const userStoryFiles = files.filter(f => f.match(/^us-\d+-.+\.md$/));
|
|
1641
|
-
for (const file of userStoryFiles) {
|
|
1642
|
-
const filePath = path.join(featurePath, file);
|
|
1643
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
1644
|
-
// Extract user story ID from content or filename
|
|
1645
|
-
const idMatch = content.match(/^id:\s*(US-\d+)/m) || file.match(/us-(\d+)/);
|
|
1646
|
-
if (!idMatch)
|
|
1647
|
-
continue;
|
|
1648
|
-
const userStoryId = idMatch[1].startsWith('US') ? idMatch[1] : `US-${idMatch[1].padStart(3, '0')}`;
|
|
1649
|
-
const acsToComplete = completedACs.get(userStoryId);
|
|
1650
|
-
if (!acsToComplete || acsToComplete.size === 0)
|
|
1651
|
-
continue;
|
|
1652
|
-
// Update AC checkboxes
|
|
1653
|
-
let updatedContent = content;
|
|
1654
|
-
let hasUpdates = false;
|
|
1655
|
-
for (const acId of acsToComplete) {
|
|
1656
|
-
// Find and update the AC checkbox
|
|
1657
|
-
// Pattern: - [ ] **AC-XXX**: Description
|
|
1658
|
-
const acPattern = new RegExp(`^(\\s*-\\s*)\\[\\s\\](\\s*\\*\\*${acId}\\*\\*:.*)$`, 'gm');
|
|
1659
|
-
// Check if pattern exists before replacing
|
|
1660
|
-
const originalContent = updatedContent;
|
|
1661
|
-
updatedContent = updatedContent.replace(acPattern, '$1[x]$2');
|
|
1662
|
-
// Check if replacement actually happened
|
|
1663
|
-
if (originalContent !== updatedContent) {
|
|
1664
|
-
hasUpdates = true;
|
|
1665
|
-
console.log(` ✅ Updated ${acId} in ${userStoryId} (${project}/${folder.name})`);
|
|
1666
|
-
}
|
|
1667
|
-
else {
|
|
1668
|
-
// Debug: pattern didn't match
|
|
1669
|
-
console.log(` ⚠️ Could not find ${acId} in ${userStoryId} (${project}/${folder.name})`);
|
|
1670
|
-
// Try to find what format the AC actually has
|
|
1671
|
-
const lineWithAc = updatedContent.split('\n').find(line => line.includes(acId));
|
|
1672
|
-
if (lineWithAc) {
|
|
1673
|
-
console.log(` Found line: "${lineWithAc}"`);
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
if (hasUpdates) {
|
|
1678
|
-
await fs.writeFile(filePath, updatedContent, 'utf-8');
|
|
1679
|
-
updatedCount++;
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
if (updatedCount > 0) {
|
|
1685
|
-
console.log(` ✅ Updated ${updatedCount} user story file(s) with completed acceptance criteria`);
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
/**
|
|
1689
|
-
* Get all project folders from specs
|
|
1690
|
-
*/
|
|
1691
|
-
async getProjectFolders() {
|
|
1692
|
-
const specsPath = path.join(this.projectRoot, '.specweave', 'docs', 'internal', 'specs');
|
|
1693
|
-
if (!await fs.pathExists(specsPath)) {
|
|
1694
|
-
return ['default'];
|
|
1695
|
-
}
|
|
1696
|
-
const entries = await fs.readdir(specsPath, { withFileTypes: true });
|
|
1697
|
-
const projectFolders = entries
|
|
1698
|
-
.filter(e => e.isDirectory() && !e.name.startsWith('_'))
|
|
1699
|
-
.map(e => e.name);
|
|
1700
|
-
return projectFolders.length > 0 ? projectFolders : ['default'];
|
|
1701
|
-
}
|
|
1702
|
-
/**
|
|
1703
|
-
* Detect project type for AC generation (backend, frontend, mobile, infrastructure, generic)
|
|
1704
|
-
*/
|
|
1705
|
-
detectProjectType(context) {
|
|
1706
|
-
const projectId = context.projectId.toLowerCase();
|
|
1707
|
-
const keywords = context.keywords.map(k => k.toLowerCase());
|
|
1708
|
-
const techStack = context.techStack.map(t => t.toLowerCase());
|
|
1709
|
-
// Direct match on project ID
|
|
1710
|
-
if (projectId.includes('backend') || projectId.includes('api') || projectId.includes('service')) {
|
|
1711
|
-
return 'backend';
|
|
1712
|
-
}
|
|
1713
|
-
if (projectId.includes('frontend') || projectId.includes('web') || projectId.includes('ui')) {
|
|
1714
|
-
return 'frontend';
|
|
1715
|
-
}
|
|
1716
|
-
if (projectId.includes('mobile') || projectId.includes('ios') || projectId.includes('android')) {
|
|
1717
|
-
return 'mobile';
|
|
1718
|
-
}
|
|
1719
|
-
if (projectId.includes('infra') || projectId.includes('devops') || projectId.includes('deployment')) {
|
|
1720
|
-
return 'infrastructure';
|
|
1721
|
-
}
|
|
1722
|
-
// Keyword-based detection
|
|
1723
|
-
const backendKeywords = ['api', 'backend', 'service', 'server', 'database'];
|
|
1724
|
-
const frontendKeywords = ['frontend', 'ui', 'component', 'react', 'web'];
|
|
1725
|
-
const mobileKeywords = ['mobile', 'ios', 'android', 'react-native'];
|
|
1726
|
-
const infraKeywords = ['infrastructure', 'devops', 'deployment', 'ci/cd', 'kubernetes'];
|
|
1727
|
-
if (keywords.some(k => backendKeywords.includes(k)))
|
|
1728
|
-
return 'backend';
|
|
1729
|
-
if (keywords.some(k => frontendKeywords.includes(k)))
|
|
1730
|
-
return 'frontend';
|
|
1731
|
-
if (keywords.some(k => mobileKeywords.includes(k)))
|
|
1732
|
-
return 'mobile';
|
|
1733
|
-
if (keywords.some(k => infraKeywords.includes(k)))
|
|
1734
|
-
return 'infrastructure';
|
|
1735
|
-
// Tech stack-based detection
|
|
1736
|
-
const backendTech = ['node.js', 'express', 'postgresql', 'mongodb', 'redis'];
|
|
1737
|
-
const frontendTech = ['react', 'next.js', 'vue', 'angular', 'typescript'];
|
|
1738
|
-
const mobileTech = ['react native', 'swift', 'kotlin', 'flutter'];
|
|
1739
|
-
const infraTech = ['docker', 'kubernetes', 'terraform', 'ansible'];
|
|
1740
|
-
if (techStack.some(t => backendTech.some(bt => t.includes(bt))))
|
|
1741
|
-
return 'backend';
|
|
1742
|
-
if (techStack.some(t => frontendTech.some(ft => t.includes(ft))))
|
|
1743
|
-
return 'frontend';
|
|
1744
|
-
if (techStack.some(t => mobileTech.some(mt => t.includes(mt))))
|
|
1745
|
-
return 'mobile';
|
|
1746
|
-
if (techStack.some(t => infraTech.some(it => t.includes(it))))
|
|
1747
|
-
return 'infrastructure';
|
|
1748
|
-
// Default to generic if no match
|
|
1749
|
-
return 'generic';
|
|
1750
|
-
}
|
|
1751
|
-
/**
|
|
1752
|
-
* Find the actual feature folder that matches an increment
|
|
1753
|
-
* Handles mapping between different naming conventions
|
|
1754
|
-
*/
|
|
1755
|
-
async findActualFeatureFolder(incrementId, declaredFeature) {
|
|
1756
|
-
const specsPath = path.join(this.projectRoot, '.specweave', 'docs', 'internal', 'specs');
|
|
1757
|
-
const projects = await this.getProjectFolders();
|
|
1758
|
-
// Extract increment number
|
|
1759
|
-
const incrementNum = incrementId.match(/^(\d+)/)?.[1];
|
|
1760
|
-
for (const project of projects) {
|
|
1761
|
-
const projectPath = path.join(specsPath, project);
|
|
1762
|
-
if (!await fs.pathExists(projectPath))
|
|
1763
|
-
continue;
|
|
1764
|
-
const entries = await fs.readdir(projectPath);
|
|
1765
|
-
const featureFolders = entries.filter(e => e.startsWith('FS-'));
|
|
1766
|
-
// Strategy 1: Direct match
|
|
1767
|
-
if (featureFolders.includes(declaredFeature)) {
|
|
1768
|
-
return declaredFeature;
|
|
1769
|
-
}
|
|
1770
|
-
// Strategy 2: Match by increment number (FS-031 for increment 0031)
|
|
1771
|
-
if (incrementNum) {
|
|
1772
|
-
const paddedNum = incrementNum.padStart(3, '0');
|
|
1773
|
-
const numericFeature = `FS-${paddedNum}`;
|
|
1774
|
-
if (featureFolders.includes(numericFeature)) {
|
|
1775
|
-
return numericFeature;
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
// Strategy 3: Match by suffix (e.g., FS-XXX-external-tool-sync matches external-tool-sync)
|
|
1779
|
-
const incrementSuffix = incrementId.replace(/^\d+-/, '');
|
|
1780
|
-
for (const folder of featureFolders) {
|
|
1781
|
-
if (folder.includes(incrementSuffix)) {
|
|
1782
|
-
return folder;
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
// Strategy 4: Match by date pattern
|
|
1786
|
-
if (declaredFeature.match(/^FS-\d{2}-\d{2}-\d{2}-/)) {
|
|
1787
|
-
// Find folder with same date prefix
|
|
1788
|
-
const datePrefix = declaredFeature.match(/^FS-\d{2}-\d{2}-\d{2}/)?.[0];
|
|
1789
|
-
for (const folder of featureFolders) {
|
|
1790
|
-
if (folder.startsWith(datePrefix)) {
|
|
1791
|
-
return folder;
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
// Strategy 5: Smart content-based matching
|
|
1796
|
-
// Look for feature folders containing user stories that reference this increment
|
|
1797
|
-
for (const folder of featureFolders) {
|
|
1798
|
-
const featurePath = path.join(projectPath, folder);
|
|
1799
|
-
const files = await fs.readdir(featurePath);
|
|
1800
|
-
const userStoryFiles = files.filter(f => f.match(/^us-\d+-.+\.md$/));
|
|
1801
|
-
for (const file of userStoryFiles) {
|
|
1802
|
-
const filePath = path.join(featurePath, file);
|
|
1803
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
1804
|
-
// Check if this user story references our increment
|
|
1805
|
-
if (content.includes(`increments/${incrementId}/`) ||
|
|
1806
|
-
content.includes(`Increment**: [${incrementId}]`)) {
|
|
1807
|
-
console.log(` 🎯 Found feature folder ${folder} by content match in ${file}`);
|
|
1808
|
-
return folder;
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
// Strategy 6: Fuzzy match on increment name parts
|
|
1813
|
-
// Split increment name into words and look for feature folders with matching words
|
|
1814
|
-
const incrementWords = incrementId.toLowerCase().split(/[-_]/);
|
|
1815
|
-
let bestMatch = null;
|
|
1816
|
-
for (const folder of featureFolders) {
|
|
1817
|
-
const folderWords = folder.toLowerCase().split(/[-_]/);
|
|
1818
|
-
let matchScore = 0;
|
|
1819
|
-
// Count matching words (ignoring numbers and 'FS' prefix)
|
|
1820
|
-
for (const word of incrementWords) {
|
|
1821
|
-
if (word.match(/^\d+$/) || word === 'fs')
|
|
1822
|
-
continue;
|
|
1823
|
-
if (folderWords.includes(word)) {
|
|
1824
|
-
matchScore++;
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
// Update best match if this score is better
|
|
1828
|
-
if (matchScore > 0 && (!bestMatch || matchScore > bestMatch.score)) {
|
|
1829
|
-
bestMatch = { folder, score: matchScore };
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
if (bestMatch && bestMatch.score >= 2) { // Require at least 2 word matches
|
|
1833
|
-
console.log(` 🎯 Found feature folder ${bestMatch.folder} by fuzzy match (score: ${bestMatch.score})`);
|
|
1834
|
-
return bestMatch.folder;
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
return null;
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
//# sourceMappingURL=spec-distributor.js.map
|