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.
Files changed (89) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +9 -0
  3. package/README.ja.md +448 -0
  4. package/README.md +520 -0
  5. package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
  6. package/bin/vibepro.js +9 -0
  7. package/docs/assets/vibepro-header.png +0 -0
  8. package/package.json +51 -0
  9. package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
  10. package/skills/vibepro-human-review/SKILL.md +73 -0
  11. package/skills/vibepro-story-refactor/SKILL.md +89 -0
  12. package/skills/vibepro-workflow/SKILL.md +139 -0
  13. package/src/agent-harness-map.js +230 -0
  14. package/src/agent-harness-scanner.js +337 -0
  15. package/src/agent-review.js +2180 -0
  16. package/src/api-boundary-scanner.js +452 -0
  17. package/src/architecture-profiler.js +423 -0
  18. package/src/authorization-scoring.js +149 -0
  19. package/src/brainbase-importer.js +534 -0
  20. package/src/change-risk-classifier.js +195 -0
  21. package/src/check-packs.js +605 -0
  22. package/src/checkpoint-manager.js +233 -0
  23. package/src/cli.js +2213 -0
  24. package/src/code-quality-scanner.js +310 -0
  25. package/src/codex-manager.js +143 -0
  26. package/src/component-style-scanner.js +336 -0
  27. package/src/coverage-report.js +99 -0
  28. package/src/database-access-scanner.js +163 -0
  29. package/src/decision-records.js +315 -0
  30. package/src/design-modernize.js +1435 -0
  31. package/src/design-system.js +1732 -0
  32. package/src/diagnostic-engine.js +1945 -0
  33. package/src/diagram-requirement-resolver.js +194 -0
  34. package/src/doctor.js +677 -0
  35. package/src/environment-graph.js +424 -0
  36. package/src/execution-state.js +849 -0
  37. package/src/explore-evidence.js +425 -0
  38. package/src/flow-design-scanner.js +896 -0
  39. package/src/flow-verifier.js +887 -0
  40. package/src/gesture-interaction-scanner.js +330 -0
  41. package/src/graph-context.js +263 -0
  42. package/src/graphify-adapter.js +189 -0
  43. package/src/html-report.js +1035 -0
  44. package/src/journey-map.js +1299 -0
  45. package/src/language.js +48 -0
  46. package/src/lazy-pattern-detector.js +182 -0
  47. package/src/local-dev-scanner.js +135 -0
  48. package/src/managed-worktree-gate.js +187 -0
  49. package/src/managed-worktree.js +766 -0
  50. package/src/merge-manager.js +501 -0
  51. package/src/network-contract-scanner.js +442 -0
  52. package/src/nocodb-story-sync.js +386 -0
  53. package/src/oss-readiness-scanner.js +417 -0
  54. package/src/performance-evidence.js +756 -0
  55. package/src/performance-measurer.js +591 -0
  56. package/src/pr-manager.js +8220 -0
  57. package/src/presets.js +682 -0
  58. package/src/public-discovery-scanner.js +519 -0
  59. package/src/refactoring-delta-reporter.js +367 -0
  60. package/src/refactoring-opportunity-generator.js +797 -0
  61. package/src/regression-risk-scanner.js +146 -0
  62. package/src/repo-status.js +266 -0
  63. package/src/report-fingerprint.js +188 -0
  64. package/src/report-pr-body-prompt-template.md +108 -0
  65. package/src/report-pr-body-schema.json +95 -0
  66. package/src/report-store.js +135 -0
  67. package/src/report-validator.js +192 -0
  68. package/src/requirement-consistency.js +1066 -0
  69. package/src/runtime-info.js +134 -0
  70. package/src/self-dogfood-scanner.js +476 -0
  71. package/src/session-learning.js +164 -0
  72. package/src/skills-manager.js +157 -0
  73. package/src/spec-drift.js +378 -0
  74. package/src/spec-fingerprint.js +445 -0
  75. package/src/spec-prompt-template.md +155 -0
  76. package/src/spec-schema.json +219 -0
  77. package/src/spec-store.js +258 -0
  78. package/src/spec-validator.js +459 -0
  79. package/src/static-site-scanner.js +316 -0
  80. package/src/story-candidate-generator.js +85 -0
  81. package/src/story-catalog-generator.js +2813 -0
  82. package/src/story-html.js +156 -0
  83. package/src/story-manager.js +2144 -0
  84. package/src/story-task-generator.js +522 -0
  85. package/src/task-manager.js +1029 -0
  86. package/src/terminal-link-scanner.js +238 -0
  87. package/src/usage-report.js +417 -0
  88. package/src/verification-evidence.js +284 -0
  89. package/src/workspace.js +126 -0
