specweave 0.24.8 → 0.24.9
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/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +3 -1
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/github-multi-repo.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/github-multi-repo.js +18 -2
- package/dist/src/cli/helpers/issue-tracker/github-multi-repo.js.map +1 -1
- package/dist/src/core/repo-structure/git-error-handler.d.ts +1 -1
- package/dist/src/core/repo-structure/git-error-handler.d.ts.map +1 -1
- package/dist/src/core/repo-structure/git-provider.d.ts +1 -1
- package/dist/src/core/repo-structure/git-provider.d.ts.map +1 -1
- package/dist/src/core/repo-structure/platform-registry.d.ts.map +1 -1
- package/dist/src/core/repo-structure/platform-registry.js +20 -9
- package/dist/src/core/repo-structure/platform-registry.js.map +1 -1
- package/dist/src/core/repo-structure/prompt-consolidator.d.ts +13 -1
- package/dist/src/core/repo-structure/prompt-consolidator.d.ts.map +1 -1
- package/dist/src/core/repo-structure/prompt-consolidator.js +38 -5
- package/dist/src/core/repo-structure/prompt-consolidator.js.map +1 -1
- package/dist/src/core/repo-structure/providers/azure-devops-provider.d.ts +64 -0
- package/dist/src/core/repo-structure/providers/azure-devops-provider.d.ts.map +1 -0
- package/dist/src/core/repo-structure/providers/azure-devops-provider.js +263 -0
- package/dist/src/core/repo-structure/providers/azure-devops-provider.js.map +1 -0
- package/dist/src/core/repo-structure/providers/bitbucket-provider.d.ts +12 -11
- package/dist/src/core/repo-structure/providers/bitbucket-provider.d.ts.map +1 -1
- package/dist/src/core/repo-structure/providers/bitbucket-provider.js +164 -30
- package/dist/src/core/repo-structure/providers/bitbucket-provider.js.map +1 -1
- package/dist/src/core/repo-structure/providers/gitlab-provider.d.ts +10 -9
- package/dist/src/core/repo-structure/providers/gitlab-provider.d.ts.map +1 -1
- package/dist/src/core/repo-structure/providers/gitlab-provider.js +182 -28
- package/dist/src/core/repo-structure/providers/gitlab-provider.js.map +1 -1
- package/dist/src/core/repo-structure/providers/index.d.ts +3 -1
- package/dist/src/core/repo-structure/providers/index.d.ts.map +1 -1
- package/dist/src/core/repo-structure/providers/index.js +10 -2
- package/dist/src/core/repo-structure/providers/index.js.map +1 -1
- package/dist/src/core/repo-structure/providers/local-provider.d.ts +61 -0
- package/dist/src/core/repo-structure/providers/local-provider.d.ts.map +1 -0
- package/dist/src/core/repo-structure/providers/local-provider.js +148 -0
- package/dist/src/core/repo-structure/providers/local-provider.js.map +1 -0
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts +11 -1
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.js +268 -84
- package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/agents/pm/AGENT.md.bak +1893 -0
- package/plugins/specweave/hooks/docs-changed.sh.backup +79 -0
- package/plugins/specweave/hooks/human-input-required.sh.backup +75 -0
- package/plugins/specweave/hooks/lib/migrate-increment-work.sh.bak +245 -0
- package/plugins/specweave/hooks/lib/sync-spec-content.sh.bak +149 -0
- package/plugins/specweave/hooks/lib/validate-spec-status.sh.bak +163 -0
- package/plugins/specweave/hooks/post-first-increment.sh.backup +61 -0
- package/plugins/specweave/hooks/post-first-increment.sh.bak +61 -0
- package/plugins/specweave/hooks/post-increment-change.sh.backup +98 -0
- package/plugins/specweave/hooks/post-increment-completion.sh.backup +231 -0
- package/plugins/specweave/hooks/post-increment-planning.sh.backup +1048 -0
- package/plugins/specweave/hooks/post-increment-status-change.sh.backup +147 -0
- package/plugins/specweave/hooks/post-spec-update.sh.backup +158 -0
- package/plugins/specweave/hooks/post-spec-update.sh.bak +158 -0
- package/plugins/specweave/hooks/post-task-completion.sh +69 -175
- package/plugins/specweave/hooks/post-user-story-complete.sh.backup +179 -0
- package/plugins/specweave/hooks/post-user-story-complete.sh.bak +179 -0
- package/plugins/specweave/hooks/pre-command-deduplication.sh.backup +83 -0
- package/plugins/specweave/hooks/pre-command-deduplication.sh.bak +83 -0
- package/plugins/specweave/hooks/pre-implementation.sh.backup +67 -0
- package/plugins/specweave/hooks/pre-task-completion.sh.backup +194 -0
- package/plugins/specweave/hooks/pre-tool-use.sh.backup +133 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh.backup +386 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh.bak +386 -0
- package/plugins/specweave/lib/hooks/auto-transition.js.bak +50 -0
- package/plugins/specweave/lib/hooks/auto-transition.ts.bak +84 -0
- package/plugins/specweave/lib/hooks/consolidated-sync.js +183 -0
- package/plugins/specweave/lib/hooks/git-diff-analyzer.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/git-diff-analyzer.d.ts.bak +89 -0
- package/plugins/specweave/lib/hooks/git-diff-analyzer.js.bak +142 -0
- package/plugins/specweave/lib/hooks/git-diff-analyzer.ts.bak +269 -0
- package/plugins/specweave/lib/hooks/invoke-translator-skill.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/invoke-translator-skill.d.ts.bak +60 -0
- package/plugins/specweave/lib/hooks/invoke-translator-skill.js.bak +155 -0
- package/plugins/specweave/lib/hooks/invoke-translator-skill.ts.bak +264 -0
- package/plugins/specweave/lib/hooks/prepare-reflection-context.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/prepare-reflection-context.d.ts.bak +42 -0
- package/plugins/specweave/lib/hooks/prepare-reflection-context.js.bak +110 -0
- package/plugins/specweave/lib/hooks/prepare-reflection-context.ts.bak +178 -0
- package/plugins/specweave/lib/hooks/reflection-config-loader.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/reflection-config-loader.d.ts.bak +45 -0
- package/plugins/specweave/lib/hooks/reflection-config-loader.js.bak +92 -0
- package/plugins/specweave/lib/hooks/reflection-config-loader.ts.bak +156 -0
- package/plugins/specweave/lib/hooks/reflection-parser.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/reflection-parser.d.ts.bak +33 -0
- package/plugins/specweave/lib/hooks/reflection-parser.js.bak +301 -0
- package/plugins/specweave/lib/hooks/reflection-parser.ts.bak +484 -0
- package/plugins/specweave/lib/hooks/reflection-prompt-builder.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/reflection-prompt-builder.d.ts.bak +56 -0
- package/plugins/specweave/lib/hooks/reflection-prompt-builder.js.bak +182 -0
- package/plugins/specweave/lib/hooks/reflection-prompt-builder.ts.bak +306 -0
- package/plugins/specweave/lib/hooks/reflection-storage.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/reflection-storage.d.ts.bak +64 -0
- package/plugins/specweave/lib/hooks/reflection-storage.js.bak +231 -0
- package/plugins/specweave/lib/hooks/reflection-storage.ts.bak +369 -0
- package/plugins/specweave/lib/hooks/run-self-reflection.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/run-self-reflection.d.ts.bak +43 -0
- package/plugins/specweave/lib/hooks/run-self-reflection.js.bak +132 -0
- package/plugins/specweave/lib/hooks/run-self-reflection.ts.bak +258 -0
- package/plugins/specweave/lib/hooks/sync-cache.js.bak +294 -0
- package/plugins/specweave/lib/hooks/sync-living-docs.d.js.bak +1 -0
- package/plugins/specweave/lib/hooks/sync-living-docs.d.ts.bak +27 -0
- package/plugins/specweave/lib/hooks/sync-living-docs.js.bak +339 -0
- package/plugins/specweave/lib/hooks/sync-us-tasks.js.bak +476 -0
- package/plugins/specweave/lib/hooks/translate-file.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/translate-file.d.ts.bak +59 -0
- package/plugins/specweave/lib/hooks/translate-file.js.bak +289 -0
- package/plugins/specweave/lib/hooks/translate-file.ts.bak +428 -0
- package/plugins/specweave/lib/hooks/translate-living-docs.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/translate-living-docs.d.ts.bak +13 -0
- package/plugins/specweave/lib/hooks/translate-living-docs.js.bak +119 -0
- package/plugins/specweave/lib/hooks/translate-living-docs.ts.bak +224 -0
- package/plugins/specweave/lib/hooks/update-ac-status.js.bak +51 -0
- package/plugins/specweave/lib/hooks/update-ac-status.ts.bak +103 -0
- package/plugins/specweave/lib/hooks/update-tasks-md.d.js.bak +1 -0
- package/plugins/specweave/lib/hooks/update-tasks-md.d.ts.bak +29 -0
- package/plugins/specweave/lib/hooks/update-tasks-md.js.bak +296 -0
- package/plugins/specweave/lib/hooks/update-tasks-md.ts.bak +489 -0
- package/plugins/specweave-ado/hooks/post-living-docs-update.sh.backup +353 -0
- package/plugins/specweave-ado/hooks/post-task-completion.sh.backup +172 -0
- package/plugins/specweave-ado/lib/ado-multi-project-sync.js +1 -0
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +424 -0
- package/plugins/specweave-github/hooks/post-task-completion.sh.backup +258 -0
- package/plugins/specweave-jira/hooks/post-task-completion.sh.backup +172 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +3 -3
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +540 -0
- package/plugins/specweave-release/hooks/post-task-completion.sh.backup +110 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* US-Task Synchronization Module
|
|
4
|
+
*
|
|
5
|
+
* Syncs task completion status from tasks.md to living docs User Story files.
|
|
6
|
+
* Updates:
|
|
7
|
+
* - Task lists in US files (replaces "No tasks defined")
|
|
8
|
+
* - AC checkboxes based on task completion
|
|
9
|
+
*
|
|
10
|
+
* Part of increment 0047-us-task-linkage implementation.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs-extra';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { parseTasksWithUSLinks, getAllTasks } from '../vendor/generators/spec/task-parser.js';
|
|
16
|
+
import { glob } from 'glob';
|
|
17
|
+
import { getCachedTasks, needsSync, recordSync, batchFileUpdates } from './sync-cache.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sync tasks from tasks.md to living docs User Story files
|
|
21
|
+
*
|
|
22
|
+
* @param {string} incrementId - Increment ID (e.g., "0047-us-task-linkage")
|
|
23
|
+
* @param {string} projectRoot - Project root directory
|
|
24
|
+
* @param {string} featureId - Feature ID (e.g., "FS-047")
|
|
25
|
+
* @param {object} options - Sync options
|
|
26
|
+
* @returns {Promise<SyncResult>} Sync result
|
|
27
|
+
*/
|
|
28
|
+
export async function syncUSTasksToLivingDocs(incrementId, projectRoot, featureId, options = {}) {
|
|
29
|
+
try {
|
|
30
|
+
console.log(` 📋 Syncing tasks to living docs User Stories...`);
|
|
31
|
+
|
|
32
|
+
const tasksPath = path.join(projectRoot, '.specweave', 'increments', incrementId, 'tasks.md');
|
|
33
|
+
|
|
34
|
+
// Check if tasks.md exists
|
|
35
|
+
if (!fs.existsSync(tasksPath)) {
|
|
36
|
+
console.log(` ⚠️ tasks.md not found, skipping task sync`);
|
|
37
|
+
return { success: true, updatedFiles: [], errors: [] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Parse tasks with US linkage (with caching for performance)
|
|
41
|
+
let tasksByUS;
|
|
42
|
+
try {
|
|
43
|
+
tasksByUS = getCachedTasks(tasksPath, parseTasksWithUSLinks);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(` ❌ Failed to parse tasks.md:`, error.message);
|
|
46
|
+
return { success: false, updatedFiles: [], errors: [error.message] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const allTasks = getAllTasks(tasksByUS);
|
|
50
|
+
console.log(` ✓ Parsed ${allTasks.length} tasks from tasks.md`);
|
|
51
|
+
|
|
52
|
+
// Check for tasks without US linkage (backward compatibility)
|
|
53
|
+
const unassignedTasks = tasksByUS['unassigned'] || [];
|
|
54
|
+
if (unassignedTasks.length > 0) {
|
|
55
|
+
console.log(` ⚠️ ${unassignedTasks.length} tasks without User Story linkage (old format)`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Get project ID (default to "specweave" for now, can be enhanced later)
|
|
59
|
+
const projectId = getProjectId(projectRoot, incrementId) || 'specweave';
|
|
60
|
+
|
|
61
|
+
const updatedFiles = [];
|
|
62
|
+
const errors = [];
|
|
63
|
+
const filesToUpdate = []; // Batch file updates
|
|
64
|
+
|
|
65
|
+
// For each User Story with tasks, update its living docs file
|
|
66
|
+
for (const [usId, tasks] of Object.entries(tasksByUS)) {
|
|
67
|
+
if (usId === 'unassigned') continue; // Skip unassigned tasks
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Find US file in living docs
|
|
71
|
+
const usFilePath = await findUSFile(projectRoot, projectId, featureId, usId);
|
|
72
|
+
|
|
73
|
+
if (!usFilePath) {
|
|
74
|
+
console.log(` ⚠️ Living docs file not found for ${usId}, skipping`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Incremental sync: Check if sync is needed
|
|
79
|
+
if (!needsSync(usFilePath, tasks, tasksPath)) {
|
|
80
|
+
console.log(` ⏭️ ${usId} unchanged, skipping sync`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Update US file with task list
|
|
85
|
+
const result = await updateUSFile(usFilePath, tasks, incrementId);
|
|
86
|
+
|
|
87
|
+
if (result.updated) {
|
|
88
|
+
filesToUpdate.push({ path: usFilePath, content: result.content });
|
|
89
|
+
recordSync(usFilePath, tasks); // Cache sync result for next run
|
|
90
|
+
console.log(` ✓ Prepared update for ${usId} (${tasks.length} tasks)`);
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(` ❌ Error updating ${usId}:`, error.message);
|
|
94
|
+
errors.push(`${usId}: ${error.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Batch write all file updates (reduce I/O)
|
|
99
|
+
if (filesToUpdate.length > 0) {
|
|
100
|
+
await batchFileUpdates(filesToUpdate);
|
|
101
|
+
updatedFiles.push(...filesToUpdate.map(f => f.path));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (updatedFiles.length > 0) {
|
|
105
|
+
console.log(` ✅ Updated ${updatedFiles.length} User Story file(s)`);
|
|
106
|
+
} else {
|
|
107
|
+
console.log(` ℹ️ No User Story files updated`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
success: errors.length === 0,
|
|
112
|
+
updatedFiles,
|
|
113
|
+
errors
|
|
114
|
+
};
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(` ❌ US-Task sync failed:`, error);
|
|
117
|
+
return { success: false, updatedFiles: [], errors: [error.message] };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Find User Story file in living docs
|
|
123
|
+
*
|
|
124
|
+
* @param {string} projectRoot - Project root
|
|
125
|
+
* @param {string} projectId - Project ID (e.g., "specweave")
|
|
126
|
+
* @param {string} featureId - Feature ID (e.g., "FS-047")
|
|
127
|
+
* @param {string} usId - User Story ID (e.g., "US-001")
|
|
128
|
+
* @returns {Promise<string|null>} Path to US file or null if not found
|
|
129
|
+
*/
|
|
130
|
+
async function findUSFile(projectRoot, projectId, featureId, usId) {
|
|
131
|
+
// Pattern: .specweave/docs/internal/specs/{project}/{feature}/us-{id}-*.md
|
|
132
|
+
const pattern = path.join(
|
|
133
|
+
projectRoot,
|
|
134
|
+
'.specweave',
|
|
135
|
+
'docs',
|
|
136
|
+
'internal',
|
|
137
|
+
'specs',
|
|
138
|
+
projectId,
|
|
139
|
+
featureId,
|
|
140
|
+
`${usId.toLowerCase()}-*.md`
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const files = await glob(pattern);
|
|
144
|
+
|
|
145
|
+
if (files.length === 0) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (files.length > 1) {
|
|
150
|
+
console.log(` ⚠️ Multiple files found for ${usId}, using first: ${path.basename(files[0])}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return files[0];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Update User Story file with task list and AC checkboxes
|
|
158
|
+
*
|
|
159
|
+
* @param {string} usFilePath - Path to US markdown file
|
|
160
|
+
* @param {Array} tasks - Tasks linked to this US
|
|
161
|
+
* @param {string} incrementId - Increment ID
|
|
162
|
+
* @returns {Promise<{updated: boolean, content: string}>} Update result
|
|
163
|
+
*/
|
|
164
|
+
async function updateUSFile(usFilePath, tasks, incrementId) {
|
|
165
|
+
let content = await fs.readFile(usFilePath, 'utf-8');
|
|
166
|
+
let updated = false;
|
|
167
|
+
|
|
168
|
+
// 1. Update origin badge (NEW - T-034)
|
|
169
|
+
const updatedOrigin = updateOriginBadge(content, usFilePath);
|
|
170
|
+
if (updatedOrigin !== content) {
|
|
171
|
+
content = updatedOrigin;
|
|
172
|
+
updated = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 2. Update task list section
|
|
176
|
+
const taskList = generateTaskList(tasks, incrementId);
|
|
177
|
+
const newTasksSection = `## Tasks\n\n${taskList}`;
|
|
178
|
+
|
|
179
|
+
// Replace existing tasks section
|
|
180
|
+
if (content.includes('## Tasks')) {
|
|
181
|
+
// Replace everything from "## Tasks" to next "##" or end of file
|
|
182
|
+
const tasksRegex = /(## Tasks\n\n)(?:.*?)(?=\n## |\n---\n|$)/s;
|
|
183
|
+
const newContent = content.replace(tasksRegex, newTasksSection);
|
|
184
|
+
|
|
185
|
+
if (newContent !== content) {
|
|
186
|
+
content = newContent;
|
|
187
|
+
updated = true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 3. Update AC checkboxes based on task completion
|
|
192
|
+
const updatedACs = updateACCheckboxes(content, tasks);
|
|
193
|
+
if (updatedACs !== content) {
|
|
194
|
+
content = updatedACs;
|
|
195
|
+
updated = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { updated, content };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Update origin badge in User Story file
|
|
203
|
+
* Detects origin from US ID (E suffix = external)
|
|
204
|
+
*
|
|
205
|
+
* @param {string} content - US file content
|
|
206
|
+
* @param {string} usFilePath - Path to US file
|
|
207
|
+
* @returns {string} Updated content with origin badge
|
|
208
|
+
*/
|
|
209
|
+
function updateOriginBadge(content, usFilePath) {
|
|
210
|
+
// Extract US ID from filename (e.g., us-001e-title.md -> US-001E)
|
|
211
|
+
const filename = path.basename(usFilePath);
|
|
212
|
+
const usIdMatch = filename.match(/^(us-\d{3}e?)-/i);
|
|
213
|
+
|
|
214
|
+
if (!usIdMatch) {
|
|
215
|
+
return content; // Can't determine US ID, skip
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const usId = usIdMatch[1].toUpperCase();
|
|
219
|
+
const isExternal = usId.endsWith('E');
|
|
220
|
+
|
|
221
|
+
// Validate origin immutability (prevent internal ↔ external changes)
|
|
222
|
+
const existingOrigin = extractExistingOrigin(content);
|
|
223
|
+
if (existingOrigin) {
|
|
224
|
+
const existingIsExternal = existingOrigin !== 'internal';
|
|
225
|
+
if (isExternal !== existingIsExternal) {
|
|
226
|
+
console.log(` ⚠️ Origin immutable: Cannot change ${usId} from ${existingOrigin} to ${isExternal ? 'external' : 'internal'}`);
|
|
227
|
+
return content; // Don't modify origin
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Determine origin badge
|
|
232
|
+
let originBadge;
|
|
233
|
+
if (isExternal) {
|
|
234
|
+
// Try to detect external source from metadata
|
|
235
|
+
const externalSource = detectExternalSource(content);
|
|
236
|
+
originBadge = getExternalOriginBadge(externalSource, content);
|
|
237
|
+
} else {
|
|
238
|
+
originBadge = '🏠 **Internal**';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check if origin badge already exists
|
|
242
|
+
const originPattern = /\*\*Origin\*\*:\s*.*/;
|
|
243
|
+
if (originPattern.test(content)) {
|
|
244
|
+
// Replace existing origin badge
|
|
245
|
+
return content.replace(originPattern, `**Origin**: ${originBadge}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Add origin badge after frontmatter (if exists) or at beginning
|
|
249
|
+
const frontmatterEnd = content.match(/^---\n.*?\n---\n/s);
|
|
250
|
+
if (frontmatterEnd) {
|
|
251
|
+
const insertPos = frontmatterEnd[0].length;
|
|
252
|
+
return content.slice(0, insertPos) + `\n**Origin**: ${originBadge}\n\n` + content.slice(insertPos);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// No frontmatter, add at beginning after title
|
|
256
|
+
const titleMatch = content.match(/^#\s+.+\n/);
|
|
257
|
+
if (titleMatch) {
|
|
258
|
+
const insertPos = titleMatch[0].length;
|
|
259
|
+
return content.slice(0, insertPos) + `\n**Origin**: ${originBadge}\n\n` + content.slice(insertPos);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Fallback: add at very beginning
|
|
263
|
+
return `**Origin**: ${originBadge}\n\n` + content;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Extract existing origin from content (for immutability validation)
|
|
268
|
+
*
|
|
269
|
+
* @param {string} content - US file content
|
|
270
|
+
* @returns {string|null} Existing origin (internal, github, jira, ado) or null
|
|
271
|
+
*/
|
|
272
|
+
function extractExistingOrigin(content) {
|
|
273
|
+
const originMatch = content.match(/\*\*Origin\*\*:\s*(.+)/);
|
|
274
|
+
if (!originMatch) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const originText = originMatch[1].toLowerCase();
|
|
279
|
+
if (originText.includes('internal')) {
|
|
280
|
+
return 'internal';
|
|
281
|
+
}
|
|
282
|
+
if (originText.includes('github')) {
|
|
283
|
+
return 'github';
|
|
284
|
+
}
|
|
285
|
+
if (originText.includes('jira')) {
|
|
286
|
+
return 'jira';
|
|
287
|
+
}
|
|
288
|
+
if (originText.includes('ado') || originText.includes('azure')) {
|
|
289
|
+
return 'ado';
|
|
290
|
+
}
|
|
291
|
+
if (originText.includes('external')) {
|
|
292
|
+
return 'external'; // Generic external
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Detect external source from content metadata
|
|
300
|
+
*
|
|
301
|
+
* @param {string} content - US file content
|
|
302
|
+
* @returns {string} External source (github, jira, ado) or 'unknown'
|
|
303
|
+
*/
|
|
304
|
+
function detectExternalSource(content) {
|
|
305
|
+
// Check frontmatter for external_source or externalSource
|
|
306
|
+
const frontmatterMatch = content.match(/^---\n(.*?)\n---/s);
|
|
307
|
+
if (frontmatterMatch) {
|
|
308
|
+
const frontmatter = frontmatterMatch[1];
|
|
309
|
+
if (frontmatter.includes('external_source: github') || frontmatter.includes('externalSource: github')) {
|
|
310
|
+
return 'github';
|
|
311
|
+
}
|
|
312
|
+
if (frontmatter.includes('external_source: jira') || frontmatter.includes('externalSource: jira')) {
|
|
313
|
+
return 'jira';
|
|
314
|
+
}
|
|
315
|
+
if (frontmatter.includes('external_source: ado') || frontmatter.includes('externalSource: ado')) {
|
|
316
|
+
return 'ado';
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check for external ID patterns
|
|
321
|
+
if (content.includes('externalId: GH-') || content.includes('external_id: GH-')) {
|
|
322
|
+
return 'github';
|
|
323
|
+
}
|
|
324
|
+
if (content.includes('externalId: JIRA-') || content.includes('external_id: JIRA-')) {
|
|
325
|
+
return 'jira';
|
|
326
|
+
}
|
|
327
|
+
if (content.includes('externalId: ADO-') || content.includes('external_id: ADO-')) {
|
|
328
|
+
return 'ado';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return 'unknown';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get external origin badge with link if available
|
|
336
|
+
*
|
|
337
|
+
* @param {string} source - External source (github, jira, ado)
|
|
338
|
+
* @param {string} content - US file content
|
|
339
|
+
* @returns {string} Origin badge markdown
|
|
340
|
+
*/
|
|
341
|
+
function getExternalOriginBadge(source, content) {
|
|
342
|
+
// Extract external ID and URL from content
|
|
343
|
+
const externalIdMatch = content.match(/external_?[iI]d:\s*([^\n]+)/);
|
|
344
|
+
const externalUrlMatch = content.match(/external_?[uU]rl:\s*([^\n]+)/);
|
|
345
|
+
|
|
346
|
+
const externalId = externalIdMatch ? externalIdMatch[1].trim() : null;
|
|
347
|
+
const externalUrl = externalUrlMatch ? externalUrlMatch[1].trim() : null;
|
|
348
|
+
|
|
349
|
+
switch (source) {
|
|
350
|
+
case 'github':
|
|
351
|
+
if (externalId && externalUrl) {
|
|
352
|
+
return `🔗 [GitHub ${externalId}](${externalUrl})`;
|
|
353
|
+
}
|
|
354
|
+
return '🔗 **GitHub**';
|
|
355
|
+
case 'jira':
|
|
356
|
+
if (externalId && externalUrl) {
|
|
357
|
+
return `🎫 [JIRA ${externalId}](${externalUrl})`;
|
|
358
|
+
}
|
|
359
|
+
return '🎫 **JIRA**';
|
|
360
|
+
case 'ado':
|
|
361
|
+
if (externalId && externalUrl) {
|
|
362
|
+
return `📋 [ADO ${externalId}](${externalUrl})`;
|
|
363
|
+
}
|
|
364
|
+
return '📋 **Azure DevOps**';
|
|
365
|
+
default:
|
|
366
|
+
return '🔗 **External**';
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Generate task list markdown
|
|
372
|
+
*
|
|
373
|
+
* @param {Array} tasks - Tasks to list
|
|
374
|
+
* @param {string} incrementId - Increment ID
|
|
375
|
+
* @returns {string} Markdown task list
|
|
376
|
+
*/
|
|
377
|
+
function generateTaskList(tasks, incrementId) {
|
|
378
|
+
if (tasks.length === 0) {
|
|
379
|
+
return '_No tasks defined for this user story_';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return tasks.map(task => {
|
|
383
|
+
const checkbox = task.status === 'completed' ? 'x' : ' ';
|
|
384
|
+
const link = `../../../../increments/${incrementId}/tasks.md#${task.id}`;
|
|
385
|
+
return `- [${checkbox}] [${task.id}](${link}): ${task.title}`;
|
|
386
|
+
}).join('\n');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Update AC checkboxes based on task completion
|
|
391
|
+
*
|
|
392
|
+
* @param {string} content - US file content
|
|
393
|
+
* @param {Array} tasks - Tasks linked to this US
|
|
394
|
+
* @returns {string} Updated content
|
|
395
|
+
*/
|
|
396
|
+
function updateACCheckboxes(content, tasks) {
|
|
397
|
+
// Collect all AC-IDs satisfied by completed tasks
|
|
398
|
+
const satisfiedACs = new Set();
|
|
399
|
+
|
|
400
|
+
tasks.forEach(task => {
|
|
401
|
+
if (task.status === 'completed' && task.satisfiesACs) {
|
|
402
|
+
task.satisfiesACs.forEach(acId => satisfiedACs.add(acId));
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Update AC checkboxes
|
|
407
|
+
let updatedContent = content;
|
|
408
|
+
|
|
409
|
+
satisfiedACs.forEach(acId => {
|
|
410
|
+
// Replace: - [ ] **AC-US1-01** with - [x] **AC-US1-01**
|
|
411
|
+
const uncheckedRegex = new RegExp(`- \\[ \\] \\*\\*${escapeRegex(acId)}\\*\\*`, 'g');
|
|
412
|
+
updatedContent = updatedContent.replace(uncheckedRegex, `- [x] **${acId}**`);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Also uncheck ACs that are no longer satisfied (tasks incomplete/pending)
|
|
416
|
+
// Extract all AC-IDs from content
|
|
417
|
+
const acPattern = /- \[[x ]\] \*\*(AC-US\d+-\d{2})\*\*/g;
|
|
418
|
+
let match;
|
|
419
|
+
const allACs = [];
|
|
420
|
+
|
|
421
|
+
while ((match = acPattern.exec(content)) !== null) {
|
|
422
|
+
allACs.push(match[1]);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Uncheck ACs not in satisfiedACs set
|
|
426
|
+
allACs.forEach(acId => {
|
|
427
|
+
if (!satisfiedACs.has(acId)) {
|
|
428
|
+
const checkedRegex = new RegExp(`- \\[x\\] \\*\\*${escapeRegex(acId)}\\*\\*`, 'g');
|
|
429
|
+
updatedContent = updatedContent.replace(checkedRegex, `- [ ] **${acId}**`);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return updatedContent;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Escape special regex characters
|
|
438
|
+
*/
|
|
439
|
+
function escapeRegex(str) {
|
|
440
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Get project ID from metadata or config
|
|
445
|
+
*
|
|
446
|
+
* @param {string} projectRoot - Project root
|
|
447
|
+
* @param {string} incrementId - Increment ID
|
|
448
|
+
* @returns {string|null} Project ID or null
|
|
449
|
+
*/
|
|
450
|
+
function getProjectId(projectRoot, incrementId) {
|
|
451
|
+
try {
|
|
452
|
+
// Try to get from metadata.json
|
|
453
|
+
const metadataPath = path.join(projectRoot, '.specweave', 'increments', incrementId, 'metadata.json');
|
|
454
|
+
if (fs.existsSync(metadataPath)) {
|
|
455
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
456
|
+
if (metadata.project) {
|
|
457
|
+
return metadata.project;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Try to get from config.json
|
|
462
|
+
const configPath = path.join(projectRoot, '.specweave', 'config.json');
|
|
463
|
+
if (fs.existsSync(configPath)) {
|
|
464
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
465
|
+
if (config.defaultProject) {
|
|
466
|
+
return config.defaultProject;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Default to "specweave" (single-project mode)
|
|
471
|
+
return 'specweave';
|
|
472
|
+
} catch (error) {
|
|
473
|
+
console.log(` ⚠️ Could not determine project ID, using default: "specweave"`);
|
|
474
|
+
return 'specweave';
|
|
475
|
+
}
|
|
476
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Translation CLI Utility
|
|
3
|
+
*
|
|
4
|
+
* Translates a single file from detected source language to target language
|
|
5
|
+
* using the translation utilities and LLM invocation.
|
|
6
|
+
*
|
|
7
|
+
* This script is called from:
|
|
8
|
+
* - Post-increment-planning hook (auto-translate spec.md, plan.md, tasks.md)
|
|
9
|
+
* - Post-task-completion hook (auto-translate living docs)
|
|
10
|
+
* - Manual /specweave:translate command
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node translate-file.js <file-path> [--target-lang en] [--preview]
|
|
14
|
+
*
|
|
15
|
+
* @see src/utils/translation.ts
|
|
16
|
+
* @see .specweave/increments/0006-llm-native-i18n/reports/DESIGN-POST-GENERATION-TRANSLATION.md
|
|
17
|
+
*/
|
|
18
|
+
import { type SupportedLanguage } from '../../../../src/utils/translation.js';
|
|
19
|
+
/**
|
|
20
|
+
* CLI options
|
|
21
|
+
*/
|
|
22
|
+
interface CLIOptions {
|
|
23
|
+
filePath: string;
|
|
24
|
+
targetLang: SupportedLanguage;
|
|
25
|
+
preview: boolean;
|
|
26
|
+
verbose: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Translation result
|
|
30
|
+
*/
|
|
31
|
+
interface FileTranslationResult {
|
|
32
|
+
success: boolean;
|
|
33
|
+
filePath: string;
|
|
34
|
+
sourceLanguage: SupportedLanguage;
|
|
35
|
+
targetLanguage: SupportedLanguage;
|
|
36
|
+
warnings: string[];
|
|
37
|
+
cost: number;
|
|
38
|
+
tokensUsed: number;
|
|
39
|
+
preview?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Main translation function
|
|
43
|
+
*
|
|
44
|
+
* @param options - CLI options
|
|
45
|
+
* @returns Translation result
|
|
46
|
+
*/
|
|
47
|
+
export declare function translateFile(options: CLIOptions): Promise<FileTranslationResult>;
|
|
48
|
+
/**
|
|
49
|
+
* Batch translate multiple files
|
|
50
|
+
*
|
|
51
|
+
* @param filePaths - Array of file paths to translate
|
|
52
|
+
* @param targetLang - Target language
|
|
53
|
+
* @param preview - Preview mode
|
|
54
|
+
* @param verbose - Verbose output
|
|
55
|
+
* @returns Array of translation results
|
|
56
|
+
*/
|
|
57
|
+
export declare function batchTranslateFiles(filePaths: string[], targetLang?: SupportedLanguage, preview?: boolean, verbose?: boolean): Promise<FileTranslationResult[]>;
|
|
58
|
+
export {};
|
|
59
|
+
//# sourceMappingURL=translate-file.d.ts.map
|