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.
Files changed (146) hide show
  1. package/.github/agents/jumpstart-adversary.agent.md +2 -1
  2. package/.github/agents/jumpstart-architect.agent.md +5 -6
  3. package/.github/agents/jumpstart-challenger.agent.md +2 -1
  4. package/.github/agents/jumpstart-devops.agent.md +2 -2
  5. package/.github/agents/jumpstart-diagram-verifier.agent.md +2 -1
  6. package/.github/agents/jumpstart-maintenance.agent.md +1 -0
  7. package/.github/agents/jumpstart-performance.agent.md +1 -0
  8. package/.github/agents/jumpstart-pm.agent.md +1 -1
  9. package/.github/agents/jumpstart-refactor.agent.md +1 -0
  10. package/.github/agents/jumpstart-requirements-extractor.agent.md +1 -0
  11. package/.github/agents/jumpstart-researcher.agent.md +1 -0
  12. package/.github/agents/jumpstart-retrospective.agent.md +1 -0
  13. package/.github/agents/jumpstart-reviewer.agent.md +2 -0
  14. package/.github/agents/jumpstart-scout.agent.md +1 -1
  15. package/.github/agents/jumpstart-scrum-master.agent.md +1 -0
  16. package/.github/agents/jumpstart-security.agent.md +2 -1
  17. package/.github/agents/jumpstart-tech-writer.agent.md +1 -0
  18. package/.github/workflows/quality.yml +19 -2
  19. package/.jumpstart/agents/analyst.md +38 -0
  20. package/.jumpstart/agents/architect.md +38 -0
  21. package/.jumpstart/agents/challenger.md +38 -0
  22. package/.jumpstart/agents/developer.md +41 -0
  23. package/.jumpstart/agents/pm.md +38 -0
  24. package/.jumpstart/agents/scout.md +33 -0
  25. package/.jumpstart/agents/ux-designer.md +4 -0
  26. package/.jumpstart/config.yaml +24 -0
  27. package/.jumpstart/schemas/timeline.schema.json +1 -0
  28. package/.jumpstart/skills/skill-creator/SKILL.md +485 -357
  29. package/.jumpstart/skills/skill-creator/agents/analyzer.md +274 -0
  30. package/.jumpstart/skills/skill-creator/agents/comparator.md +202 -0
  31. package/.jumpstart/skills/skill-creator/agents/grader.md +223 -0
  32. package/.jumpstart/skills/skill-creator/assets/eval_review.html +146 -0
  33. package/.jumpstart/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  34. package/.jumpstart/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  35. package/.jumpstart/skills/skill-creator/references/schemas.md +430 -0
  36. package/.jumpstart/skills/skill-creator/scripts/__init__.py +0 -0
  37. package/.jumpstart/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  38. package/.jumpstart/skills/skill-creator/scripts/generate_report.py +326 -0
  39. package/.jumpstart/skills/skill-creator/scripts/improve_description.py +247 -0
  40. package/.jumpstart/skills/skill-creator/scripts/package_skill.py +136 -110
  41. package/.jumpstart/skills/skill-creator/scripts/run_eval.py +310 -0
  42. package/.jumpstart/skills/skill-creator/scripts/run_loop.py +328 -0
  43. package/.jumpstart/skills/skill-creator/scripts/utils.py +47 -0
  44. package/.jumpstart/state/timeline.json +659 -0
  45. package/.jumpstart/usage-log.json +74 -3
  46. package/README.md +62 -1
  47. package/bin/cli.js +3217 -1
  48. package/bin/headless-runner.js +62 -2
  49. package/bin/lib/agent-checkpoint.js +168 -0
  50. package/bin/lib/ai-evaluation.js +104 -0
  51. package/bin/lib/ai-intake.js +152 -0
  52. package/bin/lib/ambiguity-heatmap.js +152 -0
  53. package/bin/lib/artifact-comparison.js +104 -0
  54. package/bin/lib/ast-edit-engine.js +157 -0
  55. package/bin/lib/backlog-sync.js +338 -0
  56. package/bin/lib/bcdr-planning.js +158 -0
  57. package/bin/lib/bidirectional-trace.js +199 -0
  58. package/bin/lib/branch-workflow.js +266 -0
  59. package/bin/lib/cab-output.js +119 -0
  60. package/bin/lib/chat-integration.js +122 -0
  61. package/bin/lib/ci-cd-integration.js +208 -0
  62. package/bin/lib/codebase-retrieval.js +125 -0
  63. package/bin/lib/collaboration.js +168 -0
  64. package/bin/lib/compliance-packs.js +213 -0
  65. package/bin/lib/context-chunker.js +128 -0
  66. package/bin/lib/context-onboarding.js +122 -0
  67. package/bin/lib/contract-first.js +124 -0
  68. package/bin/lib/cost-router.js +148 -0
  69. package/bin/lib/credential-boundary.js +155 -0
  70. package/bin/lib/data-classification.js +180 -0
  71. package/bin/lib/data-contracts.js +129 -0
  72. package/bin/lib/db-evolution.js +158 -0
  73. package/bin/lib/decision-conflicts.js +299 -0
  74. package/bin/lib/delivery-confidence.js +361 -0
  75. package/bin/lib/dependency-upgrade.js +153 -0
  76. package/bin/lib/design-system.js +133 -0
  77. package/bin/lib/deterministic-artifacts.js +151 -0
  78. package/bin/lib/diagram-studio.js +115 -0
  79. package/bin/lib/domain-ontology.js +140 -0
  80. package/bin/lib/ea-review-packet.js +151 -0
  81. package/bin/lib/enterprise-search.js +123 -0
  82. package/bin/lib/enterprise-templates.js +140 -0
  83. package/bin/lib/environment-promotion.js +220 -0
  84. package/bin/lib/estimation-studio.js +130 -0
  85. package/bin/lib/event-modeling.js +133 -0
  86. package/bin/lib/evidence-collector.js +179 -0
  87. package/bin/lib/finops-planner.js +182 -0
  88. package/bin/lib/fitness-functions.js +279 -0
  89. package/bin/lib/focus.js +448 -0
  90. package/bin/lib/governance-dashboard.js +165 -0
  91. package/bin/lib/guided-handoff.js +120 -0
  92. package/bin/lib/impact-analysis.js +190 -0
  93. package/bin/lib/incident-feedback.js +157 -0
  94. package/bin/lib/integrate.js +1 -1
  95. package/bin/lib/knowledge-graph.js +122 -0
  96. package/bin/lib/legacy-modernizer.js +160 -0
  97. package/bin/lib/migration-planner.js +144 -0
  98. package/bin/lib/model-governance.js +185 -0
  99. package/bin/lib/model-router.js +144 -0
  100. package/bin/lib/multi-repo.js +272 -0
  101. package/bin/lib/next-phase.js +53 -8
  102. package/bin/lib/ops-ownership.js +152 -0
  103. package/bin/lib/parallel-agents.js +257 -0
  104. package/bin/lib/pattern-library.js +115 -0
  105. package/bin/lib/persona-packs.js +99 -0
  106. package/bin/lib/plan-executor.js +366 -0
  107. package/bin/lib/platform-engineering.js +119 -0
  108. package/bin/lib/playback-summaries.js +126 -0
  109. package/bin/lib/policy-engine.js +240 -0
  110. package/bin/lib/portfolio-reporting.js +357 -0
  111. package/bin/lib/pr-package.js +197 -0
  112. package/bin/lib/project-memory.js +235 -0
  113. package/bin/lib/prompt-governance.js +130 -0
  114. package/bin/lib/promptless-mode.js +128 -0
  115. package/bin/lib/quality-graph.js +193 -0
  116. package/bin/lib/raci-matrix.js +188 -0
  117. package/bin/lib/refactor-planner.js +167 -0
  118. package/bin/lib/reference-architectures.js +304 -0
  119. package/bin/lib/release-readiness.js +171 -0
  120. package/bin/lib/repo-graph.js +262 -0
  121. package/bin/lib/requirements-baseline.js +358 -0
  122. package/bin/lib/risk-register.js +211 -0
  123. package/bin/lib/role-approval.js +249 -0
  124. package/bin/lib/role-views.js +142 -0
  125. package/bin/lib/root-cause-analysis.js +132 -0
  126. package/bin/lib/runtime-debugger.js +154 -0
  127. package/bin/lib/safe-rename.js +135 -0
  128. package/bin/lib/semantic-diff.js +335 -0
  129. package/bin/lib/sla-slo.js +210 -0
  130. package/bin/lib/spec-comments.js +147 -0
  131. package/bin/lib/spec-maturity.js +287 -0
  132. package/bin/lib/sre-integration.js +154 -0
  133. package/bin/lib/structured-elicitation.js +174 -0
  134. package/bin/lib/telemetry-feedback.js +118 -0
  135. package/bin/lib/test-generator.js +146 -0
  136. package/bin/lib/timeline.js +2 -1
  137. package/bin/lib/tool-bridge.js +107 -0
  138. package/bin/lib/tool-guardrails.js +139 -0
  139. package/bin/lib/tool-schemas.js +172 -3
  140. package/bin/lib/transcript-ingestion.js +150 -0
  141. package/bin/lib/vendor-risk.js +173 -0
  142. package/bin/lib/waiver-workflow.js +174 -0
  143. package/bin/lib/web-dashboard.js +126 -0
  144. package/bin/lib/workshop-mode.js +165 -0
  145. package/bin/lib/workstream-ownership.js +104 -0
  146. 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
+ };