@sharpee/parser-en-us 3.0.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/grammar.js CHANGED
@@ -4,16 +4,19 @@
4
4
  * @description Standard grammar patterns for English interactive fiction
5
5
  *
6
6
  * Rule Priority Guidelines:
7
- * - 100+: Semantic rules with trait constraints (e.g., .hasTrait(TraitType.ENTERABLE))
7
+ * - 100+: Specific/phrasal patterns that must outrank broader ones
8
8
  * - 100: Standard patterns
9
9
  * - 95: Synonyms/alternatives
10
10
  * - 90: Abbreviations
11
11
  *
12
- * Semantic rules should come first to match before fallback patterns.
12
+ * Parse-time gating: `.where()` scope constraints are the one parse-time
13
+ * gating mechanism. Trait-based refusal ("that's not something you can
14
+ * enter/climb/open") lives in each action's validate(), never in grammar —
15
+ * the former rule-level `.hasTrait()` API was a parse-time no-op and was
16
+ * deleted (ADR-231 D2a, 2026-07-17).
13
17
  */
14
18
  Object.defineProperty(exports, "__esModule", { value: true });
15
19
  exports.defineGrammar = defineGrammar;
16
- const world_model_1 = require("@sharpee/world-model");
17
20
  /**
18
21
  * Define English grammar rules
19
22
  * @param grammar The grammar builder to use
@@ -28,7 +31,7 @@ function defineGrammar(grammar) {
28
31
  // Scope handled by action validation - tries see/feel/hear/smell cascade
29
32
  grammar
30
33
  .forAction('if.action.examining')
31
- .verbs(['examine', 'x', 'inspect'])
34
+ .verbs(['examine', 'x', 'inspect', 'check', 'view', 'observe']) // +check/view/observe (ADR-230 D4)
32
35
  .pattern(':target')
33
36
  .build();
34
37
  // "look at" is a phrasal pattern - different structure
@@ -37,10 +40,11 @@ function defineGrammar(grammar) {
37
40
  .mapsTo('if.action.examining')
38
41
  .withPriority(95)
39
42
  .build();
40
- // Looking with optional adverbs
43
+ // Looking with optional adverbs (ADR-230 D3a: the adverb form is just
44
+ // examining — if.action.examining_carefully had no action behind it)
41
45
  grammar
42
46
  .define('look [carefully] at :target')
43
- .mapsTo('if.action.examining_carefully')
47
+ .mapsTo('if.action.examining')
44
48
  .withPriority(96)
45
49
  .build(); // Slightly higher priority, but confidence penalty for skipped optionals
46
50
  grammar
@@ -78,7 +82,7 @@ function defineGrammar(grammar) {
78
82
  // Scope handled by action validation; SceneryTrait blocks non-portable items
79
83
  grammar
80
84
  .forAction('if.action.taking')
81
- .verbs(['take', 'get', 'grab'])
85
+ .verbs(['take', 'get', 'grab', 'acquire', 'collect']) // +acquire/collect (ADR-230 D4; bare `pick` deliberately absent — it would outmatch the `pick up :item` compound)
82
86
  .pattern(':item')
83
87
  .build();
84
88
  // "pick up" is a phrasal verb - different pattern structure
@@ -90,7 +94,7 @@ function defineGrammar(grammar) {
90
94
  // Scope (carried) handled by action validation
91
95
  grammar
92
96
  .forAction('if.action.dropping')
93
- .verbs(['drop', 'discard'])
97
+ .verbs(['drop', 'discard', 'release']) // +release (ADR-230 D4 patterns reconciliation)
94
98
  .pattern(':item')
95
99
  .build();
96
100
  // "put down" is a phrasal verb - different pattern structure
@@ -133,13 +137,13 @@ function defineGrammar(grammar) {
133
137
  // Eating (ADR-087: using forAction)
134
138
  grammar
135
139
  .forAction('if.action.eating')
136
- .verbs(['eat', 'consume', 'devour'])
140
+ .verbs(['eat', 'consume', 'devour', 'munch', 'nibble']) // +munch/nibble (ADR-230 D4 patterns reconciliation)
137
141
  .pattern(':item')
138
142
  .build();
139
143
  // Drinking (ADR-087: using forAction)
140
144
  grammar
141
145
  .forAction('if.action.drinking')
142
- .verbs(['drink', 'sip', 'quaff'])
146
+ .verbs(['drink', 'sip', 'quaff', 'swallow', 'imbibe']) // +swallow/imbibe (ADR-230 D4)
143
147
  .pattern(':item')
144
148
  .build();
145
149
  // ADR-080 Phase 2: Multi-object commands
@@ -149,23 +153,20 @@ function defineGrammar(grammar) {
149
153
  // - "take knife and lamp" matches "take :item", parser creates list
150
154
  // - "take all but sword" matches "take :item", parser detects exclusion
151
155
  // Container operations
152
- // Scope handled by action validation; traits declare semantic constraints only
156
+ // Scope and trait-based refusal handled by action validation
153
157
  grammar
154
158
  .define('put :item in|into|inside :container')
155
- .hasTrait('container', world_model_1.TraitType.CONTAINER)
156
159
  .mapsTo('if.action.inserting')
157
160
  .withPriority(100)
158
161
  .build();
159
162
  grammar
160
163
  .define('insert :item in|into :container')
161
- .hasTrait('container', world_model_1.TraitType.CONTAINER)
162
164
  .mapsTo('if.action.inserting')
163
165
  .withPriority(100)
164
166
  .build();
165
167
  // Supporter operations (including hanging!)
166
168
  grammar
167
169
  .define('put :item on|onto :supporter')
168
- .hasTrait('supporter', world_model_1.TraitType.SUPPORTER)
169
170
  .mapsTo('if.action.putting')
170
171
  .withPriority(100)
171
172
  .build();
@@ -185,13 +186,11 @@ function defineGrammar(grammar) {
185
186
  .forAction('if.action.inventory')
186
187
  .verbs(['inventory', 'inv', 'i'])
187
188
  .build();
188
- // Movement (ADR-087: using forAction with directions)
189
- grammar
190
- .define('go :direction')
191
- .where('direction', { type: 'direction' })
192
- .mapsTo('if.action.going')
193
- .withPriority(100)
194
- .build();
189
+ // Movement (ADR-087). The old `go :direction` define was DELETED in
190
+ // ADR-230 Phase 6: a `:direction` slot with a `{ type: 'direction' }`
191
+ // constraint always failed at runtime ("PropertyConstraint in slot
192
+ // constraints not yet supported"), so `go north` never worked — the
193
+ // verb+direction literals in the D4 block below replace it.
195
194
  // Bare direction commands (ADR-087: consolidated with forAction)
196
195
  grammar
197
196
  .forAction('if.action.going')
@@ -211,16 +210,14 @@ function defineGrammar(grammar) {
211
210
  })
212
211
  .build();
213
212
  // Opening and closing
214
- // Scope handled by action validation; traits declare semantic constraints only
213
+ // Scope and trait-based refusal handled by action validation
215
214
  grammar
216
215
  .define('open :door')
217
- .hasTrait('door', world_model_1.TraitType.OPENABLE)
218
216
  .mapsTo('if.action.opening')
219
217
  .withPriority(100)
220
218
  .build();
221
219
  grammar
222
220
  .define('close :door')
223
- .hasTrait('door', world_model_1.TraitType.OPENABLE)
224
221
  .mapsTo('if.action.closing')
225
222
  .withPriority(100)
226
223
  .build();
@@ -229,23 +226,19 @@ function defineGrammar(grammar) {
229
226
  .forAction('if.action.switching_on')
230
227
  .verbs(['turn', 'switch', 'flip'])
231
228
  .pattern('on :device')
232
- .hasTrait('device', world_model_1.TraitType.SWITCHABLE)
233
229
  .build();
234
230
  grammar
235
231
  .forAction('if.action.switching_off')
236
232
  .verbs(['turn', 'switch', 'flip'])
237
233
  .pattern('off :device')
238
- .hasTrait('device', world_model_1.TraitType.SWITCHABLE)
239
234
  .build();
240
235
  // Alternative phrasal verb order: "turn lamp on" / "turn lamp off"
241
236
  grammar
242
237
  .define('turn :device on')
243
- .hasTrait('device', world_model_1.TraitType.SWITCHABLE)
244
238
  .mapsTo('if.action.switching_on')
245
239
  .build();
246
240
  grammar
247
241
  .define('turn :device off')
248
- .hasTrait('device', world_model_1.TraitType.SWITCHABLE)
249
242
  .mapsTo('if.action.switching_off')
250
243
  .build();
251
244
  // Pushing and pulling (ADR-087: using forAction)
@@ -257,7 +250,7 @@ function defineGrammar(grammar) {
257
250
  .build();
258
251
  grammar
259
252
  .forAction('if.action.pulling')
260
- .verbs(['pull', 'drag', 'yank'])
253
+ .verbs(['pull', 'drag', 'yank', 'tug']) // +tug (ADR-230 D4)
261
254
  .pattern(':target')
262
255
  .build();
263
256
  // Lowering and raising (ADR-090: capability dispatch)
@@ -292,6 +285,11 @@ function defineGrammar(grammar) {
292
285
  .mapsTo('if.action.restarting')
293
286
  .withPriority(100)
294
287
  .build();
288
+ // Sleeping (ADR-230 D2; +nap/doze/rest/slumber D4)
289
+ grammar
290
+ .forAction('if.action.sleeping')
291
+ .verbs(['sleep', 'nap', 'doze', 'rest', 'slumber'])
292
+ .build();
295
293
  // Quitting (ADR-087: using forAction)
296
294
  grammar
297
295
  .forAction('if.action.quitting')
@@ -393,36 +391,31 @@ function defineGrammar(grammar) {
393
391
  .withPriority(100)
394
392
  .build();
395
393
  // VERB_NOUN_NOUN patterns (Phase 2)
396
- // Scope handled by action validation; traits declare semantic constraints only
394
+ // Scope and trait-based refusal handled by action validation
397
395
  // Giving
398
396
  grammar
399
397
  .define('give :item to :recipient')
400
- .hasTrait('recipient', world_model_1.TraitType.ACTOR)
401
398
  .mapsTo('if.action.giving')
402
399
  .withPriority(100)
403
400
  .build();
404
401
  grammar
405
402
  .define('give :recipient :item')
406
- .hasTrait('recipient', world_model_1.TraitType.ACTOR)
407
403
  .mapsTo('if.action.giving')
408
404
  .withPriority(95)
409
405
  .build(); // Slightly lower priority than explicit "to"
410
406
  grammar
411
407
  .define('offer :item to :recipient')
412
- .hasTrait('recipient', world_model_1.TraitType.ACTOR)
413
408
  .mapsTo('if.action.giving')
414
409
  .withPriority(100)
415
410
  .build();
416
411
  // Showing
417
412
  grammar
418
413
  .define('show :item to :recipient')
419
- .hasTrait('recipient', world_model_1.TraitType.ACTOR)
420
414
  .mapsTo('if.action.showing')
421
415
  .withPriority(100)
422
416
  .build();
423
417
  grammar
424
418
  .define('show :recipient :item')
425
- .hasTrait('recipient', world_model_1.TraitType.ACTOR)
426
419
  .mapsTo('if.action.showing')
427
420
  .withPriority(95)
428
421
  .build();
@@ -432,6 +425,24 @@ function defineGrammar(grammar) {
432
425
  .mapsTo('if.action.throwing')
433
426
  .withPriority(100)
434
427
  .build();
428
+ // Bare general throw (no target) — reaches the action's general-throw
429
+ // branch; `throw away :item` (dropping) still wins via D2b literal
430
+ // specificity, and the at/to forms via their extra literal.
431
+ grammar
432
+ .define('throw :item')
433
+ .mapsTo('if.action.throwing')
434
+ .withPriority(95)
435
+ .build();
436
+ grammar
437
+ .define('toss :item')
438
+ .mapsTo('if.action.throwing')
439
+ .withPriority(95)
440
+ .build();
441
+ grammar
442
+ .define('hurl :item')
443
+ .mapsTo('if.action.throwing')
444
+ .withPriority(95)
445
+ .build();
435
446
  grammar
436
447
  .define('throw :item to :recipient')
437
448
  .mapsTo('if.action.throwing')
@@ -439,11 +450,21 @@ function defineGrammar(grammar) {
439
450
  .build();
440
451
  // Multiple preposition patterns (Phase 2.1)
441
452
  // Scope handled by action validation; state checks (locked, open) in action validate()
442
- // Taking from container with tool
453
+ // Taking from container with tool (ADR-230 Phase 6: remapped from the
454
+ // orphan if.action.taking_with onto removing — the tool rides the
455
+ // instrument slot and is interceptor-consultable)
443
456
  grammar
444
457
  .define('take :item from :container with|using :tool')
445
458
  .instrument('tool')
446
- .mapsTo('if.action.taking_with')
459
+ .mapsTo('if.action.removing')
460
+ .withPriority(110)
461
+ .build();
462
+ // Removing from a container (ADR-230 D2). Priority 110 like the other
463
+ // multi-preposition patterns here, so the from-form outranks taking_off's
464
+ // bare `remove :target` when the command names a source.
465
+ grammar
466
+ .define('remove :item from :container')
467
+ .mapsTo('if.action.removing')
447
468
  .withPriority(110)
448
469
  .build();
449
470
  // Unlocking with key
@@ -453,12 +474,32 @@ function defineGrammar(grammar) {
453
474
  .mapsTo('if.action.unlocking')
454
475
  .withPriority(110)
455
476
  .build();
456
- // Opening with tool
477
+ // Locking and keyless unlocking (ADR-230 D2). No trait constraint,
478
+ // mirroring the keyed unlock pattern: the action's validate owns
479
+ // not-lockable/no-key messaging (validateKeyRequirements refuses with
480
+ // no_key when the lockable requires a key).
481
+ grammar
482
+ .forAction('if.action.locking')
483
+ .verbs(['lock'])
484
+ .pattern(':target')
485
+ .build();
486
+ grammar
487
+ .define('lock :target with|using :key')
488
+ .instrument('key')
489
+ .mapsTo('if.action.locking')
490
+ .withPriority(110)
491
+ .build();
492
+ grammar
493
+ .forAction('if.action.unlocking')
494
+ .verbs(['unlock'])
495
+ .pattern(':target')
496
+ .build();
497
+ // Opening with tool (ADR-230 D3b: a tool slot on the standard opening
498
+ // action, not a separate id — if.action.opening_with had no action)
457
499
  grammar
458
500
  .define('open :container with|using :tool')
459
501
  .instrument('tool')
460
- .hasTrait('container', world_model_1.TraitType.OPENABLE)
461
- .mapsTo('if.action.opening_with')
502
+ .mapsTo('if.action.opening')
462
503
  .withPriority(110)
463
504
  .build();
464
505
  // Cutting with tool
@@ -468,10 +509,18 @@ function defineGrammar(grammar) {
468
509
  .mapsTo('if.action.cutting')
469
510
  .withPriority(110)
470
511
  .build();
512
+ // Cutting — bare form (2026-07-17, chord go-live G1 shortlist): an
513
+ // untooled cuttable was unreachable for TS and Chord alike; the tooled
514
+ // form above keeps priority 110 so it wins when a tool is named.
515
+ grammar
516
+ .forAction('if.action.cutting')
517
+ .verbs(['cut', 'slice', 'chop'])
518
+ .pattern(':target')
519
+ .build();
471
520
  // Attacking - simple patterns (ADR-087: using forAction)
472
521
  grammar
473
522
  .forAction('if.action.attacking')
474
- .verbs(['attack', 'kill', 'fight', 'slay', 'murder', 'hit', 'strike'])
523
+ .verbs(['attack', 'kill', 'fight', 'slay', 'murder', 'hit', 'strike', 'break', 'smash', 'destroy']) // +break/smash/destroy (ADR-230 D4)
475
524
  .pattern(':target')
476
525
  .build();
477
526
  // Attacking with weapon (higher priority than simple attack)
@@ -506,52 +555,196 @@ function defineGrammar(grammar) {
506
555
  .mapsTo('if.action.digging')
507
556
  .withPriority(110)
508
557
  .build();
509
- // Communication patterns with quoted strings
510
- // Scope handled by action validation; traits declare semantic constraints only
511
- grammar
512
- .define('say :message')
513
- .mapsTo('if.action.saying')
514
- .withPriority(100)
515
- .build();
558
+ // Digging bare form (2026-07-17, chord go-live G1 shortlist): same
559
+ // bare-grammar hole as cutting.
516
560
  grammar
517
- .define('say :message to :recipient')
518
- .hasTrait('recipient', world_model_1.TraitType.ACTOR)
519
- .mapsTo('if.action.saying_to')
520
- .withPriority(105)
561
+ .forAction('if.action.digging')
562
+ .verbs(['dig'])
563
+ .pattern(':target')
521
564
  .build();
565
+ // Communication patterns (saying/saying_to/writing/writing_on/shouting/
566
+ // whispering) DELETED in ADR-230 Phase 6 (sketch ruling 4/5): the ids had
567
+ // no actions — they parsed and runtime-failed in every story. Grammar
568
+ // returns with the conversation/writing systems; stories (e.g. dungeo's
569
+ // SAY) keep their own story-grammar verbs meanwhile.
570
+ // ADR-231 D4: `:topic` is a TOPIC slot — free text captured verbatim by
571
+ // the TextSlotConsumer, never a positional entity slot. The validator's
572
+ // entity-first attempt resolves an in-scope entity quietly; a topic is
573
+ // never scope-rejected.
522
574
  grammar
523
- .define('write :message')
524
- .mapsTo('if.action.writing')
575
+ .define('tell :recipient about :topic')
576
+ .topic('topic')
577
+ .mapsTo('if.action.telling')
525
578
  .withPriority(100)
526
579
  .build();
527
580
  grammar
528
- .define('write :message on :surface')
529
- .mapsTo('if.action.writing_on')
530
- .withPriority(105)
531
- .build();
532
- grammar
533
- .define('shout :message')
534
- .mapsTo('if.action.shouting')
581
+ .define('ask :recipient about :topic')
582
+ .topic('topic')
583
+ .mapsTo('if.action.asking')
535
584
  .withPriority(100)
536
585
  .build();
586
+ // asking/telling aliases (ADR-230 Phase 6 — actions revived as minimal
587
+ // interceptable stubs)
537
588
  grammar
538
- .define('whisper :message to :recipient')
539
- .hasTrait('recipient', world_model_1.TraitType.ACTOR)
540
- .mapsTo('if.action.whispering')
589
+ .define('question :recipient about :topic')
590
+ .topic('topic')
591
+ .mapsTo('if.action.asking')
541
592
  .withPriority(100)
542
593
  .build();
543
594
  grammar
544
- .define('tell :recipient about :topic')
545
- .hasTrait('recipient', world_model_1.TraitType.ACTOR)
546
- .mapsTo('if.action.telling')
595
+ .define('inquire of :recipient about :topic')
596
+ .topic('topic')
597
+ .mapsTo('if.action.asking')
547
598
  .withPriority(100)
548
599
  .build();
549
600
  grammar
550
- .define('ask :recipient about :topic')
551
- .hasTrait('recipient', world_model_1.TraitType.ACTOR)
552
- .mapsTo('if.action.asking')
601
+ .define('inform :recipient about :topic')
602
+ .topic('topic')
603
+ .mapsTo('if.action.telling')
553
604
  .withPriority(100)
554
605
  .build();
606
+ // Talking (ADR-229 R3): the core route to if.action.talking — the action
607
+ // was wired for interceptors (ADR-228 Phase 5) but previously reachable
608
+ // only via story grammar. No parse-time actor gate (the pattern all
609
+ // trait-gated verbs now follow, ADR-231 D2a): talking's validate owns
610
+ // not_actor/too_far, so those refusals flow through blocked() →
611
+ // onBlocked (interceptor-visible) instead of dying as a parse failure.
612
+ // Story grammar outranks these on priority as usual.
613
+ grammar
614
+ .define('talk to|with :target')
615
+ .mapsTo('if.action.talking')
616
+ .withPriority(100)
617
+ .build();
618
+ grammar
619
+ .define('speak to|with :target')
620
+ .mapsTo('if.action.talking')
621
+ .withPriority(100)
622
+ .build();
623
+ grammar
624
+ .define('chat with :target')
625
+ .mapsTo('if.action.talking')
626
+ .withPriority(100)
627
+ .build();
628
+ grammar
629
+ .define('converse with :target')
630
+ .mapsTo('if.action.talking')
631
+ .withPriority(100)
632
+ .build();
633
+ // ==========================================================================
634
+ // D4 SYNONYM PROMOTION (ADR-230): every verbs.ts synonym parses.
635
+ // Mechanical aliases of existing actions; phrasal/complex forms use
636
+ // .define() per the ADR-087 convention.
637
+ // ==========================================================================
638
+ // going aliases (verbs.ts: walk/run/head/travel; `move` LEFT the going
639
+ // list — Phase 1 ruling, move is manipulation-only).
640
+ // Literal expansion per direction alias, exactly like the bare-direction
641
+ // block below: a `:direction` slot inside .define() does NOT match
642
+ // direction words (the `go :direction` rule above yields
643
+ // "PropertyConstraint not yet supported" — pre-existing; bare directions
644
+ // are what players actually use). These literals also make `go north`
645
+ // work for the first time.
646
+ {
647
+ const directionAliases = {
648
+ north: ['north', 'n'],
649
+ south: ['south', 's'],
650
+ east: ['east', 'e'],
651
+ west: ['west', 'w'],
652
+ northeast: ['northeast', 'ne'],
653
+ northwest: ['northwest', 'nw'],
654
+ southeast: ['southeast', 'se'],
655
+ southwest: ['southwest', 'sw'],
656
+ up: ['up', 'u'],
657
+ down: ['down', 'd'],
658
+ in: ['in', 'inside'],
659
+ out: ['out', 'outside'],
660
+ };
661
+ for (const verb of ['go', 'walk', 'run', 'head', 'travel']) {
662
+ for (const [canonical, aliases] of Object.entries(directionAliases)) {
663
+ for (const alias of aliases) {
664
+ grammar
665
+ .define(`${verb} ${alias}`)
666
+ .mapsTo('if.action.going')
667
+ .withPriority(alias.length === 1 ? 90 : 100)
668
+ .withDefaultSemantics({ direction: canonical })
669
+ .build();
670
+ }
671
+ }
672
+ }
673
+ // `move :target <direction>` → pushing (Phase 1 move ruling), same
674
+ // literal expansion.
675
+ for (const aliases of Object.values(directionAliases)) {
676
+ for (const alias of aliases) {
677
+ grammar
678
+ .define(`move :target ${alias}`)
679
+ .mapsTo('if.action.pushing')
680
+ .withPriority(105)
681
+ .build();
682
+ }
683
+ }
684
+ }
685
+ // exiting alias
686
+ grammar.define('go out').mapsTo('if.action.exiting').withPriority(105).build(); // ADR-231 D2b: phrasal form above bare verb-plus-slot
687
+ // listening alias (hear)
688
+ grammar.define('hear').mapsTo('if.action.listening').withPriority(100).build();
689
+ grammar.define('hear :target').mapsTo('if.action.listening').withPriority(100).build();
690
+ // taking/dropping phrasal aliases
691
+ grammar.define('take up :item').mapsTo('if.action.taking').withPriority(100).build();
692
+ grammar.define('throw away :item').mapsTo('if.action.dropping').withPriority(100).build();
693
+ // putting aliases (place; move-to per the Phase 1 move ruling — putting
694
+ // resolves in/on by destination type when the preposition is `to`)
695
+ grammar.define('place :item in|into|inside :container').mapsTo('if.action.putting').withPriority(100).build();
696
+ grammar.define('place :item on|onto :supporter').mapsTo('if.action.putting').withPriority(100).build();
697
+ // 110 so the to-form outranks pushing's bare `move :target`
698
+ grammar.define('move :item to :destination').mapsTo('if.action.putting').withPriority(110).build();
699
+ // opening/closing aliases
700
+ grammar.define('unwrap :door').mapsTo('if.action.opening').withPriority(100).build();
701
+ grammar.define('uncover :door').mapsTo('if.action.opening').withPriority(100).build();
702
+ grammar.define('shut :door').mapsTo('if.action.closing').withPriority(100).build();
703
+ grammar.define('cover :door').mapsTo('if.action.closing').withPriority(100).build();
704
+ // turning (ADR-230 Phase 6 sketch ruling 1: capability dispatch like
705
+ // lowering/raising). Priority 95 so the switching phrasal forms
706
+ // (`turn :device on`, `turn on :device`) always win.
707
+ grammar.define('turn :target').mapsTo('if.action.turning').withPriority(95).build();
708
+ grammar.define('rotate :target').mapsTo('if.action.turning').withPriority(95).build();
709
+ grammar.define('twist :target').mapsTo('if.action.turning').withPriority(95).build();
710
+ // locking/unlocking aliases
711
+ grammar.define('secure :target').mapsTo('if.action.locking').withPriority(100).build();
712
+ grammar.define('unsecure :target').mapsTo('if.action.unlocking').withPriority(100).build();
713
+ // switching aliases (bare transitive forms — activate/start/deactivate/stop)
714
+ grammar.define('activate :device').mapsTo('if.action.switching_on').withPriority(100).build();
715
+ grammar.define('start :device').mapsTo('if.action.switching_on').withPriority(100).build();
716
+ grammar.define('deactivate :device').mapsTo('if.action.switching_off').withPriority(100).build();
717
+ grammar.define('stop :device').mapsTo('if.action.switching_off').withPriority(100).build();
718
+ // giving/showing aliases
719
+ grammar.define('hand :item to :recipient').mapsTo('if.action.giving').withPriority(100).build();
720
+ grammar.define('hand :recipient :item').mapsTo('if.action.giving').withPriority(100).build();
721
+ grammar.define('display :item to :recipient').mapsTo('if.action.showing').withPriority(100).build();
722
+ grammar.define('present :item to :recipient').mapsTo('if.action.showing').withPriority(100).build();
723
+ // throwing aliases
724
+ grammar.define('toss :item at :target').mapsTo('if.action.throwing').withPriority(100).build();
725
+ grammar.define('toss :item to :recipient').mapsTo('if.action.throwing').withPriority(100).build();
726
+ grammar.define('hurl :item at :target').mapsTo('if.action.throwing').withPriority(100).build();
727
+ grammar.define('hurl :item to :recipient').mapsTo('if.action.throwing').withPriority(100).build();
728
+ // patterns-array reconciliation promotions (PIN 4b): phrasal forms the
729
+ // lang help surface advertises
730
+ grammar.define('munch on :item').mapsTo('if.action.eating').withPriority(100).build();
731
+ grammar.define('nibble on :item').mapsTo('if.action.eating').withPriority(100).build();
732
+ grammar.define('drink from :target').mapsTo('if.action.drinking').withPriority(100).build();
733
+ grammar.define('sip from :target').mapsTo('if.action.drinking').withPriority(100).build();
734
+ grammar.define('let go of :item').mapsTo('if.action.dropping').withPriority(100).build();
735
+ grammar.define('open up :door').mapsTo('if.action.opening').withPriority(100).build();
736
+ grammar.define('power on :device').mapsTo('if.action.switching_on').withPriority(100).build();
737
+ grammar.define('power off :device').mapsTo('if.action.switching_off').withPriority(100).build();
738
+ grammar.define('extract :item from :container').mapsTo('if.action.removing').withPriority(110).build();
739
+ // meta aliases
740
+ grammar.define('save game').mapsTo('if.action.saving').withPriority(100).build();
741
+ grammar.define('load').mapsTo('if.action.restoring').withPriority(100).build();
742
+ grammar.define('load game').mapsTo('if.action.restoring').withPriority(100).build();
743
+ grammar.define('restore game').mapsTo('if.action.restoring').withPriority(100).build();
744
+ grammar.define('exit game').mapsTo('if.action.quitting').withPriority(105).build(); // outranks `exit :container`
745
+ grammar.define('?').mapsTo('if.action.help').withPriority(100).build();
746
+ grammar.define('commands').mapsTo('if.action.help').withPriority(100).build();
747
+ grammar.define('points').mapsTo('if.action.scoring').withPriority(100).build();
555
748
  // Touching/sensory actions (ADR-087: using forAction)
556
749
  // Scope handled by action validation
557
750
  grammar
@@ -559,52 +752,108 @@ function defineGrammar(grammar) {
559
752
  .verbs(['touch', 'rub', 'feel', 'pat', 'stroke', 'poke', 'prod'])
560
753
  .pattern(':target')
561
754
  .build();
755
+ // Listening and smelling (ADR-230 D2) — bare and targeted forms; both
756
+ // actions support the bare shape (ambient sound / room smell).
757
+ grammar
758
+ .define('listen')
759
+ .mapsTo('if.action.listening')
760
+ .withPriority(100)
761
+ .build();
762
+ grammar
763
+ .define('listen to :target')
764
+ .mapsTo('if.action.listening')
765
+ .withPriority(100)
766
+ .build();
767
+ grammar
768
+ .forAction('if.action.smelling')
769
+ .verbs(['smell', 'sniff'])
770
+ .build();
771
+ grammar
772
+ .forAction('if.action.smelling')
773
+ .verbs(['smell', 'sniff'])
774
+ .pattern(':target')
775
+ .build();
562
776
  // ============================================================================
563
777
  // ENTERING AND EXITING
564
- // Scope handled by action validation; traits declare semantic constraints only
778
+ // Scope and trait-based refusal handled by action validation
565
779
  // ============================================================================
566
780
  // Semantic: enter specific enterable thing (priority 100)
567
781
  grammar
568
782
  .define('enter :portal')
569
- .hasTrait('portal', world_model_1.TraitType.ENTERABLE)
570
783
  .mapsTo('if.action.entering')
571
784
  .withPriority(100)
572
785
  .build();
786
+ // Prepositional/phrasal entering forms sit at 105 (ADR-231 D2b): the file's
787
+ // guideline puts semantic/phrasal forms above bare verb-plus-slot forms, and
788
+ // these previously tied taking's `get :item` / climbing's `climb :target` at
789
+ // 100 (`exit game` at 105 is the precedent). The literal-before-slot
790
+ // specificity tiebreak also resolves this class structurally.
573
791
  grammar
574
792
  .define('get in :portal')
575
- .hasTrait('portal', world_model_1.TraitType.ENTERABLE)
576
793
  .mapsTo('if.action.entering')
577
- .withPriority(100)
794
+ .withPriority(105)
578
795
  .build();
579
796
  grammar
580
797
  .define('get into :portal')
581
- .hasTrait('portal', world_model_1.TraitType.ENTERABLE)
582
798
  .mapsTo('if.action.entering')
583
- .withPriority(100)
799
+ .withPriority(105)
584
800
  .build();
585
801
  grammar
586
802
  .define('climb in :portal')
587
- .hasTrait('portal', world_model_1.TraitType.ENTERABLE)
588
803
  .mapsTo('if.action.entering')
589
- .withPriority(100)
804
+ .withPriority(105)
590
805
  .build();
591
806
  grammar
592
807
  .define('climb into :portal')
593
- .hasTrait('portal', world_model_1.TraitType.ENTERABLE)
594
808
  .mapsTo('if.action.entering')
809
+ .withPriority(105)
810
+ .build();
811
+ // Climb a climbable object (priority 100) — ADR-218 §1a (ratchet F2).
812
+ // Routes `climb <thing>` and its synonyms to the climbing action's object-climb
813
+ // path. [2026-07-17, ADR-231 D2a] This block originally claimed parse-time
814
+ // CLIMBABLE gating via .hasTrait(); that call was a no-op and has been
815
+ // deleted — climbing's validate() owns the not_climbable refusal.
816
+ // The prepositional `climb in/into :portal` (entering) and `climb out` (exiting)
817
+ // forms are unaffected — they carry a preposition token these bare forms do not.
818
+ grammar
819
+ .define('climb :target')
820
+ .mapsTo('if.action.climbing')
821
+ .withPriority(100)
822
+ .build();
823
+ grammar
824
+ .define('climb up :target')
825
+ .mapsTo('if.action.climbing')
826
+ .withPriority(100)
827
+ .build();
828
+ grammar
829
+ .define('climb down :target')
830
+ .mapsTo('if.action.climbing')
831
+ .withPriority(100)
832
+ .build();
833
+ grammar
834
+ .define('scale :target')
835
+ .mapsTo('if.action.climbing')
836
+ .withPriority(100)
837
+ .build();
838
+ grammar
839
+ .define('ascend :target')
840
+ .mapsTo('if.action.climbing')
841
+ .withPriority(100)
842
+ .build();
843
+ grammar
844
+ .define('descend :target')
845
+ .mapsTo('if.action.climbing')
595
846
  .withPriority(100)
596
847
  .build();
597
848
  grammar
598
849
  .define('go in :portal')
599
- .hasTrait('portal', world_model_1.TraitType.ENTERABLE)
600
850
  .mapsTo('if.action.entering')
601
- .withPriority(100)
851
+ .withPriority(105) // ADR-231 D2b: phrasal form above bare verb-plus-slot
602
852
  .build();
603
853
  grammar
604
854
  .define('go into :portal')
605
- .hasTrait('portal', world_model_1.TraitType.ENTERABLE)
606
855
  .mapsTo('if.action.entering')
607
- .withPriority(100)
856
+ .withPriority(105) // ADR-231 D2b: phrasal form above bare verb-plus-slot
608
857
  .build();
609
858
  // Exiting (bare command, no target - exits current container/location)
610
859
  grammar
@@ -615,7 +864,7 @@ function defineGrammar(grammar) {
615
864
  grammar
616
865
  .define('get out')
617
866
  .mapsTo('if.action.exiting')
618
- .withPriority(100)
867
+ .withPriority(105) // ADR-231 D2b: outranks taking's `get :item` ("out" is not an item)
619
868
  .build();
620
869
  grammar
621
870
  .define('leave')
@@ -625,28 +874,35 @@ function defineGrammar(grammar) {
625
874
  grammar
626
875
  .define('climb out')
627
876
  .mapsTo('if.action.exiting')
628
- .withPriority(100)
877
+ .withPriority(105) // ADR-231 D2b: outranks climbing's `climb :target`
629
878
  .build();
630
879
  // Vehicle-specific synonyms (map to entering/exiting actions)
631
880
  grammar
632
881
  .define('board :vehicle')
633
- .hasTrait('vehicle', world_model_1.TraitType.ENTERABLE)
634
882
  .mapsTo('if.action.entering')
635
883
  .withPriority(100)
636
884
  .build();
637
885
  grammar
638
886
  .define('get on :vehicle')
639
- .hasTrait('vehicle', world_model_1.TraitType.ENTERABLE)
640
887
  .mapsTo('if.action.entering')
641
- .withPriority(100)
888
+ .withPriority(105) // ADR-231 D2b: phrasal form above taking's `get :item`
642
889
  .build();
643
890
  // Exiting with a target (exit specific container/vehicle)
644
891
  grammar
645
892
  .define('exit :container')
646
- .hasTrait('container', world_model_1.TraitType.ENTERABLE)
647
893
  .mapsTo('if.action.exiting')
648
894
  .withPriority(100)
649
895
  .build();
896
+ grammar
897
+ .define('get out of :container')
898
+ .mapsTo('if.action.exiting')
899
+ .withPriority(105) // ADR-231 D2b: phrasal form above taking's `get :item`
900
+ .build();
901
+ grammar
902
+ .define('climb out of :container')
903
+ .mapsTo('if.action.exiting')
904
+ .withPriority(105) // ADR-231 D2b: phrasal form above climbing's `climb :target`
905
+ .build();
650
906
  grammar
651
907
  .define('disembark')
652
908
  .mapsTo('if.action.exiting')
@@ -654,15 +910,13 @@ function defineGrammar(grammar) {
654
910
  .build();
655
911
  grammar
656
912
  .define('disembark :vehicle')
657
- .hasTrait('vehicle', world_model_1.TraitType.ENTERABLE)
658
913
  .mapsTo('if.action.exiting')
659
914
  .withPriority(100)
660
915
  .build();
661
916
  grammar
662
917
  .define('get off :vehicle')
663
- .hasTrait('vehicle', world_model_1.TraitType.ENTERABLE)
664
918
  .mapsTo('if.action.exiting')
665
- .withPriority(100)
919
+ .withPriority(105) // ADR-231 D2b: phrasal form above taking's `get :item`
666
920
  .build();
667
921
  grammar
668
922
  .define('alight')