jumpstart-mode 1.1.12 → 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 +5 -6
- package/.github/agents/jumpstart-challenger.agent.md +2 -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/workflows/quality.yml +19 -2
- package/.jumpstart/agents/analyst.md +38 -0
- package/.jumpstart/agents/architect.md +38 -0
- 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 +4 -0
- package/.jumpstart/config.yaml +24 -0
- package/.jumpstart/schemas/timeline.schema.json +1 -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/state/timeline.json +659 -0
- package/.jumpstart/usage-log.json +74 -3
- package/README.md +62 -1
- 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/semantic-diff.js +335 -0
- package/bin/lib/sla-slo.js +210 -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 +107 -0
- package/bin/lib/tool-guardrails.js +139 -0
- package/bin/lib/tool-schemas.js +172 -3
- package/bin/lib/transcript-ingestion.js +150 -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
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* semantic-diff.js — Cross-artifact Semantic Diffing
|
|
3
|
+
*
|
|
4
|
+
* Detects meaning changes, not just text changes, across PRD,
|
|
5
|
+
* architecture, APIs, and tests.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node bin/lib/semantic-diff.js compare <path1> <path2> [options]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
|
|
17
|
+
const SECTION_HEADING = /^(#{1,6})\s+(.+)$/gm;
|
|
18
|
+
const REQUIREMENT_PATTERN = /\b(REQ-\d+|E\d+-S\d+|NFR-\d+|UC-\d+|FR-\d+|AC-\d+|M\d+-T\d+)\b/g;
|
|
19
|
+
const API_ENDPOINT = /\b(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(\S+)/g;
|
|
20
|
+
const TABLE_ROW = /^\|(.+)\|$/gm;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extract structural sections from markdown.
|
|
24
|
+
* @param {string} content
|
|
25
|
+
* @returns {object[]}
|
|
26
|
+
*/
|
|
27
|
+
function extractSections(content) {
|
|
28
|
+
const sections = [];
|
|
29
|
+
const lines = content.split('\n');
|
|
30
|
+
let currentSection = { heading: '(preamble)', level: 0, content: [], startLine: 0 };
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < lines.length; i++) {
|
|
33
|
+
const headingMatch = lines[i].match(/^(#{1,6})\s+(.+)$/);
|
|
34
|
+
if (headingMatch) {
|
|
35
|
+
if (currentSection.content.length > 0 || currentSection.heading !== '(preamble)') {
|
|
36
|
+
currentSection.content = currentSection.content.join('\n').trim();
|
|
37
|
+
sections.push(currentSection);
|
|
38
|
+
}
|
|
39
|
+
currentSection = {
|
|
40
|
+
heading: headingMatch[2].trim(),
|
|
41
|
+
level: headingMatch[1].length,
|
|
42
|
+
content: [],
|
|
43
|
+
startLine: i + 1
|
|
44
|
+
};
|
|
45
|
+
} else {
|
|
46
|
+
currentSection.content.push(lines[i]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
currentSection.content = currentSection.content.join('\n').trim();
|
|
50
|
+
sections.push(currentSection);
|
|
51
|
+
|
|
52
|
+
return sections;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract requirement references from content.
|
|
57
|
+
* @param {string} content
|
|
58
|
+
* @returns {string[]}
|
|
59
|
+
*/
|
|
60
|
+
function extractRequirements(content) {
|
|
61
|
+
const matches = content.match(REQUIREMENT_PATTERN) || [];
|
|
62
|
+
return [...new Set(matches)].sort();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extract API endpoints from content.
|
|
67
|
+
* @param {string} content
|
|
68
|
+
* @returns {object[]}
|
|
69
|
+
*/
|
|
70
|
+
function extractApiEndpoints(content) {
|
|
71
|
+
const endpoints = [];
|
|
72
|
+
let match;
|
|
73
|
+
const pattern = new RegExp(API_ENDPOINT.source, 'g');
|
|
74
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
75
|
+
endpoints.push({ method: match[1], path: match[2] });
|
|
76
|
+
}
|
|
77
|
+
return endpoints;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Extract key-value pairs from table rows.
|
|
82
|
+
* @param {string} content
|
|
83
|
+
* @returns {string[][]}
|
|
84
|
+
*/
|
|
85
|
+
function extractTableData(content) {
|
|
86
|
+
const rows = [];
|
|
87
|
+
let match;
|
|
88
|
+
const pattern = new RegExp(TABLE_ROW.source, 'gm');
|
|
89
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
90
|
+
const cells = match[1].split('|').map(c => c.trim()).filter(c => c.length > 0);
|
|
91
|
+
if (cells.some(c => /^[-:]+$/.test(c))) continue; // skip separator rows
|
|
92
|
+
rows.push(cells);
|
|
93
|
+
}
|
|
94
|
+
return rows;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Normalize text for comparison: lowercase, collapse whitespace, strip punctuation.
|
|
99
|
+
* @param {string} text
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
function normalizeText(text) {
|
|
103
|
+
return text.toLowerCase()
|
|
104
|
+
.replace(/[^\w\s]/g, ' ')
|
|
105
|
+
.replace(/\s+/g, ' ')
|
|
106
|
+
.trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Compute simple similarity ratio between two strings (0-1).
|
|
111
|
+
* Uses set-based word overlap (Jaccard-like).
|
|
112
|
+
* @param {string} a
|
|
113
|
+
* @param {string} b
|
|
114
|
+
* @returns {number}
|
|
115
|
+
*/
|
|
116
|
+
function textSimilarity(a, b) {
|
|
117
|
+
const wordsA = new Set(normalizeText(a).split(' ').filter(w => w.length > 2));
|
|
118
|
+
const wordsB = new Set(normalizeText(b).split(' ').filter(w => w.length > 2));
|
|
119
|
+
if (wordsA.size === 0 && wordsB.size === 0) return 1;
|
|
120
|
+
if (wordsA.size === 0 || wordsB.size === 0) return 0;
|
|
121
|
+
|
|
122
|
+
let intersection = 0;
|
|
123
|
+
for (const w of wordsA) {
|
|
124
|
+
if (wordsB.has(w)) intersection++;
|
|
125
|
+
}
|
|
126
|
+
const union = new Set([...wordsA, ...wordsB]).size;
|
|
127
|
+
return union === 0 ? 1 : intersection / union;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Compare two artifacts and find semantic differences.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} contentA - Original content.
|
|
134
|
+
* @param {string} contentB - Modified content.
|
|
135
|
+
* @param {object} [options]
|
|
136
|
+
* @returns {object}
|
|
137
|
+
*/
|
|
138
|
+
function compareArtifacts(contentA, contentB, options = {}) {
|
|
139
|
+
const sectionsA = extractSections(contentA);
|
|
140
|
+
const sectionsB = extractSections(contentB);
|
|
141
|
+
|
|
142
|
+
const sectionChanges = [];
|
|
143
|
+
const headingsA = sectionsA.map(s => s.heading);
|
|
144
|
+
const headingsB = sectionsB.map(s => s.heading);
|
|
145
|
+
|
|
146
|
+
// Detect added sections
|
|
147
|
+
for (const sec of sectionsB) {
|
|
148
|
+
if (!headingsA.includes(sec.heading)) {
|
|
149
|
+
sectionChanges.push({
|
|
150
|
+
type: 'section_added',
|
|
151
|
+
heading: sec.heading,
|
|
152
|
+
severity: 'info'
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Detect removed sections
|
|
158
|
+
for (const sec of sectionsA) {
|
|
159
|
+
if (!headingsB.includes(sec.heading)) {
|
|
160
|
+
sectionChanges.push({
|
|
161
|
+
type: 'section_removed',
|
|
162
|
+
heading: sec.heading,
|
|
163
|
+
severity: 'warning'
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Detect modified sections
|
|
169
|
+
for (const secA of sectionsA) {
|
|
170
|
+
const secB = sectionsB.find(s => s.heading === secA.heading);
|
|
171
|
+
if (secB) {
|
|
172
|
+
const similarity = textSimilarity(secA.content, secB.content);
|
|
173
|
+
if (similarity < 0.95) {
|
|
174
|
+
sectionChanges.push({
|
|
175
|
+
type: 'section_modified',
|
|
176
|
+
heading: secA.heading,
|
|
177
|
+
similarity: Math.round(similarity * 100),
|
|
178
|
+
severity: similarity < 0.5 ? 'critical' : similarity < 0.8 ? 'warning' : 'info'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Requirement changes
|
|
185
|
+
const reqsA = extractRequirements(contentA);
|
|
186
|
+
const reqsB = extractRequirements(contentB);
|
|
187
|
+
const addedReqs = reqsB.filter(r => !reqsA.includes(r));
|
|
188
|
+
const removedReqs = reqsA.filter(r => !reqsB.includes(r));
|
|
189
|
+
|
|
190
|
+
// API endpoint changes
|
|
191
|
+
const apisA = extractApiEndpoints(contentA);
|
|
192
|
+
const apisB = extractApiEndpoints(contentB);
|
|
193
|
+
const apiKeysA = apisA.map(a => `${a.method} ${a.path}`);
|
|
194
|
+
const apiKeysB = apisB.map(a => `${a.method} ${a.path}`);
|
|
195
|
+
const addedApis = apiKeysB.filter(k => !apiKeysA.includes(k));
|
|
196
|
+
const removedApis = apiKeysA.filter(k => !apiKeysB.includes(k));
|
|
197
|
+
|
|
198
|
+
// Table changes
|
|
199
|
+
const tablesA = extractTableData(contentA);
|
|
200
|
+
const tablesB = extractTableData(contentB);
|
|
201
|
+
|
|
202
|
+
const overallSimilarity = textSimilarity(contentA, contentB);
|
|
203
|
+
const hasBreakingChanges = removedReqs.length > 0 || removedApis.length > 0
|
|
204
|
+
|| sectionChanges.some(c => c.severity === 'critical');
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
success: true,
|
|
208
|
+
overall_similarity: Math.round(overallSimilarity * 100),
|
|
209
|
+
has_breaking_changes: hasBreakingChanges,
|
|
210
|
+
section_changes: sectionChanges,
|
|
211
|
+
requirement_changes: {
|
|
212
|
+
added: addedReqs,
|
|
213
|
+
removed: removedReqs,
|
|
214
|
+
total_before: reqsA.length,
|
|
215
|
+
total_after: reqsB.length
|
|
216
|
+
},
|
|
217
|
+
api_changes: {
|
|
218
|
+
added: addedApis,
|
|
219
|
+
removed: removedApis,
|
|
220
|
+
total_before: apisA.length,
|
|
221
|
+
total_after: apisB.length
|
|
222
|
+
},
|
|
223
|
+
table_changes: {
|
|
224
|
+
rows_before: tablesA.length,
|
|
225
|
+
rows_after: tablesB.length
|
|
226
|
+
},
|
|
227
|
+
summary: {
|
|
228
|
+
sections_added: sectionChanges.filter(c => c.type === 'section_added').length,
|
|
229
|
+
sections_removed: sectionChanges.filter(c => c.type === 'section_removed').length,
|
|
230
|
+
sections_modified: sectionChanges.filter(c => c.type === 'section_modified').length,
|
|
231
|
+
requirements_added: addedReqs.length,
|
|
232
|
+
requirements_removed: removedReqs.length,
|
|
233
|
+
apis_added: addedApis.length,
|
|
234
|
+
apis_removed: removedApis.length
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Compare two artifact files on disk.
|
|
241
|
+
*
|
|
242
|
+
* @param {string} pathA - Path to original artifact.
|
|
243
|
+
* @param {string} pathB - Path to modified artifact.
|
|
244
|
+
* @param {object} [options]
|
|
245
|
+
* @returns {object}
|
|
246
|
+
*/
|
|
247
|
+
function compareFiles(pathA, pathB, options = {}) {
|
|
248
|
+
if (!fs.existsSync(pathA)) {
|
|
249
|
+
return { success: false, error: `File not found: ${pathA}` };
|
|
250
|
+
}
|
|
251
|
+
if (!fs.existsSync(pathB)) {
|
|
252
|
+
return { success: false, error: `File not found: ${pathB}` };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const contentA = fs.readFileSync(pathA, 'utf8');
|
|
256
|
+
const contentB = fs.readFileSync(pathB, 'utf8');
|
|
257
|
+
|
|
258
|
+
const result = compareArtifacts(contentA, contentB, options);
|
|
259
|
+
result.file_a = pathA;
|
|
260
|
+
result.file_b = pathB;
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Detect cross-artifact consistency issues across multiple spec files.
|
|
266
|
+
*
|
|
267
|
+
* @param {string} root - Project root.
|
|
268
|
+
* @param {object} [options]
|
|
269
|
+
* @returns {object}
|
|
270
|
+
*/
|
|
271
|
+
function crossArtifactDiff(root, options = {}) {
|
|
272
|
+
const specsDir = path.join(root, 'specs');
|
|
273
|
+
if (!fs.existsSync(specsDir)) {
|
|
274
|
+
return { success: false, error: 'specs/ directory not found' };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const artifacts = {};
|
|
278
|
+
const artifactFiles = ['challenger-brief.md', 'product-brief.md', 'prd.md', 'architecture.md', 'implementation-plan.md'];
|
|
279
|
+
|
|
280
|
+
for (const file of artifactFiles) {
|
|
281
|
+
const fullPath = path.join(specsDir, file);
|
|
282
|
+
if (fs.existsSync(fullPath)) {
|
|
283
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
284
|
+
artifacts[file] = {
|
|
285
|
+
content,
|
|
286
|
+
requirements: extractRequirements(content),
|
|
287
|
+
apis: extractApiEndpoints(content),
|
|
288
|
+
sections: extractSections(content)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const inconsistencies = [];
|
|
294
|
+
|
|
295
|
+
// Check requirement coverage flow (upstream → downstream)
|
|
296
|
+
const orderedKeys = Object.keys(artifacts);
|
|
297
|
+
for (let i = 0; i < orderedKeys.length - 1; i++) {
|
|
298
|
+
const upstream = artifacts[orderedKeys[i]];
|
|
299
|
+
const downstream = artifacts[orderedKeys[i + 1]];
|
|
300
|
+
if (!upstream || !downstream) continue;
|
|
301
|
+
|
|
302
|
+
const missingDownstream = upstream.requirements.filter(r => !downstream.requirements.includes(r));
|
|
303
|
+
if (missingDownstream.length > 0) {
|
|
304
|
+
inconsistencies.push({
|
|
305
|
+
type: 'requirement_gap',
|
|
306
|
+
upstream: orderedKeys[i],
|
|
307
|
+
downstream: orderedKeys[i + 1],
|
|
308
|
+
missing_requirements: missingDownstream,
|
|
309
|
+
severity: 'warning'
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
success: true,
|
|
316
|
+
artifacts_analyzed: Object.keys(artifacts).length,
|
|
317
|
+
inconsistencies,
|
|
318
|
+
summary: {
|
|
319
|
+
total_inconsistencies: inconsistencies.length,
|
|
320
|
+
requirement_gaps: inconsistencies.filter(i => i.type === 'requirement_gap').length
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = {
|
|
326
|
+
extractSections,
|
|
327
|
+
extractRequirements,
|
|
328
|
+
extractApiEndpoints,
|
|
329
|
+
extractTableData,
|
|
330
|
+
normalizeText,
|
|
331
|
+
textSimilarity,
|
|
332
|
+
compareArtifacts,
|
|
333
|
+
compareFiles,
|
|
334
|
+
crossArtifactDiff
|
|
335
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sla-slo.js — SLA & SLO Specification Support (Item 28)
|
|
3
|
+
*
|
|
4
|
+
* Make operational expectations first-class citizens in the PRD
|
|
5
|
+
* and architecture.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node bin/lib/sla-slo.js define|check|report [options]
|
|
9
|
+
*
|
|
10
|
+
* State file: .jumpstart/state/sla-slo.json
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const DEFAULT_STATE_FILE = path.join('.jumpstart', 'state', 'sla-slo.json');
|
|
19
|
+
|
|
20
|
+
const SLO_TYPES = ['availability', 'latency', 'throughput', 'error-rate', 'durability', 'freshness'];
|
|
21
|
+
|
|
22
|
+
const DEFAULT_SLO_TEMPLATES = {
|
|
23
|
+
'web-api': [
|
|
24
|
+
{ type: 'availability', target: 99.9, unit: 'percent', window: '30d' },
|
|
25
|
+
{ type: 'latency', target: 200, unit: 'ms', percentile: 'p99', window: '30d' },
|
|
26
|
+
{ type: 'error-rate', target: 0.1, unit: 'percent', window: '30d' }
|
|
27
|
+
],
|
|
28
|
+
'batch-processing': [
|
|
29
|
+
{ type: 'availability', target: 99.5, unit: 'percent', window: '30d' },
|
|
30
|
+
{ type: 'throughput', target: 1000, unit: 'records/sec', window: '1h' },
|
|
31
|
+
{ type: 'freshness', target: 60, unit: 'minutes', window: '24h' }
|
|
32
|
+
],
|
|
33
|
+
'data-pipeline': [
|
|
34
|
+
{ type: 'availability', target: 99.0, unit: 'percent', window: '30d' },
|
|
35
|
+
{ type: 'freshness', target: 15, unit: 'minutes', window: '24h' },
|
|
36
|
+
{ type: 'durability', target: 99.999, unit: 'percent', window: '30d' }
|
|
37
|
+
]
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function defaultState() {
|
|
41
|
+
return {
|
|
42
|
+
version: '1.0.0',
|
|
43
|
+
created_at: new Date().toISOString(),
|
|
44
|
+
last_updated: null,
|
|
45
|
+
slos: [],
|
|
46
|
+
slas: [],
|
|
47
|
+
error_budgets: []
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function loadState(stateFile) {
|
|
52
|
+
const filePath = stateFile || DEFAULT_STATE_FILE;
|
|
53
|
+
if (!fs.existsSync(filePath)) return defaultState();
|
|
54
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
|
|
55
|
+
catch { return defaultState(); }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function saveState(state, stateFile) {
|
|
59
|
+
const filePath = stateFile || DEFAULT_STATE_FILE;
|
|
60
|
+
const dir = path.dirname(filePath);
|
|
61
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
state.last_updated = new Date().toISOString();
|
|
63
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Define an SLO.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} slo - { name, service, type, target, unit, window, description? }
|
|
70
|
+
* @param {object} [options]
|
|
71
|
+
* @returns {object}
|
|
72
|
+
*/
|
|
73
|
+
function defineSLO(slo, options = {}) {
|
|
74
|
+
if (!slo || !slo.name || !slo.service || !slo.target) {
|
|
75
|
+
return { success: false, error: 'name, service, and target are required' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const type = (slo.type || 'availability').toLowerCase();
|
|
79
|
+
if (!SLO_TYPES.includes(type)) {
|
|
80
|
+
return { success: false, error: `Invalid type. Must be one of: ${SLO_TYPES.join(', ')}` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
84
|
+
const state = loadState(stateFile);
|
|
85
|
+
|
|
86
|
+
const newSLO = {
|
|
87
|
+
id: `SLO-${Date.now().toString(36).toUpperCase()}`,
|
|
88
|
+
name: slo.name,
|
|
89
|
+
service: slo.service,
|
|
90
|
+
type,
|
|
91
|
+
target: slo.target,
|
|
92
|
+
unit: slo.unit || 'percent',
|
|
93
|
+
window: slo.window || '30d',
|
|
94
|
+
description: slo.description || '',
|
|
95
|
+
created_at: new Date().toISOString()
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
state.slos.push(newSLO);
|
|
99
|
+
saveState(state, stateFile);
|
|
100
|
+
|
|
101
|
+
return { success: true, slo: newSLO };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Apply an SLO template by service type.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} serviceName
|
|
108
|
+
* @param {string} templateType - 'web-api' | 'batch-processing' | 'data-pipeline'
|
|
109
|
+
* @param {object} [options]
|
|
110
|
+
* @returns {object}
|
|
111
|
+
*/
|
|
112
|
+
function applyTemplate(serviceName, templateType, options = {}) {
|
|
113
|
+
const template = DEFAULT_SLO_TEMPLATES[templateType];
|
|
114
|
+
if (!template) {
|
|
115
|
+
return { success: false, error: `Unknown template: ${templateType}. Available: ${Object.keys(DEFAULT_SLO_TEMPLATES).join(', ')}` };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const results = [];
|
|
119
|
+
for (const t of template) {
|
|
120
|
+
const result = defineSLO({
|
|
121
|
+
name: `${serviceName} ${t.type}`,
|
|
122
|
+
service: serviceName,
|
|
123
|
+
...t
|
|
124
|
+
}, options);
|
|
125
|
+
results.push(result);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { success: true, service: serviceName, template: templateType, slos_created: results.length };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check SLO coverage in specs.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} root - Project root.
|
|
135
|
+
* @param {object} [options]
|
|
136
|
+
* @returns {object}
|
|
137
|
+
*/
|
|
138
|
+
function checkSLOCoverage(root, options = {}) {
|
|
139
|
+
const stateFile = options.stateFile || path.join(root, DEFAULT_STATE_FILE);
|
|
140
|
+
const state = loadState(stateFile);
|
|
141
|
+
|
|
142
|
+
const archFile = path.join(root, 'specs', 'architecture.md');
|
|
143
|
+
const prdFile = path.join(root, 'specs', 'prd.md');
|
|
144
|
+
|
|
145
|
+
let archHasSLO = false;
|
|
146
|
+
let prdHasSLO = false;
|
|
147
|
+
|
|
148
|
+
if (fs.existsSync(archFile)) {
|
|
149
|
+
try {
|
|
150
|
+
const content = fs.readFileSync(archFile, 'utf8');
|
|
151
|
+
archHasSLO = /\bSL[OA]\b|service.level|availability|latency.target/i.test(content);
|
|
152
|
+
} catch { /* ignore */ }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (fs.existsSync(prdFile)) {
|
|
156
|
+
try {
|
|
157
|
+
const content = fs.readFileSync(prdFile, 'utf8');
|
|
158
|
+
prdHasSLO = /\bSL[OA]\b|service.level|availability|uptime/i.test(content);
|
|
159
|
+
} catch { /* ignore */ }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
success: true,
|
|
164
|
+
defined_slos: state.slos.length,
|
|
165
|
+
architecture_mentions_slo: archHasSLO,
|
|
166
|
+
prd_mentions_slo: prdHasSLO,
|
|
167
|
+
coverage: state.slos.length > 0 ? 'defined' : 'missing',
|
|
168
|
+
recommendations: state.slos.length === 0 ? ['Define SLOs using `jumpstart-mode sla-slo define`'] : []
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Generate SLO report.
|
|
174
|
+
*
|
|
175
|
+
* @param {object} [options]
|
|
176
|
+
* @returns {object}
|
|
177
|
+
*/
|
|
178
|
+
function generateReport(options = {}) {
|
|
179
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
180
|
+
const state = loadState(stateFile);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
success: true,
|
|
184
|
+
slos: state.slos,
|
|
185
|
+
slas: state.slas,
|
|
186
|
+
total_slos: state.slos.length,
|
|
187
|
+
total_slas: state.slas.length,
|
|
188
|
+
by_service: state.slos.reduce((acc, s) => {
|
|
189
|
+
acc[s.service] = acc[s.service] || [];
|
|
190
|
+
acc[s.service].push(s);
|
|
191
|
+
return acc;
|
|
192
|
+
}, {}),
|
|
193
|
+
by_type: state.slos.reduce((acc, s) => {
|
|
194
|
+
acc[s.type] = (acc[s.type] || 0) + 1;
|
|
195
|
+
return acc;
|
|
196
|
+
}, {})
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = {
|
|
201
|
+
defaultState,
|
|
202
|
+
loadState,
|
|
203
|
+
saveState,
|
|
204
|
+
defineSLO,
|
|
205
|
+
applyTemplate,
|
|
206
|
+
checkSLOCoverage,
|
|
207
|
+
generateReport,
|
|
208
|
+
SLO_TYPES,
|
|
209
|
+
DEFAULT_SLO_TEMPLATES
|
|
210
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spec-comments.js — Inline Spec Review Comments (Item 63)
|
|
3
|
+
*
|
|
4
|
+
* Comment, resolve, assign, and approve within artifact sections.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node bin/lib/spec-comments.js add|resolve|list|assign [options]
|
|
8
|
+
*
|
|
9
|
+
* State file: .jumpstart/state/spec-comments.json
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const DEFAULT_STATE_FILE = path.join('.jumpstart', 'state', 'spec-comments.json');
|
|
18
|
+
|
|
19
|
+
const COMMENT_STATUSES = ['open', 'resolved', 'wontfix', 'deferred'];
|
|
20
|
+
|
|
21
|
+
function defaultState() {
|
|
22
|
+
return {
|
|
23
|
+
version: '1.0.0',
|
|
24
|
+
created_at: new Date().toISOString(),
|
|
25
|
+
last_updated: null,
|
|
26
|
+
comments: []
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function loadState(stateFile) {
|
|
31
|
+
const fp = stateFile || DEFAULT_STATE_FILE;
|
|
32
|
+
if (!fs.existsSync(fp)) return defaultState();
|
|
33
|
+
try { return JSON.parse(fs.readFileSync(fp, 'utf8')); }
|
|
34
|
+
catch { return defaultState(); }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function saveState(state, stateFile) {
|
|
38
|
+
const fp = stateFile || DEFAULT_STATE_FILE;
|
|
39
|
+
const dir = path.dirname(fp);
|
|
40
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
41
|
+
state.last_updated = new Date().toISOString();
|
|
42
|
+
fs.writeFileSync(fp, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Add a review comment to a spec artifact.
|
|
47
|
+
*/
|
|
48
|
+
function addComment(artifact, section, text, options = {}) {
|
|
49
|
+
if (!artifact || !text) return { success: false, error: 'artifact and text are required' };
|
|
50
|
+
|
|
51
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
52
|
+
const state = loadState(stateFile);
|
|
53
|
+
|
|
54
|
+
const comment = {
|
|
55
|
+
id: `C-${Date.now()}`,
|
|
56
|
+
artifact,
|
|
57
|
+
section: section || null,
|
|
58
|
+
text,
|
|
59
|
+
author: options.author || 'anonymous',
|
|
60
|
+
assignee: options.assignee || null,
|
|
61
|
+
status: 'open',
|
|
62
|
+
created_at: new Date().toISOString(),
|
|
63
|
+
resolved_at: null,
|
|
64
|
+
replies: []
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
state.comments.push(comment);
|
|
68
|
+
saveState(state, stateFile);
|
|
69
|
+
|
|
70
|
+
return { success: true, comment };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a comment.
|
|
75
|
+
*/
|
|
76
|
+
function resolveComment(commentId, resolution, options = {}) {
|
|
77
|
+
if (!commentId) return { success: false, error: 'commentId is required' };
|
|
78
|
+
|
|
79
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
80
|
+
const state = loadState(stateFile);
|
|
81
|
+
|
|
82
|
+
const comment = state.comments.find(c => c.id === commentId);
|
|
83
|
+
if (!comment) return { success: false, error: `Comment ${commentId} not found` };
|
|
84
|
+
|
|
85
|
+
comment.status = 'resolved';
|
|
86
|
+
comment.resolution = resolution || 'Resolved';
|
|
87
|
+
comment.resolved_at = new Date().toISOString();
|
|
88
|
+
comment.resolved_by = options.author || 'anonymous';
|
|
89
|
+
saveState(state, stateFile);
|
|
90
|
+
|
|
91
|
+
return { success: true, comment };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* List comments, optionally filtered.
|
|
96
|
+
*/
|
|
97
|
+
function listComments(options = {}) {
|
|
98
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
99
|
+
const state = loadState(stateFile);
|
|
100
|
+
|
|
101
|
+
let comments = state.comments;
|
|
102
|
+
|
|
103
|
+
if (options.artifact) {
|
|
104
|
+
comments = comments.filter(c => c.artifact === options.artifact);
|
|
105
|
+
}
|
|
106
|
+
if (options.status) {
|
|
107
|
+
comments = comments.filter(c => c.status === options.status);
|
|
108
|
+
}
|
|
109
|
+
if (options.assignee) {
|
|
110
|
+
comments = comments.filter(c => c.assignee === options.assignee);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
total: comments.length,
|
|
116
|
+
comments
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Assign a comment to a reviewer.
|
|
122
|
+
*/
|
|
123
|
+
function assignComment(commentId, assignee, options = {}) {
|
|
124
|
+
if (!commentId || !assignee) return { success: false, error: 'commentId and assignee are required' };
|
|
125
|
+
|
|
126
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
127
|
+
const state = loadState(stateFile);
|
|
128
|
+
|
|
129
|
+
const comment = state.comments.find(c => c.id === commentId);
|
|
130
|
+
if (!comment) return { success: false, error: `Comment ${commentId} not found` };
|
|
131
|
+
|
|
132
|
+
comment.assignee = assignee;
|
|
133
|
+
saveState(state, stateFile);
|
|
134
|
+
|
|
135
|
+
return { success: true, comment };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
addComment,
|
|
140
|
+
resolveComment,
|
|
141
|
+
listComments,
|
|
142
|
+
assignComment,
|
|
143
|
+
loadState,
|
|
144
|
+
saveState,
|
|
145
|
+
defaultState,
|
|
146
|
+
COMMENT_STATUSES
|
|
147
|
+
};
|