← 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 · TypeScriptexport 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,
}
}