Initial commit
This commit is contained in:
918
commands/_shared-decision-helpers.md
Normal file
918
commands/_shared-decision-helpers.md
Normal file
@@ -0,0 +1,918 @@
|
||||
# Shared Decision Helper Functions (Always-Ask Policy Implementation)
|
||||
|
||||
This file provides reusable decision-making utilities for implementing the Always-Ask Policy across all CCPM commands. These helpers enable consistent, confidence-based decision making with automatic user interaction when confidence is below thresholds.
|
||||
|
||||
## Overview
|
||||
|
||||
The Always-Ask Policy states: **When confidence < 80%, explicitly ask the user rather than making assumptions.**
|
||||
|
||||
These helper functions provide:
|
||||
- **Confidence calculation** - Score decisions 0-100 based on context
|
||||
- **Ask/proceed logic** - Automatically ask user when confidence is low
|
||||
- **User question formatting** - Standardized question templates
|
||||
- **Fuzzy matching** - Intelligent string matching with thresholds
|
||||
- **Validation** - Pre/during/post command validation
|
||||
|
||||
**Usage in commands:** Reference this file at the start of command execution:
|
||||
```markdown
|
||||
READ: commands/_shared-decision-helpers.md
|
||||
```
|
||||
|
||||
Then use the functions as described below.
|
||||
|
||||
---
|
||||
|
||||
## Core Functions
|
||||
|
||||
### 1. calculateConfidence
|
||||
|
||||
Calculates confidence score (0-100) for a decision based on multiple signals.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Calculate confidence score for a decision
|
||||
* @param {Object} context - Decision context
|
||||
* @param {any} context.input - User input to evaluate
|
||||
* @param {any} context.expected - Expected value/pattern
|
||||
* @param {Object} context.signals - Additional confidence signals
|
||||
* @param {number} context.signals.patternMatch - Pattern match confidence (0-100)
|
||||
* @param {number} context.signals.contextMatch - Context match confidence (0-100)
|
||||
* @param {number} context.signals.historicalSuccess - Historical success rate (0-100)
|
||||
* @param {number} context.signals.userPreference - User preference strength (0-100)
|
||||
* @returns {Object} Confidence result with score and reasoning
|
||||
*/
|
||||
function calculateConfidence(context) {
|
||||
let confidence = 0;
|
||||
const reasoning = [];
|
||||
|
||||
// Signal 1: Pattern Match (weight: 50%)
|
||||
if (context.signals?.patternMatch !== undefined) {
|
||||
const patternScore = context.signals.patternMatch * 0.5;
|
||||
confidence += patternScore;
|
||||
reasoning.push(`Pattern match: ${context.signals.patternMatch}% (weighted: ${patternScore.toFixed(0)})`);
|
||||
}
|
||||
|
||||
// Signal 2: Context Match (weight: 30%)
|
||||
if (context.signals?.contextMatch !== undefined) {
|
||||
const contextScore = context.signals.contextMatch * 0.3;
|
||||
confidence += contextScore;
|
||||
reasoning.push(`Context match: ${context.signals.contextMatch}% (weighted: ${contextScore.toFixed(0)})`);
|
||||
}
|
||||
|
||||
// Signal 3: Historical Success (weight: 20%)
|
||||
if (context.signals?.historicalSuccess !== undefined) {
|
||||
const historyScore = context.signals.historicalSuccess * 0.2;
|
||||
confidence += historyScore;
|
||||
reasoning.push(`Historical success: ${context.signals.historicalSuccess}% (weighted: ${historyScore.toFixed(0)})`);
|
||||
}
|
||||
|
||||
// Signal 4: User Preference (bonus: +10)
|
||||
if (context.signals?.userPreference !== undefined && context.signals.userPreference > 70) {
|
||||
const preferenceBonus = 10;
|
||||
confidence += preferenceBonus;
|
||||
reasoning.push(`User preference bonus: +${preferenceBonus}`);
|
||||
}
|
||||
|
||||
// Cap at 100
|
||||
confidence = Math.min(confidence, 100);
|
||||
|
||||
return {
|
||||
confidence: Math.round(confidence),
|
||||
reasoning: reasoning.join(', '),
|
||||
shouldAsk: confidence < 80,
|
||||
level: getConfidenceLevel(confidence)
|
||||
};
|
||||
}
|
||||
|
||||
function getConfidenceLevel(confidence) {
|
||||
if (confidence >= 95) return 'CERTAIN';
|
||||
if (confidence >= 80) return 'HIGH';
|
||||
if (confidence >= 50) return 'MEDIUM';
|
||||
return 'LOW';
|
||||
}
|
||||
```
|
||||
|
||||
**Usage Example:**
|
||||
```javascript
|
||||
// Calculate confidence for issue ID pattern
|
||||
const result = calculateConfidence({
|
||||
input: "PSN-29",
|
||||
expected: /^[A-Z]+-\d+$/,
|
||||
signals: {
|
||||
patternMatch: 100, // Exact regex match
|
||||
contextMatch: 90, // Branch name contains PSN-29
|
||||
historicalSuccess: 95 // 95% past success rate
|
||||
}
|
||||
});
|
||||
|
||||
// Result:
|
||||
// {
|
||||
// confidence: 93,
|
||||
// reasoning: "Pattern match: 100% (weighted: 50), Context match: 90% (weighted: 27), Historical success: 95% (weighted: 19)",
|
||||
// shouldAsk: false,
|
||||
// level: "HIGH"
|
||||
// }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. shouldAsk
|
||||
|
||||
Determines if user should be asked based on confidence level and policy.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Determine if user should be asked for confirmation
|
||||
* @param {number} confidence - Confidence score (0-100)
|
||||
* @param {Object} options - Additional options
|
||||
* @param {boolean} options.alwaysAsk - Force asking regardless of confidence
|
||||
* @param {boolean} options.neverAsk - Never ask (for certain operations)
|
||||
* @param {number} options.threshold - Custom confidence threshold (default: 80)
|
||||
* @returns {Object} Decision result
|
||||
*/
|
||||
function shouldAsk(confidence, options = {}) {
|
||||
// Override: Always ask (for safety-critical operations)
|
||||
if (options.alwaysAsk) {
|
||||
return {
|
||||
shouldAsk: true,
|
||||
reason: 'Safety-critical operation requires confirmation',
|
||||
displayMode: 'CONFIRM_REQUIRED'
|
||||
};
|
||||
}
|
||||
|
||||
// Override: Never ask (for validation operations)
|
||||
if (options.neverAsk) {
|
||||
return {
|
||||
shouldAsk: false,
|
||||
reason: 'Validation operation, no user input needed',
|
||||
displayMode: 'AUTO_PROCEED'
|
||||
};
|
||||
}
|
||||
|
||||
// Default threshold: 80%
|
||||
const threshold = options.threshold || 80;
|
||||
|
||||
if (confidence >= 95) {
|
||||
return {
|
||||
shouldAsk: false,
|
||||
reason: 'Certain - proceeding automatically',
|
||||
displayMode: 'AUTO_PROCEED_SILENT'
|
||||
};
|
||||
} else if (confidence >= threshold) {
|
||||
return {
|
||||
shouldAsk: false,
|
||||
reason: `High confidence (${confidence}%) - proceeding with display`,
|
||||
displayMode: 'AUTO_PROCEED_WITH_DISPLAY'
|
||||
};
|
||||
} else if (confidence >= 50) {
|
||||
return {
|
||||
shouldAsk: true,
|
||||
reason: `Medium confidence (${confidence}%) - suggesting with confirmation`,
|
||||
displayMode: 'SUGGEST_AND_CONFIRM'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
shouldAsk: true,
|
||||
reason: `Low confidence (${confidence}%) - asking user`,
|
||||
displayMode: 'ASK_WITHOUT_SUGGESTION'
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage Example:**
|
||||
```javascript
|
||||
// Check if we should ask
|
||||
const decision = shouldAsk(65);
|
||||
// Result: { shouldAsk: true, reason: "Medium confidence (65%) - suggesting with confirmation", displayMode: "SUGGEST_AND_CONFIRM" }
|
||||
|
||||
// Force asking for external writes
|
||||
const externalWrite = shouldAsk(95, { alwaysAsk: true });
|
||||
// Result: { shouldAsk: true, reason: "Safety-critical operation requires confirmation", displayMode: "CONFIRM_REQUIRED" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. askUserForClarification
|
||||
|
||||
Wrapper around AskUserQuestion tool with standardized formatting.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Ask user for clarification using AskUserQuestion tool
|
||||
* @param {Object} questionConfig - Question configuration
|
||||
* @param {string} questionConfig.question - The question to ask
|
||||
* @param {string} questionConfig.header - Short header (max 12 chars)
|
||||
* @param {Array} questionConfig.options - Array of options
|
||||
* @param {any} questionConfig.options[].label - Option label
|
||||
* @param {string} questionConfig.options[].description - Option description
|
||||
* @param {any} questionConfig.options[].value - Option value (optional, defaults to label)
|
||||
* @param {boolean} questionConfig.multiSelect - Allow multiple selections (default: false)
|
||||
* @param {any} questionConfig.suggestion - Suggested value (for pre-selection)
|
||||
* @param {number} questionConfig.confidence - Confidence in suggestion (for display)
|
||||
* @returns {Promise<any>} User's selected value(s)
|
||||
*/
|
||||
async function askUserForClarification(questionConfig) {
|
||||
const {
|
||||
question,
|
||||
header,
|
||||
options,
|
||||
multiSelect = false,
|
||||
suggestion = null,
|
||||
confidence = null
|
||||
} = questionConfig;
|
||||
|
||||
// Format options with suggestion highlighting
|
||||
const formattedOptions = options.map(opt => {
|
||||
const isSuggestion = suggestion && (opt.value === suggestion || opt.label === suggestion);
|
||||
|
||||
return {
|
||||
label: isSuggestion ? `${opt.label} ⭐ (suggested)` : opt.label,
|
||||
description: isSuggestion && confidence
|
||||
? `${opt.description} (${confidence}% confidence)`
|
||||
: opt.description
|
||||
};
|
||||
});
|
||||
|
||||
// Display question context if medium/low confidence
|
||||
if (confidence !== null && confidence < 80) {
|
||||
console.log(`\n💡 AI Confidence: ${confidence}%`);
|
||||
if (suggestion) {
|
||||
console.log(` Suggested: ${suggestion}`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Ask user
|
||||
const answer = await AskUserQuestion({
|
||||
questions: [{
|
||||
question,
|
||||
header,
|
||||
multiSelect,
|
||||
options: formattedOptions
|
||||
}]
|
||||
});
|
||||
|
||||
// Extract answer (remove suggestion marker if present)
|
||||
const rawAnswer = answer[header];
|
||||
const cleanedAnswer = Array.isArray(rawAnswer)
|
||||
? rawAnswer.map(a => a.replace(' ⭐ (suggested)', ''))
|
||||
: rawAnswer.replace(' ⭐ (suggested)', '');
|
||||
|
||||
return cleanedAnswer;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage Example:**
|
||||
```javascript
|
||||
// Ask with suggestion (medium confidence)
|
||||
const mode = await askUserForClarification({
|
||||
question: "What would you like to do?",
|
||||
header: "Mode",
|
||||
options: [
|
||||
{ label: "Create new", description: "Start from scratch" },
|
||||
{ label: "Plan existing", description: "Add plan to existing issue" },
|
||||
{ label: "Update plan", description: "Modify existing plan" }
|
||||
],
|
||||
suggestion: "Plan existing",
|
||||
confidence: 65
|
||||
});
|
||||
|
||||
// Ask without suggestion (low confidence)
|
||||
const commitType = await askUserForClarification({
|
||||
question: "Select commit type:",
|
||||
header: "Type",
|
||||
options: [
|
||||
{ label: "feat", description: "New feature" },
|
||||
{ label: "fix", description: "Bug fix" },
|
||||
{ label: "docs", description: "Documentation" }
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. displayOptionsAndConfirm
|
||||
|
||||
Display what will happen and ask for confirmation.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Display proposed action and ask for confirmation
|
||||
* @param {string} action - Action description
|
||||
* @param {Object} details - Action details to display
|
||||
* @param {Object} options - Display options
|
||||
* @param {string} options.title - Section title (default: "Proposed Action")
|
||||
* @param {string} options.emoji - Title emoji (default: "📋")
|
||||
* @param {boolean} options.requireExplicitYes - Require "yes" instead of any confirmation (default: false)
|
||||
* @returns {Promise<boolean>} True if confirmed, false otherwise
|
||||
*/
|
||||
async function displayOptionsAndConfirm(action, details, options = {}) {
|
||||
const {
|
||||
title = 'Proposed Action',
|
||||
emoji = '📋',
|
||||
requireExplicitYes = false
|
||||
} = options;
|
||||
|
||||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log(`${emoji} ${title}`);
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
console.log(action);
|
||||
console.log('');
|
||||
|
||||
// Display details
|
||||
Object.entries(details).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
console.log(`${key}:`);
|
||||
value.forEach(item => console.log(` • ${item}`));
|
||||
} else {
|
||||
console.log(`${key}: ${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
// Ask for confirmation
|
||||
const answer = await askUserForClarification({
|
||||
question: "Proceed with this action?",
|
||||
header: "Confirm",
|
||||
options: [
|
||||
{ label: "Yes, proceed", description: "Execute the action shown above" },
|
||||
{ label: "No, cancel", description: "Cancel and return" }
|
||||
]
|
||||
});
|
||||
|
||||
const confirmed = answer.toLowerCase().includes('yes');
|
||||
|
||||
if (!confirmed) {
|
||||
console.log('❌ Cancelled\n');
|
||||
}
|
||||
|
||||
return confirmed;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage Example:**
|
||||
```javascript
|
||||
// Display and confirm external write
|
||||
const confirmed = await displayOptionsAndConfirm(
|
||||
"Update Jira ticket TRAIN-456",
|
||||
{
|
||||
"Status": "In Progress → Done",
|
||||
"Comment": "Completed via /ccpm:done",
|
||||
"Labels": ["ccpm", "completed"]
|
||||
},
|
||||
{
|
||||
title: "External System Write",
|
||||
emoji: "🚨",
|
||||
requireExplicitYes: true
|
||||
}
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
// Proceed with write
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. fuzzyMatch
|
||||
|
||||
Intelligent fuzzy matching with confidence scoring.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Fuzzy match input against options with confidence scoring
|
||||
* @param {string} input - User input
|
||||
* @param {Array} options - Array of valid options (strings or objects with 'name' property)
|
||||
* @param {Object} config - Matching configuration
|
||||
* @param {number} config.threshold - Minimum similarity threshold (0-100, default: 60)
|
||||
* @param {boolean} config.caseSensitive - Case sensitive matching (default: false)
|
||||
* @param {Array} config.aliases - Map of aliases to canonical values
|
||||
* @returns {Object} Match result
|
||||
*/
|
||||
function fuzzyMatch(input, options, config = {}) {
|
||||
const {
|
||||
threshold = 60,
|
||||
caseSensitive = false,
|
||||
aliases = {}
|
||||
} = config;
|
||||
|
||||
// Normalize input
|
||||
const normalizedInput = caseSensitive ? input : input.toLowerCase().trim();
|
||||
|
||||
// Check aliases first
|
||||
if (aliases[normalizedInput]) {
|
||||
return {
|
||||
match: aliases[normalizedInput],
|
||||
confidence: 100,
|
||||
exactMatch: true,
|
||||
reason: 'Alias match'
|
||||
};
|
||||
}
|
||||
|
||||
// Extract option strings
|
||||
const optionStrings = options.map(opt =>
|
||||
typeof opt === 'string' ? opt : opt.name || opt.label || String(opt)
|
||||
);
|
||||
|
||||
// Normalize options
|
||||
const normalizedOptions = optionStrings.map(opt =>
|
||||
caseSensitive ? opt : opt.toLowerCase().trim()
|
||||
);
|
||||
|
||||
// Strategy 1: Exact match
|
||||
const exactIndex = normalizedOptions.findIndex(opt => opt === normalizedInput);
|
||||
if (exactIndex !== -1) {
|
||||
return {
|
||||
match: optionStrings[exactIndex],
|
||||
confidence: 100,
|
||||
exactMatch: true,
|
||||
reason: 'Exact match'
|
||||
};
|
||||
}
|
||||
|
||||
// Strategy 2: Starts with
|
||||
const startsWithIndex = normalizedOptions.findIndex(opt => opt.startsWith(normalizedInput));
|
||||
if (startsWithIndex !== -1) {
|
||||
return {
|
||||
match: optionStrings[startsWithIndex],
|
||||
confidence: 85,
|
||||
exactMatch: false,
|
||||
reason: 'Starts with match'
|
||||
};
|
||||
}
|
||||
|
||||
// Strategy 3: Contains
|
||||
const containsIndex = normalizedOptions.findIndex(opt => opt.includes(normalizedInput));
|
||||
if (containsIndex !== -1) {
|
||||
return {
|
||||
match: optionStrings[containsIndex],
|
||||
confidence: 70,
|
||||
exactMatch: false,
|
||||
reason: 'Contains match'
|
||||
};
|
||||
}
|
||||
|
||||
// Strategy 4: Levenshtein distance (simplified)
|
||||
const distances = normalizedOptions.map(opt => ({
|
||||
option: opt,
|
||||
distance: levenshteinDistance(normalizedInput, opt)
|
||||
}));
|
||||
|
||||
distances.sort((a, b) => a.distance - b.distance);
|
||||
const closest = distances[0];
|
||||
|
||||
// Calculate similarity percentage
|
||||
const maxLen = Math.max(normalizedInput.length, closest.option.length);
|
||||
const similarity = ((maxLen - closest.distance) / maxLen) * 100;
|
||||
|
||||
if (similarity >= threshold) {
|
||||
const matchIndex = normalizedOptions.indexOf(closest.option);
|
||||
return {
|
||||
match: optionStrings[matchIndex],
|
||||
confidence: Math.round(similarity),
|
||||
exactMatch: false,
|
||||
reason: `Fuzzy match (${closest.distance} edits)`
|
||||
};
|
||||
}
|
||||
|
||||
// No match found
|
||||
return {
|
||||
match: null,
|
||||
confidence: 0,
|
||||
exactMatch: false,
|
||||
reason: 'No match found',
|
||||
suggestions: optionStrings.slice(0, 3)
|
||||
};
|
||||
}
|
||||
|
||||
// Simplified Levenshtein distance
|
||||
function levenshteinDistance(str1, str2) {
|
||||
const len1 = str1.length;
|
||||
const len2 = str2.length;
|
||||
const matrix = Array(len1 + 1).fill(null).map(() => Array(len2 + 1).fill(0));
|
||||
|
||||
for (let i = 0; i <= len1; i++) matrix[i][0] = i;
|
||||
for (let j = 0; j <= len2; j++) matrix[0][j] = j;
|
||||
|
||||
for (let i = 1; i <= len1; i++) {
|
||||
for (let j = 1; j <= len2; j++) {
|
||||
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
||||
matrix[i][j] = Math.min(
|
||||
matrix[i - 1][j] + 1, // deletion
|
||||
matrix[i][j - 1] + 1, // insertion
|
||||
matrix[i - 1][j - 1] + cost // substitution
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[len1][len2];
|
||||
}
|
||||
```
|
||||
|
||||
**Usage Example:**
|
||||
```javascript
|
||||
// Fuzzy match state name
|
||||
const result = fuzzyMatch("in prog", [
|
||||
"Backlog",
|
||||
"In Progress",
|
||||
"In Review",
|
||||
"Done"
|
||||
]);
|
||||
// Result: { match: "In Progress", confidence: 70, exactMatch: false, reason: "Contains match" }
|
||||
|
||||
// Fuzzy match with aliases
|
||||
const typeResult = fuzzyMatch("bugfix", ["feat", "fix", "docs"], {
|
||||
aliases: {
|
||||
"bugfix": "fix",
|
||||
"feature": "feat",
|
||||
"documentation": "docs"
|
||||
}
|
||||
});
|
||||
// Result: { match: "fix", confidence: 100, exactMatch: true, reason: "Alias match" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation Functions
|
||||
|
||||
### 6. validateTransition
|
||||
|
||||
Validate workflow state transitions.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Validate if state transition is allowed
|
||||
* @param {string} fromState - Current state
|
||||
* @param {string} toState - Target state
|
||||
* @param {Object} stateMachine - State machine definition
|
||||
* @returns {Object} Validation result
|
||||
*/
|
||||
function validateTransition(fromState, toState, stateMachine) {
|
||||
const currentStateConfig = stateMachine[fromState];
|
||||
|
||||
if (!currentStateConfig) {
|
||||
return {
|
||||
valid: false,
|
||||
confidence: 0,
|
||||
error: `Unknown state: ${fromState}`,
|
||||
suggestions: Object.keys(stateMachine)
|
||||
};
|
||||
}
|
||||
|
||||
const allowedNextStates = currentStateConfig.next_states || [];
|
||||
|
||||
if (!allowedNextStates.includes(toState)) {
|
||||
return {
|
||||
valid: false,
|
||||
confidence: 0,
|
||||
error: `Cannot transition from ${fromState} to ${toState}`,
|
||||
allowedStates: allowedNextStates,
|
||||
suggestions: allowedNextStates.map(s => `Try: ${s}`)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
confidence: currentStateConfig.confidence_to_transition || 90
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern Matching Functions
|
||||
|
||||
### 7. Common Patterns
|
||||
|
||||
```javascript
|
||||
// Issue ID pattern (PROJECT-NUMBER)
|
||||
const ISSUE_ID_PATTERN = /^[A-Z]+-\d+$/;
|
||||
|
||||
function isIssueId(input) {
|
||||
return ISSUE_ID_PATTERN.test(input);
|
||||
}
|
||||
|
||||
function detectIssueIdConfidence(input) {
|
||||
if (ISSUE_ID_PATTERN.test(input)) {
|
||||
return { confidence: 95, match: true };
|
||||
}
|
||||
|
||||
// Partial match (e.g., "PSN" or "123")
|
||||
if (/^[A-Z]+$/.test(input) || /^\d+$/.test(input)) {
|
||||
return { confidence: 30, match: false, suggestion: 'Provide full issue ID (e.g., PSN-29)' };
|
||||
}
|
||||
|
||||
return { confidence: 0, match: false };
|
||||
}
|
||||
|
||||
// Quoted string pattern (for titles)
|
||||
const QUOTED_STRING_PATTERN = /^["'].*["']$/;
|
||||
|
||||
function isQuotedString(input) {
|
||||
return QUOTED_STRING_PATTERN.test(input);
|
||||
}
|
||||
|
||||
// Detect change type from update text
|
||||
function detectChangeType(text) {
|
||||
const lower = text.toLowerCase();
|
||||
|
||||
const patterns = {
|
||||
scope_change: /(add|also|include|plus|additionally|extra)/i,
|
||||
approach_change: /(instead|different|change|use.*not|replace.*with)/i,
|
||||
simplification: /(remove|don't need|skip|simpler|drop|omit)/i,
|
||||
blocker: /(blocked|can't|cannot|doesn't work|issue|problem|error)/i,
|
||||
clarification: /.*/ // Default
|
||||
};
|
||||
|
||||
for (const [type, pattern] of Object.entries(patterns)) {
|
||||
if (pattern.test(lower) && type !== 'clarification') {
|
||||
return {
|
||||
type,
|
||||
confidence: 75,
|
||||
keywords: lower.match(pattern)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'clarification',
|
||||
confidence: 50,
|
||||
keywords: []
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Display Helpers
|
||||
|
||||
### 8. Confidence Display
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Display confidence level with appropriate emoji and color
|
||||
* @param {number} confidence - Confidence score (0-100)
|
||||
* @param {string} message - Message to display
|
||||
*/
|
||||
function displayWithConfidence(confidence, message) {
|
||||
let emoji, prefix;
|
||||
|
||||
if (confidence >= 95) {
|
||||
emoji = '✅';
|
||||
prefix = 'CERTAIN';
|
||||
} else if (confidence >= 80) {
|
||||
emoji = '✅';
|
||||
prefix = `HIGH (${confidence}%)`;
|
||||
} else if (confidence >= 50) {
|
||||
emoji = '⚠️';
|
||||
prefix = `MEDIUM (${confidence}%)`;
|
||||
} else {
|
||||
emoji = '❓';
|
||||
prefix = `LOW (${confidence}%)`;
|
||||
}
|
||||
|
||||
console.log(`${emoji} ${prefix}: ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display decision summary
|
||||
* @param {Object} decision - Decision result
|
||||
*/
|
||||
function displayDecision(decision) {
|
||||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('🎯 Decision Summary');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
displayWithConfidence(decision.confidence, decision.suggestion || decision.action);
|
||||
|
||||
if (decision.reasoning) {
|
||||
console.log(`\n📊 Reasoning: ${decision.reasoning}`);
|
||||
}
|
||||
|
||||
if (decision.shouldAsk) {
|
||||
console.log(`\n👤 User input required`);
|
||||
} else {
|
||||
console.log(`\n🤖 Proceeding automatically`);
|
||||
}
|
||||
|
||||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### 9. Structured Error Messages
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Create structured error with suggestions
|
||||
* @param {string} message - Error message
|
||||
* @param {Object} details - Error details
|
||||
* @param {Array} details.suggestions - Actionable suggestions
|
||||
* @param {Array} details.availableOptions - Available options
|
||||
* @returns {Error} Enhanced error object
|
||||
*/
|
||||
function createStructuredError(message, details = {}) {
|
||||
const error = new Error(message);
|
||||
error.details = details;
|
||||
|
||||
// Format error message with suggestions
|
||||
let fullMessage = message;
|
||||
|
||||
if (details.availableOptions && details.availableOptions.length > 0) {
|
||||
fullMessage += '\n\nAvailable options:';
|
||||
details.availableOptions.forEach(opt => {
|
||||
fullMessage += `\n • ${opt}`;
|
||||
});
|
||||
}
|
||||
|
||||
if (details.suggestions && details.suggestions.length > 0) {
|
||||
fullMessage += '\n\nSuggestions:';
|
||||
details.suggestions.forEach(suggestion => {
|
||||
fullMessage += `\n • ${suggestion}`;
|
||||
});
|
||||
}
|
||||
|
||||
error.message = fullMessage;
|
||||
return error;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Example
|
||||
|
||||
Complete example showing all helpers working together:
|
||||
|
||||
```javascript
|
||||
// Example: Command routing with Always-Ask Policy
|
||||
async function executeSmartPlan(args) {
|
||||
const arg1 = args[0];
|
||||
const arg2 = args[1];
|
||||
|
||||
// Step 1: Detect mode with confidence
|
||||
const issueIdCheck = detectIssueIdConfidence(arg1);
|
||||
|
||||
let modeDecision;
|
||||
if (issueIdCheck.match) {
|
||||
// High confidence: Issue ID pattern
|
||||
if (arg2) {
|
||||
modeDecision = {
|
||||
mode: 'UPDATE',
|
||||
confidence: 95,
|
||||
reasoning: 'Issue ID with update text provided'
|
||||
};
|
||||
} else {
|
||||
modeDecision = {
|
||||
mode: 'PLAN',
|
||||
confidence: 95,
|
||||
reasoning: 'Issue ID without update text'
|
||||
};
|
||||
}
|
||||
} else if (isQuotedString(arg1)) {
|
||||
// High confidence: Quoted title
|
||||
modeDecision = {
|
||||
mode: 'CREATE',
|
||||
confidence: 90,
|
||||
reasoning: 'Quoted string indicates new task title'
|
||||
};
|
||||
} else {
|
||||
// Low confidence: Ambiguous
|
||||
modeDecision = {
|
||||
mode: null,
|
||||
confidence: 30,
|
||||
reasoning: 'Input format is ambiguous'
|
||||
};
|
||||
}
|
||||
|
||||
// Step 2: Check if we should ask
|
||||
const askDecision = shouldAsk(modeDecision.confidence);
|
||||
|
||||
// Step 3: Ask user if needed
|
||||
if (askDecision.shouldAsk) {
|
||||
displayDecision({
|
||||
...modeDecision,
|
||||
shouldAsk: true,
|
||||
action: `Detected input: "${arg1}"`
|
||||
});
|
||||
|
||||
const mode = await askUserForClarification({
|
||||
question: "What would you like to do?",
|
||||
header: "Mode",
|
||||
options: [
|
||||
{ label: "Create new", description: "Create new task with this title" },
|
||||
{ label: "Plan existing", description: "Plan existing issue" },
|
||||
{ label: "Update plan", description: "Update existing plan" }
|
||||
],
|
||||
suggestion: modeDecision.mode,
|
||||
confidence: modeDecision.confidence
|
||||
});
|
||||
|
||||
modeDecision.mode = mode.replace(' new', '').replace(' existing', '').replace(' plan', '');
|
||||
} else {
|
||||
// High confidence: Display and proceed
|
||||
displayWithConfidence(modeDecision.confidence, `Mode: ${modeDecision.mode}`);
|
||||
}
|
||||
|
||||
// Step 4: Execute mode
|
||||
switch (modeDecision.mode.toUpperCase()) {
|
||||
case 'CREATE':
|
||||
await executeCreate(arg1, arg2);
|
||||
break;
|
||||
case 'PLAN':
|
||||
await executePlan(arg1);
|
||||
break;
|
||||
case 'UPDATE':
|
||||
await executeUpdate(arg1, arg2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always calculate confidence** - Don't guess, use signals
|
||||
2. **Display confidence scores** - Be transparent with users
|
||||
3. **Provide reasoning** - Explain why confidence is at a level
|
||||
4. **Ask with suggestions** - Pre-select high confidence options
|
||||
5. **Validate early** - Catch errors before execution
|
||||
6. **Use fuzzy matching** - Be forgiving with user input
|
||||
7. **Log decisions** - Track accuracy over time
|
||||
8. **Fail gracefully** - Provide actionable error messages
|
||||
|
||||
---
|
||||
|
||||
## Testing Helpers
|
||||
|
||||
```javascript
|
||||
// Test confidence calculation
|
||||
const testConfidence = calculateConfidence({
|
||||
input: "PSN-29",
|
||||
signals: {
|
||||
patternMatch: 100,
|
||||
contextMatch: 90,
|
||||
historicalSuccess: 95
|
||||
}
|
||||
});
|
||||
console.log('Confidence test:', testConfidence);
|
||||
|
||||
// Test fuzzy matching
|
||||
const testMatch = fuzzyMatch("in prog", ["Backlog", "In Progress", "Done"]);
|
||||
console.log('Fuzzy match test:', testMatch);
|
||||
|
||||
// Test should ask
|
||||
const testAsk = shouldAsk(65);
|
||||
console.log('Should ask test:', testAsk);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
| Operation | Avg Time | Notes |
|
||||
|-----------|----------|-------|
|
||||
| calculateConfidence | <1ms | Synchronous calculation |
|
||||
| shouldAsk | <1ms | Simple threshold check |
|
||||
| askUserForClarification | Varies | Waits for user input |
|
||||
| fuzzyMatch | 1-5ms | Depends on options count |
|
||||
| displayOptionsAndConfirm | Varies | Waits for user input |
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide
|
||||
|
||||
**Old Pattern (no confidence tracking)**:
|
||||
```javascript
|
||||
if (ISSUE_ID_PATTERN.test(arg1)) {
|
||||
mode = 'PLAN';
|
||||
} else {
|
||||
mode = 'CREATE';
|
||||
}
|
||||
```
|
||||
|
||||
**New Pattern (with Always-Ask Policy)**:
|
||||
```javascript
|
||||
const issueIdCheck = detectIssueIdConfidence(arg1);
|
||||
const decision = shouldAsk(issueIdCheck.confidence);
|
||||
|
||||
if (decision.shouldAsk) {
|
||||
mode = await askUserForClarification({ ... });
|
||||
} else {
|
||||
displayWithConfidence(issueIdCheck.confidence, `Mode: ${mode}`);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [Decision Framework](../docs/architecture/decision-framework.md)
|
||||
- [Decision Trees](../docs/architecture/decision-trees-visual.md)
|
||||
- [Implementation Guide](../docs/guides/implementing-always-ask-policy.md)
|
||||
- [Workflow State Tracking](./_shared-workflow-state.md)
|
||||
Reference in New Issue
Block a user