specweave 0.32.10 → 0.33.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +162 -1
- package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts +120 -0
- package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/per-us-sync.js +276 -0
- package/dist/plugins/specweave-ado/lib/per-us-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts +4 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.js +13 -3
- package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
- package/dist/plugins/specweave-github/lib/per-us-sync.d.ts +97 -0
- package/dist/plugins/specweave-github/lib/per-us-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/per-us-sync.js +274 -0
- package/dist/plugins/specweave-github/lib/per-us-sync.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/per-us-sync.d.ts +113 -0
- package/dist/plugins/specweave-jira/lib/per-us-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/per-us-sync.js +254 -0
- package/dist/plugins/specweave-jira/lib/per-us-sync.js.map +1 -0
- package/dist/src/cli/add-child-pid.d.ts +11 -0
- package/dist/src/cli/add-child-pid.d.ts.map +1 -0
- package/dist/src/cli/add-child-pid.js +42 -0
- package/dist/src/cli/add-child-pid.js.map +1 -0
- package/dist/src/cli/add-child-process.d.ts +15 -0
- package/dist/src/cli/add-child-process.d.ts.map +1 -0
- package/dist/src/cli/add-child-process.js +40 -0
- package/dist/src/cli/add-child-process.js.map +1 -0
- package/dist/src/cli/check-watchdog.d.ts +15 -0
- package/dist/src/cli/check-watchdog.d.ts.map +1 -0
- package/dist/src/cli/check-watchdog.js +47 -0
- package/dist/src/cli/check-watchdog.js.map +1 -0
- package/dist/src/cli/cleanup-zombies.d.ts +14 -0
- package/dist/src/cli/cleanup-zombies.d.ts.map +1 -0
- package/dist/src/cli/cleanup-zombies.js +268 -0
- package/dist/src/cli/cleanup-zombies.js.map +1 -0
- package/dist/src/cli/find-session-by-pid.d.ts +14 -0
- package/dist/src/cli/find-session-by-pid.d.ts.map +1 -0
- package/dist/src/cli/find-session-by-pid.js +45 -0
- package/dist/src/cli/find-session-by-pid.js.map +1 -0
- package/dist/src/cli/get-stale-sessions.d.ts +17 -0
- package/dist/src/cli/get-stale-sessions.d.ts.map +1 -0
- package/dist/src/cli/get-stale-sessions.js +36 -0
- package/dist/src/cli/get-stale-sessions.js.map +1 -0
- package/dist/src/cli/register-session.d.ts +16 -0
- package/dist/src/cli/register-session.d.ts.map +1 -0
- package/dist/src/cli/register-session.js +48 -0
- package/dist/src/cli/register-session.js.map +1 -0
- package/dist/src/cli/remove-session.d.ts +11 -0
- package/dist/src/cli/remove-session.d.ts.map +1 -0
- package/dist/src/cli/remove-session.js +36 -0
- package/dist/src/cli/remove-session.js.map +1 -0
- package/dist/src/cli/update-heartbeat.d.ts +11 -0
- package/dist/src/cli/update-heartbeat.d.ts.map +1 -0
- package/dist/src/cli/update-heartbeat.js +36 -0
- package/dist/src/cli/update-heartbeat.js.map +1 -0
- package/dist/src/config/types.d.ts +1208 -203
- package/dist/src/config/types.d.ts.map +1 -1
- package/dist/src/core/background/job-manager.d.ts +16 -0
- package/dist/src/core/background/job-manager.d.ts.map +1 -1
- package/dist/src/core/background/job-manager.js +110 -15
- package/dist/src/core/background/job-manager.js.map +1 -1
- package/dist/src/core/config/config-manager.d.ts.map +1 -1
- package/dist/src/core/config/config-manager.js +58 -0
- package/dist/src/core/config/config-manager.js.map +1 -1
- package/dist/src/core/config/types.d.ts +80 -0
- package/dist/src/core/config/types.d.ts.map +1 -1
- package/dist/src/core/config/types.js.map +1 -1
- package/dist/src/core/increment/increment-utils.d.ts +26 -1
- package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
- package/dist/src/core/increment/increment-utils.js +66 -4
- package/dist/src/core/increment/increment-utils.js.map +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.d.ts +3 -1
- package/dist/src/core/increment/status-change-sync-trigger.d.ts.map +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.js +5 -2
- package/dist/src/core/increment/status-change-sync-trigger.js.map +1 -1
- package/dist/src/core/living-docs/cross-project-sync.d.ts +87 -15
- package/dist/src/core/living-docs/cross-project-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/cross-project-sync.js +147 -28
- package/dist/src/core/living-docs/cross-project-sync.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.d.ts.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js +48 -12
- package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.d.ts +70 -0
- package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.js +188 -0
- package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.d.ts +33 -0
- package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.js +290 -0
- package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.d.ts.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.js +114 -11
- package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.d.ts +23 -0
- package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.js +283 -0
- package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.d.ts +44 -0
- package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.js +61 -0
- package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.d.ts +126 -0
- package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.js +378 -0
- package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.d.ts.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js +57 -0
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.d.ts +82 -0
- package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.js +430 -0
- package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.d.ts +84 -0
- package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.js +387 -0
- package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/report-writer.d.ts +61 -0
- package/dist/src/core/living-docs/intelligent-analyzer/report-writer.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/report-writer.js +174 -0
- package/dist/src/core/living-docs/intelligent-analyzer/report-writer.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.js +26 -22
- package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
- package/dist/src/core/living-docs/module-analyzer.d.ts +3 -0
- package/dist/src/core/living-docs/module-analyzer.d.ts.map +1 -1
- package/dist/src/core/living-docs/module-analyzer.js +40 -1
- package/dist/src/core/living-docs/module-analyzer.js.map +1 -1
- package/dist/src/core/living-docs/types.d.ts +24 -3
- package/dist/src/core/living-docs/types.d.ts.map +1 -1
- package/dist/src/core/qa/qa-runner.js +1 -1
- package/dist/src/core/qa/qa-runner.js.map +1 -1
- package/dist/src/core/scheduler/session-sync-executor.js +1 -1
- package/dist/src/core/scheduler/session-sync-executor.js.map +1 -1
- package/dist/src/core/status-line/status-line-updater.d.ts +1 -1
- package/dist/src/core/status-line/status-line-updater.d.ts.map +1 -1
- package/dist/src/core/status-line/status-line-updater.js +4 -3
- package/dist/src/core/status-line/status-line-updater.js.map +1 -1
- package/dist/src/core/types/config.d.ts +79 -0
- package/dist/src/core/types/config.d.ts.map +1 -1
- package/dist/src/core/types/config.js.map +1 -1
- package/dist/src/importers/jira-importer.d.ts.map +1 -1
- package/dist/src/importers/jira-importer.js +18 -9
- package/dist/src/importers/jira-importer.js.map +1 -1
- package/dist/src/init/architecture/types.d.ts +140 -33
- package/dist/src/init/architecture/types.d.ts.map +1 -1
- package/dist/src/init/compliance/types.d.ts +27 -30
- package/dist/src/init/compliance/types.d.ts.map +1 -1
- package/dist/src/init/repo/types.d.ts +34 -11
- package/dist/src/init/repo/types.d.ts.map +1 -1
- package/dist/src/init/research/src/config/types.d.ts +82 -15
- package/dist/src/init/research/src/config/types.d.ts.map +1 -1
- package/dist/src/init/research/types.d.ts +93 -38
- package/dist/src/init/research/types.d.ts.map +1 -1
- package/dist/src/init/team/types.d.ts +42 -4
- package/dist/src/init/team/types.d.ts.map +1 -1
- package/dist/src/sync/ado-reconciler.js +1 -1
- package/dist/src/sync/ado-reconciler.js.map +1 -1
- package/dist/src/sync/github-reconciler.js +1 -1
- package/dist/src/sync/github-reconciler.js.map +1 -1
- package/dist/src/sync/jira-reconciler.js +1 -1
- package/dist/src/sync/jira-reconciler.js.map +1 -1
- package/dist/src/sync/sync-coordinator.d.ts +20 -0
- package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
- package/dist/src/sync/sync-coordinator.js +258 -33
- package/dist/src/sync/sync-coordinator.js.map +1 -1
- package/dist/src/types/session.d.ts +65 -0
- package/dist/src/types/session.d.ts.map +1 -0
- package/dist/src/types/session.js +8 -0
- package/dist/src/types/session.js.map +1 -0
- package/dist/src/utils/lock-manager.d.ts +48 -0
- package/dist/src/utils/lock-manager.d.ts.map +1 -0
- package/dist/src/utils/lock-manager.js +195 -0
- package/dist/src/utils/lock-manager.js.map +1 -0
- package/dist/src/utils/notification-manager.d.ts +45 -0
- package/dist/src/utils/notification-manager.d.ts.map +1 -0
- package/dist/src/utils/notification-manager.js +130 -0
- package/dist/src/utils/notification-manager.js.map +1 -0
- package/dist/src/utils/platform-utils.d.ts +136 -0
- package/dist/src/utils/platform-utils.d.ts.map +1 -0
- package/dist/src/utils/platform-utils.js +366 -0
- package/dist/src/utils/platform-utils.js.map +1 -0
- package/dist/src/utils/project-resolver.d.ts +156 -0
- package/dist/src/utils/project-resolver.d.ts.map +1 -0
- package/dist/src/utils/project-resolver.js +587 -0
- package/dist/src/utils/project-resolver.js.map +1 -0
- package/dist/src/utils/session-registry.d.ts +142 -0
- package/dist/src/utils/session-registry.d.ts.map +1 -0
- package/dist/src/utils/session-registry.js +480 -0
- package/dist/src/utils/session-registry.js.map +1 -0
- package/package.json +5 -2
- package/plugins/specweave/commands/specweave-living-docs.md +42 -0
- package/plugins/specweave/hooks/hooks.json +20 -0
- package/plugins/specweave/hooks/lib/update-active-increment.sh +2 -2
- package/plugins/specweave/hooks/lib/update-status-line.sh +1 -1
- package/plugins/specweave/hooks/post-increment-status-change.sh +3 -3
- package/plugins/specweave/hooks/post-metadata-change.sh +1 -1
- package/plugins/specweave/hooks/universal/hook-wrapper.cmd +26 -26
- package/plugins/specweave/hooks/universal/session-start.cmd +16 -16
- package/plugins/specweave/hooks/universal/session-start.ps1 +16 -16
- package/plugins/specweave/hooks/user-prompt-submit.sh +107 -5
- package/plugins/specweave/hooks/v2/guards/increment-root-guard.sh +61 -0
- package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +281 -0
- package/plugins/specweave/hooks/v2/handlers/living-specs-handler.sh +29 -0
- package/plugins/specweave/hooks/v2/session-end.sh +69 -0
- package/plugins/specweave/hooks/v2/session-start.sh +81 -0
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js +1 -1
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js.map +1 -1
- package/plugins/specweave/scripts/heartbeat.sh +110 -0
- package/plugins/specweave/scripts/progress.js +34 -4
- package/plugins/specweave/scripts/read-jobs.sh +1 -1
- package/plugins/specweave/scripts/read-progress.sh +50 -5
- package/plugins/specweave/scripts/read-workflow.sh +1 -1
- package/plugins/specweave/scripts/session-watchdog.sh +65 -0
- package/plugins/specweave/scripts/status.js +28 -11
- package/plugins/specweave-ado/lib/per-us-sync.js +247 -0
- package/plugins/specweave-ado/lib/per-us-sync.ts +410 -0
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +738 -0
- package/plugins/specweave-github/lib/github-client-v2.js +10 -3
- package/plugins/specweave-github/lib/github-client-v2.ts +15 -3
- package/plugins/specweave-github/lib/per-us-sync.js +241 -0
- package/plugins/specweave-github/lib/per-us-sync.ts +375 -0
- package/plugins/specweave-jira/lib/per-us-sync.js +224 -0
- package/plugins/specweave-jira/lib/per-us-sync.ts +366 -0
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +1107 -0
|
@@ -291,10 +291,13 @@ ${body}`;
|
|
|
291
291
|
* Search for issue by exact title match
|
|
292
292
|
*
|
|
293
293
|
* IDEMPOTENCY: Use this before creating issues to prevent duplicates
|
|
294
|
+
*
|
|
295
|
+
* @param title - Title pattern to search for (e.g., "[FS-136][US-001]")
|
|
296
|
+
* @param includeClosedIssues - If true, searches all issues (open+closed). Default: false (open only)
|
|
294
297
|
*/
|
|
295
|
-
async searchIssueByTitle(title) {
|
|
298
|
+
async searchIssueByTitle(title, includeClosedIssues = false) {
|
|
296
299
|
const escapedTitle = title.replace(/"/g, '\\"');
|
|
297
|
-
const
|
|
300
|
+
const args = [
|
|
298
301
|
"issue",
|
|
299
302
|
"list",
|
|
300
303
|
"--repo",
|
|
@@ -306,7 +309,11 @@ ${body}`;
|
|
|
306
309
|
"--limit",
|
|
307
310
|
"50"
|
|
308
311
|
// ✅ FIX: Increased from 1 to 50 to catch duplicates (Issue #0047)
|
|
309
|
-
]
|
|
312
|
+
];
|
|
313
|
+
if (includeClosedIssues) {
|
|
314
|
+
args.push("--state", "all");
|
|
315
|
+
}
|
|
316
|
+
const result = await execFileNoThrow("gh", args);
|
|
310
317
|
if (result.exitCode !== 0) {
|
|
311
318
|
return null;
|
|
312
319
|
}
|
|
@@ -392,12 +392,15 @@ export class GitHubClientV2 {
|
|
|
392
392
|
* Search for issue by exact title match
|
|
393
393
|
*
|
|
394
394
|
* IDEMPOTENCY: Use this before creating issues to prevent duplicates
|
|
395
|
+
*
|
|
396
|
+
* @param title - Title pattern to search for (e.g., "[FS-136][US-001]")
|
|
397
|
+
* @param includeClosedIssues - If true, searches all issues (open+closed). Default: false (open only)
|
|
395
398
|
*/
|
|
396
|
-
async searchIssueByTitle(title: string): Promise<GitHubIssue | null> {
|
|
399
|
+
async searchIssueByTitle(title: string, includeClosedIssues: boolean = false): Promise<GitHubIssue | null> {
|
|
397
400
|
// Escape double quotes in title for gh search
|
|
398
401
|
const escapedTitle = title.replace(/"/g, '\\"');
|
|
399
402
|
|
|
400
|
-
const
|
|
403
|
+
const args = [
|
|
401
404
|
'issue',
|
|
402
405
|
'list',
|
|
403
406
|
'--repo',
|
|
@@ -408,7 +411,16 @@ export class GitHubClientV2 {
|
|
|
408
411
|
'number,title,state,url,labels',
|
|
409
412
|
'--limit',
|
|
410
413
|
'50', // ✅ FIX: Increased from 1 to 50 to catch duplicates (Issue #0047)
|
|
411
|
-
]
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
// v0.34.0: Add --state all to search closed issues too (for closure flow)
|
|
417
|
+
// Without this, closeGitHubIssuesForUserStories() can't find already-closed issues
|
|
418
|
+
// and reports "No GitHub issue found" instead of "already closed"
|
|
419
|
+
if (includeClosedIssues) {
|
|
420
|
+
args.push('--state', 'all');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const result = await execFileNoThrow('gh', args);
|
|
412
424
|
|
|
413
425
|
if (result.exitCode !== 0) {
|
|
414
426
|
// Search failed, return null (treat as not found)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { Octokit } from "@octokit/rest";
|
|
2
|
+
import { consoleLogger } from "../../../src/utils/logger.js";
|
|
3
|
+
class PerUSGitHubSync {
|
|
4
|
+
constructor(token, projectMappings, options = {}) {
|
|
5
|
+
this.token = token;
|
|
6
|
+
this.projectMappings = projectMappings;
|
|
7
|
+
this.octokit = new Octokit({ auth: token });
|
|
8
|
+
this.logger = options.logger ?? consoleLogger;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Sync all user stories to their respective GitHub repos
|
|
12
|
+
*
|
|
13
|
+
* @param userStories - User stories with explicit project fields
|
|
14
|
+
* @param featureId - Feature ID (e.g., "FS-137")
|
|
15
|
+
* @param options - Sync options
|
|
16
|
+
*/
|
|
17
|
+
async syncUserStories(userStories, featureId, options = {}) {
|
|
18
|
+
const synced = [];
|
|
19
|
+
const failed = [];
|
|
20
|
+
const externalRefs = {};
|
|
21
|
+
const groups = this.groupByProject(userStories, options.defaultProject);
|
|
22
|
+
this.logger.log(`\u{1F4E1} Per-US GitHub Sync: ${userStories.length} USs across ${groups.size} projects`);
|
|
23
|
+
for (const [projectId, stories] of groups) {
|
|
24
|
+
const mapping = this.projectMappings[projectId]?.github;
|
|
25
|
+
if (!mapping) {
|
|
26
|
+
this.logger.warn(` \u26A0\uFE0F No GitHub mapping for project "${projectId}" - skipping ${stories.length} USs`);
|
|
27
|
+
for (const story of stories) {
|
|
28
|
+
failed.push({
|
|
29
|
+
usId: story.id,
|
|
30
|
+
projectId,
|
|
31
|
+
repo: "N/A",
|
|
32
|
+
issueNumber: 0,
|
|
33
|
+
url: "",
|
|
34
|
+
action: "skipped",
|
|
35
|
+
error: `No GitHub mapping for project "${projectId}"`
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
for (const story of stories) {
|
|
41
|
+
try {
|
|
42
|
+
const result = await this.syncUserStory(story, mapping, featureId, options);
|
|
43
|
+
synced.push({
|
|
44
|
+
...result,
|
|
45
|
+
projectId
|
|
46
|
+
});
|
|
47
|
+
if (!options.dryRun && result.action !== "skipped") {
|
|
48
|
+
externalRefs[story.id] = {
|
|
49
|
+
github: {
|
|
50
|
+
provider: "github",
|
|
51
|
+
issueNumber: result.issueNumber,
|
|
52
|
+
url: result.url,
|
|
53
|
+
targetProject: projectId,
|
|
54
|
+
lastSynced: (/* @__PURE__ */ new Date()).toISOString()
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
failed.push({
|
|
60
|
+
usId: story.id,
|
|
61
|
+
projectId,
|
|
62
|
+
repo: `${mapping.owner}/${mapping.repo}`,
|
|
63
|
+
issueNumber: 0,
|
|
64
|
+
url: "",
|
|
65
|
+
action: "skipped",
|
|
66
|
+
error: error instanceof Error ? error.message : String(error)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const created = synced.filter((r) => r.action === "created").length;
|
|
72
|
+
const updated = synced.filter((r) => r.action === "updated").length;
|
|
73
|
+
const skipped = synced.filter((r) => r.action === "skipped").length;
|
|
74
|
+
return {
|
|
75
|
+
success: failed.length === 0,
|
|
76
|
+
synced,
|
|
77
|
+
failed,
|
|
78
|
+
externalRefs,
|
|
79
|
+
summary: {
|
|
80
|
+
total: userStories.length,
|
|
81
|
+
created,
|
|
82
|
+
updated,
|
|
83
|
+
skipped,
|
|
84
|
+
failed: failed.length
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Sync a single user story to GitHub
|
|
90
|
+
*/
|
|
91
|
+
async syncUserStory(story, mapping, featureId, options) {
|
|
92
|
+
const title = `[${featureId}][${story.id}] ${story.title}`;
|
|
93
|
+
const body = this.buildIssueBody(story, featureId);
|
|
94
|
+
if (options.dryRun) {
|
|
95
|
+
this.logger.log(` \u{1F50D} [DRY-RUN] Would sync ${story.id} to ${mapping.owner}/${mapping.repo}`);
|
|
96
|
+
return {
|
|
97
|
+
usId: story.id,
|
|
98
|
+
projectId: story.project || "unknown",
|
|
99
|
+
repo: `${mapping.owner}/${mapping.repo}`,
|
|
100
|
+
issueNumber: 0,
|
|
101
|
+
url: "",
|
|
102
|
+
action: "skipped"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const existingIssue = await this.findExistingIssue(mapping, story.id);
|
|
106
|
+
if (existingIssue) {
|
|
107
|
+
const response = await this.octokit.issues.update({
|
|
108
|
+
owner: mapping.owner,
|
|
109
|
+
repo: mapping.repo,
|
|
110
|
+
issue_number: existingIssue.number,
|
|
111
|
+
title,
|
|
112
|
+
body
|
|
113
|
+
});
|
|
114
|
+
this.logger.log(` \u{1F504} Updated ${story.id} \u2192 ${mapping.owner}/${mapping.repo}#${response.data.number}`);
|
|
115
|
+
return {
|
|
116
|
+
usId: story.id,
|
|
117
|
+
projectId: story.project || "unknown",
|
|
118
|
+
repo: `${mapping.owner}/${mapping.repo}`,
|
|
119
|
+
issueNumber: response.data.number,
|
|
120
|
+
url: response.data.html_url,
|
|
121
|
+
action: "updated"
|
|
122
|
+
};
|
|
123
|
+
} else {
|
|
124
|
+
const response = await this.octokit.issues.create({
|
|
125
|
+
owner: mapping.owner,
|
|
126
|
+
repo: mapping.repo,
|
|
127
|
+
title,
|
|
128
|
+
body,
|
|
129
|
+
labels: ["specweave", "user-story"]
|
|
130
|
+
});
|
|
131
|
+
this.logger.log(` \u2705 Created ${story.id} \u2192 ${mapping.owner}/${mapping.repo}#${response.data.number}`);
|
|
132
|
+
return {
|
|
133
|
+
usId: story.id,
|
|
134
|
+
projectId: story.project || "unknown",
|
|
135
|
+
repo: `${mapping.owner}/${mapping.repo}`,
|
|
136
|
+
issueNumber: response.data.number,
|
|
137
|
+
url: response.data.html_url,
|
|
138
|
+
action: "created"
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Find existing issue by US ID in title
|
|
144
|
+
*/
|
|
145
|
+
async findExistingIssue(mapping, usId) {
|
|
146
|
+
try {
|
|
147
|
+
const response = await this.octokit.issues.listForRepo({
|
|
148
|
+
owner: mapping.owner,
|
|
149
|
+
repo: mapping.repo,
|
|
150
|
+
labels: "specweave",
|
|
151
|
+
state: "all",
|
|
152
|
+
per_page: 100
|
|
153
|
+
});
|
|
154
|
+
const existing = response.data.find(
|
|
155
|
+
(issue) => issue.title.includes(`[${usId}]`)
|
|
156
|
+
);
|
|
157
|
+
return existing ? { number: existing.number } : null;
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Build issue body from user story
|
|
164
|
+
*/
|
|
165
|
+
buildIssueBody(story, featureId) {
|
|
166
|
+
const lines = [];
|
|
167
|
+
lines.push(`# ${story.title}`);
|
|
168
|
+
lines.push("");
|
|
169
|
+
if (story.description) {
|
|
170
|
+
lines.push(story.description);
|
|
171
|
+
lines.push("");
|
|
172
|
+
}
|
|
173
|
+
if (story.acceptanceCriteria && story.acceptanceCriteria.length > 0) {
|
|
174
|
+
lines.push("## Acceptance Criteria");
|
|
175
|
+
lines.push("");
|
|
176
|
+
for (const ac of story.acceptanceCriteria) {
|
|
177
|
+
lines.push(`- [ ] ${ac}`);
|
|
178
|
+
}
|
|
179
|
+
lines.push("");
|
|
180
|
+
}
|
|
181
|
+
lines.push("---");
|
|
182
|
+
lines.push("");
|
|
183
|
+
lines.push(`**Feature**: ${featureId}`);
|
|
184
|
+
lines.push(`**User Story**: ${story.id}`);
|
|
185
|
+
if (story.project) {
|
|
186
|
+
lines.push(`**Project**: ${story.project}`);
|
|
187
|
+
}
|
|
188
|
+
if (story.board) {
|
|
189
|
+
lines.push(`**Board**: ${story.board}`);
|
|
190
|
+
}
|
|
191
|
+
lines.push("");
|
|
192
|
+
lines.push("\u{1F916} Auto-generated by SpecWeave");
|
|
193
|
+
return lines.join("\n");
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Group user stories by their explicit project field
|
|
197
|
+
*/
|
|
198
|
+
groupByProject(userStories, defaultProject) {
|
|
199
|
+
const groups = /* @__PURE__ */ new Map();
|
|
200
|
+
for (const story of userStories) {
|
|
201
|
+
const project = story.project || defaultProject || "default";
|
|
202
|
+
if (!groups.has(project)) {
|
|
203
|
+
groups.set(project, []);
|
|
204
|
+
}
|
|
205
|
+
groups.get(project).push(story);
|
|
206
|
+
}
|
|
207
|
+
return groups;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function formatPerUSSyncResults(result) {
|
|
211
|
+
const lines = [];
|
|
212
|
+
lines.push("");
|
|
213
|
+
lines.push("\u{1F4CA} Per-US GitHub Sync Results");
|
|
214
|
+
lines.push("");
|
|
215
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
216
|
+
for (const r of [...result.synced, ...result.failed]) {
|
|
217
|
+
const existing = byProject.get(r.projectId) || [];
|
|
218
|
+
existing.push(r);
|
|
219
|
+
byProject.set(r.projectId, existing);
|
|
220
|
+
}
|
|
221
|
+
for (const [projectId, results] of byProject) {
|
|
222
|
+
lines.push(`**${projectId}**:`);
|
|
223
|
+
for (const r of results) {
|
|
224
|
+
const icon = r.action === "created" ? "\u2705" : r.action === "updated" ? "\u{1F504}" : r.error ? "\u274C" : "\u23ED\uFE0F";
|
|
225
|
+
if (r.issueNumber > 0) {
|
|
226
|
+
lines.push(` ${icon} ${r.usId} \u2192 ${r.repo}#${r.issueNumber}`);
|
|
227
|
+
} else if (r.error) {
|
|
228
|
+
lines.push(` ${icon} ${r.usId}: ${r.error}`);
|
|
229
|
+
} else {
|
|
230
|
+
lines.push(` ${icon} ${r.usId} (${r.action})`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
lines.push("");
|
|
234
|
+
}
|
|
235
|
+
lines.push(`\u{1F4C8} Summary: ${result.summary.created} created, ${result.summary.updated} updated, ${result.summary.failed} failed`);
|
|
236
|
+
return lines.join("\n");
|
|
237
|
+
}
|
|
238
|
+
export {
|
|
239
|
+
PerUSGitHubSync,
|
|
240
|
+
formatPerUSSyncResults
|
|
241
|
+
};
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-US GitHub Sync (v0.34.0+)
|
|
3
|
+
*
|
|
4
|
+
* Syncs each User Story to its explicitly declared project's GitHub repo.
|
|
5
|
+
* Uses the **Project**: field in spec.md (NOT keyword-based classification).
|
|
6
|
+
*
|
|
7
|
+
* Key difference from multi-project-sync:
|
|
8
|
+
* - Multi-project sync uses keyword/heuristic classification
|
|
9
|
+
* - Per-US sync uses EXPLICIT **Project**: field from spec.md
|
|
10
|
+
*
|
|
11
|
+
* @module per-us-sync
|
|
12
|
+
* @since v0.34.0
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Octokit } from '@octokit/rest';
|
|
16
|
+
import type { UserStoryData } from '../../../src/core/living-docs/types.js';
|
|
17
|
+
import type { ProjectMappings, GitHubMapping } from '../../../src/core/types/config.js';
|
|
18
|
+
import type { USExternalRef, USExternalRefsMap } from '../../../src/core/types/increment-metadata.js';
|
|
19
|
+
import { Logger, consoleLogger } from '../../../src/utils/logger.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Result of syncing a single US to GitHub
|
|
23
|
+
*/
|
|
24
|
+
export interface USSyncResult {
|
|
25
|
+
usId: string;
|
|
26
|
+
projectId: string;
|
|
27
|
+
repo: string;
|
|
28
|
+
issueNumber: number;
|
|
29
|
+
url: string;
|
|
30
|
+
action: 'created' | 'updated' | 'skipped';
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Result of syncing all USs in an increment
|
|
36
|
+
*/
|
|
37
|
+
export interface PerUSSyncResult {
|
|
38
|
+
success: boolean;
|
|
39
|
+
synced: USSyncResult[];
|
|
40
|
+
failed: USSyncResult[];
|
|
41
|
+
externalRefs: USExternalRefsMap;
|
|
42
|
+
summary: {
|
|
43
|
+
total: number;
|
|
44
|
+
created: number;
|
|
45
|
+
updated: number;
|
|
46
|
+
skipped: number;
|
|
47
|
+
failed: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Options for per-US sync
|
|
53
|
+
*/
|
|
54
|
+
export interface PerUSSyncOptions {
|
|
55
|
+
dryRun?: boolean;
|
|
56
|
+
force?: boolean;
|
|
57
|
+
defaultProject?: string;
|
|
58
|
+
logger?: Logger;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Per-US GitHub Sync
|
|
63
|
+
*
|
|
64
|
+
* Syncs each US to its declared project's GitHub repository.
|
|
65
|
+
*/
|
|
66
|
+
export class PerUSGitHubSync {
|
|
67
|
+
private token: string;
|
|
68
|
+
private projectMappings: ProjectMappings;
|
|
69
|
+
private octokit: Octokit;
|
|
70
|
+
private logger: Logger;
|
|
71
|
+
|
|
72
|
+
constructor(
|
|
73
|
+
token: string,
|
|
74
|
+
projectMappings: ProjectMappings,
|
|
75
|
+
options: { logger?: Logger } = {}
|
|
76
|
+
) {
|
|
77
|
+
this.token = token;
|
|
78
|
+
this.projectMappings = projectMappings;
|
|
79
|
+
this.octokit = new Octokit({ auth: token });
|
|
80
|
+
this.logger = options.logger ?? consoleLogger;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Sync all user stories to their respective GitHub repos
|
|
85
|
+
*
|
|
86
|
+
* @param userStories - User stories with explicit project fields
|
|
87
|
+
* @param featureId - Feature ID (e.g., "FS-137")
|
|
88
|
+
* @param options - Sync options
|
|
89
|
+
*/
|
|
90
|
+
async syncUserStories(
|
|
91
|
+
userStories: UserStoryData[],
|
|
92
|
+
featureId: string,
|
|
93
|
+
options: PerUSSyncOptions = {}
|
|
94
|
+
): Promise<PerUSSyncResult> {
|
|
95
|
+
const synced: USSyncResult[] = [];
|
|
96
|
+
const failed: USSyncResult[] = [];
|
|
97
|
+
const externalRefs: USExternalRefsMap = {};
|
|
98
|
+
|
|
99
|
+
// Group USs by their declared project
|
|
100
|
+
const groups = this.groupByProject(userStories, options.defaultProject);
|
|
101
|
+
|
|
102
|
+
this.logger.log(`📡 Per-US GitHub Sync: ${userStories.length} USs across ${groups.size} projects`);
|
|
103
|
+
|
|
104
|
+
for (const [projectId, stories] of groups) {
|
|
105
|
+
// Get GitHub mapping for this project
|
|
106
|
+
const mapping = this.projectMappings[projectId]?.github;
|
|
107
|
+
|
|
108
|
+
if (!mapping) {
|
|
109
|
+
// No GitHub mapping for this project
|
|
110
|
+
this.logger.warn(` ⚠️ No GitHub mapping for project "${projectId}" - skipping ${stories.length} USs`);
|
|
111
|
+
for (const story of stories) {
|
|
112
|
+
failed.push({
|
|
113
|
+
usId: story.id,
|
|
114
|
+
projectId,
|
|
115
|
+
repo: 'N/A',
|
|
116
|
+
issueNumber: 0,
|
|
117
|
+
url: '',
|
|
118
|
+
action: 'skipped',
|
|
119
|
+
error: `No GitHub mapping for project "${projectId}"`
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Sync each US to this project's repo
|
|
126
|
+
for (const story of stories) {
|
|
127
|
+
try {
|
|
128
|
+
const result = await this.syncUserStory(story, mapping, featureId, options);
|
|
129
|
+
synced.push({
|
|
130
|
+
...result,
|
|
131
|
+
projectId
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Build external ref
|
|
135
|
+
if (!options.dryRun && result.action !== 'skipped') {
|
|
136
|
+
externalRefs[story.id] = {
|
|
137
|
+
github: {
|
|
138
|
+
provider: 'github',
|
|
139
|
+
issueNumber: result.issueNumber,
|
|
140
|
+
url: result.url,
|
|
141
|
+
targetProject: projectId,
|
|
142
|
+
lastSynced: new Date().toISOString()
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
failed.push({
|
|
148
|
+
usId: story.id,
|
|
149
|
+
projectId,
|
|
150
|
+
repo: `${mapping.owner}/${mapping.repo}`,
|
|
151
|
+
issueNumber: 0,
|
|
152
|
+
url: '',
|
|
153
|
+
action: 'skipped',
|
|
154
|
+
error: error instanceof Error ? error.message : String(error)
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Calculate summary
|
|
161
|
+
const created = synced.filter(r => r.action === 'created').length;
|
|
162
|
+
const updated = synced.filter(r => r.action === 'updated').length;
|
|
163
|
+
const skipped = synced.filter(r => r.action === 'skipped').length;
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
success: failed.length === 0,
|
|
167
|
+
synced,
|
|
168
|
+
failed,
|
|
169
|
+
externalRefs,
|
|
170
|
+
summary: {
|
|
171
|
+
total: userStories.length,
|
|
172
|
+
created,
|
|
173
|
+
updated,
|
|
174
|
+
skipped,
|
|
175
|
+
failed: failed.length
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Sync a single user story to GitHub
|
|
182
|
+
*/
|
|
183
|
+
private async syncUserStory(
|
|
184
|
+
story: UserStoryData,
|
|
185
|
+
mapping: GitHubMapping,
|
|
186
|
+
featureId: string,
|
|
187
|
+
options: PerUSSyncOptions
|
|
188
|
+
): Promise<USSyncResult> {
|
|
189
|
+
const title = `[${featureId}][${story.id}] ${story.title}`;
|
|
190
|
+
const body = this.buildIssueBody(story, featureId);
|
|
191
|
+
|
|
192
|
+
if (options.dryRun) {
|
|
193
|
+
this.logger.log(` 🔍 [DRY-RUN] Would sync ${story.id} to ${mapping.owner}/${mapping.repo}`);
|
|
194
|
+
return {
|
|
195
|
+
usId: story.id,
|
|
196
|
+
projectId: story.project || 'unknown',
|
|
197
|
+
repo: `${mapping.owner}/${mapping.repo}`,
|
|
198
|
+
issueNumber: 0,
|
|
199
|
+
url: '',
|
|
200
|
+
action: 'skipped'
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check for existing issue
|
|
205
|
+
const existingIssue = await this.findExistingIssue(mapping, story.id);
|
|
206
|
+
|
|
207
|
+
if (existingIssue) {
|
|
208
|
+
// Update existing issue
|
|
209
|
+
const response = await this.octokit.issues.update({
|
|
210
|
+
owner: mapping.owner,
|
|
211
|
+
repo: mapping.repo,
|
|
212
|
+
issue_number: existingIssue.number,
|
|
213
|
+
title,
|
|
214
|
+
body
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
this.logger.log(` 🔄 Updated ${story.id} → ${mapping.owner}/${mapping.repo}#${response.data.number}`);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
usId: story.id,
|
|
221
|
+
projectId: story.project || 'unknown',
|
|
222
|
+
repo: `${mapping.owner}/${mapping.repo}`,
|
|
223
|
+
issueNumber: response.data.number,
|
|
224
|
+
url: response.data.html_url,
|
|
225
|
+
action: 'updated'
|
|
226
|
+
};
|
|
227
|
+
} else {
|
|
228
|
+
// Create new issue
|
|
229
|
+
const response = await this.octokit.issues.create({
|
|
230
|
+
owner: mapping.owner,
|
|
231
|
+
repo: mapping.repo,
|
|
232
|
+
title,
|
|
233
|
+
body,
|
|
234
|
+
labels: ['specweave', 'user-story']
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
this.logger.log(` ✅ Created ${story.id} → ${mapping.owner}/${mapping.repo}#${response.data.number}`);
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
usId: story.id,
|
|
241
|
+
projectId: story.project || 'unknown',
|
|
242
|
+
repo: `${mapping.owner}/${mapping.repo}`,
|
|
243
|
+
issueNumber: response.data.number,
|
|
244
|
+
url: response.data.html_url,
|
|
245
|
+
action: 'created'
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Find existing issue by US ID in title
|
|
252
|
+
*/
|
|
253
|
+
private async findExistingIssue(
|
|
254
|
+
mapping: GitHubMapping,
|
|
255
|
+
usId: string
|
|
256
|
+
): Promise<{ number: number } | null> {
|
|
257
|
+
try {
|
|
258
|
+
const response = await this.octokit.issues.listForRepo({
|
|
259
|
+
owner: mapping.owner,
|
|
260
|
+
repo: mapping.repo,
|
|
261
|
+
labels: 'specweave',
|
|
262
|
+
state: 'all',
|
|
263
|
+
per_page: 100
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const existing = response.data.find(issue =>
|
|
267
|
+
issue.title.includes(`[${usId}]`)
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
return existing ? { number: existing.number } : null;
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Build issue body from user story
|
|
278
|
+
*/
|
|
279
|
+
private buildIssueBody(story: UserStoryData, featureId: string): string {
|
|
280
|
+
const lines: string[] = [];
|
|
281
|
+
|
|
282
|
+
lines.push(`# ${story.title}`);
|
|
283
|
+
lines.push('');
|
|
284
|
+
|
|
285
|
+
if (story.description) {
|
|
286
|
+
lines.push(story.description);
|
|
287
|
+
lines.push('');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (story.acceptanceCriteria && story.acceptanceCriteria.length > 0) {
|
|
291
|
+
lines.push('## Acceptance Criteria');
|
|
292
|
+
lines.push('');
|
|
293
|
+
for (const ac of story.acceptanceCriteria) {
|
|
294
|
+
lines.push(`- [ ] ${ac}`);
|
|
295
|
+
}
|
|
296
|
+
lines.push('');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
lines.push('---');
|
|
300
|
+
lines.push('');
|
|
301
|
+
lines.push(`**Feature**: ${featureId}`);
|
|
302
|
+
lines.push(`**User Story**: ${story.id}`);
|
|
303
|
+
if (story.project) {
|
|
304
|
+
lines.push(`**Project**: ${story.project}`);
|
|
305
|
+
}
|
|
306
|
+
if (story.board) {
|
|
307
|
+
lines.push(`**Board**: ${story.board}`);
|
|
308
|
+
}
|
|
309
|
+
lines.push('');
|
|
310
|
+
lines.push('🤖 Auto-generated by SpecWeave');
|
|
311
|
+
|
|
312
|
+
return lines.join('\n');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Group user stories by their explicit project field
|
|
317
|
+
*/
|
|
318
|
+
private groupByProject(
|
|
319
|
+
userStories: UserStoryData[],
|
|
320
|
+
defaultProject?: string
|
|
321
|
+
): Map<string, UserStoryData[]> {
|
|
322
|
+
const groups = new Map<string, UserStoryData[]>();
|
|
323
|
+
|
|
324
|
+
for (const story of userStories) {
|
|
325
|
+
const project = story.project || defaultProject || 'default';
|
|
326
|
+
|
|
327
|
+
if (!groups.has(project)) {
|
|
328
|
+
groups.set(project, []);
|
|
329
|
+
}
|
|
330
|
+
groups.get(project)!.push(story);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return groups;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Format per-US sync results for display
|
|
339
|
+
*/
|
|
340
|
+
export function formatPerUSSyncResults(result: PerUSSyncResult): string {
|
|
341
|
+
const lines: string[] = [];
|
|
342
|
+
|
|
343
|
+
lines.push('');
|
|
344
|
+
lines.push('📊 Per-US GitHub Sync Results');
|
|
345
|
+
lines.push('');
|
|
346
|
+
|
|
347
|
+
// Group by project
|
|
348
|
+
const byProject = new Map<string, USSyncResult[]>();
|
|
349
|
+
for (const r of [...result.synced, ...result.failed]) {
|
|
350
|
+
const existing = byProject.get(r.projectId) || [];
|
|
351
|
+
existing.push(r);
|
|
352
|
+
byProject.set(r.projectId, existing);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
for (const [projectId, results] of byProject) {
|
|
356
|
+
lines.push(`**${projectId}**:`);
|
|
357
|
+
for (const r of results) {
|
|
358
|
+
const icon = r.action === 'created' ? '✅' :
|
|
359
|
+
r.action === 'updated' ? '🔄' :
|
|
360
|
+
r.error ? '❌' : '⏭️';
|
|
361
|
+
if (r.issueNumber > 0) {
|
|
362
|
+
lines.push(` ${icon} ${r.usId} → ${r.repo}#${r.issueNumber}`);
|
|
363
|
+
} else if (r.error) {
|
|
364
|
+
lines.push(` ${icon} ${r.usId}: ${r.error}`);
|
|
365
|
+
} else {
|
|
366
|
+
lines.push(` ${icon} ${r.usId} (${r.action})`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
lines.push('');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
lines.push(`📈 Summary: ${result.summary.created} created, ${result.summary.updated} updated, ${result.summary.failed} failed`);
|
|
373
|
+
|
|
374
|
+
return lines.join('\n');
|
|
375
|
+
}
|