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,146 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
extractGraphNodeSourceFile,
|
|
6
|
+
getEdgeEndpoint,
|
|
7
|
+
normalizeGraphEdges,
|
|
8
|
+
normalizeGraphPath
|
|
9
|
+
} from './graph-context.js';
|
|
10
|
+
import { loadCoverage } from './coverage-report.js';
|
|
11
|
+
import { getWorkspaceDir } from './workspace.js';
|
|
12
|
+
|
|
13
|
+
// Regression-risk scanner: blast-radius core, optionally sharpened by coverage.
|
|
14
|
+
//
|
|
15
|
+
// Primary signal: module-level fan-in derived from the Graphify call graph. A
|
|
16
|
+
// module called by many distinct other modules has a large blast radius —
|
|
17
|
+
// changing it risks regressions in every caller. This is IMPACT risk, not defect
|
|
18
|
+
// probability: a static call graph cannot tell you a change is buggy, only how
|
|
19
|
+
// far its effects can reach. We name it accordingly and never claim prediction.
|
|
20
|
+
//
|
|
21
|
+
// Secondary signal (optional): real line coverage from the project's own tooling
|
|
22
|
+
// (c8/istanbul/lcov). When present, a high-blast-radius module with low coverage
|
|
23
|
+
// is escalated to `critical` — large reach AND a thin safety net is the genuine
|
|
24
|
+
// regression trap. Coverage is deliberately NOT inferred from the call graph:
|
|
25
|
+
// dogfooding VibePro showed static call-graph test-reachability mislabels
|
|
26
|
+
// CLI/subprocess-driven coverage as untested. When no coverage report exists we
|
|
27
|
+
// degrade cleanly to fan-in-only scoring with identical behavior to before.
|
|
28
|
+
|
|
29
|
+
const DEFAULT_HIGH_FAN_IN = 10;
|
|
30
|
+
const DEFAULT_MODERATE_FAN_IN = 5;
|
|
31
|
+
const DEFAULT_LOW_COVERAGE = 0.5;
|
|
32
|
+
const DEFAULT_TOP = 20;
|
|
33
|
+
const CALL_RELATIONS = new Set(['calls', 'call', 'invokes', 'uses']);
|
|
34
|
+
|
|
35
|
+
export async function scanRegressionRisk(repoRoot, options = {}) {
|
|
36
|
+
const root = path.resolve(repoRoot);
|
|
37
|
+
const graphPath = path.join(getWorkspaceDir(root), 'graphify', 'graph.json');
|
|
38
|
+
|
|
39
|
+
let graph;
|
|
40
|
+
try {
|
|
41
|
+
graph = JSON.parse(await readFile(graphPath, 'utf8'));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error.code === 'ENOENT') {
|
|
44
|
+
return {
|
|
45
|
+
status: 'skipped',
|
|
46
|
+
reason:
|
|
47
|
+
'Regression-risk scanning needs a Graphify call graph at .vibepro/graphify/graph.json. ' +
|
|
48
|
+
'Graphify is optional: run a story diagnosis or import with --run-graphify / --from <graphify-out> first.',
|
|
49
|
+
hotspots: [],
|
|
50
|
+
summary: { scored_modules: 0, high: 0, moderate: 0 }
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const loaded = await loadCoverage(root, { file: options.coverageFile });
|
|
57
|
+
return analyzeRegressionRisk(graph, {
|
|
58
|
+
...options,
|
|
59
|
+
coverage: loaded?.coverage ?? null,
|
|
60
|
+
coverageSource: loaded?.source ?? null
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Pure analysis over a loaded graph. Exported for unit testing without disk I/O.
|
|
65
|
+
export function analyzeRegressionRisk(graph, options = {}) {
|
|
66
|
+
const includePrefixes = options.includePrefixes ?? ['src/'];
|
|
67
|
+
const highFanIn = options.highFanIn ?? DEFAULT_HIGH_FAN_IN;
|
|
68
|
+
const moderateFanIn = options.moderateFanIn ?? DEFAULT_MODERATE_FAN_IN;
|
|
69
|
+
const lowCoverage = options.lowCoverage ?? DEFAULT_LOW_COVERAGE;
|
|
70
|
+
const coverage = options.coverage ?? null;
|
|
71
|
+
const hasCoverage = coverage instanceof Map && coverage.size > 0;
|
|
72
|
+
const top = options.top ?? DEFAULT_TOP;
|
|
73
|
+
|
|
74
|
+
const fileById = new Map();
|
|
75
|
+
for (const node of graph?.nodes ?? []) {
|
|
76
|
+
if (!node || typeof node.id !== 'string') continue;
|
|
77
|
+
const sourceFile = extractGraphNodeSourceFile(node);
|
|
78
|
+
if (sourceFile) fileById.set(node.id, normalizeGraphPath(sourceFile));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { edges } = normalizeGraphEdges(graph);
|
|
82
|
+
const callers = new Map(); // module -> Set of caller modules (fan-in)
|
|
83
|
+
const callees = new Map(); // module -> Set of callee modules (fan-out)
|
|
84
|
+
const inScope = (file) => Boolean(file) && includePrefixes.some((prefix) => file.startsWith(prefix));
|
|
85
|
+
const add = (map, key, value) => {
|
|
86
|
+
if (!map.has(key)) map.set(key, new Set());
|
|
87
|
+
map.get(key).add(value);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
for (const edge of edges) {
|
|
91
|
+
if (edge?.relation && !CALL_RELATIONS.has(edge.relation)) continue;
|
|
92
|
+
const sourceFile = fileById.get(getEdgeEndpoint(edge, 'source'));
|
|
93
|
+
const targetFile = fileById.get(getEdgeEndpoint(edge, 'target'));
|
|
94
|
+
if (!sourceFile || !targetFile || sourceFile === targetFile) continue; // cross-module only
|
|
95
|
+
if (inScope(targetFile)) add(callers, targetFile, sourceFile);
|
|
96
|
+
if (inScope(sourceFile)) add(callees, sourceFile, targetFile);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const hotspots = [...callers.entries()]
|
|
100
|
+
.map(([file, callerSet]) => {
|
|
101
|
+
const fanIn = callerSet.size;
|
|
102
|
+
const riskTier = fanIn >= highFanIn ? 'high' : fanIn >= moderateFanIn ? 'moderate' : 'low';
|
|
103
|
+
const coveragePct = hasCoverage && coverage.has(file)
|
|
104
|
+
? Number((coverage.get(file) * 100).toFixed(1))
|
|
105
|
+
: null;
|
|
106
|
+
// critical = large blast radius AND a thin (known) safety net.
|
|
107
|
+
const priority = riskTier === 'high' && coveragePct !== null && coveragePct < lowCoverage * 100
|
|
108
|
+
? 'critical'
|
|
109
|
+
: riskTier;
|
|
110
|
+
return {
|
|
111
|
+
file,
|
|
112
|
+
fan_in: fanIn,
|
|
113
|
+
fan_out: callees.get(file)?.size ?? 0,
|
|
114
|
+
risk_tier: riskTier,
|
|
115
|
+
coverage_pct: coveragePct,
|
|
116
|
+
priority
|
|
117
|
+
};
|
|
118
|
+
})
|
|
119
|
+
.sort((a, b) => priorityRank(b) - priorityRank(a) || b.fan_in - a.fan_in || a.file.localeCompare(b.file));
|
|
120
|
+
|
|
121
|
+
const high = hotspots.filter((h) => h.risk_tier === 'high').length;
|
|
122
|
+
const moderate = hotspots.filter((h) => h.risk_tier === 'moderate').length;
|
|
123
|
+
const critical = hotspots.filter((h) => h.priority === 'critical').length;
|
|
124
|
+
|
|
125
|
+
// With coverage data, a well-tested hub is no longer a review trigger; only
|
|
126
|
+
// high-blast-radius + low-coverage (critical) escalates. Without coverage,
|
|
127
|
+
// behavior is unchanged: any high-blast-radius module triggers review.
|
|
128
|
+
const status = hasCoverage ? (critical > 0 ? 'needs_review' : 'pass') : (high > 0 ? 'needs_review' : 'pass');
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
status,
|
|
132
|
+
hotspots: hotspots.slice(0, top),
|
|
133
|
+
summary: {
|
|
134
|
+
scored_modules: hotspots.length,
|
|
135
|
+
high,
|
|
136
|
+
moderate,
|
|
137
|
+
critical,
|
|
138
|
+
coverage_source: options.coverageSource ?? (hasCoverage ? 'provided' : null),
|
|
139
|
+
thresholds: { high_fan_in: highFanIn, moderate_fan_in: moderateFanIn, low_coverage_pct: lowCoverage * 100 }
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function priorityRank(hotspot) {
|
|
145
|
+
return { critical: 3, high: 2, moderate: 1, low: 0 }[hotspot.priority] ?? 0;
|
|
146
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { runDoctor } from './doctor.js';
|
|
5
|
+
import { DEFAULT_BRAINBASE_STORIES, getWorkspaceDir, MANIFEST_FILE } from './workspace.js';
|
|
6
|
+
|
|
7
|
+
export async function getRepoStatus(repoRoot) {
|
|
8
|
+
const root = path.resolve(repoRoot);
|
|
9
|
+
const workspaceDir = getWorkspaceDir(root);
|
|
10
|
+
const configPath = path.join(workspaceDir, 'config.json');
|
|
11
|
+
const manifestPath = path.join(workspaceDir, MANIFEST_FILE);
|
|
12
|
+
const initialized = await exists(configPath) && await exists(manifestPath);
|
|
13
|
+
|
|
14
|
+
if (!initialized) {
|
|
15
|
+
return {
|
|
16
|
+
initialized: false,
|
|
17
|
+
repo_root: root,
|
|
18
|
+
workspace: '.vibepro',
|
|
19
|
+
current_story_id: null,
|
|
20
|
+
active_stories: [],
|
|
21
|
+
latest_run: null,
|
|
22
|
+
selected_story_latest_run: null,
|
|
23
|
+
gate_status: null,
|
|
24
|
+
finding_count: 0,
|
|
25
|
+
artifacts: {},
|
|
26
|
+
next_commands: [`vibepro init ${root}`]
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const configRead = await readJsonWithStatus(configPath, 'config');
|
|
31
|
+
const manifestRead = await readJsonWithStatus(manifestPath, 'manifest');
|
|
32
|
+
if (!configRead.ok || !manifestRead.ok) {
|
|
33
|
+
const issues = [configRead, manifestRead]
|
|
34
|
+
.filter((item) => !item.ok)
|
|
35
|
+
.map((item) => ({
|
|
36
|
+
id: `VP-STATUS-${item.label.toUpperCase()}-INVALID`,
|
|
37
|
+
severity: 'block',
|
|
38
|
+
file: toWorkspacePath(root, item.filePath),
|
|
39
|
+
detail: `${item.label} JSON is invalid: ${item.error}`
|
|
40
|
+
}));
|
|
41
|
+
return {
|
|
42
|
+
initialized: true,
|
|
43
|
+
repo_root: root,
|
|
44
|
+
workspace: '.vibepro',
|
|
45
|
+
workspace_status: 'needs_repair',
|
|
46
|
+
current_story_id: null,
|
|
47
|
+
active_stories: [],
|
|
48
|
+
latest_run: null,
|
|
49
|
+
selected_story_latest_run: null,
|
|
50
|
+
gate_status: 'blocked',
|
|
51
|
+
finding_count: issues.length,
|
|
52
|
+
artifacts: {},
|
|
53
|
+
doctor: {
|
|
54
|
+
overall_status: 'needs_repair',
|
|
55
|
+
check_count: issues.length,
|
|
56
|
+
blocking_check_ids: issues.map((issue) => issue.id),
|
|
57
|
+
next_actions: [{
|
|
58
|
+
command: `repair ${toWorkspacePath(root, configRead.ok ? manifestPath : configPath)}`,
|
|
59
|
+
reason: 'VibePro workspace JSON must be valid before status, diagnose, or PR gates can be trusted.'
|
|
60
|
+
}]
|
|
61
|
+
},
|
|
62
|
+
issues,
|
|
63
|
+
next_commands: [
|
|
64
|
+
`repair ${toWorkspacePath(root, configRead.ok ? manifestPath : configPath)}`,
|
|
65
|
+
`vibepro status ${root} --json`
|
|
66
|
+
]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const config = configRead.value;
|
|
71
|
+
const manifest = manifestRead.value;
|
|
72
|
+
const currentStoryId = config.brainbase?.current_story_id ?? null;
|
|
73
|
+
const activeStories = prioritizeStory(normalizeStatusStories(config.brainbase?.stories), currentStoryId);
|
|
74
|
+
const selectedStory = activeStories.find((story) => story.story_id === currentStoryId) ?? null;
|
|
75
|
+
const runs = Array.isArray(manifest.runs) ? manifest.runs : [];
|
|
76
|
+
const latestRun = findRun(runs, manifest.latest_run) ?? runs[0] ?? null;
|
|
77
|
+
const selectedStoryLatestRun = selectedStory
|
|
78
|
+
? findLatestStoryRun(manifest, runs, selectedStory.story_id)
|
|
79
|
+
: null;
|
|
80
|
+
const primaryRun = selectedStoryLatestRun ?? latestRun;
|
|
81
|
+
const evidence = primaryRun ? await readRunEvidence(root, primaryRun) : null;
|
|
82
|
+
const findings = Array.isArray(evidence?.findings) ? evidence.findings : [];
|
|
83
|
+
const doctor = await runDoctor(root, { writeArtifacts: false });
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
initialized: true,
|
|
87
|
+
repo_root: root,
|
|
88
|
+
workspace: '.vibepro',
|
|
89
|
+
current_story_id: currentStoryId,
|
|
90
|
+
active_stories: activeStories,
|
|
91
|
+
latest_run: latestRun,
|
|
92
|
+
selected_story_latest_run: selectedStoryLatestRun,
|
|
93
|
+
gate_status: primaryRun?.gate_status ?? evidence?.gates?.[0]?.status ?? null,
|
|
94
|
+
finding_count: findings.length,
|
|
95
|
+
artifacts: primaryRun?.artifacts ?? {},
|
|
96
|
+
doctor: {
|
|
97
|
+
overall_status: doctor.overall_status,
|
|
98
|
+
check_count: doctor.checks.length,
|
|
99
|
+
blocking_check_ids: doctor.checks
|
|
100
|
+
.filter((check) => ['fixable', 'manual'].includes(check.status))
|
|
101
|
+
.map((check) => check.id),
|
|
102
|
+
next_actions: doctor.next_actions
|
|
103
|
+
},
|
|
104
|
+
next_commands: buildNextCommands(root, {
|
|
105
|
+
activeStories,
|
|
106
|
+
selectedStory,
|
|
107
|
+
latestRun,
|
|
108
|
+
selectedStoryLatestRun,
|
|
109
|
+
doctor
|
|
110
|
+
})
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function renderRepoStatus(status) {
|
|
115
|
+
const latestRun = status.latest_run;
|
|
116
|
+
const selectedStoryRun = status.selected_story_latest_run;
|
|
117
|
+
return `# VibePro Status
|
|
118
|
+
|
|
119
|
+
| 項目 | 内容 |
|
|
120
|
+
|------|------|
|
|
121
|
+
| Initialized | ${status.initialized ? 'yes' : 'no'} |
|
|
122
|
+
| Workspace | ${status.workspace} |
|
|
123
|
+
| Workspace Status | ${status.workspace_status ?? 'ok'} |
|
|
124
|
+
| Selected Story | ${status.current_story_id ?? '-'} |
|
|
125
|
+
| Active Stories | ${status.active_stories.length} |
|
|
126
|
+
| Latest Run | ${latestRun?.run_id ?? '-'} |
|
|
127
|
+
| Selected Story Latest Run | ${selectedStoryRun?.run_id ?? '-'} |
|
|
128
|
+
| Gate | ${status.gate_status ?? '-'} |
|
|
129
|
+
| Findings | ${status.finding_count} |
|
|
130
|
+
| Doctor | ${status.doctor?.overall_status ?? '-'} |
|
|
131
|
+
|
|
132
|
+
## Active Stories
|
|
133
|
+
|
|
134
|
+
${status.active_stories.length === 0 ? '- なし' : status.active_stories.map((story) => `- ${story.story_id}: ${story.title} / view:${story.view ?? '-'} / period:${story.period ?? '-'}`).join('\n')}
|
|
135
|
+
|
|
136
|
+
## Artifacts
|
|
137
|
+
|
|
138
|
+
${Object.entries(status.artifacts).length === 0 ? '- なし' : Object.entries(status.artifacts).map(([key, value]) => `- ${key}: ${value}`).join('\n')}
|
|
139
|
+
|
|
140
|
+
## Doctor
|
|
141
|
+
|
|
142
|
+
${renderDoctorStatus(status.doctor)}
|
|
143
|
+
|
|
144
|
+
## Next Commands
|
|
145
|
+
|
|
146
|
+
${status.next_commands.map((command) => `- ${command}`).join('\n')}
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function exists(filePath) {
|
|
151
|
+
try {
|
|
152
|
+
await access(filePath);
|
|
153
|
+
return true;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error.code === 'ENOENT') return false;
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function readJsonWithStatus(filePath, label) {
|
|
161
|
+
try {
|
|
162
|
+
return {
|
|
163
|
+
ok: true,
|
|
164
|
+
label,
|
|
165
|
+
filePath,
|
|
166
|
+
value: JSON.parse(await readFile(filePath, 'utf8'))
|
|
167
|
+
};
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error instanceof SyntaxError) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
label,
|
|
173
|
+
filePath,
|
|
174
|
+
error: error.message
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function toWorkspacePath(repoRoot, filePath) {
|
|
182
|
+
return path.relative(repoRoot, filePath).split(path.sep).join('/');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function findRun(runs, runId) {
|
|
186
|
+
if (!runId) return null;
|
|
187
|
+
return runs.find((run) => run.run_id === runId) ?? null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function prioritizeStory(stories, storyId) {
|
|
191
|
+
if (!storyId) return stories;
|
|
192
|
+
const selected = stories.find((story) => story.story_id === storyId);
|
|
193
|
+
if (!selected) return stories;
|
|
194
|
+
return [selected, ...stories.filter((story) => story.story_id !== storyId)];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeStatusStories(stories) {
|
|
198
|
+
const sourceStories = Array.isArray(stories) ? stories : DEFAULT_BRAINBASE_STORIES;
|
|
199
|
+
return sourceStories
|
|
200
|
+
.filter((story) => story.status !== 'archived')
|
|
201
|
+
.map((story) => ({
|
|
202
|
+
story_id: story.story_id,
|
|
203
|
+
title: story.title,
|
|
204
|
+
ssot: story.ssot ?? 'NocoDB',
|
|
205
|
+
status: story.status ?? 'active',
|
|
206
|
+
horizon: story.horizon ?? null,
|
|
207
|
+
view: typeof story.view === 'string' ? story.view : null,
|
|
208
|
+
period: typeof story.period === 'string' ? story.period : null,
|
|
209
|
+
started_at: story.started_at ?? null,
|
|
210
|
+
due_at: story.due_at ?? null
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function findLatestStoryRun(manifest, runs, storyId) {
|
|
215
|
+
const latestStoryRunId = manifest.latest_run_by_story?.[storyId] ?? null;
|
|
216
|
+
return findRun(runs, latestStoryRunId)
|
|
217
|
+
?? runs.find((run) => run.story_id === storyId)
|
|
218
|
+
?? null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function readRunEvidence(repoRoot, run) {
|
|
222
|
+
const evidencePath = run.artifacts?.evidence;
|
|
223
|
+
if (!evidencePath) return null;
|
|
224
|
+
try {
|
|
225
|
+
return JSON.parse(await readFile(path.resolve(repoRoot, evidencePath), 'utf8'));
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error.code === 'ENOENT') return null;
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildNextCommands(repoRoot, { activeStories, selectedStory, latestRun, selectedStoryLatestRun, doctor = null }) {
|
|
233
|
+
if (doctor && ['needs_maintenance', 'fixed'].includes(doctor.overall_status)) {
|
|
234
|
+
return doctor.next_commands?.length > 0
|
|
235
|
+
? doctor.next_commands
|
|
236
|
+
: [`vibepro doctor ${repoRoot}`, `vibepro doctor ${repoRoot} --fix`];
|
|
237
|
+
}
|
|
238
|
+
if (activeStories.length === 0) {
|
|
239
|
+
return [`vibepro story add ${repoRoot} --id <story-id> --title "<title>"`];
|
|
240
|
+
}
|
|
241
|
+
if (!selectedStory) {
|
|
242
|
+
return [`vibepro story select ${repoRoot} --id ${activeStories[0].story_id}`];
|
|
243
|
+
}
|
|
244
|
+
if (!latestRun && !selectedStoryLatestRun) {
|
|
245
|
+
return [`vibepro story diagnose ${repoRoot} --id ${selectedStory.story_id} --run-graphify`];
|
|
246
|
+
}
|
|
247
|
+
return [
|
|
248
|
+
`vibepro story report ${repoRoot} --id ${selectedStory.story_id}`,
|
|
249
|
+
`vibepro brainbase ${repoRoot}`
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function renderDoctorStatus(doctor) {
|
|
254
|
+
if (!doctor) return '- なし';
|
|
255
|
+
const checks = doctor.blocking_check_ids?.length > 0
|
|
256
|
+
? doctor.blocking_check_ids.join(', ')
|
|
257
|
+
: '-';
|
|
258
|
+
const actions = doctor.next_actions?.length > 0
|
|
259
|
+
? doctor.next_actions.map((action) => ` - ${action.command}: ${action.reason}`).join('\n')
|
|
260
|
+
: ' - なし';
|
|
261
|
+
return `- overall: ${doctor.overall_status}
|
|
262
|
+
- checks: ${doctor.check_count}
|
|
263
|
+
- needs action: ${checks}
|
|
264
|
+
- next actions:
|
|
265
|
+
${actions}`;
|
|
266
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
import { preparePullRequest } from './pr-manager.js';
|
|
7
|
+
import { readDrift, readInferredSpec } from './spec-store.js';
|
|
8
|
+
import { readNarrative, REPORT_KINDS } from './report-store.js';
|
|
9
|
+
import { getWorkspaceDir } from './workspace.js';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
export async function buildReportFingerprint(repoRoot, options = {}) {
|
|
14
|
+
const root = path.resolve(repoRoot);
|
|
15
|
+
const kind = options.kind;
|
|
16
|
+
if (!REPORT_KINDS.has(kind)) {
|
|
17
|
+
throw new Error(`Unsupported report kind: ${kind}`);
|
|
18
|
+
}
|
|
19
|
+
const storyId = options.storyId ?? null;
|
|
20
|
+
if (!storyId) {
|
|
21
|
+
throw new Error('storyId is required');
|
|
22
|
+
}
|
|
23
|
+
if (kind === 'pr-body') {
|
|
24
|
+
return buildPrBodyFingerprint(root, { ...options, storyId });
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Unsupported report kind: ${kind}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function buildPrBodyFingerprint(root, options) {
|
|
30
|
+
const storyId = options.storyId;
|
|
31
|
+
const prepare = await preparePullRequest(root, {
|
|
32
|
+
storyId,
|
|
33
|
+
baseRef: options.baseRef ?? null,
|
|
34
|
+
taskId: options.taskId ?? null,
|
|
35
|
+
groupId: options.groupId ?? null,
|
|
36
|
+
branchName: options.branchName ?? null,
|
|
37
|
+
allowExtraFiles: true
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const preparation = prepare?.preparation ?? null;
|
|
41
|
+
const previousNarrative = await readNarrative(root, storyId, 'pr-body');
|
|
42
|
+
const inferredSpec = await readInferredSpec(root, storyId);
|
|
43
|
+
const drift = await readDrift(root, storyId);
|
|
44
|
+
const findings = await readLatestFindings(root, preparation?.latest_story_run);
|
|
45
|
+
|
|
46
|
+
const fingerprint = {
|
|
47
|
+
schema_version: '0.1.0',
|
|
48
|
+
kind: 'pr-body',
|
|
49
|
+
story_id: storyId,
|
|
50
|
+
generated_at: new Date().toISOString(),
|
|
51
|
+
story: preparation?.story ?? null,
|
|
52
|
+
pr_context: extractPrContextSummary(preparation),
|
|
53
|
+
file_groups: summarizeFileGroups(preparation?.file_groups),
|
|
54
|
+
gate_dag: summarizeGateDag(preparation?.pr_context?.gate_dag),
|
|
55
|
+
requirement_consistency: summarizeRequirement(preparation?.pr_context?.requirement_consistency),
|
|
56
|
+
inferred_spec: inferredSpec ? {
|
|
57
|
+
story_id: inferredSpec.story_id,
|
|
58
|
+
clauses: (inferredSpec.clauses ?? []).map((clause) => ({
|
|
59
|
+
id: clause.id,
|
|
60
|
+
type: clause.type,
|
|
61
|
+
statement: clause.statement
|
|
62
|
+
}))
|
|
63
|
+
} : null,
|
|
64
|
+
drift: drift ? {
|
|
65
|
+
status: drift.status,
|
|
66
|
+
summary: drift.summary,
|
|
67
|
+
items: (drift.items ?? []).map((item) => ({
|
|
68
|
+
id: item.id,
|
|
69
|
+
axis: item.axis,
|
|
70
|
+
clause_id: item.clause_id ?? null,
|
|
71
|
+
severity: item.severity,
|
|
72
|
+
title: item.title
|
|
73
|
+
}))
|
|
74
|
+
} : null,
|
|
75
|
+
findings: findings.map((finding) => ({
|
|
76
|
+
id: finding.id,
|
|
77
|
+
title: finding.title,
|
|
78
|
+
severity: finding.severity ?? null,
|
|
79
|
+
type: finding.type ?? null
|
|
80
|
+
})),
|
|
81
|
+
numerical_truth: buildNumericalTruth({ preparation, drift, requirementConsistency: preparation?.pr_context?.requirement_consistency }),
|
|
82
|
+
previous_narrative: previousNarrative,
|
|
83
|
+
schema_for_your_output: await readJson(path.join(__dirname, 'report-pr-body-schema.json')),
|
|
84
|
+
instructions: options.includeInstructions
|
|
85
|
+
? await readFile(path.join(__dirname, 'report-pr-body-prompt-template.md'), 'utf8')
|
|
86
|
+
: null
|
|
87
|
+
};
|
|
88
|
+
fingerprint.inputs_digest = buildInputsDigest(fingerprint);
|
|
89
|
+
return fingerprint;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function extractPrContextSummary(preparation) {
|
|
93
|
+
if (!preparation) return null;
|
|
94
|
+
const ctx = preparation.pr_context ?? {};
|
|
95
|
+
return {
|
|
96
|
+
story_source_path: ctx.story_source?.path ?? null,
|
|
97
|
+
architecture_decision: ctx.architecture_decision ?? null,
|
|
98
|
+
change_summary: ctx.change_summary ?? [],
|
|
99
|
+
review_points: ctx.review_points ?? [],
|
|
100
|
+
risks: ctx.risks ?? [],
|
|
101
|
+
verification_commands: (ctx.verification_commands ?? []).map((entry) => ({
|
|
102
|
+
command: entry.command,
|
|
103
|
+
reason: entry.reason
|
|
104
|
+
}))
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function summarizeFileGroups(fileGroups) {
|
|
109
|
+
if (!fileGroups) return null;
|
|
110
|
+
const summary = {};
|
|
111
|
+
for (const [key, value] of Object.entries(fileGroups)) {
|
|
112
|
+
summary[key] = {
|
|
113
|
+
count: value.count,
|
|
114
|
+
files: (value.files ?? []).slice(0, 12)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return summary;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function summarizeGateDag(gateDag) {
|
|
121
|
+
if (!gateDag) return null;
|
|
122
|
+
return {
|
|
123
|
+
overall_status: gateDag.overall_status ?? null,
|
|
124
|
+
nodes: (gateDag.nodes ?? []).map((node) => ({
|
|
125
|
+
id: node.id,
|
|
126
|
+
type: node.type,
|
|
127
|
+
status: node.status,
|
|
128
|
+
required: node.required ?? null,
|
|
129
|
+
reason: node.reason ?? null
|
|
130
|
+
}))
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function summarizeRequirement(requirement) {
|
|
135
|
+
if (!requirement) return null;
|
|
136
|
+
return {
|
|
137
|
+
status: requirement.status,
|
|
138
|
+
summary: requirement.summary,
|
|
139
|
+
invariants: (requirement.invariants ?? []).map((entry) => ({ id: entry.id, text: entry.text })),
|
|
140
|
+
contradictions: (requirement.contradictions ?? []).map((entry) => ({ id: entry.id, title: entry.title })),
|
|
141
|
+
scenario_gaps: (requirement.scenario_gaps ?? []).map((entry) => ({ id: entry.id, title: entry.title }))
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function readLatestFindings(root, latestStoryRun) {
|
|
146
|
+
const evidencePath = latestStoryRun?.artifacts?.evidence;
|
|
147
|
+
if (!evidencePath) return [];
|
|
148
|
+
try {
|
|
149
|
+
const evidence = JSON.parse(await readFile(path.resolve(root, evidencePath), 'utf8'));
|
|
150
|
+
return Array.isArray(evidence.findings) ? evidence.findings.slice(0, 40) : [];
|
|
151
|
+
} catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildNumericalTruth({ preparation, drift, requirementConsistency }) {
|
|
157
|
+
const driftItems = drift?.items ?? [];
|
|
158
|
+
return {
|
|
159
|
+
changed_files_count: preparation?.git?.changed_files?.length ?? 0,
|
|
160
|
+
drift_total_count: driftItems.length,
|
|
161
|
+
drift_high_count: driftItems.filter((item) => item.severity === 'high').length,
|
|
162
|
+
requirement_invariant_count: requirementConsistency?.summary?.invariant_count ?? 0,
|
|
163
|
+
requirement_contradiction_count: requirementConsistency?.summary?.contradiction_count ?? 0,
|
|
164
|
+
acceptance_criteria_count: preparation?.pr_context?.story_source?.acceptance_criteria?.length ?? 0
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildInputsDigest(fingerprint) {
|
|
169
|
+
return {
|
|
170
|
+
story_sha: sha256(fingerprint.story),
|
|
171
|
+
pr_context_sha: sha256(fingerprint.pr_context),
|
|
172
|
+
drift_sha: sha256(fingerprint.drift),
|
|
173
|
+
spec_sha: sha256(fingerprint.inferred_spec)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function sha256(value) {
|
|
178
|
+
const hash = createHash('sha256');
|
|
179
|
+
hash.update(JSON.stringify(value ?? null));
|
|
180
|
+
return `sha256:${hash.digest('hex')}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function readJson(filePath) {
|
|
184
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// re-export utility for tests
|
|
188
|
+
export { getWorkspaceDir };
|