vbounce-engine 2.5.1
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 +142 -0
- package/VBOUNCE_MANIFEST.md +404 -0
- package/bin/vbounce.mjs +882 -0
- package/brains/AGENTS.md +71 -0
- package/brains/CHANGELOG.md +398 -0
- package/brains/CLAUDE.md +90 -0
- package/brains/GEMINI.md +102 -0
- package/brains/SETUP.md +195 -0
- package/brains/claude-agents/architect.md +226 -0
- package/brains/claude-agents/developer.md +133 -0
- package/brains/claude-agents/devops.md +267 -0
- package/brains/claude-agents/explorer.md +157 -0
- package/brains/claude-agents/qa.md +225 -0
- package/brains/claude-agents/scribe.md +171 -0
- package/brains/copilot/copilot-instructions.md +54 -0
- package/brains/cursor-rules/vbounce-docs.mdc +45 -0
- package/brains/cursor-rules/vbounce-process.mdc +51 -0
- package/brains/cursor-rules/vbounce-rules.mdc +29 -0
- package/brains/windsurf/.windsurfrules +35 -0
- package/docs/HOTFIX_EDGE_CASES.md +37 -0
- package/docs/IMPROVEMENT.md +46 -0
- package/docs/agent-skill-profiles.docx +0 -0
- package/docs/icons/alert.svg +1 -0
- package/docs/icons/beaker.svg +1 -0
- package/docs/icons/book.svg +1 -0
- package/docs/icons/git-branch.svg +1 -0
- package/docs/icons/git-merge.svg +1 -0
- package/docs/icons/graph.svg +1 -0
- package/docs/icons/light-bulb.svg +1 -0
- package/docs/icons/logo.svg +9 -0
- package/docs/icons/pencil.svg +1 -0
- package/docs/icons/rocket.svg +1 -0
- package/docs/icons/shield.svg +1 -0
- package/docs/icons/sync.svg +1 -0
- package/docs/icons/terminal.svg +1 -0
- package/docs/icons/tools.svg +1 -0
- package/docs/icons/zap.svg +1 -0
- package/docs/images/bounce_loop_diagram.png +0 -0
- package/docs/vbounce-os-manual.docx +0 -0
- package/package.json +48 -0
- package/scripts/close_sprint.mjs +134 -0
- package/scripts/complete_story.mjs +121 -0
- package/scripts/count_tokens.mjs +494 -0
- package/scripts/doctor.mjs +144 -0
- package/scripts/hotfix_manager.sh +157 -0
- package/scripts/init_gate_config.sh +151 -0
- package/scripts/init_sprint.mjs +129 -0
- package/scripts/post_sprint_improve.mjs +486 -0
- package/scripts/pre_gate_common.sh +576 -0
- package/scripts/pre_gate_runner.sh +176 -0
- package/scripts/prep_arch_context.mjs +178 -0
- package/scripts/prep_qa_context.mjs +152 -0
- package/scripts/prep_sprint_context.mjs +141 -0
- package/scripts/prep_sprint_summary.mjs +154 -0
- package/scripts/product_graph.mjs +387 -0
- package/scripts/product_impact.mjs +167 -0
- package/scripts/sprint_trends.mjs +160 -0
- package/scripts/suggest_improvements.mjs +363 -0
- package/scripts/update_state.mjs +132 -0
- package/scripts/validate_bounce_readiness.mjs +152 -0
- package/scripts/validate_report.mjs +165 -0
- package/scripts/validate_sprint_plan.mjs +117 -0
- package/scripts/validate_state.mjs +99 -0
- package/scripts/vdoc_match.mjs +269 -0
- package/scripts/vdoc_staleness.mjs +199 -0
- package/scripts/verify_framework.mjs +122 -0
- package/scripts/verify_framework.sh +13 -0
- package/skills/agent-team/SKILL.md +579 -0
- package/skills/agent-team/references/cleanup.md +42 -0
- package/skills/agent-team/references/delivery-sync.md +43 -0
- package/skills/agent-team/references/discovery.md +97 -0
- package/skills/agent-team/references/git-strategy.md +52 -0
- package/skills/agent-team/references/mid-sprint-triage.md +85 -0
- package/skills/agent-team/references/report-naming.md +34 -0
- package/skills/doc-manager/SKILL.md +444 -0
- package/skills/file-organization/SKILL.md +146 -0
- package/skills/file-organization/TEST-RESULTS.md +193 -0
- package/skills/file-organization/evals/evals.json +41 -0
- package/skills/file-organization/references/gitignore-template.md +53 -0
- package/skills/file-organization/references/quick-checklist.md +48 -0
- package/skills/improve/SKILL.md +296 -0
- package/skills/lesson/SKILL.md +136 -0
- package/skills/product-graph/SKILL.md +102 -0
- package/skills/react-best-practices/SKILL.md +3014 -0
- package/skills/react-best-practices/rules/_sections.md +46 -0
- package/skills/react-best-practices/rules/_template.md +28 -0
- package/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/skills/react-best-practices/rules/advanced-init-once.md +42 -0
- package/skills/react-best-practices/rules/advanced-use-latest.md +39 -0
- package/skills/react-best-practices/rules/async-api-routes.md +38 -0
- package/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/skills/react-best-practices/rules/async-dependencies.md +51 -0
- package/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/skills/react-best-practices/rules/bundle-conditional.md +31 -0
- package/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/skills/react-best-practices/rules/bundle-preload.md +50 -0
- package/skills/react-best-practices/rules/client-event-listeners.md +74 -0
- package/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/skills/react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/skills/react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/skills/react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/skills/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/skills/react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/skills/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/skills/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/skills/react-best-practices/rules/server-auth-actions.md +96 -0
- package/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/skills/react-best-practices/rules/server-cache-react.md +76 -0
- package/skills/react-best-practices/rules/server-dedup-props.md +65 -0
- package/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/skills/vibe-code-review/SKILL.md +70 -0
- package/skills/vibe-code-review/references/deep-audit.md +259 -0
- package/skills/vibe-code-review/references/pr-review.md +234 -0
- package/skills/vibe-code-review/references/quick-scan.md +178 -0
- package/skills/vibe-code-review/references/report-template.md +189 -0
- package/skills/vibe-code-review/references/trend-check.md +224 -0
- package/skills/vibe-code-review/scripts/generate-snapshot.sh +89 -0
- package/skills/vibe-code-review/scripts/pr-analyze.sh +180 -0
- package/skills/write-skill/SKILL.md +133 -0
- package/templates/bug.md +100 -0
- package/templates/change_request.md +105 -0
- package/templates/charter.md +144 -0
- package/templates/delivery_plan.md +44 -0
- package/templates/epic.md +203 -0
- package/templates/hotfix.md +58 -0
- package/templates/risk_registry.md +87 -0
- package/templates/roadmap.md +174 -0
- package/templates/spike.md +143 -0
- package/templates/sprint.md +134 -0
- package/templates/sprint_context.md +61 -0
- package/templates/sprint_report.md +215 -0
- package/templates/story.md +193 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* validate_bounce_readiness.mjs
|
|
5
|
+
* Pre-bounce gate check — verifies a story is ready to bounce.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ./.vbounce/scripts/validate_bounce_readiness.mjs STORY-005-02
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { execSync } from 'child_process';
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const ROOT = path.resolve(__dirname, '../..');
|
|
18
|
+
|
|
19
|
+
const storyId = process.argv[2];
|
|
20
|
+
if (!storyId) {
|
|
21
|
+
console.error('Usage: validate_bounce_readiness.mjs STORY-ID');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const errors = [];
|
|
26
|
+
const warnings = [];
|
|
27
|
+
|
|
28
|
+
// 1. Check state.json
|
|
29
|
+
const stateFile = path.join(ROOT, '.vbounce', 'state.json');
|
|
30
|
+
if (!fs.existsSync(stateFile)) {
|
|
31
|
+
errors.push('.vbounce/state.json not found — run: vbounce sprint init S-XX D-XX');
|
|
32
|
+
} else {
|
|
33
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
34
|
+
if (!state.stories[storyId]) {
|
|
35
|
+
errors.push(`Story "${storyId}" not found in state.json`);
|
|
36
|
+
} else {
|
|
37
|
+
const story = state.stories[storyId];
|
|
38
|
+
if (story.state !== 'Ready to Bounce') {
|
|
39
|
+
errors.push(`Story state is "${story.state}" — must be "Ready to Bounce" before bouncing`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 2. Find sprint plan
|
|
45
|
+
const sprintsDir = path.join(ROOT, 'product_plans', 'sprints');
|
|
46
|
+
let sprintPlanFound = false;
|
|
47
|
+
if (fs.existsSync(sprintsDir)) {
|
|
48
|
+
const sprintDirs = fs.readdirSync(sprintsDir);
|
|
49
|
+
for (const dir of sprintDirs) {
|
|
50
|
+
const planFile = path.join(sprintsDir, dir, `${dir}.md`);
|
|
51
|
+
if (fs.existsSync(planFile)) {
|
|
52
|
+
sprintPlanFound = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!sprintPlanFound) {
|
|
58
|
+
warnings.push('No active Sprint Plan found in product_plans/sprints/');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. Find story spec
|
|
62
|
+
let storyFile = null;
|
|
63
|
+
function findFile(dir, id) {
|
|
64
|
+
if (!fs.existsSync(dir)) return null;
|
|
65
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
66
|
+
for (const e of entries) {
|
|
67
|
+
if (e.isDirectory()) {
|
|
68
|
+
const found = findFile(path.join(dir, e.name), id);
|
|
69
|
+
if (found) return found;
|
|
70
|
+
} else if (e.name.includes(id)) {
|
|
71
|
+
return path.join(dir, e.name);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
storyFile = findFile(path.join(ROOT, 'product_plans'), storyId);
|
|
78
|
+
if (!storyFile) {
|
|
79
|
+
errors.push(`Story spec not found for "${storyId}" in product_plans/`);
|
|
80
|
+
} else {
|
|
81
|
+
const storyContent = fs.readFileSync(storyFile, 'utf8');
|
|
82
|
+
|
|
83
|
+
// Check for §1, §2, §3
|
|
84
|
+
const hasSpec = /##\s*(1\.|§1|The Spec)/i.test(storyContent);
|
|
85
|
+
const hasCriteria = /##\s*(2\.|§2|The Truth|Acceptance)/i.test(storyContent);
|
|
86
|
+
const hasGuide = /##\s*(3\.|§3|Implementation)/i.test(storyContent);
|
|
87
|
+
|
|
88
|
+
if (!hasSpec) errors.push(`Story ${storyId}: §1 (spec) section not found`);
|
|
89
|
+
if (!hasCriteria) errors.push(`Story ${storyId}: §2 (acceptance criteria) section not found`);
|
|
90
|
+
if (!hasGuide) errors.push(`Story ${storyId}: §3 (implementation guide) section not found`);
|
|
91
|
+
|
|
92
|
+
// Check for minimum content in each section
|
|
93
|
+
const specMatch = storyContent.match(/##\s*(1\.|§1|The Spec)[^\n]*\n([\s\S]*?)(?=\n##|\n---|\Z)/i);
|
|
94
|
+
if (specMatch && specMatch[2].trim().length < 30) {
|
|
95
|
+
warnings.push(`Story ${storyId}: §1 spec section appears very short — verify it's complete`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 4. Check worktree
|
|
100
|
+
const worktreeDir = path.join(ROOT, '.worktrees', storyId);
|
|
101
|
+
if (!fs.existsSync(worktreeDir)) {
|
|
102
|
+
warnings.push(`.worktrees/${storyId}/ not found — create with: git worktree add .worktrees/${storyId} -b story/${storyId} sprint/S-XX`);
|
|
103
|
+
}
|
|
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
|
+
|
|
132
|
+
// Print results
|
|
133
|
+
console.log(`Bounce readiness check: ${storyId}`);
|
|
134
|
+
console.log('');
|
|
135
|
+
|
|
136
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
137
|
+
console.log(`✓ ${storyId} is READY TO BOUNCE`);
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (warnings.length > 0) {
|
|
142
|
+
warnings.forEach(w => console.warn(` ⚠ ${w}`));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (errors.length > 0) {
|
|
146
|
+
errors.forEach(e => console.error(` ✗ ${e}`));
|
|
147
|
+
console.error(`\nNOT READY: Fix ${errors.length} error(s) before bouncing ${storyId}`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
} else {
|
|
150
|
+
console.log(` ✓ ${storyId} is ready (with warnings)`);
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* validate_report.mjs
|
|
5
|
+
*
|
|
6
|
+
* Strict YAML Frontmatter validation for V-Bounce Engine Agent Reports.
|
|
7
|
+
* Fails loudly if an agent hallucinates formatting or omits required fields,
|
|
8
|
+
* so the orchestrator can bounce the prompt back.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import yaml from 'js-yaml';
|
|
14
|
+
|
|
15
|
+
// Defined schemas for each report type
|
|
16
|
+
const ROOT_CAUSE_ENUM = [
|
|
17
|
+
'missing_tests', 'missing_validation', 'spec_ambiguity', 'adr_violation',
|
|
18
|
+
'gold_plating', 'logic_error', 'integration_gap', 'type_error',
|
|
19
|
+
'state_management', 'error_handling', 'coupling', 'duplication'
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const SCHEMAS = {
|
|
23
|
+
dev: ['status', 'correction_tax', 'tokens_used', 'tests_written', 'files_modified', 'lessons_flagged'],
|
|
24
|
+
qa: {
|
|
25
|
+
base: ['status', 'bounce_count', 'tokens_used', 'bugs_found', 'gold_plating_detected'],
|
|
26
|
+
conditional: { 'FAIL': ['failed_scenarios', 'root_cause'] }
|
|
27
|
+
},
|
|
28
|
+
arch: {
|
|
29
|
+
base: ['status', 'tokens_used'],
|
|
30
|
+
conditional: { 'PASS': ['safe_zone_score', 'ai_isms_detected', 'regression_risk'], 'FAIL': ['bounce_count', 'critical_failures', 'root_cause'] }
|
|
31
|
+
},
|
|
32
|
+
devops: {
|
|
33
|
+
base: ['type', 'status', 'tokens_used'],
|
|
34
|
+
conditional: { 'story-merge': ['conflicts_detected'], 'sprint-release': ['version'] }
|
|
35
|
+
},
|
|
36
|
+
scribe: ['mode', 'tokens_used', 'docs_created', 'docs_updated', 'docs_removed']
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function extractFrontmatter(content) {
|
|
40
|
+
// Matches "---" at the start of the file or after whitespace
|
|
41
|
+
const match = content.match(/^---\s*[\r\n]+([\s\S]*?)[\r\n]+---\s*/);
|
|
42
|
+
if (!match) {
|
|
43
|
+
throw new Error('NO_FRONTMATTER: Report missing strict YAML --- delimiters at the top of the file.');
|
|
44
|
+
}
|
|
45
|
+
return match[1];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validateDev(data) {
|
|
49
|
+
const missing = SCHEMAS.dev.filter(k => !(k in data));
|
|
50
|
+
if (missing.length > 0) throw new Error(`DEV_SCHEMA_ERROR: Missing required keys: ${missing.join(', ')}`);
|
|
51
|
+
if (!Array.isArray(data.files_modified)) throw new Error(`DEV_SCHEMA_ERROR: 'files_modified' must be an array.`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validateBugsArray(bugs, prefix) {
|
|
55
|
+
if (!Array.isArray(bugs)) throw new Error(`${prefix}: 'bugs' must be an array.`);
|
|
56
|
+
bugs.forEach((bug, i) => {
|
|
57
|
+
const bugRequired = ['scenario', 'expected', 'actual', 'files', 'severity'];
|
|
58
|
+
const bugMissing = bugRequired.filter(k => !(k in bug));
|
|
59
|
+
if (bugMissing.length > 0) throw new Error(`${prefix}: bugs[${i}] missing keys: ${bugMissing.join(', ')}`);
|
|
60
|
+
if (!Array.isArray(bug.files)) throw new Error(`${prefix}: bugs[${i}].files must be an array.`);
|
|
61
|
+
const validSeverities = ['Critical', 'High', 'Medium', 'Low'];
|
|
62
|
+
if (!validSeverities.includes(bug.severity)) throw new Error(`${prefix}: bugs[${i}].severity must be one of: ${validSeverities.join(', ')}`);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validateFailuresArray(failures, prefix) {
|
|
67
|
+
if (!Array.isArray(failures)) throw new Error(`${prefix}: 'failures' must be an array.`);
|
|
68
|
+
const validDimensions = ['Architectural Consistency', 'Error Handling', 'Data Flow', 'Duplication', 'Test Quality', 'Coupling'];
|
|
69
|
+
failures.forEach((f, i) => {
|
|
70
|
+
const fRequired = ['dimension', 'severity', 'what_wrong', 'fix_required'];
|
|
71
|
+
const fMissing = fRequired.filter(k => !(k in f));
|
|
72
|
+
if (fMissing.length > 0) throw new Error(`${prefix}: failures[${i}] missing keys: ${fMissing.join(', ')}`);
|
|
73
|
+
if (!validDimensions.includes(f.dimension)) throw new Error(`${prefix}: failures[${i}].dimension must be one of: ${validDimensions.join(', ')}`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateQA(data) {
|
|
78
|
+
const missing = SCHEMAS.qa.base.filter(k => !(k in data));
|
|
79
|
+
if (missing.length > 0) throw new Error(`QA_SCHEMA_ERROR: Missing required keys: ${missing.join(', ')}`);
|
|
80
|
+
|
|
81
|
+
if (data.status === 'FAIL') {
|
|
82
|
+
const conditionalMissing = SCHEMAS.qa.conditional.FAIL.filter(k => !(k in data));
|
|
83
|
+
if (conditionalMissing.length > 0) throw new Error(`QA_SCHEMA_ERROR: 'FAIL' status requires keys: ${conditionalMissing.join(', ')}`);
|
|
84
|
+
if (data.root_cause && !ROOT_CAUSE_ENUM.includes(data.root_cause)) {
|
|
85
|
+
throw new Error(`QA_SCHEMA_ERROR: Invalid root_cause '${data.root_cause}'. Must be one of: ${ROOT_CAUSE_ENUM.join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
if ('bugs' in data) validateBugsArray(data.bugs, 'QA_SCHEMA_ERROR');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validateArch(data) {
|
|
92
|
+
const missing = SCHEMAS.arch.base.filter(k => !(k in data));
|
|
93
|
+
if (missing.length > 0) throw new Error(`ARCH_SCHEMA_ERROR: Missing required keys: ${missing.join(', ')}`);
|
|
94
|
+
|
|
95
|
+
const s = data.status === 'PASS' ? 'PASS' : 'FAIL';
|
|
96
|
+
const conditionalMissing = SCHEMAS.arch.conditional[s].filter(k => !(k in data));
|
|
97
|
+
if (conditionalMissing.length > 0) throw new Error(`ARCH_SCHEMA_ERROR: '${s}' status requires keys: ${conditionalMissing.join(', ')}`);
|
|
98
|
+
if (s === 'FAIL' && data.root_cause && !ROOT_CAUSE_ENUM.includes(data.root_cause)) {
|
|
99
|
+
throw new Error(`ARCH_SCHEMA_ERROR: Invalid root_cause '${data.root_cause}'. Must be one of: ${ROOT_CAUSE_ENUM.join(', ')}`);
|
|
100
|
+
}
|
|
101
|
+
if (s === 'FAIL' && 'failures' in data) validateFailuresArray(data.failures, 'ARCH_SCHEMA_ERROR');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function validateDevops(data) {
|
|
105
|
+
const missing = SCHEMAS.devops.base.filter(k => !(k in data));
|
|
106
|
+
if (missing.length > 0) throw new Error(`DEVOPS_SCHEMA_ERROR: Missing required keys: ${missing.join(', ')}`);
|
|
107
|
+
|
|
108
|
+
const typeStr = String(data.type);
|
|
109
|
+
if (SCHEMAS.devops.conditional[typeStr]) {
|
|
110
|
+
const conditionalMissing = SCHEMAS.devops.conditional[typeStr].filter(k => !(k in data));
|
|
111
|
+
if (conditionalMissing.length > 0) throw new Error(`DEVOPS_SCHEMA_ERROR: '${typeStr}' type requires keys: ${conditionalMissing.join(', ')}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function main() {
|
|
116
|
+
const filePath = process.argv[2];
|
|
117
|
+
if (!filePath) {
|
|
118
|
+
console.error("Usage: validate_report.mjs <path-to-markdown-file>");
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const filename = path.basename(filePath);
|
|
123
|
+
|
|
124
|
+
// Infer agent type from filename convention
|
|
125
|
+
let agentType = 'unknown';
|
|
126
|
+
if (filename.endsWith('-dev.md')) agentType = 'dev';
|
|
127
|
+
else if (filename.endsWith('-qa.md')) agentType = 'qa';
|
|
128
|
+
else if (filename.endsWith('-arch.md')) agentType = 'arch';
|
|
129
|
+
else if (filename.endsWith('-devops.md')) agentType = 'devops';
|
|
130
|
+
else if (filename.endsWith('-scribe.md')) agentType = 'scribe';
|
|
131
|
+
|
|
132
|
+
if (agentType === 'unknown') {
|
|
133
|
+
console.error(`WARNING: Unrecognized report type for ${filename}. Ensure filename ends in -dev.md, -qa.md, -arch.md, or -devops.md.`);
|
|
134
|
+
process.exit(0); // Soft pass, not an agent workflow report
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const rawContent = fs.readFileSync(filePath, 'utf8');
|
|
139
|
+
const yamlString = extractFrontmatter(rawContent);
|
|
140
|
+
const data = yaml.load(yamlString);
|
|
141
|
+
|
|
142
|
+
if (!data || typeof data !== 'object') {
|
|
143
|
+
throw new Error("YAML_PARSE_ERROR: Frontmatter parsed to an empty or invalid object.");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (agentType === 'dev') validateDev(data);
|
|
147
|
+
if (agentType === 'qa') validateQA(data);
|
|
148
|
+
if (agentType === 'arch') validateArch(data);
|
|
149
|
+
if (agentType === 'devops') validateDevops(data);
|
|
150
|
+
if (agentType === 'scribe') {
|
|
151
|
+
const missing = SCHEMAS.scribe.filter(k => !(k in data));
|
|
152
|
+
if (missing.length > 0) throw new Error(`SCRIBE_SCHEMA_ERROR: Missing required keys: ${missing.join(', ')}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(`VALID: ${filename} matches the ${agentType.toUpperCase()} schema.`);
|
|
156
|
+
process.exit(0);
|
|
157
|
+
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// We print specifically to stdout so automation scripts can capture the payload and bounce it back to the AI
|
|
160
|
+
console.log(`VALIDATION_FAILED\n${error.message}`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
main();
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* validate_sprint_plan.mjs
|
|
5
|
+
* Validates a Sprint Plan markdown file structure and cross-checks with state.json.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ./.vbounce/scripts/validate_sprint_plan.mjs product_plans/sprints/sprint-05/sprint-05.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import yaml from 'js-yaml';
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const ROOT = path.resolve(__dirname, '../..');
|
|
18
|
+
|
|
19
|
+
const filePath = process.argv[2];
|
|
20
|
+
if (!filePath) {
|
|
21
|
+
console.error('Usage: validate_sprint_plan.mjs <path-to-sprint-plan.md>');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const absPath = path.resolve(filePath);
|
|
26
|
+
if (!fs.existsSync(absPath)) {
|
|
27
|
+
console.error(`ERROR: File not found: ${absPath}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const content = fs.readFileSync(absPath, 'utf8');
|
|
32
|
+
const errors = [];
|
|
33
|
+
const warnings = [];
|
|
34
|
+
|
|
35
|
+
// 1. Extract YAML frontmatter
|
|
36
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
37
|
+
if (!fmMatch) {
|
|
38
|
+
errors.push('Missing YAML frontmatter (--- delimiters)');
|
|
39
|
+
} else {
|
|
40
|
+
let fm;
|
|
41
|
+
try {
|
|
42
|
+
fm = yaml.load(fmMatch[1]);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
errors.push(`Invalid YAML frontmatter: ${e.message}`);
|
|
45
|
+
fm = {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const required = ['sprint_id', 'sprint_goal', 'dates', 'status', 'delivery'];
|
|
49
|
+
for (const f of required) {
|
|
50
|
+
if (!fm[f]) errors.push(`Frontmatter missing required field: "${f}"`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Cross-check with state.json
|
|
54
|
+
const stateFile = path.join(ROOT, '.vbounce', 'state.json');
|
|
55
|
+
if (fs.existsSync(stateFile)) {
|
|
56
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
57
|
+
|
|
58
|
+
// Find story IDs in §1 table
|
|
59
|
+
const tableRowRegex = /\|\s*\d+\s*\|\s*\[?(STORY-[\w-]+)/g;
|
|
60
|
+
const planStoryIds = new Set();
|
|
61
|
+
let m;
|
|
62
|
+
while ((m = tableRowRegex.exec(content)) !== null) {
|
|
63
|
+
planStoryIds.add(m[1]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const stateStoryIds = new Set(Object.keys(state.stories || {}));
|
|
67
|
+
|
|
68
|
+
// Check for stories in plan but not in state
|
|
69
|
+
for (const id of planStoryIds) {
|
|
70
|
+
if (!stateStoryIds.has(id)) {
|
|
71
|
+
warnings.push(`Story ${id} is in Sprint Plan §1 but NOT in state.json`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for stories in state but not in plan
|
|
76
|
+
for (const id of stateStoryIds) {
|
|
77
|
+
if (!planStoryIds.has(id)) {
|
|
78
|
+
warnings.push(`Story ${id} is in state.json but NOT in Sprint Plan §1`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 3. Check §4 Execution Log if sprint is Completed
|
|
84
|
+
if (fm.status === 'Completed') {
|
|
85
|
+
if (!content.includes('<!-- EXECUTION_LOG_START -->') && !content.includes('## 4.') && !content.includes('## §4')) {
|
|
86
|
+
errors.push('Sprint is Completed but §4 Execution Log section is missing');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 4. Check §1 table columns
|
|
92
|
+
if (!content.includes('| Priority |') && !content.includes('|Priority|')) {
|
|
93
|
+
errors.push('§1 Active Scope table missing or malformed (expected "Priority" column header)');
|
|
94
|
+
}
|
|
95
|
+
if (!content.includes('V-Bounce State')) {
|
|
96
|
+
errors.push('§1 Active Scope table missing "V-Bounce State" column');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Print results
|
|
100
|
+
console.log(`Validating: ${filePath}`);
|
|
101
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
102
|
+
console.log('✓ Sprint Plan is valid');
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (warnings.length > 0) {
|
|
107
|
+
console.warn('Warnings:');
|
|
108
|
+
warnings.forEach(w => console.warn(` ⚠ ${w}`));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (errors.length > 0) {
|
|
112
|
+
console.error('Errors:');
|
|
113
|
+
errors.forEach(e => console.error(` ✗ ${e}`));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
} else {
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* validate_state.mjs
|
|
5
|
+
* Validates .vbounce/state.json schema.
|
|
6
|
+
* Usage: ./scripts/validate_state.mjs
|
|
7
|
+
* Also exportable: import { validateState } from './validate_state.mjs'
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const ROOT = path.resolve(__dirname, '../..');
|
|
16
|
+
const STATE_FILE = path.join(ROOT, '.vbounce', 'state.json');
|
|
17
|
+
|
|
18
|
+
const VALID_STATES = [
|
|
19
|
+
'Draft', 'Refinement', 'Ready to Bounce', 'Bouncing',
|
|
20
|
+
'QA Passed', 'Architect Passed', 'Done', 'Escalated', 'Parking Lot'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validates a state object. Returns { valid, errors }.
|
|
25
|
+
* @param {object} state
|
|
26
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
27
|
+
*/
|
|
28
|
+
export function validateState(state) {
|
|
29
|
+
const errors = [];
|
|
30
|
+
|
|
31
|
+
if (!state || typeof state !== 'object') {
|
|
32
|
+
return { valid: false, errors: ['state.json must be a JSON object'] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!state.sprint_id || !/^S-\d{2}$/.test(state.sprint_id)) {
|
|
36
|
+
errors.push(`sprint_id "${state.sprint_id}" must match S-XX format (e.g. S-05)`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!state.delivery_id || !/^D-\d{2}$/.test(state.delivery_id)) {
|
|
40
|
+
errors.push(`delivery_id "${state.delivery_id}" must match D-NN format (e.g. D-02)`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!state.stories || typeof state.stories !== 'object') {
|
|
44
|
+
errors.push('stories field must be an object');
|
|
45
|
+
} else {
|
|
46
|
+
for (const [id, story] of Object.entries(state.stories)) {
|
|
47
|
+
if (!VALID_STATES.includes(story.state)) {
|
|
48
|
+
errors.push(`Story ${id}: invalid state "${story.state}". Must be one of: ${VALID_STATES.join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
if (typeof story.qa_bounces !== 'number' || !Number.isInteger(story.qa_bounces) || story.qa_bounces < 0) {
|
|
51
|
+
errors.push(`Story ${id}: qa_bounces must be a non-negative integer, got "${story.qa_bounces}"`);
|
|
52
|
+
}
|
|
53
|
+
if (typeof story.arch_bounces !== 'number' || !Number.isInteger(story.arch_bounces) || story.arch_bounces < 0) {
|
|
54
|
+
errors.push(`Story ${id}: arch_bounces must be a non-negative integer, got "${story.arch_bounces}"`);
|
|
55
|
+
}
|
|
56
|
+
if (story.state === 'Done' && story.worktree) {
|
|
57
|
+
errors.push(`Story ${id}: state is "Done" but worktree "${story.worktree}" is still set (should be null)`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (state.updated_at) {
|
|
63
|
+
const d = new Date(state.updated_at);
|
|
64
|
+
if (isNaN(d.getTime())) {
|
|
65
|
+
errors.push(`updated_at "${state.updated_at}" is not a valid ISO 8601 timestamp`);
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
errors.push('updated_at field is required');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { valid: errors.length === 0, errors };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// CLI entry point
|
|
75
|
+
if (process.argv[1] && fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]))) {
|
|
76
|
+
if (!fs.existsSync(STATE_FILE)) {
|
|
77
|
+
console.error(`ERROR: ${STATE_FILE} not found. Run: vbounce sprint init S-XX D-XX`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let state;
|
|
82
|
+
try {
|
|
83
|
+
state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.error(`ERROR: state.json is not valid JSON — ${e.message}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { valid, errors } = validateState(state);
|
|
90
|
+
|
|
91
|
+
if (valid) {
|
|
92
|
+
console.log(`VALID: state.json — sprint ${state.sprint_id}, ${Object.keys(state.stories || {}).length} stories`);
|
|
93
|
+
process.exit(0);
|
|
94
|
+
} else {
|
|
95
|
+
console.error('INVALID: state.json has errors:');
|
|
96
|
+
errors.forEach(e => console.error(` - ${e}`));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|