@@ -0,0 +1,238 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const CANDIDATE_PATHS = [
5
+ 'public/modules/file-preview-config.js',
6
+ 'public/modules/xterm-file-links.js',
7
+ 'public/modules/iframe-contextmenu-handler.js',
8
+ 'public/ttyd/custom_ttyd_index.html',
9
+ 'server/controllers/session/shared-methods.js',
10
+ 'server/controllers/session/context-handlers.js'
11
+ ];
12
+ const GATE_EFFECTS = ['block', 'review', 'info'];
13
+ const REQUIRED_IMAGE_PREVIEW_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp'];
14
+
15
+ export async function scanTerminalLinkContracts(repoRoot) {
16
+ const root = path.resolve(repoRoot);
17
+ const files = await collectFiles(root);
18
+ const result = {
19
+ schema_version: '0.1.0',
20
+ scanned_files: files.length,
21
+ dot_directory_link_hits: [],
22
+ wrapped_terminal_link_hits: [],
23
+ dot_directory_tree_hits: [],
24
+ image_preview_extension_hits: [],
25
+ risk_summary: {
26
+ dot_directory_link_hits: { block: 0, review: 0, info: 0 },
27
+ wrapped_terminal_link_hits: { block: 0, review: 0, info: 0 },
28
+ dot_directory_tree_hits: { block: 0, review: 0, info: 0 },
29
+ image_preview_extension_hits: { block: 0, review: 0, info: 0 }
30
+ },
31
+ status: 'ok'
32
+ };
33
+
34
+ for (const file of files) {
35
+ const content = await readFile(file.absolutePath, 'utf8');
36
+ collectDotDirectoryLinkHits(result.dot_directory_link_hits, file.relativePath, content);
37
+ collectWrappedTerminalLinkHits(result.wrapped_terminal_link_hits, file.relativePath, content);
38
+ collectDotDirectoryTreeHits(result.dot_directory_tree_hits, file.relativePath, content);
39
+ collectImagePreviewExtensionHits(result.image_preview_extension_hits, file.relativePath, content);
40
+ }
41
+
42
+ result.risk_summary = {
43
+ dot_directory_link_hits: summarizeGateEffects(result.dot_directory_link_hits),
44
+ wrapped_terminal_link_hits: summarizeGateEffects(result.wrapped_terminal_link_hits),
45
+ dot_directory_tree_hits: summarizeGateEffects(result.dot_directory_tree_hits),
46
+ image_preview_extension_hits: summarizeGateEffects(result.image_preview_extension_hits)
47
+ };
48
+ result.status = allHits(result).length > 0 ? 'needs_review' : 'ok';
49
+ return result;
50
+ }
51
+
52
+ export function renderTerminalLinkReport({ runId, terminalLinkContracts }) {
53
+ if (!terminalLinkContracts) {
54
+ return `# ターミナルリンク契約診断結果
55
+
56
+ | 項目 | 内容 |
57
+ |------|------|
58
+ | Run ID | ${runId} |
59
+ | 状態 | 未生成 |
60
+ `;
61
+ }
62
+
63
+ return `# ターミナルリンク契約診断結果
64
+
65
+ | 項目 | 内容 |
66
+ |------|------|
67
+ | Run ID | ${runId} |
68
+ | Status | ${terminalLinkContracts.status} |
69
+ | 走査ファイル | ${terminalLinkContracts.scanned_files}件 |
70
+ | dot directoryリンク候補 | ${formatRiskCount(terminalLinkContracts.dot_directory_link_hits, terminalLinkContracts.risk_summary?.dot_directory_link_hits)} |
71
+ | 折り返しリンク候補 | ${formatRiskCount(terminalLinkContracts.wrapped_terminal_link_hits, terminalLinkContracts.risk_summary?.wrapped_terminal_link_hits)} |
72
+ | dot directoryツリー候補 | ${formatRiskCount(terminalLinkContracts.dot_directory_tree_hits, terminalLinkContracts.risk_summary?.dot_directory_tree_hits)} |
73
+ | 画像プレビュー拡張子候補 | ${formatRiskCount(terminalLinkContracts.image_preview_extension_hits, terminalLinkContracts.risk_summary?.image_preview_extension_hits)} |
74
+
75
+ ## dot directoryリンク候補
76
+
77
+ ${formatHits(terminalLinkContracts.dot_directory_link_hits)}
78
+
79
+ ## 折り返しリンク候補
80
+
81
+ ${formatHits(terminalLinkContracts.wrapped_terminal_link_hits)}
82
+
83
+ ## dot directoryツリー候補
84
+
85
+ ${formatHits(terminalLinkContracts.dot_directory_tree_hits)}
86
+
87
+ ## 画像プレビュー拡張子候補
88
+
89
+ ${formatHits(terminalLinkContracts.image_preview_extension_hits)}
90
+ `;
91
+ }
92
+
93
+ function collectDotDirectoryLinkHits(hits, file, content) {
94
+ if (!isTerminalLinkFile(file, content)) return;
95
+ const hasStrictStarter = content.includes('[a-zA-Z0-9_][a-zA-Z0-9_/.\\\\-]*')
96
+ || content.includes('[a-zA-Z0-9_][a-zA-Z0-9_/.\\-]*');
97
+ const supportsDotDirectory = content.includes('\\\\.[a-zA-Z0-9_]')
98
+ || content.includes('\\.[a-zA-Z0-9_]')
99
+ || content.includes('PATH_START');
100
+ if (!hasStrictStarter || supportsDotDirectory) return;
101
+
102
+ hits.push({
103
+ file,
104
+ line: lineNumberOf(content, '[a-zA-Z0-9_]'),
105
+ kind: 'dot_directory_file_link_not_supported',
106
+ excerpt: excerptAround(content, '[a-zA-Z0-9_]'),
107
+ confidence: 'high',
108
+ gate_effect: 'review',
109
+ recommendation: 'terminal file link regex should accept dot-prefixed relative directories such as .vibepro/pr/story/pr-prepare.html.'
110
+ });
111
+ }
112
+
113
+ function collectWrappedTerminalLinkHits(hits, file, content) {
114
+ if (!isTerminalLinkFile(file, content)) return;
115
+ if (!content.includes('CONTINUATION') && !content.includes('registerLinkProvider')) return;
116
+ if (!content.includes('^(\\\\s+)') && !content.includes("'^(\\\\s+)'")) return;
117
+
118
+ hits.push({
119
+ file,
120
+ line: lineNumberOf(content, '^(\\\\s+)'),
121
+ kind: 'wrapped_terminal_continuation_requires_indent',
122
+ excerpt: excerptAround(content, '^(\\\\s+)'),
123
+ confidence: 'high',
124
+ gate_effect: 'review',
125
+ recommendation: 'terminal continuation detection should handle hard wraps that continue at column 1, not only indented continuation lines.'
126
+ });
127
+ }
128
+
129
+ function collectDotDirectoryTreeHits(hits, file, content) {
130
+ if (!/folder-tree|readTree|hasVisibleChildren|EXCLUDED_DIRS/i.test(content)) return;
131
+ if (!content.includes("startsWith('.')")) return;
132
+ if (content.includes('VISIBLE_DOT_DIRS') || content.includes('.vibepro')) return;
133
+
134
+ hits.push({
135
+ file,
136
+ line: lineNumberOf(content, "startsWith('.')"),
137
+ kind: 'dot_directory_tree_hidden_without_allowlist',
138
+ excerpt: excerptAround(content, "startsWith('.')"),
139
+ confidence: 'high',
140
+ gate_effect: 'review',
141
+ recommendation: 'folder tree hidden-file filtering should allow generated review artifact directories such as .vibepro while still excluding .git and other internals.'
142
+ });
143
+ }
144
+
145
+ function collectImagePreviewExtensionHits(hits, file, content) {
146
+ if (file !== 'public/modules/file-preview-config.js' && !content.includes('BROWSER_PREVIEWABLE_EXTENSIONS')) return;
147
+
148
+ const browserBody = extractSetBody(content, 'BROWSER_PREVIEWABLE_EXTENSIONS');
149
+ if (!browserBody) return;
150
+
151
+ const imageBody = extractSetBody(content, 'IMAGE_EXTENSIONS');
152
+ const browserUsesImageSet = /\.\.\.\s*IMAGE_EXTENSIONS/.test(browserBody);
153
+ const previewableSource = browserUsesImageSet && imageBody ? imageBody : browserBody;
154
+ const missing = REQUIRED_IMAGE_PREVIEW_EXTENSIONS.filter((ext) => !hasQuotedExtension(previewableSource, ext));
155
+ if (missing.length === 0) return;
156
+
157
+ hits.push({
158
+ file,
159
+ line: lineNumberOf(content, 'BROWSER_PREVIEWABLE_EXTENSIONS'),
160
+ kind: 'browser_preview_image_extensions_missing',
161
+ missing_extensions: missing,
162
+ excerpt: excerptAround(content, 'BROWSER_PREVIEWABLE_EXTENSIONS'),
163
+ confidence: 'high',
164
+ gate_effect: 'review',
165
+ recommendation: `file viewer browser-preview contract should include common image extensions: ${REQUIRED_IMAGE_PREVIEW_EXTENSIONS.join(', ')}. Missing: ${missing.join(', ')}.`
166
+ });
167
+ }
168
+
169
+ function isTerminalLinkFile(file, content) {
170
+ return /xterm|ttyd|terminal/i.test(file) || /registerLinkProvider|OPEN_FILE|filePathRegex|XTERM_FILE_TOKEN_REGEX/.test(content);
171
+ }
172
+
173
+ function allHits(result) {
174
+ return [
175
+ ...result.dot_directory_link_hits,
176
+ ...result.wrapped_terminal_link_hits,
177
+ ...result.dot_directory_tree_hits,
178
+ ...result.image_preview_extension_hits
179
+ ];
180
+ }
181
+
182
+ async function collectFiles(root, current = root) {
183
+ const files = [];
184
+ for (const relativePath of CANDIDATE_PATHS) {
185
+ const absolutePath = path.join(root, relativePath);
186
+ try {
187
+ const fileStat = await stat(absolutePath);
188
+ if (!fileStat.isFile() || fileStat.size > 1024 * 1024) continue;
189
+ files.push({ absolutePath, relativePath });
190
+ } catch (error) {
191
+ if (error.code !== 'ENOENT') throw error;
192
+ }
193
+ }
194
+ return files;
195
+ }
196
+
197
+ function summarizeGateEffects(items = []) {
198
+ const summary = Object.fromEntries(GATE_EFFECTS.map((effect) => [effect, 0]));
199
+ for (const item of items) {
200
+ if (summary[item.gate_effect] !== undefined) summary[item.gate_effect] += 1;
201
+ }
202
+ return summary;
203
+ }
204
+
205
+ function formatRiskCount(items = [], summary = summarizeGateEffects(items)) {
206
+ return `${items.length}件 (block: ${summary.block ?? 0}, review: ${summary.review ?? 0}, info: ${summary.info ?? 0})`;
207
+ }
208
+
209
+ function formatHits(hits = []) {
210
+ if (hits.length === 0) return '- なし';
211
+ return hits.map((hit) => `- ${hit.file}:${hit.line} ${hit.kind} confidence=${hit.confidence} gate_effect=${hit.gate_effect} \`${hit.excerpt}\``).join('\n');
212
+ }
213
+
214
+ function lineNumberOf(content, needle) {
215
+ const index = content.indexOf(needle);
216
+ if (index < 0) return 1;
217
+ return content.slice(0, index).split(/\r?\n/).length;
218
+ }
219
+
220
+ function excerptAround(content, needle) {
221
+ const index = content.indexOf(needle);
222
+ if (index < 0) return '';
223
+ return content
224
+ .slice(Math.max(0, index - 60), index + needle.length + 80)
225
+ .replace(/\s+/g, ' ')
226
+ .trim()
227
+ .slice(0, 180);
228
+ }
229
+
230
+ function extractSetBody(content, exportName) {
231
+ const match = content.match(new RegExp(`${exportName}\\s*=\\s*new\\s+Set\\s*\\(\\s*\\[([\\s\\S]*?)\\]\\s*\\)`, 'm'));
232
+ return match?.[1] || null;
233
+ }
234
+
235
+ function hasQuotedExtension(content, extension) {
236
+ const escaped = extension.replace('.', '\\.');
237
+ return new RegExp(`['"\`]${escaped}['"\`]`).test(content);
238
+ }
@@ -0,0 +1,417 @@
1
+ import { readdir, readFile, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { resolveHumanOutputLanguage } from './language.js';
5
+ import { getWorkspaceDir, toWorkspaceRelative } from './workspace.js';
6
+
7
+ export async function createUsageReport(repoRoot, options = {}) {
8
+ const root = path.resolve(repoRoot);
9
+ const workspaceDir = getWorkspaceDir(root);
10
+ const since = parseSince(options.since);
11
+ const language = await resolveHumanOutputLanguage(root, { language: options.language }).catch(() => options.language ?? 'ja');
12
+ const prArtifacts = await collectPrArtifacts(root, workspaceDir, since);
13
+ const reviewArtifacts = await collectReviewArtifacts(root, workspaceDir, since);
14
+ const executionArtifacts = await collectExecutionArtifacts(root, workspaceDir, since);
15
+ const logs = await collectUsageLogs(root, options);
16
+ const storyMap = new Map();
17
+ for (const artifact of prArtifacts) {
18
+ const story = ensureStoryUsage(storyMap, artifact.story_id);
19
+ story.artifacts.push(artifact.path);
20
+ if (artifact.kind === 'pr_prepare') {
21
+ story.prepared = true;
22
+ story.prepare_count += 1;
23
+ story.ready_for_pr_create ||= artifact.data?.gate_status?.ready_for_pr_create === true;
24
+ story.blocked ||= artifact.data?.gate_status?.ready_for_pr_create === false;
25
+ story.waiver_required ||= artifact.data?.gate_status?.execution_gate?.waiver_required === true;
26
+ story.latest_gate_status = artifact.data?.gate_status?.overall_status ?? story.latest_gate_status;
27
+ }
28
+ if (artifact.kind === 'pr_create') {
29
+ story.pr_create_count += 1;
30
+ story.pr_created ||= Boolean(artifact.data?.pr_url) || artifact.data?.status === 'created';
31
+ story.waiver_required ||= artifact.data?.gate_override?.allowed === true;
32
+ story.latest_pr_url = artifact.data?.pr_url ?? story.latest_pr_url;
33
+ }
34
+ if (artifact.kind === 'gate_dag') collectGateMetrics(artifact.data, artifact.story_id, storyMap);
35
+ }
36
+ for (const artifact of reviewArtifacts) {
37
+ const story = ensureStoryUsage(storyMap, artifact.story_id);
38
+ story.artifacts.push(artifact.path);
39
+ story.agent_review.required_role_count += artifact.data?.roles?.length ?? 0;
40
+ story.agent_review.pass_count += artifact.data?.pass_count ?? 0;
41
+ story.agent_review.block_count += artifact.data?.block_count ?? 0;
42
+ story.agent_review.stale_count += artifact.data?.stale_count ?? 0;
43
+ story.agent_review.timeout_count += artifact.data?.lifecycle?.timed_out_count ?? 0;
44
+ story.agent_review.replaced_count += artifact.data?.lifecycle?.replaced_count ?? 0;
45
+ }
46
+ for (const artifact of executionArtifacts) {
47
+ const story = ensureStoryUsage(storyMap, artifact.story_id);
48
+ story.artifacts.push(artifact.path);
49
+ story.execution_state_count += 1;
50
+ story.blocked ||= artifact.data?.completion_status === 'blocked';
51
+ story.latest_execution_status = artifact.data?.completion_status ?? story.latest_execution_status;
52
+ }
53
+ for (const finding of logs.raw_pr_create_mentions) {
54
+ const story = ensureStoryUsage(storyMap, finding.story_id ?? 'unknown-log-story');
55
+ story.raw_pr_bypass_suspected = true;
56
+ story.log_findings.push(finding);
57
+ }
58
+ const stories = [...storyMap.values()].sort((a, b) => a.story_id.localeCompare(b.story_id));
59
+ const gate_metrics = buildGateMetrics(prArtifacts);
60
+ const agent_review = buildAgentReviewMetrics(stories);
61
+ const value_signals = buildValueSignals(stories);
62
+ return {
63
+ schema_version: '0.1.0',
64
+ generated_at: new Date().toISOString(),
65
+ output: { language },
66
+ since: since ? since.toISOString() : null,
67
+ artifact_counts: {
68
+ pr: prArtifacts.length,
69
+ review: reviewArtifacts.length,
70
+ execution: executionArtifacts.length,
71
+ logs: logs.files.length
72
+ },
73
+ stories,
74
+ gate_metrics,
75
+ agent_review,
76
+ value_signals,
77
+ log_signals: logs
78
+ };
79
+ }
80
+
81
+ export function renderUsageReport(report) {
82
+ const language = report.output?.language ?? 'ja';
83
+ const storyRows = report.stories.length
84
+ ? report.stories.map((story) => (
85
+ `- ${story.story_id}: prepared=${story.prepared} blocked=${story.blocked} ready=${story.ready_for_pr_create} pr_created=${story.pr_created} waiver_required=${story.waiver_required} raw_pr_bypass_suspected=${story.raw_pr_bypass_suspected} stale_evidence=${story.stale_evidence} story_source_mismatch=${story.story_source_mismatch}`
86
+ )).join('\n')
87
+ : '- none';
88
+ const gateRows = report.gate_metrics.length
89
+ ? report.gate_metrics.map((gate) => (
90
+ `- ${gate.gate_id}: block=${gate.block_count} waiver=${gate.waiver_count} critical_unresolved=${gate.critical_unresolved_count}`
91
+ )).join('\n')
92
+ : '- none';
93
+ const reviewRows = report.agent_review.by_story.length
94
+ ? report.agent_review.by_story.map((item) => (
95
+ `- ${item.story_id}: required=${item.required_role_count} pass=${item.pass_count} block=${item.block_count} timeout=${item.timeout_count} replaced=${item.replaced_count} stale=${item.stale_count}`
96
+ )).join('\n')
97
+ : '- none';
98
+ const valueSignals = report.value_signals ?? {};
99
+ const valueRows = [
100
+ `- waiver_required: ${valueSignals.waiver_required_story_count ?? 0}/${valueSignals.story_count ?? 0} (${formatRate(valueSignals.waiver_required_rate)})`,
101
+ `- stale_evidence: ${valueSignals.stale_evidence_story_count ?? 0}/${valueSignals.story_count ?? 0} (${formatRate(valueSignals.stale_evidence_rate)})`,
102
+ `- story_source_mismatch: ${valueSignals.story_source_mismatch_story_count ?? 0}/${valueSignals.story_count ?? 0} (${formatRate(valueSignals.story_source_mismatch_rate)})`
103
+ ].join('\n');
104
+ if (language === 'en') {
105
+ return `# VibePro Usage Report
106
+
107
+ - since: ${report.since ?? 'all'}
108
+ - stories: ${report.stories.length}
109
+ - artifacts: pr=${report.artifact_counts.pr} review=${report.artifact_counts.review} execution=${report.artifact_counts.execution} logs=${report.artifact_counts.logs}
110
+
111
+ ## Stories
112
+
113
+ ${storyRows}
114
+
115
+ ## Gates
116
+
117
+ ${gateRows}
118
+
119
+ ## Agent Review
120
+
121
+ ${reviewRows}
122
+
123
+ ## Value Signals
124
+
125
+ ${valueRows}
126
+
127
+ ## Log Signals
128
+
129
+ - raw gh pr create mentions: ${report.log_signals.raw_pr_create_mentions.length}
130
+ - VibePro command mentions: ${report.log_signals.vibepro_command_mentions.length}
131
+ `;
132
+ }
133
+ return `# VibePro利用状況レポート
134
+
135
+ - 対象期間: ${report.since ?? '全期間'}
136
+ - Story数: ${report.stories.length}
137
+ - artifact数: pr=${report.artifact_counts.pr} review=${report.artifact_counts.review} execution=${report.artifact_counts.execution} logs=${report.artifact_counts.logs}
138
+
139
+ ## Story別
140
+
141
+ ${storyRows}
142
+
143
+ ## Gate別
144
+
145
+ ${gateRows}
146
+
147
+ ## Agent Review
148
+
149
+ ${reviewRows}
150
+
151
+ ## Value Signals
152
+
153
+ ${valueRows}
154
+
155
+ ## ログ補助シグナル
156
+
157
+ - raw gh pr create mentions: ${report.log_signals.raw_pr_create_mentions.length}
158
+ - VibePro command mentions: ${report.log_signals.vibepro_command_mentions.length}
159
+ `;
160
+ }
161
+
162
+ function ensureStoryUsage(storyMap, storyId) {
163
+ const key = storyId || 'unknown';
164
+ if (!storyMap.has(key)) {
165
+ storyMap.set(key, {
166
+ story_id: key,
167
+ prepared: false,
168
+ blocked: false,
169
+ ready_for_pr_create: false,
170
+ pr_created: false,
171
+ waiver_required: false,
172
+ raw_pr_bypass_suspected: false,
173
+ stale_evidence: false,
174
+ story_source_mismatch: false,
175
+ prepare_count: 0,
176
+ pr_create_count: 0,
177
+ execution_state_count: 0,
178
+ latest_gate_status: null,
179
+ latest_execution_status: null,
180
+ latest_pr_url: null,
181
+ artifacts: [],
182
+ log_findings: [],
183
+ agent_review: {
184
+ required_role_count: 0,
185
+ pass_count: 0,
186
+ block_count: 0,
187
+ timeout_count: 0,
188
+ replaced_count: 0,
189
+ stale_count: 0
190
+ },
191
+ gate_metrics: {}
192
+ });
193
+ }
194
+ return storyMap.get(key);
195
+ }
196
+
197
+ async function collectPrArtifacts(root, workspaceDir, since) {
198
+ const prDir = path.join(workspaceDir, 'pr');
199
+ const storyDirs = await safeReaddir(prDir);
200
+ const artifacts = [];
201
+ for (const storyId of storyDirs) {
202
+ const storyDir = path.join(prDir, storyId);
203
+ for (const [file, kind] of [['pr-prepare.json', 'pr_prepare'], ['pr-create.json', 'pr_create'], ['gate-dag.json', 'gate_dag']]) {
204
+ const filePath = path.join(storyDir, file);
205
+ const data = await readJsonIfExists(filePath);
206
+ if (!data || !isWithinSince(data.created_at ?? data.generated_at ?? data.updated_at, since)) continue;
207
+ artifacts.push({ kind, story_id: data.story?.story_id ?? data.story_id ?? storyId, path: toWorkspaceRelative(root, filePath), data });
208
+ }
209
+ }
210
+ return artifacts;
211
+ }
212
+
213
+ async function collectReviewArtifacts(root, workspaceDir, since) {
214
+ const reviewDir = path.join(workspaceDir, 'reviews');
215
+ const artifacts = [];
216
+ for (const storyId of await safeReaddir(reviewDir)) {
217
+ for (const stage of await safeReaddir(path.join(reviewDir, storyId))) {
218
+ const filePath = path.join(reviewDir, storyId, stage, 'review-summary.json');
219
+ const data = await readJsonIfExists(filePath);
220
+ if (!data || !isWithinSince(data.updated_at, since)) continue;
221
+ artifacts.push({ kind: 'review_summary', story_id: data.story_id ?? storyId, path: toWorkspaceRelative(root, filePath), data });
222
+ }
223
+ }
224
+ return artifacts;
225
+ }
226
+
227
+ async function collectExecutionArtifacts(root, workspaceDir, since) {
228
+ const executionDir = path.join(workspaceDir, 'executions');
229
+ const artifacts = [];
230
+ for (const storyId of await safeReaddir(executionDir)) {
231
+ const filePath = path.join(executionDir, storyId, 'state.json');
232
+ const data = await readJsonIfExists(filePath);
233
+ if (!data || !isWithinSince(data.updated_at ?? data.started_at, since)) continue;
234
+ artifacts.push({ kind: 'execution_state', story_id: data.story_id ?? storyId, path: toWorkspaceRelative(root, filePath), data });
235
+ }
236
+ return artifacts;
237
+ }
238
+
239
+ async function collectUsageLogs(root, options = {}) {
240
+ const files = [...(options.logs ?? []), ...(options.codexLogs ?? []), ...(options.claudeLogs ?? [])]
241
+ .map((file) => path.resolve(root, file));
242
+ const rawMentions = [];
243
+ const vibeproMentions = [];
244
+ for (const file of files) {
245
+ const text = await readTextIfExists(file);
246
+ if (!text) continue;
247
+ const relative = toWorkspaceRelative(root, file);
248
+ let latestStoryId = inferStoryId(text);
249
+ for (const [lineIndex, line] of text.split(/\r?\n/).entries()) {
250
+ const lineStoryId = inferStoryId(line);
251
+ if (lineStoryId) latestStoryId = lineStoryId;
252
+ const rawMatches = [...line.matchAll(/(?:^|[^A-Za-z0-9_-])(gh\s+pr\s+create)(?=$|[^A-Za-z0-9_-])/g)];
253
+ const vibeproMatches = [...line.matchAll(/(?:^|[^A-Za-z0-9_-])(vibepro\s+[a-z][^`]*)/g)];
254
+ for (const _match of rawMatches) {
255
+ rawMentions.push({
256
+ file: relative,
257
+ line: lineIndex + 1,
258
+ story_id: lineStoryId ?? latestStoryId,
259
+ signal: 'raw_gh_pr_create'
260
+ });
261
+ }
262
+ for (const match of vibeproMatches) {
263
+ vibeproMentions.push({
264
+ file: relative,
265
+ line: lineIndex + 1,
266
+ story_id: lineStoryId ?? latestStoryId,
267
+ command: normalizeLogCommand(match[1])
268
+ });
269
+ }
270
+ }
271
+ }
272
+ return {
273
+ files: files.map((file) => toWorkspaceRelative(root, file)),
274
+ raw_pr_create_mentions: rawMentions,
275
+ vibepro_command_mentions: vibeproMentions
276
+ };
277
+ }
278
+
279
+ function normalizeLogCommand(value) {
280
+ return String(value ?? '')
281
+ .trim()
282
+ .replace(/[`"'))】\].,;:、。]+$/g, '')
283
+ .trim();
284
+ }
285
+
286
+ function buildGateMetrics(prArtifacts) {
287
+ const metrics = new Map();
288
+ const ensure = (gateId) => {
289
+ if (!metrics.has(gateId)) metrics.set(gateId, { gate_id: gateId, block_count: 0, waiver_count: 0, critical_unresolved_count: 0 });
290
+ return metrics.get(gateId);
291
+ };
292
+ for (const artifact of prArtifacts) {
293
+ if (artifact.kind === 'gate_dag') {
294
+ for (const node of artifact.data?.nodes ?? []) {
295
+ const metric = ensure(node.id ?? 'unknown_gate');
296
+ if (['block', 'needs_evidence', 'needs_review', 'failed'].includes(node.status)) metric.block_count += 1;
297
+ if (node.status === 'bypassed') metric.waiver_count += 1;
298
+ }
299
+ }
300
+ if (artifact.kind === 'pr_prepare') {
301
+ for (const gate of artifact.data?.gate_status?.critical_unresolved_gates ?? []) {
302
+ ensure(gate.id ?? 'unknown_gate').critical_unresolved_count += 1;
303
+ }
304
+ }
305
+ if (artifact.kind === 'pr_create' && artifact.data?.gate_override?.allowed) {
306
+ for (const gate of artifact.data?.gate_override?.unresolved_gates ?? []) {
307
+ ensure(gate.id ?? 'unknown_gate').waiver_count += 1;
308
+ }
309
+ }
310
+ }
311
+ return [...metrics.values()].sort((a, b) => a.gate_id.localeCompare(b.gate_id));
312
+ }
313
+
314
+ function collectGateMetrics(gateDag, storyId, storyMap) {
315
+ const story = ensureStoryUsage(storyMap, storyId);
316
+ for (const node of gateDag?.nodes ?? []) {
317
+ const gateId = node.id ?? 'unknown_gate';
318
+ const metric = story.gate_metrics[gateId] ?? { block_count: 0, waiver_count: 0, critical_unresolved_count: 0 };
319
+ if (['block', 'needs_evidence', 'needs_review', 'failed'].includes(node.status)) metric.block_count += 1;
320
+ if (node.status === 'bypassed') metric.waiver_count += 1;
321
+ story.gate_metrics[gateId] = metric;
322
+ if (node.status === 'stale_evidence') story.stale_evidence = true;
323
+ if (node.status === 'story_source_mismatch') story.story_source_mismatch = true;
324
+ }
325
+ }
326
+
327
+ function buildValueSignals(stories) {
328
+ const storyCount = stories.length;
329
+ const waiverRequiredCount = stories.filter((story) => story.waiver_required).length;
330
+ const staleEvidenceCount = stories.filter((story) => story.stale_evidence).length;
331
+ const storySourceMismatchCount = stories.filter((story) => story.story_source_mismatch).length;
332
+ return {
333
+ story_count: storyCount,
334
+ waiver_required_story_count: waiverRequiredCount,
335
+ stale_evidence_story_count: staleEvidenceCount,
336
+ story_source_mismatch_story_count: storySourceMismatchCount,
337
+ waiver_required_rate: calculateRate(waiverRequiredCount, storyCount),
338
+ stale_evidence_rate: calculateRate(staleEvidenceCount, storyCount),
339
+ story_source_mismatch_rate: calculateRate(storySourceMismatchCount, storyCount)
340
+ };
341
+ }
342
+
343
+ function calculateRate(count, total) {
344
+ if (!total) return null;
345
+ return Number((count / total).toFixed(4));
346
+ }
347
+
348
+ function formatRate(value) {
349
+ if (typeof value !== 'number') return '-';
350
+ return `${Math.round(value * 100)}%`;
351
+ }
352
+
353
+ function buildAgentReviewMetrics(stories) {
354
+ return {
355
+ totals: stories.reduce((totals, story) => ({
356
+ required_role_count: totals.required_role_count + story.agent_review.required_role_count,
357
+ pass_count: totals.pass_count + story.agent_review.pass_count,
358
+ block_count: totals.block_count + story.agent_review.block_count,
359
+ timeout_count: totals.timeout_count + story.agent_review.timeout_count,
360
+ replaced_count: totals.replaced_count + story.agent_review.replaced_count,
361
+ stale_count: totals.stale_count + story.agent_review.stale_count
362
+ }), { required_role_count: 0, pass_count: 0, block_count: 0, timeout_count: 0, replaced_count: 0, stale_count: 0 }),
363
+ by_story: stories.map((story) => ({ story_id: story.story_id, ...story.agent_review }))
364
+ };
365
+ }
366
+
367
+ async function safeReaddir(dir) {
368
+ try {
369
+ const entries = await readdir(dir);
370
+ const dirs = [];
371
+ for (const entry of entries) {
372
+ const full = path.join(dir, entry);
373
+ if ((await stat(full)).isDirectory()) dirs.push(entry);
374
+ }
375
+ return dirs.sort();
376
+ } catch (error) {
377
+ if (error.code === 'ENOENT') return [];
378
+ throw error;
379
+ }
380
+ }
381
+
382
+ async function readJsonIfExists(filePath) {
383
+ try {
384
+ return JSON.parse(await readFile(filePath, 'utf8'));
385
+ } catch (error) {
386
+ if (error.code === 'ENOENT') return null;
387
+ throw error;
388
+ }
389
+ }
390
+
391
+ async function readTextIfExists(filePath) {
392
+ try {
393
+ return await readFile(filePath, 'utf8');
394
+ } catch (error) {
395
+ if (error.code === 'ENOENT') return '';
396
+ throw error;
397
+ }
398
+ }
399
+
400
+ function parseSince(value) {
401
+ if (!value) return null;
402
+ const parsed = new Date(value);
403
+ if (Number.isNaN(parsed.getTime())) throw new Error(`usage report --since is not a valid date: ${value}`);
404
+ return parsed;
405
+ }
406
+
407
+ function isWithinSince(value, since) {
408
+ if (!since || !value) return true;
409
+ const parsed = new Date(value);
410
+ if (Number.isNaN(parsed.getTime())) return true;
411
+ return parsed >= since;
412
+ }
413
+
414
+ function inferStoryId(text) {
415
+ const match = text.match(/story-[a-z0-9][a-z0-9-]+/i);
416
+ return match ? match[0] : null;
417
+ }