specweave 0.18.1 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +229 -1817
- package/README.md +68 -0
- package/bin/specweave.js +62 -6
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.d.ts.map +1 -1
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.js +3 -0
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.js.map +1 -1
- package/dist/plugins/specweave/lib/hooks/update-ac-status.d.ts +21 -0
- package/dist/plugins/specweave/lib/hooks/update-ac-status.d.ts.map +1 -0
- package/dist/plugins/specweave/lib/hooks/update-ac-status.js +162 -0
- package/dist/plugins/specweave/lib/hooks/update-ac-status.js.map +1 -0
- package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.js +65 -6
- package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/completion-calculator.d.ts +112 -0
- package/dist/plugins/specweave-github/lib/completion-calculator.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/completion-calculator.js +301 -0
- package/dist/plugins/specweave-github/lib/completion-calculator.js.map +1 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts +3 -3
- package/dist/plugins/specweave-github/lib/duplicate-detector.js +3 -3
- package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts +7 -0
- package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/epic-content-builder.js +42 -0
- package/dist/plugins/specweave-github/lib/epic-content-builder.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts +14 -0
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.js +51 -0
- package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-epic-sync.js +1 -1
- package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +87 -0
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-feature-sync.js +412 -0
- package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-spec-content-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-content-sync.js +64 -13
- package/dist/plugins/specweave-github/lib/github-spec-content-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/progress-comment-builder.d.ts +78 -0
- package/dist/plugins/specweave-github/lib/progress-comment-builder.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/progress-comment-builder.js +237 -0
- package/dist/plugins/specweave-github/lib/progress-comment-builder.js.map +1 -0
- package/dist/plugins/specweave-github/lib/user-story-content-builder.d.ts +97 -0
- package/dist/plugins/specweave-github/lib/user-story-content-builder.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/user-story-content-builder.js +301 -0
- package/dist/plugins/specweave-github/lib/user-story-content-builder.js.map +1 -0
- package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts +83 -0
- package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/user-story-issue-builder.js +386 -0
- package/dist/plugins/specweave-github/lib/user-story-issue-builder.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts +8 -6
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js +78 -117
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js.map +1 -1
- package/dist/src/cli/commands/import-docs.js +4 -4
- package/dist/src/cli/commands/import-docs.js.map +1 -1
- package/dist/src/cli/commands/init-multiproject.d.ts.map +1 -1
- package/dist/src/cli/commands/init-multiproject.js +17 -18
- package/dist/src/cli/commands/init-multiproject.js.map +1 -1
- package/dist/src/cli/commands/migrate-to-multiproject.d.ts.map +1 -1
- package/dist/src/cli/commands/migrate-to-multiproject.js +8 -4
- package/dist/src/cli/commands/migrate-to-multiproject.js.map +1 -1
- package/dist/src/cli/commands/switch-project.d.ts.map +1 -1
- package/dist/src/cli/commands/switch-project.js +9 -26
- package/dist/src/cli/commands/switch-project.js.map +1 -1
- package/dist/src/cli/commands/sync-spec-content.js +3 -0
- package/dist/src/cli/commands/sync-spec-content.js.map +1 -1
- package/dist/src/core/deduplication/command-deduplicator.d.ts +166 -0
- package/dist/src/core/deduplication/command-deduplicator.d.ts.map +1 -0
- package/dist/src/core/deduplication/command-deduplicator.js +254 -0
- package/dist/src/core/deduplication/command-deduplicator.js.map +1 -0
- package/dist/src/core/increment/active-increment-manager.d.ts +42 -15
- package/dist/src/core/increment/active-increment-manager.d.ts.map +1 -1
- package/dist/src/core/increment/active-increment-manager.js +113 -46
- package/dist/src/core/increment/active-increment-manager.js.map +1 -1
- package/dist/src/core/increment/conflict-resolver.d.ts +40 -0
- package/dist/src/core/increment/conflict-resolver.d.ts.map +1 -0
- package/dist/src/core/increment/conflict-resolver.js +219 -0
- package/dist/src/core/increment/conflict-resolver.js.map +1 -0
- package/dist/src/core/increment/discipline-checker.d.ts.map +1 -1
- package/dist/src/core/increment/discipline-checker.js +7 -1
- package/dist/src/core/increment/discipline-checker.js.map +1 -1
- package/dist/src/core/increment/duplicate-detector.d.ts +52 -0
- package/dist/src/core/increment/duplicate-detector.d.ts.map +1 -0
- package/dist/src/core/increment/duplicate-detector.js +276 -0
- package/dist/src/core/increment/duplicate-detector.js.map +1 -0
- package/dist/src/core/increment/increment-archiver.d.ts +90 -0
- package/dist/src/core/increment/increment-archiver.d.ts.map +1 -0
- package/dist/src/core/increment/increment-archiver.js +368 -0
- package/dist/src/core/increment/increment-archiver.js.map +1 -0
- package/dist/src/core/increment/increment-reopener.d.ts +165 -0
- package/dist/src/core/increment/increment-reopener.d.ts.map +1 -0
- package/dist/src/core/increment/increment-reopener.js +390 -0
- package/dist/src/core/increment/increment-reopener.js.map +1 -0
- package/dist/src/core/increment/metadata-manager.d.ts +26 -1
- package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
- package/dist/src/core/increment/metadata-manager.js +143 -5
- package/dist/src/core/increment/metadata-manager.js.map +1 -1
- package/dist/src/core/increment/recent-work-scanner.d.ts +121 -0
- package/dist/src/core/increment/recent-work-scanner.d.ts.map +1 -0
- package/dist/src/core/increment/recent-work-scanner.js +303 -0
- package/dist/src/core/increment/recent-work-scanner.js.map +1 -0
- package/dist/src/core/increment/types.d.ts +1 -0
- package/dist/src/core/increment/types.d.ts.map +1 -1
- package/dist/src/core/increment-utils.d.ts +112 -0
- package/dist/src/core/increment-utils.d.ts.map +1 -0
- package/dist/src/core/increment-utils.js +210 -0
- package/dist/src/core/increment-utils.js.map +1 -0
- package/dist/src/core/living-docs/ac-project-specific-generator.d.ts +65 -0
- package/dist/src/core/living-docs/ac-project-specific-generator.d.ts.map +1 -0
- package/dist/src/core/living-docs/ac-project-specific-generator.js +175 -0
- package/dist/src/core/living-docs/ac-project-specific-generator.js.map +1 -0
- package/dist/src/core/living-docs/feature-archiver.d.ts +130 -0
- package/dist/src/core/living-docs/feature-archiver.d.ts.map +1 -0
- package/dist/src/core/living-docs/feature-archiver.js +549 -0
- package/dist/src/core/living-docs/feature-archiver.js.map +1 -0
- package/dist/src/core/living-docs/feature-id-manager.d.ts +81 -0
- package/dist/src/core/living-docs/feature-id-manager.d.ts.map +1 -0
- package/dist/src/core/living-docs/feature-id-manager.js +339 -0
- package/dist/src/core/living-docs/feature-id-manager.js.map +1 -0
- package/dist/src/core/living-docs/hierarchy-mapper.d.ts +144 -83
- package/dist/src/core/living-docs/hierarchy-mapper.d.ts.map +1 -1
- package/dist/src/core/living-docs/hierarchy-mapper.js +488 -270
- package/dist/src/core/living-docs/hierarchy-mapper.js.map +1 -1
- package/dist/src/core/living-docs/index.d.ts +6 -0
- package/dist/src/core/living-docs/index.d.ts.map +1 -1
- package/dist/src/core/living-docs/index.js +6 -0
- package/dist/src/core/living-docs/index.js.map +1 -1
- package/dist/src/core/living-docs/project-detector.d.ts +6 -0
- package/dist/src/core/living-docs/project-detector.d.ts.map +1 -1
- package/dist/src/core/living-docs/project-detector.js +35 -1
- package/dist/src/core/living-docs/project-detector.js.map +1 -1
- package/dist/src/core/living-docs/spec-distributor.d.ts +100 -26
- package/dist/src/core/living-docs/spec-distributor.d.ts.map +1 -1
- package/dist/src/core/living-docs/spec-distributor.js +1275 -258
- package/dist/src/core/living-docs/spec-distributor.js.map +1 -1
- package/dist/src/core/living-docs/task-project-specific-generator.d.ts +109 -0
- package/dist/src/core/living-docs/task-project-specific-generator.d.ts.map +1 -0
- package/dist/src/core/living-docs/task-project-specific-generator.js +221 -0
- package/dist/src/core/living-docs/task-project-specific-generator.js.map +1 -0
- package/dist/src/core/living-docs/types.d.ts +143 -0
- package/dist/src/core/living-docs/types.d.ts.map +1 -1
- package/dist/src/core/project-manager.d.ts +2 -17
- package/dist/src/core/project-manager.d.ts.map +1 -1
- package/dist/src/core/project-manager.js +68 -48
- package/dist/src/core/project-manager.js.map +1 -1
- package/dist/src/core/spec-content-sync.d.ts +1 -1
- package/dist/src/core/spec-content-sync.d.ts.map +1 -1
- package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -1
- package/dist/src/core/sync/enhanced-content-builder.js +2 -1
- package/dist/src/core/sync/enhanced-content-builder.js.map +1 -1
- package/dist/src/core/sync/performance-optimizer.d.ts +153 -0
- package/dist/src/core/sync/performance-optimizer.d.ts.map +1 -0
- package/dist/src/core/sync/performance-optimizer.js +220 -0
- package/dist/src/core/sync/performance-optimizer.js.map +1 -0
- package/dist/src/core/sync/retry-handler.d.ts +98 -0
- package/dist/src/core/sync/retry-handler.d.ts.map +1 -0
- package/dist/src/core/sync/retry-handler.js +196 -0
- package/dist/src/core/sync/retry-handler.js.map +1 -0
- package/dist/src/core/types/config.d.ts +94 -0
- package/dist/src/core/types/config.d.ts.map +1 -1
- package/dist/src/core/types/config.js +16 -0
- package/dist/src/core/types/config.js.map +1 -1
- package/dist/src/core/types/increment-metadata.d.ts +6 -0
- package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
- package/dist/src/core/types/increment-metadata.js +10 -1
- package/dist/src/core/types/increment-metadata.js.map +1 -1
- package/dist/src/integrations/jira/jira-incremental-mapper.d.ts.map +1 -1
- package/dist/src/integrations/jira/jira-incremental-mapper.js +4 -8
- package/dist/src/integrations/jira/jira-incremental-mapper.js.map +1 -1
- package/dist/src/integrations/jira/jira-mapper.d.ts.map +1 -1
- package/dist/src/integrations/jira/jira-mapper.js +4 -8
- package/dist/src/integrations/jira/jira-mapper.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/COMMANDS.md +13 -4
- package/plugins/specweave/commands/specweave-abandon.md +22 -20
- package/plugins/specweave/commands/specweave-archive-features.md +121 -0
- package/plugins/specweave/commands/specweave-archive-increments.md +82 -0
- package/plugins/specweave/commands/specweave-archive.md +363 -0
- package/plugins/specweave/commands/specweave-backlog.md +211 -0
- package/plugins/specweave/commands/specweave-fix-duplicates.md +517 -0
- package/plugins/specweave/commands/specweave-increment.md +4 -3
- package/plugins/specweave/commands/specweave-progress.md +176 -27
- package/plugins/specweave/commands/specweave-reopen.md +391 -0
- package/plugins/specweave/commands/specweave-restore-feature.md +90 -0
- package/plugins/specweave/commands/specweave-restore.md +309 -0
- package/plugins/specweave/commands/specweave-resume.md +51 -23
- package/plugins/specweave/commands/specweave-status.md +41 -7
- package/plugins/specweave/commands/specweave-sync-specs.md +425 -0
- package/plugins/specweave/hooks/hooks.json +4 -0
- package/plugins/specweave/hooks/lib/sync-spec-content.sh +2 -2
- package/plugins/specweave/hooks/post-task-completion.sh +39 -0
- package/plugins/specweave/hooks/pre-command-deduplication.sh +83 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh +1 -1
- package/plugins/specweave/lib/hooks/sync-living-docs.js +2 -0
- package/plugins/specweave/lib/hooks/sync-living-docs.ts +4 -0
- package/plugins/specweave/lib/hooks/update-ac-status.js +102 -0
- package/plugins/specweave/lib/hooks/update-ac-status.ts +192 -0
- package/plugins/specweave/skills/archive-increments/SKILL.md +198 -0
- package/plugins/specweave/skills/increment-planner/scripts/feature-utils.js +14 -0
- package/plugins/specweave/skills/smart-reopen-detector/SKILL.md +244 -0
- package/plugins/specweave-ado/lib/ado-spec-content-sync.js +49 -5
- package/plugins/specweave-ado/lib/ado-spec-content-sync.ts +72 -6
- package/plugins/specweave-confluent/.claude-plugin/plugin.json +23 -0
- package/plugins/specweave-confluent/README.md +375 -0
- package/plugins/specweave-confluent/agents/confluent-architect/AGENT.md +306 -0
- package/plugins/specweave-confluent/skills/confluent-kafka-connect/SKILL.md +453 -0
- package/plugins/specweave-confluent/skills/confluent-ksqldb/SKILL.md +470 -0
- package/plugins/specweave-confluent/skills/confluent-schema-registry/SKILL.md +316 -0
- package/plugins/specweave-github/agents/github-task-splitter/AGENT.md +2 -2
- package/plugins/specweave-github/agents/user-story-updater/AGENT.md +148 -0
- package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +1 -1
- package/plugins/specweave-github/commands/specweave-github-update-user-story.md +156 -0
- package/plugins/specweave-github/hooks/post-task-completion.sh +10 -9
- package/plugins/specweave-github/lib/completion-calculator.js +262 -0
- package/plugins/specweave-github/lib/completion-calculator.ts +434 -0
- package/plugins/specweave-github/lib/duplicate-detector.js +3 -3
- package/plugins/specweave-github/lib/duplicate-detector.ts +4 -4
- package/plugins/specweave-github/lib/epic-content-builder.js +38 -0
- package/plugins/specweave-github/lib/epic-content-builder.ts +59 -0
- package/plugins/specweave-github/lib/github-client-v2.js +49 -0
- package/plugins/specweave-github/lib/github-client-v2.ts +59 -0
- package/plugins/specweave-github/lib/github-epic-sync.ts +1 -1
- package/plugins/specweave-github/lib/github-feature-sync.js +381 -0
- package/plugins/specweave-github/lib/github-feature-sync.ts +568 -0
- package/plugins/specweave-github/lib/github-spec-content-sync.js +40 -10
- package/plugins/specweave-github/lib/github-spec-content-sync.ts +82 -14
- package/plugins/specweave-github/lib/progress-comment-builder.js +229 -0
- package/plugins/specweave-github/lib/progress-comment-builder.ts +324 -0
- package/plugins/specweave-github/lib/user-story-content-builder.js +299 -0
- package/plugins/specweave-github/lib/user-story-content-builder.ts +413 -0
- package/plugins/specweave-github/lib/user-story-issue-builder.js +344 -0
- package/plugins/specweave-github/lib/user-story-issue-builder.ts +543 -0
- package/plugins/specweave-github/skills/github-issue-standard/SKILL.md +189 -0
- package/plugins/{specweave-ado/lib/enhanced-ado-sync.js → specweave-jira/lib/enhanced-jira-sync.js} +25 -61
- package/plugins/specweave-jira/lib/{enhanced-jira-sync.ts.disabled → enhanced-jira-sync.ts} +26 -52
- package/plugins/specweave-kafka/.claude-plugin/plugin.json +26 -0
- package/plugins/specweave-kafka/IMPLEMENTATION-COMPLETE.md +483 -0
- package/plugins/specweave-kafka/README.md +242 -0
- package/plugins/specweave-kafka/agents/kafka-architect/AGENT.md +235 -0
- package/plugins/specweave-kafka/agents/kafka-devops/AGENT.md +209 -0
- package/plugins/specweave-kafka/agents/kafka-observability/AGENT.md +266 -0
- package/plugins/specweave-kafka/commands/deploy.md +99 -0
- package/plugins/specweave-kafka/commands/dev-env.md +176 -0
- package/plugins/specweave-kafka/commands/mcp-configure.md +101 -0
- package/plugins/specweave-kafka/commands/monitor-setup.md +96 -0
- package/plugins/specweave-kafka/docker/kafka-local/docker-compose.yml +187 -0
- package/plugins/specweave-kafka/docker/redpanda/docker-compose.yml +199 -0
- package/plugins/specweave-kafka/docker/templates/consumer-nodejs.js +225 -0
- package/plugins/specweave-kafka/docker/templates/consumer-python.py +220 -0
- package/plugins/specweave-kafka/docker/templates/producer-nodejs.js +168 -0
- package/plugins/specweave-kafka/docker/templates/producer-python.py +167 -0
- package/plugins/specweave-kafka/lib/adapters/apache-kafka-adapter.js +438 -0
- package/plugins/specweave-kafka/lib/adapters/apache-kafka-adapter.ts +541 -0
- package/plugins/specweave-kafka/lib/adapters/platform-adapter.js +47 -0
- package/plugins/specweave-kafka/lib/adapters/platform-adapter.ts +343 -0
- package/plugins/specweave-kafka/lib/cli/kcat-wrapper.js +258 -0
- package/plugins/specweave-kafka/lib/cli/kcat-wrapper.ts +298 -0
- package/plugins/specweave-kafka/lib/cli/types.js +10 -0
- package/plugins/specweave-kafka/lib/cli/types.ts +92 -0
- package/plugins/specweave-kafka/lib/connectors/connector-catalog.js +305 -0
- package/plugins/specweave-kafka/lib/connectors/connector-catalog.ts +528 -0
- package/plugins/specweave-kafka/lib/documentation/diagram-generator.js +114 -0
- package/plugins/specweave-kafka/lib/documentation/diagram-generator.ts +195 -0
- package/plugins/specweave-kafka/lib/documentation/exporter.js +210 -0
- package/plugins/specweave-kafka/lib/documentation/exporter.ts +338 -0
- package/plugins/specweave-kafka/lib/documentation/schema-catalog-generator.js +60 -0
- package/plugins/specweave-kafka/lib/documentation/schema-catalog-generator.ts +130 -0
- package/plugins/specweave-kafka/lib/documentation/topology-generator.js +143 -0
- package/plugins/specweave-kafka/lib/documentation/topology-generator.ts +290 -0
- package/plugins/specweave-kafka/lib/mcp/detector.js +298 -0
- package/plugins/specweave-kafka/lib/mcp/detector.ts +352 -0
- package/plugins/specweave-kafka/lib/mcp/types.js +21 -0
- package/plugins/specweave-kafka/lib/mcp/types.ts +77 -0
- package/plugins/specweave-kafka/lib/multi-cluster/cluster-config-manager.js +193 -0
- package/plugins/specweave-kafka/lib/multi-cluster/cluster-config-manager.ts +362 -0
- package/plugins/specweave-kafka/lib/multi-cluster/cluster-switcher.js +188 -0
- package/plugins/specweave-kafka/lib/multi-cluster/cluster-switcher.ts +359 -0
- package/plugins/specweave-kafka/lib/multi-cluster/health-aggregator.js +195 -0
- package/plugins/specweave-kafka/lib/multi-cluster/health-aggregator.ts +380 -0
- package/plugins/specweave-kafka/lib/observability/opentelemetry-kafka.js +209 -0
- package/plugins/specweave-kafka/lib/observability/opentelemetry-kafka.ts +358 -0
- package/plugins/specweave-kafka/lib/patterns/advanced-ksqldb-patterns.js +354 -0
- package/plugins/specweave-kafka/lib/patterns/advanced-ksqldb-patterns.ts +563 -0
- package/plugins/specweave-kafka/lib/patterns/circuit-breaker-resilience.js +259 -0
- package/plugins/specweave-kafka/lib/patterns/circuit-breaker-resilience.ts +516 -0
- package/plugins/specweave-kafka/lib/patterns/dead-letter-queue.js +233 -0
- package/plugins/specweave-kafka/lib/patterns/dead-letter-queue.ts +423 -0
- package/plugins/specweave-kafka/lib/patterns/exactly-once-semantics.js +266 -0
- package/plugins/specweave-kafka/lib/patterns/exactly-once-semantics.ts +445 -0
- package/plugins/specweave-kafka/lib/patterns/flink-kafka-integration.js +312 -0
- package/plugins/specweave-kafka/lib/patterns/flink-kafka-integration.ts +561 -0
- package/plugins/specweave-kafka/lib/patterns/multi-dc-replication.js +289 -0
- package/plugins/specweave-kafka/lib/patterns/multi-dc-replication.ts +607 -0
- package/plugins/specweave-kafka/lib/patterns/rate-limiting-backpressure.js +264 -0
- package/plugins/specweave-kafka/lib/patterns/rate-limiting-backpressure.ts +498 -0
- package/plugins/specweave-kafka/lib/patterns/stream-processing-optimization.js +263 -0
- package/plugins/specweave-kafka/lib/patterns/stream-processing-optimization.ts +549 -0
- package/plugins/specweave-kafka/lib/patterns/tiered-storage-compaction.js +205 -0
- package/plugins/specweave-kafka/lib/patterns/tiered-storage-compaction.ts +399 -0
- package/plugins/specweave-kafka/lib/performance/performance-optimizer.js +249 -0
- package/plugins/specweave-kafka/lib/performance/performance-optimizer.ts +427 -0
- package/plugins/specweave-kafka/lib/security/kafka-security.js +252 -0
- package/plugins/specweave-kafka/lib/security/kafka-security.ts +494 -0
- package/plugins/specweave-kafka/lib/utils/capacity-planner.js +203 -0
- package/plugins/specweave-kafka/lib/utils/capacity-planner.ts +469 -0
- package/plugins/specweave-kafka/lib/utils/config-validator.js +419 -0
- package/plugins/specweave-kafka/lib/utils/config-validator.ts +564 -0
- package/plugins/specweave-kafka/lib/utils/partitioning.js +329 -0
- package/plugins/specweave-kafka/lib/utils/partitioning.ts +473 -0
- package/plugins/specweave-kafka/lib/utils/sizing.js +221 -0
- package/plugins/specweave-kafka/lib/utils/sizing.ts +374 -0
- package/plugins/specweave-kafka/monitoring/grafana/dashboards/kafka-broker-metrics.json +628 -0
- package/plugins/specweave-kafka/monitoring/grafana/dashboards/kafka-cluster-overview.json +564 -0
- package/plugins/specweave-kafka/monitoring/grafana/dashboards/kafka-consumer-lag.json +509 -0
- package/plugins/specweave-kafka/monitoring/grafana/dashboards/kafka-jvm-metrics.json +674 -0
- package/plugins/specweave-kafka/monitoring/grafana/dashboards/kafka-topic-metrics.json +578 -0
- package/plugins/specweave-kafka/monitoring/grafana/provisioning/dashboards/kafka.yml +17 -0
- package/plugins/specweave-kafka/monitoring/grafana/provisioning/datasources/prometheus.yml +17 -0
- package/plugins/specweave-kafka/monitoring/prometheus/kafka-alerts.yml +415 -0
- package/plugins/specweave-kafka/monitoring/prometheus/kafka-jmx-exporter.yml +256 -0
- package/plugins/specweave-kafka/package.json +41 -0
- package/plugins/specweave-kafka/skills/kafka-architecture/SKILL.md +647 -0
- package/plugins/specweave-kafka/skills/kafka-cli-tools/SKILL.md +433 -0
- package/plugins/specweave-kafka/skills/kafka-iac-deployment/SKILL.md +449 -0
- package/plugins/specweave-kafka/skills/kafka-kubernetes/SKILL.md +667 -0
- package/plugins/specweave-kafka/skills/kafka-mcp-integration/SKILL.md +273 -0
- package/plugins/specweave-kafka/skills/kafka-observability/SKILL.md +576 -0
- package/plugins/specweave-kafka/templates/config/broker-production.properties +254 -0
- package/plugins/specweave-kafka/templates/config/consumer-low-latency.properties +112 -0
- package/plugins/specweave-kafka/templates/config/producer-high-throughput.properties +120 -0
- package/plugins/specweave-kafka/templates/migration/mirrormaker2-config.properties +234 -0
- package/plugins/specweave-kafka/templates/monitoring/grafana/multi-cluster-dashboard.json +686 -0
- package/plugins/specweave-kafka/terraform/apache-kafka/main.tf +347 -0
- package/plugins/specweave-kafka/terraform/apache-kafka/outputs.tf +107 -0
- package/plugins/specweave-kafka/terraform/apache-kafka/templates/kafka-broker-init.sh.tpl +216 -0
- package/plugins/specweave-kafka/terraform/apache-kafka/variables.tf +156 -0
- package/plugins/specweave-kafka/terraform/aws-msk/main.tf +362 -0
- package/plugins/specweave-kafka/terraform/aws-msk/outputs.tf +93 -0
- package/plugins/specweave-kafka/terraform/aws-msk/templates/server.properties.tpl +32 -0
- package/plugins/specweave-kafka/terraform/aws-msk/variables.tf +235 -0
- package/plugins/specweave-kafka/terraform/azure-event-hubs/main.tf +281 -0
- package/plugins/specweave-kafka/terraform/azure-event-hubs/outputs.tf +118 -0
- package/plugins/specweave-kafka/terraform/azure-event-hubs/variables.tf +148 -0
- package/plugins/specweave-kafka/tsconfig.json +21 -0
- package/plugins/specweave-kafka-streams/.claude-plugin/plugin.json +23 -0
- package/plugins/specweave-kafka-streams/README.md +310 -0
- package/plugins/specweave-kafka-streams/skills/kafka-streams-topology/SKILL.md +539 -0
- package/plugins/specweave-n8n/.claude-plugin/plugin.json +22 -0
- package/plugins/specweave-n8n/README.md +354 -0
- package/plugins/specweave-n8n/skills/n8n-kafka-workflows/SKILL.md +504 -0
- package/plugins/specweave-release/commands/specweave-release-platform.md +1 -1
- package/plugins/specweave-release/hooks/post-task-completion.sh +2 -2
- package/src/templates/AGENTS.md.template +601 -7
- package/src/templates/CLAUDE.md.template +188 -88
- package/dist/locales/de/.gitkeep +0 -0
- package/dist/locales/de/cli.json +0 -108
- package/dist/locales/en/cli.json +0 -287
- package/dist/locales/en/errors.json +0 -7
- package/dist/locales/en/templates.json +0 -6
- package/dist/locales/es/.gitkeep +0 -0
- package/dist/locales/es/cli.json +0 -41
- package/dist/locales/fr/.gitkeep +0 -0
- package/dist/locales/fr/cli.json +0 -108
- package/dist/locales/ja/.gitkeep +0 -0
- package/dist/locales/ja/cli.json +0 -108
- package/dist/locales/ko/.gitkeep +0 -0
- package/dist/locales/ko/cli.json +0 -108
- package/dist/locales/pt/.gitkeep +0 -0
- package/dist/locales/pt/cli.json +0 -108
- package/dist/locales/ru/.gitkeep +0 -0
- package/dist/locales/ru/cli.json +0 -269
- package/dist/locales/zh/.gitkeep +0 -0
- package/dist/locales/zh/cli.json +0 -108
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts +0 -25
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts.map +0 -1
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js +0 -191
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js.map +0 -1
- package/dist/spec-parser.js +0 -629
- package/dist/src/core/sync/spec-content-sync.d.ts +0 -88
- package/dist/src/core/sync/spec-content-sync.d.ts.map +0 -1
- package/dist/src/core/sync/spec-content-sync.js +0 -5
- package/dist/src/core/sync/spec-content-sync.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/plugins/specweave-ado/commands/specweave-ado-sync-spec.md +0 -255
- package/plugins/specweave-github/commands/specweave-github-sync-epic.md +0 -248
- package/plugins/specweave-github/commands/specweave-github-sync-from.md +0 -147
- package/plugins/specweave-github/commands/specweave-github-sync-spec.md +0 -208
- package/plugins/specweave-github/commands/specweave-github-sync-tasks.md +0 -530
- package/plugins/specweave-jira/commands/specweave-jira-sync-epic.md +0 -267
- package/plugins/specweave-jira/commands/specweave-jira-sync-spec.md +0 -240
|
@@ -13,6 +13,8 @@ import fs from 'fs-extra';
|
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import { HierarchyMapper } from './hierarchy-mapper.js';
|
|
15
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';
|
|
16
18
|
/**
|
|
17
19
|
* SpecDistributor - Distributes increment specs into hierarchical living docs
|
|
18
20
|
*/
|
|
@@ -36,15 +38,15 @@ export class SpecDistributor {
|
|
|
36
38
|
createBackups: true,
|
|
37
39
|
...config,
|
|
38
40
|
};
|
|
39
|
-
// Initialize HierarchyMapper
|
|
40
|
-
this.hierarchyMapper = new HierarchyMapper(projectRoot
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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);
|
|
45
47
|
}
|
|
46
48
|
/**
|
|
47
|
-
* Distribute increment spec into epic + user
|
|
49
|
+
* Distribute increment spec into universal hierarchy (epic + feature + user stories)
|
|
48
50
|
*/
|
|
49
51
|
async distribute(incrementId) {
|
|
50
52
|
const errors = [];
|
|
@@ -54,46 +56,80 @@ export class SpecDistributor {
|
|
|
54
56
|
if (!this.githubRemote) {
|
|
55
57
|
this.githubRemote = await detectPrimaryGitHubRemote(this.projectRoot);
|
|
56
58
|
}
|
|
57
|
-
// Step
|
|
59
|
+
// Step 1: Parse increment spec (with epic and project detection)
|
|
58
60
|
const parsed = await this.parseIncrementSpec(incrementId);
|
|
59
|
-
// Step
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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)`);
|
|
70
94
|
}
|
|
71
|
-
// Step
|
|
95
|
+
// Step 4: Detect feature mapping (REQUIRED)
|
|
72
96
|
console.log(` 🔍 Detecting feature folder for ${incrementId}...`);
|
|
73
|
-
const
|
|
74
|
-
console.log(` 📁 Mapped to ${
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// Step
|
|
80
|
-
const
|
|
81
|
-
// Step
|
|
82
|
-
const
|
|
83
|
-
// Step
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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();
|
|
88
124
|
return {
|
|
89
|
-
epic,
|
|
90
|
-
userStories,
|
|
125
|
+
epic: featureFile, // Type compatibility hack
|
|
126
|
+
userStories: allUserStories,
|
|
91
127
|
incrementId,
|
|
92
|
-
specId:
|
|
93
|
-
totalStories:
|
|
94
|
-
totalFiles: 1 +
|
|
95
|
-
epicPath,
|
|
96
|
-
userStoryPaths,
|
|
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,
|
|
97
133
|
success: true,
|
|
98
134
|
errors,
|
|
99
135
|
warnings,
|
|
@@ -164,18 +200,34 @@ export class SpecDistributor {
|
|
|
164
200
|
// Extract overview (try multiple sections)
|
|
165
201
|
let overview = '';
|
|
166
202
|
// Try "Quick Overview" or "Executive Summary"
|
|
167
|
-
let overviewMatch = bodyContent.match(/##\s+(?:Quick\s+)?(?:Overview|Executive\s+Summary)\s*\n+([\s\S]*?)(?=\n##|\n
|
|
168
|
-
if (overviewMatch)
|
|
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) {
|
|
169
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
|
+
}
|
|
170
220
|
if (!overview) {
|
|
171
221
|
// Try "Overview" section
|
|
172
|
-
overviewMatch = bodyContent.match(/##\s+Overview\s*\n+([\s\S]*?)(?=\n##|\n
|
|
173
|
-
if (overviewMatch)
|
|
174
|
-
|
|
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
|
+
}
|
|
175
227
|
}
|
|
176
228
|
if (!overview) {
|
|
177
229
|
// Try "Problem Statement" section
|
|
178
|
-
const problemMatch = bodyContent.match(/##\s+Problem\s+Statement\s*\n+([\s\S]*?)(?=\n##|\n
|
|
230
|
+
const problemMatch = bodyContent.match(/##\s+Problem\s+Statement\s*\n+([\s\S]*?)(?=\n##|\n---|$)/im);
|
|
179
231
|
if (problemMatch) {
|
|
180
232
|
// Take first paragraph only
|
|
181
233
|
const firstPara = problemMatch[1].trim().split('\n\n')[0];
|
|
@@ -202,14 +254,23 @@ export class SpecDistributor {
|
|
|
202
254
|
}
|
|
203
255
|
// Extract user stories
|
|
204
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();
|
|
205
261
|
return {
|
|
206
262
|
incrementId,
|
|
207
263
|
title,
|
|
208
264
|
overview,
|
|
209
265
|
businessValue,
|
|
266
|
+
epic: frontmatter.epic, // Epic ID from frontmatter (optional)
|
|
210
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,
|
|
211
272
|
userStories,
|
|
212
|
-
externalLinks, //
|
|
273
|
+
externalLinks, // External links from metadata.json
|
|
213
274
|
};
|
|
214
275
|
}
|
|
215
276
|
/**
|
|
@@ -241,30 +302,98 @@ export class SpecDistributor {
|
|
|
241
302
|
}
|
|
242
303
|
return links;
|
|
243
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
|
+
}
|
|
244
357
|
/**
|
|
245
358
|
* Extract user stories from increment spec
|
|
246
359
|
*/
|
|
247
360
|
async extractUserStories(content, incrementId) {
|
|
248
361
|
const userStories = [];
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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();
|
|
256
380
|
// Extract description (As a... I want... So that...) - supports both inline and separate line formats
|
|
257
|
-
const descMatch = storyContent.match(/\*\*As a\*\*\s+(.*?)\
|
|
381
|
+
const descMatch = storyContent.match(/\*\*As a\*\*\s+(.*?)\n\*\*I want\*\*\s+(.*?)\n\*\*So that\*\*\s+(.*?)(?:\n|$)/is);
|
|
258
382
|
const description = descMatch
|
|
259
383
|
? `**As a** ${descMatch[1].trim()}\n**I want** ${descMatch[2].trim()}\n**So that** ${descMatch[3].trim()}`
|
|
260
384
|
: '';
|
|
261
|
-
// Extract acceptance criteria
|
|
262
|
-
|
|
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
|
+
}
|
|
263
391
|
// Extract business rationale
|
|
264
392
|
const rationaleMatch = storyContent.match(/\*\*Business Rationale\*\*:\s+(.*?)(?=\n\n---|\n\n##|$)/is);
|
|
265
393
|
const businessRationale = rationaleMatch ? rationaleMatch[1].trim() : undefined;
|
|
266
|
-
// Extract phase
|
|
267
|
-
const
|
|
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);
|
|
268
397
|
const phase = phaseMatch ? phaseMatch[1] : undefined;
|
|
269
398
|
// Determine status (assume complete if in completed increment)
|
|
270
399
|
const status = 'complete'; // Can be enhanced later
|
|
@@ -286,87 +415,145 @@ export class SpecDistributor {
|
|
|
286
415
|
*/
|
|
287
416
|
extractAcceptanceCriteria(content) {
|
|
288
417
|
const criteria = [];
|
|
289
|
-
// Pattern: - [x] **AC-US1-01**: Description (P1, testable)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
+
}
|
|
306
469
|
}
|
|
307
470
|
return criteria;
|
|
308
471
|
}
|
|
309
472
|
/**
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
* NEW (v0.18.0): Uses feature folder name as ID (e.g., FS-25-11-14-release-management)
|
|
313
|
-
*/
|
|
314
|
-
async classifyContent(parsed, epicMapping) {
|
|
315
|
-
// Use feature folder name as ID (e.g., FS-25-11-14-release-management)
|
|
316
|
-
// This ensures ID matches folder name
|
|
317
|
-
const specId = epicMapping.featureFolder;
|
|
318
|
-
return {
|
|
319
|
-
epic: {
|
|
320
|
-
id: specId,
|
|
321
|
-
title: parsed.title,
|
|
322
|
-
overview: parsed.overview,
|
|
323
|
-
businessValue: parsed.businessValue,
|
|
324
|
-
status: 'complete',
|
|
325
|
-
},
|
|
326
|
-
userStories: parsed.userStories,
|
|
327
|
-
implementationHistory: [
|
|
328
|
-
{
|
|
329
|
-
increment: parsed.incrementId,
|
|
330
|
-
stories: parsed.userStories.map((us) => us.id),
|
|
331
|
-
status: 'complete',
|
|
332
|
-
date: new Date().toISOString().split('T')[0],
|
|
333
|
-
},
|
|
334
|
-
],
|
|
335
|
-
externalLinks: parsed.externalLinks || {},
|
|
336
|
-
relatedDocs: [],
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Generate epic file
|
|
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
|
|
341
475
|
*/
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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;
|
|
370
557
|
}
|
|
371
558
|
/**
|
|
372
559
|
* Generate user story files
|
|
@@ -489,36 +676,6 @@ export class SpecDistributor {
|
|
|
489
676
|
.replace(/^-|-$/g, '');
|
|
490
677
|
return `${id.toLowerCase()}-${slug}.md`;
|
|
491
678
|
}
|
|
492
|
-
/**
|
|
493
|
-
* Write feature file to disk (NEW: writes to FEATURE.md instead of README.md)
|
|
494
|
-
*/
|
|
495
|
-
async writeEpicFile(epic, epicMapping) {
|
|
496
|
-
// Write to feature-folder/FEATURE.md (feature overview - high-level summary)
|
|
497
|
-
const featurePath = path.join(epicMapping.featurePath, 'FEATURE.md');
|
|
498
|
-
const content = this.formatEpicFile(epic);
|
|
499
|
-
await fs.ensureDir(path.dirname(featurePath));
|
|
500
|
-
await fs.writeFile(featurePath, content, 'utf-8');
|
|
501
|
-
console.log(` ✅ Written feature overview to ${epicMapping.featureFolder}/FEATURE.md`);
|
|
502
|
-
return featurePath;
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* Write user story files to disk (NEW: writes directly to feature folder)
|
|
506
|
-
*/
|
|
507
|
-
async writeUserStoryFiles(userStories, epicMapping) {
|
|
508
|
-
// Write user stories directly to feature folder (not in subfolder)
|
|
509
|
-
const featureDir = epicMapping.featurePath;
|
|
510
|
-
await fs.ensureDir(featureDir);
|
|
511
|
-
const paths = [];
|
|
512
|
-
for (const userStory of userStories) {
|
|
513
|
-
const filename = this.generateUserStoryFilename(userStory.id, userStory.title);
|
|
514
|
-
const filePath = path.join(featureDir, filename);
|
|
515
|
-
const content = this.formatUserStoryFile(userStory);
|
|
516
|
-
await fs.writeFile(filePath, content, 'utf-8');
|
|
517
|
-
paths.push(filePath);
|
|
518
|
-
}
|
|
519
|
-
console.log(` ✅ Written ${userStories.length} user stories directly to ${epicMapping.featureFolder}/`);
|
|
520
|
-
return paths;
|
|
521
|
-
}
|
|
522
679
|
/**
|
|
523
680
|
* Format epic file as markdown
|
|
524
681
|
*/
|
|
@@ -651,7 +808,7 @@ export class SpecDistributor {
|
|
|
651
808
|
// Frontmatter
|
|
652
809
|
lines.push('---');
|
|
653
810
|
lines.push(`id: ${userStory.id}`);
|
|
654
|
-
lines.push(`
|
|
811
|
+
lines.push(`feature: ${userStory.epic}`); // ✅ FIX: Use 'feature:' not 'epic:' (Universal Hierarchy)
|
|
655
812
|
lines.push(`title: "${userStory.title}"`);
|
|
656
813
|
lines.push(`status: ${userStory.status}`);
|
|
657
814
|
if (userStory.priority)
|
|
@@ -659,40 +816,81 @@ export class SpecDistributor {
|
|
|
659
816
|
lines.push(`created: ${userStory.created}`);
|
|
660
817
|
if (userStory.completed)
|
|
661
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
|
+
}
|
|
662
827
|
lines.push('---');
|
|
663
828
|
lines.push('');
|
|
664
|
-
// Title
|
|
665
|
-
|
|
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);
|
|
666
838
|
lines.push('');
|
|
667
|
-
// Feature link (
|
|
668
|
-
|
|
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})`);
|
|
669
846
|
lines.push('');
|
|
670
847
|
// Description
|
|
671
|
-
|
|
672
|
-
|
|
848
|
+
if (userStory.description) {
|
|
849
|
+
lines.push(userStory.description);
|
|
850
|
+
lines.push('');
|
|
851
|
+
}
|
|
673
852
|
lines.push('---');
|
|
674
853
|
lines.push('');
|
|
675
854
|
// Acceptance Criteria
|
|
676
855
|
lines.push('## Acceptance Criteria');
|
|
677
856
|
lines.push('');
|
|
678
|
-
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
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*');
|
|
682
866
|
}
|
|
683
867
|
lines.push('');
|
|
684
868
|
lines.push('---');
|
|
685
869
|
lines.push('');
|
|
686
|
-
//
|
|
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)
|
|
687
885
|
lines.push('## Implementation');
|
|
688
886
|
lines.push('');
|
|
689
|
-
|
|
887
|
+
const incrementLink = userStory.implementation.tasks[0]?.path.replace(/#.*$/, '') || `../../../../../increments/${userStory.implementation.increment}/tasks.md`;
|
|
888
|
+
lines.push(`**Increment**: [${userStory.implementation.increment}](${incrementLink})`);
|
|
690
889
|
lines.push('');
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
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('');
|
|
694
893
|
}
|
|
695
|
-
lines.push('');
|
|
696
894
|
// Business Rationale
|
|
697
895
|
if (userStory.businessRationale) {
|
|
698
896
|
lines.push('---');
|
|
@@ -726,98 +924,917 @@ export class SpecDistributor {
|
|
|
726
924
|
* Update tasks.md with bidirectional links to user stories (CRITICAL!)
|
|
727
925
|
*
|
|
728
926
|
* This creates bidirectional traceability:
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
|
|
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
|
|
734
953
|
*/
|
|
735
|
-
async
|
|
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) {
|
|
736
1427
|
const tasksPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId, 'tasks.md');
|
|
737
|
-
// Check if tasks.md exists
|
|
738
1428
|
if (!fs.existsSync(tasksPath)) {
|
|
739
|
-
console.
|
|
1429
|
+
console.warn(` ⚠️ tasks.md not found for ${incrementId}, skipping bidirectional linking`);
|
|
740
1430
|
return;
|
|
741
1431
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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'));
|
|
768
1484
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
return match; // Link already exists
|
|
1485
|
+
else {
|
|
1486
|
+
updatedSections.push(section);
|
|
772
1487
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
+
}
|
|
784
1521
|
}
|
|
785
1522
|
else {
|
|
786
|
-
|
|
1523
|
+
updatedSections.push(section);
|
|
787
1524
|
}
|
|
788
1525
|
}
|
|
789
|
-
|
|
790
|
-
|
|
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`);
|
|
791
1530
|
}
|
|
792
1531
|
}
|
|
793
1532
|
/**
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
* Extracts AC-IDs from tasks (e.g., AC-US1-01) and maps them to user stories (e.g., US-001)
|
|
1533
|
+
* Update acceptance criteria status in user stories based on completed tasks
|
|
1534
|
+
* This method synchronizes AC checkboxes with task completion status
|
|
797
1535
|
*/
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
+
}
|
|
817
1681
|
}
|
|
818
1682
|
}
|
|
819
1683
|
}
|
|
820
|
-
|
|
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;
|
|
821
1838
|
}
|
|
822
1839
|
}
|
|
823
1840
|
//# sourceMappingURL=spec-distributor.js.map
|