vibe-fabric 0.1.0
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/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/cli/commands/claude.d.ts +19 -0
- package/dist/cli/commands/claude.d.ts.map +1 -0
- package/dist/cli/commands/claude.js +107 -0
- package/dist/cli/commands/claude.js.map +1 -0
- package/dist/cli/commands/coverage.d.ts +37 -0
- package/dist/cli/commands/coverage.d.ts.map +1 -0
- package/dist/cli/commands/coverage.js +374 -0
- package/dist/cli/commands/coverage.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +30 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +187 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/gaps.d.ts +52 -0
- package/dist/cli/commands/gaps.d.ts.map +1 -0
- package/dist/cli/commands/gaps.js +487 -0
- package/dist/cli/commands/gaps.js.map +1 -0
- package/dist/cli/commands/help.d.ts +7 -0
- package/dist/cli/commands/help.d.ts.map +1 -0
- package/dist/cli/commands/help.js +51 -0
- package/dist/cli/commands/help.js.map +1 -0
- package/dist/cli/commands/init.d.ts +39 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +246 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/prd.d.ts +30 -0
- package/dist/cli/commands/prd.d.ts.map +1 -0
- package/dist/cli/commands/prd.js +179 -0
- package/dist/cli/commands/prd.js.map +1 -0
- package/dist/cli/commands/repo/add.d.ts +36 -0
- package/dist/cli/commands/repo/add.d.ts.map +1 -0
- package/dist/cli/commands/repo/add.js +303 -0
- package/dist/cli/commands/repo/add.js.map +1 -0
- package/dist/cli/commands/scope.d.ts +36 -0
- package/dist/cli/commands/scope.d.ts.map +1 -0
- package/dist/cli/commands/scope.js +312 -0
- package/dist/cli/commands/scope.js.map +1 -0
- package/dist/cli/commands/send.d.ts +43 -0
- package/dist/cli/commands/send.d.ts.map +1 -0
- package/dist/cli/commands/send.js +469 -0
- package/dist/cli/commands/send.js.map +1 -0
- package/dist/cli/commands/status.d.ts +32 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +422 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +37 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +299 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/version.d.ts +7 -0
- package/dist/cli/commands/version.d.ts.map +1 -0
- package/dist/cli/commands/version.js +45 -0
- package/dist/cli/commands/version.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +65 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/ui/components/ActiveScopes.d.ts +13 -0
- package/dist/cli/ui/components/ActiveScopes.d.ts.map +1 -0
- package/dist/cli/ui/components/ActiveScopes.js +25 -0
- package/dist/cli/ui/components/ActiveScopes.js.map +1 -0
- package/dist/cli/ui/components/EmptyState.d.ts +24 -0
- package/dist/cli/ui/components/EmptyState.d.ts.map +1 -0
- package/dist/cli/ui/components/EmptyState.js +13 -0
- package/dist/cli/ui/components/EmptyState.js.map +1 -0
- package/dist/cli/ui/components/Header.d.ts +11 -0
- package/dist/cli/ui/components/Header.d.ts.map +1 -0
- package/dist/cli/ui/components/Header.js +32 -0
- package/dist/cli/ui/components/Header.js.map +1 -0
- package/dist/cli/ui/components/PrdCoverage.d.ts +12 -0
- package/dist/cli/ui/components/PrdCoverage.d.ts.map +1 -0
- package/dist/cli/ui/components/PrdCoverage.js +15 -0
- package/dist/cli/ui/components/PrdCoverage.js.map +1 -0
- package/dist/cli/ui/components/RecentActivity.d.ts +12 -0
- package/dist/cli/ui/components/RecentActivity.d.ts.map +1 -0
- package/dist/cli/ui/components/RecentActivity.js +40 -0
- package/dist/cli/ui/components/RecentActivity.js.map +1 -0
- package/dist/cli/ui/components/RepoList.d.ts +12 -0
- package/dist/cli/ui/components/RepoList.d.ts.map +1 -0
- package/dist/cli/ui/components/RepoList.js +24 -0
- package/dist/cli/ui/components/RepoList.js.map +1 -0
- package/dist/cli/ui/components/Shortcuts.d.ts +7 -0
- package/dist/cli/ui/components/Shortcuts.d.ts.map +1 -0
- package/dist/cli/ui/components/Shortcuts.js +7 -0
- package/dist/cli/ui/components/Shortcuts.js.map +1 -0
- package/dist/cli/ui/components/index.d.ts +12 -0
- package/dist/cli/ui/components/index.d.ts.map +1 -0
- package/dist/cli/ui/components/index.js +12 -0
- package/dist/cli/ui/components/index.js.map +1 -0
- package/dist/cli/ui/dashboard.d.ts +9 -0
- package/dist/cli/ui/dashboard.d.ts.map +1 -0
- package/dist/cli/ui/dashboard.js +102 -0
- package/dist/cli/ui/dashboard.js.map +1 -0
- package/dist/core/commands.d.ts +32 -0
- package/dist/core/commands.d.ts.map +1 -0
- package/dist/core/commands.js +361 -0
- package/dist/core/commands.js.map +1 -0
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +78 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/coverage.d.ts +32 -0
- package/dist/core/coverage.d.ts.map +1 -0
- package/dist/core/coverage.js +286 -0
- package/dist/core/coverage.js.map +1 -0
- package/dist/core/dashboard/data.d.ts +73 -0
- package/dist/core/dashboard/data.d.ts.map +1 -0
- package/dist/core/dashboard/data.js +250 -0
- package/dist/core/dashboard/data.js.map +1 -0
- package/dist/core/dependencies.d.ts +39 -0
- package/dist/core/dependencies.d.ts.map +1 -0
- package/dist/core/dependencies.js +160 -0
- package/dist/core/dependencies.js.map +1 -0
- package/dist/core/doctor/auth.d.ts +22 -0
- package/dist/core/doctor/auth.d.ts.map +1 -0
- package/dist/core/doctor/auth.js +147 -0
- package/dist/core/doctor/auth.js.map +1 -0
- package/dist/core/doctor/config.d.ts +26 -0
- package/dist/core/doctor/config.d.ts.map +1 -0
- package/dist/core/doctor/config.js +172 -0
- package/dist/core/doctor/config.js.map +1 -0
- package/dist/core/doctor/environment.d.ts +26 -0
- package/dist/core/doctor/environment.d.ts.map +1 -0
- package/dist/core/doctor/environment.js +145 -0
- package/dist/core/doctor/environment.js.map +1 -0
- package/dist/core/doctor/index.d.ts +44 -0
- package/dist/core/doctor/index.d.ts.map +1 -0
- package/dist/core/doctor/index.js +134 -0
- package/dist/core/doctor/index.js.map +1 -0
- package/dist/core/doctor/repos.d.ts +22 -0
- package/dist/core/doctor/repos.d.ts.map +1 -0
- package/dist/core/doctor/repos.js +262 -0
- package/dist/core/doctor/repos.js.map +1 -0
- package/dist/core/doctor/sync.d.ts +18 -0
- package/dist/core/doctor/sync.d.ts.map +1 -0
- package/dist/core/doctor/sync.js +146 -0
- package/dist/core/doctor/sync.js.map +1 -0
- package/dist/core/gaps.d.ts +70 -0
- package/dist/core/gaps.d.ts.map +1 -0
- package/dist/core/gaps.js +448 -0
- package/dist/core/gaps.js.map +1 -0
- package/dist/core/github.d.ts +38 -0
- package/dist/core/github.d.ts.map +1 -0
- package/dist/core/github.js +102 -0
- package/dist/core/github.js.map +1 -0
- package/dist/core/prd/analyzer.d.ts +44 -0
- package/dist/core/prd/analyzer.d.ts.map +1 -0
- package/dist/core/prd/analyzer.js +259 -0
- package/dist/core/prd/analyzer.js.map +1 -0
- package/dist/core/prd/check.d.ts +17 -0
- package/dist/core/prd/check.d.ts.map +1 -0
- package/dist/core/prd/check.js +154 -0
- package/dist/core/prd/check.js.map +1 -0
- package/dist/core/prd/index.d.ts +6 -0
- package/dist/core/prd/index.d.ts.map +1 -0
- package/dist/core/prd/index.js +6 -0
- package/dist/core/prd/index.js.map +1 -0
- package/dist/core/project.d.ts +13 -0
- package/dist/core/project.d.ts.map +1 -0
- package/dist/core/project.js +810 -0
- package/dist/core/project.js.map +1 -0
- package/dist/core/prompts.d.ts +52 -0
- package/dist/core/prompts.d.ts.map +1 -0
- package/dist/core/prompts.js +266 -0
- package/dist/core/prompts.js.map +1 -0
- package/dist/core/repo/framework.d.ts +38 -0
- package/dist/core/repo/framework.d.ts.map +1 -0
- package/dist/core/repo/framework.js +142 -0
- package/dist/core/repo/framework.js.map +1 -0
- package/dist/core/repo/index.d.ts +6 -0
- package/dist/core/repo/index.d.ts.map +1 -0
- package/dist/core/repo/index.js +6 -0
- package/dist/core/repo/index.js.map +1 -0
- package/dist/core/repo/templates/claude-agents.d.ts +6 -0
- package/dist/core/repo/templates/claude-agents.d.ts.map +1 -0
- package/dist/core/repo/templates/claude-agents.js +173 -0
- package/dist/core/repo/templates/claude-agents.js.map +1 -0
- package/dist/core/repo/templates/claude-commands.d.ts +6 -0
- package/dist/core/repo/templates/claude-commands.d.ts.map +1 -0
- package/dist/core/repo/templates/claude-commands.js +278 -0
- package/dist/core/repo/templates/claude-commands.js.map +1 -0
- package/dist/core/repo/templates/claude-prompts.d.ts +6 -0
- package/dist/core/repo/templates/claude-prompts.d.ts.map +1 -0
- package/dist/core/repo/templates/claude-prompts.js +258 -0
- package/dist/core/repo/templates/claude-prompts.js.map +1 -0
- package/dist/core/repo/templates/claude-scripts.d.ts +6 -0
- package/dist/core/repo/templates/claude-scripts.d.ts.map +1 -0
- package/dist/core/repo/templates/claude-scripts.js +212 -0
- package/dist/core/repo/templates/claude-scripts.js.map +1 -0
- package/dist/core/repo/templates/index.d.ts +22 -0
- package/dist/core/repo/templates/index.d.ts.map +1 -0
- package/dist/core/repo/templates/index.js +121 -0
- package/dist/core/repo/templates/index.js.map +1 -0
- package/dist/core/repo/templates/vibe-readme.d.ts +6 -0
- package/dist/core/repo/templates/vibe-readme.d.ts.map +1 -0
- package/dist/core/repo/templates/vibe-readme.js +204 -0
- package/dist/core/repo/templates/vibe-readme.js.map +1 -0
- package/dist/core/repo/templates/vibe-scripts.d.ts +6 -0
- package/dist/core/repo/templates/vibe-scripts.d.ts.map +1 -0
- package/dist/core/repo/templates/vibe-scripts.js +308 -0
- package/dist/core/repo/templates/vibe-scripts.js.map +1 -0
- package/dist/core/repo/validation.d.ts +46 -0
- package/dist/core/repo/validation.d.ts.map +1 -0
- package/dist/core/repo/validation.js +154 -0
- package/dist/core/repo/validation.js.map +1 -0
- package/dist/core/runner.d.ts +38 -0
- package/dist/core/runner.d.ts.map +1 -0
- package/dist/core/runner.js +124 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/core/send.d.ts +83 -0
- package/dist/core/send.d.ts.map +1 -0
- package/dist/core/send.js +565 -0
- package/dist/core/send.js.map +1 -0
- package/dist/core/status.d.ts +76 -0
- package/dist/core/status.d.ts.map +1 -0
- package/dist/core/status.js +430 -0
- package/dist/core/status.js.map +1 -0
- package/dist/core/sync/aggregator.d.ts +22 -0
- package/dist/core/sync/aggregator.d.ts.map +1 -0
- package/dist/core/sync/aggregator.js +278 -0
- package/dist/core/sync/aggregator.js.map +1 -0
- package/dist/core/sync/completion.d.ts +37 -0
- package/dist/core/sync/completion.d.ts.map +1 -0
- package/dist/core/sync/completion.js +264 -0
- package/dist/core/sync/completion.js.map +1 -0
- package/dist/core/sync/index.d.ts +51 -0
- package/dist/core/sync/index.d.ts.map +1 -0
- package/dist/core/sync/index.js +200 -0
- package/dist/core/sync/index.js.map +1 -0
- package/dist/core/sync/scanner.d.ts +39 -0
- package/dist/core/sync/scanner.d.ts.map +1 -0
- package/dist/core/sync/scanner.js +364 -0
- package/dist/core/sync/scanner.js.map +1 -0
- package/dist/types/config.d.ts +157 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +58 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/coverage.d.ts +100 -0
- package/dist/types/coverage.d.ts.map +1 -0
- package/dist/types/coverage.js +8 -0
- package/dist/types/coverage.js.map +1 -0
- package/dist/types/doctor.d.ts +68 -0
- package/dist/types/doctor.d.ts.map +1 -0
- package/dist/types/doctor.js +5 -0
- package/dist/types/doctor.js.map +1 -0
- package/dist/types/gaps.d.ts +129 -0
- package/dist/types/gaps.d.ts.map +1 -0
- package/dist/types/gaps.js +8 -0
- package/dist/types/gaps.js.map +1 -0
- package/dist/types/prompts.d.ts +99 -0
- package/dist/types/prompts.d.ts.map +1 -0
- package/dist/types/prompts.js +5 -0
- package/dist/types/prompts.js.map +1 -0
- package/dist/types/runner.d.ts +156 -0
- package/dist/types/runner.d.ts.map +1 -0
- package/dist/types/runner.js +41 -0
- package/dist/types/runner.js.map +1 -0
- package/dist/types/send.d.ts +157 -0
- package/dist/types/send.d.ts.map +1 -0
- package/dist/types/send.js +18 -0
- package/dist/types/send.js.map +1 -0
- package/dist/types/status.d.ts +150 -0
- package/dist/types/status.d.ts.map +1 -0
- package/dist/types/status.js +15 -0
- package/dist/types/status.js.map +1 -0
- package/dist/types/sync.d.ts +259 -0
- package/dist/types/sync.d.ts.map +1 -0
- package/dist/types/sync.js +38 -0
- package/dist/types/sync.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync status health checks
|
|
3
|
+
* Validates sync data freshness
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, statSync } from 'fs';
|
|
6
|
+
import { readdir } from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { findProjectRoot } from '../project.js';
|
|
9
|
+
/**
|
|
10
|
+
* Default stale threshold in minutes
|
|
11
|
+
*/
|
|
12
|
+
const STALE_THRESHOLD_MINUTES = 30;
|
|
13
|
+
/**
|
|
14
|
+
* Get last sync time from sync-cache directory
|
|
15
|
+
*/
|
|
16
|
+
async function getLastSyncTime(projectRoot) {
|
|
17
|
+
const syncCachePath = path.join(projectRoot, 'docs/sync-cache');
|
|
18
|
+
if (!existsSync(syncCachePath)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const entries = await readdir(syncCachePath, { withFileTypes: true });
|
|
23
|
+
let latestTime = null;
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const entryPath = path.join(syncCachePath, entry.name);
|
|
26
|
+
const stats = statSync(entryPath);
|
|
27
|
+
const mtime = stats.mtime;
|
|
28
|
+
if (!latestTime || mtime > latestTime) {
|
|
29
|
+
latestTime = mtime;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return latestTime;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Format time difference as human-readable string
|
|
40
|
+
*/
|
|
41
|
+
function formatTimeDiff(minutes) {
|
|
42
|
+
if (minutes < 1) {
|
|
43
|
+
return 'just now';
|
|
44
|
+
}
|
|
45
|
+
if (minutes < 60) {
|
|
46
|
+
return `${Math.floor(minutes)} minute${Math.floor(minutes) === 1 ? '' : 's'} ago`;
|
|
47
|
+
}
|
|
48
|
+
const hours = Math.floor(minutes / 60);
|
|
49
|
+
if (hours < 24) {
|
|
50
|
+
return `${hours} hour${hours === 1 ? '' : 's'} ago`;
|
|
51
|
+
}
|
|
52
|
+
const days = Math.floor(hours / 24);
|
|
53
|
+
return `${days} day${days === 1 ? '' : 's'} ago`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Sync cache exists check
|
|
57
|
+
*/
|
|
58
|
+
export const syncCacheExistsCheck = {
|
|
59
|
+
name: 'Sync cache exists',
|
|
60
|
+
category: 'sync',
|
|
61
|
+
critical: false,
|
|
62
|
+
run: async () => {
|
|
63
|
+
const cwd = process.cwd();
|
|
64
|
+
const projectRoot = findProjectRoot(cwd);
|
|
65
|
+
if (!projectRoot) {
|
|
66
|
+
return {
|
|
67
|
+
name: 'Sync cache exists',
|
|
68
|
+
category: 'sync',
|
|
69
|
+
status: 'warning',
|
|
70
|
+
details: 'No project found',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const syncCachePath = path.join(projectRoot, 'docs/sync-cache');
|
|
74
|
+
if (existsSync(syncCachePath)) {
|
|
75
|
+
return {
|
|
76
|
+
name: 'Sync cache exists',
|
|
77
|
+
category: 'sync',
|
|
78
|
+
status: 'passed',
|
|
79
|
+
details: 'docs/sync-cache directory exists',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
name: 'Sync cache exists',
|
|
84
|
+
category: 'sync',
|
|
85
|
+
status: 'warning',
|
|
86
|
+
details: 'docs/sync-cache not found',
|
|
87
|
+
fix: 'Run `vibe sync` to create sync cache',
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Sync data freshness check
|
|
93
|
+
*/
|
|
94
|
+
export const syncDataFreshCheck = {
|
|
95
|
+
name: 'Sync data fresh',
|
|
96
|
+
category: 'sync',
|
|
97
|
+
critical: false,
|
|
98
|
+
run: async () => {
|
|
99
|
+
const cwd = process.cwd();
|
|
100
|
+
const projectRoot = findProjectRoot(cwd);
|
|
101
|
+
if (!projectRoot) {
|
|
102
|
+
return {
|
|
103
|
+
name: 'Sync data fresh',
|
|
104
|
+
category: 'sync',
|
|
105
|
+
status: 'warning',
|
|
106
|
+
details: 'No project found',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const lastSync = await getLastSyncTime(projectRoot);
|
|
110
|
+
if (!lastSync) {
|
|
111
|
+
return {
|
|
112
|
+
name: 'Sync data fresh',
|
|
113
|
+
category: 'sync',
|
|
114
|
+
status: 'warning',
|
|
115
|
+
details: 'No sync data found',
|
|
116
|
+
fix: 'Run `vibe sync` to sync data from repositories',
|
|
117
|
+
fixable: true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const ageMinutes = (Date.now() - lastSync.getTime()) / 1000 / 60;
|
|
121
|
+
if (ageMinutes < STALE_THRESHOLD_MINUTES) {
|
|
122
|
+
return {
|
|
123
|
+
name: 'Sync data fresh',
|
|
124
|
+
category: 'sync',
|
|
125
|
+
status: 'passed',
|
|
126
|
+
details: `Last synced ${formatTimeDiff(ageMinutes)}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
name: 'Sync data fresh',
|
|
131
|
+
category: 'sync',
|
|
132
|
+
status: 'warning',
|
|
133
|
+
details: `Data is ${formatTimeDiff(ageMinutes)} old (stale after ${STALE_THRESHOLD_MINUTES}min)`,
|
|
134
|
+
fix: 'Run `vibe sync` to refresh data',
|
|
135
|
+
fixable: true,
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* All sync checks
|
|
141
|
+
*/
|
|
142
|
+
export const syncChecks = [
|
|
143
|
+
syncCacheExistsCheck,
|
|
144
|
+
syncDataFreshCheck,
|
|
145
|
+
];
|
|
146
|
+
//# sourceMappingURL=sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../../src/core/doctor/sync.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core gaps module - gap analysis between PRD and implementation
|
|
3
|
+
*
|
|
4
|
+
* Identifies missing implementations, partial implementations,
|
|
5
|
+
* and orphaned implementations using cached data only.
|
|
6
|
+
*/
|
|
7
|
+
import { Gap, GapPriority, MissingGap, PartialGap, OrphanedGap, GapSummary, SuggestedScope, GapAnalysisData, GapsJsonOutput, GapFilterOptions } from '../types/gaps.js';
|
|
8
|
+
/**
|
|
9
|
+
* Parsed requirement from PRD
|
|
10
|
+
*/
|
|
11
|
+
interface ParsedRequirement {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
module: string;
|
|
15
|
+
priority: string;
|
|
16
|
+
tags: string[];
|
|
17
|
+
description: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parse requirement files from PRD modules
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseRequirements(projectPath: string): Promise<ParsedRequirement[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Assign gap priority based on tags and conditions
|
|
25
|
+
*/
|
|
26
|
+
export declare function assignGapPriority(tags: string[], hasBlocker: boolean, originalPriority: string): GapPriority;
|
|
27
|
+
/**
|
|
28
|
+
* Find requirements without linked scopes (missing implementations)
|
|
29
|
+
*/
|
|
30
|
+
export declare function findMissingImplementations(projectPath: string): Promise<MissingGap[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Find scopes that are in progress but not completed (partial implementations)
|
|
33
|
+
*/
|
|
34
|
+
export declare function findPartialImplementations(projectPath: string): Promise<PartialGap[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Find orphaned implementations from sync cache
|
|
37
|
+
* Note: This is a simplified implementation that looks for
|
|
38
|
+
* implementation plans not linked to PRD requirements
|
|
39
|
+
*/
|
|
40
|
+
export declare function findOrphanedImplementations(projectPath: string): Promise<OrphanedGap[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Calculate gap summary from gap lists
|
|
43
|
+
*/
|
|
44
|
+
export declare function calculateSummary(missing: MissingGap[], partial: PartialGap[], orphaned: OrphanedGap[]): GapSummary;
|
|
45
|
+
/**
|
|
46
|
+
* Generate scope suggestions from missing gaps
|
|
47
|
+
*/
|
|
48
|
+
export declare function generateSuggestions(gaps: MissingGap[]): SuggestedScope[];
|
|
49
|
+
/**
|
|
50
|
+
* Apply filters to gaps
|
|
51
|
+
*/
|
|
52
|
+
export declare function filterGaps<T extends Gap>(gaps: T[], options: GapFilterOptions): T[];
|
|
53
|
+
/**
|
|
54
|
+
* Perform complete gap analysis
|
|
55
|
+
*/
|
|
56
|
+
export declare function analyzeGaps(projectPath: string, options?: GapFilterOptions): Promise<GapAnalysisData>;
|
|
57
|
+
/**
|
|
58
|
+
* Get list of available module names
|
|
59
|
+
*/
|
|
60
|
+
export declare function getModuleNames(projectPath: string): Promise<string[]>;
|
|
61
|
+
/**
|
|
62
|
+
* Convert gap analysis data to JSON output format
|
|
63
|
+
*/
|
|
64
|
+
export declare function toGapsJsonOutput(data: GapAnalysisData): GapsJsonOutput;
|
|
65
|
+
/**
|
|
66
|
+
* Check if there are any P1 gaps (for CI mode)
|
|
67
|
+
*/
|
|
68
|
+
export declare function hasP1Gaps(data: GapAnalysisData): boolean;
|
|
69
|
+
export {};
|
|
70
|
+
//# sourceMappingURL=gaps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gaps.d.ts","sourceRoot":"","sources":["../../src/core/gaps.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,EACL,GAAG,EACH,WAAW,EACX,UAAU,EACV,UAAU,EACV,WAAW,EACX,UAAU,EAGV,cAAc,EACd,eAAe,EACf,cAAc,EACd,gBAAgB,EACjB,MAAM,kBAAkB,CAAC;AAc1B;;GAEG;AACH,UAAU,iBAAiB;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAmEzF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EAAE,EACd,UAAU,EAAE,OAAO,EACnB,gBAAgB,EAAE,MAAM,GACvB,WAAW,CAkBb;AAED;;GAEG;AACH,wBAAsB,0BAA0B,CAC9C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,UAAU,EAAE,CAAC,CA6BvB;AAoDD;;GAEG;AACH,wBAAsB,0BAA0B,CAC9C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,UAAU,EAAE,CAAC,CA6BvB;AAED;;;;GAIG;AACH,wBAAsB,2BAA2B,CAC/C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,WAAW,EAAE,CAAC,CAwExB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,UAAU,EAAE,EACrB,OAAO,EAAE,UAAU,EAAE,EACrB,QAAQ,EAAE,WAAW,EAAE,GACtB,UAAU,CAuBZ;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,cAAc,EAAE,CAqExE;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,GAAG,EACtC,IAAI,EAAE,CAAC,EAAE,EACT,OAAO,EAAE,gBAAgB,GACxB,CAAC,EAAE,CAqBL;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAkC1B;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAa3E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,cAAc,CAmBtE;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAExD"}
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core gaps module - gap analysis between PRD and implementation
|
|
3
|
+
*
|
|
4
|
+
* Identifies missing implementations, partial implementations,
|
|
5
|
+
* and orphaned implementations using cached data only.
|
|
6
|
+
*/
|
|
7
|
+
import { readFile, readdir } from 'fs/promises';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { loadScopes } from './coverage.js';
|
|
11
|
+
/**
|
|
12
|
+
* Priority tags that indicate P1 (security/compliance)
|
|
13
|
+
*/
|
|
14
|
+
const P1_TAGS = ['security', 'compliance', 'critical', 'blocking'];
|
|
15
|
+
/**
|
|
16
|
+
* Priority tags that indicate P2 (core functionality)
|
|
17
|
+
*/
|
|
18
|
+
const P2_TAGS = ['core', 'essential', 'important'];
|
|
19
|
+
/**
|
|
20
|
+
* Parse requirement files from PRD modules
|
|
21
|
+
*/
|
|
22
|
+
export async function parseRequirements(projectPath) {
|
|
23
|
+
const requirements = [];
|
|
24
|
+
const modulesDir = path.join(projectPath, 'docs', 'prd', 'modules');
|
|
25
|
+
try {
|
|
26
|
+
if (!existsSync(modulesDir)) {
|
|
27
|
+
return requirements;
|
|
28
|
+
}
|
|
29
|
+
const moduleDirs = await readdir(modulesDir);
|
|
30
|
+
for (const moduleDir of moduleDirs) {
|
|
31
|
+
const modulePath = path.join(modulesDir, moduleDir);
|
|
32
|
+
try {
|
|
33
|
+
const files = await readdir(modulePath);
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
if (!file.endsWith('.md') || file.startsWith('_'))
|
|
36
|
+
continue;
|
|
37
|
+
const filePath = path.join(modulePath, file);
|
|
38
|
+
const content = await readFile(filePath, 'utf-8');
|
|
39
|
+
// Check if this is a requirement file
|
|
40
|
+
const idMatch = content.match(/\*\*ID\*\*\s*\|\s*`?(REQ-[A-Z]+-\d+)`?/);
|
|
41
|
+
if (!idMatch || !idMatch[1])
|
|
42
|
+
continue;
|
|
43
|
+
const id = idMatch[1];
|
|
44
|
+
// Extract title
|
|
45
|
+
const titleMatch = content.match(/^#\s+(?:REQ:\s*)?(.+?)(?:\n|$)/m);
|
|
46
|
+
const title = titleMatch?.[1] ?? id;
|
|
47
|
+
// Extract priority
|
|
48
|
+
const priorityMatch = content.match(/\*\*Priority\*\*\s*\|\s*`?(P[1234])`?/);
|
|
49
|
+
const priority = priorityMatch?.[1] ?? 'P3';
|
|
50
|
+
// Extract tags
|
|
51
|
+
const tagsMatch = content.match(/\*\*Tags\*\*\s*\|\s*([^|\n]+)/);
|
|
52
|
+
const tagsStr = tagsMatch?.[1]?.trim() ?? '';
|
|
53
|
+
const tags = tagsStr
|
|
54
|
+
.split(',')
|
|
55
|
+
.map((t) => t.trim().toLowerCase())
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
// Extract description
|
|
58
|
+
const descMatch = content.match(/## Description\s*\n\n([^\n]+)/);
|
|
59
|
+
const description = descMatch?.[1] ?? '';
|
|
60
|
+
requirements.push({
|
|
61
|
+
id,
|
|
62
|
+
title,
|
|
63
|
+
module: moduleDir,
|
|
64
|
+
priority,
|
|
65
|
+
tags,
|
|
66
|
+
description,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Can't read module directory
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Can't read modules directory
|
|
77
|
+
}
|
|
78
|
+
return requirements;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Assign gap priority based on tags and conditions
|
|
82
|
+
*/
|
|
83
|
+
export function assignGapPriority(tags, hasBlocker, originalPriority) {
|
|
84
|
+
// P1: Security/compliance tags or blocking status
|
|
85
|
+
if (tags.some((tag) => P1_TAGS.includes(tag)) || hasBlocker) {
|
|
86
|
+
return 'P1';
|
|
87
|
+
}
|
|
88
|
+
// P2: Core functionality tags or elevated from blocker
|
|
89
|
+
if (tags.some((tag) => P2_TAGS.includes(tag))) {
|
|
90
|
+
return 'P2';
|
|
91
|
+
}
|
|
92
|
+
// Use original priority from requirement if available
|
|
93
|
+
if (originalPriority === 'P1')
|
|
94
|
+
return 'P1';
|
|
95
|
+
if (originalPriority === 'P2')
|
|
96
|
+
return 'P2';
|
|
97
|
+
if (originalPriority === 'P3')
|
|
98
|
+
return 'P3';
|
|
99
|
+
// Default for orphaned
|
|
100
|
+
return 'P4';
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Find requirements without linked scopes (missing implementations)
|
|
104
|
+
*/
|
|
105
|
+
export async function findMissingImplementations(projectPath) {
|
|
106
|
+
const requirements = await parseRequirements(projectPath);
|
|
107
|
+
const scopes = await loadScopes(projectPath);
|
|
108
|
+
const missing = [];
|
|
109
|
+
for (const req of requirements) {
|
|
110
|
+
// Check if any scope links to this requirement
|
|
111
|
+
const linkedScope = scopes.find((scope) => scope.requirements.includes(req.id));
|
|
112
|
+
if (!linkedScope) {
|
|
113
|
+
const priority = assignGapPriority(req.tags, false, req.priority);
|
|
114
|
+
missing.push({
|
|
115
|
+
type: 'missing',
|
|
116
|
+
priority,
|
|
117
|
+
requirementId: req.id,
|
|
118
|
+
title: req.title,
|
|
119
|
+
module: req.module,
|
|
120
|
+
impact: req.description.slice(0, 100) || 'No description available',
|
|
121
|
+
tags: req.tags,
|
|
122
|
+
suggestion: `Create a scope to implement ${req.id}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return missing;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Parse scope file for additional details (progress, blockers)
|
|
130
|
+
*/
|
|
131
|
+
async function parseScopeDetails(projectPath, scope) {
|
|
132
|
+
// Determine the directory based on status
|
|
133
|
+
const statusToDir = {
|
|
134
|
+
draft: 'drafts',
|
|
135
|
+
ready: 'ready',
|
|
136
|
+
sent: 'sent',
|
|
137
|
+
completed: 'completed',
|
|
138
|
+
};
|
|
139
|
+
const dir = statusToDir[scope.status] ?? 'drafts';
|
|
140
|
+
const scopesDir = path.join(projectPath, 'docs', 'scopes', dir);
|
|
141
|
+
// Find the scope file
|
|
142
|
+
try {
|
|
143
|
+
const files = await readdir(scopesDir);
|
|
144
|
+
const scopeFile = files.find((f) => f.startsWith(`${scope.id}-`) || f === `${scope.id}.md`);
|
|
145
|
+
if (!scopeFile) {
|
|
146
|
+
return { progress: 0, blocked: false };
|
|
147
|
+
}
|
|
148
|
+
const filePath = path.join(scopesDir, scopeFile);
|
|
149
|
+
const content = await readFile(filePath, 'utf-8');
|
|
150
|
+
// Extract progress (if available)
|
|
151
|
+
const progressMatch = content.match(/\*\*Progress\*\*\s*\|\s*`?(\d+)%?`?/);
|
|
152
|
+
const progress = progressMatch && progressMatch[1] ? parseInt(progressMatch[1], 10) : 0;
|
|
153
|
+
// Check for blockers
|
|
154
|
+
const blockerMatch = content.match(/\*\*Blocked\*\*\s*\|\s*`?([^|\n`]+)`?/);
|
|
155
|
+
const blockerValue = blockerMatch?.[1]?.trim();
|
|
156
|
+
const blocked = blockerValue !== undefined &&
|
|
157
|
+
blockerValue.toLowerCase() !== 'no' &&
|
|
158
|
+
blockerValue !== '-';
|
|
159
|
+
const blockerReason = blocked ? blockerValue : undefined;
|
|
160
|
+
return { progress, blocked, blockerReason };
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return { progress: 0, blocked: false };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Find scopes that are in progress but not completed (partial implementations)
|
|
168
|
+
*/
|
|
169
|
+
export async function findPartialImplementations(projectPath) {
|
|
170
|
+
const scopes = await loadScopes(projectPath);
|
|
171
|
+
const partial = [];
|
|
172
|
+
for (const scope of scopes) {
|
|
173
|
+
// Only include sent or ready scopes (in progress but not completed)
|
|
174
|
+
if (scope.status !== 'sent' && scope.status !== 'ready') {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const details = await parseScopeDetails(projectPath, scope);
|
|
178
|
+
// A partial gap is a scope that's in progress with < 100% completion
|
|
179
|
+
const priority = assignGapPriority([], details.blocked, 'P3');
|
|
180
|
+
partial.push({
|
|
181
|
+
type: 'partial',
|
|
182
|
+
priority: details.blocked ? 'P2' : priority,
|
|
183
|
+
scopeId: scope.id,
|
|
184
|
+
title: scope.title,
|
|
185
|
+
status: scope.status,
|
|
186
|
+
progress: details.progress,
|
|
187
|
+
blocked: details.blocked,
|
|
188
|
+
blockerReason: details.blockerReason,
|
|
189
|
+
requirements: scope.requirements,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return partial;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Find orphaned implementations from sync cache
|
|
196
|
+
* Note: This is a simplified implementation that looks for
|
|
197
|
+
* implementation plans not linked to PRD requirements
|
|
198
|
+
*/
|
|
199
|
+
export async function findOrphanedImplementations(projectPath) {
|
|
200
|
+
const orphaned = [];
|
|
201
|
+
const syncCacheDir = path.join(projectPath, 'docs', 'sync-cache');
|
|
202
|
+
try {
|
|
203
|
+
if (!existsSync(syncCacheDir)) {
|
|
204
|
+
return orphaned;
|
|
205
|
+
}
|
|
206
|
+
// Load last sync data to get repository information
|
|
207
|
+
const lastSyncPath = path.join(syncCacheDir, 'last-sync.json');
|
|
208
|
+
if (!existsSync(lastSyncPath)) {
|
|
209
|
+
return orphaned;
|
|
210
|
+
}
|
|
211
|
+
const lastSyncContent = await readFile(lastSyncPath, 'utf-8');
|
|
212
|
+
const lastSync = JSON.parse(lastSyncContent);
|
|
213
|
+
// Load all requirements for checking links
|
|
214
|
+
const requirements = await parseRequirements(projectPath);
|
|
215
|
+
const reqIds = new Set(requirements.map((r) => r.id));
|
|
216
|
+
// Check each repository's impl-plans for orphaned implementations
|
|
217
|
+
for (const [repoAlias, _repoData] of Object.entries(lastSync.repos ?? {})) {
|
|
218
|
+
const repoDir = path.join(syncCacheDir, 'repos', repoAlias);
|
|
219
|
+
if (!existsSync(repoDir))
|
|
220
|
+
continue;
|
|
221
|
+
const implPlansDir = path.join(repoDir, 'impl-plans');
|
|
222
|
+
if (!existsSync(implPlansDir))
|
|
223
|
+
continue;
|
|
224
|
+
try {
|
|
225
|
+
const planFiles = await readdir(implPlansDir);
|
|
226
|
+
for (const planFile of planFiles) {
|
|
227
|
+
if (!planFile.endsWith('.md'))
|
|
228
|
+
continue;
|
|
229
|
+
const planPath = path.join(implPlansDir, planFile);
|
|
230
|
+
const content = await readFile(planPath, 'utf-8');
|
|
231
|
+
// Extract requirement references from the plan
|
|
232
|
+
const reqMatches = content.matchAll(/REQ-[A-Z]+-\d+/g);
|
|
233
|
+
const linkedReqs = new Set([...reqMatches].map((m) => m[0]));
|
|
234
|
+
// Check if all linked requirements exist
|
|
235
|
+
const orphanedReqs = [...linkedReqs].filter((r) => !reqIds.has(r));
|
|
236
|
+
if (orphanedReqs.length > 0 || linkedReqs.size === 0) {
|
|
237
|
+
const titleMatch = content.match(/^#\s+(.+?)(?:\n|$)/m);
|
|
238
|
+
const identifier = titleMatch?.[1] ?? planFile.replace('.md', '');
|
|
239
|
+
orphaned.push({
|
|
240
|
+
type: 'orphaned',
|
|
241
|
+
priority: 'P4',
|
|
242
|
+
identifier,
|
|
243
|
+
repository: repoAlias,
|
|
244
|
+
files: [planFile],
|
|
245
|
+
suggestedAction: linkedReqs.size === 0
|
|
246
|
+
? 'Link to PRD requirements or remove if obsolete'
|
|
247
|
+
: `Update to use valid requirements (${orphanedReqs.join(', ')} not found)`,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Can't read impl-plans directory
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Can't read sync cache
|
|
259
|
+
}
|
|
260
|
+
return orphaned;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Calculate gap summary from gap lists
|
|
264
|
+
*/
|
|
265
|
+
export function calculateSummary(missing, partial, orphaned) {
|
|
266
|
+
const allGaps = [...missing, ...partial, ...orphaned];
|
|
267
|
+
const byPriority = {
|
|
268
|
+
P1: 0,
|
|
269
|
+
P2: 0,
|
|
270
|
+
P3: 0,
|
|
271
|
+
P4: 0,
|
|
272
|
+
total: allGaps.length,
|
|
273
|
+
};
|
|
274
|
+
const byType = {
|
|
275
|
+
missing: missing.length,
|
|
276
|
+
partial: partial.length,
|
|
277
|
+
orphaned: orphaned.length,
|
|
278
|
+
total: allGaps.length,
|
|
279
|
+
};
|
|
280
|
+
for (const gap of allGaps) {
|
|
281
|
+
byPriority[gap.priority]++;
|
|
282
|
+
}
|
|
283
|
+
return { byPriority, byType };
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Generate scope suggestions from missing gaps
|
|
287
|
+
*/
|
|
288
|
+
export function generateSuggestions(gaps) {
|
|
289
|
+
// Group gaps by module
|
|
290
|
+
const byModule = new Map();
|
|
291
|
+
for (const gap of gaps) {
|
|
292
|
+
const existing = byModule.get(gap.module) ?? [];
|
|
293
|
+
existing.push(gap);
|
|
294
|
+
byModule.set(gap.module, existing);
|
|
295
|
+
}
|
|
296
|
+
const suggestions = [];
|
|
297
|
+
for (const [module, moduleGaps] of byModule) {
|
|
298
|
+
// Further group by priority within module
|
|
299
|
+
const p1Gaps = moduleGaps.filter((g) => g.priority === 'P1');
|
|
300
|
+
const p2Gaps = moduleGaps.filter((g) => g.priority === 'P2');
|
|
301
|
+
const otherGaps = moduleGaps.filter((g) => g.priority !== 'P1' && g.priority !== 'P2');
|
|
302
|
+
// Create suggestions for P1 gaps (one per gap due to urgency)
|
|
303
|
+
for (const gap of p1Gaps) {
|
|
304
|
+
suggestions.push({
|
|
305
|
+
title: `Implement ${gap.requirementId}: ${gap.title}`,
|
|
306
|
+
priority: 'P1',
|
|
307
|
+
requirements: [gap.requirementId],
|
|
308
|
+
estimatedEffort: 'M',
|
|
309
|
+
module,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
// Bundle P2 gaps by module (max 3 per scope)
|
|
313
|
+
if (p2Gaps.length > 0) {
|
|
314
|
+
for (let i = 0; i < p2Gaps.length; i += 3) {
|
|
315
|
+
const batch = p2Gaps.slice(i, i + 3);
|
|
316
|
+
const effort = batch.length >= 3 ? 'L' : batch.length >= 2 ? 'M' : 'S';
|
|
317
|
+
suggestions.push({
|
|
318
|
+
title: `${module}: ${batch.map((g) => g.title).join(', ')}`,
|
|
319
|
+
priority: 'P2',
|
|
320
|
+
requirements: batch.map((g) => g.requirementId),
|
|
321
|
+
estimatedEffort: effort,
|
|
322
|
+
module,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Bundle other gaps by module (max 5 per scope)
|
|
327
|
+
if (otherGaps.length > 0) {
|
|
328
|
+
for (let i = 0; i < otherGaps.length; i += 5) {
|
|
329
|
+
const batch = otherGaps.slice(i, i + 5);
|
|
330
|
+
const effort = batch.length >= 4 ? 'L' : batch.length >= 2 ? 'M' : 'S';
|
|
331
|
+
suggestions.push({
|
|
332
|
+
title: `${module}: ${batch.map((g) => g.title).join(', ')}`,
|
|
333
|
+
priority: 'P3',
|
|
334
|
+
requirements: batch.map((g) => g.requirementId),
|
|
335
|
+
estimatedEffort: effort,
|
|
336
|
+
module,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Sort by priority
|
|
342
|
+
suggestions.sort((a, b) => {
|
|
343
|
+
const order = { P1: 0, P2: 1, P3: 2, P4: 3 };
|
|
344
|
+
return order[a.priority] - order[b.priority];
|
|
345
|
+
});
|
|
346
|
+
return suggestions;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Apply filters to gaps
|
|
350
|
+
*/
|
|
351
|
+
export function filterGaps(gaps, options) {
|
|
352
|
+
let filtered = [...gaps];
|
|
353
|
+
if (options.priority) {
|
|
354
|
+
filtered = filtered.filter((g) => g.priority === options.priority);
|
|
355
|
+
}
|
|
356
|
+
if (options.type) {
|
|
357
|
+
filtered = filtered.filter((g) => g.type === options.type);
|
|
358
|
+
}
|
|
359
|
+
if (options.module) {
|
|
360
|
+
filtered = filtered.filter((g) => {
|
|
361
|
+
if (g.type === 'missing') {
|
|
362
|
+
return g.module.toLowerCase() === options.module.toLowerCase();
|
|
363
|
+
}
|
|
364
|
+
return true; // Other types don't have module field
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return filtered;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Perform complete gap analysis
|
|
371
|
+
*/
|
|
372
|
+
export async function analyzeGaps(projectPath, options = {}) {
|
|
373
|
+
// Find all gaps
|
|
374
|
+
let missing = await findMissingImplementations(projectPath);
|
|
375
|
+
let partial = await findPartialImplementations(projectPath);
|
|
376
|
+
let orphaned = await findOrphanedImplementations(projectPath);
|
|
377
|
+
// Apply filters
|
|
378
|
+
if (options.priority || options.module) {
|
|
379
|
+
missing = filterGaps(missing, options);
|
|
380
|
+
partial = filterGaps(partial, options);
|
|
381
|
+
orphaned = filterGaps(orphaned, options);
|
|
382
|
+
}
|
|
383
|
+
if (options.type) {
|
|
384
|
+
if (options.type !== 'missing')
|
|
385
|
+
missing = [];
|
|
386
|
+
if (options.type !== 'partial')
|
|
387
|
+
partial = [];
|
|
388
|
+
if (options.type !== 'orphaned')
|
|
389
|
+
orphaned = [];
|
|
390
|
+
}
|
|
391
|
+
// Calculate summary
|
|
392
|
+
const summary = calculateSummary(missing, partial, orphaned);
|
|
393
|
+
// Generate suggestions from missing gaps
|
|
394
|
+
const suggestions = generateSuggestions(missing);
|
|
395
|
+
return {
|
|
396
|
+
summary,
|
|
397
|
+
gaps: {
|
|
398
|
+
missing,
|
|
399
|
+
partial,
|
|
400
|
+
orphaned,
|
|
401
|
+
},
|
|
402
|
+
suggestions,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Get list of available module names
|
|
407
|
+
*/
|
|
408
|
+
export async function getModuleNames(projectPath) {
|
|
409
|
+
const modulesDir = path.join(projectPath, 'docs', 'prd', 'modules');
|
|
410
|
+
try {
|
|
411
|
+
if (!existsSync(modulesDir)) {
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
const entries = await readdir(modulesDir, { withFileTypes: true });
|
|
415
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Convert gap analysis data to JSON output format
|
|
423
|
+
*/
|
|
424
|
+
export function toGapsJsonOutput(data) {
|
|
425
|
+
const allGaps = [
|
|
426
|
+
...data.gaps.missing,
|
|
427
|
+
...data.gaps.partial,
|
|
428
|
+
...data.gaps.orphaned,
|
|
429
|
+
];
|
|
430
|
+
// Sort by priority
|
|
431
|
+
allGaps.sort((a, b) => {
|
|
432
|
+
const order = { P1: 0, P2: 1, P3: 2, P4: 3 };
|
|
433
|
+
return order[a.priority] - order[b.priority];
|
|
434
|
+
});
|
|
435
|
+
return {
|
|
436
|
+
timestamp: new Date().toISOString(),
|
|
437
|
+
summary: data.summary,
|
|
438
|
+
gaps: allGaps,
|
|
439
|
+
suggestions: data.suggestions,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Check if there are any P1 gaps (for CI mode)
|
|
444
|
+
*/
|
|
445
|
+
export function hasP1Gaps(data) {
|
|
446
|
+
return data.summary.byPriority.P1 > 0;
|
|
447
|
+
}
|
|
448
|
+
//# sourceMappingURL=gaps.js.map
|