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,1945 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { scanApiBoundary } from './api-boundary-scanner.js';
|
|
5
|
+
import { profileArchitecture } from './architecture-profiler.js';
|
|
6
|
+
import { scanCodeQuality } from './code-quality-scanner.js';
|
|
7
|
+
import { scanComponentStyle } from './component-style-scanner.js';
|
|
8
|
+
import { scanDatabaseAccess } from './database-access-scanner.js';
|
|
9
|
+
import { renderFlowDesignReport, scanFlowDesign } from './flow-design-scanner.js';
|
|
10
|
+
import { renderGestureInteractionReport, scanGestureInteraction } from './gesture-interaction-scanner.js';
|
|
11
|
+
import { scanLocalDev } from './local-dev-scanner.js';
|
|
12
|
+
import { scanNetworkContracts } from './network-contract-scanner.js';
|
|
13
|
+
import { renderTerminalLinkReport, scanTerminalLinkContracts } from './terminal-link-scanner.js';
|
|
14
|
+
import {
|
|
15
|
+
buildRefactoringActionCandidates,
|
|
16
|
+
buildRefactoringCampaigns,
|
|
17
|
+
buildRefactoringOpportunities
|
|
18
|
+
} from './refactoring-opportunity-generator.js';
|
|
19
|
+
import {
|
|
20
|
+
buildRefactoringDelta,
|
|
21
|
+
renderRefactoringDelta,
|
|
22
|
+
renderRefactoringDeltaCompact
|
|
23
|
+
} from './refactoring-delta-reporter.js';
|
|
24
|
+
import {
|
|
25
|
+
buildGraphContextForFiles,
|
|
26
|
+
buildGraphContextForRoutes,
|
|
27
|
+
buildGraphIndex,
|
|
28
|
+
emptyGraphContext,
|
|
29
|
+
normalizeGraphEdges,
|
|
30
|
+
normalizeGraphPath
|
|
31
|
+
} from './graph-context.js';
|
|
32
|
+
import { buildRequirementConsistency, renderRequirementConsistencyReport } from './requirement-consistency.js';
|
|
33
|
+
import { buildSpecDrift, renderDriftMarkdown } from './spec-drift.js';
|
|
34
|
+
import { readInferredSpec } from './spec-store.js';
|
|
35
|
+
import { scanStaticSite } from './static-site-scanner.js';
|
|
36
|
+
import { resolveStoryContext } from './story-manager.js';
|
|
37
|
+
import { createStoryTasks } from './story-task-generator.js';
|
|
38
|
+
import { collectRuntimeInfo } from './runtime-info.js';
|
|
39
|
+
import { getWorkspaceDir, initWorkspace, readManifest, toWorkspaceRelative, writeManifest } from './workspace.js';
|
|
40
|
+
import {
|
|
41
|
+
renderPerformanceEvidenceSummary,
|
|
42
|
+
summarizeStoryPerformanceEvidence
|
|
43
|
+
} from './performance-evidence.js';
|
|
44
|
+
import { assertOutputLanguage, resolveOutputLanguage } from './language.js';
|
|
45
|
+
|
|
46
|
+
export async function runDiagnosis(repoRoot, options = {}) {
|
|
47
|
+
await initWorkspace(repoRoot);
|
|
48
|
+
const root = path.resolve(repoRoot);
|
|
49
|
+
const runId = options.runId ?? new Date().toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/:/g, '');
|
|
50
|
+
const runDir = path.join(getWorkspaceDir(root), 'diagnostics', runId);
|
|
51
|
+
await mkdir(runDir, { recursive: true });
|
|
52
|
+
|
|
53
|
+
const graphPath = path.join(getWorkspaceDir(root), 'graphify', 'graph.json');
|
|
54
|
+
const graph = JSON.parse(await readFile(graphPath, 'utf8'));
|
|
55
|
+
const config = JSON.parse(await readFile(path.join(getWorkspaceDir(root), 'config.json'), 'utf8'));
|
|
56
|
+
const language = options.language ? assertOutputLanguage(options.language) : resolveOutputLanguage(config);
|
|
57
|
+
const { currentStory } = resolveStoryContext(config);
|
|
58
|
+
const manifest = await readManifest(root);
|
|
59
|
+
const toolchain = await collectRuntimeInfo();
|
|
60
|
+
const { evidence, graphIndex } = await buildEvidence(root, graph, runId, currentStory, config);
|
|
61
|
+
evidence.output = { language };
|
|
62
|
+
evidence.toolchain = toolchain;
|
|
63
|
+
evidence.performance_evidence = await summarizeStoryPerformanceEvidence(root, currentStory.story_id);
|
|
64
|
+
const inferredSpec = await readInferredSpec(root, currentStory.story_id);
|
|
65
|
+
evidence.requirement_consistency = await buildRequirementConsistency(root, {
|
|
66
|
+
story: currentStory,
|
|
67
|
+
inferredSpec
|
|
68
|
+
});
|
|
69
|
+
evidence.inferred_spec = inferredSpec
|
|
70
|
+
? { story_id: inferredSpec.story_id, clauses: inferredSpec.clauses?.length ?? 0, generated_at: inferredSpec.generated_at ?? null }
|
|
71
|
+
: null;
|
|
72
|
+
evidence.spec_drift = inferredSpec
|
|
73
|
+
? await buildSpecDrift(root, { storyId: currentStory.story_id, spec: inferredSpec })
|
|
74
|
+
: { status: 'inconclusive', reason: 'no inferred spec', items: [] };
|
|
75
|
+
const findings = buildFindings(evidence);
|
|
76
|
+
evidence.findings = findings;
|
|
77
|
+
evidence.action_candidates = await buildActionCandidates(root, evidence, graphIndex);
|
|
78
|
+
attachFindingGraphContexts(evidence.findings, evidence.action_candidates);
|
|
79
|
+
evidence.finding_review = buildFindingReview({ findings, actionCandidates: evidence.action_candidates });
|
|
80
|
+
evidence.gates = buildGates(findings);
|
|
81
|
+
const previousRun = findPreviousStoryRun(manifest, currentStory.story_id, runId);
|
|
82
|
+
evidence.refactoring_delta = buildRefactoringDelta({
|
|
83
|
+
beforeEvidence: await readRunEvidenceIfExists(root, previousRun),
|
|
84
|
+
afterEvidence: evidence,
|
|
85
|
+
beforeRun: previousRun
|
|
86
|
+
});
|
|
87
|
+
const gateStatus = evidence.gates[0]?.status ?? 'unknown';
|
|
88
|
+
const storyTasks = await createStoryTasks(root, {
|
|
89
|
+
story: currentStory,
|
|
90
|
+
evidence,
|
|
91
|
+
runId,
|
|
92
|
+
gateStatus
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const evidencePath = path.join(runDir, 'evidence.json');
|
|
96
|
+
const summaryPath = path.join(runDir, 'summary.md');
|
|
97
|
+
const riskPath = path.join(runDir, 'risk-register.md');
|
|
98
|
+
const staticSitePath = path.join(runDir, 'static-site-check-result.md');
|
|
99
|
+
const componentStylePath = path.join(runDir, 'component-style-check-result.md');
|
|
100
|
+
const flowDesignPath = path.join(runDir, 'flow-design-check-result.md');
|
|
101
|
+
const gestureInteractionPath = path.join(runDir, 'gesture-interaction-check-result.md');
|
|
102
|
+
const terminalLinkPath = path.join(runDir, 'terminal-link-check-result.md');
|
|
103
|
+
const architectureProfilePath = path.join(runDir, 'architecture-profile.md');
|
|
104
|
+
const findingReviewPath = path.join(runDir, 'finding-review.md');
|
|
105
|
+
const refactoringDeltaPath = path.join(runDir, 'refactoring-delta.md');
|
|
106
|
+
const requirementConsistencyPath = path.join(runDir, 'requirement-consistency.md');
|
|
107
|
+
const specDriftPath = path.join(runDir, 'spec-drift.md');
|
|
108
|
+
|
|
109
|
+
await writeFile(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`);
|
|
110
|
+
await writeFile(summaryPath, renderSummary({ runId, evidence, findings }));
|
|
111
|
+
await writeFile(riskPath, renderRiskRegister({
|
|
112
|
+
runId,
|
|
113
|
+
findings,
|
|
114
|
+
apiBoundary: evidence.api_boundary,
|
|
115
|
+
actionCandidates: evidence.action_candidates,
|
|
116
|
+
findingReview: evidence.finding_review
|
|
117
|
+
}));
|
|
118
|
+
await writeFile(staticSitePath, renderStaticSiteCheck({
|
|
119
|
+
runId,
|
|
120
|
+
staticSite: evidence.static_site,
|
|
121
|
+
profile: evidence.architecture_profile
|
|
122
|
+
}));
|
|
123
|
+
await writeFile(componentStylePath, renderComponentStyleCheck({
|
|
124
|
+
runId,
|
|
125
|
+
componentStyle: evidence.component_style
|
|
126
|
+
}));
|
|
127
|
+
await writeFile(flowDesignPath, renderFlowDesignReport({
|
|
128
|
+
runId,
|
|
129
|
+
flowDesign: evidence.flow_design
|
|
130
|
+
}));
|
|
131
|
+
await writeFile(gestureInteractionPath, renderGestureInteractionReport({
|
|
132
|
+
runId,
|
|
133
|
+
gestureInteraction: evidence.gesture_interaction
|
|
134
|
+
}));
|
|
135
|
+
await writeFile(terminalLinkPath, renderTerminalLinkReport({
|
|
136
|
+
runId,
|
|
137
|
+
terminalLinkContracts: evidence.terminal_link_contracts
|
|
138
|
+
}));
|
|
139
|
+
await writeFile(architectureProfilePath, renderArchitectureProfile({
|
|
140
|
+
runId,
|
|
141
|
+
profile: evidence.architecture_profile,
|
|
142
|
+
checkCatalog: evidence.check_catalog
|
|
143
|
+
}));
|
|
144
|
+
await writeFile(findingReviewPath, renderFindingReview({
|
|
145
|
+
runId,
|
|
146
|
+
findingReview: evidence.finding_review
|
|
147
|
+
}));
|
|
148
|
+
await writeFile(refactoringDeltaPath, renderRefactoringDelta(evidence.refactoring_delta));
|
|
149
|
+
await writeFile(requirementConsistencyPath, renderRequirementConsistencyReport(evidence.requirement_consistency));
|
|
150
|
+
await writeFile(specDriftPath, renderDriftMarkdown(evidence.spec_drift));
|
|
151
|
+
|
|
152
|
+
const run = {
|
|
153
|
+
run_id: runId,
|
|
154
|
+
story_id: currentStory.story_id,
|
|
155
|
+
story: currentStory,
|
|
156
|
+
created_at: new Date().toISOString(),
|
|
157
|
+
gate_status: gateStatus,
|
|
158
|
+
toolchain,
|
|
159
|
+
artifacts: {
|
|
160
|
+
summary: toWorkspaceRelative(root, summaryPath),
|
|
161
|
+
risk_register: toWorkspaceRelative(root, riskPath),
|
|
162
|
+
evidence: toWorkspaceRelative(root, evidencePath),
|
|
163
|
+
static_site_check: toWorkspaceRelative(root, staticSitePath),
|
|
164
|
+
component_style_check: toWorkspaceRelative(root, componentStylePath),
|
|
165
|
+
flow_design_check: toWorkspaceRelative(root, flowDesignPath),
|
|
166
|
+
gesture_interaction_check: toWorkspaceRelative(root, gestureInteractionPath),
|
|
167
|
+
terminal_link_check: toWorkspaceRelative(root, terminalLinkPath),
|
|
168
|
+
architecture_profile: toWorkspaceRelative(root, architectureProfilePath),
|
|
169
|
+
finding_review: toWorkspaceRelative(root, findingReviewPath),
|
|
170
|
+
refactoring_delta: toWorkspaceRelative(root, refactoringDeltaPath),
|
|
171
|
+
requirement_consistency: toWorkspaceRelative(root, requirementConsistencyPath),
|
|
172
|
+
spec_drift: toWorkspaceRelative(root, specDriftPath),
|
|
173
|
+
...storyTasks.artifacts
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
manifest.latest_run = runId;
|
|
177
|
+
manifest.latest_run_by_story = {
|
|
178
|
+
...(manifest.latest_run_by_story ?? {}),
|
|
179
|
+
[currentStory.story_id]: runId
|
|
180
|
+
};
|
|
181
|
+
manifest.runs = [run, ...(manifest.runs ?? []).filter((item) => item.run_id !== runId)];
|
|
182
|
+
await writeManifest(root, manifest);
|
|
183
|
+
|
|
184
|
+
return { runDir, run };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function findPreviousStoryRun(manifest, storyId, currentRunId) {
|
|
188
|
+
const runs = (manifest?.runs ?? []).filter((run) => run?.run_id && run.run_id !== currentRunId);
|
|
189
|
+
const latestRunIdForStory = manifest?.latest_run_by_story?.[storyId];
|
|
190
|
+
return runs.find((run) => run.run_id === latestRunIdForStory)
|
|
191
|
+
?? runs.find((run) => run.story_id === storyId)
|
|
192
|
+
?? runs.find((run) => run.run_id === manifest?.latest_run)
|
|
193
|
+
?? runs[0]
|
|
194
|
+
?? null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function readRunEvidenceIfExists(repoRoot, run) {
|
|
198
|
+
const evidencePath = run?.artifacts?.evidence;
|
|
199
|
+
if (!evidencePath) return null;
|
|
200
|
+
try {
|
|
201
|
+
return JSON.parse(await readFile(path.resolve(repoRoot, evidencePath), 'utf8'));
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error.code === 'ENOENT') return null;
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function buildEvidence(repoRoot, graph, runId, story, config = {}) {
|
|
209
|
+
const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
|
|
210
|
+
const { edges, sourceKey: edgeSourceKey } = normalizeGraphEdges(graph);
|
|
211
|
+
const extractedEdges = edges.filter((edge) => edge.confidence === 'EXTRACTED');
|
|
212
|
+
const inferredEdges = edges.filter((edge) => edge.confidence === 'INFERRED');
|
|
213
|
+
const ambiguousEdges = edges.filter((edge) => edge.confidence === 'AMBIGUOUS');
|
|
214
|
+
const graphIndex = buildGraphIndex({ nodes, edges });
|
|
215
|
+
const architectureProfile = await profileArchitecture(repoRoot);
|
|
216
|
+
const checkCatalog = {
|
|
217
|
+
selected_views: architectureProfile.selected_views,
|
|
218
|
+
applicable_checks: architectureProfile.applicable_checks
|
|
219
|
+
};
|
|
220
|
+
const evidence = {
|
|
221
|
+
schema_version: '0.1.0',
|
|
222
|
+
run_id: runId,
|
|
223
|
+
story_id: story.story_id,
|
|
224
|
+
story,
|
|
225
|
+
graphify: {
|
|
226
|
+
node_count: nodes.length,
|
|
227
|
+
edge_count: edges.length,
|
|
228
|
+
edge_source_key: edgeSourceKey,
|
|
229
|
+
extracted_edges: extractedEdges,
|
|
230
|
+
inferred_edges: inferredEdges,
|
|
231
|
+
ambiguous_edges: ambiguousEdges,
|
|
232
|
+
quality_notices: buildGraphQualityNotices({ inferredEdges })
|
|
233
|
+
},
|
|
234
|
+
architecture_profile: architectureProfile,
|
|
235
|
+
check_catalog: checkCatalog,
|
|
236
|
+
api_boundary: architectureProfile.applicable_checks.includes('api-boundary')
|
|
237
|
+
? await scanApiBoundary(repoRoot, architectureProfile)
|
|
238
|
+
: null,
|
|
239
|
+
network_contracts: await scanNetworkContracts(repoRoot),
|
|
240
|
+
database_access: architectureProfile.applicable_checks.includes('database-access')
|
|
241
|
+
? await scanDatabaseAccess(repoRoot)
|
|
242
|
+
: null,
|
|
243
|
+
local_dev: await scanLocalDev(repoRoot),
|
|
244
|
+
code_quality: architectureProfile.applicable_checks.includes('code-quality')
|
|
245
|
+
? await scanCodeQuality(repoRoot)
|
|
246
|
+
: null,
|
|
247
|
+
static_site: await scanStaticSite(repoRoot),
|
|
248
|
+
component_style: architectureProfile.applicable_checks.includes('component-style')
|
|
249
|
+
? await scanComponentStyle(repoRoot)
|
|
250
|
+
: null,
|
|
251
|
+
flow_design: await scanFlowDesign(repoRoot, { story, config }),
|
|
252
|
+
gesture_interaction: architectureProfile.applicable_checks.includes('component-style')
|
|
253
|
+
? await scanGestureInteraction(repoRoot)
|
|
254
|
+
: null,
|
|
255
|
+
terminal_link_contracts: await scanTerminalLinkContracts(repoRoot),
|
|
256
|
+
refactoring_opportunities: [],
|
|
257
|
+
refactoring_campaigns: [],
|
|
258
|
+
action_candidates: [],
|
|
259
|
+
findings: [],
|
|
260
|
+
finding_review: {
|
|
261
|
+
schema_version: '0.1.0',
|
|
262
|
+
status: 'not_generated',
|
|
263
|
+
items: [],
|
|
264
|
+
summary: {}
|
|
265
|
+
},
|
|
266
|
+
requirement_consistency: {
|
|
267
|
+
schema_version: '0.1.0',
|
|
268
|
+
status: 'not_generated',
|
|
269
|
+
summary: {},
|
|
270
|
+
invariants: [],
|
|
271
|
+
scenario_gaps: [],
|
|
272
|
+
contradictions: [],
|
|
273
|
+
policy_refs: [],
|
|
274
|
+
code_scenarios: []
|
|
275
|
+
},
|
|
276
|
+
gates: []
|
|
277
|
+
};
|
|
278
|
+
evidence.refactoring_opportunities = attachRefactoringGraphContexts(
|
|
279
|
+
buildRefactoringOpportunities(evidence),
|
|
280
|
+
graphIndex
|
|
281
|
+
);
|
|
282
|
+
evidence.refactoring_campaigns = buildRefactoringCampaigns(evidence);
|
|
283
|
+
|
|
284
|
+
return { graphIndex, evidence };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildFindings(evidence) {
|
|
288
|
+
const findings = [];
|
|
289
|
+
const applicableChecks = new Set(evidence.check_catalog?.applicable_checks ?? []);
|
|
290
|
+
if (evidence.graphify.ambiguous_edges.length > 0) {
|
|
291
|
+
findings.push({
|
|
292
|
+
id: 'VP-GRAPH-001',
|
|
293
|
+
severity: 'Medium',
|
|
294
|
+
category: '文脈品質',
|
|
295
|
+
title: '曖昧な依存関係が残っている',
|
|
296
|
+
detail: `graphify が ${evidence.graphify.ambiguous_edges.length} 件の曖昧な関係を検出した。`,
|
|
297
|
+
recommendation: '本番化判断に使う前に、対象の関係を人間または追加調査で確認する。'
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
if (applicableChecks.has('static-entry') && !evidence.static_site.has_index_html) {
|
|
301
|
+
findings.push({
|
|
302
|
+
id: 'VP-STATIC-001',
|
|
303
|
+
severity: 'High',
|
|
304
|
+
category: '静的サイト',
|
|
305
|
+
title: 'ルートの index.html が見つからない',
|
|
306
|
+
detail: '静的サイトとして配信する入口ファイルが確認できない。',
|
|
307
|
+
recommendation: '公開対象のルートに index.html を配置するか、配信設定の入口を明示する。'
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const gateSecretHits = filterGateRelevant(evidence.static_site.secret_hits);
|
|
311
|
+
const gateXssHits = filterGateRelevant(evidence.static_site.xss_risk_hits);
|
|
312
|
+
if (applicableChecks.has('secrets') && gateSecretHits.length > 0) {
|
|
313
|
+
const secretSummary = summarizeGateEffects(evidence.static_site.secret_hits);
|
|
314
|
+
findings.push({
|
|
315
|
+
id: 'VP-STATIC-002',
|
|
316
|
+
severity: secretSummary.block > 0 ? 'Critical' : 'High',
|
|
317
|
+
category: 'セキュリティ',
|
|
318
|
+
title: '秘密情報の可能性がある値が含まれている',
|
|
319
|
+
detail: `${gateSecretHits.length} 件のgate対象の秘密情報候補を検出した。内訳: ${formatGateSummary(secretSummary)}。`,
|
|
320
|
+
recommendation: '公開前に該当値を削除し、必要な値はサーバー側または安全な環境変数管理へ移す。'
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (applicableChecks.has('xss') && gateXssHits.length > 0) {
|
|
324
|
+
const xssSummary = summarizeGateEffects(evidence.static_site.xss_risk_hits);
|
|
325
|
+
findings.push({
|
|
326
|
+
id: 'VP-STATIC-003',
|
|
327
|
+
severity: 'High',
|
|
328
|
+
category: 'セキュリティ',
|
|
329
|
+
title: 'XSS につながり得る DOM 操作がある',
|
|
330
|
+
detail: `${gateXssHits.length} 件のgate対象の危険なDOM操作候補を検出した。内訳: ${formatGateSummary(xssSummary)}。`,
|
|
331
|
+
recommendation: 'ユーザー入力をHTMLとして挿入しない。必要な場合はサニタイズし、textContentなど安全な代替を使う。'
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (applicableChecks.has('static-publish-surface') && evidence.static_site.non_static_files.length > 0) {
|
|
335
|
+
findings.push({
|
|
336
|
+
id: 'VP-STATIC-004',
|
|
337
|
+
severity: 'Medium',
|
|
338
|
+
category: '配信設計',
|
|
339
|
+
title: '静的配信対象外のファイルが混在している',
|
|
340
|
+
detail: `${evidence.static_site.non_static_files.length} 件の非静的ファイル候補を検出した。`,
|
|
341
|
+
recommendation: '公開ディレクトリにサーバーコード、設定ファイル、生成前ソースを含めない構成に分離する。'
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (applicableChecks.has('database-access') && evidence.database_access) {
|
|
345
|
+
const unboundedQueries = filterGateRelevant(evidence.database_access.unbounded_find_many);
|
|
346
|
+
if (unboundedQueries.length > 0) {
|
|
347
|
+
const querySummary = summarizeGateEffects(evidence.database_access.unbounded_find_many);
|
|
348
|
+
findings.push({
|
|
349
|
+
id: 'VP-DB-001',
|
|
350
|
+
severity: 'Medium',
|
|
351
|
+
category: 'パフォーマンス',
|
|
352
|
+
title: '未ページングのDB一覧取得候補がある',
|
|
353
|
+
detail: `${unboundedQueries.length} 件のruntime queryで件数上限のない Prisma findMany 候補を検出した。内訳: ${formatGateSummary(querySummary)}。`,
|
|
354
|
+
recommendation: '公開APIやユーザー操作に紐づく一覧取得には take/skip/cursor 等の上限を設け、必要ならページング仕様をStoryに落とす。'
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (evidence.local_dev) {
|
|
359
|
+
const heavyDevScripts = filterGateRelevant(evidence.local_dev.heavy_dev_scripts ?? []);
|
|
360
|
+
if (heavyDevScripts.length > 0) {
|
|
361
|
+
const scriptSummary = summarizeGateEffects(evidence.local_dev.heavy_dev_scripts);
|
|
362
|
+
findings.push({
|
|
363
|
+
id: 'VP-PERF-001',
|
|
364
|
+
severity: 'Medium',
|
|
365
|
+
category: 'パフォーマンス',
|
|
366
|
+
title: 'ローカルdev起動が複数runtimeを同時起動している',
|
|
367
|
+
detail: `${heavyDevScripts.length} 件のdev scriptでNext.js dev serverと複数worker等の同時起動候補を検出した。内訳: ${formatGateSummary(scriptSummary)}。`,
|
|
368
|
+
recommendation: 'UI確認用のweb-only dev scriptとworker起動scriptを分離し、ローカル調査時は必要なruntimeだけを起動できるようにする。'
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (applicableChecks.has('code-quality') && evidence.code_quality) {
|
|
373
|
+
const authorizationOrderRisks = filterGateRelevant(evidence.code_quality.authorization_order_risks);
|
|
374
|
+
if (authorizationOrderRisks.length > 0) {
|
|
375
|
+
const authzSummary = summarizeGateEffects(evidence.code_quality.authorization_order_risks);
|
|
376
|
+
findings.push({
|
|
377
|
+
id: 'VP-SEC-004',
|
|
378
|
+
severity: 'High',
|
|
379
|
+
category: 'セキュリティ',
|
|
380
|
+
title: '認可判定前に一覧・集計DB取得候補がある',
|
|
381
|
+
detail: `${authorizationOrderRisks.length} 件のruntime codeで403/Access denied判定より前にbulk DB read候補を検出した。内訳: ${formatGateSummary(authzSummary)}。`,
|
|
382
|
+
recommendation: '所有者確認や権限判定を先に行い、一覧・集計・外部I/Oは認可後に移動する。'
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
const duplicateQueryShapes = filterGateRelevant(evidence.code_quality.duplicate_query_shapes);
|
|
386
|
+
if (duplicateQueryShapes.length > 0) {
|
|
387
|
+
const duplicateSummary = summarizeGateEffects(evidence.code_quality.duplicate_query_shapes);
|
|
388
|
+
findings.push({
|
|
389
|
+
id: 'VP-DRY-001',
|
|
390
|
+
severity: 'Medium',
|
|
391
|
+
category: 'リファクタリング',
|
|
392
|
+
title: '重複したDB query形状の候補がある',
|
|
393
|
+
detail: `${duplicateQueryShapes.length} 種類のPrisma query形状が複数箇所に出現している。内訳: ${formatGateSummary(duplicateSummary)}。`,
|
|
394
|
+
recommendation: '同じwhere/select/orderByを繰り返す箇所は、用途が同じならservice/helperへ集約し、違う用途なら責務境界をStoryで明示する。'
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
const responsibilityHotspots = filterGateRelevant(evidence.code_quality.responsibility_hotspots);
|
|
398
|
+
if (responsibilityHotspots.length > 0) {
|
|
399
|
+
const hotspotSummary = summarizeGateEffects(evidence.code_quality.responsibility_hotspots);
|
|
400
|
+
findings.push({
|
|
401
|
+
id: 'VP-ARCH-001',
|
|
402
|
+
severity: 'Medium',
|
|
403
|
+
category: '責務分離',
|
|
404
|
+
title: '責務が混在している大きなruntime file候補がある',
|
|
405
|
+
detail: `${responsibilityHotspots.length} 件のruntime fileで、DB・認証・検証・外部I/Oなど複数責務の集中を検出した。内訳: ${formatGateSummary(hotspotSummary)}。`,
|
|
406
|
+
recommendation: 'route/action/serviceの境界を見直し、DB取得、認可、入力検証、通知・外部I/O、レスポンス整形を分離するStoryに落とす。'
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (applicableChecks.has('external-resources') && evidence.static_site.external_resources.length > 0) {
|
|
411
|
+
findings.push({
|
|
412
|
+
id: 'VP-STATIC-005',
|
|
413
|
+
severity: 'Low',
|
|
414
|
+
category: '外部依存',
|
|
415
|
+
title: '外部リソースを直接読み込んでいる',
|
|
416
|
+
detail: `${evidence.static_site.external_resources.length} 件の外部リソース参照を検出した。`,
|
|
417
|
+
recommendation: '可用性、改ざん、CSP、ライセンスの観点で読み込み元を確認する。'
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
if (evidence.terminal_link_contracts) {
|
|
421
|
+
const terminalLinkHits = filterGateRelevant([
|
|
422
|
+
...(evidence.terminal_link_contracts.dot_directory_link_hits ?? []),
|
|
423
|
+
...(evidence.terminal_link_contracts.wrapped_terminal_link_hits ?? []),
|
|
424
|
+
...(evidence.terminal_link_contracts.dot_directory_tree_hits ?? []),
|
|
425
|
+
...(evidence.terminal_link_contracts.image_preview_extension_hits ?? [])
|
|
426
|
+
]);
|
|
427
|
+
if (terminalLinkHits.length > 0) {
|
|
428
|
+
const terminalLinkSummary = summarizeGateEffects(terminalLinkHits);
|
|
429
|
+
findings.push({
|
|
430
|
+
id: 'VP-TERM-001',
|
|
431
|
+
severity: 'High',
|
|
432
|
+
category: 'ターミナルプレビュー',
|
|
433
|
+
title: 'ターミナル/ファイルビューアのプレビュー契約欠落候補がある',
|
|
434
|
+
detail: `${terminalLinkHits.length} 件のterminal/file tree contract欠落候補を検出した。内訳: ${formatGateSummary(terminalLinkSummary)}。`,
|
|
435
|
+
recommendation: 'terminal linkifierは.vibepro等のdot directory相対パスとcolumn 1 hard wrapを扱い、folder treeは.vibeproをallowlistで表示する。file viewerはpng/jpg/jpeg/gif/webpなどの画像をブラウザプレビュー対象に含める。',
|
|
436
|
+
target_files: uniqueFiles(terminalLinkHits.map((hit) => hit.file)),
|
|
437
|
+
terminal_link_hits: terminalLinkHits
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (applicableChecks.has('component-style') && evidence.component_style) {
|
|
442
|
+
const legacyStyleHits = filterGateRelevant(evidence.component_style.legacy_style_hits);
|
|
443
|
+
if (legacyStyleHits.length > 0) {
|
|
444
|
+
const legacySummary = summarizeGateEffects(evidence.component_style.legacy_style_hits);
|
|
445
|
+
findings.push({
|
|
446
|
+
id: 'VP-UI-001',
|
|
447
|
+
severity: 'Medium',
|
|
448
|
+
category: 'UI品質',
|
|
449
|
+
title: '旧デザインコンポーネントのトークン候補が残っている',
|
|
450
|
+
detail: `${legacyStyleHits.length} 件のUI sourceで旧デザイン由来の色・角丸・影トークン候補を検出した。内訳: ${formatGateSummary(legacySummary)}。`,
|
|
451
|
+
recommendation: '対象コンポーネントをdesign tokenまたは新しいcomponent styleへ置き換え、スクリーンショット証跡で確認する。'
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
const interactionReliabilityHits = filterGateRelevant(evidence.component_style.interaction_reliability_hits);
|
|
455
|
+
if (interactionReliabilityHits.length > 0) {
|
|
456
|
+
const interactionSummary = summarizeGateEffects(evidence.component_style.interaction_reliability_hits);
|
|
457
|
+
findings.push({
|
|
458
|
+
id: 'VP-UI-002',
|
|
459
|
+
severity: 'Medium',
|
|
460
|
+
category: 'UI操作信頼性',
|
|
461
|
+
title: 'クリック可能要素のhit targetが不安定になるCSS候補がある',
|
|
462
|
+
detail: `${interactionReliabilityHits.length} 件のUI sourceで、hover/focus中の移動、小さすぎる操作領域、transition: all、またはアイコンSVGが実クリックtargetを奪う候補を検出した。内訳: ${formatGateSummary(interactionSummary)}。`,
|
|
463
|
+
recommendation: 'クリック可能要素はhover/focus/press中にhit targetを移動させず、状態表現は色・border・shadowに限定する。高頻度操作のtargetは最低28px程度を確保する。アイコンボタン配下のsvg/svg *はpointer-events:noneにし、Playwrightではlocator.clickだけでなくelementFromPoint(center)とpage.mouse.clickで物理クリックを確認する。'
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (evidence.gesture_interaction) {
|
|
468
|
+
const gestureHits = filterGateRelevant([
|
|
469
|
+
...(evidence.gesture_interaction.touch_action_hits ?? []),
|
|
470
|
+
...(evidence.gesture_interaction.overlay_pointer_hits ?? []),
|
|
471
|
+
...(evidence.gesture_interaction.drag_tap_hits ?? []),
|
|
472
|
+
...(evidence.gesture_interaction.carousel_hits ?? []),
|
|
473
|
+
...(evidence.gesture_interaction.map_marker_hits ?? [])
|
|
474
|
+
]);
|
|
475
|
+
if (gestureHits.length > 0) {
|
|
476
|
+
const gestureSummary = summarizeGateEffects(gestureHits);
|
|
477
|
+
findings.push({
|
|
478
|
+
id: 'VP-GESTURE-001',
|
|
479
|
+
severity: 'Medium',
|
|
480
|
+
category: 'ジェスチャー操作品質',
|
|
481
|
+
title: 'map/carousel/touch操作の契約不足候補がある',
|
|
482
|
+
detail: `${gestureHits.length} 件のUI sourceで、touch-action、overlay pointer-events、drag/tap抑止、carousel hit area、またはmap marker layeringの確認候補を検出した。内訳: ${formatGateSummary(gestureSummary)}。`,
|
|
483
|
+
recommendation: 'スワイプ、地図移動、tap、scrollの優先順位をStory/E2Eに明示し、Playwrightではdrag後のURL不変、scrollLeft/active card変化、elementFromPointでのhit targetを確認する。',
|
|
484
|
+
target_files: uniqueFiles(gestureHits.map((hit) => hit.file)),
|
|
485
|
+
gesture_hits: gestureHits
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (evidence.flow_design) {
|
|
490
|
+
const noUiCodeHits = (evidence.flow_design.value_alignment_hits ?? [])
|
|
491
|
+
.filter((hit) => hit.kind === 'ui_story_without_code_scan');
|
|
492
|
+
if (noUiCodeHits.length > 0) {
|
|
493
|
+
findings.push(buildFlowFinding({
|
|
494
|
+
id: 'VP-FLOW-000',
|
|
495
|
+
severity: 'Critical',
|
|
496
|
+
title: 'UI Storyなのに導線検査対象のUIコードが走査できない',
|
|
497
|
+
hits: noUiCodeHits,
|
|
498
|
+
detail: `${noUiCodeHits.length} 件のUIコード未走査状態を検出した。`,
|
|
499
|
+
recommendation: '対象repoでVibeProを実行するか、flow_design.code_rootsで実UI実装パスを指定する。'
|
|
500
|
+
}));
|
|
501
|
+
}
|
|
502
|
+
const ambiguousPrimaryActions = evidence.flow_design.ambiguous_primary_action_hits ?? [];
|
|
503
|
+
if (ambiguousPrimaryActions.length > 0) {
|
|
504
|
+
findings.push(buildFlowFinding({
|
|
505
|
+
id: 'VP-FLOW-001',
|
|
506
|
+
severity: 'Medium',
|
|
507
|
+
title: '主操作の意味が状態によって変わる可能性がある',
|
|
508
|
+
hits: ambiguousPrimaryActions,
|
|
509
|
+
detail: `${ambiguousPrimaryActions.length} 件の主操作意味の曖昧さを検出した。`,
|
|
510
|
+
recommendation: '主ボタンは検索、選択、保存、遷移のいずれかに意味を固定し、状態差分は明示ラベルや別操作に分ける。'
|
|
511
|
+
}));
|
|
512
|
+
}
|
|
513
|
+
const silentNoops = (evidence.flow_design.silent_noop_hits ?? [])
|
|
514
|
+
.filter((hit) => hit.gate_effect !== 'info');
|
|
515
|
+
if (silentNoops.length > 0) {
|
|
516
|
+
findings.push(buildFlowFinding({
|
|
517
|
+
id: 'VP-FLOW-002',
|
|
518
|
+
severity: 'Medium',
|
|
519
|
+
title: '押しても何も起きない導線候補がある',
|
|
520
|
+
hits: silentNoops,
|
|
521
|
+
detail: `${silentNoops.length} 件のsilent noop候補を検出した。`,
|
|
522
|
+
recommendation: '空入力や前提不足の経路では、disabled、エラー表示、フォーカス移動、補助文のいずれかを実装する。'
|
|
523
|
+
}));
|
|
524
|
+
}
|
|
525
|
+
const selectionSideEffects = evidence.flow_design.selection_side_effect_hits ?? [];
|
|
526
|
+
if (selectionSideEffects.length > 0) {
|
|
527
|
+
findings.push(buildFlowFinding({
|
|
528
|
+
id: 'VP-FLOW-003',
|
|
529
|
+
severity: 'High',
|
|
530
|
+
title: '候補選択が保存または遷移まで実行する導線候補がある',
|
|
531
|
+
hits: selectionSideEffects,
|
|
532
|
+
detail: `${selectionSideEffects.length} 件の選択副作用候補を検出した。`,
|
|
533
|
+
recommendation: '候補クリックは選択または入力欄反映に限定し、保存や画面遷移は明示ボタンに分ける。'
|
|
534
|
+
}));
|
|
535
|
+
}
|
|
536
|
+
const questionDeadEnds = evidence.flow_design.question_dead_end_hits ?? [];
|
|
537
|
+
if (questionDeadEnds.length > 0) {
|
|
538
|
+
findings.push(buildFlowFinding({
|
|
539
|
+
id: 'VP-FLOW-004',
|
|
540
|
+
severity: 'High',
|
|
541
|
+
title: '質問回答後に次の入力UIへ進まない導線候補がある',
|
|
542
|
+
hits: questionDeadEnds,
|
|
543
|
+
detail: `${questionDeadEnds.length} 件の質問dead end候補を検出した。`,
|
|
544
|
+
recommendation: '質問の回答後は、次質問、対応入力UIの展開、focus/scroll、画面遷移のいずれかに接続する。'
|
|
545
|
+
}));
|
|
546
|
+
}
|
|
547
|
+
const deadUiStates = evidence.flow_design.dead_ui_state_hits ?? [];
|
|
548
|
+
if (deadUiStates.length > 0) {
|
|
549
|
+
findings.push(buildFlowFinding({
|
|
550
|
+
id: 'VP-FLOW-005',
|
|
551
|
+
severity: 'Medium',
|
|
552
|
+
title: '到達不能な表示state候補がある',
|
|
553
|
+
hits: deadUiStates,
|
|
554
|
+
detail: `${deadUiStates.length} 件の到達不能UI state候補を検出した。`,
|
|
555
|
+
recommendation: '保存や遷移の直前にしか設定されない表示stateは削除するか、確認画面として実際に見える順序へ移動する。'
|
|
556
|
+
}));
|
|
557
|
+
}
|
|
558
|
+
const interactiveContracts = evidence.flow_design.interactive_contract_hits ?? [];
|
|
559
|
+
if (interactiveContracts.length > 0) {
|
|
560
|
+
findings.push(buildFlowFinding({
|
|
561
|
+
id: 'VP-FLOW-006',
|
|
562
|
+
severity: 'High',
|
|
563
|
+
title: 'クリック可能に見えるUIに操作契約がない候補がある',
|
|
564
|
+
hits: interactiveContracts,
|
|
565
|
+
detail: `${interactiveContracts.length} 件のinteractive element contract違反候補を検出した。`,
|
|
566
|
+
recommendation: 'クリック可能に見える要素は、保存、表示変化、画面遷移、scroll/focus、disabled、または準備中表示のいずれかに分類できるようにする。画面単位で全クリック可能要素を棚卸しし、Playwrightでは主要導線だけでなく押せそうなUIの反応も確認する。'
|
|
567
|
+
}));
|
|
568
|
+
}
|
|
569
|
+
const valueAlignmentHits = (evidence.flow_design.value_alignment_hits ?? [])
|
|
570
|
+
.filter((hit) => hit.kind !== 'ui_story_without_code_scan');
|
|
571
|
+
if (valueAlignmentHits.length > 0) {
|
|
572
|
+
findings.push(buildFlowFinding({
|
|
573
|
+
id: 'VP-FLOW-007',
|
|
574
|
+
severity: 'High',
|
|
575
|
+
title: 'Storyの価値観contractから逸脱する表示候補がある',
|
|
576
|
+
hits: valueAlignmentHits,
|
|
577
|
+
detail: `${valueAlignmentHits.length} 件の価値観contract逸脱候補を検出した。`,
|
|
578
|
+
recommendation: '禁止ラベルや旧来の判断癖を補強する導線を、Story/Spec上の価値観に沿う表現へ置き換える。'
|
|
579
|
+
}));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (applicableChecks.has('api-boundary') && evidence.api_boundary) {
|
|
583
|
+
const privilegedUnprotected = evidence.api_boundary.routes
|
|
584
|
+
.filter((route) => route.risk_hints.includes('privileged_route_unprotected'));
|
|
585
|
+
if (privilegedUnprotected.length > 0) {
|
|
586
|
+
const statusSummary = summarizeProtectionForRoutes(privilegedUnprotected);
|
|
587
|
+
findings.push({
|
|
588
|
+
id: 'VP-API-001',
|
|
589
|
+
severity: 'High',
|
|
590
|
+
category: 'API境界',
|
|
591
|
+
title: '管理系または内部系APIの保護根拠が不足している',
|
|
592
|
+
detail: `${privilegedUnprotected.length} 件の管理系または内部系API候補で保護根拠が不足している。状態別: ${formatInlineSummary(statusSummary)}。`,
|
|
593
|
+
recommendation: 'APIを除外しているmiddleware matcher、route内の認証参照、署名検証のいずれで保護するかを明示する。'
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
const debugExposed = evidence.api_boundary.routes
|
|
597
|
+
.filter((route) => route.risk_hints.includes('debug_route_exposed'));
|
|
598
|
+
if (debugExposed.length > 0) {
|
|
599
|
+
findings.push({
|
|
600
|
+
id: 'VP-API-002',
|
|
601
|
+
severity: 'High',
|
|
602
|
+
category: 'API境界',
|
|
603
|
+
title: 'debug/test API候補が公開面に残っている',
|
|
604
|
+
detail: `${debugExposed.length} 件のdebug/test API候補で保護根拠が確認できない。`,
|
|
605
|
+
recommendation: '公開環境から削除するか、認証・環境制限・ルーティング制限を明示する。'
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
const webhooksWithoutSignature = evidence.api_boundary.routes
|
|
609
|
+
.filter((route) => route.risk_hints.includes('webhook_signature_not_detected'));
|
|
610
|
+
if (webhooksWithoutSignature.length > 0) {
|
|
611
|
+
findings.push({
|
|
612
|
+
id: 'VP-API-003',
|
|
613
|
+
severity: 'High',
|
|
614
|
+
category: 'API境界',
|
|
615
|
+
title: 'webhook APIの署名検証が確認できない',
|
|
616
|
+
detail: `${webhooksWithoutSignature.length} 件のwebhook API候補で署名検証らしき実装が確認できない。`,
|
|
617
|
+
recommendation: 'Webhook送信元の署名検証、リプレイ対策、許可イベントの検証を実装または明示する。'
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (evidence.network_contracts) {
|
|
622
|
+
const missingRoutes = evidence.network_contracts.missing_routes ?? [];
|
|
623
|
+
if (missingRoutes.length > 0) {
|
|
624
|
+
findings.push({
|
|
625
|
+
id: 'VP-NET-001',
|
|
626
|
+
severity: 'Critical',
|
|
627
|
+
category: 'Network Contract',
|
|
628
|
+
title: 'UI/API client callに対応するAPI routeが存在しない',
|
|
629
|
+
detail: `${missingRoutes.length} 件の /api client call が対応する Next.js API route を持っていない。`,
|
|
630
|
+
recommendation: 'HTTP APIとして呼ぶなら app/api または pages/api にroute実体を追加する。Server Action / server functionのまま使う設計ならfetch置換を戻す。',
|
|
631
|
+
target_files: uniqueFiles(missingRoutes.map((item) => item.file)),
|
|
632
|
+
network_contract_hits: missingRoutes
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
const highRiskReplacements = evidence.network_contracts.high_risk_replacements ?? [];
|
|
636
|
+
if (highRiskReplacements.length > 0) {
|
|
637
|
+
findings.push({
|
|
638
|
+
id: 'VP-NET-002',
|
|
639
|
+
severity: 'High',
|
|
640
|
+
category: 'Network Contract',
|
|
641
|
+
title: 'server function呼び出しがHTTP API呼び出しへ置換されている',
|
|
642
|
+
detail: `${highRiskReplacements.length} 件の差分で既存server function呼び出しが新規 /api client call へ置換されている。`,
|
|
643
|
+
recommendation: 'route実体、入力schema、認証、runtime制約、E2Eネットワーク証跡が揃っているか確認する。',
|
|
644
|
+
target_files: uniqueFiles(highRiskReplacements.map((item) => item.file)),
|
|
645
|
+
network_contract_hits: highRiskReplacements
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
const dynamicCalls = evidence.network_contracts.dynamic_calls ?? [];
|
|
649
|
+
if (dynamicCalls.length > 0) {
|
|
650
|
+
findings.push({
|
|
651
|
+
id: 'VP-NET-003',
|
|
652
|
+
severity: 'Medium',
|
|
653
|
+
category: 'Network Contract',
|
|
654
|
+
title: '静的にroute実体を確定できないAPI client callがある',
|
|
655
|
+
detail: `${dynamicCalls.length} 件の動的 /api path を検出した。`,
|
|
656
|
+
recommendation: 'template literalやwrapper経由のAPI pathは、候補route・テスト・Playwrightネットワーク証跡で契約を補強する。',
|
|
657
|
+
target_files: uniqueFiles(dynamicCalls.map((item) => item.file)),
|
|
658
|
+
network_contract_hits: dynamicCalls
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (evidence.requirement_consistency?.status === 'contradicted') {
|
|
663
|
+
findings.push({
|
|
664
|
+
id: 'VP-REQ-001',
|
|
665
|
+
severity: 'High',
|
|
666
|
+
category: '要件整合性',
|
|
667
|
+
title: 'Story要件とコード上の状態遷移が矛盾している可能性がある',
|
|
668
|
+
detail: `${evidence.requirement_consistency.summary?.contradiction_count ?? 0} 件の要件矛盾候補を検出した。`,
|
|
669
|
+
recommendation: 'Story不変条件、実装分岐、テスト期待値を突き合わせ、業務ルールとして正しい状態遷移を明示する。'
|
|
670
|
+
});
|
|
671
|
+
} else if (evidence.requirement_consistency?.status === 'needs_review') {
|
|
672
|
+
findings.push({
|
|
673
|
+
id: 'VP-REQ-002',
|
|
674
|
+
severity: 'Medium',
|
|
675
|
+
category: '要件整合性',
|
|
676
|
+
title: 'Storyに明示されていない重要シナリオ分岐がある',
|
|
677
|
+
detail: `${evidence.requirement_consistency.summary?.scenario_gap_count ?? 0} 件のシナリオ確認候補を検出した。`,
|
|
678
|
+
recommendation: '受け入れ基準に重要分岐を追加するか、既存ポリシーへの参照をStoryに残す。'
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
return findings;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function buildGraphQualityNotices({ inferredEdges }) {
|
|
685
|
+
const notices = [];
|
|
686
|
+
if (inferredEdges.length > 0) {
|
|
687
|
+
notices.push({
|
|
688
|
+
id: 'VP-GRAPH-002',
|
|
689
|
+
level: 'info',
|
|
690
|
+
category: '文脈品質',
|
|
691
|
+
title: '推論された依存関係がある',
|
|
692
|
+
detail: `graphify が ${inferredEdges.length} 件の推論関係を検出した。`,
|
|
693
|
+
recommendation: '推論関係は診断質問として扱い、検証済み事実として扱わない。'
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
return notices;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function buildFlowFinding({ id, severity, title, hits, detail, recommendation }) {
|
|
700
|
+
return {
|
|
701
|
+
id,
|
|
702
|
+
severity,
|
|
703
|
+
category: '導線設計',
|
|
704
|
+
title,
|
|
705
|
+
detail,
|
|
706
|
+
recommendation,
|
|
707
|
+
target_files: uniqueFiles((hits ?? []).map((hit) => hit.file)),
|
|
708
|
+
flow_hits: hits ?? []
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function buildActionCandidates(repoRoot, evidence, graphIndex) {
|
|
713
|
+
const candidates = [];
|
|
714
|
+
const apiBoundary = evidence.api_boundary;
|
|
715
|
+
|
|
716
|
+
if (apiBoundary) {
|
|
717
|
+
const privilegedUnprotected = apiBoundary.routes
|
|
718
|
+
.filter((route) => route.risk_hints.includes('privileged_route_unprotected'));
|
|
719
|
+
if (privilegedUnprotected.length > 0) {
|
|
720
|
+
const graphContext = buildGraphContextForRoutes(privilegedUnprotected, graphIndex);
|
|
721
|
+
candidates.push({
|
|
722
|
+
id: 'VP-ACTION-API-001',
|
|
723
|
+
finding_id: 'VP-API-001',
|
|
724
|
+
scope: 'api_boundary',
|
|
725
|
+
title: '管理系または内部系APIの保護方針を決める',
|
|
726
|
+
target_count: privilegedUnprotected.length,
|
|
727
|
+
execution_policy: 'proposal_only',
|
|
728
|
+
mutates_repository: false,
|
|
729
|
+
confidence: privilegedUnprotected.some((route) => route.protection?.status === 'unknown') ? 'medium' : 'high',
|
|
730
|
+
recommendation: 'middlewareでAPIを保護するか、route内認証を追加するかを決め、対象routeごとに保護根拠を明示する。',
|
|
731
|
+
route_examples: buildRouteExamples(privilegedUnprotected),
|
|
732
|
+
graph_context: graphContext,
|
|
733
|
+
implementation_plan: await buildImplementationPlanForAction({
|
|
734
|
+
repoRoot,
|
|
735
|
+
actionId: 'VP-ACTION-API-001',
|
|
736
|
+
routes: privilegedUnprotected,
|
|
737
|
+
apiBoundary,
|
|
738
|
+
graphContext
|
|
739
|
+
})
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const debugExposed = apiBoundary.routes
|
|
744
|
+
.filter((route) => route.risk_hints.includes('debug_route_exposed'));
|
|
745
|
+
if (debugExposed.length > 0) {
|
|
746
|
+
const graphContext = buildGraphContextForRoutes(debugExposed, graphIndex);
|
|
747
|
+
candidates.push({
|
|
748
|
+
id: 'VP-ACTION-API-002',
|
|
749
|
+
finding_id: 'VP-API-002',
|
|
750
|
+
scope: 'api_boundary',
|
|
751
|
+
title: 'debug/test APIの公開可否を確認する',
|
|
752
|
+
target_count: debugExposed.length,
|
|
753
|
+
execution_policy: 'proposal_only',
|
|
754
|
+
mutates_repository: false,
|
|
755
|
+
confidence: 'high',
|
|
756
|
+
recommendation: '本番公開が不要なdebug/test APIは削除し、必要な場合は認証または環境制限を明示する。',
|
|
757
|
+
route_examples: buildRouteExamples(debugExposed),
|
|
758
|
+
graph_context: graphContext,
|
|
759
|
+
implementation_plan: await buildImplementationPlanForAction({
|
|
760
|
+
repoRoot,
|
|
761
|
+
actionId: 'VP-ACTION-API-002',
|
|
762
|
+
routes: debugExposed,
|
|
763
|
+
apiBoundary,
|
|
764
|
+
graphContext
|
|
765
|
+
})
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const webhooksWithoutSignature = apiBoundary.routes
|
|
770
|
+
.filter((route) => route.risk_hints.includes('webhook_signature_not_detected'));
|
|
771
|
+
if (webhooksWithoutSignature.length > 0) {
|
|
772
|
+
const graphContext = buildGraphContextForRoutes(webhooksWithoutSignature, graphIndex);
|
|
773
|
+
candidates.push({
|
|
774
|
+
id: 'VP-ACTION-API-003',
|
|
775
|
+
finding_id: 'VP-API-003',
|
|
776
|
+
scope: 'api_boundary',
|
|
777
|
+
title: 'webhook APIの署名検証方針を確認する',
|
|
778
|
+
target_count: webhooksWithoutSignature.length,
|
|
779
|
+
execution_policy: 'proposal_only',
|
|
780
|
+
mutates_repository: false,
|
|
781
|
+
confidence: 'high',
|
|
782
|
+
recommendation: 'Webhook送信元の署名検証、リプレイ対策、許可イベントの検証を実装または明示する。',
|
|
783
|
+
route_examples: buildRouteExamples(webhooksWithoutSignature),
|
|
784
|
+
graph_context: graphContext,
|
|
785
|
+
implementation_plan: await buildImplementationPlanForAction({
|
|
786
|
+
repoRoot,
|
|
787
|
+
actionId: 'VP-ACTION-API-003',
|
|
788
|
+
routes: webhooksWithoutSignature,
|
|
789
|
+
apiBoundary,
|
|
790
|
+
graphContext
|
|
791
|
+
})
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
candidates.push(...buildRefactoringActionCandidates(evidence));
|
|
797
|
+
return candidates;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async function buildImplementationPlanForAction({ repoRoot, actionId, routes, apiBoundary, graphContext }) {
|
|
801
|
+
const readFirstFiles = buildReadFirstFiles({ routes, apiBoundary, graphContext });
|
|
802
|
+
const preFixBriefing = await buildPreFixBriefingForAction({
|
|
803
|
+
repoRoot,
|
|
804
|
+
actionId,
|
|
805
|
+
routes,
|
|
806
|
+
apiBoundary,
|
|
807
|
+
graphContext,
|
|
808
|
+
readFirstFiles
|
|
809
|
+
});
|
|
810
|
+
return {
|
|
811
|
+
priority: resolveImplementationPriority({ actionId, routes, graphContext }),
|
|
812
|
+
rationale: buildImplementationRationale({ routes, graphContext }),
|
|
813
|
+
read_first_files: readFirstFiles,
|
|
814
|
+
steps: buildImplementationSteps(actionId),
|
|
815
|
+
acceptance_criteria: buildAcceptanceCriteria(actionId),
|
|
816
|
+
pre_fix_briefing: preFixBriefing
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async function buildPreFixBriefingForAction({ repoRoot, actionId, routes, apiBoundary, graphContext, readFirstFiles }) {
|
|
821
|
+
const codeSignals = await buildCodeSignalsForFiles(repoRoot, readFirstFiles.map((item) => item.file));
|
|
822
|
+
const authHelpers = buildAuthHelpers({ actionId, routes, codeSignals });
|
|
823
|
+
return {
|
|
824
|
+
current_boundary: buildCurrentBoundary({ apiBoundary, routes }),
|
|
825
|
+
auth_helpers: authHelpers,
|
|
826
|
+
target_routes: routes.map((route) => ({
|
|
827
|
+
file: route.file,
|
|
828
|
+
route_path: route.route_path,
|
|
829
|
+
methods: route.methods ?? [],
|
|
830
|
+
classification: route.classification,
|
|
831
|
+
protection_status: route.protection?.status ?? 'unknown',
|
|
832
|
+
protection_evidence: route.protection?.evidence ?? [],
|
|
833
|
+
risk_hints: route.risk_hints ?? []
|
|
834
|
+
})),
|
|
835
|
+
code_signals: codeSignals,
|
|
836
|
+
strategy_options: buildStrategyOptions({ actionId, routes, graphContext, codeSignals, authHelpers }),
|
|
837
|
+
recommended_strategy: buildRecommendedStrategy({ actionId, routes, apiBoundary, codeSignals })
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function buildCurrentBoundary({ apiBoundary, routes }) {
|
|
842
|
+
return {
|
|
843
|
+
middleware: {
|
|
844
|
+
matchers: apiBoundary.middleware?.matchers ?? [],
|
|
845
|
+
excludes_api: (apiBoundary.middleware?.matchers ?? []).some((matcher) => matcher.includes('(?!api') || matcher.includes('(?!/api')),
|
|
846
|
+
files: apiBoundary.middleware?.evidence ?? []
|
|
847
|
+
},
|
|
848
|
+
route_protection: summarizeProtectionForRoutes(routes)
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async function buildCodeSignalsForFiles(repoRoot, files) {
|
|
853
|
+
const signals = [];
|
|
854
|
+
for (const file of files) {
|
|
855
|
+
const content = await readTextIfExists(path.join(repoRoot, file));
|
|
856
|
+
if (!content) continue;
|
|
857
|
+
signals.push(extractCodeSignals(file, content));
|
|
858
|
+
}
|
|
859
|
+
return signals;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function extractCodeSignals(file, content) {
|
|
863
|
+
const code = stripCodeComments(content);
|
|
864
|
+
const functionNames = [
|
|
865
|
+
...code.matchAll(/export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/g),
|
|
866
|
+
...code.matchAll(/function\s+([A-Za-z0-9_$]+)/g),
|
|
867
|
+
...code.matchAll(/export\s+const\s+([A-Za-z0-9_$]+)\s*=/g)
|
|
868
|
+
].map((match) => match[1]);
|
|
869
|
+
const imports = [...code.matchAll(/import\s+[^'"]*['"]([^'"]+)['"]/g)]
|
|
870
|
+
.map((match) => match[1])
|
|
871
|
+
.slice(0, 20);
|
|
872
|
+
return {
|
|
873
|
+
file,
|
|
874
|
+
functions: [...new Set(functionNames)],
|
|
875
|
+
imports: [...new Set(imports)],
|
|
876
|
+
auth_references: extractMatches(code, [
|
|
877
|
+
/\b(auth\.api\.getSession|getServerSession|requireAuth|currentUser|getSession|validateSession|authenticateApiKey)\b/g,
|
|
878
|
+
/\b(session|token|authorization|Bearer)\b/gi
|
|
879
|
+
]),
|
|
880
|
+
signature_references: extractMatches(code, [
|
|
881
|
+
/\b(signature|stripe-signature|verifyWebhook|verifySignature|constructEvent|webhooks\.verify|verify)\b/gi
|
|
882
|
+
]),
|
|
883
|
+
env_guard_references: extractMatches(code, [
|
|
884
|
+
/\b(process\.env\.[A-Z0-9_]+|NODE_ENV)\b/g
|
|
885
|
+
])
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function stripCodeComments(content) {
|
|
890
|
+
return content
|
|
891
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
892
|
+
.replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function extractMatches(content, patterns) {
|
|
896
|
+
const values = [];
|
|
897
|
+
for (const pattern of patterns) {
|
|
898
|
+
for (const match of content.matchAll(pattern)) {
|
|
899
|
+
values.push(match[1] ?? match[0]);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return [...new Set(values)].slice(0, 20);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const AUTH_HELPER_PATTERN = /auth|session|user|token/i;
|
|
906
|
+
const SIGNATURE_HELPER_PATTERN = /verify|signature|webhook|constructEvent/i;
|
|
907
|
+
const ENV_GUARD_PATTERN = /env|environment|nodeEnv|production/i;
|
|
908
|
+
|
|
909
|
+
function buildAuthHelpers({ actionId, routes, codeSignals }) {
|
|
910
|
+
return codeSignals
|
|
911
|
+
.map((signal) => buildHelperForAction({ actionId, routes, signal }))
|
|
912
|
+
.filter(Boolean);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function buildHelperForAction({ actionId, routes, signal }) {
|
|
916
|
+
if (actionId === 'VP-ACTION-API-003') {
|
|
917
|
+
return buildSignatureHelper({ routes, signal });
|
|
918
|
+
}
|
|
919
|
+
if (actionId === 'VP-ACTION-API-002') {
|
|
920
|
+
return buildDebugGuardHelper(signal);
|
|
921
|
+
}
|
|
922
|
+
return buildRouteAuthHelper(signal);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function buildRouteAuthHelper(signal) {
|
|
926
|
+
const functions = signal.functions.filter((name) => AUTH_HELPER_PATTERN.test(name)).slice(0, 10);
|
|
927
|
+
if (functions.length === 0 && signal.auth_references.length === 0) return null;
|
|
928
|
+
return {
|
|
929
|
+
category: 'auth',
|
|
930
|
+
file: signal.file,
|
|
931
|
+
functions,
|
|
932
|
+
imports: signal.imports,
|
|
933
|
+
auth_references: signal.auth_references,
|
|
934
|
+
signature_references: [],
|
|
935
|
+
env_guard_references: []
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function buildDebugGuardHelper(signal) {
|
|
940
|
+
const authFunctions = signal.functions.filter((name) => AUTH_HELPER_PATTERN.test(name));
|
|
941
|
+
const envFunctions = signal.functions.filter((name) => ENV_GUARD_PATTERN.test(name));
|
|
942
|
+
const hasAuthSignal = authFunctions.length > 0 || signal.auth_references.length > 0;
|
|
943
|
+
const hasEnvSignal = envFunctions.length > 0 || signal.env_guard_references.length > 0;
|
|
944
|
+
if (!hasAuthSignal && !hasEnvSignal) return null;
|
|
945
|
+
return {
|
|
946
|
+
category: hasAuthSignal ? 'auth' : 'environment',
|
|
947
|
+
file: signal.file,
|
|
948
|
+
functions: uniqueFiles([...authFunctions, ...envFunctions]).slice(0, 10),
|
|
949
|
+
imports: signal.imports,
|
|
950
|
+
auth_references: hasAuthSignal ? signal.auth_references : [],
|
|
951
|
+
signature_references: [],
|
|
952
|
+
env_guard_references: hasEnvSignal ? signal.env_guard_references : []
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function buildSignatureHelper({ routes, signal }) {
|
|
957
|
+
const providers = getWebhookProviders(routes);
|
|
958
|
+
const targetRouteFiles = new Set(routes.map((route) => route.file));
|
|
959
|
+
const hasSignatureSignal = signal.functions.some((name) => SIGNATURE_HELPER_PATTERN.test(name))
|
|
960
|
+
|| signal.signature_references.length > 0;
|
|
961
|
+
if (!hasSignatureSignal) return null;
|
|
962
|
+
if (providers.length > 0 && !matchesAnyProvider(signal, providers)) return null;
|
|
963
|
+
const functions = signal.functions.filter((name) => SIGNATURE_HELPER_PATTERN.test(name)).slice(0, 10);
|
|
964
|
+
if (targetRouteFiles.has(signal.file) && functions.length === 0) return null;
|
|
965
|
+
if (functions.length === 0 && signal.signature_references.length === 0) return null;
|
|
966
|
+
return {
|
|
967
|
+
category: 'signature',
|
|
968
|
+
file: signal.file,
|
|
969
|
+
functions,
|
|
970
|
+
imports: signal.imports,
|
|
971
|
+
auth_references: [],
|
|
972
|
+
signature_references: signal.signature_references,
|
|
973
|
+
env_guard_references: []
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function buildStrategyOptions({ actionId, routes, graphContext, codeSignals, authHelpers }) {
|
|
978
|
+
const routeFiles = routes.map((route) => route.file);
|
|
979
|
+
const helperFiles = (authHelpers ?? buildAuthHelpers({ actionId, routes, codeSignals })).map((helper) => helper.file);
|
|
980
|
+
if (actionId === 'VP-ACTION-API-001') {
|
|
981
|
+
return [
|
|
982
|
+
{
|
|
983
|
+
id: 'middleware-matcher',
|
|
984
|
+
label: '方針A: middleware matcherで対象APIを保護する',
|
|
985
|
+
target_count: routes.length,
|
|
986
|
+
candidate_files: uniqueFiles(['src/middleware.ts', ...routeFiles]),
|
|
987
|
+
benefits: ['保護境界を一箇所で管理できる', '対象routeが多い場合に変更点を集約できる'],
|
|
988
|
+
cautions: ['public APIやwebhookを巻き込まないmatcher設計が必要', '現在API全体を除外している場合は設計変更の影響が大きい']
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
id: 'route-level-auth',
|
|
992
|
+
label: '方針B: route-level authを各APIに追加する',
|
|
993
|
+
target_count: routes.length,
|
|
994
|
+
candidate_files: uniqueFiles([...routeFiles, ...helperFiles]),
|
|
995
|
+
benefits: ['routeごとに保護根拠が明確になる', 'webhook/public APIを巻き込みにくい'],
|
|
996
|
+
cautions: ['対象route数が多い場合は重複実装を避けるhelper化が必要']
|
|
997
|
+
}
|
|
998
|
+
];
|
|
999
|
+
}
|
|
1000
|
+
if (actionId === 'VP-ACTION-API-002') {
|
|
1001
|
+
return [
|
|
1002
|
+
{
|
|
1003
|
+
id: 'delete-debug-routes',
|
|
1004
|
+
label: '方針A: 本番不要なdebug/test APIを削除する',
|
|
1005
|
+
target_count: routes.length,
|
|
1006
|
+
candidate_files: uniqueFiles(routeFiles),
|
|
1007
|
+
benefits: ['公開面を最小化できる', '保護漏れの再発リスクを減らせる'],
|
|
1008
|
+
cautions: ['開発・検証運用で使っていないか確認が必要']
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
id: 'restrict-debug-routes',
|
|
1012
|
+
label: '方針B: 残すrouteへ認証または環境制限を追加する',
|
|
1013
|
+
target_count: routes.length,
|
|
1014
|
+
candidate_files: uniqueFiles([...routeFiles, ...helperFiles]),
|
|
1015
|
+
benefits: ['必要な運用APIを残せる', '段階的に公開面を狭められる'],
|
|
1016
|
+
cautions: ['本番環境で無効化されることを診断で確認する必要がある']
|
|
1017
|
+
}
|
|
1018
|
+
];
|
|
1019
|
+
}
|
|
1020
|
+
return [
|
|
1021
|
+
{
|
|
1022
|
+
id: 'provider-signature-verification',
|
|
1023
|
+
label: '方針A: providerごとの署名検証をroute内に追加する',
|
|
1024
|
+
target_count: routes.length,
|
|
1025
|
+
candidate_files: uniqueFiles(routeFiles),
|
|
1026
|
+
benefits: ['送信元仕様に沿った検証をrouteで明示できる', 'VibeProの署名検証検出に乗りやすい'],
|
|
1027
|
+
cautions: ['providerごとの署名ヘッダーと再送仕様の確認が必要']
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
id: 'connect-existing-helper',
|
|
1031
|
+
label: '方針B: 既存helperへ接続する',
|
|
1032
|
+
target_count: routes.length,
|
|
1033
|
+
candidate_files: uniqueFiles([...routeFiles, ...helperFiles]),
|
|
1034
|
+
benefits: ['既存の検証ロジックを再利用できる', '重複実装を避けられる'],
|
|
1035
|
+
cautions: ['helperが署名検証、リプレイ対策、許可イベント検証を満たすか確認が必要']
|
|
1036
|
+
}
|
|
1037
|
+
];
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function uniqueFiles(files) {
|
|
1041
|
+
return [...new Set(files.filter(Boolean))];
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function buildRecommendedStrategy({ actionId, routes, apiBoundary, codeSignals }) {
|
|
1045
|
+
const excludesApi = (apiBoundary.middleware?.matchers ?? []).some((matcher) => matcher.includes('(?!api') || matcher.includes('(?!/api'));
|
|
1046
|
+
const hasSignatureHelper = hasProviderSignatureHelper(routes, codeSignals);
|
|
1047
|
+
if (actionId === 'VP-ACTION-API-001') {
|
|
1048
|
+
return excludesApi
|
|
1049
|
+
? { id: 'route-level-auth', reason: '現在middlewareがAPI全体を除外しているため、webhook/public APIを巻き込まないroute-level authを優先する。' }
|
|
1050
|
+
: { id: 'middleware-matcher', reason: 'middlewareでAPI保護境界を管理できる状態なので、対象routeが多い場合はmatcher集約を優先する。' };
|
|
1051
|
+
}
|
|
1052
|
+
if (actionId === 'VP-ACTION-API-002') {
|
|
1053
|
+
return { id: 'delete-debug-routes', reason: 'debug/test APIは本番公開面から消すのが最も単純で再発しにくい。' };
|
|
1054
|
+
}
|
|
1055
|
+
return hasSignatureHelper
|
|
1056
|
+
? { id: 'connect-existing-helper', reason: 'graphify hubまたは読取対象に署名検証候補があるため、既存helper接続を優先する。' }
|
|
1057
|
+
: { id: 'provider-signature-verification', reason: '既存の署名検証helperが確認できないため、providerごとの検証をrouteに明示する。' };
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function hasProviderSignatureHelper(routes, codeSignals) {
|
|
1061
|
+
const providers = getWebhookProviders(routes);
|
|
1062
|
+
if (providers.length === 0) {
|
|
1063
|
+
return codeSignals.some((signal) => signal.signature_references.length > 0);
|
|
1064
|
+
}
|
|
1065
|
+
return codeSignals.some((signal) => (
|
|
1066
|
+
(signal.signature_references.length > 0 || signal.functions.some((name) => SIGNATURE_HELPER_PATTERN.test(name)))
|
|
1067
|
+
&& matchesAnyProvider(signal, providers)
|
|
1068
|
+
));
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function getWebhookProviders(routes) {
|
|
1072
|
+
return routes
|
|
1073
|
+
.map((route) => {
|
|
1074
|
+
return /\/api\/webhooks\/([^/]+)/.exec(route.route_path)?.[1]
|
|
1075
|
+
?? /\/api\/([^/]+)\/webhook(?:s)?(?:\/|$)/.exec(route.route_path)?.[1]
|
|
1076
|
+
?? /\/webhook(?:s)?\/([^/]+)/.exec(route.route_path)?.[1];
|
|
1077
|
+
})
|
|
1078
|
+
.filter(Boolean);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function matchesAnyProvider(signal, providers) {
|
|
1082
|
+
const haystack = [
|
|
1083
|
+
signal.file,
|
|
1084
|
+
...signal.functions,
|
|
1085
|
+
...signal.signature_references
|
|
1086
|
+
].join(' ').toLowerCase();
|
|
1087
|
+
return providers.some((provider) => haystack.includes(provider.toLowerCase()));
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function resolveImplementationPriority({ routes, graphContext }) {
|
|
1091
|
+
if (routes.length > 0) return 'high';
|
|
1092
|
+
if ((graphContext?.related_edge_count ?? 0) > 0) return 'medium';
|
|
1093
|
+
return 'low';
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function buildImplementationRationale({ routes, graphContext }) {
|
|
1097
|
+
const context = graphContext ?? emptyGraphContext();
|
|
1098
|
+
const topCommunity = context.affected_communities[0];
|
|
1099
|
+
const communityText = topCommunity
|
|
1100
|
+
? `最大community ${topCommunity.id} は ${topCommunity.route_count} route / ${topCommunity.node_count} node / ${topCommunity.edge_count} edge。`
|
|
1101
|
+
: '対応するgraph communityは未特定。';
|
|
1102
|
+
return `${routes.length}件のrouteが対象。graphifyでは ${context.matched_node_count} node / ${context.related_edge_count} edge に接続し、impactは ${context.impact_score}。${communityText}`;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function buildReadFirstFiles({ routes, apiBoundary, graphContext }) {
|
|
1106
|
+
const files = [];
|
|
1107
|
+
const seen = new Set();
|
|
1108
|
+
const add = (file, reason) => {
|
|
1109
|
+
const normalized = normalizeGraphPath(file ?? '');
|
|
1110
|
+
if (!normalized || seen.has(normalized)) return;
|
|
1111
|
+
seen.add(normalized);
|
|
1112
|
+
files.push({ file: normalized, reason });
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
for (const route of routes.slice(0, 10)) {
|
|
1116
|
+
add(route.file, `対象API route: ${route.route_path}`);
|
|
1117
|
+
}
|
|
1118
|
+
for (const evidence of apiBoundary.middleware?.evidence ?? []) {
|
|
1119
|
+
add(evidence.file, 'API保護方針を確認するmiddleware');
|
|
1120
|
+
}
|
|
1121
|
+
for (const node of graphContext?.hub_nodes ?? []) {
|
|
1122
|
+
add(node.source_file, `graphify hub: ${node.label} (degree ${node.degree})`);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
return files.slice(0, 12);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function buildImplementationSteps(actionId) {
|
|
1129
|
+
if (actionId === 'VP-ACTION-API-001') {
|
|
1130
|
+
return [
|
|
1131
|
+
{
|
|
1132
|
+
id: 'confirm-protection-boundary',
|
|
1133
|
+
title: '保護境界を決める',
|
|
1134
|
+
detail: '対象routeをmiddleware matcherで守るか、route内認証で守るかを決める。middleware matcherを使う場合はAPI除外条件との整合を確認する。'
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
id: 'add-protection-evidence',
|
|
1138
|
+
title: '保護根拠を追加する',
|
|
1139
|
+
detail: '選んだ方式に沿って、対象routeごとに認証参照またはmatcher対象であることをコード上に明示する。'
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
id: 'rerun-diagnosis',
|
|
1143
|
+
title: '診断を再実行する',
|
|
1144
|
+
detail: 'VibePro診断を再実行し、対象routeの保護状態とrisk hintが改善したことを確認する。'
|
|
1145
|
+
}
|
|
1146
|
+
];
|
|
1147
|
+
}
|
|
1148
|
+
if (actionId === 'VP-ACTION-API-002') {
|
|
1149
|
+
return [
|
|
1150
|
+
{
|
|
1151
|
+
id: 'classify-debug-route',
|
|
1152
|
+
title: '公開要否を判定する',
|
|
1153
|
+
detail: 'debug/test APIが本番で不要なら削除する。必要な場合は用途、利用者、公開環境を限定する。'
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
id: 'restrict-debug-route',
|
|
1157
|
+
title: '公開面を制限する',
|
|
1158
|
+
detail: '削除しないrouteには認証、環境変数による本番停止、または内部経路限定を追加する。'
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
id: 'rerun-diagnosis',
|
|
1162
|
+
title: '診断を再実行する',
|
|
1163
|
+
detail: 'VibePro診断を再実行し、debug_route_exposedが残っていないことを確認する。'
|
|
1164
|
+
}
|
|
1165
|
+
];
|
|
1166
|
+
}
|
|
1167
|
+
return [
|
|
1168
|
+
{
|
|
1169
|
+
id: 'confirm-webhook-provider',
|
|
1170
|
+
title: 'Webhook送信元を確認する',
|
|
1171
|
+
detail: '送信元ごとの署名ヘッダー、署名方式、許可イベント、再送仕様を確認する。'
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
id: 'add-signature-verification',
|
|
1175
|
+
title: '署名検証を追加する',
|
|
1176
|
+
detail: 'route内で署名検証、リプレイ対策、許可イベント検証を実装または既存実装へ接続する。'
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
id: 'rerun-diagnosis',
|
|
1180
|
+
title: '診断を再実行する',
|
|
1181
|
+
detail: 'VibePro診断を再実行し、webhook_signature_checkが保護根拠として検出されることを確認する。'
|
|
1182
|
+
}
|
|
1183
|
+
];
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function buildAcceptanceCriteria(actionId) {
|
|
1187
|
+
if (actionId === 'VP-ACTION-API-001') {
|
|
1188
|
+
return [
|
|
1189
|
+
'対象routeごとにmiddleware matcherまたはroute内認証の保護根拠が確認できる。',
|
|
1190
|
+
'VibePro診断でprivileged_route_unprotectedが対象routeから消える。'
|
|
1191
|
+
];
|
|
1192
|
+
}
|
|
1193
|
+
if (actionId === 'VP-ACTION-API-002') {
|
|
1194
|
+
return [
|
|
1195
|
+
'本番不要なdebug/test APIは削除されている。',
|
|
1196
|
+
'残すdebug/test APIは認証または環境制限で公開面が限定されている。',
|
|
1197
|
+
'VibePro診断でdebug_route_exposedが対象routeから消える。'
|
|
1198
|
+
];
|
|
1199
|
+
}
|
|
1200
|
+
return [
|
|
1201
|
+
'Webhook routeで送信元の署名検証が実行される。',
|
|
1202
|
+
'リプレイ対策と許可イベント検証の方針がコード上で確認できる。',
|
|
1203
|
+
'VibePro診断でwebhook_signature_checkが保護根拠として検出される。'
|
|
1204
|
+
];
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function attachFindingGraphContexts(findings, candidates) {
|
|
1208
|
+
const contextByFindingId = new Map(
|
|
1209
|
+
candidates
|
|
1210
|
+
.filter((candidate) => candidate.finding_id && candidate.graph_context)
|
|
1211
|
+
.map((candidate) => [candidate.finding_id, candidate.graph_context])
|
|
1212
|
+
);
|
|
1213
|
+
for (const finding of findings) {
|
|
1214
|
+
if (contextByFindingId.has(finding.id)) {
|
|
1215
|
+
finding.graph_context = contextByFindingId.get(finding.id);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function buildFindingReview({ findings, actionCandidates }) {
|
|
1221
|
+
const candidatesByFindingId = new Map(
|
|
1222
|
+
actionCandidates
|
|
1223
|
+
.filter((candidate) => candidate.finding_id)
|
|
1224
|
+
.map((candidate) => [candidate.finding_id, candidate])
|
|
1225
|
+
);
|
|
1226
|
+
const items = findings.map((finding) => {
|
|
1227
|
+
const suggestedClassification = suggestFindingReviewClassification(finding);
|
|
1228
|
+
const candidate = candidatesByFindingId.get(finding.id);
|
|
1229
|
+
return {
|
|
1230
|
+
finding_id: finding.id,
|
|
1231
|
+
review_status: 'unreviewed',
|
|
1232
|
+
suggested_classification: suggestedClassification,
|
|
1233
|
+
allowed_classifications: [
|
|
1234
|
+
'true_positive',
|
|
1235
|
+
'false_positive',
|
|
1236
|
+
'false_negative',
|
|
1237
|
+
'detector_gap',
|
|
1238
|
+
'implementation_gap'
|
|
1239
|
+
],
|
|
1240
|
+
rationale: buildFindingReviewRationale(finding, suggestedClassification),
|
|
1241
|
+
review_questions: buildFindingReviewQuestions(finding, suggestedClassification),
|
|
1242
|
+
evidence_refs: buildFindingEvidenceRefs(finding, candidate),
|
|
1243
|
+
action_candidate_id: candidate?.id ?? null,
|
|
1244
|
+
reviewer_notes: ''
|
|
1245
|
+
};
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
return {
|
|
1249
|
+
schema_version: '0.1.0',
|
|
1250
|
+
status: findings.length === 0 ? 'no_findings' : 'needs_review',
|
|
1251
|
+
policy: 'この分類は初期レビュー票であり、true_positive/false_positive は人間の確認後に確定する。',
|
|
1252
|
+
summary: summarizeFindingReview(items),
|
|
1253
|
+
items
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function suggestFindingReviewClassification(finding) {
|
|
1258
|
+
if (finding.id?.startsWith('VP-GRAPH-')) return 'detector_gap';
|
|
1259
|
+
return 'implementation_gap';
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function buildFindingReviewRationale(finding, classification) {
|
|
1263
|
+
if (classification === 'detector_gap') {
|
|
1264
|
+
return `${finding.id} は診断に使う依存関係や文脈の確度に関する検出であり、実装修正より先に検出根拠の確認が必要。`;
|
|
1265
|
+
}
|
|
1266
|
+
return `${finding.id} は対象リポジトリ内の公開面、API境界、または配信設計に対する実装不足候補として検出された。`;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function buildFindingReviewQuestions(finding, classification) {
|
|
1270
|
+
const common = [
|
|
1271
|
+
'検出根拠は対象リポジトリの現在のコードと一致しているか。',
|
|
1272
|
+
'同種の未検出リスクが周辺ファイルに残っていないか。',
|
|
1273
|
+
'再診断でこのfindingが消える完了条件を具体化できるか。'
|
|
1274
|
+
];
|
|
1275
|
+
if (classification === 'detector_gap') {
|
|
1276
|
+
return [
|
|
1277
|
+
'graphifyまたはVibePro検出器の根拠は実際の依存関係を表しているか。',
|
|
1278
|
+
'検出器のfalse positiveまたはfalse negativeとして修正すべきか。',
|
|
1279
|
+
...common
|
|
1280
|
+
];
|
|
1281
|
+
}
|
|
1282
|
+
return [
|
|
1283
|
+
'実装不足として修正すべきtrue positiveか、既存実装を検出できていないdetector gapか。',
|
|
1284
|
+
'本番運用上の例外として受け入れるなら、その根拠をコードまたは設定に残せるか。',
|
|
1285
|
+
...common
|
|
1286
|
+
];
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function buildFindingEvidenceRefs(finding, candidate) {
|
|
1290
|
+
return {
|
|
1291
|
+
finding_detail: finding.detail,
|
|
1292
|
+
recommendation: finding.recommendation,
|
|
1293
|
+
graph_context: finding.graph_context ?? null,
|
|
1294
|
+
target_files: candidate?.target_files ?? [],
|
|
1295
|
+
route_examples: candidate?.route_examples ?? [],
|
|
1296
|
+
implementation_plan: candidate?.implementation_plan ?? null
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function summarizeFindingReview(items) {
|
|
1301
|
+
const summary = {
|
|
1302
|
+
total: items.length,
|
|
1303
|
+
unreviewed: 0,
|
|
1304
|
+
true_positive: 0,
|
|
1305
|
+
false_positive: 0,
|
|
1306
|
+
false_negative: 0,
|
|
1307
|
+
detector_gap: 0,
|
|
1308
|
+
implementation_gap: 0
|
|
1309
|
+
};
|
|
1310
|
+
for (const item of items) {
|
|
1311
|
+
summary[item.review_status] = (summary[item.review_status] ?? 0) + 1;
|
|
1312
|
+
summary[item.suggested_classification] = (summary[item.suggested_classification] ?? 0) + 1;
|
|
1313
|
+
}
|
|
1314
|
+
return summary;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function attachRefactoringGraphContexts(opportunities, graphIndex) {
|
|
1318
|
+
return opportunities.map((opportunity) => ({
|
|
1319
|
+
...opportunity,
|
|
1320
|
+
graph_context: buildGraphContextForFiles(opportunity.target_files ?? [], graphIndex)
|
|
1321
|
+
}));
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function buildRouteExamples(routes) {
|
|
1325
|
+
return routes.slice(0, 10).map((route) => ({
|
|
1326
|
+
file: route.file,
|
|
1327
|
+
route_path: route.route_path,
|
|
1328
|
+
classification: route.classification,
|
|
1329
|
+
protection_status: route.protection?.status ?? 'unknown',
|
|
1330
|
+
risk_hints: route.risk_hints ?? []
|
|
1331
|
+
}));
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function buildGates(findings) {
|
|
1335
|
+
const hasCritical = findings.some((finding) => finding.severity === 'Critical');
|
|
1336
|
+
const hasMediumOrHigher = findings.some((finding) => ['Critical', 'High', 'Medium'].includes(finding.severity));
|
|
1337
|
+
return [{
|
|
1338
|
+
id: 'production-readiness',
|
|
1339
|
+
status: hasCritical ? 'block' : hasMediumOrHigher ? 'needs_review' : 'pass',
|
|
1340
|
+
reason: hasCritical
|
|
1341
|
+
? '公開前に必ず解消すべき項目がある'
|
|
1342
|
+
: hasMediumOrHigher
|
|
1343
|
+
? '文脈品質または適用チェックに確認が必要な項目がある'
|
|
1344
|
+
: '重大な確認項目は検出されていない'
|
|
1345
|
+
}];
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function renderSummary({ runId, evidence, findings }) {
|
|
1349
|
+
const profile = evidence.architecture_profile ?? {};
|
|
1350
|
+
const applicableChecks = evidence.check_catalog?.applicable_checks ?? [];
|
|
1351
|
+
const runtime = formatRuntimeSummary(evidence.toolchain);
|
|
1352
|
+
return `# VibePro 診断サマリー
|
|
1353
|
+
|
|
1354
|
+
| 項目 | 内容 |
|
|
1355
|
+
|------|------|
|
|
1356
|
+
| Run ID | ${runId} |
|
|
1357
|
+
| Story | ${evidence.story.title} |
|
|
1358
|
+
| Story ID | ${evidence.story_id} |
|
|
1359
|
+
| VibePro Runtime | ${runtime} |
|
|
1360
|
+
| 種別 | ${profile.app_type ?? 'unknown'} |
|
|
1361
|
+
| 描画方式 | ${profile.rendering ?? '-'} |
|
|
1362
|
+
| 適用チェック | ${applicableChecks.join(', ') || '-'} |
|
|
1363
|
+
| graphify nodes | ${evidence.graphify.node_count} |
|
|
1364
|
+
| graphify edges | ${evidence.graphify.edge_count} |
|
|
1365
|
+
| 共通スキャン対象 | ${evidence.static_site.scanned_files}件 |
|
|
1366
|
+
| 秘密情報候補 | ${formatRiskCount(evidence.static_site.secret_hits, evidence.static_site.risk_summary?.secret_hits)} |
|
|
1367
|
+
| XSSリスク候補 | ${formatRiskCount(evidence.static_site.xss_risk_hits, evidence.static_site.risk_summary?.xss_risk_hits)} |
|
|
1368
|
+
| UI旧トークン候補 | ${formatRiskCount(evidence.component_style?.legacy_style_hits ?? [], evidence.component_style?.risk_summary?.legacy_style_hits)} |
|
|
1369
|
+
| UI操作信頼性候補 | ${formatRiskCount(evidence.component_style?.interaction_reliability_hits ?? [], evidence.component_style?.risk_summary?.interaction_reliability_hits)} |
|
|
1370
|
+
| UIコンポーネント種別 | ${(evidence.component_style?.component_kinds ?? []).join(', ') || '-'} |
|
|
1371
|
+
| Gesture Interaction Gate | ${evidence.gesture_interaction?.status ?? 'not_generated'} |
|
|
1372
|
+
| Gesture Interaction候補 | ${formatRiskCount([
|
|
1373
|
+
...(evidence.gesture_interaction?.touch_action_hits ?? []),
|
|
1374
|
+
...(evidence.gesture_interaction?.overlay_pointer_hits ?? []),
|
|
1375
|
+
...(evidence.gesture_interaction?.drag_tap_hits ?? []),
|
|
1376
|
+
...(evidence.gesture_interaction?.carousel_hits ?? []),
|
|
1377
|
+
...(evidence.gesture_interaction?.map_marker_hits ?? [])
|
|
1378
|
+
])} |
|
|
1379
|
+
| Terminal Link契約 | ${evidence.terminal_link_contracts?.status ?? 'not_generated'} |
|
|
1380
|
+
| Terminal Link候補 | ${formatRiskCount([
|
|
1381
|
+
...(evidence.terminal_link_contracts?.dot_directory_link_hits ?? []),
|
|
1382
|
+
...(evidence.terminal_link_contracts?.wrapped_terminal_link_hits ?? []),
|
|
1383
|
+
...(evidence.terminal_link_contracts?.dot_directory_tree_hits ?? []),
|
|
1384
|
+
...(evidence.terminal_link_contracts?.image_preview_extension_hits ?? [])
|
|
1385
|
+
])} |
|
|
1386
|
+
| Flow Design Gate | ${evidence.flow_design?.status ?? 'not_generated'} |
|
|
1387
|
+
| Flow Design UI走査 | ${evidence.flow_design?.summary?.scanned_ui_files ?? 0}件 |
|
|
1388
|
+
| Flow Design検出候補 | ${flowDesignHitCount(evidence.flow_design)}件 |
|
|
1389
|
+
| 重いdev script候補 | ${formatRiskCount(evidence.local_dev?.heavy_dev_scripts ?? [], evidence.local_dev?.risk_summary?.heavy_dev_scripts)} |
|
|
1390
|
+
| runtime probe plan | ${evidence.local_dev?.runtime_probe_plan?.status ?? '-'} (${evidence.local_dev?.runtime_probe_plan?.commands?.length ?? 0} commands) |
|
|
1391
|
+
| DB未ページング候補 | ${formatRiskCount(evidence.database_access?.unbounded_find_many ?? [], evidence.database_access?.risk_summary?.unbounded_find_many)} |
|
|
1392
|
+
| 認可前bulk DB候補 | ${formatRiskCount(evidence.code_quality?.authorization_order_risks ?? [], evidence.code_quality?.risk_summary?.authorization_order_risks)} |
|
|
1393
|
+
| 重複query形状候補 | ${formatRiskCount(evidence.code_quality?.duplicate_query_shapes ?? [], evidence.code_quality?.risk_summary?.duplicate_query_shapes)} |
|
|
1394
|
+
| 責務混在候補 | ${formatRiskCount(evidence.code_quality?.responsibility_hotspots ?? [], evidence.code_quality?.risk_summary?.responsibility_hotspots)} |
|
|
1395
|
+
| リファクタリング機会 | ${evidence.refactoring_opportunities?.length ?? 0}件 |
|
|
1396
|
+
| リファクタリングcampaign | ${evidence.refactoring_campaigns?.length ?? 0}件 |
|
|
1397
|
+
| API route | ${evidence.api_boundary?.route_count ?? 0}件 |
|
|
1398
|
+
| Network Contract | ${evidence.network_contracts?.status ?? 'not_generated'} |
|
|
1399
|
+
| API client call | ${evidence.network_contracts?.api_client_call_count ?? 0}件 |
|
|
1400
|
+
| API route欠落 | ${evidence.network_contracts?.missing_routes?.length ?? 0}件 |
|
|
1401
|
+
| Requirement Gate | ${evidence.requirement_consistency?.status ?? 'not_generated'} |
|
|
1402
|
+
| 要件不変条件 | ${evidence.requirement_consistency?.summary?.invariant_count ?? 0}件 |
|
|
1403
|
+
| シナリオ確認候補 | ${evidence.requirement_consistency?.summary?.scenario_gap_count ?? 0}件 |
|
|
1404
|
+
| 要件矛盾候補 | ${evidence.requirement_consistency?.summary?.contradiction_count ?? 0}件 |
|
|
1405
|
+
| Performance Metrics | ${evidence.performance_evidence?.metric_count ?? 0}件 |
|
|
1406
|
+
| Performance Comparable | ${evidence.performance_evidence?.comparable_count ?? 0}件 |
|
|
1407
|
+
| Performance Unknown | ${evidence.performance_evidence?.not_comparable_count ?? 0}件 |
|
|
1408
|
+
| 検出事項 | ${findings.length}件 |
|
|
1409
|
+
|
|
1410
|
+
## アーキテクチャView
|
|
1411
|
+
|
|
1412
|
+
${renderArchitectureViewTable(profile)}
|
|
1413
|
+
|
|
1414
|
+
## API境界
|
|
1415
|
+
|
|
1416
|
+
${renderApiBoundarySummary(evidence.api_boundary)}
|
|
1417
|
+
|
|
1418
|
+
## Network Contract
|
|
1419
|
+
|
|
1420
|
+
${renderNetworkContractSummary(evidence.network_contracts)}
|
|
1421
|
+
|
|
1422
|
+
## ゲート状態
|
|
1423
|
+
|
|
1424
|
+
${evidence.gates.map((gate) => `- ${gate.id}: ${gate.status} - ${gate.reason}`).join('\n')}
|
|
1425
|
+
|
|
1426
|
+
## Requirement Consistency
|
|
1427
|
+
|
|
1428
|
+
${renderRequirementConsistencySummary(evidence.requirement_consistency)}
|
|
1429
|
+
|
|
1430
|
+
## Flow Design
|
|
1431
|
+
|
|
1432
|
+
${renderFlowDesignSummary(evidence.flow_design)}
|
|
1433
|
+
|
|
1434
|
+
## Performance Evidence
|
|
1435
|
+
|
|
1436
|
+
${renderPerformanceEvidenceSummary(evidence.performance_evidence)}
|
|
1437
|
+
|
|
1438
|
+
## 主な検出事項
|
|
1439
|
+
|
|
1440
|
+
${findings.length === 0 ? '- なし' : findings.map((finding) => `- ${finding.id}: ${finding.title}(${finding.severity})`).join('\n')}
|
|
1441
|
+
|
|
1442
|
+
## 文脈品質ノート
|
|
1443
|
+
|
|
1444
|
+
${renderGraphQualityNotices(evidence.graphify?.quality_notices)}
|
|
1445
|
+
|
|
1446
|
+
## 診断レビュー
|
|
1447
|
+
|
|
1448
|
+
${renderFindingReviewSummary(evidence.finding_review)}
|
|
1449
|
+
|
|
1450
|
+
## リファクタリング差分
|
|
1451
|
+
|
|
1452
|
+
${renderRefactoringDeltaCompact(evidence.refactoring_delta)}
|
|
1453
|
+
|
|
1454
|
+
## 次アクション候補
|
|
1455
|
+
|
|
1456
|
+
${renderActionCandidates(evidence.action_candidates)}
|
|
1457
|
+
`;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function formatRuntimeSummary(toolchain) {
|
|
1461
|
+
if (!toolchain) return '-';
|
|
1462
|
+
const name = toolchain.package?.name ?? 'vibepro';
|
|
1463
|
+
const version = toolchain.package?.version ?? 'unknown';
|
|
1464
|
+
const commit = toolchain.source_git?.commit ? String(toolchain.source_git.commit).slice(0, 12) : 'no-git';
|
|
1465
|
+
const dirty = toolchain.source_git?.dirty == null ? '-' : String(toolchain.source_git.dirty);
|
|
1466
|
+
return `${name}@${version} commit=${commit} dirty=${dirty}`;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function renderRequirementConsistencySummary(requirement) {
|
|
1470
|
+
if (!requirement) return '- 未生成';
|
|
1471
|
+
const gaps = (requirement.scenario_gaps ?? []).slice(0, 5)
|
|
1472
|
+
.map((item) => `- Scenario Gap: ${item.detail}`)
|
|
1473
|
+
.join('\n');
|
|
1474
|
+
const contradictions = (requirement.contradictions ?? []).slice(0, 5)
|
|
1475
|
+
.map((item) => `- Potential Contradiction: ${item.detail}`)
|
|
1476
|
+
.join('\n');
|
|
1477
|
+
return [
|
|
1478
|
+
`- Status: ${requirement.status}`,
|
|
1479
|
+
`- Invariants: ${requirement.summary?.invariant_count ?? 0}`,
|
|
1480
|
+
`- Scenario Gaps: ${requirement.summary?.scenario_gap_count ?? 0}`,
|
|
1481
|
+
`- Contradictions: ${requirement.summary?.contradiction_count ?? 0}`,
|
|
1482
|
+
gaps,
|
|
1483
|
+
contradictions
|
|
1484
|
+
].filter(Boolean).join('\n');
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function renderFlowDesignSummary(flowDesign) {
|
|
1488
|
+
if (!flowDesign) return '- 未生成';
|
|
1489
|
+
return [
|
|
1490
|
+
`- Status: ${flowDesign.status}`,
|
|
1491
|
+
`- UI Files: ${flowDesign.summary?.scanned_ui_files ?? 0}`,
|
|
1492
|
+
`- Silent Noops: ${flowDesign.summary?.silent_noop_count ?? 0}`,
|
|
1493
|
+
`- Selection Side Effects: ${flowDesign.summary?.selection_side_effect_count ?? 0}`,
|
|
1494
|
+
`- Question Dead Ends: ${flowDesign.summary?.question_dead_end_count ?? 0}`,
|
|
1495
|
+
`- Dead UI States: ${flowDesign.summary?.dead_ui_state_count ?? 0}`,
|
|
1496
|
+
`- Value Alignment: ${flowDesign.summary?.value_alignment_count ?? 0}`
|
|
1497
|
+
].join('\n');
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function flowDesignHitCount(flowDesign) {
|
|
1501
|
+
if (!flowDesign) return 0;
|
|
1502
|
+
return [
|
|
1503
|
+
flowDesign.silent_noop_hits,
|
|
1504
|
+
flowDesign.ambiguous_primary_action_hits,
|
|
1505
|
+
flowDesign.selection_side_effect_hits,
|
|
1506
|
+
flowDesign.question_dead_end_hits,
|
|
1507
|
+
flowDesign.dead_ui_state_hits,
|
|
1508
|
+
flowDesign.value_alignment_hits
|
|
1509
|
+
].reduce((total, hits) => total + (hits?.length ?? 0), 0);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function renderGraphQualityNotices(notices) {
|
|
1513
|
+
if (!Array.isArray(notices) || notices.length === 0) return '- なし';
|
|
1514
|
+
return notices.map((notice) => `- ${notice.id}: ${notice.title}(${notice.level})`).join('\n');
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function renderApiBoundarySummary(apiBoundary) {
|
|
1518
|
+
if (!apiBoundary) return '- api-boundary は適用されていない';
|
|
1519
|
+
const summary = apiBoundary.summary ?? {};
|
|
1520
|
+
const rows = Object.entries(summary)
|
|
1521
|
+
.map(([classification, count]) => `| ${classification} | ${count} |`)
|
|
1522
|
+
.join('\n');
|
|
1523
|
+
const protectionRows = Object.entries(apiBoundary.protection_summary ?? {})
|
|
1524
|
+
.map(([status, count]) => `| ${status} | ${count} |`)
|
|
1525
|
+
.join('\n');
|
|
1526
|
+
return `### 分類別
|
|
1527
|
+
|
|
1528
|
+
| 分類 | 件数 |
|
|
1529
|
+
|------|------|
|
|
1530
|
+
${rows || '| - | 0 |'}
|
|
1531
|
+
|
|
1532
|
+
### 保護状態別
|
|
1533
|
+
|
|
1534
|
+
| 保護状態 | 件数 |
|
|
1535
|
+
|----------|------|
|
|
1536
|
+
${protectionRows || '| - | 0 |'}`;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function renderNetworkContractSummary(networkContracts) {
|
|
1540
|
+
if (!networkContracts) return '- 未生成';
|
|
1541
|
+
const missing = networkContracts.missing_routes ?? [];
|
|
1542
|
+
const dynamic = networkContracts.dynamic_calls ?? [];
|
|
1543
|
+
const replacements = networkContracts.high_risk_replacements ?? [];
|
|
1544
|
+
const rows = [
|
|
1545
|
+
...missing.slice(0, 8).map((item) => ['missing_route', item.api_path, item.file, item.line ?? '-']),
|
|
1546
|
+
...dynamic.slice(0, 5).map((item) => ['dynamic_path', item.api_path, item.file, item.line ?? '-']),
|
|
1547
|
+
...replacements.slice(0, 5).map((item) => ['server_function_replaced', item.introduced_api_calls?.map((call) => call.api_path?.value).join(', ') || '-', item.file, '-'])
|
|
1548
|
+
];
|
|
1549
|
+
return [
|
|
1550
|
+
`- Status: ${networkContracts.status}`,
|
|
1551
|
+
`- Routes: ${networkContracts.route_count ?? 0}`,
|
|
1552
|
+
`- API client calls: ${networkContracts.api_client_call_count ?? 0}`,
|
|
1553
|
+
`- Missing routes: ${missing.length}`,
|
|
1554
|
+
`- Dynamic calls: ${dynamic.length}`,
|
|
1555
|
+
`- Server function replacements: ${replacements.length}`,
|
|
1556
|
+
rows.length > 0
|
|
1557
|
+
? renderSimpleMarkdownTable(['Kind', 'API', 'File', 'Line'], rows)
|
|
1558
|
+
: '- 問題なし'
|
|
1559
|
+
].join('\n');
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
function renderSimpleMarkdownTable(headers, rows) {
|
|
1563
|
+
return [
|
|
1564
|
+
`| ${headers.join(' | ')} |`,
|
|
1565
|
+
`| ${headers.map(() => '---').join(' | ')} |`,
|
|
1566
|
+
...rows.map((row) => `| ${row.map((cell) => String(cell ?? '-').replace(/\|/g, '\\|')).join(' | ')} |`)
|
|
1567
|
+
].join('\n');
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function renderArchitectureViewTable(profile) {
|
|
1571
|
+
const views = profile.views ?? {};
|
|
1572
|
+
return `| View | 判定 |
|
|
1573
|
+
|------|------|
|
|
1574
|
+
| Structure | ${[
|
|
1575
|
+
...(views.structure?.containers ?? []),
|
|
1576
|
+
...(views.structure?.components ?? []),
|
|
1577
|
+
...(views.structure?.frameworks ?? [])
|
|
1578
|
+
].join(', ') || '-'} |
|
|
1579
|
+
| Runtime | ${[
|
|
1580
|
+
`${views.runtime?.entrypoints?.length ?? 0} entrypoints`,
|
|
1581
|
+
...(views.runtime?.server_boundaries ?? [])
|
|
1582
|
+
].join(', ')} |
|
|
1583
|
+
| Data | ${[
|
|
1584
|
+
...(views.data?.stores ?? []),
|
|
1585
|
+
...(views.data?.access_patterns ?? [])
|
|
1586
|
+
].join(', ') || '-'} |
|
|
1587
|
+
| Security | ${[
|
|
1588
|
+
`${views.security?.auth_boundaries?.length ?? 0} auth boundaries`,
|
|
1589
|
+
`${views.security?.secret_files?.length ?? 0} secret files`
|
|
1590
|
+
].join(', ')} |
|
|
1591
|
+
| Deployment | ${(views.deployment?.targets ?? []).join(', ') || '-'} |
|
|
1592
|
+
| Quality | ${[
|
|
1593
|
+
...(views.quality?.test_tools ?? []),
|
|
1594
|
+
...(views.quality?.ci ?? [])
|
|
1595
|
+
].join(', ') || '-'} |`;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function renderArchitectureProfile({ runId, profile, checkCatalog }) {
|
|
1599
|
+
return `# 構造プロファイル
|
|
1600
|
+
|
|
1601
|
+
| 項目 | 内容 |
|
|
1602
|
+
|------|------|
|
|
1603
|
+
| Run ID | ${runId} |
|
|
1604
|
+
| 種別 | ${profile.app_type} |
|
|
1605
|
+
| 描画方式 | ${profile.rendering ?? '-'} |
|
|
1606
|
+
| パッケージ管理 | ${profile.package_manager ?? '-'} |
|
|
1607
|
+
| 言語 | ${profile.languages.length === 0 ? '-' : profile.languages.join(', ')} |
|
|
1608
|
+
| API route | ${profile.has_api_routes ? 'あり' : 'なし'} |
|
|
1609
|
+
| DB | ${profile.has_database ? profile.database.join(', ') || 'あり' : 'なし'} |
|
|
1610
|
+
| 認証 | ${profile.has_auth ? profile.auth.join(', ') || 'あり' : 'なし'} |
|
|
1611
|
+
| 配信 | ${profile.deployment.length === 0 ? '-' : profile.deployment.join(', ')} |
|
|
1612
|
+
|
|
1613
|
+
## View
|
|
1614
|
+
|
|
1615
|
+
${renderArchitectureViewTable(profile)}
|
|
1616
|
+
|
|
1617
|
+
## 適用チェック
|
|
1618
|
+
|
|
1619
|
+
${checkCatalog.applicable_checks.map((check) => `- ${check}`).join('\n')}
|
|
1620
|
+
|
|
1621
|
+
## 根拠
|
|
1622
|
+
|
|
1623
|
+
${profile.evidence.length === 0 ? '- なし' : profile.evidence.map((item) => `- ${item.kind}: ${item.file ?? '-'} ${item.detail ?? ''}`.trim()).join('\n')}
|
|
1624
|
+
`;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function renderStaticSiteCheck({ runId, staticSite, profile }) {
|
|
1628
|
+
const title = profile?.app_type === 'static_site' ? '静的サイト診断結果' : '共通スキャン結果';
|
|
1629
|
+
return `# ${title}
|
|
1630
|
+
|
|
1631
|
+
| 項目 | 内容 |
|
|
1632
|
+
|------|------|
|
|
1633
|
+
| Run ID | ${runId} |
|
|
1634
|
+
| index.html | ${staticSite.has_index_html ? 'あり' : 'なし'} |
|
|
1635
|
+
| 走査ファイル | ${staticSite.scanned_files}件 |
|
|
1636
|
+
| 秘密情報候補 | ${formatRiskCount(staticSite.secret_hits, staticSite.risk_summary?.secret_hits)} |
|
|
1637
|
+
| XSSリスク候補 | ${formatRiskCount(staticSite.xss_risk_hits, staticSite.risk_summary?.xss_risk_hits)} |
|
|
1638
|
+
| 外部リソース | ${staticSite.external_resources.length}件 |
|
|
1639
|
+
| 非静的ファイル候補 | ${staticSite.non_static_files.length}件 |
|
|
1640
|
+
|
|
1641
|
+
## 秘密情報候補
|
|
1642
|
+
|
|
1643
|
+
${staticSite.secret_hits.length === 0 ? '- なし' : staticSite.secret_hits.map((hit) => `- ${hit.file}:${hit.line} ${hit.kind} source_kind=${hit.source_kind ?? '-'} confidence=${hit.confidence ?? '-'} gate_effect=${hit.gate_effect ?? '-'} \`${hit.excerpt}\``).join('\n')}
|
|
1644
|
+
|
|
1645
|
+
## XSSリスク候補
|
|
1646
|
+
|
|
1647
|
+
${staticSite.xss_risk_hits.length === 0 ? '- なし' : staticSite.xss_risk_hits.map((hit) => `- ${hit.file}:${hit.line} ${hit.kind} source_kind=${hit.source_kind ?? '-'} confidence=${hit.confidence ?? '-'} gate_effect=${hit.gate_effect ?? '-'} \`${hit.excerpt}\``).join('\n')}
|
|
1648
|
+
|
|
1649
|
+
## 外部リソース
|
|
1650
|
+
|
|
1651
|
+
${staticSite.external_resources.length === 0 ? '- なし' : staticSite.external_resources.map((resource) => `- ${resource.file}:${resource.line} ${resource.tag} ${resource.url}`).join('\n')}
|
|
1652
|
+
|
|
1653
|
+
## 非静的ファイル候補
|
|
1654
|
+
|
|
1655
|
+
${staticSite.non_static_files.length === 0 ? '- なし' : staticSite.non_static_files.map((file) => `- ${file.file} (${file.extension})`).join('\n')}
|
|
1656
|
+
`;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function renderComponentStyleCheck({ runId, componentStyle }) {
|
|
1660
|
+
if (!componentStyle) {
|
|
1661
|
+
return `# コンポーネントスタイル診断結果
|
|
1662
|
+
|
|
1663
|
+
| 項目 | 内容 |
|
|
1664
|
+
|------|------|
|
|
1665
|
+
| Run ID | ${runId} |
|
|
1666
|
+
| 状態 | component-style は適用されていない |
|
|
1667
|
+
`;
|
|
1668
|
+
}
|
|
1669
|
+
return `# コンポーネントスタイル診断結果
|
|
1670
|
+
|
|
1671
|
+
| 項目 | 内容 |
|
|
1672
|
+
|------|------|
|
|
1673
|
+
| Run ID | ${runId} |
|
|
1674
|
+
| 走査ファイル | ${componentStyle.scanned_files}件 |
|
|
1675
|
+
| 検出コンポーネント種別 | ${componentStyle.component_kinds.join(', ') || '-'} |
|
|
1676
|
+
| 旧トークン候補 | ${formatRiskCount(componentStyle.legacy_style_hits, componentStyle.risk_summary?.legacy_style_hits)} |
|
|
1677
|
+
| 操作信頼性候補 | ${formatRiskCount(componentStyle.interaction_reliability_hits ?? [], componentStyle.risk_summary?.interaction_reliability_hits)} |
|
|
1678
|
+
| design-system marker | ${componentStyle.design_system_markers.length}件 |
|
|
1679
|
+
| 置換確認可能 | ${componentStyle.coverage?.replacement_observable ? 'yes' : 'no'} |
|
|
1680
|
+
|
|
1681
|
+
## コンポーネントInventory
|
|
1682
|
+
|
|
1683
|
+
${componentStyle.component_inventory.length === 0 ? '- なし' : componentStyle.component_inventory.slice(0, 80).map((item) => `- ${item.file}:${item.line} ${item.kind} \`${item.excerpt}\``).join('\n')}
|
|
1684
|
+
|
|
1685
|
+
## 旧トークン候補
|
|
1686
|
+
|
|
1687
|
+
${componentStyle.legacy_style_hits.length === 0 ? '- なし' : componentStyle.legacy_style_hits.map((hit) => `- ${hit.file}:${hit.line} ${hit.kind} token=${hit.token} confidence=${hit.confidence} gate_effect=${hit.gate_effect} \`${hit.excerpt}\``).join('\n')}
|
|
1688
|
+
|
|
1689
|
+
## 操作信頼性候補
|
|
1690
|
+
|
|
1691
|
+
${(componentStyle.interaction_reliability_hits ?? []).length === 0 ? '- なし' : componentStyle.interaction_reliability_hits.map((hit) => `- ${hit.file}:${hit.line} ${hit.kind} selector=${hit.selector} confidence=${hit.confidence} gate_effect=${hit.gate_effect} \`${hit.excerpt}\``).join('\n')}
|
|
1692
|
+
|
|
1693
|
+
## design-system marker
|
|
1694
|
+
|
|
1695
|
+
${componentStyle.design_system_markers.length === 0 ? '- なし' : componentStyle.design_system_markers.slice(0, 80).map((marker) => `- ${marker.file}:${marker.line} ${marker.marker} \`${marker.excerpt}\``).join('\n')}
|
|
1696
|
+
`;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function renderRiskRegister({ runId, findings, apiBoundary, actionCandidates, findingReview }) {
|
|
1700
|
+
return `# VibePro リスク台帳
|
|
1701
|
+
|
|
1702
|
+
| 項目 | 内容 |
|
|
1703
|
+
|------|------|
|
|
1704
|
+
| Run ID | ${runId} |
|
|
1705
|
+
| 検出リスク | ${findings.length}件 |
|
|
1706
|
+
|
|
1707
|
+
| ID | カテゴリ | リスク概要 | 深刻度 | 推奨対応 |
|
|
1708
|
+
|----|----------|------------|--------|----------|
|
|
1709
|
+
${findings.length === 0 ? '| - | - | 検出なし | - | - |' : findings.map((finding) => `| ${finding.id} | ${finding.category} | ${finding.title} | ${finding.severity} | ${finding.recommendation} |`).join('\n')}
|
|
1710
|
+
|
|
1711
|
+
## API境界の保護状態
|
|
1712
|
+
|
|
1713
|
+
${renderApiProtectionStateTable(apiBoundary)}
|
|
1714
|
+
|
|
1715
|
+
## 診断レビュー分類
|
|
1716
|
+
|
|
1717
|
+
${renderFindingReviewTable(findingReview)}
|
|
1718
|
+
|
|
1719
|
+
## 次アクション候補
|
|
1720
|
+
|
|
1721
|
+
${renderActionCandidates(actionCandidates)}
|
|
1722
|
+
`;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
function renderFindingReview({ runId, findingReview }) {
|
|
1726
|
+
return `# VibePro 診断レビュー
|
|
1727
|
+
|
|
1728
|
+
| 項目 | 内容 |
|
|
1729
|
+
|------|------|
|
|
1730
|
+
| Run ID | ${runId} |
|
|
1731
|
+
| Status | ${findingReview?.status ?? 'unknown'} |
|
|
1732
|
+
| Total | ${findingReview?.summary?.total ?? 0}件 |
|
|
1733
|
+
| Unreviewed | ${findingReview?.summary?.unreviewed ?? 0}件 |
|
|
1734
|
+
| Suggested implementation_gap | ${findingReview?.summary?.implementation_gap ?? 0}件 |
|
|
1735
|
+
| Suggested detector_gap | ${findingReview?.summary?.detector_gap ?? 0}件 |
|
|
1736
|
+
|
|
1737
|
+
${findingReview?.policy ?? ''}
|
|
1738
|
+
|
|
1739
|
+
Allowed classifications: true_positive, false_positive, false_negative, detector_gap, implementation_gap
|
|
1740
|
+
|
|
1741
|
+
## 分類表
|
|
1742
|
+
|
|
1743
|
+
${renderFindingReviewTable(findingReview)}
|
|
1744
|
+
|
|
1745
|
+
## 確認観点
|
|
1746
|
+
|
|
1747
|
+
${renderFindingReviewQuestions(findingReview)}
|
|
1748
|
+
`;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
function renderFindingReviewSummary(findingReview) {
|
|
1752
|
+
const summary = findingReview?.summary ?? {};
|
|
1753
|
+
return `- Status: ${findingReview?.status ?? 'unknown'}
|
|
1754
|
+
- 未レビュー: ${summary.unreviewed ?? 0}件
|
|
1755
|
+
- suggested implementation_gap: ${summary.implementation_gap ?? 0}件
|
|
1756
|
+
- suggested detector_gap: ${summary.detector_gap ?? 0}件
|
|
1757
|
+
- 正本: finding-review.md と evidence.json の finding_review`;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function renderFindingReviewTable(findingReview) {
|
|
1761
|
+
const items = Array.isArray(findingReview?.items) ? findingReview.items : [];
|
|
1762
|
+
if (items.length === 0) return '| Finding | Status | Suggested | Action | Rationale |\n|---------|--------|-----------|--------|-----------|\n| - | - | - | - | - |';
|
|
1763
|
+
return `| Finding | Status | Suggested | Action | Rationale |
|
|
1764
|
+
|---------|--------|-----------|--------|-----------|
|
|
1765
|
+
${items.map((item) => `| ${item.finding_id} | ${item.review_status} | ${item.suggested_classification} | ${item.action_candidate_id ?? '-'} | ${item.rationale} |`).join('\n')}`;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
function renderFindingReviewQuestions(findingReview) {
|
|
1769
|
+
const items = Array.isArray(findingReview?.items) ? findingReview.items : [];
|
|
1770
|
+
if (items.length === 0) return '- なし';
|
|
1771
|
+
return items.map((item) => `### ${item.finding_id}
|
|
1772
|
+
|
|
1773
|
+
${item.review_questions.map((question) => `- ${question}`).join('\n')}`).join('\n\n');
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
function renderActionCandidates(candidates) {
|
|
1777
|
+
const items = Array.isArray(candidates) ? candidates : [];
|
|
1778
|
+
if (items.length === 0) return '- なし';
|
|
1779
|
+
return `| ID | 対応する検出事項 | 候補 | 対象 | Impact | Community | 読むファイル | 方針 |
|
|
1780
|
+
|----|------------------|------|------|--------|-----------|------------|------|
|
|
1781
|
+
${items.map((candidate) => `| ${candidate.id} | ${candidate.finding_id} | ${candidate.title} | ${candidate.target_count}件 | ${formatGraphImpact(candidate.graph_context)} | ${formatGraphCommunities(candidate.graph_context)} | ${formatReadFirstFiles(candidate.implementation_plan)} | ${candidate.execution_policy} / mutates_repository=${candidate.mutates_repository} |`).join('\n')}
|
|
1782
|
+
|
|
1783
|
+
${renderImplementationPlans(items)}`;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function formatGraphImpact(graphContext) {
|
|
1787
|
+
if (!graphContext) return '-';
|
|
1788
|
+
return `${graphContext.impact_score ?? 0} (${graphContext.related_edge_count ?? 0} edges)`;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function formatGraphCommunities(graphContext) {
|
|
1792
|
+
const communities = graphContext?.affected_communities ?? [];
|
|
1793
|
+
if (communities.length === 0) return '-';
|
|
1794
|
+
return communities
|
|
1795
|
+
.slice(0, 3)
|
|
1796
|
+
.map((community) => {
|
|
1797
|
+
const scope = (community.route_count ?? 0) > 0
|
|
1798
|
+
? `route: ${community.route_count}`
|
|
1799
|
+
: `file: ${community.file_count ?? 0}`;
|
|
1800
|
+
return `${community.id}(${scope}, node: ${community.node_count}, edge: ${community.edge_count})`;
|
|
1801
|
+
})
|
|
1802
|
+
.join(', ');
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function formatReadFirstFiles(implementationPlan) {
|
|
1806
|
+
const files = implementationPlan?.read_first_files ?? [];
|
|
1807
|
+
if (files.length === 0) return '-';
|
|
1808
|
+
return selectRepresentativeReadFirstFiles(files, implementationPlan?.pre_fix_briefing).map((item) => item.file).join('<br>');
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function selectRepresentativeReadFirstFiles(files, briefing) {
|
|
1812
|
+
const selected = [];
|
|
1813
|
+
const seen = new Set();
|
|
1814
|
+
const add = (item) => {
|
|
1815
|
+
if (!item || seen.has(item.file)) return;
|
|
1816
|
+
seen.add(item.file);
|
|
1817
|
+
selected.push(item);
|
|
1818
|
+
};
|
|
1819
|
+
const helpers = briefing?.auth_helpers ?? [];
|
|
1820
|
+
const helperFiles = new Set(helpers.map((helper) => helper.file));
|
|
1821
|
+
const hasSignatureHelper = helpers.some((helper) => helper.category === 'signature');
|
|
1822
|
+
add(files[0]);
|
|
1823
|
+
add(files.find((item) => helperFiles.has(item.file)));
|
|
1824
|
+
add(files.find((item) => item.reason.includes('graphify hub') && helperFiles.has(item.file)));
|
|
1825
|
+
if (!hasSignatureHelper) add(files.find((item) => item.reason.includes('middleware')));
|
|
1826
|
+
for (const item of files) add(item);
|
|
1827
|
+
return selected.slice(0, 3);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function renderImplementationPlans(candidates) {
|
|
1831
|
+
const items = candidates.filter((candidate) => candidate.implementation_plan);
|
|
1832
|
+
if (items.length === 0) return '';
|
|
1833
|
+
return `### 実装手順
|
|
1834
|
+
|
|
1835
|
+
${items.map((candidate) => renderImplementationPlan(candidate)).join('\n\n')}`;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
function renderImplementationPlan(candidate) {
|
|
1839
|
+
const plan = candidate.implementation_plan;
|
|
1840
|
+
return `#### ${candidate.id}: ${candidate.title}
|
|
1841
|
+
|
|
1842
|
+
- 優先度: ${plan.priority}
|
|
1843
|
+
- 理由: ${plan.rationale}
|
|
1844
|
+
- 読むファイル: ${plan.read_first_files.length === 0 ? '-' : plan.read_first_files.map((item) => `${item.file}(${item.reason})`).join(', ')}
|
|
1845
|
+
|
|
1846
|
+
${renderPreFixBriefing(plan.pre_fix_briefing)}
|
|
1847
|
+
|
|
1848
|
+
${plan.steps.map((step, index) => `${index + 1}. ${step.title}: ${step.detail}`).join('\n')}
|
|
1849
|
+
|
|
1850
|
+
完了条件:
|
|
1851
|
+
${plan.acceptance_criteria.map((item) => `- ${item}`).join('\n')}`;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
function renderPreFixBriefing(briefing) {
|
|
1855
|
+
if (!briefing) return '';
|
|
1856
|
+
if (briefing.opportunity) {
|
|
1857
|
+
return `修正前ブリーフィング:
|
|
1858
|
+
- リファクタリング機会: ${briefing.opportunity.id} / ${briefing.opportunity.refactoring_intent}
|
|
1859
|
+
- Campaign: ${briefing.campaign?.id ?? '-'} / rank=${briefing.campaign?.rank ?? '-'}
|
|
1860
|
+
- 推奨抽象化: ${briefing.opportunity.suggested_abstraction?.label ?? '-'}
|
|
1861
|
+
- 対象ファイル: ${briefing.target_files?.slice(0, 5).join(', ') || '-'}
|
|
1862
|
+
- 推奨方針: ${briefing.recommended_strategy?.id ?? '-'} - ${briefing.recommended_strategy?.reason ?? '-'}
|
|
1863
|
+
- 方針: ${briefing.strategy_options?.map((option) => option.label).join(' / ') || '-'}`;
|
|
1864
|
+
}
|
|
1865
|
+
return `修正前ブリーフィング:
|
|
1866
|
+
- 現在の境界: middleware excludes_api=${briefing.current_boundary?.middleware?.excludes_api ?? false}, route protection=${formatInlineSummary(briefing.current_boundary?.route_protection ?? {})}
|
|
1867
|
+
- 認証/署名候補: ${formatAuthHelpers(briefing.auth_helpers)}
|
|
1868
|
+
- 対象route: ${briefing.target_routes?.slice(0, 5).map((route) => `${route.route_path} (${route.methods.join(', ') || '-'})`).join(', ') || '-'}
|
|
1869
|
+
- 推奨方針: ${briefing.recommended_strategy?.id ?? '-'} - ${briefing.recommended_strategy?.reason ?? '-'}
|
|
1870
|
+
- 方針: ${briefing.strategy_options?.map((option) => option.label).join(' / ') || '-'}`;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
function formatAuthHelpers(helpers = []) {
|
|
1874
|
+
if (helpers.length === 0) return '-';
|
|
1875
|
+
return helpers
|
|
1876
|
+
.slice(0, 5)
|
|
1877
|
+
.map((helper) => `${formatHelperCategory(helper.category)}${helper.file}${helper.functions.length > 0 ? `:${helper.functions.slice(0, 3).join(',')}` : ''}`)
|
|
1878
|
+
.join(', ');
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
function formatHelperCategory(category) {
|
|
1882
|
+
const labels = {
|
|
1883
|
+
auth: '認証:',
|
|
1884
|
+
signature: '署名:',
|
|
1885
|
+
environment: '環境:'
|
|
1886
|
+
};
|
|
1887
|
+
return labels[category] ?? '';
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
function renderApiProtectionStateTable(apiBoundary) {
|
|
1891
|
+
if (!apiBoundary) return '- api-boundary は適用されていない';
|
|
1892
|
+
const rows = Object.entries(apiBoundary.protection_summary ?? {})
|
|
1893
|
+
.map(([status, count]) => `| ${status} | ${count} |`)
|
|
1894
|
+
.join('\n');
|
|
1895
|
+
return `| 保護状態 | 件数 |
|
|
1896
|
+
|----------|------|
|
|
1897
|
+
${rows || '| - | 0 |'}`;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
function summarizeProtectionForRoutes(routes) {
|
|
1901
|
+
const summary = {};
|
|
1902
|
+
for (const route of routes) {
|
|
1903
|
+
const status = route.protection?.status ?? 'unknown';
|
|
1904
|
+
summary[status] = (summary[status] ?? 0) + 1;
|
|
1905
|
+
}
|
|
1906
|
+
return summary;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function formatInlineSummary(summary) {
|
|
1910
|
+
const entries = Object.entries(summary);
|
|
1911
|
+
if (entries.length === 0) return '-';
|
|
1912
|
+
return entries.map(([key, count]) => `${key}: ${count}件`).join(', ');
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
function filterGateRelevant(hits = []) {
|
|
1916
|
+
return hits.filter((hit) => hit.gate_effect === 'block' || hit.gate_effect === 'review');
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
function summarizeGateEffects(hits = []) {
|
|
1920
|
+
const summary = { block: 0, review: 0, info: 0 };
|
|
1921
|
+
for (const hit of hits) {
|
|
1922
|
+
if (hit.gate_effect === 'block') summary.block += 1;
|
|
1923
|
+
else if (hit.gate_effect === 'review') summary.review += 1;
|
|
1924
|
+
else summary.info += 1;
|
|
1925
|
+
}
|
|
1926
|
+
return summary;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function formatGateSummary(summary = {}) {
|
|
1930
|
+
return `block: ${summary.block ?? 0}件, review: ${summary.review ?? 0}件, info: ${summary.info ?? 0}件`;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
function formatRiskCount(hits = [], summary = null) {
|
|
1934
|
+
const effectiveSummary = summary ?? summarizeGateEffects(hits);
|
|
1935
|
+
return `${hits.length}件 (${formatGateSummary(effectiveSummary)})`;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
async function readTextIfExists(filePath) {
|
|
1939
|
+
try {
|
|
1940
|
+
return await readFile(filePath, 'utf8');
|
|
1941
|
+
} catch (error) {
|
|
1942
|
+
if (error.code === 'ENOENT') return '';
|
|
1943
|
+
throw error;
|
|
1944
|
+
}
|
|
1945
|
+
}
|