@sandrinio/vbounce 1.7.0 → 1.9.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/README.md +168 -160
- package/bin/vbounce.mjs +250 -25
- package/brains/AGENTS.md +4 -4
- package/brains/CHANGELOG.md +2 -2
- package/brains/CLAUDE.md +4 -4
- package/brains/GEMINI.md +5 -5
- package/brains/SETUP.md +15 -15
- package/brains/claude-agents/architect.md +1 -1
- package/brains/claude-agents/developer.md +1 -1
- package/brains/claude-agents/devops.md +1 -1
- package/brains/claude-agents/qa.md +2 -1
- package/brains/claude-agents/scribe.md +1 -1
- package/brains/copilot/copilot-instructions.md +3 -3
- package/brains/cursor-rules/vbounce-docs.mdc +2 -2
- package/brains/cursor-rules/vbounce-process.mdc +3 -3
- package/brains/cursor-rules/vbounce-rules.mdc +2 -2
- package/brains/windsurf/.windsurfrules +2 -2
- package/docs/HOTFIX_EDGE_CASES.md +1 -1
- package/package.json +5 -5
- package/scripts/doctor.mjs +3 -3
- package/scripts/hotfix_manager.sh +2 -2
- package/scripts/init_gate_config.sh +1 -1
- package/scripts/pre_gate_common.sh +1 -1
- package/scripts/pre_gate_runner.sh +1 -1
- package/scripts/prep_qa_context.mjs +19 -1
- package/scripts/prep_sprint_context.mjs +24 -1
- package/scripts/suggest_improvements.mjs +1 -1
- package/scripts/validate_bounce_readiness.mjs +27 -0
- package/scripts/validate_report.mjs +1 -1
- package/scripts/vdoc_match.mjs +269 -0
- package/scripts/vdoc_staleness.mjs +199 -0
- package/scripts/verify_framework.mjs +1 -1
- package/skills/agent-team/SKILL.md +18 -11
- package/skills/doc-manager/SKILL.md +5 -5
- package/skills/improve/SKILL.md +2 -2
- package/templates/sprint_report.md +6 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Hotfix Workflow: Edge Cases & Mitigations
|
|
2
2
|
|
|
3
|
-
This document outlines the critical edge cases, failure modes, and required mitigations for the **V-Bounce
|
|
3
|
+
This document outlines the critical edge cases, failure modes, and required mitigations for the **V-Bounce Engine Hotfix (L1 Trivial)** workflow.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandrinio/vbounce",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "V-Bounce
|
|
3
|
+
"version": "1.9.0",
|
|
4
|
+
"description": "V-Bounce Engine: Turn your AI coding assistant into a full engineering team through structured SDLC skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"vbounce": "./bin/vbounce.mjs"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/sandrinio/v-bounce-
|
|
14
|
+
"url": "git+https://github.com/sandrinio/v-bounce-engine.git"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"ai",
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"author": "sandrinio",
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"bugs": {
|
|
31
|
-
"url": "https://github.com/sandrinio/v-bounce-
|
|
31
|
+
"url": "https://github.com/sandrinio/v-bounce-engine/issues"
|
|
32
32
|
},
|
|
33
|
-
"homepage": "https://github.com/sandrinio/v-bounce-
|
|
33
|
+
"homepage": "https://github.com/sandrinio/v-bounce-engine#readme",
|
|
34
34
|
"files": [
|
|
35
35
|
"bin",
|
|
36
36
|
"brains",
|
package/scripts/doctor.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* doctor.mjs
|
|
5
|
-
* V-Bounce
|
|
5
|
+
* V-Bounce Engine Health Check — validates all configs, templates, state files
|
|
6
6
|
* Usage: vbounce doctor
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -42,7 +42,7 @@ const templatesDir = path.join(ROOT, 'templates');
|
|
|
42
42
|
let templateCount = 0;
|
|
43
43
|
for (const t of requiredTemplates) {
|
|
44
44
|
if (fs.existsSync(path.join(templatesDir, t))) templateCount++;
|
|
45
|
-
else fail(`templates/${t} missing`, `Create from V-Bounce
|
|
45
|
+
else fail(`templates/${t} missing`, `Create from V-Bounce Engine template`);
|
|
46
46
|
}
|
|
47
47
|
if (templateCount === requiredTemplates.length) pass(`templates/ complete (${templateCount}/${requiredTemplates.length})`);
|
|
48
48
|
|
|
@@ -130,7 +130,7 @@ if (fs.existsSync(path.join(ROOT, 'vbounce.config.json'))) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// Print results
|
|
133
|
-
console.log('\nV-Bounce
|
|
133
|
+
console.log('\nV-Bounce Engine Health Check');
|
|
134
134
|
console.log('========================');
|
|
135
135
|
checks.forEach(c => console.log(c));
|
|
136
136
|
console.log('');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
# V-Bounce
|
|
3
|
+
# V-Bounce Engine: Hotfix Manager
|
|
4
4
|
# Handles edge cases for L1 Trivial tasks to save tokens and ensure framework integrity.
|
|
5
5
|
|
|
6
6
|
set -euo pipefail
|
|
@@ -14,7 +14,7 @@ REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || {
|
|
|
14
14
|
COMMAND="${1:-}"
|
|
15
15
|
|
|
16
16
|
function show_help {
|
|
17
|
-
echo "V-Bounce
|
|
17
|
+
echo "V-Bounce Engine — Hotfix Manager"
|
|
18
18
|
echo ""
|
|
19
19
|
echo "Usage: ./scripts/hotfix_manager.sh <command> [args]"
|
|
20
20
|
echo ""
|
|
@@ -31,7 +31,7 @@ fi
|
|
|
31
31
|
# Resolve to absolute path
|
|
32
32
|
WORKTREE_PATH="$(cd "$WORKTREE_PATH" && pwd)"
|
|
33
33
|
|
|
34
|
-
echo -e "${CYAN}V-Bounce
|
|
34
|
+
echo -e "${CYAN}V-Bounce Engine Pre-Gate Scanner${NC}"
|
|
35
35
|
echo -e "Gate: ${YELLOW}${GATE_TYPE}${NC}"
|
|
36
36
|
echo -e "Target: ${WORKTREE_PATH}"
|
|
37
37
|
echo ""
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import fs from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
|
+
import { execSync } from 'child_process';
|
|
16
17
|
import yaml from 'js-yaml';
|
|
17
18
|
|
|
18
19
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -96,7 +97,23 @@ const filesModified = Array.isArray(devFm.files_modified)
|
|
|
96
97
|
? devFm.files_modified.map(f => `- ${f}`).join('\n')
|
|
97
98
|
: '_Not specified in dev report_';
|
|
98
99
|
|
|
99
|
-
// 5.
|
|
100
|
+
// 5. vdoc context (optional — graceful skip if no manifest)
|
|
101
|
+
let vdocSection = '';
|
|
102
|
+
const vdocMatchScript = path.join(__dirname, 'vdoc_match.mjs');
|
|
103
|
+
if (fs.existsSync(vdocMatchScript) && fs.existsSync(path.join(ROOT, 'vdocs', '_manifest.json'))) {
|
|
104
|
+
try {
|
|
105
|
+
const modifiedFiles = Array.isArray(devFm.files_modified) ? devFm.files_modified.join(',') : '';
|
|
106
|
+
const vdocArgs = [`--story`, storyId];
|
|
107
|
+
if (modifiedFiles) vdocArgs.push('--files', modifiedFiles);
|
|
108
|
+
vdocArgs.push('--context');
|
|
109
|
+
const vdocOutput = execSync(`node "${vdocMatchScript}" ${vdocArgs.map(a => `"${a}"`).join(' ')}`, { cwd: ROOT, encoding: 'utf8', timeout: 10000 }).trim();
|
|
110
|
+
if (vdocOutput && !vdocOutput.includes('No vdoc matches')) {
|
|
111
|
+
vdocSection = vdocOutput;
|
|
112
|
+
}
|
|
113
|
+
} catch { /* vdoc matching failed — skip silently */ }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 6. Assemble context pack
|
|
100
117
|
const lines = [
|
|
101
118
|
`# QA Context: ${storyId}`,
|
|
102
119
|
`> Generated: ${new Date().toISOString().split('T')[0]}`,
|
|
@@ -117,6 +134,7 @@ const lines = [
|
|
|
117
134
|
`## Files Modified`,
|
|
118
135
|
filesModified,
|
|
119
136
|
'',
|
|
137
|
+
...(vdocSection ? [vdocSection, ''] : []),
|
|
120
138
|
`## Relevant Lessons`,
|
|
121
139
|
lessonsExcerpt,
|
|
122
140
|
];
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import fs from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
|
+
import { execSync } from 'child_process';
|
|
16
17
|
|
|
17
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
19
|
const ROOT = path.resolve(__dirname, '..');
|
|
@@ -75,7 +76,28 @@ const storyRows = Object.entries(state.stories || {})
|
|
|
75
76
|
.map(([id, s]) => `| ${id} | ${s.state} | ${s.qa_bounces} | ${s.arch_bounces} | ${s.worktree || '—'} |`)
|
|
76
77
|
.join('\n');
|
|
77
78
|
|
|
78
|
-
// 6.
|
|
79
|
+
// 6. vdoc summary (optional — graceful skip if no manifest)
|
|
80
|
+
let vdocSummary = '';
|
|
81
|
+
const manifestPath = path.join(ROOT, 'vdocs', '_manifest.json');
|
|
82
|
+
if (fs.existsSync(manifestPath)) {
|
|
83
|
+
try {
|
|
84
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
85
|
+
const docCount = (manifest.documentation || []).length;
|
|
86
|
+
const docList = (manifest.documentation || []).slice(0, 10)
|
|
87
|
+
.map(d => `| ${d.filepath} | ${d.title} | ${(d.tags || []).slice(0, 4).join(', ')} | ${(d.deps || []).join(', ') || '—'} |`)
|
|
88
|
+
.join('\n');
|
|
89
|
+
vdocSummary = [
|
|
90
|
+
`## Product Documentation (vdoc)`,
|
|
91
|
+
`> ${docCount} feature doc(s) available in vdocs/`,
|
|
92
|
+
'',
|
|
93
|
+
`| Doc | Title | Tags | Dependencies |`,
|
|
94
|
+
`|-----|-------|------|-------------|`,
|
|
95
|
+
docList,
|
|
96
|
+
].join('\n');
|
|
97
|
+
} catch { /* skip on parse error */ }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 7. Assemble context pack
|
|
79
101
|
const lines = [
|
|
80
102
|
`# Sprint Context: ${sprintId}`,
|
|
81
103
|
`> Generated: ${new Date().toISOString().split('T')[0]} | Sprint: ${sprintId} | Delivery: ${state.delivery_id}`,
|
|
@@ -91,6 +113,7 @@ const lines = [
|
|
|
91
113
|
`|-------|-------|------------|--------------|----------|`,
|
|
92
114
|
storyRows || '| (no stories) | — | — | — | — |',
|
|
93
115
|
'',
|
|
116
|
+
...(vdocSummary ? [vdocSummary, ''] : []),
|
|
94
117
|
`## Relevant Lessons`,
|
|
95
118
|
lessonsExcerpt,
|
|
96
119
|
'',
|
|
@@ -159,7 +159,7 @@ suggestions.push({
|
|
|
159
159
|
num: itemNum++,
|
|
160
160
|
category: 'Health',
|
|
161
161
|
title: 'Run vbounce doctor',
|
|
162
|
-
detail: 'Verify the V-Bounce
|
|
162
|
+
detail: 'Verify the V-Bounce Engine installation is healthy after this sprint.',
|
|
163
163
|
recommendation: 'Run: `vbounce doctor` — checks brain files, templates, scripts, state.json validity.',
|
|
164
164
|
target: 'scripts/doctor.mjs',
|
|
165
165
|
effort: 'Trivial',
|
|
@@ -102,6 +102,33 @@ if (!fs.existsSync(worktreeDir)) {
|
|
|
102
102
|
warnings.push(`.worktrees/${storyId}/ not found — create with: git worktree add .worktrees/${storyId} -b story/${storyId} sprint/S-XX`);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// 5. vdoc impact check (warning only — never blocks bounce)
|
|
106
|
+
const manifestPath = path.join(ROOT, 'vdocs', '_manifest.json');
|
|
107
|
+
if (fs.existsSync(manifestPath) && storyFile) {
|
|
108
|
+
try {
|
|
109
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
110
|
+
const docs = manifest.documentation || [];
|
|
111
|
+
const storyLower = fs.readFileSync(storyFile, 'utf8').toLowerCase();
|
|
112
|
+
|
|
113
|
+
// Extract file paths mentioned in the story
|
|
114
|
+
const storyFileRefs = storyLower.match(/(?:src|lib|app|pages|components|api|services|scripts)\/[^\s,)'"]+/g) || [];
|
|
115
|
+
|
|
116
|
+
for (const doc of docs) {
|
|
117
|
+
const docKeyFiles = (doc.keyFiles || []).map(f => f.toLowerCase());
|
|
118
|
+
const overlap = docKeyFiles.filter(kf =>
|
|
119
|
+
storyFileRefs.some(sf => sf.includes(kf) || kf.includes(sf))
|
|
120
|
+
);
|
|
121
|
+
if (overlap.length > 0) {
|
|
122
|
+
warnings.push(`vdoc impact: ${doc.filepath} — key files overlap with story scope (${overlap.slice(0, 3).join(', ')}). Doc may need updating post-sprint.`);
|
|
123
|
+
const deps = doc.deps || [];
|
|
124
|
+
if (deps.length > 0) {
|
|
125
|
+
warnings.push(` ↳ Blast radius: ${deps.join(', ')} may also be affected`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch { /* skip on manifest parse error */ }
|
|
130
|
+
}
|
|
131
|
+
|
|
105
132
|
// Print results
|
|
106
133
|
console.log(`Bounce readiness check: ${storyId}`);
|
|
107
134
|
console.log('');
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* validate_report.mjs
|
|
5
5
|
*
|
|
6
|
-
* Strict YAML Frontmatter validation for V-Bounce
|
|
6
|
+
* Strict YAML Frontmatter validation for V-Bounce Engine Agent Reports.
|
|
7
7
|
* Fails loudly if an agent hallucinates formatting or omits required fields,
|
|
8
8
|
* so the orchestrator can bounce the prompt back.
|
|
9
9
|
*/
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* vdoc_match.mjs
|
|
5
|
+
* Reads vdocs/_manifest.json and matches story scope against doc tags,
|
|
6
|
+
* descriptions, and key files. Returns relevant doc paths and blast radius.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ./scripts/vdoc_match.mjs --story STORY-005-02
|
|
10
|
+
* ./scripts/vdoc_match.mjs --files "src/auth/index.ts,src/middleware/auth.ts"
|
|
11
|
+
* ./scripts/vdoc_match.mjs --keywords "authentication,jwt,login"
|
|
12
|
+
*
|
|
13
|
+
* Output: JSON to stdout, human-readable to stderr
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
22
|
+
|
|
23
|
+
// ── Config ────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const configPath = path.join(ROOT, 'vbounce.config.json');
|
|
26
|
+
const config = fs.existsSync(configPath)
|
|
27
|
+
? JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
|
28
|
+
: {};
|
|
29
|
+
|
|
30
|
+
const MAX_MATCHES = config.vdocMaxMatches || 3;
|
|
31
|
+
const VDOCS_DIR = path.join(ROOT, 'vdocs');
|
|
32
|
+
const MANIFEST_PATH = path.join(VDOCS_DIR, '_manifest.json');
|
|
33
|
+
const SLICES_DIR = path.join(VDOCS_DIR, '_slices');
|
|
34
|
+
|
|
35
|
+
// ── Parse args ────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const args = process.argv.slice(2);
|
|
38
|
+
let storyId = null;
|
|
39
|
+
let inputFiles = [];
|
|
40
|
+
let inputKeywords = [];
|
|
41
|
+
let outputFormat = 'human'; // human | json | context
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < args.length; i++) {
|
|
44
|
+
switch (args[i]) {
|
|
45
|
+
case '--story': storyId = args[++i]; break;
|
|
46
|
+
case '--files': inputFiles = args[++i].split(',').map(f => f.trim()); break;
|
|
47
|
+
case '--keywords': inputKeywords = args[++i].split(',').map(k => k.trim().toLowerCase()); break;
|
|
48
|
+
case '--json': outputFormat = 'json'; break;
|
|
49
|
+
case '--context': outputFormat = 'context'; break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!storyId && inputFiles.length === 0 && inputKeywords.length === 0) {
|
|
54
|
+
console.error('Usage: vdoc_match.mjs --story STORY-ID | --files "f1,f2" | --keywords "k1,k2"');
|
|
55
|
+
console.error(' --json Output JSON to stdout');
|
|
56
|
+
console.error(' --context Output context-pack markdown to stdout');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Load manifest ─────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(MANIFEST_PATH)) {
|
|
63
|
+
if (outputFormat === 'json') {
|
|
64
|
+
console.log(JSON.stringify({ matches: [], blastRadius: [], reason: 'no_manifest' }));
|
|
65
|
+
} else {
|
|
66
|
+
console.error('No vdocs/_manifest.json found — skipping vdoc matching');
|
|
67
|
+
}
|
|
68
|
+
process.exit(0); // Graceful no-op
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
|
|
72
|
+
const docs = manifest.documentation || [];
|
|
73
|
+
|
|
74
|
+
// ── Extract search terms from story ───────────────────────────────
|
|
75
|
+
|
|
76
|
+
if (storyId) {
|
|
77
|
+
// Find story spec to extract keywords and file references
|
|
78
|
+
const storyPattern = new RegExp(storyId.replace(/[-]/g, '[-]'));
|
|
79
|
+
const storyFiles = findFiles(path.join(ROOT, 'product_plans'), storyPattern);
|
|
80
|
+
|
|
81
|
+
if (storyFiles.length > 0) {
|
|
82
|
+
const storyContent = fs.readFileSync(storyFiles[0], 'utf8').toLowerCase();
|
|
83
|
+
|
|
84
|
+
// Extract file paths mentioned in the story
|
|
85
|
+
const fileRefs = storyContent.match(/(?:src|lib|app|pages|components|api|services|scripts)\/[^\s,)'"]+/g) || [];
|
|
86
|
+
inputFiles.push(...fileRefs);
|
|
87
|
+
|
|
88
|
+
// Extract tags from story frontmatter
|
|
89
|
+
const tagMatch = storyContent.match(/tags:\s*\[([^\]]+)\]/);
|
|
90
|
+
if (tagMatch) {
|
|
91
|
+
inputKeywords.push(...tagMatch[1].split(',').map(t => t.trim().replace(/['"]/g, '')));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Use story name parts as keywords
|
|
95
|
+
const nameParts = storyId.split('-').slice(3).join('-').replace(/_/g, ' ').split(/\s+/);
|
|
96
|
+
inputKeywords.push(...nameParts.filter(p => p.length > 2));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Score each doc ────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
const scored = docs.map(doc => {
|
|
103
|
+
let score = 0;
|
|
104
|
+
const reasons = [];
|
|
105
|
+
|
|
106
|
+
// 1. File path overlap (strongest signal)
|
|
107
|
+
const docKeyFiles = extractKeyFiles(doc);
|
|
108
|
+
const fileOverlap = inputFiles.filter(f => docKeyFiles.some(kf => kf.includes(f) || f.includes(kf)));
|
|
109
|
+
if (fileOverlap.length > 0) {
|
|
110
|
+
score += fileOverlap.length * 10;
|
|
111
|
+
reasons.push(`file overlap: ${fileOverlap.join(', ')}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 2. Tag match
|
|
115
|
+
const docTags = (doc.tags || []).map(t => t.toLowerCase());
|
|
116
|
+
const tagOverlap = inputKeywords.filter(k => docTags.includes(k));
|
|
117
|
+
if (tagOverlap.length > 0) {
|
|
118
|
+
score += tagOverlap.length * 5;
|
|
119
|
+
reasons.push(`tag match: ${tagOverlap.join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 3. Description keyword match
|
|
123
|
+
const desc = (doc.description || '').toLowerCase();
|
|
124
|
+
const descMatches = inputKeywords.filter(k => desc.includes(k));
|
|
125
|
+
if (descMatches.length > 0) {
|
|
126
|
+
score += descMatches.length * 3;
|
|
127
|
+
reasons.push(`description match: ${descMatches.join(', ')}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 4. Title keyword match
|
|
131
|
+
const title = (doc.title || '').toLowerCase();
|
|
132
|
+
const titleMatches = inputKeywords.filter(k => title.includes(k));
|
|
133
|
+
if (titleMatches.length > 0) {
|
|
134
|
+
score += titleMatches.length * 2;
|
|
135
|
+
reasons.push(`title match: ${titleMatches.join(', ')}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { ...doc, score, reasons };
|
|
139
|
+
}).filter(d => d.score > 0).sort((a, b) => b.score - a.score);
|
|
140
|
+
|
|
141
|
+
// ── Top matches ───────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
const topMatches = scored.slice(0, MAX_MATCHES);
|
|
144
|
+
|
|
145
|
+
// ── Blast radius (deps of matched docs) ──────────────────────────
|
|
146
|
+
|
|
147
|
+
const matchedFeatures = new Set(topMatches.map(d => d.title));
|
|
148
|
+
const blastRadius = [];
|
|
149
|
+
|
|
150
|
+
for (const match of topMatches) {
|
|
151
|
+
const deps = match.deps || [];
|
|
152
|
+
for (const dep of deps) {
|
|
153
|
+
if (!matchedFeatures.has(dep)) {
|
|
154
|
+
const depDoc = docs.find(d => d.title === dep || d.filepath.toLowerCase().includes(dep.toLowerCase().replace(/\s+/g, '_')));
|
|
155
|
+
if (depDoc) {
|
|
156
|
+
blastRadius.push({ feature: dep, doc: depDoc.filepath, triggeredBy: match.title });
|
|
157
|
+
} else {
|
|
158
|
+
blastRadius.push({ feature: dep, doc: null, triggeredBy: match.title });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Output ────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
const result = {
|
|
167
|
+
matches: topMatches.map(d => ({
|
|
168
|
+
filepath: d.filepath,
|
|
169
|
+
title: d.title,
|
|
170
|
+
score: d.score,
|
|
171
|
+
reasons: d.reasons,
|
|
172
|
+
deps: d.deps || [],
|
|
173
|
+
})),
|
|
174
|
+
blastRadius,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (outputFormat === 'json') {
|
|
178
|
+
console.log(JSON.stringify(result, null, 2));
|
|
179
|
+
} else if (outputFormat === 'context') {
|
|
180
|
+
// Output context-pack markdown using slices where available
|
|
181
|
+
if (topMatches.length === 0) {
|
|
182
|
+
console.log('<!-- No vdoc matches found -->');
|
|
183
|
+
} else {
|
|
184
|
+
console.log('## vdoc Context');
|
|
185
|
+
console.log('');
|
|
186
|
+
for (const match of topMatches) {
|
|
187
|
+
const sliceName = match.filepath.replace(/_DOC\.md$/, '_SLICE.md').replace(/\.md$/, '_SLICE.md');
|
|
188
|
+
const slicePath = path.join(SLICES_DIR, sliceName);
|
|
189
|
+
const fullPath = path.join(VDOCS_DIR, match.filepath);
|
|
190
|
+
|
|
191
|
+
if (fs.existsSync(slicePath)) {
|
|
192
|
+
console.log(fs.readFileSync(slicePath, 'utf8'));
|
|
193
|
+
} else if (fs.existsSync(fullPath)) {
|
|
194
|
+
// Fallback: extract Overview section from full doc
|
|
195
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
196
|
+
const overviewMatch = content.match(/## Overview\s*\n(?:<!--[^>]*-->\s*\n)?\s*([\s\S]*?)(?=\n---|\n##)/);
|
|
197
|
+
if (overviewMatch) {
|
|
198
|
+
console.log(`### ${match.title}`);
|
|
199
|
+
console.log(overviewMatch[1].trim().split('\n').slice(0, 5).join('\n'));
|
|
200
|
+
console.log('');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
console.log('');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (blastRadius.length > 0) {
|
|
207
|
+
console.log('### Blast Radius Warning');
|
|
208
|
+
console.log('Changes to matched features may also affect:');
|
|
209
|
+
for (const br of blastRadius) {
|
|
210
|
+
console.log(`- **${br.feature}** (triggered by ${br.triggeredBy})${br.doc ? ` — see ${br.doc}` : ''}`);
|
|
211
|
+
}
|
|
212
|
+
console.log('');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
// Human-readable to stderr
|
|
217
|
+
if (topMatches.length === 0) {
|
|
218
|
+
console.error('No vdoc matches found for the given scope.');
|
|
219
|
+
} else {
|
|
220
|
+
console.error(`\n✓ ${topMatches.length} vdoc match(es) found:\n`);
|
|
221
|
+
for (const match of topMatches) {
|
|
222
|
+
console.error(` ${match.filepath} (score: ${match.score})`);
|
|
223
|
+
console.error(` ${match.reasons.join(' | ')}`);
|
|
224
|
+
}
|
|
225
|
+
if (blastRadius.length > 0) {
|
|
226
|
+
console.error(`\n⚠ Blast radius — also affected:`);
|
|
227
|
+
for (const br of blastRadius) {
|
|
228
|
+
console.error(` ${br.feature} (via ${br.triggeredBy})`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
console.error('');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
function findFiles(dir, pattern) {
|
|
238
|
+
const results = [];
|
|
239
|
+
if (!fs.existsSync(dir)) return results;
|
|
240
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
241
|
+
for (const e of entries) {
|
|
242
|
+
const full = path.join(dir, e.name);
|
|
243
|
+
if (e.isDirectory()) results.push(...findFiles(full, pattern));
|
|
244
|
+
else if (pattern.test(e.name)) results.push(full);
|
|
245
|
+
}
|
|
246
|
+
return results;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function extractKeyFiles(doc) {
|
|
250
|
+
// From manifest entry — check if keyFiles exist in frontmatter-style
|
|
251
|
+
const files = [];
|
|
252
|
+
if (doc.keyFiles) files.push(...doc.keyFiles);
|
|
253
|
+
|
|
254
|
+
// Also try reading the doc to get Key Files table
|
|
255
|
+
const docPath = path.join(VDOCS_DIR, doc.filepath);
|
|
256
|
+
if (fs.existsSync(docPath)) {
|
|
257
|
+
const content = fs.readFileSync(docPath, 'utf8');
|
|
258
|
+
const keyFilesMatch = content.match(/## Key Files[\s\S]*?\|[^|]+\|[^|]+\|[^|]+\|([\s\S]*?)(?=\n---|\n##)/);
|
|
259
|
+
if (keyFilesMatch) {
|
|
260
|
+
const rows = keyFilesMatch[1].split('\n').filter(r => r.includes('|'));
|
|
261
|
+
for (const row of rows) {
|
|
262
|
+
const cells = row.split('|').map(c => c.trim());
|
|
263
|
+
const filePath = cells[1]?.replace(/`/g, '');
|
|
264
|
+
if (filePath && filePath.includes('/')) files.push(filePath);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return files;
|
|
269
|
+
}
|