vibepro 0.1.0-alpha.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 +201 -0
- package/NOTICE +9 -0
- package/README.ja.md +448 -0
- package/README.md +520 -0
- package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
- package/bin/vibepro.js +9 -0
- package/docs/assets/vibepro-header.png +0 -0
- package/package.json +51 -0
- package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
- package/skills/vibepro-human-review/SKILL.md +73 -0
- package/skills/vibepro-story-refactor/SKILL.md +89 -0
- package/skills/vibepro-workflow/SKILL.md +139 -0
- package/src/agent-harness-map.js +230 -0
- package/src/agent-harness-scanner.js +337 -0
- package/src/agent-review.js +2180 -0
- package/src/api-boundary-scanner.js +452 -0
- package/src/architecture-profiler.js +423 -0
- package/src/authorization-scoring.js +149 -0
- package/src/brainbase-importer.js +534 -0
- package/src/change-risk-classifier.js +195 -0
- package/src/check-packs.js +605 -0
- package/src/checkpoint-manager.js +233 -0
- package/src/cli.js +2213 -0
- package/src/code-quality-scanner.js +310 -0
- package/src/codex-manager.js +143 -0
- package/src/component-style-scanner.js +336 -0
- package/src/coverage-report.js +99 -0
- package/src/database-access-scanner.js +163 -0
- package/src/decision-records.js +315 -0
- package/src/design-modernize.js +1435 -0
- package/src/design-system.js +1732 -0
- package/src/diagnostic-engine.js +1945 -0
- package/src/diagram-requirement-resolver.js +194 -0
- package/src/doctor.js +677 -0
- package/src/environment-graph.js +424 -0
- package/src/execution-state.js +849 -0
- package/src/explore-evidence.js +425 -0
- package/src/flow-design-scanner.js +896 -0
- package/src/flow-verifier.js +887 -0
- package/src/gesture-interaction-scanner.js +330 -0
- package/src/graph-context.js +263 -0
- package/src/graphify-adapter.js +189 -0
- package/src/html-report.js +1035 -0
- package/src/journey-map.js +1299 -0
- package/src/language.js +48 -0
- package/src/lazy-pattern-detector.js +182 -0
- package/src/local-dev-scanner.js +135 -0
- package/src/managed-worktree-gate.js +187 -0
- package/src/managed-worktree.js +766 -0
- package/src/merge-manager.js +501 -0
- package/src/network-contract-scanner.js +442 -0
- package/src/nocodb-story-sync.js +386 -0
- package/src/oss-readiness-scanner.js +417 -0
- package/src/performance-evidence.js +756 -0
- package/src/performance-measurer.js +591 -0
- package/src/pr-manager.js +8220 -0
- package/src/presets.js +682 -0
- package/src/public-discovery-scanner.js +519 -0
- package/src/refactoring-delta-reporter.js +367 -0
- package/src/refactoring-opportunity-generator.js +797 -0
- package/src/regression-risk-scanner.js +146 -0
- package/src/repo-status.js +266 -0
- package/src/report-fingerprint.js +188 -0
- package/src/report-pr-body-prompt-template.md +108 -0
- package/src/report-pr-body-schema.json +95 -0
- package/src/report-store.js +135 -0
- package/src/report-validator.js +192 -0
- package/src/requirement-consistency.js +1066 -0
- package/src/runtime-info.js +134 -0
- package/src/self-dogfood-scanner.js +476 -0
- package/src/session-learning.js +164 -0
- package/src/skills-manager.js +157 -0
- package/src/spec-drift.js +378 -0
- package/src/spec-fingerprint.js +445 -0
- package/src/spec-prompt-template.md +155 -0
- package/src/spec-schema.json +219 -0
- package/src/spec-store.js +258 -0
- package/src/spec-validator.js +459 -0
- package/src/static-site-scanner.js +316 -0
- package/src/story-candidate-generator.js +85 -0
- package/src/story-catalog-generator.js +2813 -0
- package/src/story-html.js +156 -0
- package/src/story-manager.js +2144 -0
- package/src/story-task-generator.js +522 -0
- package/src/task-manager.js +1029 -0
- package/src/terminal-link-scanner.js +238 -0
- package/src/usage-report.js +417 -0
- package/src/verification-evidence.js +284 -0
- package/src/workspace.js +126 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { getWorkspaceDir, toWorkspaceRelative } from './workspace.js';
|
|
5
|
+
|
|
6
|
+
export async function createStoryTasks(repoRoot, { story, evidence, runId, gateStatus }) {
|
|
7
|
+
const root = path.resolve(repoRoot);
|
|
8
|
+
const tasksDir = path.join(getWorkspaceDir(root), 'stories', story.story_id, 'tasks');
|
|
9
|
+
|
|
10
|
+
const canonicalTasksJsonPath = path.join(tasksDir, 'tasks.json');
|
|
11
|
+
const existingTaskState = await readTaskStateIfExists(canonicalTasksJsonPath);
|
|
12
|
+
const taskState = buildStoryTaskState({ story, evidence, runId, gateStatus, existingTaskState });
|
|
13
|
+
const outputDir = shouldPreserveCanonicalTasks(existingTaskState)
|
|
14
|
+
? path.join(getWorkspaceDir(root), 'stories', story.story_id, 'diagnostics', safeRunId(runId))
|
|
15
|
+
: tasksDir;
|
|
16
|
+
await mkdir(outputDir, { recursive: true });
|
|
17
|
+
|
|
18
|
+
const tasksJsonPath = path.join(outputDir, 'tasks.json');
|
|
19
|
+
const tasksMarkdownPath = path.join(outputDir, 'tasks.md');
|
|
20
|
+
|
|
21
|
+
await writeFile(tasksJsonPath, `${JSON.stringify(taskState, null, 2)}\n`);
|
|
22
|
+
await writeFile(tasksMarkdownPath, renderStoryTasks(taskState));
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
taskState,
|
|
26
|
+
artifacts: {
|
|
27
|
+
story_tasks_json: toWorkspaceRelative(root, tasksJsonPath),
|
|
28
|
+
story_tasks_markdown: toWorkspaceRelative(root, tasksMarkdownPath)
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function readTaskStateIfExists(tasksJsonPath) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(await readFile(tasksJsonPath, 'utf8'));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error.code === 'ENOENT') return null;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function shouldPreserveCanonicalTasks(taskState) {
|
|
43
|
+
if (!taskState) return false;
|
|
44
|
+
if (taskState.source_run?.run_id === 'story-plan') return true;
|
|
45
|
+
return (taskState.tasks ?? []).some((task) => task.source_type === 'story_plan_candidate');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function safeRunId(runId) {
|
|
49
|
+
return String(runId ?? 'diagnosis-run').replace(/[\\/]/g, '_');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function readStoryTasks(repoRoot, artifactPath) {
|
|
53
|
+
if (!artifactPath) return emptyStoryTaskState();
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(await readFile(path.resolve(repoRoot, artifactPath), 'utf8'));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error.code === 'ENOENT') return emptyStoryTaskState();
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildStoryTaskState({ story, evidence, runId, gateStatus, existingTaskState = null }) {
|
|
63
|
+
const actionCandidates = Array.isArray(evidence.action_candidates) ? evidence.action_candidates : [];
|
|
64
|
+
const findings = Array.isArray(evidence.findings) ? evidence.findings : [];
|
|
65
|
+
const actionFindingIds = new Set(actionCandidates.map((candidate) => candidate.finding_id));
|
|
66
|
+
const activeTasks = [
|
|
67
|
+
...findings
|
|
68
|
+
.filter((finding) => !actionFindingIds.has(finding.id))
|
|
69
|
+
.filter((finding) => shouldCreateFindingTask(finding))
|
|
70
|
+
.flatMap((finding) => buildFindingTasks({ finding, evidence })),
|
|
71
|
+
...actionCandidates.map((candidate) => buildActionTask(candidate))
|
|
72
|
+
];
|
|
73
|
+
const tasks = applyCompletionStatus({
|
|
74
|
+
activeTasks,
|
|
75
|
+
existingTaskState,
|
|
76
|
+
currentSourceIds: new Set([
|
|
77
|
+
...findings.map((finding) => finding.id),
|
|
78
|
+
...actionCandidates.map((candidate) => candidate.id)
|
|
79
|
+
]),
|
|
80
|
+
runId
|
|
81
|
+
})
|
|
82
|
+
.sort(compareTasks)
|
|
83
|
+
.map((task, index) => ({
|
|
84
|
+
...task,
|
|
85
|
+
order: task.order ?? ((index + 1) * 10)
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
schema_version: '0.1.0',
|
|
90
|
+
generated_at: new Date().toISOString(),
|
|
91
|
+
story,
|
|
92
|
+
source_run: {
|
|
93
|
+
run_id: runId,
|
|
94
|
+
gate_status: gateStatus ?? evidence.gates?.[0]?.status ?? 'unknown'
|
|
95
|
+
},
|
|
96
|
+
tasks
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function renderStoryTasks(taskState) {
|
|
101
|
+
const tasks = Array.isArray(taskState.tasks) ? taskState.tasks : [];
|
|
102
|
+
return `# VibePro 生成タスク
|
|
103
|
+
|
|
104
|
+
| 項目 | 内容 |
|
|
105
|
+
|------|------|
|
|
106
|
+
| Story | ${taskState.story?.title ?? '-'} |
|
|
107
|
+
| Story ID | ${taskState.story?.story_id ?? '-'} |
|
|
108
|
+
| Run ID | ${taskState.source_run?.run_id ?? '-'} |
|
|
109
|
+
| Gate | ${taskState.source_run?.gate_status ?? '-'} |
|
|
110
|
+
| タスク数 | ${tasks.length} |
|
|
111
|
+
|
|
112
|
+
| ID | Finding | 優先度 | 対象 | 方針 | 状態 |
|
|
113
|
+
|----|---------|--------|------|------|------|
|
|
114
|
+
${tasks.length === 0 ? '| - | - | - | - | - | - |' : tasks.map((task) => `| ${task.id} | ${task.finding_id ?? '-'} | ${task.priority} | ${task.target_count ?? task.target_files.length}件 | ${task.recommended_strategy?.id ?? '-'} | ${task.status} |`).join('\n')}
|
|
115
|
+
|
|
116
|
+
${tasks.map(renderTaskDetail).join('\n\n')}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderTaskDetail(task) {
|
|
120
|
+
return `## ${task.id}: ${task.title}
|
|
121
|
+
|
|
122
|
+
- Source: ${task.source_type} / ${task.source_id}
|
|
123
|
+
- Execution: ${task.execution_policy} / mutates_repository=${task.mutates_repository}
|
|
124
|
+
- Target files: ${task.target_files.length === 0 ? '-' : task.target_files.join(', ')}
|
|
125
|
+
- Target groups: ${formatTargetGroups(task.target_groups)}
|
|
126
|
+
- Read first: ${task.read_first_files.length === 0 ? '-' : task.read_first_files.map((item) => item.file).join(', ')}
|
|
127
|
+
- Recommended strategy: ${task.recommended_strategy?.id ?? '-'}
|
|
128
|
+
|
|
129
|
+
完了条件:
|
|
130
|
+
${task.acceptance_criteria.length === 0 ? '- 診断結果を確認する' : task.acceptance_criteria.map((item) => `- ${item}`).join('\n')}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildActionTask(candidate) {
|
|
134
|
+
const plan = candidate.implementation_plan ?? {};
|
|
135
|
+
const targetRoutes = plan.pre_fix_briefing?.target_routes ?? [];
|
|
136
|
+
const targetFiles = uniqueFiles(targetRoutes.length > 0
|
|
137
|
+
? targetRoutes.map((route) => route.file)
|
|
138
|
+
: [
|
|
139
|
+
...(candidate.target_files ?? []),
|
|
140
|
+
...(candidate.route_examples ?? []).map((route) => route.file)
|
|
141
|
+
]);
|
|
142
|
+
const targetGroups = buildTargetGroups({ targetRoutes, candidate, plan });
|
|
143
|
+
return {
|
|
144
|
+
id: candidate.id.replace('VP-ACTION-', 'VP-TASK-'),
|
|
145
|
+
source_type: 'action_candidate',
|
|
146
|
+
source_id: candidate.id,
|
|
147
|
+
finding_id: candidate.finding_id,
|
|
148
|
+
title: candidate.title,
|
|
149
|
+
priority: normalizePriority(plan.priority ?? severityToPriority(candidate.severity)),
|
|
150
|
+
status: 'todo',
|
|
151
|
+
order: resolveActionOrder(candidate),
|
|
152
|
+
execution_policy: candidate.execution_policy ?? 'proposal_only',
|
|
153
|
+
mutates_repository: Boolean(candidate.mutates_repository),
|
|
154
|
+
target_count: targetRoutes.length > 0 ? targetRoutes.length : candidate.target_count ?? targetFiles.length,
|
|
155
|
+
target_files: targetFiles,
|
|
156
|
+
target_routes: targetRoutes,
|
|
157
|
+
target_groups: targetGroups,
|
|
158
|
+
read_first_files: plan.read_first_files ?? [],
|
|
159
|
+
recommended_strategy: plan.pre_fix_briefing?.recommended_strategy ?? null,
|
|
160
|
+
implementation_steps: plan.steps ?? [],
|
|
161
|
+
acceptance_criteria: plan.acceptance_criteria ?? [],
|
|
162
|
+
graph_context: candidate.graph_context ?? null,
|
|
163
|
+
pre_fix_briefing: plan.pre_fix_briefing ?? null
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildFindingTasks({ finding, evidence }) {
|
|
168
|
+
if (finding.id === 'VP-STATIC-002') {
|
|
169
|
+
return buildSecretFindingTasks({ finding, evidence });
|
|
170
|
+
}
|
|
171
|
+
if (finding.id === 'VP-DB-001') {
|
|
172
|
+
return buildDatabaseFindingTasks({ finding, evidence });
|
|
173
|
+
}
|
|
174
|
+
return [buildFindingTask({
|
|
175
|
+
finding,
|
|
176
|
+
targetFiles: resolveFindingTargetFiles(finding, evidence),
|
|
177
|
+
priority: severityToPriority(finding.severity),
|
|
178
|
+
order: resolveFindingOrder(finding),
|
|
179
|
+
gateEffect: null
|
|
180
|
+
})];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildDatabaseFindingTasks({ finding, evidence }) {
|
|
184
|
+
const hits = (evidence.database_access?.unbounded_find_many ?? [])
|
|
185
|
+
.filter((hit) => hit.gate_effect !== 'info');
|
|
186
|
+
const groups = groupBy(hits, (hit) => resolveDatabaseTaskGroup(hit.file));
|
|
187
|
+
return Object.values(groups).map((group, index) => {
|
|
188
|
+
const targetFiles = uniqueFiles(group.hits.map((hit) => hit.file));
|
|
189
|
+
return buildFindingTask({
|
|
190
|
+
finding,
|
|
191
|
+
id: `VP-TASK-DB-001-${group.id.toUpperCase().replace(/-/g, '_')}`,
|
|
192
|
+
title: `${finding.title}(${group.title})`,
|
|
193
|
+
targetFiles,
|
|
194
|
+
priority: severityToPriority(finding.severity),
|
|
195
|
+
order: 55 + index,
|
|
196
|
+
gateEffect: null,
|
|
197
|
+
targetGroups: [{
|
|
198
|
+
id: group.id,
|
|
199
|
+
title: group.title,
|
|
200
|
+
target_count: targetFiles.length,
|
|
201
|
+
target_files: targetFiles
|
|
202
|
+
}],
|
|
203
|
+
recommendedStrategy: {
|
|
204
|
+
id: 'add-query-boundary',
|
|
205
|
+
reason: 'route/domain単位でtake/skip/cursorまたは集計API分離を入れ、挙動差分を小さくする。'
|
|
206
|
+
},
|
|
207
|
+
acceptanceCriteria: [
|
|
208
|
+
`${group.title} の公開APIまたはユーザー操作に紐づく一覧取得に take/skip/cursor 等の上限がある。`,
|
|
209
|
+
'再診断でこのgroupの未ページング候補が減っている。'
|
|
210
|
+
]
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resolveDatabaseTaskGroup(file) {
|
|
216
|
+
if (file.startsWith('src/app/api/') || file.startsWith('app/api/')) {
|
|
217
|
+
const apiPath = file.replace(/^src\/app\/api\//, '').replace(/^app\/api\//, '');
|
|
218
|
+
const segments = apiPath.split('/').filter(Boolean);
|
|
219
|
+
const first = normalizeRouteSegment(segments[0] ?? 'api');
|
|
220
|
+
const second = normalizeRouteSegment(segments[1] ?? '');
|
|
221
|
+
if (first === 'admin' && second) return { id: `api-admin-${second}`, title: `Admin API / ${second}` };
|
|
222
|
+
if (first === 'analytics' && second) return { id: 'api-analytics', title: 'Analytics API' };
|
|
223
|
+
if (first === 'v1' && second) return { id: `api-v1-${second}`, title: `v1 API / ${second}` };
|
|
224
|
+
if (first === 'projects') return { id: 'api-projects', title: 'Projects API' };
|
|
225
|
+
return { id: `api-${first}`, title: `API / ${first}` };
|
|
226
|
+
}
|
|
227
|
+
if (file.startsWith('src/lib/services/')) {
|
|
228
|
+
const servicePath = file.replace(/^src\/lib\/services\//, '');
|
|
229
|
+
const [first] = servicePath.split('/').filter(Boolean);
|
|
230
|
+
if (first && servicePath.includes('/')) return { id: `services-${slugify(first)}`, title: `Services / ${first}` };
|
|
231
|
+
return { id: 'services-core', title: 'Services / core' };
|
|
232
|
+
}
|
|
233
|
+
return { id: 'runtime-other', title: 'Runtime other' };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function normalizeRouteSegment(segment) {
|
|
237
|
+
return slugify(segment.replace(/^\[(.+)\]$/, '$1'));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function slugify(value) {
|
|
241
|
+
return String(value || 'unknown')
|
|
242
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
243
|
+
.toLowerCase()
|
|
244
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
245
|
+
.replace(/^-+|-+$/g, '') || 'unknown';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function groupBy(items, keyFn) {
|
|
249
|
+
const groups = {};
|
|
250
|
+
for (const item of items) {
|
|
251
|
+
const key = keyFn(item);
|
|
252
|
+
const id = typeof key === 'string' ? key : key.id;
|
|
253
|
+
const title = typeof key === 'string' ? key : key.title;
|
|
254
|
+
groups[id] ??= { id, title, hits: [] };
|
|
255
|
+
groups[id].hits.push(item);
|
|
256
|
+
}
|
|
257
|
+
return groups;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function buildSecretFindingTasks({ finding, evidence }) {
|
|
261
|
+
const blockFiles = resolveSecretTargetFiles(evidence, 'block');
|
|
262
|
+
const reviewFiles = resolveSecretTargetFiles(evidence, 'review');
|
|
263
|
+
return [
|
|
264
|
+
blockFiles.length > 0 ? buildFindingTask({
|
|
265
|
+
finding,
|
|
266
|
+
id: 'VP-TASK-STATIC-002-BLOCK',
|
|
267
|
+
title: `${finding.title}(即時対応)`,
|
|
268
|
+
targetFiles: blockFiles,
|
|
269
|
+
priority: 'critical',
|
|
270
|
+
order: 10,
|
|
271
|
+
gateEffect: 'block'
|
|
272
|
+
}) : null,
|
|
273
|
+
reviewFiles.length > 0 ? buildFindingTask({
|
|
274
|
+
finding,
|
|
275
|
+
id: 'VP-TASK-STATIC-002-REVIEW',
|
|
276
|
+
title: `${finding.title}(要確認)`,
|
|
277
|
+
targetFiles: reviewFiles,
|
|
278
|
+
priority: 'high',
|
|
279
|
+
order: 15,
|
|
280
|
+
gateEffect: 'review'
|
|
281
|
+
}) : null
|
|
282
|
+
].filter(Boolean);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function buildFindingTask({
|
|
286
|
+
finding,
|
|
287
|
+
id = null,
|
|
288
|
+
title = null,
|
|
289
|
+
targetFiles = [],
|
|
290
|
+
priority,
|
|
291
|
+
order,
|
|
292
|
+
gateEffect,
|
|
293
|
+
targetGroups = [],
|
|
294
|
+
recommendedStrategy = null,
|
|
295
|
+
acceptanceCriteria = null
|
|
296
|
+
}) {
|
|
297
|
+
return {
|
|
298
|
+
id: id ?? finding.id.replace('VP-', 'VP-TASK-'),
|
|
299
|
+
source_type: 'finding',
|
|
300
|
+
source_id: finding.id,
|
|
301
|
+
finding_id: finding.id,
|
|
302
|
+
title: title ?? finding.title,
|
|
303
|
+
priority,
|
|
304
|
+
status: 'todo',
|
|
305
|
+
order,
|
|
306
|
+
gate_effect: gateEffect,
|
|
307
|
+
execution_policy: 'proposal_only',
|
|
308
|
+
mutates_repository: false,
|
|
309
|
+
target_count: targetFiles.length,
|
|
310
|
+
target_files: targetFiles,
|
|
311
|
+
target_routes: [],
|
|
312
|
+
target_groups: targetGroups,
|
|
313
|
+
read_first_files: targetFiles.map((file) => ({
|
|
314
|
+
file,
|
|
315
|
+
reason: `検出事項 ${finding.id} の確認対象`
|
|
316
|
+
})),
|
|
317
|
+
recommended_strategy: recommendedStrategy ?? {
|
|
318
|
+
id: 'manual-review',
|
|
319
|
+
reason: finding.recommendation
|
|
320
|
+
},
|
|
321
|
+
implementation_steps: [{
|
|
322
|
+
id: 'review-finding',
|
|
323
|
+
title: '検出内容を確認する',
|
|
324
|
+
detail: finding.detail
|
|
325
|
+
}],
|
|
326
|
+
acceptance_criteria: acceptanceCriteria ?? [finding.recommendation],
|
|
327
|
+
graph_context: finding.graph_context ?? null,
|
|
328
|
+
pre_fix_briefing: null
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function applyCompletionStatus({ activeTasks, existingTaskState, currentSourceIds, runId }) {
|
|
333
|
+
const activeTaskIds = new Set(activeTasks.map((task) => task.id));
|
|
334
|
+
const completedTasks = (existingTaskState?.tasks ?? [])
|
|
335
|
+
.filter((task) => !activeTaskIds.has(task.id))
|
|
336
|
+
.filter((task) => task.source_type !== 'story_plan_candidate')
|
|
337
|
+
.filter((task) => task.source_id && !currentSourceIds.has(task.source_id))
|
|
338
|
+
.map((task) => ({
|
|
339
|
+
...task,
|
|
340
|
+
status: 'done',
|
|
341
|
+
completed_at: new Date().toISOString(),
|
|
342
|
+
completion_evidence: {
|
|
343
|
+
run_id: runId,
|
|
344
|
+
reason: `source ${task.source_id} was not detected in the latest diagnosis`
|
|
345
|
+
}
|
|
346
|
+
}));
|
|
347
|
+
return [...activeTasks, ...completedTasks];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function shouldCreateFindingTask(finding) {
|
|
351
|
+
if (['VP-DB-001', 'VP-PERF-001'].includes(finding.id)) return true;
|
|
352
|
+
return ['Critical', 'High'].includes(finding.severity);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function resolveFindingTargetFiles(finding, evidence) {
|
|
356
|
+
if (Array.isArray(finding.target_files) && finding.target_files.length > 0) {
|
|
357
|
+
return uniqueFiles(finding.target_files);
|
|
358
|
+
}
|
|
359
|
+
if (finding.id === 'VP-STATIC-002') {
|
|
360
|
+
return uniqueFiles([
|
|
361
|
+
...resolveSecretTargetFiles(evidence, 'block'),
|
|
362
|
+
...resolveSecretTargetFiles(evidence, 'review')
|
|
363
|
+
]);
|
|
364
|
+
}
|
|
365
|
+
if (finding.id === 'VP-STATIC-003') {
|
|
366
|
+
return uniqueFiles((evidence.static_site?.xss_risk_hits ?? [])
|
|
367
|
+
.filter((hit) => hit.gate_effect !== 'info')
|
|
368
|
+
.map((hit) => hit.file));
|
|
369
|
+
}
|
|
370
|
+
if (finding.id === 'VP-SEC-004') {
|
|
371
|
+
return uniqueFiles((evidence.code_quality?.authorization_order_risks ?? [])
|
|
372
|
+
.filter((hit) => hit.gate_effect !== 'info')
|
|
373
|
+
.map((hit) => hit.file));
|
|
374
|
+
}
|
|
375
|
+
if (finding.id === 'VP-DB-001') {
|
|
376
|
+
return uniqueFiles((evidence.database_access?.unbounded_find_many ?? [])
|
|
377
|
+
.filter((hit) => hit.gate_effect !== 'info')
|
|
378
|
+
.map((hit) => hit.file));
|
|
379
|
+
}
|
|
380
|
+
if (finding.id === 'VP-PERF-001') {
|
|
381
|
+
return uniqueFiles((evidence.local_dev?.heavy_dev_scripts ?? [])
|
|
382
|
+
.filter((hit) => hit.gate_effect !== 'info')
|
|
383
|
+
.map((hit) => hit.file));
|
|
384
|
+
}
|
|
385
|
+
if (finding.id === 'VP-DRY-001') {
|
|
386
|
+
return uniqueFiles((evidence.code_quality?.duplicate_query_shapes ?? [])
|
|
387
|
+
.filter((hit) => hit.gate_effect !== 'info')
|
|
388
|
+
.flatMap((hit) => hit.files ?? []));
|
|
389
|
+
}
|
|
390
|
+
if (finding.id === 'VP-ARCH-001') {
|
|
391
|
+
return uniqueFiles((evidence.code_quality?.responsibility_hotspots ?? [])
|
|
392
|
+
.filter((hit) => hit.gate_effect !== 'info')
|
|
393
|
+
.map((hit) => hit.file));
|
|
394
|
+
}
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function resolveSecretTargetFiles(evidence, gateEffect) {
|
|
399
|
+
return uniqueFiles((evidence.static_site?.secret_hits ?? [])
|
|
400
|
+
.filter((hit) => hit.gate_effect === gateEffect)
|
|
401
|
+
.map((hit) => hit.file));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function buildTargetGroups({ targetRoutes, candidate, plan }) {
|
|
405
|
+
if (!Array.isArray(targetRoutes) || targetRoutes.length === 0) return [];
|
|
406
|
+
const groups = new Map();
|
|
407
|
+
for (const route of targetRoutes) {
|
|
408
|
+
const id = resolveRouteGroupId(route);
|
|
409
|
+
const current = groups.get(id) ?? {
|
|
410
|
+
id,
|
|
411
|
+
title: resolveRouteGroupTitle(id),
|
|
412
|
+
classification: route.classification ?? 'unknown',
|
|
413
|
+
route_count: 0,
|
|
414
|
+
target_files: [],
|
|
415
|
+
routes: [],
|
|
416
|
+
recommended_strategy: plan.pre_fix_briefing?.recommended_strategy ?? null,
|
|
417
|
+
read_first_files: [],
|
|
418
|
+
acceptance_criteria: plan.acceptance_criteria ?? []
|
|
419
|
+
};
|
|
420
|
+
current.route_count += 1;
|
|
421
|
+
current.target_files = uniqueFiles([...current.target_files, route.file]);
|
|
422
|
+
current.routes.push(route);
|
|
423
|
+
groups.set(id, current);
|
|
424
|
+
}
|
|
425
|
+
return [...groups.values()].map((group) => ({
|
|
426
|
+
...group,
|
|
427
|
+
read_first_files: selectGroupReadFirstFiles(group, plan.read_first_files ?? [])
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function resolveRouteGroupId(route) {
|
|
432
|
+
const segments = (route.route_path ?? '')
|
|
433
|
+
.split('/')
|
|
434
|
+
.filter(Boolean)
|
|
435
|
+
.filter((segment) => segment !== 'api' && !/^\[.+\]$/.test(segment));
|
|
436
|
+
if (segments[0] === 'admin') {
|
|
437
|
+
return segments[1] || 'admin';
|
|
438
|
+
}
|
|
439
|
+
if (segments[0] === 'batch-jobs') return 'batch-jobs';
|
|
440
|
+
if (segments[0] === 'companies') return 'companies';
|
|
441
|
+
if (segments[0] === 'pdf-compress') return 'pdf-compress';
|
|
442
|
+
if (segments[0] === 'v1' && segments[1]) return `v1-${segments[1]}`;
|
|
443
|
+
if (segments[0] === 'debug') return segments.slice(0, 2).join('-');
|
|
444
|
+
return segments.slice(0, 2).join('-') || route.classification || 'unknown';
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function resolveRouteGroupTitle(id) {
|
|
448
|
+
return id
|
|
449
|
+
.split('-')
|
|
450
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
451
|
+
.join(' ');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function selectGroupReadFirstFiles(group, readFirstFiles) {
|
|
455
|
+
const files = new Set(group.target_files);
|
|
456
|
+
return (readFirstFiles ?? []).filter((item) => files.has(item.file));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function formatTargetGroups(groups = []) {
|
|
460
|
+
if (!Array.isArray(groups) || groups.length === 0) return '-';
|
|
461
|
+
return groups.map((group) => `${group.id}(${group.route_count})`).join(', ');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function compareTasks(a, b) {
|
|
465
|
+
return resolveTaskSort(a) - resolveTaskSort(b) || a.id.localeCompare(b.id);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function resolveTaskSort(task) {
|
|
469
|
+
if (task.order != null) return task.order;
|
|
470
|
+
const priorityOrder = { critical: 10, high: 50, medium: 80, low: 100 };
|
|
471
|
+
return priorityOrder[task.priority] ?? 100;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function resolveFindingOrder(finding) {
|
|
475
|
+
if (finding.id === 'VP-STATIC-002') return 10;
|
|
476
|
+
if (finding.id === 'VP-PERF-001') return 45;
|
|
477
|
+
if (finding.id === 'VP-DB-001') return 55;
|
|
478
|
+
if (finding.severity === 'Critical') return 10;
|
|
479
|
+
if (finding.severity === 'High') return 50;
|
|
480
|
+
return 100;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function resolveActionOrder(candidate) {
|
|
484
|
+
if (candidate.id === 'VP-ACTION-API-002') return 20;
|
|
485
|
+
if (candidate.id === 'VP-ACTION-API-003') return 30;
|
|
486
|
+
if (candidate.id === 'VP-ACTION-API-001') return 40;
|
|
487
|
+
if (candidate.id === 'VP-ACTION-DRY-001') return 60;
|
|
488
|
+
if (candidate.id === 'VP-ACTION-ARCH-001') return 70;
|
|
489
|
+
return 60;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function severityToPriority(severity) {
|
|
493
|
+
const values = {
|
|
494
|
+
Critical: 'critical',
|
|
495
|
+
High: 'high',
|
|
496
|
+
Medium: 'medium',
|
|
497
|
+
Low: 'low'
|
|
498
|
+
};
|
|
499
|
+
return values[severity] ?? 'medium';
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function normalizePriority(priority) {
|
|
503
|
+
if (priority === 'critical') return 'critical';
|
|
504
|
+
if (priority === 'high') return 'high';
|
|
505
|
+
if (priority === 'medium') return 'medium';
|
|
506
|
+
if (priority === 'low') return 'low';
|
|
507
|
+
return 'medium';
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function uniqueFiles(files) {
|
|
511
|
+
return [...new Set((files ?? []).filter(Boolean))];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function emptyStoryTaskState() {
|
|
515
|
+
return {
|
|
516
|
+
schema_version: '0.1.0',
|
|
517
|
+
generated_at: null,
|
|
518
|
+
story: null,
|
|
519
|
+
source_run: null,
|
|
520
|
+
tasks: []
|
|
521
|
+
};
|
|
522
|
+
}
|