mirror of
https://github.com/stablyai/orca.git
synced 2026-10-05 08:02:33 +00:00
Audit sweep over `src/cli`. 23 cases retired and 2 `it.each` tables collapsed
to the rows their parameter actually reaches.
What went, by pattern:
- Table rows whose varied parameter production never reads, so every row ran
one identical path.
- Second and third invocations of a contract already proven by the case above
them, differing only in a field the assertion ignores.
- Argument-shape and private-predicate checks duplicated at the real CLI
boundary, where the same input is already driven end to end.
- Assertions whose expected value came from the same helper under test.
`src/cli/command-suggestion.ts` loses `export { levenshtein }`, a re-export no
production caller used. The one test that stubs edit distance spies on
`../shared/edit-distance` directly, which is the module `command-suggestion`
imports, so the seam it needs is unaffected.
Kept deliberately: `orchestration-lifecycle-json-rejection.test.ts` and
`orchestration-migration.test.ts`, both named in `config/reliability-gates.jsonc`
as sole evidence for a gate.
While auditing the latter, its replay dimension turned out to be inert --
`it.each([false, true])` varies `lifecycle.duplicate`, and `hasLifecycleVerdict`
(`orchestration-worker-settlement.ts:112-132`) reads only `action`, `authority`
and `outcome`. The gate at `reliability-gates.jsonc:15861` nonetheless records
"first and replayed legacy worker_done settlements are accepted". Left exactly
as found and reported rather than collapsed, because correcting a gate's claim
or adding real replay coverage is the owner's call.
Verified: `pnpm test src/cli` (131 files, 1474 passed), `pnpm tc`,
`check-reliability-gates.mjs` (140 gates), `check:code-quality:changed`.
142 lines
5.3 KiB
TypeScript
142 lines
5.3 KiB
TypeScript
import { specPaths, type CommandSpec } from './command-spec'
|
|
import { levenshtein } from '../shared/edit-distance'
|
|
|
|
// Why: rank the live registry so typo recovery cannot drift from accepted paths.
|
|
|
|
const SUGGESTION_THRESHOLD = 3
|
|
const MAX_SUGGESTIONS = 3
|
|
|
|
// Why: a close typo of a destructive verb (`remov`→`remove`) still signals that
|
|
// intent, but `move` (distance 2 from `remove`) does not — keep this at 1 so
|
|
// genuine recovery works while unrelated verbs stay locked out. #6303
|
|
const DESTRUCTIVE_INTENT_THRESHOLD = 1
|
|
|
|
function finalToken(path: string[]): string {
|
|
return path.at(-1) ?? ''
|
|
}
|
|
|
|
// Why: destructiveness is declared on the spec (single source of truth); the
|
|
// intent verbs are the final tokens of every destructive path/alias so the guard
|
|
// tracks the registry instead of a hand-maintained list.
|
|
function destructiveVerbs(specs: CommandSpec[]): Set<string> {
|
|
const verbs = new Set<string>()
|
|
for (const spec of specs) {
|
|
if (spec.destructive) {
|
|
for (const path of specPaths(spec)) {
|
|
verbs.add(finalToken(path))
|
|
}
|
|
}
|
|
}
|
|
return verbs
|
|
}
|
|
|
|
// Why: deletion is irreversible and suggestions flow into agents' recovery
|
|
// channel (--json nextSteps), so only unlock destructive candidates when the
|
|
// input token is itself a near-miss of a destructive verb. #6303
|
|
function intendsDestruction(inputToken: string, verbs: Set<string>): boolean {
|
|
for (const verb of verbs) {
|
|
if (
|
|
Math.abs(inputToken.length - verb.length) <= DESTRUCTIVE_INTENT_THRESHOLD &&
|
|
levenshtein(inputToken, verb) <= DESTRUCTIVE_INTENT_THRESHOLD
|
|
) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
export type CommandErrorData = {
|
|
suggestions: string[]
|
|
nextSteps: string[]
|
|
}
|
|
|
|
// Why: one bounded near-match ranking keeps command and flag recovery consistent.
|
|
function rankByDistance(scored: { label: string; distance: number }[]): string[] {
|
|
return scored
|
|
.filter((entry) => entry.distance > 0 && entry.distance <= SUGGESTION_THRESHOLD)
|
|
.sort((a, b) => a.distance - b.distance || a.label.localeCompare(b.label))
|
|
.slice(0, MAX_SUGGESTIONS)
|
|
.map((entry) => entry.label)
|
|
}
|
|
|
|
// Why: same-depth matching avoids suggesting parent groups or unrelated commands.
|
|
export function suggestCommands(specs: CommandSpec[], commandPath: string[]): string[] {
|
|
const input = commandPath.join(' ')
|
|
// Why: only surface destructive commands when the user actually reached for one;
|
|
// otherwise a benign typo could recover into an irreversible action. #6303
|
|
const allowDestructive = intendsDestruction(finalToken(commandPath), destructiveVerbs(specs))
|
|
const seen = new Set<string>()
|
|
const scored: { label: string; distance: number }[] = []
|
|
for (const spec of specs) {
|
|
if (spec.hidden) {
|
|
continue
|
|
}
|
|
if (spec.destructive && !allowDestructive) {
|
|
continue
|
|
}
|
|
const candidates = specPaths(spec).map((path) =>
|
|
commandPath.length === 1 ? path.slice(0, 1) : path
|
|
)
|
|
for (const candidate of candidates) {
|
|
if (candidate.length !== commandPath.length) {
|
|
continue
|
|
}
|
|
const joined = candidate.join(' ')
|
|
if (seen.has(joined)) {
|
|
continue
|
|
}
|
|
seen.add(joined)
|
|
if (Math.abs(input.length - joined.length) <= SUGGESTION_THRESHOLD) {
|
|
scored.push({ label: joined, distance: levenshtein(input, joined) })
|
|
}
|
|
}
|
|
}
|
|
return rankByDistance(scored)
|
|
}
|
|
|
|
export function unknownCommandData(specs: CommandSpec[], commandPath: string[]): CommandErrorData {
|
|
const suggestions = suggestCommands(specs, commandPath)
|
|
const nextSteps = suggestions.length
|
|
? [`Did you mean: ${suggestions.map((path) => `orca ${path}`).join(', ')}`]
|
|
: []
|
|
return { suggestions, nextSteps }
|
|
}
|
|
|
|
export type FlagErrorData = {
|
|
validFlags: string[]
|
|
suggestions: string[]
|
|
nextSteps: string[]
|
|
}
|
|
|
|
// Why: edit distance cannot recover a rename. `orchestration check` is the one verb
|
|
// that identifies its caller with `--terminal` while every sibling uses `--from`, so
|
|
// the near-miss ranking answered `--json`/`--run` and left the caller stuck (#16904).
|
|
// A synonym only fires where the typed flag is rejected and its partner is accepted.
|
|
const FLAG_SYNONYMS: Readonly<Record<string, string>> = { from: 'terminal' }
|
|
|
|
function suggestFlags(flag: string, validFlags: string[]): string[] {
|
|
const synonym = FLAG_SYNONYMS[flag]
|
|
const scored: { label: string; distance: number }[] = []
|
|
for (const candidate of validFlags) {
|
|
if (Math.abs(flag.length - candidate.length) <= SUGGESTION_THRESHOLD) {
|
|
scored.push({ label: candidate, distance: levenshtein(flag, candidate) })
|
|
}
|
|
}
|
|
const ranked = rankByDistance(scored)
|
|
return synonym && validFlags.includes(synonym)
|
|
? [synonym, ...ranked.filter((name) => name !== synonym)].slice(0, MAX_SUGGESTIONS)
|
|
: ranked
|
|
}
|
|
|
|
// Why: include the accepted set so agents can recover without another help call.
|
|
export function unknownFlagData(flag: string, validFlags: string[]): FlagErrorData {
|
|
const sortedValid = [...validFlags].sort((a, b) => a.localeCompare(b))
|
|
const suggestions = suggestFlags(flag, sortedValid)
|
|
const nextSteps: string[] = []
|
|
if (suggestions.length > 0) {
|
|
nextSteps.push(`Did you mean: ${suggestions.map((name) => `--${name}`).join(', ')}`)
|
|
}
|
|
nextSteps.push(`Valid flags: ${sortedValid.map((name) => `--${name}`).join(', ')}`)
|
|
return { validFlags: sortedValid, suggestions, nextSteps }
|
|
}
|