specweave 0.22.0 → 0.22.2
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 +211 -0
- package/README.md +5 -5
- package/bin/specweave.js +5 -8
- package/dist/plugins/specweave-github/lib/CodeValidator.d.ts +1 -1
- package/dist/plugins/specweave-github/lib/CodeValidator.js +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts +10 -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 +26 -0
- package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
- package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/task-sync.js +7 -0
- package/dist/plugins/specweave-github/lib/task-sync.js.map +1 -1
- package/dist/src/cli/commands/migrate-to-profiles.d.ts +1 -0
- package/dist/src/cli/commands/migrate-to-profiles.d.ts.map +1 -1
- package/dist/src/cli/commands/migrate-to-profiles.js +12 -1
- package/dist/src/cli/commands/migrate-to-profiles.js.map +1 -1
- package/dist/src/cli/commands/next-command.d.ts +52 -0
- package/dist/src/cli/commands/next-command.d.ts.map +1 -0
- package/dist/src/cli/commands/next-command.js +204 -0
- package/dist/src/cli/commands/next-command.js.map +1 -0
- package/dist/src/cli/commands/sync-specs.d.ts +16 -0
- package/dist/src/cli/commands/sync-specs.d.ts.map +1 -0
- package/dist/src/cli/commands/sync-specs.js +130 -0
- package/dist/src/cli/commands/sync-specs.js.map +1 -0
- package/dist/src/cli/count-tasks.d.ts +20 -0
- package/dist/src/cli/count-tasks.d.ts.map +1 -0
- package/dist/src/cli/count-tasks.js +50 -0
- package/dist/src/cli/count-tasks.js.map +1 -0
- package/dist/src/config/ConfigManager.d.ts.map +1 -1
- package/dist/src/config/ConfigManager.js +2 -1
- package/dist/src/config/ConfigManager.js.map +1 -1
- package/dist/src/config/types.d.ts +50 -50
- package/dist/src/core/cicd/state-manager.d.ts +8 -0
- package/dist/src/core/cicd/state-manager.d.ts.map +1 -1
- package/dist/src/core/cicd/state-manager.js +60 -15
- package/dist/src/core/cicd/state-manager.js.map +1 -1
- package/dist/src/core/cost-tracker.d.ts.map +1 -1
- package/dist/src/core/cost-tracker.js +2 -1
- package/dist/src/core/cost-tracker.js.map +1 -1
- package/dist/src/core/iac/template-engine.d.ts.map +1 -1
- package/dist/src/core/iac/template-engine.js +28 -0
- package/dist/src/core/iac/template-engine.js.map +1 -1
- package/dist/src/core/iac/template-generator.d.ts +53 -0
- package/dist/src/core/iac/template-generator.d.ts.map +1 -0
- package/dist/src/core/iac/template-generator.js +125 -0
- package/dist/src/core/iac/template-generator.js.map +1 -0
- package/dist/src/core/increment/status-auto-transition.js +3 -3
- package/dist/src/core/increment/status-auto-transition.js.map +1 -1
- package/dist/src/core/living-docs/CodeValidator.js +1 -1
- package/dist/src/core/living-docs/CodeValidator.js.map +1 -1
- package/dist/src/core/living-docs/content-distributor.d.ts.map +1 -1
- package/dist/src/core/living-docs/content-distributor.js +11 -1
- package/dist/src/core/living-docs/content-distributor.js.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.d.ts +166 -0
- package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -0
- package/dist/src/core/living-docs/living-docs-sync.js +726 -0
- package/dist/src/core/living-docs/living-docs-sync.js.map +1 -0
- package/dist/src/core/living-docs/task-project-specific-generator.d.ts +7 -3
- package/dist/src/core/living-docs/task-project-specific-generator.d.ts.map +1 -1
- package/dist/src/core/living-docs/task-project-specific-generator.js +40 -24
- package/dist/src/core/living-docs/task-project-specific-generator.js.map +1 -1
- package/dist/src/core/plugin-loader.d.ts +7 -0
- package/dist/src/core/plugin-loader.d.ts.map +1 -1
- package/dist/src/core/plugin-loader.js +18 -1
- package/dist/src/core/plugin-loader.js.map +1 -1
- package/dist/src/core/serverless/platform-data-loader.d.ts +8 -0
- package/dist/src/core/serverless/platform-data-loader.d.ts.map +1 -1
- package/dist/src/core/serverless/platform-data-loader.js +14 -0
- package/dist/src/core/serverless/platform-data-loader.js.map +1 -1
- package/dist/src/core/serverless/types.d.ts +1 -1
- package/dist/src/core/serverless/types.d.ts.map +1 -1
- package/dist/src/core/status-line/status-line-manager.d.ts +6 -2
- package/dist/src/core/status-line/status-line-manager.d.ts.map +1 -1
- package/dist/src/core/status-line/status-line-manager.js +11 -5
- package/dist/src/core/status-line/status-line-manager.js.map +1 -1
- package/dist/src/core/status-line/task-counter.d.ts +69 -0
- package/dist/src/core/status-line/task-counter.d.ts.map +1 -0
- package/dist/src/core/status-line/task-counter.js +107 -0
- package/dist/src/core/status-line/task-counter.js.map +1 -0
- package/dist/src/core/workflow/autonomous-executor.d.ts +111 -0
- package/dist/src/core/workflow/autonomous-executor.d.ts.map +1 -0
- package/dist/src/core/workflow/autonomous-executor.js +275 -0
- package/dist/src/core/workflow/autonomous-executor.js.map +1 -0
- package/dist/src/core/workflow/backlog-scanner.d.ts +94 -0
- package/dist/src/core/workflow/backlog-scanner.d.ts.map +1 -0
- package/dist/src/core/workflow/backlog-scanner.js +170 -0
- package/dist/src/core/workflow/backlog-scanner.js.map +1 -0
- package/dist/src/core/workflow/command-invoker.d.ts +86 -0
- package/dist/src/core/workflow/command-invoker.d.ts.map +1 -0
- package/dist/src/core/workflow/command-invoker.js +131 -0
- package/dist/src/core/workflow/command-invoker.js.map +1 -0
- package/dist/src/core/workflow/cost-estimator.d.ts +120 -0
- package/dist/src/core/workflow/cost-estimator.d.ts.map +1 -0
- package/dist/src/core/workflow/cost-estimator.js +222 -0
- package/dist/src/core/workflow/cost-estimator.js.map +1 -0
- package/dist/src/core/workflow/index.d.ts +20 -0
- package/dist/src/core/workflow/index.d.ts.map +1 -0
- package/dist/src/core/workflow/index.js +24 -0
- package/dist/src/core/workflow/index.js.map +1 -0
- package/dist/src/core/workflow/state-manager.d.ts +107 -0
- package/dist/src/core/workflow/state-manager.d.ts.map +1 -0
- package/dist/src/core/workflow/state-manager.js +126 -0
- package/dist/src/core/workflow/state-manager.js.map +1 -0
- package/dist/src/core/workflow/workflow-orchestrator.d.ts +93 -0
- package/dist/src/core/workflow/workflow-orchestrator.d.ts.map +1 -0
- package/dist/src/core/workflow/workflow-orchestrator.js +195 -0
- package/dist/src/core/workflow/workflow-orchestrator.js.map +1 -0
- package/dist/src/init/architecture/types.d.ts +10 -10
- package/dist/src/metrics/dora-calculator.js +2 -2
- package/dist/src/metrics/dora-calculator.js.map +1 -1
- package/dist/src/utils/pricing-constants.d.ts +5 -2
- package/dist/src/utils/pricing-constants.d.ts.map +1 -1
- package/dist/src/utils/pricing-constants.js +3 -2
- package/dist/src/utils/pricing-constants.js.map +1 -1
- package/package.json +4 -4
- package/plugins/specweave/agents/infrastructure/AGENT.md +88 -46
- package/plugins/specweave/agents/pm/AGENT.md +58 -1
- package/plugins/specweave/commands/specweave-archive-features.md +1 -1
- package/plugins/specweave/commands/specweave-archive-increments.md +1 -1
- package/plugins/specweave/commands/specweave-check-hooks.md +5 -0
- package/plugins/specweave/commands/specweave-done.md +12 -0
- package/plugins/specweave/commands/specweave-plan.md +1 -1
- package/plugins/specweave/commands/specweave-progress.md +108 -379
- package/plugins/specweave/commands/specweave-reopen.md +1 -1
- package/plugins/specweave/commands/specweave-restore-feature.md +1 -1
- package/plugins/specweave/commands/specweave-sync-specs.md +20 -48
- package/plugins/specweave/hooks/lib/update-status-line.sh +44 -35
- package/plugins/specweave/hooks/lib/validate-spec-status.sh +163 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh +17 -35
- package/plugins/specweave/lib/hooks/update-tasks-md.js +52 -9
- package/plugins/specweave/lib/hooks/update-tasks-md.ts +77 -16
- package/plugins/specweave/templates/iac/aws-lambda/defaults.json +24 -0
- package/plugins/specweave/templates/iac/aws-lambda/templates/README.md.hbs +260 -0
- package/plugins/specweave/templates/iac/aws-lambda/templates/environments/dev.tfvars.hbs +34 -0
- package/plugins/specweave/templates/iac/aws-lambda/templates/environments/prod.tfvars.hbs +37 -0
- package/plugins/specweave/templates/iac/aws-lambda/templates/environments/staging.tfvars.hbs +35 -0
- package/plugins/specweave/templates/iac/aws-lambda/templates/outputs.tf.hbs +77 -0
- package/plugins/specweave/templates/iac/aws-lambda/templates/providers.tf.hbs +36 -0
- package/plugins/specweave/templates/iac/aws-lambda/templates/variables.tf.hbs +115 -0
- package/plugins/specweave/templates/iac/azure-functions/defaults.json +25 -0
- package/plugins/specweave/templates/iac/azure-functions/templates/README.md.hbs +268 -0
- package/plugins/specweave/templates/iac/azure-functions/templates/environments/dev.tfvars.hbs +34 -0
- package/plugins/specweave/templates/iac/azure-functions/templates/environments/prod.tfvars.hbs +46 -0
- package/plugins/specweave/templates/iac/azure-functions/templates/environments/staging.tfvars.hbs +34 -0
- package/plugins/specweave/templates/iac/azure-functions/templates/main.tf.hbs +225 -0
- package/plugins/specweave/templates/iac/azure-functions/templates/outputs.tf.hbs +89 -0
- package/plugins/specweave/templates/iac/azure-functions/templates/provider.tf.hbs +27 -0
- package/plugins/specweave/templates/iac/azure-functions/templates/providers.tf.hbs +35 -0
- package/plugins/specweave/templates/iac/azure-functions/templates/variables.tf.hbs +124 -0
- package/plugins/specweave/templates/iac/firebase/defaults.json +29 -0
- package/plugins/specweave/templates/iac/firebase/templates/README.md.hbs +35 -0
- package/plugins/specweave/templates/iac/firebase/templates/environments/dev.tfvars.hbs +7 -0
- package/plugins/specweave/templates/iac/firebase/templates/environments/prod.tfvars.hbs +7 -0
- package/plugins/specweave/templates/iac/firebase/templates/environments/staging.tfvars.hbs +7 -0
- package/plugins/specweave/templates/iac/firebase/templates/main.tf.hbs +90 -0
- package/plugins/specweave/templates/iac/firebase/templates/outputs.tf.hbs +15 -0
- package/plugins/specweave/templates/iac/firebase/templates/providers.tf.hbs +23 -0
- package/plugins/specweave/templates/iac/firebase/templates/variables.tf.hbs +42 -0
- package/plugins/specweave/templates/iac/gcp-cloud-functions/defaults.json +26 -0
- package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/README.md.hbs +299 -0
- package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/environments/dev.tfvars.hbs +36 -0
- package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/environments/prod.tfvars.hbs +48 -0
- package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/environments/staging.tfvars.hbs +41 -0
- package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/main.tf.hbs +192 -0
- package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/outputs.tf.hbs +66 -0
- package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/providers.tf.hbs +25 -0
- package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/variables.tf.hbs +119 -0
- package/plugins/specweave/templates/iac/supabase/defaults.json +15 -0
- package/plugins/specweave/templates/iac/supabase/templates/README.md.hbs +46 -0
- package/plugins/specweave/templates/iac/supabase/templates/main.tf.hbs +50 -0
- package/plugins/specweave-github/agents/github-manager/AGENT.md +39 -7
- package/plugins/specweave-github/commands/specweave-github-create-issue.md +5 -5
- package/plugins/specweave-github/lib/CodeValidator.ts +1 -1
- package/plugins/specweave-github/lib/github-client-v2.js +29 -0
- package/plugins/specweave-github/lib/github-client-v2.ts +30 -0
- package/plugins/specweave-github/lib/task-sync.js +4 -0
- package/plugins/specweave-github/lib/task-sync.ts +7 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +3 -3
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +2022 -0
- package/src/templates/CLAUDE.md.template +31 -0
- package/dist/src/core/living-docs/ThreeLayerSyncManager.d.ts +0 -116
- package/dist/src/core/living-docs/ThreeLayerSyncManager.d.ts.map +0 -1
- package/dist/src/core/living-docs/ThreeLayerSyncManager.js +0 -356
- package/dist/src/core/living-docs/ThreeLayerSyncManager.js.map +0 -1
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Living Docs Sync - Simplified sync mechanism
|
|
3
|
+
*
|
|
4
|
+
* Syncs increment specs to living docs structure:
|
|
5
|
+
* - .specweave/docs/internal/specs/_features/FS-XXX/FEATURE.md
|
|
6
|
+
* - .specweave/docs/internal/specs/specweave/FS-XXX/README.md
|
|
7
|
+
* - .specweave/docs/internal/specs/specweave/FS-XXX/us-*.md
|
|
8
|
+
*
|
|
9
|
+
* Uses FeatureIDManager for automatic feature ID assignment (greenfield vs brownfield)
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'fs-extra';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import yaml from 'yaml';
|
|
14
|
+
import { FeatureIDManager } from './feature-id-manager.js';
|
|
15
|
+
import { TaskProjectSpecificGenerator } from './task-project-specific-generator.js';
|
|
16
|
+
export class LivingDocsSync {
|
|
17
|
+
constructor(projectRoot) {
|
|
18
|
+
this.projectRoot = projectRoot;
|
|
19
|
+
this.featureIdManager = new FeatureIDManager(projectRoot);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Sync an increment to living docs
|
|
23
|
+
*/
|
|
24
|
+
async syncIncrement(incrementId, options = {}) {
|
|
25
|
+
const result = {
|
|
26
|
+
success: false,
|
|
27
|
+
featureId: '',
|
|
28
|
+
incrementId,
|
|
29
|
+
filesCreated: [],
|
|
30
|
+
filesUpdated: [],
|
|
31
|
+
errors: []
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
// Step 1: Build feature registry (auto-generates IDs for greenfield)
|
|
35
|
+
await this.featureIdManager.buildRegistry();
|
|
36
|
+
// Step 2: Get/assign feature ID
|
|
37
|
+
const featureId = await this.getFeatureIdForIncrement(incrementId);
|
|
38
|
+
result.featureId = featureId;
|
|
39
|
+
console.log(`📚 Syncing ${incrementId} → ${featureId}...`);
|
|
40
|
+
// Step 3: Parse increment spec
|
|
41
|
+
const parsed = await this.parseIncrementSpec(incrementId);
|
|
42
|
+
// Step 4: Create living docs structure
|
|
43
|
+
const basePath = path.join(this.projectRoot, '.specweave/docs/internal/specs');
|
|
44
|
+
// Create _features/FS-XXX/FEATURE.md
|
|
45
|
+
const featurePath = path.join(basePath, '_features', featureId);
|
|
46
|
+
const featureFile = path.join(featurePath, 'FEATURE.md');
|
|
47
|
+
if (!options.dryRun) {
|
|
48
|
+
await fs.ensureDir(featurePath);
|
|
49
|
+
const featureContent = this.generateFeatureFile(featureId, parsed, incrementId);
|
|
50
|
+
await fs.writeFile(featureFile, featureContent, 'utf-8');
|
|
51
|
+
result.filesCreated.push(featureFile);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
result.filesCreated.push(featureFile + ' (dry-run)');
|
|
55
|
+
}
|
|
56
|
+
// Create specweave/FS-XXX/README.md
|
|
57
|
+
const projectPath = path.join(basePath, 'specweave', featureId);
|
|
58
|
+
const readmePath = path.join(projectPath, 'README.md');
|
|
59
|
+
if (!options.dryRun) {
|
|
60
|
+
await fs.ensureDir(projectPath);
|
|
61
|
+
const readmeContent = this.generateReadmeFile(featureId, parsed, incrementId);
|
|
62
|
+
await fs.writeFile(readmePath, readmeContent, 'utf-8');
|
|
63
|
+
result.filesCreated.push(readmePath);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
result.filesCreated.push(readmePath + ' (dry-run)');
|
|
67
|
+
}
|
|
68
|
+
// Create user story files
|
|
69
|
+
for (const story of parsed.userStories) {
|
|
70
|
+
// CRITICAL: Find existing file by US-ID first to prevent duplicates
|
|
71
|
+
const existingFile = await this.findExistingUserStoryFile(projectPath, story.id);
|
|
72
|
+
let storyFile;
|
|
73
|
+
if (existingFile) {
|
|
74
|
+
// Reuse existing file (prevent duplicate creation)
|
|
75
|
+
storyFile = path.join(projectPath, existingFile);
|
|
76
|
+
console.log(` ♻️ Reusing existing file: ${existingFile}`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// Create new file with standardized naming
|
|
80
|
+
const storySlug = story.title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
81
|
+
storyFile = path.join(projectPath, `${story.id.toLowerCase()}-${storySlug}.md`);
|
|
82
|
+
}
|
|
83
|
+
if (!options.dryRun) {
|
|
84
|
+
const storyContent = this.generateUserStoryFile(story, featureId, incrementId, parsed);
|
|
85
|
+
await fs.writeFile(storyFile, storyContent, 'utf-8');
|
|
86
|
+
result.filesCreated.push(storyFile);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
result.filesCreated.push(storyFile + ' (dry-run)');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Step 5: Clean up duplicates and temp files BEFORE syncing tasks
|
|
93
|
+
if (!options.dryRun) {
|
|
94
|
+
await this.cleanupDuplicateFiles(featureId, projectPath);
|
|
95
|
+
await this.cleanupTempFiles(projectPath);
|
|
96
|
+
}
|
|
97
|
+
// Step 6: Sync tasks from increment to user stories
|
|
98
|
+
if (!options.dryRun) {
|
|
99
|
+
await this.syncTasksToUserStories(incrementId, featureId, parsed.userStories, projectPath);
|
|
100
|
+
}
|
|
101
|
+
// Step 7: Sync to external tools (GitHub, JIRA, ADO)
|
|
102
|
+
if (!options.dryRun) {
|
|
103
|
+
await this.syncToExternalTools(incrementId, featureId, projectPath);
|
|
104
|
+
}
|
|
105
|
+
// Step 8: Final cleanup (remove any temp files created during sync)
|
|
106
|
+
if (!options.dryRun) {
|
|
107
|
+
await this.cleanupTempFiles(projectPath);
|
|
108
|
+
}
|
|
109
|
+
result.success = true;
|
|
110
|
+
console.log(`✅ Synced ${incrementId} → ${featureId}`);
|
|
111
|
+
console.log(` Created: ${result.filesCreated.length} files`);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
result.errors.push(`Sync failed: ${error}`);
|
|
116
|
+
console.error(`❌ Sync failed for ${incrementId}:`, error);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get feature ID for increment (auto-generates for greenfield)
|
|
122
|
+
*
|
|
123
|
+
* CRITICAL: Validates feature ID format matches increment type (greenfield vs brownfield)
|
|
124
|
+
* - Greenfield (SpecWeave-native): FS-XXX (e.g., FS-031, FS-043)
|
|
125
|
+
* - Brownfield (imported): FS-YY-MM-DD-name (e.g., FS-25-11-14-jira-epic)
|
|
126
|
+
*/
|
|
127
|
+
async getFeatureIdForIncrement(incrementId) {
|
|
128
|
+
// Extract increment number (e.g., "0040-name" → 40)
|
|
129
|
+
const match = incrementId.match(/^(\d{4})-/);
|
|
130
|
+
if (!match) {
|
|
131
|
+
throw new Error(`Invalid increment ID format: ${incrementId}`);
|
|
132
|
+
}
|
|
133
|
+
const num = parseInt(match[1], 10);
|
|
134
|
+
// Check if increment has explicit feature ID
|
|
135
|
+
const metadataPath = path.join(this.projectRoot, '.specweave/increments', incrementId, 'metadata.json');
|
|
136
|
+
if (await fs.pathExists(metadataPath)) {
|
|
137
|
+
const metadata = await fs.readJson(metadataPath);
|
|
138
|
+
// Check if brownfield (imported from external tool)
|
|
139
|
+
const isBrownfield = metadata.imported === true || metadata.source === 'external';
|
|
140
|
+
if (metadata.feature) {
|
|
141
|
+
// Validate format matches increment type
|
|
142
|
+
const isDateFormat = /^FS-\d{2}-\d{2}-\d{2}/.test(metadata.feature);
|
|
143
|
+
const isIncrementFormat = /^FS-\d{3}$/.test(metadata.feature);
|
|
144
|
+
if (isBrownfield && isDateFormat) {
|
|
145
|
+
// ✅ Brownfield with correct date format
|
|
146
|
+
return metadata.feature;
|
|
147
|
+
}
|
|
148
|
+
else if (!isBrownfield && isIncrementFormat) {
|
|
149
|
+
// ✅ Greenfield with correct increment format
|
|
150
|
+
return metadata.feature;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// ⚠️ Format mismatch - log warning and auto-generate correct format
|
|
154
|
+
console.warn(`⚠️ Feature ID format mismatch for ${incrementId}:`);
|
|
155
|
+
console.warn(` Found: ${metadata.feature}`);
|
|
156
|
+
console.warn(` Expected: ${isBrownfield ? 'FS-YY-MM-DD-name (brownfield)' : 'FS-XXX (greenfield)'}`);
|
|
157
|
+
console.warn(` Auto-generating correct format...`);
|
|
158
|
+
// Fall through to auto-generation
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Auto-generate for greenfield: FS-040, FS-041, etc.
|
|
163
|
+
const autoGenId = `FS-${String(num).padStart(3, '0')}`;
|
|
164
|
+
console.log(` 📝 Generated feature ID: ${autoGenId}`);
|
|
165
|
+
return autoGenId;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Parse increment spec.md
|
|
169
|
+
*/
|
|
170
|
+
async parseIncrementSpec(incrementId) {
|
|
171
|
+
const specPath = path.join(this.projectRoot, '.specweave/increments', incrementId, 'spec.md');
|
|
172
|
+
if (!await fs.pathExists(specPath)) {
|
|
173
|
+
throw new Error(`Spec file not found: ${specPath}`);
|
|
174
|
+
}
|
|
175
|
+
const content = await fs.readFile(specPath, 'utf-8');
|
|
176
|
+
// Extract frontmatter
|
|
177
|
+
let frontmatter = {};
|
|
178
|
+
let bodyContent = content;
|
|
179
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
180
|
+
if (frontmatterMatch) {
|
|
181
|
+
try {
|
|
182
|
+
frontmatter = yaml.parse(frontmatterMatch[1]) || {};
|
|
183
|
+
bodyContent = content.slice(frontmatterMatch[0].length).trim();
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
console.warn(`Failed to parse frontmatter for ${incrementId}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Extract title
|
|
190
|
+
let title = frontmatter.title || '';
|
|
191
|
+
if (!title) {
|
|
192
|
+
const headingMatch = bodyContent.match(/^#\s+(.+)$/m);
|
|
193
|
+
if (headingMatch) {
|
|
194
|
+
title = headingMatch[1].replace(/^(SPEC-\d+:|Increment\s+\d+:)\s*/, '').trim();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!title) {
|
|
198
|
+
title = incrementId.replace(/^\d+-/, '').split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
199
|
+
}
|
|
200
|
+
// Extract overview
|
|
201
|
+
let overview = '';
|
|
202
|
+
const overviewMatch = bodyContent.match(/##\s+(?:Overview|Problem Statement|Quick Overview)\s*\n+([\s\S]*?)(?=\n##|\Z)/i);
|
|
203
|
+
if (overviewMatch) {
|
|
204
|
+
overview = overviewMatch[1].trim().split('\n\n')[0];
|
|
205
|
+
}
|
|
206
|
+
// Extract user stories
|
|
207
|
+
const userStories = this.extractUserStories(bodyContent);
|
|
208
|
+
// Extract acceptance criteria
|
|
209
|
+
const acceptanceCriteria = this.extractAcceptanceCriteria(bodyContent);
|
|
210
|
+
return {
|
|
211
|
+
title,
|
|
212
|
+
overview,
|
|
213
|
+
status: frontmatter.status || 'planning',
|
|
214
|
+
priority: frontmatter.priority || 'P1',
|
|
215
|
+
created: frontmatter.created || new Date().toISOString().split('T')[0],
|
|
216
|
+
userStories,
|
|
217
|
+
acceptanceCriteria,
|
|
218
|
+
frontmatter
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Extract user stories from spec content
|
|
223
|
+
*/
|
|
224
|
+
extractUserStories(content) {
|
|
225
|
+
const stories = [];
|
|
226
|
+
const lines = content.split('\n');
|
|
227
|
+
for (let i = 0; i < lines.length; i++) {
|
|
228
|
+
const headingMatch = lines[i].match(/^###+\s+(US-\d+):\s+(.+)/);
|
|
229
|
+
if (!headingMatch)
|
|
230
|
+
continue;
|
|
231
|
+
const id = headingMatch[1];
|
|
232
|
+
const title = headingMatch[2];
|
|
233
|
+
// Collect all lines until next US heading or end
|
|
234
|
+
const storyLines = [];
|
|
235
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
236
|
+
if (lines[j].match(/^###+\s+US-\d+:/)) {
|
|
237
|
+
break; // Found next US heading
|
|
238
|
+
}
|
|
239
|
+
storyLines.push(lines[j]);
|
|
240
|
+
}
|
|
241
|
+
const storyContent = storyLines.join('\n');
|
|
242
|
+
// Extract description
|
|
243
|
+
let description = '';
|
|
244
|
+
const descMatch = storyContent.match(/\*\*As a\*\*\s*([^\n]+)\s*\n\*\*I want\*\*\s*([^\n]+)\s*\n\*\*So that\*\*\s*([^\n]+)/i);
|
|
245
|
+
if (descMatch) {
|
|
246
|
+
description = `**As a** ${descMatch[1].trim()}\n**I want** ${descMatch[2].trim()}\n**So that** ${descMatch[3].trim()}`;
|
|
247
|
+
}
|
|
248
|
+
// Extract acceptance criteria IDs
|
|
249
|
+
const acIds = [];
|
|
250
|
+
const acPattern = /AC-US\d+-\d+/g;
|
|
251
|
+
let acMatch;
|
|
252
|
+
while ((acMatch = acPattern.exec(storyContent)) !== null) {
|
|
253
|
+
if (!acIds.includes(acMatch[0])) {
|
|
254
|
+
acIds.push(acMatch[0]);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
stories.push({
|
|
258
|
+
id,
|
|
259
|
+
title,
|
|
260
|
+
description,
|
|
261
|
+
acceptanceCriteria: acIds
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return stories;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Extract acceptance criteria from spec content
|
|
268
|
+
*/
|
|
269
|
+
extractAcceptanceCriteria(content) {
|
|
270
|
+
const criteria = [];
|
|
271
|
+
// Pattern: - [x] AC-US1-01: Description
|
|
272
|
+
const acPattern = /^[-*]\s+\[([ x])\]\s+(AC-US\d+-\d+):\s+(.+?)$/gm;
|
|
273
|
+
let match;
|
|
274
|
+
while ((match = acPattern.exec(content)) !== null) {
|
|
275
|
+
const completed = match[1] === 'x';
|
|
276
|
+
const id = match[2];
|
|
277
|
+
const description = match[3];
|
|
278
|
+
// Extract user story ID (AC-US1-01 → US-001)
|
|
279
|
+
const usMatch = id.match(/AC-US(\d+)-\d+/);
|
|
280
|
+
const userStoryId = usMatch ? `US-${usMatch[1].padStart(3, '0')}` : '';
|
|
281
|
+
criteria.push({
|
|
282
|
+
id,
|
|
283
|
+
userStoryId,
|
|
284
|
+
description,
|
|
285
|
+
completed
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return criteria;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Generate FEATURE.md content
|
|
292
|
+
*/
|
|
293
|
+
generateFeatureFile(featureId, parsed, incrementId) {
|
|
294
|
+
const lines = [];
|
|
295
|
+
// Frontmatter
|
|
296
|
+
lines.push('---');
|
|
297
|
+
lines.push(`id: ${featureId}`);
|
|
298
|
+
lines.push(`title: "${parsed.title}"`);
|
|
299
|
+
lines.push(`type: feature`);
|
|
300
|
+
lines.push(`status: ${parsed.status}`);
|
|
301
|
+
lines.push(`priority: ${parsed.priority}`);
|
|
302
|
+
lines.push(`created: ${parsed.created}`);
|
|
303
|
+
lines.push(`lastUpdated: ${new Date().toISOString().split('T')[0]}`);
|
|
304
|
+
lines.push('---');
|
|
305
|
+
lines.push('');
|
|
306
|
+
// Title
|
|
307
|
+
lines.push(`# ${parsed.title}`);
|
|
308
|
+
lines.push('');
|
|
309
|
+
// Overview
|
|
310
|
+
if (parsed.overview) {
|
|
311
|
+
lines.push('## Overview');
|
|
312
|
+
lines.push('');
|
|
313
|
+
lines.push(parsed.overview);
|
|
314
|
+
lines.push('');
|
|
315
|
+
}
|
|
316
|
+
// Implementation History
|
|
317
|
+
lines.push('## Implementation History');
|
|
318
|
+
lines.push('');
|
|
319
|
+
lines.push('| Increment | Status | Completion Date |');
|
|
320
|
+
lines.push('|-----------|--------|----------------|');
|
|
321
|
+
const statusEmoji = parsed.status === 'completed' ? '✅' : '⏳';
|
|
322
|
+
lines.push(`| [${incrementId}](../../../../increments/${incrementId}/spec.md) | ${statusEmoji} ${parsed.status} | ${parsed.created} |`);
|
|
323
|
+
lines.push('');
|
|
324
|
+
// User Stories
|
|
325
|
+
if (parsed.userStories.length > 0) {
|
|
326
|
+
lines.push('## User Stories');
|
|
327
|
+
lines.push('');
|
|
328
|
+
for (const story of parsed.userStories) {
|
|
329
|
+
const storySlug = story.title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
330
|
+
const storyFile = `../../specweave/${featureId}/${story.id.toLowerCase()}-${storySlug}.md`;
|
|
331
|
+
lines.push(`- [${story.id}: ${story.title}](${storyFile})`);
|
|
332
|
+
}
|
|
333
|
+
lines.push('');
|
|
334
|
+
}
|
|
335
|
+
return lines.join('\n');
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Generate README.md content
|
|
339
|
+
*/
|
|
340
|
+
generateReadmeFile(featureId, parsed, incrementId) {
|
|
341
|
+
const lines = [];
|
|
342
|
+
lines.push('---');
|
|
343
|
+
lines.push(`id: ${featureId}-specweave`);
|
|
344
|
+
lines.push(`title: "${parsed.title} - SpecWeave Implementation"`);
|
|
345
|
+
lines.push(`feature: ${featureId}`);
|
|
346
|
+
lines.push(`project: specweave`);
|
|
347
|
+
lines.push(`type: feature-context`);
|
|
348
|
+
lines.push(`status: ${parsed.status}`);
|
|
349
|
+
lines.push('---');
|
|
350
|
+
lines.push('');
|
|
351
|
+
lines.push(`# ${parsed.title}`);
|
|
352
|
+
lines.push('');
|
|
353
|
+
lines.push(`**Feature**: [${featureId}](../../_features/${featureId}/FEATURE.md)`);
|
|
354
|
+
lines.push('');
|
|
355
|
+
if (parsed.overview) {
|
|
356
|
+
lines.push('## Overview');
|
|
357
|
+
lines.push('');
|
|
358
|
+
lines.push(parsed.overview);
|
|
359
|
+
lines.push('');
|
|
360
|
+
}
|
|
361
|
+
lines.push('## User Stories');
|
|
362
|
+
lines.push('');
|
|
363
|
+
lines.push('See user story files in this directory.');
|
|
364
|
+
lines.push('');
|
|
365
|
+
return lines.join('\n');
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Generate user story file content
|
|
369
|
+
*/
|
|
370
|
+
generateUserStoryFile(story, featureId, incrementId, parsed) {
|
|
371
|
+
const lines = [];
|
|
372
|
+
// Frontmatter
|
|
373
|
+
lines.push('---');
|
|
374
|
+
lines.push(`id: ${story.id}`);
|
|
375
|
+
lines.push(`feature: ${featureId}`);
|
|
376
|
+
lines.push(`title: "${story.title}"`);
|
|
377
|
+
lines.push(`status: ${parsed.status}`);
|
|
378
|
+
lines.push(`priority: ${parsed.priority}`);
|
|
379
|
+
lines.push(`created: ${parsed.created}`);
|
|
380
|
+
lines.push('---');
|
|
381
|
+
lines.push('');
|
|
382
|
+
// Title
|
|
383
|
+
lines.push(`# ${story.id}: ${story.title}`);
|
|
384
|
+
lines.push('');
|
|
385
|
+
// Feature link
|
|
386
|
+
lines.push(`**Feature**: [${featureId}](../../_features/${featureId}/FEATURE.md)`);
|
|
387
|
+
lines.push('');
|
|
388
|
+
// Description
|
|
389
|
+
if (story.description) {
|
|
390
|
+
lines.push(story.description);
|
|
391
|
+
lines.push('');
|
|
392
|
+
}
|
|
393
|
+
lines.push('---');
|
|
394
|
+
lines.push('');
|
|
395
|
+
// Acceptance Criteria
|
|
396
|
+
lines.push('## Acceptance Criteria');
|
|
397
|
+
lines.push('');
|
|
398
|
+
const storyCriteria = parsed.acceptanceCriteria.filter(ac => ac.userStoryId === story.id);
|
|
399
|
+
if (storyCriteria.length > 0) {
|
|
400
|
+
for (const ac of storyCriteria) {
|
|
401
|
+
const checkbox = ac.completed ? '[x]' : '[ ]';
|
|
402
|
+
lines.push(`- ${checkbox} **${ac.id}**: ${ac.description}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
lines.push('No acceptance criteria defined.');
|
|
407
|
+
}
|
|
408
|
+
lines.push('');
|
|
409
|
+
lines.push('---');
|
|
410
|
+
lines.push('');
|
|
411
|
+
// Implementation
|
|
412
|
+
lines.push('## Implementation');
|
|
413
|
+
lines.push('');
|
|
414
|
+
lines.push(`**Increment**: [${incrementId}](../../../../increments/${incrementId}/spec.md)`);
|
|
415
|
+
lines.push('');
|
|
416
|
+
lines.push('**Tasks**: See increment tasks.md for implementation details.');
|
|
417
|
+
lines.push('');
|
|
418
|
+
return lines.join('\n');
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Sync tasks from increment to user story files
|
|
422
|
+
*
|
|
423
|
+
* Populates the ## Tasks section in each user story file with tasks from increment tasks.md
|
|
424
|
+
*/
|
|
425
|
+
async syncTasksToUserStories(incrementId, featureId, userStories, projectPath) {
|
|
426
|
+
const taskGenerator = new TaskProjectSpecificGenerator(this.projectRoot);
|
|
427
|
+
for (const story of userStories) {
|
|
428
|
+
try {
|
|
429
|
+
// Generate project-specific tasks for this user story
|
|
430
|
+
const tasks = await taskGenerator.generateProjectSpecificTasks(incrementId, story.id, // e.g., "US-001"
|
|
431
|
+
undefined // No project filter (use all tasks mapped to this user story)
|
|
432
|
+
);
|
|
433
|
+
// Format tasks as markdown
|
|
434
|
+
const tasksMarkdown = taskGenerator.formatTasksAsMarkdown(tasks);
|
|
435
|
+
// Update user story file
|
|
436
|
+
const storySlug = story.title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
437
|
+
const storyFile = path.join(projectPath, `${story.id.toLowerCase()}-${storySlug}.md`);
|
|
438
|
+
await this.updateTasksSection(storyFile, tasksMarkdown);
|
|
439
|
+
console.log(` ✅ Synced ${tasks.length} tasks to ${story.id}`);
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
console.error(` ⚠️ Failed to sync tasks for ${story.id}:`, error);
|
|
443
|
+
// Continue with other user stories even if one fails
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Update ## Tasks section in user story file
|
|
449
|
+
*/
|
|
450
|
+
async updateTasksSection(userStoryFile, tasksMarkdown) {
|
|
451
|
+
const content = await fs.readFile(userStoryFile, 'utf-8');
|
|
452
|
+
// Replace existing ## Tasks section
|
|
453
|
+
const tasksRegex = /##\s+Tasks\s+([\s\S]*?)(?=\n##|$)/;
|
|
454
|
+
if (tasksRegex.test(content)) {
|
|
455
|
+
// Replace existing section
|
|
456
|
+
const updatedContent = content.replace(tasksRegex, `## Tasks\n\n${tasksMarkdown}\n`);
|
|
457
|
+
await fs.writeFile(userStoryFile, updatedContent, 'utf-8');
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
// Add new section before "Related" section or at the end
|
|
461
|
+
const relatedRegex = /\n---\n\n\*\*Related\*\*:/;
|
|
462
|
+
if (relatedRegex.test(content)) {
|
|
463
|
+
// Insert before "Related" section
|
|
464
|
+
const updatedContent = content.replace(relatedRegex, `\n\n## Tasks\n\n${tasksMarkdown}\n\n---\n\n**Related**:`);
|
|
465
|
+
await fs.writeFile(userStoryFile, updatedContent, 'utf-8');
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
// Append at the end
|
|
469
|
+
const updatedContent = content + `\n\n## Tasks\n\n${tasksMarkdown}\n`;
|
|
470
|
+
await fs.writeFile(userStoryFile, updatedContent, 'utf-8');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Sync to external tools (GitHub, JIRA, ADO)
|
|
476
|
+
*
|
|
477
|
+
* AC-US5-01: Detect external tool configuration from metadata.json
|
|
478
|
+
* AC-US5-02: When GitHub configured, trigger GitHub sync
|
|
479
|
+
* AC-US5-03: When no external tools configured, skip
|
|
480
|
+
* AC-US5-05: External tool failures don't break living docs sync
|
|
481
|
+
*/
|
|
482
|
+
async syncToExternalTools(incrementId, featureId, projectPath) {
|
|
483
|
+
try {
|
|
484
|
+
// 1. Detect external tool configuration from metadata.json
|
|
485
|
+
const externalTools = await this.detectExternalTools(incrementId);
|
|
486
|
+
if (externalTools.length === 0) {
|
|
487
|
+
// AC-US5-03: No external tools configured, skip
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
console.log(`\n📡 Syncing to external tools: ${externalTools.join(', ')}`);
|
|
491
|
+
// 2. Sync to each configured external tool
|
|
492
|
+
for (const tool of externalTools) {
|
|
493
|
+
try {
|
|
494
|
+
switch (tool) {
|
|
495
|
+
case 'github':
|
|
496
|
+
await this.syncToGitHub(featureId, projectPath);
|
|
497
|
+
break;
|
|
498
|
+
case 'jira':
|
|
499
|
+
await this.syncToJira(featureId, projectPath);
|
|
500
|
+
break;
|
|
501
|
+
case 'ado':
|
|
502
|
+
await this.syncToADO(featureId, projectPath);
|
|
503
|
+
break;
|
|
504
|
+
default:
|
|
505
|
+
console.warn(` ⚠️ Unknown external tool: ${tool}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
// AC-US5-05: External tool failures are logged but don't break living docs sync
|
|
510
|
+
console.error(` ⚠️ Failed to sync to ${tool}:`, error);
|
|
511
|
+
console.error(` Living docs sync will continue...`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
// AC-US5-05: External tool failures don't break living docs sync
|
|
517
|
+
console.error(` ⚠️ External tool sync failed:`, error);
|
|
518
|
+
console.error(` Living docs sync completed successfully despite external tool errors`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Detect external tool configuration from increment metadata.json
|
|
523
|
+
*
|
|
524
|
+
* AC-US5-01: Detect external tool configuration from metadata.json
|
|
525
|
+
*
|
|
526
|
+
* Returns: Array of tool names (['github'], ['github', 'jira'], or [])
|
|
527
|
+
*/
|
|
528
|
+
async detectExternalTools(incrementId) {
|
|
529
|
+
const metadataPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId, 'metadata.json');
|
|
530
|
+
if (!fs.existsSync(metadataPath)) {
|
|
531
|
+
return [];
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
const metadata = await fs.readJson(metadataPath);
|
|
535
|
+
const tools = [];
|
|
536
|
+
// Check for GitHub configuration
|
|
537
|
+
if (metadata.github && (metadata.github.milestone || metadata.github.user_story_issues)) {
|
|
538
|
+
tools.push('github');
|
|
539
|
+
}
|
|
540
|
+
// Check for JIRA configuration
|
|
541
|
+
if (metadata.jira) {
|
|
542
|
+
tools.push('jira');
|
|
543
|
+
}
|
|
544
|
+
// Check for ADO configuration
|
|
545
|
+
if (metadata.ado || metadata.azure_devops) {
|
|
546
|
+
tools.push('ado');
|
|
547
|
+
}
|
|
548
|
+
return tools;
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
console.warn(` ⚠️ Failed to read metadata.json: ${error}`);
|
|
552
|
+
return [];
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Sync to GitHub Issues
|
|
557
|
+
*
|
|
558
|
+
* AC-US5-02: When GitHub configured, trigger GitHub sync
|
|
559
|
+
*
|
|
560
|
+
* Uses GitHubFeatureSync.syncFeatureToGitHub() which is idempotent:
|
|
561
|
+
* - Uses existing milestone if it exists
|
|
562
|
+
* - Updates existing issues (triple idempotency check)
|
|
563
|
+
* - Only creates new issues if they don't exist
|
|
564
|
+
*/
|
|
565
|
+
async syncToGitHub(featureId, projectPath) {
|
|
566
|
+
try {
|
|
567
|
+
console.log(` 🔄 Syncing to GitHub...`);
|
|
568
|
+
// Dynamic import to avoid circular dependencies
|
|
569
|
+
const { GitHubClientV2 } = await import('../../../plugins/specweave-github/lib/github-client-v2.js');
|
|
570
|
+
const { GitHubFeatureSync } = await import('../../../plugins/specweave-github/lib/github-feature-sync.js');
|
|
571
|
+
// Load GitHub config from environment
|
|
572
|
+
const profile = {
|
|
573
|
+
provider: 'github',
|
|
574
|
+
displayName: 'GitHub',
|
|
575
|
+
config: {
|
|
576
|
+
owner: process.env.GITHUB_OWNER || '',
|
|
577
|
+
repo: process.env.GITHUB_REPO || '',
|
|
578
|
+
token: process.env.GITHUB_TOKEN || ''
|
|
579
|
+
},
|
|
580
|
+
timeRange: {
|
|
581
|
+
default: '1M', // 1 month
|
|
582
|
+
max: '3M' // 3 months
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
if (!profile.config.token || !profile.config.owner || !profile.config.repo) {
|
|
586
|
+
console.warn(` ⚠️ GitHub credentials not configured (GITHUB_TOKEN, GITHUB_OWNER, GITHUB_REPO)`);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
// Initialize GitHub client and sync
|
|
590
|
+
const client = new GitHubClientV2(profile);
|
|
591
|
+
const specsDir = path.join(this.projectRoot, '.specweave/docs/internal/specs');
|
|
592
|
+
const sync = new GitHubFeatureSync(client, specsDir, this.projectRoot);
|
|
593
|
+
// Sync feature to GitHub (idempotent - safe to run multiple times)
|
|
594
|
+
const result = await sync.syncFeatureToGitHub(featureId);
|
|
595
|
+
console.log(` ✅ Synced to GitHub: ${result.issuesUpdated} updated, ${result.issuesCreated} created`);
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
if (error instanceof Error && error.message.includes('Cannot find module')) {
|
|
599
|
+
console.warn(` ⚠️ GitHub plugin not installed - skipping GitHub sync`);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Sync to JIRA (placeholder for future implementation)
|
|
608
|
+
*/
|
|
609
|
+
async syncToJira(featureId, projectPath) {
|
|
610
|
+
console.log(` ⚠️ JIRA sync not yet implemented - skipping`);
|
|
611
|
+
// TODO: Implement JIRA sync when specweave-jira plugin is available
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Sync to Azure DevOps (placeholder for future implementation)
|
|
615
|
+
*/
|
|
616
|
+
async syncToADO(featureId, projectPath) {
|
|
617
|
+
console.log(` ⚠️ ADO sync not yet implemented - skipping`);
|
|
618
|
+
// TODO: Implement ADO sync when specweave-ado plugin is available
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Find existing user story file by US-ID
|
|
622
|
+
*
|
|
623
|
+
* Searches for any file matching pattern: us-{id}-*.md
|
|
624
|
+
* Example: US-001 matches us-001-status-line.md or us-001-status-line-priority-p1.md
|
|
625
|
+
*
|
|
626
|
+
* @param projectPath - Path to feature folder (e.g., .specweave/docs/internal/specs/specweave/FS-043)
|
|
627
|
+
* @param userStoryId - User story ID (e.g., "US-001")
|
|
628
|
+
* @returns Filename if found, null otherwise
|
|
629
|
+
*/
|
|
630
|
+
async findExistingUserStoryFile(projectPath, userStoryId) {
|
|
631
|
+
try {
|
|
632
|
+
const files = await fs.readdir(projectPath);
|
|
633
|
+
// Look for file matching: us-001-*.md (case insensitive)
|
|
634
|
+
const usIdLower = userStoryId.toLowerCase(); // us-001
|
|
635
|
+
const matchingFiles = files.filter(f => {
|
|
636
|
+
const match = f.match(/^(us-\d+)-/);
|
|
637
|
+
return match && match[1] === usIdLower;
|
|
638
|
+
});
|
|
639
|
+
if (matchingFiles.length === 0) {
|
|
640
|
+
return null; // No existing file found
|
|
641
|
+
}
|
|
642
|
+
if (matchingFiles.length === 1) {
|
|
643
|
+
return matchingFiles[0]; // Exactly one file found ✅
|
|
644
|
+
}
|
|
645
|
+
// Multiple files found - return most recent
|
|
646
|
+
console.warn(` ⚠️ Found ${matchingFiles.length} files for ${userStoryId}, using most recent`);
|
|
647
|
+
const fileTimes = await Promise.all(matchingFiles.map(async (f) => ({
|
|
648
|
+
file: f,
|
|
649
|
+
mtime: (await fs.stat(path.join(projectPath, f))).mtime.getTime()
|
|
650
|
+
})));
|
|
651
|
+
fileTimes.sort((a, b) => b.mtime - a.mtime); // Newest first
|
|
652
|
+
return fileTimes[0].file;
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
// Directory doesn't exist yet (first sync)
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Clean up duplicate user story files
|
|
661
|
+
*
|
|
662
|
+
* Strategy:
|
|
663
|
+
* 1. List all user story files in feature folder
|
|
664
|
+
* 2. Group by user story ID (US-001, US-002, etc.)
|
|
665
|
+
* 3. If multiple files found for same US:
|
|
666
|
+
* - Keep the file WITH most recent modification time
|
|
667
|
+
* - Delete older files
|
|
668
|
+
* - Log warning
|
|
669
|
+
*/
|
|
670
|
+
async cleanupDuplicateFiles(featureId, projectPath) {
|
|
671
|
+
const files = await fs.readdir(projectPath);
|
|
672
|
+
// Group files by user story ID
|
|
673
|
+
const filesByStory = new Map();
|
|
674
|
+
for (const file of files) {
|
|
675
|
+
// Match pattern: us-001-*, us-002-*, etc.
|
|
676
|
+
const match = file.match(/^(us-\d+)-/);
|
|
677
|
+
if (match) {
|
|
678
|
+
const storyId = match[1].toUpperCase(); // US-001
|
|
679
|
+
if (!filesByStory.has(storyId)) {
|
|
680
|
+
filesByStory.set(storyId, []);
|
|
681
|
+
}
|
|
682
|
+
filesByStory.get(storyId).push(file);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// Check for duplicates
|
|
686
|
+
for (const [storyId, storyFiles] of filesByStory.entries()) {
|
|
687
|
+
if (storyFiles.length > 1) {
|
|
688
|
+
console.warn(` ⚠️ Found ${storyFiles.length} duplicate files for ${storyId}`);
|
|
689
|
+
// Find the most recent file
|
|
690
|
+
const fileTimes = await Promise.all(storyFiles.map(async (f) => ({
|
|
691
|
+
file: f,
|
|
692
|
+
mtime: (await fs.stat(path.join(projectPath, f))).mtime.getTime()
|
|
693
|
+
})));
|
|
694
|
+
fileTimes.sort((a, b) => b.mtime - a.mtime); // Newest first
|
|
695
|
+
const keepFile = fileTimes[0].file;
|
|
696
|
+
const deleteFiles = fileTimes.slice(1).map(f => f.file);
|
|
697
|
+
console.warn(` → Keeping: ${keepFile} (most recent)`);
|
|
698
|
+
// Delete older files
|
|
699
|
+
for (const file of deleteFiles) {
|
|
700
|
+
const filePath = path.join(projectPath, file);
|
|
701
|
+
await fs.remove(filePath);
|
|
702
|
+
console.warn(` ✅ Deleted: ${file}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Clean up temporary files left behind by sync operations
|
|
709
|
+
*
|
|
710
|
+
* Removes:
|
|
711
|
+
* - .tmp files (temporary update files)
|
|
712
|
+
* - .backup files (backup files from updates)
|
|
713
|
+
* - Any other temporary files
|
|
714
|
+
*/
|
|
715
|
+
async cleanupTempFiles(projectPath) {
|
|
716
|
+
const files = await fs.readdir(projectPath);
|
|
717
|
+
for (const file of files) {
|
|
718
|
+
if (file.endsWith('.tmp') || file.endsWith('.backup')) {
|
|
719
|
+
const filePath = path.join(projectPath, file);
|
|
720
|
+
await fs.remove(filePath);
|
|
721
|
+
console.log(` 🧹 Cleaned up: ${file}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
//# sourceMappingURL=living-docs-sync.js.map
|