Skip to content
← Public packages

@kentcdodds/kody-issue-triage

Loop-safe triage for Kody run errors and fleet package-runtime error-rate elevations. Wakes Cole (Grok Bot) to decide; Cursor agents only when Cole escalates.

src/decision-fields.ts

196 lines · 6.5 KB · TypeScript
export type DecisionOption = {
	label?: string
	text?: string
	difficulty?: string
	change?: string
}

export type DecisionOptionInput = string | DecisionOption

export type DecisionFields = {
	title?: string | null
	context?: string | null
	recommendation?: string | null
	/** Correct long-term fix. Shown even when it matches recommendation. */
	rightFix?: string | null
	/** Required when rightFix differs from recommendation. */
	whyNotRightFix?: string | null
	options?: DecisionOptionInput[] | string | null
	summary?: string | null
}

function normalizeDifficulty(value: unknown): 'Easy' | 'Medium' | 'Hard' | null {
	if (typeof value !== 'string') return null
	const normalized =
		value.trim().charAt(0).toUpperCase() + value.trim().slice(1).toLowerCase()
	if (normalized === 'Easy' || normalized === 'Medium' || normalized === 'Hard') {
		return normalized
	}
	return null
}

function normalizeChange(value: unknown): 'low' | 'medium' | 'radical' | null {
	if (typeof value !== 'string') return null
	const key = value.trim().toLowerCase().replace(/[\s_-]+/g, '')
	if (key === 'low' || key === 'lowchange') return 'low'
	if (key === 'medium' || key === 'mediumchange') return 'medium'
	if (key === 'radical' || key === 'high' || key === 'highchange') return 'radical'
	return null
}

function formatImpactTag(difficulty?: unknown, change?: unknown): string | null {
	const d = normalizeDifficulty(difficulty)
	const c = normalizeChange(change)
	if (!d && !c) return null
	const dRank = d === 'Hard' ? 3 : d === 'Medium' ? 2 : d === 'Easy' ? 1 : 0
	const cRank = c === 'radical' ? 3 : c === 'medium' ? 2 : c === 'low' ? 1 : 0
	const rank = Math.max(dRank, cRank)
	if (!d || !c || dRank === cRank) {
		if (rank === 1) return '🟢 Easy · low change'
		if (rank === 2) return '🟡 Medium'
		return '🔴 Hard · radical'
	}
	const emoji = rank === 3 ? '🔴' : rank === 2 ? '🟡' : '🟢'
	const changeLabel =
		c === 'low' ? 'low change' : c === 'medium' ? 'medium change' : 'radical'
	return `${emoji} ${d} · ${changeLabel}`
}

function optionLine(item: unknown): string {
	if (typeof item === 'string') {
		return item.replace(/^[0-9]+[.)]\s+/, '').trim()
	}
	if (!item || typeof item !== 'object') {
		return String(item || '')
			.replace(/^[0-9]+[.)]\s+/, '')
			.trim()
	}
	const rec = item as DecisionOption
	const label = String(rec.label || rec.text || '').trim()
	if (!label) return ''
	const tag = formatImpactTag(rec.difficulty, rec.change)
	if (!tag || /[🟢🟡🔴]/.test(label)) return label
	return `${label} · ${tag}`
}

export function normalizeDecisionOptions(
	options?: DecisionOptionInput[] | string | null,
): string[] {
	const raw = Array.isArray(options)
		? options
		: typeof options === 'string'
			? options.split('\n')
			: []
	const seen = new Set<string>()
	const out: string[] = []
	for (const line of raw) {
		const value = optionLine(line)
		if (!value || seen.has(value)) continue
		seen.add(value)
		out.push(value)
		if (out.length >= 4) break
	}
	return out
}

export function hasDecisionShape(input: DecisionFields) {
	return Boolean(
		String(input.title || '').trim() ||
			String(input.context || '').trim() ||
			String(input.recommendation || '').trim() ||
			String(input.rightFix || '').trim() ||
			normalizeDecisionOptions(input.options).length > 0,
	)
}

function clamp(text: string, maxLength: number) {
	if (text.length <= maxLength) return text
	if (maxLength <= 1) return '…'
	return `${text.slice(0, maxLength - 1).trimEnd()}…`
}

/** If rightFix is omitted, copy recommendation so the card always has both. */
export function withDefaultRightFix(input: DecisionFields): DecisionFields {
	const recommendation = String(input.recommendation || '').trim()
	const rightFix = String(input.rightFix || '').trim()
	return {
		...input,
		recommendation: recommendation || input.recommendation,
		rightFix: rightFix || recommendation || input.rightFix,
	}
}

/**
 * Compact stored/email body. When first-class fields exist, prefer those over
 * a transcript dump. Legacy callers that only pass `summary` keep working.
 */
export function composeDecisionSummary(
	input: DecisionFields,
	maxLength = 1800,
) {
	input = withDefaultRightFix(input)
	const title = String(input.title || '').trim()
	const context = String(input.context || '').trim()
	const recommendation = String(input.recommendation || '').trim()
	const rightFix = String(input.rightFix || '').trim()
	const whyNotRightFix = String(input.whyNotRightFix || '').trim()
	const options = normalizeDecisionOptions(input.options)
	const summary = String(input.summary || '').trim()
	const structured = Boolean(context || recommendation || rightFix || options.length)
	if (!structured) return clamp(summary || title, maxLength)

	const parts: string[] = []
	if (title) parts.push(title)
	if (context) parts.push(`Context: ${context}`)
	if (recommendation) parts.push(`Recommendation: ${recommendation}`)
	if (rightFix) parts.push(`Right fix: ${rightFix}`)
	if (
		rightFix &&
		recommendation &&
		rightFix.trim().toLowerCase().replace(/\s+/g, ' ') !==
			recommendation.trim().toLowerCase().replace(/\s+/g, ' ')
	) {
		parts.push(
			`Why the recommendation isn't the right fix: ${whyNotRightFix || 'These differ; the agent did not explain why.'}`,
		)
	}
	if (options.length) {
		parts.push(
			`Options:\n${options.map((option, index) => `${index + 1}. ${option}`).join('\n')}`,
		)
	}
	if (
		summary &&
		summary !== recommendation &&
		summary !== context &&
		summary !== title
	) {
		parts.push(summary)
	}
	return clamp(parts.join('\n\n'), maxLength)
}

/**
 * First-class fields for format-report. When structured fields exist, omit
 * `summary` so Discord does not append a log dump after Context/Options.
 */
export function formatReportDecisionFields(input: DecisionFields) {
	input = withDefaultRightFix(input)
	const title = String(input.title || '').trim()
	const context = String(input.context || '').trim()
	const recommendation = String(input.recommendation || '').trim()
	const rightFix = String(input.rightFix || '').trim()
	const whyNotRightFix = String(input.whyNotRightFix || '').trim()
	const options = normalizeDecisionOptions(input.options)
	const summary = String(input.summary || '').trim()
	const structured = Boolean(context || recommendation || rightFix || options.length)
	return {
		title: title || undefined,
		context: context || undefined,
		recommendation: recommendation || undefined,
		rightFix: rightFix || undefined,
		whyNotRightFix: whyNotRightFix || undefined,
		options: options.length ? options : undefined,
		summary: structured ? undefined : summary || undefined,
	}
}