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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* data-contracts.js — Data Contract Governance (Item 84)
|
|
3
|
+
*
|
|
4
|
+
* Schema evolution, versioning, lineage, and producer-consumer compatibility.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node bin/lib/data-contracts.js register|validate|lineage|report [options]
|
|
8
|
+
*
|
|
9
|
+
* State file: .jumpstart/state/data-contracts.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', 'data-contracts.json');
|
|
18
|
+
|
|
19
|
+
const COMPATIBILITY_MODES = ['backward', 'forward', 'full', 'none'];
|
|
20
|
+
|
|
21
|
+
function defaultState() {
|
|
22
|
+
return { version: '1.0.0', contracts: [], lineage: [], last_updated: null };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadState(stateFile) {
|
|
26
|
+
const fp = stateFile || DEFAULT_STATE_FILE;
|
|
27
|
+
if (!fs.existsSync(fp)) return defaultState();
|
|
28
|
+
try { return JSON.parse(fs.readFileSync(fp, 'utf8')); }
|
|
29
|
+
catch { return defaultState(); }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function saveState(state, stateFile) {
|
|
33
|
+
const fp = stateFile || DEFAULT_STATE_FILE;
|
|
34
|
+
const dir = path.dirname(fp);
|
|
35
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
state.last_updated = new Date().toISOString();
|
|
37
|
+
fs.writeFileSync(fp, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function registerContract(name, schema, options = {}) {
|
|
41
|
+
if (!name || !schema) return { success: false, error: 'name and schema are required' };
|
|
42
|
+
|
|
43
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
44
|
+
const state = loadState(stateFile);
|
|
45
|
+
|
|
46
|
+
const contract = {
|
|
47
|
+
id: `DC-${Date.now()}`,
|
|
48
|
+
name,
|
|
49
|
+
version: options.version || '1.0.0',
|
|
50
|
+
schema,
|
|
51
|
+
producer: options.producer || null,
|
|
52
|
+
consumers: options.consumers || [],
|
|
53
|
+
compatibility: options.compatibility || 'backward',
|
|
54
|
+
created_at: new Date().toISOString()
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
state.contracts.push(contract);
|
|
58
|
+
saveState(state, stateFile);
|
|
59
|
+
|
|
60
|
+
return { success: true, contract };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validateCompatibility(contractId, newSchema, options = {}) {
|
|
64
|
+
if (!contractId || !newSchema) return { success: false, error: 'contractId and newSchema are required' };
|
|
65
|
+
|
|
66
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
67
|
+
const state = loadState(stateFile);
|
|
68
|
+
|
|
69
|
+
const contract = state.contracts.find(c => c.id === contractId);
|
|
70
|
+
if (!contract) return { success: false, error: `Contract ${contractId} not found` };
|
|
71
|
+
|
|
72
|
+
const oldFields = new Set(Object.keys(contract.schema));
|
|
73
|
+
const newFields = new Set(Object.keys(newSchema));
|
|
74
|
+
|
|
75
|
+
const added = [...newFields].filter(f => !oldFields.has(f));
|
|
76
|
+
const removed = [...oldFields].filter(f => !newFields.has(f));
|
|
77
|
+
|
|
78
|
+
let compatible = true;
|
|
79
|
+
const issues = [];
|
|
80
|
+
|
|
81
|
+
if (contract.compatibility === 'backward' && removed.length > 0) {
|
|
82
|
+
compatible = false;
|
|
83
|
+
issues.push({ type: 'breaking_removal', fields: removed });
|
|
84
|
+
}
|
|
85
|
+
if (contract.compatibility === 'forward' && added.length > 0) {
|
|
86
|
+
compatible = false;
|
|
87
|
+
issues.push({ type: 'forward_incompatible', fields: added });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { success: true, compatible, issues, added, removed };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function trackLineage(sourceContract, targetContract, options = {}) {
|
|
94
|
+
if (!sourceContract || !targetContract) return { success: false, error: 'source and target contracts are required' };
|
|
95
|
+
|
|
96
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
97
|
+
const state = loadState(stateFile);
|
|
98
|
+
|
|
99
|
+
const entry = {
|
|
100
|
+
source: sourceContract,
|
|
101
|
+
target: targetContract,
|
|
102
|
+
transformation: options.transformation || 'direct',
|
|
103
|
+
created_at: new Date().toISOString()
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
state.lineage.push(entry);
|
|
107
|
+
saveState(state, stateFile);
|
|
108
|
+
|
|
109
|
+
return { success: true, lineage: entry };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function generateReport(options = {}) {
|
|
113
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
114
|
+
const state = loadState(stateFile);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
success: true,
|
|
118
|
+
total_contracts: state.contracts.length,
|
|
119
|
+
total_lineage: state.lineage.length,
|
|
120
|
+
contracts: state.contracts.map(c => ({ id: c.id, name: c.name, version: c.version, compatibility: c.compatibility })),
|
|
121
|
+
lineage: state.lineage
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
registerContract, validateCompatibility, trackLineage, generateReport,
|
|
127
|
+
loadState, saveState, defaultState,
|
|
128
|
+
COMPATIBILITY_MODES
|
|
129
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* db-evolution.js — Database Evolution Planner (Item 49)
|
|
3
|
+
*
|
|
4
|
+
* Generate migration strategy, backward compatibility plan,
|
|
5
|
+
* data validation, and rollback approach.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node bin/lib/db-evolution.js plan|validate|report [options]
|
|
9
|
+
*
|
|
10
|
+
* State file: .jumpstart/state/db-evolution.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', 'db-evolution.json');
|
|
19
|
+
|
|
20
|
+
const MIGRATION_TYPES = ['add-column', 'drop-column', 'rename-column', 'add-table', 'drop-table',
|
|
21
|
+
'add-index', 'drop-index', 'modify-type', 'add-constraint', 'data-migration'];
|
|
22
|
+
|
|
23
|
+
const RISK_LEVELS = {
|
|
24
|
+
'add-column': 'low', 'add-table': 'low', 'add-index': 'low',
|
|
25
|
+
'rename-column': 'medium', 'modify-type': 'medium', 'add-constraint': 'medium',
|
|
26
|
+
'drop-column': 'high', 'drop-table': 'high', 'drop-index': 'medium',
|
|
27
|
+
'data-migration': 'high'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function defaultState() {
|
|
31
|
+
return {
|
|
32
|
+
version: '1.0.0',
|
|
33
|
+
created_at: new Date().toISOString(),
|
|
34
|
+
last_updated: null,
|
|
35
|
+
migrations: [],
|
|
36
|
+
rollback_scripts: []
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadState(stateFile) {
|
|
41
|
+
const filePath = stateFile || DEFAULT_STATE_FILE;
|
|
42
|
+
if (!fs.existsSync(filePath)) return defaultState();
|
|
43
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
|
|
44
|
+
catch { return defaultState(); }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function saveState(state, stateFile) {
|
|
48
|
+
const filePath = stateFile || DEFAULT_STATE_FILE;
|
|
49
|
+
const dir = path.dirname(filePath);
|
|
50
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
51
|
+
state.last_updated = new Date().toISOString();
|
|
52
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Plan a database migration.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} migration - { name, type, description, table?, column?, backward_compatible? }
|
|
59
|
+
* @param {object} [options]
|
|
60
|
+
* @returns {object}
|
|
61
|
+
*/
|
|
62
|
+
function planMigration(migration, options = {}) {
|
|
63
|
+
if (!migration || !migration.name || !migration.type) {
|
|
64
|
+
return { success: false, error: 'name and type are required' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!MIGRATION_TYPES.includes(migration.type)) {
|
|
68
|
+
return { success: false, error: `Invalid type. Must be one of: ${MIGRATION_TYPES.join(', ')}` };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
72
|
+
const state = loadState(stateFile);
|
|
73
|
+
|
|
74
|
+
const risk = RISK_LEVELS[migration.type] || 'medium';
|
|
75
|
+
|
|
76
|
+
const mig = {
|
|
77
|
+
id: `DB-${(state.migrations.length + 1).toString().padStart(3, '0')}`,
|
|
78
|
+
name: migration.name,
|
|
79
|
+
type: migration.type,
|
|
80
|
+
description: migration.description || '',
|
|
81
|
+
table: migration.table || null,
|
|
82
|
+
column: migration.column || null,
|
|
83
|
+
risk_level: risk,
|
|
84
|
+
backward_compatible: migration.backward_compatible !== false,
|
|
85
|
+
rollback_strategy: migration.rollback_strategy || (risk === 'low' ? 'reverse-migration' : 'backup-restore'),
|
|
86
|
+
validation_steps: migration.validation_steps || ['row-count', 'schema-compare'],
|
|
87
|
+
status: 'planned',
|
|
88
|
+
created_at: new Date().toISOString()
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
state.migrations.push(mig);
|
|
92
|
+
saveState(state, stateFile);
|
|
93
|
+
|
|
94
|
+
return { success: true, migration: mig };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validate migration plan for safety.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} migrationId
|
|
101
|
+
* @param {object} [options]
|
|
102
|
+
* @returns {object}
|
|
103
|
+
*/
|
|
104
|
+
function validateMigration(migrationId, options = {}) {
|
|
105
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
106
|
+
const state = loadState(stateFile);
|
|
107
|
+
|
|
108
|
+
const mig = state.migrations.find(m => m.id === migrationId);
|
|
109
|
+
if (!mig) return { success: false, error: `Migration not found: ${migrationId}` };
|
|
110
|
+
|
|
111
|
+
const warnings = [];
|
|
112
|
+
if (!mig.backward_compatible) warnings.push('Migration is not backward compatible');
|
|
113
|
+
if (mig.risk_level === 'high') warnings.push('High-risk migration — requires explicit approval');
|
|
114
|
+
if (!mig.rollback_strategy) warnings.push('No rollback strategy defined');
|
|
115
|
+
if (mig.type === 'drop-table' || mig.type === 'drop-column') {
|
|
116
|
+
warnings.push('Destructive operation — ensure data backup exists');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
success: true,
|
|
121
|
+
migration_id: migrationId,
|
|
122
|
+
safe: warnings.length === 0,
|
|
123
|
+
warnings,
|
|
124
|
+
risk_level: mig.risk_level,
|
|
125
|
+
backward_compatible: mig.backward_compatible
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Generate DB evolution report.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} [options]
|
|
133
|
+
* @returns {object}
|
|
134
|
+
*/
|
|
135
|
+
function generateReport(options = {}) {
|
|
136
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
137
|
+
const state = loadState(stateFile);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
success: true,
|
|
141
|
+
total_migrations: state.migrations.length,
|
|
142
|
+
by_type: state.migrations.reduce((acc, m) => { acc[m.type] = (acc[m.type] || 0) + 1; return acc; }, {}),
|
|
143
|
+
by_risk: state.migrations.reduce((acc, m) => { acc[m.risk_level] = (acc[m.risk_level] || 0) + 1; return acc; }, {}),
|
|
144
|
+
high_risk: state.migrations.filter(m => m.risk_level === 'high'),
|
|
145
|
+
migrations: state.migrations
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
defaultState,
|
|
151
|
+
loadState,
|
|
152
|
+
saveState,
|
|
153
|
+
planMigration,
|
|
154
|
+
validateMigration,
|
|
155
|
+
generateReport,
|
|
156
|
+
MIGRATION_TYPES,
|
|
157
|
+
RISK_LEVELS
|
|
158
|
+
};
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decision-conflicts.js — Decision Conflict Detection
|
|
3
|
+
*
|
|
4
|
+
* Detect when ADRs, PRD decisions, architecture docs, and code
|
|
5
|
+
* choices contradict one another.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node bin/lib/decision-conflicts.js detect|report [options]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const CONFLICT_TYPES = ['technology', 'pattern', 'constraint', 'requirement', 'terminology'];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract decisions from ADR files.
|
|
20
|
+
* @param {string} decisionsDir - Path to specs/decisions/.
|
|
21
|
+
* @returns {object[]}
|
|
22
|
+
*/
|
|
23
|
+
function extractADRDecisions(decisionsDir) {
|
|
24
|
+
const decisions = [];
|
|
25
|
+
|
|
26
|
+
if (!fs.existsSync(decisionsDir)) return decisions;
|
|
27
|
+
|
|
28
|
+
const files = fs.readdirSync(decisionsDir).filter(f => f.endsWith('.md'));
|
|
29
|
+
for (const file of files) {
|
|
30
|
+
const content = fs.readFileSync(path.join(decisionsDir, file), 'utf8');
|
|
31
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
32
|
+
const statusMatch = content.match(/\*\*Status[:\s]*\*?\*?\s*(.+)/i) || content.match(/Status[:\s]+(.+)/i);
|
|
33
|
+
const decisionMatch = content.match(/##\s+Decision\s*\n([\s\S]*?)(?=\n##|\n$|$)/i);
|
|
34
|
+
|
|
35
|
+
decisions.push({
|
|
36
|
+
source: `specs/decisions/${file}`,
|
|
37
|
+
type: 'adr',
|
|
38
|
+
title: titleMatch ? titleMatch[1].trim() : file,
|
|
39
|
+
status: statusMatch ? statusMatch[1].trim().toLowerCase() : 'unknown',
|
|
40
|
+
decision_text: decisionMatch ? decisionMatch[1].trim() : '',
|
|
41
|
+
technologies: extractTechReferences(content),
|
|
42
|
+
patterns: extractPatternReferences(content)
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return decisions;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract decisions from architecture doc.
|
|
51
|
+
* @param {string} archPath
|
|
52
|
+
* @returns {object[]}
|
|
53
|
+
*/
|
|
54
|
+
function extractArchDecisions(archPath) {
|
|
55
|
+
if (!fs.existsSync(archPath)) return [];
|
|
56
|
+
|
|
57
|
+
const content = fs.readFileSync(archPath, 'utf8');
|
|
58
|
+
const decisions = [];
|
|
59
|
+
const sections = content.split(/^##\s+/m);
|
|
60
|
+
|
|
61
|
+
for (const section of sections) {
|
|
62
|
+
if (section.trim().length === 0) continue;
|
|
63
|
+
const titleLine = section.split('\n')[0].trim();
|
|
64
|
+
const sectionContent = section.split('\n').slice(1).join('\n');
|
|
65
|
+
|
|
66
|
+
decisions.push({
|
|
67
|
+
source: 'specs/architecture.md',
|
|
68
|
+
type: 'architecture',
|
|
69
|
+
title: titleLine,
|
|
70
|
+
decision_text: sectionContent.trim().slice(0, 500),
|
|
71
|
+
technologies: extractTechReferences(sectionContent),
|
|
72
|
+
patterns: extractPatternReferences(sectionContent)
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return decisions;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extract decisions from PRD.
|
|
81
|
+
* @param {string} prdPath
|
|
82
|
+
* @returns {object[]}
|
|
83
|
+
*/
|
|
84
|
+
function extractPRDDecisions(prdPath) {
|
|
85
|
+
if (!fs.existsSync(prdPath)) return [];
|
|
86
|
+
|
|
87
|
+
const content = fs.readFileSync(prdPath, 'utf8');
|
|
88
|
+
const decisions = [];
|
|
89
|
+
|
|
90
|
+
// Extract NFRs and constraints
|
|
91
|
+
const nfrSection = content.match(/##\s+(?:Non-Functional|NFR|Constraints).*?\n([\s\S]*?)(?=\n##|\n$|$)/i);
|
|
92
|
+
if (nfrSection) {
|
|
93
|
+
decisions.push({
|
|
94
|
+
source: 'specs/prd.md',
|
|
95
|
+
type: 'prd',
|
|
96
|
+
title: 'Non-Functional Requirements',
|
|
97
|
+
decision_text: nfrSection[1].trim().slice(0, 500),
|
|
98
|
+
technologies: extractTechReferences(nfrSection[1]),
|
|
99
|
+
patterns: extractPatternReferences(nfrSection[1])
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Extract technology decisions mentioned in PRD
|
|
104
|
+
const techSection = content.match(/##\s+(?:Tech|Technology|Stack|Technical).*?\n([\s\S]*?)(?=\n##|\n$|$)/i);
|
|
105
|
+
if (techSection) {
|
|
106
|
+
decisions.push({
|
|
107
|
+
source: 'specs/prd.md',
|
|
108
|
+
type: 'prd',
|
|
109
|
+
title: 'Technology Decisions',
|
|
110
|
+
decision_text: techSection[1].trim().slice(0, 500),
|
|
111
|
+
technologies: extractTechReferences(techSection[1]),
|
|
112
|
+
patterns: extractPatternReferences(techSection[1])
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return decisions;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Extract technology references from text.
|
|
121
|
+
* @param {string} text
|
|
122
|
+
* @returns {string[]}
|
|
123
|
+
*/
|
|
124
|
+
function extractTechReferences(text) {
|
|
125
|
+
const techTerms = [
|
|
126
|
+
'react', 'vue', 'angular', 'svelte', 'next\\.js', 'nuxt',
|
|
127
|
+
'express', 'fastify', 'koa', 'hapi', 'nestjs',
|
|
128
|
+
'postgresql', 'mysql', 'mongodb', 'redis', 'sqlite', 'dynamodb',
|
|
129
|
+
'docker', 'kubernetes', 'aws', 'azure', 'gcp',
|
|
130
|
+
'graphql', 'rest', 'grpc', 'websocket',
|
|
131
|
+
'typescript', 'javascript', 'python', 'go', 'rust', 'java',
|
|
132
|
+
'kafka', 'rabbitmq', 'sqs', 'nats',
|
|
133
|
+
'openai', 'langchain', 'pinecone', 'chromadb'
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const found = [];
|
|
137
|
+
for (const tech of techTerms) {
|
|
138
|
+
const pattern = new RegExp(`\\b${tech}\\b`, 'gi');
|
|
139
|
+
if (pattern.test(text)) {
|
|
140
|
+
found.push(tech.replace(/\\\./g, '.'));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return [...new Set(found)];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Extract architectural pattern references from text.
|
|
148
|
+
* @param {string} text
|
|
149
|
+
* @returns {string[]}
|
|
150
|
+
*/
|
|
151
|
+
function extractPatternReferences(text) {
|
|
152
|
+
const patternTerms = [
|
|
153
|
+
'microservice', 'monolith', 'serverless', 'event-driven', 'event sourcing',
|
|
154
|
+
'cqrs', 'saga', 'domain-driven', 'hexagonal', 'clean architecture',
|
|
155
|
+
'mvc', 'mvvm', 'repository pattern', 'factory', 'singleton',
|
|
156
|
+
'pub-sub', 'message queue', 'circuit breaker', 'api gateway',
|
|
157
|
+
'rag', 'agent', 'multi-agent'
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
const found = [];
|
|
161
|
+
for (const pattern of patternTerms) {
|
|
162
|
+
const regex = new RegExp(`\\b${pattern}s?\\b`, 'gi');
|
|
163
|
+
if (regex.test(text)) {
|
|
164
|
+
found.push(pattern);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return [...new Set(found)];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Detect conflicts between decisions.
|
|
172
|
+
*
|
|
173
|
+
* @param {object[]} decisions - Array of extracted decisions.
|
|
174
|
+
* @returns {object[]}
|
|
175
|
+
*/
|
|
176
|
+
function findConflicts(decisions) {
|
|
177
|
+
const conflicts = [];
|
|
178
|
+
|
|
179
|
+
// Technology conflicts: same category but different choices
|
|
180
|
+
const techBySource = {};
|
|
181
|
+
for (const d of decisions) {
|
|
182
|
+
for (const tech of d.technologies) {
|
|
183
|
+
if (!techBySource[tech]) techBySource[tech] = [];
|
|
184
|
+
techBySource[tech].push(d);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check for competing technologies in the same category
|
|
189
|
+
const competing = {
|
|
190
|
+
frontend: ['react', 'vue', 'angular', 'svelte'],
|
|
191
|
+
backend: ['express', 'fastify', 'koa', 'hapi', 'nestjs'],
|
|
192
|
+
database: ['postgresql', 'mysql', 'mongodb', 'sqlite', 'dynamodb'],
|
|
193
|
+
messaging: ['kafka', 'rabbitmq', 'sqs', 'nats'],
|
|
194
|
+
cloud: ['aws', 'azure', 'gcp']
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
for (const [category, techs] of Object.entries(competing)) {
|
|
198
|
+
const usedTechs = techs.filter(t => techBySource[t]);
|
|
199
|
+
if (usedTechs.length > 1) {
|
|
200
|
+
const sources = usedTechs.flatMap(t => techBySource[t].map(d => d.source));
|
|
201
|
+
conflicts.push({
|
|
202
|
+
type: 'technology',
|
|
203
|
+
category,
|
|
204
|
+
description: `Competing ${category} technologies referenced: ${usedTechs.join(', ')}`,
|
|
205
|
+
technologies: usedTechs,
|
|
206
|
+
sources: [...new Set(sources)],
|
|
207
|
+
severity: 'warning'
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Pattern conflicts: contradictory patterns
|
|
213
|
+
const patternBySource = {};
|
|
214
|
+
for (const d of decisions) {
|
|
215
|
+
for (const pattern of d.patterns) {
|
|
216
|
+
if (!patternBySource[pattern]) patternBySource[pattern] = [];
|
|
217
|
+
patternBySource[pattern].push(d);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const contradictory = [
|
|
222
|
+
['microservice', 'monolith'],
|
|
223
|
+
['event sourcing', 'cqrs'],
|
|
224
|
+
['serverless', 'kubernetes']
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
for (const [p1, p2] of contradictory) {
|
|
228
|
+
if (patternBySource[p1] && patternBySource[p2]) {
|
|
229
|
+
const sources = [
|
|
230
|
+
...patternBySource[p1].map(d => d.source),
|
|
231
|
+
...patternBySource[p2].map(d => d.source)
|
|
232
|
+
];
|
|
233
|
+
conflicts.push({
|
|
234
|
+
type: 'pattern',
|
|
235
|
+
description: `Potentially contradictory patterns: "${p1}" and "${p2}"`,
|
|
236
|
+
patterns: [p1, p2],
|
|
237
|
+
sources: [...new Set(sources)],
|
|
238
|
+
severity: 'warning'
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return conflicts;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Detect decision conflicts across all project artifacts.
|
|
248
|
+
*
|
|
249
|
+
* @param {string} root - Project root.
|
|
250
|
+
* @param {object} [options]
|
|
251
|
+
* @returns {object}
|
|
252
|
+
*/
|
|
253
|
+
function detectConflicts(root, options = {}) {
|
|
254
|
+
const decisions = [];
|
|
255
|
+
|
|
256
|
+
// Collect decisions from all sources
|
|
257
|
+
const decisionsDir = path.join(root, 'specs', 'decisions');
|
|
258
|
+
decisions.push(...extractADRDecisions(decisionsDir));
|
|
259
|
+
|
|
260
|
+
const archPath = path.join(root, 'specs', 'architecture.md');
|
|
261
|
+
decisions.push(...extractArchDecisions(archPath));
|
|
262
|
+
|
|
263
|
+
const prdPath = path.join(root, 'specs', 'prd.md');
|
|
264
|
+
decisions.push(...extractPRDDecisions(prdPath));
|
|
265
|
+
|
|
266
|
+
if (decisions.length === 0) {
|
|
267
|
+
return { success: true, conflicts: [], message: 'No decisions found to analyze' };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const conflicts = findConflicts(decisions);
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
success: true,
|
|
274
|
+
total_decisions: decisions.length,
|
|
275
|
+
decisions_by_source: {
|
|
276
|
+
adr: decisions.filter(d => d.type === 'adr').length,
|
|
277
|
+
architecture: decisions.filter(d => d.type === 'architecture').length,
|
|
278
|
+
prd: decisions.filter(d => d.type === 'prd').length
|
|
279
|
+
},
|
|
280
|
+
conflicts,
|
|
281
|
+
summary: {
|
|
282
|
+
total_conflicts: conflicts.length,
|
|
283
|
+
technology_conflicts: conflicts.filter(c => c.type === 'technology').length,
|
|
284
|
+
pattern_conflicts: conflicts.filter(c => c.type === 'pattern').length,
|
|
285
|
+
has_conflicts: conflicts.length > 0
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = {
|
|
291
|
+
extractADRDecisions,
|
|
292
|
+
extractArchDecisions,
|
|
293
|
+
extractPRDDecisions,
|
|
294
|
+
extractTechReferences,
|
|
295
|
+
extractPatternReferences,
|
|
296
|
+
findConflicts,
|
|
297
|
+
detectConflicts,
|
|
298
|
+
CONFLICT_TYPES
|
|
299
|
+
};
|