docs(mystery): spec for engine prereqs (verbs, disambiguation, ending UI) #1

Merged
ejlewis merged 19 commits from feat/engine-prereqs into main 2026-05-09 15:10:20 -05:00
3 changed files with 46 additions and 11 deletions
Showing only changes of commit 46f851bc3a - Show all commits
+36 -7
View File
@@ -158,7 +158,7 @@ describe('parser — verb + target', () => {
}) })
describe('parser — disambiguation', () => { describe('parser — disambiguation', () => {
it('returns disambiguation request when two candidates match', () => { it('returns ambiguous when two candidates match', () => {
const ctx: ParserContext = { const ctx: ParserContext = {
knownItems: ['brass-key', 'iron-key'], knownItems: ['brass-key', 'iron-key'],
knownEncounters: [], knownEncounters: [],
@@ -171,12 +171,11 @@ describe('parser — disambiguation', () => {
awaitingDisambiguation: null, awaitingDisambiguation: null,
} }
const result = parse('take key', ctx) const result = parse('take key', ctx)
expect(result.kind).toBe('unknown') expect(result.kind).toBe('ambiguous')
if (result.kind === 'unknown') { if (result.kind === 'ambiguous') {
// Parser flags ambiguity by returning unknown-noun; the dispatcher expect(result.verb).toBe('take')
// turns this into a PendingDisambiguation. (Keeping parser pure: it expect(result.rawNoun).toBe('key')
// signals; the dispatcher decides UI flow.) expect(result.candidates).toEqual(['brass-key', 'iron-key'])
expect(result.reason).toBe('unknown-noun')
} }
}) })
@@ -268,3 +267,33 @@ describe('stop-word stripping', () => {
expect(cmd.kind).toBe('verb-target') expect(cmd.kind).toBe('verb-target')
}) })
}) })
describe('ambiguous noun', () => {
const ctx: ParserContext = {
knownItems: ['iron-key', 'brass-key'],
knownEncounters: [],
visibleNouns: [
{ id: 'iron-key', aliases: ['key', 'iron key'] },
{ id: 'brass-key', aliases: ['key', 'brass key'] },
],
inventoryItemIds: [],
lastNoun: null,
awaitingDisambiguation: null,
}
it('returns ambiguous when two aliases match the same noun phrase', () => {
const cmd = parse('take key', ctx)
expect(cmd).toEqual({
kind: 'ambiguous',
verb: 'take',
rawNoun: 'key',
candidates: ['iron-key', 'brass-key'],
})
})
it('still returns verb-target when the phrase is unambiguous', () => {
const cmd = parse('take iron key', ctx)
expect(cmd.kind).toBe('verb-target')
if (cmd.kind === 'verb-target') expect(cmd.target.canonical).toBe('iron-key')
})
})
+9 -4
View File
@@ -173,11 +173,16 @@ export function parse(rawInput: string, ctx: ParserContext): ParsedCommand {
return { kind: 'unknown', raw: trimmed, reason: 'unknown-noun' } return { kind: 'unknown', raw: trimmed, reason: 'unknown-noun' }
} }
// Multiple candidates → ambiguous. Parser signals; the dispatcher records the // Multiple candidates → ambiguous. Dedupe by id; if only one distinct id
// PendingDisambiguation in state so the next turn's input is interpreted as // remains, two aliases of the same item matched — not truly ambiguous.
// a disambiguation reply.
if (candidates.length > 1) { if (candidates.length > 1) {
return { kind: 'unknown', raw: trimmed, reason: 'unknown-noun' } const uniqueIds = [...new Set(candidates.map((c) => c.id))]
if (uniqueIds.length === 1) {
// Two aliases of the same item — not actually ambiguous.
const id = uniqueIds[0]!
return { kind: 'verb-target', verb, target: { canonical: id, raw: candidates[0]!.alias } }
}
return { kind: 'ambiguous', verb, rawNoun: targetRaw, candidates: uniqueIds }
} }
const target = candidates[0]! const target = candidates[0]!
+1
View File
@@ -24,6 +24,7 @@ export type ParsedCommand =
| { kind: 'verb-only'; verb: Verb | 'look' | 'inventory' | 'wait' } | { kind: 'verb-only'; verb: Verb | 'look' | 'inventory' | 'wait' }
| { kind: 'verb-target'; verb: Verb; target: NounRef } | { kind: 'verb-target'; verb: Verb; target: NounRef }
| { kind: 'verb-target-prep'; verb: Verb; target: NounRef; preposition: string; indirect: NounRef } | { kind: 'verb-target-prep'; verb: Verb; target: NounRef; preposition: string; indirect: NounRef }
| { kind: 'ambiguous'; verb: Verb; rawNoun: string; candidates: string[] }
| { kind: 'go'; direction: Direction } | { kind: 'go'; direction: Direction }
| { kind: 'meta'; verb: MetaVerb } | { kind: 'meta'; verb: MetaVerb }
| { kind: 'disambiguation'; chosen: string } | { kind: 'disambiguation'; chosen: string }