chati-dev 3.2.4 → 3.3.0
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/LICENSE +96 -0
- package/README.md +2 -2
- package/bin/chati.js +46 -0
- package/framework/agents/build/dev.md +122 -1
- package/framework/agents/deploy/devops.md +128 -3
- package/framework/agents/discover/brief.md +77 -15
- package/framework/agents/discover/brownfield-wu.md +2 -2
- package/framework/agents/discover/greenfield-wu.md +3 -3
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +3 -3
- package/framework/agents/plan/phases.md +127 -2
- package/framework/agents/plan/tasks.md +127 -2
- package/framework/agents/plan/ux.md +269 -22
- package/framework/agents/quality/qa-implementation.md +172 -8
- package/framework/agents/quality/qa-planning.md +147 -2
- package/framework/config.yaml +9 -5
- package/framework/constitution.md +7 -1
- package/framework/context/quality.md +1 -1
- package/framework/context/root.md +1 -1
- package/framework/hooks/constitution-guard.js +18 -2
- package/framework/hooks/mode-governance.js +3 -3
- package/framework/hooks/read-protection.js +10 -2
- package/framework/i18n/en.yaml +6 -0
- package/framework/i18n/es.yaml +6 -0
- package/framework/i18n/fr.yaml +6 -0
- package/framework/i18n/pt.yaml +6 -0
- package/framework/orchestrator/chati.md +102 -6
- package/framework/schemas/task.schema.json +1 -1
- package/framework/tasks/architect-dep-audit.md +128 -0
- package/framework/tasks/architect-stack-selection.md +28 -0
- package/framework/workflows/brownfield-fullstack.yaml +2 -2
- package/framework/workflows/brownfield-service.yaml +2 -2
- package/framework/workflows/brownfield-ui.yaml +2 -2
- package/framework/workflows/greenfield-fullstack.yaml +6 -2
- package/framework/workflows/quick-flow.yaml +7 -5
- package/framework/workflows/standard-flow.yaml +171 -0
- package/package.json +5 -3
- package/src/api/index.js +129 -0
- package/src/autonomy/build-loop.js +93 -6
- package/src/autonomy/build-state.js +20 -2
- package/src/autonomy/cause-analyzer.js +177 -0
- package/src/autonomy/escalation.js +214 -0
- package/src/autonomy/safety-net.js +23 -5
- package/src/autonomy/worktree-manager.js +245 -0
- package/src/config/agent-customizer.js +227 -0
- package/src/config/ide-configs.js +57 -27
- package/src/decision/analyzer.js +148 -0
- package/src/decision/registry-healer.js +38 -21
- package/src/extensions/loader.js +151 -0
- package/src/extensions/registry.js +134 -0
- package/src/gates/circuit-breaker.js +32 -0
- package/src/gates/g3-implementation.js +30 -4
- package/src/gates/g4-qa-implementation.js +34 -5
- package/src/gates/gate-base.js +9 -0
- package/src/health/auto-fix.js +216 -0
- package/src/installer/core.js +24 -11
- package/src/installer/provider-overlay.js +82 -0
- package/src/installer/templates.js +22 -10
- package/src/installer/transaction.js +3 -2
- package/src/installer/validator.js +74 -0
- package/src/intelligence/context-status.js +9 -5
- package/src/intelligence/document-sharder.js +221 -0
- package/src/intelligence/elicitation.js +265 -0
- package/src/intelligence/timeline.js +5 -0
- package/src/memory/gotchas.js +78 -2
- package/src/merger/semantic-merger.js +292 -0
- package/src/orchestrator/agent-selector.js +20 -0
- package/src/orchestrator/handoff-engine.js +77 -0
- package/src/orchestrator/index.js +0 -8
- package/src/orchestrator/intent-classifier.js +182 -0
- package/src/orchestrator/pipeline-manager.js +125 -1
- package/src/orchestrator/session-manager.js +164 -2
- package/src/quality/metrics-collector.js +283 -0
- package/src/quality/test-runner.js +368 -0
- package/src/telemetry/collector.js +83 -0
- package/src/telemetry/config.js +119 -0
- package/src/telemetry/index.js +11 -0
- package/src/telemetry/schema.js +104 -0
- package/src/telemetry/sender.js +60 -0
- package/src/terminal/cli-registry.js +7 -1
- package/src/terminal/cost-tracker.js +197 -0
- package/src/terminal/handoff-parser.js +61 -4
- package/src/terminal/prompt-builder.js +56 -18
- package/src/terminal/rate-limiter.js +172 -0
- package/src/terminal/run-agent.js +39 -0
- package/src/terminal/run-parallel.js +22 -1
- package/src/terminal/spawner.js +181 -3
- package/src/upgrade/migrator.js +2 -2
- package/src/utils/event-bus.js +126 -0
- package/src/utils/file-lock.js +291 -0
- package/src/utils/schema-validator.js +226 -0
- package/src/wizard/i18n.js +11 -0
- package/src/wizard/index.js +42 -20
- package/src/wizard/questions.js +200 -39
- package/src/autonomy/execution-profile.js +0 -151
- package/src/intelligence/file-tracker.js +0 -117
- package/src/memory/gotchas-auto-capture.js +0 -253
- package/src/orchestrator/pipeline-state.js +0 -223
- package/src/terminal/wave-analyzer.js +0 -143
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
3
4
|
import { hashFile } from './file-hasher.js';
|
|
4
5
|
import { loadManifest } from './manifest.js';
|
|
6
|
+
import { ADAPTABLE_FILES } from '../config/framework-adapter.js';
|
|
7
|
+
import { validateSchema, CONFIG_SCHEMA } from '../utils/schema-validator.js';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* Validate chati.dev installation
|
|
@@ -20,6 +23,7 @@ export async function validateInstallation(targetDir) {
|
|
|
20
23
|
memories: { pass: false, details: [] },
|
|
21
24
|
context: { pass: false, details: [] },
|
|
22
25
|
integrity: { pass: false, details: [] },
|
|
26
|
+
config: { pass: false, details: [] },
|
|
23
27
|
total: 0,
|
|
24
28
|
passed: 0,
|
|
25
29
|
};
|
|
@@ -171,6 +175,76 @@ export async function validateInstallation(targetDir) {
|
|
|
171
175
|
results.total += 1;
|
|
172
176
|
if (results.context.pass) results.passed += 1;
|
|
173
177
|
|
|
178
|
+
// Check provider overlays (if multiple providers configured)
|
|
179
|
+
const configYamlPath = join(targetDir, 'chati.dev', 'config.yaml');
|
|
180
|
+
if (existsSync(configYamlPath)) {
|
|
181
|
+
try {
|
|
182
|
+
const configContent = readFileSync(configYamlPath, 'utf-8');
|
|
183
|
+
const enabledProviders = [];
|
|
184
|
+
let primaryProvider = null;
|
|
185
|
+
let currentProvider = null;
|
|
186
|
+
|
|
187
|
+
for (const line of configContent.split('\n')) {
|
|
188
|
+
const provLine = line.match(/^\s{4}(\w+):$/);
|
|
189
|
+
if (provLine && ['claude', 'gemini', 'codex'].includes(provLine[1])) {
|
|
190
|
+
currentProvider = provLine[1];
|
|
191
|
+
}
|
|
192
|
+
if (currentProvider && line.includes('enabled: true')) {
|
|
193
|
+
enabledProviders.push(currentProvider);
|
|
194
|
+
}
|
|
195
|
+
if (currentProvider && line.includes('primary: true')) {
|
|
196
|
+
primaryProvider = currentProvider;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Validate overlay completeness for each secondary provider
|
|
201
|
+
if (enabledProviders.length > 1 && primaryProvider) {
|
|
202
|
+
const secondaryProviders = enabledProviders.filter(p => p !== primaryProvider);
|
|
203
|
+
const overlayDetails = [];
|
|
204
|
+
|
|
205
|
+
for (const provider of secondaryProviders) {
|
|
206
|
+
let present = 0;
|
|
207
|
+
let missing = 0;
|
|
208
|
+
const missingFiles = [];
|
|
209
|
+
|
|
210
|
+
for (const file of ADAPTABLE_FILES) {
|
|
211
|
+
const overlayPath = join(targetDir, 'chati.dev', '.adapted', provider, file);
|
|
212
|
+
if (existsSync(overlayPath)) {
|
|
213
|
+
present++;
|
|
214
|
+
} else {
|
|
215
|
+
missing++;
|
|
216
|
+
missingFiles.push(file);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
overlayDetails.push({ provider, present, missing, missingFiles });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
results.context.details.push({ overlays: overlayDetails });
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// Config parsing is best-effort for validation
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check config.yaml schema validity
|
|
231
|
+
if (existsSync(configYamlPath)) {
|
|
232
|
+
try {
|
|
233
|
+
const configData = yaml.load(readFileSync(configYamlPath, 'utf-8'));
|
|
234
|
+
const configValidation = validateSchema(configData, CONFIG_SCHEMA);
|
|
235
|
+
results.config.pass = configValidation.valid;
|
|
236
|
+
results.config.details.push({
|
|
237
|
+
errors: configValidation.errors,
|
|
238
|
+
warnings: configValidation.warnings,
|
|
239
|
+
});
|
|
240
|
+
} catch (err) {
|
|
241
|
+
results.config.pass = false;
|
|
242
|
+
results.config.details.push({ error: `YAML parse error: ${err.message}` });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
results.total += 1;
|
|
246
|
+
if (results.config.pass) results.passed += 1;
|
|
247
|
+
|
|
174
248
|
// Check integrity: verify installed files match manifest hashes
|
|
175
249
|
const manifest = loadManifest(targetDir);
|
|
176
250
|
if (manifest && manifest.files) {
|
|
@@ -2,11 +2,15 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
|
|
4
4
|
const BRACKETS = [
|
|
5
|
-
{ name: 'FRESH', min: 60, max: 100, layers: ['L0', 'L1', 'L2', 'L3', 'L4'], budget:
|
|
6
|
-
{ name: 'MODERATE', min: 40, max: 60, layers: ['L0', 'L1', 'L2', 'L3'
|
|
7
|
-
{ name: 'DEPLETED', min: 25, max: 40, layers: ['L0', 'L1', 'L2'], budget:
|
|
8
|
-
{ name: 'CRITICAL', min: 0, max: 25, layers: ['L0', 'L1'], budget:
|
|
5
|
+
{ name: 'FRESH', min: 60, max: 100, layers: ['L0', 'L1', 'L2', 'L3', 'L4'], budget: 8000 },
|
|
6
|
+
{ name: 'MODERATE', min: 40, max: 60, layers: ['L0', 'L1', 'L2', 'L3'], budget: 5000 },
|
|
7
|
+
{ name: 'DEPLETED', min: 25, max: 40, layers: ['L0', 'L1', 'L2'], budget: 3000 },
|
|
8
|
+
{ name: 'CRITICAL', min: 0, max: 25, layers: ['L0', 'L1'], budget: 1500 },
|
|
9
9
|
];
|
|
10
|
+
// NOTE: This BRACKETS array is intentionally duplicated from src/context/bracket-tracker.js
|
|
11
|
+
// for module isolation. bracket-tracker.js is the canonical source of truth.
|
|
12
|
+
// If bracket ranges or budgets change, update BOTH files.
|
|
13
|
+
// Format differs: here = array with `budget`, there = object with `tokenBudget`.
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* Get context status based on session state
|
|
@@ -40,7 +44,7 @@ export function getContextStatus(targetDir) {
|
|
|
40
44
|
|
|
41
45
|
const bracket = BRACKETS.find(b => remainingPercent >= b.min && remainingPercent <= b.max) || BRACKETS[0];
|
|
42
46
|
|
|
43
|
-
const memoryLevels = { FRESH: '
|
|
47
|
+
const memoryLevels = { FRESH: 'full', MODERATE: 'chunks', DEPLETED: 'metadata', CRITICAL: 'none' };
|
|
44
48
|
|
|
45
49
|
return {
|
|
46
50
|
bracket: bracket.name,
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Document sharding for large PRDs and specifications.
|
|
3
|
+
*
|
|
4
|
+
* Splits large documents into manageable shards while respecting
|
|
5
|
+
* heading boundaries and providing overlap for context continuity.
|
|
6
|
+
*
|
|
7
|
+
* Constitution Article XII — Context Bracket Management.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Constants
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/** Default maximum shard size in characters (~2K tokens). */
|
|
15
|
+
const DEFAULT_MAX_SHARD_SIZE = 8_000;
|
|
16
|
+
|
|
17
|
+
/** Overlap characters between shards for context continuity. */
|
|
18
|
+
const DEFAULT_OVERLAP = 800;
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Public API
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} Shard
|
|
26
|
+
* @property {number} index - Shard index (0-based)
|
|
27
|
+
* @property {string} content - Shard content
|
|
28
|
+
* @property {string[]} sections - Section headings in this shard
|
|
29
|
+
* @property {number} startOffset - Character offset in original document
|
|
30
|
+
* @property {number} endOffset - End character offset
|
|
31
|
+
* @property {boolean} hasOverlap - Whether this shard has overlap from previous
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Split a document into shards, respecting heading boundaries.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} content - Full document content
|
|
38
|
+
* @param {{ maxShardSize?: number, overlap?: number }} [options={}]
|
|
39
|
+
* @returns {Shard[]}
|
|
40
|
+
*/
|
|
41
|
+
export function shardDocument(content, options = {}) {
|
|
42
|
+
if (!content || typeof content !== 'string') {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const maxSize = options.maxShardSize || DEFAULT_MAX_SHARD_SIZE;
|
|
47
|
+
const overlap = options.overlap || DEFAULT_OVERLAP;
|
|
48
|
+
|
|
49
|
+
// If content fits in one shard, return as-is
|
|
50
|
+
if (content.length <= maxSize) {
|
|
51
|
+
return [{
|
|
52
|
+
index: 0,
|
|
53
|
+
content,
|
|
54
|
+
sections: extractSections(content),
|
|
55
|
+
startOffset: 0,
|
|
56
|
+
endOffset: content.length,
|
|
57
|
+
hasOverlap: false,
|
|
58
|
+
}];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Find heading boundaries (## or # markers)
|
|
62
|
+
const headingPattern = /^#{1,3}\s+.+$/gm;
|
|
63
|
+
const headings = [];
|
|
64
|
+
let match;
|
|
65
|
+
|
|
66
|
+
while ((match = headingPattern.exec(content)) !== null) {
|
|
67
|
+
headings.push({ offset: match.index, text: match[0].trim() });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Build shards at heading boundaries
|
|
71
|
+
const shards = [];
|
|
72
|
+
let currentStart = 0;
|
|
73
|
+
|
|
74
|
+
while (currentStart < content.length) {
|
|
75
|
+
let endPos = currentStart + maxSize;
|
|
76
|
+
|
|
77
|
+
if (endPos >= content.length) {
|
|
78
|
+
// Last shard
|
|
79
|
+
endPos = content.length;
|
|
80
|
+
} else {
|
|
81
|
+
// Find the best heading boundary to split at
|
|
82
|
+
const bestBreak = findBestBreak(headings, currentStart, endPos);
|
|
83
|
+
if (bestBreak > currentStart) {
|
|
84
|
+
endPos = bestBreak;
|
|
85
|
+
} else {
|
|
86
|
+
// No heading found — break at paragraph boundary
|
|
87
|
+
const paragraphBreak = content.lastIndexOf('\n\n', endPos);
|
|
88
|
+
if (paragraphBreak > currentStart) {
|
|
89
|
+
endPos = paragraphBreak;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const shardContent = content.slice(currentStart, endPos);
|
|
95
|
+
const hasOverlap = currentStart > 0;
|
|
96
|
+
|
|
97
|
+
shards.push({
|
|
98
|
+
index: shards.length,
|
|
99
|
+
content: shardContent,
|
|
100
|
+
sections: extractSections(shardContent),
|
|
101
|
+
startOffset: currentStart,
|
|
102
|
+
endOffset: endPos,
|
|
103
|
+
hasOverlap,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Advance with overlap
|
|
107
|
+
currentStart = Math.max(currentStart + 1, endPos - overlap);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return shards;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Reassemble shards back into a document, removing overlaps.
|
|
115
|
+
*
|
|
116
|
+
* @param {Shard[]} shards
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
export function reassembleShards(shards) {
|
|
120
|
+
if (!shards || shards.length === 0) return '';
|
|
121
|
+
if (shards.length === 1) return shards[0].content;
|
|
122
|
+
|
|
123
|
+
// Sort by index
|
|
124
|
+
const sorted = [...shards].sort((a, b) => a.index - b.index);
|
|
125
|
+
|
|
126
|
+
// Use non-overlapping portions
|
|
127
|
+
const parts = [];
|
|
128
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
129
|
+
const shard = sorted[i];
|
|
130
|
+
if (i === 0) {
|
|
131
|
+
// First shard: use everything up to where next shard's unique content starts
|
|
132
|
+
if (sorted.length > 1) {
|
|
133
|
+
const nextStart = sorted[1].startOffset;
|
|
134
|
+
const overlapStart = shard.endOffset - nextStart;
|
|
135
|
+
if (overlapStart > 0) {
|
|
136
|
+
parts.push(shard.content.slice(0, shard.content.length - overlapStart));
|
|
137
|
+
} else {
|
|
138
|
+
parts.push(shard.content);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
parts.push(shard.content);
|
|
142
|
+
}
|
|
143
|
+
} else if (i === sorted.length - 1) {
|
|
144
|
+
// Last shard: use from overlap end to end
|
|
145
|
+
const prevEnd = sorted[i - 1].endOffset;
|
|
146
|
+
const overlapLength = prevEnd - shard.startOffset;
|
|
147
|
+
if (overlapLength > 0) {
|
|
148
|
+
parts.push(shard.content.slice(overlapLength));
|
|
149
|
+
} else {
|
|
150
|
+
parts.push(shard.content);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
// Middle shard: use from overlap end to before next shard's overlap
|
|
154
|
+
const prevEnd = sorted[i - 1].endOffset;
|
|
155
|
+
const startOverlap = prevEnd - shard.startOffset;
|
|
156
|
+
const nextStart = sorted[i + 1].startOffset;
|
|
157
|
+
const endOverlap = shard.endOffset - nextStart;
|
|
158
|
+
|
|
159
|
+
const start = Math.max(0, startOverlap);
|
|
160
|
+
const end = endOverlap > 0 ? shard.content.length - endOverlap : shard.content.length;
|
|
161
|
+
parts.push(shard.content.slice(start, end));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return parts.join('');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Find the shard containing a specific section heading.
|
|
170
|
+
*
|
|
171
|
+
* @param {Shard[]} shards
|
|
172
|
+
* @param {string} sectionName - Heading text to find (partial match)
|
|
173
|
+
* @returns {Shard|null}
|
|
174
|
+
*/
|
|
175
|
+
export function getShardForSection(shards, sectionName) {
|
|
176
|
+
if (!shards || !sectionName) return null;
|
|
177
|
+
|
|
178
|
+
const lower = sectionName.toLowerCase();
|
|
179
|
+
return shards.find(shard =>
|
|
180
|
+
shard.sections.some(s => s.toLowerCase().includes(lower))
|
|
181
|
+
) || null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Helpers
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Find the best heading boundary to break at within a range.
|
|
190
|
+
*
|
|
191
|
+
* @param {{ offset: number, text: string }[]} headings
|
|
192
|
+
* @param {number} start
|
|
193
|
+
* @param {number} end
|
|
194
|
+
* @returns {number} Offset of best break point, or 0 if none found
|
|
195
|
+
*/
|
|
196
|
+
function findBestBreak(headings, start, end) {
|
|
197
|
+
// Find the last heading that starts before end and after start
|
|
198
|
+
let bestOffset = 0;
|
|
199
|
+
for (const heading of headings) {
|
|
200
|
+
if (heading.offset > start && heading.offset <= end) {
|
|
201
|
+
bestOffset = heading.offset;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return bestOffset;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Extract section headings from content.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} content
|
|
211
|
+
* @returns {string[]}
|
|
212
|
+
*/
|
|
213
|
+
function extractSections(content) {
|
|
214
|
+
const headings = content.match(/^#{1,3}\s+.+$/gm) || [];
|
|
215
|
+
return headings.map(h => h.replace(/^#+\s+/, '').trim());
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Exported constants for testing.
|
|
220
|
+
*/
|
|
221
|
+
export { DEFAULT_MAX_SHARD_SIZE, DEFAULT_OVERLAP };
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Advanced elicitation techniques for Brief agent.
|
|
3
|
+
*
|
|
4
|
+
* Provides 15 structured questioning techniques that adapt
|
|
5
|
+
* to project context for deeper requirement extraction.
|
|
6
|
+
*
|
|
7
|
+
* Constitution Protocol 8 — Interaction Model.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Techniques
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} Technique
|
|
16
|
+
* @property {string} id - Unique technique identifier
|
|
17
|
+
* @property {string} name - Human-readable name
|
|
18
|
+
* @property {string} description - When to use this technique
|
|
19
|
+
* @property {string} template - Prompt template with {placeholders}
|
|
20
|
+
* @property {string[]} bestFor - Context categories where this excels
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* All 15 elicitation techniques.
|
|
25
|
+
* @type {Technique[]}
|
|
26
|
+
*/
|
|
27
|
+
export const TECHNIQUES = [
|
|
28
|
+
{
|
|
29
|
+
id: 'open-ended',
|
|
30
|
+
name: 'Open-Ended Questions',
|
|
31
|
+
description: 'Discover broad goals and motivations',
|
|
32
|
+
template: 'What is the primary problem you want {project} to solve?',
|
|
33
|
+
bestFor: ['greenfield', 'discovery', 'early'],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'closed',
|
|
37
|
+
name: 'Closed Questions',
|
|
38
|
+
description: 'Confirm specific decisions quickly',
|
|
39
|
+
template: 'Should {feature} support {option_a} or {option_b}?',
|
|
40
|
+
bestFor: ['confirmation', 'late', 'refinement'],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'scaling',
|
|
44
|
+
name: 'Scaling Questions',
|
|
45
|
+
description: 'Quantify priorities and satisfaction levels',
|
|
46
|
+
template: 'On a scale of 1-10, how important is {aspect} for your project?',
|
|
47
|
+
bestFor: ['prioritization', 'nfr', 'tradeoffs'],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'five-whys',
|
|
51
|
+
name: 'Five Whys',
|
|
52
|
+
description: 'Drill to root cause of requirements',
|
|
53
|
+
template: 'You mentioned {requirement}. Why is that important? (And why is that? Repeat 5x)',
|
|
54
|
+
bestFor: ['root-cause', 'deep-analysis', 'unclear'],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'scenario',
|
|
58
|
+
name: 'Scenario-Based',
|
|
59
|
+
description: 'Explore behavior through concrete situations',
|
|
60
|
+
template: 'Imagine a user is trying to {action}. Walk me through what should happen step by step.',
|
|
61
|
+
bestFor: ['user-flow', 'ux', 'behavior'],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'constraint',
|
|
65
|
+
name: 'Constraint Analysis',
|
|
66
|
+
description: 'Identify limitations and boundaries',
|
|
67
|
+
template: 'What are the hard constraints for {project}? (budget, timeline, technology, team size)',
|
|
68
|
+
bestFor: ['planning', 'nfr', 'feasibility'],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'analogy',
|
|
72
|
+
name: 'Analogy Questions',
|
|
73
|
+
description: 'Reference existing products for clarity',
|
|
74
|
+
template: 'Is {project} more like {product_a} or {product_b}? What would you change?',
|
|
75
|
+
bestFor: ['greenfield', 'vision', 'discovery'],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'day-in-life',
|
|
79
|
+
name: 'Day in the Life',
|
|
80
|
+
description: 'Understand user workflows holistically',
|
|
81
|
+
template: 'Walk me through a typical day for a {persona} using {project}.',
|
|
82
|
+
bestFor: ['user-research', 'ux', 'personas'],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'persona',
|
|
86
|
+
name: 'Persona Questions',
|
|
87
|
+
description: 'Define user types and their needs',
|
|
88
|
+
template: 'Who are the main types of users for {project}? What does each one need most?',
|
|
89
|
+
bestFor: ['user-research', 'discovery', 'early'],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'exception',
|
|
93
|
+
name: 'Exception Questions',
|
|
94
|
+
description: 'Explore edge cases and error scenarios',
|
|
95
|
+
template: 'What should happen if {action} fails or produces unexpected results?',
|
|
96
|
+
bestFor: ['edge-cases', 'error-handling', 'robustness'],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'moscow',
|
|
100
|
+
name: 'MoSCoW Priority',
|
|
101
|
+
description: 'Classify features by criticality',
|
|
102
|
+
template: 'For the features you described, which are Must-Have, Should-Have, Could-Have, and Won\'t-Have?',
|
|
103
|
+
bestFor: ['prioritization', 'planning', 'scope'],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'prototype',
|
|
107
|
+
name: 'Prototype Questions',
|
|
108
|
+
description: 'Validate UI/UX assumptions early',
|
|
109
|
+
template: 'If I showed you a rough mockup of {feature}, what would you check first?',
|
|
110
|
+
bestFor: ['ux', 'visual', 'validation'],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'acceptance',
|
|
114
|
+
name: 'Acceptance Criteria',
|
|
115
|
+
description: 'Define clear "done" criteria',
|
|
116
|
+
template: 'How will you know {feature} is working correctly? What does success look like?',
|
|
117
|
+
bestFor: ['quality', 'testing', 'refinement'],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'edge-case',
|
|
121
|
+
name: 'Edge Case Exploration',
|
|
122
|
+
description: 'Probe boundaries systematically',
|
|
123
|
+
template: 'What happens when {input} is empty/null/very large/negative? How should {feature} behave?',
|
|
124
|
+
bestFor: ['edge-cases', 'robustness', 'quality'],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'stakeholder-map',
|
|
128
|
+
name: 'Stakeholder Mapping',
|
|
129
|
+
description: 'Identify all parties and their interests',
|
|
130
|
+
template: 'Besides end users, who else cares about {project}? (investors, ops team, regulators, partners)',
|
|
131
|
+
bestFor: ['stakeholders', 'discovery', 'enterprise'],
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Public API
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Select the top N most relevant techniques for a given context.
|
|
141
|
+
*
|
|
142
|
+
* @param {{ phase?: string, projectType?: string, keywords?: string[], userLevel?: string }} context
|
|
143
|
+
* @param {number} [topN=3]
|
|
144
|
+
* @returns {Technique[]}
|
|
145
|
+
*/
|
|
146
|
+
export function selectTechniques(context, topN = 3) {
|
|
147
|
+
const scored = TECHNIQUES.map(technique => ({
|
|
148
|
+
technique,
|
|
149
|
+
score: scoreTechnique(technique, context),
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
// Sort by score descending
|
|
153
|
+
scored.sort((a, b) => b.score - a.score);
|
|
154
|
+
|
|
155
|
+
return scored.slice(0, topN).map(s => s.technique);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build a formatted elicitation prompt for a specific technique.
|
|
160
|
+
*
|
|
161
|
+
* @param {Technique} technique
|
|
162
|
+
* @param {{ project?: string, feature?: string, action?: string, persona?: string }} projectContext
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
export function buildElicitationPrompt(technique, projectContext = {}) {
|
|
166
|
+
let prompt = technique.template;
|
|
167
|
+
|
|
168
|
+
// Replace placeholders with context values or generic defaults
|
|
169
|
+
const defaults = {
|
|
170
|
+
project: 'the project',
|
|
171
|
+
feature: 'this feature',
|
|
172
|
+
action: 'perform this action',
|
|
173
|
+
persona: 'a typical user',
|
|
174
|
+
requirement: 'this requirement',
|
|
175
|
+
aspect: 'this aspect',
|
|
176
|
+
input: 'the input',
|
|
177
|
+
option_a: 'option A',
|
|
178
|
+
option_b: 'option B',
|
|
179
|
+
product_a: 'Product A',
|
|
180
|
+
product_b: 'Product B',
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const merged = { ...defaults, ...projectContext };
|
|
184
|
+
|
|
185
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
186
|
+
prompt = prompt.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return prompt;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get a technique by ID.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} id
|
|
196
|
+
* @returns {Technique|null}
|
|
197
|
+
*/
|
|
198
|
+
export function getTechnique(id) {
|
|
199
|
+
return TECHNIQUES.find(t => t.id === id) || null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Scoring
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Score how relevant a technique is for a given context.
|
|
208
|
+
*
|
|
209
|
+
* @param {Technique} technique
|
|
210
|
+
* @param {{ phase?: string, projectType?: string, keywords?: string[], userLevel?: string }} context
|
|
211
|
+
* @returns {number}
|
|
212
|
+
*/
|
|
213
|
+
function scoreTechnique(technique, context) {
|
|
214
|
+
let score = 0;
|
|
215
|
+
|
|
216
|
+
// Phase matching
|
|
217
|
+
if (context.phase) {
|
|
218
|
+
const phaseMap = {
|
|
219
|
+
discover: ['discovery', 'early', 'greenfield', 'user-research', 'vision'],
|
|
220
|
+
plan: ['planning', 'prioritization', 'scope', 'tradeoffs', 'nfr', 'feasibility'],
|
|
221
|
+
build: ['refinement', 'quality', 'testing', 'edge-cases', 'robustness'],
|
|
222
|
+
deploy: ['confirmation', 'late', 'validation'],
|
|
223
|
+
};
|
|
224
|
+
const phaseKeywords = phaseMap[context.phase] || [];
|
|
225
|
+
for (const kw of phaseKeywords) {
|
|
226
|
+
if (technique.bestFor.includes(kw)) {
|
|
227
|
+
score += 3;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Project type matching
|
|
233
|
+
if (context.projectType === 'greenfield' && technique.bestFor.includes('greenfield')) {
|
|
234
|
+
score += 2;
|
|
235
|
+
}
|
|
236
|
+
if (context.projectType === 'brownfield' && technique.bestFor.includes('refinement')) {
|
|
237
|
+
score += 2;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Keyword matching
|
|
241
|
+
if (context.keywords) {
|
|
242
|
+
for (const kw of context.keywords) {
|
|
243
|
+
if (technique.bestFor.includes(kw.toLowerCase())) {
|
|
244
|
+
score += 2;
|
|
245
|
+
}
|
|
246
|
+
if (technique.description.toLowerCase().includes(kw.toLowerCase())) {
|
|
247
|
+
score += 1;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// User level adjustment
|
|
253
|
+
if (context.userLevel === 'beginner') {
|
|
254
|
+
if (['open-ended', 'scenario', 'analogy'].includes(technique.id)) {
|
|
255
|
+
score += 1; // Easier techniques for beginners
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (context.userLevel === 'expert') {
|
|
259
|
+
if (['five-whys', 'constraint', 'stakeholder-map'].includes(technique.id)) {
|
|
260
|
+
score += 1; // More advanced for experts
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return score;
|
|
265
|
+
}
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* results, handoffs, and deviations.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @deprecated Not currently imported by any production module.
|
|
11
|
+
* Retained for potential future integration. Remove if still unused by v4.0.
|
|
12
|
+
*/
|
|
13
|
+
|
|
9
14
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
10
15
|
import { join, dirname } from 'path';
|
|
11
16
|
|