jumpstart-mode 1.1.11 → 1.1.13
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/.github/agents/jumpstart-adversary.agent.md +2 -1
- package/.github/agents/jumpstart-architect.agent.md +6 -7
- package/.github/agents/jumpstart-challenger.agent.md +2 -1
- package/.github/agents/jumpstart-developer.agent.md +1 -1
- package/.github/agents/jumpstart-devops.agent.md +2 -2
- package/.github/agents/jumpstart-diagram-verifier.agent.md +2 -1
- package/.github/agents/jumpstart-maintenance.agent.md +1 -0
- package/.github/agents/jumpstart-performance.agent.md +1 -0
- package/.github/agents/jumpstart-pm.agent.md +1 -1
- package/.github/agents/jumpstart-refactor.agent.md +1 -0
- package/.github/agents/jumpstart-requirements-extractor.agent.md +1 -0
- package/.github/agents/jumpstart-researcher.agent.md +1 -0
- package/.github/agents/jumpstart-retrospective.agent.md +1 -0
- package/.github/agents/jumpstart-reviewer.agent.md +2 -0
- package/.github/agents/jumpstart-scout.agent.md +1 -1
- package/.github/agents/jumpstart-scrum-master.agent.md +1 -0
- package/.github/agents/jumpstart-security.agent.md +2 -1
- package/.github/agents/jumpstart-tech-writer.agent.md +1 -0
- package/.github/agents/jumpstart-uiux-designer.agent.md +66 -0
- package/.github/workflows/quality.yml +19 -2
- package/.jumpstart/agents/analyst.md +38 -0
- package/.jumpstart/agents/architect.md +39 -1
- package/.jumpstart/agents/challenger.md +38 -0
- package/.jumpstart/agents/developer.md +41 -0
- package/.jumpstart/agents/pm.md +38 -0
- package/.jumpstart/agents/scout.md +33 -0
- package/.jumpstart/agents/ux-designer.md +29 -9
- package/.jumpstart/commands/commands.md +6 -5
- package/.jumpstart/config.yaml +25 -1
- package/.jumpstart/roadmap.md +1 -1
- package/.jumpstart/schemas/timeline.schema.json +1 -0
- package/.jumpstart/skills/README.md +1 -0
- package/.jumpstart/skills/quality-gates/SKILL.md +126 -0
- package/.jumpstart/skills/skill-creator/SKILL.md +485 -357
- package/.jumpstart/skills/skill-creator/agents/analyzer.md +274 -0
- package/.jumpstart/skills/skill-creator/agents/comparator.md +202 -0
- package/.jumpstart/skills/skill-creator/agents/grader.md +223 -0
- package/.jumpstart/skills/skill-creator/assets/eval_review.html +146 -0
- package/.jumpstart/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/.jumpstart/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/.jumpstart/skills/skill-creator/references/schemas.md +430 -0
- package/.jumpstart/skills/skill-creator/scripts/__init__.py +0 -0
- package/.jumpstart/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/.jumpstart/skills/skill-creator/scripts/generate_report.py +326 -0
- package/.jumpstart/skills/skill-creator/scripts/improve_description.py +247 -0
- package/.jumpstart/skills/skill-creator/scripts/package_skill.py +136 -110
- package/.jumpstart/skills/skill-creator/scripts/run_eval.py +310 -0
- package/.jumpstart/skills/skill-creator/scripts/run_loop.py +328 -0
- package/.jumpstart/skills/skill-creator/scripts/utils.py +47 -0
- package/.jumpstart/skills/ui-ux-pro-max/SKILL.md +266 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/.jumpstart/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/.jumpstart/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/.jumpstart/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/.jumpstart/state/timeline.json +659 -0
- package/.jumpstart/templates/model-map.md +1 -1
- package/.jumpstart/templates/ux-design.md +3 -3
- package/.jumpstart/usage-log.json +74 -3
- package/AGENTS.md +1 -1
- package/README.md +64 -3
- package/bin/cli.js +3217 -1
- package/bin/headless-runner.js +62 -2
- package/bin/lib/agent-checkpoint.js +168 -0
- package/bin/lib/ai-evaluation.js +104 -0
- package/bin/lib/ai-intake.js +152 -0
- package/bin/lib/ambiguity-heatmap.js +152 -0
- package/bin/lib/artifact-comparison.js +104 -0
- package/bin/lib/ast-edit-engine.js +157 -0
- package/bin/lib/backlog-sync.js +338 -0
- package/bin/lib/bcdr-planning.js +158 -0
- package/bin/lib/bidirectional-trace.js +199 -0
- package/bin/lib/branch-workflow.js +266 -0
- package/bin/lib/cab-output.js +119 -0
- package/bin/lib/chat-integration.js +122 -0
- package/bin/lib/ci-cd-integration.js +208 -0
- package/bin/lib/codebase-retrieval.js +125 -0
- package/bin/lib/collaboration.js +168 -0
- package/bin/lib/compliance-packs.js +213 -0
- package/bin/lib/context-chunker.js +128 -0
- package/bin/lib/context-onboarding.js +122 -0
- package/bin/lib/contract-first.js +124 -0
- package/bin/lib/cost-router.js +148 -0
- package/bin/lib/credential-boundary.js +155 -0
- package/bin/lib/data-classification.js +180 -0
- package/bin/lib/data-contracts.js +129 -0
- package/bin/lib/db-evolution.js +158 -0
- package/bin/lib/decision-conflicts.js +299 -0
- package/bin/lib/delivery-confidence.js +361 -0
- package/bin/lib/dependency-upgrade.js +153 -0
- package/bin/lib/design-system.js +133 -0
- package/bin/lib/deterministic-artifacts.js +151 -0
- package/bin/lib/diagram-studio.js +115 -0
- package/bin/lib/domain-ontology.js +140 -0
- package/bin/lib/ea-review-packet.js +151 -0
- package/bin/lib/enterprise-search.js +123 -0
- package/bin/lib/enterprise-templates.js +140 -0
- package/bin/lib/environment-promotion.js +220 -0
- package/bin/lib/estimation-studio.js +130 -0
- package/bin/lib/event-modeling.js +133 -0
- package/bin/lib/evidence-collector.js +179 -0
- package/bin/lib/finops-planner.js +182 -0
- package/bin/lib/fitness-functions.js +279 -0
- package/bin/lib/focus.js +448 -0
- package/bin/lib/governance-dashboard.js +165 -0
- package/bin/lib/guided-handoff.js +120 -0
- package/bin/lib/impact-analysis.js +190 -0
- package/bin/lib/incident-feedback.js +157 -0
- package/bin/lib/integrate.js +1 -1
- package/bin/lib/knowledge-graph.js +122 -0
- package/bin/lib/legacy-modernizer.js +160 -0
- package/bin/lib/migration-planner.js +144 -0
- package/bin/lib/model-governance.js +185 -0
- package/bin/lib/model-router.js +144 -0
- package/bin/lib/multi-repo.js +272 -0
- package/bin/lib/next-phase.js +53 -8
- package/bin/lib/ops-ownership.js +152 -0
- package/bin/lib/parallel-agents.js +257 -0
- package/bin/lib/pattern-library.js +115 -0
- package/bin/lib/persona-packs.js +99 -0
- package/bin/lib/plan-executor.js +366 -0
- package/bin/lib/platform-engineering.js +119 -0
- package/bin/lib/playback-summaries.js +126 -0
- package/bin/lib/policy-engine.js +240 -0
- package/bin/lib/portfolio-reporting.js +357 -0
- package/bin/lib/pr-package.js +197 -0
- package/bin/lib/project-memory.js +235 -0
- package/bin/lib/prompt-governance.js +130 -0
- package/bin/lib/promptless-mode.js +128 -0
- package/bin/lib/quality-graph.js +193 -0
- package/bin/lib/raci-matrix.js +188 -0
- package/bin/lib/refactor-planner.js +167 -0
- package/bin/lib/reference-architectures.js +304 -0
- package/bin/lib/release-readiness.js +171 -0
- package/bin/lib/repo-graph.js +262 -0
- package/bin/lib/requirements-baseline.js +358 -0
- package/bin/lib/risk-register.js +211 -0
- package/bin/lib/role-approval.js +249 -0
- package/bin/lib/role-views.js +142 -0
- package/bin/lib/root-cause-analysis.js +132 -0
- package/bin/lib/runtime-debugger.js +154 -0
- package/bin/lib/safe-rename.js +135 -0
- package/bin/lib/secret-scanner.js +313 -0
- package/bin/lib/semantic-diff.js +335 -0
- package/bin/lib/sla-slo.js +210 -0
- package/bin/lib/smoke-tester.js +344 -0
- package/bin/lib/spec-comments.js +147 -0
- package/bin/lib/spec-maturity.js +287 -0
- package/bin/lib/sre-integration.js +154 -0
- package/bin/lib/structured-elicitation.js +174 -0
- package/bin/lib/telemetry-feedback.js +118 -0
- package/bin/lib/test-generator.js +146 -0
- package/bin/lib/timeline.js +2 -1
- package/bin/lib/tool-bridge.js +159 -0
- package/bin/lib/tool-guardrails.js +139 -0
- package/bin/lib/tool-schemas.js +281 -3
- package/bin/lib/transcript-ingestion.js +150 -0
- package/bin/lib/type-checker.js +261 -0
- package/bin/lib/uat-coverage.js +411 -0
- package/bin/lib/vendor-risk.js +173 -0
- package/bin/lib/waiver-workflow.js +174 -0
- package/bin/lib/web-dashboard.js +126 -0
- package/bin/lib/workshop-mode.js +165 -0
- package/bin/lib/workstream-ownership.js +104 -0
- package/package.json +1 -1
- package/.github/agents/jumpstart-ux-designer.agent.md +0 -45
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* repo-graph.js — Automated Repo Understanding Graph
|
|
3
|
+
*
|
|
4
|
+
* Builds and persists a semantic graph of domains, modules, APIs,
|
|
5
|
+
* ownership, decisions, and dependencies for a project repository.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node bin/lib/repo-graph.js build|query|export [options]
|
|
9
|
+
*
|
|
10
|
+
* Output file: .jumpstart/state/repo-graph.json
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const DEFAULT_GRAPH_FILE = path.join('.jumpstart', 'state', 'repo-graph.json');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default empty graph structure.
|
|
22
|
+
* @returns {object}
|
|
23
|
+
*/
|
|
24
|
+
function defaultRepoGraph() {
|
|
25
|
+
return {
|
|
26
|
+
version: '1.0.0',
|
|
27
|
+
generated_at: new Date().toISOString(),
|
|
28
|
+
last_updated: null,
|
|
29
|
+
nodes: {}, // id → { id, type, name, metadata }
|
|
30
|
+
edges: [] // { from, to, relationship }
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load a repo graph from disk.
|
|
36
|
+
* @param {string} [graphFile]
|
|
37
|
+
* @returns {object}
|
|
38
|
+
*/
|
|
39
|
+
function loadRepoGraph(graphFile) {
|
|
40
|
+
const filePath = graphFile || DEFAULT_GRAPH_FILE;
|
|
41
|
+
if (!fs.existsSync(filePath)) {
|
|
42
|
+
return defaultRepoGraph();
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
46
|
+
} catch {
|
|
47
|
+
return defaultRepoGraph();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Save a repo graph to disk.
|
|
53
|
+
* @param {object} graph
|
|
54
|
+
* @param {string} [graphFile]
|
|
55
|
+
*/
|
|
56
|
+
function saveRepoGraph(graph, graphFile) {
|
|
57
|
+
const filePath = graphFile || DEFAULT_GRAPH_FILE;
|
|
58
|
+
const dir = path.dirname(filePath);
|
|
59
|
+
if (!fs.existsSync(dir)) {
|
|
60
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
graph.last_updated = new Date().toISOString();
|
|
63
|
+
fs.writeFileSync(filePath, JSON.stringify(graph, null, 2) + '\n', 'utf8');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Add or update a node in the graph.
|
|
68
|
+
* @param {object} graph
|
|
69
|
+
* @param {string} id
|
|
70
|
+
* @param {string} type - domain|module|api|file|decision|owner|dependency
|
|
71
|
+
* @param {object} [metadata]
|
|
72
|
+
*/
|
|
73
|
+
function upsertNode(graph, id, type, metadata = {}) {
|
|
74
|
+
graph.nodes[id] = {
|
|
75
|
+
...(graph.nodes[id] || {}),
|
|
76
|
+
id,
|
|
77
|
+
type,
|
|
78
|
+
...metadata,
|
|
79
|
+
updated_at: new Date().toISOString()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Add an edge between two nodes (avoids duplicates).
|
|
85
|
+
* @param {object} graph
|
|
86
|
+
* @param {string} from
|
|
87
|
+
* @param {string} to
|
|
88
|
+
* @param {string} relationship
|
|
89
|
+
*/
|
|
90
|
+
function addEdge(graph, from, to, relationship) {
|
|
91
|
+
const exists = graph.edges.some(
|
|
92
|
+
e => e.from === from && e.to === to && e.relationship === relationship
|
|
93
|
+
);
|
|
94
|
+
if (!exists) {
|
|
95
|
+
graph.edges.push({ from, to, relationship });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build the semantic graph from the project file system.
|
|
101
|
+
* Scans src/, specs/, and config files to auto-discover structure.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} root - Project root.
|
|
104
|
+
* @param {object} [options]
|
|
105
|
+
* @returns {object} Populated graph.
|
|
106
|
+
*/
|
|
107
|
+
function buildRepoGraph(root, options = {}) {
|
|
108
|
+
const graph = defaultRepoGraph();
|
|
109
|
+
const srcDir = path.join(root, options.srcDir || 'src');
|
|
110
|
+
const specsDir = path.join(root, 'specs');
|
|
111
|
+
const graphFile = options.graphFile || path.join(root, DEFAULT_GRAPH_FILE);
|
|
112
|
+
|
|
113
|
+
// 1. Scan src directory for modules and files
|
|
114
|
+
if (fs.existsSync(srcDir)) {
|
|
115
|
+
upsertNode(graph, 'src', 'module', { name: 'Source Root', path: 'src' });
|
|
116
|
+
|
|
117
|
+
const walk = (dir, parentId) => {
|
|
118
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
119
|
+
const full = path.join(dir, entry.name);
|
|
120
|
+
const rel = path.relative(root, full).replace(/\\/g, '/');
|
|
121
|
+
const nodeId = `file:${rel}`;
|
|
122
|
+
|
|
123
|
+
if (entry.isDirectory()) {
|
|
124
|
+
upsertNode(graph, nodeId, 'module', { name: entry.name, path: rel });
|
|
125
|
+
addEdge(graph, parentId, nodeId, 'contains');
|
|
126
|
+
walk(full, nodeId);
|
|
127
|
+
} else if (entry.isFile()) {
|
|
128
|
+
// Classify file type
|
|
129
|
+
let fileType = 'file';
|
|
130
|
+
const lower = entry.name.toLowerCase();
|
|
131
|
+
if (lower.includes('api') || lower.includes('route') || lower.includes('endpoint')) {
|
|
132
|
+
fileType = 'api';
|
|
133
|
+
} else if (lower.includes('model') || lower.includes('schema') || lower.includes('entity')) {
|
|
134
|
+
fileType = 'model';
|
|
135
|
+
} else if (lower.includes('service') || lower.includes('controller')) {
|
|
136
|
+
fileType = 'service';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
upsertNode(graph, nodeId, fileType, { name: entry.name, path: rel });
|
|
140
|
+
addEdge(graph, parentId, nodeId, 'contains');
|
|
141
|
+
|
|
142
|
+
// Extract any ownership annotations (e.g., @owner comments)
|
|
143
|
+
try {
|
|
144
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
145
|
+
const ownerMatch = content.match(/@owner[:\s]+(\S+)/i);
|
|
146
|
+
if (ownerMatch) {
|
|
147
|
+
const ownerId = `owner:${ownerMatch[1]}`;
|
|
148
|
+
upsertNode(graph, ownerId, 'owner', { name: ownerMatch[1] });
|
|
149
|
+
addEdge(graph, ownerId, nodeId, 'owns');
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
// skip unreadable files
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
walk(srcDir, 'src');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 2. Scan specs for decisions
|
|
162
|
+
const decisionsDir = path.join(specsDir, 'decisions');
|
|
163
|
+
if (fs.existsSync(decisionsDir)) {
|
|
164
|
+
upsertNode(graph, 'decisions', 'module', { name: 'Architecture Decisions', path: 'specs/decisions' });
|
|
165
|
+
for (const file of fs.readdirSync(decisionsDir)) {
|
|
166
|
+
if (file.endsWith('.md')) {
|
|
167
|
+
const nodeId = `decision:${file}`;
|
|
168
|
+
const adrPath = path.join(decisionsDir, file);
|
|
169
|
+
let title = file;
|
|
170
|
+
try {
|
|
171
|
+
const content = fs.readFileSync(adrPath, 'utf8');
|
|
172
|
+
const titleMatch = content.match(/^# (.+)/m);
|
|
173
|
+
if (titleMatch) title = titleMatch[1];
|
|
174
|
+
} catch {
|
|
175
|
+
// use filename as title
|
|
176
|
+
}
|
|
177
|
+
upsertNode(graph, nodeId, 'decision', { name: title, path: `specs/decisions/${file}` });
|
|
178
|
+
addEdge(graph, 'decisions', nodeId, 'contains');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 3. Add spec files as nodes
|
|
184
|
+
const specFiles = ['challenger-brief.md', 'product-brief.md', 'prd.md', 'architecture.md', 'implementation-plan.md'];
|
|
185
|
+
upsertNode(graph, 'specs', 'module', { name: 'Specifications', path: 'specs' });
|
|
186
|
+
for (const sf of specFiles) {
|
|
187
|
+
const specPath = path.join(specsDir, sf);
|
|
188
|
+
if (fs.existsSync(specPath)) {
|
|
189
|
+
const nodeId = `spec:${sf}`;
|
|
190
|
+
upsertNode(graph, nodeId, 'spec', { name: sf, path: `specs/${sf}` });
|
|
191
|
+
addEdge(graph, 'specs', nodeId, 'contains');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const nodeCount = Object.keys(graph.nodes).length;
|
|
196
|
+
const edgeCount = graph.edges.length;
|
|
197
|
+
|
|
198
|
+
saveRepoGraph(graph, graphFile);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
success: true,
|
|
202
|
+
node_count: nodeCount,
|
|
203
|
+
edge_count: edgeCount,
|
|
204
|
+
graph_file: graphFile
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Query the graph for nodes matching a type or name pattern.
|
|
210
|
+
*
|
|
211
|
+
* @param {object} graph
|
|
212
|
+
* @param {object} query - { type?, nameContains? }
|
|
213
|
+
* @returns {object[]} Matching nodes.
|
|
214
|
+
*/
|
|
215
|
+
function queryGraph(graph, query = {}) {
|
|
216
|
+
let nodes = Object.values(graph.nodes);
|
|
217
|
+
|
|
218
|
+
if (query.type) {
|
|
219
|
+
nodes = nodes.filter(n => n.type === query.type);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (query.nameContains) {
|
|
223
|
+
const lower = query.nameContains.toLowerCase();
|
|
224
|
+
nodes = nodes.filter(n => n.name && n.name.toLowerCase().includes(lower));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (query.id) {
|
|
228
|
+
nodes = nodes.filter(n => n.id === query.id);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return nodes;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Find all neighbours of a node (one-hop).
|
|
236
|
+
*
|
|
237
|
+
* @param {object} graph
|
|
238
|
+
* @param {string} nodeId
|
|
239
|
+
* @returns {{ incoming: object[], outgoing: object[] }}
|
|
240
|
+
*/
|
|
241
|
+
function getNeighbours(graph, nodeId) {
|
|
242
|
+
const outgoing = graph.edges
|
|
243
|
+
.filter(e => e.from === nodeId)
|
|
244
|
+
.map(e => ({ node: graph.nodes[e.to] || { id: e.to }, relationship: e.relationship }));
|
|
245
|
+
|
|
246
|
+
const incoming = graph.edges
|
|
247
|
+
.filter(e => e.to === nodeId)
|
|
248
|
+
.map(e => ({ node: graph.nodes[e.from] || { id: e.from }, relationship: e.relationship }));
|
|
249
|
+
|
|
250
|
+
return { incoming, outgoing };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = {
|
|
254
|
+
defaultRepoGraph,
|
|
255
|
+
loadRepoGraph,
|
|
256
|
+
saveRepoGraph,
|
|
257
|
+
upsertNode,
|
|
258
|
+
addEdge,
|
|
259
|
+
buildRepoGraph,
|
|
260
|
+
queryGraph,
|
|
261
|
+
getNeighbours
|
|
262
|
+
};
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* requirements-baseline.js — Requirements Baseline & Change Control
|
|
3
|
+
*
|
|
4
|
+
* Freezes approved requirements and forces impact assessment when
|
|
5
|
+
* downstream changes are proposed.
|
|
6
|
+
*
|
|
7
|
+
* Baseline file: .jumpstart/state/requirements-baseline.json
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node bin/lib/requirements-baseline.js freeze|check|impact|status [options]
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
|
|
19
|
+
const DEFAULT_BASELINE_FILE = path.join('.jumpstart', 'state', 'requirements-baseline.json');
|
|
20
|
+
|
|
21
|
+
const ARTIFACT_TYPES = ['challenger-brief', 'product-brief', 'prd', 'architecture', 'implementation-plan'];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default empty baseline registry.
|
|
25
|
+
* @returns {object}
|
|
26
|
+
*/
|
|
27
|
+
function defaultBaseline() {
|
|
28
|
+
return {
|
|
29
|
+
version: '1.0.0',
|
|
30
|
+
created_at: new Date().toISOString(),
|
|
31
|
+
last_updated: null,
|
|
32
|
+
frozen: false,
|
|
33
|
+
baselines: [],
|
|
34
|
+
change_requests: []
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load baseline from disk.
|
|
40
|
+
* @param {string} [baselineFile]
|
|
41
|
+
* @returns {object}
|
|
42
|
+
*/
|
|
43
|
+
function loadBaseline(baselineFile) {
|
|
44
|
+
const filePath = baselineFile || DEFAULT_BASELINE_FILE;
|
|
45
|
+
if (!fs.existsSync(filePath)) {
|
|
46
|
+
return defaultBaseline();
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
50
|
+
} catch {
|
|
51
|
+
return defaultBaseline();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Save baseline to disk.
|
|
57
|
+
* @param {object} baseline
|
|
58
|
+
* @param {string} [baselineFile]
|
|
59
|
+
*/
|
|
60
|
+
function saveBaseline(baseline, baselineFile) {
|
|
61
|
+
const filePath = baselineFile || DEFAULT_BASELINE_FILE;
|
|
62
|
+
const dir = path.dirname(filePath);
|
|
63
|
+
if (!fs.existsSync(dir)) {
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
baseline.last_updated = new Date().toISOString();
|
|
67
|
+
fs.writeFileSync(filePath, JSON.stringify(baseline, null, 2) + '\n', 'utf8');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Compute SHA-256 hash of content.
|
|
72
|
+
* @param {string} content
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
75
|
+
function hashContent(content) {
|
|
76
|
+
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extract requirement IDs from content.
|
|
81
|
+
* Matches patterns like REQ-001, E01-S01, NFR-001, UC-001, etc.
|
|
82
|
+
* @param {string} content
|
|
83
|
+
* @returns {string[]}
|
|
84
|
+
*/
|
|
85
|
+
function extractRequirementIds(content) {
|
|
86
|
+
const patterns = [
|
|
87
|
+
/\b(REQ-\d+)\b/g,
|
|
88
|
+
/\b(E\d+-S\d+)\b/g,
|
|
89
|
+
/\b(NFR-\d+)\b/g,
|
|
90
|
+
/\b(UC-\d+)\b/g,
|
|
91
|
+
/\b(FR-\d+)\b/g,
|
|
92
|
+
/\b(AC-\d+)\b/g
|
|
93
|
+
];
|
|
94
|
+
const ids = new Set();
|
|
95
|
+
for (const pattern of patterns) {
|
|
96
|
+
let match;
|
|
97
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
98
|
+
ids.add(match[1]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return [...ids].sort();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Freeze current requirements as baseline.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} root - Project root.
|
|
108
|
+
* @param {object} [options]
|
|
109
|
+
* @returns {object}
|
|
110
|
+
*/
|
|
111
|
+
function freezeBaseline(root, options = {}) {
|
|
112
|
+
const baselineFile = options.baselineFile || path.join(root, DEFAULT_BASELINE_FILE);
|
|
113
|
+
const baseline = loadBaseline(baselineFile);
|
|
114
|
+
|
|
115
|
+
const specsDir = path.join(root, 'specs');
|
|
116
|
+
if (!fs.existsSync(specsDir)) {
|
|
117
|
+
return { success: false, error: 'specs/ directory not found' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const snapshots = [];
|
|
121
|
+
const artifactMap = {
|
|
122
|
+
'challenger-brief': 'specs/challenger-brief.md',
|
|
123
|
+
'product-brief': 'specs/product-brief.md',
|
|
124
|
+
'prd': 'specs/prd.md',
|
|
125
|
+
'architecture': 'specs/architecture.md',
|
|
126
|
+
'implementation-plan': 'specs/implementation-plan.md'
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
for (const [type, relPath] of Object.entries(artifactMap)) {
|
|
130
|
+
const fullPath = path.join(root, relPath);
|
|
131
|
+
if (fs.existsSync(fullPath)) {
|
|
132
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
133
|
+
snapshots.push({
|
|
134
|
+
type,
|
|
135
|
+
path: relPath,
|
|
136
|
+
hash: hashContent(content),
|
|
137
|
+
requirement_ids: extractRequirementIds(content),
|
|
138
|
+
frozen_at: new Date().toISOString()
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (snapshots.length === 0) {
|
|
144
|
+
return { success: false, error: 'No spec artifacts found to freeze' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const baselineEntry = {
|
|
148
|
+
id: `baseline-${Date.now()}`,
|
|
149
|
+
frozen_at: new Date().toISOString(),
|
|
150
|
+
frozen_by: options.approver || 'system',
|
|
151
|
+
snapshots,
|
|
152
|
+
total_requirements: snapshots.reduce((sum, s) => sum + s.requirement_ids.length, 0)
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
baseline.baselines.push(baselineEntry);
|
|
156
|
+
baseline.frozen = true;
|
|
157
|
+
saveBaseline(baseline, baselineFile);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
success: true,
|
|
161
|
+
baseline_id: baselineEntry.id,
|
|
162
|
+
artifacts_frozen: snapshots.length,
|
|
163
|
+
total_requirements: baselineEntry.total_requirements,
|
|
164
|
+
snapshots: snapshots.map(s => ({ type: s.type, path: s.path, requirements: s.requirement_ids.length }))
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if any frozen artifacts have changed since the baseline.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} root - Project root.
|
|
172
|
+
* @param {object} [options]
|
|
173
|
+
* @returns {object}
|
|
174
|
+
*/
|
|
175
|
+
function checkBaseline(root, options = {}) {
|
|
176
|
+
const baselineFile = options.baselineFile || path.join(root, DEFAULT_BASELINE_FILE);
|
|
177
|
+
const baseline = loadBaseline(baselineFile);
|
|
178
|
+
|
|
179
|
+
if (!baseline.frozen || baseline.baselines.length === 0) {
|
|
180
|
+
return { success: true, frozen: false, message: 'No frozen baseline found' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const latestBaseline = baseline.baselines[baseline.baselines.length - 1];
|
|
184
|
+
const changes = [];
|
|
185
|
+
const unchanged = [];
|
|
186
|
+
|
|
187
|
+
for (const snapshot of latestBaseline.snapshots) {
|
|
188
|
+
const fullPath = path.join(root, snapshot.path);
|
|
189
|
+
if (!fs.existsSync(fullPath)) {
|
|
190
|
+
changes.push({
|
|
191
|
+
type: snapshot.type,
|
|
192
|
+
path: snapshot.path,
|
|
193
|
+
change: 'deleted',
|
|
194
|
+
severity: 'critical'
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
200
|
+
const currentHash = hashContent(content);
|
|
201
|
+
|
|
202
|
+
if (currentHash !== snapshot.hash) {
|
|
203
|
+
const currentIds = extractRequirementIds(content);
|
|
204
|
+
const addedIds = currentIds.filter(id => !snapshot.requirement_ids.includes(id));
|
|
205
|
+
const removedIds = snapshot.requirement_ids.filter(id => !currentIds.includes(id));
|
|
206
|
+
|
|
207
|
+
changes.push({
|
|
208
|
+
type: snapshot.type,
|
|
209
|
+
path: snapshot.path,
|
|
210
|
+
change: 'modified',
|
|
211
|
+
severity: removedIds.length > 0 ? 'critical' : addedIds.length > 0 ? 'warning' : 'info',
|
|
212
|
+
added_requirements: addedIds,
|
|
213
|
+
removed_requirements: removedIds
|
|
214
|
+
});
|
|
215
|
+
} else {
|
|
216
|
+
unchanged.push({ type: snapshot.type, path: snapshot.path });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const drifted = changes.length > 0;
|
|
221
|
+
return {
|
|
222
|
+
success: true,
|
|
223
|
+
frozen: true,
|
|
224
|
+
baseline_id: latestBaseline.id,
|
|
225
|
+
frozen_at: latestBaseline.frozen_at,
|
|
226
|
+
drifted,
|
|
227
|
+
changes,
|
|
228
|
+
unchanged,
|
|
229
|
+
summary: {
|
|
230
|
+
total_artifacts: latestBaseline.snapshots.length,
|
|
231
|
+
changed: changes.length,
|
|
232
|
+
unchanged: unchanged.length,
|
|
233
|
+
critical: changes.filter(c => c.severity === 'critical').length
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Perform impact assessment for a proposed change.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} artifactPath - Path to the changed artifact.
|
|
242
|
+
* @param {string} root - Project root.
|
|
243
|
+
* @param {object} [options]
|
|
244
|
+
* @returns {object}
|
|
245
|
+
*/
|
|
246
|
+
function assessImpact(artifactPath, root, options = {}) {
|
|
247
|
+
const baselineFile = options.baselineFile || path.join(root, DEFAULT_BASELINE_FILE);
|
|
248
|
+
const baseline = loadBaseline(baselineFile);
|
|
249
|
+
|
|
250
|
+
if (!baseline.frozen || baseline.baselines.length === 0) {
|
|
251
|
+
return { success: true, impact: 'none', message: 'No frozen baseline — changes are unconstrained' };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const relPath = path.relative(root, path.resolve(root, artifactPath)).replace(/\\/g, '/');
|
|
255
|
+
const latestBaseline = baseline.baselines[baseline.baselines.length - 1];
|
|
256
|
+
const snapshot = latestBaseline.snapshots.find(s => s.path === relPath);
|
|
257
|
+
|
|
258
|
+
if (!snapshot) {
|
|
259
|
+
return { success: true, impact: 'none', message: `${relPath} is not part of the frozen baseline` };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const fullPath = path.join(root, relPath);
|
|
263
|
+
if (!fs.existsSync(fullPath)) {
|
|
264
|
+
return {
|
|
265
|
+
success: true,
|
|
266
|
+
impact: 'critical',
|
|
267
|
+
artifact: relPath,
|
|
268
|
+
assessment: {
|
|
269
|
+
change_type: 'deletion',
|
|
270
|
+
affected_requirements: snapshot.requirement_ids,
|
|
271
|
+
downstream_artifacts: ARTIFACT_TYPES.filter(t => t !== snapshot.type)
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
277
|
+
const currentHash = hashContent(content);
|
|
278
|
+
|
|
279
|
+
if (currentHash === snapshot.hash) {
|
|
280
|
+
return { success: true, impact: 'none', message: 'Artifact matches frozen baseline' };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const currentIds = extractRequirementIds(content);
|
|
284
|
+
const addedIds = currentIds.filter(id => !snapshot.requirement_ids.includes(id));
|
|
285
|
+
const removedIds = snapshot.requirement_ids.filter(id => !currentIds.includes(id));
|
|
286
|
+
const unchangedIds = currentIds.filter(id => snapshot.requirement_ids.includes(id));
|
|
287
|
+
|
|
288
|
+
const typeIndex = ARTIFACT_TYPES.indexOf(snapshot.type);
|
|
289
|
+
const downstreamTypes = ARTIFACT_TYPES.slice(typeIndex + 1);
|
|
290
|
+
|
|
291
|
+
const impactLevel = removedIds.length > 0 ? 'critical'
|
|
292
|
+
: addedIds.length > 3 ? 'high'
|
|
293
|
+
: addedIds.length > 0 ? 'medium'
|
|
294
|
+
: 'low';
|
|
295
|
+
|
|
296
|
+
const changeRequest = {
|
|
297
|
+
id: `cr-${Date.now()}`,
|
|
298
|
+
artifact: relPath,
|
|
299
|
+
artifact_type: snapshot.type,
|
|
300
|
+
requested_at: new Date().toISOString(),
|
|
301
|
+
impact_level: impactLevel,
|
|
302
|
+
added_requirements: addedIds,
|
|
303
|
+
removed_requirements: removedIds,
|
|
304
|
+
downstream_artifacts: downstreamTypes,
|
|
305
|
+
status: 'pending_review'
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
baseline.change_requests.push(changeRequest);
|
|
309
|
+
saveBaseline(baseline, baselineFile);
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
success: true,
|
|
313
|
+
impact: impactLevel,
|
|
314
|
+
change_request_id: changeRequest.id,
|
|
315
|
+
artifact: relPath,
|
|
316
|
+
assessment: {
|
|
317
|
+
change_type: removedIds.length > 0 ? 'breaking' : 'additive',
|
|
318
|
+
added_requirements: addedIds,
|
|
319
|
+
removed_requirements: removedIds,
|
|
320
|
+
unchanged_requirements: unchangedIds.length,
|
|
321
|
+
downstream_artifacts: downstreamTypes,
|
|
322
|
+
requires_re_approval: impactLevel === 'critical' || impactLevel === 'high'
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get baseline status.
|
|
329
|
+
*
|
|
330
|
+
* @param {object} [options]
|
|
331
|
+
* @returns {object}
|
|
332
|
+
*/
|
|
333
|
+
function getBaselineStatus(options = {}) {
|
|
334
|
+
const baselineFile = options.baselineFile || DEFAULT_BASELINE_FILE;
|
|
335
|
+
const baseline = loadBaseline(baselineFile);
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
frozen: baseline.frozen,
|
|
340
|
+
total_baselines: baseline.baselines.length,
|
|
341
|
+
total_change_requests: baseline.change_requests.length,
|
|
342
|
+
pending_change_requests: baseline.change_requests.filter(cr => cr.status === 'pending_review').length,
|
|
343
|
+
latest_baseline: baseline.baselines.length > 0 ? baseline.baselines[baseline.baselines.length - 1] : null
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = {
|
|
348
|
+
defaultBaseline,
|
|
349
|
+
loadBaseline,
|
|
350
|
+
saveBaseline,
|
|
351
|
+
hashContent,
|
|
352
|
+
extractRequirementIds,
|
|
353
|
+
freezeBaseline,
|
|
354
|
+
checkBaseline,
|
|
355
|
+
assessImpact,
|
|
356
|
+
getBaselineStatus,
|
|
357
|
+
ARTIFACT_TYPES
|
|
358
|
+
};
|